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.
- checksums.yaml +4 -4
- data/app/assets/builds/easy_admin.base.js +7 -0
- data/app/assets/builds/easy_admin.base.js.map +2 -2
- data/app/assets/builds/easy_admin.css +207 -35
- data/app/components/easy_admin/fields/form/belongs_to_component.rb +0 -1
- data/app/components/easy_admin/form_layout_component.rb +553 -0
- data/app/components/easy_admin/permissions/user_role_permissions_component.rb +1 -3
- data/app/components/easy_admin/show_layout_component.rb +694 -24
- data/app/controllers/easy_admin/application_controller.rb +0 -5
- data/app/controllers/easy_admin/batch_actions_controller.rb +0 -1
- data/app/controllers/easy_admin/concerns/inline_field_editing.rb +4 -11
- data/app/controllers/easy_admin/concerns/resource_loading.rb +10 -9
- data/app/controllers/easy_admin/concerns/resource_pagination.rb +3 -0
- data/app/controllers/easy_admin/dashboards_controller.rb +0 -1
- data/app/controllers/easy_admin/resources_controller.rb +1 -5
- data/app/controllers/easy_admin/row_actions_controller.rb +1 -4
- data/app/helpers/easy_admin/fields_helper.rb +8 -22
- data/app/javascript/easy_admin/controllers/infinite_scroll_controller.js +12 -0
- data/app/views/easy_admin/resources/edit.html.erb +2 -2
- data/app/views/easy_admin/resources/new.html.erb +2 -2
- data/app/views/easy_admin/resources/show.html.erb +3 -1
- data/lib/easy_admin/field.rb +3 -2
- data/lib/easy_admin/layouts/builders/base_layout_builder.rb +245 -0
- data/lib/easy_admin/layouts/builders/form_layout_builder.rb +208 -0
- data/lib/easy_admin/layouts/builders/index_layout_builder.rb +22 -0
- data/lib/easy_admin/layouts/builders/show_layout_builder.rb +199 -0
- data/lib/easy_admin/layouts/dsl.rb +200 -0
- data/lib/easy_admin/layouts/layout_context.rb +189 -0
- data/lib/easy_admin/layouts/nodes/base_node.rb +88 -0
- data/lib/easy_admin/layouts/nodes/divider.rb +27 -0
- data/lib/easy_admin/layouts/nodes/field_node.rb +57 -0
- data/lib/easy_admin/layouts/nodes/grid.rb +60 -0
- data/lib/easy_admin/layouts/nodes/render_node.rb +41 -0
- data/lib/easy_admin/layouts/nodes/root.rb +25 -0
- data/lib/easy_admin/layouts/nodes/section.rb +46 -0
- data/lib/easy_admin/layouts/nodes/spacer.rb +17 -0
- data/lib/easy_admin/layouts/nodes/stubs.rb +109 -0
- data/lib/easy_admin/layouts/nodes/tab.rb +40 -0
- data/lib/easy_admin/layouts/nodes/tabs.rb +40 -0
- data/lib/easy_admin/layouts.rb +28 -0
- data/lib/easy_admin/permissions/resource_permissions.rb +1 -5
- data/lib/easy_admin/resource/base.rb +2 -2
- data/lib/easy_admin/resource/dsl.rb +2 -11
- data/lib/easy_admin/resource/field_registry.rb +58 -2
- data/lib/easy_admin/resource.rb +0 -9
- data/lib/easy_admin/resource_modules.rb +21 -4
- data/lib/easy_admin/version.rb +1 -1
- data/lib/generators/easy_admin/permissions/install_generator.rb +0 -10
- data/lib/generators/easy_admin/permissions/templates/migrations/create_permission_tables.rb +33 -3
- metadata +21 -9
- data/lib/easy_admin/resource/form_builder.rb +0 -123
- data/lib/easy_admin/resource/layout_builder.rb +0 -249
- data/lib/easy_admin/resource/show_builder.rb +0 -359
- data/lib/generators/easy_admin/permissions/templates/migrations/update_users_for_permissions.rb +0 -6
- data/lib/generators/easy_admin/rbac/rbac_generator.rb +0 -244
- data/lib/generators/easy_admin/rbac/templates/add_rbac_to_admin_users.rb +0 -23
- 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.
|
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.
|
20
|
-
|
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: "
|
26
|
-
div(class: "col-
|
27
|
-
div(class: "
|
28
|
-
div(class: "
|
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: "
|
160
|
-
div(class: "
|
161
|
-
h5(class: "
|
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: "
|
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-
|
245
|
-
div(class: "
|
246
|
-
|
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: "
|
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-
|
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
|
-
|
267
|
-
|
268
|
-
|
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-
|
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
|
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
|