easy-admin-rails 0.1.14 → 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 (93) 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/components/easy_admin/versions/diff_modal_component.rb +5 -1
  13. data/app/controllers/easy_admin/application_controller.rb +131 -1
  14. data/app/controllers/easy_admin/batch_actions_controller.rb +27 -0
  15. data/app/controllers/easy_admin/concerns/belongs_to_editing.rb +201 -0
  16. data/app/controllers/easy_admin/concerns/inline_field_editing.rb +297 -0
  17. data/app/controllers/easy_admin/concerns/resource_authorization.rb +55 -0
  18. data/app/controllers/easy_admin/concerns/resource_filtering.rb +178 -0
  19. data/app/controllers/easy_admin/concerns/resource_loading.rb +149 -0
  20. data/app/controllers/easy_admin/concerns/resource_pagination.rb +135 -0
  21. data/app/controllers/easy_admin/dashboard_controller.rb +2 -1
  22. data/app/controllers/easy_admin/dashboards_controller.rb +6 -40
  23. data/app/controllers/easy_admin/resources_controller.rb +13 -762
  24. data/app/controllers/easy_admin/row_actions_controller.rb +25 -0
  25. data/app/helpers/easy_admin/fields_helper.rb +61 -9
  26. data/app/javascript/easy_admin/controllers/event_emitter_controller.js +2 -4
  27. data/app/javascript/easy_admin/controllers/infinite_scroll_controller.js +0 -10
  28. data/app/javascript/easy_admin/controllers/jsoneditor_controller.js +1 -4
  29. data/app/javascript/easy_admin/controllers/permission_toggle_controller.js +227 -0
  30. data/app/javascript/easy_admin/controllers/role_preview_controller.js +93 -0
  31. data/app/javascript/easy_admin/controllers/select_field_controller.js +1 -2
  32. data/app/javascript/easy_admin/controllers/settings_button_controller.js +1 -2
  33. data/app/javascript/easy_admin/controllers/settings_sidebar_controller.js +1 -4
  34. data/app/javascript/easy_admin/controllers/turbo_stream_redirect.js +0 -2
  35. data/app/javascript/easy_admin/controllers.js +5 -1
  36. data/app/models/easy_admin/admin_user.rb +6 -0
  37. data/app/policies/admin_user_policy.rb +36 -0
  38. data/app/policies/application_policy.rb +83 -0
  39. data/app/views/easy_admin/application/authorization_failure.turbo_stream.erb +8 -0
  40. data/app/views/easy_admin/dashboards/card.html.erb +5 -0
  41. data/app/views/easy_admin/dashboards/card.turbo_stream.erb +7 -0
  42. data/app/views/easy_admin/dashboards/card_error.html.erb +3 -0
  43. data/app/views/easy_admin/dashboards/card_error.turbo_stream.erb +5 -0
  44. data/app/views/easy_admin/dashboards/show.turbo_stream.erb +7 -0
  45. data/app/views/easy_admin/resources/belongs_to_edit_attached.html.erb +6 -0
  46. data/app/views/easy_admin/resources/belongs_to_edit_attached.turbo_stream.erb +8 -0
  47. data/app/views/easy_admin/resources/belongs_to_reattach.html.erb +5 -0
  48. data/app/views/easy_admin/resources/edit.html.erb +1 -1
  49. data/app/views/easy_admin/resources/edit_field.html.erb +5 -0
  50. data/app/views/easy_admin/resources/edit_field.turbo_stream.erb +7 -0
  51. data/app/views/easy_admin/resources/index.html.erb +1 -1
  52. data/app/views/easy_admin/resources/index_frame.html.erb +8 -142
  53. data/app/views/easy_admin/resources/update_belongs_to_attached.turbo_stream.erb +25 -0
  54. data/app/views/layouts/easy_admin/application.html.erb +15 -2
  55. data/config/initializers/easy_admin_permissions.rb +73 -0
  56. data/db/seeds/easy_admin_permissions.rb +121 -0
  57. data/lib/easy-admin-rails.rb +2 -0
  58. data/lib/easy_admin/permissions/component.rb +168 -0
  59. data/lib/easy_admin/permissions/configuration.rb +37 -0
  60. data/lib/easy_admin/permissions/controller.rb +164 -0
  61. data/lib/easy_admin/permissions/dsl.rb +180 -0
  62. data/lib/easy_admin/permissions/models.rb +44 -0
  63. data/lib/easy_admin/permissions/permission_denied_component.rb +121 -0
  64. data/lib/easy_admin/permissions/resource_permissions.rb +231 -0
  65. data/lib/easy_admin/permissions/role_definition.rb +45 -0
  66. data/lib/easy_admin/permissions/role_denied_component.rb +159 -0
  67. data/lib/easy_admin/permissions/role_dsl.rb +73 -0
  68. data/lib/easy_admin/permissions/user_extensions.rb +129 -0
  69. data/lib/easy_admin/permissions.rb +113 -0
  70. data/lib/easy_admin/resource/base.rb +119 -0
  71. data/lib/easy_admin/resource/configuration.rb +148 -0
  72. data/lib/easy_admin/resource/dsl.rb +117 -0
  73. data/lib/easy_admin/resource/field_registry.rb +189 -0
  74. data/lib/easy_admin/resource/form_builder.rb +123 -0
  75. data/lib/easy_admin/resource/layout_builder.rb +249 -0
  76. data/lib/easy_admin/resource/scope_manager.rb +252 -0
  77. data/lib/easy_admin/resource/show_builder.rb +359 -0
  78. data/lib/easy_admin/resource.rb +8 -835
  79. data/lib/easy_admin/resource_modules.rb +11 -0
  80. data/lib/easy_admin/version.rb +1 -1
  81. data/lib/generators/easy_admin/permissions/install_generator.rb +90 -0
  82. data/lib/generators/easy_admin/permissions/templates/initializers/permissions.rb +37 -0
  83. data/lib/generators/easy_admin/permissions/templates/migrations/create_permission_tables.rb +27 -0
  84. data/lib/generators/easy_admin/permissions/templates/migrations/update_users_for_permissions.rb +6 -0
  85. data/lib/generators/easy_admin/permissions/templates/models/permission.rb +9 -0
  86. data/lib/generators/easy_admin/permissions/templates/models/role.rb +9 -0
  87. data/lib/generators/easy_admin/permissions/templates/models/role_permission.rb +9 -0
  88. data/lib/generators/easy_admin/permissions/templates/models/user_role.rb +9 -0
  89. data/lib/generators/easy_admin/permissions/templates/policies/application_policy.rb +47 -0
  90. data/lib/generators/easy_admin/permissions/templates/policies/user_policy.rb +36 -0
  91. data/lib/generators/easy_admin/permissions/templates/seeds/permissions.rb +89 -0
  92. metadata +62 -5
  93. data/db/migrate/20250101000001_create_easy_admin_admin_users.rb +0 -45
