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.
Files changed (92) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/builds/easy_admin.base.js +254 -18
  3. data/app/assets/builds/easy_admin.base.js.map +4 -4
  4. data/app/assets/builds/easy_admin.css +112 -18
  5. data/app/components/easy_admin/base_component.rb +1 -0
  6. data/app/components/easy_admin/form_tabs_component.rb +5 -2
  7. data/app/components/easy_admin/navbar_component.rb +5 -1
  8. data/app/components/easy_admin/permissions/user_role_assignment_component.rb +254 -0
  9. data/app/components/easy_admin/permissions/user_role_permissions_component.rb +186 -0
  10. data/app/components/easy_admin/resources/index_component.rb +1 -4
  11. data/app/components/easy_admin/sidebar_component.rb +67 -2
  12. data/app/controllers/easy_admin/application_controller.rb +131 -1
  13. data/app/controllers/easy_admin/batch_actions_controller.rb +27 -0
  14. data/app/controllers/easy_admin/concerns/belongs_to_editing.rb +201 -0
  15. data/app/controllers/easy_admin/concerns/inline_field_editing.rb +297 -0
  16. data/app/controllers/easy_admin/concerns/resource_authorization.rb +55 -0
  17. data/app/controllers/easy_admin/concerns/resource_filtering.rb +178 -0
  18. data/app/controllers/easy_admin/concerns/resource_loading.rb +149 -0
  19. data/app/controllers/easy_admin/concerns/resource_pagination.rb +135 -0
  20. data/app/controllers/easy_admin/dashboard_controller.rb +2 -1
  21. data/app/controllers/easy_admin/dashboards_controller.rb +6 -40
  22. data/app/controllers/easy_admin/resources_controller.rb +13 -762
  23. data/app/controllers/easy_admin/row_actions_controller.rb +25 -0
  24. data/app/helpers/easy_admin/fields_helper.rb +61 -9
  25. data/app/javascript/easy_admin/controllers/event_emitter_controller.js +2 -4
  26. data/app/javascript/easy_admin/controllers/infinite_scroll_controller.js +0 -10
  27. data/app/javascript/easy_admin/controllers/jsoneditor_controller.js +1 -4
  28. data/app/javascript/easy_admin/controllers/permission_toggle_controller.js +227 -0
  29. data/app/javascript/easy_admin/controllers/role_preview_controller.js +93 -0
  30. data/app/javascript/easy_admin/controllers/select_field_controller.js +1 -2
  31. data/app/javascript/easy_admin/controllers/settings_button_controller.js +1 -2
  32. data/app/javascript/easy_admin/controllers/settings_sidebar_controller.js +1 -4
  33. data/app/javascript/easy_admin/controllers/turbo_stream_redirect.js +0 -2
  34. data/app/javascript/easy_admin/controllers.js +5 -1
  35. data/app/models/easy_admin/admin_user.rb +6 -0
  36. data/app/policies/admin_user_policy.rb +36 -0
  37. data/app/policies/application_policy.rb +83 -0
  38. data/app/views/easy_admin/application/authorization_failure.turbo_stream.erb +8 -0
  39. data/app/views/easy_admin/dashboards/card.html.erb +5 -0
  40. data/app/views/easy_admin/dashboards/card.turbo_stream.erb +7 -0
  41. data/app/views/easy_admin/dashboards/card_error.html.erb +3 -0
  42. data/app/views/easy_admin/dashboards/card_error.turbo_stream.erb +5 -0
  43. data/app/views/easy_admin/dashboards/show.turbo_stream.erb +7 -0
  44. data/app/views/easy_admin/resources/belongs_to_edit_attached.html.erb +6 -0
  45. data/app/views/easy_admin/resources/belongs_to_edit_attached.turbo_stream.erb +8 -0
  46. data/app/views/easy_admin/resources/belongs_to_reattach.html.erb +5 -0
  47. data/app/views/easy_admin/resources/edit.html.erb +1 -1
  48. data/app/views/easy_admin/resources/edit_field.html.erb +5 -0
  49. data/app/views/easy_admin/resources/edit_field.turbo_stream.erb +7 -0
  50. data/app/views/easy_admin/resources/index.html.erb +1 -1
  51. data/app/views/easy_admin/resources/index_frame.html.erb +8 -142
  52. data/app/views/easy_admin/resources/update_belongs_to_attached.turbo_stream.erb +25 -0
  53. data/app/views/layouts/easy_admin/application.html.erb +15 -2
  54. data/config/initializers/easy_admin_permissions.rb +73 -0
  55. data/db/seeds/easy_admin_permissions.rb +121 -0
  56. data/lib/easy-admin-rails.rb +2 -0
  57. data/lib/easy_admin/permissions/component.rb +168 -0
  58. data/lib/easy_admin/permissions/configuration.rb +37 -0
  59. data/lib/easy_admin/permissions/controller.rb +164 -0
  60. data/lib/easy_admin/permissions/dsl.rb +180 -0
  61. data/lib/easy_admin/permissions/models.rb +44 -0
  62. data/lib/easy_admin/permissions/permission_denied_component.rb +121 -0
  63. data/lib/easy_admin/permissions/resource_permissions.rb +231 -0
  64. data/lib/easy_admin/permissions/role_definition.rb +45 -0
  65. data/lib/easy_admin/permissions/role_denied_component.rb +159 -0
  66. data/lib/easy_admin/permissions/role_dsl.rb +73 -0
  67. data/lib/easy_admin/permissions/user_extensions.rb +129 -0
  68. data/lib/easy_admin/permissions.rb +113 -0
  69. data/lib/easy_admin/resource/base.rb +119 -0
  70. data/lib/easy_admin/resource/configuration.rb +148 -0
  71. data/lib/easy_admin/resource/dsl.rb +117 -0
  72. data/lib/easy_admin/resource/field_registry.rb +189 -0
  73. data/lib/easy_admin/resource/form_builder.rb +123 -0
  74. data/lib/easy_admin/resource/layout_builder.rb +249 -0
  75. data/lib/easy_admin/resource/scope_manager.rb +252 -0
  76. data/lib/easy_admin/resource/show_builder.rb +359 -0
  77. data/lib/easy_admin/resource.rb +8 -835
  78. data/lib/easy_admin/resource_modules.rb +11 -0
  79. data/lib/easy_admin/version.rb +1 -1
  80. data/lib/generators/easy_admin/permissions/install_generator.rb +90 -0
  81. data/lib/generators/easy_admin/permissions/templates/initializers/permissions.rb +37 -0
  82. data/lib/generators/easy_admin/permissions/templates/migrations/create_permission_tables.rb +27 -0
  83. data/lib/generators/easy_admin/permissions/templates/migrations/update_users_for_permissions.rb +6 -0
  84. data/lib/generators/easy_admin/permissions/templates/models/permission.rb +9 -0
  85. data/lib/generators/easy_admin/permissions/templates/models/role.rb +9 -0
  86. data/lib/generators/easy_admin/permissions/templates/models/role_permission.rb +9 -0
  87. data/lib/generators/easy_admin/permissions/templates/models/user_role.rb +9 -0
  88. data/lib/generators/easy_admin/permissions/templates/policies/application_policy.rb +47 -0
  89. data/lib/generators/easy_admin/permissions/templates/policies/user_policy.rb +36 -0
  90. data/lib/generators/easy_admin/permissions/templates/seeds/permissions.rb +89 -0
  91. metadata +62 -5
  92. 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
- before_action :load_resource_class
6
- before_action :load_record, only: [:show, :edit, :update, :destroy, :edit_field, :update_field, :belongs_to_reattach, :belongs_to_edit_attached, :update_belongs_to_attached, :versions, :revert_version, :version_diff]
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
- # Set up data needed for both HTML and turbo stream
10
- sort_field = params[:sort]
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
- render plain: EasyAdmin::Resources::IndexFrameComponent.new(
77
- resource_class: @resource_class,
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
- # For infinite scroll - append new rows to existing table
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