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
@@ -0,0 +1,20 @@
1
+ module QuickAdmin
2
+ class DashboardController < ApplicationController
3
+ def index
4
+ @resources_count = {}
5
+
6
+ QuickAdmin.resources.each do |name, resource|
7
+ begin
8
+ if resource.model_class
9
+ @resources_count[name] = resource.model_class.count
10
+ else
11
+ @resources_count[name] = 0
12
+ end
13
+ rescue => e
14
+ @resources_count[name] = 0
15
+ Rails.logger.warn "QuickAdmin: Could not count #{name}: #{e.message}"
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,229 @@
1
+ require 'ostruct'
2
+
3
+ module QuickAdmin
4
+ class ResourcesController < ApplicationController
5
+ before_action :set_resource_config
6
+ before_action :set_resource, only: %i[show edit update destroy attachment]
7
+
8
+ def index
9
+ @pagy, @resources = pagy(filtered_resources, items: QuickAdmin.config.per_page)
10
+
11
+ respond_to do |format|
12
+ format.html
13
+ if params[:search].present? || params[:filter].present?
14
+ format.turbo_stream
15
+ format.any { render :index, formats: :turbo_stream }
16
+ end
17
+ end
18
+ end
19
+
20
+ def attachment
21
+ field = params[:field]
22
+ attachment_ref = @resource.public_send(field) if @resource.respond_to?(field)
23
+ @blob = if params[:signed_id].present?
24
+ begin
25
+ ActiveStorage::Blob.find_signed(params[:signed_id])
26
+ rescue ActiveSupport::MessageVerifier::InvalidSignature
27
+ nil
28
+ end
29
+ else
30
+ attachment_ref&.blob
31
+ end
32
+
33
+ respond_to do |format|
34
+ format.html { render :attachment }
35
+ format.turbo_stream { render :attachment }
36
+ end
37
+ rescue => e
38
+ Rails.logger.error "Attachment error: #{e.message}"
39
+ @blob = nil
40
+ respond_to do |format|
41
+ format.html { render :attachment }
42
+ format.turbo_stream { render :attachment }
43
+ end
44
+ end
45
+
46
+ def show; end
47
+
48
+ def new
49
+ @resource = resource_class.new
50
+ end
51
+
52
+ def create
53
+ @resource = resource_class.new(resource_params)
54
+
55
+ if @resource.save
56
+ respond_to do |format|
57
+ format.html { redirect_to quick_admin.resources_path(resource_name), notice: 'Record created successfully.' }
58
+ format.turbo_stream { refresh_resources_list }
59
+ format.any { refresh_resources_list; render :create, formats: :turbo_stream }
60
+ end
61
+ else
62
+ respond_to do |format|
63
+ format.html { render :new }
64
+ format.turbo_stream { render :new }
65
+ end
66
+ end
67
+ end
68
+
69
+ def edit; end
70
+
71
+ def update
72
+ if @resource.update(resource_params)
73
+ respond_to do |format|
74
+ format.html { redirect_to quick_admin.resources_path(resource_name), notice: 'Record updated successfully.' }
75
+ format.turbo_stream { refresh_resources_list }
76
+ format.any { refresh_resources_list; render :update, formats: :turbo_stream }
77
+ end
78
+ else
79
+ respond_to do |format|
80
+ format.html { render :edit }
81
+ format.turbo_stream { render :edit }
82
+ end
83
+ end
84
+ end
85
+
86
+ def destroy
87
+ @resource.destroy
88
+ respond_to do |format|
89
+ format.html { redirect_to quick_admin.resources_path(resource_name), notice: 'Record deleted successfully.' }
90
+ format.turbo_stream { refresh_resources_list }
91
+ format.any { refresh_resources_list; render :destroy, formats: :turbo_stream }
92
+ end
93
+ end
94
+
95
+ def bulk_destroy
96
+ ids = params[:ids] || []
97
+ resource_class.where(id: ids).destroy_all
98
+
99
+ respond_to do |format|
100
+ format.html { redirect_to quick_admin.resources_path(resource_name), notice: "#{ids.count} records deleted successfully." }
101
+ format.turbo_stream { refresh_resources_list }
102
+ format.any { refresh_resources_list; render :bulk_destroy, formats: :turbo_stream }
103
+ end
104
+ end
105
+
106
+ private
107
+
108
+ def set_resource_config
109
+ @resource_config = QuickAdmin.resources[resource_name]
110
+ return if @resource_config&.model_class
111
+
112
+ redirect_to quick_admin.root_path, alert: "Resource '#{resource_name}' not found or model not available"
113
+ end
114
+
115
+ def set_resource
116
+ @resource = resource_class.find(params[:id])
117
+ end
118
+
119
+ def resource_name
120
+ params[:resource_name] || params[:id]
121
+ end
122
+
123
+ def resource_class
124
+ @resource_config.model_class
125
+ end
126
+
127
+ # Applies search, filter, and sort parameters to the resource collection.
128
+ # All user inputs are properly sanitized to prevent SQL injection.
129
+ #
130
+ # @return [ActiveRecord::Relation] filtered and sorted collection
131
+ def filtered_resources
132
+ resources = resource_class.all
133
+
134
+ # Apply search - uses parameterized queries to prevent SQL injection
135
+ if params[:search].present? && @resource_config.searchable_fields.any?
136
+ search_term = "%#{params[:search].to_s.downcase}%"
137
+ # Quote column names to prevent SQL injection
138
+ quote_col = resource_class.connection.method(:quote_column_name)
139
+ search_sql = @resource_config.searchable_fields.map do |field|
140
+ "LOWER(#{quote_col.call(field)}) LIKE :search"
141
+ end.join(' OR ')
142
+ resources = resources.where(search_sql, search: search_term)
143
+ end
144
+
145
+ # Apply filters
146
+ if params[:filter].present?
147
+ params[:filter].each do |field, value|
148
+ next if value.blank?
149
+
150
+ column = resource_class.columns_hash[field]
151
+ if column
152
+ case column.type
153
+ when :boolean
154
+ boolean_value = value == 'true'
155
+ resources = resources.where(field => boolean_value)
156
+ when :date, :datetime, :timestamp
157
+ begin
158
+ day = Date.parse(value)
159
+ rescue ArgumentError
160
+ day = nil
161
+ end
162
+ next unless day
163
+ if column.type == :date
164
+ resources = resources.where(field => day)
165
+ else
166
+ # filter by whole day for datetime/timestamp
167
+ resources = resources.where(field => day.beginning_of_day..day.end_of_day)
168
+ end
169
+ else
170
+ resources = resources.where(field => value)
171
+ end
172
+ else
173
+ resources = resources.where(field => value)
174
+ end
175
+ end
176
+ end
177
+
178
+ # Apply sorting
179
+ if params[:sort].present? && @resource_config.fields.include?(params[:sort])
180
+ direction = params[:direction] == 'desc' ? 'desc' : 'asc'
181
+ # Sanitize column name to prevent SQL injection
182
+ sort_column = resource_class.connection.quote_column_name(params[:sort])
183
+ resources = resources.order(Arel.sql("#{sort_column} #{direction}"))
184
+ else
185
+ resources = resources.order(:id)
186
+ end
187
+
188
+ resources
189
+ end
190
+
191
+ # Returns strong parameters for the resource.
192
+ # Only allows whitelisted fields specified in resource configuration.
193
+ # Falls back to all configured fields if no editable fields are specified.
194
+ #
195
+ # @return [ActionController::Parameters] permitted parameters
196
+ def resource_params
197
+ permitted_fields = @resource_config.editable_fields
198
+ permitted_fields = @resource_config.fields if permitted_fields.empty?
199
+
200
+ params.require(resource_name.singularize.to_sym).permit(permitted_fields)
201
+ end
202
+
203
+ def refresh_resources_list
204
+ @pagy, @resources = pagy(filtered_resources, items: QuickAdmin.config.per_page)
205
+ end
206
+
207
+ def pagy(collection, items:)
208
+ # Simple pagination implementation
209
+ page = [params.fetch(:page, 1).to_i, 1].max
210
+ offset = (page - 1) * items
211
+
212
+ total_count = collection.count
213
+ total_pages = (total_count.to_f / items).ceil
214
+
215
+ paginated_collection = collection.limit(items).offset(offset)
216
+
217
+ pagy_object = OpenStruct.new(
218
+ page: page,
219
+ items: items,
220
+ count: total_count,
221
+ pages: total_pages,
222
+ prev: page > 1 ? page - 1 : nil,
223
+ next: page < total_pages ? page + 1 : nil
224
+ )
225
+
226
+ [pagy_object, paginated_collection]
227
+ end
228
+ end
229
+ end
@@ -0,0 +1,141 @@
1
+ module QuickAdmin
2
+ module ApplicationHelper
3
+ def display_field_value(resource, field)
4
+ value = resource.send(field)
5
+
6
+ case value
7
+ when NilClass
8
+ content_tag(:span, "—", class: "null-value")
9
+ when TrueClass, FalseClass
10
+ content_tag(:span, value ? "Yes" : "No", class: "boolean-value #{value}")
11
+ when Time, DateTime
12
+ value.strftime("%Y-%m-%d %H:%M")
13
+ when Date
14
+ value.strftime("%Y-%m-%d")
15
+ when ActiveStorage::Attached::One
16
+ if value.attached?
17
+ blob = value.blob
18
+ if value.image?
19
+ thumb = image_tag(main_app.rails_blob_path(blob, only_path: true), class: "thumbnail", size: "50x50")
20
+ link_to(thumb, quick_admin.resource_attachment_path(resource_name, resource.id, field, blob.signed_id), data: { turbo_frame: "modal" })
21
+ else
22
+ link_to(blob.filename, main_app.rails_blob_path(blob, only_path: true), target: "_blank", class: "file-link")
23
+ end
24
+ else
25
+ content_tag(:span, "No file", class: "null-value")
26
+ end
27
+ when ActiveStorage::Attached::Many
28
+ if value.attached?
29
+ content_tag(:div, class: "attachments") do
30
+ value.map do |attachment|
31
+ blob = attachment.blob
32
+ if attachment.image?
33
+ thumb = image_tag(main_app.rails_blob_path(blob, only_path: true), class: "thumbnail", size: "50x50")
34
+ link_to(thumb, quick_admin.resource_attachment_path(resource_name, resource.id, field, blob.signed_id), data: { turbo_frame: "modal" })
35
+ else
36
+ link_to(blob.filename, main_app.rails_blob_path(blob, only_path: true), target: "_blank", class: "file-link")
37
+ end
38
+ end.join(" ").html_safe
39
+ end
40
+ else
41
+ content_tag(:span, "No files", class: "null-value")
42
+ end
43
+ when String
44
+ if value.length > 100
45
+ content_tag(:div, class: "truncated-text") do
46
+ content_tag(:span, truncate(value, length: 100)) +
47
+ content_tag(:button, "Show more",
48
+ class: "btn btn-link btn-sm",
49
+ data: { action: "click->text-expander#toggle" })
50
+ end
51
+ else
52
+ simple_format(value)
53
+ end
54
+ else
55
+ value.to_s
56
+ end
57
+ end
58
+
59
+ def render_form_field(form, resource, field)
60
+ column = resource.class.columns_hash[field]
61
+
62
+ case column&.type
63
+ when :boolean
64
+ content_tag(:div, class: "form-check") do
65
+ form.check_box(field, class: "form-check-input") +
66
+ form.label(field, field.humanize, class: "form-check-label")
67
+ end
68
+ when :text
69
+ if QuickAdmin.config.trix_enabled? && QuickAdmin.config.text_editor == :trix
70
+ form.rich_text_area(field, class: "form-control")
71
+ else
72
+ form.text_area(field, class: "form-control", rows: 4)
73
+ end
74
+ when :date
75
+ form.date_field(field, class: "form-control")
76
+ when :datetime, :timestamp
77
+ form.datetime_local_field(field, class: "form-control")
78
+ when :integer, :bigint
79
+ form.number_field(field, class: "form-control")
80
+ when :decimal, :float
81
+ form.number_field(field, step: :any, class: "form-control")
82
+ else
83
+ # Check if it's a file field (Active Storage)
84
+ if resource.class.reflect_on_attachment(field)
85
+ if resource.class.reflect_on_attachment(field).macro == :has_many_attached
86
+ form.file_field(field, multiple: true, class: "form-control", accept: file_accept_types)
87
+ else
88
+ form.file_field(field, class: "form-control", accept: file_accept_types)
89
+ end
90
+ else
91
+ form.text_field(field, class: "form-control")
92
+ end
93
+ end
94
+ end
95
+
96
+ def filter_options_for_field(model_class, field)
97
+ return [] unless model_class.respond_to?(:column_names)
98
+
99
+ column = model_class.columns_hash[field]
100
+ case column&.type
101
+ when :boolean
102
+ [['True', 'true'], ['False', 'false']]
103
+ when :string
104
+ model_class.distinct.pluck(field).compact.uniq.map { |v| [v.to_s, v.to_s] }
105
+ when :date, :datetime, :timestamp
106
+ dates = model_class.where.not(field => nil).order(field => :desc).limit(200).pluck(field)
107
+ days = dates.map { |d| d.to_date }.uniq.first(10)
108
+ days.map { |day| [day.strftime('%Y-%m-%d'), day.strftime('%Y-%m-%d')] }
109
+ else
110
+ values = model_class.distinct.pluck(field).compact.uniq
111
+ values.map { |v| [v.to_s, v.to_s] }
112
+ end
113
+ rescue
114
+ []
115
+ end
116
+
117
+ def sort_direction(field)
118
+ if params[:sort] == field && params[:direction] == 'asc'
119
+ 'desc'
120
+ else
121
+ 'asc'
122
+ end
123
+ end
124
+
125
+ def file_accept_types
126
+ "image/*,application/pdf,.doc,.docx,.txt"
127
+ end
128
+
129
+ def resource_name
130
+ params[:resource_name] || params[:id]
131
+ end
132
+
133
+ def current_resource_name
134
+ resource_name
135
+ end
136
+
137
+ def current_resource_config
138
+ @resource_config
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,41 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title><%= QuickAdmin.config.app_name %></title>
5
+ <meta name="viewport" content="width=device-width,initial-scale=1">
6
+ <%= csrf_meta_tags %>
7
+ <%= csp_meta_tag %>
8
+
9
+ <%= stylesheet_link_tag "quick_admin/application", "data-turbo-track": "reload" %>
10
+ <%= javascript_importmap_tags if respond_to?(:javascript_importmap_tags) %>
11
+ <%= javascript_include_tag "quick_admin/application", "data-turbo-track": "reload", defer: true %>
12
+ </head>
13
+
14
+ <body>
15
+ <div class="quick-admin-container">
16
+ <nav class="quick-admin-nav">
17
+ <div class="nav-header">
18
+ <%= link_to QuickAdmin.config.app_name, quick_admin.root_path, class: "nav-brand" %>
19
+ </div>
20
+
21
+ <ul class="nav-menu">
22
+ <li><%= link_to "Dashboard", quick_admin.root_path, class: "nav-link" %></li>
23
+ <% @quick_admin_resources.each do |name, resource| %>
24
+ <li><%= link_to resource.display_name, quick_admin.resources_path(name), class: "nav-link" %></li>
25
+ <% end %>
26
+ </ul>
27
+ </nav>
28
+
29
+ <main class="quick-admin-main">
30
+ <% flash.each do |type, message| %>
31
+ <div class="alert alert-<%= type == 'notice' ? 'success' : 'danger' %>" data-auto-dismiss="true">
32
+ <%= message %>
33
+ <button type="button" class="alert-close">&times;</button>
34
+ </div>
35
+ <% end %>
36
+
37
+ <%= yield %>
38
+ </main>
39
+ </div>
40
+ </body>
41
+ </html>
@@ -0,0 +1,36 @@
1
+ <div class="dashboard">
2
+ <h1>Dashboard</h1>
3
+
4
+ <div class="dashboard-stats">
5
+ <% @resources_count.each do |name, count| %>
6
+ <% resource = QuickAdmin.resources[name] %>
7
+ <div class="stat-card">
8
+ <h3><%= resource.display_name %></h3>
9
+ <div class="stat-number"><%= count %></div>
10
+ <div class="stat-actions">
11
+ <%= link_to "View All", quick_admin.resources_path(name), class: "btn btn-primary" %>
12
+ <%= link_to "Add New", quick_admin.new_resource_path(name), class: "btn btn-secondary" %>
13
+ </div>
14
+ </div>
15
+ <% end %>
16
+ </div>
17
+
18
+ <% if @resources_count.empty? %>
19
+ <div class="empty-state">
20
+ <h2>No Resources Configured</h2>
21
+ <p>Configure your admin resources in your Rails application to get started.</p>
22
+ <pre class="code-example">
23
+ # config/initializers/quick_admin.rb
24
+ QuickAdmin.configure do |config|
25
+ config.mount_path = '/admin'
26
+ end
27
+
28
+ QuickAdmin.resource :user do |r|
29
+ r.fields :name, :email, :created_at
30
+ r.searchable :name, :email
31
+ r.editable :name, :email
32
+ end
33
+ </pre>
34
+ </div>
35
+ <% end %>
36
+ </div>
@@ -0,0 +1,39 @@
1
+ <%
2
+ # Handle resource_name parameter
3
+ resource_name_param = defined?(resource_name) ? resource_name : @resource_config.model_name
4
+ %>
5
+
6
+ <%= form_with model: [QuickAdmin::Engine, resource_name_param.singularize.to_sym, resource],
7
+ url: resource.persisted? ? quick_admin.resource_path(resource_name_param, resource) : quick_admin.resources_path(resource_name_param),
8
+ method: resource.persisted? ? :patch : :post,
9
+ local: false,
10
+ class: "resource-form" do |form| %>
11
+
12
+ <% if resource.errors.any? %>
13
+ <div class="form-errors">
14
+ <h4><%= pluralize(resource.errors.count, "error") %> prohibited this record from being saved:</h4>
15
+ <ul>
16
+ <% resource.errors.full_messages.each do |message| %>
17
+ <li><%= message %></li>
18
+ <% end %>
19
+ </ul>
20
+ </div>
21
+ <% end %>
22
+
23
+ <% editable_fields = @resource_config.editable_fields.any? ? @resource_config.editable_fields : @resource_config.fields %>
24
+ <% editable_fields.each do |field| %>
25
+ <% column = resource.class.columns_hash[field] %>
26
+ <div class="form-group">
27
+ <% unless column&.type == :boolean %>
28
+ <%= form.label field, field.humanize, class: "form-label" %>
29
+ <% end %>
30
+ <%= render_form_field(form, resource, field) %>
31
+ </div>
32
+ <% end %>
33
+
34
+ <div class="form-actions">
35
+ <%= form.submit resource.persisted? ? "Update #{@resource_config.model_name.singularize.humanize}" : "Create #{@resource_config.model_name.singularize.humanize}",
36
+ class: "btn btn-primary" %>
37
+ <button type="button" class="btn btn-secondary" onclick="document.getElementById('modal').innerHTML = ''; document.body.style.overflow = '';">Cancel</button>
38
+ </div>
39
+ <% end %>
@@ -0,0 +1,29 @@
1
+ <div class="pagination-wrapper">
2
+ <div class="pagination-info">
3
+ Showing <%= (@pagy.page - 1) * @pagy.items + 1 %> to
4
+ <%= [@pagy.page * @pagy.items, @pagy.count].min %> of
5
+ <%= @pagy.count %> results
6
+ </div>
7
+
8
+ <% if @pagy.pages > 1 %>
9
+ <nav class="pagination-nav">
10
+ <% if @pagy.prev %>
11
+ <%= link_to "← Previous",
12
+ quick_admin.resources_path(resource_name, page: @pagy.prev, **request.query_parameters.except('page')),
13
+ class: "pagination-link",
14
+ data: { turbo_frame: "resources_list" } %>
15
+ <% end %>
16
+
17
+ <span class="pagination-current">
18
+ Page <%= @pagy.page %> of <%= @pagy.pages %>
19
+ </span>
20
+
21
+ <% if @pagy.next %>
22
+ <%= link_to "Next →",
23
+ quick_admin.resources_path(resource_name, page: @pagy.next, **request.query_parameters.except('page')),
24
+ class: "pagination-link",
25
+ data: { turbo_frame: "resources_list" } %>
26
+ <% end %>
27
+ </nav>
28
+ <% end %>
29
+ </div>
@@ -0,0 +1,32 @@
1
+ <%
2
+ # Handle resource_name parameter
3
+ resource_name_param = defined?(resource_name) ? resource_name : params[:resource_name]
4
+ %>
5
+
6
+ <tr class="resource-row" id="resource_<%= resource.id %>">
7
+ <td>
8
+ <input type="checkbox" name="ids[]" value="<%= resource.id %>"
9
+ data-bulk-actions="item">
10
+ </td>
11
+ <% @resource_config.fields.each do |field| %>
12
+ <td>
13
+ <%= display_field_value(resource, field) %>
14
+ </td>
15
+ <% end %>
16
+ <td class="actions">
17
+ <%= link_to "View", quick_admin.resource_path(resource_name_param, resource),
18
+ class: "btn btn-sm btn-info",
19
+ data: { turbo_frame: "modal" } %>
20
+ <%= link_to "Edit", quick_admin.edit_resource_path(resource_name_param, resource),
21
+ class: "btn btn-sm btn-warning",
22
+ data: { turbo_frame: "modal" } %>
23
+ <%= link_to "Delete", quick_admin.resource_path(resource_name_param, resource),
24
+ class: "btn btn-sm btn-danger",
25
+ data: {
26
+ turbo_method: :delete,
27
+ turbo_confirm: "Are you sure?",
28
+ confirm: "Are you sure?",
29
+ turbo_frame: "_top"
30
+ } %>
31
+ </td>
32
+ </tr>
@@ -0,0 +1,58 @@
1
+ <%
2
+ # Set local variables to instance variables if they're available
3
+ resource_config = @resource_config
4
+ resource_name_param = defined?(resource_name) ? resource_name : params[:resource_name]
5
+ %>
6
+
7
+ <div class="table-container">
8
+ <% if resources.any? %>
9
+ <%= form_with url: quick_admin.bulk_destroy_resources_path(resource_name_param),
10
+ method: :delete, local: false,
11
+ data: { bulk_actions: "form", turbo_confirm: "Are you sure?" },
12
+ class: "bulk-form" do |f| %>
13
+
14
+ <div class="bulk-actions">
15
+ <label class="bulk-select">
16
+ <input type="checkbox" data-bulk-actions="toggle-all">
17
+ Select All
18
+ </label>
19
+ <button type="submit" class="btn btn-danger" data-bulk-actions="submit" disabled>
20
+ Delete Selected
21
+ </button>
22
+ </div>
23
+
24
+ <table class="resources-table">
25
+ <thead>
26
+ <tr>
27
+ <th></th>
28
+ <% resource_config.fields.each do |field| %>
29
+ <th>
30
+ <%= link_to field.humanize,
31
+ quick_admin.resources_path(resource_name_param, sort: field, direction: sort_direction(field)),
32
+ data: { turbo_frame: "resources_list" },
33
+ class: "sortable #{'sorted' if params[:sort] == field}" %>
34
+ </th>
35
+ <% end %>
36
+ <th>Actions</th>
37
+ </tr>
38
+ </thead>
39
+ <tbody id="resources_tbody">
40
+ <% resources.each do |resource| %>
41
+ <%= render "resource_row", resource: resource, resource_name: resource_name_param %>
42
+ <% end %>
43
+ </tbody>
44
+ </table>
45
+ <% end %>
46
+
47
+ <div class="pagination">
48
+ <%= render "pagination", pagy: pagy %>
49
+ </div>
50
+ <% else %>
51
+ <div class="empty-state">
52
+ <p>No <%= resource_config.display_name.downcase %> found.</p>
53
+ <%= link_to "Add the first #{resource_config.model_name.singularize.humanize}",
54
+ quick_admin.new_resource_path(resource_name_param),
55
+ class: "btn btn-primary" %>
56
+ </div>
57
+ <% end %>
58
+ </div>
@@ -0,0 +1,21 @@
1
+ <turbo-frame id="modal">
2
+ <div class="modal-overlay" onclick="document.getElementById('modal').innerHTML = ''; document.body.style.overflow = '';">
3
+ <div class="modal-content" onclick="event.stopPropagation()">
4
+ <div class="modal-header">
5
+ <h2>Preview</h2>
6
+ <button type="button" class="modal-close" onclick="document.getElementById('modal').innerHTML = ''; document.body.style.overflow = '';">&times;</button>
7
+ </div>
8
+ <div class="modal-body">
9
+ <% if @blob&.image? %>
10
+ <%= image_tag main_app.rails_blob_path(@blob, only_path: true), alt: "Attachment preview", style: "width: 100%; height: auto;" %>
11
+ <% elsif @blob %>
12
+ <p>
13
+ <%= link_to @blob.filename, main_app.rails_blob_path(@blob, only_path: true), target: "_blank", class: "file-link" %>
14
+ </p>
15
+ <% else %>
16
+ <p class="null-value">No attachment found.</p>
17
+ <% end %>
18
+ </div>
19
+ </div>
20
+ </div>
21
+ </turbo-frame>
@@ -0,0 +1,4 @@
1
+ <%= turbo_stream.update "modal", "" %>
2
+ <%= turbo_stream.update "resources_list" do %>
3
+ <%= render "resources_list", resources: @resources, pagy: @pagy %>
4
+ <% end %>
@@ -0,0 +1,9 @@
1
+ <%= turbo_stream.update "modal", "" %>
2
+ <%= turbo_stream.update "resources_list" do %>
3
+ <%= render "resources_list", resources: @resources, pagy: @pagy %>
4
+ <% end %>
5
+ <%= turbo_stream.append "body" do %>
6
+ <div class="flash-message alert alert-success" style="position: fixed; top: 20px; right: 20px; z-index: 9999;" data-auto-dismiss="true">
7
+ Record created successfully!
8
+ </div>
9
+ <% end %>
@@ -0,0 +1,4 @@
1
+ <%= turbo_stream.update "modal", "" %>
2
+ <%= turbo_stream.update "resources_list" do %>
3
+ <%= render "resources_list", resources: @resources, pagy: @pagy %>
4
+ <% end %>