easy-admin-rails 0.1.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 (203) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +28 -0
  4. data/Rakefile +8 -0
  5. data/app/assets/builds/easy_admin.base.js +43505 -0
  6. data/app/assets/builds/easy_admin.base.js.map +7 -0
  7. data/app/assets/builds/easy_admin.css +6141 -0
  8. data/app/assets/config/easy_admin_manifest.js +1 -0
  9. data/app/assets/images/jsoneditor-icons.svg +749 -0
  10. data/app/assets/stylesheets/easy_admin/application.tailwind.css +390 -0
  11. data/app/components/easy_admin/base_component.rb +35 -0
  12. data/app/components/easy_admin/batch_action_bar_component.rb +125 -0
  13. data/app/components/easy_admin/batch_action_form_component.rb +124 -0
  14. data/app/components/easy_admin/combined_filters_component.rb +232 -0
  15. data/app/components/easy_admin/confirmation_modal_component.rb +61 -0
  16. data/app/components/easy_admin/context_menu_component.rb +161 -0
  17. data/app/components/easy_admin/dashboards/base_card_component.rb +152 -0
  18. data/app/components/easy_admin/dashboards/card_error_component.rb +23 -0
  19. data/app/components/easy_admin/dashboards/card_factory.rb +90 -0
  20. data/app/components/easy_admin/dashboards/card_stream_component.rb +22 -0
  21. data/app/components/easy_admin/dashboards/cards/base_card_component.rb +54 -0
  22. data/app/components/easy_admin/dashboards/cards/chart_card_component.rb +175 -0
  23. data/app/components/easy_admin/dashboards/cards/custom_card_component.rb +50 -0
  24. data/app/components/easy_admin/dashboards/cards/metric_card_component.rb +164 -0
  25. data/app/components/easy_admin/dashboards/cards/table_card_component.rb +148 -0
  26. data/app/components/easy_admin/dashboards/chart_card_component.rb +44 -0
  27. data/app/components/easy_admin/dashboards/metric_card_component.rb +56 -0
  28. data/app/components/easy_admin/dashboards/refresh_stream_component.rb +279 -0
  29. data/app/components/easy_admin/dashboards/show_component.rb +163 -0
  30. data/app/components/easy_admin/dashboards/table_card_component.rb +52 -0
  31. data/app/components/easy_admin/date_picker_component.rb +188 -0
  32. data/app/components/easy_admin/fields/base_component.rb +101 -0
  33. data/app/components/easy_admin/fields/belongs_to_edit_modal_component.rb +117 -0
  34. data/app/components/easy_admin/fields/form/belongs_to_component.rb +82 -0
  35. data/app/components/easy_admin/fields/form/boolean_component.rb +100 -0
  36. data/app/components/easy_admin/fields/form/date_component.rb +55 -0
  37. data/app/components/easy_admin/fields/form/datetime_component.rb +55 -0
  38. data/app/components/easy_admin/fields/form/email_component.rb +55 -0
  39. data/app/components/easy_admin/fields/form/file_component.rb +190 -0
  40. data/app/components/easy_admin/fields/form/has_many_component.rb +416 -0
  41. data/app/components/easy_admin/fields/form/json_component.rb +81 -0
  42. data/app/components/easy_admin/fields/form/number_component.rb +55 -0
  43. data/app/components/easy_admin/fields/form/select_component.rb +326 -0
  44. data/app/components/easy_admin/fields/form/text_component.rb +55 -0
  45. data/app/components/easy_admin/fields/form/textarea_component.rb +54 -0
  46. data/app/components/easy_admin/fields/index/belongs_to_component.rb +93 -0
  47. data/app/components/easy_admin/fields/index/boolean_component.rb +29 -0
  48. data/app/components/easy_admin/fields/index/date_component.rb +13 -0
  49. data/app/components/easy_admin/fields/index/datetime_component.rb +13 -0
  50. data/app/components/easy_admin/fields/index/email_component.rb +24 -0
  51. data/app/components/easy_admin/fields/index/filters/base_component.rb +48 -0
  52. data/app/components/easy_admin/fields/index/filters/boolean_component.rb +96 -0
  53. data/app/components/easy_admin/fields/index/filters/date_component.rb +182 -0
  54. data/app/components/easy_admin/fields/index/filters/number_component.rb +30 -0
  55. data/app/components/easy_admin/fields/index/filters/select_component.rb +101 -0
  56. data/app/components/easy_admin/fields/index/filters/string_component.rb +32 -0
  57. data/app/components/easy_admin/fields/index/json_component.rb +23 -0
  58. data/app/components/easy_admin/fields/index/number_component.rb +20 -0
  59. data/app/components/easy_admin/fields/index/select_component.rb +25 -0
  60. data/app/components/easy_admin/fields/index/text_component.rb +20 -0
  61. data/app/components/easy_admin/fields/inline_edit_modal_component.rb +135 -0
  62. data/app/components/easy_admin/fields/inline_edit_trigger_component.rb +144 -0
  63. data/app/components/easy_admin/fields/show/belongs_to_component.rb +93 -0
  64. data/app/components/easy_admin/fields/show/boolean_component.rb +21 -0
  65. data/app/components/easy_admin/fields/show/date_component.rb +13 -0
  66. data/app/components/easy_admin/fields/show/datetime_component.rb +13 -0
  67. data/app/components/easy_admin/fields/show/email_component.rb +19 -0
  68. data/app/components/easy_admin/fields/show/file_component.rb +304 -0
  69. data/app/components/easy_admin/fields/show/has_many_component.rb +192 -0
  70. data/app/components/easy_admin/fields/show/json_component.rb +45 -0
  71. data/app/components/easy_admin/fields/show/number_component.rb +20 -0
  72. data/app/components/easy_admin/fields/show/select_component.rb +25 -0
  73. data/app/components/easy_admin/fields/show/text_component.rb +17 -0
  74. data/app/components/easy_admin/fields/show/textarea_component.rb +26 -0
  75. data/app/components/easy_admin/filters_component.rb +120 -0
  76. data/app/components/easy_admin/form_tabs_component.rb +166 -0
  77. data/app/components/easy_admin/infinite_scroll_component.rb +82 -0
  78. data/app/components/easy_admin/lazy_chart_card_component.rb +128 -0
  79. data/app/components/easy_admin/lazy_metric_card_component.rb +76 -0
  80. data/app/components/easy_admin/modal_frame_component.rb +26 -0
  81. data/app/components/easy_admin/navbar_component.rb +226 -0
  82. data/app/components/easy_admin/notification_component.rb +83 -0
  83. data/app/components/easy_admin/pagination_component.rb +188 -0
  84. data/app/components/easy_admin/quick_filters_component.rb +65 -0
  85. data/app/components/easy_admin/resource_pagination_component.rb +14 -0
  86. data/app/components/easy_admin/resources/index_component.rb +211 -0
  87. data/app/components/easy_admin/resources/index_frame_component.rb +88 -0
  88. data/app/components/easy_admin/resources/show_page_actions_component.rb +324 -0
  89. data/app/components/easy_admin/resources/table_cell_component.rb +145 -0
  90. data/app/components/easy_admin/resources/table_component.rb +206 -0
  91. data/app/components/easy_admin/resources/table_row_component.rb +160 -0
  92. data/app/components/easy_admin/row_action_form_component.rb +127 -0
  93. data/app/components/easy_admin/scopes_component.rb +224 -0
  94. data/app/components/easy_admin/settings_sidebar_component.rb +140 -0
  95. data/app/components/easy_admin/show_layout_component.rb +600 -0
  96. data/app/components/easy_admin/sidebar_component.rb +174 -0
  97. data/app/components/easy_admin/turbo/response_component.rb +40 -0
  98. data/app/components/easy_admin/turbo/stream_component.rb +28 -0
  99. data/app/controllers/easy_admin/application_controller.rb +66 -0
  100. data/app/controllers/easy_admin/batch_actions_controller.rb +166 -0
  101. data/app/controllers/easy_admin/confirmation_modal_controller.rb +20 -0
  102. data/app/controllers/easy_admin/dashboard_controller.rb +6 -0
  103. data/app/controllers/easy_admin/dashboards_controller.rb +123 -0
  104. data/app/controllers/easy_admin/passwords_controller.rb +15 -0
  105. data/app/controllers/easy_admin/registrations_controller.rb +52 -0
  106. data/app/controllers/easy_admin/resources_controller.rb +907 -0
  107. data/app/controllers/easy_admin/row_actions_controller.rb +216 -0
  108. data/app/controllers/easy_admin/sessions_controller.rb +32 -0
  109. data/app/controllers/easy_admin/settings_controller.rb +94 -0
  110. data/app/helpers/easy_admin/application_helper.rb +4 -0
  111. data/app/helpers/easy_admin/dashboards_helper.rb +121 -0
  112. data/app/helpers/easy_admin/fields_helper.rb +27 -0
  113. data/app/helpers/easy_admin/pagy_helper.rb +30 -0
  114. data/app/helpers/easy_admin/resources_helper.rb +39 -0
  115. data/app/javascript/easy_admin/application.js +12 -0
  116. data/app/javascript/easy_admin/controllers/batch_modal_controller.js +66 -0
  117. data/app/javascript/easy_admin/controllers/batch_selection_controller.js +223 -0
  118. data/app/javascript/easy_admin/controllers/chart_controller.js +216 -0
  119. data/app/javascript/easy_admin/controllers/collapsible_filters_controller.js +118 -0
  120. data/app/javascript/easy_admin/controllers/confirmation_modal_controller.js +64 -0
  121. data/app/javascript/easy_admin/controllers/context_menu_controller.js +227 -0
  122. data/app/javascript/easy_admin/controllers/date_picker_controller.js +309 -0
  123. data/app/javascript/easy_admin/controllers/dropdown_controller.js +63 -0
  124. data/app/javascript/easy_admin/controllers/event_emitter_controller.js +19 -0
  125. data/app/javascript/easy_admin/controllers/file_controller.js +121 -0
  126. data/app/javascript/easy_admin/controllers/form_tabs_controller.js +100 -0
  127. data/app/javascript/easy_admin/controllers/has_many_search_controller.js +76 -0
  128. data/app/javascript/easy_admin/controllers/infinite_scroll_controller.js +174 -0
  129. data/app/javascript/easy_admin/controllers/ios_alert_controller.js +195 -0
  130. data/app/javascript/easy_admin/controllers/jsoneditor_controller.js +88 -0
  131. data/app/javascript/easy_admin/controllers/modal_controller.js +75 -0
  132. data/app/javascript/easy_admin/controllers/navbar_scroll_controller.js +76 -0
  133. data/app/javascript/easy_admin/controllers/notification_controller.js +48 -0
  134. data/app/javascript/easy_admin/controllers/row_action_controller.js +124 -0
  135. data/app/javascript/easy_admin/controllers/row_modal_controller.js +59 -0
  136. data/app/javascript/easy_admin/controllers/select_field_controller.js +618 -0
  137. data/app/javascript/easy_admin/controllers/settings_button_controller.js +8 -0
  138. data/app/javascript/easy_admin/controllers/settings_sidebar_controller.js +186 -0
  139. data/app/javascript/easy_admin/controllers/sidebar_controller.js +102 -0
  140. data/app/javascript/easy_admin/controllers/sidebar_mobile_controller.js +23 -0
  141. data/app/javascript/easy_admin/controllers/sidebar_nav_controller.js +96 -0
  142. data/app/javascript/easy_admin/controllers/table_controller.js +28 -0
  143. data/app/javascript/easy_admin/controllers/table_row_controller.js +16 -0
  144. data/app/javascript/easy_admin/controllers/toggle_switch_controller.js +22 -0
  145. data/app/javascript/easy_admin/controllers/turbo_stream_redirect.js +9 -0
  146. data/app/javascript/easy_admin/controllers.js +54 -0
  147. data/app/javascript/easy_admin.base.js +4 -0
  148. data/app/models/easy_admin/admin_user.rb +53 -0
  149. data/app/models/easy_admin/application_record.rb +5 -0
  150. data/app/views/easy_admin/dashboard/index.html.erb +3 -0
  151. data/app/views/easy_admin/dashboards/show.html.erb +7 -0
  152. data/app/views/easy_admin/passwords/edit.html.erb +42 -0
  153. data/app/views/easy_admin/passwords/new.html.erb +41 -0
  154. data/app/views/easy_admin/registrations/new.html.erb +65 -0
  155. data/app/views/easy_admin/resources/_redirect.turbo_stream.erb +3 -0
  156. data/app/views/easy_admin/resources/_table_rows.html.erb +46 -0
  157. data/app/views/easy_admin/resources/edit.html.erb +151 -0
  158. data/app/views/easy_admin/resources/index.html.erb +12 -0
  159. data/app/views/easy_admin/resources/index.turbo_stream.erb +139 -0
  160. data/app/views/easy_admin/resources/index_frame.html.erb +142 -0
  161. data/app/views/easy_admin/resources/new.html.erb +100 -0
  162. data/app/views/easy_admin/resources/show.html.erb +31 -0
  163. data/app/views/easy_admin/sessions/new.html.erb +55 -0
  164. data/app/views/easy_admin/settings/_form.html.erb +51 -0
  165. data/app/views/easy_admin/settings/index.html.erb +53 -0
  166. data/app/views/layouts/easy_admin/application.html.erb +48 -0
  167. data/app/views/layouts/easy_admin/auth.html.erb +34 -0
  168. data/config/initializers/easy_admin_card_factory.rb +27 -0
  169. data/config/initializers/pagy.rb +15 -0
  170. data/config/initializers/rack_mini_profiler.rb +67 -0
  171. data/config/routes.rb +70 -0
  172. data/db/migrate/20250101000001_create_easy_admin_admin_users.rb +45 -0
  173. data/lib/easy-admin.rb +32 -0
  174. data/lib/easy_admin/action.rb +159 -0
  175. data/lib/easy_admin/batch_action.rb +134 -0
  176. data/lib/easy_admin/configuration.rb +75 -0
  177. data/lib/easy_admin/dashboard.rb +110 -0
  178. data/lib/easy_admin/dashboard_registry.rb +30 -0
  179. data/lib/easy_admin/delete_action.rb +22 -0
  180. data/lib/easy_admin/engine.rb +54 -0
  181. data/lib/easy_admin/field.rb +118 -0
  182. data/lib/easy_admin/resource.rb +806 -0
  183. data/lib/easy_admin/resource_registry.rb +22 -0
  184. data/lib/easy_admin/types/json_type.rb +25 -0
  185. data/lib/easy_admin/version.rb +3 -0
  186. data/lib/generators/easy_admin/auth_generator.rb +69 -0
  187. data/lib/generators/easy_admin/card/card_generator.rb +94 -0
  188. data/lib/generators/easy_admin/card/templates/card_component.rb.erb +127 -0
  189. data/lib/generators/easy_admin/card/templates/card_component_spec.rb.erb +122 -0
  190. data/lib/generators/easy_admin/install/templates/easy_admin.rb +31 -0
  191. data/lib/generators/easy_admin/install_generator.rb +25 -0
  192. data/lib/generators/easy_admin/rbac/rbac_generator.rb +244 -0
  193. data/lib/generators/easy_admin/rbac/templates/add_rbac_to_admin_users.rb +23 -0
  194. data/lib/generators/easy_admin/rbac/templates/super_admin.rb +34 -0
  195. data/lib/generators/easy_admin/resource_generator.rb +43 -0
  196. data/lib/generators/easy_admin/templates/AUTH_README +35 -0
  197. data/lib/generators/easy_admin/templates/README +27 -0
  198. data/lib/generators/easy_admin/templates/create_easy_admin_admin_users.rb +45 -0
  199. data/lib/generators/easy_admin/templates/devise.rb +267 -0
  200. data/lib/generators/easy_admin/templates/easy_admin.rb +24 -0
  201. data/lib/generators/easy_admin/templates/resource.rb +29 -0
  202. data/lib/tasks/easy_admin_tasks.rake +4 -0
  203. metadata +445 -0
