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.
Files changed (31) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +60 -0
  3. data/lib/card/filter_query.rb +78 -0
  4. data/set/abstract/0_filter/filter_fields.rb +46 -0
  5. data/set/abstract/0_filter/filter_form/_compact_filter_input.haml +7 -0
  6. data/set/abstract/0_filter/filter_form/compact_filter_form.haml +46 -0
  7. data/set/abstract/0_filter/filter_form/compact_quick_filters.haml +14 -0
  8. data/set/abstract/0_filter/filter_form/filter_bars.haml +4 -0
  9. data/set/abstract/0_filter/filter_form/filter_sort_dropdown.haml +12 -0
  10. data/set/abstract/0_filter/filter_form/filtered_results_header.haml +33 -0
  11. data/set/abstract/0_filter/filter_form/offcanvas_filters.haml +9 -0
  12. data/set/abstract/0_filter/filter_form/open_filter_button.haml +1 -0
  13. data/set/abstract/0_filter/filter_form/select_item.haml +15 -0
  14. data/set/abstract/0_filter/filter_form/selectable_filtered_content.haml +2 -0
  15. data/set/abstract/0_filter/filter_form.rb +123 -0
  16. data/set/abstract/0_filter/filter_input_fields/check_filter.haml +13 -0
  17. data/set/abstract/0_filter/filter_input_fields.rb +111 -0
  18. data/set/abstract/0_filter.rb +131 -0
  19. data/set/abstract/cached_type_options.rb +8 -0
  20. data/set/abstract/cql_search/filter.rb +33 -0
  21. data/set/abstract/filter_link.rb +15 -0
  22. data/set/abstract/filterable_list.rb +16 -0
  23. data/set/abstract/filtered_list/filter_items.haml +28 -0
  24. data/set/abstract/filtered_list/filtered_list_input.haml +7 -0
  25. data/set/abstract/filtered_list.rb +105 -0
  26. data/set/abstract/list/filter.rb +1 -0
  27. data/set/abstract/search/filter_inclusion.rb +7 -0
  28. data/set/all/filtered_list_item.haml +8 -0
  29. data/set/all/filtered_list_item.rb +11 -0
  30. data/set/type/list.rb +4 -0
  31. 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,4 @@
1
+ %form._filter-form._filter-bars{ filter_form_args }
2
+ .filter-form.accordion
3
+ - filter_map.each do |item|
4
+ = filter_bar item
@@ -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
+ &nbsp;
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,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,2 @@
1
+ ._selectable-filtered-content._over-card-link
2
+ = render_compact_filtered_content hide: :export_button
@@ -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
@@ -0,0 +1,8 @@
1
+ ._filtered-list-item.row{ data: wrap_data }
2
+ .col-1._handle
3
+ = icon_tag :reorder
4
+ .col-10
5
+ = interior
6
+ .col-1.filtered-list-item-button
7
+ %button._filtered-list-item-delete.btn.btn-secondary.btn-sm.m-2{ type: "button"}
8
+ = icon_tag :remove
@@ -0,0 +1,11 @@
1
+ basket[:list_input_options] << :filtered_list
2
+
3
+ def supports_content_option_view?
4
+ super || (item_name == "filtered list")
5
+ end
6
+
7
+ format :html do
8
+ wrapper :filtered_list_item, template: :haml do
9
+ haml :filtered_list_item
10
+ end
11
+ end
data/set/type/list.rb ADDED
@@ -0,0 +1,4 @@
1
+ def input_type_content_options
2
+ # TODO: need to make super work here somehow or refactor around baskets
3
+ %w[multiselect checkbox list] << "filtered list"
4
+ end
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: []