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,52 @@
1
+ module EasyAdmin
2
+ module Dashboards
3
+ class TableCardComponent < BaseCardComponent
4
+ private
5
+
6
+ def render_card_actions
7
+ if card[:show_view_all]
8
+ a(
9
+ href: card[:view_all_url] || "#",
10
+ class: "inline-flex items-center px-3 py-1.5 bg-blue-50 text-blue-700 text-sm font-medium rounded-lg hover:bg-blue-100 transition-colors"
11
+ ) do
12
+ plain "View All"
13
+ end
14
+ end
15
+
16
+ if card[:allow_export]
17
+ button(
18
+ class: "inline-flex items-center px-3 py-1.5 bg-gray-100 text-gray-700 text-sm font-medium rounded-lg hover:bg-gray-200 transition-colors ml-2",
19
+ type: "button"
20
+ ) do
21
+ plain "Export"
22
+ end
23
+ end
24
+ end
25
+
26
+ def render_skeleton
27
+ div(class: "animate-pulse") do
28
+ # Table header skeleton
29
+ div(class: "grid gap-4 mb-4", style: "grid-template-columns: repeat(#{card[:columns] || 4}, minmax(0, 1fr));") do
30
+ columns = card[:columns] || 4
31
+ columns.times do
32
+ div(class: "h-4 bg-gray-200 rounded")
33
+ end
34
+ end
35
+
36
+ # Table rows skeleton
37
+ div(class: "space-y-3") do
38
+ rows = card[:preview_rows] || 5
39
+ rows.times do
40
+ div(class: "grid gap-4", style: "grid-template-columns: repeat(#{card[:columns] || 4}, minmax(0, 1fr));") do
41
+ columns = card[:columns] || 4
42
+ columns.times do
43
+ div(class: "h-4 bg-gray-100 rounded")
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,188 @@
1
+ module EasyAdmin
2
+ class DatePickerComponent < Phlex::HTML
3
+ attr_reader :name, :value, :label, :options
4
+
5
+ def initialize(name:, value: nil, label: nil, **options)
6
+ @name = name
7
+ @value = value
8
+ @label = label
9
+ @options = options
10
+ end
11
+
12
+ def view_template
13
+ div(
14
+ class: "relative",
15
+ data: { controller: "date-picker" }
16
+ ) do
17
+ if label
18
+ label(for: input_id, class: "block text-sm font-medium text-gray-700 mb-1") { plain label }
19
+ end
20
+
21
+ div(class: "relative") do
22
+ input(
23
+ type: "text",
24
+ id: input_id,
25
+ name: name,
26
+ value: formatted_value,
27
+ class: input_classes,
28
+ placeholder: options[:placeholder] || "Select date",
29
+ readonly: true,
30
+ data: {
31
+ date_picker_target: "input",
32
+ action: "click->date-picker#toggle focus->date-picker#toggle"
33
+ }
34
+ )
35
+
36
+ div(class: "absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none text-gray-400") do
37
+ calendar_icon
38
+ end
39
+ end
40
+
41
+ render_date_picker_modal
42
+ end
43
+ end
44
+
45
+ private
46
+
47
+ def input_id
48
+ @input_id ||= "#{name}_#{SecureRandom.hex(4)}"
49
+ end
50
+
51
+ def input_classes
52
+ classes = ["block w-full px-3 py-2 pr-10 text-sm border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 placeholder-gray-400"]
53
+ classes << options[:class] if options[:class]
54
+ classes.join(" ")
55
+ end
56
+
57
+ def formatted_value
58
+ return "" unless value
59
+
60
+ case value
61
+ when Date
62
+ value.strftime("%B %d, %Y")
63
+ when Time
64
+ value.to_date.strftime("%B %d, %Y")
65
+ when String
66
+ begin
67
+ Date.parse(value).strftime("%B %d, %Y")
68
+ rescue ArgumentError
69
+ value
70
+ end
71
+ else
72
+ value.to_s
73
+ end
74
+ end
75
+
76
+ def calendar_icon
77
+ unsafe_raw <<~SVG
78
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
79
+ <rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect>
80
+ <line x1="16" y1="2" x2="16" y2="6"></line>
81
+ <line x1="8" y1="2" x2="8" y2="6"></line>
82
+ <line x1="3" y1="10" x2="21" y2="10"></line>
83
+ </svg>
84
+ SVG
85
+ end
86
+
87
+ def render_date_picker_modal
88
+ div(
89
+ class: "absolute z-[9999] mt-2 hidden opacity-0 scale-95 transition-all duration-200 ease-out left-0 right-0 sm:left-auto sm:right-auto sm:min-w-[320px]",
90
+ data: {
91
+ date_picker_target: "modal",
92
+ action: "click->date-picker#clickOutside"
93
+ }
94
+ ) do
95
+ div(
96
+ class: "bg-white rounded-lg shadow-xl border border-gray-200 p-4 mx-2 sm:mx-0 max-w-sm sm:max-w-none",
97
+ data: { action: "click->date-picker#preventClose" }
98
+ ) do
99
+ render_date_picker_header
100
+ render_date_picker_calendar
101
+ render_date_picker_footer
102
+ end
103
+ end
104
+ end
105
+
106
+ def render_date_picker_header
107
+ div(class: "flex items-center justify-between mb-4") do
108
+ button(
109
+ type: "button",
110
+ class: "p-2 hover:bg-gray-100 rounded-lg transition-colors group",
111
+ data: { action: "click->date-picker#previousMonth" }
112
+ ) do
113
+ unsafe_raw <<~SVG
114
+ <svg class="w-4 h-4 text-gray-600 group-hover:text-gray-900" fill="none" stroke="currentColor" viewBox="0 0 24 24">
115
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
116
+ </svg>
117
+ SVG
118
+ end
119
+
120
+ div(
121
+ class: "text-base font-semibold text-gray-900",
122
+ data: { date_picker_target: "monthYear" }
123
+ ) do
124
+ # Will be populated by JavaScript
125
+ end
126
+
127
+ button(
128
+ type: "button",
129
+ class: "p-2 hover:bg-gray-100 rounded-lg transition-colors group",
130
+ data: { action: "click->date-picker#nextMonth" }
131
+ ) do
132
+ unsafe_raw <<~SVG
133
+ <svg class="w-4 h-4 text-gray-600 group-hover:text-gray-900" fill="none" stroke="currentColor" viewBox="0 0 24 24">
134
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
135
+ </svg>
136
+ SVG
137
+ end
138
+ end
139
+ end
140
+
141
+ def render_date_picker_calendar
142
+ div do
143
+ # Days of week header
144
+ div(class: "grid grid-cols-7 gap-1 mb-3") do
145
+ %w[Sun Mon Tue Wed Thu Fri Sat].each do |day|
146
+ div(class: "text-xs sm:text-sm text-center text-gray-500 font-medium py-2") { plain day }
147
+ end
148
+ end
149
+
150
+ # Calendar grid (populated by JavaScript)
151
+ div(
152
+ class: "grid grid-cols-7 gap-1 sm:gap-2",
153
+ data: { date_picker_target: "grid" }
154
+ )
155
+ end
156
+ end
157
+
158
+ def render_date_picker_footer
159
+ div(class: "flex flex-col sm:flex-row sm:items-center sm:justify-between mt-4 pt-4 border-t border-gray-200 space-y-3 sm:space-y-0") do
160
+ button(
161
+ type: "button",
162
+ class: "px-4 py-2.5 sm:px-3 sm:py-1.5 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors touch-manipulation",
163
+ data: { action: "click->date-picker#today" }
164
+ ) do
165
+ plain "Today"
166
+ end
167
+
168
+ div(class: "flex items-center space-x-3 sm:space-x-2") do
169
+ button(
170
+ type: "button",
171
+ class: "flex-1 sm:flex-none px-4 py-2.5 sm:px-3 sm:py-1.5 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors touch-manipulation",
172
+ data: { action: "click->date-picker#clear" }
173
+ ) do
174
+ plain "Clear"
175
+ end
176
+
177
+ button(
178
+ type: "button",
179
+ class: "flex-1 sm:flex-none px-4 py-2.5 sm:px-3 sm:py-1.5 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700 transition-colors touch-manipulation",
180
+ data: { action: "click->date-picker#close" }
181
+ ) do
182
+ plain "Done"
183
+ end
184
+ end
185
+ end
186
+ end
187
+ end
188
+ end
@@ -0,0 +1,101 @@
1
+ module EasyAdmin
2
+ module Fields
3
+ class BaseComponent < Phlex::HTML
4
+ # Register Turbo custom elements
5
+ register_element :turbo_frame
6
+ register_element :turbo_stream
7
+ register_element :template
8
+
9
+ # Include Rails helpers for form building, URL generation, and text manipulation
10
+ include ActionView::Helpers::DateHelper
11
+ include ActionView::Helpers::TextHelper
12
+ include ActionView::Helpers::UrlHelper
13
+ include ActionView::Helpers::NumberHelper
14
+ include EasyAdmin::DashboardsHelper
15
+ include EasyAdmin::FieldsHelper
16
+
17
+ # Add method to access all Rails helpers if needed
18
+ def helpers
19
+ @helpers ||= Class.new do
20
+ include ActionView::Helpers
21
+ include Rails.application.routes.url_helpers
22
+ include EasyAdmin::Engine.routes.url_helpers
23
+ end.new
24
+ end
25
+
26
+ # Direct access to EasyAdmin URL helpers
27
+ def easy_admin_url_helpers
28
+ @easy_admin_url_helpers ||= EasyAdmin::Engine.routes.url_helpers
29
+ end
30
+
31
+ # Direct access to Rails URL helpers
32
+ def rails_url_helpers
33
+ @rails_url_helpers ||= Rails.application.routes.url_helpers
34
+ end
35
+
36
+ attr_reader :field, :value, :record, :form
37
+
38
+ def initialize(field:, value: nil, record: nil, form: nil, **options)
39
+ @field = field
40
+ @value = value
41
+ @record = record
42
+ @form = form
43
+ @options = options
44
+ end
45
+
46
+ def view_template
47
+ # Override this in subclasses
48
+ end
49
+
50
+ private
51
+
52
+ def field_name
53
+ field[:name]
54
+ end
55
+
56
+ def field_label
57
+ field[:label]
58
+ end
59
+
60
+ def field_type
61
+ field[:type]
62
+ end
63
+
64
+ def required?
65
+ field[:required]
66
+ end
67
+
68
+ def readonly?
69
+ field[:readonly]
70
+ end
71
+
72
+ def field_id
73
+ "#{field_name}_#{SecureRandom.hex(3)}"
74
+ end
75
+
76
+ def css_classes(*additional_classes)
77
+ classes = additional_classes.compact
78
+ classes.join(" ")
79
+ end
80
+
81
+ def format_value(val = value)
82
+ return "" if val.blank?
83
+
84
+ case field_type
85
+ when :boolean
86
+ val ? "Yes" : "No"
87
+ when :date
88
+ val.respond_to?(:strftime) ? val.strftime('%B %d, %Y') : val
89
+ when :datetime
90
+ val.respond_to?(:strftime) ? val.strftime('%B %d, %Y at %I:%M %p') : val
91
+ when :email
92
+ val.to_s
93
+ when :text
94
+ val.to_s
95
+ else
96
+ val.to_s
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,117 @@
1
+ module EasyAdmin
2
+ module Fields
3
+ class BelongsToEditModalComponent < EasyAdmin::BaseComponent
4
+ def initialize(record:, resource_class:, parent_record:, parent_field:)
5
+ @record = record
6
+ @resource_class = resource_class
7
+ @parent_record = parent_record
8
+ @parent_field = parent_field
9
+ end
10
+
11
+ def view_template
12
+ turbo_frame(id: "modal") do
13
+ div(
14
+ id: "modal-backdrop",
15
+ class: "fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50 opacity-100 transition-opacity duration-300",
16
+ data: {
17
+ controller: "modal",
18
+ action: "click->modal#closeOnBackdrop"
19
+ }
20
+ ) do
21
+ div(class: "relative top-20 mx-auto p-5 border w-11/12 md:w-3/4 lg:w-1/2 xl:w-2/5 shadow-lg rounded-md bg-white") do
22
+ div(class: "mt-3") do
23
+ # Modal header
24
+ div(class: "flex items-center justify-between mb-4") do
25
+ h3(class: "text-lg font-medium text-gray-900") do
26
+ "Edit #{@resource_class.singular_title}"
27
+ end
28
+ button(
29
+ class: "text-gray-400 hover:text-gray-600 focus:outline-none",
30
+ data: { action: "click->modal#close" }
31
+ ) do
32
+ unsafe_raw <<~SVG
33
+ <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
34
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
35
+ </svg>
36
+ SVG
37
+ end
38
+ end
39
+
40
+ # Modal form
41
+ form(
42
+ action: update_attached_url,
43
+ method: "patch",
44
+ data: {
45
+ turbo_frame: "_top",
46
+ controller: "form",
47
+ action: "submit->form#submit turbo:submit-end->modal#handleSubmitEnd"
48
+ }
49
+ ) do
50
+ render_form_fields
51
+
52
+ # Form actions
53
+ div(class: "flex justify-end space-x-3 mt-6") do
54
+ button(
55
+ type: "button",
56
+ class: "px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500",
57
+ data: { action: "click->modal#close" }
58
+ ) do
59
+ "Cancel"
60
+ end
61
+
62
+ button(
63
+ type: "submit",
64
+ class: "px-4 py-2 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
65
+ ) do
66
+ "Save Changes"
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
75
+
76
+ private
77
+
78
+ def render_form_fields
79
+ # Create a form builder for the associated record
80
+ form_builder = ActionView::Helpers::FormBuilder.new(
81
+ @resource_class.param_key,
82
+ @record,
83
+ helpers,
84
+ {}
85
+ )
86
+
87
+ # Render each form field for the associated record
88
+ @resource_class.form_fields.each do |field_config|
89
+ next if field_config[:readonly]
90
+
91
+ current_value = @record.public_send(field_config[:name]) if @record.respond_to?(field_config[:name])
92
+
93
+ field_component = field_component(
94
+ field_config,
95
+ action: :form,
96
+ value: current_value,
97
+ record: @record,
98
+ form: form_builder
99
+ )
100
+
101
+ render field_component
102
+ end
103
+ end
104
+
105
+ def update_attached_url
106
+ # We need to create a route that handles updating the attached record
107
+ # and then refreshes the parent table cell
108
+ easy_admin_url_helpers.update_belongs_to_attached_resource_path(
109
+ @parent_record.class.name.underscore.pluralize,
110
+ id: @parent_record.id,
111
+ field: @parent_field[:name],
112
+ attached_id: @record.id
113
+ )
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,82 @@
1
+ module EasyAdmin
2
+ module Fields
3
+ module Form
4
+ class BelongsToComponent < SelectComponent
5
+ private
6
+
7
+ # Override to ensure single select mode
8
+ def multiple?
9
+ false
10
+ end
11
+
12
+ # Override to use proper field name for belongs_to associations
13
+ def form_field_name
14
+ model_name = form.object.class.name.underscore
15
+ "#{model_name}[#{association_key}]"
16
+ end
17
+
18
+ # Override to handle belongs_to association field name
19
+ def association_key
20
+ # Convert belongs_to association name to foreign key
21
+ # e.g., user -> user_id, category -> category_id
22
+ if field_name.to_s.end_with?('_id')
23
+ field_name
24
+ else
25
+ "#{field_name}_id"
26
+ end
27
+ end
28
+
29
+ # Override to always use suggest mode for belongs_to
30
+ def suggest_mode?
31
+ true
32
+ end
33
+
34
+ # Override to ensure suggest configuration exists
35
+ def field
36
+ @field_with_suggest ||= begin
37
+ original_field = super
38
+ original_field.merge(
39
+ suggest: original_field[:suggest] || { search_fields: [:name], limit: 10 }
40
+ )
41
+ end
42
+ end
43
+
44
+ # Override to generate proper suggest URL for belongs_to fields
45
+ def suggest_url
46
+ # Extract resource name from form object
47
+ resource_name = form.object.class.name.underscore.pluralize
48
+
49
+ # For belongs_to fields, use the association name (not the foreign key)
50
+ search_field_name = field_name
51
+
52
+ url = easy_admin_url_helpers.suggest_resource_path(resource_name, field: search_field_name)
53
+ Rails.logger.debug "BelongsTo suggest URL for #{field_name}: #{url} (resource: #{resource_name}, field: #{search_field_name})"
54
+ url
55
+ end
56
+
57
+ # Override to always return empty options since we use suggest mode
58
+ def options
59
+ []
60
+ end
61
+
62
+ # Override to get current value using association key
63
+ def current_selected_values
64
+ current_value = form.object.public_send(association_key) if form.object.respond_to?(association_key)
65
+ current_value.present? ? [current_value] : []
66
+ end
67
+
68
+ # Override to ensure hidden input uses the foreign key value
69
+ def render_hidden_inputs
70
+ # Single select - one hidden input with foreign key value
71
+ current_value = form.object.public_send(association_key) if form.object.respond_to?(association_key)
72
+ input(
73
+ type: "hidden",
74
+ name: form_field_name,
75
+ value: current_value || "",
76
+ data: { select_field_target: "hiddenInput" }
77
+ )
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,100 @@
1
+ module EasyAdmin
2
+ module Fields
3
+ module Form
4
+ class BooleanComponent < BaseComponent
5
+ def view_template
6
+ div(class: "mb-4") do
7
+ div(class: "flex items-center justify-between") do
8
+ div(class: "flex flex-col") do
9
+ label(for: field_id, class: label_classes) do
10
+ plain field_label
11
+ if required?
12
+ span(class: "text-red-500 ml-1") { "*" }
13
+ end
14
+ end
15
+
16
+ if field[:help_text]
17
+ p(class: "mt-1 text-sm text-gray-500") { field[:help_text] }
18
+ end
19
+ end
20
+
21
+ render_toggle_switch
22
+ end
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ def render_toggle_switch
29
+ container_attributes = {
30
+ class: "relative inline-flex items-center",
31
+ data: { controller: "toggle-switch" }
32
+ }
33
+
34
+ div(**container_attributes) do
35
+ # Hidden input for unchecked state
36
+ input(type: "hidden", name: form_field_name, value: "0")
37
+
38
+ # Hidden checkbox that holds the actual value
39
+ input(
40
+ type: "checkbox",
41
+ name: form_field_name,
42
+ id: field_id,
43
+ value: "1",
44
+ checked: current_value,
45
+ class: "sr-only",
46
+ data: { toggle_switch_target: "checkbox" }
47
+ )
48
+
49
+ # Visual toggle switch (clickable)
50
+ div(
51
+ class: "toggle-switch cursor-pointer",
52
+ data: { action: "click->toggle-switch#toggle" }
53
+ ) do
54
+ span(
55
+ class: "toggle-slider",
56
+ data: { toggle_switch_target: "slider" }
57
+ )
58
+ end
59
+
60
+ # Optional: Status text
61
+ if field[:show_status]
62
+ span(class: "ml-3 text-sm font-medium text-gray-700") do
63
+ span(class: "toggle-status-text", data: { toggle_switch_target: "statusText" }) do
64
+ current_value ? "Enabled" : "Disabled"
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
70
+
71
+ def toggle_switch_classes
72
+ base_classes = "relative inline-flex items-center cursor-pointer"
73
+ disabled_classes = field[:disabled] ? "opacity-50 cursor-not-allowed" : ""
74
+
75
+ "#{base_classes} #{disabled_classes}".strip
76
+ end
77
+
78
+ def label_classes
79
+ "block text-sm font-medium text-gray-700 mb-1"
80
+ end
81
+
82
+ def form_field_name
83
+ model_name = form.object.class.name.underscore
84
+ "#{model_name}[#{field_name}]"
85
+ end
86
+
87
+ def current_value
88
+ value = form.object.public_send(field_name) if form.object.respond_to?(field_name)
89
+ # Convert various truthy values to boolean
90
+ case value
91
+ when true, 1, "1", "true", "on", "yes"
92
+ true
93
+ else
94
+ false
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,55 @@
1
+ module EasyAdmin
2
+ module Fields
3
+ module Form
4
+ class DateComponent < 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: "date",
15
+ name: form_field_name,
16
+ id: field_id,
17
+ value: current_value,
18
+ class: input_classes,
19
+ required: required?
20
+ )
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 input_classes
34
+ base_classes = "block w-full px-3 py-2 border rounded-md shadow-sm text-sm"
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
+ value = form.object.public_send(field_name) if form.object.respond_to?(field_name)
50
+ value.respond_to?(:strftime) ? value.strftime("%Y-%m-%d") : value
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end