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
|
@@ -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">×</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 = '';">×</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,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 %>
|