easy-admin-rails 0.1.15 → 0.2.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 +4 -4
- data/app/assets/builds/easy_admin.base.js +254 -18
- data/app/assets/builds/easy_admin.base.js.map +4 -4
- data/app/assets/builds/easy_admin.css +112 -18
- data/app/components/easy_admin/base_component.rb +1 -0
- data/app/components/easy_admin/form_tabs_component.rb +5 -2
- data/app/components/easy_admin/navbar_component.rb +5 -1
- data/app/components/easy_admin/permissions/user_role_assignment_component.rb +254 -0
- data/app/components/easy_admin/permissions/user_role_permissions_component.rb +186 -0
- data/app/components/easy_admin/resources/index_component.rb +1 -4
- data/app/components/easy_admin/sidebar_component.rb +67 -2
- data/app/controllers/easy_admin/application_controller.rb +131 -1
- data/app/controllers/easy_admin/batch_actions_controller.rb +27 -0
- data/app/controllers/easy_admin/concerns/belongs_to_editing.rb +201 -0
- data/app/controllers/easy_admin/concerns/inline_field_editing.rb +297 -0
- data/app/controllers/easy_admin/concerns/resource_authorization.rb +55 -0
- data/app/controllers/easy_admin/concerns/resource_filtering.rb +178 -0
- data/app/controllers/easy_admin/concerns/resource_loading.rb +149 -0
- data/app/controllers/easy_admin/concerns/resource_pagination.rb +135 -0
- data/app/controllers/easy_admin/dashboard_controller.rb +2 -1
- data/app/controllers/easy_admin/dashboards_controller.rb +6 -40
- data/app/controllers/easy_admin/resources_controller.rb +13 -762
- data/app/controllers/easy_admin/row_actions_controller.rb +25 -0
- data/app/helpers/easy_admin/fields_helper.rb +61 -9
- data/app/javascript/easy_admin/controllers/event_emitter_controller.js +2 -4
- data/app/javascript/easy_admin/controllers/infinite_scroll_controller.js +0 -10
- data/app/javascript/easy_admin/controllers/jsoneditor_controller.js +1 -4
- data/app/javascript/easy_admin/controllers/permission_toggle_controller.js +227 -0
- data/app/javascript/easy_admin/controllers/role_preview_controller.js +93 -0
- data/app/javascript/easy_admin/controllers/select_field_controller.js +1 -2
- data/app/javascript/easy_admin/controllers/settings_button_controller.js +1 -2
- data/app/javascript/easy_admin/controllers/settings_sidebar_controller.js +1 -4
- data/app/javascript/easy_admin/controllers/turbo_stream_redirect.js +0 -2
- data/app/javascript/easy_admin/controllers.js +5 -1
- data/app/models/easy_admin/admin_user.rb +6 -0
- data/app/policies/admin_user_policy.rb +36 -0
- data/app/policies/application_policy.rb +83 -0
- data/app/views/easy_admin/application/authorization_failure.turbo_stream.erb +8 -0
- data/app/views/easy_admin/dashboards/card.html.erb +5 -0
- data/app/views/easy_admin/dashboards/card.turbo_stream.erb +7 -0
- data/app/views/easy_admin/dashboards/card_error.html.erb +3 -0
- data/app/views/easy_admin/dashboards/card_error.turbo_stream.erb +5 -0
- data/app/views/easy_admin/dashboards/show.turbo_stream.erb +7 -0
- data/app/views/easy_admin/resources/belongs_to_edit_attached.html.erb +6 -0
- data/app/views/easy_admin/resources/belongs_to_edit_attached.turbo_stream.erb +8 -0
- data/app/views/easy_admin/resources/belongs_to_reattach.html.erb +5 -0
- data/app/views/easy_admin/resources/edit.html.erb +1 -1
- data/app/views/easy_admin/resources/edit_field.html.erb +5 -0
- data/app/views/easy_admin/resources/edit_field.turbo_stream.erb +7 -0
- data/app/views/easy_admin/resources/index.html.erb +1 -1
- data/app/views/easy_admin/resources/index_frame.html.erb +8 -142
- data/app/views/easy_admin/resources/update_belongs_to_attached.turbo_stream.erb +25 -0
- data/app/views/layouts/easy_admin/application.html.erb +15 -2
- data/config/initializers/easy_admin_permissions.rb +73 -0
- data/db/seeds/easy_admin_permissions.rb +121 -0
- data/lib/easy-admin-rails.rb +2 -0
- data/lib/easy_admin/permissions/component.rb +168 -0
- data/lib/easy_admin/permissions/configuration.rb +37 -0
- data/lib/easy_admin/permissions/controller.rb +164 -0
- data/lib/easy_admin/permissions/dsl.rb +180 -0
- data/lib/easy_admin/permissions/models.rb +44 -0
- data/lib/easy_admin/permissions/permission_denied_component.rb +121 -0
- data/lib/easy_admin/permissions/resource_permissions.rb +231 -0
- data/lib/easy_admin/permissions/role_definition.rb +45 -0
- data/lib/easy_admin/permissions/role_denied_component.rb +159 -0
- data/lib/easy_admin/permissions/role_dsl.rb +73 -0
- data/lib/easy_admin/permissions/user_extensions.rb +129 -0
- data/lib/easy_admin/permissions.rb +113 -0
- data/lib/easy_admin/resource/base.rb +119 -0
- data/lib/easy_admin/resource/configuration.rb +148 -0
- data/lib/easy_admin/resource/dsl.rb +117 -0
- data/lib/easy_admin/resource/field_registry.rb +189 -0
- data/lib/easy_admin/resource/form_builder.rb +123 -0
- data/lib/easy_admin/resource/layout_builder.rb +249 -0
- data/lib/easy_admin/resource/scope_manager.rb +252 -0
- data/lib/easy_admin/resource/show_builder.rb +359 -0
- data/lib/easy_admin/resource.rb +8 -835
- data/lib/easy_admin/resource_modules.rb +11 -0
- data/lib/easy_admin/version.rb +1 -1
- data/lib/generators/easy_admin/permissions/install_generator.rb +90 -0
- data/lib/generators/easy_admin/permissions/templates/initializers/permissions.rb +37 -0
- data/lib/generators/easy_admin/permissions/templates/migrations/create_permission_tables.rb +27 -0
- data/lib/generators/easy_admin/permissions/templates/migrations/update_users_for_permissions.rb +6 -0
- data/lib/generators/easy_admin/permissions/templates/models/permission.rb +9 -0
- data/lib/generators/easy_admin/permissions/templates/models/role.rb +9 -0
- data/lib/generators/easy_admin/permissions/templates/models/role_permission.rb +9 -0
- data/lib/generators/easy_admin/permissions/templates/models/user_role.rb +9 -0
- data/lib/generators/easy_admin/permissions/templates/policies/application_policy.rb +47 -0
- data/lib/generators/easy_admin/permissions/templates/policies/user_policy.rb +36 -0
- data/lib/generators/easy_admin/permissions/templates/seeds/permissions.rb +89 -0
- metadata +62 -5
- data/db/migrate/20250101000001_create_easy_admin_admin_users.rb +0 -45
@@ -0,0 +1,297 @@
|
|
1
|
+
module EasyAdmin
|
2
|
+
module Concerns
|
3
|
+
# InlineFieldEditing concern handles inline field editing functionality
|
4
|
+
# Provides methods for editing individual fields without full form submission
|
5
|
+
module InlineFieldEditing
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
# Field editing actions
|
9
|
+
def edit_field
|
10
|
+
@field_name = params[:field]
|
11
|
+
@field_config = find_field_config_by_name_or_association(@field_name)
|
12
|
+
|
13
|
+
unless @field_config
|
14
|
+
render json: { error: "Field '#{@field_name}' not found" }, status: :not_found
|
15
|
+
return
|
16
|
+
end
|
17
|
+
|
18
|
+
respond_to do |format|
|
19
|
+
format.html { render template: 'easy_admin/resources/edit_field', layout: false }
|
20
|
+
format.turbo_stream { render template: 'easy_admin/resources/edit_field' }
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def update_field
|
25
|
+
@field_name = params[:field]
|
26
|
+
@field_config = find_field_config_by_name_or_association(@field_name)
|
27
|
+
|
28
|
+
unless @field_config
|
29
|
+
render json: { error: "Field '#{@field_name}' not found" }, status: :not_found
|
30
|
+
return
|
31
|
+
end
|
32
|
+
|
33
|
+
# Determine the correct parameter key (handle complex model names)
|
34
|
+
param_key = determine_param_key_for_update_field
|
35
|
+
|
36
|
+
# For belongs_to fields, we need to handle foreign key updates
|
37
|
+
if @field_config[:type] == :belongs_to
|
38
|
+
update_attrs = build_belongs_to_update_attributes(param_key)
|
39
|
+
else
|
40
|
+
update_attrs = build_regular_field_update_attributes(param_key)
|
41
|
+
end
|
42
|
+
|
43
|
+
if @record.update(update_attrs)
|
44
|
+
handle_successful_field_update
|
45
|
+
else
|
46
|
+
handle_failed_field_update
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Get autocomplete suggestions for fields
|
51
|
+
def suggest
|
52
|
+
# Try to find field by name first, then by association name
|
53
|
+
field_config = @resource_class.fields_config.find { |f| f[:name].to_s == params[:field] }
|
54
|
+
|
55
|
+
# If not found by field name, try to find by association name
|
56
|
+
if !field_config
|
57
|
+
field_config = @resource_class.fields_config.find { |f| f[:association].to_s == params[:field] }
|
58
|
+
end
|
59
|
+
|
60
|
+
Rails.logger.debug "Suggest field_config for #{params[:field]}: #{field_config}"
|
61
|
+
|
62
|
+
unless field_config && field_config[:suggest]
|
63
|
+
render json: { error: "Field not found or suggest not configured" }, status: :not_found
|
64
|
+
return
|
65
|
+
end
|
66
|
+
|
67
|
+
search_term = params[:q].to_s.strip
|
68
|
+
limit = field_config[:suggest][:limit] || 10
|
69
|
+
|
70
|
+
results = get_suggest_options(field_config, search_term, limit)
|
71
|
+
|
72
|
+
render json: { options: results }
|
73
|
+
end
|
74
|
+
|
75
|
+
private
|
76
|
+
|
77
|
+
# Helper method to find field config by name or association name
|
78
|
+
def find_field_config_by_name_or_association(field_identifier)
|
79
|
+
# Try to find field by name first
|
80
|
+
field_config = find_field_config(field_identifier)
|
81
|
+
|
82
|
+
# If not found by field name, try to find by association name
|
83
|
+
if !field_config
|
84
|
+
field_config = @resource_class.fields_config.find { |f| f[:association].to_s == field_identifier }
|
85
|
+
end
|
86
|
+
|
87
|
+
field_config
|
88
|
+
end
|
89
|
+
|
90
|
+
# Build update attributes for belongs_to fields
|
91
|
+
def build_belongs_to_update_attributes(param_key)
|
92
|
+
association_name = @field_config[:name]
|
93
|
+
foreign_key = get_foreign_key_name(association_name)
|
94
|
+
field_value = params.dig(param_key, foreign_key)
|
95
|
+
{ foreign_key.to_sym => field_value }
|
96
|
+
end
|
97
|
+
|
98
|
+
# Build update attributes for regular fields
|
99
|
+
def build_regular_field_update_attributes(param_key)
|
100
|
+
# Create a field object to handle normalization
|
101
|
+
field_obj = EasyAdmin::Field.new(@field_name, @field_config[:type], @field_config)
|
102
|
+
|
103
|
+
# Get the field value from params
|
104
|
+
field_value = params.dig(param_key, @field_name)
|
105
|
+
|
106
|
+
# Normalize the input
|
107
|
+
update_attrs = { @field_name => field_value }
|
108
|
+
field_obj.normalize_input!(update_attrs)
|
109
|
+
update_attrs
|
110
|
+
end
|
111
|
+
|
112
|
+
# Handle successful field update response
|
113
|
+
def handle_successful_field_update
|
114
|
+
respond_to do |format|
|
115
|
+
format.turbo_stream do
|
116
|
+
# Update the table cell with the new value
|
117
|
+
render turbo_stream: [
|
118
|
+
turbo_stream.replace(
|
119
|
+
"#{helpers.dom_id(@record)}_#{@field_config[:name]}",
|
120
|
+
EasyAdmin::Resources::TableCellComponent.new(
|
121
|
+
record: @record.reload,
|
122
|
+
field_config: @field_config,
|
123
|
+
resource_class: @resource_class
|
124
|
+
).call
|
125
|
+
),
|
126
|
+
turbo_stream.update("notifications",
|
127
|
+
EasyAdmin::NotificationComponent.new(
|
128
|
+
type: :success,
|
129
|
+
message: "#{@field_config[:label]} updated successfully!",
|
130
|
+
title: "Success"
|
131
|
+
).call
|
132
|
+
)
|
133
|
+
]
|
134
|
+
end
|
135
|
+
format.json { render json: { status: 'success' } }
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
# Handle failed field update response
|
140
|
+
def handle_failed_field_update
|
141
|
+
respond_to do |format|
|
142
|
+
format.turbo_stream do
|
143
|
+
render turbo_stream: turbo_stream.update("notifications",
|
144
|
+
EasyAdmin::NotificationComponent.new(
|
145
|
+
type: :error,
|
146
|
+
message: @record.errors.full_messages.join(', '),
|
147
|
+
title: "Error"
|
148
|
+
).call
|
149
|
+
)
|
150
|
+
end
|
151
|
+
format.json { render json: { errors: @record.errors } }
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
# Get suggestion options for field autocomplete
|
156
|
+
def get_suggest_options(field_config, search_term, limit)
|
157
|
+
# Handle different field types that support suggest
|
158
|
+
case field_config[:type]
|
159
|
+
when :belongs_to
|
160
|
+
association_name = field_config[:association] || field_config[:name]
|
161
|
+
get_belongs_to_suggest_options(association_name, field_config, search_term, limit)
|
162
|
+
when :has_many
|
163
|
+
association_name = field_config[:association] || field_config[:name]
|
164
|
+
get_has_many_suggest_options(association_name, field_config, search_term, limit)
|
165
|
+
when :select
|
166
|
+
# For regular select fields, filter static options
|
167
|
+
get_static_suggest_options(field_config, search_term, limit)
|
168
|
+
else
|
169
|
+
[]
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
# Get suggestion options for belongs_to fields
|
174
|
+
def get_belongs_to_suggest_options(association_name, field_config, search_term, limit)
|
175
|
+
model_class = @resource_class.model_class
|
176
|
+
|
177
|
+
# Get the associated model class
|
178
|
+
if model_class.reflect_on_association(association_name)
|
179
|
+
association_class = model_class.reflect_on_association(association_name).klass
|
180
|
+
else
|
181
|
+
association_class = association_name.to_s.classify.constantize rescue nil
|
182
|
+
end
|
183
|
+
|
184
|
+
return [] unless association_class
|
185
|
+
|
186
|
+
# Get display method and search fields from field config
|
187
|
+
display_method = field_config[:display_method] || :name
|
188
|
+
suggest_config = field_config[:suggest] || {}
|
189
|
+
search_fields = suggest_config[:search_fields] || [display_method]
|
190
|
+
|
191
|
+
# Build search query
|
192
|
+
records = association_class.all
|
193
|
+
|
194
|
+
if search_term.present?
|
195
|
+
# Build WHERE conditions for search across multiple fields
|
196
|
+
conditions = search_fields.map do |field|
|
197
|
+
if association_class.columns_hash[field.to_s]&.type == :string
|
198
|
+
"#{field} LIKE ?"
|
199
|
+
else
|
200
|
+
"CAST(#{field} AS TEXT) LIKE ?"
|
201
|
+
end
|
202
|
+
end.join(" OR ")
|
203
|
+
|
204
|
+
search_pattern = "%#{search_term}%"
|
205
|
+
records = records.where(conditions, *([search_pattern] * search_fields.length))
|
206
|
+
end
|
207
|
+
|
208
|
+
# Apply limit and format as [label, value] pairs
|
209
|
+
records.limit(limit).map do |record|
|
210
|
+
label = if record.respond_to?(display_method)
|
211
|
+
record.public_send(display_method)
|
212
|
+
elsif record.respond_to?(:name)
|
213
|
+
record.name
|
214
|
+
elsif record.respond_to?(:title)
|
215
|
+
record.title
|
216
|
+
else
|
217
|
+
record.to_s
|
218
|
+
end
|
219
|
+
[label, record.id]
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
# Get suggestion options for has_many fields
|
224
|
+
def get_has_many_suggest_options(association_name, field_config, search_term, limit)
|
225
|
+
model_class = @resource_class.model_class
|
226
|
+
|
227
|
+
# Get the associated model class
|
228
|
+
if model_class.reflect_on_association(association_name)
|
229
|
+
association_class = model_class.reflect_on_association(association_name).klass
|
230
|
+
else
|
231
|
+
association_class = association_name.to_s.singularize.classify.constantize rescue nil
|
232
|
+
end
|
233
|
+
|
234
|
+
return [] unless association_class
|
235
|
+
|
236
|
+
# Get display method and search fields from field config
|
237
|
+
display_method = field_config[:display_method] || :name
|
238
|
+
suggest_config = field_config[:suggest] || {}
|
239
|
+
search_fields = suggest_config[:search_fields] || [display_method]
|
240
|
+
|
241
|
+
Rails.logger.debug "HasMany suggest: association=#{association_name}, display_method=#{display_method}, search_fields=#{search_fields}"
|
242
|
+
|
243
|
+
# Build search query
|
244
|
+
records = association_class.all
|
245
|
+
|
246
|
+
if search_term.present?
|
247
|
+
# Build WHERE conditions for search across multiple fields
|
248
|
+
conditions = search_fields.map do |field|
|
249
|
+
if association_class.columns_hash[field.to_s]&.type == :string
|
250
|
+
"#{field} LIKE ?"
|
251
|
+
else
|
252
|
+
"CAST(#{field} AS TEXT) LIKE ?"
|
253
|
+
end
|
254
|
+
end.join(" OR ")
|
255
|
+
|
256
|
+
search_pattern = "%#{search_term}%"
|
257
|
+
records = records.where(conditions, *([search_pattern] * search_fields.length))
|
258
|
+
end
|
259
|
+
|
260
|
+
# Apply limit and format as [label, value] pairs
|
261
|
+
results = records.limit(limit).map do |record|
|
262
|
+
label = if record.respond_to?(display_method)
|
263
|
+
raw_label = record.public_send(display_method)
|
264
|
+
Rails.logger.debug "HasMany record #{record.id}: raw_label=#{raw_label.class}:#{raw_label}"
|
265
|
+
raw_label.to_s
|
266
|
+
elsif record.respond_to?(:name)
|
267
|
+
record.name.to_s
|
268
|
+
elsif record.respond_to?(:title)
|
269
|
+
record.title.to_s
|
270
|
+
else
|
271
|
+
record.to_s
|
272
|
+
end
|
273
|
+
[label, record.id]
|
274
|
+
end
|
275
|
+
|
276
|
+
Rails.logger.debug "HasMany suggest final results: #{results}"
|
277
|
+
results
|
278
|
+
end
|
279
|
+
|
280
|
+
# Get suggestion options for static select fields
|
281
|
+
def get_static_suggest_options(field_config, search_term, limit)
|
282
|
+
options = field_config[:options] || []
|
283
|
+
|
284
|
+
if search_term.present?
|
285
|
+
# Filter options based on search term
|
286
|
+
filtered_options = options.select do |option|
|
287
|
+
text = option.is_a?(Array) ? option[0] : option.to_s
|
288
|
+
text.downcase.include?(search_term.downcase)
|
289
|
+
end
|
290
|
+
filtered_options.take(limit)
|
291
|
+
else
|
292
|
+
options.take(limit)
|
293
|
+
end
|
294
|
+
end
|
295
|
+
end
|
296
|
+
end
|
297
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module EasyAdmin
|
2
|
+
module Concerns
|
3
|
+
# ResourceAuthorization concern handles authorization checks for resource actions
|
4
|
+
# Provides before_action callbacks and authorization methods for all CRUD operations
|
5
|
+
module ResourceAuthorization
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
included do
|
9
|
+
before_action :authorize_resource_access!, only: [:index]
|
10
|
+
before_action :authorize_record_access!, only: [:show]
|
11
|
+
before_action :authorize_record_creation!, only: [:new, :create]
|
12
|
+
before_action :authorize_record_update!, only: [
|
13
|
+
:edit, :update, :edit_field, :update_field,
|
14
|
+
:belongs_to_reattach, :belongs_to_edit_attached, :update_belongs_to_attached
|
15
|
+
]
|
16
|
+
before_action :authorize_record_destruction!, only: [:destroy]
|
17
|
+
before_action :authorize_versioning_access!, only: [:versions, :revert_version, :version_diff]
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
# Authorize access to resource index
|
23
|
+
def authorize_resource_access!
|
24
|
+
authorize! @resource_class.model_class, to: :index?
|
25
|
+
end
|
26
|
+
|
27
|
+
# Authorize access to view a specific record
|
28
|
+
def authorize_record_access!
|
29
|
+
authorize! @record, to: :show?
|
30
|
+
end
|
31
|
+
|
32
|
+
# Authorize creation of new records
|
33
|
+
def authorize_record_creation!
|
34
|
+
authorize! @resource_class.model_class, to: :create?
|
35
|
+
end
|
36
|
+
|
37
|
+
# Authorize updating of existing records
|
38
|
+
def authorize_record_update!
|
39
|
+
authorize! @record, to: :update?
|
40
|
+
end
|
41
|
+
|
42
|
+
# Authorize destruction of existing records
|
43
|
+
def authorize_record_destruction!
|
44
|
+
authorize! @record, to: :destroy?
|
45
|
+
end
|
46
|
+
|
47
|
+
# Authorize access to versioning features (PaperTrail integration)
|
48
|
+
def authorize_versioning_access!
|
49
|
+
# Check if user can view record and has versioning permissions
|
50
|
+
authorize! @record, to: :show?
|
51
|
+
authorize! @record, to: :manage_versions?
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,178 @@
|
|
1
|
+
module EasyAdmin
|
2
|
+
module Concerns
|
3
|
+
# ResourceFiltering concern handles all filtering, searching, and scoping logic
|
4
|
+
# Provides methods for applying scopes, search, period filters, and sorting
|
5
|
+
module ResourceFiltering
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
private
|
9
|
+
|
10
|
+
# Determine the current scope based on params and resource configuration
|
11
|
+
def determine_current_scope
|
12
|
+
if params[:scope].present?
|
13
|
+
scope_config = @resource_class.scopes.find { |s| s[:name].to_s == params[:scope].to_s }
|
14
|
+
scope_config ? scope_config[:name] : @resource_class.default_scope&.dig(:name)
|
15
|
+
else
|
16
|
+
@resource_class.default_scope&.dig(:name)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# Apply scope filtering to records
|
21
|
+
def apply_scope(records)
|
22
|
+
return records unless @resource_class.has_scopes? && @current_scope
|
23
|
+
|
24
|
+
scope_config = @resource_class.scopes.find { |s| s[:name] == @current_scope }
|
25
|
+
|
26
|
+
if scope_config && scope_config[:name] != :all
|
27
|
+
scope_method = scope_config[:scope_method]
|
28
|
+
if records.respond_to?(scope_method)
|
29
|
+
records.public_send(scope_method)
|
30
|
+
else
|
31
|
+
records
|
32
|
+
end
|
33
|
+
else
|
34
|
+
records
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# Calculate counts for all scopes (for scope filters UI)
|
39
|
+
def calculate_scope_counts
|
40
|
+
counts = {}
|
41
|
+
|
42
|
+
@resource_class.scopes.each do |scope_config|
|
43
|
+
scope_name = scope_config[:name]
|
44
|
+
|
45
|
+
if scope_name == :all
|
46
|
+
counts[scope_name] = @resource_class.model_class.count
|
47
|
+
else
|
48
|
+
scope_method = scope_config[:scope_method]
|
49
|
+
if @resource_class.model_class.respond_to?(scope_method)
|
50
|
+
counts[scope_name] = @resource_class.model_class.public_send(scope_method).count
|
51
|
+
else
|
52
|
+
counts[scope_name] = 0
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
counts
|
58
|
+
end
|
59
|
+
|
60
|
+
# Apply period-based filtering (today, 7d, 30d, etc.)
|
61
|
+
def apply_period_filter(records)
|
62
|
+
return records unless params[:period].present? && records.respond_to?(:where)
|
63
|
+
|
64
|
+
date_range = case params[:period]
|
65
|
+
when 'today'
|
66
|
+
Date.current.beginning_of_day..Date.current.end_of_day
|
67
|
+
when '7d'
|
68
|
+
7.days.ago.beginning_of_day..Date.current.end_of_day
|
69
|
+
when '30d'
|
70
|
+
30.days.ago.beginning_of_day..Date.current.end_of_day
|
71
|
+
when '90d'
|
72
|
+
90.days.ago.beginning_of_day..Date.current.end_of_day
|
73
|
+
when '1y'
|
74
|
+
1.year.ago.beginning_of_day..Date.current.end_of_day
|
75
|
+
else
|
76
|
+
return records
|
77
|
+
end
|
78
|
+
|
79
|
+
# Try to filter by created_at, updated_at, or the first timestamp field
|
80
|
+
timestamp_field = determine_timestamp_field(records)
|
81
|
+
return records unless timestamp_field
|
82
|
+
|
83
|
+
records.where(timestamp_field => date_range)
|
84
|
+
end
|
85
|
+
|
86
|
+
# Apply sorting to records
|
87
|
+
def apply_sorting(records, sort_field, sort_direction)
|
88
|
+
# Always apply sorting if sort_field is present
|
89
|
+
return records unless sort_field.present?
|
90
|
+
|
91
|
+
# Get the model class from the relation
|
92
|
+
model_class = records.respond_to?(:klass) ? records.klass : records.class
|
93
|
+
|
94
|
+
if records.respond_to?(:order) && model_class.column_names.include?(sort_field.to_s)
|
95
|
+
records.order("#{sort_field} #{sort_direction}")
|
96
|
+
else
|
97
|
+
records
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
# Check if any filtering parameters are active
|
102
|
+
def has_active_filters?
|
103
|
+
# Check if any filtering parameters are present
|
104
|
+
params[:q].present? ||
|
105
|
+
params[:search].present? ||
|
106
|
+
params[:period].present? ||
|
107
|
+
(params[:scope].present? && params[:scope].to_s != @resource_class.default_scope&.dig(:name).to_s)
|
108
|
+
end
|
109
|
+
|
110
|
+
# Determine the best timestamp field for period filtering
|
111
|
+
def determine_timestamp_field(records)
|
112
|
+
# Get the model class from the relation or class
|
113
|
+
model_class = if records.is_a?(Class) && records < ActiveRecord::Base
|
114
|
+
# If records is already an ActiveRecord model class
|
115
|
+
records
|
116
|
+
elsif records.respond_to?(:klass)
|
117
|
+
# For ActiveRecord relations
|
118
|
+
records.klass
|
119
|
+
elsif records.respond_to?(:model)
|
120
|
+
records.model
|
121
|
+
else
|
122
|
+
records.class
|
123
|
+
end
|
124
|
+
|
125
|
+
# Try common timestamp fields in order of preference
|
126
|
+
%w[created_at updated_at published_at date timestamp].find do |field|
|
127
|
+
model_class.column_names.include?(field)
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
# Apply Ransack search filtering
|
132
|
+
def apply_ransack_search(records)
|
133
|
+
return records unless params[:q].present?
|
134
|
+
|
135
|
+
@search_query = records.ransack(params[:q])
|
136
|
+
@search_query.result
|
137
|
+
end
|
138
|
+
|
139
|
+
# Apply text-based search (fallback when Ransack not used)
|
140
|
+
def apply_text_search(records, sort_field, sort_direction)
|
141
|
+
return records unless params[:search].present?
|
142
|
+
|
143
|
+
@resource_class.search_records(
|
144
|
+
params[:search],
|
145
|
+
sort_field: sort_field,
|
146
|
+
sort_direction: sort_direction,
|
147
|
+
records: records
|
148
|
+
)
|
149
|
+
end
|
150
|
+
|
151
|
+
# Build the filtered and sorted record set
|
152
|
+
def build_filtered_records(sort_field, sort_direction)
|
153
|
+
# Start with base records (ensure we have a relation, not just a class)
|
154
|
+
records = apply_scope(@resource_class.model_class.all)
|
155
|
+
|
156
|
+
# Apply eager loading for index action
|
157
|
+
if @resource_class.index_includes.any?
|
158
|
+
records = records.includes(@resource_class.index_includes)
|
159
|
+
end
|
160
|
+
|
161
|
+
# Apply Ransack filtering if present
|
162
|
+
records = apply_ransack_search(records)
|
163
|
+
|
164
|
+
# Apply text search if present (fallback/additional search)
|
165
|
+
if params[:search].present?
|
166
|
+
records = apply_text_search(records, sort_field, sort_direction)
|
167
|
+
else
|
168
|
+
records = apply_sorting(records, sort_field, sort_direction)
|
169
|
+
end
|
170
|
+
|
171
|
+
# Apply period filtering
|
172
|
+
records = apply_period_filter(records)
|
173
|
+
|
174
|
+
records
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
@@ -0,0 +1,149 @@
|
|
1
|
+
module EasyAdmin
|
2
|
+
module Concerns
|
3
|
+
# ResourceLoading concern handles loading resource classes and individual records
|
4
|
+
# Provides before_action callbacks and helper methods for resource/record loading
|
5
|
+
module ResourceLoading
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
included do
|
9
|
+
before_action :load_resource_class
|
10
|
+
before_action :load_record, only: [
|
11
|
+
:show, :edit, :update, :destroy,
|
12
|
+
:edit_field, :update_field,
|
13
|
+
:belongs_to_reattach, :belongs_to_edit_attached, :update_belongs_to_attached,
|
14
|
+
:versions, :revert_version, :version_diff
|
15
|
+
]
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
# Load the resource class based on the route parameter
|
21
|
+
def load_resource_class
|
22
|
+
resource_name = params[:resource_name]
|
23
|
+
@resource_class = EasyAdmin::ResourceRegistry.find_resource(resource_name)
|
24
|
+
|
25
|
+
unless @resource_class
|
26
|
+
raise ActionController::RoutingError, "Resource '#{resource_name}' not found"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# Load individual record for actions that operate on a specific record
|
31
|
+
def load_record
|
32
|
+
@record = @resource_class.find_record(params[:id])
|
33
|
+
end
|
34
|
+
|
35
|
+
# Helper method to find field configuration
|
36
|
+
def find_field_config(field_name)
|
37
|
+
@resource_class.fields_config.find { |field| field[:name].to_s == field_name.to_s }
|
38
|
+
end
|
39
|
+
|
40
|
+
# Helper method to find resource class for a given model
|
41
|
+
def find_resource_class_for_model(model_class)
|
42
|
+
# Try to find the resource class by convention (e.g., User -> UserResource)
|
43
|
+
resource_class_name = "#{model_class.name}Resource"
|
44
|
+
begin
|
45
|
+
resource_class_name.constantize
|
46
|
+
rescue NameError
|
47
|
+
# If convention doesn't work, search through all resource classes
|
48
|
+
EasyAdmin.resource_registry.values.find { |rc| rc.model_class == model_class }
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Get permitted attributes for a resource class
|
53
|
+
def get_permitted_attributes(resource_class)
|
54
|
+
# Get all editable field names from the resource class
|
55
|
+
permitted_attrs = []
|
56
|
+
|
57
|
+
# Check both form fields and all fields (including hidden ones)
|
58
|
+
all_fields = resource_class.form_fields + resource_class.fields_config
|
59
|
+
unique_fields = all_fields.uniq { |f| f[:name] }
|
60
|
+
|
61
|
+
Rails.logger.debug "🔍 [ResourceLoading] All fields for #{resource_class}:"
|
62
|
+
unique_fields.each_with_index do |field_config, index|
|
63
|
+
Rails.logger.debug "🔍 [ResourceLoading] Field #{index}: #{field_config[:name]} (type: #{field_config[:type]}, readonly: #{field_config[:readonly]})"
|
64
|
+
next if field_config[:readonly]
|
65
|
+
|
66
|
+
field_name = field_config[:name]
|
67
|
+
|
68
|
+
# Handle different field types that might need special parameter handling
|
69
|
+
case field_config[:type]
|
70
|
+
when :has_many
|
71
|
+
# For has_many relationships, we need to permit an array of IDs
|
72
|
+
permitted_attrs << { "#{field_name.to_s.singularize}_ids" => [] }
|
73
|
+
when :belongs_to
|
74
|
+
# For belongs_to, we need the foreign key
|
75
|
+
if field_config[:multiple]
|
76
|
+
permitted_attrs << { "#{field_name}_ids" => [] }
|
77
|
+
else
|
78
|
+
association_name = field_config[:association] || field_name
|
79
|
+
foreign_key = get_foreign_key_name(association_name)
|
80
|
+
permitted_attrs << foreign_key.to_sym
|
81
|
+
end
|
82
|
+
when :select
|
83
|
+
# For select fields with multiple option
|
84
|
+
if field_config[:multiple]
|
85
|
+
permitted_attrs << { field_name => [] }
|
86
|
+
else
|
87
|
+
permitted_attrs << field_name
|
88
|
+
end
|
89
|
+
when :json
|
90
|
+
# For JSON fields, permit nested parameters as a hash
|
91
|
+
permitted_attrs << { field_name => {} }
|
92
|
+
else
|
93
|
+
# For regular fields, check if it's the permissions_cache field which needs hash permission
|
94
|
+
if field_name == :permissions_cache
|
95
|
+
permitted_attrs << { field_name => {} }
|
96
|
+
else
|
97
|
+
permitted_attrs << field_name
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
Rails.logger.debug "🔍 [ResourceLoading] Final permitted attributes: #{permitted_attrs.inspect}"
|
103
|
+
permitted_attrs
|
104
|
+
end
|
105
|
+
|
106
|
+
# Get foreign key name for belongs_to association
|
107
|
+
def get_foreign_key_name(association_name)
|
108
|
+
model_class = @resource_class.model_class
|
109
|
+
if model_class.reflect_on_association(association_name)
|
110
|
+
model_class.reflect_on_association(association_name).foreign_key
|
111
|
+
else
|
112
|
+
"#{association_name}_id"
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
# Get record parameters for create/update actions
|
117
|
+
def record_params
|
118
|
+
permitted_attrs = get_permitted_attributes(@resource_class)
|
119
|
+
|
120
|
+
# Try resource param_key first, then fall back to model's natural param key
|
121
|
+
param_key = if params.key?(@resource_class.param_key)
|
122
|
+
@resource_class.param_key
|
123
|
+
elsif params.key?(@resource_class.model_class.model_name.param_key)
|
124
|
+
@resource_class.model_class.model_name.param_key
|
125
|
+
else
|
126
|
+
# Handle namespaced models that use slash format (e.g., "catalog/payment_method")
|
127
|
+
namespaced_param_key = @resource_class.model_class.name.underscore
|
128
|
+
params.key?(namespaced_param_key) ? namespaced_param_key : @resource_class.param_key
|
129
|
+
end
|
130
|
+
|
131
|
+
params.require(param_key).permit(*permitted_attrs)
|
132
|
+
end
|
133
|
+
|
134
|
+
# Determine parameter key for field updates
|
135
|
+
def determine_param_key_for_update_field
|
136
|
+
# Try resource param_key first, then fall back to model's natural param key
|
137
|
+
if params.key?(@resource_class.param_key)
|
138
|
+
@resource_class.param_key
|
139
|
+
elsif params.key?(@resource_class.model_class.model_name.param_key)
|
140
|
+
@resource_class.model_class.model_name.param_key
|
141
|
+
else
|
142
|
+
# Handle namespaced models that use slash format (e.g., "catalog/payment_method")
|
143
|
+
namespaced_param_key = @resource_class.model_class.name.underscore
|
144
|
+
params.key?(namespaced_param_key) ? namespaced_param_key : @resource_class.param_key
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|