├── .fasterer.yml ├── .github ├── FUNDING.yml └── workflows │ ├── linters.yml │ └── specs.yml ├── .gitignore ├── .reviewdog.yml ├── .rspec ├── .rubocop.yml ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── activeadmin_select_many.gemspec ├── app └── assets │ ├── javascripts │ └── activeadmin │ │ └── select_many.js │ └── stylesheets │ └── activeadmin │ └── _select_many.sass ├── bin ├── fasterer ├── rails ├── rake ├── rspec └── rubocop ├── config └── locales │ └── en.yml ├── lib ├── activeadmin │ ├── select_many.rb │ └── select_many │ │ ├── engine.rb │ │ └── version.rb ├── activeadmin_select_many.rb └── formtastic │ └── inputs │ ├── select_many_input.rb │ └── select_one_input.rb ├── screenshot.png └── spec ├── dummy ├── .ruby-version ├── .tool-versions ├── Rakefile ├── app │ ├── admin │ │ ├── authors.rb │ │ ├── dashboard.rb │ │ ├── posts.rb │ │ └── tags.rb │ ├── assets │ │ ├── config │ │ │ └── manifest.js │ │ ├── images │ │ │ └── .keep │ │ ├── javascripts │ │ │ └── active_admin.js │ │ └── stylesheets │ │ │ ├── active_admin.scss │ │ │ └── application.css │ ├── channels │ │ └── application_cable │ │ │ ├── channel.rb │ │ │ └── connection.rb │ ├── controllers │ │ ├── application_controller.rb │ │ └── concerns │ │ │ └── .keep │ ├── helpers │ │ └── application_helper.rb │ ├── javascript │ │ └── packs │ │ │ └── application.js │ ├── jobs │ │ └── application_job.rb │ ├── mailers │ │ └── application_mailer.rb │ ├── models │ │ ├── application_record.rb │ │ ├── author.rb │ │ ├── concerns │ │ │ └── .keep │ │ ├── post.rb │ │ ├── post_tag.rb │ │ ├── profile.rb │ │ └── tag.rb │ └── views │ │ └── layouts │ │ ├── application.html.erb │ │ ├── mailer.html.erb │ │ └── mailer.text.erb ├── bin │ ├── rails │ ├── rake │ └── setup ├── config.ru ├── config │ ├── application.rb │ ├── boot.rb │ ├── cable.yml │ ├── database.yml │ ├── environment.rb │ ├── environments │ │ ├── development.rb │ │ ├── production.rb │ │ └── test.rb │ ├── initializers │ │ ├── active_admin.rb │ │ ├── application_controller_renderer.rb │ │ ├── assets.rb │ │ ├── backtrace_silencers.rb │ │ ├── content_security_policy.rb │ │ ├── cookies_serializer.rb │ │ ├── filter_parameter_logging.rb │ │ ├── inflections.rb │ │ ├── mime_types.rb │ │ └── wrap_parameters.rb │ ├── locales │ │ └── en.yml │ ├── puma.rb │ ├── routes.rb │ ├── spring.rb │ └── storage.yml ├── db │ ├── migrate │ │ ├── 20170806125915_create_active_storage_tables.active_storage.rb │ │ ├── 20180101010101_create_active_admin_comments.rb │ │ ├── 20180607053251_create_authors.rb │ │ ├── 20180607053254_create_profiles.rb │ │ ├── 20180607053255_create_tags.rb │ │ ├── 20180607053257_create_post_tags.rb │ │ └── 20180607053739_create_posts.rb │ └── schema.rb ├── lib │ └── assets │ │ └── .keep └── public │ ├── 404.html │ ├── 422.html │ ├── 500.html │ ├── apple-touch-icon-precomposed.png │ ├── apple-touch-icon.png │ └── favicon.ico ├── rails_helper.rb ├── spec_helper.rb ├── support ├── capybara.rb └── drivers.rb └── system ├── select_many_spec.rb └── select_one_spec.rb /.fasterer.yml: -------------------------------------------------------------------------------- 1 | --- 2 | exclude_paths: 3 | - bin/* 4 | - db/schema.rb 5 | - spec/dummy/**/* 6 | - vendor/**/* 7 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [blocknotes] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /.github/workflows/linters.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Linters 3 | 4 | on: 5 | push: 6 | branches: 7 | - master 8 | pull_request: 9 | 10 | jobs: 11 | reviewdog: 12 | name: reviewdog 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - name: Check out code 17 | uses: actions/checkout@v2 18 | 19 | - name: Set up Ruby 20 | uses: ruby/setup-ruby@v1 21 | with: 22 | ruby-version: '2.7' 23 | bundler-cache: true # runs 'bundle install' and caches installed gems automatically 24 | 25 | - uses: reviewdog/action-setup@v1 26 | with: 27 | reviewdog_version: latest 28 | 29 | - name: Run reviewdog 30 | env: 31 | REVIEWDOG_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }} 32 | run: | 33 | reviewdog -fail-on-error -reporter=github-pr-review -runners=fasterer,rubocop 34 | 35 | # NOTE: check with: reviewdog -fail-on-error -reporter=github-pr-review -runners=fasterer -diff="git diff" -tee 36 | -------------------------------------------------------------------------------- /.github/workflows/specs.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Specs 3 | 4 | on: 5 | push: 6 | branches: [master] 7 | pull_request: 8 | branches: [master] 9 | 10 | jobs: 11 | test: 12 | runs-on: ubuntu-latest 13 | 14 | strategy: 15 | matrix: 16 | ruby: ['2.5', '2.6', '2.7'] 17 | 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v2 21 | 22 | - name: Set up Ruby 23 | uses: ruby/setup-ruby@v1 24 | with: 25 | ruby-version: ${{ matrix.ruby }} 26 | bundler-cache: true 27 | 28 | - name: Run tests 29 | run: bundle exec rake 30 | 31 | - name: Archive screenshots for failed tests 32 | uses: actions/upload-artifact@v2 33 | if: failure() 34 | with: 35 | name: test-failed-screenshots 36 | path: spec/dummy/tmp/screenshots 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.orig 3 | 4 | /.rspec_failures 5 | /.rubocop-* 6 | /Gemfile.lock 7 | 8 | /_misc/ 9 | /spec/dummy/db/*.sqlite3* 10 | /spec/dummy/log/ 11 | /spec/dummy/storage/ 12 | /spec/dummy/tmp/ 13 | 14 | /coverage/ 15 | -------------------------------------------------------------------------------- /.reviewdog.yml: -------------------------------------------------------------------------------- 1 | --- 2 | runner: 3 | fasterer: 4 | cmd: bin/fasterer 5 | level: info 6 | rubocop: 7 | cmd: bin/rubocop 8 | level: info 9 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --require rails_helper 3 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | --- 2 | inherit_from: 3 | - https://relaxed.ruby.style/rubocop.yml 4 | 5 | require: 6 | - rubocop-packaging 7 | - rubocop-performance 8 | - rubocop-rails 9 | - rubocop-rspec 10 | 11 | AllCops: 12 | Exclude: 13 | - bin/* 14 | - db/schema.rb 15 | - spec/dummy/**/* 16 | - vendor/**/* 17 | NewCops: enable 18 | 19 | Gemspec/RequiredRubyVersion: 20 | Enabled: false 21 | 22 | Naming/FileName: 23 | Enabled: false 24 | 25 | Layout/LineLength: 26 | Enabled: true 27 | Max: 120 28 | 29 | RSpec/ExampleLength: 30 | Max: 10 31 | 32 | RSpec/MultipleExpectations: 33 | Max: 12 34 | 35 | Style/HashEachMethods: 36 | Enabled: true 37 | 38 | Style/HashTransformKeys: 39 | Enabled: true 40 | 41 | Style/HashTransformValues: 42 | Enabled: true 43 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | gemspec 6 | 7 | group :development, :test do 8 | gem 'activestorage', '~> 6.0.3.2' 9 | gem 'capybara', '~> 3.33.0' 10 | gem 'puma', '~> 4.3.5' 11 | gem 'rspec_junit_formatter', '~> 0.4.1' 12 | gem 'rspec-rails', '~> 4.0.1' 13 | gem 'selenium-webdriver', '~> 3.142.7' 14 | gem 'simplecov', '~> 0.19.0' 15 | gem 'sprockets-rails', '~> 3.2' 16 | gem 'sqlite3', '~> 1.4.2' 17 | 18 | # Linters 19 | gem 'fasterer' 20 | gem 'rubocop' 21 | gem 'rubocop-packaging' 22 | gem 'rubocop-performance' 23 | gem 'rubocop-rails' 24 | gem 'rubocop-rspec' 25 | 26 | # Tools 27 | gem 'pry-rails' 28 | end 29 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 Mattia Roccoberton 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ActiveAdmin Select Many 2 | [![gem version](https://badge.fury.io/rb/activeadmin_select_many.svg)](https://badge.fury.io/rb/activeadmin_select_many) 3 | [![gem downloads](https://badgen.net/rubygems/dt/activeadmin_select_many)](https://rubygems.org/gems/activeadmin_select_many) 4 | [![specs](https://github.com/blocknotes/activeadmin_select_many/actions/workflows/specs.yml/badge.svg)](https://github.com/blocknotes/activeadmin_select_many/actions/workflows/specs.yml) 5 | 6 | An Active Admin plugin which improves one-to-many / many-to-many / many-to-one associations selection using 2 new inputs: **select_many** and **select_one** (jQuery required). 7 | 8 | Features for *select_many*: 9 | 10 | - search box; 11 | - available items on the left, selected items on the right; 12 | - local/remote collections; 13 | - double click to add/remove items; 14 | - sortable (with up/down buttons); 15 | - key bindings to improve accessibility. 16 | 17 | Features for *select_one*: 18 | 19 | - search box; 20 | - selected items on the right; 21 | - remote collections; 22 | - counter of items found; 23 | - can be used as filter; 24 | - key bindings to improve accessibility. 25 | 26 | ![screenshot](screenshot.png) 27 | 28 | *(inspired by RailsAdmin associations selector)* 29 | 30 | ## Install 31 | 32 | - Add to your Gemfile: 33 | `gem 'activeadmin_select_many'` 34 | - Execute bundle 35 | - Add at the end of your ActiveAdmin styles (_app/assets/stylesheets/active_admin.scss_): 36 | `@import 'activeadmin/select_many';` 37 | - Add at the end of your ActiveAdmin javascripts (_app/assets/javascripts/active_admin.js_): 38 | `//= require activeadmin/select_many` 39 | - Use the input with `as: :select_many` in Active Admin model conf 40 | 41 | ## Options 42 | 43 | - **collection**: local collection 44 | - **counter_limit**: if results count is greater than or equal to this limit a '+' is shown 45 | - **filter_form**: for *select_one* only, allow to use it as filter 46 | - **include_blank**: for *select_one* only, default true, allow to include a blank value on top 47 | - **member_label**: key to use as text for select options 48 | - **placeholder**: placeholder string for search box 49 | - **remote_collection**: JSON path 50 | - **search_param**: parameter to use as search key (ransack format) 51 | - **selected**: force value selection (array for *select_many*, single value for *select_one*) 52 | - **size**: number of rows of both the selects (default: 4) 53 | - **sortable**: set to true to enable sortable buttons (default: not set) 54 | 55 | ## Examples with select_many 56 | 57 | Add to ActiveAdmin model config, in *form* block. 58 | 59 | - Local collection (no AJAX calls): 60 | `f.input :sections, as: :select_many` 61 | - Remote collection (using AJAX): 62 | `f.input :tags, as: :select_many, remote_collection: admin_tags_path( format: :json )` 63 | - Changing search param and text key (default: *name*): 64 | `f.input :tags, as: :select_many, remote_collection: admin_tags_path( format: :json ), search_param: 'category_contains', member_label: 'category', placeholder: 'Type something...'` 65 | - Sortable (items position must be saved manually): 66 | `f.input :tags, as: :select_many, remote_collection: admin_tags_path( format: :json ), sortable: true` 67 | 68 | Example to update *position* field: 69 | 70 | ```rb 71 | after_save :on_after_save 72 | controller do 73 | def on_after_save( object ) 74 | if params[:article][:section_ids] 75 | order = {} 76 | params[:article][:section_ids].each_with_index { |id, i| order[id.to_i] = i } 77 | object.sections.each { |item| item.update_column( :position, order[item.id].to_i ) } 78 | end 79 | end 80 | end 81 | ``` 82 | 83 | Example to enable JSON response on an ActiveAdmin model: 84 | 85 | ```rb 86 | ActiveAdmin.register Tag do 87 | config.per_page = 30 # to limit served items 88 | config.sort_order = 'name_asc' 89 | index download_links: [:json] 90 | end 91 | ``` 92 | 93 | ## Examples with select_one 94 | 95 | In a form: 96 | 97 | `f.input :article, as: :select_one, placeholder: 'Search...', remote_collection: admin_articles_path( format: :json ), search_param: 'title_contains', member_label: 'title'` 98 | 99 | As filter: 100 | 101 | `filter :article_id_eq, as: :select_one, filter_form: true, placeholder: 'Search...', search_param: 'title_contains', member_label: 'title', remote_collection: '/admin/articles.json'` 102 | 103 | ## Notes 104 | 105 | - To use this plugins with ActiveAdmin 1.x please use the version 0.3.4 106 | 107 | ## Do you like it? Star it! 108 | 109 | If you use this component just star it. A developer is more motivated to improve a project when there is some interest. My other [Active Admin components](https://github.com/blocknotes?utf8=✓&tab=repositories&q=activeadmin&type=source). 110 | 111 | Or consider offering me a coffee, it's a small thing but it is greatly appreciated: [about me](https://www.blocknot.es/about-me). 112 | 113 | ## Contributors 114 | 115 | - [Mattia Roccoberton](http://blocknot.es): author 116 | 117 | ## License 118 | 119 | [MIT](LICENSE.txt) 120 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'bundler/gem_tasks' 4 | 5 | begin 6 | require 'rspec/core/rake_task' 7 | 8 | RSpec::Core::RakeTask.new(:spec) do |t| 9 | # t.ruby_opts = %w[-w] 10 | t.rspec_opts = ['--color', '--format documentation'] 11 | end 12 | 13 | task default: :spec 14 | rescue LoadError 15 | puts '! LoadError: no RSpec available' 16 | end 17 | -------------------------------------------------------------------------------- /activeadmin_select_many.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | lib = File.expand_path('../lib', __FILE__) 4 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 5 | require 'activeadmin/select_many/version' 6 | 7 | Gem::Specification.new do |spec| 8 | spec.name = 'activeadmin_select_many' 9 | spec.version = ActiveAdmin::SelectMany::VERSION 10 | spec.summary = 'SelectMany plugin for ActiveAdmin' 11 | spec.description = 'An Active Admin plugin which improves one-to-many and many-to-many associations selection (jQuery required)' 12 | spec.license = 'MIT' 13 | spec.authors = ['Mattia Roccoberton'] 14 | spec.email = 'mat@blocknot.es' 15 | spec.homepage = 'https://github.com/blocknotes/activeadmin_select_many' 16 | 17 | spec.files = Dir['{app,config,lib}/**/*', 'LICENSE.txt', 'Rakefile', 'README.md'] 18 | spec.require_paths = ['lib'] 19 | 20 | spec.add_runtime_dependency 'activeadmin', '~> 2.0' 21 | spec.add_runtime_dependency 'sassc', '~> 2.4' 22 | end 23 | -------------------------------------------------------------------------------- /app/assets/javascripts/activeadmin/select_many.js: -------------------------------------------------------------------------------- 1 | function smActivate(target) { 2 | if(target.tagName.toLowerCase() == 'option') { 3 | var parent = $(this).closest('.select_many'); 4 | var opt = $(target); 5 | var dst = parent.find($(this).data('select') == 'src' ? '[data-select="dst"]' : '[data-select="src"]'); 6 | dst.append($('