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.
Files changed (54) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +9 -0
  3. data/.rspec +1 -0
  4. data/.rubocop.yml +10 -0
  5. data/.travis.yml +16 -0
  6. data/.yardopts +2 -0
  7. data/Appraisals +15 -0
  8. data/CHANGELOG.md +7 -0
  9. data/Gemfile +3 -0
  10. data/LICENSE.txt +25 -0
  11. data/README.md +247 -0
  12. data/Rakefile +6 -0
  13. data/activeadmin-searchable_select.gemspec +37 -0
  14. data/app/assets/javascripts/active_admin/searchable_select.js.coffee +3 -0
  15. data/app/assets/javascripts/active_admin/searchable_select/init.js.coffee +29 -0
  16. data/app/assets/stylesheets/active_admin/searchable_select.scss +5 -0
  17. data/bin/rspec +17 -0
  18. data/gemfiles/rails_4.2_active_admin_1.0.0.pre4.gemfile +9 -0
  19. data/gemfiles/rails_5.1_active_admin_1.0.gemfile +8 -0
  20. data/gemfiles/rails_5.1_active_admin_1.1.gemfile +8 -0
  21. data/lib/activeadmin-searchable_select.rb +6 -0
  22. data/lib/activeadmin/inputs/filters/searchable_select_input.rb +13 -0
  23. data/lib/activeadmin/inputs/searchable_select_input.rb +11 -0
  24. data/lib/activeadmin/searchable_select.rb +20 -0
  25. data/lib/activeadmin/searchable_select/engine.rb +11 -0
  26. data/lib/activeadmin/searchable_select/option_collection.rb +103 -0
  27. data/lib/activeadmin/searchable_select/resource_dsl_extension.rb +39 -0
  28. data/lib/activeadmin/searchable_select/resource_extension.rb +10 -0
  29. data/lib/activeadmin/searchable_select/select_input_extension.rb +130 -0
  30. data/lib/activeadmin/searchable_select/version.rb +5 -0
  31. data/spec/features/ajax_params_spec.rb +53 -0
  32. data/spec/features/end_to_end_spec.rb +83 -0
  33. data/spec/features/filter_input_spec.rb +191 -0
  34. data/spec/features/form_input_spec.rb +178 -0
  35. data/spec/features/inline_ajax_setting_spec.rb +41 -0
  36. data/spec/features/input_errors_spec.rb +55 -0
  37. data/spec/features/options_dsl_spec.rb +248 -0
  38. data/spec/internal/app/assets/javascripts/active_admin.js +2 -0
  39. data/spec/internal/app/assets/stylesheets/active_admin.scss +2 -0
  40. data/spec/internal/app/controllers/application_controller.rb +5 -0
  41. data/spec/internal/config/database.yml +3 -0
  42. data/spec/internal/config/initializers/assets.rb +3 -0
  43. data/spec/internal/config/routes.rb +3 -0
  44. data/spec/internal/db/schema.rb +26 -0
  45. data/spec/internal/log/.gitignore +1 -0
  46. data/spec/internal/public/favicon.ico +0 -0
  47. data/spec/rails_helper.rb +13 -0
  48. data/spec/spec_helper.rb +96 -0
  49. data/spec/support/active_admin_helpers.rb +9 -0
  50. data/spec/support/capybara.rb +8 -0
  51. data/spec/support/models.rb +21 -0
  52. data/spec/support/pluck_polyfill.rb +12 -0
  53. data/spec/support/reset_settings.rb +5 -0
  54. metadata +311 -0
@@ -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,9 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "rails", "~> 4.2"
6
+ gem "activeadmin", "1.0.0.pre4"
7
+ gem "jquery-ui-rails", "~> 5.0"
8
+
9
+ gemspec path: "../"
@@ -0,0 +1,8 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "rails", "~> 5.1"
6
+ gem "activeadmin", "~> 1.0"
7
+
8
+ gemspec path: "../"
@@ -0,0 +1,8 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "rails", "~> 5.1"
6
+ gem "activeadmin", "~> 1.1"
7
+
8
+ gemspec path: "../"
@@ -0,0 +1,6 @@
1
+ # rubocop:disable Style/FileName
2
+
3
+ require 'activeadmin/searchable_select'
4
+
5
+ require 'activeadmin/inputs/filters/searchable_select_input'
6
+ require 'activeadmin/inputs/searchable_select_input'
@@ -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,11 @@
1
+ require 'active_admin'
2
+ require 'select2-rails'
3
+
4
+ module ActiveAdmin
5
+ module SearchableSelect
6
+ # @api private
7
+ class Engine < ::Rails::Engine
8
+ engine_name 'activeadmin_searchable_select'
9
+ end
10
+ end
11
+ 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,10 @@
1
+ module ActiveAdmin
2
+ module SearchableSelect
3
+ # @api private
4
+ module ResourceExtension
5
+ def searchable_select_option_collections
6
+ @searchable_select_option_collections ||= {}
7
+ end
8
+ end
9
+ end
10
+ 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,5 @@
1
+ module ActiveAdmin
2
+ module SearchableSelect
3
+ VERSION = '1.0.0'.freeze
4
+ end
5
+ 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