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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +28 -0
- data/Rakefile +8 -0
- data/app/assets/builds/easy_admin.base.js +43505 -0
- data/app/assets/builds/easy_admin.base.js.map +7 -0
- data/app/assets/builds/easy_admin.css +6141 -0
- data/app/assets/config/easy_admin_manifest.js +1 -0
- data/app/assets/images/jsoneditor-icons.svg +749 -0
- data/app/assets/stylesheets/easy_admin/application.tailwind.css +390 -0
- data/app/components/easy_admin/base_component.rb +35 -0
- data/app/components/easy_admin/batch_action_bar_component.rb +125 -0
- data/app/components/easy_admin/batch_action_form_component.rb +124 -0
- data/app/components/easy_admin/combined_filters_component.rb +232 -0
- data/app/components/easy_admin/confirmation_modal_component.rb +61 -0
- data/app/components/easy_admin/context_menu_component.rb +161 -0
- data/app/components/easy_admin/dashboards/base_card_component.rb +152 -0
- data/app/components/easy_admin/dashboards/card_error_component.rb +23 -0
- data/app/components/easy_admin/dashboards/card_factory.rb +90 -0
- data/app/components/easy_admin/dashboards/card_stream_component.rb +22 -0
- data/app/components/easy_admin/dashboards/cards/base_card_component.rb +54 -0
- data/app/components/easy_admin/dashboards/cards/chart_card_component.rb +175 -0
- data/app/components/easy_admin/dashboards/cards/custom_card_component.rb +50 -0
- data/app/components/easy_admin/dashboards/cards/metric_card_component.rb +164 -0
- data/app/components/easy_admin/dashboards/cards/table_card_component.rb +148 -0
- data/app/components/easy_admin/dashboards/chart_card_component.rb +44 -0
- data/app/components/easy_admin/dashboards/metric_card_component.rb +56 -0
- data/app/components/easy_admin/dashboards/refresh_stream_component.rb +279 -0
- data/app/components/easy_admin/dashboards/show_component.rb +163 -0
- data/app/components/easy_admin/dashboards/table_card_component.rb +52 -0
- data/app/components/easy_admin/date_picker_component.rb +188 -0
- data/app/components/easy_admin/fields/base_component.rb +101 -0
- data/app/components/easy_admin/fields/belongs_to_edit_modal_component.rb +117 -0
- data/app/components/easy_admin/fields/form/belongs_to_component.rb +82 -0
- data/app/components/easy_admin/fields/form/boolean_component.rb +100 -0
- data/app/components/easy_admin/fields/form/date_component.rb +55 -0
- data/app/components/easy_admin/fields/form/datetime_component.rb +55 -0
- data/app/components/easy_admin/fields/form/email_component.rb +55 -0
- data/app/components/easy_admin/fields/form/file_component.rb +190 -0
- data/app/components/easy_admin/fields/form/has_many_component.rb +416 -0
- data/app/components/easy_admin/fields/form/json_component.rb +81 -0
- data/app/components/easy_admin/fields/form/number_component.rb +55 -0
- data/app/components/easy_admin/fields/form/select_component.rb +326 -0
- data/app/components/easy_admin/fields/form/text_component.rb +55 -0
- data/app/components/easy_admin/fields/form/textarea_component.rb +54 -0
- data/app/components/easy_admin/fields/index/belongs_to_component.rb +93 -0
- data/app/components/easy_admin/fields/index/boolean_component.rb +29 -0
- data/app/components/easy_admin/fields/index/date_component.rb +13 -0
- data/app/components/easy_admin/fields/index/datetime_component.rb +13 -0
- data/app/components/easy_admin/fields/index/email_component.rb +24 -0
- data/app/components/easy_admin/fields/index/filters/base_component.rb +48 -0
- data/app/components/easy_admin/fields/index/filters/boolean_component.rb +96 -0
- data/app/components/easy_admin/fields/index/filters/date_component.rb +182 -0
- data/app/components/easy_admin/fields/index/filters/number_component.rb +30 -0
- data/app/components/easy_admin/fields/index/filters/select_component.rb +101 -0
- data/app/components/easy_admin/fields/index/filters/string_component.rb +32 -0
- data/app/components/easy_admin/fields/index/json_component.rb +23 -0
- data/app/components/easy_admin/fields/index/number_component.rb +20 -0
- data/app/components/easy_admin/fields/index/select_component.rb +25 -0
- data/app/components/easy_admin/fields/index/text_component.rb +20 -0
- data/app/components/easy_admin/fields/inline_edit_modal_component.rb +135 -0
- data/app/components/easy_admin/fields/inline_edit_trigger_component.rb +144 -0
- data/app/components/easy_admin/fields/show/belongs_to_component.rb +93 -0
- data/app/components/easy_admin/fields/show/boolean_component.rb +21 -0
- data/app/components/easy_admin/fields/show/date_component.rb +13 -0
- data/app/components/easy_admin/fields/show/datetime_component.rb +13 -0
- data/app/components/easy_admin/fields/show/email_component.rb +19 -0
- data/app/components/easy_admin/fields/show/file_component.rb +304 -0
- data/app/components/easy_admin/fields/show/has_many_component.rb +192 -0
- data/app/components/easy_admin/fields/show/json_component.rb +45 -0
- data/app/components/easy_admin/fields/show/number_component.rb +20 -0
- data/app/components/easy_admin/fields/show/select_component.rb +25 -0
- data/app/components/easy_admin/fields/show/text_component.rb +17 -0
- data/app/components/easy_admin/fields/show/textarea_component.rb +26 -0
- data/app/components/easy_admin/filters_component.rb +120 -0
- data/app/components/easy_admin/form_tabs_component.rb +166 -0
- data/app/components/easy_admin/infinite_scroll_component.rb +82 -0
- data/app/components/easy_admin/lazy_chart_card_component.rb +128 -0
- data/app/components/easy_admin/lazy_metric_card_component.rb +76 -0
- data/app/components/easy_admin/modal_frame_component.rb +26 -0
- data/app/components/easy_admin/navbar_component.rb +226 -0
- data/app/components/easy_admin/notification_component.rb +83 -0
- data/app/components/easy_admin/pagination_component.rb +188 -0
- data/app/components/easy_admin/quick_filters_component.rb +65 -0
- data/app/components/easy_admin/resource_pagination_component.rb +14 -0
- data/app/components/easy_admin/resources/index_component.rb +211 -0
- data/app/components/easy_admin/resources/index_frame_component.rb +88 -0
- data/app/components/easy_admin/resources/show_page_actions_component.rb +324 -0
- data/app/components/easy_admin/resources/table_cell_component.rb +145 -0
- data/app/components/easy_admin/resources/table_component.rb +206 -0
- data/app/components/easy_admin/resources/table_row_component.rb +160 -0
- data/app/components/easy_admin/row_action_form_component.rb +127 -0
- data/app/components/easy_admin/scopes_component.rb +224 -0
- data/app/components/easy_admin/settings_sidebar_component.rb +140 -0
- data/app/components/easy_admin/show_layout_component.rb +600 -0
- data/app/components/easy_admin/sidebar_component.rb +174 -0
- data/app/components/easy_admin/turbo/response_component.rb +40 -0
- data/app/components/easy_admin/turbo/stream_component.rb +28 -0
- data/app/controllers/easy_admin/application_controller.rb +66 -0
- data/app/controllers/easy_admin/batch_actions_controller.rb +166 -0
- data/app/controllers/easy_admin/confirmation_modal_controller.rb +20 -0
- data/app/controllers/easy_admin/dashboard_controller.rb +6 -0
- data/app/controllers/easy_admin/dashboards_controller.rb +123 -0
- data/app/controllers/easy_admin/passwords_controller.rb +15 -0
- data/app/controllers/easy_admin/registrations_controller.rb +52 -0
- data/app/controllers/easy_admin/resources_controller.rb +907 -0
- data/app/controllers/easy_admin/row_actions_controller.rb +216 -0
- data/app/controllers/easy_admin/sessions_controller.rb +32 -0
- data/app/controllers/easy_admin/settings_controller.rb +94 -0
- data/app/helpers/easy_admin/application_helper.rb +4 -0
- data/app/helpers/easy_admin/dashboards_helper.rb +121 -0
- data/app/helpers/easy_admin/fields_helper.rb +27 -0
- data/app/helpers/easy_admin/pagy_helper.rb +30 -0
- data/app/helpers/easy_admin/resources_helper.rb +39 -0
- data/app/javascript/easy_admin/application.js +12 -0
- data/app/javascript/easy_admin/controllers/batch_modal_controller.js +66 -0
- data/app/javascript/easy_admin/controllers/batch_selection_controller.js +223 -0
- data/app/javascript/easy_admin/controllers/chart_controller.js +216 -0
- data/app/javascript/easy_admin/controllers/collapsible_filters_controller.js +118 -0
- data/app/javascript/easy_admin/controllers/confirmation_modal_controller.js +64 -0
- data/app/javascript/easy_admin/controllers/context_menu_controller.js +227 -0
- data/app/javascript/easy_admin/controllers/date_picker_controller.js +309 -0
- data/app/javascript/easy_admin/controllers/dropdown_controller.js +63 -0
- data/app/javascript/easy_admin/controllers/event_emitter_controller.js +19 -0
- data/app/javascript/easy_admin/controllers/file_controller.js +121 -0
- data/app/javascript/easy_admin/controllers/form_tabs_controller.js +100 -0
- data/app/javascript/easy_admin/controllers/has_many_search_controller.js +76 -0
- data/app/javascript/easy_admin/controllers/infinite_scroll_controller.js +174 -0
- data/app/javascript/easy_admin/controllers/ios_alert_controller.js +195 -0
- data/app/javascript/easy_admin/controllers/jsoneditor_controller.js +88 -0
- data/app/javascript/easy_admin/controllers/modal_controller.js +75 -0
- data/app/javascript/easy_admin/controllers/navbar_scroll_controller.js +76 -0
- data/app/javascript/easy_admin/controllers/notification_controller.js +48 -0
- data/app/javascript/easy_admin/controllers/row_action_controller.js +124 -0
- data/app/javascript/easy_admin/controllers/row_modal_controller.js +59 -0
- data/app/javascript/easy_admin/controllers/select_field_controller.js +618 -0
- data/app/javascript/easy_admin/controllers/settings_button_controller.js +8 -0
- data/app/javascript/easy_admin/controllers/settings_sidebar_controller.js +186 -0
- data/app/javascript/easy_admin/controllers/sidebar_controller.js +102 -0
- data/app/javascript/easy_admin/controllers/sidebar_mobile_controller.js +23 -0
- data/app/javascript/easy_admin/controllers/sidebar_nav_controller.js +96 -0
- data/app/javascript/easy_admin/controllers/table_controller.js +28 -0
- data/app/javascript/easy_admin/controllers/table_row_controller.js +16 -0
- data/app/javascript/easy_admin/controllers/toggle_switch_controller.js +22 -0
- data/app/javascript/easy_admin/controllers/turbo_stream_redirect.js +9 -0
- data/app/javascript/easy_admin/controllers.js +54 -0
- data/app/javascript/easy_admin.base.js +4 -0
- data/app/models/easy_admin/admin_user.rb +53 -0
- data/app/models/easy_admin/application_record.rb +5 -0
- data/app/views/easy_admin/dashboard/index.html.erb +3 -0
- data/app/views/easy_admin/dashboards/show.html.erb +7 -0
- data/app/views/easy_admin/passwords/edit.html.erb +42 -0
- data/app/views/easy_admin/passwords/new.html.erb +41 -0
- data/app/views/easy_admin/registrations/new.html.erb +65 -0
- data/app/views/easy_admin/resources/_redirect.turbo_stream.erb +3 -0
- data/app/views/easy_admin/resources/_table_rows.html.erb +46 -0
- data/app/views/easy_admin/resources/edit.html.erb +151 -0
- data/app/views/easy_admin/resources/index.html.erb +12 -0
- data/app/views/easy_admin/resources/index.turbo_stream.erb +139 -0
- data/app/views/easy_admin/resources/index_frame.html.erb +142 -0
- data/app/views/easy_admin/resources/new.html.erb +100 -0
- data/app/views/easy_admin/resources/show.html.erb +31 -0
- data/app/views/easy_admin/sessions/new.html.erb +55 -0
- data/app/views/easy_admin/settings/_form.html.erb +51 -0
- data/app/views/easy_admin/settings/index.html.erb +53 -0
- data/app/views/layouts/easy_admin/application.html.erb +48 -0
- data/app/views/layouts/easy_admin/auth.html.erb +34 -0
- data/config/initializers/easy_admin_card_factory.rb +27 -0
- data/config/initializers/pagy.rb +15 -0
- data/config/initializers/rack_mini_profiler.rb +67 -0
- data/config/routes.rb +70 -0
- data/db/migrate/20250101000001_create_easy_admin_admin_users.rb +45 -0
- data/lib/easy-admin.rb +32 -0
- data/lib/easy_admin/action.rb +159 -0
- data/lib/easy_admin/batch_action.rb +134 -0
- data/lib/easy_admin/configuration.rb +75 -0
- data/lib/easy_admin/dashboard.rb +110 -0
- data/lib/easy_admin/dashboard_registry.rb +30 -0
- data/lib/easy_admin/delete_action.rb +22 -0
- data/lib/easy_admin/engine.rb +54 -0
- data/lib/easy_admin/field.rb +118 -0
- data/lib/easy_admin/resource.rb +806 -0
- data/lib/easy_admin/resource_registry.rb +22 -0
- data/lib/easy_admin/types/json_type.rb +25 -0
- data/lib/easy_admin/version.rb +3 -0
- data/lib/generators/easy_admin/auth_generator.rb +69 -0
- data/lib/generators/easy_admin/card/card_generator.rb +94 -0
- data/lib/generators/easy_admin/card/templates/card_component.rb.erb +127 -0
- data/lib/generators/easy_admin/card/templates/card_component_spec.rb.erb +122 -0
- data/lib/generators/easy_admin/install/templates/easy_admin.rb +31 -0
- data/lib/generators/easy_admin/install_generator.rb +25 -0
- data/lib/generators/easy_admin/rbac/rbac_generator.rb +244 -0
- data/lib/generators/easy_admin/rbac/templates/add_rbac_to_admin_users.rb +23 -0
- data/lib/generators/easy_admin/rbac/templates/super_admin.rb +34 -0
- data/lib/generators/easy_admin/resource_generator.rb +43 -0
- data/lib/generators/easy_admin/templates/AUTH_README +35 -0
- data/lib/generators/easy_admin/templates/README +27 -0
- data/lib/generators/easy_admin/templates/create_easy_admin_admin_users.rb +45 -0
- data/lib/generators/easy_admin/templates/devise.rb +267 -0
- data/lib/generators/easy_admin/templates/easy_admin.rb +24 -0
- data/lib/generators/easy_admin/templates/resource.rb +29 -0
- data/lib/tasks/easy_admin_tasks.rake +4 -0
- 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,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,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,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>
|