easy-admin-rails 0.2.6 → 0.2.7

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 (57) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/builds/easy_admin.base.js +7 -0
  3. data/app/assets/builds/easy_admin.base.js.map +2 -2
  4. data/app/assets/builds/easy_admin.css +207 -35
  5. data/app/components/easy_admin/fields/form/belongs_to_component.rb +0 -1
  6. data/app/components/easy_admin/form_layout_component.rb +553 -0
  7. data/app/components/easy_admin/permissions/user_role_permissions_component.rb +1 -3
  8. data/app/components/easy_admin/show_layout_component.rb +694 -24
  9. data/app/controllers/easy_admin/application_controller.rb +0 -5
  10. data/app/controllers/easy_admin/batch_actions_controller.rb +0 -1
  11. data/app/controllers/easy_admin/concerns/inline_field_editing.rb +4 -11
  12. data/app/controllers/easy_admin/concerns/resource_loading.rb +10 -9
  13. data/app/controllers/easy_admin/concerns/resource_pagination.rb +3 -0
  14. data/app/controllers/easy_admin/dashboards_controller.rb +0 -1
  15. data/app/controllers/easy_admin/resources_controller.rb +1 -5
  16. data/app/controllers/easy_admin/row_actions_controller.rb +1 -4
  17. data/app/helpers/easy_admin/fields_helper.rb +8 -22
  18. data/app/javascript/easy_admin/controllers/infinite_scroll_controller.js +12 -0
  19. data/app/views/easy_admin/resources/edit.html.erb +2 -2
  20. data/app/views/easy_admin/resources/new.html.erb +2 -2
  21. data/app/views/easy_admin/resources/show.html.erb +3 -1
  22. data/lib/easy_admin/field.rb +3 -2
  23. data/lib/easy_admin/layouts/builders/base_layout_builder.rb +245 -0
  24. data/lib/easy_admin/layouts/builders/form_layout_builder.rb +208 -0
  25. data/lib/easy_admin/layouts/builders/index_layout_builder.rb +22 -0
  26. data/lib/easy_admin/layouts/builders/show_layout_builder.rb +199 -0
  27. data/lib/easy_admin/layouts/dsl.rb +200 -0
  28. data/lib/easy_admin/layouts/layout_context.rb +189 -0
  29. data/lib/easy_admin/layouts/nodes/base_node.rb +88 -0
  30. data/lib/easy_admin/layouts/nodes/divider.rb +27 -0
  31. data/lib/easy_admin/layouts/nodes/field_node.rb +57 -0
  32. data/lib/easy_admin/layouts/nodes/grid.rb +60 -0
  33. data/lib/easy_admin/layouts/nodes/render_node.rb +41 -0
  34. data/lib/easy_admin/layouts/nodes/root.rb +25 -0
  35. data/lib/easy_admin/layouts/nodes/section.rb +46 -0
  36. data/lib/easy_admin/layouts/nodes/spacer.rb +17 -0
  37. data/lib/easy_admin/layouts/nodes/stubs.rb +109 -0
  38. data/lib/easy_admin/layouts/nodes/tab.rb +40 -0
  39. data/lib/easy_admin/layouts/nodes/tabs.rb +40 -0
  40. data/lib/easy_admin/layouts.rb +28 -0
  41. data/lib/easy_admin/permissions/resource_permissions.rb +1 -5
  42. data/lib/easy_admin/resource/base.rb +2 -2
  43. data/lib/easy_admin/resource/dsl.rb +2 -11
  44. data/lib/easy_admin/resource/field_registry.rb +58 -2
  45. data/lib/easy_admin/resource.rb +0 -9
  46. data/lib/easy_admin/resource_modules.rb +21 -4
  47. data/lib/easy_admin/version.rb +1 -1
  48. data/lib/generators/easy_admin/permissions/install_generator.rb +0 -10
  49. data/lib/generators/easy_admin/permissions/templates/migrations/create_permission_tables.rb +33 -3
  50. metadata +21 -9
  51. data/lib/easy_admin/resource/form_builder.rb +0 -123
  52. data/lib/easy_admin/resource/layout_builder.rb +0 -249
  53. data/lib/easy_admin/resource/show_builder.rb +0 -359
  54. data/lib/generators/easy_admin/permissions/templates/migrations/update_users_for_permissions.rb +0 -6
  55. data/lib/generators/easy_admin/rbac/rbac_generator.rb +0 -244
  56. data/lib/generators/easy_admin/rbac/templates/add_rbac_to_admin_users.rb +0 -23
  57. data/lib/generators/easy_admin/rbac/templates/super_admin.rb +0 -34
