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,227 @@
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
2
|
+
import { get, post } from "@rails/request.js"
|
3
|
+
|
4
|
+
export default class extends Controller {
|
5
|
+
static targets = ["menu"]
|
6
|
+
static values = {
|
7
|
+
recordId: String,
|
8
|
+
resourceName: String
|
9
|
+
}
|
10
|
+
|
11
|
+
connect() {
|
12
|
+
// Hide menu when clicking outside
|
13
|
+
this.boundHideMenu = this.hideMenuOnOutsideClick.bind(this)
|
14
|
+
document.addEventListener('click', this.boundHideMenu)
|
15
|
+
|
16
|
+
// Hide menu when scrolling
|
17
|
+
this.boundHideOnScroll = this.hideMenu.bind(this)
|
18
|
+
document.addEventListener('scroll', this.boundHideOnScroll, true)
|
19
|
+
}
|
20
|
+
|
21
|
+
disconnect() {
|
22
|
+
document.removeEventListener('click', this.boundHideMenu)
|
23
|
+
document.removeEventListener('scroll', this.boundHideOnScroll, true)
|
24
|
+
this.removeExistingMenu()
|
25
|
+
}
|
26
|
+
|
27
|
+
showMenu(event) {
|
28
|
+
// Prevent default right-click menu
|
29
|
+
event.preventDefault()
|
30
|
+
event.stopPropagation()
|
31
|
+
|
32
|
+
// Remove any existing menu
|
33
|
+
this.removeExistingMenu()
|
34
|
+
|
35
|
+
// Get row data
|
36
|
+
const row = event.currentTarget
|
37
|
+
const recordId = this.recordIdValue || row.dataset.recordId
|
38
|
+
const resourceName = this.resourceNameValue || row.dataset.resourceName
|
39
|
+
|
40
|
+
if (!recordId || !resourceName) {
|
41
|
+
console.error('Missing recordId or resourceName for context menu')
|
42
|
+
return
|
43
|
+
}
|
44
|
+
|
45
|
+
// Store position for menu placement
|
46
|
+
this.menuX = event.clientX
|
47
|
+
this.menuY = event.clientY
|
48
|
+
|
49
|
+
// Load context menu via Turbo
|
50
|
+
this.loadContextMenu(resourceName, recordId)
|
51
|
+
}
|
52
|
+
|
53
|
+
async loadContextMenu(resourceName, recordId) {
|
54
|
+
try {
|
55
|
+
// Load menu content via Turbo - it will update the context-menu-{recordId} container
|
56
|
+
const response = await get(`/admin/${resourceName}/${recordId}/context_menu`, {
|
57
|
+
responseKind: 'turbo-stream'
|
58
|
+
})
|
59
|
+
|
60
|
+
// After turbo stream loads, position and show the menu
|
61
|
+
setTimeout(() => {
|
62
|
+
this.positionAndShowMenu(recordId)
|
63
|
+
}, 10)
|
64
|
+
|
65
|
+
} catch (error) {
|
66
|
+
console.error('Failed to load context menu:', error)
|
67
|
+
this.removeExistingMenu()
|
68
|
+
}
|
69
|
+
}
|
70
|
+
|
71
|
+
positionAndShowMenu(recordId) {
|
72
|
+
const menuContainer = document.getElementById(`context-menu-${recordId}`)
|
73
|
+
const contextMenu = menuContainer?.querySelector('.context-menu')
|
74
|
+
|
75
|
+
if (!menuContainer || !contextMenu) return
|
76
|
+
|
77
|
+
// Get menu dimensions after it's been rendered
|
78
|
+
const menuRect = contextMenu.getBoundingClientRect()
|
79
|
+
const windowWidth = window.innerWidth
|
80
|
+
const windowHeight = window.innerHeight
|
81
|
+
|
82
|
+
// Calculate position to keep menu within viewport
|
83
|
+
let x = this.menuX
|
84
|
+
let y = this.menuY
|
85
|
+
|
86
|
+
// Adjust horizontal position if menu would overflow right edge
|
87
|
+
if (x + menuRect.width > windowWidth) {
|
88
|
+
x = windowWidth - menuRect.width - 10
|
89
|
+
}
|
90
|
+
|
91
|
+
// Adjust vertical position if menu would overflow bottom edge
|
92
|
+
if (y + menuRect.height > windowHeight) {
|
93
|
+
y = windowHeight - menuRect.height - 10
|
94
|
+
}
|
95
|
+
|
96
|
+
// Ensure menu doesn't go off left or top edges
|
97
|
+
x = Math.max(10, x)
|
98
|
+
y = Math.max(10, y)
|
99
|
+
|
100
|
+
// Apply final position to container and enable pointer events
|
101
|
+
menuContainer.style.left = `${x}px`
|
102
|
+
menuContainer.style.top = `${y}px`
|
103
|
+
menuContainer.classList.remove('pointer-events-none')
|
104
|
+
|
105
|
+
// Make context menu visible with animation
|
106
|
+
contextMenu.classList.add('visible')
|
107
|
+
}
|
108
|
+
|
109
|
+
hideMenu() {
|
110
|
+
this.removeExistingMenu()
|
111
|
+
}
|
112
|
+
|
113
|
+
hideMenuOnOutsideClick(event) {
|
114
|
+
// Don't hide if clicking inside the menu
|
115
|
+
const menuContainer = document.getElementById('context-menu-container')
|
116
|
+
if (menuContainer && menuContainer.contains(event.target)) {
|
117
|
+
return
|
118
|
+
}
|
119
|
+
|
120
|
+
// Hide the menu
|
121
|
+
this.hideMenu()
|
122
|
+
}
|
123
|
+
|
124
|
+
removeExistingMenu() {
|
125
|
+
// Hide all context menus
|
126
|
+
const allMenus = document.querySelectorAll('[id^="context-menu-"]')
|
127
|
+
allMenus.forEach(menu => {
|
128
|
+
const contextMenu = menu.querySelector('.context-menu')
|
129
|
+
if (contextMenu) {
|
130
|
+
contextMenu.classList.remove('visible')
|
131
|
+
}
|
132
|
+
menu.classList.add('pointer-events-none')
|
133
|
+
menu.innerHTML = ''
|
134
|
+
})
|
135
|
+
}
|
136
|
+
|
137
|
+
// Action handlers
|
138
|
+
executeAction(event) {
|
139
|
+
const button = event.currentTarget
|
140
|
+
const actionClass = button.dataset.actionClass
|
141
|
+
const executionMode = button.dataset.executionMode
|
142
|
+
const recordId = button.dataset.recordId
|
143
|
+
const resourceName = button.dataset.resourceName
|
144
|
+
const confirmMessage = button.dataset.confirm
|
145
|
+
|
146
|
+
// Hide menu immediately
|
147
|
+
this.hideMenu()
|
148
|
+
|
149
|
+
// For instant actions, show confirmation modal first
|
150
|
+
if (executionMode === 'instant' && confirmMessage) {
|
151
|
+
this.showConfirmationModal(confirmMessage, resourceName, recordId, actionClass)
|
152
|
+
} else {
|
153
|
+
// Execute immediately if no confirmation needed
|
154
|
+
this.performAction(resourceName, recordId, actionClass)
|
155
|
+
}
|
156
|
+
}
|
157
|
+
|
158
|
+
showModal(event) {
|
159
|
+
const button = event.currentTarget
|
160
|
+
const actionClass = button.dataset.actionClass
|
161
|
+
const recordId = button.dataset.recordId
|
162
|
+
const resourceName = button.dataset.resourceName
|
163
|
+
|
164
|
+
// Hide menu immediately
|
165
|
+
this.hideMenu()
|
166
|
+
|
167
|
+
// Load modal form
|
168
|
+
this.loadModalForm(resourceName, recordId, actionClass)
|
169
|
+
}
|
170
|
+
|
171
|
+
async showConfirmationModal(message, resourceName, recordId, actionClass) {
|
172
|
+
try {
|
173
|
+
// Store action details for when confirmation is received
|
174
|
+
this.pendingAction = { resourceName, recordId, actionClass }
|
175
|
+
|
176
|
+
// Listen for confirmation modal events
|
177
|
+
document.addEventListener('confirmation-modal:confirmed', this.handleConfirmation.bind(this), { once: true })
|
178
|
+
document.addEventListener('confirmation-modal:cancelled', this.handleCancellation.bind(this), { once: true })
|
179
|
+
|
180
|
+
// Load confirmation modal
|
181
|
+
const response = await get('/admin/confirmation_modal', {
|
182
|
+
responseKind: 'turbo-stream',
|
183
|
+
query: {
|
184
|
+
message: message,
|
185
|
+
title: 'Confirm Action',
|
186
|
+
confirm_text: 'Execute',
|
187
|
+
danger: 'true'
|
188
|
+
}
|
189
|
+
})
|
190
|
+
} catch (error) {
|
191
|
+
console.error('Failed to load confirmation modal:', error)
|
192
|
+
}
|
193
|
+
}
|
194
|
+
|
195
|
+
handleConfirmation() {
|
196
|
+
if (this.pendingAction) {
|
197
|
+
const { resourceName, recordId, actionClass } = this.pendingAction
|
198
|
+
this.performAction(resourceName, recordId, actionClass)
|
199
|
+
this.pendingAction = null
|
200
|
+
}
|
201
|
+
}
|
202
|
+
|
203
|
+
handleCancellation() {
|
204
|
+
this.pendingAction = null
|
205
|
+
}
|
206
|
+
|
207
|
+
async performAction(resourceName, recordId, actionClass) {
|
208
|
+
try {
|
209
|
+
const response = await post(`/admin/${resourceName}/${recordId}/actions/${actionClass}`, {
|
210
|
+
responseKind: 'turbo-stream',
|
211
|
+
body: new FormData() // Empty form data for instant actions
|
212
|
+
})
|
213
|
+
} catch (error) {
|
214
|
+
console.error('Failed to execute action:', error)
|
215
|
+
}
|
216
|
+
}
|
217
|
+
|
218
|
+
async loadModalForm(resourceName, recordId, actionClass) {
|
219
|
+
try {
|
220
|
+
const response = await get(`/admin/${resourceName}/${recordId}/actions/${actionClass}/form`, {
|
221
|
+
responseKind: 'turbo-stream'
|
222
|
+
})
|
223
|
+
} catch (error) {
|
224
|
+
console.error('Failed to load modal form:', error)
|
225
|
+
}
|
226
|
+
}
|
227
|
+
}
|
@@ -0,0 +1,309 @@
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
2
|
+
|
3
|
+
export default class extends Controller {
|
4
|
+
static targets = ["input", "modal", "grid", "monthYear"]
|
5
|
+
|
6
|
+
connect() {
|
7
|
+
this.currentDate = new Date()
|
8
|
+
this.selectedDate = this.parseInputValue()
|
9
|
+
this.viewDate = new Date(this.selectedDate || this.currentDate)
|
10
|
+
this.isOpen = false
|
11
|
+
|
12
|
+
// Ensure modal starts with correct classes
|
13
|
+
this.modalTarget.classList.add('hidden', 'opacity-0', 'scale-95')
|
14
|
+
|
15
|
+
this.updateDisplay()
|
16
|
+
}
|
17
|
+
|
18
|
+
disconnect() {
|
19
|
+
this.close()
|
20
|
+
}
|
21
|
+
|
22
|
+
// Toggle date picker modal
|
23
|
+
toggle(event) {
|
24
|
+
if (event) {
|
25
|
+
event.preventDefault()
|
26
|
+
event.stopPropagation()
|
27
|
+
}
|
28
|
+
|
29
|
+
if (this.isOpen) {
|
30
|
+
this.close()
|
31
|
+
} else {
|
32
|
+
this.open()
|
33
|
+
}
|
34
|
+
}
|
35
|
+
|
36
|
+
// Open date picker
|
37
|
+
open() {
|
38
|
+
this.isOpen = true
|
39
|
+
this.adjustModalPosition()
|
40
|
+
this.modalTarget.classList.remove('hidden')
|
41
|
+
// Use setTimeout to ensure the transition works
|
42
|
+
setTimeout(() => {
|
43
|
+
this.modalTarget.classList.add('opacity-100', 'scale-100')
|
44
|
+
this.modalTarget.classList.remove('opacity-0', 'scale-95')
|
45
|
+
}, 10)
|
46
|
+
this.updateCalendar()
|
47
|
+
|
48
|
+
// Add click outside listener with a small delay to prevent immediate close
|
49
|
+
setTimeout(() => {
|
50
|
+
document.addEventListener('click', this.handleClickOutside)
|
51
|
+
document.addEventListener('keydown', this.handleKeydown)
|
52
|
+
}, 100)
|
53
|
+
}
|
54
|
+
|
55
|
+
// Adjust modal position for mobile
|
56
|
+
adjustModalPosition() {
|
57
|
+
const inputRect = this.inputTarget.getBoundingClientRect()
|
58
|
+
const viewport = {
|
59
|
+
width: window.innerWidth,
|
60
|
+
height: window.innerHeight
|
61
|
+
}
|
62
|
+
|
63
|
+
// Check if we're on mobile (viewport width < 640px - sm breakpoint)
|
64
|
+
if (viewport.width < 640) {
|
65
|
+
// On mobile, center the modal horizontally and add some top margin
|
66
|
+
this.modalTarget.style.left = '0'
|
67
|
+
this.modalTarget.style.right = '0'
|
68
|
+
this.modalTarget.style.top = '8px' // mt-2 equivalent
|
69
|
+
} else {
|
70
|
+
// On desktop, remove any mobile-specific positioning
|
71
|
+
this.modalTarget.style.left = ''
|
72
|
+
this.modalTarget.style.right = ''
|
73
|
+
this.modalTarget.style.top = ''
|
74
|
+
}
|
75
|
+
}
|
76
|
+
|
77
|
+
// Close date picker
|
78
|
+
close() {
|
79
|
+
this.isOpen = false
|
80
|
+
this.modalTarget.classList.add('opacity-0', 'scale-95')
|
81
|
+
this.modalTarget.classList.remove('opacity-100', 'scale-100')
|
82
|
+
|
83
|
+
// Wait for animation to complete before hiding
|
84
|
+
setTimeout(() => {
|
85
|
+
if (!this.isOpen) {
|
86
|
+
this.modalTarget.classList.add('hidden')
|
87
|
+
}
|
88
|
+
}, 200)
|
89
|
+
|
90
|
+
// Remove listeners
|
91
|
+
document.removeEventListener('click', this.handleClickOutside)
|
92
|
+
document.removeEventListener('keydown', this.handleKeydown)
|
93
|
+
}
|
94
|
+
|
95
|
+
// Handle click outside (called from data-action on modal)
|
96
|
+
clickOutside(event) {
|
97
|
+
// Only close if the click target is the modal backdrop itself
|
98
|
+
if (event.target === this.modalTarget) {
|
99
|
+
this.close()
|
100
|
+
}
|
101
|
+
}
|
102
|
+
|
103
|
+
// Prevent modal close when clicking inside
|
104
|
+
preventClose(event) {
|
105
|
+
event.stopPropagation()
|
106
|
+
}
|
107
|
+
|
108
|
+
// Navigation methods
|
109
|
+
previousMonth() {
|
110
|
+
this.viewDate.setMonth(this.viewDate.getMonth() - 1)
|
111
|
+
this.updateCalendar()
|
112
|
+
}
|
113
|
+
|
114
|
+
nextMonth() {
|
115
|
+
this.viewDate.setMonth(this.viewDate.getMonth() + 1)
|
116
|
+
this.updateCalendar()
|
117
|
+
}
|
118
|
+
|
119
|
+
// Quick actions
|
120
|
+
today() {
|
121
|
+
this.selectedDate = new Date()
|
122
|
+
this.viewDate = new Date()
|
123
|
+
this.updateInput()
|
124
|
+
this.updateCalendar()
|
125
|
+
}
|
126
|
+
|
127
|
+
clear() {
|
128
|
+
this.selectedDate = null
|
129
|
+
this.inputTarget.value = ""
|
130
|
+
this.updateCalendar()
|
131
|
+
}
|
132
|
+
|
133
|
+
// Select a date
|
134
|
+
selectDate(event) {
|
135
|
+
const day = parseInt(event.target.dataset.day)
|
136
|
+
if (!day) return
|
137
|
+
|
138
|
+
this.selectedDate = new Date(this.viewDate.getFullYear(), this.viewDate.getMonth(), day)
|
139
|
+
this.updateInput()
|
140
|
+
this.updateCalendar()
|
141
|
+
this.close()
|
142
|
+
}
|
143
|
+
|
144
|
+
// Update the calendar display
|
145
|
+
updateCalendar() {
|
146
|
+
this.updateMonthYear()
|
147
|
+
this.updateGrid()
|
148
|
+
}
|
149
|
+
|
150
|
+
// Update month/year display
|
151
|
+
updateMonthYear() {
|
152
|
+
const monthNames = [
|
153
|
+
'January', 'February', 'March', 'April', 'May', 'June',
|
154
|
+
'July', 'August', 'September', 'October', 'November', 'December'
|
155
|
+
]
|
156
|
+
|
157
|
+
const month = monthNames[this.viewDate.getMonth()]
|
158
|
+
const year = this.viewDate.getFullYear()
|
159
|
+
|
160
|
+
this.monthYearTarget.textContent = `${month} ${year}`
|
161
|
+
}
|
162
|
+
|
163
|
+
// Update calendar grid
|
164
|
+
updateGrid() {
|
165
|
+
const year = this.viewDate.getFullYear()
|
166
|
+
const month = this.viewDate.getMonth()
|
167
|
+
|
168
|
+
// First day of the month
|
169
|
+
const firstDay = new Date(year, month, 1)
|
170
|
+
const lastDay = new Date(year, month + 1, 0)
|
171
|
+
|
172
|
+
// Start from Sunday of the week containing the first day
|
173
|
+
const startDate = new Date(firstDay)
|
174
|
+
startDate.setDate(startDate.getDate() - startDate.getDay())
|
175
|
+
|
176
|
+
// Generate 6 weeks (42 days)
|
177
|
+
const days = []
|
178
|
+
const currentDate = new Date(startDate)
|
179
|
+
|
180
|
+
for (let i = 0; i < 42; i++) {
|
181
|
+
days.push(new Date(currentDate))
|
182
|
+
currentDate.setDate(currentDate.getDate() + 1)
|
183
|
+
}
|
184
|
+
|
185
|
+
// Render days
|
186
|
+
this.gridTarget.innerHTML = days.map(date => {
|
187
|
+
const day = date.getDate()
|
188
|
+
const isCurrentMonth = date.getMonth() === month
|
189
|
+
const isToday = this.isSameDay(date, new Date())
|
190
|
+
const isSelected = this.selectedDate && this.isSameDay(date, this.selectedDate)
|
191
|
+
const isWeekend = date.getDay() === 0 || date.getDay() === 6
|
192
|
+
|
193
|
+
let classes = ['text-sm', 'p-2', 'sm:p-2', 'py-3', 'sm:py-2', 'rounded-lg', 'transition-all', 'duration-200', 'touch-manipulation', 'min-h-[40px]', 'sm:min-h-0']
|
194
|
+
|
195
|
+
if (!isCurrentMonth) {
|
196
|
+
classes.push('text-gray-300', 'cursor-not-allowed')
|
197
|
+
} else if (isSelected) {
|
198
|
+
classes.push('bg-blue-600', 'text-white', 'font-semibold', 'shadow-sm')
|
199
|
+
} else if (isToday) {
|
200
|
+
classes.push('bg-blue-50', 'text-blue-600', 'font-semibold', 'hover:bg-blue-100')
|
201
|
+
} else if (isWeekend) {
|
202
|
+
classes.push('text-gray-500', 'hover:bg-gray-100')
|
203
|
+
} else {
|
204
|
+
classes.push('text-gray-700', 'hover:bg-gray-100')
|
205
|
+
}
|
206
|
+
|
207
|
+
return `
|
208
|
+
<button type="button"
|
209
|
+
class="${classes.join(' ')}"
|
210
|
+
data-day="${isCurrentMonth ? day : ''}"
|
211
|
+
data-action="click->date-picker#selectDate"
|
212
|
+
${!isCurrentMonth ? 'disabled' : ''}>
|
213
|
+
${day}
|
214
|
+
</button>
|
215
|
+
`
|
216
|
+
}).join('')
|
217
|
+
}
|
218
|
+
|
219
|
+
// Update input field
|
220
|
+
updateInput() {
|
221
|
+
if (this.selectedDate) {
|
222
|
+
this.inputTarget.value = this.formatDate(this.selectedDate)
|
223
|
+
}
|
224
|
+
}
|
225
|
+
|
226
|
+
// Update display on connect
|
227
|
+
updateDisplay() {
|
228
|
+
if (this.selectedDate) {
|
229
|
+
this.inputTarget.value = this.formatDate(this.selectedDate)
|
230
|
+
}
|
231
|
+
}
|
232
|
+
|
233
|
+
// Parse input value to Date
|
234
|
+
parseInputValue() {
|
235
|
+
const value = this.inputTarget.value
|
236
|
+
if (!value) return null
|
237
|
+
|
238
|
+
try {
|
239
|
+
return new Date(value)
|
240
|
+
} catch (e) {
|
241
|
+
return null
|
242
|
+
}
|
243
|
+
}
|
244
|
+
|
245
|
+
// Format date for display
|
246
|
+
formatDate(date) {
|
247
|
+
return date.toLocaleDateString('en-US', {
|
248
|
+
year: 'numeric',
|
249
|
+
month: 'long',
|
250
|
+
day: 'numeric'
|
251
|
+
})
|
252
|
+
}
|
253
|
+
|
254
|
+
// Check if two dates are the same day
|
255
|
+
isSameDay(date1, date2) {
|
256
|
+
return date1.getFullYear() === date2.getFullYear() &&
|
257
|
+
date1.getMonth() === date2.getMonth() &&
|
258
|
+
date1.getDate() === date2.getDate()
|
259
|
+
}
|
260
|
+
|
261
|
+
// Bound methods for event listeners
|
262
|
+
handleClickOutside = (event) => {
|
263
|
+
// Don't close if clicking the input or inside the modal
|
264
|
+
if (!this.element.contains(event.target) && !this.modalTarget.contains(event.target)) {
|
265
|
+
this.close()
|
266
|
+
}
|
267
|
+
}
|
268
|
+
|
269
|
+
handleKeydown = (event) => {
|
270
|
+
if (event.key === 'Escape') {
|
271
|
+
this.close()
|
272
|
+
} else if (this.isOpen) {
|
273
|
+
switch(event.key) {
|
274
|
+
case 'ArrowLeft':
|
275
|
+
event.preventDefault()
|
276
|
+
this.changeSelectedDate(-1)
|
277
|
+
break
|
278
|
+
case 'ArrowRight':
|
279
|
+
event.preventDefault()
|
280
|
+
this.changeSelectedDate(1)
|
281
|
+
break
|
282
|
+
case 'ArrowUp':
|
283
|
+
event.preventDefault()
|
284
|
+
this.changeSelectedDate(-7)
|
285
|
+
break
|
286
|
+
case 'ArrowDown':
|
287
|
+
event.preventDefault()
|
288
|
+
this.changeSelectedDate(7)
|
289
|
+
break
|
290
|
+
case 'Enter':
|
291
|
+
event.preventDefault()
|
292
|
+
if (this.selectedDate) {
|
293
|
+
this.updateInput()
|
294
|
+
this.close()
|
295
|
+
}
|
296
|
+
break
|
297
|
+
}
|
298
|
+
}
|
299
|
+
}
|
300
|
+
|
301
|
+
changeSelectedDate(days) {
|
302
|
+
if (!this.selectedDate) {
|
303
|
+
this.selectedDate = new Date()
|
304
|
+
}
|
305
|
+
this.selectedDate.setDate(this.selectedDate.getDate() + days)
|
306
|
+
this.viewDate = new Date(this.selectedDate)
|
307
|
+
this.updateCalendar()
|
308
|
+
}
|
309
|
+
}
|
@@ -0,0 +1,63 @@
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
2
|
+
|
3
|
+
export default class extends Controller {
|
4
|
+
static targets = ["trigger", "menu", "chevron"]
|
5
|
+
static classes = ["open"]
|
6
|
+
|
7
|
+
connect() {
|
8
|
+
this.close()
|
9
|
+
this.setupClickOutside()
|
10
|
+
}
|
11
|
+
|
12
|
+
disconnect() {
|
13
|
+
if (this.clickOutsideHandler) {
|
14
|
+
document.removeEventListener('click', this.clickOutsideHandler)
|
15
|
+
}
|
16
|
+
}
|
17
|
+
|
18
|
+
setupClickOutside() {
|
19
|
+
this.clickOutsideHandler = (event) => {
|
20
|
+
if (!this.element.contains(event.target)) {
|
21
|
+
this.close()
|
22
|
+
}
|
23
|
+
}
|
24
|
+
document.addEventListener('click', this.clickOutsideHandler)
|
25
|
+
}
|
26
|
+
|
27
|
+
toggle() {
|
28
|
+
if (this.isOpen()) {
|
29
|
+
this.close()
|
30
|
+
} else {
|
31
|
+
this.open()
|
32
|
+
}
|
33
|
+
}
|
34
|
+
|
35
|
+
open() {
|
36
|
+
this.menuTarget.classList.remove('hidden')
|
37
|
+
this.menuTarget.classList.add('block')
|
38
|
+
|
39
|
+
// Rotate chevron up
|
40
|
+
if (this.hasChevronTarget) {
|
41
|
+
this.chevronTarget.classList.add('rotate-180')
|
42
|
+
}
|
43
|
+
|
44
|
+
// Add a small delay for smooth animation
|
45
|
+
setTimeout(() => {
|
46
|
+
this.menuTarget.classList.add('animate-fadeIn')
|
47
|
+
}, 10)
|
48
|
+
}
|
49
|
+
|
50
|
+
close() {
|
51
|
+
this.menuTarget.classList.add('hidden')
|
52
|
+
this.menuTarget.classList.remove('block', 'animate-fadeIn')
|
53
|
+
|
54
|
+
// Rotate chevron back down
|
55
|
+
if (this.hasChevronTarget) {
|
56
|
+
this.chevronTarget.classList.remove('rotate-180')
|
57
|
+
}
|
58
|
+
}
|
59
|
+
|
60
|
+
isOpen() {
|
61
|
+
return !this.menuTarget.classList.contains('hidden')
|
62
|
+
}
|
63
|
+
}
|
@@ -0,0 +1,19 @@
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
2
|
+
|
3
|
+
export default class extends Controller {
|
4
|
+
static values = { event: String, detail: Object }
|
5
|
+
|
6
|
+
emit() {
|
7
|
+
const eventName = this.eventValue
|
8
|
+
const eventDetail = this.hasDetailValue ? this.detailValue : {}
|
9
|
+
|
10
|
+
console.log(`Event emitter dispatching: ${eventName}`, eventDetail)
|
11
|
+
|
12
|
+
const customEvent = new CustomEvent(eventName, {
|
13
|
+
detail: eventDetail,
|
14
|
+
bubbles: true
|
15
|
+
})
|
16
|
+
|
17
|
+
document.dispatchEvent(customEvent)
|
18
|
+
}
|
19
|
+
}
|