@@ -1,841 +1,14 @@
1
1
  module EasyAdmin
2
2
  class Resource
3
- include EasyAdmin::Versioning
4
-
5
- class_attribute :model_class_name, :resource_name, :fields_config, :form_tabs_config, :show_layout_config, :scopes_config, :pagination_config, :includes_config, :batch_actions_config, :batch_actions_enabled, :row_actions_config, :custom_title
6
-
7
- def self.inherited(subclass)
8
- super
9
- # Automatically set model class from resource name
10
- subclass.resource_name = subclass.name.demodulize.gsub(/Resource$/, '')
11
- subclass.model_class_name = subclass.resource_name
12
- subclass.fields_config = []
13
- subclass.form_tabs_config = []
14
- subclass.show_layout_config = []
15
- subclass.scopes_config = []
16
- subclass.pagination_config = {
17
- type: :standard,
18
- per_page: 20,
19
- max_per_page: 100
20
- }
21
- subclass.includes_config = {
22
- index: [],
23
- show: [],
24
- form: []
25
- }
26
- subclass.batch_actions_config = []
27
- subclass.batch_actions_enabled = false
28
-
29
- # Register the resource
30
- EasyAdmin::ResourceRegistry.register(subclass)
31
- end
3
+ include EasyAdmin::ResourceModules::Base
32
4
 