@@ -1,12 +1,16 @@
1
+ require 'securerandom'
2
+
1
3
  module EasyAdmin
2
4
  class ShowLayoutComponent < Phlex::HTML
5
+ include EasyAdmin::FieldsHelper
6
+
3
7
  def initialize(resource_class:, record:)
4
8
  @resource_class = resource_class
5
9
  @record = record
6
10
  end
7
11
 
8
12
  def view_template
9
- if @resource_class.has_show_layout?
13
+ if @resource_class.has_custom_show_layout?
10
14
  render_custom_layout
11
15
  else
12
16
  render_default_layout
@@ -16,16 +20,633 @@ module EasyAdmin
16
20
  private
17
21
 
18
22
  def render_custom_layout
19
- @resource_class.show_layout.each do |element|
20
- render_element(element)
23
+ layout_type, layout_content = @resource_class.show_layout_definition
24
+
25
+ case layout_type
26
+ when :component
27
+ render layout_content.new(record: @record)
28
+ when :ast
29
+ render_ast_layout(layout_content)
30
+ end
31
+ end
32
+
33
+ def render_ast_layout(root_node)
34
+ render_node(root_node)
35
+ end
36
+
37
+ def render_node(node)
38
+ # Create layout context for the node
39
+ context = EasyAdmin::Layouts::LayoutContext.new(
40
+ record: @record,
41
+ resource_class: @resource_class,
42
+ current_user: nil, # TODO: get from controller
43
+ view_context: self
44
+ )
45
+
46
+ # Check if node is visible
47
+ visible = node.visible?(context)
48
+ return unless visible
49
+
50
+
51
+ case node
52
+ when EasyAdmin::Layouts::Nodes::Root
53
+ render_root_node(node)
54
+ when EasyAdmin::Layouts::Nodes::Row
55
+ render_row_node(node)
56
+ when EasyAdmin::Layouts::Nodes::Column
57
+ render_column_node(node)
58
+ when EasyAdmin::Layouts::Nodes::Card
59
+ render_card_node(node)
60
+ when EasyAdmin::Layouts::Nodes::FieldNode
61
+ render_field_node(node)
62
+ when EasyAdmin::Layouts::Nodes::Heading
63
+ render_heading_node(node)
64
+ when EasyAdmin::Layouts::Nodes::Divider
65
+ render_divider_node(node)
66
+ when EasyAdmin::Layouts::Nodes::MetricCard
67
+ render_metric_card_node(node)
68
+ when EasyAdmin::Layouts::Nodes::Content
69
+ render_content_node(node, context)
70
+ when EasyAdmin::Layouts::Nodes::Tabs
71
+ render_tabs_node(node)
72
+ when EasyAdmin::Layouts::Nodes::Tab
73
+ render_tab_node(node)
74
+ when EasyAdmin::Layouts::Nodes::Section
75
+ render_section_node(node)
76
+ when EasyAdmin::Layouts::Nodes::Grid
77
+ render_grid_node(node)
78
+ when EasyAdmin::Layouts::Nodes::Spacer
79
+ render_spacer_node(node)
80
+ when EasyAdmin::Layouts::Nodes::RenderNode
81
+ render_render_node(node, context)
82
+ else
83
+ # Fallback for unknown node types
84
+ render_children(node)
85
+ end
86
+ end
87
+
88
+ def render_root_node(node)
89
+ div(class: "easy-admin-layout") do
90
+ render_children(node)
91
+ end
92
+ end
93
+
94
+ def render_row_node(node)
95
+ classes = ["grid", "items-stretch"]
96
+
97
+ # Check if any child column has explicit size - if so, use 12-column grid
98
+ has_sized_columns = node.children.any? { |child|
99
+ child.is_a?(EasyAdmin::Layouts::Nodes::Column) && child.attributes[:size]
100
+ }
101
+
102
+ if has_sized_columns
103
+ # Use 12-column grid for Bootstrap-style column sizing
104
+ classes << "grid-cols-1 md:grid-cols-12"
105
+ elsif node.attributes[:columns]
106
+ # Use simple equal-width columns
107
+ columns = node.attributes[:columns]
108
+ case columns
109
+ when 1
110
+ classes << "grid-cols-1"
111
+ when 2
112
+ classes << "grid-cols-1 md:grid-cols-2"
113
+ when 3
114
+ classes << "grid-cols-1 md:grid-cols-2 lg:grid-cols-3"
115
+ when 4
116
+ classes << "grid-cols-1 md:grid-cols-2 lg:grid-cols-4"
117
+ when 5
118
+ classes << "grid-cols-1 md:grid-cols-3 lg:grid-cols-5"
119
+ when 6
120
+ classes << "grid-cols-1 md:grid-cols-3 lg:grid-cols-6"
121
+ else
122
+ classes << "grid-cols-1 md:grid-cols-#{[columns, 12].min}"
123
+ end
124
+ else
125
+ classes << "grid-cols-1"
126
+ end
127
+
128
+ # Add spacing with Tailwind gap classes
129
+ spacing = node.attributes[:spacing] || "medium"
130
+ case spacing
131
+ when "small", "sm"
132
+ classes << "gap-2"
133
+ when "medium", "md"
134
+ classes << "gap-4"
135
+ when "large", "lg"
136
+ classes << "gap-6"
137
+ else
138
+ classes << "gap-4"
139
+ end
140
+
141
+ div(class: classes.join(" ")) do
142
+ render_children(node)
143
+ end
144
+ end
145
+
146
+ def render_column_node(node)
147
+ classes = ["flex", "flex-col", "h-full"]
148
+
149
+ # Handle column spanning in Tailwind CSS Grid
150
+ if node.attributes[:size]
151
+ size = node.attributes[:size]
152
+ # On mobile, all columns take full width, on desktop use specified size
153
+ case size
154
+ when 1
155
+ classes << "col-span-1 md:col-span-1"
156
+ when 2
157
+ classes << "col-span-1 md:col-span-2"
158
+ when 3
159
+ classes << "col-span-1 md:col-span-3"
160
+ when 4
161
+ classes << "col-span-1 md:col-span-4"
162
+ when 5
163
+ classes << "col-span-1 md:col-span-5"
164
+ when 6
165
+ classes << "col-span-1 md:col-span-6"
166
+ when 7
167
+ classes << "col-span-1 md:col-span-7"
168
+ when 8
169
+ classes << "col-span-1 md:col-span-8"
170
+ when 9
171
+ classes << "col-span-1 md:col-span-9"
172
+ when 10
173
+ classes << "col-span-1 md:col-span-10"
174
+ when 11
175
+ classes << "col-span-1 md:col-span-11"
176
+ when 12
177
+ classes << "col-span-1 md:col-span-12"
178
+ else
179
+ classes << "col-span-full"
180
+ end
181
+ end
182
+
183
+ # Add responsive column spans if specified
184
+ if node.attributes[:sm]
185
+ classes << "sm:col-span-#{node.attributes[:sm]}"
186
+ end
187
+ if node.attributes[:md]
188
+ classes << "md:col-span-#{node.attributes[:md]}"
189
+ end
190
+ if node.attributes[:lg]
191
+ classes << "lg:col-span-#{node.attributes[:lg]}"
192
+ end
193
+
194
+ # Add alignment classes with Tailwind
195
+ if node.attributes[:align]
196
+ case node.attributes[:align]
197
+ when 'center'
198
+ classes << "flex justify-center"
199
+ when 'right'
200
+ classes << "flex justify-end"
201
+ when 'left'
202
+ classes << "flex justify-start"
203
+ end
204
+ end
205
+
206
+ div(class: classes.join(" ")) do
207
+ render_children(node)
208
+ end
209
+ end
210
+
211
+ def render_card_node(node)
212
+ # Build card classes with Tailwind
213
+ card_classes = ["bg-white", "rounded-lg", "shadow-sm", "border", "mb-4", "flex-1", "flex", "flex-col", "h-full"]
214
+
215
+ # Add color styling with Tailwind
216
+ if node.attributes[:color]
217
+ case node.attributes[:color]
218
+ when "primary"
219
+ card_classes << "border-blue-200"
220
+ when "secondary"
221
+ card_classes << "border-gray-200"
222
+ when "success"
223
+ card_classes << "border-green-200"
224
+ when "info"
225
+ card_classes << "border-cyan-200"
226
+ when "warning"
227
+ card_classes << "border-yellow-200"
228
+ when "danger"
229
+ card_classes << "border-red-200"
230
+ when "light"
231
+ card_classes << "border-gray-100"
232
+ when "dark"
233
+ card_classes << "border-gray-800"
234
+ else
235
+ card_classes << "border-gray-200"
236
+ end
237
+ else
238
+ card_classes << "border-gray-200"
239
+ end
240
+
241
+ div(class: card_classes.join(" ")) do
242
+ # Card header
243
+ if node.attributes[:title]
244
+ header_classes = ["px-6", "py-4", "border-b", "border-gray-200"]
245
+
246
+ # Add header color styling with Tailwind
247
+ if node.attributes[:color]
248
+ case node.attributes[:color]
249
+ when "primary"
250
+ header_classes << "bg-blue-50"
251
+ when "secondary"
252
+ header_classes << "bg-gray-50"
253
+ when "success"
254
+ header_classes << "bg-green-50"
255
+ when "info"
256
+ header_classes << "bg-cyan-50"
257
+ when "warning"
258
+ header_classes << "bg-yellow-50"
259
+ when "danger"
260
+ header_classes << "bg-red-50"
261
+ when "light"
262
+ header_classes << "bg-gray-25"
263
+ when "dark"
264
+ header_classes << "bg-gray-800 text-white"
265
+ else
266
+ header_classes << "bg-gray-50"
267
+ end
268
+ else
269
+ header_classes << "bg-gray-50"
270
+ end
271
+
272
+ div(class: header_classes.join(" ")) do
273
+ h3(class: "text-lg font-semibold text-gray-900") { node.attributes[:title] }
274
+ end
275
+ end
276
+
277
+ # Card body with better spacing for field groups
278
+ div(class: "px-6 py-5 flex-1") do
279
+ render_children(node)
280
+ end
281
+ end
282
+ end
283
+
284
+ def render_field_node(node)
285
+ field_config = @resource_class.find_field_config(node.field_name)
286
+
287
+ if field_config
288
+ # Check if label should be shown (node attributes override field config)
289
+ show_label = node.attributes[:label] != false &&
290
+ (node.attributes.key?(:label) ? node.attributes[:label] : field_config[:label])
291
+
292
+
293
+ div(class: "mb-3 py-2 border-b border-gray-100 last:border-b-0") do
294
+ if show_label
295
+ div(class: "text-xs font-semibold text-gray-500 uppercase tracking-wide mb-1") do
296
+ show_label == true ? (field_config[:label] || node.field_name.to_s.humanize) : show_label
297
+ end
298
+ end
299
+ div(class: "text-sm text-gray-900 font-medium break-words max-w-full overflow-hidden") do
300
+ render_field_value(field_config)
301
+ end
302
+ end
303
+ else
304
+ div(class: "mb-2 px-3 py-2 bg-yellow-50 border border-yellow-200 text-yellow-800 rounded-md text-sm") do
305
+ "Field '#{node.field_name}' not found"
306
+ end
307
+ end
308
+ end
309
+
310
+ def render_heading_node(node)
311
+ level = node.attributes[:level] || 2
312
+ tag_name = "h#{level}"
313
+ classes = ["mb-3", "font-semibold"]
314
+
315
+ # Add appropriate text sizing based on heading level
316
+ case level
317
+ when 1
318
+ classes << "text-xl text-gray-900"
319
+ when 2
320
+ classes << "text-lg text-gray-900"
321
+ when 3
322
+ classes << "text-base text-gray-800"
323
+ when 4
324
+ classes << "text-sm text-gray-800"
325
+ else
326
+ classes << "text-sm text-gray-700"
327
+ end
328
+
329
+ # Override with custom size if specified
330
+ if node.attributes[:size]
331
+ case node.attributes[:size]
332
+ when "small"
333
+ classes[-1] = "text-sm text-gray-700"
334
+ when "medium"
335
+ classes[-1] = "text-base text-gray-800"
336
+ when "large"
337
+ classes[-1] = "text-lg text-gray-900"
338
+ end
339
+ end
340
+
341
+ send(tag_name, class: classes.join(" ")) { node.text }
342
+ end
343
+
344
+ def render_divider_node(node)
345
+ margin = node.attributes[:margin] || 4
346
+ hr(class: "my-#{margin} border-gray-200")
347
+ end
348
+
349
+ def render_metric_card_node(node)
350
+ title = node.attributes[:title]
351
+ value = node.attributes[:value]
352
+
353
+ # Evaluate value if it's a proc
354
+ display_value = case value
355
+ when Proc
356
+ value.call(@record)
357
+ else
358
+ value
359
+ end
360
+
361
+ div(class: "bg-white rounded-lg shadow-sm border text-center") do
362
+ div(class: "p-6") do
363
+ h5(class: "text-sm font-medium text-gray-500 mb-1") { title }
364
+ h2(class: "text-2xl font-bold text-blue-600") { display_value }
365
+ end
366
+ end
367
+ end
368
+
369
+ def render_content_node(node, context)
370
+ if node.block
371
+ result = context.instance_exec(&node.block)
372
+ if result.respond_to?(:call)
373
+ render result
374
+ else
375
+ unsafe_raw result.to_s
376
+ end
377
+ end
378
+ end
379
+
380
+ def render_tabs_node(node)
381
+ tabs_id = node.attributes[:id] || "tabs-#{SecureRandom.hex(4)}"
382
+ tab_type = node.attributes[:type] || :horizontal
383
+
384
+ div(class: "easy-admin-tabs",
385
+ data: {
386
+ controller: tab_type == :vertical ? "vertical-tabs" : "form-tabs",
387
+ action: "tab:selected@window->form-tabs#handleTabSelection"
388
+ }) do
389
+
390
+ # Tab navigation
391
+ render_tab_navigation(node, tabs_id, tab_type)
392
+
393
+ # Tab content
394
+ render_tab_content(node, tabs_id)
395
+ end
396
+ end
397
+
398
+ def render_tab_navigation(tabs_node, tabs_id, tab_type)
399
+ if tab_type == :vertical
400
+ render_vertical_tab_navigation(tabs_node, tabs_id)
401
+ else
402
+ render_horizontal_tab_navigation(tabs_node, tabs_id)
403
+ end
404
+ end
405
+
406
+ def render_horizontal_tab_navigation(tabs_node, tabs_id)
407
+ # Using Bootstrap tabs with form-tabs controller
408
+ div(class: "border-b border-gray-200") do
409
+ nav(class: "-mb-px flex space-x-8", aria: { label: "Tabs" }) do
410
+ tabs_node.children.each_with_index do |tab_node, index|
411
+ tab_id = "#{tabs_id}-#{tab_node.attributes[:name]&.parameterize || index}"
412
+ tab_label = tab_node.attributes[:label] || tab_node.attributes[:name]
413
+
414
+ button(
415
+ class: "whitespace-nowrap py-2 px-1 border-b-2 font-medium text-sm #{index == 0 ? 'text-blue-600 border-blue-600' : 'text-gray-500 border-transparent hover:text-gray-700 hover:border-gray-300'}",
416
+ data: {
417
+ form_tabs_target: "tabButton",
418
+ tab_id: tab_id,
419
+ action: "click->form-tabs#switchTab"
420
+ },
421
+ type: "button"
422
+ ) do
423
+ # Tab icon if provided
424
+ if tab_node.attributes[:icon]
425
+ i(class: "#{tab_node.attributes[:icon]} me-2")
426
+ end
427
+
428
+ # Tab label
429
+ span { tab_label }
430
+
431
+ # Tab badge if provided
432
+ if tab_node.attributes[:badge]
433
+ span(class: "ml-2 py-0.5 px-2 rounded-full text-xs font-medium bg-gray-100 text-gray-900") { tab_node.attributes[:badge] }
434
+ end
435
+ end
436
+ end
437
+ end
438
+ end
439
+ end
440
+
441
+ def render_vertical_tab_navigation(tabs_node, tabs_id)
442
+ # Using Tailwind classes with vertical-tabs controller
443
+ div(class: "flex") do
444
+ nav(class: "flex-col -mb-px mr-8") do
445
+ tabs_node.children.each_with_index do |tab_node, index|
446
+ tab_id = "#{tabs_id}-#{tab_node.attributes[:name]&.parameterize || index}"
447
+ tab_label = tab_node.attributes[:label] || tab_node.attributes[:name]
448
+
449
+ button(
450
+ class: "block border-l-4 py-2 pl-3 pr-4 text-left text-sm font-medium #{index == 0 ? 'bg-blue-50 text-blue-700 border-blue-200' : 'text-gray-700 hover:bg-gray-100 hover:text-gray-900 border-transparent'}",
451
+ data: {
452
+ vertical_tabs_target: "tab",
453
+ tab_id: tab_id,
454
+ action: "click->vertical-tabs#switchTab"
455
+ },
456
+ type: "button"
457
+ ) do
458
+ # Tab icon if provided
459
+ if tab_node.attributes[:icon]
460
+ i(class: "#{tab_node.attributes[:icon]} mr-2")
461
+ end
462
+
463
+ # Tab label
464
+ span { tab_label }
465
+
466
+ # Tab badge if provided
467
+ if tab_node.attributes[:badge]
468
+ span(class: "ml-2 py-0.5 px-2 rounded-full text-xs font-medium bg-blue-100 text-blue-800") { tab_node.attributes[:badge] }
469
+ end
470
+ end
471
+ end
472
+ end
473
+ end
474
+ end
475
+
476
+ def render_tab_content(tabs_node, tabs_id)
477
+ div(class: "mt-4") do
478
+ tabs_node.children.each_with_index do |tab_node, index|
479
+ tab_id = "#{tabs_id}-#{tab_node.attributes[:name]&.parameterize || index}"
480
+
481
+ # Content panel for both form-tabs and vertical-tabs controllers
482
+ div(
483
+ class: "#{index == 0 ? 'block' : 'hidden'}",
484
+ id: "#{tab_id}-panel",
485
+ data: {
486
+ form_tabs_target: "tabPanel",
487
+ vertical_tabs_target: "content",
488
+ tab_id: tab_id
489
+ }
490
+ ) do
491
+ render_children(tab_node)
492
+ end
493
+ end
494
+ end
495
+ end
496
+
497
+ def render_tab_node(node)
498
+ # Tab nodes are rendered by their parent tabs container
499
+ # This method should not be called directly
500
+ render_children(node)
501
+ end
502
+
503
+ def render_section_node(node)
504
+ classes = ["mb-4"]
505
+
506
+ div(class: classes.join(" ")) do
507
+ if node.attributes[:title]
508
+ h3(class: "mb-3 text-lg font-semibold") { node.attributes[:title] }
509
+ end
510
+
511
+ render_children(node)
512
+ end
513
+ end
514
+
515
+ def render_grid_node(node)
516
+ columns = node.attributes[:columns] || 2
517
+ classes = ["grid"]
518
+
519
+ # Add responsive column classes with Tailwind CSS Grid
520
+ case columns
521
+ when 1
522
+ classes << "grid-cols-1"
523
+ when 2
524
+ classes << "grid-cols-1 md:grid-cols-2"
525
+ when 3
526
+ classes << "grid-cols-1 md:grid-cols-2 lg:grid-cols-3"
527
+ when 4
528
+ classes << "grid-cols-1 md:grid-cols-2 lg:grid-cols-4"
529
+ else
530
+ classes << "grid-cols-1 md:grid-cols-#{[columns, 6].min}"
531
+ end
532
+
533
+ # Add spacing
534
+ spacing = node.attributes[:spacing] || "medium"
535
+ classes << spacing_class(spacing)
536
+
537
+ div(class: classes.join(" ")) do
538
+ render_children(node)
539
+ end
540
+ end
541
+
542
+ def render_spacer_node(node)
543
+ size = node.attributes[:size] || 4
544
+ div(class: "my-#{size}")
545
+ end
546
+
547
+ def render_render_node(node, context)
548
+ component_class = node.component_class
549
+ props = node.props || {}
550
+
551
+ # Add context data to props
552
+ enhanced_props = props.merge(
553
+ record: @record,
554
+ resource_class: @resource_class
555
+ )
556
+
557
+ if component_class
558
+ component = component_class.new(**enhanced_props)
559
+ render component
560
+ else
561
+ div(class: "bg-yellow-50 border border-yellow-200 text-yellow-800 px-4 py-3 rounded-md") do
562
+ "Component class not found: #{node.component_class}"
563
+ end
564
+ end
565
+ end
566
+
567
+ def render_children(node)
568
+ node.children.each { |child| render_node(child) }
569
+ end
570
+
571
+ def spacing_class(spacing)
572
+ case spacing
573
+ when "small"
574
+ "g-2"
575
+ when "medium"
576
+ "g-3"
577
+ when "large"
578
+ "g-4"
579
+ else
580
+ "g-3"
581
+ end
582
+ end
583
+
584
+ def render_field_value(field_config)
585
+ # Get the value and format it
586
+ value = @record.send(field_config[:name])
587
+ formatted_value = format_field_value(value, field_config)
588
+
589
+ # Render based on field type with compact styling
590
+ case field_config[:type]
591
+ when :boolean
592
+ span(class: "inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium #{value ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'}") {
593
+ formatted_value
594
+ }
595
+ when :email
596
+ a(href: "mailto:#{value}", class: "text-blue-600 hover:text-blue-800 underline") {
597
+ formatted_value
598
+ }
599
+ when :select, :belongs_to
600
+ if value.present?
601
+ span(class: "inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800") {
602
+ formatted_value
603
+ }
604
+ else
605
+ span(class: "text-gray-400") { "—" }
606
+ end
607
+ when :datetime, :date
608
+ span(class: "text-gray-700") {
609
+ formatted_value
610
+ }
611
+ else
612
+ span { formatted_value }
613
+ end
614
+ end
615
+
616
+ def format_field_value(value, field_config)
617
+ case field_config[:type]
618
+ when :boolean
619
+ value ? "Yes" : "No"
620
+ when :date
621
+ value&.strftime("%B %d, %Y")
622
+ when :datetime
623
+ value&.strftime("%B %d, %Y at %I:%M %p")
624
+ when :belongs_to
625
+ # Handle belongs_to associations with display method
626
+ if value.present?
627
+ display_method = field_config[:display_method] || :name
628
+ if value.respond_to?(display_method)
629
+ value.public_send(display_method)
630
+ elsif value.respond_to?(:name)
631
+ value.name
632
+ elsif value.respond_to?(:title)
633
+ value.title
634
+ else
635
+ value.to_s
636
+ end
637
+ else
638
+ "—"
639
+ end
640
+ else
641
+ value.present? ? value.to_s : "—"
21
642
  end
