card-mod-filter 0.2
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 +7 -0
- data/README.md +60 -0
- data/lib/card/filter_query.rb +78 -0
- data/set/abstract/0_filter/filter_fields.rb +46 -0
- data/set/abstract/0_filter/filter_form/_compact_filter_input.haml +7 -0
- data/set/abstract/0_filter/filter_form/compact_filter_form.haml +46 -0
- data/set/abstract/0_filter/filter_form/compact_quick_filters.haml +14 -0
- data/set/abstract/0_filter/filter_form/filter_bars.haml +4 -0
- data/set/abstract/0_filter/filter_form/filter_sort_dropdown.haml +12 -0
- data/set/abstract/0_filter/filter_form/filtered_results_header.haml +33 -0
- data/set/abstract/0_filter/filter_form/offcanvas_filters.haml +9 -0
- data/set/abstract/0_filter/filter_form/open_filter_button.haml +1 -0
- data/set/abstract/0_filter/filter_form/select_item.haml +15 -0
- data/set/abstract/0_filter/filter_form/selectable_filtered_content.haml +2 -0
- data/set/abstract/0_filter/filter_form.rb +123 -0
- data/set/abstract/0_filter/filter_input_fields/check_filter.haml +13 -0
- data/set/abstract/0_filter/filter_input_fields.rb +111 -0
- data/set/abstract/0_filter.rb +131 -0
- data/set/abstract/cached_type_options.rb +8 -0
- data/set/abstract/cql_search/filter.rb +33 -0
- data/set/abstract/filter_link.rb +15 -0
- data/set/abstract/filterable_list.rb +16 -0
- data/set/abstract/filtered_list/filter_items.haml +28 -0
- data/set/abstract/filtered_list/filtered_list_input.haml +7 -0
- data/set/abstract/filtered_list.rb +105 -0
- data/set/abstract/list/filter.rb +1 -0
- data/set/abstract/search/filter_inclusion.rb +7 -0
- data/set/all/filtered_list_item.haml +8 -0
- data/set/all/filtered_list_item.rb +11 -0
- data/set/type/list.rb +4 -0
- metadata +116 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 8a74d8d165d6938cdcc7dba028a7e9d4c51f5c81bafff831be97e2b941af7cd3
|
4
|
+
data.tar.gz: 8de27d3a0e862aef786b5121d0f51ae4809f15ea90ed6ba1d7ab83f1f8c0a6c2
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 1b3b5cb680b5be0c170c6a9c9b651e1aadac8a56fdc5ac772363bdf1741640a51c676419ea3df0c131bb0b8529330507e930d44acfbf2d03e3d12d1da26493a5
|
7
|
+
data.tar.gz: 525485ae8d031e33f3f653cfbe99a70f67c1098071ea7985a7107852e32f5b5b5743c8b1c86f610472c76c06e3b457398f48dba48904978df6e3b9957877a4b4
|
data/README.md
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
<!--
|
2
|
+
# @title README - mod: filter
|
3
|
+
-->
|
4
|
+
|
5
|
+
# Filter mod
|
6
|
+
|
7
|
+
This mod provides a framework for enhancing card searches with advanced filtering
|
8
|
+
interfaces.
|
9
|
+
|
10
|
+
Include the Abstract::Filter set in any search set to add both:
|
11
|
+
|
12
|
+
- a `filter_form` view with progressive disclosure filter accordions inside an offcanvas,
|
13
|
+
and
|
14
|
+
- a `compact_filter_form` view that opens compact filter interfaces in place above search
|
15
|
+
results.
|
16
|
+
|
17
|
+
The mod also adds support for a `filtered_list` view of list/pointer cards that makes it
|
18
|
+
easier to choose list items from options too numerous or complicated to fit into a
|
19
|
+
standard dropdown.
|
20
|
+
|
21
|
+
Filters for a given set can be specified with the `#filter_map` method, which should
|
22
|
+
return a list of filter configurations. Each configuration can be a Symbol or a Hash.
|
23
|
+
For example, here is a simple configuration with three filters.
|
24
|
+
|
25
|
+
```
|
26
|
+
def filter_map
|
27
|
+
[name topic bookmark]
|
28
|
+
end
|
29
|
+
```
|
30
|
+
|
31
|
+
For each filter you can use methods to define:
|
32
|
+
1. labels, eg `#filter_topic_label` (same as the filter key if not defined)
|
33
|
+
2. options, eg `#filter_topic_options` (empty by default)
|
34
|
+
3. a default value, eg `#filter_topic_default` (blank unless specified), and
|
35
|
+
4. a type, eg `#filter_topic_type` (defaults to text).
|
36
|
+
|
37
|
+
Type options include:
|
38
|
+
|
39
|
+
- text
|
40
|
+
- autocomplete
|
41
|
+
- radio
|
42
|
+
- check
|
43
|
+
- select
|
44
|
+
- multiselect
|
45
|
+
- range
|
46
|
+
|
47
|
+
It is also possible to write custom filter types in the following manner:
|
48
|
+
|
49
|
+
```
|
50
|
+
def filter_myfield_type
|
51
|
+
:my_custom_type
|
52
|
+
end
|
53
|
+
|
54
|
+
def my_custom_type_filter
|
55
|
+
# returns filter ui
|
56
|
+
end
|
57
|
+
```
|
58
|
+
|
59
|
+
Note that fields that use radio and check filtering in the `filter_form` view will use
|
60
|
+
select and multiselect filtering respectively in the `compact_filter_form` view.
|
@@ -0,0 +1,78 @@
|
|
1
|
+
class Card
|
2
|
+
# Class for generating CQL based on filter params
|
3
|
+
class FilterQuery
|
4
|
+
def initialize filter_keys_with_values, extra_cql={}
|
5
|
+
@filter_cql = Hash.new { |h, k| h[k] = [] }
|
6
|
+
@rules = yield if block_given?
|
7
|
+
@rules ||= {}
|
8
|
+
@filter_keys_with_values = filter_keys_with_values
|
9
|
+
@extra_cql = extra_cql
|
10
|
+
prepare_filter_cql
|
11
|
+
end
|
12
|
+
|
13
|
+
def add_to_cql key, value
|
14
|
+
@filter_cql[key] << value
|
15
|
+
end
|
16
|
+
|
17
|
+
def add_rule key, value
|
18
|
+
return unless value.present?
|
19
|
+
|
20
|
+
case @rules[key]
|
21
|
+
when Symbol
|
22
|
+
send("#{@rules[key]}_rule", key, value)
|
23
|
+
when Proc
|
24
|
+
@rules[key].call(key, value).each do |cql_key, val|
|
25
|
+
@filter_cql[cql_key] << val
|
26
|
+
end
|
27
|
+
else
|
28
|
+
send "#{key}_cql", value
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def to_cql
|
33
|
+
@cql = {}
|
34
|
+
@filter_cql.each do |cql_key, values|
|
35
|
+
next if values.empty?
|
36
|
+
|
37
|
+
case cql_key
|
38
|
+
when :right_plus, :left_plus, :type
|
39
|
+
merge_using_and cql_key, values
|
40
|
+
else
|
41
|
+
merge_using_array cql_key, values
|
42
|
+
end
|
43
|
+
end
|
44
|
+
@cql.merge @extra_cql
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def prepare_filter_cql
|
50
|
+
@filter_keys_with_values.each do |key, values|
|
51
|
+
add_rule key, values
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def merge_using_array cql_key, values
|
56
|
+
@cql[cql_key] = values.one? ? values.first : values
|
57
|
+
end
|
58
|
+
|
59
|
+
def merge_using_and cql_key, values
|
60
|
+
hash = build_nested_hash cql_key, values
|
61
|
+
@cql.deep_merge! hash
|
62
|
+
end
|
63
|
+
|
64
|
+
# nest values with the same key using :and
|
65
|
+
def build_nested_hash key, values
|
66
|
+
return { key => values[0] } if values.one?
|
67
|
+
|
68
|
+
val = values.pop
|
69
|
+
{ key => val, and: build_nested_hash(key, values) }
|
70
|
+
end
|
71
|
+
|
72
|
+
def name_cql name
|
73
|
+
return unless name.present?
|
74
|
+
|
75
|
+
@filter_cql[:name] = ["match", name]
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
format :html do
|
2
|
+
def filter_config category
|
3
|
+
@filter_config ||= {}
|
4
|
+
@filter_config[category] ||=
|
5
|
+
%i[type default options label].each_with_object({}) do |trait, hash|
|
6
|
+
method = "filter_#{category}_#{trait}"
|
7
|
+
hash[trait] = send method if respond_to? method
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def filter_name_type
|
12
|
+
:text
|
13
|
+
end
|
14
|
+
|
15
|
+
def filter_label field
|
16
|
+
filter_config(field)[:label] || filter_label_from_name(field)
|
17
|
+
end
|
18
|
+
|
19
|
+
def filter_label_from_name field
|
20
|
+
Card.fetch_name(field) { field.to_s.sub(/^\*/, "").titleize }
|
21
|
+
end
|
22
|
+
|
23
|
+
def filter_closer_value field, value
|
24
|
+
try("filter_#{field}_closer_value", value) || value
|
25
|
+
end
|
26
|
+
|
27
|
+
def filter_options raw
|
28
|
+
return raw if raw.is_a? Array
|
29
|
+
|
30
|
+
raw.each_with_object([]) do |(key, value), array|
|
31
|
+
array << [key, value.to_s]
|
32
|
+
array
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def type_options type_codename, order="asc", max_length=nil
|
37
|
+
Card.cache.fetch "#{type_codename}-TYPE-OPTIONS" do
|
38
|
+
res = Card.search type: type_codename, return: :name, sort_by: "name", dir: order
|
39
|
+
max_length ? (res.map { |i| [trim_option(i, max_length), i] }) : res
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def trim_option option, max_length
|
44
|
+
option.size > max_length ? "#{option[0..max_length]}..." : option
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,7 @@
|
|
1
|
+
._filter-input.filter-input-group.input-group.input-group-sm{"data-category": field[:key],
|
2
|
+
"class": "_filter-input-#{field[:key]}" }
|
3
|
+
%button.btn.btn-secondary.input-group-text.p-1._delete-filter-input{:type => "button"}
|
4
|
+
= icon_tag :remove
|
5
|
+
%span.input-group-text._selected-category.text-muted
|
6
|
+
= field[:label]
|
7
|
+
= field[:input_field]
|
@@ -0,0 +1,46 @@
|
|
1
|
+
- @compact_filter_form = true
|
2
|
+
.card.w-100.nodblclick
|
3
|
+
.card-body
|
4
|
+
._compact-filter
|
5
|
+
// FILTERING PROTOTYPES
|
6
|
+
._filter-input-field-prototypes.d-none
|
7
|
+
- compact_filter_form_fields.each do |field|
|
8
|
+
= haml_partial :compact_filter_input, field: field
|
9
|
+
|
10
|
+
// FORM
|
11
|
+
%form._compact-filter-form._filter-form{ filter_form_args }
|
12
|
+
%input{ type: :hidden,
|
13
|
+
name: "filter[not_ids]",
|
14
|
+
class: "_not-ids",
|
15
|
+
value: params.dig(:filter, :not_ids) }
|
16
|
+
= render_compact_quick_filters
|
17
|
+
.filter-and-sort.d-flex.flex-wrap
|
18
|
+
= render_filter_sort_dropdown
|
19
|
+
|
20
|
+
|
21
|
+
// FILTERING
|
22
|
+
.filter-in-filter-form
|
23
|
+
= icon_tag :filter, "filter-section-icon"
|
24
|
+
._filter-container.d-flex.flew-row.flex-wrap.align-items-start
|
25
|
+
// FILTERS inserted here dynamically from prototypes
|
26
|
+
|
27
|
+
// ADD FILTER DROPDOWN
|
28
|
+
.dropdown._add-filter-dropdown.me-2
|
29
|
+
%button.btn.btn-sm.btn-primary._filter-dropdown.dropdown-toggle{"aria-expanded": "false",
|
30
|
+
"aria-haspopup": "true",
|
31
|
+
"data-bs-toggle": "dropdown",
|
32
|
+
type: "button" }
|
33
|
+
More Filters
|
34
|
+
.dropdown-menu
|
35
|
+
- compact_filter_form_fields.each do |field|
|
36
|
+
%a{ class: "dropdown-item _filter-category-select",
|
37
|
+
href: "#",
|
38
|
+
"data-category": field[:key],
|
39
|
+
"data-label": field[:label],
|
40
|
+
"data-active": ("true" if field[:active])}
|
41
|
+
= field[:label]
|
42
|
+
|
43
|
+
// RESET BUTTON
|
44
|
+
._reset-filter{ "data-reset": reset_filter_data }
|
45
|
+
%button.btn.btn-sm.btn-secondary{ type: "button" }
|
46
|
+
= icon_tag :reset
|
@@ -0,0 +1,14 @@
|
|
1
|
+
- if (quick_filters = quick_filter_list).present?
|
2
|
+
.quick-filter._quick-filter
|
3
|
+
|
4
|
+
= icon_tag :quick_filter, "filter-section-icon"
|
5
|
+
.quick-filter-links.d-flex.flex-wrap
|
6
|
+
- quick_filters.each do |hash|
|
7
|
+
- f = quick_filter_item hash.clone, hash.keys.first
|
8
|
+
%a{ "data-filter": f[:filter],
|
9
|
+
href: "#",
|
10
|
+
class: f[:class],
|
11
|
+
title: "Quick Filter: #{f[:text]}" }
|
12
|
+
= f[:icon]
|
13
|
+
= f[:text]
|
14
|
+
= custom_quick_filters
|
@@ -0,0 +1,12 @@
|
|
1
|
+
// SORTING
|
2
|
+
- options = sort_options
|
3
|
+
- current = current_sort
|
4
|
+
- if options.present?
|
5
|
+
.sort-in-filter-form
|
6
|
+
.input-group.input-group-sm.flex-nowrap.sort-input-group.mb-2.me-2
|
7
|
+
%span.input-group-text.text-muted Sort
|
8
|
+
= select_tag "sort_by",
|
9
|
+
options_for_select(options, current),
|
10
|
+
class: "pointer-select _filter-sort form-control",
|
11
|
+
include_blank: ("--" unless options.values.include? current),
|
12
|
+
"data-minimum-results-for-search": "Infinity"
|
@@ -0,0 +1,33 @@
|
|
1
|
+
.filtered-results-header._filtered-results-header
|
2
|
+
%form.filtered-results-form{ filter_form_args }
|
3
|
+
.header-top-row.mb-4.d-flex.justify-content-between.align-items-center
|
4
|
+
.filter-badges._filter-closers
|
5
|
+
- removables = removable_filters
|
6
|
+
- removables.each do |key, value|
|
7
|
+
- filter = filter_hash_without key, value
|
8
|
+
%a.btn.btn-sm.btn-outline-primary.m-1.ms-0{ "data-filter": JSON(filter),
|
9
|
+
class: "filter-closer close-filter-#{key}" }
|
10
|
+
= "#{filter_label key}: "
|
11
|
+
%span.fw-bold
|
12
|
+
= filter_closer_value key, value
|
13
|
+
|
14
|
+
= icon_tag :remove, class: "ms-2"
|
15
|
+
- if removables.size > 1
|
16
|
+
%a.clear-filters.m-1.ms-0{ "data-filter": "{}", href: "#" }
|
17
|
+
Clear All
|
18
|
+
|
19
|
+
|
20
|
+
._filters-button.ms-3
|
21
|
+
%a.text-reset.fw-bold{ "data-bs": { toggle: "offcanvas",
|
22
|
+
target: "##{offcanvas_filter_id}" },
|
23
|
+
title: "All Filters",
|
24
|
+
href: "" }
|
25
|
+
= icon_tag :filter_list
|
26
|
+
ALL FILTERS
|
27
|
+
|
28
|
+
.header-middle-row.my-4
|
29
|
+
= render_filtered_results_visualization
|
30
|
+
.header-bottom-row.mb-2.mt-4.d-flex.justify-content-between.align-items-center
|
31
|
+
= render_filtered_results_stats
|
32
|
+
= render_filter_sort_dropdown
|
33
|
+
|
@@ -0,0 +1,9 @@
|
|
1
|
+
._offcanvas-filter.offcanvas.offcanvas-end{ id: offcanvas_filter_id,
|
2
|
+
"data-bs": {
|
3
|
+
scroll: "true", backdrop: "false"
|
4
|
+
} }
|
5
|
+
.offcanvas-header
|
6
|
+
%h5.offcanvas-title Filters
|
7
|
+
%button.btn.btn-close{ "data-bs": { dismiss: "offcanvas" } }
|
8
|
+
.offcanvas-body{ data: { path: path, query: JSON(filter: filter_hash) } }
|
9
|
+
|
@@ -0,0 +1 @@
|
|
1
|
+
|
@@ -0,0 +1,15 @@
|
|
1
|
+
.filter-header.card-header
|
2
|
+
%h5
|
3
|
+
Select Item
|
4
|
+
-#.badge.bg-secondary
|
5
|
+
-# = card.count
|
6
|
+
.filter-selector.p-2
|
7
|
+
- if card.count > 0
|
8
|
+
.filter-bulk-selector.pb-3
|
9
|
+
%input#select-all._select-all{name: "", type: "checkbox", value: ""}
|
10
|
+
%label{for: "select-all"}
|
11
|
+
select
|
12
|
+
%span._unselected-items
|
13
|
+
= search_with_params.size
|
14
|
+
following
|
15
|
+
= render :checkbox_list
|
@@ -0,0 +1,123 @@
|
|
1
|
+
format :html do
|
2
|
+
view :filter_bars, cache: :never, template: :haml
|
3
|
+
|
4
|
+
# ~~~~ Compact (inline) sort and filter ui
|
5
|
+
# including prototypes, filters, sorting, "More", and reset
|
6
|
+
|
7
|
+
view :compact_filter_form, cache: :never, template: :haml
|
8
|
+
view :filter_sort_dropdown, cache: :never, template: :haml
|
9
|
+
view :compact_quick_filters, cache: :never, template: :haml
|
10
|
+
|
11
|
+
# ~~~~ FILTER RESULTS
|
12
|
+
|
13
|
+
view :filtered_content do
|
14
|
+
wrap true, class: "_filtered-content nodblclick" do
|
15
|
+
[render_offcanvas_filters, render_filtered_results(home_view: :filtered_results)]
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
view :compact_filtered_content do
|
20
|
+
wrap true, class: "_filtered-content nodblclick" do
|
21
|
+
voo.hide! :filtered_results_header
|
22
|
+
[render_compact_filter_form, render_filtered_results(home_view: :filtered_results)]
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
view :filtered_results, cache: :never do
|
27
|
+
wrap true, class: "_filter-result-slot" do
|
28
|
+
[render_filtered_results_header, render_core, render_filtered_results_footer]
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
view :offcanvas_filters, template: :haml, cache: :never
|
33
|
+
view :filtered_results_header, template: :haml, cache: :never
|
34
|
+
view :filtered_results_stats, cache: :never do
|
35
|
+
labeled_badge count_with_params, "Results"
|
36
|
+
end
|
37
|
+
|
38
|
+
# for override
|
39
|
+
view(:filtered_results_visualization) { "" }
|
40
|
+
view(:filtered_results_footer) { "" }
|
41
|
+
|
42
|
+
view :selectable_filtered_content, template: :haml, cache: :never
|
43
|
+
|
44
|
+
before(:select_item) { class_up "card-slot", "_filter-result-slot" }
|
45
|
+
view :select_item, cache: :never, wrap: :slot, template: :haml
|
46
|
+
|
47
|
+
def filter_form_args
|
48
|
+
{
|
49
|
+
action: path,
|
50
|
+
class: "slotter",
|
51
|
+
method: "get",
|
52
|
+
"accept-charset": "UTF-8",
|
53
|
+
"data-remote": true,
|
54
|
+
"data-slot-selector": "._filter-result-slot",
|
55
|
+
"data-filter": filter_hash.to_json
|
56
|
+
}
|
57
|
+
end
|
58
|
+
|
59
|
+
def compact_filter_form_fields
|
60
|
+
@compact_filter_form_fields ||=
|
61
|
+
all_filter_keys.map do |key|
|
62
|
+
{ key: key,
|
63
|
+
label: filter_label(key),
|
64
|
+
input_field: filter_input_field(key, compact: true),
|
65
|
+
active: active_filter?(key) }
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def filter_bar item
|
70
|
+
item = { key: item } unless item.is_a? Hash
|
71
|
+
body = filter_bar_content item
|
72
|
+
title = filter_label item[:key]
|
73
|
+
context = item[:key].to_name.safe_key
|
74
|
+
accordion_item title, body: body, open: item[:open], context: context
|
75
|
+
end
|
76
|
+
|
77
|
+
def filter_bar_content item
|
78
|
+
if item[:type] == :group
|
79
|
+
accordion do
|
80
|
+
item[:filters].map do |subitem|
|
81
|
+
filter_bar subitem
|
82
|
+
end.join
|
83
|
+
end
|
84
|
+
else
|
85
|
+
filter_input_field item[:key]
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def offcanvas_filter_id
|
90
|
+
"d0-#{card.name.safe_key}-offCanvasFilters"
|
91
|
+
end
|
92
|
+
|
93
|
+
def reset_filter_data
|
94
|
+
JSON default_filter_hash
|
95
|
+
end
|
96
|
+
|
97
|
+
def quick_filter_item hash, filter_key
|
98
|
+
{
|
99
|
+
text: (hash.delete(:text) || hash[filter_key]),
|
100
|
+
class: css_classes(hash.delete(:class),
|
101
|
+
"_compact-filter-link quick-filter-by-#{filter_key}"),
|
102
|
+
filter: JSON(hash[:filter] || hash)
|
103
|
+
}
|
104
|
+
end
|
105
|
+
|
106
|
+
# for override
|
107
|
+
def quick_filter_list
|
108
|
+
[]
|
109
|
+
end
|
110
|
+
|
111
|
+
# for override
|
112
|
+
def custom_quick_filters
|
113
|
+
""
|
114
|
+
end
|
115
|
+
|
116
|
+
def active_filter? field
|
117
|
+
if filter_keys_from_params.present?
|
118
|
+
filter_hash.key? field
|
119
|
+
else
|
120
|
+
default_filter_hash.key? field
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
- tag_method = field_type == :check ? :check_box_tag : :radio_button_tag
|
2
|
+
.filter-list._filter-list{ class: "filter-#{field_type}-list _filter-#{field}-field"}
|
3
|
+
- options.each_with_index do |(label, value), index|
|
4
|
+
- id = "filter-#{input_name.to_name.key}-#{value.to_name.key}"
|
5
|
+
- value ||= label
|
6
|
+
.form-check{ class: ("_more-filter-option" if index > 10) }
|
7
|
+
%label.form-check-label{ for: id }<
|
8
|
+
= label
|
9
|
+
= send tag_method, input_name, value, value.in?(default),
|
10
|
+
id: id, class: "form-check-input _submit-on-change"
|
11
|
+
- if options.size > 10
|
12
|
+
._show-more-filter-options.show-more-filter-options
|
13
|
+
%a{ href: "#" } show more
|
@@ -0,0 +1,111 @@
|
|
1
|
+
format :html do
|
2
|
+
COMPACT_FILTER_TYPES = { radio: :select, check: :multiselect }.freeze
|
3
|
+
|
4
|
+
def filter_input_field field, default: nil, compact: false
|
5
|
+
fc = filter_config field
|
6
|
+
default ||= fc[:default]
|
7
|
+
filter_type = (compact && COMPACT_FILTER_TYPES[fc[:type]]) || fc[:type] || :text
|
8
|
+
send "#{filter_type}_filter", field, default, fc[:options]
|
9
|
+
end
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
# ~~~~~~~ FILTER TYPES ~~~~~~~~~~~~~~~ #
|
14
|
+
|
15
|
+
def check_filter *args
|
16
|
+
check_or_radio_filter :check, *args
|
17
|
+
end
|
18
|
+
|
19
|
+
def radio_filter *args
|
20
|
+
check_or_radio_filter :radio, *args
|
21
|
+
end
|
22
|
+
|
23
|
+
def select_filter field, default, options, multiple: false
|
24
|
+
options = filter_options options
|
25
|
+
options = [["--", ""]] + options unless default
|
26
|
+
select_filter_tag field, default, options, multiple: multiple
|
27
|
+
end
|
28
|
+
|
29
|
+
def multiselect_filter field, default, options
|
30
|
+
select_filter field, default, options, multiple: true
|
31
|
+
end
|
32
|
+
|
33
|
+
def autocomplete_filter type_code, _default, options_card=nil
|
34
|
+
options_card ||= Card::Name[type_code, :type, :by_name]
|
35
|
+
text_filter type_code, "", class: "#{type_code}_autocomplete",
|
36
|
+
"data-options-card": options_card
|
37
|
+
end
|
38
|
+
|
39
|
+
def text_filter field, default, opts
|
40
|
+
opts ||= {}
|
41
|
+
value = filter_param(field) || default
|
42
|
+
text_filter_with_name_and_value filter_input_name(field), value, opts
|
43
|
+
end
|
44
|
+
|
45
|
+
def range_filter field, default, opts
|
46
|
+
opts ||= {}
|
47
|
+
default ||= {}
|
48
|
+
add_class opts, "simple-text range-filter-field"
|
49
|
+
wrap_with :div, class: "input-group" do
|
50
|
+
[range_sign(:from),
|
51
|
+
sub_text_filter(field, :from, default, opts),
|
52
|
+
range_sign(:to),
|
53
|
+
sub_text_filter(field, :to, default, opts)]
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# ~~~~~~~ HELPER METHODS ~~~~~~~~~~~~~~~ #
|
58
|
+
|
59
|
+
def check_or_radio_filter check_or_radio, field, default, options
|
60
|
+
haml :check_filter,
|
61
|
+
field_type: check_or_radio,
|
62
|
+
field: field,
|
63
|
+
input_name: filter_input_name(field, multi: (check_or_radio == :check)),
|
64
|
+
options: filter_options(options),
|
65
|
+
default: Array.wrap(filter_param(field) || default)
|
66
|
+
end
|
67
|
+
|
68
|
+
def text_filter_with_name_and_value name, value, opts
|
69
|
+
opts[:class] ||= "simple-text"
|
70
|
+
add_class opts, "form-control _submit-after-typing"
|
71
|
+
text_field_tag name, value, opts
|
72
|
+
end
|
73
|
+
|
74
|
+
def select_filter_tag field, default, options, multiple: false, disabled: false
|
75
|
+
klasses = "_filter_input_field filter-input filter-input-#{field} " \
|
76
|
+
"_submit-on-change form-control " \
|
77
|
+
"pointer-#{'multi' if multiple}select"
|
78
|
+
# not sure form-control does much here?
|
79
|
+
klasses << " _no-select2" if @compact_filter_form # select2 initiated once active
|
80
|
+
|
81
|
+
select_tag filter_input_name(field, multi: multiple),
|
82
|
+
options_for_select(options, (filter_param(field) || default)),
|
83
|
+
id: "filter-input-#{unique_id}",
|
84
|
+
multiple: multiple,
|
85
|
+
class: klasses,
|
86
|
+
disabled: disabled
|
87
|
+
end
|
88
|
+
|
89
|
+
def range_sign side
|
90
|
+
dir = side == :from ? "greater" : "less"
|
91
|
+
icon_tag("#{dir}_than", class: "input-group-text range-sign")
|
92
|
+
end
|
93
|
+
|
94
|
+
def sub_text_filter field, subfield, default={}, opts={}
|
95
|
+
name = filter_input_name field, subfield: subfield
|
96
|
+
value = filter_hash.dig(field, subfield) || default[subfield]
|
97
|
+
text_filter_with_name_and_value name, value, opts
|
98
|
+
end
|
99
|
+
|
100
|
+
def filter_input_name field, subfield: nil, multi: false
|
101
|
+
parts = [filter_prefix, "[#{field}]"]
|
102
|
+
parts << "[#{subfield}]" if subfield
|
103
|
+
parts << "[]" if multi
|
104
|
+
parts.join
|
105
|
+
end
|
106
|
+
|
107
|
+
# for override
|
108
|
+
def filter_prefix
|
109
|
+
"filter"
|
110
|
+
end
|
111
|
+
end
|
@@ -0,0 +1,131 @@
|
|
1
|
+
include_set Abstract::BsBadge
|
2
|
+
|
3
|
+
format do
|
4
|
+
def filter_class
|
5
|
+
Card::FilterQuery
|
6
|
+
end
|
7
|
+
|
8
|
+
def filter_map
|
9
|
+
[{ key: :name, open: true }]
|
10
|
+
end
|
11
|
+
|
12
|
+
def filter_keys_from_params
|
13
|
+
filter_hash.keys.map(&:to_sym) - [:not_ids]
|
14
|
+
end
|
15
|
+
|
16
|
+
def sort_options
|
17
|
+
{ "Alphabetical": :name, "Recently Added": :create }
|
18
|
+
end
|
19
|
+
|
20
|
+
# all filter keys in the order they were selected
|
21
|
+
def all_filter_keys
|
22
|
+
@all_filter_keys ||= filter_keys_from_params | filter_keys
|
23
|
+
end
|
24
|
+
|
25
|
+
def current_sort
|
26
|
+
sort_param || default_sort_option
|
27
|
+
end
|
28
|
+
|
29
|
+
def default_sort_option
|
30
|
+
cql = card.cql_content || {}
|
31
|
+
cql[:sort_by] || cql[:sort]
|
32
|
+
end
|
33
|
+
|
34
|
+
def filter_param field
|
35
|
+
filter_hash[field.to_sym]
|
36
|
+
end
|
37
|
+
|
38
|
+
def filter_hash
|
39
|
+
@filter_hash ||= filter_hash_from_params || voo.filter || default_filter_hash
|
40
|
+
end
|
41
|
+
|
42
|
+
def filter_hash_from_params
|
43
|
+
param = Env.params[:filter]
|
44
|
+
if param.blank?
|
45
|
+
nil
|
46
|
+
elsif param.to_s == "empty"
|
47
|
+
{}
|
48
|
+
else
|
49
|
+
Env.hash(param).deep_symbolize_keys
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def sort_param
|
54
|
+
@sort_param ||= safe_sql_param :sort_by
|
55
|
+
end
|
56
|
+
|
57
|
+
def safe_sql_param key
|
58
|
+
param = Env.params[key]
|
59
|
+
param.blank? ? nil : Card::Query.safe_sql(param)
|
60
|
+
end
|
61
|
+
|
62
|
+
def filter_keys
|
63
|
+
filter_keys_from_map_list(filter_map).flatten.compact
|
64
|
+
end
|
65
|
+
|
66
|
+
def filter_keys_from_map_list list
|
67
|
+
list.map do |item|
|
68
|
+
case item
|
69
|
+
when Symbol then item
|
70
|
+
when Hash then filter_keys_from_map_hash item
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def filter_keys_from_map_hash item
|
76
|
+
item[:filters] ? filter_keys_from_map_list(item[:filters]) : item[:key]
|
77
|
+
end
|
78
|
+
|
79
|
+
def filter_keys_with_values
|
80
|
+
filter_keys.map do |key|
|
81
|
+
values = filter_param(key)
|
82
|
+
values.present? ? [key, values] : next
|
83
|
+
end.compact
|
84
|
+
end
|
85
|
+
|
86
|
+
# initial values for filtered search
|
87
|
+
def default_filter_hash
|
88
|
+
{}
|
89
|
+
end
|
90
|
+
|
91
|
+
def filter_hash_without key, value
|
92
|
+
filter_hash.clone.tap do |hash|
|
93
|
+
case hash[key]
|
94
|
+
when Array
|
95
|
+
hash[key] = hash[key] - Array.wrap(value)
|
96
|
+
else
|
97
|
+
hash.delete key
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def removable_filters
|
103
|
+
each_removable_filter do |key, value, array|
|
104
|
+
if value.is_a? Array
|
105
|
+
value.each { |v| array << [key, v] }
|
106
|
+
elsif !empty_filter_value_hash? value
|
107
|
+
array << [key, value]
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def empty_filter_value_hash? value
|
113
|
+
value.is_a?(Hash) && value.values.present? && !value.values.select(&:present?).any?
|
114
|
+
end
|
115
|
+
|
116
|
+
def each_removable_filter
|
117
|
+
filter_hash&.each_with_object([]) do |(key, val), arr|
|
118
|
+
yield key, val, arr if val.present? && filter_config(key)[:default] != val
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def extra_paging_path_args
|
123
|
+
super.merge filter_and_sort_hash
|
124
|
+
end
|
125
|
+
|
126
|
+
def filter_and_sort_hash
|
127
|
+
{ filter: filter_hash }.tap do |hash|
|
128
|
+
hash[:sort_by] = sort_param if sort_param
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
@@ -0,0 +1,8 @@
|
|
1
|
+
# in FormHelper, #type_options has a special caching for the type options in filters.
|
2
|
+
#
|
3
|
+
# If this mod is included in the type set, it will clear that cache when items are
|
4
|
+
# added or deleted.
|
5
|
+
|
6
|
+
event :clear_cached_type_options, :integrate do
|
7
|
+
Card.cache.delete "#{type_code}-TYPE-OPTIONS"
|
8
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# needs to be included here because Abstract::Search does not have Abstract::Filter
|
2
|
+
# when Abstract::CqlSearch includes Abstract::Search.
|
3
|
+
include_set Abstract::Filter
|
4
|
+
|
5
|
+
format do
|
6
|
+
def search_params
|
7
|
+
super.merge filter_and_sort_cql
|
8
|
+
end
|
9
|
+
|
10
|
+
def filter_and_sort_cql
|
11
|
+
filter_cql.merge sort_cql
|
12
|
+
end
|
13
|
+
|
14
|
+
def filter_cql
|
15
|
+
return {} if filter_hash.empty?
|
16
|
+
|
17
|
+
filter_cql_from_params
|
18
|
+
end
|
19
|
+
|
20
|
+
# separate method is needed for tests
|
21
|
+
def filter_cql_from_params
|
22
|
+
filter_class.new(filter_keys_with_values, blocked_id_cql).to_cql
|
23
|
+
end
|
24
|
+
|
25
|
+
def sort_cql
|
26
|
+
{ sort_by: current_sort }
|
27
|
+
end
|
28
|
+
|
29
|
+
def blocked_id_cql
|
30
|
+
not_ids = filter_param :not_ids
|
31
|
+
not_ids.present? ? { id: ["not in", not_ids.split(",")] } : {}
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
|
2
|
+
|
3
|
+
format :html do
|
4
|
+
def filterable filter_hash={}, html_opts={}
|
5
|
+
add_class html_opts, "_filterable _noFilterUrlUpdates"
|
6
|
+
html_opts[:data] ||= {}
|
7
|
+
html_opts[:data][:filter] = filter_hash
|
8
|
+
wrap_with :div, yield, html_opts
|
9
|
+
end
|
10
|
+
|
11
|
+
def filtering selector=nil
|
12
|
+
selector ||= "._compact-filter:visible"
|
13
|
+
wrap_with :div, yield, class: "_filtering", "data-filter-selector": selector
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# Makes it so that a List can be viewed (and filtered) as a Search
|
2
|
+
|
3
|
+
include_set Abstract::CqlSearch
|
4
|
+
include_set Abstract::SearchViews
|
5
|
+
|
6
|
+
def cql_content
|
7
|
+
{ referred_to_by: "_" }
|
8
|
+
end
|
9
|
+
|
10
|
+
def item_cards args={}
|
11
|
+
standard_item_cards args
|
12
|
+
end
|
13
|
+
|
14
|
+
def count
|
15
|
+
item_strings.size
|
16
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
._filter-items.container-fluid.nodblclick._noFilterUrlUpdates{ data: filter_items_data }
|
2
|
+
.row
|
3
|
+
= nest filter_card, view: :compact_filter_form
|
4
|
+
._unselected.col-6.border.mt-2.px-0
|
5
|
+
= nest filter_card, view: :select_item,
|
6
|
+
items: { view: implicit_item_view },
|
7
|
+
cql: { limit: 10 }
|
8
|
+
._selected.col-6.border.mt-2.ps-0.pe-0
|
9
|
+
.selected-box
|
10
|
+
.card-header
|
11
|
+
%h5
|
12
|
+
Selected
|
13
|
+
.badge.bg-secondary
|
14
|
+
%span._selected-items 0
|
15
|
+
._selected-item-list{ style: "display:none" }
|
16
|
+
.deselector.p-2
|
17
|
+
%input#deselect-all._deselect-all{ type: "checkbox", checked: true, disabled: true }
|
18
|
+
%label{ for: "deselect-all" }
|
19
|
+
deselect
|
20
|
+
%span._selected-items 0
|
21
|
+
following
|
22
|
+
._selected-bin.p-2
|
23
|
+
._filter-help.alert.alert-secondary
|
24
|
+
Filter and select items to add them here.
|
25
|
+
.form-group
|
26
|
+
.selected-item-buttons.p-2
|
27
|
+
= button_tag "Cancel", class: "cancel-modal", data: { dismiss: :modal }
|
28
|
+
= render :add_selected_link
|
@@ -0,0 +1,7 @@
|
|
1
|
+
.filtered-list-editor
|
2
|
+
%ul.pointer-list.filtered-list-review._filtered-list.list-group.vertical.p-0
|
3
|
+
- card.item_cards(context: :raw).each do |item_card|
|
4
|
+
= nest_item item_card, view: filtered_item_view, wrap: filtered_item_wrap
|
5
|
+
%br
|
6
|
+
.clearfix
|
7
|
+
= add_item_modal_link
|
@@ -0,0 +1,105 @@
|
|
1
|
+
def unique_items?
|
2
|
+
true
|
3
|
+
end
|
4
|
+
|
5
|
+
def not_ids_value
|
6
|
+
unique_items? ? item_ids.map(&:to_s).join(",") : ""
|
7
|
+
end
|
8
|
+
|
9
|
+
format :html do
|
10
|
+
view :filtered_list, unknown: true do
|
11
|
+
filtered_list_input
|
12
|
+
end
|
13
|
+
|
14
|
+
view :filter_items_modal, unknown: true, wrap: :modal do
|
15
|
+
render_filter_items
|
16
|
+
end
|
17
|
+
|
18
|
+
view :filter_items, unknown: true, wrap: :slot, template: :haml
|
19
|
+
|
20
|
+
def filtered_item_view
|
21
|
+
implicit_item_view
|
22
|
+
end
|
23
|
+
|
24
|
+
def filtered_item_wrap
|
25
|
+
:filtered_list_item
|
26
|
+
end
|
27
|
+
|
28
|
+
def filtered_list_input
|
29
|
+
with_nest_mode :normal do
|
30
|
+
wrap { haml :filtered_list_input }
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def add_item_modal_link text=nil
|
35
|
+
modal_link (text || "Add Item"),
|
36
|
+
size: :xl,
|
37
|
+
class: "btn btn-sm btn-outline-secondary _add-item-link",
|
38
|
+
path: {
|
39
|
+
view: :filter_items_modal,
|
40
|
+
slot: filter_items_modal_slot,
|
41
|
+
filter: filter_items_default_filter,
|
42
|
+
# each key value below is there to help support new cards configured
|
43
|
+
# by type_plus_right sets. do not remove without testing that case
|
44
|
+
# (not currently covered by specs)
|
45
|
+
type: :list.cardname,
|
46
|
+
filter_card: filter_card.name,
|
47
|
+
filter_items: filter_item_config
|
48
|
+
}
|
49
|
+
end
|
50
|
+
|
51
|
+
def filter_items_default_filter
|
52
|
+
{ not_ids: card.not_ids_value }
|
53
|
+
end
|
54
|
+
|
55
|
+
def filter_items_data
|
56
|
+
{
|
57
|
+
"slot-selector": "modal-origin",
|
58
|
+
"item-selector": "._filtered-list-item",
|
59
|
+
item: filter_item_config
|
60
|
+
}
|
61
|
+
end
|
62
|
+
|
63
|
+
def filter_item_config
|
64
|
+
%i[view wrap duplicable].each_with_object({}) do |key, hash|
|
65
|
+
hash[key] = params.dig(:filter_items, key) || send("filtered_item_#{key}")
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def filtered_item_duplicable
|
70
|
+
!card.unique_items?
|
71
|
+
end
|
72
|
+
|
73
|
+
def filter_items_modal_slot
|
74
|
+
{ hide: [:modal_footer] }
|
75
|
+
end
|
76
|
+
|
77
|
+
view :add_selected_link, unknown: true do
|
78
|
+
button_tag "Add Selected", class: "_add-selected btn btn-primary", disabled: true
|
79
|
+
end
|
80
|
+
|
81
|
+
def filtered_list_item item_card
|
82
|
+
nest_item item_card do |rendered, item_view|
|
83
|
+
wrap_item rendered, item_view
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
# for override
|
88
|
+
# @return [Card] search card on which filtering is based
|
89
|
+
def filter_card
|
90
|
+
filter_card_from_params || default_filter_card
|
91
|
+
end
|
92
|
+
|
93
|
+
def default_filter_card
|
94
|
+
fcard = card.options_card
|
95
|
+
return fcard if fcard.respond_to? :cql_hash
|
96
|
+
|
97
|
+
fcard.fetch :referred_to_by, new: {}
|
98
|
+
end
|
99
|
+
|
100
|
+
def filter_card_from_params
|
101
|
+
return unless params[:filter_card]&.present?
|
102
|
+
|
103
|
+
Card.fetch params[:filter_card], new: {}
|
104
|
+
end
|
105
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
include_set Abstract::FilteredList
|
@@ -0,0 +1,7 @@
|
|
1
|
+
# this file is a little strange.
|
2
|
+
#
|
3
|
+
# It is necessary to have a new file in the search directory rather than adding
|
4
|
+
# #include_set directly to search.rb, because this makes it so that
|
5
|
+
# extra_paging_path_args overrides the code in search/search_params.
|
6
|
+
|
7
|
+
include_set Filter
|
data/set/type/list.rb
ADDED
metadata
ADDED
@@ -0,0 +1,116 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: card-mod-filter
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: '0.2'
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Philipp Kühl
|
8
|
+
- Ethan McCutchen
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2023-05-02 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: card
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
requirements:
|
18
|
+
- - ">="
|
19
|
+
- !ruby/object:Gem::Version
|
20
|
+
version: '0'
|
21
|
+
type: :runtime
|
22
|
+
prerelease: false
|
23
|
+
version_requirements: !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - ">="
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
version: '0'
|
28
|
+
- !ruby/object:Gem::Dependency
|
29
|
+
name: card-mod-search
|
30
|
+
requirement: !ruby/object:Gem::Requirement
|
31
|
+
requirements:
|
32
|
+
- - ">="
|
33
|
+
- !ruby/object:Gem::Version
|
34
|
+
version: '0'
|
35
|
+
type: :runtime
|
36
|
+
prerelease: false
|
37
|
+
version_requirements: !ruby/object:Gem::Requirement
|
38
|
+
requirements:
|
39
|
+
- - ">="
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
version: '0'
|
42
|
+
- !ruby/object:Gem::Dependency
|
43
|
+
name: card-mod-bootstrap
|
44
|
+
requirement: !ruby/object:Gem::Requirement
|
45
|
+
requirements:
|
46
|
+
- - ">="
|
47
|
+
- !ruby/object:Gem::Version
|
48
|
+
version: '0'
|
49
|
+
type: :runtime
|
50
|
+
prerelease: false
|
51
|
+
version_requirements: !ruby/object:Gem::Requirement
|
52
|
+
requirements:
|
53
|
+
- - ">="
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: '0'
|
56
|
+
description: ''
|
57
|
+
email:
|
58
|
+
- info@decko.org
|
59
|
+
executables: []
|
60
|
+
extensions: []
|
61
|
+
extra_rdoc_files: []
|
62
|
+
files:
|
63
|
+
- README.md
|
64
|
+
- lib/card/filter_query.rb
|
65
|
+
- set/abstract/0_filter.rb
|
66
|
+
- set/abstract/0_filter/filter_fields.rb
|
67
|
+
- set/abstract/0_filter/filter_form.rb
|
68
|
+
- set/abstract/0_filter/filter_form/_compact_filter_input.haml
|
69
|
+
- set/abstract/0_filter/filter_form/compact_filter_form.haml
|
70
|
+
- set/abstract/0_filter/filter_form/compact_quick_filters.haml
|
71
|
+
- set/abstract/0_filter/filter_form/filter_bars.haml
|
72
|
+
- set/abstract/0_filter/filter_form/filter_sort_dropdown.haml
|
73
|
+
- set/abstract/0_filter/filter_form/filtered_results_header.haml
|
74
|
+
- set/abstract/0_filter/filter_form/offcanvas_filters.haml
|
75
|
+
- set/abstract/0_filter/filter_form/open_filter_button.haml
|
76
|
+
- set/abstract/0_filter/filter_form/select_item.haml
|
77
|
+
- set/abstract/0_filter/filter_form/selectable_filtered_content.haml
|
78
|
+
- set/abstract/0_filter/filter_input_fields.rb
|
79
|
+
- set/abstract/0_filter/filter_input_fields/check_filter.haml
|
80
|
+
- set/abstract/cached_type_options.rb
|
81
|
+
- set/abstract/cql_search/filter.rb
|
82
|
+
- set/abstract/filter_link.rb
|
83
|
+
- set/abstract/filterable_list.rb
|
84
|
+
- set/abstract/filtered_list.rb
|
85
|
+
- set/abstract/filtered_list/filter_items.haml
|
86
|
+
- set/abstract/filtered_list/filtered_list_input.haml
|
87
|
+
- set/abstract/list/filter.rb
|
88
|
+
- set/abstract/search/filter_inclusion.rb
|
89
|
+
- set/all/filtered_list_item.haml
|
90
|
+
- set/all/filtered_list_item.rb
|
91
|
+
- set/type/list.rb
|
92
|
+
homepage: http://decko.org
|
93
|
+
licenses:
|
94
|
+
- GPL-3.0
|
95
|
+
metadata:
|
96
|
+
card-mod: filter
|
97
|
+
post_install_message:
|
98
|
+
rdoc_options: []
|
99
|
+
require_paths:
|
100
|
+
- lib
|
101
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
102
|
+
requirements:
|
103
|
+
- - ">="
|
104
|
+
- !ruby/object:Gem::Version
|
105
|
+
version: '2.5'
|
106
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
requirements: []
|
112
|
+
rubygems_version: 3.2.33
|
113
|
+
signing_key:
|
114
|
+
specification_version: 4
|
115
|
+
summary: filter
|
116
|
+
test_files: []
|