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,96 @@
1
+ module EasyAdmin
2
+ module Fields
3
+ module Index
4
+ module Filters
5
+ class BooleanComponent < BaseComponent
6
+ private
7
+
8
+ def render_filter_input
9
+ div(
10
+ class: "relative",
11
+ data: {
12
+ controller: "select-field",
13
+ select_field_multiple_value: "false",
14
+ select_field_placeholder_value: "Select...",
15
+ field_name: @field[:name]
16
+ }
17
+ ) do
18
+ # Display input
19
+ div(class: "relative") do
20
+ input(
21
+ type: "text",
22
+ class: single_select_input_classes,
23
+ placeholder: "Select...",
24
+ readonly: true,
25
+ value: current_display_value,
26
+ data: {
27
+ select_field_target: "display",
28
+ action: "click->select-field#toggleDropdown"
29
+ }
30
+ )
31
+ # Dropdown arrow
32
+ div(class: "absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none") do
33
+ 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>')
34
+ end
35
+ end
36
+
37
+ # Dropdown menu
38
+ div(
39
+ 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",
40
+ data: { select_field_target: "dropdown" }
41
+ ) do
42
+ render_option("", "All")
43
+ render_option("true", "Yes")
44
+ render_option("false", "No")
45
+ end
46
+
47
+ # Hidden input for the actual value
48
+ input(
49
+ type: "hidden",
50
+ name: "q[#{@field[:name]}_eq]",
51
+ value: @search_params["#{@field[:name]}_eq"] || "",
52
+ data: { select_field_target: "hiddenInput" }
53
+ )
54
+ end
55
+ end
56
+
57
+ def render_option(value, text)
58
+ selected = @search_params["#{@field[:name]}_eq"].to_s == value.to_s
59
+
60
+ div(
61
+ class: "select-option px-3 py-2 text-sm cursor-pointer hover:bg-blue-50 hover:text-blue-900 transition-colors duration-150 #{selected ? 'bg-blue-50 text-blue-900' : 'text-gray-900'}",
62
+ data: {
63
+ value: value,
64
+ action: "click->select-field#selectOption",
65
+ select_field_target: "option"
66
+ }
67
+ ) do
68
+ text
69
+ end
70
+ end
71
+
72
+ def current_display_value
73
+ case @search_params["#{@field[:name]}_eq"]
74
+ when "true"
75
+ "Yes"
76
+ when "false"
77
+ "No"
78
+ else
79
+ "All"
80
+ end
81
+ end
82
+
83
+ def single_select_input_classes
84
+ base_classes = "block w-full px-3 py-2 pr-10 text-sm border rounded-md cursor-pointer"
85
+ state_classes = "border-gray-300 placeholder-gray-400 bg-white"
86
+ focus_classes = "focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500"
87
+ hover_classes = "hover:border-gray-400"
88
+ transition_classes = "transition-colors duration-200"
89
+
90
+ "#{base_classes} #{state_classes} #{focus_classes} #{hover_classes} #{transition_classes}"
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,182 @@
1
+ module EasyAdmin
2
+ module Fields
3
+ module Index
4
+ module Filters
5
+ class DateComponent < BaseComponent
6
+ private
7
+
8
+ def render_filter_input
9
+ div(class: "grid grid-cols-2 gap-2") do
10
+ # From date picker
11
+ div(class: "relative", data: { controller: "date-picker" }) do
12
+ div(class: "relative") do
13
+ input(
14
+ type: "text",
15
+ name: "q[#{@field[:name]}_gteq]",
16
+ value: format_display_value(@search_params["#{@field[:name]}_gteq"]),
17
+ placeholder: "From date",
18
+ class: "#{base_input_classes} pr-10",
19
+ data: {
20
+ date_picker_target: "input",
21
+ action: "click->date-picker#toggle focus->date-picker#toggle"
22
+ },
23
+ readonly: true
24
+ )
25
+ div(class: "absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none text-gray-400") do
26
+ render_calendar_icon
27
+ end
28
+ end
29
+ render_date_picker_modal("from")
30
+ end
31
+
32
+ # To date picker
33
+ div(class: "relative", data: { controller: "date-picker" }) do
34
+ div(class: "relative") do
35
+ input(
36
+ type: "text",
37
+ name: "q[#{@field[:name]}_lteq]",
38
+ value: format_display_value(@search_params["#{@field[:name]}_lteq"]),
39
+ placeholder: "To date",
40
+ class: "#{base_input_classes} pr-10",
41
+ data: {
42
+ date_picker_target: "input",
43
+ action: "click->date-picker#toggle focus->date-picker#toggle"
44
+ },
45
+ readonly: true
46
+ )
47
+ div(class: "absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none text-gray-400") do
48
+ render_calendar_icon
49
+ end
50
+ end
51
+ render_date_picker_modal("to")
52
+ end
53
+ end
54
+ end
55
+
56
+ def format_display_value(value)
57
+ return "" unless value.present?
58
+
59
+ case value
60
+ when Date
61
+ value.strftime("%B %d, %Y")
62
+ when Time
63
+ value.to_date.strftime("%B %d, %Y")
64
+ when String
65
+ begin
66
+ Date.parse(value).strftime("%B %d, %Y")
67
+ rescue ArgumentError
68
+ value
69
+ end
70
+ else
71
+ value.to_s
72
+ end
73
+ end
74
+
75
+ def render_calendar_icon
76
+ unsafe_raw <<~SVG
77
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
78
+ <rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect>
79
+ <line x1="16" y1="2" x2="16" y2="6"></line>
80
+ <line x1="8" y1="2" x2="8" y2="6"></line>
81
+ <line x1="3" y1="10" x2="21" y2="10"></line>
82
+ </svg>
83
+ SVG
84
+ end
85
+
86
+ def render_date_picker_modal(suffix)
87
+ div(
88
+ class: "absolute z-50 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]",
89
+ data: {
90
+ date_picker_target: "modal",
91
+ action: "click->date-picker#clickOutside"
92
+ }
93
+ ) do
94
+ div(
95
+ 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",
96
+ data: { action: "click->date-picker#preventClose" }
97
+ ) do
98
+ render_date_picker_header
99
+ render_date_picker_calendar
100
+ render_date_picker_footer
101
+ end
102
+ end
103
+ end
104
+
105
+ def render_date_picker_header
106
+ div(class: "flex items-center justify-between mb-4") do
107
+ button(
108
+ type: "button",
109
+ class: "p-2 hover:bg-gray-100 rounded-lg transition-colors group",
110
+ data: { action: "click->date-picker#previousMonth" }
111
+ ) do
112
+ unsafe_raw <<~SVG
113
+ <svg class="w-4 h-4 text-gray-600 group-hover:text-gray-900" fill="none" stroke="currentColor" viewBox="0 0 24 24">
114
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
115
+ </svg>
116
+ SVG
117
+ end
118
+
119
+ div(
120
+ class: "text-base font-semibold text-gray-900",
121
+ data: { date_picker_target: "monthYear" }
122
+ )
123
+
124
+ button(
125
+ type: "button",
126
+ class: "p-2 hover:bg-gray-100 rounded-lg transition-colors group",
127
+ data: { action: "click->date-picker#nextMonth" }
128
+ ) do
129
+ unsafe_raw <<~SVG
130
+ <svg class="w-4 h-4 text-gray-600 group-hover:text-gray-900" fill="none" stroke="currentColor" viewBox="0 0 24 24">
131
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
132
+ </svg>
133
+ SVG
134
+ end
135
+ end
136
+ end
137
+
138
+ def render_date_picker_calendar
139
+ div do
140
+ # Days of week header
141
+ div(class: "grid grid-cols-7 gap-1 mb-3") do
142
+ %w[Sun Mon Tue Wed Thu Fri Sat].each do |day|
143
+ div(class: "text-xs sm:text-sm text-center text-gray-500 font-medium py-2") { day }
144
+ end
145
+ end
146
+
147
+ # Calendar grid (populated by JavaScript)
148
+ div(
149
+ class: "grid grid-cols-7 gap-1 sm:gap-2",
150
+ data: { date_picker_target: "grid" }
151
+ )
152
+ end
153
+ end
154
+
155
+ def render_date_picker_footer
156
+ 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
157
+ button(
158
+ type: "button",
159
+ 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",
160
+ data: { action: "click->date-picker#today" }
161
+ ) { "Today" }
162
+
163
+ div(class: "flex items-center space-x-3 sm:space-x-2") do
164
+ button(
165
+ type: "button",
166
+ 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",
167
+ data: { action: "click->date-picker#clear" }
168
+ ) { "Clear" }
169
+
170
+ button(
171
+ type: "button",
172
+ 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",
173
+ data: { action: "click->date-picker#close" }
174
+ ) { "Done" }
175
+ end
176
+ end
177
+ end
178
+ end
179
+ end
180
+ end
181
+ end
182
+ end
@@ -0,0 +1,30 @@
1
+ module EasyAdmin
2
+ module Fields
3
+ module Index
4
+ module Filters
5
+ class NumberComponent < BaseComponent
6
+ private
7
+
8
+ def render_filter_input
9
+ div(class: "grid grid-cols-2 gap-2") do
10
+ input(
11
+ type: "number",
12
+ name: "q[#{@field[:name]}_gteq]",
13
+ value: @search_params["#{@field[:name]}_gteq"],
14
+ placeholder: "Min",
15
+ class: base_input_classes
16
+ )
17
+ input(
18
+ type: "number",
19
+ name: "q[#{@field[:name]}_lteq]",
20
+ value: @search_params["#{@field[:name]}_lteq"],
21
+ placeholder: "Max",
22
+ class: base_input_classes
23
+ )
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,101 @@
1
+ module EasyAdmin
2
+ module Fields
3
+ module Index
4
+ module Filters
5
+ class SelectComponent < BaseComponent
6
+ private
7
+
8
+ def render_filter_input
9
+ div(
10
+ class: "relative",
11
+ data: {
12
+ controller: "select-field",
13
+ select_field_multiple_value: "false",
14
+ select_field_placeholder_value: "Select #{@field[:label].downcase}...",
15
+ field_name: @field[:name]
16
+ }
17
+ ) do
18
+ # Display input
19
+ div(class: "relative") do
20
+ input(
21
+ type: "text",
22
+ class: single_select_input_classes,
23
+ placeholder: "Select #{@field[:label].downcase}...",
24
+ readonly: true,
25
+ value: current_display_value,
26
+ data: {
27
+ select_field_target: "display",
28
+ action: "click->select-field#toggleDropdown"
29
+ }
30
+ )
31
+ # Dropdown arrow
32
+ div(class: "absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none") do
33
+ 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>')
34
+ end
35
+ end
36
+
37
+ # Dropdown menu
38
+ div(
39
+ 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",
40
+ data: { select_field_target: "dropdown" }
41
+ ) do
42
+ render_option("", "All")
43
+
44
+ if @field[:options]
45
+ @field[:options].each do |option_value, option_label|
46
+ render_option(option_value, option_label)
47
+ end
48
+ end
49
+ end
50
+
51
+ # Hidden input for the actual value
52
+ input(
53
+ type: "hidden",
54
+ name: "q[#{@field[:name]}_eq]",
55
+ value: @search_params["#{@field[:name]}_eq"] || "",
56
+ data: { select_field_target: "hiddenInput" }
57
+ )
58
+ end
59
+ end
60
+
61
+ def render_option(value, text)
62
+ selected = @search_params["#{@field[:name]}_eq"].to_s == value.to_s
63
+
64
+ div(
65
+ class: "select-option px-3 py-2 text-sm cursor-pointer hover:bg-blue-50 hover:text-blue-900 transition-colors duration-150 #{selected ? 'bg-blue-50 text-blue-900' : 'text-gray-900'}",
66
+ data: {
67
+ value: value,
68
+ action: "click->select-field#selectOption",
69
+ select_field_target: "option"
70
+ }
71
+ ) do
72
+ text.to_s
73
+ end
74
+ end
75
+
76
+ def current_display_value
77
+ return "All" if @search_params["#{@field[:name]}_eq"].blank?
78
+
79
+ if @field[:options]
80
+ @field[:options].each do |option_value, option_label|
81
+ return option_label.to_s if option_value.to_s == @search_params["#{@field[:name]}_eq"].to_s
82
+ end
83
+ end
84
+
85
+ @search_params["#{@field[:name]}_eq"].to_s
86
+ end
87
+
88
+ def single_select_input_classes
89
+ base_classes = "block w-full px-3 py-2 pr-10 text-sm border rounded-md cursor-pointer"
90
+ state_classes = "border-gray-300 placeholder-gray-400 bg-white"
91
+ focus_classes = "focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500"
92
+ hover_classes = "hover:border-gray-400"
93
+ transition_classes = "transition-colors duration-200"
94
+
95
+ "#{base_classes} #{state_classes} #{focus_classes} #{hover_classes} #{transition_classes}"
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,32 @@
1
+ module EasyAdmin
2
+ module Fields
3
+ module Index
4
+ module Filters
5
+ class StringComponent < BaseComponent
6
+ private
7
+
8
+ def render_filter_input
9
+ input(
10
+ type: "text",
11
+ id: field_id,
12
+ name: "q[#{@field[:name]}_cont]",
13
+ value: @search_params["#{@field[:name]}_cont"],
14
+ placeholder: "Search #{@field[:label].downcase}...",
15
+ class: input_classes
16
+ )
17
+ end
18
+
19
+ def input_classes
20
+ base_classes = "block w-full px-3 py-2 border rounded-md shadow-sm text-sm"
21
+ state_classes = "border-gray-300 placeholder-gray-400"
22
+ focus_classes = "focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500"
23
+ hover_classes = "hover:border-gray-400"
24
+ transition_classes = "transition-colors duration-200"
25
+
26
+ "#{base_classes} #{state_classes} #{focus_classes} #{hover_classes} #{transition_classes}"
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,23 @@
1
+ module EasyAdmin
2
+ module Fields
3
+ module Index
4
+ class JsonComponent < Fields::BaseComponent
5
+ def view_template
6
+ div(class: "text-center") do
7
+ if json_value.present?
8
+ span(class: "text-green-600 font-bold text-lg") { "+" }
9
+ else
10
+ span(class: "text-gray-400 font-bold text-lg") { "−" }
11
+ end
12
+ end
13
+ end
14
+
15
+ private
16
+
17
+ def json_value
18
+ @json_value ||= value
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,20 @@
1
+ module EasyAdmin
2
+ module Fields
3
+ module Index
4
+ class NumberComponent < BaseComponent
5
+ def view_template
6
+ span(class: "ea-cell-value ea-cell-number") do
7
+ format_value
8
+ end
9
+ end
10
+
11
+ private
12
+
13
+ def format_value
14
+ return "" if value.blank?
15
+ value.to_s
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,25 @@
1
+ module EasyAdmin
2
+ module Fields
3
+ module Index
4
+ class SelectComponent < BaseComponent
5
+ def view_template
6
+ span(class: "ea-cell-value ea-cell-select") do
7
+ plain format_value
8
+ end
9
+ end
10
+
11
+ private
12
+
13
+ def format_value
14
+ return "" if value.blank?
15
+
16
+ if value.is_a?(Array)
17
+ value.join(", ")
18
+ else
19
+ value.to_s
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,20 @@
1
+ module EasyAdmin
2
+ module Fields
3
+ module Index
4
+ class TextComponent < BaseComponent
5
+ def view_template
6
+ span(class: "ea-cell-value") do
7
+ truncate(format_value, length: 50)
8
+ end
9
+ end
10
+
11
+ private
12
+
13
+ def truncate(text, length:)
14
+ return text if text.length <= length
15
+ "#{text[0, length]}..."
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,135 @@
1
+ module EasyAdmin
2
+ module Fields
3
+ class InlineEditModalComponent < EasyAdmin::BaseComponent
4
+ def initialize(record:, field_config:, resource_class:)
5
+ @record = record
6
+ @field_config = field_config
7
+ @resource_class = resource_class
8
+ end
9
+
10
+ def view_template
11
+ turbo_frame(id: "modal") do
12
+ div(
13
+ id: "modal-backdrop",
14
+ 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",
15
+ data: {
16
+ controller: "modal",
17
+ action: "click->modal#closeOnBackdrop"
18
+ }
19
+ ) do
20
+ 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
21
+ div(class: "mt-3") do
22
+ # Modal header
23
+ div(class: "flex items-center justify-between mb-4") do
24
+ h3(class: "text-lg font-medium text-gray-900") do
25
+ "Edit #{@field_config[:label]}"
26
+ end
27
+ button(
28
+ class: "text-gray-400 hover:text-gray-600 focus:outline-none",
29
+ data: { action: "click->modal#close" }
30
+ ) do
31
+ unsafe_raw <<~SVG
32
+ <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
33
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
34
+ </svg>
35
+ SVG
36
+ end
37
+ end
38
+
39
+ # Modal form
40
+ form(
41
+ action: update_field_url,
42
+ method: "patch",
43
+ data: {
44
+ turbo_frame: "_top",
45
+ controller: "form",
46
+ action: "submit->form#submit turbo:submit-end->modal#handleSubmitEnd"
47
+ }
48
+ ) do
49
+ render_form_field
50
+
51
+ # Form actions
52
+ div(class: "flex justify-end space-x-3 mt-6") do
53
+ button(
54
+ type: "button",
55
+ 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",
56
+ data: { action: "click->modal#close" }
57
+ ) do
58
+ "Cancel"
59
+ end
60
+
61
+ button(
62
+ type: "submit",
63
+ 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"
64
+ ) do
65
+ "Save Changes"
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
74
+
75
+ private
76
+
77
+ def render_form_field
78
+ # For converted belongs_to fields, we need to get the foreign key value
79
+ if @field_config[:original_type] == :belongs_to
80
+ current_value = @record.public_send(@field_config[:name]) # This is already the foreign key
81
+ else
82
+ current_value = @record.public_send(@field_config[:name])
83
+ end
84
+
85
+
86
+ # Create a form builder for the field component
87
+ # Make sure the record has the current value set for the field
88
+ if @field_config[:original_type] == :belongs_to
89
+ # For belongs_to fields, ensure the foreign key is accessible
90
+ @record.define_singleton_method(@field_config[:name]) { current_value } unless @record.respond_to?(@field_config[:name])
91
+ end
92
+
93
+ form_builder = ActionView::Helpers::FormBuilder.new(
94
+ @resource_class.param_key,
95
+ @record,
96
+ helpers,
97
+ {}
98
+ )
99
+
100
+ # Get the field component instance and render it
101
+ field_component = field_component(
102
+ @field_config,
103
+ action: :form,
104
+ value: current_value,
105
+ record: @record,
106
+ form: form_builder
107
+ )
108
+
109
+
110
+ render field_component
111
+ end
112
+
113
+ def field_input_id
114
+ "field_#{@field_config[:name]}"
115
+ end
116
+
117
+ def update_field_url
118
+ # For belongs_to fields that were converted to select, we need to use the belongs_to_reattach route
119
+ if @field_config[:original_type] == :belongs_to
120
+ easy_admin_url_helpers.belongs_to_reattach_resource_path(
121
+ @resource_class.route_key,
122
+ id: @record.id,
123
+ field: @field_config[:original_name]
124
+ )
125
+ else
126
+ easy_admin_url_helpers.update_field_resource_path(
127
+ @resource_class.route_key,
128
+ id: @record.id,
129
+ field: @field_config[:name]
130
+ )
131
+ end
132
+ end
133
+ end
134
+ end
135
+ end