activeadmin-autocompletes 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +16 -0
- data/CODE_OF_CONDUCT.md +26 -0
- data/CONTRIBUTING.md +23 -0
- data/GOVERNANCE.md +25 -0
- data/LICENSE.md +21 -0
- data/README.md +111 -0
- data/SECURITY.md +25 -0
- data/activeadmin-autocompletes.gemspec +65 -0
- data/app/assets/controllers/activeadmin_autocompletes/filter_autocomplete_controller.js +324 -0
- data/app/views/active_admin/autocompletes/_field.html.erb +70 -0
- data/config/locales/en.yml +4 -0
- data/config/locales/fi.yml +4 -0
- data/config/locales/sv.yml +4 -0
- data/config/polyrun_coverage.yml +8 -0
- data/lib/activeadmin/autocompletes/engine.rb +43 -0
- data/lib/activeadmin/autocompletes/filter_autocomplete.rb +40 -0
- data/lib/activeadmin/autocompletes/input_support.rb +113 -0
- data/lib/activeadmin/autocompletes/version.rb +7 -0
- data/lib/activeadmin/autocompletes.rb +9 -0
- data/lib/activeadmin/inputs/autocomplete_input.rb +34 -0
- data/lib/activeadmin/inputs/filters/autocomplete_input.rb +39 -0
- data/lib/activeadmin-autocompletes.rb +3 -0
- data/sig/activeadmin/autocompletes.rbs +16 -0
- metadata +330 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
<%
|
|
2
|
+
object_name = local_assigns.fetch(:object_name)
|
|
3
|
+
field_name = local_assigns.fetch(:field_name)
|
|
4
|
+
field_id = "#{object_name}_#{field_name}".gsub(/[\[\]]+/, "_").gsub(/_+\z/, "")
|
|
5
|
+
list_id = "#{field_id}_options"
|
|
6
|
+
controller = local_assigns.fetch(:controller_name, "activeadmin-autocompletes--filter-autocomplete")
|
|
7
|
+
value_field = local_assigns.fetch(:value_field, false)
|
|
8
|
+
multiple = local_assigns.fetch(:multiple, false)
|
|
9
|
+
initial_value = local_assigns.fetch(:initial_value, nil)
|
|
10
|
+
initial_label = local_assigns.fetch(:initial_label, nil)
|
|
11
|
+
selected_items = Array(local_assigns.fetch(:selected_items, []))
|
|
12
|
+
display_value = value_field && !multiple ? initial_label.to_s : (multiple ? "" : initial_value.to_s)
|
|
13
|
+
placeholder = local_assigns.fetch(:placeholder, I18n.t("active_admin.autocompletes.filter_placeholder", default: "Type to search"))
|
|
14
|
+
label_text = local_assigns.fetch(:label_text, field_name.to_s.humanize)
|
|
15
|
+
search_url = local_assigns.fetch(:search_url)
|
|
16
|
+
page_size = local_assigns[:page_size]
|
|
17
|
+
params_payload = local_assigns.fetch(:params, {})
|
|
18
|
+
param_fields_payload = local_assigns.fetch(:param_fields, {})
|
|
19
|
+
field_param_name = multiple ? "#{object_name}[#{field_name}][]" : "#{object_name}[#{field_name}]"
|
|
20
|
+
%>
|
|
21
|
+
<div
|
|
22
|
+
data-controller="<%= controller %>"
|
|
23
|
+
data-<%= controller %>-url-value="<%= search_url %>"
|
|
24
|
+
data-<%= controller %>-multiple-value="<%= multiple %>"
|
|
25
|
+
<% if page_size.present? %>data-<%= controller %>-page-size-value="<%= page_size %>"<% end %>
|
|
26
|
+
<% if params_payload.present? %>data-<%= controller %>-params-value="<%= ERB::Util.html_escape(params_payload.to_json) %>"<% end %>
|
|
27
|
+
<% if param_fields_payload.present? %>data-<%= controller %>-param-fields-value="<%= ERB::Util.html_escape(param_fields_payload.to_json) %>"<% end %>
|
|
28
|
+
data-field-name="<%= field_param_name %>"
|
|
29
|
+
data-testid="filter-autocomplete-<%= field_name %>"
|
|
30
|
+
class="relative w-full"
|
|
31
|
+
>
|
|
32
|
+
<% if multiple %>
|
|
33
|
+
<div data-<%= controller %>-target="chips" class="mb-2 flex flex-wrap gap-1"></div>
|
|
34
|
+
<% selected_items.each do |item| %>
|
|
35
|
+
<input
|
|
36
|
+
type="hidden"
|
|
37
|
+
name="<%= field_param_name %>"
|
|
38
|
+
value="<%= ERB::Util.html_escape(item.fetch(:value).to_s) %>"
|
|
39
|
+
data-label="<%= ERB::Util.html_escape(item.fetch(:label).to_s) %>"
|
|
40
|
+
data-<%= controller %>-target="selected" />
|
|
41
|
+
<% end %>
|
|
42
|
+
<% elsif value_field %>
|
|
43
|
+
<input
|
|
44
|
+
type="hidden"
|
|
45
|
+
name="<%= field_param_name %>"
|
|
46
|
+
value="<%= ERB::Util.html_escape(initial_value.to_s) %>"
|
|
47
|
+
data-<%= controller %>-target="value" />
|
|
48
|
+
<% end %>
|
|
49
|
+
<input
|
|
50
|
+
id="<%= field_id %>"
|
|
51
|
+
type="text"
|
|
52
|
+
<% unless value_field || multiple %>name="<%= field_param_name %>"<% end %>
|
|
53
|
+
value="<%= ERB::Util.html_escape(display_value) %>"
|
|
54
|
+
placeholder="<%= ERB::Util.html_escape(placeholder) %>"
|
|
55
|
+
autocomplete="off"
|
|
56
|
+
class="w-full"
|
|
57
|
+
role="combobox"
|
|
58
|
+
aria-label="<%= ERB::Util.html_escape(label_text.to_s) %>"
|
|
59
|
+
aria-autocomplete="list"
|
|
60
|
+
aria-controls="<%= list_id %>"
|
|
61
|
+
aria-expanded="false"
|
|
62
|
+
data-<%= controller %>-target="query"
|
|
63
|
+
data-action="input-><%= controller %>#search focus-><%= controller %>#open keydown-><%= controller %>#navigate" />
|
|
64
|
+
<ul
|
|
65
|
+
id="<%= list_id %>"
|
|
66
|
+
data-<%= controller %>-target="list"
|
|
67
|
+
data-action="scroll-><%= controller %>#loadMore"
|
|
68
|
+
class="hidden absolute z-20 mt-1 max-h-60 w-full overflow-auto rounded border border-gray-300 bg-white shadow-lg dark:border-gray-600 dark:bg-gray-900"
|
|
69
|
+
role="listbox"></ul>
|
|
70
|
+
</div>
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveAdmin
|
|
4
|
+
module Autocompletes
|
|
5
|
+
class Engine < ::Rails::Engine
|
|
6
|
+
engine_name "activeadmin_autocompletes"
|
|
7
|
+
|
|
8
|
+
initializer "activeadmin_autocompletes.load" do
|
|
9
|
+
require "activeadmin/autocompletes/filter_autocomplete"
|
|
10
|
+
require "activeadmin/autocompletes/input_support"
|
|
11
|
+
require "activeadmin/inputs/filters/autocomplete_input"
|
|
12
|
+
require "activeadmin/inputs/autocomplete_input"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
initializer "activeadmin_autocompletes.assets" do |app|
|
|
16
|
+
assets_path = root.join("app/assets")
|
|
17
|
+
if app.config.respond_to?(:importmap)
|
|
18
|
+
app.config.importmap.cache_sweepers << assets_path.join("controllers")
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
initializer "activeadmin_autocompletes.importmap", after: "active_admin.importmap" do
|
|
23
|
+
next unless defined?(ActiveAdmin) && ActiveAdmin.respond_to?(:importmap) && ActiveAdmin.importmap
|
|
24
|
+
|
|
25
|
+
ActiveAdmin.importmap.draw do
|
|
26
|
+
pin "controllers/activeadmin_autocompletes/filter_autocomplete_controller",
|
|
27
|
+
to: "activeadmin_autocompletes/filter_autocomplete_controller.js"
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
initializer "activeadmin_autocompletes.i18n" do
|
|
32
|
+
I18n.load_path += Dir[root.join("config/locales/*.yml")]
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
initializer "activeadmin_autocompletes.view_paths", after: :add_view_paths do
|
|
36
|
+
views_path = root.join("app/views").to_s
|
|
37
|
+
ActiveSupport.on_load(:action_controller) do
|
|
38
|
+
prepend_view_path(views_path)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveAdmin
|
|
4
|
+
module Autocompletes
|
|
5
|
+
module FilterAutocomplete
|
|
6
|
+
DEFAULT_LIMIT = 20
|
|
7
|
+
MAX_LIMIT = 50
|
|
8
|
+
MAX_QUERY_CHARS = 200
|
|
9
|
+
|
|
10
|
+
def self.clamp_limit(raw)
|
|
11
|
+
raw.to_i.clamp(1, MAX_LIMIT)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def self.clamp_query(raw)
|
|
15
|
+
query = raw.to_s.strip
|
|
16
|
+
query = query.byteslice(0, MAX_QUERY_CHARS) if query.bytesize > MAX_QUERY_CHARS
|
|
17
|
+
query.empty? ? nil : query
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def filter_autocomplete(attribute, action: nil, &block)
|
|
21
|
+
raise ArgumentError, "filter_autocomplete requires a block" unless block
|
|
22
|
+
|
|
23
|
+
action_name = (action || :"#{attribute}_options").to_sym
|
|
24
|
+
|
|
25
|
+
collection_action action_name, method: :get do
|
|
26
|
+
authorize! :index, config.resource_class
|
|
27
|
+
|
|
28
|
+
limit = ActiveAdmin::Autocompletes::FilterAutocomplete.clamp_limit(
|
|
29
|
+
params.fetch(:limit, ActiveAdmin::Autocompletes::FilterAutocomplete::DEFAULT_LIMIT)
|
|
30
|
+
)
|
|
31
|
+
offset = [params.fetch(:offset, 0).to_i, 0].max
|
|
32
|
+
query = ActiveAdmin::Autocompletes::FilterAutocomplete.clamp_query(params[:q])
|
|
33
|
+
|
|
34
|
+
items = instance_exec(query:, limit:, offset:, &block)
|
|
35
|
+
render json: Array(items)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support/core_ext/object/blank"
|
|
4
|
+
|
|
5
|
+
module ActiveAdmin
|
|
6
|
+
module Autocompletes
|
|
7
|
+
module InputSupport
|
|
8
|
+
CONTROLLER = "activeadmin-autocompletes--filter-autocomplete"
|
|
9
|
+
PARTIAL = "active_admin/autocompletes/field"
|
|
10
|
+
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
def autocomplete_search_url
|
|
14
|
+
url = options[:url]
|
|
15
|
+
url = template.instance_exec(&url) if url.respond_to?(:call)
|
|
16
|
+
url.to_s
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def autocomplete_placeholder
|
|
20
|
+
options[:placeholder].presence || I18n.t(
|
|
21
|
+
"active_admin.autocompletes.filter_placeholder",
|
|
22
|
+
default: "Type to search"
|
|
23
|
+
)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def value_field?
|
|
27
|
+
options[:value_field].present?
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def multiple?
|
|
31
|
+
options[:multiple].present?
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def page_size_value
|
|
35
|
+
raw = options[:page_size]
|
|
36
|
+
return if raw.blank?
|
|
37
|
+
|
|
38
|
+
FilterAutocomplete.clamp_limit(raw)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def static_params
|
|
42
|
+
raw = options[:params]
|
|
43
|
+
return {} if raw.blank?
|
|
44
|
+
|
|
45
|
+
hash = raw.respond_to?(:call) ? template.instance_exec(&raw) : raw
|
|
46
|
+
stringify_option_hash(hash, keep_nil: true)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def param_fields
|
|
50
|
+
raw = options[:param_fields]
|
|
51
|
+
return {} if raw.blank?
|
|
52
|
+
|
|
53
|
+
stringify_option_hash(raw, keep_nil: false)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def selected_items_for(current_value)
|
|
57
|
+
return [] unless multiple?
|
|
58
|
+
|
|
59
|
+
labels = options[:initial_labels]
|
|
60
|
+
Array(current_value).filter_map do |value|
|
|
61
|
+
next if value.blank?
|
|
62
|
+
|
|
63
|
+
{value: value, label: label_for_value(value, labels)}
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def stringify_option_hash(raw, keep_nil:)
|
|
68
|
+
out = {}
|
|
69
|
+
raw.to_h.each do |key, value|
|
|
70
|
+
out[key.to_s] = keep_nil ? value&.to_s : value.to_s
|
|
71
|
+
end
|
|
72
|
+
out
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def initial_label_for(current_value)
|
|
76
|
+
label = options[:initial_label]
|
|
77
|
+
return "" if current_value.blank? && label.blank?
|
|
78
|
+
|
|
79
|
+
label_for_value(current_value, label)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def label_for_value(value, labels)
|
|
83
|
+
resolved = if labels.respond_to?(:call)
|
|
84
|
+
template.instance_exec(value, &labels)
|
|
85
|
+
elsif labels.is_a?(Hash)
|
|
86
|
+
labels[value] || labels[value.to_s]
|
|
87
|
+
else
|
|
88
|
+
labels
|
|
89
|
+
end
|
|
90
|
+
resolved.to_s.presence || value.to_s
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def autocomplete_locals(object_name:, field_name:, current_value:)
|
|
94
|
+
{
|
|
95
|
+
object_name: object_name,
|
|
96
|
+
field_name: field_name,
|
|
97
|
+
search_url: autocomplete_search_url,
|
|
98
|
+
placeholder: autocomplete_placeholder,
|
|
99
|
+
initial_value: current_value,
|
|
100
|
+
initial_label: initial_label_for(current_value),
|
|
101
|
+
selected_items: selected_items_for(current_value),
|
|
102
|
+
label_text: label_from_options,
|
|
103
|
+
controller_name: CONTROLLER,
|
|
104
|
+
value_field: value_field?,
|
|
105
|
+
multiple: multiple?,
|
|
106
|
+
page_size: page_size_value,
|
|
107
|
+
params: static_params,
|
|
108
|
+
param_fields: param_fields
|
|
109
|
+
}
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "activeadmin/autocompletes/input_support"
|
|
4
|
+
|
|
5
|
+
module ActiveAdmin
|
|
6
|
+
module Inputs
|
|
7
|
+
class AutocompleteInput < ::Formtastic::Inputs::StringInput
|
|
8
|
+
include ActiveAdmin::Autocompletes::InputSupport
|
|
9
|
+
|
|
10
|
+
def to_html
|
|
11
|
+
input_wrapping do
|
|
12
|
+
label_html << template.render(
|
|
13
|
+
PARTIAL,
|
|
14
|
+
**autocomplete_locals(
|
|
15
|
+
object_name: builder.object_name,
|
|
16
|
+
field_name: method,
|
|
17
|
+
current_value: current_value
|
|
18
|
+
)
|
|
19
|
+
)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def current_value
|
|
26
|
+
return options[:selected] if options.key?(:selected)
|
|
27
|
+
|
|
28
|
+
object.public_send(method) if object.respond_to?(method)
|
|
29
|
+
rescue NoMethodError
|
|
30
|
+
nil
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "activeadmin/autocompletes/input_support"
|
|
4
|
+
|
|
5
|
+
module ActiveAdmin
|
|
6
|
+
module Inputs
|
|
7
|
+
module Filters
|
|
8
|
+
class AutocompleteInput < ::Formtastic::Inputs::StringInput
|
|
9
|
+
include Base
|
|
10
|
+
include ActiveAdmin::Autocompletes::InputSupport
|
|
11
|
+
|
|
12
|
+
def to_html
|
|
13
|
+
input_wrapping do
|
|
14
|
+
label_html << template.render(
|
|
15
|
+
PARTIAL,
|
|
16
|
+
**autocomplete_locals(
|
|
17
|
+
object_name: builder.object_name,
|
|
18
|
+
field_name: input_name,
|
|
19
|
+
current_value: current_value
|
|
20
|
+
)
|
|
21
|
+
)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def input_name
|
|
26
|
+
method
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def current_value
|
|
32
|
+
@object.public_send(method).presence if @object.respond_to?(method)
|
|
33
|
+
rescue NoMethodError
|
|
34
|
+
nil
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
module ActiveAdmin
|
|
2
|
+
module Autocompletes
|
|
3
|
+
VERSION: String
|
|
4
|
+
|
|
5
|
+
module FilterAutocomplete
|
|
6
|
+
DEFAULT_LIMIT: Integer
|
|
7
|
+
MAX_LIMIT: Integer
|
|
8
|
+
MAX_QUERY_CHARS: Integer
|
|
9
|
+
|
|
10
|
+
def self.clamp_limit: (untyped raw) -> Integer
|
|
11
|
+
def self.clamp_query: (untyped raw) -> String?
|
|
12
|
+
|
|
13
|
+
def filter_autocomplete: (untyped attribute, ?action: untyped?) { (query: String?, limit: Integer, offset: Integer) -> untyped } -> void
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|