crud_components 0.1.0 → 0.2.0
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 +29 -0
- data/README.md +13 -9
- data/RELEASING.md +14 -9
- data/app/views/crud_components/_filter.html.erb +20 -0
- data/app/views/crud_components/filters/_date.html.erb +5 -0
- data/app/views/crud_components/filters/_number.html.erb +6 -0
- data/app/views/crud_components/filters/_presence.html.erb +13 -0
- data/config/locales/crud_components.de.yml +7 -0
- data/config/locales/crud_components.en.yml +7 -0
- data/crud_components.gemspec +1 -0
- data/docs/fields.md +20 -48
- data/docs/filtering.md +166 -0
- data/docs/performance.md +1 -1
- data/docs/security.md +9 -7
- data/docs/views.md +8 -2
- data/lib/crud_components/dynamic_column.rb +11 -3
- data/lib/crud_components/fields/attachment_field.rb +23 -1
- data/lib/crud_components/fields/base.rb +71 -11
- data/lib/crud_components/fields/belongs_to_field.rb +38 -10
- data/lib/crud_components/fields/boolean_field.rb +2 -2
- data/lib/crud_components/fields/date_field.rb +2 -2
- data/lib/crud_components/fields/enum_field.rb +4 -4
- data/lib/crud_components/fields/has_many_field.rb +22 -2
- data/lib/crud_components/fields/numeric_field.rb +2 -2
- data/lib/crud_components/fields/path_field.rb +6 -6
- data/lib/crud_components/fields/string_field.rb +3 -3
- data/lib/crud_components/helpers.rb +19 -5
- data/lib/crud_components/like_spec.rb +32 -33
- data/lib/crud_components/presenters/collection.rb +17 -8
- data/lib/crud_components/presenters/filter.rb +27 -3
- data/lib/crud_components/query.rb +45 -6
- data/lib/crud_components/structure.rb +5 -2
- data/lib/crud_components/typed_filter.rb +113 -0
- data/lib/crud_components/version.rb +1 -1
- data/lib/crud_components.rb +8 -0
- metadata +8 -1
|
@@ -15,7 +15,7 @@ module CrudComponents
|
|
|
15
15
|
attr_reader :model, :structure, :fieldset, :query, :layout, :param_prefix, :owner
|
|
16
16
|
|
|
17
17
|
def initialize(view:, records:, fieldset: nil, query: :auto, layout: :table,
|
|
18
|
-
param_prefix: nil, actions: true, group_by: nil,
|
|
18
|
+
param_prefix: nil, actions: true, search_bar: true, group_by: nil,
|
|
19
19
|
extra_columns: nil, picker: false, picked_columns: :auto)
|
|
20
20
|
super(view: view)
|
|
21
21
|
unless records.respond_to?(:klass)
|
|
@@ -31,6 +31,7 @@ module CrudComponents
|
|
|
31
31
|
@layout = layout
|
|
32
32
|
@param_prefix = param_prefix
|
|
33
33
|
@actions_enabled = actions
|
|
34
|
+
@search_bar_enabled = search_bar
|
|
34
35
|
# Two orthogonal column-picker knobs (see ColumnSelection): the gear is on
|
|
35
36
|
# iff `picker`; the selection comes from the param (`:auto`) or verbatim
|
|
36
37
|
# from the resolved Array — they never both read the param.
|
|
@@ -181,8 +182,13 @@ module CrudComponents
|
|
|
181
182
|
@filter_fields ||= static? ? [] : query.filter_fields
|
|
182
183
|
end
|
|
183
184
|
|
|
185
|
+
# Match by name, not object identity: a prebuilt Query carries its *own*
|
|
186
|
+
# DynamicField instances (from its extra_fields), while the collection built
|
|
187
|
+
# its own from extra_columns. Same column, different objects — `include?`
|
|
188
|
+
# would miss them and silently drop the inline filter/sort (issue #21).
|
|
189
|
+
# Declared fields are memoized on the Structure, so they share a name too.
|
|
184
190
|
def filterable_field?(field)
|
|
185
|
-
filter_fields.
|
|
191
|
+
filter_fields.any? { |f| f.name == field.name }
|
|
186
192
|
end
|
|
187
193
|
|
|
188
194
|
def filter_form_id
|
|
@@ -192,7 +198,7 @@ module CrudComponents
|
|
|
192
198
|
|
|
193
199
|
# ── header search (?q=) and reset ──────────────────────────────────────
|
|
194
200
|
def searchable?
|
|
195
|
-
!static? && query && query.searchable?
|
|
201
|
+
@search_bar_enabled && !static? && query && query.searchable?
|
|
196
202
|
end
|
|
197
203
|
|
|
198
204
|
def search_param_name
|
|
@@ -238,7 +244,7 @@ module CrudComponents
|
|
|
238
244
|
|
|
239
245
|
# ── sorting ──────────────────────────────────────────────────────────
|
|
240
246
|
def sortable_field?(field)
|
|
241
|
-
!static? && query && query.sortable_fields.
|
|
247
|
+
!static? && query && query.sortable_fields.any? { |f| f.name == field.name }
|
|
242
248
|
end
|
|
243
249
|
|
|
244
250
|
def sort_url(field)
|
|
@@ -355,11 +361,14 @@ module CrudComponents
|
|
|
355
361
|
@actions_enabled && (custom_actions_partial.present? || row_action_definitions.any?)
|
|
356
362
|
end
|
|
357
363
|
|
|
358
|
-
# The trailing column exists when
|
|
359
|
-
#
|
|
360
|
-
#
|
|
364
|
+
# The trailing column exists when something needs to live in it: row
|
|
365
|
+
# actions (in the body cells), the column picker (its gear in the header
|
|
366
|
+
# cell), or the inline filter row (its submit/reset button sits in this
|
|
367
|
+
# column). Without it a filterable table would render filter inputs with
|
|
368
|
+
# no button to apply them. Keeping it here also keeps the header, rows and
|
|
369
|
+
# width all in agreement.
|
|
361
370
|
def trailing_column?
|
|
362
|
-
actions_column? || column_picker?
|
|
371
|
+
actions_column? || column_picker? || filterable?
|
|
363
372
|
end
|
|
364
373
|
|
|
365
374
|
def custom_actions_partial
|
|
@@ -6,16 +6,21 @@ module CrudComponents
|
|
|
6
6
|
class Filter < Base
|
|
7
7
|
attr_reader :model, :structure, :query
|
|
8
8
|
|
|
9
|
-
def initialize(view:, model:, fieldset: nil, query: nil, param_prefix: nil)
|
|
9
|
+
def initialize(view:, model:, fieldset: nil, query: nil, param_prefix: nil, extra_columns: nil, sort: false)
|
|
10
10
|
super(view: view)
|
|
11
11
|
@model = model.is_a?(Class) ? model : model.klass
|
|
12
12
|
@structure = Structure.for(@model)
|
|
13
|
+
@sort = sort
|
|
14
|
+
# Dynamic columns become extra query fields, so their filters appear in the
|
|
15
|
+
# form exactly like a declared field's — mirrors crud_collection's
|
|
16
|
+
# extra_columns: (issue #22). A prebuilt query already carries its own.
|
|
17
|
+
dynamic_fields = Array(extra_columns).map { |c| c.to_field(@model) }
|
|
13
18
|
@query = if query.is_a?(Query)
|
|
14
19
|
query
|
|
15
20
|
else
|
|
16
21
|
Query.new(@model, view.request.query_parameters,
|
|
17
22
|
fieldset: @structure.fieldset(fieldset || :index),
|
|
18
|
-
ability: ability, param_prefix: param_prefix)
|
|
23
|
+
ability: ability, param_prefix: param_prefix, extra_fields: dynamic_fields)
|
|
19
24
|
end
|
|
20
25
|
end
|
|
21
26
|
|
|
@@ -27,10 +32,29 @@ module CrudComponents
|
|
|
27
32
|
def param_name(key) = query.param_name(key)
|
|
28
33
|
def value(key) = query.value(key)
|
|
29
34
|
|
|
30
|
-
#
|
|
35
|
+
# ── sorting (headerless surfaces) ──────────────────────────────────────
|
|
36
|
+
# Whether to render the sort picker: asked for, and there's something to
|
|
37
|
+
# sort by. A table carries its own header sort links, so this is opt-in.
|
|
38
|
+
def sort_control? = @sort && query.sortable_fields.any?
|
|
39
|
+
|
|
40
|
+
# The fields offered in the sort picker, as [human_name, name] pairs.
|
|
41
|
+
def sort_field_choices
|
|
42
|
+
query.sortable_fields.map { |f| [f.human_name, f.name.to_s] }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# [field_name, direction] currently in effect, or [nil, 'asc'] when unsorted.
|
|
46
|
+
def sort_state
|
|
47
|
+
current, dir = query.sort_state
|
|
48
|
+
[current, dir || 'asc']
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Keep foreign params; our own controls resubmit themselves. Sort/dir are
|
|
52
|
+
# kept as hidden inputs (to preserve the current sort across an Apply) unless
|
|
53
|
+
# the sort picker renders them — then they'd duplicate, so we drop them.
|
|
31
54
|
def preserved_params
|
|
32
55
|
own = fields.flat_map { |f| [param_name(f.name.to_s), param_name("#{f.name}_geq"), param_name("#{f.name}_leq")] }
|
|
33
56
|
own += [param_name('q'), param_name('page'), param_name('per')]
|
|
57
|
+
own += [param_name('sort'), param_name('dir')] if sort_control?
|
|
34
58
|
view.request.query_parameters.reject { |key, _| own.include?(key) }
|
|
35
59
|
end
|
|
36
60
|
end
|
|
@@ -7,6 +7,10 @@ module CrudComponents
|
|
|
7
7
|
class Query
|
|
8
8
|
SORT_DIRECTIONS = %w[asc desc].freeze
|
|
9
9
|
|
|
10
|
+
# The reserved params the query itself reads. Pagination (page/per) is the
|
|
11
|
+
# host's — it lives in the controller, never here — so it is not listed.
|
|
12
|
+
RESERVED_PARAMS = %w[q sort dir].freeze
|
|
13
|
+
|
|
10
14
|
attr_reader :model, :structure, :fieldset, :param_prefix
|
|
11
15
|
|
|
12
16
|
def initialize(model, params, fieldset: nil, ability: nil, param_prefix: nil, extra_fields: [])
|
|
@@ -42,11 +46,38 @@ module CrudComponents
|
|
|
42
46
|
# Current value of a (logical, unprefixed) param — for filter controls.
|
|
43
47
|
def value(key) = param(key)
|
|
44
48
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
49
|
+
# The (prefixed) request-param names this query reads: every visible filter
|
|
50
|
+
# field's value and `_geq`/`_leq` bounds, plus the reserved q/sort/dir. The
|
|
51
|
+
# single source of truth for a strong-params permit list, so it can't drift
|
|
52
|
+
# from the columns:
|
|
53
|
+
# params.permit(*query.permitted_keys)
|
|
54
|
+
def permitted_keys
|
|
55
|
+
(filter_param_keys + RESERVED_PARAMS).map { |key| param_name(key) }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# The subset of the current request params this query reads, present values
|
|
59
|
+
# only, keyed by their real (prefixed) param names. Feed it to
|
|
60
|
+
# filter-preserving links (pagers, breadcrumbs, "reset" targets) instead of
|
|
61
|
+
# keeping a hand-maintained copy of the params.
|
|
62
|
+
def filter_params
|
|
63
|
+
permitted_keys.each_with_object({}) do |key, kept|
|
|
64
|
+
raw = @params[key]
|
|
65
|
+
kept[key] = raw if raw.is_a?(String) && raw.present?
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# The active filter and search values keyed by their logical (unprefixed)
|
|
70
|
+
# name — for rendering active-filter chips. Range bounds appear as
|
|
71
|
+
# `<field>_geq` / `<field>_leq`; the search box as `q`.
|
|
72
|
+
def active_filters
|
|
73
|
+
(filter_param_keys + ['q']).each_with_object({}) do |key, active|
|
|
74
|
+
val = param(key)
|
|
75
|
+
active[key] = val if val
|
|
76
|
+
end
|
|
48
77
|
end
|
|
49
78
|
|
|
79
|
+
def active? = active_filters.any?
|
|
80
|
+
|
|
50
81
|
# [field_name_string, direction_string] or nil.
|
|
51
82
|
def sort_state
|
|
52
83
|
field = current_sort_field
|
|
@@ -57,6 +88,14 @@ module CrudComponents
|
|
|
57
88
|
|
|
58
89
|
private
|
|
59
90
|
|
|
91
|
+
# Logical (unprefixed) filter param keys: each visible filter field's value
|
|
92
|
+
# plus its `_geq`/`_leq` bounds. Bounds are listed for every field even
|
|
93
|
+
# though only ranges use them — apply_filters reads all three uniformly, so
|
|
94
|
+
# the permit list mirrors exactly what can reach SQL.
|
|
95
|
+
def filter_param_keys
|
|
96
|
+
filter_fields.flat_map { |field| [field.name.to_s, "#{field.name}_geq", "#{field.name}_leq"] }
|
|
97
|
+
end
|
|
98
|
+
|
|
60
99
|
def prefix = param_prefix ? "#{param_prefix}_" : ''
|
|
61
100
|
|
|
62
101
|
def param(key)
|
|
@@ -75,12 +114,12 @@ module CrudComponents
|
|
|
75
114
|
|
|
76
115
|
def apply_filters(scope)
|
|
77
116
|
filter_fields.reduce(scope) do |current, field|
|
|
78
|
-
|
|
117
|
+
value = param(field.name.to_s)
|
|
79
118
|
geq = param("#{field.name}_geq")
|
|
80
119
|
leq = param("#{field.name}_leq")
|
|
81
|
-
next current unless
|
|
120
|
+
next current unless value || geq || leq
|
|
82
121
|
|
|
83
|
-
field.apply_filter(current,
|
|
122
|
+
field.apply_filter(current, value: value, geq: geq, leq: leq)
|
|
84
123
|
end
|
|
85
124
|
end
|
|
86
125
|
|
|
@@ -189,9 +189,12 @@ module CrudComponents
|
|
|
189
189
|
@search_in_spec ||= (@search_decl.presence || default_search_spec)
|
|
190
190
|
end
|
|
191
191
|
|
|
192
|
+
# "Search what you see": with no search_in declared, ?q= covers the text
|
|
193
|
+
# shown on the index — own string/text columns, plus associations through
|
|
194
|
+
# their label. Columns you never display (and a model's hidden secrets) are
|
|
195
|
+
# never reached. Derived from the index fieldset; declared search_in wins.
|
|
192
196
|
def default_search_spec
|
|
193
|
-
|
|
194
|
-
.map { |col| col.name.to_sym }
|
|
197
|
+
fieldset_fields(fieldset(:index)).filter_map(&:search_spec_entry).uniq
|
|
195
198
|
end
|
|
196
199
|
|
|
197
200
|
def searchable?
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
module CrudComponents
|
|
2
|
+
# The mechanism behind a typed dynamic-column filter (built internally by
|
|
3
|
+
# {Fields::Base} — you don't construct one). It pairs a value type with an apply
|
|
4
|
+
# block: the type drives casting and the rendered control, the block reaches SQL.
|
|
5
|
+
#
|
|
6
|
+
# A {DynamicColumn} gets one when its `filter:` block declares keyword params; the
|
|
7
|
+
# type comes from the column's `as:` (overridable with `filter_as:`). The block
|
|
8
|
+
# declares which of `eq:` / `geq:` / `leq:` / `contains:` it handles and is called
|
|
9
|
+
# with only those — each value already cast to the type (numeric → BigDecimal,
|
|
10
|
+
# date → Date, boolean → true/false), or nil when the param is blank or doesn't
|
|
11
|
+
# parse. Which control renders follows from the type and the declared keywords: a
|
|
12
|
+
# numeric/date block that asks for a bound (`geq:`/`leq:`) renders a range; one
|
|
13
|
+
# that asks only for `eq:` renders a single field.
|
|
14
|
+
#
|
|
15
|
+
# @api private
|
|
16
|
+
class TypedFilter
|
|
17
|
+
TYPES = %i[text numeric date boolean select].freeze
|
|
18
|
+
|
|
19
|
+
# Every keyword a block may declare. The bare `?field=` value binds to
|
|
20
|
+
# `contains:` when the block asks for it, otherwise to `eq:`; the `_geq`/`_leq`
|
|
21
|
+
# params bind to `geq:`/`leq:`.
|
|
22
|
+
KEYWORDS = %i[eq contains geq leq choices].freeze
|
|
23
|
+
|
|
24
|
+
attr_reader :type, :choices
|
|
25
|
+
|
|
26
|
+
def initialize(type, apply, choices: nil)
|
|
27
|
+
unless TYPES.include?(type)
|
|
28
|
+
raise ArgumentError, "unknown filter type #{type.inspect} — one of #{TYPES.map(&:inspect).join(', ')}"
|
|
29
|
+
end
|
|
30
|
+
raise ArgumentError, 'a typed filter needs an apply block (a callable)' unless apply.respond_to?(:call)
|
|
31
|
+
|
|
32
|
+
@type = type
|
|
33
|
+
@apply = apply
|
|
34
|
+
@choices = choices
|
|
35
|
+
@keywords = declared_keywords(apply)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# The filter partial to render (`crud_components/filters/_<control>`).
|
|
39
|
+
def control
|
|
40
|
+
case type
|
|
41
|
+
when :numeric then range? ? :number_range : :number
|
|
42
|
+
when :date then range? ? :date_range : :date
|
|
43
|
+
else type
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Apply with the raw param values (Strings or nil): `value` is the bare
|
|
48
|
+
# `?field=`, `geq`/`leq` the bounds. Each is cast to the type and routed to the
|
|
49
|
+
# keyword the block declared; a value that doesn't cast becomes nil, so junk
|
|
50
|
+
# never reaches SQL.
|
|
51
|
+
def apply(scope, value: nil, geq: nil, leq: nil)
|
|
52
|
+
single = (@keywords.include?(:contains) && !@keywords.include?(:eq)) ? :contains : :eq
|
|
53
|
+
values = { single => cast(value), geq: cast(geq), leq: cast(leq), choices: @choices }
|
|
54
|
+
@apply.call(scope, **values.slice(*@keywords))
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# The `[label, value]` pairs a `:select` offers, or nil for any other type. A
|
|
58
|
+
# callable `choices` is resolved here (passed the query when it takes an arg).
|
|
59
|
+
def filter_choices(query = nil)
|
|
60
|
+
return nil unless type == :select
|
|
61
|
+
|
|
62
|
+
raw = @choices.respond_to?(:call) ? (@choices.arity.zero? ? @choices.call : @choices.call(query)) : @choices
|
|
63
|
+
Array(raw).map { |opt| opt.is_a?(Array) ? opt : [opt.to_s, opt] }
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
TRUE_VALUES = %w[true t 1 yes on].freeze
|
|
69
|
+
FALSE_VALUES = %w[false f 0 no off].freeze
|
|
70
|
+
|
|
71
|
+
def range? = (@keywords & %i[geq leq]).any?
|
|
72
|
+
|
|
73
|
+
# The keywords the block declared. A block with `**` (keyrest) takes everything.
|
|
74
|
+
def declared_keywords(apply)
|
|
75
|
+
params = apply.respond_to?(:parameters) ? apply.parameters : []
|
|
76
|
+
return KEYWORDS if params.any? { |kind, _| kind == :keyrest }
|
|
77
|
+
|
|
78
|
+
params.filter_map { |kind, name| name if %i[key keyreq].include?(kind) }
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def cast(value)
|
|
82
|
+
return nil if value.nil? || (value.respond_to?(:empty?) && value.empty?)
|
|
83
|
+
|
|
84
|
+
case type
|
|
85
|
+
when :numeric then cast_decimal(value)
|
|
86
|
+
when :date then cast_date(value)
|
|
87
|
+
when :boolean then cast_boolean(value)
|
|
88
|
+
else value
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def cast_decimal(value)
|
|
93
|
+
decimal = BigDecimal(value.to_s)
|
|
94
|
+
decimal.finite? ? decimal : nil
|
|
95
|
+
rescue ArgumentError, TypeError
|
|
96
|
+
nil
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def cast_date(value)
|
|
100
|
+
Date.parse(value.to_s)
|
|
101
|
+
rescue ArgumentError, TypeError, RangeError
|
|
102
|
+
nil
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def cast_boolean(value)
|
|
106
|
+
down = value.to_s.downcase
|
|
107
|
+
return true if TRUE_VALUES.include?(down)
|
|
108
|
+
return false if FALSE_VALUES.include?(down)
|
|
109
|
+
|
|
110
|
+
nil
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
data/lib/crud_components.rb
CHANGED
|
@@ -17,6 +17,13 @@ module CrudComponents
|
|
|
17
17
|
# nullable columns offer it as a "not set" choice). Improbable as a real
|
|
18
18
|
# value, so it never collides with a genuine enum key or boolean string.
|
|
19
19
|
NULL_FILTER_VALUE = '__null__'.freeze
|
|
20
|
+
|
|
21
|
+
# The two non-blank values of an attachment **presence** filter — its 3-state
|
|
22
|
+
# control (any / present / absent) submits these, and the query turns them into
|
|
23
|
+
# an EXISTS / NOT EXISTS (`where.associated` / `where.missing`) over the backing
|
|
24
|
+
# attachment association rather than a value match. See {Fields::AttachmentField}.
|
|
25
|
+
PRESENT_FILTER_VALUE = 'present'.freeze
|
|
26
|
+
ABSENT_FILTER_VALUE = 'absent'.freeze
|
|
20
27
|
end
|
|
21
28
|
|
|
22
29
|
require_relative 'crud_components/version'
|
|
@@ -25,6 +32,7 @@ require_relative 'crud_components/config'
|
|
|
25
32
|
require_relative 'crud_components/permission_context'
|
|
26
33
|
require_relative 'crud_components/like_spec'
|
|
27
34
|
require_relative 'crud_components/where_like'
|
|
35
|
+
require_relative 'crud_components/typed_filter'
|
|
28
36
|
require_relative 'crud_components/fields/base'
|
|
29
37
|
require_relative 'crud_components/fields/string_field'
|
|
30
38
|
require_relative 'crud_components/fields/text_field'
|
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.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Anatoly Zelenin
|
|
@@ -75,6 +75,7 @@ extensions: []
|
|
|
75
75
|
extra_rdoc_files: []
|
|
76
76
|
files:
|
|
77
77
|
- ".gitignore"
|
|
78
|
+
- CHANGELOG.md
|
|
78
79
|
- Gemfile
|
|
79
80
|
- LICENSE
|
|
80
81
|
- README.md
|
|
@@ -110,8 +111,11 @@ files:
|
|
|
110
111
|
- app/views/crud_components/fields/_text.html.erb
|
|
111
112
|
- app/views/crud_components/fields/_url.html.erb
|
|
112
113
|
- app/views/crud_components/filters/_boolean.html.erb
|
|
114
|
+
- app/views/crud_components/filters/_date.html.erb
|
|
113
115
|
- app/views/crud_components/filters/_date_range.html.erb
|
|
116
|
+
- app/views/crud_components/filters/_number.html.erb
|
|
114
117
|
- app/views/crud_components/filters/_number_range.html.erb
|
|
118
|
+
- app/views/crud_components/filters/_presence.html.erb
|
|
115
119
|
- app/views/crud_components/filters/_select.html.erb
|
|
116
120
|
- app/views/crud_components/filters/_text.html.erb
|
|
117
121
|
- app/views/crud_components/form_fields/_belongs_to.html.erb
|
|
@@ -130,6 +134,7 @@ files:
|
|
|
130
134
|
- crud_components.gemspec
|
|
131
135
|
- docs/extending.md
|
|
132
136
|
- docs/fields.md
|
|
137
|
+
- docs/filtering.md
|
|
133
138
|
- docs/forms.md
|
|
134
139
|
- docs/performance.md
|
|
135
140
|
- docs/security.md
|
|
@@ -173,6 +178,7 @@ files:
|
|
|
173
178
|
- lib/crud_components/query.rb
|
|
174
179
|
- lib/crud_components/route_resolver.rb
|
|
175
180
|
- lib/crud_components/structure.rb
|
|
181
|
+
- lib/crud_components/typed_filter.rb
|
|
176
182
|
- lib/crud_components/version.rb
|
|
177
183
|
- lib/crud_components/where_like.rb
|
|
178
184
|
- lib/generators/crud_components/install/install_generator.rb
|
|
@@ -188,6 +194,7 @@ licenses:
|
|
|
188
194
|
metadata:
|
|
189
195
|
homepage_uri: https://github.com/itadventurer/crud_components
|
|
190
196
|
source_code_uri: https://github.com/itadventurer/crud_components
|
|
197
|
+
changelog_uri: https://github.com/itadventurer/crud_components/blob/main/CHANGELOG.md
|
|
191
198
|
rubygems_mfa_required: 'true'
|
|
192
199
|
rdoc_options: []
|
|
193
200
|
require_paths:
|