quick_admin 0.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 (41) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +46 -0
  3. data/MIT-LICENSE +20 -0
  4. data/README.md +270 -0
  5. data/Rakefile +6 -0
  6. data/app/assets/javascripts/quick_admin/application.js +245 -0
  7. data/app/assets/javascripts/quick_admin/controllers/alert_controller.js +28 -0
  8. data/app/assets/javascripts/quick_admin/controllers/bulk_actions_controller.js +49 -0
  9. data/app/assets/javascripts/quick_admin/controllers/modal_controller.js +35 -0
  10. data/app/assets/javascripts/quick_admin/controllers/search_controller.js +26 -0
  11. data/app/assets/javascripts/quick_admin/controllers/text_expander_controller.js +22 -0
  12. data/app/assets/stylesheets/quick_admin/application.css +617 -0
  13. data/app/controllers/quick_admin/application_controller.rb +34 -0
  14. data/app/controllers/quick_admin/dashboard_controller.rb +20 -0
  15. data/app/controllers/quick_admin/resources_controller.rb +229 -0
  16. data/app/helpers/quick_admin/application_helper.rb +141 -0
  17. data/app/views/layouts/quick_admin/application.html.erb +41 -0
  18. data/app/views/quick_admin/dashboard/index.html.erb +36 -0
  19. data/app/views/quick_admin/resources/_form.html.erb +39 -0
  20. data/app/views/quick_admin/resources/_pagination.html.erb +29 -0
  21. data/app/views/quick_admin/resources/_resource_row.html.erb +32 -0
  22. data/app/views/quick_admin/resources/_resources_list.html.erb +58 -0
  23. data/app/views/quick_admin/resources/attachment.html.erb +21 -0
  24. data/app/views/quick_admin/resources/bulk_destroy.turbo_stream.erb +4 -0
  25. data/app/views/quick_admin/resources/create.turbo_stream.erb +9 -0
  26. data/app/views/quick_admin/resources/destroy.turbo_stream.erb +4 -0
  27. data/app/views/quick_admin/resources/edit.html.erb +14 -0
  28. data/app/views/quick_admin/resources/index.html.erb +38 -0
  29. data/app/views/quick_admin/resources/index.turbo_stream.erb +3 -0
  30. data/app/views/quick_admin/resources/new.html.erb +14 -0
  31. data/app/views/quick_admin/resources/show.html.erb +38 -0
  32. data/app/views/quick_admin/resources/update.turbo_stream.erb +9 -0
  33. data/config/routes.rb +14 -0
  34. data/lib/generators/quick_admin/install_generator.rb +37 -0
  35. data/lib/generators/quick_admin/templates/quick_admin.rb +43 -0
  36. data/lib/quick_admin/configuration.rb +59 -0
  37. data/lib/quick_admin/engine.rb +20 -0
  38. data/lib/quick_admin/resource.rb +102 -0
  39. data/lib/quick_admin/version.rb +3 -0
  40. data/lib/quick_admin.rb +70 -0
  41. metadata +146 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 5af5176fddb574f8fc9b446503a342c2145b0321056bee52e3eec007d6e4f380