33
- # DSL for setting custom model name
34
- def self.model(model_name)
35
- self.model_class_name = model_name.to_s
36
- end
37
-
38
- # DSL for setting custom title
39
- def self.resource_title(title_name)
40
- self.custom_title = title_name.to_s
41
- end
5
+ # Legacy methods - kept for backward compatibility
6
+ # These delegate to the appropriate modules
42
7
 
43
- # DSL for defining fields
44
- def self.field(name, type = :string, **options)
45
- self.fields_config += [{
46
- name: name,
47
- type: type,
48
- label: options[:label] || name.to_s.humanize,
49
- sortable: options.fetch(:sortable, true),
50
- searchable: options.fetch(:searchable, false),
51
- filterable: options.fetch(:filterable, false),
52
- required: options.fetch(:required, false),
53
- readonly: options.fetch(:readonly, false),
54
- format: options[:format],
55
- options: options[:options], # for select fields
56
- multiple: options[:multiple], # for select fields
57
- placeholder: options[:placeholder], # for select fields
58
- suggest: options[:suggest], # for dynamic option loading
59
- display_method: options[:display_method], # for association fields
60
- help_text: options[:help_text],
61
- editable: options[:editable],
62
- association: options[:association],
63
- foreign_key: options[:foreign_key] # for belongs_to fields
64
- }]
65
- end
66
-
67
- # Convenience methods for common field types
68
- def self.id_field(**options)
69
- field(:id, :number, readonly: true, **options)
70
- end
71
-
72
- def self.text_field(name, **options)
73
- field(name, :string, **options)
74
- end
75
-
76
- def self.textarea_field(name, **options)
77
- field(name, :text, **options)
78
- end
79
-
80
- def self.number_field(name, **options)
81
- field(name, :number, **options)
82
- end
83
-
84
- def self.email_field(name, **options)
85
- field(name, :email, **options)
86
- end
87
-
88
- def self.date_field(name, **options)
89
- field(name, :date, **options)
90
- end
91
-
92
- def self.datetime_field(name, **options)
93
- field(name, :datetime, **options)
94
- end
95
-
96
- def self.boolean_field(name, **options)
97
- field(name, :boolean, **options)
98
- end
99
-
100
- def self.select_field(name, options:, **other_options)
101
- field(name, :select, options: options, **other_options)
102
- end
103
-
104
- def self.belongs_to_field(name, **options)
105
- field(name, :belongs_to, **options)
106
- end
107
-
108
- def self.has_many_field(name, **options)
109
- field(name, :has_many, **options)
110
- end
111
-
112
- def self.file_field(name, **options)
113
- field(name, :file, **options)
114
- end
115
-
116
- def self.json_field(name, **options)
117
- field(name, :json, **options)
118
- end
119
-
120
- # Scope definition
121
- def self.scope(name, **options)
122
- scope_config = {
123
- name: name,
124
- label: options[:label] || name.to_s.humanize,
125
- scope_method: options[:scope] || name,
126
- default: options.fetch(:default, false),
127
- icon: options[:icon],
128
- color: options[:color] || 'blue',
129
- count: options.fetch(:show_count, true)
130
- }
131
-
132
- self.scopes_config += [scope_config]
133
- end
134
-
135
- # Pagination configuration
136
- def self.pagination(type: :standard, per_page: 20, **options)
137
- self.pagination_config = {
138
- type: type,
139
- per_page: per_page,
140
- max_per_page: options[:max_per_page] || 100,
141
- infinite_scroll: options[:infinite_scroll] || false,
142
- countless: options[:countless] || false
143
- }
144
- end
145
-
146
- # Configure eager loading for associations
147
- def self.includes(associations, actions: [:index, :show, :form])
148
- actions = Array(actions)
149
-
150
- actions.each do |action|
151
- if self.includes_config.key?(action)
152
- self.includes_config = self.includes_config.merge(
153
- action => Array(associations)
154
- )
155
- end
156
- end
157
- end
158
-
159
- # Convenience methods for pagination configuration
160
- def self.per_page(count)
161
- pagination(per_page: count)
162
- end
163
-
164
- def self.infinite_scroll(**options)
165
- pagination(type: :infinite_scroll, infinite_scroll: true, **options)
166
- end
167
-
168
- def self.countless_pagination(**options)
169
- pagination(countless: true, **options)
170
- end
171
-
172
- # Get the associated model class
173
- def self.model_class
174
- model_class_name.constantize
175
- rescue NameError
176
- raise "Model class #{model_class_name} not found. Make sure the model exists."
177
- end
178
-
179
- # Get resource title/name
180
- def self.title
181
- return custom_title.pluralize if custom_title.present?
182
- resource_name.underscore.humanize.pluralize
183
- end
184
-
185
- def self.singular_title
186
- return custom_title.singularize if custom_title.present?
187
- resource_name.underscore.humanize
188
- end
189
-
190
- # Get fields for different contexts
191
- def self.index_fields
192
- fields_config.select { |field| ![:text, :has_many, :file].include?(field[:type]) }
193
- end
194
-
195
- def self.sortable_fields
196
- fields_config.select { |field| field[:sortable] }
197
- end
198
-
199
- def self.filterable_fields
200
- fields_config.select { |field| field[:filterable] }
201
- end
202
-
203
- def self.show_fields
204
- fields_config
205
- end
206
-
207
- def self.form_fields
208
- if form_tabs_config.any?
209
- # If tabs are configured, return fields from all tabs
210
- form_tabs_config.flat_map { |tab| tab[:fields] }.reject { |field| field[:readonly] }
211
- else
212
- # Default behavior: return all non-readonly fields
213
- fields_config.reject { |field| field[:readonly] }
214
- end
215
- end
216
-
217
- def self.form_tabs
218
- form_tabs_config
219
- end
220
-
221
- def self.has_form_tabs?
222
- form_tabs_config.any?
223
- end
224
-
225
- # DSL for defining form with tabs
226
- def self.form(&block)
227
- form_builder = FormBuilder.new(self)
228
- form_builder.instance_eval(&block)
229
- end
230
-
231
- # DSL for defining show layout
232
- def self.show(&block)
233
- show_builder = ShowBuilder.new(self)
234
- show_builder.instance_eval(&block)
235
- end
236
-
237
- def self.show_layout
238
- show_layout_config
239
- end
240
-
241
- def self.has_show_layout?
242
- show_layout_config.any?
243
- end
244
-
245
- def self.scopes
246
- scopes_config
247
- end
248
-
249
- def self.has_scopes?
250
- scopes_config.any?
251
- end
252
-
253
- def self.default_scope
254
- scopes_config.find { |scope| scope[:default] } || scopes_config.first
255
- end
256
-
257
- # Pagination helper methods
258
- def self.items_per_page
259
- pagination_config[:per_page]
260
- end
261
-
262
- def self.max_items_per_page
263
- pagination_config[:max_per_page]
264
- end
265
-
266
- def self.pagination_type
267
- pagination_config[:type]
268
- end
269
-
270
- def self.infinite_scroll_enabled?
271
- pagination_config[:infinite_scroll]
272
- end
273
-
274
- def self.countless_enabled?
275
- pagination_config[:countless]
276
- end
277
-
278
- # Includes configuration helpers
279
- def self.index_includes
280
- includes_config[:index] || []
281
- end
282
-
283
- def self.show_includes
284
- includes_config[:show] || []
285
- end
286
-
287
- def self.form_includes
288
- includes_config[:form] || []
289
- end
290
-
291
- class FormBuilder
292
- def initialize(resource_class)
293
- @resource_class = resource_class
294
- @current_tab = nil
295
- end
296
-
297
- def tab(name = "General", **options, &block)
298
- tab_config = {
299
- name: name,
300
- label: options[:label] || name,
301
- icon: options[:icon],
302
- fields: []
303
- }
304
-
305
- @current_tab = tab_config
306
- @resource_class.form_tabs_config << tab_config
307
-
308
- instance_eval(&block) if block_given?
309
- @current_tab = nil
310
- end
311
-
312
- # Field definition methods within tabs
313
- def field(name, type = :string, **options)
314
- field_config = {
315
- name: name,
316
- type: type,
317
- label: options[:label] || name.to_s.humanize,
318
- sortable: options.fetch(:sortable, true),
319
- searchable: options.fetch(:searchable, false),
320
- filterable: options.fetch(:filterable, false),
321
- required: options.fetch(:required, false),
322
- readonly: options.fetch(:readonly, false),
323
- format: options[:format],
324
- options: options[:options],
325
- multiple: options[:multiple],
326
- placeholder: options[:placeholder],
327
- help_text: options[:help_text],
328
- editable: options[:editable],
329
- association: options[:association],
330
- foreign_key: options[:foreign_key]
331
- }
332
-
333
- if @current_tab
334
- @current_tab[:fields] << field_config
335
- end
336
-
337
- # Also add to main fields_config for compatibility
338
- @resource_class.fields_config << field_config unless @resource_class.fields_config.any? { |f| f[:name] == name }
339
- end
340
-
341
- def text_field(name, **options)
342
- field(name, :string, **options)
343
- end
344
-
345
- def textarea_field(name, **options)
346
- field(name, :text, **options)
347
- end
348
-
349
- def number_field(name, **options)
350
- field(name, :number, **options)
351
- end
352
-
353
- def email_field(name, **options)
354
- field(name, :email, **options)
355
- end
356
-
357
- def date_field(name, **options)
358
- field(name, :date, **options)
359
- end
360
-
361
- def datetime_field(name, **options)
362
- field(name, :datetime, **options)
363
- end
364
-
365
- def boolean_field(name, **options)
366
- field(name, :boolean, **options)
367
- end
368
-
369
- def select_field(name, options:, **other_options)
370
- field(name, :select, options: options, **other_options)
371
- end
372
-
373
- def belongs_to_field(name, **options)
374
- field(name, :belongs_to, **options)
375
- end
376
-
377
- def has_many_field(name, **options)
378
- field(name, :has_many, **options)
379
- end
380
-
381
- def file_field(name, **options)
382
- field(name, :file, **options)
383
- end
384
-
385
- def json_field(name, **options)
386
- field(name, :json, **options)
387
- end
388
- end
389
-
390
- class ShowBuilder
391
- PREDEFINED_COLORS = %w[primary secondary success danger warning info light dark white transparent].freeze
392
- COLUMN_SIZES = [1, 2, 3, 4, 6, 8, 12].freeze
393
- SPACING_OPTIONS = %w[none small medium large].freeze
394
- PADDING_OPTIONS = %w[none small medium large].freeze
395
- HEADING_SIZES = %w[small medium large].freeze
396
- DIVIDER_STYLES = %w[default dashed dotted thick].freeze
397
- SHADOW_OPTIONS = %w[none small medium large].freeze
398
-
399
- def initialize(resource_class)
400
- @resource_class = resource_class
401
- @context_stack = []
402
- @current_row = nil
403
- @current_column = nil
404
- @current_card = nil
405
- @current_tab_container = nil
406
- @current_tab = nil
407
- end
408
-
409
- # Layout methods
410
- def row(columns: 1, spacing: "medium", &block)
411
- validate_spacing!(spacing)
412
-
413
- row_config = {
414
- type: :row,
415
- columns_count: columns,
416
- spacing: spacing,
417
- columns: []
418
- }
419
-
420
- # Save current context
421
- push_context
422
- @current_row = row_config
423
-
424
- # Add to parent context
425
- if @current_column
426
- @current_column[:elements] << row_config
427
- else
428
- @resource_class.show_layout_config << row_config
429
- end
430
-
431
- instance_eval(&block) if block_given?
432
-
433
- # Restore context
434
- pop_context
435
- end
436
-
437
- def column(size: nil, &block)
438
- raise "column must be called within a row block" unless @current_row
439
-
440
- # Auto-calculate size if not provided
441
- calculated_size = size || (12 / [@current_row[:columns_count], 1].max)
442
- validate_column_size!(calculated_size)
443
-
444
- column_config = {
445
- type: :column,
446
- size: calculated_size,
447
- elements: []
448
- }
449
-
450
- # Save current context
451
- push_context
452
- @current_column = column_config
453
- @current_row[:columns] << column_config
454
-
455
- instance_eval(&block) if block_given?
456
-
457
- # Restore context
458
- pop_context
459
- end
460
-
461
- # Card methods
462
- def card(title: nil, color: "light", padding: "medium", shadow: "small", &block)
463
- raise "card must be called within a column block" unless @current_column
464
- validate_color!(color)
465
- validate_padding!(padding)
466
- validate_shadow!(shadow)
467
-
468
- card_config = {
469
- type: :card,
470
- title: title,
471
- color: color,
472
- padding: padding,
473
- shadow: shadow,
474
- elements: []
475
- }
476
-
477
- # Save current context
478
- push_context
479
- @current_card = card_config
480
- @current_column[:elements] << card_config
481
-
482
- instance_eval(&block) if block_given?
483
-
484
- # Restore context
485
- pop_context
486
- end
487
-
488
- def metric_card(title:, value:, color: "primary", icon: nil, trend: nil)
489
- raise "metric_card must be called within a column block" unless @current_column
490
- validate_color!(color)
491
-
492
- metric_config = {
493
- type: :metric_card,
494
- title: title,
495
- value: value,
496
- icon: icon,
497
- color: color,
498
- trend: trend
499
- }
500
-
501
- @current_column[:elements] << metric_config
502
- end
503
-
504
- def chart_card(title:, chart_type:, **options, &block)
505
- raise "chart_card must be called within a column block" unless @current_column
506
-
507
- chart_config = {
508
- type: :chart_card,
509
- title: title,
510
- chart_type: chart_type, # :line, :bar, :pie, :donut, :area
511
- height: options[:height] || 350,
512
- data_source: options[:data_source],
513
- css_classes: options[:class] || "card",
514
- config: {}
515
- }
516
-
517
- if block_given?
518
- chart_builder = ChartBuilder.new
519
- chart_builder.instance_eval(&block)
520
- chart_config[:config] = chart_builder.config
521
- end
522
-
523
- @current_column[:elements] << chart_config
524
- end
525
-
526
- # Tab methods
527
- def tabs(**options, &block)
528
- container = @current_card || @current_column
529
- raise "tabs must be called within a card or column block" unless container
530
-
531
- tabs_config = {
532
- type: :tabs,
533
- css_classes: options[:class] || "tabs-menu",
534
- tabs: []
535
- }
536
-
537
- # Save current context
538
- push_context
539
- @current_tab_container = tabs_config
540
- container[:elements] << tabs_config
541
-
542
- instance_eval(&block) if block_given?
543
-
544
- # Restore context
545
- pop_context
546
- end
547
-
548
- def tab(name, **options, &block)
549
- raise "tab must be called within a tabs block" unless @current_tab_container
550
-
551
- tab_config = {
552
- name: name,
553
- label: options[:label] || name,
554
- icon: options[:icon],
555
- active: options[:active] || false,
556
- elements: []
557
- }
558
-
559
- # Save current context
560
- push_context
561
- @current_tab = tab_config
562
- @current_tab_container[:tabs] << tab_config
563
-
564
- instance_eval(&block) if block_given?
565
-
566
- # Restore context
567
- pop_context
568
- end
569
-
570
- # Field display methods
571
- def field(name, **options)
572
- container = @current_tab || @current_card || @current_column
573
- raise "field must be called within a tab, card, or column block" unless container
574
-
575
- # Allow hiding labels with label: false
576
- label_value = if options.key?(:label) && options[:label] == false
577
- nil
578
- else
579
- options[:label] || name.to_s.humanize
580
- end
581
-
582
- field_config = {
583
- type: :field,
584
- name: name,
585
- label: label_value,
586
- field_type: options[:field_type] || :default,
587
- format: options[:format],
588
- css_classes: options[:class],
589
- show_label: options.fetch(:show_label, true)
590
- }
591
-
592
- container[:elements] << field_config
593
- end
594
-
595
- def fields(*field_names, **options)
596
- field_names.each do |name|
597
- field(name, **options)
598
- end
599
- end
600
-
601
- # Content methods
602
- def content(html = nil, **options, &block)
603
- container = @current_tab || @current_card || @current_column
604
- raise "content must be called within a tab, card, or column block" unless container
605
-
606
- content_config = {
607
- type: :content,
608
- html: html,
609
- css_classes: options[:class],
610
- block: block
611
- }
612
-
613
- container[:elements] << content_config
614
- end
615
-
616
- def heading(text, level: 2, size: "medium")
617
- container = @current_tab || @current_card || @current_column
618
- raise "heading must be called within a tab, card, or column block" unless container
619
-
620
- validate_heading_level!(level)
621
- validate_heading_size!(size)
622
-
623
- heading_config = {
624
- type: :heading,
625
- text: text,
626
- level: level,
627
- size: size
628
- }
629
-
630
- container[:elements] << heading_config
631
- end
632
-
633
- def divider(style: "default")
634
- container = @current_tab || @current_card || @current_column
635
- raise "divider must be called within a tab, card, or column block" unless container
636
-
637
- validate_divider_style!(style)
638
-
639
- divider_config = {
640
- type: :divider,
641
- style: style
642
- }
643
-
644
- container[:elements] << divider_config
645
- end
646
-
647
- private
648
-
649
- def validate_color!(color)
650
- return if PREDEFINED_COLORS.include?(color.to_s)
651
- raise ArgumentError, "Invalid color '#{color}'. Available colors: #{PREDEFINED_COLORS.join(', ')}"
652
- end
653
-
654
- def validate_column_size!(size)
655
- return if COLUMN_SIZES.include?(size)
656
- raise ArgumentError, "Invalid column size '#{size}'. Available sizes: #{COLUMN_SIZES.join(', ')}"
657
- end
658
-
659
- def validate_spacing!(spacing)
660
- return if SPACING_OPTIONS.include?(spacing.to_s)
661
- raise ArgumentError, "Invalid spacing '#{spacing}'. Available options: #{SPACING_OPTIONS.join(', ')}"
662
- end
663
-
664
- def validate_padding!(padding)
665
- return if PADDING_OPTIONS.include?(padding.to_s)
666
- raise ArgumentError, "Invalid padding '#{padding}'. Available options: #{PADDING_OPTIONS.join(', ')}"
667
- end
668
-
669
- def validate_shadow!(shadow)
670
- return if SHADOW_OPTIONS.include?(shadow.to_s)
671
- raise ArgumentError, "Invalid shadow '#{shadow}'. Available options: #{SHADOW_OPTIONS.join(', ')}"
672
- end
673
-
674
- def validate_heading_level!(level)
675
- return if (1..6).include?(level)
676
- raise ArgumentError, "Invalid heading level '#{level}'. Must be between 1 and 6"
677
- end
678
-
679
- def validate_heading_size!(size)
680
- return if HEADING_SIZES.include?(size.to_s)
681
- raise ArgumentError, "Invalid heading size '#{size}'. Available sizes: #{HEADING_SIZES.join(', ')}"
682
- end
683
-
684
- def validate_divider_style!(style)
685
- return if DIVIDER_STYLES.include?(style.to_s)
686
- raise ArgumentError, "Invalid divider style '#{style}'. Available styles: #{DIVIDER_STYLES.join(', ')}"
687
- end
688
-
689
- def push_context
690
- @context_stack.push({
691
- row: @current_row,
692
- column: @current_column,
693
- card: @current_card,
694
- tab_container: @current_tab_container,
695
- tab: @current_tab
696
- })
697
- end
698
-
699
- def pop_context
700
- if @context_stack.any?
701
- context = @context_stack.pop
702
- @current_row = context[:row]
703
- @current_column = context[:column]
704
- @current_card = context[:card]
705
- @current_tab_container = context[:tab_container]
706
- @current_tab = context[:tab]
707
- else
708
- @current_row = nil
709
- @current_column = nil
710
- @current_card = nil
711
- @current_tab_container = nil
712
- @current_tab = nil
713
- end
714
- end
715
- end
716
-
717
- class ChartBuilder
718
- attr_reader :config
719
-
720
- def initialize
721
- @config = {}
722
- end
723
-
724
- def series(data)
725
- @config[:series] = data
726
- end
727
-
728
- def categories(data)
729
- @config[:categories] = data
730
- end
731
-
732
- def colors(data)
733
- @config[:colors] = data
734
- end
735
-
736
- def title(text)
737
- @config[:title] = text
738
- end
739
-
740
- def subtitle(text)
741
- @config[:subtitle] = text
742
- end
743
- end
744
-
745
- # Query scopes
746
- def self.all_records(sort_field: nil, sort_direction: 'asc')
747
- records = model_class.all
748
-
749
- if sort_field.present? && sortable_fields.any? { |f| f[:name].to_s == sort_field.to_s }
750
- records = records.order("#{sort_field} #{sort_direction}")
751
- end
752
-
753
- records
754
- end
755
-
756
- def self.find_record(id)
757
- model_class.find(id)
758
- end
759
-
760
- def self.search_records(query, sort_field: nil, sort_direction: 'asc', records: nil)
761
- records ||= model_class.all
762
- searchable_fields = fields_config.select { |field| field[:searchable] }
763
-
764
- if searchable_fields.any? && query.present?
765
- conditions = searchable_fields.map do |field|
766
- "#{field[:name]} ILIKE ?"
767
- end.join(' OR ')
768
-
769
- values = Array.new(searchable_fields.length, "%#{query}%")
770
- records = records.where(conditions, *values)
771
- end
772
-
773
- if sort_field.present? && sortable_fields.any? { |f| f[:name].to_s == sort_field.to_s }
774
- records = records.order("#{sort_field} #{sort_direction}")
775
- end
776
-
777
- records
778
- end
779
-
780
- # Batch Actions DSL
781
- def self.enable_batch_actions
782
- self.batch_actions_enabled = true
783
- end
784
-
785
- def self.batch_action(action_class, options = {})
786
- self.batch_actions_config = batch_actions_config + [{
787
- class: action_class,
788
- condition: options[:if]
789
- }]
790
- end
791
-
792
- def self.available_batch_actions(context = {})
793
- return [] unless batch_actions_enabled
794
-
795
- batch_actions_config.select do |config|
796
- condition = config[:condition]
797
- condition.nil? || (condition.respond_to?(:call) ? context.instance_eval(&condition) : condition)
798
- end.map { |config| config[:class] }
799
- end
800
-
801
- # Row Actions DSL (single record actions)
802
- def self.row_action(action_class, options = {})
803
- self.row_actions_config ||= []
804
-
805
- self.row_actions_config = row_actions_config + [{
806
- class: action_class,
807
- condition: options[:if]
808
- }]
809
- end
810
-
811
- def self.row_actions
812
- row_actions_config || []
813
- end
814
-
815
- def self.has_row_actions?
816
- row_actions_config.present?
817
- end
818
-
819
- # Route helpers
820
- def self.route_key
821
- resource_name.underscore.pluralize
822
- end
823
-
824
- def self.param_key
825
- resource_name.underscore
826
- end
827
-
828
- # Versioning DSL - Simple API for enabling version viewing
829
- def self.versioning(enabled = true, **options)
830
- if enabled
831
- enable_versioning(options)
832
- else
833
- self.versioning_enabled = false
834
- end
835
- end
836
-
837
- def self.has_versioning?
838
- model_has_versions?
839
- end
8
+ # Deprecated classes - kept for backward compatibility
9
+ # Use EasyAdmin::ResourceModules::FormBuilder instead
10
+ FormBuilder = EasyAdmin::ResourceModules::FormBuilder
11
+ ShowBuilder = EasyAdmin::ResourceModules::ShowBuilder
12
+ ChartBuilder = EasyAdmin::ResourceModules::ChartBuilder
840
13
  end
841
14
  end