activeadmin-tom_select 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (196) hide show
  1. checksums.yaml +7 -0
  2. data/.actrc +20 -0
  3. data/.claude/commands/fix-tests.md +203 -0
  4. data/.github/workflows/ci.yml +174 -0
  5. data/.github/workflows/npm-publish.yml +50 -0
  6. data/.gitignore +35 -0
  7. data/.npmignore +58 -0
  8. data/.rspec +1 -0
  9. data/.rubocop.yml +75 -0
  10. data/.yardopts +2 -0
  11. data/AGENTS.md +39 -0
  12. data/Appraisals +9 -0
  13. data/CHANGELOG.md +64 -0
  14. data/CLAUDE.md +157 -0
  15. data/Gemfile +12 -0
  16. data/Gemfile.lock +368 -0
  17. data/LICENSE.txt +25 -0
  18. data/README.md +483 -0
  19. data/Rakefile +4 -0
  20. data/activeadmin-tom_select.gemspec +43 -0
  21. data/bin/rspec +17 -0
  22. data/config/database.yml +16 -0
  23. data/docs/activeadmin-4-detailed-reference.md +932 -0
  24. data/docs/activeadmin-4-gem-migration-guide.md +313 -0
  25. data/docs/combustion.md +213 -0
  26. data/docs/fail.png +0 -0
  27. data/docs/guide-update-your-app.md +283 -0
  28. data/docs/normal.png +0 -0
  29. data/docs/propshaft-readme.md +320 -0
  30. data/docs/propshaft-upgrade.md +484 -0
  31. data/docs/setup-activeadmin-app.md +552 -0
  32. data/docs/setup-activeadmin-gem.md +535 -0
  33. data/docs/tailwind/blog-page.md +341 -0
  34. data/docs/tailwind/upgrade-guide-enhanced.md +438 -0
  35. data/docs/tailwind/upgrade-guide.md +416 -0
  36. data/docs/tailwind-4/active_admin.rake +38 -0
  37. data/docs/tailwind-4/active_admin.tailwind.css +415 -0
  38. data/docs/tailwind-4/tailwind-active_admin.config.js +18 -0
  39. data/docs/test-app-change.md +154 -0
  40. data/docs/test-environment-fixes.md +58 -0
  41. data/docs/update-tom-select.md +184 -0
  42. data/docs/upload-system.md +225 -0
  43. data/gemfiles/rails_7.x_active_admin_4.x.gemfile +10 -0
  44. data/gemfiles/rails_7.x_active_admin_4.x.gemfile.lock +377 -0
  45. data/gemfiles/rails_8.x_active_admin_4.x.gemfile +10 -0
  46. data/gemfiles/rails_8.x_active_admin_4.x.gemfile.lock +372 -0
  47. data/lefthook.yml +17 -0
  48. data/lib/activeadmin/inputs/filters/searchable_select_input.rb +19 -0
  49. data/lib/activeadmin/inputs/searchable_select_input.rb +16 -0
  50. data/lib/activeadmin/tom_select/engine.rb +17 -0
  51. data/lib/activeadmin/tom_select/option_collection.rb +128 -0
  52. data/lib/activeadmin/tom_select/resource_dsl_extension.rb +56 -0
  53. data/lib/activeadmin/tom_select/resource_extension.rb +10 -0
  54. data/lib/activeadmin/tom_select/select_input_extension.rb +168 -0
  55. data/lib/activeadmin/tom_select/version.rb +5 -0
  56. data/lib/activeadmin/tom_select.rb +20 -0
  57. data/lib/activeadmin-tom_select.rb +5 -0
  58. data/lib/generators/active_admin/tom_select/install/install_generator.rb +180 -0
  59. data/npm-package/package-lock.json +51 -0
  60. data/npm-package/package.json +43 -0
  61. data/npm-package/src/index.js +153 -0
  62. data/npm-package/src/tom-select-tailwind.css +392 -0
  63. data/sonar-project.properties +25 -0
  64. data/spec/features/ajax_params_spec.rb +31 -0
  65. data/spec/features/asset_pipeline_diagnostic_spec.rb +155 -0
  66. data/spec/features/end_to_end_spec.rb +273 -0
  67. data/spec/features/filter_input_spec.rb +144 -0
  68. data/spec/features/form_input_spec.rb +122 -0
  69. data/spec/features/inline_ajax_setting_spec.rb +26 -0
  70. data/spec/features/input_errors_spec.rb +76 -0
  71. data/spec/features/input_html_options_spec.rb +30 -0
  72. data/spec/features/options_dsl_spec.rb +230 -0
  73. data/spec/features/production_build_spec.rb +108 -0
  74. data/spec/internal/.node-version +1 -0
  75. data/spec/internal/Gemfile +43 -0
  76. data/spec/internal/Gemfile.lock +333 -0
  77. data/spec/internal/Procfile.dev +3 -0
  78. data/spec/internal/README.md +24 -0
  79. data/spec/internal/Rakefile +6 -0
  80. data/spec/internal/app/admin/categories.rb +26 -0
  81. data/spec/internal/app/admin/dashboard.rb +29 -0
  82. data/spec/internal/app/admin/option_types.rb +19 -0
  83. data/spec/internal/app/admin/option_values.rb +30 -0
  84. data/spec/internal/app/admin/posts.rb +27 -0
  85. data/spec/internal/app/admin/products.rb +22 -0
  86. data/spec/internal/app/admin/rgb_colors.rb +25 -0
  87. data/spec/internal/app/admin/tag_names.rb +21 -0
  88. data/spec/internal/app/admin/test_ajax_params_category.rb +10 -0
  89. data/spec/internal/app/admin/test_ajax_params_post.rb +20 -0
  90. data/spec/internal/app/admin/test_form_post_class.rb +7 -0
  91. data/spec/internal/app/admin/test_form_post_custom.rb +11 -0
  92. data/spec/internal/app/admin/test_form_post_resource.rb +11 -0
  93. data/spec/internal/app/admin/test_form_post_resource_custom.rb +12 -0
  94. data/spec/internal/app/admin/test_inline_ajax_post.rb +9 -0
  95. data/spec/internal/app/admin/test_input_html_post.rb +11 -0
  96. data/spec/internal/app/admin/test_posts_display_text.rb +9 -0
  97. data/spec/internal/app/admin/test_posts_filter.rb +9 -0
  98. data/spec/internal/app/admin/test_posts_named.rb +9 -0
  99. data/spec/internal/app/admin/test_posts_pagination.rb +9 -0
  100. data/spec/internal/app/admin/test_posts_payload_lambda.rb +11 -0
  101. data/spec/internal/app/admin/test_posts_payload_proc.rb +9 -0
  102. data/spec/internal/app/admin/test_posts_scope_lambda.rb +8 -0
  103. data/spec/internal/app/admin/test_posts_scope_params.rb +8 -0
  104. data/spec/internal/app/admin/test_posts_scope_user.rb +8 -0
  105. data/spec/internal/app/admin/test_posts_text_attr.rb +5 -0
  106. data/spec/internal/app/admin/users.rb +23 -0
  107. data/spec/internal/app/admin/variants.rb +31 -0
  108. data/spec/internal/app/assets/config/manifest.js +2 -0
  109. data/spec/internal/app/assets/images/.keep +0 -0
  110. data/spec/internal/app/assets/stylesheets/active_admin.tailwind.css +16 -0
  111. data/spec/internal/app/assets/stylesheets/application.tailwind.css +15 -0
  112. data/spec/internal/app/controllers/application_controller.rb +9 -0
  113. data/spec/internal/app/controllers/concerns/.keep +0 -0
  114. data/spec/internal/app/helpers/application_helper.rb +2 -0
  115. data/spec/internal/app/javascript/active_admin.js +19 -0
  116. data/spec/internal/app/javascript/application.js +2 -0
  117. data/spec/internal/app/jobs/application_job.rb +7 -0
  118. data/spec/internal/app/mailers/application_mailer.rb +4 -0
  119. data/spec/internal/app/models/admin_user.rb +9 -0
  120. data/spec/internal/app/models/application_record.rb +3 -0
  121. data/spec/internal/app/models/article.rb +12 -0
  122. data/spec/internal/app/models/category.rb +12 -0
  123. data/spec/internal/app/models/color.rb +9 -0
  124. data/spec/internal/app/models/concerns/.keep +0 -0
  125. data/spec/internal/app/models/internal/tag_name.rb +14 -0
  126. data/spec/internal/app/models/internal_tag_name.rb +11 -0
  127. data/spec/internal/app/models/option_type.rb +12 -0
  128. data/spec/internal/app/models/option_value.rb +4 -0
  129. data/spec/internal/app/models/post.rb +15 -0
  130. data/spec/internal/app/models/product.rb +12 -0
  131. data/spec/internal/app/models/rgb_color.rb +16 -0
  132. data/spec/internal/app/models/tag.rb +12 -0
  133. data/spec/internal/app/models/tagging.rb +12 -0
  134. data/spec/internal/app/models/user.rb +12 -0
  135. data/spec/internal/app/models/variant.rb +12 -0
  136. data/spec/internal/app/views/layouts/application.html.erb +28 -0
  137. data/spec/internal/app/views/layouts/mailer.html.erb +13 -0
  138. data/spec/internal/app/views/layouts/mailer.text.erb +1 -0
  139. data/spec/internal/app/views/pwa/manifest.json.erb +22 -0
  140. data/spec/internal/app/views/pwa/service-worker.js +26 -0
  141. data/spec/internal/bin/bundle +117 -0
  142. data/spec/internal/bin/dev +11 -0
  143. data/spec/internal/bin/rackup +27 -0
  144. data/spec/internal/bin/rails +4 -0
  145. data/spec/internal/bin/rake +4 -0
  146. data/spec/internal/bin/setup +37 -0
  147. data/spec/internal/config/application.rb +50 -0
  148. data/spec/internal/config/boot.rb +3 -0
  149. data/spec/internal/config/credentials.yml.enc +1 -0
  150. data/spec/internal/config/database.yml +32 -0
  151. data/spec/internal/config/environment.rb +5 -0
  152. data/spec/internal/config/environments/development.rb +63 -0
  153. data/spec/internal/config/environments/production.rb +86 -0
  154. data/spec/internal/config/environments/test.rb +50 -0
  155. data/spec/internal/config/initializers/active_admin.rb +54 -0
  156. data/spec/internal/config/initializers/assets.rb +8 -0
  157. data/spec/internal/config/initializers/content_security_policy.rb +25 -0
  158. data/spec/internal/config/initializers/devise.rb +315 -0
  159. data/spec/internal/config/initializers/filter_parameter_logging.rb +8 -0
  160. data/spec/internal/config/initializers/inflections.rb +16 -0
  161. data/spec/internal/config/initializers/searchable_select.rb +6 -0
  162. data/spec/internal/config/locales/devise.en.yml +65 -0
  163. data/spec/internal/config/locales/en.yml +31 -0
  164. data/spec/internal/config/master.key +1 -0
  165. data/spec/internal/config/puma.rb +38 -0
  166. data/spec/internal/config/routes.rb +17 -0
  167. data/spec/internal/config.ru +6 -0
  168. data/spec/internal/db/schema.rb +174 -0
  169. data/spec/internal/db/seeds.rb +167 -0
  170. data/spec/internal/esbuild.config.js +34 -0
  171. data/spec/internal/lib/tasks/.keep +0 -0
  172. data/spec/internal/lib/tasks/active_admin.rake +55 -0
  173. data/spec/internal/log/.keep +0 -0
  174. data/spec/internal/package-lock.json +1954 -0
  175. data/spec/internal/package.json +21 -0
  176. data/spec/internal/public/400.html +114 -0
  177. data/spec/internal/public/404.html +114 -0
  178. data/spec/internal/public/406-unsupported-browser.html +114 -0
  179. data/spec/internal/public/422.html +114 -0
  180. data/spec/internal/public/500.html +114 -0
  181. data/spec/internal/public/icon.png +0 -0
  182. data/spec/internal/public/icon.svg +3 -0
  183. data/spec/internal/public/robots.txt +1 -0
  184. data/spec/internal/script/.keep +0 -0
  185. data/spec/internal/storage/.keep +0 -0
  186. data/spec/internal/tailwind.config.js +23 -0
  187. data/spec/internal/vendor/.keep +0 -0
  188. data/spec/internal/yarn.lock +824 -0
  189. data/spec/rails_helper.rb +62 -0
  190. data/spec/spec_helper.rb +138 -0
  191. data/spec/support/active_admin_helpers.rb +17 -0
  192. data/spec/support/capybara.rb +8 -0
  193. data/spec/support/models.rb +11 -0
  194. data/spec/support/pluck_polyfill.rb +12 -0
  195. data/spec/support/reset_settings.rb +5 -0
  196. metadata +497 -0
