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,192 @@
1
+ module EasyAdmin
2
+ module Fields
3
+ module Show
4
+ class HasManyComponent < BaseComponent
5
+ private
6
+
7
+ def view_template
8
+ div(class: "field-display ea-has-many-field") do
9
+ # Only show label if show_label is not explicitly false
10
+ if field.fetch(:show_label, true)
11
+ div(class: "field-label font-medium text-sm text-gray-700 mb-3") { plain field_label }
12
+ end
13
+
14
+ div(class: "field-value") do
15
+ render_has_many_table
16
+ end
17
+ end
18
+ end
19
+
20
+ def render_has_many_table
21
+ if value.respond_to?(:each) && value.any?
22
+ div(class: "overflow-hidden shadow ring-1 ring-black ring-opacity-5 md:rounded-lg") do
23
+ table(class: "table-striped min-w-full divide-y divide-gray-300") do
24
+ thead(class: "bg-gray-50") do
25
+ tr do
26
+ association_fields.each do |field|
27
+ th(scope: "col", class: "px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wide") do
28
+ plain field[:label]
29
+ end
30
+ end
31
+ th(scope: "col", class: "relative px-6 py-3") do
32
+ span(class: "sr-only") { "Actions" }
33
+ end
34
+ end
35
+ end
36
+ tbody(class: "divide-y divide-gray-200 bg-white") do
37
+ records_to_show.each do |item|
38
+ tr(class: "hover:bg-gray-100 transition-colors duration-150") do
39
+ association_fields.each do |field|
40
+ td(class: "whitespace-nowrap px-6 py-4 text-sm") do
41
+ div(class: "text-gray-900 font-medium") do
42
+ field_value = item.public_send(field[:name])
43
+ plain format_association_value(field_value, field[:type])
44
+ end
45
+ end
46
+ end
47
+ td(class: "relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-6") do
48
+ div(class: "flex items-center justify-end space-x-2") do
49
+ a(href: view_path_for(item),
50
+ class: "inline-flex items-center justify-center w-8 h-8 text-blue-600 hover:text-blue-800 hover:bg-blue-50 rounded-lg transition-colors duration-150",
51
+ title: "View") do
52
+ unsafe_raw %(<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
53
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
54
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/>
55
+ </svg>)
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
64
+
65
+ if value.count > 5
66
+ div(class: "text-sm text-gray-500 mt-3 text-center") do
67
+ "Showing #{records_to_show.count} of #{value.count} #{field_label.downcase}."
68
+ end
69
+ end
70
+ else
71
+ div(class: "text-center py-8 text-gray-500") do
72
+ div(class: "text-lg") { "📄" }
73
+ div(class: "mt-2 text-sm") { "No #{field_label.downcase} found." }
74
+ end
75
+ end
76
+ end
77
+
78
+ def render_skeleton_loader
79
+ div(class: "ea-has-many-skeleton") do
80
+ skeleton_count.times do
81
+ div(class: "ea-skeleton-item") do
82
+ div(class: "ea-skeleton-line ea-skeleton-line--title")
83
+ div(class: "ea-skeleton-line ea-skeleton-line--subtitle")
84
+ div(class: "ea-skeleton-actions") do
85
+ 3.times do
86
+ div(class: "ea-skeleton-button")
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
93
+
94
+ def turbo_frame_id
95
+ "has_many_#{field_name}_#{record.id}"
96
+ end
97
+
98
+ def lazy_load_url
99
+ # TODO: Create proper route helper for has_many lazy loading
100
+ # For now, construct URL dynamically based on engine mount point
101
+ "#{EasyAdmin::Engine.routes.url_helpers.resources_path(resource_name: resource_route_key).chomp('/')}/#{record.id}/has_many/#{field_name}"
102
+ end
103
+
104
+ def resource_route_key
105
+ record.class.name.underscore.pluralize
106
+ end
107
+
108
+ def skeleton_count
109
+ field[:skeleton_count] || 3
110
+ end
111
+
112
+ def association_fields
113
+ @association_fields ||= determine_association_fields
114
+ end
115
+
116
+ def determine_association_fields
117
+ return field[:fields] if field[:fields]
118
+
119
+ # Get the association class
120
+ association_class = determine_association_class
121
+ return [] unless association_class
122
+
123
+ # Default fields - try to get the most common ones
124
+ fields = []
125
+
126
+ # Add ID field
127
+ fields << { name: :id, label: "ID", type: :number }
128
+
129
+ # Try common display fields
130
+ if association_class.column_names.include?('name')
131
+ fields << { name: :name, label: "Name", type: :text }
132
+ elsif association_class.column_names.include?('title')
133
+ fields << { name: :title, label: "Title", type: :text }
134
+ end
135
+
136
+ # Add timestamps if available
137
+ if association_class.column_names.include?('created_at')
138
+ fields << { name: :created_at, label: "Created", type: :datetime }
139
+ end
140
+
141
+ fields.take(3) # Limit to 3 fields for readability
142
+ end
143
+
144
+ def determine_association_class
145
+ return field[:association] if field[:association]
146
+
147
+ # Try to determine from the field name or value
148
+ if value.respond_to?(:first) && value.first
149
+ value.first.class
150
+ elsif field_name.to_s.end_with?('s')
151
+ # Pluralized association name, try to singularize and classify
152
+ field_name.to_s.singularize.classify.constantize
153
+ else
154
+ field_name.to_s.classify.constantize
155
+ end
156
+ rescue NameError
157
+ nil
158
+ end
159
+
160
+ def records_to_show
161
+ @records_to_show ||= value.limit(5)
162
+ end
163
+
164
+ def format_association_value(val, type)
165
+ return "—" if val.blank?
166
+
167
+ case type
168
+ when :datetime
169
+ val.respond_to?(:strftime) ? val.strftime('%b %d, %Y at %I:%M %p') : val.to_s
170
+ when :date
171
+ val.respond_to?(:strftime) ? val.strftime('%b %d, %Y') : val.to_s
172
+ when :boolean
173
+ val ? "Yes" : "No"
174
+ else
175
+ val.to_s
176
+ end
177
+ end
178
+
179
+ def view_path_for(record)
180
+ return field[:view_path_proc].call(record) if field[:view_path_proc]
181
+
182
+ association_class = record.class
183
+ EasyAdmin::Engine.routes.url_helpers.resource_path(
184
+ resource_name: association_class.model_name.route_key,
185
+ id: record.id
186
+ )
187
+ end
188
+
189
+ end
190
+ end
191
+ end
192
+ end
@@ -0,0 +1,45 @@
1
+ module EasyAdmin
2
+ module Fields
3
+ module Show
4
+ class JsonComponent < Fields::BaseComponent
5
+ def view_template
6
+ div(class: "space-y-2") do
7
+ div(class: "text-sm font-medium text-gray-700") { field_label }
8
+ div(class: "bg-white border rounded-lg overflow-hidden") do
9
+ if json_value.present?
10
+ div(
11
+ data: {
12
+ controller: "jsoneditor",
13
+ jsoneditor_mode_value: "view",
14
+ jsoneditor_data_value: parsed_json_data.to_json
15
+ },
16
+ style: "height: 300px;"
17
+ )
18
+ else
19
+ div(class: "p-4 text-center text-gray-400 italic") { "No data" }
20
+ end
21
+ end
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def json_value
28
+ @json_value ||= value
29
+ end
30
+
31
+ def parsed_json_data
32
+ if json_value.is_a?(String)
33
+ begin
34
+ JSON.parse(json_value)
35
+ rescue JSON::ParserError
36
+ { error: "Invalid JSON", raw_value: json_value }
37
+ end
38
+ else
39
+ json_value
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,20 @@
1
+ module EasyAdmin
2
+ module Fields
3
+ module Show
4
+ class NumberComponent < BaseComponent
5
+ def view_template
6
+ div(class: "ea-show-value ea-show-number") do
7
+ format_value
8
+ end
9
+ end
10
+
11
+ private
12
+
13
+ def format_value
14
+ return "" if value.blank?
15
+ value.to_s
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,25 @@
1
+ module EasyAdmin
2
+ module Fields
3
+ module Show
4
+ class SelectComponent < BaseComponent
5
+ def view_template
6
+ div(class: "ea-show-value ea-show-select") do
7
+ if value.present?
8
+ if value.is_a?(Array)
9
+ div(class: "ea-select-multiple") do
10
+ value.each do |item|
11
+ span(class: "ea-select-tag") { plain item.to_s }
12
+ end
13
+ end
14
+ else
15
+ plain value.to_s
16
+ end
17
+ else
18
+ span(class: "ea-empty-value") { plain "—" }
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,17 @@
1
+ module EasyAdmin
2
+ module Fields
3
+ module Show
4
+ class TextComponent < BaseComponent
5
+ def view_template
6
+ div(class: "ea-show-value") do
7
+ if value.present?
8
+ value.to_s
9
+ else
10
+ span(class: "ea-empty-value") { "—" }
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,26 @@
1
+ module EasyAdmin
2
+ module Fields
3
+ module Show
4
+ class TextareaComponent < BaseComponent
5
+ def view_template
6
+ div(class: "ea-show-value") do
7
+ if value.present?
8
+ div(class: "ea-text-content") do
9
+ simple_format(value)
10
+ end
11
+ else
12
+ span(class: "ea-empty-value") { "—" }
13
+ end
14
+ end
15
+ end
16
+
17
+ private
18
+
19
+ def simple_format(text)
20
+ # Simple implementation of Rails' simple_format helper
21
+ text.to_s.gsub(/\r\n?/, "\n").gsub(/\n/, "<br>").html_safe
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,120 @@
1
+ module EasyAdmin
2
+ class FiltersComponent < Phlex::HTML
3
+ def initialize(resource_class:, current_params: {}, search_params: {})
4
+ @resource_class = resource_class
5
+ @current_params = current_params
6
+ @search_params = search_params || {}
7
+ end
8
+
9
+ def view_template
10
+ return unless @resource_class.filterable_fields.any?
11
+
12
+ div(class: "bg-white shadow rounded-lg mb-6") do
13
+ form(
14
+ method: "get",
15
+ action: route_helper.resources_path(@resource_class.route_key),
16
+ data: { turbo_frame: "table-frame" },
17
+ class: "space-y-4"
18
+ ) do
19
+ # Preserve existing params (except search params)
20
+ preserve_params
21
+
22
+ div(class: "p-6 border-b border-gray-200") do
23
+ div(class: "flex items-center justify-between mb-4") do
24
+ div do
25
+ h3(class: "text-lg font-medium text-gray-900") { "Filters" }
26
+ p(class: "text-sm text-gray-500") { "Filter #{@resource_class.title.downcase} by specific criteria" }
27
+ end
28
+
29
+ # Clear filters button
30
+ if has_active_filters?
31
+ a(
32
+ href: build_clear_filters_url,
33
+ class: "text-sm text-red-600 hover:text-red-800 font-medium",
34
+ data: { turbo_frame: "table-frame" }
35
+ ) { "Clear all filters" }
36
+ end
37
+ end
38
+
39
+ div(class: "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4") do
40
+ @resource_class.filterable_fields.each do |field|
41
+ render_filter_field(field)
42
+ end
43
+ end
44
+ end
45
+
46
+ div(class: "px-6 py-4 bg-gray-50") do
47
+ div(class: "flex items-center justify-end space-x-3") do
48
+ button(
49
+ type: "submit",
50
+ class: "inline-flex items-center px-4 py-2 bg-blue-600 text-white text-sm font-medium rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
51
+ ) do
52
+ unsafe_raw '<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
53
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707v6.586l-4-4V9.414a1 1 0 00-.293-.707L3.293 6.293A1 1 0 013 5.586V4z"></path>
54
+ </svg>'
55
+ span { "Apply Filters" }
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
62
+
63
+ private
64
+
65
+ def route_helper
66
+ @route_helper ||= EasyAdmin::Engine.routes.url_helpers
67
+ end
68
+
69
+ def preserve_params
70
+ # Preserve non-filter params (scope, sort, direction, period, page)
71
+ preserved_params = @current_params.except(:q)
72
+ preserved_params.each do |key, value|
73
+ input(type: "hidden", name: key, value: value)
74
+ end
75
+ end
76
+
77
+ def render_filter_field(field)
78
+ component_class = filter_component_for_field(field)
79
+ render component_class.new(field: field, search_params: @search_params)
80
+ end
81
+
82
+ def filter_component_for_field(field)
83
+ case field[:type]
84
+ when :string, :email, :text
85
+ EasyAdmin::Fields::Index::Filters::StringComponent
86
+ when :boolean
87
+ EasyAdmin::Fields::Index::Filters::BooleanComponent
88
+ when :select, :belongs_to
89
+ EasyAdmin::Fields::Index::Filters::SelectComponent
90
+ when :number, :integer
91
+ EasyAdmin::Fields::Index::Filters::NumberComponent
92
+ when :date, :datetime
93
+ EasyAdmin::Fields::Index::Filters::DateComponent
94
+ else
95
+ EasyAdmin::Fields::Index::Filters::StringComponent
96
+ end
97
+ end
98
+
99
+ def has_active_filters?
100
+ return false unless @search_params.present?
101
+
102
+ @search_params.each do |key, value|
103
+ return true if value.present?
104
+ end
105
+
106
+ false
107
+ end
108
+
109
+ def build_clear_filters_url
110
+ base_url = route_helper.resources_path(@resource_class.route_key)
111
+ clear_params = @current_params.except(:q)
112
+
113
+ if clear_params.present? && !clear_params.empty?
114
+ "#{base_url}?#{clear_params.to_query}"
115
+ else
116
+ base_url
117
+ end
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,166 @@
1
+ module EasyAdmin
2
+ class FormTabsComponent < Phlex::HTML
3
+ include EasyAdmin::FieldsHelper
4
+
5
+ def initialize(resource_class:, form:)
6
+ @resource_class = resource_class
7
+ @form = form
8
+ end
9
+
10
+ def view_template
11
+ div(
12
+ class: "form-tabs",
13
+ data: {
14
+ controller: "form-tabs",
15
+ action: "tab:selected@window->form-tabs#handleTabSelection"
16
+ }
17
+ ) do
18
+ render_tab_navigation
19
+ render_tab_content
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ def render_tab_navigation
26
+ nav(class: "border-b border-gray-200 mb-4 sm:mb-6") do
27
+ # Mobile dropdown for tabs
28
+ div(class: "sm:hidden relative", data: { controller: "dropdown" }) do
29
+ button(
30
+ type: "button",
31
+ class: "flex items-center justify-between w-full px-3 py-2 text-base border border-gray-300 rounded-md bg-white focus:outline-none focus:ring-blue-500 focus:border-blue-500",
32
+ data: {
33
+ action: "click->dropdown#toggle",
34
+ dropdown_target: "trigger",
35
+ form_tabs_target: "mobileButton"
36
+ }
37
+ ) do
38
+ span(data: { form_tabs_target: "mobileButtonText" }) { @resource_class.form_tabs.first[:label] }
39
+ unsafe_raw('<svg class="w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/></svg>')
40
+ end
41
+
42
+ # Dropdown menu
43
+ div(
44
+ class: "absolute right-0 mt-2 w-full bg-white rounded-md shadow-lg ring-1 ring-black ring-opacity-5 z-10 hidden",
45
+ data: { dropdown_target: "menu" }
46
+ ) do
47
+ div(class: "py-1") do
48
+ @resource_class.form_tabs.each do |tab|
49
+ button(
50
+ type: "button",
51
+ class: "block w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 hover:text-gray-900",
52
+ data: {
53
+ action: "click->dropdown#close click->form-tabs#emitTabSelection",
54
+ tab_id: tab[:name].downcase.gsub(/\s+/, '-'),
55
+ tab_label: tab[:label]
56
+ }
57
+ ) do
58
+ tab[:label]
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
64
+
65
+ # Desktop tabs
66
+ ul(class: "hidden sm:flex flex-wrap -mb-px text-sm font-medium text-center", role: "tablist") do
67
+ @resource_class.form_tabs.each_with_index do |tab, index|
68
+ render_tab_button(tab, index)
69
+ end
70
+ end
71
+ end
72
+ end
73
+
74
+ def render_tab_button(tab, index)
75
+ li(class: "mr-2", role: "presentation") do
76
+ button(
77
+ class: tab_button_classes(index == 0),
78
+ id: "#{tab[:name].downcase.gsub(/\s+/, '-')}-tab",
79
+ data: {
80
+ form_tabs_target: "tabButton",
81
+ tab_id: tab[:name].downcase.gsub(/\s+/, '-'),
82
+ action: "click->form-tabs#switchTab"
83
+ },
84
+ type: "button",
85
+ role: "tab"
86
+ ) do
87
+ if tab[:icon]
88
+ span(class: "inline-flex items-center") do
89
+ unsafe_raw(tab_icon(tab[:icon]))
90
+ span(class: "ml-2") { tab[:label] }
91
+ end
92
+ else
93
+ plain tab[:label]
94
+ end
95
+ end
96
+ end
97
+ end
98
+
99
+ def render_tab_content
100
+ div(class: "tab-content") do
101
+ @resource_class.form_tabs.each_with_index do |tab, index|
102
+ render_tab_panel(tab, index)
103
+ end
104
+ end
105
+ end
106
+
107
+ def render_tab_panel(tab, index)
108
+ div(
109
+ class: tab_panel_classes(index == 0),
110
+ id: "#{tab[:name].downcase.gsub(/\s+/, '-')}-panel",
111
+ data: { form_tabs_target: "tabPanel" },
112
+ role: "tabpanel"
113
+ ) do
114
+ div(class: "bg-white shadow-sm rounded-lg border border-gray-200") do
115
+ div(class: "px-4 py-5 sm:p-6") do
116
+ h3(class: "text-lg font-medium leading-6 text-gray-900 mb-6") do
117
+ tab[:label]
118
+ end
119
+
120
+ div(class: "grid grid-cols-1 gap-6") do
121
+ tab[:fields].each do |field|
122
+ div do
123
+ unsafe_raw render_field(field, action: :form, form: @form)
124
+ end
125
+ end
126
+ end
127
+ end
128
+ end
129
+ end
130
+ end
131
+
132
+ def tab_button_classes(active = false)
133
+ base_classes = "inline-flex items-center p-4 border-b-2 rounded-t-lg transition-colors duration-200"
134
+
135
+ if active
136
+ active_classes = "text-blue-600 border-blue-600"
137
+ else
138
+ inactive_classes = "text-gray-500 border-transparent hover:text-gray-600 hover:border-gray-300"
139
+ end
140
+
141
+ "#{base_classes} #{active ? active_classes : inactive_classes}"
142
+ end
143
+
144
+ def tab_panel_classes(active = false)
145
+ base_classes = "tab-panel"
146
+ visibility_classes = active ? "block" : "hidden"
147
+
148
+ "#{base_classes} #{visibility_classes}"
149
+ end
150
+
151
+ def tab_icon(icon_name)
152
+ case icon_name.to_sym
153
+ when :user
154
+ '<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/></svg>'
155
+ when :security
156
+ '<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"/></svg>'
157
+ when :settings
158
+ '<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/></svg>'
159
+ when :info
160
+ '<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>'
161
+ else
162
+ '<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/></svg>'
163
+ end
164
+ end
165
+ end
166
+ end