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,324 @@
1
+ module EasyAdmin
2
+ module Resources
3
+ class ShowPageActionsComponent < BaseComponent
4
+ def initialize(record:, resource_class:, current_user: nil)
5
+ @record = record
6
+ @resource_class = resource_class
7
+ @current_user = current_user
8
+ end
9
+
10
+ def view_template
11
+ div(class: "mt-4 sm:ml-16 sm:mt-0 sm:flex-none") do
12
+ # Desktop: Show buttons inline (hidden on mobile)
13
+ div(class: "hidden sm:flex sm:items-center sm:space-x-3") do
14
+ render_edit_action
15
+ render_row_actions
16
+ render_delete_action
17
+ end
18
+
19
+ # Mobile: Show dropdown (hidden on desktop)
20
+ div(class: "sm:hidden relative") do
21
+ render_mobile_dropdown
22
+ end
23
+
24
+ # Context menu container for this record
25
+ div(
26
+ id: "context-menu-#{@record.id}",
27
+ class: "fixed z-50 pointer-events-none"
28
+ )
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ def render_mobile_dropdown
35
+ div(
36
+ class: "relative",
37
+ data: { controller: "dropdown" }
38
+ ) do
39
+ # Dropdown trigger button
40
+ button(
41
+ type: "button",
42
+ class: "w-full flex items-center justify-center rounded-md bg-blue-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-500 transition-colors duration-150",
43
+ data: { action: "click->dropdown#toggle" }
44
+ ) do
45
+ plain "Actions"
46
+ unsafe_raw <<~SVG
47
+ <svg class="ml-2 h-4 w-4 text-white transition-transform duration-200" data-dropdown-target="chevron" viewBox="0 0 20 20" fill="currentColor">
48
+ <path fill-rule="evenodd" d="M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z" clip-rule="evenodd"/>
49
+ </svg>
50
+ SVG
51
+ end
52
+
53
+ # Dropdown menu
54
+ div(
55
+ class: "absolute left-0 right-0 z-10 mt-2 origin-top-left rounded-md bg-white shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none hidden",
56
+ data: { dropdown_target: "menu" }
57
+ ) do
58
+ div(class: "py-1") do
59
+ # Edit action in dropdown
60
+ render_mobile_edit_action
61
+
62
+ # Custom row actions in dropdown
63
+ render_mobile_row_actions
64
+
65
+ # Delete action in dropdown (with separator)
66
+ if has_actions_to_show?
67
+ div(class: "border-t border-gray-100 my-1")
68
+ end
69
+ render_mobile_delete_action
70
+ end
71
+ end
72
+ end
73
+ end
74
+
75
+ def render_edit_action
76
+ a(
77
+ href: easy_admin_url_helpers.edit_resource_path(@resource_class.route_key, @record),
78
+ class: "inline-flex items-center rounded-md bg-blue-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600 transition-colors duration-150"
79
+ ) do
80
+ unsafe_raw <<~SVG
81
+ <svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
82
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/>
83
+ </svg>
84
+ SVG
85
+ plain "Edit"
86
+ end
87
+ end
88
+
89
+ def render_row_actions
90
+ return unless @resource_class.has_row_actions?
91
+
92
+ available_actions = get_available_actions
93
+
94
+ available_actions.each do |action_data|
95
+ action_instance = action_data[:instance]
96
+ action_class = action_data[:class]
97
+
98
+ render_action_button(action_class, action_instance)
99
+ end
100
+ end
101
+
102
+ def render_action_button(action_class, action_instance)
103
+ button_class = case action_instance.class.style
104
+ when :danger
105
+ "inline-flex items-center rounded-md bg-red-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-500 transition-colors duration-150"
106
+ when :warning
107
+ "inline-flex items-center rounded-md bg-yellow-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-yellow-500 transition-colors duration-150"
108
+ when :success
109
+ "inline-flex items-center rounded-md bg-green-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-green-500 transition-colors duration-150"
110
+ else
111
+ "inline-flex items-center rounded-md bg-gray-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-gray-500 transition-colors duration-150"
112
+ end
113
+
114
+ button(
115
+ type: "button",
116
+ class: button_class,
117
+ data: {
118
+ controller: "row-action",
119
+ action: "click->row-action#execute",
120
+ row_action_action_class_value: action_class.name,
121
+ row_action_execution_mode_value: action_instance.class.execution_mode.to_s,
122
+ row_action_confirm_value: action_instance.class.confirm_message,
123
+ row_action_record_id_value: @record.id,
124
+ row_action_resource_name_value: @resource_class.route_key
125
+ }
126
+ ) do
127
+ if action_instance.class.icon
128
+ span(class: "mr-2") { action_instance.class.icon }
129
+ else
130
+ render_default_icon(action_instance)
131
+ end
132
+ plain action_instance.class.label
133
+ end
134
+ end
135
+
136
+ def render_delete_action
137
+ button(
138
+ type: "button",
139
+ class: "inline-flex items-center rounded-md bg-red-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-600 transition-colors duration-150",
140
+ data: {
141
+ controller: "row-action",
142
+ action: "click->row-action#execute",
143
+ row_action_action_class_value: "EasyAdmin::DeleteAction",
144
+ row_action_execution_mode_value: "instant",
145
+ row_action_confirm_value: "Are you sure you want to delete this #{@resource_class.singular_title.downcase}?",
146
+ row_action_record_id_value: @record.id,
147
+ row_action_resource_name_value: @resource_class.route_key
148
+ }
149
+ ) do
150
+ unsafe_raw <<~SVG
151
+ <svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
152
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
153
+ </svg>
154
+ SVG
155
+ plain "Delete"
156
+ end
157
+ end
158
+
159
+ def render_mobile_edit_action
160
+ a(
161
+ href: easy_admin_url_helpers.edit_resource_path(@resource_class.route_key, @record),
162
+ class: "group flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 hover:text-gray-900"
163
+ ) do
164
+ unsafe_raw <<~SVG
165
+ <svg class="mr-3 h-4 w-4 text-gray-400 group-hover:text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
166
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/>
167
+ </svg>
168
+ SVG
169
+ plain "Edit"
170
+ end
171
+ end
172
+
173
+ def render_mobile_row_actions
174
+ return unless @resource_class.has_row_actions?
175
+
176
+ available_actions = get_available_actions
177
+
178
+ available_actions.each do |action_data|
179
+ action_instance = action_data[:instance]
180
+ action_class = action_data[:class]
181
+
182
+ render_mobile_action_item(action_class, action_instance)
183
+ end
184
+ end
185
+
186
+ def render_mobile_action_item(action_class, action_instance)
187
+ text_color = case action_instance.class.style
188
+ when :danger
189
+ "text-red-700 hover:bg-red-50 hover:text-red-900"
190
+ when :warning
191
+ "text-yellow-700 hover:bg-yellow-50 hover:text-yellow-900"
192
+ when :success
193
+ "text-green-700 hover:bg-green-50 hover:text-green-900"
194
+ else
195
+ "text-gray-700 hover:bg-gray-100 hover:text-gray-900"
196
+ end
197
+
198
+ button(
199
+ type: "button",
200
+ class: "group flex w-full items-center px-4 py-2 text-sm #{text_color}",
201
+ data: {
202
+ controller: "row-action",
203
+ action: "click->row-action#execute",
204
+ row_action_action_class_value: action_class.name,
205
+ row_action_execution_mode_value: action_instance.class.execution_mode.to_s,
206
+ row_action_confirm_value: action_instance.class.confirm_message,
207
+ row_action_record_id_value: @record.id,
208
+ row_action_resource_name_value: @resource_class.route_key
209
+ }
210
+ ) do
211
+ if action_instance.class.icon
212
+ span(class: "mr-3 text-base") { action_instance.class.icon }
213
+ else
214
+ render_mobile_default_icon(action_instance)
215
+ end
216
+ plain action_instance.class.label
217
+ end
218
+ end
219
+
220
+ def render_mobile_delete_action
221
+ button(
222
+ type: "button",
223
+ class: "group flex w-full items-center px-4 py-2 text-sm text-red-700 hover:bg-red-50 hover:text-red-900",
224
+ data: {
225
+ controller: "row-action",
226
+ action: "click->row-action#execute",
227
+ row_action_action_class_value: "EasyAdmin::DeleteAction",
228
+ row_action_execution_mode_value: "instant",
229
+ row_action_confirm_value: "Are you sure you want to delete this #{@resource_class.singular_title.downcase}?",
230
+ row_action_record_id_value: @record.id,
231
+ row_action_resource_name_value: @resource_class.route_key
232
+ }
233
+ ) do
234
+ unsafe_raw <<~SVG
235
+ <svg class="mr-3 h-4 w-4 text-red-400 group-hover:text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
236
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
237
+ </svg>
238
+ SVG
239
+ plain "Delete"
240
+ end
241
+ end
242
+
243
+ def render_mobile_default_icon(action_instance)
244
+ icon_color = case action_instance.class.style
245
+ when :danger
246
+ "text-red-400 group-hover:text-red-500"
247
+ when :warning
248
+ "text-yellow-400 group-hover:text-yellow-500"
249
+ when :success
250
+ "text-green-400 group-hover:text-green-500"
251
+ else
252
+ "text-gray-400 group-hover:text-gray-500"
253
+ end
254
+
255
+ case action_instance.class.style
256
+ when :danger
257
+ unsafe_raw <<~SVG
258
+ <svg class="mr-3 h-4 w-4 #{icon_color}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
259
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>
260
+ </svg>
261
+ SVG
262
+ when :success
263
+ unsafe_raw <<~SVG
264
+ <svg class="mr-3 h-4 w-4 #{icon_color}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
265
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
266
+ </svg>
267
+ SVG
268
+ else
269
+ unsafe_raw <<~SVG
270
+ <svg class="mr-3 h-4 w-4 #{icon_color}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
271
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/>
272
+ </svg>
273
+ SVG
274
+ end
275
+ end
276
+
277
+ def has_actions_to_show?
278
+ @resource_class.has_row_actions? && get_available_actions.any?
279
+ end
280
+
281
+ def get_available_actions
282
+ return [] unless @resource_class.has_row_actions?
283
+
284
+ @resource_class.row_actions.map do |action_config|
285
+ action_instance = action_config[:class].new(
286
+ record: @record,
287
+ current_user: @current_user,
288
+ resource_class: @resource_class
289
+ )
290
+
291
+ next unless action_instance.visible? && action_instance.permitted?
292
+
293
+ {
294
+ class: action_config[:class],
295
+ instance: action_instance
296
+ }
297
+ end.compact
298
+ end
299
+
300
+ def render_default_icon(action_instance)
301
+ case action_instance.class.style
302
+ when :danger
303
+ unsafe_raw <<~SVG
304
+ <svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
305
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>
306
+ </svg>
307
+ SVG
308
+ when :success
309
+ unsafe_raw <<~SVG
310
+ <svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
311
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
312
+ </svg>
313
+ SVG
314
+ else
315
+ unsafe_raw <<~SVG
316
+ <svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
317
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/>
318
+ </svg>
319
+ SVG
320
+ end
321
+ end
322
+ end
323
+ end
324
+ end
@@ -0,0 +1,145 @@
1
+ module EasyAdmin
2
+ module Resources
3
+ class TableCellComponent < BaseComponent
4
+ def initialize(record:, field_config:, resource_class:, mobile_hide: false)
5
+ @record = record
6
+ @field_config = field_config
7
+ @resource_class = resource_class
8
+ @mobile_hide = mobile_hide
9
+ end
10
+
11
+ def view_template
12
+ td(id: cell_id, class: cell_classes) do
13
+ div(class: "flex items-center justify-between group-hover:bg-transparent") do
14
+ div(class: "flex-1 text-gray-900 font-medium") do
15
+ render_field_value
16
+ end
17
+
18
+ if field_editable?
19
+ div(class: "ml-2 opacity-0 group-hover:opacity-100 transition-opacity duration-200") do
20
+ render_inline_edit_trigger
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ def render_field_value
30
+ # Use the existing field rendering logic from the Rails views
31
+ field_value = @record.public_send(@field_config[:name])
32
+
33
+ # Use the same render_field helper that the ERB templates use
34
+ unsafe_raw render_field(
35
+ @field_config,
36
+ action: :index,
37
+ value: field_value,
38
+ record: @record
39
+ )
40
+ end
41
+
42
+ def render_inline_edit_trigger
43
+ div do
44
+ if field_editable_via_menu?
45
+ render_context_menu_trigger
46
+ else
47
+ render_modal_trigger
48
+ end
49
+ end
50
+ end
51
+
52
+ def render_modal_trigger
53
+ # Simple modal trigger - create a field-like object for the component
54
+ field_obj = create_field_for_trigger
55
+ render EasyAdmin::Fields::InlineEditTriggerComponent.new(
56
+ record: @record,
57
+ field: field_obj
58
+ )
59
+ end
60
+
61
+ def render_context_menu_trigger
62
+ # Simple context menu trigger - create a field-like object for the component
63
+ field_obj = create_field_for_trigger
64
+ render EasyAdmin::Fields::InlineEditTriggerComponent.new(
65
+ record: @record,
66
+ field: field_obj,
67
+ position: :left
68
+ )
69
+ end
70
+
71
+ def create_field_for_trigger
72
+ # Create a simple object that has the methods the InlineEditTriggerComponent needs
73
+ OpenStruct.new(
74
+ name: field[:name],
75
+ type: field[:type],
76
+ label: field[:label] || field[:name].to_s.humanize,
77
+ editable: field[:editable]
78
+ ).tap do |f|
79
+ def f.editable?
80
+ editable.present?
81
+ end
82
+
83
+ def f.editable_via_menu?
84
+ type == :belongs_to && editable&.dig(:via) == :menu_or_modal
85
+ end
86
+ end
87
+ end
88
+
89
+ def render_pencil_icon
90
+ unsafe_raw <<~SVG
91
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
92
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z"/>
93
+ </svg>
94
+ SVG
95
+ end
96
+
97
+ def cell_classes
98
+ [
99
+ "whitespace-nowrap px-6 py-4 text-sm relative",
100
+ @mobile_hide ? "mobile-hide" : nil
101
+ ].compact.join(" ")
102
+ end
103
+
104
+ def cell_id
105
+ "#{helpers.dom_id(@record)}_#{@field_config[:name]}"
106
+ end
107
+
108
+ def field
109
+ @field ||= @field_config
110
+ end
111
+
112
+ def field_editable?
113
+ field[:editable].present?
114
+ end
115
+
116
+ def field_editable_via_menu?
117
+ field[:type] == :belongs_to && field[:editable]&.dig(:via) == :menu_or_modal
118
+ end
119
+
120
+ def edit_field_url
121
+ easy_admin_url_helpers.edit_field_resource_path(
122
+ @resource_class.route_key,
123
+ id: @record.id,
124
+ field: @field_config[:name]
125
+ )
126
+ end
127
+
128
+ def belongs_to_reattach_url
129
+ easy_admin_url_helpers.belongs_to_reattach_resource_path(
130
+ @resource_class.route_key,
131
+ id: @record.id,
132
+ field: @field_config[:name]
133
+ )
134
+ end
135
+
136
+ def belongs_to_edit_attached_url
137
+ easy_admin_url_helpers.belongs_to_edit_attached_resource_path(
138
+ @resource_class.route_key,
139
+ id: @record.id,
140
+ field: @field_config[:name]
141
+ )
142
+ end
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,206 @@
1
+ module EasyAdmin
2
+ module Resources
3
+ class TableComponent < BaseComponent
4
+ def initialize(resource_class:, records:, current_params: {}, current_user: nil)
5
+ @resource_class = resource_class
6
+ @records = records
7
+ @current_params = current_params
8
+ @current_user = current_user
9
+ end
10
+
11
+ def view_template
12
+ div(data: batch_actions_enabled? ? {
13
+ controller: "batch-selection",
14
+ batch_selection_resource_name_value: @resource_class.route_key,
15
+ action: "batch-action:completed@window->batch-selection#clearSelection"
16
+ } : {}) do
17
+ table(class: "table-striped min-w-full divide-y divide-gray-300") do
18
+ render_table_header
19
+ render_table_body
20
+ end
21
+
22
+ # Render batch action bar if enabled
23
+ render_batch_action_bar if batch_actions_enabled?
24
+
25
+ # Context menu containers for each record
26
+ render_context_menu_containers
27
+ end
28
+ end
29
+
30
+ private
31
+
32
+ def render_table_header
33
+ thead(class: "bg-gray-50") do
34
+ tr do
35
+ # Checkbox column for batch actions
36
+ if batch_actions_enabled?
37
+ th(scope: "col", class: "relative w-16 sm:w-12 px-3 sm:px-6 py-3") do
38
+ div(class: "absolute left-2 sm:left-4 top-1/2 -mt-3") do
39
+ render_ios_checkbox(
40
+ target: "selectAll",
41
+ action: "change->batch-selection#toggleAll",
42
+ data: { select_all: true }
43
+ )
44
+ end
45
+ end
46
+ end
47
+
48
+ @resource_class.index_fields.each_with_index do |field, index|
49
+ render_header_cell(field, index)
50
+ end
51
+ th(scope: "col", class: "relative px-6 py-3") do
52
+ span(class: "sr-only") { "Actions" }
53
+ end
54
+ end
55
+ end
56
+ end
57
+
58
+ def render_header_cell(field, index)
59
+ th(
60
+ scope: "col",
61
+ class: "px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wide #{'mobile-hide' if index > 2}"
62
+ ) do
63
+ if field[:sortable]
64
+ render_sortable_header(field)
65
+ else
66
+ field[:label]
67
+ end
68
+ end
69
+ end
70
+
71
+ def render_sortable_header(field)
72
+ div(class: "flex items-center justify-between") do
73
+ next_direction = determine_next_direction(field)
74
+
75
+ a(
76
+ href: sort_url(field[:name], next_direction),
77
+ class: "group hover:text-gray-900 transition-colors duration-150"
78
+ ) do
79
+ field[:label]
80
+ end
81
+
82
+ span(class: "ml-2 flex-none text-gray-400 hover:text-gray-600") do
83
+ render_sort_icon(field)
84
+ end
85
+ end
86
+ end
87
+
88
+ def render_sort_icon(field)
89
+ if currently_sorted_by?(field)
90
+ if current_sort_ascending?
91
+ # Up arrow for ascending
92
+ unsafe_raw <<~SVG
93
+ <svg class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
94
+ <path fill-rule="evenodd" d="M14.77 12.79a.75.75 0 01-1.06-.02L10 8.832 6.29 12.77a.75.75 0 11-1.08-1.04l4.25-4.5a.75.75 0 011.08 0l4.25 4.5a.75.75 0 01-.02 1.06z" clip-rule="evenodd"/>
95
+ </svg>
96
+ SVG
97
+ else
98
+ # Down arrow for descending
99
+ unsafe_raw <<~SVG
100
+ <svg class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
101
+ <path fill-rule="evenodd" d="M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z" clip-rule="evenodd"/>
102
+ </svg>
103
+ SVG
104
+ end
105
+ else
106
+ # Default down arrow for unsorted
107
+ unsafe_raw <<~SVG
108
+ <svg class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
109
+ <path fill-rule="evenodd" d="M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z" clip-rule="evenodd"/>
110
+ </svg>
111
+ SVG
112
+ end
113
+ end
114
+
115
+ def render_table_body
116
+ tbody(id: "records-container", class: "divide-y divide-gray-200 bg-white") do
117
+ @records.each do |record|
118
+ render EasyAdmin::Resources::TableRowComponent.new(
119
+ record: record,
120
+ resource_class: @resource_class
121
+ )
122
+ end
123
+ end
124
+ end
125
+
126
+ def determine_next_direction(field)
127
+ if currently_sorted_by?(field) && current_sort_ascending?
128
+ 'desc'
129
+ else
130
+ 'asc'
131
+ end
132
+ end
133
+
134
+ def currently_sorted_by?(field)
135
+ @current_params[:sort] == field[:name].to_s
136
+ end
137
+
138
+ def current_sort_ascending?
139
+ @current_params[:direction] == 'asc'
140
+ end
141
+
142
+ def sort_url(field_name, direction)
143
+ # Build URL with current params but update sort and direction
144
+ updated_params = @current_params.merge(sort: field_name, direction: direction)
145
+ base_url = easy_admin_url_helpers.resources_path(@resource_class.route_key)
146
+ query_string = updated_params.present? ? updated_params.to_query : ""
147
+ query_string.present? ? "#{base_url}?#{query_string}" : base_url
148
+ end
149
+
150
+ def batch_actions_enabled?
151
+ @resource_class.batch_actions_enabled
152
+ end
153
+
154
+ def render_batch_action_bar
155
+ render EasyAdmin::BatchActionBarComponent.new(
156
+ resource_class: @resource_class,
157
+ current_user: @current_user
158
+ )
159
+ end
160
+
161
+ def render_context_menu_containers
162
+ @records.each do |record|
163
+ div(
164
+ id: "context-menu-#{record.id}",
165
+ class: "fixed z-50 pointer-events-none"
166
+ )
167
+ end
168
+ end
169
+
170
+ def render_ios_checkbox(target: nil, action: nil, value: nil, data: {})
171
+ label(class: "inline-flex items-center cursor-pointer group") do
172
+ input(
173
+ type: "checkbox",
174
+ value: value,
175
+ class: "sr-only peer",
176
+ data: data.merge({
177
+ batch_selection_target: target,
178
+ action: action
179
+ }.compact)
180
+ )
181
+
182
+ # Custom checkbox design
183
+ div(
184
+ class: "relative w-6 h-6 bg-white border-2 border-gray-300 rounded-md
185
+ transition-all duration-200 ease-out
186
+ peer-checked:bg-blue-500 peer-checked:border-blue-500
187
+ peer-focus:ring-2 peer-focus:ring-blue-500 peer-focus:ring-opacity-50
188
+ group-hover:border-gray-400 peer-checked:group-hover:border-blue-600
189
+ shadow-sm"
190
+ ) do
191
+ # Checkmark icon (hidden by default, shown when checked)
192
+ unsafe_raw <<~SVG
193
+ <svg class="absolute inset-0 w-4 h-4 m-auto text-white opacity-0
194
+ transition-opacity duration-200 ease-out
195
+ peer-checked:opacity-100 pointer-events-none"
196
+ fill="none" stroke="currentColor" viewBox="0 0 24 24">
197
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="3"
198
+ d="M5 13l4 4L19 7"/>
199
+ </svg>
200
+ SVG
201
+ end
202
+ end
203
+ end
204
+ end
205
+ end
206
+ end