easy-admin-rails 0.1.15 → 0.2.1
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 +160 -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
@@ -2,114 +2,31 @@ module EasyAdmin
|
|
2
2
|
class ResourcesController < ApplicationController
|
3
3
|
include EasyAdmin::ResourceVersions
|
4
4
|
|
5
|
-
|
6
|
-
|
5
|
+
# Include concerns for better organization
|
6
|
+
include EasyAdmin::Concerns::ResourceLoading
|
7
|
+
include EasyAdmin::Concerns::ResourceFiltering
|
8
|
+
include EasyAdmin::Concerns::ResourcePagination
|
9
|
+
include EasyAdmin::Concerns::InlineFieldEditing
|
10
|
+
include EasyAdmin::Concerns::BelongsToEditing
|
11
|
+
include EasyAdmin::Concerns::ResourceAuthorization
|
7
12
|
|
8
13
|
def index
|
9
|
-
#
|
10
|
-
|
11
|
-
sort_direction = params[:direction] == 'desc' ? 'desc' : 'asc'
|
12
|
-
|
13
|
-
# Handle scope filtering
|
14
|
-
@current_scope = determine_current_scope
|
15
|
-
|
16
|
-
# Only calculate scope counts for HTML requests, not for infinite scroll (turbo_stream)
|
17
|
-
if @resource_class.has_scopes? && !request.format.turbo_stream?
|
18
|
-
@scope_counts = calculate_scope_counts
|
19
|
-
end
|
20
|
-
|
21
|
-
# Start with base records (ensure we have a relation, not just a class)
|
22
|
-
@records = apply_scope(@resource_class.model_class.all)
|
23
|
-
|
24
|
-
# Apply eager loading for index action
|
25
|
-
if @resource_class.index_includes.any?
|
26
|
-
@records = @records.includes(@resource_class.index_includes)
|
27
|
-
end
|
28
|
-
|
29
|
-
# Apply Ransack filtering if present
|
30
|
-
if params[:q].present?
|
31
|
-
@search_query = @records.ransack(params[:q])
|
32
|
-
@records = @search_query.result
|
33
|
-
end
|
34
|
-
|
35
|
-
# Apply text search if present (fallback/additional search)
|
36
|
-
if params[:search].present?
|
37
|
-
@records = @resource_class.search_records(params[:search], sort_field: sort_field, sort_direction: sort_direction, records: @records)
|
38
|
-
else
|
39
|
-
@records = apply_sorting(@records, sort_field, sort_direction)
|
40
|
-
end
|
41
|
-
|
42
|
-
# Apply period filtering
|
43
|
-
@records = apply_period_filter(@records)
|
44
|
-
|
45
|
-
# Apply pagination using Pagy with resource-specific configuration
|
46
|
-
items_per_page = @resource_class.respond_to?(:items_per_page) ? @resource_class.items_per_page : 20
|
47
|
-
|
48
|
-
# Reset to page 1 if filtering/searching is applied to avoid overflow
|
49
|
-
current_page = if has_active_filters?
|
50
|
-
1
|
51
|
-
else
|
52
|
-
params[:page] || 1
|
53
|
-
end
|
54
|
-
|
55
|
-
pagination_options = {
|
56
|
-
items: items_per_page,
|
57
|
-
limit: items_per_page,
|
58
|
-
page: current_page,
|
59
|
-
overflow: :last_page # Handle overflow by redirecting to last valid page
|
60
|
-
}
|
61
|
-
|
62
|
-
# Use countless pagination if enabled for performance
|
63
|
-
if @resource_class.respond_to?(:countless_enabled?) && @resource_class.countless_enabled?
|
64
|
-
@pagy, @records = pagy_countless(@records, **pagination_options)
|
65
|
-
else
|
66
|
-
@pagy, @records = pagy(@records, **pagination_options)
|
67
|
-
end
|
68
|
-
@current_sort_field = sort_field
|
69
|
-
@current_sort_direction = sort_direction
|
70
|
-
@current_period = params[:period]
|
14
|
+
# Prepare pagination data using concern methods
|
15
|
+
prepare_index_pagination_data
|
71
16
|
|
72
17
|
respond_to do |format|
|
73
18
|
format.html do
|
74
19
|
# Check if this is a turbo frame request
|
75
20
|
if turbo_frame_request?
|
76
|
-
|
77
|
-
|
78
|
-
records: @records,
|
79
|
-
pagy: @pagy,
|
80
|
-
current_params: params.permit(:search, :scope, :sort, :direction, :period, :page, q: {}),
|
81
|
-
current_path: request.path,
|
82
|
-
current_user: current_admin_user
|
83
|
-
).call
|
21
|
+
@current_params = params.permit(:search, :scope, :sort, :direction, :period, :page, q: {})
|
22
|
+
render template: 'easy_admin/resources/index_frame', layout: false
|
84
23
|
else
|
85
24
|
# Regular page load - shows skeleton with layout
|
86
25
|
# Will render app/views/easy_admin/resources/index.html.erb
|
87
26
|
end
|
88
27
|
end
|
89
28
|
format.turbo_stream do
|
90
|
-
|
91
|
-
table_rows_html = @records.map do |record|
|
92
|
-
EasyAdmin::Resources::TableRowComponent.new(
|
93
|
-
record: record,
|
94
|
-
resource_class: @resource_class
|
95
|
-
).call
|
96
|
-
end.join.html_safe
|
97
|
-
|
98
|
-
render turbo_stream: [
|
99
|
-
turbo_stream.append("records-container", table_rows_html),
|
100
|
-
turbo_stream.replace("infinite-scroll-container", EasyAdmin::InfiniteScrollComponent.new(
|
101
|
-
pagy: @pagy,
|
102
|
-
resource_class: @resource_class,
|
103
|
-
current_params: params.permit(:search, :scope, :sort, :direction, :period, :page, q: {}),
|
104
|
-
current_path: request.path
|
105
|
-
).call),
|
106
|
-
turbo_stream.replace("combined-filters", EasyAdmin::CombinedFiltersComponent.new(
|
107
|
-
resource_class: @resource_class,
|
108
|
-
current_params: params.permit(:search, :scope, :sort, :direction, :period, :page, q: {}),
|
109
|
-
search_params: params[:q] || {},
|
110
|
-
current_period: @current_period
|
111
|
-
).call)
|
112
|
-
]
|
29
|
+
render turbo_stream: build_infinite_scroll_turbo_stream
|
113
30
|
end
|
114
31
|
end
|
115
32
|
end
|
@@ -194,640 +111,8 @@ module EasyAdmin
|
|
194
111
|
notice: "#{@resource_class.singular_title} was successfully deleted."
|
195
112
|
end
|
196
113
|
|
197
|
-
# Inline field editing actions
|
198
|
-
def edit_field
|
199
|
-
@field_name = params[:field]
|
200
|
-
@field_config = find_field_config(@field_name)
|
201
|
-
|
202
|
-
unless @field_config
|
203
|
-
render json: { error: "Field '#{@field_name}' not found" }, status: :not_found
|
204
|
-
return
|
205
|
-
end
|
206
|
-
|
207
|
-
respond_to do |format|
|
208
|
-
format.html do
|
209
|
-
render plain: EasyAdmin::Fields::InlineEditModalComponent.new(
|
210
|
-
record: @record,
|
211
|
-
field_config: @field_config,
|
212
|
-
resource_class: @resource_class
|
213
|
-
).call
|
214
|
-
end
|
215
|
-
format.turbo_stream do
|
216
|
-
render turbo_stream: turbo_stream.update("modal",
|
217
|
-
EasyAdmin::Fields::InlineEditModalComponent.new(
|
218
|
-
record: @record,
|
219
|
-
field_config: @field_config,
|
220
|
-
resource_class: @resource_class
|
221
|
-
).call
|
222
|
-
)
|
223
|
-
end
|
224
|
-
end
|
225
|
-
end
|
226
|
-
|
227
|
-
def update_field
|
228
|
-
@field_name = params[:field]
|
229
|
-
@field_config = find_field_config(@field_name)
|
230
|
-
|
231
|
-
unless @field_config
|
232
|
-
render json: { error: "Field '#{@field_name}' not found" }, status: :not_found
|
233
|
-
return
|
234
|
-
end
|
235
|
-
|
236
|
-
# Determine the correct parameter key (handle complex model names)
|
237
|
-
param_key = determine_param_key_for_update_field
|
238
|
-
|
239
|
-
# For belongs_to fields, we need to handle foreign key updates
|
240
|
-
if @field_config[:type] == :belongs_to
|
241
|
-
association_name = @field_config[:name]
|
242
|
-
foreign_key = get_foreign_key_name(association_name)
|
243
|
-
field_value = params.dig(param_key, foreign_key)
|
244
|
-
update_attrs = { foreign_key.to_sym => field_value }
|
245
|
-
else
|
246
|
-
# Create a field object to handle normalization
|
247
|
-
field_obj = EasyAdmin::Field.new(@field_name, @field_config[:type], @field_config)
|
248
|
-
|
249
|
-
# Get the field value from params
|
250
|
-
field_value = params.dig(param_key, @field_name)
|
251
|
-
|
252
|
-
# Normalize the input
|
253
|
-
update_attrs = { @field_name => field_value }
|
254
|
-
field_obj.normalize_input!(update_attrs)
|
255
|
-
end
|
256
|
-
|
257
|
-
if @record.update(update_attrs)
|
258
|
-
respond_to do |format|
|
259
|
-
format.turbo_stream do
|
260
|
-
# Update the table cell with the new value
|
261
|
-
render turbo_stream: [
|
262
|
-
turbo_stream.replace(
|
263
|
-
"#{helpers.dom_id(@record)}_#{@field_name}",
|
264
|
-
EasyAdmin::Resources::TableCellComponent.new(
|
265
|
-
record: @record.reload,
|
266
|
-
field_config: @field_config,
|
267
|
-
resource_class: @resource_class
|
268
|
-
).call
|
269
|
-
),
|
270
|
-
turbo_stream.update("notifications",
|
271
|
-
EasyAdmin::NotificationComponent.new(
|
272
|
-
type: :success,
|
273
|
-
message: "#{@field_config[:label]} updated successfully!",
|
274
|
-
title: "Success"
|
275
|
-
).call
|
276
|
-
)
|
277
|
-
]
|
278
|
-
end
|
279
|
-
format.json { render json: { status: 'success' } }
|
280
|
-
end
|
281
|
-
else
|
282
|
-
respond_to do |format|
|
283
|
-
format.turbo_stream do
|
284
|
-
render turbo_stream: turbo_stream.update("notifications",
|
285
|
-
EasyAdmin::NotificationComponent.new(
|
286
|
-
type: :error,
|
287
|
-
message: @record.errors.full_messages.join(', '),
|
288
|
-
title: "Error"
|
289
|
-
).call
|
290
|
-
)
|
291
|
-
end
|
292
|
-
format.json { render json: { errors: @record.errors } }
|
293
|
-
end
|
294
|
-
end
|
295
|
-
end
|
296
|
-
|
297
|
-
def belongs_to_reattach
|
298
|
-
@field_name = params[:field]
|
299
|
-
@field_config = find_field_config(@field_name)
|
300
|
-
|
301
|
-
unless @field_config
|
302
|
-
render json: { error: "Field '#{@field_name}' not found" }, status: :not_found
|
303
|
-
return
|
304
|
-
end
|
305
|
-
|
306
|
-
# Convert belongs_to field to select field format for reattaching
|
307
|
-
select_field_config = prepare_belongs_to_as_select(@field_config)
|
308
|
-
|
309
|
-
respond_to do |format|
|
310
|
-
format.html do
|
311
|
-
render plain: EasyAdmin::Fields::InlineEditModalComponent.new(
|
312
|
-
record: @record,
|
313
|
-
field_config: select_field_config,
|
314
|
-
resource_class: @resource_class
|
315
|
-
).call
|
316
|
-
end
|
317
|
-
end
|
318
|
-
end
|
319
|
-
|
320
|
-
def belongs_to_edit_attached
|
321
|
-
@field_name = params[:field]
|
322
|
-
@field_config = find_field_config(@field_name)
|
323
|
-
|
324
|
-
unless @field_config
|
325
|
-
render json: { error: "Field '#{@field_name}' not found" }, status: :not_found
|
326
|
-
return
|
327
|
-
end
|
328
|
-
|
329
|
-
unless @field_config[:type] == :belongs_to
|
330
|
-
render json: { error: "Field '#{@field_name}' is not a belongs_to field" }, status: :bad_request
|
331
|
-
return
|
332
|
-
end
|
333
|
-
|
334
|
-
# Get the associated record
|
335
|
-
associated_record = @record.public_send(@field_config[:name])
|
336
|
-
|
337
|
-
unless associated_record
|
338
|
-
render json: { error: "No associated record found" }, status: :not_found
|
339
|
-
return
|
340
|
-
end
|
341
|
-
|
342
|
-
# Get the resource class for the associated record
|
343
|
-
association_name = @field_config[:name]
|
344
|
-
associated_model_class = @resource_class.model_class.reflect_on_association(association_name).klass
|
345
|
-
associated_resource_class = find_resource_class_for_model(associated_model_class)
|
346
|
-
|
347
|
-
unless associated_resource_class
|
348
|
-
render json: { error: "No resource class found for #{associated_model_class.name}" }, status: :not_found
|
349
|
-
return
|
350
|
-
end
|
351
|
-
|
352
|
-
respond_to do |format|
|
353
|
-
format.html do
|
354
|
-
render plain: EasyAdmin::Fields::BelongsToEditModalComponent.new(
|
355
|
-
record: associated_record,
|
356
|
-
resource_class: associated_resource_class,
|
357
|
-
parent_record: @record,
|
358
|
-
parent_field: @field_config
|
359
|
-
).call
|
360
|
-
end
|
361
|
-
format.turbo_stream do
|
362
|
-
render turbo_stream: turbo_stream.update("modal",
|
363
|
-
EasyAdmin::Fields::BelongsToEditModalComponent.new(
|
364
|
-
record: associated_record,
|
365
|
-
resource_class: associated_resource_class,
|
366
|
-
parent_record: @record,
|
367
|
-
parent_field: @field_config
|
368
|
-
).call
|
369
|
-
)
|
370
|
-
end
|
371
|
-
end
|
372
|
-
end
|
373
|
-
|
374
|
-
def update_belongs_to_attached
|
375
|
-
@field_name = params[:field]
|
376
|
-
@field_config = find_field_config(@field_name)
|
377
|
-
@attached_id = params[:attached_id]
|
378
|
-
|
379
|
-
unless @field_config
|
380
|
-
render json: { error: "Field '#{@field_name}' not found" }, status: :not_found
|
381
|
-
return
|
382
|
-
end
|
383
|
-
|
384
|
-
# Get the associated record
|
385
|
-
association_name = @field_config[:name]
|
386
|
-
associated_model_class = @resource_class.model_class.reflect_on_association(association_name).klass
|
387
|
-
associated_resource_class = find_resource_class_for_model(associated_model_class)
|
388
|
-
|
389
|
-
# Find the attached record
|
390
|
-
attached_record = associated_model_class.find(@attached_id)
|
391
|
-
|
392
|
-
unless attached_record
|
393
|
-
render json: { error: "Attached record not found" }, status: :not_found
|
394
|
-
return
|
395
|
-
end
|
396
|
-
|
397
|
-
# Get the update parameters for the attached record
|
398
|
-
raw_params = params.dig(associated_resource_class.param_key) || {}
|
399
|
-
|
400
|
-
# Get permitted attributes from the resource class
|
401
|
-
permitted_attributes = get_permitted_attributes(associated_resource_class)
|
402
|
-
update_params = raw_params.permit(*permitted_attributes)
|
403
|
-
|
404
|
-
if attached_record.update(update_params)
|
405
|
-
respond_to do |format|
|
406
|
-
format.turbo_stream do
|
407
|
-
# Update the parent table cell to reflect any changes in the associated record
|
408
|
-
render turbo_stream: [
|
409
|
-
turbo_stream.replace(
|
410
|
-
"#{helpers.dom_id(@record)}_#{@field_name}",
|
411
|
-
EasyAdmin::Resources::TableCellComponent.new(
|
412
|
-
record: @record.reload,
|
413
|
-
field_config: @field_config,
|
414
|
-
resource_class: @resource_class
|
415
|
-
).call
|
416
|
-
),
|
417
|
-
turbo_stream.update("notifications",
|
418
|
-
EasyAdmin::NotificationComponent.new(
|
419
|
-
type: :success,
|
420
|
-
message: "#{associated_resource_class.singular_title} updated successfully!",
|
421
|
-
title: "Success"
|
422
|
-
).call
|
423
|
-
)
|
424
|
-
]
|
425
|
-
end
|
426
|
-
end
|
427
|
-
else
|
428
|
-
respond_to do |format|
|
429
|
-
format.turbo_stream do
|
430
|
-
render turbo_stream: turbo_stream.update("notifications",
|
431
|
-
EasyAdmin::NotificationComponent.new(
|
432
|
-
type: :error,
|
433
|
-
message: attached_record.errors.full_messages.join(', '),
|
434
|
-
title: "Error"
|
435
|
-
).call
|
436
|
-
)
|
437
|
-
end
|
438
|
-
end
|
439
|
-
end
|
440
|
-
end
|
441
|
-
|
442
|
-
def suggest
|
443
|
-
field_config = @resource_class.fields_config.find { |f| f[:name].to_s == params[:field] }
|
444
|
-
|
445
|
-
Rails.logger.debug "Suggest field_config for #{params[:field]}: #{field_config}"
|
446
|
-
|
447
|
-
unless field_config && field_config[:suggest]
|
448
|
-
render json: { error: "Field not found or suggest not configured" }, status: :not_found
|
449
|
-
return
|
450
|
-
end
|
451
|
-
|
452
|
-
search_term = params[:q].to_s.strip
|
453
|
-
limit = field_config[:suggest][:limit] || 10
|
454
|
-
|
455
|
-
results = get_suggest_options(field_config, search_term, limit)
|
456
|
-
|
457
|
-
render json: { options: results }
|
458
|
-
end
|
459
|
-
|
460
114
|
private
|
461
115
|
|
462
|
-
def load_resource_class
|
463
|
-
resource_name = params[:resource_name]
|
464
|
-
@resource_class = EasyAdmin::ResourceRegistry.find_resource(resource_name)
|
465
|
-
|
466
|
-
unless @resource_class
|
467
|
-
raise ActionController::RoutingError, "Resource '#{resource_name}' not found"
|
468
|
-
end
|
469
|
-
end
|
470
|
-
|
471
|
-
def load_record
|
472
|
-
@record = @resource_class.find_record(params[:id])
|
473
|
-
end
|
474
|
-
|
475
|
-
def record_params
|
476
|
-
permitted_attrs = get_permitted_attributes(@resource_class)
|
477
|
-
|
478
|
-
# Try resource param_key first, then fall back to model's natural param key
|
479
|
-
param_key = if params.key?(@resource_class.param_key)
|
480
|
-
@resource_class.param_key
|
481
|
-
elsif params.key?(@resource_class.model_class.model_name.param_key)
|
482
|
-
@resource_class.model_class.model_name.param_key
|
483
|
-
else
|
484
|
-
# Handle namespaced models that use slash format (e.g., "catalog/payment_method")
|
485
|
-
namespaced_param_key = @resource_class.model_class.name.underscore
|
486
|
-
params.key?(namespaced_param_key) ? namespaced_param_key : @resource_class.param_key
|
487
|
-
end
|
488
|
-
|
489
|
-
result = params.require(param_key).permit(*permitted_attrs)
|
490
|
-
result
|
491
|
-
end
|
492
|
-
|
493
|
-
def determine_current_scope
|
494
|
-
if params[:scope].present?
|
495
|
-
scope_config = @resource_class.scopes.find { |s| s[:name].to_s == params[:scope].to_s }
|
496
|
-
scope_config ? scope_config[:name] : @resource_class.default_scope&.dig(:name)
|
497
|
-
else
|
498
|
-
@resource_class.default_scope&.dig(:name)
|
499
|
-
end
|
500
|
-
end
|
501
|
-
|
502
|
-
def apply_scope(records)
|
503
|
-
return records unless @resource_class.has_scopes? && @current_scope
|
504
|
-
|
505
|
-
scope_config = @resource_class.scopes.find { |s| s[:name] == @current_scope }
|
506
|
-
|
507
|
-
if scope_config && scope_config[:name] != :all
|
508
|
-
scope_method = scope_config[:scope_method]
|
509
|
-
if records.respond_to?(scope_method)
|
510
|
-
records.public_send(scope_method)
|
511
|
-
else
|
512
|
-
records
|
513
|
-
end
|
514
|
-
else
|
515
|
-
records
|
516
|
-
end
|
517
|
-
end
|
518
|
-
|
519
|
-
def calculate_scope_counts
|
520
|
-
counts = {}
|
521
|
-
|
522
|
-
@resource_class.scopes.each do |scope_config|
|
523
|
-
scope_name = scope_config[:name]
|
524
|
-
|
525
|
-
if scope_name == :all
|
526
|
-
counts[scope_name] = @resource_class.model_class.count
|
527
|
-
else
|
528
|
-
scope_method = scope_config[:scope_method]
|
529
|
-
if @resource_class.model_class.respond_to?(scope_method)
|
530
|
-
counts[scope_name] = @resource_class.model_class.public_send(scope_method).count
|
531
|
-
else
|
532
|
-
counts[scope_name] = 0
|
533
|
-
end
|
534
|
-
end
|
535
|
-
end
|
536
|
-
|
537
|
-
counts
|
538
|
-
end
|
539
|
-
|
540
|
-
def apply_period_filter(records)
|
541
|
-
return records unless params[:period].present? && records.respond_to?(:where)
|
542
|
-
|
543
|
-
date_range = case params[:period]
|
544
|
-
when 'today'
|
545
|
-
Date.current.beginning_of_day..Date.current.end_of_day
|
546
|
-
when '7d'
|
547
|
-
7.days.ago.beginning_of_day..Date.current.end_of_day
|
548
|
-
when '30d'
|
549
|
-
30.days.ago.beginning_of_day..Date.current.end_of_day
|
550
|
-
when '90d'
|
551
|
-
90.days.ago.beginning_of_day..Date.current.end_of_day
|
552
|
-
when '1y'
|
553
|
-
1.year.ago.beginning_of_day..Date.current.end_of_day
|
554
|
-
else
|
555
|
-
return records
|
556
|
-
end
|
557
|
-
|
558
|
-
# Try to filter by created_at, updated_at, or the first timestamp field
|
559
|
-
timestamp_field = determine_timestamp_field(records)
|
560
|
-
return records unless timestamp_field
|
561
|
-
|
562
|
-
records.where(timestamp_field => date_range)
|
563
|
-
end
|
564
|
-
|
565
|
-
def find_field_config(field_name)
|
566
|
-
@resource_class.fields_config.find { |field| field[:name].to_s == field_name.to_s }
|
567
|
-
end
|
568
|
-
|
569
|
-
def find_resource_class_for_model(model_class)
|
570
|
-
# Try to find the resource class by convention (e.g., User -> UserResource)
|
571
|
-
resource_class_name = "#{model_class.name}Resource"
|
572
|
-
begin
|
573
|
-
resource_class_name.constantize
|
574
|
-
rescue NameError
|
575
|
-
# If convention doesn't work, search through all resource classes
|
576
|
-
EasyAdmin.resource_registry.values.find { |rc| rc.model_class == model_class }
|
577
|
-
end
|
578
|
-
end
|
579
|
-
|
580
|
-
def get_permitted_attributes(resource_class)
|
581
|
-
# Get all editable field names from the resource class
|
582
|
-
permitted_attrs = []
|
583
|
-
|
584
|
-
resource_class.form_fields.each do |field_config|
|
585
|
-
next if field_config[:readonly]
|
586
|
-
|
587
|
-
field_name = field_config[:name]
|
588
|
-
|
589
|
-
# Handle different field types that might need special parameter handling
|
590
|
-
case field_config[:type]
|
591
|
-
when :has_many
|
592
|
-
# For has_many relationships, we need to permit an array of IDs
|
593
|
-
permitted_attrs << { "#{field_name.to_s.singularize}_ids" => [] }
|
594
|
-
when :belongs_to
|
595
|
-
# For belongs_to, we need the foreign key
|
596
|
-
if field_config[:multiple]
|
597
|
-
permitted_attrs << { "#{field_name}_ids" => [] }
|
598
|
-
else
|
599
|
-
foreign_key = get_foreign_key_name(field_name)
|
600
|
-
permitted_attrs << foreign_key.to_sym
|
601
|
-
end
|
602
|
-
when :select
|
603
|
-
# For select fields with multiple option
|
604
|
-
if field_config[:multiple]
|
605
|
-
permitted_attrs << { field_name => [] }
|
606
|
-
else
|
607
|
-
permitted_attrs << field_name
|
608
|
-
end
|
609
|
-
else
|
610
|
-
# For regular fields
|
611
|
-
permitted_attrs << field_name
|
612
|
-
end
|
613
|
-
end
|
614
|
-
|
615
|
-
permitted_attrs
|
616
|
-
end
|
617
|
-
|
618
|
-
def prepare_belongs_to_as_select(field_config)
|
619
|
-
# Convert belongs_to field to select field for reattaching
|
620
|
-
association_name = field_config[:name]
|
621
|
-
foreign_key = get_foreign_key_name(association_name)
|
622
|
-
|
623
|
-
# Get available options for the select
|
624
|
-
options = get_belongs_to_options(association_name, field_config)
|
625
|
-
|
626
|
-
# Return field config formatted as a select field, preserving original info
|
627
|
-
field_config.merge(
|
628
|
-
type: :select,
|
629
|
-
name: foreign_key, # Use foreign key instead of association name
|
630
|
-
label: "Select #{field_config[:label]}",
|
631
|
-
options: options,
|
632
|
-
original_type: :belongs_to,
|
633
|
-
original_name: association_name
|
634
|
-
)
|
635
|
-
end
|
636
|
-
|
637
|
-
def get_foreign_key_name(association_name)
|
638
|
-
model_class = @resource_class.model_class
|
639
|
-
if model_class.reflect_on_association(association_name)
|
640
|
-
model_class.reflect_on_association(association_name).foreign_key
|
641
|
-
else
|
642
|
-
"#{association_name}_id"
|
643
|
-
end
|
644
|
-
end
|
645
|
-
|
646
|
-
def get_belongs_to_options(association_name, field_config)
|
647
|
-
model_class = @resource_class.model_class
|
648
|
-
|
649
|
-
# Get the associated model class
|
650
|
-
if model_class.reflect_on_association(association_name)
|
651
|
-
association_class = model_class.reflect_on_association(association_name).klass
|
652
|
-
else
|
653
|
-
association_class = association_name.to_s.classify.constantize rescue nil
|
654
|
-
end
|
655
|
-
|
656
|
-
return [] unless association_class
|
657
|
-
|
658
|
-
# Get display method from field config
|
659
|
-
display_method = field_config[:display_method] || :name
|
660
|
-
|
661
|
-
# Get all records and format as [label, value] pairs
|
662
|
-
association_class.all.limit(100).map do |record|
|
663
|
-
label = if record.respond_to?(display_method)
|
664
|
-
record.public_send(display_method)
|
665
|
-
elsif record.respond_to?(:name)
|
666
|
-
record.name
|
667
|
-
elsif record.respond_to?(:title)
|
668
|
-
record.title
|
669
|
-
else
|
670
|
-
record.to_s
|
671
|
-
end
|
672
|
-
[label, record.id]
|
673
|
-
end
|
674
|
-
end
|
675
|
-
|
676
|
-
def get_suggest_options(field_config, search_term, limit)
|
677
|
-
# Handle different field types that support suggest
|
678
|
-
case field_config[:type]
|
679
|
-
when :belongs_to
|
680
|
-
get_belongs_to_suggest_options(field_config[:name], field_config, search_term, limit)
|
681
|
-
when :has_many
|
682
|
-
get_has_many_suggest_options(field_config[:name], field_config, search_term, limit)
|
683
|
-
when :select
|
684
|
-
# For regular select fields, filter static options
|
685
|
-
get_static_suggest_options(field_config, search_term, limit)
|
686
|
-
else
|
687
|
-
[]
|
688
|
-
end
|
689
|
-
end
|
690
|
-
|
691
|
-
def get_belongs_to_suggest_options(association_name, field_config, search_term, limit)
|
692
|
-
model_class = @resource_class.model_class
|
693
|
-
|
694
|
-
# Get the associated model class
|
695
|
-
if model_class.reflect_on_association(association_name)
|
696
|
-
association_class = model_class.reflect_on_association(association_name).klass
|
697
|
-
else
|
698
|
-
association_class = association_name.to_s.classify.constantize rescue nil
|
699
|
-
end
|
700
|
-
|
701
|
-
return [] unless association_class
|
702
|
-
|
703
|
-
# Get display method and search fields from field config
|
704
|
-
display_method = field_config[:display_method] || :name
|
705
|
-
suggest_config = field_config[:suggest] || {}
|
706
|
-
search_fields = suggest_config[:search_fields] || [display_method]
|
707
|
-
|
708
|
-
# Build search query
|
709
|
-
records = association_class.all
|
710
|
-
|
711
|
-
if search_term.present?
|
712
|
-
# Build WHERE conditions for search across multiple fields
|
713
|
-
conditions = search_fields.map do |field|
|
714
|
-
if association_class.columns_hash[field.to_s]&.type == :string
|
715
|
-
"#{field} LIKE ?"
|
716
|
-
else
|
717
|
-
"CAST(#{field} AS TEXT) LIKE ?"
|
718
|
-
end
|
719
|
-
end.join(" OR ")
|
720
|
-
|
721
|
-
search_pattern = "%#{search_term}%"
|
722
|
-
records = records.where(conditions, *([search_pattern] * search_fields.length))
|
723
|
-
end
|
724
|
-
|
725
|
-
# Apply limit and format as [label, value] pairs
|
726
|
-
records.limit(limit).map do |record|
|
727
|
-
label = if record.respond_to?(display_method)
|
728
|
-
record.public_send(display_method)
|
729
|
-
elsif record.respond_to?(:name)
|
730
|
-
record.name
|
731
|
-
elsif record.respond_to?(:title)
|
732
|
-
record.title
|
733
|
-
else
|
734
|
-
record.to_s
|
735
|
-
end
|
736
|
-
[label, record.id]
|
737
|
-
end
|
738
|
-
end
|
739
|
-
|
740
|
-
def get_has_many_suggest_options(association_name, field_config, search_term, limit)
|
741
|
-
model_class = @resource_class.model_class
|
742
|
-
|
743
|
-
# Get the associated model class
|
744
|
-
if model_class.reflect_on_association(association_name)
|
745
|
-
association_class = model_class.reflect_on_association(association_name).klass
|
746
|
-
else
|
747
|
-
association_class = association_name.to_s.singularize.classify.constantize rescue nil
|
748
|
-
end
|
749
|
-
|
750
|
-
return [] unless association_class
|
751
|
-
|
752
|
-
# Get display method and search fields from field config
|
753
|
-
display_method = field_config[:display_method] || :name
|
754
|
-
suggest_config = field_config[:suggest] || {}
|
755
|
-
search_fields = suggest_config[:search_fields] || [display_method]
|
756
|
-
|
757
|
-
Rails.logger.debug "HasMany suggest: association=#{association_name}, display_method=#{display_method}, search_fields=#{search_fields}"
|
758
|
-
|
759
|
-
# Build search query
|
760
|
-
records = association_class.all
|
761
|
-
|
762
|
-
if search_term.present?
|
763
|
-
# Build WHERE conditions for search across multiple fields
|
764
|
-
conditions = search_fields.map do |field|
|
765
|
-
if association_class.columns_hash[field.to_s]&.type == :string
|
766
|
-
"#{field} LIKE ?"
|
767
|
-
else
|
768
|
-
"CAST(#{field} AS TEXT) LIKE ?"
|
769
|
-
end
|
770
|
-
end.join(" OR ")
|
771
|
-
|
772
|
-
search_pattern = "%#{search_term}%"
|
773
|
-
records = records.where(conditions, *([search_pattern] * search_fields.length))
|
774
|
-
end
|
775
|
-
|
776
|
-
# Apply limit and format as [label, value] pairs
|
777
|
-
results = records.limit(limit).map do |record|
|
778
|
-
label = if record.respond_to?(display_method)
|
779
|
-
raw_label = record.public_send(display_method)
|
780
|
-
Rails.logger.debug "HasMany record #{record.id}: raw_label=#{raw_label.class}:#{raw_label}"
|
781
|
-
raw_label.to_s
|
782
|
-
elsif record.respond_to?(:name)
|
783
|
-
record.name.to_s
|
784
|
-
elsif record.respond_to?(:title)
|
785
|
-
record.title.to_s
|
786
|
-
else
|
787
|
-
record.to_s
|
788
|
-
end
|
789
|
-
[label, record.id]
|
790
|
-
end
|
791
|
-
|
792
|
-
Rails.logger.debug "HasMany suggest final results: #{results}"
|
793
|
-
results
|
794
|
-
end
|
795
|
-
|
796
|
-
def get_static_suggest_options(field_config, search_term, limit)
|
797
|
-
options = field_config[:options] || []
|
798
|
-
|
799
|
-
if search_term.present?
|
800
|
-
# Filter options based on search term
|
801
|
-
filtered_options = options.select do |option|
|
802
|
-
text = option.is_a?(Array) ? option[0] : option.to_s
|
803
|
-
text.downcase.include?(search_term.downcase)
|
804
|
-
end
|
805
|
-
filtered_options.take(limit)
|
806
|
-
else
|
807
|
-
options.take(limit)
|
808
|
-
end
|
809
|
-
end
|
810
|
-
|
811
|
-
def determine_timestamp_field(records)
|
812
|
-
# Get the model class from the relation or class
|
813
|
-
model_class = if records.is_a?(Class) && records < ActiveRecord::Base
|
814
|
-
# If records is already an ActiveRecord model class
|
815
|
-
records
|
816
|
-
elsif records.respond_to?(:klass)
|
817
|
-
# For ActiveRecord relations
|
818
|
-
records.klass
|
819
|
-
elsif records.respond_to?(:model)
|
820
|
-
records.model
|
821
|
-
else
|
822
|
-
records.class
|
823
|
-
end
|
824
|
-
|
825
|
-
# Try common timestamp fields in order of preference
|
826
|
-
%w[created_at updated_at published_at date timestamp].find do |field|
|
827
|
-
model_class.column_names.include?(field)
|
828
|
-
end
|
829
|
-
end
|
830
|
-
|
831
116
|
# Helper methods for lazy loading
|
832
117
|
def find_metric_config(metric_id)
|
833
118
|
find_element_in_layout(@resource_class.show_layout, :metric_card) do |element|
|
@@ -897,39 +182,5 @@ module EasyAdmin
|
|
897
182
|
"Tab content loaded"
|
898
183
|
end
|
899
184
|
end
|
900
|
-
|
901
|
-
def apply_sorting(records, sort_field, sort_direction)
|
902
|
-
return records unless sort_field.present?
|
903
|
-
|
904
|
-
# Get the model class from the relation
|
905
|
-
model_class = records.respond_to?(:klass) ? records.klass : records.class
|
906
|
-
|
907
|
-
if records.respond_to?(:order) && model_class.column_names.include?(sort_field.to_s)
|
908
|
-
records.order("#{sort_field} #{sort_direction}")
|
909
|
-
else
|
910
|
-
records
|
911
|
-
end
|
912
|
-
end
|
913
|
-
|
914
|
-
def has_active_filters?
|
915
|
-
# Check if any filtering parameters are present
|
916
|
-
params[:q].present? ||
|
917
|
-
params[:search].present? ||
|
918
|
-
params[:period].present? ||
|
919
|
-
(params[:scope].present? && params[:scope] != determine_current_scope[:scope])
|
920
|
-
end
|
921
|
-
|
922
|
-
def determine_param_key_for_update_field
|
923
|
-
# Try resource param_key first, then fall back to model's natural param key
|
924
|
-
if params.key?(@resource_class.param_key)
|
925
|
-
@resource_class.param_key
|
926
|
-
elsif params.key?(@resource_class.model_class.model_name.param_key)
|
927
|
-
@resource_class.model_class.model_name.param_key
|
928
|
-
else
|
929
|
-
# Handle namespaced models that use slash format (e.g., "catalog/payment_method")
|
930
|
-
namespaced_param_key = @resource_class.model_class.name.underscore
|
931
|
-
params.key?(namespaced_param_key) ? namespaced_param_key : @resource_class.param_key
|
932
|
-
end
|
933
|
-
end
|
934
185
|
end
|
935
|
-
end
|
186
|
+
end
|