22
643
  end
23
644
 
24
645
  def render_default_layout
25
- div(class: "row row-sm") do
26
- div(class: "col-12") do
27
- div(class: "card") do
28
- div(class: "card-body") do
646
+ div(class: "grid grid-cols-1 gap-4") do
647
+ div(class: "col-span-1") do
648
+ div(class: "bg-white rounded-lg shadow-sm border") do
649
+ div(class: "p-6") do
29
650
  @resource_class.show_fields.each do |field|
30
651
  render_default_field(field)
31
652
  end
@@ -156,11 +777,11 @@ module EasyAdmin
156
777
  class: card_css_classes(chart_config)
157
778
  ) do
158
779
  # Loading skeleton
159
- div(class: "card") do
160
- div(class: "card-header") do
161
- h5(class: "card-title") { chart_config[:title] }
780
+ div(class: "bg-white rounded-lg shadow-sm border") do
781
+ div(class: "px-6 py-4 border-b border-gray-200") do
782
+ h5(class: "text-lg font-semibold text-gray-900") { chart_config[:title] }
162
783
  end
163
- div(class: "card-body") do
784
+ div(class: "p-6") do
164
785
  div(class: "animate-pulse") do
165
786
  div(class: "h-64 bg-gray-300 rounded")
166
787
  end
