├── .gitignore ├── Changes.md ├── Gemfile ├── Gemfile.lock ├── Guardfile ├── LICENSE ├── README.md ├── Rakefile ├── app ├── assets │ └── javascripts │ │ └── smart_listing.coffee.erb ├── helpers │ └── smart_listing │ │ ├── application_helper.rb │ │ └── helper.rb └── views │ ├── kaminari │ └── smart_listing │ │ ├── _first_page.html.erb │ │ ├── _gap.html.erb │ │ ├── _last_page.html.erb │ │ ├── _next_page.html.erb │ │ ├── _page.html.erb │ │ ├── _paginator.html.erb │ │ └── _prev_page.html.erb │ └── smart_listing │ ├── _action_custom.html.erb │ ├── _action_delete.html.erb │ ├── _action_edit.html.erb │ ├── _action_show.html.erb │ ├── _item_new.html.erb │ ├── _pagination_per_page_link.html.erb │ ├── _pagination_per_page_links.html.erb │ ├── _sortable.html.erb │ ├── _update_list.js.erb │ ├── create.js.erb │ ├── destroy.js.erb │ ├── edit.js.erb │ ├── index.js.erb │ ├── item │ ├── _create.js.erb │ ├── _create_continue.js.erb │ ├── _destroy.js.erb │ ├── _edit.js.erb │ ├── _new.js.erb │ ├── _remove.js.erb │ └── _update.js.erb │ ├── new.js.erb │ └── update.js.erb ├── bin └── rails ├── config ├── locales │ └── en.yml └── routes.rb ├── lib ├── generators │ └── smart_listing │ │ ├── install_generator.rb │ │ ├── templates │ │ └── initializer.rb │ │ └── views_generator.rb ├── smart_listing.rb ├── smart_listing │ ├── config.rb │ ├── engine.rb │ └── version.rb └── tasks │ └── smart_list_tasks.rake ├── smart_listing.gemspec └── spec ├── dummy ├── Gemfile ├── Gemfile.lock ├── README.rdoc ├── Rakefile ├── app │ ├── assets │ │ ├── images │ │ │ └── .keep │ │ ├── javascripts │ │ │ └── application.js │ │ └── stylesheets │ │ │ └── application.css.scss │ ├── controllers │ │ ├── admin │ │ │ └── users_controller.rb │ │ ├── application_controller.rb │ │ ├── concerns │ │ │ └── .keep │ │ └── users_controller.rb │ ├── helpers │ │ └── application_helper.rb │ ├── mailers │ │ └── .keep │ ├── models │ │ ├── .keep │ │ ├── application_record.rb │ │ ├── concerns │ │ │ └── .keep │ │ └── user.rb │ └── views │ │ ├── admin │ │ └── users │ │ │ ├── _form.html.erb │ │ │ ├── _item.html.erb │ │ │ ├── _list.html.erb │ │ │ └── index.html.erb │ │ ├── layouts │ │ └── application.html.erb │ │ └── users │ │ ├── _list.html.erb │ │ ├── _searchable_list.html.erb │ │ ├── _sortable_list.html.erb │ │ ├── index.html.erb │ │ ├── index.js.erb │ │ ├── searchable.html.erb │ │ └── searchable.js.erb ├── bin │ ├── bundle │ ├── rails │ └── rake ├── config.ru ├── config │ ├── application.rb │ ├── boot.rb │ ├── database.yml │ ├── environment.rb │ ├── environments │ │ ├── development.rb │ │ ├── production.rb │ │ └── test.rb │ ├── initializers │ │ ├── assets.rb │ │ ├── backtrace_silencers.rb │ │ ├── cookies_serializer.rb │ │ ├── filter_parameter_logging.rb │ │ ├── inflections.rb │ │ ├── mime_types.rb │ │ ├── session_store.rb │ │ ├── smart_listing.rb │ │ └── wrap_parameters.rb │ ├── locales │ │ └── en.yml │ ├── routes.rb │ └── secrets.yml ├── db │ ├── migrate │ │ └── 20180126065408_create_user.rb │ ├── schema.rb │ └── seeds.rb ├── fixtures │ └── users.yml ├── lib │ └── assets │ │ └── .keep ├── log │ └── .keep └── public │ ├── 404.html │ ├── 422.html │ ├── 500.html │ └── favicon.ico ├── features ├── custom_filters_spec.rb ├── manage_items_spec.rb └── view_items_spec.rb ├── helpers └── smart_listing │ └── helper_spec.rb ├── lib └── smart_listing_spec.rb ├── rails_helper.rb ├── spec_helper.rb └── support └── capybara └── wait_for_ajax.rb /.gitignore: -------------------------------------------------------------------------------- 1 | *.rbc 2 | *.sassc 3 | .sass-cache 4 | capybara-*.html 5 | .rspec 6 | .rvmrc 7 | /.bundle 8 | /vendor/bundle 9 | /log/* 10 | /tmp/* 11 | /db/*.sqlite3 12 | /public/system/* 13 | /coverage/ 14 | /spec/tmp/* 15 | **.orig 16 | rerun.txt 17 | pickle-email-*.html 18 | .project 19 | config/initializers/secret_token.rb 20 | *.gem 21 | /test/* 22 | 23 | .ruby-version 24 | .ruby-gemset 25 | 26 | *.swp 27 | *.swo 28 | 29 | spec/dummy/db/*.sqlite3 30 | spec/dummy/log/*.log 31 | spec/dummy/tmp/ 32 | tags 33 | 34 | .byebug_history 35 | -------------------------------------------------------------------------------- /Changes.md: -------------------------------------------------------------------------------- 1 | 1.2.3 2 | ----------- 3 | 4 | - Fix sorting to mitigate possible SQL-injection and improve tests [Ivan Korunkov] 5 | 6 | 1.2.2 7 | ----------- 8 | 9 | - Remove duplicated href key from config template #146 [nfilzi] 10 | - Replace deprecated .any? with .present? #143 [AakLak] 11 | - Development environment update #140 [mizinsky] 12 | - Fix sanitize_params method #137 [mizinsky] 13 | - Enable to configure global remote option and it to affects sortable helper #131 [kitabatake] 14 | - Kaminari update [mizinsky] 15 | - Update Readme for Rails >= 5.1 Users [mizinsky] 16 | 17 | 1.2.1 18 | ----------- 19 | 20 | - Allow to render outside of controllers [bval] 21 | - Documentation fixes [blackcofla] 22 | - Use id.to_json so integers and uuids will both work [sevgibson] 23 | - Fix popover in bootstrap 4 [sevgibson] 24 | - Fix Kaminari #num_pages deprecation warning [tylerhunt] 25 | - Add support for Turbolinks 5 [wynksaiddestroy] 26 | - Use #empty? for AC::Params [phoffer] 27 | - Fix indentation in some files [boy-papan] 28 | 29 | 1.2.0 30 | ----------- 31 | 32 | - Rails 5 support and Kaminari update [akostadinov] 33 | - Better handling of nested controls params 34 | - Fix controls not fading out list. Related to #51 35 | - Config now includes element templates 36 | - Add ability to pass locals to list view [GeorgeDewar] 37 | 38 | 1.1.2 39 | ----------- 40 | 41 | - Some bugfixing: #20, #46, #58 42 | 43 | 1.1.0 44 | ----------- 45 | 46 | - Config profiles 47 | - Remove duplicate href key [wynksaiddestroy] 48 | - API refactoring [GCorbel] 49 | - Feature Specs [GCorbel] 50 | - Avoid smart listing controls double initialization [lacco] 51 | - Turbolinks support [wynksaiddestroy] 52 | - Better form controls handling 53 | - Possibility to specify sort directions 54 | 55 | 1.0.0 56 | ----------- 57 | 58 | - JS Events triggered on item actions 59 | - Fix filter resetting 60 | - Fix new item autoshow 61 | - Possibility to pass custom title to default actions 62 | - Confirmation tweaks 63 | - Multiple smart listings isolation 64 | - New sorting architecture (and implicit sorting attributes) 65 | - Controls helper 66 | - Slightly changed item action templates 67 | 68 | 0.9.8 69 | ----------- 70 | 71 | - Custom popovers support 72 | 73 | 0.9.7 74 | ----------- 75 | 76 | - Some bugfixing 77 | - Fix listing sorting XSS bug 78 | - Add possibility to display new item form by default 79 | - "Save & continue" support 80 | 81 | 0.9.6 82 | ----------- 83 | 84 | - Some bugfixing 85 | - Initial setup generator 86 | 87 | 0.9.5 88 | ----------- 89 | 90 | - Fix collection counting bug 91 | - Add builtin show action 92 | - Make CSS class and data attribute names generic and customizable (SmartListing.configure) 93 | - Make JavaScript more customizable 94 | 95 | 0.9.4 96 | ----------- 97 | 98 | - Possibility to callback action 99 | - Changes in templates 100 | 101 | 0.9.3 102 | ----------- 103 | 104 | - Possibility to specify kaminari options 105 | - Possibility to generate views and customize them in the app 106 | - Better custom action handling 107 | 108 | 0.9.2 109 | ----------- 110 | 111 | - Add possibility to specify available page sizes in options hash 112 | 113 | 0.9.0 114 | ----------- 115 | 116 | - Initial release 117 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec 4 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | smart_listing (1.2.2) 5 | coffee-rails 6 | jquery-rails 7 | kaminari (>= 0.17) 8 | rails (>= 3.2) 9 | 10 | GEM 11 | remote: https://rubygems.org/ 12 | specs: 13 | actioncable (5.1.4) 14 | actionpack (= 5.1.4) 15 | nio4r (~> 2.0) 16 | websocket-driver (~> 0.6.1) 17 | actionmailer (5.1.4) 18 | actionpack (= 5.1.4) 19 | actionview (= 5.1.4) 20 | activejob (= 5.1.4) 21 | mail (~> 2.5, >= 2.5.4) 22 | rails-dom-testing (~> 2.0) 23 | actionpack (5.1.4) 24 | actionview (= 5.1.4) 25 | activesupport (= 5.1.4) 26 | rack (~> 2.0) 27 | rack-test (>= 0.6.3) 28 | rails-dom-testing (~> 2.0) 29 | rails-html-sanitizer (~> 1.0, >= 1.0.2) 30 | actionview (5.1.4) 31 | activesupport (= 5.1.4) 32 | builder (~> 3.1) 33 | erubi (~> 1.4) 34 | rails-dom-testing (~> 2.0) 35 | rails-html-sanitizer (~> 1.0, >= 1.0.3) 36 | activejob (5.1.4) 37 | activesupport (= 5.1.4) 38 | globalid (>= 0.3.6) 39 | activemodel (5.1.4) 40 | activesupport (= 5.1.4) 41 | activerecord (5.1.4) 42 | activemodel (= 5.1.4) 43 | activesupport (= 5.1.4) 44 | arel (~> 8.0) 45 | activesupport (5.1.4) 46 | concurrent-ruby (~> 1.0, >= 1.0.2) 47 | i18n (~> 0.7) 48 | minitest (~> 5.1) 49 | tzinfo (~> 1.1) 50 | addressable (2.5.2) 51 | public_suffix (>= 2.0.2, < 4.0) 52 | arel (8.0.0) 53 | autoprefixer-rails (7.2.5) 54 | execjs 55 | bootstrap-sass (3.3.7) 56 | autoprefixer-rails (>= 5.2.1) 57 | sass (>= 3.3.4) 58 | builder (3.2.3) 59 | byebug (9.1.0) 60 | capybara (2.13.0) 61 | addressable 62 | mime-types (>= 1.16) 63 | nokogiri (>= 1.3.3) 64 | rack (>= 1.0.0) 65 | rack-test (>= 0.5.4) 66 | xpath (~> 2.0) 67 | capybara-webkit (1.14.0) 68 | capybara (>= 2.3.0, < 2.14.0) 69 | json 70 | coderay (1.1.2) 71 | coffee-rails (4.2.2) 72 | coffee-script (>= 2.2.0) 73 | railties (>= 4.0.0) 74 | coffee-script (2.4.1) 75 | coffee-script-source 76 | execjs 77 | coffee-script-source (1.12.2) 78 | concurrent-ruby (1.0.5) 79 | crass (1.0.3) 80 | database_cleaner (1.6.2) 81 | diff-lcs (1.3) 82 | erubi (1.7.0) 83 | execjs (2.7.0) 84 | ffi (1.9.18) 85 | formatador (0.2.5) 86 | globalid (0.4.1) 87 | activesupport (>= 4.2.0) 88 | guard (2.14.2) 89 | formatador (>= 0.2.4) 90 | listen (>= 2.7, < 4.0) 91 | lumberjack (>= 1.0.12, < 2.0) 92 | nenv (~> 0.1) 93 | notiffany (~> 0.0) 94 | pry (>= 0.9.12) 95 | shellany (~> 0.0) 96 | thor (>= 0.18.1) 97 | guard-compat (1.2.1) 98 | guard-rspec (4.7.3) 99 | guard (~> 2.1) 100 | guard-compat (~> 1.1) 101 | rspec (>= 2.99.0, < 4.0) 102 | i18n (0.9.3) 103 | concurrent-ruby (~> 1.0) 104 | jquery-rails (4.3.3) 105 | rails-dom-testing (>= 1, < 3) 106 | railties (>= 4.2.0) 107 | thor (>= 0.14, < 2.0) 108 | json (2.1.0) 109 | kaminari (1.1.1) 110 | activesupport (>= 4.1.0) 111 | kaminari-actionview (= 1.1.1) 112 | kaminari-activerecord (= 1.1.1) 113 | kaminari-core (= 1.1.1) 114 | kaminari-actionview (1.1.1) 115 | actionview 116 | kaminari-core (= 1.1.1) 117 | kaminari-activerecord (1.1.1) 118 | activerecord 119 | kaminari-core (= 1.1.1) 120 | kaminari-core (1.1.1) 121 | listen (3.1.5) 122 | rb-fsevent (~> 0.9, >= 0.9.4) 123 | rb-inotify (~> 0.9, >= 0.9.7) 124 | ruby_dep (~> 1.2) 125 | loofah (2.1.1) 126 | crass (~> 1.0.2) 127 | nokogiri (>= 1.5.9) 128 | lumberjack (1.0.12) 129 | mail (2.7.0) 130 | mini_mime (>= 0.1.1) 131 | method_source (0.9.0) 132 | mime-types (3.1) 133 | mime-types-data (~> 3.2015) 134 | mime-types-data (3.2016.0521) 135 | mini_mime (1.0.0) 136 | mini_portile2 (2.3.0) 137 | minitest (5.11.2) 138 | nenv (0.3.0) 139 | nio4r (2.3.1) 140 | nokogiri (1.8.1) 141 | mini_portile2 (~> 2.3.0) 142 | notiffany (0.1.1) 143 | nenv (~> 0.1) 144 | shellany (~> 0.0) 145 | pry (0.11.3) 146 | coderay (~> 1.1.0) 147 | method_source (~> 0.9.0) 148 | public_suffix (3.0.1) 149 | rack (2.0.3) 150 | rack-test (0.8.2) 151 | rack (>= 1.0, < 3) 152 | rails (5.1.4) 153 | actioncable (= 5.1.4) 154 | actionmailer (= 5.1.4) 155 | actionpack (= 5.1.4) 156 | actionview (= 5.1.4) 157 | activejob (= 5.1.4) 158 | activemodel (= 5.1.4) 159 | activerecord (= 5.1.4) 160 | activesupport (= 5.1.4) 161 | bundler (>= 1.3.0) 162 | railties (= 5.1.4) 163 | sprockets-rails (>= 2.0.0) 164 | rails-dom-testing (2.0.3) 165 | activesupport (>= 4.2.0) 166 | nokogiri (>= 1.6) 167 | rails-html-sanitizer (1.0.3) 168 | loofah (~> 2.0) 169 | railties (5.1.4) 170 | actionpack (= 5.1.4) 171 | activesupport (= 5.1.4) 172 | method_source 173 | rake (>= 0.8.7) 174 | thor (>= 0.18.1, < 2.0) 175 | rake (12.3.0) 176 | rb-fsevent (0.10.2) 177 | rb-inotify (0.9.10) 178 | ffi (>= 0.5.0, < 2) 179 | rspec (3.7.0) 180 | rspec-core (~> 3.7.0) 181 | rspec-expectations (~> 3.7.0) 182 | rspec-mocks (~> 3.7.0) 183 | rspec-core (3.7.1) 184 | rspec-support (~> 3.7.0) 185 | rspec-expectations (3.7.0) 186 | diff-lcs (>= 1.2.0, < 2.0) 187 | rspec-support (~> 3.7.0) 188 | rspec-mocks (3.7.0) 189 | diff-lcs (>= 1.2.0, < 2.0) 190 | rspec-support (~> 3.7.0) 191 | rspec-rails (3.7.2) 192 | actionpack (>= 3.0) 193 | activesupport (>= 3.0) 194 | railties (>= 3.0) 195 | rspec-core (~> 3.7.0) 196 | rspec-expectations (~> 3.7.0) 197 | rspec-mocks (~> 3.7.0) 198 | rspec-support (~> 3.7.0) 199 | rspec-support (3.7.0) 200 | ruby_dep (1.5.0) 201 | sass (3.5.5) 202 | sass-listen (~> 4.0.0) 203 | sass-listen (4.0.0) 204 | rb-fsevent (~> 0.9, >= 0.9.4) 205 | rb-inotify (~> 0.9, >= 0.9.7) 206 | shellany (0.0.1) 207 | sprockets (3.7.2) 208 | concurrent-ruby (~> 1.0) 209 | rack (> 1, < 3) 210 | sprockets-rails (3.2.1) 211 | actionpack (>= 4.0) 212 | activesupport (>= 4.0) 213 | sprockets (>= 3.0.0) 214 | sqlite3 (1.3.13) 215 | thor (0.20.0) 216 | thread_safe (0.3.6) 217 | tzinfo (1.2.4) 218 | thread_safe (~> 0.1) 219 | websocket-driver (0.6.5) 220 | websocket-extensions (>= 0.1.0) 221 | websocket-extensions (0.1.3) 222 | xpath (2.1.0) 223 | nokogiri (~> 1.3) 224 | 225 | PLATFORMS 226 | ruby 227 | 228 | DEPENDENCIES 229 | bootstrap-sass 230 | byebug 231 | capybara (< 2.14) 232 | capybara-webkit (~> 1.14) 233 | database_cleaner 234 | guard-rspec 235 | rspec-rails 236 | smart_listing! 237 | sqlite3 238 | 239 | BUNDLED WITH 240 | 1.16.2 241 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | # A sample Guardfile 2 | # More info at https://github.com/guard/guard#readme 3 | 4 | ## Uncomment and set this to only include directories you want to watch 5 | # directories %w(app lib config test spec features) \ 6 | # .select{|d| Dir.exists?(d) ? d : UI.warning("Directory #{d} does not exist")} 7 | 8 | ## Note: if you are using the `directories` clause above and you are not 9 | ## watching the project directory ('.'), then you will want to move 10 | ## the Guardfile to a watched dir and symlink it back, e.g. 11 | # 12 | # $ mkdir config 13 | # $ mv Guardfile config/ 14 | # $ ln -s config/Guardfile . 15 | # 16 | # and, you'll have to watch "config/Guardfile" instead of "Guardfile" 17 | 18 | # Note: The cmd option is now required due to the increasing number of ways 19 | # rspec may be run, below are examples of the most common uses. 20 | # * bundler: 'bundle exec rspec' 21 | # * bundler binstubs: 'bin/rspec' 22 | # * spring: 'bin/rspec' (This will use spring if running and you have 23 | # installed the spring binstubs per the docs) 24 | # * zeus: 'zeus rspec' (requires the server to be started separately) 25 | # * 'just' rspec: 'rspec' 26 | 27 | guard :rspec, cmd: "bundle exec rspec" do 28 | require "guard/rspec/dsl" 29 | dsl = Guard::RSpec::Dsl.new(self) 30 | 31 | # Feel free to open issues for suggestions and improvements 32 | 33 | # RSpec files 34 | rspec = dsl.rspec 35 | watch(rspec.spec_helper) { rspec.spec_dir } 36 | watch(rspec.spec_support) { rspec.spec_dir } 37 | watch(rspec.spec_files) 38 | 39 | # Ruby files 40 | ruby = dsl.ruby 41 | dsl.watch_spec_files_for(ruby.lib_files) 42 | 43 | # Rails files 44 | rails = dsl.rails(view_extensions: %w(erb haml slim)) 45 | dsl.watch_spec_files_for(rails.app_files) 46 | dsl.watch_spec_files_for(rails.views) 47 | 48 | watch(rails.controllers) do |m| 49 | [ 50 | rspec.spec.call("routing/#{m[1]}_routing"), 51 | rspec.spec.call("controllers/#{m[1]}_controller"), 52 | rspec.spec.call("acceptance/#{m[1]}") 53 | ] 54 | end 55 | 56 | # Rails config changes 57 | watch(rails.spec_helper) { rspec.spec_dir } 58 | watch(rails.routes) { "#{rspec.spec_dir}/routing" } 59 | watch(rails.app_controller) { "#{rspec.spec_dir}/controllers" } 60 | 61 | # Capybara features specs 62 | watch(rails.view_dirs) { |m| rspec.spec.call("features/#{m[1]}") } 63 | watch(rails.layouts) { |m| rspec.spec.call("features/#{m[1]}") } 64 | 65 | # Turnip features and steps 66 | watch(%r{^spec/acceptance/(.+)\.feature$}) 67 | watch(%r{^spec/acceptance/steps/(.+)_steps\.rb$}) do |m| 68 | Dir[File.join("**/#{m[1]}.feature")][0] || "spec/acceptance" 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Sology (www.sology.eu) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SmartListing 2 | 3 | SmartListing helps creating AJAX-enabled lists of ActiveRecord collections or arrays with pagination, filtering, sorting and in-place editing. 4 | 5 | [See it in action](http://showcase.sology.eu/smart_listing) 6 | 7 | ## Installation 8 | 9 | Add to your Gemfile: 10 | 11 | ```ruby 12 | gem "smart_listing" 13 | ``` 14 | 15 | Then run: 16 | 17 | ```sh 18 | $ bundle install 19 | ``` 20 | 21 | Also, you need to add SmartListing to your asset pipeline: 22 | 23 | ``` 24 | //= require smart_listing 25 | ``` 26 | 27 | __Rails >= 5.1 users__: Rails 5.1 has dropped jQuery dependency from the default stack in favour of `rails-ujs`. SmartListing still requires jQuery so make sure that you use `jquery_ujs` from `jquery-rails` gem and have following requires in your asset pipeline before `smart_listing`: 28 | ``` 29 | //= require jquery 30 | //= require jquery_ujs 31 | ``` 32 | 33 | ### Initializer 34 | 35 | Optionally you can also install some configuration initializer: 36 | 37 | ```sh 38 | $ rails generate smart_listing:install 39 | ``` 40 | 41 | It will be placed in `config/initializers/smart_listing.rb` and will allow you to tweak some configuration settings like HTML classes and data attributes names. 42 | 43 | ### Custom views 44 | 45 | SmartListing comes with some built-in views which are by default compatible with Bootstrap 3. You can easily change them after installing: 46 | 47 | ```sh 48 | $ rails generate smart_listing:views 49 | ``` 50 | 51 | Files will be placed in `app/views/smart_listing`. 52 | 53 | ## Usage 54 | 55 | Let's start with a controller. In order to use SmartListing, in most cases you need to include controller extensions and SmartListing helper methods: 56 | 57 | ```ruby 58 | include SmartListing::Helper::ControllerExtensions 59 | helper SmartListing::Helper 60 | ``` 61 | 62 | Next, put following code in controller action you desire: 63 | 64 | ```ruby 65 | @users = smart_listing_create(:users, User.active, partial: "users/listing") 66 | ``` 67 | 68 | This will create SmartListing named `:users` consisting of ActiveRecord scope `User.active` elements and rendered by partial `users/listing`. You can also use arrays instead of ActiveRecord collections. Just put `array: true` option just like for Kaminari. 69 | 70 | In the main view (typically something like `index.html.erb` or `index.html.haml`), use this method to render listing: 71 | 72 | ```ruby 73 | smart_listing_render(:users) 74 | ``` 75 | 76 | `smart_listing_render` does some magic and renders `users/listing` partial which may look like this (in HAML): 77 | 78 | ```haml 79 | - unless smart_listing.empty? 80 | %table 81 | %thead 82 | %tr 83 | %th User name 84 | %th Email 85 | %tbody 86 | - smart_listing.collection.each do |user| 87 | %tr 88 | %td= user.name 89 | %td= user.email 90 | 91 | = smart_listing.paginate 92 | - else 93 | %p.warning No records! 94 | ``` 95 | 96 | You can see that listing template has access to special `smart_listing` local variable which is basically an instance of `SmartListing::Helper::Builder`. It provides you with some helper methods that ease rendering of SmartListing: 97 | 98 | * `Builder#paginate` - renders Kaminari pagination, 99 | * `Builder#pagination_per_page_links` - display some link that allow you to customize Kaminari's `per_page`, 100 | * `Builder#collection` - accesses underlying list of items, 101 | * `Builder#empty?` - checks if collection is empty, 102 | * `Builder#count` - returns collection count, 103 | * `Builder#render` - basic template's `render` wrapper that automatically adds `smart_listing` local variable, 104 | 105 | There are also other methods that will be described in detail below. 106 | 107 | If you are using SmartListing with AJAX on (by default), one last thing required to make pagination (and other features) work is to create JS template for main view (typically something like `index.js.erb`): 108 | 109 | ```erb 110 | <%= smart_listing_update(:users) %> 111 | ``` 112 | 113 | ### Sorting 114 | 115 | SmartListing supports two modes of sorting: implicit and explicit. Implicit mode is enabled by default. In this mode, you define sort columns directly in the view: 116 | 117 | ```haml 118 | - unless smart_listing.empty? 119 | %table 120 | %thead 121 | %tr 122 | %th= smart_listing.sortable "User name", :name 123 | %th= smart_listing.sortable "Email", :email 124 | %tbody 125 | - smart_listing.collection.each do |user| 126 | %tr 127 | %td= user.name 128 | %td= user.email 129 | 130 | = smart_listing.paginate 131 | - else 132 | %p.warning No records! 133 | ``` 134 | 135 | In this case `:name` and `:email` are sorting column names. `Builder#sortable` renders special link containing column name and sort order (either `asc`, `desc`, or empty value). 136 | 137 | You can also specify default sort order in the controller: 138 | 139 | ```ruby 140 | @users = smart_listing_create(:users, User.active, partial: "users/listing", default_sort: {name: "asc"}) 141 | ``` 142 | 143 | Implicit mode is convenient with simple data sets. In case you want to sort by joined column names, we advise you to use explicit sorting: 144 | ```ruby 145 | @users = smart_listing_create :users, User.active.joins(:stats), partial: "users/listing", 146 | sort_attributes: [[:last_signin, "stats.last_signin_at"]], 147 | default_sort: {last_signin: "desc"} 148 | ``` 149 | 150 | Note that `:sort_attributes` are array which of course means, that order of attributes matters. 151 | 152 | There's also a possibility to specify available sort directions using `:sort_dirs` option which is by default `[nil, "asc", "desc"]`. 153 | 154 | ### List item management and in-place editing 155 | 156 | In order to allow managing and editing list items, we need to reorganize our views a bit. Basically, each item needs to have its own partial: 157 | 158 | ```haml 159 | - unless smart_listing.empty? 160 | %table 161 | %thead 162 | %tr 163 | %th= smart_listing.sortable "User name", "name" 164 | %th= smart_listing.sortable "Email", "email" 165 | %th 166 | %tbody 167 | - smart_listing.collection.each do |user| 168 | %tr.editable{data: {id: user.id}} 169 | = smart_listing.render partial: 'users/user', locals: {user: user} 170 | = smart_listing.item_new colspan: 3, link: new_user_path 171 | 172 | = smart_listing.paginate 173 | - else 174 | %p.warning No records! 175 | ``` 176 | 177 | `` has now `editable` class and `data-id` attribute. These are essential to make it work. We've used also a new helper: `Builder#new_item`. It renders new row which is used for adding new items. `:link` needs to be valid url to new resource action which renders JS: 178 | 179 | ```ruby 180 | <%= smart_listing_item :users, :new, @new_user, "users/form" %> 181 | ``` 182 | 183 | Note that `new` action does not need to create SmartListing (via `smart_listing_create`). It just initializes `@new_user` and renders JS view. 184 | 185 | New partial for user (`users/user`) may look like this: 186 | ```haml 187 | %td= user.name 188 | %td= user.email 189 | %td.actions= smart_listing_item_actions [{name: :show, url: user_path(user)}, {name: :edit, url: edit_user_path(user)}, {name: :destroy, url: user_path(user)}] 190 | ``` 191 | 192 | `smart_listing_item_actions` renders here links that allow to edit and destroy user item. `:show`, `:edit` and `:destroy` are built-in actions, you can also define your `:custom` actions. Again. ``'s class `actions` is important. 193 | 194 | Controller actions referenced by above urls are again plain Ruby on Rails actions that render JS like: 195 | 196 | ```erb 197 | <%= smart_listing_item :users, :new, @user, "users/form" %> 198 | <%= smart_listing_item :users, :edit, @user, "users/form" %> 199 | <%= smart_listing_item :users, :destroy, @user %> 200 | ``` 201 | 202 | Partial name supplied to `smart_listing_item` (`users/form`) references `@user` as `object` and may look like this: 203 | 204 | ```haml 205 | %td{colspan: 3} 206 | - if object.persisted? 207 | %p Edit user 208 | - else 209 | %p Add user 210 | 211 | = form_for object, url: object.new_record? ? users_path : user_path(object), remote: true do |f| 212 | %p 213 | Name: 214 | = f.text_field :name 215 | %p 216 | Email: 217 | = f.text_field :email 218 | %p= f.submit "Save" 219 | ``` 220 | 221 | And one last thing are `create` and `update` controller actions JS view: 222 | 223 | ```ruby 224 | <%= smart_listing_item :users, :create, @user, @user.persisted? ? "users/user" : "users/form" %> 225 | <%= smart_listing_item :users, :update, @user, @user.valid? ? "users/user" : "users/form" %> 226 | ``` 227 | 228 | ### Controls (filtering) 229 | 230 | SmartListing controls allow you to change somehow presented data. This is typically used for filtering records. Let's see how view with controls may look like: 231 | 232 | ```haml 233 | = smart_listing_controls_for(:users) do 234 | .filter.input-append 235 | = text_field_tag :filter, '', class: "search", placeholder: "Type name here", autocomplete: "off" 236 | %button.btn.disabled{type: "submit"} 237 | %span.glyphicon.glyphicon-search 238 | ``` 239 | 240 | This gives you nice Bootstrap-enabled filter field with keychange handler. Of course you can use any other form fields in controls too. 241 | 242 | When form field changes its value, form is submitted and request is made. This needs to be handled in controller: 243 | 244 | ```ruby 245 | users_scope = User.active.joins(:stats) 246 | users_scope = users_scope.like(params[:filter]) if params[:filter] 247 | @users = smart_listing_create :users, users_scope, partial: "users/listing" 248 | ``` 249 | 250 | Then, JS view is rendered and your SmartListing updated. That's it! 251 | 252 | ### Simplified views 253 | 254 | You don't need to create all the JS views in case you want to simply use one SmartListing per controller. Just use helper methods without their first attribute (name) ie. `smart_listing_create(User.active, partial: "users/listing")`. Then define two helper methods: 255 | 256 | * `smart_listing_resource` returning single object, 257 | * `smart_listing_collection` returning collection of objects. 258 | 259 | SmartListing default views will user these methods to render your list properly. 260 | 261 | ### More customization 262 | 263 | Apart from standard SmartListing initializer, you can also define custom config profiles. In order to do this, use following syntax: 264 | 265 | ```ruby 266 | SmartListing.configure(:awesome_profile) do |config| 267 | # put your definitions here 268 | end 269 | ``` 270 | 271 | In order to use this profile, create helper method named `smart_listing_config_profile` returning profile name and put into your JS `SmartListing.config.merge()` function call. `merge()` function expects parameter with config attributes hash or reads body data-attribute named `smart-listing-config`. Hash of config attributes can be obtained by using helper method `SmartListing.config(:awesome_profile).to_json`. 272 | 273 | ## Not enough? 274 | 275 | For more information and some use cases, see the [Showcase](http://showcase.sology.eu/smart_listing) 276 | 277 | ## Credits 278 | 279 | SmartListing uses great pagination gem Kaminari https://github.com/amatsuda/kaminari 280 | 281 | Created by Sology http://www.sology.eu 282 | 283 | Initial development sponsored by Smart Language Apps Limited http://smartlanguageapps.com/ 284 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | begin 2 | require 'bundler/setup' 3 | rescue LoadError 4 | puts 'You must `gem install bundler` and `bundle install` to run rake tasks' 5 | end 6 | 7 | require 'rdoc/task' 8 | 9 | RDoc::Task.new(:rdoc) do |rdoc| 10 | rdoc.rdoc_dir = 'rdoc' 11 | rdoc.title = 'SmartListing' 12 | rdoc.options << '--line-numbers' 13 | rdoc.rdoc_files.include('README.rdoc') 14 | rdoc.rdoc_files.include('lib/**/*.rb') 15 | end 16 | 17 | APP_RAKEFILE = File.expand_path("../spec/dummy/Rakefile", __FILE__) 18 | load 'rails/tasks/engine.rake' 19 | 20 | 21 | 22 | Bundler::GemHelper.install_tasks 23 | 24 | require 'rake/testtask' 25 | 26 | Rake::TestTask.new(:test) do |t| 27 | t.libs << 'lib' 28 | t.libs << 'test' 29 | t.pattern = 'test/**/*_test.rb' 30 | t.verbose = false 31 | end 32 | 33 | 34 | task default: :test 35 | -------------------------------------------------------------------------------- /app/assets/javascripts/smart_listing.coffee.erb: -------------------------------------------------------------------------------- 1 | # endsWith polyfill 2 | if !String::endsWith 3 | String::endsWith = (search, this_len) -> 4 | if this_len == undefined or this_len > @length 5 | this_len = @length 6 | @substring(this_len - (search.length), this_len) == search 7 | 8 | # Useful when SmartListing target url is different than current one 9 | $.rails.href = (element) -> 10 | element.attr("href") || element.data("<%= SmartListing.config.data_attributes(:href) %>") || window.location.pathname 11 | 12 | class window.SmartListing 13 | class Config 14 | @options: <%= SmartListing.config.dump_json %> 15 | 16 | @merge: (d) -> 17 | $.extend true, @options, d || $("body").data("smart-listing-config") 18 | 19 | @class: (name)-> 20 | @options["constants"]["classes"][name] 21 | 22 | @class_name: (name) -> 23 | ".#{@class(name)}" 24 | 25 | @data_attribute: (name)-> 26 | @options["constants"]["data_attributes"][name] 27 | 28 | @selector: (name)-> 29 | @options["constants"]["selectors"][name] 30 | 31 | @element_template: (name)-> 32 | @options["constants"]["element_templates"][name] 33 | 34 | @bootstrap_commands: (name)-> 35 | @options["constants"]["bootstrap_commands"][name] 36 | 37 | @config: Config 38 | 39 | 40 | constructor: (e) -> 41 | @container = e 42 | @name = @container.attr("id") 43 | @loading = @container.find(SmartListing.config.class_name("loading")) 44 | @content = @container.find(SmartListing.config.class_name("content")) 45 | @status = $("#{SmartListing.config.class_name("status")} [data-#{SmartListing.config.data_attribute("main")}='#{@name}']") 46 | @confirmed = null 47 | @popovers = {} 48 | 49 | @container.on "ajax:before", "#{SmartListing.config.class_name("item_actions")}, #{SmartListing.config.class_name("pagination_container")}", (e) => 50 | @fadeLoading() 51 | 52 | @container.on "ajax:success", (e) => 53 | if $(e.target).is("#{SmartListing.config.class_name("item_actions")} #{SmartListing.config.selector("item_action_destroy")}") 54 | # handle HEAD OK response for deletion request 55 | editable = $(e.target).closest(SmartListing.config.class_name("editable")) 56 | if @container.find(SmartListing.config.class_name("editable")).length == 1 57 | @reload() 58 | return false 59 | else 60 | editable.remove() 61 | 62 | @container.trigger("smart_listing:destroy", editable) 63 | 64 | @changeItemCount(-1) 65 | @refresh() 66 | 67 | @fadeLoaded() 68 | return false 69 | 70 | @container.on "click", SmartListing.config.selector("edit_cancel"), (event) => 71 | editable = $(event.currentTarget).closest(SmartListing.config.class_name("editable")) 72 | if(editable.length > 0) 73 | # Cancel edit 74 | @cancelEdit(editable) 75 | else 76 | # Cancel new record 77 | @container.find(SmartListing.config.class_name("new_item_placeholder")).addClass(SmartListing.config.class("hidden")) 78 | @container.find(SmartListing.config.class_name("new_item_action")).removeClass(SmartListing.config.class("hidden")) 79 | 80 | @setAutoshow(false) 81 | false 82 | 83 | @container.on "click", "#{SmartListing.config.class_name("item_actions")} a[data-#{SmartListing.config.data_attribute("confirmation")}]", (event) => 84 | $.fn.smart_listing.confirm $(event.currentTarget), $(event.currentTarget).data(SmartListing.config.data_attribute("confirmation")) 85 | 86 | @container.on "click", "#{SmartListing.config.class_name("item_actions")} a[data-#{SmartListing.config.data_attribute("popover")}]", (event) => 87 | name = $(event.currentTarget).data(SmartListing.config.data_attribute("popover")) 88 | if jQuery.isFunction(@popovers[name]) 89 | @popovers[name]($(event.currentTarget)) 90 | false 91 | 92 | 93 | @container.on "click", "input[type=text]#{SmartListing.config.class_name("autoselect")}", (event) -> 94 | $(this).select() 95 | 96 | @container.on "change", SmartListing.config.class_name("callback"), (event) => 97 | checkbox = $(event.currentTarget) 98 | id = checkbox.closest(SmartListing.config.selector("row")).data(SmartListing.config.data_attribute("id")) 99 | data = {} 100 | data[checkbox.val()] = checkbox.is(":checked") 101 | $.ajax({ 102 | beforeSend: (xhr, settings) -> 103 | xhr.setRequestHeader "accept", "*/*;q=0.5, " + settings.accepts.script 104 | url: @container.data(SmartListing.config.data_attribute("callback_href")), 105 | type: "POST", 106 | data: data, 107 | }) 108 | 109 | fadeLoading: => 110 | $.fn.smart_listing.onLoading(@content, @loading) 111 | 112 | fadeLoaded: => 113 | $.fn.smart_listing.onLoaded(@content, @loading) 114 | 115 | itemCount: => 116 | parseInt(@container.data(SmartListing.config.data_attribute("item_count"))) 117 | 118 | maxCount: => 119 | parseInt(@container.data(SmartListing.config.data_attribute("max_count"))) 120 | 121 | setAutoshow: (v) => 122 | 123 | @container.data(SmartListing.config.data_attribute("autoshow"), v) 124 | 125 | changeItemCount: (value) => 126 | count = @container.data(SmartListing.config.data_attribute("item_count")) + value 127 | @container.data(SmartListing.config.data_attribute("item_count"), count) 128 | @container.find(SmartListing.config.selector("pagination_count")).html(count) 129 | 130 | cancelEdit: (editable) => 131 | if editable.data(SmartListing.config.data_attribute("inline_edit_backup")) 132 | editable.html(editable.data(SmartListing.config.data_attribute("inline_edit_backup"))) 133 | editable.removeClass(SmartListing.config.class("inline_editing")) 134 | editable.removeData(SmartListing.config.data_attribute("inline_edit_backup")) 135 | 136 | # Callback called when record is added/deleted using ajax request 137 | refresh: () => 138 | header = @content.find(SmartListing.config.selector("head")) 139 | footer = @content.find(SmartListing.config.class_name("pagination_per_page")) 140 | no_records = @content.find(SmartListing.config.class_name("no_records")) 141 | 142 | if @itemCount() == 0 143 | header.hide() 144 | footer.hide() 145 | no_records.show() 146 | else 147 | header.show() 148 | footer.show() 149 | no_records.hide() 150 | 151 | if @maxCount() 152 | if @itemCount() >= @maxCount() 153 | @container.find(SmartListing.config.class_name("new_item_placeholder")).addClass(SmartListing.config.class("hidden")) 154 | @container.find(SmartListing.config.class_name("new_item_action")).addClass(SmartListing.config.class("hidden")) 155 | else 156 | if @container.data(SmartListing.config.data_attribute("autoshow")) 157 | @container.find(SmartListing.config.class_name("new_item_placeholder")).removeClass(SmartListing.config.class("hidden")) 158 | @container.find(SmartListing.config.class_name("new_item_action")).addClass(SmartListing.config.class("hidden")) 159 | else 160 | @container.find(SmartListing.config.class_name("new_item_placeholder")).addClass(SmartListing.config.class("hidden")) 161 | @container.find(SmartListing.config.class_name("new_item_action")).removeClass(SmartListing.config.class("hidden")) 162 | 163 | @status.each (index, status) => 164 | $(status).find(SmartListing.config.class_name("limit")).html(@maxCount() - @itemCount()) 165 | if @maxCount() - @itemCount() == 0 166 | $(status).find(SmartListing.config.class_name("limit_alert")).show() 167 | else 168 | $(status).find(SmartListing.config.class_name("limit_alert")).hide() 169 | 170 | # Trigger AJAX request to reload the list 171 | reload: () => 172 | $.rails.handleRemote(@container) 173 | 174 | params: (value) => 175 | if value 176 | @container.data(SmartListing.config.data_attribute("params"), value) 177 | else 178 | @container.data(SmartListing.config.data_attribute("params")) 179 | 180 | registerPopover: (name, callback) => 181 | @popovers[name] = callback 182 | 183 | editable: (id) => 184 | @container.find("#{SmartListing.config.class_name("editable")}[data-#{SmartListing.config.data_attribute("id")}=#{id}]") 185 | 186 | ################################################################################################# 187 | # Methods executed by rails UJS: 188 | 189 | new_item: (content) => 190 | if !@maxCount() || (@itemCount() < @maxCount()) 191 | new_item_action = @container.find(SmartListing.config.class_name("new_item_action")) 192 | new_item_placeholder = @container.find(SmartListing.config.class_name("new_item_placeholder")).addClass(SmartListing.config.class("hidden")) 193 | 194 | @container.find(SmartListing.config.class_name("editable")).each (i, v) => 195 | @cancelEdit($(v)) 196 | 197 | new_item_action.addClass(SmartListing.config.class("hidden")) 198 | new_item_placeholder.removeClass(SmartListing.config.class("hidden")) 199 | new_item_placeholder.html(content) 200 | new_item_placeholder.addClass(SmartListing.config.class("inline_editing")) 201 | 202 | @container.trigger("smart_listing:new", new_item_placeholder) 203 | 204 | @fadeLoaded() 205 | 206 | create: (id, success, content) => 207 | new_item_action = @container.find(SmartListing.config.class_name("new_item_action")) 208 | new_item_placeholder = @container.find(SmartListing.config.class_name("new_item_placeholder")) 209 | 210 | if success 211 | new_item_placeholder.addClass(SmartListing.config.class("hidden")) 212 | new_item_action.removeClass(SmartListing.config.class("hidden")) 213 | 214 | new_item = $(SmartListing.config.element_template("row")).addClass(SmartListing.config.class("editable")) 215 | new_item.attr("data-#{SmartListing.config.data_attribute("id")}", id) 216 | new_item.html(content) 217 | 218 | if new_item_placeholder.length != 0 219 | if new_item_placeholder.data("insert-mode") == "after" 220 | new_item_placeholder.after(new_item) 221 | else 222 | new_item_placeholder.before(new_item) 223 | else 224 | @content.append(new_item) 225 | 226 | @container.trigger("smart_listing:create:success", new_item) 227 | 228 | @changeItemCount(1) 229 | @refresh() 230 | else 231 | new_item_placeholder.html(content) 232 | 233 | @container.trigger("smart_listing:create:fail", new_item_placeholder) 234 | 235 | @fadeLoaded() 236 | 237 | edit: (id, content) => 238 | @container.find(SmartListing.config.class_name("editable")).each (i, v) => 239 | @cancelEdit($(v)) 240 | @container.find(SmartListing.config.class_name("new_item_placeholder")).addClass(SmartListing.config.class("hidden")) 241 | @container.find(SmartListing.config.class_name("new_item_action")).removeClass(SmartListing.config.class("hidden")) 242 | 243 | editable = @editable(id) 244 | editable.data(SmartListing.config.data_attribute("inline_edit_backup"), editable.html()) 245 | editable.html(content) 246 | editable.addClass(SmartListing.config.class("inline_editing")) 247 | 248 | @container.trigger("smart_listing:edit", editable) 249 | 250 | @fadeLoaded() 251 | 252 | update: (id, success, content) => 253 | editable = @editable(id) 254 | if success 255 | editable.removeClass(SmartListing.config.class("inline_editing")) 256 | editable.removeData(SmartListing.config.data_attribute("inline_edit_backup")) 257 | editable.html(content) 258 | 259 | @container.trigger("smart_listing:update:success", editable) 260 | 261 | @refresh() 262 | else 263 | editable.html(content) 264 | 265 | @container.trigger("smart_listing:update:fail", editable) 266 | 267 | @fadeLoaded() 268 | 269 | destroy: (id, destroyed) => 270 | # No need to do anything here, already handled by ajax:success handler 271 | 272 | remove: (id) => 273 | editable = @editable(id) 274 | editable.remove() 275 | 276 | @container.trigger("smart_listing:remove", editable) 277 | 278 | update_list: (content, data) => 279 | @container.data(SmartListing.config.data_attribute("params"), $.extend(@container.data(SmartListing.config.data_attribute("params")), data[SmartListing.config.data_attribute("params")])) 280 | @container.data(SmartListing.config.data_attribute("max_count"), data[SmartListing.config.data_attribute("max_count")]) 281 | @container.data(SmartListing.config.data_attribute("item_count"), data[SmartListing.config.data_attribute("item_count")]) 282 | 283 | @content.html(content) 284 | 285 | @refresh() 286 | @fadeLoaded() 287 | 288 | @container.trigger("smart_listing:update_list", @container) 289 | 290 | $.fn.smart_listing = () -> 291 | map = $(this).map () -> 292 | if !$(this).data(SmartListing.config.data_attribute("main")) 293 | $(this).data(SmartListing.config.data_attribute("main"), new SmartListing($(this))) 294 | $(this).data(SmartListing.config.data_attribute("main")) 295 | if map.length == 1 296 | map[0] 297 | else 298 | map 299 | 300 | $.fn.smart_listing.observeField = (field, opts = {}) -> 301 | key_timeout = null 302 | last_value = null 303 | options = 304 | onFilled: () -> 305 | onEmpty: () -> 306 | onChange: () -> 307 | options = $.extend(options, opts) 308 | 309 | keyChange = () -> 310 | if field.val().length > 0 311 | options.onFilled() 312 | else 313 | options.onEmpty() 314 | 315 | if field.val() == last_value && field.val().length != 0 316 | return 317 | lastValue = field.val() 318 | 319 | options.onChange() 320 | 321 | field.data(SmartListing.config.data_attribute("observed"), true) 322 | 323 | field.bind "keydown", (e) -> 324 | if(key_timeout) 325 | clearTimeout(key_timeout) 326 | 327 | key_timeout = setTimeout(-> 328 | keyChange() 329 | , 400) 330 | 331 | $.fn.smart_listing.showPopover = (elem, body) -> 332 | elem.popover(SmartListing.config.bootstrap_commands("popover_destroy")) 333 | elem.popover(content: body, html: true, trigger: "manual") 334 | elem.popover("show") 335 | 336 | $.fn.smart_listing.showConfirmation = (confirmation_elem, msg, confirm_callback) -> 337 | buildPopover = (confirmation_elem, msg) -> 338 | deletion_popover = $("
").addClass("confirmation_box") 339 | deletion_popover.append($("

