activeadmin-searchable_select 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|