4
+ data.tar.gz: bb6312b10474f3ebd563331e32036a961e28c5b3234eada5d1de8b47f002b98c
5
+ SHA512:
6
+ metadata.gz: 6f6ae346359efd5216aceff8a414103827e105f61f566122410aee4dcbd1b85bae005f28f92c719ab39ec6ad0c4d2f9af61bf4889f179914b18b89f11fbaf6b3
7
+ data.tar.gz: 3c8a42162cc052453aca127e994e495ab2b0872a7838f5389357d10eb8080551ea121c0f88c5e1b27f182f9aeafa49f438f057d1077af5f5f37168e318cea1ae
data/CHANGELOG.md ADDED
@@ -0,0 +1,46 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.1.0] - 2025-01-03
9
+
10
+ ### Added
11
+ - Initial release of QuickAdmin gem
12
+ - Basic CRUD operations for Rails models
13
+ - Search functionality across specified fields
14
+ - Filtering by model attributes (boolean, date, datetime, text)
15
+ - Sorting by any column
16
+ - Bulk delete operations
17
+ - Turbo/Hotwire integration for seamless UX
18
+ - Active Storage support for file uploads
19
+ - Modal forms for create/edit operations
20
+ - Responsive design with mobile support
21
+ - Pagination support
22
+ - Configurable authentication (Devise, custom, or none)
23
+ - Configurable CSS framework support (Tailwind, Bootstrap, or built-in)
24
+ - Configurable text editor support (Trix, Lexical, or textarea)
25
+ - Resource configuration DSL
26
+ - Dashboard with resource counts
27
+ - Built-in vanilla JavaScript controllers for search, filters, bulk actions, and modals
28
+
29
+ ### Security
30
+ - CSRF protection via Rails default `protect_from_forgery`
31
+ - Parameterized SQL queries for search functionality
32
+ - Active Storage signed URLs for attachment access
33
+ - Input sanitization through Rails strong parameters
34
+
35
+ ## [Unreleased]
36
+
37
+ ### Changed
38
+ - Improved SQL injection protection in ORDER BY clause
39
+ - Enhanced error handling in attachment handling
40
+ - Better documentation and inline comments
41
+
42
+ ### Added
43
+ - Comprehensive usage guide
44
+ - Contributing guidelines
45
+ - Security policy
46
+ - Code of conduct
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2025 QuickAdmin
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,270 @@
1
+ # QuickAdmin
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/quick_admin.svg)](https://badge.fury.io/rb/quick_admin)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
+
6
+ A lightweight, minimal Rails admin interface gem with modern Turbo/Hotwire integration.
7
+
8
+ QuickAdmin provides a simple, no-hassle admin interface for your Rails applications without the complexity and overhead of heavier admin frameworks.
9
+
10
+ ## Features
11
+
12
+ - ✅ **Minimal Dependencies**: Only Rails required
13
+ - ✅ **Modern UX**: Built with Turbo and Stimulus
14
+ - ✅ **CRUD Operations**: Create, read, update, delete
15
+ - ✅ **Search & Filtering**: Real-time search and filtering
16
+ - ✅ **Mass Actions**: Bulk delete and operations
17
+ - ✅ **File Uploads**: Active Storage integration
18
+ - ✅ **Responsive Design**: Works on all devices
19
+ - ✅ **Configurable**: Optional Devise, Tailwind, Bootstrap, Trix support
20
+
21
+ ## Quick Start
22
+
23
+ ```bash
24
+ # Add to Gemfile
25
+ gem 'quick_admin'
26
+
27
+ # Install
28
+ bundle install
29
+ rails generate quick_admin:install
30
+
31
+ # Configure (config/initializers/quick_admin.rb)
32
+ QuickAdmin.resource :user do |r|
33
+ r.fields :name, :email, :created_at
34
+ r.searchable :name, :email
35
+ r.editable :name, :email
36
+ end
37
+
38
+ # Visit http://localhost:3000/admin
39
+ ```
40
+
41
+ ## Installation
42
+
43
+ Add this line to your application's Gemfile:
44
+
45
+ ```ruby
46
+ gem 'quick_admin'
47
+ ```
48
+
49
+ And then execute:
50
+
51
+ ```bash
52
+ $ bundle install
53
+ $ rails generate quick_admin:install
54
+ ```
55
+
56
+ ## Configuration
57
+
58
+ Configure QuickAdmin in `config/initializers/quick_admin.rb`:
59
+
60
+ ```ruby
61
+ QuickAdmin.configure do |config|
62
+ config.authentication = :devise # Optional: :devise, :custom, or nil
63
+ config.css_framework = :tailwind # Optional: :tailwind, :bootstrap, or :none
64
+ config.text_editor = :trix # Optional: :trix, :lexical, or :textarea
65
+ config.per_page = 25
66
+ config.mount_path = '/admin'
67
+ config.app_name = 'My Admin'
68
+ end
69
+ ```
70
+
71
+ ## Resource Configuration
72
+
73
+ Define your admin resources:
74
+
75
+ ```ruby
76
+ QuickAdmin.resource :user do |r|
77
+ r.fields :name, :email, :created_at, :updated_at
78
+ r.searchable :name, :email
79
+ r.filterable :created_at
80
+ r.editable :name, :email
81
+ r.name 'Users'
82
+ end
83
+
84
+ QuickAdmin.resource :post do |r|
85
+ r.fields :title, :content, :published, :author, :created_at
86
+ r.searchable :title, :content
87
+ r.filterable :published, :created_at
88
+ r.editable :title, :content, :published
89
+ end
90
+ ```
91
+
92
+ ## Field Types
93
+
94
+ QuickAdmin automatically detects and handles various field types:
95
+
96
+ - **Text**: String, text fields
97
+ - **Numbers**: Integer, decimal, float
98
+ - **Dates**: Date, datetime, timestamp
99
+ - **Booleans**: True/false values
100
+ - **Files**: Active Storage attachments
101
+ - **Rich Text**: With Trix integration (optional)
102
+
103
+ ## Authentication
104
+
105
+ ### No Authentication (Default)
106
+ ```ruby
107
+ config.authentication = nil
108
+ ```
109
+
110
+ ### With Devise
111
+ ```ruby
112
+ config.authentication = :devise
113
+ ```
114
+
115
+ ### Custom Authentication
116
+ ```ruby
117
+ config.authentication = :custom
118
+
119
+ # Override in your ApplicationController
120
+ class ApplicationController < ActionController::Base
121
+ private
122
+
123
+ def admin_authenticated?
124
+ current_user&.admin?
125
+ end
126
+ end
127
+ ```
128
+
129
+ ## Styling
130
+
131
+ ### Built-in Styles (Default)
132
+ ```ruby
133
+ config.css_framework = :none
134
+ ```
135
+
136
+ ### With Tailwind CSS
137
+ ```ruby
138
+ config.css_framework = :tailwind
139
+ ```
140
+
141
+ ### With Bootstrap
142
+ ```ruby
143
+ config.css_framework = :bootstrap
144
+ ```
145
+
146
+ ## Text Editors
147
+
148
+ ### Simple Textarea (Default)
149
+ ```ruby
150
+ config.text_editor = :textarea
151
+ ```
152
+
153
+ ### With Trix (Action Text)
154
+ ```ruby
155
+ config.text_editor = :trix
156
+ ```
157
+
158
+ ### With Lexical
159
+ ```ruby
160
+ config.text_editor = :lexical
161
+ ```
162
+
163
+ ## Usage
164
+
165
+ After configuration, visit your admin interface at the configured mount path (default: `/admin`).
166
+
167
+ ### Features Available:
168
+ - Browse all configured resources
169
+ - Create, edit, and delete records
170
+ - Search across specified fields
171
+ - Filter by configured fields
172
+ - Sort by any column
173
+ - Bulk delete operations
174
+ - File upload and preview
175
+ - Responsive modal forms
176
+
177
+ ## ⚠️ Security Warning
178
+
179
+ **By default, QuickAdmin does NOT require authentication.** This is intentional for development but **MUST be changed for production**.
180
+
181
+ ```ruby
182
+ # ❌ NEVER use this in production
183
+ QuickAdmin.configure do |config|
184
+ config.authentication = nil
185
+ end
186
+
187
+ # ✅ Always use authentication in production
188
+ QuickAdmin.configure do |config|
189
+ config.authentication = :devise # or :custom
190
+ end
191
+ ```
192
+
193
+ See [SECURITY.md](SECURITY.md) for detailed security guidelines.
194
+
195
+ ## Requirements
196
+
197
+ - Ruby >= 3.0.0
198
+ - Rails >= 7.0.0
199
+ - Turbo Rails >= 2.0
200
+
201
+ ## Documentation
202
+
203
+ - 📖 [Usage Guide](USAGE_GUIDE.md) - Comprehensive usage documentation
204
+ - 🔒 [Security Policy](SECURITY.md) - Security best practices
205
+ - 🤝 [Contributing](CONTRIBUTING.md) - How to contribute
206
+ - 📋 [Changelog](CHANGELOG.md) - Version history
207
+ - 📜 [Code of Conduct](CODE_OF_CONDUCT.md) - Community guidelines
208
+
209
+ ## Development
210
+
211
+ After checking out the repo, run:
212
+
213
+ ```bash
214
+ bundle install
215
+ bundle exec rspec
216
+
217
+ # Run the dummy app
218
+ cd spec/dummy
219
+ rails db:migrate
220
+ rails server
221
+ # Visit http://localhost:3000/admin
222
+ ```
223
+
224
+ ## Roadmap
225
+
226
+ - [ ] Advanced filtering options
227
+ - [ ] Export to CSV/Excel
228
+ - [ ] Custom actions per resource
229
+ - [ ] Batch update operations
230
+ - [ ] Activity logging/audit trail
231
+ - [ ] Dashboard customization
232
+ - [ ] Role-based permissions
233
+ - [ ] API endpoint generation
234
+
235
+ ## Contributing
236
+
237
+ We welcome contributions! Please read [CONTRIBUTING.md](CONTRIBUTING.md) for details on our code of conduct and the process for submitting pull requests.
238
+
239
+ ### Quick Contribution Guide
240
+
241
+ 1. Fork the repository
242
+ 2. Create your feature branch (`git checkout -b feature/amazing-feature`)
243
+ 3. Add tests for your changes
244
+ 4. Ensure all tests pass (`bundle exec rspec`)
245
+ 5. Commit your changes (`git commit -am 'Add amazing feature'`)
246
+ 6. Push to the branch (`git push origin feature/amazing-feature`)
247
+ 7. Open a Pull Request
248
+
249
+ ## Support
250
+
251
+ - 🐛 [Report a bug](https://github.com/yourusername/quick_admin/issues/new?labels=bug)
252
+ - 💡 [Request a feature](https://github.com/yourusername/quick_admin/issues/new?labels=enhancement)
253
+ - 💬 [Ask a question](https://github.com/yourusername/quick_admin/discussions)
254
+
255
+ ## Alternatives
256
+
257
+ QuickAdmin is designed to be minimal. If you need more features, consider:
258
+
259
+ - [ActiveAdmin](https://github.com/activeadmin/activeadmin) - Feature-rich admin framework
260
+ - [RailsAdmin](https://github.com/railsadminteam/rails_admin) - Traditional admin interface
261
+ - [Administrate](https://github.com/thoughtbot/administrate) - Thoughtbot's admin framework
262
+ - [Avo](https://avohq.io/) - Modern admin panel (commercial)
263
+
264
+ ## License
265
+
266
+ The gem is available as open source under the [MIT License](https://opensource.org/licenses/MIT).
267
+
268
+ ## Acknowledgments
269
+
270
+ Built with ❤️ by the Rails community. Special thanks to all contributors!
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task default: :spec
@@ -0,0 +1,245 @@
1
+ // QuickAdmin JavaScript Application
2
+
3
+ // Simple vanilla JS approach to avoid import issues
4
+ document.addEventListener('DOMContentLoaded', function() {
5
+
6
+ // Global confirmation handler for delete and dangerous actions
7
+ // Works without turbo-rails by reading data-turbo-confirm or data-confirm
8
+ document.addEventListener('click', function(e) {
9
+ const el = e.target.closest('[data-turbo-confirm],[data-confirm]');
10
+ if (!el) return;
11
+ const msg = el.getAttribute('data-turbo-confirm') || el.getAttribute('data-confirm');
12
+ if (msg && !window.confirm(msg)) {
13
+ e.preventDefault();
14
+ e.stopImmediatePropagation();
15
+ }
16
+ }, true);
17
+
18
+ // Turbo-specific click handler for anchors (covers data-turbo-method)
19
+ document.addEventListener('turbo:click', function(e) {
20
+ const link = e.target && e.target.closest && e.target.closest('a[data-turbo-confirm],a[data-confirm]');
21
+ if (!link) return;
22
+ const msg = link.getAttribute('data-turbo-confirm') || link.getAttribute('data-confirm');
23
+ if (msg && !window.confirm(msg)) {
24
+ e.preventDefault();
25
+ e.stopImmediatePropagation();
26
+ }
27
+ }, true);
28
+
29
+ // Final guard: confirmation right before Turbo sends the request
30
+ document.addEventListener('turbo:before-fetch-request', function(e) {
31
+ const el = e.target && (e.target.closest ? e.target.closest('[data-turbo-confirm],[data-confirm]') : null);
32
+ if (!el) return;
33
+ const msg = el.getAttribute('data-turbo-confirm') || el.getAttribute('data-confirm');
34
+ if (msg && !window.confirm(msg)) {
35
+ e.preventDefault();
36
+ }
37
+ }, true);
38
+
39
+ document.addEventListener('submit', function(e) {
40
+ const form = e.target;
41
+ if (!(form instanceof HTMLFormElement)) return;
42
+ // Prefer the exact submitter (button) the user clicked
43
+ const submitter = e.submitter || document.activeElement || form.querySelector('[type="submit"]');
44
+ let msg = null;
45
+ if (submitter) {
46
+ msg = submitter.getAttribute('data-turbo-confirm') || submitter.getAttribute('data-confirm');
47
+ }
48
+ if (!msg) {
49
+ msg = form.getAttribute('data-turbo-confirm') || form.getAttribute('data-confirm');
50
+ }
51
+ if (msg && !window.confirm(msg)) {
52
+ e.preventDefault();
53
+ e.stopImmediatePropagation();
54
+ }
55
+ }, true);
56
+
57
+ // Modal functionality
58
+ document.addEventListener('click', function(e) {
59
+ // Close modal when clicking overlay
60
+ if (e.target.classList.contains('modal-overlay')) {
61
+ closeModal();
62
+ }
63
+
64
+ // Close modal when clicking close button
65
+ if (e.target.classList.contains('modal-close') ||
66
+ e.target.hasAttribute('data-action') && e.target.getAttribute('data-action').includes('modal#close')) {
67
+ e.preventDefault();
68
+ closeModal();
69
+ }
70
+ });
71
+
72
+ // Close modal on Escape key
73
+ document.addEventListener('keydown', function(e) {
74
+ if (e.key === 'Escape') {
75
+ closeModal();
76
+ }
77
+ });
78
+
79
+ function closeModal() {
80
+ const modal = document.getElementById('modal');
81
+ if (modal) {
82
+ modal.innerHTML = '';
83
+ document.body.style.overflow = '';
84
+ }
85
+ }
86
+
87
+ // Search functionality with debouncing
88
+ function initializeSearch() {
89
+ const searchInputs = document.querySelectorAll('[data-search="input"]');
90
+ searchInputs.forEach(function(input) {
91
+ if (!input.hasAttribute('data-initialized')) {
92
+ let timeout;
93
+ input.addEventListener('input', function() {
94
+ clearTimeout(timeout);
95
+ timeout = setTimeout(function() {
96
+ input.closest('form').submit();
97
+ }, 300);
98
+ });
99
+ input.setAttribute('data-initialized', 'true');
100
+ }
101
+ });
102
+ }
103
+
104
+ // Filter functionality
105
+ function initializeFilters() {
106
+ const filterSelects = document.querySelectorAll('[data-filter="select"]');
107
+ filterSelects.forEach(function(select) {
108
+ if (!select.hasAttribute('data-initialized')) {
109
+ select.addEventListener('change', function() {
110
+ select.closest('form').submit();
111
+ });
112
+ select.setAttribute('data-initialized', 'true');
113
+ }
114
+ });
115
+ }
116
+
117
+ // Initialize search and filters on page load
118
+ initializeSearch();
119
+ initializeFilters();
120
+
121
+ // Bulk actions functionality
122
+ function initializeBulkActions() {
123
+ const bulkForms = document.querySelectorAll('[data-bulk-actions="form"]');
124
+ bulkForms.forEach(function(form) {
125
+ const toggleAll = form.querySelector('[data-bulk-actions="toggle-all"]');
126
+ const items = form.querySelectorAll('[data-bulk-actions="item"]');
127
+ const submitBtn = form.querySelector('[data-bulk-actions="submit"]');
128
+
129
+ // Remove existing event listeners to prevent duplicates
130
+ if (toggleAll && !toggleAll.hasAttribute('data-initialized')) {
131
+ toggleAll.addEventListener('change', function() {
132
+ const checked = toggleAll.checked;
133
+ items.forEach(function(item) {
134
+ item.checked = checked;
135
+ });
136
+ updateSubmitButton();
137
+ });
138
+ toggleAll.setAttribute('data-initialized', 'true');
139
+ }
140
+
141
+ items.forEach(function(item) {
142
+ if (!item.hasAttribute('data-initialized')) {
143
+ item.addEventListener('change', updateSubmitButton);
144
+ item.setAttribute('data-initialized', 'true');
145
+ }
146
+ });
147
+
148
+ function updateSubmitButton() {
149
+ const checkedItems = form.querySelectorAll('[data-bulk-actions="item"]:checked');
150
+ if (submitBtn) {
151
+ submitBtn.disabled = checkedItems.length === 0;
152
+ if (checkedItems.length > 0) {
153
+ submitBtn.textContent = `Delete Selected (${checkedItems.length})`;
154
+ } else {
155
+ submitBtn.textContent = "Delete Selected";
156
+ }
157
+ }
158
+ }
159
+ });
160
+ }
161
+
162
+ // Initialize bulk actions on page load
163
+ initializeBulkActions();
164
+
165
+ // Re-initialize all components after turbo updates
166
+ document.addEventListener('turbo:frame-load', function(event) {
167
+ console.log('Turbo frame loaded:', event.target.id);
168
+ initializeSearch();
169
+ initializeFilters();
170
+ initializeBulkActions();
171
+ initializeAlerts();
172
+ });
173
+
174
+ // Also reinitialize after turbo stream actions
175
+ document.addEventListener('turbo:before-stream-render', function(event) {
176
+ console.log('Turbo stream before render:', event.detail.newStream);
177
+ // Clear existing initializations so they can be re-added
178
+ document.querySelectorAll('[data-initialized]').forEach(function(element) {
179
+ element.removeAttribute('data-initialized');
180
+ });
181
+ });
182
+
183
+ // Additional event listeners for debugging and ensuring functionality
184
+ document.addEventListener('turbo:submit-start', function(event) {
185
+ console.log('Form submission started:', event.target);
186
+ });
187
+
188
+ document.addEventListener('turbo:submit-end', function(event) {
189
+ console.log('Form submission ended:', event.target, 'Success:', event.detail.success);
190
+ if (event.detail.success) {
191
+ // Reinitialize everything after successful form submission
192
+ setTimeout(function() {
193
+ initializeSearch();
194
+ initializeFilters();
195
+ initializeBulkActions();
196
+ initializeAlerts();
197
+ }, 100);
198
+ }
199
+ });
200
+
201
+ // Handle turbo stream rendering completion
202
+ document.addEventListener('turbo:before-stream-render', function() {
203
+ // Clear all flash messages before new ones are added
204
+ document.querySelectorAll('.flash-message').forEach(function(msg) {
205
+ msg.remove();
206
+ });
207
+ });
208
+
209
+ // Auto-dismiss alerts function
210
+ function initializeAlerts() {
211
+ const alerts = document.querySelectorAll('[data-auto-dismiss="true"]:not([data-alert-initialized])');
212
+ alerts.forEach(function(alert) {
213
+ alert.setAttribute('data-alert-initialized', 'true');
214
+ setTimeout(function() {
215
+ if (alert.parentNode) {
216
+ alert.style.opacity = '0';
217
+ alert.style.transform = 'translateY(-10px)';
218
+ setTimeout(function() {
219
+ if (alert.parentNode) {
220
+ alert.remove();
221
+ }
222
+ }, 300);
223
+ }
224
+ }, 3000);
225
+ });
226
+ }
227
+
228
+ // Initialize alerts on page load
229
+ initializeAlerts();
230
+
231
+ // Alert close buttons
232
+ document.addEventListener('click', function(e) {
233
+ if (e.target.classList.contains('alert-close')) {
234
+ const alert = e.target.closest('.alert');
235
+ if (alert) {
236
+ alert.style.opacity = '0';
237
+ alert.style.transform = 'translateY(-10px)';
238
+ setTimeout(function() {
239
+ alert.remove();
240
+ }, 300);
241
+ }
242
+ }
243
+ });
244
+
245
+ });
@@ -0,0 +1,28 @@
1
+ import { Controller } from "@hotwire/stimulus"
2
+
3
+ export default class extends Controller {
4
+ static values = { autoDismiss: Boolean }
5
+
6
+ connect() {
7
+ if (this.autoDismissValue) {
8
+ this.timeout = setTimeout(() => {
9
+ this.dismiss()
10
+ }, 5000)
11
+ }
12
+ }
13
+
14
+ disconnect() {
15
+ if (this.timeout) {
16
+ clearTimeout(this.timeout)
17
+ }
18
+ }
19
+
20
+ dismiss() {
21
+ this.element.style.opacity = "0"
22
+ this.element.style.transform = "translateY(-10px)"
23
+
24
+ setTimeout(() => {
25
+ this.element.remove()
26
+ }, 300)
27
+ }
28
+ }
@@ -0,0 +1,49 @@
1
+ import { Controller } from "@hotwire/stimulus"
2
+
3
+ export default class extends Controller {
4
+ static targets = ["item", "submitBtn", "toggleAll"]
5
+
6
+ connect() {
7
+ this.updateSubmitButton()
8
+ }
9
+
10
+ toggleAll(event) {
11
+ const checked = event.target.checked
12
+ this.itemTargets.forEach(item => {
13
+ item.checked = checked
14
+ })
15
+ this.updateSubmitButton()
16
+ }
17
+
18
+ toggleItem() {
19
+ this.updateSubmitButton()
20
+ this.updateToggleAll()
21
+ }
22
+
23
+ updateSubmitButton() {
24
+ const checkedItems = this.itemTargets.filter(item => item.checked)
25
+ this.submitBtnTarget.disabled = checkedItems.length === 0
26
+
27
+ if (checkedItems.length > 0) {
28
+ this.submitBtnTarget.textContent = `Delete Selected (${checkedItems.length})`
29
+ } else {
30
+ this.submitBtnTarget.textContent = "Delete Selected"
31
+ }
32
+ }
33
+
34
+ updateToggleAll() {
35
+ const checkedItems = this.itemTargets.filter(item => item.checked)
36
+ const toggleAll = this.toggleAllTarget
37
+
38
+ if (checkedItems.length === 0) {
39
+ toggleAll.checked = false
40
+ toggleAll.indeterminate = false
41
+ } else if (checkedItems.length === this.itemTargets.length) {
42
+ toggleAll.checked = true
43
+ toggleAll.indeterminate = false
44
+ } else {
45
+ toggleAll.checked = false
46
+ toggleAll.indeterminate = true
47
+ }
48
+ }
49
+ }