").html(msg)) 340 | deletion_popover.append($("

") 341 | .append($(" 16 | <% end %> 17 | 18 | <%= smart_listing_render %> 19 | -------------------------------------------------------------------------------- /spec/dummy/app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Dummy 5 | <%= stylesheet_link_tag 'application', media: 'all' %> 6 | <%= javascript_include_tag 'application' %> 7 | <%= csrf_meta_tags %> 8 | 9 | 10 | 11 | <%= yield %> 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /spec/dummy/app/views/users/_list.html.erb: -------------------------------------------------------------------------------- 1 | <% unless smart_listing.empty? %> 2 | 3 | 4 | 5 | 6 | 7 | 8 | <% smart_listing.collection.each do |user| %> 9 | 10 | 11 | 12 | 13 | <% end %> 14 | 15 |
NameEmail
<%= user.name %><%= user.email %>
16 | <% # Render nice pagination links fitted for Bootstrap 3 by default %> 17 | <%= smart_listing.paginate %> 18 | <%= smart_listing.pagination_per_page_links %> 19 | <% else %> 20 |

No records!

21 | <% end %> 22 | -------------------------------------------------------------------------------- /spec/dummy/app/views/users/_searchable_list.html.erb: -------------------------------------------------------------------------------- 1 | <% unless smart_listing.empty? %> 2 | 3 | 4 | 5 | 6 | 7 | 8 | <% smart_listing.collection.each do |user| %> 9 | 10 | 11 | 12 | 13 | <% end %> 14 | 15 |
NameEmail
<%= user.name %><%= user.email %>
16 | <% # Render nice pagination links fitted for Bootstrap 3 by default %> 17 | <%= smart_listing.paginate %> 18 | <%= smart_listing.pagination_per_page_links %> 19 | <% else %> 20 |

