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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +46 -0
- data/MIT-LICENSE +20 -0
- data/README.md +270 -0
- data/Rakefile +6 -0
- data/app/assets/javascripts/quick_admin/application.js +245 -0
- data/app/assets/javascripts/quick_admin/controllers/alert_controller.js +28 -0
- data/app/assets/javascripts/quick_admin/controllers/bulk_actions_controller.js +49 -0
- data/app/assets/javascripts/quick_admin/controllers/modal_controller.js +35 -0
- data/app/assets/javascripts/quick_admin/controllers/search_controller.js +26 -0
- data/app/assets/javascripts/quick_admin/controllers/text_expander_controller.js +22 -0
- data/app/assets/stylesheets/quick_admin/application.css +617 -0
- data/app/controllers/quick_admin/application_controller.rb +34 -0
- data/app/controllers/quick_admin/dashboard_controller.rb +20 -0
- data/app/controllers/quick_admin/resources_controller.rb +229 -0
- data/app/helpers/quick_admin/application_helper.rb +141 -0
- data/app/views/layouts/quick_admin/application.html.erb +41 -0
- data/app/views/quick_admin/dashboard/index.html.erb +36 -0
- data/app/views/quick_admin/resources/_form.html.erb +39 -0
- data/app/views/quick_admin/resources/_pagination.html.erb +29 -0
- data/app/views/quick_admin/resources/_resource_row.html.erb +32 -0
- data/app/views/quick_admin/resources/_resources_list.html.erb +58 -0
- data/app/views/quick_admin/resources/attachment.html.erb +21 -0
- data/app/views/quick_admin/resources/bulk_destroy.turbo_stream.erb +4 -0
- data/app/views/quick_admin/resources/create.turbo_stream.erb +9 -0
- data/app/views/quick_admin/resources/destroy.turbo_stream.erb +4 -0
- data/app/views/quick_admin/resources/edit.html.erb +14 -0
- data/app/views/quick_admin/resources/index.html.erb +38 -0
- data/app/views/quick_admin/resources/index.turbo_stream.erb +3 -0
- data/app/views/quick_admin/resources/new.html.erb +14 -0
- data/app/views/quick_admin/resources/show.html.erb +38 -0
- data/app/views/quick_admin/resources/update.turbo_stream.erb +9 -0
- data/config/routes.rb +14 -0
- data/lib/generators/quick_admin/install_generator.rb +37 -0
- data/lib/generators/quick_admin/templates/quick_admin.rb +43 -0
- data/lib/quick_admin/configuration.rb +59 -0
- data/lib/quick_admin/engine.rb +20 -0
- data/lib/quick_admin/resource.rb +102 -0
- data/lib/quick_admin/version.rb +3 -0
- data/lib/quick_admin.rb +70 -0
- 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
|
+
[](https://badge.fury.io/rb/quick_admin)
|
|
4
|
+
[](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,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
|
+
}
|