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,186 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class extends Controller {
4
+ static targets = ["container"]
5
+
6
+ connect() {
7
+ console.log('Settings sidebar controller connected')
8
+ console.log('Container target:', this.containerTarget)
9
+ this.isOpen = false
10
+ // Listen for global events to open/close
11
+ document.addEventListener('settings:open', this.open.bind(this))
12
+ document.addEventListener('settings:close', this.close.bind(this))
13
+ }
14
+
15
+
16
+ open() {
17
+ console.log('Settings sidebar open() called')
18
+ this.isOpen = true
19
+
20
+ // Add subtle bounce animation and slide in
21
+ this.containerTarget.classList.remove('translate-x-full')
22
+ this.containerTarget.classList.add('translate-x-0')
23
+ this.containerTarget.style.transform = 'translateX(-8px)'
24
+
25
+ setTimeout(() => {
26
+ this.containerTarget.style.transform = 'translateX(0)'
27
+ }, 100)
28
+
29
+
30
+ // Prevent body scroll and add iOS-like behavior
31
+ document.body.classList.add('overflow-hidden')
32
+
33
+ // Add subtle scale animation to content
34
+ const content = this.containerTarget.querySelector('.flex-1')
35
+ if (content) {
36
+ content.style.transform = 'scale(0.95)'
37
+ content.style.opacity = '0'
38
+ setTimeout(() => {
39
+ content.style.transform = 'scale(1)'
40
+ content.style.opacity = '1'
41
+ }, 150)
42
+ }
43
+ }
44
+
45
+ close() {
46
+ this.isOpen = false
47
+
48
+ // Add exit animation
49
+ const content = this.containerTarget.querySelector('.flex-1')
50
+ if (content) {
51
+ content.style.transform = 'scale(0.95)'
52
+ content.style.opacity = '0.8'
53
+ }
54
+
55
+ // Slide out with slight bounce
56
+ this.containerTarget.style.transform = 'translateX(8px)'
57
+ setTimeout(() => {
58
+ this.containerTarget.classList.remove('translate-x-0')
59
+ this.containerTarget.classList.add('translate-x-full')
60
+ this.containerTarget.style.transform = ''
61
+ }, 100)
62
+
63
+
64
+ // Re-enable body scroll with delay
65
+ setTimeout(() => {
66
+ document.body.classList.remove('overflow-hidden')
67
+ }, 300)
68
+ }
69
+
70
+ toggle() {
71
+ if (this.isOpen) {
72
+ this.close()
73
+ } else {
74
+ this.open()
75
+ }
76
+ }
77
+
78
+ toggleFeature(event) {
79
+ const checkbox = event.target
80
+ const label = checkbox.nextElementSibling
81
+ const toggle = label.querySelector('span')
82
+
83
+ // Add haptic-like feedback with scale animation
84
+ label.style.transform = 'scale(0.95)'
85
+ setTimeout(() => {
86
+ label.style.transform = 'scale(1)'
87
+ }, 100)
88
+
89
+ if (checkbox.checked) {
90
+ // Enable state - green gradient
91
+ label.classList.remove('bg-gray-300', 'hover:bg-gray-400')
92
+ label.classList.add('bg-gradient-to-r', 'from-green-400', 'to-green-600', 'shadow-lg', 'shadow-green-400/30')
93
+ toggle.classList.remove('translate-x-1')
94
+ toggle.classList.add('translate-x-6')
95
+
96
+ // Add checkmark
97
+ const checkmark = toggle.querySelector('svg')
98
+ if (!checkmark) {
99
+ toggle.innerHTML = `
100
+ <svg class="w-3 h-3 text-green-600 absolute inset-0 m-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24">
101
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M5 13l4 4L19 7"></path>
102
+ </svg>
103
+ `
104
+ }
105
+ } else {
106
+ // Disable state - gray
107
+ label.classList.remove('bg-gradient-to-r', 'from-green-400', 'to-green-600', 'shadow-lg', 'shadow-green-400/30')
108
+ label.classList.add('bg-gray-300', 'hover:bg-gray-400')
109
+ toggle.classList.remove('translate-x-6')
110
+ toggle.classList.add('translate-x-1')
111
+
112
+ // Remove checkmark
113
+ toggle.innerHTML = ''
114
+ }
115
+
116
+ // Add subtle success animation to the parent card
117
+ const card = checkbox.closest('.group')
118
+ if (card) {
119
+ card.classList.add('ring-2', 'ring-green-200')
120
+ setTimeout(() => {
121
+ card.classList.remove('ring-2', 'ring-green-200')
122
+ }, 500)
123
+ }
124
+ }
125
+
126
+ submitForm(event) {
127
+ // Let turbo handle the form submission
128
+ // Show loading state
129
+ const submitButton = event.target.querySelector('button[type="submit"]')
130
+ if (submitButton) {
131
+ const originalText = submitButton.innerHTML
132
+ submitButton.innerHTML = '<svg class="w-4 h-4 mr-2 animate-spin" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg><span>Saving...</span>'
133
+ submitButton.disabled = true
134
+
135
+ // Restore button after delay (turbo will handle the response)
136
+ setTimeout(() => {
137
+ submitButton.innerHTML = originalText
138
+ submitButton.disabled = false
139
+ this.showNotification('Settings updated successfully', 'success')
140
+ }, 1000)
141
+ }
142
+ }
143
+
144
+ showNotification(message, type = 'info') {
145
+ // Create notification element
146
+ const notification = document.createElement('div')
147
+ notification.className = `fixed top-4 right-4 z-[60] px-4 py-3 rounded-lg shadow-lg text-white transition-all duration-300 transform translate-x-full ${
148
+ type === 'success' ? 'bg-green-600' :
149
+ type === 'error' ? 'bg-red-600' :
150
+ 'bg-blue-600'
151
+ }`
152
+ notification.textContent = message
153
+
154
+ document.body.appendChild(notification)
155
+
156
+ // Slide in
157
+ setTimeout(() => {
158
+ notification.classList.remove('translate-x-full')
159
+ notification.classList.add('translate-x-0')
160
+ }, 100)
161
+
162
+ // Slide out and remove after 3 seconds
163
+ setTimeout(() => {
164
+ notification.classList.remove('translate-x-0')
165
+ notification.classList.add('translate-x-full')
166
+ setTimeout(() => notification.remove(), 300)
167
+ }, 3000)
168
+ }
169
+
170
+ // Handle escape key
171
+ handleKeydown = (event) => {
172
+ if (event.key === 'Escape' && this.isOpen) {
173
+ this.close()
174
+ }
175
+ }
176
+
177
+ initialize() {
178
+ document.addEventListener('keydown', this.handleKeydown)
179
+ }
180
+
181
+ disconnect() {
182
+ document.removeEventListener('settings:open', this.open.bind(this))
183
+ document.removeEventListener('settings:close', this.close.bind(this))
184
+ document.removeEventListener('keydown', this.handleKeydown)
185
+ }
186
+ }
@@ -0,0 +1,102 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class extends Controller {
4
+ static targets = ["mobileToggle"]
5
+ static values = {
6
+ storageKey: { type: String, default: "easyAdminSidebarCollapsed" }
7
+ }
8
+
9
+ connect() {
10
+ // Restore sidebar state from localStorage on page load
11
+ this.restoreSidebarState()
12
+
13
+ // Mark as restored to enable transitions
14
+ setTimeout(() => {
15
+ document.body.classList.add('sidebar-restored')
16
+ }, 50)
17
+
18
+ // Listen for window resize to handle mobile/desktop transitions
19
+ this.handleResize = this.handleResize.bind(this)
20
+ window.addEventListener('resize', this.handleResize)
21
+ }
22
+
23
+ disconnect() {
24
+ window.removeEventListener('resize', this.handleResize)
25
+ }
26
+
27
+ toggle() {
28
+ const isMobile = window.innerWidth < 1024
29
+
30
+ if (isMobile) {
31
+ // Mobile: delegate to mobile controller
32
+ const mobileController = document.querySelector('[data-controller*="sidebar-mobile"]')
33
+ if (mobileController) {
34
+ this.application.getControllerForElementAndIdentifier(mobileController, 'sidebar-mobile')?.toggle()
35
+ }
36
+ } else {
37
+ // Desktop: toggle sidebar collapse
38
+ const isCurrentlyCollapsed = document.body.classList.contains("sidebar--collapsed")
39
+
40
+ if (isCurrentlyCollapsed) {
41
+ document.body.classList.remove("sidebar--collapsed")
42
+ this.saveSidebarState(false) // expanded
43
+ } else {
44
+ document.body.classList.add("sidebar--collapsed")
45
+ this.saveSidebarState(true) // collapsed
46
+ }
47
+
48
+ this.dispatch("toggled", { detail: { isOpen: !isCurrentlyCollapsed, isMobile } })
49
+ }
50
+ }
51
+
52
+
53
+ // Save sidebar collapsed state to localStorage
54
+ saveSidebarState(isCollapsed) {
55
+ try {
56
+ localStorage.setItem(this.storageKeyValue, JSON.stringify(isCollapsed))
57
+ } catch (e) {
58
+ // Handle localStorage not available (private browsing, etc.)
59
+ console.warn('Could not save sidebar state to localStorage:', e)
60
+ }
61
+ }
62
+
63
+ // Restore sidebar state from localStorage
64
+ restoreSidebarState() {
65
+ const isMobile = window.innerWidth < 1024
66
+
67
+ // Only restore state on desktop
68
+ if (isMobile) {
69
+ return
70
+ }
71
+
72
+ try {
73
+ const savedState = localStorage.getItem(this.storageKeyValue)
74
+
75
+ if (savedState !== null) {
76
+ const isCollapsed = JSON.parse(savedState)
77
+
78
+ if (isCollapsed) {
79
+ document.body.classList.add("sidebar--collapsed")
80
+ } else {
81
+ document.body.classList.remove("sidebar--collapsed")
82
+ }
83
+ }
84
+ // If no saved state, use default (expanded)
85
+ } catch (e) {
86
+ console.warn('Could not restore sidebar state from localStorage:', e)
87
+ }
88
+ }
89
+
90
+ // Handle window resize - ensure mobile state is reset properly
91
+ handleResize() {
92
+ const isMobile = window.innerWidth < 1024
93
+
94
+ if (isMobile) {
95
+ // On mobile, remove desktop collapse class
96
+ document.body.classList.remove("sidebar--collapsed")
97
+ } else {
98
+ // On desktop, restore saved state
99
+ this.restoreSidebarState()
100
+ }
101
+ }
102
+ }
@@ -0,0 +1,23 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class extends Controller {
4
+ static targets = ["container", "overlay"]
5
+
6
+ open() {
7
+ this.containerTarget.classList.add("sidebar--open")
8
+ this.overlayTarget.classList.add("sidebar-overlay--visible")
9
+ }
10
+
11
+ close() {
12
+ this.containerTarget.classList.remove("sidebar--open")
13
+ this.overlayTarget.classList.remove("sidebar-overlay--visible")
14
+ }
15
+
16
+ toggle() {
17
+ if (this.containerTarget.classList.contains("sidebar--open")) {
18
+ this.close()
19
+ } else {
20
+ this.open()
21
+ }
22
+ }
23
+ }
@@ -0,0 +1,96 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class extends Controller {
4
+ static targets = ["toggle", "submenu", "chevron"]
5
+
6
+ toggle(event) {
7
+ event.preventDefault()
8
+
9
+ const toggleIndex = this.toggleTargets.indexOf(event.currentTarget)
10
+ const submenu = this.submenuTargets[toggleIndex]
11
+ const chevron = this.chevronTargets[toggleIndex]
12
+ const isCurrentlyExpanded = submenu.classList.contains('block')
13
+
14
+ if (isCurrentlyExpanded) {
15
+ // Collapsing
16
+ this.collapse(submenu, chevron)
17
+ } else {
18
+ // Expanding
19
+ this.expand(submenu, chevron)
20
+ }
21
+
22
+ // Dispatch custom event
23
+ this.dispatch("toggled", {
24
+ detail: {
25
+ expanded: !isCurrentlyExpanded,
26
+ button: event.currentTarget,
27
+ submenu: submenu
28
+ }
29
+ })
30
+ }
31
+
32
+ // Method to expand a specific item with smooth animation
33
+ expand(submenu, chevron) {
34
+ // Show the submenu
35
+ submenu.classList.remove('hidden')
36
+ submenu.classList.add('block')
37
+
38
+ // Rotate the chevron
39
+ if (chevron) {
40
+ chevron.classList.add('rotate-90')
41
+ }
42
+
43
+ // Get the natural height for smooth animation
44
+ submenu.style.maxHeight = 'none'
45
+ const height = submenu.scrollHeight
46
+ submenu.style.maxHeight = '0'
47
+
48
+ // Force a reflow
49
+ submenu.offsetHeight
50
+
51
+ // Animate to natural height
52
+ submenu.style.transition = 'max-height 0.3s ease-out'
53
+ submenu.style.maxHeight = height + 'px'
54
+
55
+ // Remove inline style after animation completes
56
+ setTimeout(() => {
57
+ submenu.style.maxHeight = ''
58
+ submenu.style.transition = ''
59
+ }, 300)
60
+ }
61
+
62
+ // Method to collapse a specific item
63
+ collapse(submenu, chevron) {
64
+ // Set current height explicitly for smooth collapse
65
+ const height = submenu.scrollHeight
66
+ submenu.style.maxHeight = height + 'px'
67
+ submenu.style.transition = 'max-height 0.3s ease-out'
68
+
69
+ // Force a reflow
70
+ submenu.offsetHeight
71
+
72
+ // Rotate chevron back
73
+ if (chevron) {
74
+ chevron.classList.remove('rotate-90')
75
+ }
76
+
77
+ // Animate to 0
78
+ submenu.style.maxHeight = '0'
79
+
80
+ // Clean up after animation
81
+ setTimeout(() => {
82
+ submenu.classList.add('hidden')
83
+ submenu.classList.remove('block')
84
+ submenu.style.maxHeight = ''
85
+ submenu.style.transition = ''
86
+ }, 300)
87
+ }
88
+
89
+ // Collapse all expanded items
90
+ collapseAll() {
91
+ this.toggleTargets.forEach((button, index) => {
92
+ const submenu = this.submenuTargets[index]
93
+ this.collapse(button, submenu)
94
+ })
95
+ }
96
+ }
@@ -0,0 +1,28 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class extends Controller {
4
+ static targets = ["row", "card"]
5
+
6
+ connect() {
7
+ // Handle clicks on table rows and mobile cards
8
+ this.element.addEventListener("click", this.handleClick.bind(this))
9
+ }
10
+
11
+ handleClick(event) {
12
+ // Find the closest row or card element
13
+ const clickableElement = event.target.closest("[data-href]")
14
+
15
+ if (!clickableElement) return
16
+
17
+ // Don't navigate if clicking on buttons or links
18
+ if (event.target.closest("a, button")) return
19
+
20
+ // Get the URL from data-href attribute
21
+ const href = clickableElement.dataset.href
22
+
23
+ if (href) {
24
+ // Navigate to the show page
25
+ window.location.href = href
26
+ }
27
+ }
28
+ }
@@ -0,0 +1,16 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class extends Controller {
4
+ static values = { url: String }
5
+
6
+ navigate(event) {
7
+ // Only navigate if we didn't click on an action button or inline edit trigger
8
+ if (!event.target.closest('[data-action*="stopPropagation"]')) {
9
+ window.location = this.urlValue
10
+ }
11
+ }
12
+
13
+ stopPropagation(event) {
14
+ event.stopPropagation()
15
+ }
16
+ }
@@ -0,0 +1,22 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class extends Controller {
4
+ static targets = ["statusText", "checkbox", "slider"]
5
+
6
+ toggle(event) {
7
+ // Prevent the default behavior since we'll handle the state change
8
+ event.preventDefault()
9
+
10
+ // Find the hidden checkbox and toggle its state
11
+ const checkbox = this.checkboxTarget
12
+ checkbox.checked = !checkbox.checked
13
+
14
+ // Trigger change event so forms know the value changed
15
+ checkbox.dispatchEvent(new Event('change', { bubbles: true }))
16
+
17
+ // Update status text if present
18
+ if (this.hasStatusTextTarget) {
19
+ this.statusTextTarget.textContent = checkbox.checked ? "Enabled" : "Disabled"
20
+ }
21
+ }
22
+ }
@@ -0,0 +1,9 @@
1
+ import { Turbo } from "@hotwired/turbo-rails"
2
+
3
+ Turbo.StreamActions.redirect = function() {
4
+ const url = this.getAttribute("url")
5
+ console.log("Redirect URL:", url)
6
+ console.log("Element:", this)
7
+
8
+ Turbo.visit(url, { frame: '_top', action: 'advance' })
9
+ }
@@ -0,0 +1,54 @@
1
+ import { application } from './application'
2
+
3
+ import SidebarController from './controllers/sidebar_controller'
4
+ import SidebarNavController from './controllers/sidebar_nav_controller'
5
+ import SidebarMobileController from './controllers/sidebar_mobile_controller'
6
+ import SelectFieldController from './controllers/select_field_controller'
7
+ import ChartController from './controllers/chart_controller'
8
+ import DatePickerController from './controllers/date_picker_controller'
9
+ import DropdownController from './controllers/dropdown_controller'
10
+ import ToggleSwitchController from './controllers/toggle_switch_controller'
11
+ import FormTabsController from './controllers/form_tabs_controller'
12
+ import CollapsibleFiltersController from './controllers/collapsible_filters_controller'
13
+ import NavbarScrollController from './controllers/navbar_scroll_controller'
14
+ import HasManySearchController from './controllers/has_many_search_controller'
15
+ import NotificationController from './controllers/notification_controller'
16
+ import InfiniteScrollController from './controllers/infinite_scroll_controller'
17
+ import FileController from './controllers/file_controller'
18
+ import SettingsSidebarController from './controllers/settings_sidebar_controller'
19
+ import EventEmitterController from './controllers/event_emitter_controller'
20
+ import ModalController from './controllers/modal_controller'
21
+ import BatchSelectionController from './controllers/batch_selection_controller'
22
+ import BatchModalController from './controllers/batch_modal_controller'
23
+ import ConfirmationModalController from './controllers/confirmation_modal_controller'
24
+ import ContextMenuController from './controllers/context_menu_controller'
25
+ import RowModalController from './controllers/row_modal_controller'
26
+ import RowActionController from './controllers/row_action_controller'
27
+ import JsoneditorController from './controllers/jsoneditor_controller'
28
+
29
+ // Register controllers
30
+ application.register('sidebar', SidebarController)
31
+ application.register('sidebar-nav', SidebarNavController)
32
+ application.register('sidebar-mobile', SidebarMobileController)
33
+ application.register('select-field', SelectFieldController)
34
+ application.register('chart', ChartController)
35
+ application.register('date-picker', DatePickerController)
36
+ application.register('dropdown', DropdownController)
37
+ application.register('toggle-switch', ToggleSwitchController)
38
+ application.register('form-tabs', FormTabsController)
39
+ application.register('collapsible-filters', CollapsibleFiltersController)
40
+ application.register('navbar-scroll', NavbarScrollController)
41
+ application.register('has-many-search', HasManySearchController)
42
+ application.register('notification', NotificationController)
43
+ application.register('infinite-scroll', InfiniteScrollController)
44
+ application.register('file', FileController)
45
+ application.register('settings-sidebar', SettingsSidebarController)
46
+ application.register('event-emitter', EventEmitterController)
47
+ application.register('modal', ModalController)
48
+ application.register('batch-selection', BatchSelectionController)
49
+ application.register('batch-modal', BatchModalController)
50
+ application.register('confirmation-modal', ConfirmationModalController)
51
+ application.register('context-menu', ContextMenuController)
52
+ application.register('row-modal', RowModalController)
53
+ application.register('row-action', RowActionController)
54
+ application.register('jsoneditor', JsoneditorController)
@@ -0,0 +1,4 @@
1
+ // EasyAdmin base JavaScript
2
+ import './easy_admin/application'
3
+ import './easy_admin/controllers'
4
+ import "@hotwired/turbo-rails"
@@ -0,0 +1,53 @@
1
+ module EasyAdmin
2
+ class AdminUser < ApplicationRecord
3
+ self.table_name = "easy_admin_admin_users"
4
+
5
+ # Devise modules
6
+ devise :database_authenticatable, :registerable,
7
+ :recoverable, :rememberable, :validatable,
8
+ :trackable, :lockable, :timeoutable
9
+
10
+ # Validations
11
+ validates :email, presence: true, uniqueness: { case_sensitive: false }
12
+ validates :first_name, :last_name, presence: true
13
+
14
+ # Scopes
15
+ scope :active, -> { where(locked_at: nil) }
16
+ scope :recently_signed_in, -> { where("last_sign_in_at > ?", 1.week.ago) }
17
+
18
+ # Instance methods
19
+ def full_name
20
+ "#{first_name} #{last_name}".strip
21
+ end
22
+
23
+ def display_name
24
+ full_name.present? ? full_name : email
25
+ end
26
+
27
+ def initials
28
+ "#{first_name&.first}#{last_name&.first}".upcase
29
+ end
30
+
31
+ def active?
32
+ !locked_at.present?
33
+ end
34
+
35
+ def recently_active?
36
+ last_sign_in_at && last_sign_in_at > 1.week.ago
37
+ end
38
+
39
+ # Class methods
40
+ def self.create_default_admin!
41
+ return if exists?
42
+
43
+ create!(
44
+ email: "admin@example.com",
45
+ password: "password",
46
+ password_confirmation: "password",
47
+ first_name: "Admin",
48
+ last_name: "User",
49
+ confirmed_at: Time.current
50
+ )
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,5 @@
1
+ module EasyAdmin
2
+ class ApplicationRecord < ActiveRecord::Base
3
+ self.abstract_class = true
4
+ end
5
+ end
@@ -0,0 +1,3 @@
1
+ <h1>EasyAdmin Dashboard</h1>
2
+ <p>Welcome to EasyAdmin!</p>
3
+ <p>This is your admin dashboard.</p>
@@ -0,0 +1,7 @@
1
+ <% content_for :title, @dashboard_class.title %>
2
+
3
+ <%== EasyAdmin::Dashboards::ShowComponent.new(
4
+ dashboard_class: @dashboard_class,
5
+ params: params,
6
+ request_path: request.path
7
+ ).call %>
@@ -0,0 +1,42 @@
1
+ <!-- Form -->
2
+ <div class="p-10">
3
+ <%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :put, class: "space-y-8" }) do |f| %>
4
+ <%= f.hidden_field :reset_password_token %>
5
+
6
+ <!-- Input Fields Container -->
7
+ <div class="space-y-4">
8
+ <!-- Password Field -->
9
+ <div class="relative">
10
+ <%= f.password_field :password, autofocus: true, autocomplete: "new-password",
11
+ class: "w-full px-6 py-5 bg-gray-50/80 border-0 text-gray-900 placeholder-gray-400 focus:bg-gray-100/80 focus:ring-0 focus:outline-none text-base rounded-2xl transition-colors duration-200 font-medium",
12
+ placeholder: "New password" %>
13
+ </div>
14
+
15
+ <!-- Password Confirmation Field -->
16
+ <div class="relative">
17
+ <%= f.password_field :password_confirmation, autocomplete: "new-password",
18
+ class: "w-full px-6 py-5 bg-gray-50/80 border-0 text-gray-900 placeholder-gray-400 focus:bg-gray-100/80 focus:ring-0 focus:outline-none text-base rounded-2xl transition-colors duration-200 font-medium",
19
+ placeholder: "Confirm new password" %>
20
+ </div>
21
+ </div>
22
+
23
+ <% if @minimum_password_length %>
24
+ <div class="text-sm text-gray-500 text-center font-medium">
25
+ Password must be at least <%= @minimum_password_length %> characters
26
+ </div>
27
+ <% end %>
28
+
29
+ <!-- Submit Button -->
30
+ <div class="pt-4">
31
+ <%= f.submit "Change Password",
32
+ class: "w-full bg-gradient-to-r from-blue-500 to-blue-600 hover:from-blue-600 hover:to-blue-700 text-white font-bold py-5 px-6 rounded-2xl transition-all duration-200 focus:outline-none focus:ring-4 focus:ring-blue-500/30 text-base shadow-xl shadow-blue-500/20 active:scale-[0.98]" %>
33
+ </div>
34
+
35
+ <% end %>
36
+
37
+ <!-- Links -->
38
+ <div class="text-center pt-6">
39
+ <%= link_to "Back to Sign In", new_session_path(resource_name),
40
+ class: "block text-base text-blue-500 hover:text-blue-600 font-bold transition-colors duration-200" %>
41
+ </div>
42
+ </div>