No records!

21 | <% end %> 22 | -------------------------------------------------------------------------------- /spec/dummy/app/views/users/_sortable_list.html.erb: -------------------------------------------------------------------------------- 1 | <% unless smart_listing.empty? %> 2 | 3 | 4 | 7 | 10 | 11 | 12 | <% smart_listing.collection.each do |user| %> 13 | 14 | 15 | 16 | 17 | <% end %> 18 | 19 |
5 | <%= smart_listing.sortable "Name", "name" %> 6 |
<%= user.name %><%= user.email %>
20 | <%= smart_listing.paginate %> 21 | <%= smart_listing.pagination_per_page_links %> 22 | <% else %> 23 |

No records!

24 | <% end %> 25 | -------------------------------------------------------------------------------- /spec/dummy/app/views/users/index.html.erb: -------------------------------------------------------------------------------- 1 | <%= smart_listing_render %> 2 | -------------------------------------------------------------------------------- /spec/dummy/app/views/users/index.js.erb: -------------------------------------------------------------------------------- 1 | <%= smart_listing_update %> 2 | -------------------------------------------------------------------------------- /spec/dummy/app/views/users/searchable.html.erb: -------------------------------------------------------------------------------- 1 | <%= smart_listing_controls_for(:users, {class: 'form-inline text-right'}) do %> 2 |
3 | <%= text_field_tag :filter, '', 4 | class: 'search form-control', placeholder: 'Search...', 5 | autocomplete: :off %> 6 |
7 | 10 | <% end %> 11 | <%= smart_listing_render %> 12 | 13 | -------------------------------------------------------------------------------- /spec/dummy/app/views/users/searchable.js.erb: -------------------------------------------------------------------------------- 1 | <%= smart_listing_update %> 2 | 3 | 4 | -------------------------------------------------------------------------------- /spec/dummy/bin/bundle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 3 | load Gem.bin_path('bundler', 'bundle') 4 | -------------------------------------------------------------------------------- /spec/dummy/bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | APP_PATH = File.expand_path('../../config/application', __FILE__) 3 | require_relative '../config/boot' 4 | require 'rails/commands' 5 | -------------------------------------------------------------------------------- /spec/dummy/bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require_relative '../config/boot' 3 | require 'rake' 4 | Rake.application.run 5 | -------------------------------------------------------------------------------- /spec/dummy/config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require ::File.expand_path('../config/environment', __FILE__) 4 | run Rails.application 5 | -------------------------------------------------------------------------------- /spec/dummy/config/application.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../boot', __FILE__) 2 | 3 | # Pick the frameworks you want: 4 | require "active_record/railtie" 5 | require "action_controller/railtie" 6 | require "action_mailer/railtie" 7 | require "action_view/railtie" 8 | require "sprockets/railtie" 9 | # require "rails/test_unit/railtie" 10 | 11 | Bundler.require(*Rails.groups) 12 | require "smart_listing" 13 | 14 | module Dummy 15 | class Application < Rails::Application 16 | # Settings in config/environments/* take precedence over those specified here. 17 | # Application configuration should go into files in config/initializers 18 | # -- all .rb files in that directory are automatically loaded. 19 | 20 | # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. 21 | # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. 22 | # config.time_zone = 'Central Time (US & Canada)' 23 | 24 | # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. 25 | # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] 26 | # config.i18n.default_locale = :de 27 | end 28 | end 29 | 30 | -------------------------------------------------------------------------------- /spec/dummy/config/boot.rb: -------------------------------------------------------------------------------- 1 | # Set up gems listed in the Gemfile. 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../../../Gemfile', __FILE__) 3 | 4 | require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) 5 | $LOAD_PATH.unshift File.expand_path('../../../../lib', __FILE__) 6 | -------------------------------------------------------------------------------- /spec/dummy/config/database.yml: -------------------------------------------------------------------------------- 1 | # SQLite version 3.x 2 | # gem install sqlite3 3 | # 4 | # Ensure the SQLite 3 gem is defined in your Gemfile 5 | # gem 'sqlite3' 6 | # 7 | default: &default 8 | adapter: sqlite3 9 | pool: 5 10 | timeout: 5000 11 | 12 | development: 13 | <<: *default 14 | database: db/development.sqlite3 15 | 16 | # Warning: The database defined as "test" will be erased and 17 | # re-generated from your development database when you run "rake". 18 | # Do not set this db to the same as development or production. 19 | test: 20 | <<: *default 21 | database: db/test.sqlite3 22 | 23 | production: 24 | <<: *default 25 | database: db/production.sqlite3 26 | -------------------------------------------------------------------------------- /spec/dummy/config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require File.expand_path('../application', __FILE__) 3 | 4 | require "jquery-rails" 5 | require 'coffee-rails' 6 | require 'bootstrap-sass' 7 | 8 | # Initialize the Rails application. 9 | Rails.application.initialize! 10 | -------------------------------------------------------------------------------- /spec/dummy/config/environments/development.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # In the development environment your application's code is reloaded on 5 | # every request. This slows down response time but is perfect for development 6 | # since you don't have to restart the web server when you make code changes. 7 | config.cache_classes = false 8 | 9 | # Do not eager load code on boot. 10 | config.eager_load = false 11 | 12 | # Show full error reports and disable caching. 13 | config.consider_all_requests_local = true 14 | config.action_controller.perform_caching = false 15 | 16 | # Don't care if the mailer can't send. 17 | config.action_mailer.raise_delivery_errors = false 18 | 19 | # Print deprecation notices to the Rails logger. 20 | config.active_support.deprecation = :log 21 | 22 | # Raise an error on page load if there are pending migrations. 23 | config.active_record.migration_error = :page_load 24 | 25 | # Debug mode disables concatenation and preprocessing of assets. 26 | # This option may cause significant delays in view rendering with a large 27 | # number of complex assets. 28 | config.assets.debug = true 29 | 30 | # Adds additional error checking when serving assets at runtime. 31 | # Checks for improperly declared sprockets dependencies. 32 | # Raises helpful error messages. 33 | config.assets.raise_runtime_errors = true 34 | 35 | # Raises error for missing translations 36 | # config.action_view.raise_on_missing_translations = true 37 | end 38 | -------------------------------------------------------------------------------- /spec/dummy/config/environments/production.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # Code is not reloaded between requests. 5 | config.cache_classes = true 6 | 7 | # Eager load code on boot. This eager loads most of Rails and 8 | # your application in memory, allowing both threaded web servers 9 | # and those relying on copy on write to perform better. 10 | # Rake tasks automatically ignore this option for performance. 11 | config.eager_load = true 12 | 13 | # Full error reports are disabled and caching is turned on. 14 | config.consider_all_requests_local = false 15 | config.action_controller.perform_caching = true 16 | 17 | # Enable Rack::Cache to put a simple HTTP cache in front of your application 18 | # Add `rack-cache` to your Gemfile before enabling this. 19 | # For large-scale production use, consider using a caching reverse proxy like nginx, varnish or squid. 20 | # config.action_dispatch.rack_cache = true 21 | 22 | # Disable Rails's static asset server (Apache or nginx will already do this). 23 | config.serve_static_assets = false 24 | 25 | # Compress JavaScripts and CSS. 26 | config.assets.js_compressor = :uglifier 27 | # config.assets.css_compressor = :sass 28 | 29 | # Do not fallback to assets pipeline if a precompiled asset is missed. 30 | config.assets.compile = false 31 | 32 | # Generate digests for assets URLs. 33 | config.assets.digest = true 34 | 35 | # `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb 36 | 37 | # Specifies the header that your server uses for sending files. 38 | # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for apache 39 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx 40 | 41 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 42 | # config.force_ssl = true 43 | 44 | # Set to :debug to see everything in the log. 45 | config.log_level = :info 46 | 47 | # Prepend all log lines with the following tags. 48 | # config.log_tags = [ :subdomain, :uuid ] 49 | 50 | # Use a different logger for distributed setups. 51 | # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new) 52 | 53 | # Use a different cache store in production. 54 | # config.cache_store = :mem_cache_store 55 | 56 | # Enable serving of images, stylesheets, and JavaScripts from an asset server. 57 | # config.action_controller.asset_host = "http://assets.example.com" 58 | 59 | # Ignore bad email addresses and do not raise email delivery errors. 60 | # Set this to true and configure the email server for immediate delivery to raise delivery errors. 61 | # config.action_mailer.raise_delivery_errors = false 62 | 63 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 64 | # the I18n.default_locale when a translation cannot be found). 65 | config.i18n.fallbacks = true 66 | 67 | # Send deprecation notices to registered listeners. 68 | config.active_support.deprecation = :notify 69 | 70 | # Disable automatic flushing of the log to improve performance. 71 | # config.autoflush_log = false 72 | 73 | # Use default logging formatter so that PID and timestamp are not suppressed. 74 | config.log_formatter = ::Logger::Formatter.new 75 | 76 | # Do not dump schema after migrations. 77 | config.active_record.dump_schema_after_migration = false 78 | end 79 | -------------------------------------------------------------------------------- /spec/dummy/config/environments/test.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # The test environment is used exclusively to run your application's 5 | # test suite. You never need to work with it otherwise. Remember that 6 | # your test database is "scratch space" for the test suite and is wiped 7 | # and recreated between test runs. Don't rely on the data there! 8 | config.cache_classes = false 9 | 10 | # Do not eager load code on boot. This avoids loading your whole application 11 | # just for the purpose of running a single test. If you are using a tool that 12 | # preloads Rails for running tests, you may have to set it to true. 13 | config.eager_load = false 14 | 15 | # Configure static asset server for tests with Cache-Control for performance. 16 | config.serve_static_assets = true 17 | config.static_cache_control = 'public, max-age=3600' 18 | 19 | # Show full error reports and disable caching. 20 | config.consider_all_requests_local = true 21 | config.action_controller.perform_caching = false 22 | 23 | # Raise exceptions instead of rendering exception templates. 24 | config.action_dispatch.show_exceptions = false 25 | 26 | # Disable request forgery protection in test environment. 27 | config.action_controller.allow_forgery_protection = false 28 | 29 | # Tell Action Mailer not to deliver emails to the real world. 30 | # The :test delivery method accumulates sent emails in the 31 | # ActionMailer::Base.deliveries array. 32 | config.action_mailer.delivery_method = :test 33 | 34 | # Print deprecation notices to the stderr. 35 | config.active_support.deprecation = :stderr 36 | 37 | # Raises error for missing translations 38 | # config.action_view.raise_on_missing_translations = true 39 | end 40 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/assets.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Version of your assets, change this if you want to expire all your assets. 4 | Rails.application.config.assets.version = '1.0' 5 | 6 | # Precompile additional assets. 7 | # application.js, application.css, and all non-JS/CSS in app/assets folder are already added. 8 | # Rails.application.config.assets.precompile += %w( search.js ) 9 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } 5 | 6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. 7 | # Rails.backtrace_cleaner.remove_silencers! 8 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/cookies_serializer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | Rails.application.config.action_dispatch.cookies_serializer = :json -------------------------------------------------------------------------------- /spec/dummy/config/initializers/filter_parameter_logging.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Configure sensitive parameters which will be filtered from the log file. 4 | Rails.application.config.filter_parameters += [:password] 5 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format. Inflections 4 | # are locale specific, and you may define rules for as many different 5 | # locales as you wish. All of these examples are active by default: 6 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 7 | # inflect.plural /^(ox)$/i, '\1en' 8 | # inflect.singular /^(ox)en/i, '\1' 9 | # inflect.irregular 'person', 'people' 10 | # inflect.uncountable %w( fish sheep ) 11 | # end 12 | 13 | # These inflection rules are supported but not enabled by default: 14 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 15 | # inflect.acronym 'RESTful' 16 | # end 17 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new mime types for use in respond_to blocks: 4 | # Mime::Type.register "text/richtext", :rtf 5 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/session_store.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | Rails.application.config.session_store :cookie_store, key: '_dummy_session' 4 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/smart_listing.rb: -------------------------------------------------------------------------------- 1 | SmartListing.configure do |config| 2 | config.global_options({ 3 | #:param_names => { # param names 4 | #:page => :page, 5 | #:per_page => :per_page, 6 | #:sort => :sort, 7 | #}, 8 | #:array => false, # controls whether smart list should be using arrays or AR collections 9 | #:max_count => nil, # limit number of rows 10 | #:unlimited_per_page => false, # allow infinite page size 11 | #:paginate => true, # allow pagination 12 | #:memorize_per_page => false, # save per page settings in the cookie 13 | :page_sizes => [3, 10], # set available page sizes array 14 | #:kaminari_options => {:theme => "smart_listing"}, # Kaminari's paginate helper options 15 | #:sort_dirs => [nil, "asc", "desc"], # Default sorting directions cycle of sortables 16 | }) 17 | 18 | config.constants :classes, { 19 | #:main => "smart-listing", 20 | #:editable => "editable", 21 | #:content => "content", 22 | #:loading => "loading", 23 | #:status => "smart-listing-status", 24 | #:item_actions => "actions", 25 | #:new_item_placeholder => "new-item-placeholder", 26 | #:new_item_action => "new-item-action", 27 | #:new_item_button => "btn", 28 | #:hidden => "hidden", 29 | #:autoselect => "autoselect", 30 | #:callback => "callback", 31 | #:pagination_per_page => "pagination-per-page text-center", 32 | #:inline_editing => "info", 33 | #:no_records => "no-records", 34 | #:limit => "smart-listing-limit", 35 | #:limit_alert => "smart-listing-limit-alert", 36 | #:controls => "smart-listing-controls", 37 | #:controls_reset => "reset", 38 | #:filtering => "filter", 39 | #:filtering_search => "glyphicon-search", 40 | #:filtering_cancel => "glyphicon-remove", 41 | #:filtering_disabled => "disabled", 42 | #:sortable => "sortable", 43 | #:icon_new => "glyphicon glyphicon-plus", 44 | #:icon_edit => "glyphicon glyphicon-pencil", 45 | #:icon_trash => "glyphicon glyphicon-trash", 46 | #:icon_inactive => "glyphicon glyphicon-circle", 47 | #:icon_show => "glyphicon glyphicon-share-alt", 48 | #:icon_sort_none => "glyphicon glyphicon-resize-vertical", 49 | #:icon_sort_up => "glyphicon glyphicon-chevron-up", 50 | #:icon_sort_down => "glyphicon glyphicon-chevron-down", 51 | #:muted => "text-muted", 52 | } 53 | 54 | config.constants :data_attributes, { 55 | #:main => "smart-listing", 56 | #:controls_initialized => "smart-listing-controls-initialized", 57 | #:confirmation => "confirmation", 58 | #:id => "id", 59 | #:href => "href", 60 | #:callback_href => "callback-href", 61 | #:max_count => "max-count", 62 | #:item_count => "item-count", 63 | #:inline_edit_backup => "smart-listing-edit-backup", 64 | #:params => "params", 65 | #:observed => "observed", 66 | #:href => "href", 67 | #:autoshow => "autoshow", 68 | #:popover => "slpopover", 69 | } 70 | 71 | config.constants :selectors, { 72 | #:item_action_destroy => "a.destroy", 73 | #:edit_cancel => "button.cancel", 74 | #:row => "tr", 75 | #:head => "thead", 76 | #:filtering_icon => "i" 77 | #:filtering_button => "button", 78 | #:filtering_icon => "button span", 79 | #:filtering_input => ".filter input", 80 | #:pagination_count => ".pagination-per-page .count", 81 | } 82 | end 83 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # This file contains settings for ActionController::ParamsWrapper which 4 | # is enabled by default. 5 | 6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. 7 | ActiveSupport.on_load(:action_controller) do 8 | wrap_parameters format: [:json] if respond_to?(:wrap_parameters) 9 | end 10 | 11 | # To enable root element in JSON for ActiveRecord objects. 12 | # ActiveSupport.on_load(:active_record) do 13 | # self.include_root_in_json = true 14 | # end 15 | -------------------------------------------------------------------------------- /spec/dummy/config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Files in the config/locales directory are used for internationalization 2 | # and are automatically loaded by Rails. If you want to use locales other 3 | # than English, add the necessary files in this directory. 4 | # 5 | # To use the locales, use `I18n.t`: 6 | # 7 | # I18n.t 'hello' 8 | # 9 | # In views, this is aliased to just `t`: 10 | # 11 | # <%= t('hello') %> 12 | # 13 | # To use a different locale, set it with `I18n.locale`: 14 | # 15 | # I18n.locale = :es 16 | # 17 | # This would use the information in config/locales/es.yml. 18 | # 19 | # To learn more, please read the Rails Internationalization guide 20 | # available at http://guides.rubyonrails.org/i18n.html. 21 | 22 | en: 23 | hello: "Hello world" 24 | -------------------------------------------------------------------------------- /spec/dummy/config/routes.rb: -------------------------------------------------------------------------------- 1 | Rails.application.routes.draw do 2 | resources :users do 3 | collection do 4 | get 'sortable' 5 | get 'searchable' 6 | end 7 | end 8 | 9 | namespace :admin do 10 | resources :users do 11 | member do 12 | put 'change_name' 13 | end 14 | end 15 | end 16 | # The priority is based upon order of creation: first created -> highest priority. 17 | # See how all your routes lay out with "rake routes". 18 | 19 | # You can have the root of your site routed with "root" 20 | root 'users#index' 21 | 22 | # Example of regular route: 23 | # get 'products/:id' => 'catalog#view' 24 | 25 | # Example of named route that can be invoked with purchase_url(id: product.id) 26 | # get 'products/:id/purchase' => 'catalog#purchase', as: :purchase 27 | 28 | # Example resource route (maps HTTP verbs to controller actions automatically): 29 | # resources :products 30 | 31 | # Example resource route with options: 32 | # resources :products do 33 | # member do 34 | # get 'short' 35 | # post 'toggle' 36 | # end 37 | # 38 | # collection do 39 | # get 'sold' 40 | # end 41 | # end 42 | 43 | # Example resource route with sub-resources: 44 | # resources :products do 45 | # resources :comments, :sales 46 | # resource :seller 47 | # end 48 | 49 | # Example resource route with more complex sub-resources: 50 | # resources :products do 51 | # resources :comments 52 | # resources :sales do 53 | # get 'recent', on: :collection 54 | # end 55 | # end 56 | 57 | # Example resource route with concerns: 58 | # concern :toggleable do 59 | # post 'toggle' 60 | # end 61 | # resources :posts, concerns: :toggleable 62 | # resources :photos, concerns: :toggleable 63 | 64 | # Example resource route within a namespace: 65 | # namespace :admin do 66 | # # Directs /admin/products/* to Admin::ProductsController 67 | # # (app/controllers/admin/products_controller.rb) 68 | # resources :products 69 | # end 70 | end 71 | -------------------------------------------------------------------------------- /spec/dummy/config/secrets.yml: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Your secret key is used for verifying the integrity of signed cookies. 4 | # If you change this key, all old signed cookies will become invalid! 5 | 6 | # Make sure the secret is at least 30 characters and all random, 7 | # no regular words or you'll be exposed to dictionary attacks. 8 | # You can use `rake secret` to generate a secure secret key. 9 | 10 | # Make sure the secrets in this file are kept private 11 | # if you're sharing your code publicly. 12 | 13 | development: 14 | secret_key_base: cb71bf666c97336b3d87e2aaf8b4fc962264491f7eadaf838f8ef290d115631b7a640c9a60704e9c3a110692d06cd105e87c4cd5591d659c3ce44c7b5128ec18 15 | 16 | test: 17 | secret_key_base: f3ab27e4cd06e477d716bce701634636d2db5b835592dde50e7e64fa59adef9db7fce71c7b7bfe581089280a6f0e39e67a0efeae559a3f89e7138a44b9fda4a1 18 | 19 | # Do not keep production secrets in the repository, 20 | # instead read values from the environment. 21 | production: 22 | secret_key_base: <%= ENV["SECRET_KEY_BASE"] %> 23 | -------------------------------------------------------------------------------- /spec/dummy/db/migrate/20180126065408_create_user.rb: -------------------------------------------------------------------------------- 1 | class CreateUser < ActiveRecord::Migration[5.1] 2 | def change 3 | create_table :users do |t| 4 | t.string :name 5 | t.string :email 6 | t.boolean :boolean 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/dummy/db/schema.rb: -------------------------------------------------------------------------------- 1 | # This file is auto-generated from the current state of the database. Instead 2 | # of editing this file, please use the migrations feature of Active Record to 3 | # incrementally modify your database, and then regenerate this schema definition. 4 | # 5 | # Note that this schema.rb definition is the authoritative source for your 6 | # database schema. If you need to create the application database on another 7 | # system, you should be using db:schema:load, not running all the migrations 8 | # from scratch. The latter is a flawed and unsustainable approach (the more migrations 9 | # you'll amass, the slower it'll run and the greater likelihood for issues). 10 | # 11 | # It's strongly recommended that you check this file into your version control system. 12 | 13 | ActiveRecord::Schema.define(version: 20180126065408) do 14 | 15 | create_table "users", force: :cascade do |t| 16 | t.string "name" 17 | t.string "email" 18 | t.boolean "boolean" 19 | end 20 | 21 | end 22 | -------------------------------------------------------------------------------- /spec/dummy/db/seeds.rb: -------------------------------------------------------------------------------- 1 | User.find_or_create_by(id: 1, name: "Betty", email: "betty@email.com", boolean: false) 2 | User.find_or_create_by(id: 2, name: "Aaron", email: "aaron@email.com", boolean: true) 3 | User.find_or_create_by(id: 3, name: "Jane", email: "jane@test.eu", boolean: false) 4 | User.find_or_create_by(id: 4, name: "Edward", email: "edward@test.eu", boolean: true) 5 | User.find_or_create_by(id: 5, name: "Nicholas", email: "salohcin@email.com", boolean: false) 6 | User.find_or_create_by(id: 6, name: "Lisa", email: "asil@email.com", boolean: true) 7 | User.find_or_create_by(id: 7, name: "Sara", email: "aras@test.eu", boolean: false) 8 | User.find_or_create_by(id: 8, name: "Robin", email: "nibor@test.eu", boolean: true) 9 | -------------------------------------------------------------------------------- /spec/dummy/fixtures/users.yml: -------------------------------------------------------------------------------- 1 | user1: 2 | id: 1 3 | name: "Betty" 4 | email: "betty@email.com" 5 | boolean: false 6 | user2: 7 | id: 2 8 | name: "Aaron" 9 | email: "aaron@email.com" 10 | boolean: true 11 | user3: 12 | id: 3 13 | name: "Jane" 14 | email: "jane@test.eu" 15 | boolean: false 16 | user4: 17 | id: 4 18 | name: "Edward" 19 | email: "edward@test.eu" 20 | boolean: true 21 | user5: 22 | id: 5 23 | name: "Nicholas" 24 | email: "salohcin@email.com" 25 | boolean: false 26 | user6: 27 | id: 6 28 | name: "Lisa" 29 | email: "asil@email.com" 30 | boolean: true 31 | user7: 32 | id: 7 33 | name: "Sara" 34 | email: "aras@test.eu" 35 | boolean: false 36 | user8: 37 | id: 8 38 | name: "Robin" 39 | email: "nibor@test.eu" 40 | boolean: true 41 | -------------------------------------------------------------------------------- /spec/dummy/lib/assets/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sology/smart_listing/08ea04a6c50485c81bd0a0b64f07ec8a5ea2539e/spec/dummy/lib/assets/.keep -------------------------------------------------------------------------------- /spec/dummy/log/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sology/smart_listing/08ea04a6c50485c81bd0a0b64f07ec8a5ea2539e/spec/dummy/log/.keep -------------------------------------------------------------------------------- /spec/dummy/public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The page you were looking for doesn't exist (404) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

