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.
@@ -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,4 @@
1
+ en:
2
+ active_admin:
3
+ autocompletes:
4
+ filter_placeholder: Type to search
@@ -0,0 +1,4 @@
1
+ fi:
2
+ active_admin:
3
+ autocompletes:
4
+ filter_placeholder: Kirjoita hakeaksesi
@@ -0,0 +1,4 @@
1
+ sv:
2
+ active_admin:
3
+ autocompletes:
4
+ filter_placeholder: Skriv för att söka
@@ -0,0 +1,8 @@
1
+ track_under:
2
+ - lib
3
+ - app
4
+ minimum_line_percent: 100
5
+ report_formats:
6
+ - html
7
+ - cobertura
8
+ - console
@@ -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,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveAdmin
4
+ module Autocompletes
5
+ VERSION = "0.3.0"
6
+ end
7
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "activeadmin/autocompletes/version"
4
+ require "activeadmin/autocompletes/engine"
5
+
6
+ module ActiveAdmin
7
+ module Autocompletes
8
+ end
9
+ 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,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "activeadmin/autocompletes"
@@ -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