@@ -0,0 +1,600 @@
1
+ module EasyAdmin
2
+ class ShowLayoutComponent < Phlex::HTML
3
+ def initialize(resource_class:, record:)
4
+ @resource_class = resource_class
5
+ @record = record
6
+ end
7
+
8
+ def view_template
9
+ if @resource_class.has_show_layout?
10
+ render_custom_layout
11
+ else
12
+ render_default_layout
13
+ end
14
+ end
15
+
16
+ private
17
+
18
+ def render_custom_layout
19
+ @resource_class.show_layout.each do |element|
20
+ render_element(element)
21
+ end
22
+ end
23
+
24
+ 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
29
+ @resource_class.show_fields.each do |field|
30
+ render_default_field(field)
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+
38
+ def render_element(element)
39
+ case element[:type]
40
+ when :row
41
+ render_row(element)
42
+ else
43
+ # Handle unknown element types gracefully
44
+ nil
45
+ end
46
+ end
47
+
48
+ def render_row(row_config)
49
+ div(class: row_css_classes(row_config)) do
50
+ row_config[:columns].each do |column|
51
+ # Add row context to column config
52
+ column[:row_columns_count] = row_config[:columns_count]
53
+ render_column(column)
54
+ end
55
+ end
56
+ end
57
+
58
+ def render_column(column_config)
59
+ div(class: column_css_classes(column_config)) do
60
+ column_config[:elements].each do |element|
61
+ render_column_element(element)
62
+ end
63
+ end
64
+ end
65
+
66
+ def render_column_element(element)
67
+ case element[:type]
68
+ when :card
69
+ render_card(element)
70
+ when :metric_card
71
+ render_metric_card(element)
72
+ when :chart_card
73
+ render_chart_card(element)
74
+ when :field
75
+ render_field_element(element)
76
+ when :content
77
+ render_content_element(element)
78
+ when :heading
79
+ render_heading_element(element)
80
+ when :divider
81
+ render_divider_element(element)
82
+ when :tabs
83
+ render_tabs_element(element)
84
+ end
85
+ end
86
+
87
+ def render_card(card_config)
88
+ div(class: card_css_classes(card_config)) do
89
+ if card_config[:title]
90
+ # Title gets standard padding even if card padding is none
91
+ title_padding = card_config[:padding] == "none" ? "px-4 py-4 md:px-5 md:py-5" : "px-4 py-4 md:px-5 md:py-5"
92
+ div(class: "#{title_padding} border-b border-gray-50") do
93
+ title_color = card_config[:color] == "dark" ? "text-white" : "text-gray-900"
94
+ h5(class: "text-base md:text-lg font-medium #{title_color}") { card_config[:title] }
95
+ end
96
+ end
97
+
98
+ # Content area respects card padding setting
99
+ if card_config[:padding] == "none"
100
+ # No additional padding - content fills the card
101
+ card_config[:elements].each do |element|
102
+ render_card_element(element)
103
+ end
104
+ else
105
+ # Standard padding for content
106
+ div(class: "p-4 md:p-5") do
107
+ card_config[:elements].each do |element|
108
+ render_card_element(element)
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
114
+
115
+ def render_metric_card(metric_config)
116
+ # Evaluate value if it's a lambda/proc
117
+ value = if metric_config[:value].respond_to?(:call)
118
+ metric_config[:value].call(@record)
119
+ else
120
+ metric_config[:value].to_s
121
+ end
122
+
123
+ div(class: metric_card_css_classes(metric_config)) do
124
+ div(class: "p-4 md:p-6 text-center") do
125
+ if metric_config[:icon]
126
+ div(class: "mb-2 md:mb-3") do
127
+ i(class: "#{metric_config[:icon]} text-lg md:text-xl")
128
+ end
129
+ end
130
+
131
+ h3(class: "text-xl md:text-2xl font-bold mb-1") { value }
132
+ p(class: "text-xs md:text-sm opacity-90 mb-0") { metric_config[:title] }
133
+
134
+ if metric_config[:trend]
135
+ small(class: "text-xs opacity-75 block mt-1") do
136
+ case metric_config[:trend]
137
+ when "increase"
138
+ "📈 Trending up"
139
+ when "decrease"
140
+ "📉 Trending down"
141
+ else
142
+ "📊 Stable"
143
+ end
144
+ end
145
+ end
146
+ end
147
+ end
148
+ end
149
+
150
+ def render_chart_card(chart_config)
151
+ # Lazy load chart cards via turbo-frame for performance
152
+ turbo_frame(
153
+ id: "chart-#{chart_config.object_id}",
154
+ src: lazy_chart_path(chart_config),
155
+ loading: "lazy",
156
+ class: card_css_classes(chart_config)
157
+ ) do
158
+ # Loading skeleton
159
+ div(class: "card") do
160
+ div(class: "card-header") do
161
+ h5(class: "card-title") { chart_config[:title] }
162
+ end
163
+ div(class: "card-body") do
164
+ div(class: "animate-pulse") do
165
+ div(class: "h-64 bg-gray-300 rounded")
166
+ end
167
+ end
168
+ end
169
+ end
170
+ end
171
+
172
+ def render_tabs_element(tabs_config)
173
+ div(data: { controller: "form-tabs" }) do
174
+ # Mobile-friendly tab navigation with horizontal scroll
175
+ div(class: "border-b border-gray-200 overflow-x-auto") do
176
+ nav(class: "-mb-px flex space-x-4 md:space-x-8 min-w-max px-4 md:px-0") do
177
+ tabs_config[:tabs].each_with_index do |tab, index|
178
+ button(
179
+ type: "button",
180
+ class: "whitespace-nowrap py-2 px-2 md:px-1 border-b-2 font-medium text-sm #{index == 0 || tab[:active] ? 'border-blue-600 text-blue-600' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'} flex-shrink-0",
181
+ data: {
182
+ form_tabs_target: "tabButton",
183
+ tab_id: tab[:name].downcase,
184
+ action: "click->form-tabs#switchTab"
185
+ }
186
+ ) do
187
+ if tab[:icon]
188
+ unsafe_raw "<i class=\"#{tab[:icon]} mr-1 md:mr-2 text-xs md:text-sm\"></i>"
189
+ end
190
+ span(class: "text-xs md:text-sm") { tab[:label] }
191
+ end
192
+ end
193
+ end
194
+ end
195
+
196
+ # Tab content with mobile padding
197
+ div(class: "mt-4 px-4 md:px-0") do
198
+ tabs_config[:tabs].each_with_index do |tab, index|
199
+ div(
200
+ class: "#{index == 0 || tab[:active] ? 'block' : 'hidden'}",
201
+ id: "#{tab[:name].downcase}-panel",
202
+ data: { form_tabs_target: "tabPanel" }
203
+ ) do
204
+ # Lazy load tab content that contains expensive elements
205
+ if tab_contains_expensive_elements?(tab)
206
+ turbo_frame(
207
+ id: "tab-#{tab[:name].downcase}-content",
208
+ src: lazy_tab_path(tab),
209
+ loading: "lazy"
210
+ ) do
211
+ render_loading_skeleton
212
+ end
213
+ else
214
+ tab[:elements].each do |element|
215
+ render_card_element(element)
216
+ end
217
+ end
218
+ end
219
+ end
220
+ end
221
+ end
222
+ end
223
+
224
+ def render_card_element(element)
225
+ case element[:type]
226
+ when :field
227
+ render_field_element(element)
228
+ when :content
229
+ render_content_element(element)
230
+ when :heading
231
+ render_heading_element(element)
232
+ when :divider
233
+ render_divider_element(element)
234
+ when :tabs
235
+ render_tabs_element(element)
236
+ end
237
+ end
238
+
239
+ def render_field_element(field_config)
240
+ # Skip field if label is explicitly set to false/nil and show_label is false
241
+ show_label = field_config[:show_label] != false && field_config[:label].present?
242
+
243
+ 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]}:" }
247
+ end
248
+ div(class: "col-sm-9") do
249
+ render_field_value(field_config)
250
+ end
251
+ end
252
+ else
253
+ # Field without label (inline or label: false)
254
+ div(class: "mb-3") do
255
+ render_field_value(field_config)
256
+ end
257
+ end
258
+ end
259
+
260
+ def render_field_value(field_config)
261
+ # Find the matching field configuration
262
+ field_definition = @resource_class.fields_config.find { |f| f[:name] == field_config[:name] }
263
+
264
+ 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)
269
+ else
270
+ span(class: "text-muted") { "Field not found" }
271
+ end
272
+ end
273
+
274
+ def render_content_element(content_config)
275
+ if content_config[:block]
276
+ # Execute the block in the context of this component
277
+ instance_eval(&content_config[:block])
278
+ else
279
+ div(class: content_config[:css_classes]) do
280
+ unsafe_raw content_config[:html]
281
+ end
282
+ end
283
+ end
284
+
285
+ def render_heading_element(heading_config)
286
+ tag_name = "h#{heading_config[:level]}"
287
+ public_send(tag_name, class: heading_css_classes(heading_config)) do
288
+ heading_config[:text]
289
+ end
290
+ end
291
+
292
+ def render_divider_element(divider_config)
293
+ hr(class: divider_css_classes(divider_config))
294
+ end
295
+
296
+ def render_default_field(field)
297
+ div(class: "mb-3 row") do
298
+ div(class: "col-sm-3") do
299
+ strong { "#{field[:label]}:" }
300
+ end
301
+ div(class: "col-sm-9") do
302
+ field_value = @record.public_send(field[:name])
303
+ render_field_component(field, field_value)
304
+ end
305
+ end
306
+ end
307
+
308
+ # Render field components directly
309
+ def render_field_component(field_definition, field_value, show_label: true)
310
+ component_class = field_component_class(field_definition[:type])
311
+
312
+ if component_class
313
+ # Create a modified field definition with show_label setting
314
+ modified_field = field_definition.merge(show_label: show_label)
315
+
316
+ component_instance = component_class.new(
317
+ field: modified_field,
318
+ value: field_value,
319
+ record: @record
320
+ )
321
+ render component_instance
322
+ else
323
+ # Fallback rendering for unknown field types
324
+ render_fallback_field(field_value, field_definition)
325
+ end
326
+ end
327
+
328
+ def field_component_class(field_type)
329
+ case field_type
330
+ when :string, :text
331
+ EasyAdmin::Fields::Show::TextComponent
332
+ when :number
333
+ EasyAdmin::Fields::Show::NumberComponent
334
+ when :email
335
+ EasyAdmin::Fields::Show::EmailComponent
336
+ when :date
337
+ EasyAdmin::Fields::Show::DateComponent
338
+ when :datetime
339
+ EasyAdmin::Fields::Show::DatetimeComponent
340
+ when :boolean
341
+ EasyAdmin::Fields::Show::BooleanComponent
342
+ when :select
343
+ EasyAdmin::Fields::Show::SelectComponent
344
+ when :belongs_to
345
+ EasyAdmin::Fields::Show::BelongsToComponent
346
+ when :has_many
347
+ EasyAdmin::Fields::Show::HasManyComponent
348
+ when :file
349
+ EasyAdmin::Fields::Show::FileComponent
350
+ when :json
351
+ EasyAdmin::Fields::Show::JsonComponent
352
+ else
353
+ nil
354
+ end
355
+ end
356
+
357
+ def render_fallback_field(field_value, field_definition)
358
+ case field_value
359
+ when true, false
360
+ span(class: "inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium #{'bg-green-100 text-green-800' if field_value} #{'bg-gray-100 text-gray-800' unless field_value}") do
361
+ field_value ? "Yes" : "No"
362
+ end
363
+ when Date, DateTime, Time
364
+ span(class: "text-gray-900") { field_value.strftime("%B %d, %Y") }
365
+ when Numeric
366
+ span(class: "text-gray-900") { field_value.to_s }
367
+ else
368
+ span(class: "text-gray-900") { field_value.to_s }
369
+ end
370
+ end
371
+
372
+ def render_loading_skeleton
373
+ div(class: "animate-pulse") do
374
+ 3.times do
375
+ div(class: "mb-4") do
376
+ div(class: "h-4 bg-gray-300 rounded w-1/4 mb-2")
377
+ div(class: "h-6 bg-gray-300 rounded w-3/4")
378
+ end
379
+ end
380
+ end
381
+ end
382
+
383
+ # Helper methods for lazy loading paths
384
+ def lazy_metric_path(metric_config)
385
+ # Generate path for lazy loading metric partial
386
+ EasyAdmin::Engine.routes.url_helpers.resource_metric_path(
387
+ resource_name: @resource_class.route_key,
388
+ id: @record.id,
389
+ metric_id: metric_config.object_id
390
+ )
391
+ end
392
+
393
+ def lazy_chart_path(chart_config)
394
+ # Generate path for lazy loading chart partial
395
+ EasyAdmin::Engine.routes.url_helpers.resource_chart_path(
396
+ resource_name: @resource_class.route_key,
397
+ id: @record.id,
398
+ chart_id: chart_config.object_id
399
+ )
400
+ end
401
+
402
+ def lazy_tab_path(tab_config)
403
+ # Generate path for lazy loading tab content
404
+ EasyAdmin::Engine.routes.url_helpers.resource_tab_content_path(
405
+ resource_name: @resource_class.route_key,
406
+ id: @record.id,
407
+ tab_name: tab_config[:name].downcase
408
+ )
409
+ end
410
+
411
+ def tab_contains_expensive_elements?(tab)
412
+ # Check if tab contains charts, metrics, or other expensive elements
413
+ tab[:elements].any? do |element|
414
+ [:chart_card, :metric_card, :dashboard].include?(element[:type])
415
+ end
416
+ end
417
+
418
+ # Turbo frame helper (for Phlex compatibility)
419
+ def turbo_frame(id:, src: nil, loading: nil, **options, &block)
420
+ turbo_frame_tag(id, src: src, loading: loading, **options, &block)
421
+ end
422
+
423
+ def turbo_frame_tag(id, **options, &block)
424
+ # Create turbo-frame element
425
+ element_options = { id: id }
426
+ element_options[:src] = options[:src] if options[:src]
427
+ element_options[:loading] = options[:loading] if options[:loading]
428
+ element_options[:class] = options[:class] if options[:class]
429
+
430
+ # Convert to HTML attributes format
431
+ html_attributes = element_options.map { |k, v| "#{k}=\"#{v}\"" }.join(" ")
432
+
433
+ unsafe_raw "<turbo-frame #{html_attributes}>"
434
+ yield if block_given?
435
+ unsafe_raw "</turbo-frame>"
436
+ end
437
+
438
+ private
439
+
440
+ # CSS class helpers with static classes for Tailwind detection
441
+ def row_css_classes(row_config)
442
+ # Only apply negative margins for multi-column layouts
443
+ has_multiple_columns = row_config[:columns_count] > 1
444
+
445
+ base_classes = "flex flex-col md:flex-row md:flex-wrap mb-6"
446
+
447
+ case row_config[:spacing]
448
+ when "none"
449
+ negative_margin = has_multiple_columns ? "md:-mx-0" : ""
450
+ "#{base_classes} space-y-0 md:space-y-0 #{negative_margin}"
451
+ when "small"
452
+ negative_margin = has_multiple_columns ? "md:-mx-1" : ""
453
+ "#{base_classes} space-y-2 md:space-y-0 #{negative_margin}"
454
+ when "medium"
455
+ negative_margin = has_multiple_columns ? "md:-mx-2" : ""
456
+ "#{base_classes} space-y-4 md:space-y-0 #{negative_margin}"
457
+ when "large"
458
+ negative_margin = has_multiple_columns ? "md:-mx-4" : ""
459
+ "#{base_classes} space-y-6 md:space-y-0 #{negative_margin}"
460
+ else
461
+ negative_margin = has_multiple_columns ? "md:-mx-2" : ""
462
+ "#{base_classes} space-y-4 md:space-y-0 #{negative_margin}"
463
+ end
464
+ end
465
+
466
+ def column_css_classes(column_config)
467
+ # Calculate width based on the total number of columns in the row
468
+ total_columns = column_config[:row_columns_count] || 1
469
+
470
+ # Only add horizontal padding for multi-column layouts
471
+ padding_classes = total_columns > 1 ? "px-0 md:px-2" : "px-0"
472
+ base_classes = "w-full mb-4 md:mb-0 #{padding_classes}"
473
+
474
+ case total_columns
475
+ when 1
476
+ "#{base_classes} md:w-full"
477
+ when 2
478
+ "#{base_classes} md:w-1/2"
479
+ when 3
480
+ "#{base_classes} md:w-1/3"
481
+ when 4
482
+ "#{base_classes} md:w-1/4"
483
+ when 6
484
+ "#{base_classes} md:w-1/6"
485
+ else
486
+ "#{base_classes} md:flex-1"
487
+ end
488
+ end
489
+
490
+ def card_css_classes(card_config)
491
+ classes = ["rounded-2xl mb-4 md:mb-0 overflow-hidden"]
492
+
493
+ # Color classes
494
+ case card_config[:color]
495
+ when "primary"
496
+ classes << "bg-blue-50 border border-blue-100"
497
+ when "secondary"
498
+ classes << "bg-gray-50 border border-gray-200"
499
+ when "success"
500
+ classes << "bg-green-50 border border-green-100"
501
+ when "danger"
502
+ classes << "bg-red-50 border border-red-100"
503
+ when "warning"
504
+ classes << "bg-yellow-50 border border-yellow-100"
505
+ when "info"
506
+ classes << "bg-cyan-50 border border-cyan-100"
507
+ when "dark"
508
+ classes << "bg-gray-800 text-white border border-gray-700"
509
+ when "white"
510
+ classes << "bg-white border border-gray-200"
511
+ when "transparent"
512
+ classes << "bg-transparent border-0"
513
+ classes.delete_if { |c| c.include?("rounded") } # Remove rounded corners for transparent
514
+ classes << "rounded-none"
515
+ else # light
516
+ classes << "bg-gray-50 border border-gray-200"
517
+ end
518
+
519
+ # Shadow classes
520
+ case card_config[:shadow]
521
+ when "none"
522
+ # No shadow
523
+ when "small"
524
+ classes << "shadow-sm"
525
+ when "medium"
526
+ classes << "shadow-md"
527
+ when "large"
528
+ classes << "shadow-lg"
529
+ else
530
+ classes << "shadow-sm" # default
531
+ end
532
+
533
+ # Padding classes
534
+ padding = card_config[:padding] || "medium"
535
+ case padding
536
+ when "none"
537
+ classes << "p-0"
538
+ when "small"
539
+ classes << "p-2 sm:p-3"
540
+ when "medium"
541
+ classes << "p-4 sm:p-6"
542
+ when "large"
543
+ classes << "p-6 sm:p-8"
544
+ else
545
+ classes << "p-4 sm:p-6" # default
546
+ end
547
+
548
+ classes.join(" ")
549
+ end
550
+
551
+ def metric_card_css_classes(metric_config)
552
+ case metric_config[:color]
553
+ when "primary"
554
+ "rounded-2xl shadow-sm border border-gray-100 h-full mb-4 md:mb-0 overflow-hidden bg-blue-500 text-white"
555
+ when "secondary"
556
+ "rounded-2xl shadow-sm border border-gray-100 h-full mb-4 md:mb-0 overflow-hidden bg-gray-500 text-white"
557
+ when "success"
558
+ "rounded-2xl shadow-sm border border-gray-100 h-full mb-4 md:mb-0 overflow-hidden bg-green-500 text-white"
559
+ when "danger"
560
+ "rounded-2xl shadow-sm border border-gray-100 h-full mb-4 md:mb-0 overflow-hidden bg-red-500 text-white"
561
+ when "warning"
562
+ "rounded-2xl shadow-sm border border-gray-100 h-full mb-4 md:mb-0 overflow-hidden bg-orange-500 text-white"
563
+ when "info"
564
+ "rounded-2xl shadow-sm border border-gray-100 h-full mb-4 md:mb-0 overflow-hidden bg-blue-400 text-white"
565
+ when "dark"
566
+ "rounded-2xl shadow-sm border border-gray-100 h-full mb-4 md:mb-0 overflow-hidden bg-gray-800 text-white"
567
+ else
568
+ "rounded-2xl shadow-sm border border-gray-100 h-full mb-4 md:mb-0 overflow-hidden bg-gray-100 text-gray-900"
569
+ end
570
+ end
571
+
572
+ def heading_css_classes(heading_config)
573
+ case heading_config[:size]
574
+ when "small"
575
+ "mb-3 font-semibold text-gray-900 text-sm"
576
+ when "medium"
577
+ "mb-3 font-semibold text-gray-900 text-base"
578
+ when "large"
579
+ "mb-3 font-semibold text-gray-900 text-lg"
580
+ else
581
+ "mb-3 font-semibold text-gray-900 text-base"
582
+ end
583
+ end
584
+
585
+ def divider_css_classes(divider_config)
586
+ case divider_config[:style]
587
+ when "default"
588
+ "border-0 border-t my-4 border-gray-200"
589
+ when "dashed"
590
+ "border-0 border-t my-4 border-dashed border-gray-300"
591
+ when "dotted"
592
+ "border-0 border-t my-4 border-dotted border-gray-300"
593
+ when "thick"
594
+ "border-0 border-t-2 my-4 border-gray-400"
595
+ else
596
+ "border-0 border-t my-4 border-gray-200"
597
+ end
598
+ end
599
+ end
600
+ end