The page you were looking for doesn't exist.

62 |

You may have mistyped the address or the page may have moved.

63 |
64 |

If you are the application owner check the logs for more information.

65 |
66 | 67 | 68 | -------------------------------------------------------------------------------- /spec/dummy/public/422.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The change you wanted was rejected (422) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

The change you wanted was rejected.

62 |

Maybe you tried to change something you didn't have access to.

63 |
64 |

If you are the application owner check the logs for more information.

65 |
66 | 67 | 68 | -------------------------------------------------------------------------------- /spec/dummy/public/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | We're sorry, but something went wrong (500) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

We're sorry, but something went wrong.

62 |
63 |

If you are the application owner check the logs for more information.

64 |
65 | 66 | 67 | -------------------------------------------------------------------------------- /spec/dummy/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sology/smart_listing/08ea04a6c50485c81bd0a0b64f07ec8a5ea2539e/spec/dummy/public/favicon.ico -------------------------------------------------------------------------------- /spec/features/custom_filters_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | feature 'Combine custom filtering' do 4 | fixtures :users 5 | 6 | scenario 'The user search user, change pagination and change page', js: true do 7 | 8 | visit admin_users_path 9 | #page_sizes => [3, 10] 10 | within(".pagination-per-page") { click_on "10" } 11 | expect(page).to have_selector('tr.editable', count: 8) 12 | fill_in "filter", with: "test" 13 | expect(page).to have_selector('tr.editable', count: 4) 14 | within(".pagination-per-page") { click_on "3" } 15 | within(".pagination") { click_on "2" } 16 | expect(page).to have_selector('tr.editable', count: 1) 17 | 18 | end 19 | 20 | scenario 'The user sort users and change page', js: true do 21 | 22 | visit admin_users_path 23 | find('.name a.sortable').click 24 | expect(page).to have_content("Aaron") 25 | expect(page).to_not have_content("Jane") 26 | within(".pagination") { click_on "2" } 27 | expect(page).to have_content("Jane") 28 | expect(page).to_not have_content("Aaron") 29 | 30 | end 31 | 32 | scenario 'The user combine filters', js: true do 33 | 34 | visit admin_users_path 35 | fill_in "filter", with: "email" 36 | find('input#boolean').click 37 | expect(page).to have_selector('tr.editable', count: 2) 38 | 39 | end 40 | 41 | scenario 'The user combine filters and sort users', js: true do 42 | 43 | visit admin_users_path 44 | fill_in "filter", with: "test" 45 | find('input#boolean').click 46 | wait_for_ajax 47 | expect(page).to have_selector('tr.editable', count: 2) 48 | click_link 'Name' 49 | expect(page).to have_selector('tr.editable', count: 2) 50 | expect(page.find(:css, "tbody > tr:nth-child(1)")).to have_content("Edward") 51 | expect(page.find(:css, "tbody > tr:nth-child(2)")).to have_content("Robin") 52 | 53 | end 54 | 55 | scenario 'The user combine filters, sort and change page', js: true do 56 | 57 | visit admin_users_path 58 | check 'boolean' 59 | wait_for_ajax 60 | expect(find(:css, '.email a.sortable')[:href]).to include("boolean") 61 | click_link 'Email' 62 | expect(page.find(:css, "tbody > tr:nth-child(2)")).to have_content("Lisa") 63 | within(".pagination") { click_on "2" } 64 | expect(page.find(:css, "tbody > tr:nth-child(1)")).to have_content("Robin") 65 | expect(page.find(:css, '.count')).to have_content("4") 66 | 67 | end 68 | 69 | end 70 | -------------------------------------------------------------------------------- /spec/features/manage_items_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | feature "Manage items" do 4 | scenario "Add a new item", js: true do 5 | visit admin_users_path 6 | 7 | click_on "New item" 8 | fill_in "Name", with: "Test name" 9 | fill_in "Email", with: "Test email" 10 | click_on "Save" 11 | 12 | expect(page).to have_content("Test name") 13 | end 14 | 15 | scenario "Edit an item", js: true do 16 | User.create(name: "Name 1", email: "Email 1") 17 | visit admin_users_path 18 | 19 | find('.edit').click 20 | fill_in "Name", with: "Name 2" 21 | fill_in "Email", with: "Email 2" 22 | click_on "Save" 23 | 24 | expect(page).to have_content("Name 2") 25 | expect(page).to_not have_content("Name 1") 26 | end 27 | 28 | scenario "Delete an item", js: true do 29 | User.create(name: "Name 1", email: "Email 1") 30 | 31 | visit admin_users_path 32 | find('.destroy').click 33 | within('.confirmation_box') { click_on "Yes" } 34 | 35 | expect(page).to_not have_content("Name 1") 36 | end 37 | 38 | scenario "Use a custom action", js: true do 39 | User.create(name: "Name 1", email: "Email 1") 40 | 41 | visit admin_users_path 42 | find('.change_name').click 43 | 44 | expect(page).to have_content("Changed Name") 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /spec/features/view_items_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | feature 'View a list of items' do 4 | fixtures :users 5 | scenario 'The user navigate through users', js: true do 6 | 7 | visit root_path 8 | #page_sizes => [3, 10] 9 | expect(page).to have_content("Betty") 10 | expect(page).to_not have_content("Edward") 11 | 12 | within(".pagination") { click_on "2" } 13 | 14 | expect(page).to have_content("Edward") 15 | expect(page).to_not have_content("Betty") 16 | end 17 | 18 | scenario "The user sort users", js: true do 19 | 20 | visit sortable_users_path 21 | 22 | find('.name a').click 23 | expect(find(:xpath, "//table/tbody/tr[1]")).to have_content("Aaron") 24 | expect(find(:xpath, "//table/tbody/tr[2]")).to have_content("Betty") 25 | 26 | find('.name a').click 27 | expect(find(:xpath, "//table/tbody/tr[1]")).to have_content("Sara") 28 | expect(find(:xpath, "//table/tbody/tr[2]")).to have_content("Robin") 29 | end 30 | 31 | scenario "The user search user", js: true do 32 | visit admin_users_path 33 | 34 | fill_in "filter", with: "ja" 35 | 36 | expect(page).to have_content("Jane") 37 | expect(page).to_not have_content("Aaron") 38 | 39 | fill_in "filter", with: "ni" 40 | 41 | expect(page).to_not have_content("Nicholas") 42 | expect(page).to_not have_content("Jane") 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /spec/helpers/smart_listing/helper_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | require 'smart_listing/helper' 3 | 4 | module SmartListing::Helper 5 | class UsersController < ApplicationController 6 | include ControllerExtensions 7 | helper SmartListing::Helper 8 | 9 | attr_accessor :smart_listings 10 | 11 | def params 12 | { value: 'params' } 13 | end 14 | 15 | def cookies 16 | { value: 'cookies' } 17 | end 18 | 19 | def smart_listing_collection 20 | [1, 2] 21 | end 22 | end 23 | 24 | describe ControllerExtensions do 25 | describe "#smart_listing_create" do 26 | it "create a list with params and cookies" do 27 | controller = UsersController.new 28 | list = build_list 29 | 30 | expect(list).to receive(:setup).with(controller.params, 31 | controller.cookies) 32 | 33 | controller.smart_listing_create 34 | end 35 | 36 | it "assign a list in smart listings with the name" do 37 | controller = UsersController.new 38 | list = build_list 39 | 40 | controller.smart_listing_create 41 | 42 | expect(controller.smart_listings[:users]).to eq list 43 | end 44 | 45 | it 'return the collection of the list' do 46 | controller = UsersController.new 47 | collection1 = double 48 | collection2 = double 49 | build_list(collection: collection1) 50 | 51 | controller.smart_listing_create(collection: collection2) 52 | 53 | actual = controller.smart_listings[:users].collection 54 | expect(actual).to eq collection1 55 | end 56 | 57 | def build_list(collection: {}) 58 | double(collection: collection, setup: nil).tap do |list| 59 | allow(SmartListing::Base).to receive(:new).and_return(list) 60 | end 61 | end 62 | end 63 | 64 | describe '#smart_listing' do 65 | it 'give the list with name' do 66 | controller = UsersController.new 67 | list = double 68 | controller.smart_listings = { test: list } 69 | expect(controller.smart_listing(:test)).to eq list 70 | end 71 | end 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /spec/lib/smart_listing_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | module SmartListing 4 | describe Base do 5 | describe '#per_page' do 6 | 7 | context "when there is no specification in params or cookies" do 8 | it 'take first value in the page sizes' do 9 | options = { page_sizes: [1] } 10 | list = build_list(options: options) 11 | 12 | list.setup({}, {}) 13 | 14 | expect(list.per_page).to eq 1 15 | end 16 | end 17 | 18 | context 'when a value is in params' do 19 | context 'when the value is in the list of page_sizes' do 20 | it 'set the per_page as in the value' do 21 | options = { page_sizes: [1, 2] } 22 | list = build_list(options: options) 23 | 24 | list.setup({ "users_smart_listing" => { per_page: "2" } }, {}) 25 | 26 | expect(list.per_page).to eq 2 27 | end 28 | end 29 | 30 | context 'when the value is not in the list of page_sizes' do 31 | it 'take first value in the page sizes' do 32 | options = { page_sizes: [1, 2] } 33 | list = build_list(options: options) 34 | 35 | list.setup({ "users_smart_listing" => { per_page: "3" } }, {}) 36 | 37 | expect(list.per_page).to eq 1 38 | end 39 | end 40 | end 41 | 42 | context 'when a value is in cookies' do 43 | context 'when the memorization is enabled' do 44 | it 'set the value in the cookies' do 45 | options = { page_sizes: [1, 2], memorize_per_page: true } 46 | list = build_list(options: options) 47 | 48 | list.setup({}, { "users_smart_listing" => { per_page: "2" } }) 49 | 50 | expect(list.per_page).to eq 2 51 | end 52 | end 53 | 54 | context 'when the memorization is disabled' do 55 | it 'take first value in the page sizes' do 56 | options = { page_sizes: [1, 2], memorize_per_page: false } 57 | list = build_list(options: options) 58 | 59 | list.setup({}, { "users_smart_listing" => { per_page: "2" } }) 60 | 61 | expect(list.per_page).to eq 1 62 | end 63 | end 64 | end 65 | 66 | context 'when the per page value is at 0' do 67 | context 'when the unlimited per page option is enabled' do 68 | it 'set the per page at 0' do 69 | options = { page_sizes: [1, 2], unlimited_per_page: true } 70 | list = build_list(options: options) 71 | 72 | list.setup({ "users_smart_listing" => { per_page: "0" } }, {}) 73 | 74 | expect(list.per_page).to eq 0 75 | end 76 | end 77 | 78 | context 'when the unlimited per page option is disabled' do 79 | it 'take first value in the page sizes' do 80 | options = { page_sizes: [1, 2], unlimited_per_page: false } 81 | list = build_list(options: options) 82 | 83 | list.setup({}, {}) 84 | 85 | expect(list.per_page).to eq 1 86 | end 87 | end 88 | end 89 | 90 | context 'when the memorization of per page is enabled' do 91 | it 'save the perpage in the cookies' do 92 | options = { page_sizes: [1], memorize_per_page: true } 93 | list = build_list(options: options) 94 | cookies = {} 95 | 96 | list.setup({}, cookies) 97 | 98 | expect(cookies["users_smart_listing"][:per_page]).to eq 1 99 | end 100 | end 101 | end 102 | 103 | describe '#sort' do 104 | context 'with :implicit attributes' do 105 | context 'when there is a value in params' do 106 | it 'set sort with the given value' do 107 | list = build_list 108 | params = { "users_smart_listing" => { sort: { "name" => "asc" } } } 109 | 110 | list.setup(params, {}) 111 | 112 | expect(list.sort).to eq 'name' => 'asc' 113 | expect(list.collection.order_values).to match_array(['name asc']) 114 | end 115 | 116 | it 'set sort with the given value without direction' do 117 | list = build_list 118 | params = { 'users_smart_listing' => { sort: { 'name' => '' } } } 119 | 120 | list.setup(params, {}) 121 | 122 | expect(list.sort).to eq 'name' => '' 123 | expect(list.collection.order_values).to match_array(['name ']) 124 | end 125 | 126 | it 'does not set sort with the unknown given value' do 127 | list = build_list 128 | params = { 'users_smart_listing' => { sort: { 'login' => '' } } } 129 | 130 | list.setup(params, {}) 131 | 132 | expect(list.sort).to eq({}) 133 | expect(list.collection.order_values).to match_array([]) 134 | end 135 | 136 | it 'does not set sort with the given value with unknown direction' do 137 | list = build_list 138 | params = { 'users_smart_listing' => { sort: { 'name' => 'dasc' } } } 139 | 140 | list.setup(params, {}) 141 | 142 | expect(list.sort).to eq({}) 143 | expect(list.collection.order_values).to match_array([]) 144 | end 145 | end 146 | 147 | context 'when there is no value in params' do 148 | it 'take the value in options' do 149 | options = { default_sort: { 'email' => 'asc' } } 150 | list = build_list(options: options) 151 | 152 | list.setup({}, {}) 153 | 154 | expect(list.sort).to eq 'email' => 'asc' 155 | expect(list.collection.order_values).to match_array(['email asc']) 156 | end 157 | end 158 | end 159 | 160 | context 'with sort_attributes' do 161 | context 'when there is a value in params' do 162 | it 'set sort with the given value' do 163 | options = { sort_attributes: [[:username, 'users.name']] } 164 | list = build_list(options: options) 165 | params = { 'users_smart_listing' => { sort: { 'username' => 'asc' } } } 166 | 167 | list.setup(params, {}) 168 | 169 | expect(list.sort).to eq username: 'asc' 170 | expect(list.collection.order_values).to match_array(['users.name asc']) 171 | end 172 | 173 | it 'set sort with the given value without direction' do 174 | options = { sort_attributes: [[:username, 'users.name']] } 175 | list = build_list(options: options) 176 | params = { 'users_smart_listing' => { sort: { 'username' => '' } } } 177 | 178 | list.setup(params, {}) 179 | 180 | expect(list.sort).to eq username: '' 181 | expect(list.collection.order_values).to match_array(['users.name ']) 182 | end 183 | 184 | it 'does not set sort with the unknown given value' do 185 | options = { sort_attributes: [[:username, 'users.name']] } 186 | list = build_list(options: options) 187 | params = { 'users_smart_listing' => { sort: { 'login' => 'asc' } } } 188 | 189 | list.setup(params, {}) 190 | 191 | expect(list.sort).to eq({}) 192 | expect(list.collection.order_values).to match_array([]) 193 | end 194 | 195 | it 'does not set sort with the given value with unknown direction' do 196 | options = { sort_attributes: [[:username, 'users.name']] } 197 | list = build_list(options: options) 198 | params = { 'users_smart_listing' => { sort: { 'username' => 'dasc' } } } 199 | 200 | list.setup(params, {}) 201 | 202 | expect(list.sort).to eq({}) 203 | expect(list.collection.order_values).to match_array([]) 204 | end 205 | end 206 | 207 | context 'when there is no value in params' do 208 | it 'take the value in options' do 209 | options = { default_sort: { username: 'desc' }, sort_attributes: [[:username, 'users.name']] } 210 | list = build_list(options: options) 211 | 212 | list.setup({}, {}) 213 | 214 | expect(list.sort).to eq username: 'desc' 215 | expect(list.collection.order_values).to match_array(['users.name desc']) 216 | end 217 | end 218 | end 219 | end 220 | 221 | describe '#page' do 222 | context 'when the page is in the range' do 223 | it 'set the value with the given params' do 224 | User.create 225 | User.create 226 | options = { page_sizes: [1] } 227 | list = build_list(options: options) 228 | 229 | list.setup({ "users_smart_listing" => { page: 2 } }, {}) 230 | 231 | expect(list.page).to eq 2 232 | end 233 | end 234 | 235 | context 'when the page is out of range' do 236 | it 'set the value to the last page' do 237 | User.create 238 | User.create 239 | options = { page_sizes: [1] } 240 | list = build_list(options: options) 241 | 242 | list.setup({ "users_smart_listing" => { page: 3 } }, {}) 243 | 244 | expect(list.page).to eq 2 245 | end 246 | end 247 | end 248 | 249 | describe '#collection' do 250 | context 'when the collection is an array' do 251 | it 'sort the collection by the first attribute' do 252 | user1 = User.create(name: '1') 253 | user2 = User.create(name: '2') 254 | options = { array: true } 255 | list = build_list(options: options) 256 | 257 | params = { "users_smart_listing" => { sort: { "name" => "desc" } } } 258 | list.setup(params, {}) 259 | 260 | expect(list.collection.first).to eq user2 261 | expect(list.collection.last).to eq user1 262 | end 263 | 264 | it 'give only the given number per page' do 265 | user1 = User.create(name: '1') 266 | user2 = User.create(name: '2') 267 | options = { page_sizes: [1], array: true } 268 | list = build_list(options: options) 269 | 270 | list.setup({},{}) 271 | 272 | expect(list.collection).to include user1 273 | expect(list.collection).to_not include user2 274 | end 275 | 276 | it 'give the right page' do 277 | user1 = User.create(name: '1') 278 | user2 = User.create(name: '2') 279 | options = { page_sizes: [1], array: true } 280 | list = build_list(options: options) 281 | 282 | list.setup({ "users_smart_listing" => { page: 2 } }, {}) 283 | 284 | expect(list.collection).to include user2 285 | expect(list.collection).to_not include user1 286 | end 287 | end 288 | 289 | context 'when the collection is not an array' do 290 | it 'sort the collection by the given option' do 291 | user1 = User.create(name: '1') 292 | user2 = User.create(name: '2') 293 | options = { default_sort: { 'name' => 'desc' } } 294 | list = build_list(options: options) 295 | 296 | list.setup({},{}) 297 | 298 | expect(list.collection.first).to eq user2 299 | expect(list.collection.last).to eq user1 300 | end 301 | 302 | it 'give only the given number per page' do 303 | user1 = User.create(name: '1') 304 | user2 = User.create(name: '2') 305 | options = { page_sizes: [1] } 306 | list = build_list(options: options) 307 | 308 | list.setup({},{}) 309 | 310 | expect(list.collection).to include user1 311 | expect(list.collection).to_not include user2 312 | end 313 | 314 | it 'give the right page' do 315 | user1 = User.create(name: '1') 316 | user2 = User.create(name: '2') 317 | options = { page_sizes: [1] } 318 | list = build_list(options: options) 319 | 320 | list.setup({ "users_smart_listing" => { page: 2 } }, {}) 321 | 322 | expect(list.collection).to include user2 323 | expect(list.collection).to_not include user1 324 | end 325 | end 326 | end 327 | 328 | def build_list(options: {}) 329 | Base.new(:users, User.all, options) 330 | end 331 | end 332 | end 333 | -------------------------------------------------------------------------------- /spec/rails_helper.rb: -------------------------------------------------------------------------------- 1 | # This file is copied to spec/ when you run 'rails generate rspec:install' 2 | ENV["RAILS_ENV"] ||= 'test' 3 | require 'spec_helper' 4 | require File.expand_path("../dummy/config/environment", __FILE__) 5 | require 'rspec/rails' 6 | require 'database_cleaner' 7 | 8 | # Requires supporting ruby files with custom matchers and macros, etc, in 9 | # spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are 10 | # run as spec files by default. This means that files in spec/support that end 11 | # in _spec.rb will both be required and run as specs, causing the specs to be 12 | # run twice. It is recommended that you do not name files matching this glob to 13 | # end with _spec.rb. You can configure this pattern with the --pattern 14 | # option on the command line or in ~/.rspec, .rspec or `.rspec-local`. 15 | 16 | Dir[Rails.root.join("../support/**/*.rb")].each { |f| require f } 17 | 18 | # Checks for pending migrations before tests are run. 19 | # If you are not using ActiveRecord, you can remove this line. 20 | ActiveRecord::Migration.maintain_test_schema! 21 | 22 | RSpec.configure do |config| 23 | # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures 24 | config.fixture_path = "#{::Rails.root}/fixtures" 25 | 26 | # If you're not using ActiveRecord, or you'd prefer not to run each of your 27 | # examples within a transaction, remove the following line or assign false 28 | # instead of true. 29 | config.use_transactional_fixtures = false 30 | 31 | # RSpec Rails can automatically mix in different behaviours to your tests 32 | # based on their file location, for example enabling you to call `get` and 33 | # `post` in specs under `spec/controllers`. 34 | # 35 | # You can disable this behaviour by removing the line below, and instead 36 | # explicitly tag your specs with their type, e.g.: 37 | # 38 | # RSpec.describe UsersController, :type => :controller do 39 | # # ... 40 | # end 41 | # 42 | # The different available types are documented in the features, such as in 43 | # https://relishapp.com/rspec/rspec-rails/docs 44 | config.infer_spec_type_from_file_location! 45 | 46 | config.include WaitForAjax, type: :feature 47 | 48 | config.before :each do 49 | DatabaseCleaner.start 50 | end 51 | 52 | config.after do 53 | DatabaseCleaner.clean 54 | end 55 | end 56 | 57 | DatabaseCleaner.strategy = :truncation 58 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'capybara/rspec' 2 | require 'capybara-webkit' 3 | 4 | # This file was generated by the `rails generate rspec:install` command. Conventionally, all 5 | # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. 6 | # The generated `.rspec` file contains `--require spec_helper` which will cause this 7 | # file to always be loaded, without a need to explicitly require it in any files. 8 | # 9 | # Given that it is always loaded, you are encouraged to keep this file as 10 | # light-weight as possible. Requiring heavyweight dependencies from this file 11 | # will add to the boot time of your test suite on EVERY test run, even for an 12 | # individual file that may not need all of that loaded. Instead, make a 13 | # separate helper file that requires this one and then use it only in the specs 14 | # that actually need it. 15 | # 16 | # The `.rspec` file also contains a few flags that are not defaults but that 17 | # users commonly want. 18 | # 19 | # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration 20 | RSpec.configure do |config| 21 | # The settings below are suggested to provide a good initial experience 22 | # with RSpec, but feel free to customize to your heart's content. 23 | # These two settings work together to allow you to limit a spec run 24 | # to individual examples or groups you care about by tagging them with 25 | # `:focus` metadata. When nothing is tagged with `:focus`, all examples 26 | # get run. 27 | config.filter_run :focus 28 | config.run_all_when_everything_filtered = true 29 | 30 | # Many RSpec users commonly either run the entire suite or an individual 31 | # file, and it's useful to allow more verbose output when running an 32 | # individual spec file. 33 | if config.files_to_run.one? 34 | # Use the documentation formatter for detailed output, 35 | # unless a formatter has already been configured 36 | # (e.g. via a command-line flag). 37 | # config.default_formatter = 'doc' 38 | end 39 | 40 | # Print the 10 slowest examples and example groups at the 41 | # end of the spec run, to help surface which specs are running 42 | # particularly slow. 43 | # config.profile_examples = 10 44 | 45 | # Run specs in random order to surface order dependencies. If you find an 46 | # order dependency and want to debug it, you can fix the order by providing 47 | # the seed, which is printed after each run. 48 | # --seed 1234 49 | config.order = :random 50 | 51 | # Seed global randomization in this process using the `--seed` CLI option. 52 | # Setting this allows you to use `--seed` to deterministically reproduce 53 | # test failures related to randomization by passing the same `--seed` value 54 | # as the one that triggered the failure. 55 | Kernel.srand config.seed 56 | 57 | # rspec-expectations config goes here. You can use an alternate 58 | # assertion/expectation library such as wrong or the stdlib/minitest 59 | # assertions if you prefer. 60 | config.expect_with :rspec do |expectations| 61 | # Enable only the newer, non-monkey-patching expect syntax. 62 | # For more details, see: 63 | # - http://myronmars.to/n/dev-blog/2012/06/rspecs-new-expectation-syntax 64 | expectations.syntax = :expect 65 | end 66 | 67 | # rspec-mocks config goes here. You can use an alternate test double 68 | # library (such as bogus or mocha) by changing the `mock_with` option here. 69 | config.mock_with :rspec do |mocks| 70 | # Enable only the newer, non-monkey-patching expect syntax. 71 | # For more details, see: 72 | # - http://teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/ 73 | mocks.syntax = :expect 74 | 75 | # Prevents you from mocking or stubbing a method that does not exist on 76 | # a real object. This is generally recommended. 77 | mocks.verify_partial_doubles = true 78 | end 79 | end 80 | 81 | Capybara.javascript_driver = :webkit 82 | -------------------------------------------------------------------------------- /spec/support/capybara/wait_for_ajax.rb: -------------------------------------------------------------------------------- 1 | module WaitForAjax 2 | def wait_for_ajax 3 | Timeout.timeout(Capybara.default_max_wait_time) do 4 | loop until finished_all_ajax_requests? 5 | end 6 | end 7 | 8 | def finished_all_ajax_requests? 9 | page.evaluate_script('jQuery.active').zero? 10 | end 11 | end 12 | --------------------------------------------------------------------------------