@@ -241,33 +862,82 @@ module EasyAdmin
241
862
  show_label = field_config[:show_label] != false && field_config[:label].present?
242
863
 
243
864
  if show_label && field_config[:field_type] != :inline
244
- div(class: "mb-3 row") do
245
- div(class: "col-sm-3") do
246
- strong { "#{field_config[:label]}:" }
865
+ div(class: "mb-4") do
866
+ div(class: "text-sm font-medium text-gray-700 mb-1") do
867
+ field_config[:label]
247
868
  end
248
- div(class: "col-sm-9") do
869
+ div(class: "text-sm text-gray-900") do
249
870
  render_field_value(field_config)
250
871
  end
251
872
  end
252
873
  else
253
874
  # Field without label (inline or label: false)
254
- div(class: "mb-3") do
875
+ div(class: "mb-4") do
255
876
  render_field_value(field_config)
256
877
  end
257
878
  end
258
879
  end
259
880
 
260
881
  def render_field_value(field_config)
261
- # Find the matching field configuration
262
882
  field_definition = @resource_class.fields_config.find { |f| f[:name] == field_config[:name] }
263
883
 
264
884
  if field_definition
265
- field_value = @record.public_send(field_config[:name])
266
- # Pass show_label setting to field component
267
- show_label = field_config[:show_label] != false && field_config[:label].present?
268
- render_field_component(field_definition, field_value, show_label: show_label)
885
+ field_value = @record.public_send(field_config[:name]) if @record
886
+
887
+ begin
888
+ component = EasyAdmin::Field.render(
889
+ field_definition[:type] || :text,
890
+ action: :show,
891
+ field: field_definition,
892
+ value: field_value,
893
+ record: @record
894
+ )
895
+
896
+ result = component.call
897
+
898
+ unsafe_raw result
899
+ rescue => e
900
+ case field_definition[:type]
901
+ when :boolean
902
+ field_value ? "Yes" : "No"
903
+ when :datetime, :date
904
+ field_value&.strftime("%B %d, %Y at %l:%M %p")
905
+ when :belongs_to
906
+ # Handle belongs_to associations
907
+ if field_value.respond_to?(field_definition[:display_method])
908
+ field_value.public_send(field_definition[:display_method])
909
+ elsif field_value.respond_to?(:name)
910
+ field_value.name
911
+ elsif field_value.respond_to?(:title)
912
+ field_value.title
913
+ else
914
+ field_value.to_s
915
+ end
916
+ when :has_many
917
+ # Handle has_many associations
918
+ if field_value.respond_to?(:count)
919
+ "#{field_value.count} #{field_config[:name].to_s.humanize.downcase}"
920
+ else
921
+ field_value.to_s
922
+ end
923
+ when :json
924
+ # Handle JSON fields
925
+ if field_value.is_a?(Hash) || field_value.is_a?(Array)
926
+ field_value.empty? ? "No data" : JSON.pretty_generate(field_value)
927
+ else
928
+ field_value.to_s
929
+ end
930
+ else
931
+ # Regular text fields
932
+ if field_value.present?
933
+ field_value.to_s.truncate(200)
934
+ else
935
+ "—"
936
+ end
937
+ end
938
+ end
269
939
  else
270
- span(class: "text-muted") { "Field not found" }
940
+ span(class: "text-gray-500") { "Field not found" }
271
941
  end
272
942
  end
273
943
 
@@ -294,8 +964,8 @@ module EasyAdmin
294
964
  end
295
965
 
296
966
  def render_default_field(field)
297
- div(class: "mb-3 row") do
298
- div(class: "col-sm-3") do
967
+ div(class: "mb-3 grid grid-cols-12 gap-4 items-center") do
968
+ div(class: "col-span-12 sm:col-span-3") do
299
969
  strong { "#{field[:label]}:" }
300
970
  end
301
971
  div(class: "col-sm-9") do