activeadmin-searchable_select 1.0.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/.gitignore +9 -0
- data/.rspec +1 -0
- data/.rubocop.yml +10 -0
- data/.travis.yml +16 -0
- data/.yardopts +2 -0
- data/Appraisals +15 -0
- data/CHANGELOG.md +7 -0
- data/Gemfile +3 -0
- data/LICENSE.txt +25 -0
- data/README.md +247 -0
- data/Rakefile +6 -0
- data/activeadmin-searchable_select.gemspec +37 -0
- data/app/assets/javascripts/active_admin/searchable_select.js.coffee +3 -0
- data/app/assets/javascripts/active_admin/searchable_select/init.js.coffee +29 -0
- data/app/assets/stylesheets/active_admin/searchable_select.scss +5 -0
- data/bin/rspec +17 -0
- data/gemfiles/rails_4.2_active_admin_1.0.0.pre4.gemfile +9 -0
- data/gemfiles/rails_5.1_active_admin_1.0.gemfile +8 -0
- data/gemfiles/rails_5.1_active_admin_1.1.gemfile +8 -0
- data/lib/activeadmin-searchable_select.rb +6 -0
- data/lib/activeadmin/inputs/filters/searchable_select_input.rb +13 -0
- data/lib/activeadmin/inputs/searchable_select_input.rb +11 -0
- data/lib/activeadmin/searchable_select.rb +20 -0
- data/lib/activeadmin/searchable_select/engine.rb +11 -0
- data/lib/activeadmin/searchable_select/option_collection.rb +103 -0
- data/lib/activeadmin/searchable_select/resource_dsl_extension.rb +39 -0
- data/lib/activeadmin/searchable_select/resource_extension.rb +10 -0
- data/lib/activeadmin/searchable_select/select_input_extension.rb +130 -0
- data/lib/activeadmin/searchable_select/version.rb +5 -0
- data/spec/features/ajax_params_spec.rb +53 -0
- data/spec/features/end_to_end_spec.rb +83 -0
- data/spec/features/filter_input_spec.rb +191 -0
- data/spec/features/form_input_spec.rb +178 -0
- data/spec/features/inline_ajax_setting_spec.rb +41 -0
- data/spec/features/input_errors_spec.rb +55 -0
- data/spec/features/options_dsl_spec.rb +248 -0
- data/spec/internal/app/assets/javascripts/active_admin.js +2 -0
- data/spec/internal/app/assets/stylesheets/active_admin.scss +2 -0
- data/spec/internal/app/controllers/application_controller.rb +5 -0
- data/spec/internal/config/database.yml +3 -0
- data/spec/internal/config/initializers/assets.rb +3 -0
- data/spec/internal/config/routes.rb +3 -0
- data/spec/internal/db/schema.rb +26 -0
- data/spec/internal/log/.gitignore +1 -0
- data/spec/internal/public/favicon.ico +0 -0
- data/spec/rails_helper.rb +13 -0
- data/spec/spec_helper.rb +96 -0
- data/spec/support/active_admin_helpers.rb +9 -0
- data/spec/support/capybara.rb +8 -0
- data/spec/support/models.rb +21 -0
- data/spec/support/pluck_polyfill.rb +12 -0
- data/spec/support/reset_settings.rb +5 -0
- metadata +311 -0
data/bin/rspec
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
#
|
4
|
+
# This file was generated by Bundler.
|
5
|
+
#
|
6
|
+
# The application 'rspec' is installed as part of a gem, and
|
7
|
+
# this file is here to facilitate running it.
|
8
|
+
#
|
9
|
+
|
10
|
+
require "pathname"
|
11
|
+
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
|
12
|
+
Pathname.new(__FILE__).realpath)
|
13
|
+
|
14
|
+
require "rubygems"
|
15
|
+
require "bundler/setup"
|
16
|
+
|
17
|
+
load Gem.bin_path("rspec-core", "rspec")
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module ActiveAdmin
|
2
|
+
module Inputs
|
3
|
+
module Filters
|
4
|
+
# Searchable select input type for ActiveAdmin filters.
|
5
|
+
#
|
6
|
+
# @see ActiveAdmin::SearchableSelect::SelectInputExtension
|
7
|
+
# SelectInputExtension for list of available options.
|
8
|
+
class SearchableSelectInput < SelectInput
|
9
|
+
include SearchableSelect::SelectInputExtension
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
module ActiveAdmin
|
2
|
+
module Inputs
|
3
|
+
# Searchable select input type for ActiveAdmin filters.
|
4
|
+
#
|
5
|
+
# @see ActiveAdmin::SearchableSelect::SelectInputExtension
|
6
|
+
# SelectInputExtension for list of available options.
|
7
|
+
class SearchableSelectInput < Formtastic::Inputs::SelectInput
|
8
|
+
include SearchableSelect::SelectInputExtension
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'activeadmin/searchable_select/engine'
|
2
|
+
require 'activeadmin/searchable_select/option_collection'
|
3
|
+
require 'activeadmin/searchable_select/resource_extension'
|
4
|
+
require 'activeadmin/searchable_select/resource_dsl_extension'
|
5
|
+
require 'activeadmin/searchable_select/select_input_extension'
|
6
|
+
require 'activeadmin/searchable_select/version'
|
7
|
+
|
8
|
+
ActiveAdmin::Resource.send :include, ActiveAdmin::SearchableSelect::ResourceExtension
|
9
|
+
ActiveAdmin::ResourceDSL.send :include, ActiveAdmin::SearchableSelect::ResourceDSLExtension
|
10
|
+
|
11
|
+
module ActiveAdmin
|
12
|
+
# Global settings for searchable selects
|
13
|
+
module SearchableSelect
|
14
|
+
# Statically render all options into searchable selects with
|
15
|
+
# `ajax` option set to true. This can be used to ease ui driven
|
16
|
+
# integration testing.
|
17
|
+
mattr_accessor :inline_ajax_options
|
18
|
+
self.inline_ajax_options = false
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
module ActiveAdmin
|
2
|
+
module SearchableSelect
|
3
|
+
# @api private
|
4
|
+
class OptionCollection
|
5
|
+
def initialize(name, options)
|
6
|
+
@name = name
|
7
|
+
@scope = extract_scope_option(options)
|
8
|
+
@display_text = extract_display_text_option(options)
|
9
|
+
@filter = extract_filter_option(options)
|
10
|
+
@per_page = options.fetch(:per_page, 10)
|
11
|
+
end
|
12
|
+
|
13
|
+
def scope(template, params)
|
14
|
+
case @scope
|
15
|
+
when Proc
|
16
|
+
if @scope.arity.zero?
|
17
|
+
template.instance_exec(&@scope)
|
18
|
+
else
|
19
|
+
template.instance_exec(params, &@scope)
|
20
|
+
end
|
21
|
+
else
|
22
|
+
@scope
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def display_text(record)
|
27
|
+
@display_text.call(record)
|
28
|
+
end
|
29
|
+
|
30
|
+
def collection_action_name
|
31
|
+
"#{@name}_options"
|
32
|
+
end
|
33
|
+
|
34
|
+
def as_json(template, params)
|
35
|
+
records, more = fetch_records(template, params)
|
36
|
+
|
37
|
+
results = records.map do |record|
|
38
|
+
{
|
39
|
+
id: record.id,
|
40
|
+
text: display_text(record)
|
41
|
+
}
|
42
|
+
end
|
43
|
+
|
44
|
+
{ results: results, pagination: { more: more } }
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
attr_reader :per_page
|
50
|
+
|
51
|
+
def fetch_records(template, params)
|
52
|
+
paginate(filter(scope(template, params), params[:term]),
|
53
|
+
params[:page])
|
54
|
+
end
|
55
|
+
|
56
|
+
def filter(scope, term)
|
57
|
+
term ? @filter.call(term, scope) : scope
|
58
|
+
end
|
59
|
+
|
60
|
+
def paginate(scope, page_index)
|
61
|
+
page_index = page_index.to_i
|
62
|
+
|
63
|
+
records = scope.limit(per_page + 1).offset(page_index * per_page).to_a
|
64
|
+
|
65
|
+
[
|
66
|
+
records.slice(0, per_page),
|
67
|
+
records.size > per_page
|
68
|
+
]
|
69
|
+
end
|
70
|
+
|
71
|
+
def extract_scope_option(options)
|
72
|
+
options.fetch(:scope) do
|
73
|
+
raise('Missing option: scope. ' \
|
74
|
+
'Pass the collection of items to render options for.')
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def extract_display_text_option(options)
|
79
|
+
options.fetch(:display_text) do
|
80
|
+
text_attribute = options.fetch(:text_attribute) do
|
81
|
+
raise('Missing option: display_text or text_attribute. ' \
|
82
|
+
'Either pass a proc to determine the display text for a record ' \
|
83
|
+
'or set the text_attribute option.')
|
84
|
+
end
|
85
|
+
|
86
|
+
->(record) { record.send(text_attribute) }
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def extract_filter_option(options)
|
91
|
+
options.fetch(:filter) do
|
92
|
+
text_attribute = options.fetch(:text_attribute) do
|
93
|
+
raise('Missing option: filter or text_attribute. ' \
|
94
|
+
'Either pass a proc which filters the scope according to a given ' \
|
95
|
+
'or set the text_attribute option to apply a default Ransack filter.')
|
96
|
+
end
|
97
|
+
|
98
|
+
->(term, scope) { scope.ransack("#{text_attribute}_cont" => term).result }
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module ActiveAdmin
|
2
|
+
module SearchableSelect
|
3
|
+
# Mixin for ActiveAdmin resource DSL
|
4
|
+
module ResourceDSLExtension
|
5
|
+
# Define a collection action to serve options JSON data for
|
6
|
+
# searchable selects.
|
7
|
+
#
|
8
|
+
# @param scope [ActiveRecord::Relation, Proc] Either a
|
9
|
+
# collection of records to create options for or a proc
|
10
|
+
# returning such a collection. Procs are evaluated in the
|
11
|
+
# context of the collection action defined by this
|
12
|
+
# method. Procs can optionally take a single `params` argument
|
13
|
+
# containing data defined under the `params` key of the
|
14
|
+
# input's `ajax` option. Required.
|
15
|
+
#
|
16
|
+
# @param text_attribute [Symbol] Name of attribute to use as
|
17
|
+
# display name and to filter by search term.
|
18
|
+
#
|
19
|
+
# @param display_text [Proc] Takes the record as
|
20
|
+
# parameter. Required if `text_attribute` is not present.
|
21
|
+
#
|
22
|
+
# @param filter [Proc] Takes the search term and an Active
|
23
|
+
# Record scope as parameters and needs to return a scope of
|
24
|
+
# filtered records. Required if `text_attribute` is not
|
25
|
+
# present.
|
26
|
+
#
|
27
|
+
# @param name [Symbol] Optional collection name if helper is
|
28
|
+
# used multiple times within one resource.
|
29
|
+
def searchable_select_options(name: :all, **options)
|
30
|
+
option_collection = OptionCollection.new(name, options)
|
31
|
+
config.searchable_select_option_collections[name] = option_collection
|
32
|
+
|
33
|
+
collection_action(option_collection.collection_action_name) do
|
34
|
+
render(json: option_collection.as_json(self, params))
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,130 @@
|
|
1
|
+
module ActiveAdmin
|
2
|
+
module SearchableSelect
|
3
|
+
# Mixin for searchable select inputs.
|
4
|
+
#
|
5
|
+
# Supports the same options as inputs of type `:select`.
|
6
|
+
#
|
7
|
+
# Adds support for an `ajax` option to fetch options data from a
|
8
|
+
# JSON endpoint. Pass either `true` to use defaults or a hash
|
9
|
+
# containing some of the following options:
|
10
|
+
#
|
11
|
+
# - `resource`: ActiveRecord model class of ActiveAdmin resource
|
12
|
+
# which provides the collection action to fetch options
|
13
|
+
# from. By default the resource is auto detected via the name
|
14
|
+
# of the input attribute.
|
15
|
+
#
|
16
|
+
# - `collection_name`: Name passed to the
|
17
|
+
# `searchable_select_options` method that defines the collection
|
18
|
+
# action to fetch options from.
|
19
|
+
#
|
20
|
+
# - `params`: Hash of query parameters that shall be passed to the
|
21
|
+
# options endpoint.
|
22
|
+
#
|
23
|
+
# If the `ajax` option is present, the `collection` option is
|
24
|
+
# ignored.
|
25
|
+
module SelectInputExtension
|
26
|
+
# @api private
|
27
|
+
def input_html_options
|
28
|
+
options = super
|
29
|
+
options[:class] = [options[:class], 'searchable-select-input'].compact.join(' ')
|
30
|
+
options.merge('data-ajax-url' => ajax_url)
|
31
|
+
end
|
32
|
+
|
33
|
+
# @api private
|
34
|
+
def collection_from_options
|
35
|
+
return super unless options[:ajax]
|
36
|
+
|
37
|
+
if SearchableSelect.inline_ajax_options
|
38
|
+
all_options_collection
|
39
|
+
else
|
40
|
+
selected_value_collection
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def ajax_url
|
47
|
+
return unless options[:ajax]
|
48
|
+
template.polymorphic_path([:admin, ajax_resource_class],
|
49
|
+
action: option_collection.collection_action_name,
|
50
|
+
**ajax_params)
|
51
|
+
end
|
52
|
+
|
53
|
+
def all_options_collection
|
54
|
+
option_collection_scope.all.map do |record|
|
55
|
+
option_for_record(record)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def selected_value_collection
|
60
|
+
[selected_value_option].compact
|
61
|
+
end
|
62
|
+
|
63
|
+
def selected_value_option
|
64
|
+
option_for_record(selected_record) if selected_record
|
65
|
+
end
|
66
|
+
|
67
|
+
def option_for_record(record)
|
68
|
+
[option_collection.display_text(record), record.id]
|
69
|
+
end
|
70
|
+
|
71
|
+
def selected_record
|
72
|
+
@selected_record ||=
|
73
|
+
selected_value && option_collection_scope.find_by_id(selected_value)
|
74
|
+
end
|
75
|
+
|
76
|
+
def selected_value
|
77
|
+
@object.send(input_name) if @object
|
78
|
+
end
|
79
|
+
|
80
|
+
def option_collection_scope
|
81
|
+
option_collection.scope(template, ajax_params)
|
82
|
+
end
|
83
|
+
|
84
|
+
def option_collection
|
85
|
+
ajax_resource
|
86
|
+
.searchable_select_option_collections
|
87
|
+
.fetch(ajax_option_collection_name) do
|
88
|
+
raise("No option collection named '#{ajax_option_collection_name}' " \
|
89
|
+
"defined in '#{ajax_resource_class.name}' admin.")
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def ajax_resource
|
94
|
+
@ajax_resource ||=
|
95
|
+
template.active_admin_namespace.resource_for(ajax_resource_class) ||
|
96
|
+
raise("No admin found for '#{ajax_resource_class.name}' to fetch " \
|
97
|
+
'options for searchable select input from.')
|
98
|
+
end
|
99
|
+
|
100
|
+
def ajax_resource_class
|
101
|
+
ajax_options.fetch(:resource) do
|
102
|
+
raise_cannot_auto_detect_resource unless reflection
|
103
|
+
reflection.klass
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def raise_cannot_auto_detect_resource
|
108
|
+
raise('Cannot auto detect resource to fetch options for searchable select input from. ' \
|
109
|
+
"Explicitly pass class of an ActiveAdmin resource:\n\n" \
|
110
|
+
" f.input(:custom_category,\n" \
|
111
|
+
" type: :searchable_select,\n" \
|
112
|
+
" ajax: {\n" \
|
113
|
+
" resource: Category\n" \
|
114
|
+
" })\n")
|
115
|
+
end
|
116
|
+
|
117
|
+
def ajax_option_collection_name
|
118
|
+
ajax_options.fetch(:collection_name, :all)
|
119
|
+
end
|
120
|
+
|
121
|
+
def ajax_params
|
122
|
+
ajax_options.fetch(:params, {})
|
123
|
+
end
|
124
|
+
|
125
|
+
def ajax_options
|
126
|
+
options[:ajax] == true ? {} : options[:ajax]
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'rails_helper'
|
2
|
+
|
3
|
+
require 'support/models'
|
4
|
+
require 'support/capybara'
|
5
|
+
require 'support/active_admin_helpers'
|
6
|
+
|
7
|
+
RSpec.describe 'ajax params', type: :request do
|
8
|
+
before(:each) do
|
9
|
+
ActiveAdminHelpers.setup do
|
10
|
+
ActiveAdmin.register(Category) do
|
11
|
+
searchable_select_options(scope: lambda do |params|
|
12
|
+
Category.where(created_by_id: params[:created_by])
|
13
|
+
end,
|
14
|
+
text_attribute: :name)
|
15
|
+
end
|
16
|
+
|
17
|
+
ActiveAdmin.register(Post) do
|
18
|
+
form do |f|
|
19
|
+
f.input(:category,
|
20
|
+
as: :searchable_select,
|
21
|
+
ajax: {
|
22
|
+
params: {
|
23
|
+
created_by: current_user.id
|
24
|
+
}
|
25
|
+
})
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
it 'passes parameters when rendering selected item' do
|
32
|
+
user = User.create
|
33
|
+
category = Category.create(name: 'Travel', created_by: user)
|
34
|
+
post = Post.create(category: category)
|
35
|
+
|
36
|
+
ApplicationController.current_user = user
|
37
|
+
get "/admin/posts/#{post.id}/edit"
|
38
|
+
|
39
|
+
expect(response.body).to have_selector('.searchable-select-input option[selected]',
|
40
|
+
text: 'Travel')
|
41
|
+
end
|
42
|
+
|
43
|
+
it 'includes parameters in ajax url' do
|
44
|
+
user = User.create
|
45
|
+
|
46
|
+
ApplicationController.current_user = user
|
47
|
+
get '/admin/posts/new'
|
48
|
+
|
49
|
+
url_matcher = "?created_by=#{user.id}"
|
50
|
+
expect(response.body).to have_selector('.searchable-select-input' \
|
51
|
+
"[data-ajax-url*='#{url_matcher}']")
|
52
|
+
end
|
53
|
+
end
|