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.
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 +160 -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
@@ -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