card-mod-filter 0.2
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|