@@ -0,0 +1,168 @@
1
+ module ActiveAdmin
2
+ module SearchableSelect
3
+ # Mixin for searchable select inputs.
4
+ #
5
+ # Supports the same options as inputs of type `:select`.
6
+ #
7
+ # Adds support for an `ajax` option to fetch options data from a
8
+ # JSON endpoint. Pass either `true` to use defaults or a hash
9
+ # containing some of the following options:
10
+ #
11
+ # - `resource`: ActiveRecord model class of ActiveAdmin resource
12
+ # which provides the collection action to fetch options
13
+ # from. By default the resource is auto detected via the name
14
+ # of the input attribute.
15
+ #
16
+ # - `collection_name`: Name passed to the
17
+ # `searchable_select_options` method that defines the collection
18
+ # action to fetch options from.
19
+ #
20
+ # - `params`: Hash of query parameters that shall be passed to the
21
+ # options endpoint.
22
+ #
23
+ # - `path_params`: Hash of parameters, which would be passed to the
24
+ # dynamic collection path generation for the resource.
25
+ # e.g `admin_articles_path(path_params)`
26
+ #
27
+ # If the `ajax` option is present, the `collection` option is
28
+ # ignored.
29
+ module SelectInputExtension
30
+ # @api private
31
+ def to_html
32
+ super
33
+ rescue RuntimeError => e
34
+ # In development/test, display the error message
35
+ raise e unless Rails.env.development? || Rails.env.test?
36
+
37
+ template.content_tag(:div, e.message, class: 'searchable-select-error')
38
+ end
39
+
40
+ # @api private
41
+ def input_html_options
42
+ super.tap do |options|
43
+ options[:class] = [options[:class], 'searchable-select-input'].compact.join(' ')
44
+ options['data-ajax-url'] = ajax_url if ajax? && !SearchableSelect.inline_ajax_options
45
+ options['data-clearable'] = true if clearable?
46
+ end
47
+ end
48
+
49
+ # @api private
50
+ def collection_from_options
51
+ return super unless options[:ajax]
52
+
53
+ collection = if SearchableSelect.inline_ajax_options
54
+ all_options_collection
55
+ else
56
+ selected_value_collection
57
+ end
58
+
59
+ # Remove any empty/blank options since we use clear button instead
60
+ collection.reject { |item| item.first.to_s.strip.empty? && item.last.to_s.strip.empty? }
61
+ end
62
+
63
+ private
64
+
65
+ def ajax?
66
+ options[:ajax].present?
67
+ end
68
+
69
+ def clearable?
70
+ # Default to true unless explicitly set to false
71
+ options.fetch(:clearable, true)
72
+ end
73
+
74
+ def ajax_url
75
+ return unless options[:ajax]
76
+
77
+ [ajax_resource.route_collection_path(path_params),
78
+ '/',
79
+ option_collection.collection_action_name,
80
+ '?',
81
+ ajax_params.to_query].join
82
+ end
83
+
84
+ def all_options_collection
85
+ option_collection_scope.all.map do |record|
86
+ option_for_record(record)
87
+ end
88
+ end
89
+
90
+ def selected_value_collection
91
+ selected_records.collect { |s| option_for_record(s) }
92
+ end
93
+
94
+ def option_for_record(record)
95
+ [option_collection.display_text(record), record.id]
96
+ end
97
+
98
+ def selected_records
99
+ @selected_records ||=
100
+ if selected_values
101
+ option_collection_scope.where(id: selected_values)
102
+ else
103
+ []
104
+ end
105
+ end
106
+
107
+ def selected_values
108
+ @object&.send(input_name)
109
+ end
110
+
111
+ def option_collection_scope
112
+ option_collection.scope(template, path_params.merge(ajax_params))
113
+ end
114
+
115
+ def option_collection
116
+ ajax_resource
117
+ .searchable_select_option_collections
118
+ .fetch(ajax_option_collection_name) do
119
+ raise("No option collection named '#{ajax_option_collection_name}' " \
120
+ "defined in '#{ajax_resource_class.name}' admin.")
121
+ end
122
+ end
123
+
124
+ def ajax_resource
125
+ @ajax_resource ||=
126
+ template.active_admin_namespace.resource_for(ajax_resource_class) ||
127
+ raise("No admin found for '#{ajax_resource_class.name}' to fetch " \
128
+ 'options for searchable select input from.')
129
+ end
130
+
131
+ def ajax_resource_class
132
+ ajax_options.fetch(:resource) do
133
+ raise_cannot_auto_detect_resource unless reflection
134
+ reflection.klass
135
+ end
136
+ end
137
+
138
+ def raise_cannot_auto_detect_resource
139
+ raise('Cannot auto detect resource to fetch options for searchable select input from. ' \
140
+ "Explicitly pass class of an ActiveAdmin resource:\n\n " \
141
+ "f.input(:custom_category,\n " \
142
+ "type: :searchable_select,\n " \
143
+ "ajax: {\n " \
144
+ "resource: Category\n " \
145
+ "})\n")
146
+ end
147
+
148
+ def ajax_option_collection_name
149
+ ajax_options.fetch(:collection_name, :all)
150
+ end
151
+
152
+ def ajax_params
153
+ ajax_options.fetch(:params, {})
154
+ end
155
+
156
+ def path_params
157
+ ajax_options.fetch(:path_params, {})
158
+ end
159
+
160
+ def ajax_options
161
+ # ActiveAdmin 4 may transform ajax hash to boolean
162
+ return {} if options[:ajax] == true || options[:ajax].nil?
163
+
164
+ options[:ajax]
165
+ end
166
+ end
167
+ end
168
+ end
@@ -0,0 +1,5 @@
1
+ module ActiveAdmin
2
+ module SearchableSelect
3
+ VERSION = '4.1.0'.freeze
4
+ end
5
+ end
@@ -0,0 +1,20 @@
1
+ require 'activeadmin/tom_select/engine'
2
+ require 'activeadmin/tom_select/option_collection'
3
+ require 'activeadmin/tom_select/resource_extension'
4
+ require 'activeadmin/tom_select/resource_dsl_extension'
5
+ require 'activeadmin/tom_select/select_input_extension'
6
+ require 'activeadmin/tom_select/version'
7
+
8
+ ActiveAdmin::Resource.include ActiveAdmin::SearchableSelect::ResourceExtension
9
+ ActiveAdmin::ResourceDSL.include ActiveAdmin::SearchableSelect::ResourceDSLExtension
10
+
11
+ module ActiveAdmin
12
+ # Global settings for searchable selects
13
+ module SearchableSelect
14
+ # Statically render all options into searchable selects with
15
+ # `ajax` option set to true. This can be used to ease ui driven
16
+ # integration testing.
17
+ mattr_accessor :inline_ajax_options
18
+ self.inline_ajax_options = false
19
+ end
20
+ end
@@ -0,0 +1,5 @@
1
+ # Main entry point for activeadmin-tom_select gem
2
+ require 'activeadmin/tom_select'
3
+
4
+ require 'activeadmin/inputs/filters/searchable_select_input'
5
+ require 'activeadmin/inputs/searchable_select_input'
@@ -0,0 +1,180 @@
1
+ require 'rails/generators'
2
+
3
+ module ActiveAdmin
4
+ module TomSelect
5
+ module Generators
6
+ class InstallGenerator < Rails::Generators::Base
7
+ source_root File.expand_path('templates', __dir__)
8
+
9
+ desc 'Installs ActiveAdmin Tom Select for ActiveAdmin 4.x'
10
+
11
+ class_option :bundler,
12
+ type: :string,
13
+ default: 'esbuild',
14
+ desc: 'JavaScript bundler to use (esbuild, importmap, webpack)',
15
+ enum: %w[esbuild importmap webpack]
16
+
17
+ def install_npm_package
18
+ return unless options[:bundler] != 'importmap'
19
+
20
+ say 'Installing @rocket-sensei/activeadmin-tom_select npm package...', :green
21
+ run 'npm install @rocket-sensei/activeadmin-tom_select'
22
+ end
23
+
24
+ def setup_javascript
25
+ case options[:bundler]
26
+ when 'esbuild'
27
+ setup_esbuild
28
+ when 'importmap'
29
+ setup_importmap
30
+ when 'webpack'
31
+ setup_webpack
32
+ end
33
+ end
34
+
35
+ def setup_stylesheets
36
+ say 'Tom Select styles are now included in the package', :green
37
+ end
38
+
39
+ def show_post_install_message
40
+ say "\n✅ ActiveAdmin Tom Select has been installed!", :green
41
+
42
+ case options[:bundler]
43
+ when 'esbuild'
44
+ say "\nMake sure to rebuild your JavaScript:", :yellow
45
+ say ' npm run build', :cyan
46
+ say "\nFor development with watch mode:", :yellow
47
+ say ' npm run build -- --watch', :cyan
48
+ when 'importmap'
49
+ say "\nRestart your Rails server to load the new pins.", :yellow
50
+ when 'webpack'
51
+ say "\nRecompile your webpack bundles:", :yellow
52
+ say ' bin/webpack', :cyan
53
+ end
54
+
55
+ say "\n📚 Usage example:", :green
56
+ say <<~RUBY
57
+
58
+ # In your ActiveAdmin resource:
59
+ ActiveAdmin.register User do
60
+ searchable_select_options(
61
+ scope: User.all,
62
+ text_attribute: :name
63
+ )
64
+ end
65
+
66
+ ActiveAdmin.register Post do
67
+ form do |f|
68
+ f.inputs do
69
+ f.input :user, as: :searchable_select, ajax: true
70
+ end
71
+ f.actions
72
+ end
73
+ end
74
+ RUBY
75
+ end
76
+
77
+ PACKAGE_JSON_FILE = 'package.json'.freeze
78
+
79
+ private
80
+
81
+ def setup_esbuild
82
+ say 'Setting up for esbuild...', :green
83
+
84
+ # Check if app/javascript/active_admin.js exists
85
+ js_file = 'app/javascript/active_admin.js'
86
+
87
+ if File.exist?(js_file)
88
+ say "Adding tom_select to #{js_file}...", :green
89
+ append_to_file js_file do
90
+ <<~JS
91
+
92
+ // ActiveAdmin Tom Select
93
+ import '@rocket-sensei/activeadmin-tom_select';
94
+ JS
95
+ end
96
+ else
97
+ say "Creating #{js_file}...", :green
98
+ create_file js_file do
99
+ <<~JS
100
+ import "@activeadmin/activeadmin";
101
+
102
+ // ActiveAdmin Tom Select
103
+ import '@rocket-sensei/activeadmin-tom_select';
104
+ JS
105
+ end
106
+ end
107
+
108
+ # Update package.json scripts if needed
109
+ return unless File.exist?(PACKAGE_JSON_FILE)
110
+
111
+ package_json = JSON.parse(File.read(PACKAGE_JSON_FILE))
112
+
113
+ return if package_json['scripts'] && package_json['scripts']['build']
114
+
115
+ say 'Adding build script to package.json...', :green
116
+ package_json['scripts'] ||= {}
117
+ package_json['scripts']['build'] =
118
+ 'esbuild app/javascript/*.* --bundle --sourcemap --format=esm ' \
119
+ '--outdir=app/assets/builds --public-path=/assets'
120
+
121
+ File.write(PACKAGE_JSON_FILE, JSON.pretty_generate(package_json))
122
+ end
123
+
124
+ def setup_importmap
125
+ say 'Setting up for importmap...', :green
126
+
127
+ # Add pins to importmap.rb
128
+ if File.exist?('config/importmap.rb')
129
+ say 'Adding pins to config/importmap.rb...', :green
130
+ append_to_file 'config/importmap.rb' do
131
+ <<~RUBY
132
+
133
+ # ActiveAdmin Tom Select
134
+ pin "activeadmin-tom_select", to: "activeadmin-tom_select.js"
135
+ RUBY
136
+ end
137
+ end
138
+
139
+ # Copy the vendor JavaScript file
140
+ say 'Copying vendor JavaScript file...', :green
141
+ copy_file '../../../../vendor/assets/javascripts/activeadmin-tom_select.js',
142
+ 'app/assets/javascripts/activeadmin-tom_select.js'
143
+
144
+ # Update application.js
145
+ js_file = 'app/javascript/application.js'
146
+ return unless File.exist?(js_file)
147
+
148
+ say "Adding imports to #{js_file}...", :green
149
+ append_to_file js_file do
150
+ <<~JS
151
+
152
+ // ActiveAdmin Tom Select
153
+ import "activeadmin-tom_select"
154
+ JS
155
+ end
156
+ end
157
+
158
+ def setup_webpack
159
+ say 'Setting up for webpack...', :green
160
+
161
+ js_file = 'app/javascript/packs/active_admin.js'
162
+
163
+ if File.exist?(js_file)
164
+ say "Adding tom_select to #{js_file}...", :green
165
+ append_to_file js_file do
166
+ <<~JS
167
+
168
+ // ActiveAdmin Tom Select
169
+ import '@rocket-sensei/activeadmin-tom_select';
170
+ JS
171
+ end
172
+ else
173
+ say 'Please manually add the tom_select import ' \
174
+ 'to your ActiveAdmin JavaScript pack', :yellow
175
+ end
176
+ end
177
+ end
178
+ end
179
+ end
180
+ end
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@rocket-sensei/activeadmin-tom_select",
3
+ "version": "4.1.0",
4
+ "lockfileVersion": 3,
5
+ "requires": true,
6
+ "packages": {
7
+ "": {
8
+ "name": "@rocket-sensei/activeadmin-tom_select",
9
+ "version": "4.1.0",
10
+ "license": "MIT",
11
+ "peerDependencies": {
12
+ "tom-select": "^2.4.3"
13
+ }
14
+ },
15
+ "node_modules/@orchidjs/sifter": {
16
+ "version": "1.1.0",
17
+ "resolved": "https://registry.npmjs.org/@orchidjs/sifter/-/sifter-1.1.0.tgz",
18
+ "integrity": "sha512-mYwHCfr736cIWWdhhSZvDbf90AKt2xyrJspKFC3qyIJG1LtrJeJunYEqCGG4Aq2ijENbc4WkOjszcvNaIAS/pQ==",
19
+ "license": "Apache-2.0",
20
+ "peer": true,
21
+ "dependencies": {
22
+ "@orchidjs/unicode-variants": "^1.1.2"
23
+ }
24
+ },
25
+ "node_modules/@orchidjs/unicode-variants": {
26
+ "version": "1.1.2",
27
+ "resolved": "https://registry.npmjs.org/@orchidjs/unicode-variants/-/unicode-variants-1.1.2.tgz",
28
+ "integrity": "sha512-5DobW1CHgnBROOEpFlEXytED5OosEWESFvg/VYmH0143oXcijYTprRYJTs+55HzGM4IqxiLFSuqEzu9mPNwVsA==",
29
+ "license": "Apache-2.0",
30
+ "peer": true
31
+ },
32
+ "node_modules/tom-select": {
33
+ "version": "2.4.3",
34
+ "resolved": "https://registry.npmjs.org/tom-select/-/tom-select-2.4.3.tgz",
35
+ "integrity": "sha512-MFFrMxP1bpnAMPbdvPCZk0KwYxLqhYZso39torcdoefeV/NThNyDu8dV96/INJ5XQVTL3O55+GqQ78Pkj5oCfw==",
36
+ "license": "Apache-2.0",
37
+ "peer": true,
38
+ "dependencies": {
39
+ "@orchidjs/sifter": "^1.1.0",
40
+ "@orchidjs/unicode-variants": "^1.1.2"
41
+ },
42
+ "engines": {
43
+ "node": "*"
44
+ },
45
+ "funding": {
46
+ "type": "opencollective",
47
+ "url": "https://opencollective.com/tom-select"
48
+ }
49
+ }
50
+ }
51
+ }
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@rocket-sensei/activeadmin-tom_select",
3
+ "version": "4.1.0",
4
+ "description": "Use Tom Select for searchable selects in Active Admin forms and filters.",
5
+ "main": "src/index.js",
6
+ "module": "src/index.js",
7
+ "style": "src/tom-select-tailwind.css",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./src/index.js",
11
+ "require": "./src/index.js",
12
+ "default": "./src/index.js"
13
+ },
14
+ "./css": "./src/tom-select-tailwind.css",
15
+ "./style": "./src/tom-select-tailwind.css"
16
+ },
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/rs-pro/activeadmin-tom_select.git"
20
+ },
21
+ "author": "Rocket Sensei <info@rs.pro>",
22
+ "license": "MIT",
23
+ "private": false,
24
+ "bugs": {
25
+ "url": "https://github.com/rs-pro/activeadmin-tom_select/issues"
26
+ },
27
+ "homepage": "https://github.com/rs-pro/activeadmin-tom_select#readme",
28
+ "keywords": [
29
+ "tom-select",
30
+ "active",
31
+ "admin",
32
+ "searchable",
33
+ "select",
34
+ "activeadmin",
35
+ "rails"
36
+ ],
37
+ "peerDependencies": {
38
+ "tom-select": "^2.4.3"
39
+ },
40
+ "files": [
41
+ "src/**/*"
42
+ ]
43
+ }
@@ -0,0 +1,153 @@
1
+ // ES Module version for ActiveAdmin 4+ with esbuild/webpack
2
+ // Tom Select version - no jQuery dependency required
3
+ import TomSelect from 'tom-select';
4
+
5
+ const MODULE_NAME = 'ActiveAdmin Searchable Select';
6
+ const SELECTOR = '.searchable-select-input';
7
+ const INITIALIZED_CLASS = 'tom-select-initialized';
8
+
9
+ // Core initialization function
10
+ export function initSearchableSelects(inputs, extra) {
11
+ if (!inputs || inputs.length === 0) return;
12
+
13
+ // Handle both NodeList and array of elements
14
+ const elements = inputs instanceof NodeList ? Array.from(inputs) :
15
+ inputs.length !== undefined ? inputs : [inputs];
16
+
17
+ elements.forEach(element => {
18
+ // Skip if already initialized
19
+ if (element.classList.contains(INITIALIZED_CLASS)) return;
20
+
21
+ // Mark as initialized
22
+ element.classList.add(INITIALIZED_CLASS);
23
+
24
+ // Get options from data attributes
25
+ const dataOptions = element.dataset.searchableSelect ?
26
+ JSON.parse(element.dataset.searchableSelect) : {};
27
+
28
+ // Merge with extra options
29
+ const options = Object.assign({}, extra || {}, dataOptions);
30
+
31
+ // Configure AJAX if URL is provided
32
+ const ajaxUrl = element.dataset.ajaxUrl;
33
+ if (ajaxUrl) {
34
+ // Configure virtual scroll for pagination
35
+ options.plugins = options.plugins || [];
36
+ if (!options.plugins.includes('virtual_scroll')) {
37
+ options.plugins.push('virtual_scroll');
38
+ }
39
+
40
+ // Set max options for virtual scroll
41
+ options.maxOptions = options.maxOptions || 200;
42
+
43
+ // Configure the first URL for pagination
44
+ options.firstUrl = function(query) {
45
+ const url = new URL(ajaxUrl, window.location.href);
46
+ url.searchParams.set('term', query);
47
+ url.searchParams.set('page', 1);
48
+ return url.toString();
49
+ };
50
+
51
+ // Main load function with pagination support
52
+ options.load = function(query, callback) {
53
+ // Get the appropriate URL (either first or next)
54
+ const url = this.getUrl(query);
55
+
56
+ fetch(url)
57
+ .then(response => response.json())
58
+ .then(json => {
59
+ // Handle pagination info if present
60
+ if (json.pagination && json.pagination.more) {
61
+ // Set up the next URL for virtual scroll (1-based pagination)
62
+ const nextUrl = new URL(ajaxUrl, window.location.href);
63
+ nextUrl.searchParams.set('term', query);
64
+ // Backend now uses 1-based pagination and returns current page
65
+ const nextPage = (json.pagination.current || 1) + 1;
66
+ nextUrl.searchParams.set('page', nextPage);
67
+ this.setNextUrl(query, nextUrl.toString());
68
+ }
69
+
70
+ callback(json.results || json);
71
+ })
72
+ .catch(() => callback());
73
+ };
74
+
75
+ // Map Select2-style options to Tom Select
76
+ options.valueField = options.valueField || 'id';
77
+ options.labelField = options.labelField || 'text';
78
+ options.searchField = options.searchField || ['text'];
79
+
80
+ // Enable remote loading features
81
+ options.preload = options.preload !== false ? 'focus' : false;
82
+ options.loadThrottle = options.loadThrottle || 300;
83
+ }
84
+
85
+ // Handle placeholder
86
+ if (element.placeholder) {
87
+ options.placeholder = element.placeholder;
88
+ }
89
+
90
+ // Check if element should be clearable (default to true for searchable selects)
91
+ const isClearable = element.dataset.clearable !== 'false';
92
+
93
+ // Map common Select2 options to Tom Select equivalents
94
+ if (options.allowClear || isClearable) {
95
+ // Don't add empty option - we use clear_button plugin instead
96
+ // options.allowEmptyOption = true;
97
+
98
+ // Add clear_button plugin (make sure plugins array exists)
99
+ options.plugins = options.plugins || [];
100
+ if (!options.plugins.includes('clear_button')) {
101
+ options.plugins.push('clear_button');
102
+ }
103
+ }
104
+
105
+ if (options.minimumInputLength) {
106
+ options.shouldLoad = function(query) {
107
+ return query.length >= options.minimumInputLength;
108
+ };
109
+ }
110
+
111
+ // Initialize Tom Select
112
+ new TomSelect(element, options);
113
+ });
114
+ }
115
+
116
+ // Auto-initialize on common events
117
+ export function setupAutoInit() {
118
+ // Initialize on DOM ready
119
+ if (document.readyState === 'loading') {
120
+ document.addEventListener('DOMContentLoaded', function() {
121
+ initSearchableSelects(document.querySelectorAll(SELECTOR));
122
+ });
123
+ } else {
124
+ initSearchableSelects(document.querySelectorAll(SELECTOR));
125
+ }
126
+
127
+ // Support Turbo (Rails 7+)
128
+ document.addEventListener('turbo:load', function() {
129
+ initSearchableSelects(document.querySelectorAll(`${SELECTOR}:not(.${INITIALIZED_CLASS})`));
130
+ });
131
+
132
+ // ActiveAdmin 4 uses .has-many-add button click for dynamic content
133
+ document.addEventListener('click', function(event) {
134
+ if (event.target.closest('.has-many-add')) {
135
+ setTimeout(function() {
136
+ initSearchableSelects(
137
+ document.querySelectorAll(`${SELECTOR}:not(.${INITIALIZED_CLASS})`)
138
+ );
139
+ }, 10);
140
+ }
141
+ });
142
+
143
+ // Support has_many_add:after event (ActiveAdmin specific)
144
+ document.addEventListener('has_many_add:after', function(event) {
145
+ const fieldset = event.detail || event.target;
146
+ if (fieldset) {
147
+ const selects = fieldset.querySelectorAll(`${SELECTOR}:not(.${INITIALIZED_CLASS})`);
148
+ initSearchableSelects(selects);
149
+ }
150
+ });
151
+
152
+ console.log(`${MODULE_NAME} (Tom Select) initialized`);
153
+ }