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,416 @@
1
+ module EasyAdmin
2
+ module Fields
3
+ module Form
4
+ class HasManyComponent < 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.to_s.singularize}_ids"
23
+ }
24
+ ) do
25
+ render_multiple_select_container
26
+ render_select_dropdown
27
+ render_clear_button
28
+ render_hidden_inputs
29
+ end
30
+
31
+ if field[:help_text]
32
+ p(class: "mt-1 text-sm text-gray-500") { field[:help_text] }
33
+ end
34
+ end
35
+ end
36
+
37
+ private
38
+
39
+ def multiple?
40
+ true
41
+ end
42
+
43
+ def suggest_mode?
44
+ result = field[:suggest].present?
45
+ Rails.logger.debug "HasMany suggest_mode? #{field_name}: #{result}, suggest config: #{field[:suggest]}"
46
+ result
47
+ end
48
+
49
+ def suggest_url
50
+ return "" unless suggest_mode?
51
+
52
+ # Extract resource name from form object
53
+ resource_name = form.object.class.name.underscore.pluralize
54
+
55
+ # For has_many fields, use the association name
56
+ search_field_name = field_name
57
+
58
+ easy_admin_url_helpers.suggest_resource_path(resource_name, field: search_field_name)
59
+ end
60
+
61
+ def placeholder
62
+ field[:placeholder] || "Search #{field_label.downcase}..."
63
+ end
64
+
65
+ def label_classes
66
+ "block text-sm font-medium text-gray-700 mb-1"
67
+ end
68
+
69
+ def select_container_classes
70
+ base_classes = "relative"
71
+ classes = [base_classes]
72
+ classes << "select-multiple"
73
+ classes.join(" ")
74
+ end
75
+
76
+ def render_multiple_select_container
77
+ 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
78
+ div(class: "flex flex-wrap items-center gap-1 p-2 pr-8") do
79
+ # Selected items container - will be populated by Stimulus controller
80
+ div(class: "flex flex-wrap items-center gap-1", data: { select_field_target: "selectedItems" }) do
81
+ current_selected_values.each do |selected_value|
82
+ render_selected_item(selected_value)
83
+ end
84
+ end
85
+
86
+ # Search input
87
+ input(
88
+ type: "text",
89
+ class: "flex-1 min-w-20 border-none outline-none text-sm placeholder-gray-400 bg-transparent",
90
+ placeholder: current_selected_values.empty? ? placeholder : "",
91
+ data: {
92
+ select_field_target: "search",
93
+ action: "input->select-field#filter keydown->select-field#handleKeydown focus->select-field#openDropdown"
94
+ }
95
+ )
96
+ end
97
+ end
98
+ end
99
+
100
+ def render_selected_item(selected_value)
101
+ span(
102
+ 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",
103
+ data: {
104
+ value: selected_value,
105
+ action: "click->select-field#removeItem"
106
+ }
107
+ ) do
108
+ plain display_text_for_value(selected_value)
109
+ span(class: "remove-item ml-1 text-blue-600 hover:text-blue-800 font-medium") { plain "×" }
110
+ end
111
+ end
112
+
113
+ def render_select_dropdown
114
+ div(
115
+ 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",
116
+ data: { select_field_target: "dropdown" }
117
+ ) do
118
+ if suggest_mode?
119
+ # For suggest mode, options will be loaded dynamically
120
+ render_no_results_message
121
+ render_loading_message
122
+ else
123
+ # For static mode, render all options
124
+ options.each do |option|
125
+ render_option(option)
126
+ end
127
+ render_no_results_message
128
+ end
129
+ end
130
+ end
131
+
132
+ def render_option(option)
133
+ option_value, option_text = extract_option_value_and_text(option)
134
+
135
+ div(
136
+ 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",
137
+ data: {
138
+ value: option_value,
139
+ action: "click->select-field#selectOption",
140
+ select_field_target: "option"
141
+ }
142
+ ) do
143
+ plain option_text.to_s
144
+ end
145
+ end
146
+
147
+ def render_no_results_message
148
+ div(
149
+ class: "select-no-results px-3 py-2 text-sm text-gray-500 text-center",
150
+ data: { select_field_target: "noResults" },
151
+ style: "display: none;"
152
+ ) do
153
+ plain "No options found"
154
+ end
155
+ end
156
+
157
+ def render_loading_message
158
+ div(
159
+ class: "select-loading px-3 py-2 text-sm text-gray-500 text-center",
160
+ data: { select_field_target: "loading" },
161
+ style: "display: none;"
162
+ ) do
163
+ plain "Loading..."
164
+ end
165
+ end
166
+
167
+ def render_clear_button
168
+ button(
169
+ type: "button",
170
+ class: "select-clear absolute top-2 right-8 p-1 text-gray-400 hover:text-gray-600 bg-white rounded transition-colors duration-150",
171
+ data: { action: "click->select-field#clearAll" },
172
+ title: "Clear all selections"
173
+ ) do
174
+ 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>')
175
+ end
176
+ end
177
+
178
+ def extract_option_value_and_text(option)
179
+ if option.is_a?(Array)
180
+ [option[1], option[0]]
181
+ elsif option.is_a?(Hash)
182
+ [option[:value] || option["value"], option[:text] || option["text"] || option[:label] || option["label"]]
183
+ else
184
+ [option, option]
185
+ end
186
+ end
187
+
188
+ def display_text_for_value(value)
189
+ # Find the display text for the given value
190
+ options.each do |option|
191
+ option_value, option_text = extract_option_value_and_text(option)
192
+ return option_text if option_value.to_s == value.to_s
193
+ end
194
+
195
+ # If not found in options and we have a current association, try to get it from there
196
+ if form.object&.respond_to?(field_name)
197
+ association_records = form.object.public_send(field_name)
198
+ found_record = nil
199
+
200
+ if association_records.respond_to?(:find)
201
+ found_record = association_records.find { |r| r.id.to_s == value.to_s }
202
+ elsif association_records.respond_to?(:detect)
203
+ found_record = association_records.detect { |r| r.id.to_s == value.to_s }
204
+ end
205
+
206
+ if found_record
207
+ return display_text_for(found_record)
208
+ end
209
+ end
210
+
211
+ value.to_s
212
+ end
213
+
214
+ def options
215
+ @options ||= build_options
216
+ end
217
+
218
+ def form_field_name
219
+ model_name = form.object.class.name.underscore
220
+ "#{model_name}[#{field_name.to_s.singularize}_ids]"
221
+ end
222
+
223
+ def current_selected_values
224
+ if form.object&.respond_to?(field_name)
225
+ association_records = form.object.public_send(field_name)
226
+ if association_records.respond_to?(:pluck)
227
+ association_records.pluck(:id).map(&:to_s)
228
+ elsif association_records.respond_to?(:map)
229
+ association_records.map { |r| r.id.to_s }
230
+ else
231
+ []
232
+ end
233
+ else
234
+ []
235
+ end
236
+ end
237
+
238
+ # Override to render hidden inputs properly for has_many associations
239
+ # def view_template
240
+ # div(class: "mb-4") do
241
+ # label(for: field_id, class: label_classes) do
242
+ # plain field_label
243
+ # if required?
244
+ # span(class: "text-red-500 ml-1") { "*" }
245
+ # end
246
+ # end
247
+ #
248
+ # div(
249
+ # class: select_container_classes,
250
+ # data: {
251
+ # controller: "select-field",
252
+ # select_field_multiple_value: multiple?.to_s,
253
+ # select_field_placeholder_value: placeholder,
254
+ # field_name: field_name
255
+ # }
256
+ # ) do
257
+ # if multiple?
258
+ # render_multiple_select_container
259
+ # else
260
+ # render_single_select_input
261
+ # end
262
+ # render_select_dropdown
263
+ # render_clear_button if multiple?
264
+ #
265
+ # # Render hidden inputs INSIDE the controller scope
266
+ # render_hidden_inputs
267
+ # end
268
+ #
269
+ # if field[:help_text]
270
+ # p(class: "mt-1 text-sm text-gray-500") { field[:help_text] }
271
+ # end
272
+ # end
273
+ # end
274
+
275
+ def render_hidden_inputs
276
+ current_selected_values.each do |selected_value|
277
+ input(
278
+ type: "hidden",
279
+ name: "#{form_field_name}[]",
280
+ value: selected_value,
281
+ data: { select_field_target: "hiddenInput" }
282
+ )
283
+ end
284
+ end
285
+
286
+ def build_options
287
+ available_options.map do |item|
288
+ [display_text_for(item), item.id]
289
+ end
290
+ end
291
+
292
+ def available_options
293
+ @available_options ||= load_available_options
294
+ end
295
+
296
+ def load_available_options
297
+ # Get the association class
298
+ association_class = get_association_class
299
+ return [] unless association_class
300
+
301
+ # Load all available options (limit for performance if needed)
302
+ association_class.limit(field[:limit] || 1000)
303
+ end
304
+
305
+ def selected_ids
306
+ @selected_ids ||= load_selected_ids
307
+ end
308
+
309
+ def load_selected_ids
310
+ if form.object&.respond_to?(field_name)
311
+ association_records = form.object.public_send(field_name)
312
+ if association_records.respond_to?(:pluck)
313
+ association_records.pluck(:id).map(&:to_s)
314
+ elsif association_records.respond_to?(:map)
315
+ association_records.map { |r| r.id.to_s }
316
+ else
317
+ []
318
+ end
319
+ else
320
+ []
321
+ end
322
+ end
323
+
324
+ def get_association_class
325
+ # Try to get the association class from the model
326
+ if form.object.class.respond_to?(:reflect_on_association)
327
+ association = form.object.class.reflect_on_association(field_name.to_sym)
328
+ return association&.klass
329
+ end
330
+
331
+ # Fallback: try to constantize the association name
332
+ begin
333
+ association_name.singularize.camelize.constantize
334
+ rescue NameError
335
+ nil
336
+ end
337
+ end
338
+
339
+ def association_name
340
+ @association_name ||= determine_association_name
341
+ end
342
+
343
+ def determine_association_name
344
+ return field[:association_name] if field[:association_name]
345
+ field_name.to_s
346
+ end
347
+
348
+ def search_url
349
+ # TODO: Create proper route helper for associations search
350
+ field[:search_url] || "#{EasyAdmin::Engine.routes.url_helpers.resources_path(resource_name: 'associations').chomp('/')}/search"
351
+ end
352
+
353
+ def hidden_field_name_base
354
+ return field[:form_field_name] if field[:form_field_name]
355
+ model_name = form.object.class.name.underscore
356
+ "#{model_name}[#{field_name.to_s.singularize}_ids]"
357
+ end
358
+
359
+ def display_text_for(item)
360
+ display_method = field[:display_method] || :name
361
+
362
+ if display_method.respond_to?(:call)
363
+ display_method.call(item)
364
+ elsif item.respond_to?(display_method)
365
+ item.send(display_method)
366
+ elsif item.respond_to?(:name)
367
+ item.name
368
+ elsif item.respond_to?(:title)
369
+ item.title
370
+ elsif item.respond_to?(:to_label)
371
+ item.to_label
372
+ else
373
+ "##{item.id}"
374
+ end
375
+ end
376
+
377
+ def details_for(item)
378
+ if field[:details_method]
379
+ item.send(field[:details_method])
380
+ elsif item.respond_to?(:created_at)
381
+ "Created #{item.created_at.strftime('%b %d, %Y')}"
382
+ else
383
+ "ID: #{item.id}"
384
+ end
385
+ end
386
+
387
+ def view_path_for(item)
388
+ field[:view_path_proc]&.call(item) ||
389
+ EasyAdmin::Engine.routes.url_helpers.resource_path(
390
+ resource_name: item.class.name.underscore.pluralize,
391
+ id: item.id
392
+ )
393
+ end
394
+
395
+ def edit_path_for(item)
396
+ field[:edit_path_proc]&.call(item) ||
397
+ EasyAdmin::Engine.routes.url_helpers.edit_resource_path(
398
+ resource_name: item.class.name.underscore.pluralize,
399
+ id: item.id
400
+ )
401
+ end
402
+
403
+ def new_path
404
+ field[:new_path_proc]&.call ||
405
+ EasyAdmin::Engine.routes.url_helpers.new_resource_path(
406
+ resource_name: association_name.singularize.pluralize
407
+ )
408
+ end
409
+
410
+ def hidden_field_name
411
+ "#{hidden_field_name_base}[]"
412
+ end
413
+ end
414
+ end
415
+ end
416
+ end
@@ -0,0 +1,81 @@
1
+ module EasyAdmin
2
+ module Fields
3
+ module Form
4
+ class JsonComponent < Fields::BaseComponent
5
+ def view_template
6
+ div(class: "space-y-2") do
7
+ label(
8
+ for: field_id,
9
+ class: "block text-sm font-medium text-gray-700"
10
+ ) { field_label }
11
+
12
+ div(
13
+ class: "relative",
14
+ data: {
15
+ controller: "jsoneditor",
16
+ jsoneditor_mode_value: "code",
17
+ jsoneditor_data_value: current_json_value.to_json
18
+ }
19
+ ) do
20
+ # Hidden field to store the JSON value
21
+ input(
22
+ type: "hidden",
23
+ name: form_field_name,
24
+ id: "#{field_id}_hidden",
25
+ value: current_json_value_string,
26
+ data: { jsoneditor_target: "hiddenField" }
27
+ )
28
+
29
+ # JSONEditor container
30
+ div(
31
+ id: field_id,
32
+ data: { jsoneditor_target: "editor" },
33
+ style: "height: 400px; border: 1px solid #d1d5db; border-radius: 0.375rem;"
34
+ )
35
+ end
36
+
37
+ if field[:help_text].present?
38
+ p(class: "text-sm text-gray-500") { field[:help_text] }
39
+ end
40
+ end
41
+ end
42
+
43
+ private
44
+
45
+ def form_field_name
46
+ model_name = form.object.class.name.underscore
47
+ "#{model_name}[#{field_name}]"
48
+ end
49
+
50
+ def current_json_value_string
51
+ current_value = form.object.public_send(field_name) || value
52
+ return "" unless current_value.present?
53
+
54
+ if current_value.is_a?(String)
55
+ current_value
56
+ else
57
+ JSON.pretty_generate(current_value)
58
+ end
59
+ end
60
+
61
+ def current_json_value
62
+ current_value = form.object.public_send(field_name) || value
63
+
64
+ if current_value.present?
65
+ if current_value.is_a?(String)
66
+ begin
67
+ JSON.parse(current_value)
68
+ rescue JSON::ParserError
69
+ { error: "Invalid JSON", raw_value: current_value }
70
+ end
71
+ else
72
+ current_value
73
+ end
74
+ else
75
+ {}
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,55 @@
1
+ module EasyAdmin
2
+ module Fields
3
+ module Form
4
+ class NumberComponent < 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: "number",
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