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,326 @@
1
+ module EasyAdmin
2
+ module Fields
3
+ module Form
4
+ class SelectComponent < EasyAdmin::Fields::BaseComponent
5
+ def view_template
6
+ div(class: "mb-4") do
7
+ label(for: field_id, class: label_classes) do
8
+ plain field_label
9
+ if required?
10
+ span(class: "text-red-500 ml-1") { "*" }
11
+ end
12
+ end
13
+
14
+ div(
15
+ class: select_container_classes,
16
+ data: {
17
+ controller: "select-field",
18
+ select_field_multiple_value: multiple?.to_s,
19
+ select_field_placeholder_value: placeholder,
20
+ select_field_suggest_value: suggest_mode?.to_s,
21
+ select_field_suggest_url_value: suggest_mode? ? suggest_url : "",
22
+ field_name: field_name
23
+ }
24
+ ) do
25
+ if multiple?
26
+ render_multiple_select_container
27
+ else
28
+ render_single_select_input
29
+ end
30
+ render_select_dropdown
31
+ render_clear_button if multiple?
32
+ render_hidden_inputs
33
+ end
34
+
35
+ if field[:help_text]
36
+ p(class: "mt-1 text-sm text-gray-500") { field[:help_text] }
37
+ end
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ def multiple?
44
+ result = field[:multiple] == true
45
+ result
46
+ end
47
+
48
+ def options
49
+ field[:options] || []
50
+ end
51
+
52
+ def placeholder
53
+ field[:placeholder] || "Search #{field_label.downcase}..."
54
+ end
55
+
56
+ def suggest_mode?
57
+ field[:suggest].present?
58
+ end
59
+
60
+ def suggest_url
61
+ return "" unless suggest_mode?
62
+
63
+ # Extract resource name from form object
64
+ resource_name = form.object.class.name.underscore.pluralize
65
+
66
+ # For belongs_to fields that were converted to select, use the original association name
67
+ search_field_name = if field[:original_type] == :belongs_to && field[:original_name]
68
+ field[:original_name]
69
+ else
70
+ field_name
71
+ end
72
+
73
+ easy_admin_url_helpers.suggest_resource_path(resource_name, field: search_field_name)
74
+ end
75
+
76
+ def label_classes
77
+ "block text-sm font-medium text-gray-700 mb-1"
78
+ end
79
+
80
+ def select_container_classes
81
+ base_classes = "relative"
82
+ classes = [base_classes]
83
+ classes << "select-multiple" if multiple?
84
+ classes.join(" ")
85
+ end
86
+
87
+ def render_multiple_select_container
88
+ div(class: "relative border border-gray-300 rounded-md bg-white min-h-10 focus-within:ring-1 focus-within:ring-blue-500 focus-within:border-blue-500 transition-colors duration-200") do
89
+ div(class: "flex flex-wrap items-center gap-1 p-2 pr-8") do
90
+ # Selected items container - will be populated by Stimulus controller
91
+ div(class: "flex flex-wrap items-center gap-1", data: { select_field_target: "selectedItems" }) do
92
+ current_selected_values.each do |selected_value|
93
+ render_selected_item(selected_value)
94
+ end
95
+ end
96
+
97
+ # Search input
98
+ input(
99
+ type: "text",
100
+ class: "flex-1 min-w-20 border-none outline-none text-sm placeholder-gray-400 bg-transparent",
101
+ placeholder: current_selected_values.empty? ? placeholder : "",
102
+ data: {
103
+ select_field_target: "search",
104
+ action: "input->select-field#filter keydown->select-field#handleKeydown focus->select-field#openDropdown"
105
+ }
106
+ )
107
+ end
108
+ end
109
+ end
110
+
111
+ def render_search_input
112
+ input(
113
+ type: "text",
114
+ class: search_input_classes,
115
+ placeholder: placeholder,
116
+ data: {
117
+ select_field_target: "search",
118
+ action: "input->select-field#filter keydown->select-field#handleKeydown"
119
+ }
120
+ )
121
+ end
122
+
123
+ def render_single_select_input
124
+ div(class: "relative") do
125
+ input(
126
+ type: "text",
127
+ class: single_select_input_classes,
128
+ placeholder: suggest_mode? ? placeholder : (field[:placeholder] || "Select #{field_label.downcase}..."),
129
+ readonly: !suggest_mode?,
130
+ value: suggest_mode? ? "" : current_display_value,
131
+ data: suggest_mode? ? {
132
+ select_field_target: "search",
133
+ action: "input->select-field#filter keydown->select-field#handleKeydown focus->select-field#openDropdown"
134
+ } : {
135
+ select_field_target: "display",
136
+ action: "click->select-field#toggleDropdown"
137
+ }
138
+ )
139
+ # Dropdown arrow
140
+ div(class: "absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none") do
141
+ 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"></path></svg>')
142
+ end
143
+ end
144
+ end
145
+
146
+ private
147
+
148
+ def search_input_classes
149
+ base_classes = "block w-full px-3 py-2 text-sm border rounded-md"
150
+ state_classes = "border-gray-300 placeholder-gray-400 bg-white"
151
+ focus_classes = "focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500"
152
+ hover_classes = "hover:border-gray-400"
153
+ transition_classes = "transition-colors duration-200"
154
+
155
+ "#{base_classes} #{state_classes} #{focus_classes} #{hover_classes} #{transition_classes}"
156
+ end
157
+
158
+ def single_select_input_classes
159
+ base_classes = "block w-full px-3 py-2 pr-10 text-sm border rounded-md cursor-pointer"
160
+ state_classes = "border-gray-300 placeholder-gray-400 bg-white"
161
+ focus_classes = "focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500"
162
+ hover_classes = "hover:border-gray-400"
163
+ transition_classes = "transition-colors duration-200"
164
+
165
+ "#{base_classes} #{state_classes} #{focus_classes} #{hover_classes} #{transition_classes}"
166
+ end
167
+
168
+ def selected_items_classes
169
+ base_classes = "selected-items flex flex-wrap gap-1"
170
+ padding_classes = "p-2 min-h-10"
171
+ border_classes = "border border-gray-300 rounded-md bg-white"
172
+
173
+ "#{base_classes} #{padding_classes} #{border_classes}"
174
+ end
175
+
176
+ def render_selected_item(selected_value)
177
+ span(
178
+ class: "selected-item inline-flex items-center px-2 py-1 text-xs bg-blue-100 text-blue-800 rounded cursor-pointer hover:bg-blue-200 transition-colors duration-150",
179
+ data: {
180
+ value: selected_value,
181
+ action: "click->select-field#removeItem"
182
+ }
183
+ ) do
184
+ plain display_text_for_value(selected_value)
185
+ span(class: "remove-item ml-1 text-blue-600 hover:text-blue-800 font-medium") { plain "×" }
186
+ end
187
+ end
188
+
189
+ def render_select_dropdown
190
+ div(
191
+ class: "select-dropdown absolute z-50 w-full mt-1 bg-white border border-gray-300 rounded-md shadow-lg max-h-60 overflow-auto opacity-0 invisible transform scale-95 transition-all duration-200 ease-out",
192
+ data: { select_field_target: "dropdown" }
193
+ ) do
194
+ if suggest_mode?
195
+ # For suggest mode, options will be loaded dynamically
196
+ render_no_results_message
197
+ render_loading_message
198
+ else
199
+ # For static mode, render all options
200
+ options.each do |option|
201
+ render_option(option)
202
+ end
203
+ render_no_results_message
204
+ end
205
+ end
206
+ end
207
+
208
+ def render_option(option)
209
+ option_value, option_text = extract_option_value_and_text(option)
210
+
211
+ div(
212
+ class: "select-option px-3 py-2 text-sm text-gray-900 cursor-pointer hover:bg-blue-50 hover:text-blue-900 transition-colors duration-150",
213
+ data: {
214
+ value: option_value,
215
+ action: "click->select-field#selectOption",
216
+ select_field_target: "option"
217
+ }
218
+ ) do
219
+ plain option_text.to_s
220
+ end
221
+ end
222
+
223
+ def render_no_results_message
224
+ div(
225
+ class: "select-no-results px-3 py-2 text-sm text-gray-500 text-center",
226
+ data: { select_field_target: "noResults" },
227
+ style: "display: none;"
228
+ ) do
229
+ plain "No options found"
230
+ end
231
+ end
232
+
233
+ def render_loading_message
234
+ div(
235
+ class: "select-loading px-3 py-2 text-sm text-gray-500 text-center",
236
+ data: { select_field_target: "loading" },
237
+ style: "display: none;"
238
+ ) do
239
+ plain "Loading..."
240
+ end
241
+ end
242
+
243
+ def render_clear_button
244
+ button(
245
+ type: "button",
246
+ class: "select-clear absolute top-2 right-8 p-1 text-gray-400 hover:text-gray-600 bg-white rounded transition-colors duration-150",
247
+ data: { action: "click->select-field#clearAll" },
248
+ title: "Clear all selections"
249
+ ) do
250
+ unsafe_raw('<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="M6 18L18 6M6 6l12 12"/></svg>')
251
+ end
252
+ end
253
+
254
+ def render_hidden_inputs
255
+ if multiple?
256
+ current_selected_values.each do |selected_value|
257
+ input(
258
+ type: "hidden",
259
+ name: "#{form_field_name}[]",
260
+ value: selected_value,
261
+ data: { select_field_target: "hiddenInput" }
262
+ )
263
+ end
264
+ else
265
+ # Single select - one hidden input
266
+ current_value = form.object.public_send(field_name) if form.object.respond_to?(field_name)
267
+ input(
268
+ type: "hidden",
269
+ name: form_field_name,
270
+ value: current_value || "",
271
+ data: { select_field_target: "hiddenInput" }
272
+ )
273
+ end
274
+ end
275
+
276
+ def extract_option_value_and_text(option)
277
+ if option.is_a?(Array)
278
+ [option[1], option[0]]
279
+ elsif option.is_a?(Hash)
280
+ [option[:value] || option["value"], option[:text] || option["text"] || option[:label] || option["label"]]
281
+ else
282
+ [option, option]
283
+ end
284
+ end
285
+
286
+ def form_field_name
287
+ model_name = form.object.class.name.underscore
288
+ "#{model_name}[#{field_name}]"
289
+ end
290
+
291
+ def current_selected_values
292
+ current_value = form.object.public_send(field_name) if form.object.respond_to?(field_name)
293
+
294
+ if multiple?
295
+ current_value.is_a?(Array) ? current_value : []
296
+ else
297
+ current_value.present? ? [current_value] : []
298
+ end
299
+ end
300
+
301
+ def current_display_value
302
+ current_value = form.object.public_send(field_name) if form.object.respond_to?(field_name)
303
+ return "" if current_value.blank?
304
+
305
+ # Find the display text for the current value
306
+ options.each do |option|
307
+ option_value, option_text = extract_option_value_and_text(option)
308
+ return option_text if option_value.to_s == current_value.to_s
309
+ end
310
+
311
+ current_value.to_s
312
+ end
313
+
314
+ def display_text_for_value(value)
315
+ # Find the display text for the given value
316
+ options.each do |option|
317
+ option_value, option_text = extract_option_value_and_text(option)
318
+ return option_text if option_value.to_s == value.to_s
319
+ end
320
+
321
+ value.to_s
322
+ end
323
+ end
324
+ end
325
+ end
326
+ end
@@ -0,0 +1,55 @@
1
+ module EasyAdmin
2
+ module Fields
3
+ module Form
4
+ class TextComponent < BaseComponent
5
+ def view_template
6
+ div(class: "mb-4") do
7
+ label(for: field_id, class: label_classes) do
8
+ plain field_label
9
+ if required?
10
+ span(class: "text-red-500 ml-1") { "*" }
11
+ end
12
+ end
13
+ input(
14
+ type: "text",
15
+ name: form_field_name,
16
+ id: field_id,
17
+ value: current_value,
18
+ class: input_classes,
19
+ required: required?,
20
+ placeholder: field[:placeholder] || "Enter #{field_label.downcase}"
21
+ )
22
+ if field[:help_text]
23
+ p(class: "mt-1 text-sm text-gray-500") { field[:help_text] }
24
+ end
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ def label_classes
31
+ "block text-sm font-medium text-gray-700 mb-1"
32
+ end
33
+
34
+ def input_classes
35
+ base_classes = "block w-full px-3 py-2 border rounded-md shadow-sm text-sm"
36
+ state_classes = "border-gray-300 placeholder-gray-400"
37
+ focus_classes = "focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500"
38
+ hover_classes = "hover:border-gray-400"
39
+ transition_classes = "transition-colors duration-200"
40
+
41
+ "#{base_classes} #{state_classes} #{focus_classes} #{hover_classes} #{transition_classes}"
42
+ end
43
+
44
+ def form_field_name
45
+ model_name = form.object.class.name.underscore
46
+ "#{model_name}[#{field_name}]"
47
+ end
48
+
49
+ def current_value
50
+ form.object.public_send(field_name) if form.object.respond_to?(field_name)
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,54 @@
1
+ module EasyAdmin
2
+ module Fields
3
+ module Form
4
+ class TextareaComponent < BaseComponent
5
+ def view_template
6
+ div(class: "mb-4") do
7
+ label(for: field_id, class: label_classes) do
8
+ plain field_label
9
+ if required?
10
+ span(class: "text-red-500 ml-1") { "*" }
11
+ end
12
+ end
13
+ textarea(
14
+ name: form_field_name,
15
+ id: field_id,
16
+ class: textarea_classes,
17
+ required: required?,
18
+ rows: field[:rows] || 4,
19
+ placeholder: field[:placeholder] || "Enter #{field_label.downcase}"
20
+ ) { current_value }
21
+ if field[:help_text]
22
+ p(class: "mt-1 text-sm text-gray-500") { field[:help_text] }
23
+ end
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ def label_classes
30
+ "block text-sm font-medium text-gray-700 mb-1"
31
+ end
32
+
33
+ def textarea_classes
34
+ base_classes = "block w-full px-3 py-2 border rounded-md shadow-sm text-sm resize-y"
35
+ state_classes = "border-gray-300 placeholder-gray-400"
36
+ focus_classes = "focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500"
37
+ hover_classes = "hover:border-gray-400"
38
+ transition_classes = "transition-colors duration-200"
39
+
40
+ "#{base_classes} #{state_classes} #{focus_classes} #{hover_classes} #{transition_classes}"
41
+ end
42
+
43
+ def form_field_name
44
+ model_name = form.object.class.name.underscore
45
+ "#{model_name}[#{field_name}]"
46
+ end
47
+
48
+ def current_value
49
+ form.object.public_send(field_name) if form.object.respond_to?(field_name)
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,93 @@
1
+ module EasyAdmin
2
+ module Fields
3
+ module Index
4
+ class BelongsToComponent < BaseComponent
5
+ private
6
+
7
+ def view_template
8
+ div(class: "field-cell") do
9
+ if association_record
10
+ if field[:show_link]
11
+ a(
12
+ href: view_path_for(association_record),
13
+ class: "field-link field-link--compact"
14
+ ) do
15
+ plain display_text
16
+ end
17
+ else
18
+ span(class: "field-text") { plain display_text }
19
+ end
20
+ else
21
+ span(class: "field-empty") { plain "—" }
22
+ end
23
+ end
24
+ end
25
+
26
+ def association_record
27
+ @association_record ||= load_association_record
28
+ end
29
+
30
+ def load_association_record
31
+ return nil if value.blank?
32
+
33
+ association_class = determine_association
34
+
35
+ if value.respond_to?(:id)
36
+ # Already loaded record
37
+ value
38
+ else
39
+ # ID value, need to load the record
40
+ association_class.find_by(id: value)
41
+ end
42
+ end
43
+
44
+ def determine_association
45
+ return field[:association] if field[:association]
46
+
47
+ # Try to determine from attribute name
48
+ if field_name.to_s.end_with?('_id')
49
+ field_name.to_s.gsub('_id', '').classify.constantize
50
+ else
51
+ field_name.to_s.classify.constantize
52
+ end
53
+ rescue NameError
54
+ raise ArgumentError, "Could not determine association for #{field_name}. Please specify :association option."
55
+ end
56
+
57
+ def display_text
58
+ return "—" unless association_record
59
+
60
+ text = if field[:display_method]
61
+ association_record.send(field[:display_method])
62
+ elsif association_record.respond_to?(:name)
63
+ association_record.name
64
+ elsif association_record.respond_to?(:title)
65
+ association_record.title
66
+ elsif association_record.respond_to?(:to_label)
67
+ association_record.to_label
68
+ else
69
+ "##{association_record.id}"
70
+ end
71
+
72
+ # Truncate for index display
73
+ if field[:truncate] != false
74
+ truncate_length = field[:truncate] || 30
75
+ text.to_s.length > truncate_length ? "#{text.to_s[0...truncate_length]}..." : text.to_s
76
+ else
77
+ text.to_s
78
+ end
79
+ end
80
+
81
+ def view_path_for(record)
82
+ return field[:view_path_proc].call(record) if field[:view_path_proc]
83
+
84
+ association_class = record.class
85
+ EasyAdmin::Engine.routes.url_helpers.resource_path(
86
+ resource_name: association_class.model_name.route_key,
87
+ id: record.id
88
+ )
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,29 @@
1
+ module EasyAdmin
2
+ module Fields
3
+ module Index
4
+ class BooleanComponent < BaseComponent
5
+ def view_template
6
+ span(class: badge_classes) do
7
+ if value
8
+ unsafe_raw '<span class="flex items-center"><svg class="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"></path></svg>Active</span>'
9
+ else
10
+ unsafe_raw '<span class="flex items-center"><svg class="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"></path></svg>Inactive</span>'
11
+ end
12
+ end
13
+ end
14
+
15
+ private
16
+
17
+ def badge_classes
18
+ base_classes = "inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium"
19
+
20
+ if value
21
+ "#{base_classes} bg-green-100 text-green-800"
22
+ else
23
+ "#{base_classes} bg-gray-100 text-gray-800"
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,13 @@
1
+ module EasyAdmin
2
+ module Fields
3
+ module Index
4
+ class DateComponent < BaseComponent
5
+ def view_template
6
+ span(class: "ea-cell-value") do
7
+ format_value
8
+ end
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ module EasyAdmin
2
+ module Fields
3
+ module Index
4
+ class DatetimeComponent < BaseComponent
5
+ def view_template
6
+ span(class: "ea-cell-value") do
7
+ format_value
8
+ end
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,24 @@
1
+ module EasyAdmin
2
+ module Fields
3
+ module Index
4
+ class EmailComponent < BaseComponent
5
+ def view_template
6
+ if value.present?
7
+ a(href: "mailto:#{value}", class: "ea-link") do
8
+ truncate(value.to_s, length: 30)
9
+ end
10
+ else
11
+ span(class: "ea-empty-value") { "—" }
12
+ end
13
+ end
14
+
15
+ private
16
+
17
+ def truncate(text, length:)
18
+ return text if text.length <= length
19
+ "#{text[0, length]}..."
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,48 @@
1
+ module EasyAdmin
2
+ module Fields
3
+ module Index
4
+ module Filters
5
+ class BaseComponent < Phlex::HTML
6
+ def initialize(field:, search_params: {})
7
+ @field = field
8
+ @search_params = search_params
9
+ end
10
+
11
+ def view_template
12
+ div(class: "space-y-2") do
13
+ render_label
14
+ render_filter_input
15
+ end
16
+ end
17
+
18
+ private
19
+
20
+ def render_label
21
+ label(
22
+ for: field_id,
23
+ class: "block text-sm font-medium text-gray-700"
24
+ ) { @field[:label] }
25
+ end
26
+
27
+ def render_filter_input
28
+ # Override in subclasses
29
+ end
30
+
31
+ def field_id
32
+ "filter_#{@field[:name]}"
33
+ end
34
+
35
+ def base_input_classes
36
+ base_classes = "block w-full px-3 py-2 border rounded-md shadow-sm text-sm"
37
+ state_classes = "border-gray-300 placeholder-gray-400"
38
+ focus_classes = "focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500"
39
+ hover_classes = "hover:border-gray-400"
40
+ transition_classes = "transition-colors duration-200"
41
+
42
+ "#{base_classes} #{state_classes} #{focus_classes} #{hover_classes} #{transition_classes}"
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end