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,160 @@
1
+ module EasyAdmin
2
+ module Resources
3
+ class TableRowComponent < BaseComponent
4
+ def initialize(record:, resource_class:)
5
+ @record = record
6
+ @resource_class = resource_class
7
+ end
8
+
9
+ def view_template
10
+ tr(
11
+ id: helpers.dom_id(@record),
12
+ class: "group hover:bg-gray-100 transition-colors duration-150",
13
+ data: {
14
+ controller: "context-menu",
15
+ action: "contextmenu->context-menu#showMenu",
16
+ context_menu_record_id_value: @record.id,
17
+ context_menu_resource_name_value: @resource_class.route_key
18
+ }
19
+ ) do
20
+ # Checkbox cell for batch actions
21
+ if @resource_class.batch_actions_enabled
22
+ td(class: "relative w-16 sm:w-12 px-3 sm:px-6 py-4") do
23
+ div(class: "absolute left-2 sm:left-4 top-1/2 -mt-3") do
24
+ render_ios_checkbox(
25
+ target: "checkbox",
26
+ action: "change->batch-selection#toggleItem",
27
+ value: @record.id
28
+ )
29
+ end
30
+ end
31
+ end
32
+
33
+ @resource_class.index_fields.each_with_index do |field_config, index|
34
+ render EasyAdmin::Resources::TableCellComponent.new(
35
+ record: @record,
36
+ field_config: field_config,
37
+ resource_class: @resource_class,
38
+ mobile_hide: index > 2
39
+ )
40
+ end
41
+
42
+ render_actions_cell
43
+ end
44
+ end
45
+
46
+ private
47
+
48
+ def render_actions_cell
49
+ td(class: "relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-6") do
50
+ div(class: "flex items-center justify-end space-x-2 sm:mobile-actions") do
51
+ render_view_action
52
+ render_edit_action
53
+ render_delete_action
54
+ end
55
+ end
56
+ end
57
+
58
+ def render_view_action
59
+ a(
60
+ href: resource_show_url,
61
+ class: "inline-flex items-center justify-center w-8 h-8 text-blue-600 hover:text-blue-800 hover:bg-blue-50 rounded-lg transition-colors duration-150",
62
+ data: {
63
+ action: "click->table-row#stopPropagation",
64
+ turbo: "false"
65
+ },
66
+ title: "View"
67
+ ) do
68
+ unsafe_raw <<~SVG
69
+ <svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
70
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
71
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/>
72
+ </svg>
73
+ SVG
74
+ end
75
+ end
76
+
77
+ def render_edit_action
78
+ a(
79
+ href: resource_edit_url,
80
+ class: "inline-flex items-center justify-center w-8 h-8 text-green-600 hover:text-green-800 hover:bg-green-50 rounded-lg transition-colors duration-150",
81
+ data: {
82
+ action: "click->table-row#stopPropagation",
83
+ turbo: "false"
84
+ },
85
+ title: "Edit"
86
+ ) do
87
+ unsafe_raw <<~SVG
88
+ <svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
89
+ <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"/>
90
+ </svg>
91
+ SVG
92
+ end
93
+ end
94
+
95
+ def render_delete_action
96
+ a(
97
+ href: resource_show_url,
98
+ method: :delete,
99
+ class: "inline-flex items-center justify-center w-8 h-8 text-red-600 hover:text-red-800 hover:bg-red-50 rounded-lg transition-colors duration-150",
100
+ data: {
101
+ action: "click->table-row#stopPropagation",
102
+ confirm: "Are you sure you want to delete this #{@resource_class.singular_title.downcase}?",
103
+ method: :delete,
104
+ turbo: "false"
105
+ },
106
+ title: "Delete"
107
+ ) do
108
+ unsafe_raw <<~SVG
109
+ <svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
110
+ <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"/>
111
+ </svg>
112
+ SVG
113
+ end
114
+ end
115
+
116
+ def resource_show_url
117
+ easy_admin_url_helpers.resource_path(@resource_class.route_key, @record)
118
+ end
119
+
120
+ def resource_edit_url
121
+ easy_admin_url_helpers.edit_resource_path(@resource_class.route_key, @record)
122
+ end
123
+
124
+ def render_ios_checkbox(target: nil, action: nil, value: nil, data: {})
125
+ label(class: "inline-flex items-center cursor-pointer group") do
126
+ input(
127
+ type: "checkbox",
128
+ value: value,
129
+ class: "sr-only peer",
130
+ data: data.merge({
131
+ batch_selection_target: target,
132
+ action: action
133
+ }.compact)
134
+ )
135
+
136
+ # Custom checkbox design
137
+ div(
138
+ class: "relative w-6 h-6 bg-white border-2 border-gray-300 rounded-md
139
+ transition-all duration-200 ease-out
140
+ peer-checked:bg-blue-500 peer-checked:border-blue-500
141
+ peer-focus:ring-2 peer-focus:ring-blue-500 peer-focus:ring-opacity-50
142
+ group-hover:border-gray-400 peer-checked:group-hover:border-blue-600
143
+ shadow-sm"
144
+ ) do
145
+ # Checkmark icon (hidden by default, shown when checked)
146
+ unsafe_raw <<~SVG
147
+ <svg class="absolute inset-0 w-4 h-4 m-auto text-white opacity-0
148
+ transition-opacity duration-200 ease-out
149
+ peer-checked:opacity-100 pointer-events-none"
150
+ fill="none" stroke="currentColor" viewBox="0 0 24 24">
151
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="3"
152
+ d="M5 13l4 4L19 7"/>
153
+ </svg>
154
+ SVG
155
+ end
156
+ end
157
+ end
158
+ end
159
+ end
160
+ end
@@ -0,0 +1,127 @@
1
+ module EasyAdmin
2
+ class RowActionFormComponent < BaseComponent
3
+ def initialize(record:, action_class:, resource_class:, submit_url:)
4
+ @record = record
5
+ @action_class = action_class
6
+ @resource_class = resource_class
7
+ @submit_url = submit_url
8
+ end
9
+
10
+ def view_template
11
+ div(
12
+ id: "modal",
13
+ class: "fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50",
14
+ data: {
15
+ controller: "row-modal",
16
+ row_modal_submit_url_value: @submit_url,
17
+ row_modal_record_id_value: @record.id,
18
+ row_modal_action_class_value: @action_class.name
19
+ }
20
+ ) do
21
+ div(class: "relative top-20 mx-auto p-5 border w-11/12 max-w-md shadow-lg rounded-md bg-white") do
22
+ # Header
23
+ div(class: "flex items-center justify-between pb-4 mb-4 border-b border-gray-200") do
24
+ h3(class: "text-lg font-semibold text-gray-900") do
25
+ @action_class.modal_title || @action_class.label || @action_class.name.humanize
26
+ end
27
+ button(
28
+ type: "button",
29
+ class: "text-gray-400 hover:text-gray-600 text-xl",
30
+ data: { action: "click->row-modal#close" }
31
+ ) { "×" }
32
+ end
33
+
34
+ # Description
35
+ if @action_class.modal_description.present?
36
+ p(class: "text-sm text-gray-600 mb-4") { @action_class.modal_description }
37
+ end
38
+
39
+ # Record info
40
+ div(class: "mb-4 p-3 bg-gray-50 rounded") do
41
+ div(class: "text-sm text-gray-600") do
42
+ "Record: #{@record.respond_to?(:title) ? @record.title : @record.to_s}"
43
+ end
44
+ end
45
+
46
+ # Simple form fields
47
+ div(class: "space-y-4", data: { row_modal_target: "form" }) do
48
+ render_simple_fields
49
+ end
50
+
51
+ # Action buttons
52
+ div(class: "flex justify-end space-x-3 pt-4 border-t border-gray-200 mt-4") 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",
56
+ data: { action: "click->row-modal#close" }
57
+ ) { "Cancel" }
58
+
59
+ button(
60
+ type: "button",
61
+ class: "px-4 py-2 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-md hover:bg-blue-700",
62
+ data: { action: "click->row-modal#submit" }
63
+ ) { @action_class.label }
64
+ end
65
+ end
66
+ end
67
+ end
68
+
69
+ private
70
+
71
+ def render_simple_fields
72
+ @action_class.attribute_types.each do |attr_name, attr_type|
73
+ next if attr_name.in?(['record', 'current_user', 'resource_class', 'params', 'selected_records'])
74
+
75
+ field_config = {
76
+ name: attr_name,
77
+ type: determine_field_type(attr_type),
78
+ label: attr_name.humanize,
79
+ form_name: "row_action"
80
+ }
81
+
82
+ # Get current value from the action instance
83
+ action_instance = @action_class.new(record: @record, resource_class: @resource_class)
84
+ current_value = action_instance.public_send(attr_name)
85
+
86
+ # Create a simple record-like object for the field component
87
+ field_record = OpenStruct.new(attr_name => current_value)
88
+
89
+ # Create a mock form object
90
+ mock_form = OpenStruct.new(
91
+ object: OpenStruct.new(class: OpenStruct.new(name: OpenStruct.new(underscore: 'row_action')))
92
+ )
93
+
94
+ component = field_component(
95
+ field_config,
96
+ action: :form,
97
+ value: current_value,
98
+ record: field_record,
99
+ form: mock_form
100
+ )
101
+
102
+ render component
103
+ end
104
+ end
105
+
106
+ def determine_field_type(attr_type)
107
+ case attr_type.type
108
+ when :string
109
+ :text
110
+ when :text
111
+ :textarea
112
+ when :boolean
113
+ :boolean
114
+ when :integer, :float
115
+ :number
116
+ when :date
117
+ :date
118
+ when :datetime
119
+ :datetime
120
+ when :json
121
+ :json
122
+ else
123
+ :text
124
+ end
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,224 @@
1
+ module EasyAdmin
2
+ class ScopesComponent < Phlex::HTML
3
+ def initialize(resource_class:, current_scope: nil, counts: {})
4
+ @resource_class = resource_class
5
+ @current_scope = current_scope
6
+ @counts = counts
7
+ end
8
+
9
+ private
10
+
11
+ def view_template
12
+ return unless @resource_class.has_scopes?
13
+
14
+ div(class: "mb-6") do
15
+ div(class: "sm:hidden") do
16
+ # Mobile dropdown for scopes
17
+ render_mobile_scope_selector
18
+ end
19
+
20
+ div(class: "hidden sm:block") do
21
+ # Desktop iOS-style segmented control
22
+ render_desktop_scope_tabs
23
+ end
24
+ end
25
+ end
26
+
27
+ def render_mobile_scope_selector
28
+ # Beautiful mobile dropdown using dropdown controller
29
+ div(class: "relative", data: { controller: "dropdown" }) do
30
+ # Trigger button showing current scope
31
+ button(
32
+ type: "button",
33
+ class: "w-full flex items-center justify-between px-4 py-3 bg-white border border-gray-300 rounded-xl shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500",
34
+ data: {
35
+ action: "click->dropdown#toggle",
36
+ dropdown_target: "trigger"
37
+ }
38
+ ) do
39
+ # Current scope display
40
+ div(class: "flex items-center space-x-3") do
41
+ current_scope_config = @resource_class.scopes.find { |s| is_current_scope?(s) }
42
+ if current_scope_config
43
+ if current_scope_config[:icon]
44
+ span(class: "text-xl") { current_scope_config[:icon] }
45
+ end
46
+ div(class: "flex flex-col items-start") do
47
+ span(class: "font-medium text-gray-900") { current_scope_config[:label] }
48
+ if current_scope_config[:count] && @counts[current_scope_config[:name]]
49
+ span(class: "text-xs text-gray-500") { "#{@counts[current_scope_config[:name]]} items" }
50
+ end
51
+ end
52
+ end
53
+ end
54
+
55
+ # Chevron
56
+ svg(
57
+ class: "w-5 h-5 text-gray-400 transition-transform duration-200",
58
+ fill: "none",
59
+ stroke: "currentColor",
60
+ viewBox: "0 0 24 24",
61
+ data: { dropdown_target: "chevron" }
62
+ ) do |svg|
63
+ svg.path(
64
+ stroke_linecap: "round",
65
+ stroke_linejoin: "round",
66
+ stroke_width: "2",
67
+ d: "M19 9l-7 7-7-7"
68
+ )
69
+ end
70
+ end
71
+
72
+ # Dropdown menu
73
+ div(
74
+ class: "absolute z-10 w-full mt-1 bg-white border border-gray-200 rounded-xl shadow-lg hidden",
75
+ data: { dropdown_target: "menu" }
76
+ ) do
77
+ div(class: "py-2") do
78
+ @resource_class.scopes.each do |scope|
79
+ render_mobile_scope_option(scope)
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
85
+
86
+ def render_mobile_scope_option(scope)
87
+ is_current = is_current_scope?(scope)
88
+
89
+ a(
90
+ href: scope_path(scope),
91
+ class: "flex items-center justify-between px-4 py-3 text-sm hover:bg-gray-50 #{is_current ? 'bg-blue-50 text-blue-700 font-medium' : 'text-gray-700'}",
92
+ data: { turbo_prefetch: "false" }
93
+ ) do
94
+ div(class: "flex items-center space-x-3") do
95
+ # Icon
96
+ if scope[:icon]
97
+ span(class: "text-lg") { scope[:icon] }
98
+ end
99
+
100
+ # Label and count
101
+ div(class: "flex flex-col items-start") do
102
+ span(class: "font-medium") { scope[:label] }
103
+ if scope[:count] && @counts[scope[:name]]
104
+ span(class: "text-xs #{is_current ? 'text-blue-600' : 'text-gray-500'}") do
105
+ "#{@counts[scope[:name]]} items"
106
+ end
107
+ end
108
+ end
109
+ end
110
+
111
+ # Checkmark for current selection
112
+ if is_current
113
+ svg(
114
+ class: "w-5 h-5 text-blue-600",
115
+ fill: "currentColor",
116
+ viewBox: "0 0 20 20"
117
+ ) do |svg|
118
+ svg.path(
119
+ fill_rule: "evenodd",
120
+ d: "M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z",
121
+ clip_rule: "evenodd"
122
+ )
123
+ end
124
+ end
125
+ end
126
+ end
127
+
128
+ def render_desktop_scope_tabs
129
+ nav(class: "isolate flex rounded-xl bg-gray-100 p-1 shadow-sm") do
130
+ @resource_class.scopes.each_with_index do |scope, index|
131
+ render_scope_tab(scope, index)
132
+ end
133
+ end
134
+ end
135
+
136
+ def render_scope_tab(scope, index)
137
+ is_current = is_current_scope?(scope)
138
+ is_first = index == 0
139
+ is_last = index == @resource_class.scopes.length - 1
140
+
141
+ a(
142
+ href: scope_path(scope),
143
+ class: scope_tab_classes(is_current, is_first, is_last),
144
+ data: { turbo_prefetch: "false" }
145
+ ) do
146
+ div(class: "flex items-center space-x-2") do
147
+ # Icon
148
+ if scope[:icon]
149
+ span(class: "text-lg") { scope[:icon] }
150
+ end
151
+
152
+ # Label and count
153
+ div do
154
+ span(class: "font-medium") { scope[:label] }
155
+ if scope[:count] && @counts[scope[:name]]
156
+ span(class: count_badge_classes(is_current)) do
157
+ @counts[scope[:name]].to_s
158
+ end
159
+ end
160
+ end
161
+ end
162
+ end
163
+ end
164
+
165
+ def scope_tab_classes(is_current, is_first, is_last)
166
+ classes = [
167
+ "group relative min-w-0 flex-1 overflow-hidden py-3 px-4 text-center text-sm font-medium transition-all duration-200 ease-in-out focus:z-10"
168
+ ]
169
+
170
+ if is_current
171
+ classes << "bg-white text-gray-900 shadow-sm ring-1 ring-gray-300"
172
+ else
173
+ classes << "text-gray-600 hover:bg-white/50 hover:text-gray-900"
174
+ end
175
+
176
+ if is_first
177
+ classes << "rounded-l-lg"
178
+ end
179
+
180
+ if is_last
181
+ classes << "rounded-r-lg"
182
+ end
183
+
184
+ classes.join(" ")
185
+ end
186
+
187
+ def count_badge_classes(is_current)
188
+ base_classes = "ml-2 inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium"
189
+
190
+ if is_current
191
+ "#{base_classes} bg-blue-100 text-blue-800"
192
+ else
193
+ "#{base_classes} bg-gray-200 text-gray-700 group-hover:bg-blue-100 group-hover:text-blue-800"
194
+ end
195
+ end
196
+
197
+ def scope_path(scope)
198
+ query_params = {}
199
+ query_params[:scope] = scope[:name] unless scope[:name] == :all
200
+
201
+ # Use engine route helper and build query string manually
202
+ base_path = EasyAdmin::Engine.routes.url_helpers.resources_path(@resource_class.route_key)
203
+ query_string = query_params.any? ? query_params.to_query : ""
204
+ query_string.present? ? "#{base_path}?#{query_string}" : base_path
205
+ end
206
+
207
+ def is_current_scope?(scope)
208
+ if @current_scope
209
+ @current_scope.to_s == scope[:name].to_s
210
+ else
211
+ scope[:default] || scope[:name] == :all
212
+ end
213
+ end
214
+
215
+ def scope_label_with_count(scope)
216
+ label = scope[:label]
217
+ if scope[:count] && @counts[scope[:name]]
218
+ "#{label} (#{@counts[scope[:name]]})"
219
+ else
220
+ label
221
+ end
222
+ end
223
+ end
224
+ end
@@ -0,0 +1,140 @@
1
+ module EasyAdmin
2
+ class SettingsSidebarComponent < Phlex::HTML
3
+ register_element :turbo_frame
4
+
5
+ def initialize(feature_toggles: [], expanded: false)
6
+ @feature_toggles = feature_toggles
7
+ @expanded = expanded
8
+ end
9
+
10
+ def view_template
11
+ div(
12
+ id: "settings-sidebar",
13
+ class: "fixed inset-y-0 right-0 z-50 w-96 bg-white shadow-2xl transform transition-all duration-500 ease-out #{@expanded ? 'translate-x-0' : 'translate-x-full'} backdrop-blur-sm",
14
+ style: "background: rgba(255, 255, 255, 0.95);",
15
+ data: {
16
+ controller: "settings-sidebar",
17
+ settings_sidebar_target: "container"
18
+ }
19
+ ) do
20
+ # Header
21
+ div(class: "relative px-6 py-6 bg-gradient-to-br from-blue-50 via-white to-purple-50 border-b border-gray-100") do
22
+ div(class: "text-center") do
23
+ div(class: "inline-flex items-center justify-center w-12 h-12 bg-gradient-to-br from-blue-500 to-purple-600 rounded-full mb-3 shadow-lg") do
24
+ unsafe_raw <<~SVG
25
+ <svg class="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
26
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/>
27
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
28
+ </svg>
29
+ SVG
30
+ end
31
+ h2(class: "text-xl font-bold bg-gradient-to-r from-gray-800 to-gray-600 bg-clip-text text-transparent") { "Settings" }
32
+ end
33
+
34
+ # Close button - iOS style
35
+ button(
36
+ class: "absolute top-4 right-4 w-8 h-8 flex items-center justify-center bg-gray-100 hover:bg-gray-200 rounded-full transition-all duration-200 transform hover:scale-105",
37
+ data: { action: "click->settings-sidebar#close" },
38
+ title: "Close"
39
+ ) do
40
+ unsafe_raw <<~SVG
41
+ <svg class="w-4 h-4 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
42
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M6 18L18 6M6 6l12 12"></path>
43
+ </svg>
44
+ SVG
45
+ end
46
+ end
47
+
48
+ # Content
49
+ div(class: "flex-1 overflow-y-auto px-6 py-8 bg-gradient-to-b from-gray-50/30 to-white") do
50
+ turbo_frame(
51
+ id: "settings-form",
52
+ class: "space-y-8"
53
+ ) do
54
+ form(
55
+ action: route_helper.settings_path,
56
+ method: "patch",
57
+ class: "space-y-8"
58
+ ) do
59
+ # Feature Toggles Section
60
+ div do
61
+ h3(class: "text-md font-medium text-gray-900 mb-4") { "Feature Toggles" }
62
+
63
+ div(class: "space-y-4") do
64
+ @feature_toggles.each do |feature|
65
+ render_feature_toggle(feature)
66
+ end
67
+ end
68
+ end
69
+
70
+ # Save button
71
+ div(class: "pt-6 border-t border-gray-200") do
72
+ button(
73
+ type: "submit",
74
+ class: "w-full inline-flex items-center justify-center px-4 py-2 bg-blue-600 text-white text-sm font-medium rounded-lg hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors"
75
+ ) do
76
+ unsafe_raw <<~SVG
77
+ <svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
78
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
79
+ </svg>
80
+ SVG
81
+ span { "Save Settings" }
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
87
+
88
+ end
89
+ end
90
+
91
+ private
92
+
93
+ def route_helper
94
+ @route_helper ||= EasyAdmin::Engine.routes.url_helpers
95
+ end
96
+
97
+ def render_feature_toggle(feature)
98
+ div(class: "flex items-center justify-between p-4 bg-gray-50 rounded-lg border border-gray-200") do
99
+ div(class: "flex-1") do
100
+ label(
101
+ for: "features_#{feature[:name]}",
102
+ class: "block text-sm font-medium text-gray-900 cursor-pointer"
103
+ ) { feature[:name].humanize }
104
+
105
+ if feature[:description].present?
106
+ p(class: "text-xs text-gray-600 mt-1") { feature[:description] }
107
+ end
108
+ end
109
+
110
+ # Toggle switch
111
+ div(class: "ml-4") do
112
+ input(
113
+ type: "hidden",
114
+ name: "features[#{feature[:name]}]",
115
+ value: "0"
116
+ )
117
+
118
+ input(
119
+ type: "checkbox",
120
+ id: "features_#{feature[:name]}",
121
+ name: "features[#{feature[:name]}]",
122
+ value: "1",
123
+ checked: feature[:enabled],
124
+ class: "sr-only",
125
+ data: { action: "change->settings-sidebar#toggleFeature" }
126
+ )
127
+
128
+ label(
129
+ for: "features_#{feature[:name]}",
130
+ class: "relative inline-flex items-center h-6 rounded-full w-11 cursor-pointer transition-colors ease-in-out duration-200 #{feature[:enabled] ? 'bg-blue-600' : 'bg-gray-300'}"
131
+ ) do
132
+ span(
133
+ class: "inline-block w-4 h-4 transform bg-white rounded-full transition ease-in-out duration-200 #{feature[:enabled] ? 'translate-x-6' : 'translate-x-1'}"
134
+ )
135
+ end
136
+ end
137
+ end
138
+ end
139
+ end
140
+ end