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,188 @@
1
+ module EasyAdmin
2
+ class PaginationComponent < Phlex::HTML
3
+ def initialize(pagy:, request_params: nil)
4
+ @pagy = pagy
5
+ @request_params = request_params || ActionController::Parameters.new.permit!
6
+ end
7
+
8
+ def view_template
9
+ return unless @pagy.pages > 1
10
+
11
+ nav(class: "flex items-center justify-between bg-white px-4 py-3 sm:px-6 border-t border-gray-200") do
12
+ render_mobile_pagination
13
+ render_desktop_pagination
14
+ end
15
+ end
16
+
17
+ private
18
+
19
+ def render_mobile_pagination
20
+ div(class: "flex flex-1 justify-between sm:hidden") do
21
+ render_prev_link_mobile
22
+ render_page_info
23
+ render_next_link_mobile
24
+ end
25
+ end
26
+
27
+ def render_desktop_pagination
28
+ div(class: "hidden sm:flex sm:flex-1 sm:items-center sm:justify-between") do
29
+ render_page_stats
30
+ render_desktop_nav
31
+ end
32
+ end
33
+
34
+ def render_prev_link_mobile
35
+ if @pagy.prev
36
+ a(
37
+ href: page_url(@pagy.prev),
38
+ class: "relative inline-flex items-center rounded-md bg-white px-3 py-2 text-sm font-medium text-gray-700 border border-gray-300 hover:bg-gray-50 transition-colors duration-200"
39
+ ) { "Previous" }
40
+ else
41
+ span(
42
+ class: "relative inline-flex items-center rounded-md bg-gray-100 px-3 py-2 text-sm font-medium text-gray-400 border border-gray-300 cursor-not-allowed"
43
+ ) { "Previous" }
44
+ end
45
+ end
46
+
47
+ def render_next_link_mobile
48
+ if @pagy.next
49
+ a(
50
+ href: page_url(@pagy.next),
51
+ class: "relative ml-3 inline-flex items-center rounded-md bg-white px-3 py-2 text-sm font-medium text-gray-700 border border-gray-300 hover:bg-gray-50 transition-colors duration-200"
52
+ ) { "Next" }
53
+ else
54
+ span(
55
+ class: "relative ml-3 inline-flex items-center rounded-md bg-gray-100 px-3 py-2 text-sm font-medium text-gray-400 border border-gray-300 cursor-not-allowed"
56
+ ) { "Next" }
57
+ end
58
+ end
59
+
60
+ def render_page_info
61
+ div(class: "flex items-center justify-center flex-1") do
62
+ p(class: "text-sm text-gray-700 text-center") do
63
+ plain "Page "
64
+ span(class: "font-medium") { @pagy.page.to_s }
65
+ # Only show "of X" if we have total pages (not in countless mode)
66
+ if @pagy.respond_to?(:pages) && @pagy.pages > 0
67
+ plain " of "
68
+ span(class: "font-medium") { @pagy.pages.to_s }
69
+ end
70
+ end
71
+ end
72
+ end
73
+
74
+ def render_page_stats
75
+ # Removed record count display for performance
76
+ # Just show simple page info instead
77
+ p(class: "text-sm text-gray-700") do
78
+ plain "Page "
79
+ span(class: "font-medium") { @pagy.page.to_s }
80
+ # Only show "of X" if we have total pages (not in countless mode)
81
+ if @pagy.respond_to?(:pages) && @pagy.pages > 0
82
+ plain " of "
83
+ span(class: "font-medium") { @pagy.pages.to_s }
84
+ end
85
+ end
86
+ end
87
+
88
+ def render_desktop_nav
89
+ div(class: "flex items-center space-x-1") do
90
+ # Previous button
91
+ if @pagy.prev
92
+ a(
93
+ href: page_url(@pagy.prev),
94
+ class: "relative inline-flex items-center justify-center w-10 h-10 rounded-lg bg-white text-gray-500 border border-gray-300 hover:bg-gray-50 hover:text-gray-700 transition-all duration-200"
95
+ ) do
96
+ unsafe_raw('<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path></svg>')
97
+ end
98
+ else
99
+ span(
100
+ class: "relative inline-flex items-center justify-center w-10 h-10 rounded-lg bg-gray-100 text-gray-400 border border-gray-300 cursor-not-allowed"
101
+ ) do
102
+ unsafe_raw('<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path></svg>')
103
+ end
104
+ end
105
+
106
+ # Page numbers
107
+ render_page_links
108
+
109
+ # Next button
110
+ if @pagy.next
111
+ a(
112
+ href: page_url(@pagy.next),
113
+ class: "relative inline-flex items-center justify-center w-10 h-10 rounded-lg bg-white text-gray-500 border border-gray-300 hover:bg-gray-50 hover:text-gray-700 transition-all duration-200"
114
+ ) do
115
+ unsafe_raw('<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path></svg>')
116
+ end
117
+ else
118
+ span(
119
+ class: "relative inline-flex items-center justify-center w-10 h-10 rounded-lg bg-gray-100 text-gray-400 border border-gray-300 cursor-not-allowed"
120
+ ) do
121
+ unsafe_raw('<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path></svg>')
122
+ end
123
+ end
124
+ end
125
+ end
126
+
127
+ def render_page_links
128
+ # Handle both regular and countless pagination modes
129
+ series = begin
130
+ if @pagy.respond_to?(:series) && (@pagy.respond_to?(:pages) ? @pagy.pages > 0 : true)
131
+ @pagy.series(size: 7)
132
+ else
133
+ # Fallback: just show current page
134
+ [@pagy.page]
135
+ end
136
+ rescue => e
137
+ # If series fails, fall back to current page only
138
+ [@pagy.page]
139
+ end
140
+
141
+ # Ensure we have a valid array
142
+ series = [@pagy.page] if series.nil? || !series.respond_to?(:each)
143
+
144
+ series.each do |item|
145
+ case item
146
+ when Integer
147
+ if item == @pagy.page
148
+ # Current page
149
+ span(
150
+ class: "relative inline-flex items-center justify-center w-10 h-10 rounded-lg bg-blue-600 text-white font-semibold border border-blue-600 shadow-sm"
151
+ ) { item.to_s }
152
+ else
153
+ # Other pages
154
+ a(
155
+ href: page_url(item),
156
+ class: "relative inline-flex items-center justify-center w-10 h-10 rounded-lg bg-white text-gray-700 font-medium border border-gray-300 hover:bg-blue-50 hover:text-blue-700 hover:border-blue-300 transition-all duration-200"
157
+ ) { item.to_s }
158
+ end
159
+ when String
160
+ # Gap
161
+ span(
162
+ class: "relative inline-flex items-center justify-center w-10 h-10 text-gray-500"
163
+ ) { "..." }
164
+ when :gap
165
+ # Gap (symbol version)
166
+ span(
167
+ class: "relative inline-flex items-center justify-center w-10 h-10 text-gray-500"
168
+ ) { "..." }
169
+ end
170
+ end
171
+ end
172
+
173
+ def page_url(page)
174
+ # Permit all parameters that we want to preserve
175
+ permitted_params = @request_params.permit(:resource_name, :scope, :search, :sort, :direction, :period, :date_from, :date_to, :segment, :refresh)
176
+ current_params = permitted_params.except(:page).to_h
177
+ current_params[:page] = page if page > 1
178
+
179
+ query_string = current_params.any? ? "?#{current_params.to_query}" : ""
180
+ "#{current_url_path}#{query_string}"
181
+ end
182
+
183
+ def current_url_path
184
+ # This will be overridden by the specific implementation
185
+ "/"
186
+ end
187
+ end
188
+ end
@@ -0,0 +1,65 @@
1
+ module EasyAdmin
2
+ class QuickFiltersComponent < Phlex::HTML
3
+ def initialize(resource_class:, current_period: nil, current_params: nil)
4
+ @resource_class = resource_class
5
+ @current_period = current_period
6
+ @current_params = current_params || ActionController::Parameters.new.permit!
7
+ end
8
+
9
+ def view_template
10
+ div(class: "bg-white rounded-xl shadow-sm border border-gray-200 mb-4 sm:mb-6") do
11
+ div(class: "px-4 sm:px-6 py-3 border-b border-gray-100") do
12
+ h3(class: "text-sm font-semibold text-gray-900") { "Quick Filters" }
13
+ end
14
+ div(class: "p-4 sm:p-6") do
15
+ div(class: "flex flex-wrap gap-2") do
16
+ render_quick_filter_link("Today", "today")
17
+ render_quick_filter_link("Last 7 Days", "7d")
18
+ render_quick_filter_link("Last 30 Days", "30d")
19
+ render_quick_filter_link("Last 90 Days", "90d")
20
+ render_quick_filter_link("Last Year", "1y")
21
+ render_clear_filter_link
22
+ end
23
+ end
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ def render_quick_filter_link(label, period)
30
+ active = @current_period == period
31
+ css_classes = if active
32
+ "inline-flex items-center px-4 py-2 text-sm font-medium rounded-lg transition-all duration-200 bg-blue-600 text-white shadow-sm"
33
+ else
34
+ "inline-flex items-center px-4 py-2 text-sm font-medium rounded-lg transition-all duration-200 bg-white text-gray-700 border border-gray-300 hover:bg-gray-50"
35
+ end
36
+
37
+ a(href: build_filter_url(period), class: css_classes, data: { turbo_prefetch: "false" }) { label }
38
+ end
39
+
40
+ def render_clear_filter_link
41
+ return unless @current_period.present?
42
+
43
+ a(
44
+ href: build_filter_url(nil),
45
+ class: "inline-flex items-center px-4 py-2 text-sm font-medium rounded-lg transition-all duration-200 bg-white text-red-600 border border-red-300 hover:bg-red-50",
46
+ data: { turbo_prefetch: "false" }
47
+ ) { "Clear Filter" }
48
+ end
49
+
50
+ def build_filter_url(period)
51
+ route_helper = EasyAdmin::Engine.routes.url_helpers
52
+ current_params = @current_params.permit(:scope, :search, :sort, :direction, :page).to_h
53
+
54
+ if period
55
+ current_params[:period] = period
56
+ else
57
+ current_params.delete(:period)
58
+ end
59
+
60
+ base_url = route_helper.resources_path(@resource_class.route_key)
61
+ query_string = current_params.to_query
62
+ query_string.present? ? "#{base_url}?#{query_string}" : base_url
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,14 @@
1
+ module EasyAdmin
2
+ class ResourcePaginationComponent < PaginationComponent
3
+ def initialize(pagy:, resource_class:, request_params: nil)
4
+ @resource_class = resource_class
5
+ super(pagy: pagy, request_params: request_params)
6
+ end
7
+
8
+ private
9
+
10
+ def current_url_path
11
+ EasyAdmin::Engine.routes.url_helpers.resources_path(@resource_class.route_key)
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,211 @@
1
+ module EasyAdmin
2
+ module Resources
3
+ class IndexComponent < BaseComponent
4
+ def initialize(resource_class:, current_scope: nil, scope_counts: {}, current_params: {}, search_params: {}, current_period: nil, current_user: nil)
5
+ @resource_class = resource_class
6
+ @current_scope = current_scope
7
+ @scope_counts = scope_counts
8
+ @current_params = current_params
9
+ @search_params = search_params
10
+ @current_period = current_period
11
+ @current_user = current_user
12
+ end
13
+
14
+ def view_template
15
+ div do
16
+ render_page_header
17
+ render_scopes if @resource_class.has_scopes?
18
+ render_combined_filters
19
+ render_search_banner if @current_params[:search].present?
20
+ render_period_banner if @current_params[:period].present?
21
+ render_table_frame
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def render_page_header
28
+ div(class: "mb-6") do
29
+ div(class: "sm:flex sm:items-center") do
30
+ div(class: "sm:flex-auto") do
31
+ h1(class: "text-2xl font-bold leading-6 text-gray-900") { @resource_class.title }
32
+ p(class: "mt-2 text-sm text-gray-700") do
33
+ "A list of all #{@resource_class.title.downcase} in your account."
34
+ end
35
+ end
36
+ div(class: "mt-4 sm:ml-16 sm:mt-0 sm:flex-none") do
37
+ render_new_resource_link
38
+ end
39
+ end
40
+ end
41
+ end
42
+
43
+ def render_new_resource_link
44
+ a(
45
+ href: easy_admin_url_helpers.new_resource_path(@resource_class.route_key),
46
+ class: "block rounded-md bg-blue-600 px-3 py-2 text-center 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"
47
+ ) do
48
+ unsafe_raw <<~SVG
49
+ <svg class="inline-block w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
50
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/>
51
+ </svg>
52
+ SVG
53
+ plain "New #{@resource_class.singular_title}"
54
+ end
55
+ end
56
+
57
+ def render_scopes
58
+ render EasyAdmin::ScopesComponent.new(
59
+ resource_class: @resource_class,
60
+ current_scope: @current_scope,
61
+ counts: @scope_counts
62
+ )
63
+ end
64
+
65
+ def render_combined_filters
66
+ render EasyAdmin::CombinedFiltersComponent.new(
67
+ resource_class: @resource_class,
68
+ current_params: @current_params,
69
+ search_params: @search_params,
70
+ current_period: @current_period
71
+ )
72
+ end
73
+
74
+ def render_search_banner
75
+ div(class: "mb-4 rounded-md bg-blue-50 p-4") do
76
+ div(class: "flex") do
77
+ div(class: "flex-shrink-0") do
78
+ unsafe_raw <<~SVG
79
+ <svg class="h-5 w-5 text-blue-400" viewBox="0 0 20 20" fill="currentColor">
80
+ <path fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z" clip-rule="evenodd"/>
81
+ </svg>
82
+ SVG
83
+ end
84
+ div(class: "ml-3") do
85
+ p(class: "text-sm text-blue-700") do
86
+ "Search results for \""
87
+ strong { @current_params[:search] }
88
+ "\""
89
+ a(
90
+ href: easy_admin_url_helpers.resources_path(@resource_class.route_key),
91
+ class: "font-medium text-blue-700 hover:text-blue-600 ml-2"
92
+ ) { "Clear search" }
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
98
+
99
+ def render_period_banner
100
+ div(class: "mb-4 rounded-md bg-green-50 p-4") do
101
+ div(class: "flex") do
102
+ div(class: "flex-shrink-0") do
103
+ unsafe_raw <<~SVG
104
+ <svg class="h-5 w-5 text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
105
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/>
106
+ </svg>
107
+ SVG
108
+ end
109
+ div(class: "ml-3") do
110
+ p(class: "text-sm text-green-700") do
111
+ "Showing results for "
112
+ strong { period_label(@current_params[:period]) }
113
+ a(
114
+ href: clear_period_url,
115
+ class: "font-medium text-green-700 hover:text-green-600 ml-2",
116
+ data: { turbo_prefetch: "false" }
117
+ ) { "Clear filter" }
118
+ end
119
+ end
120
+ end
121
+ end
122
+ end
123
+
124
+ def render_table_frame
125
+ turbo_frame(
126
+ id: "table-frame",
127
+ src: frame_src_url,
128
+ loading: "lazy"
129
+ ) do
130
+ render_skeleton_table
131
+ end
132
+ end
133
+
134
+ def render_skeleton_table
135
+ div(class: "mt-8 flow-root") do
136
+ div(class: "mobile-table-wrapper -mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8") do
137
+ div(class: "inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8") do
138
+ div(class: "overflow-hidden shadow ring-1 ring-black ring-opacity-5 md:rounded-lg") do
139
+ table(class: "table-striped min-w-full divide-y divide-gray-300") do
140
+ render_skeleton_header
141
+ render_skeleton_body
142
+ end
143
+ end
144
+ end
145
+ end
146
+ end
147
+ end
148
+
149
+ def render_skeleton_header
150
+ thead(class: "bg-gray-50") do
151
+ tr do
152
+ @resource_class.index_fields.each_with_index do |field, index|
153
+ th(
154
+ scope: "col",
155
+ class: "px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wide #{'mobile-hide' if index > 2}"
156
+ ) { field[:label] }
157
+ end
158
+ th(scope: "col", class: "relative px-6 py-3") do
159
+ span(class: "sr-only") { "Actions" }
160
+ end
161
+ end
162
+ end
163
+ end
164
+
165
+ def render_skeleton_body
166
+ tbody(class: "divide-y divide-gray-200 bg-white") do
167
+ 5.times do
168
+ tr(class: "animate-pulse") do
169
+ @resource_class.index_fields.each_with_index do |field, index|
170
+ td(class: "whitespace-nowrap px-6 py-4 text-sm #{'mobile-hide' if index > 2}") do
171
+ div(class: "h-4 bg-gray-200 rounded")
172
+ end
173
+ end
174
+ td(class: "relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-6") do
175
+ div(class: "flex items-center justify-end space-x-2") do
176
+ 3.times do
177
+ div(class: "w-8 h-8 bg-gray-200 rounded-lg")
178
+ end
179
+ end
180
+ end
181
+ end
182
+ end
183
+ end
184
+ end
185
+
186
+ def period_label(period)
187
+ case period
188
+ when 'today' then 'Today'
189
+ when '7d' then 'Last 7 Days'
190
+ when '30d' then 'Last 30 Days'
191
+ when '90d' then 'Last 90 Days'
192
+ when '1y' then 'Last Year'
193
+ else period.humanize
194
+ end
195
+ end
196
+
197
+ def clear_period_url
198
+ base_url = easy_admin_url_helpers.resources_path(@resource_class.route_key)
199
+ clear_params = @current_params.except(:period)
200
+ query_string = clear_params.present? ? clear_params.to_query : ""
201
+ query_string.present? ? "#{base_url}?#{query_string}" : base_url
202
+ end
203
+
204
+ def frame_src_url
205
+ base_url = easy_admin_url_helpers.resources_path(@resource_class.route_key)
206
+ query_string = @current_params.present? ? @current_params.to_query : ""
207
+ query_string.present? ? "#{base_url}?#{query_string}" : base_url
208
+ end
209
+ end
210
+ end
211
+ end
@@ -0,0 +1,88 @@
1
+ module EasyAdmin
2
+ module Resources
3
+ class IndexFrameComponent < BaseComponent
4
+ def initialize(resource_class:, records:, pagy: nil, current_params: {}, current_path: nil, current_user: nil)
5
+ @resource_class = resource_class
6
+ @records = records
7
+ @pagy = pagy
8
+ @current_params = current_params
9
+ @current_path = current_path
10
+ @current_user = current_user
11
+ end
12
+
13
+ def view_template
14
+ turbo_frame(id: "table-frame") do
15
+ if @records.any?
16
+ render_table_content
17
+ else
18
+ render_empty_state
19
+ end
20
+ render_pagination if @pagy
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ def render_table_content
27
+ div(class: "mt-8 flow-root") do
28
+ div(class: "mobile-table-wrapper -mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8") do
29
+ div(class: "inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8") do
30
+ div(class: "overflow-hidden shadow ring-1 ring-black ring-opacity-5 md:rounded-lg") do
31
+ render EasyAdmin::Resources::TableComponent.new(
32
+ records: @records,
33
+ resource_class: @resource_class,
34
+ current_params: @current_params,
35
+ current_user: @current_user
36
+ )
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+
43
+ def render_empty_state
44
+ div(class: "text-center py-12") do
45
+ unsafe_raw <<~SVG
46
+ <svg class="mx-auto h-12 w-12 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
47
+ <path vector-effect="non-scaling-stroke" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 13h6m-3-3v6m-9 1V7a2 2 0 012-2h6l2 2h6a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2z"/>
48
+ </svg>
49
+ SVG
50
+ h3(class: "mt-2 text-sm font-semibold text-gray-900") { "No #{@resource_class.title.downcase}" }
51
+ p(class: "mt-1 text-sm text-gray-500") { "Get started by creating a new #{@resource_class.singular_title.downcase}." }
52
+ div(class: "mt-6") do
53
+ a(
54
+ href: easy_admin_url_helpers.new_resource_path(@resource_class.route_key),
55
+ 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"
56
+ ) do
57
+ unsafe_raw <<~SVG
58
+ <svg class="-ml-0.5 mr-1.5 h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
59
+ <path d="M10.75 4.75a.75.75 0 00-1.5 0v4.5h-4.5a.75.75 0 000 1.5h4.5v4.5a.75.75 0 001.5 0v-4.5h4.5a.75.75 0 000-1.5h-4.5v-4.5z"/>
60
+ </svg>
61
+ SVG
62
+ "New #{@resource_class.singular_title}"
63
+ end
64
+ end
65
+ end
66
+ end
67
+
68
+ def render_pagination
69
+ div(id: "infinite-scroll-container") do
70
+ if @resource_class.respond_to?(:infinite_scroll_enabled?) && @resource_class.infinite_scroll_enabled?
71
+ render EasyAdmin::InfiniteScrollComponent.new(
72
+ pagy: @pagy,
73
+ resource_class: @resource_class,
74
+ current_params: @current_params,
75
+ current_path: @current_path
76
+ )
77
+ else
78
+ render EasyAdmin::ResourcePaginationComponent.new(
79
+ pagy: @pagy,
80
+ resource_class: @resource_class,
81
+ request_params: @current_params
82
+ )
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end