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,223 @@
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
2
|
+
import { get, post } from "@rails/request.js"
|
3
|
+
|
4
|
+
export default class extends Controller {
|
5
|
+
static targets = ["checkbox", "actionBar", "actionBarContent", "counter", "selectAll", "selectedIds"]
|
6
|
+
static values = { resourceName: String }
|
7
|
+
|
8
|
+
connect() {
|
9
|
+
this.selectedIds = new Set()
|
10
|
+
this.updateUI()
|
11
|
+
}
|
12
|
+
|
13
|
+
toggleItem(event) {
|
14
|
+
const checkbox = event.target
|
15
|
+
const id = checkbox.value
|
16
|
+
const row = checkbox.closest('tr')
|
17
|
+
|
18
|
+
if (checkbox.checked) {
|
19
|
+
this.selectedIds.add(id)
|
20
|
+
// Add selected styling to the row
|
21
|
+
row?.classList.add('bg-blue-50', 'border-blue-200')
|
22
|
+
row?.classList.remove('hover:bg-gray-100')
|
23
|
+
} else {
|
24
|
+
this.selectedIds.delete(id)
|
25
|
+
// Remove selected styling from the row
|
26
|
+
row?.classList.remove('bg-blue-50', 'border-blue-200')
|
27
|
+
row?.classList.add('hover:bg-gray-100')
|
28
|
+
}
|
29
|
+
|
30
|
+
this.updateUI()
|
31
|
+
}
|
32
|
+
|
33
|
+
toggleAll(event) {
|
34
|
+
const checkAll = event.target.checked
|
35
|
+
const itemCheckboxes = this.checkboxTargets.filter(cb => !cb.dataset.selectAll)
|
36
|
+
|
37
|
+
itemCheckboxes.forEach(checkbox => {
|
38
|
+
checkbox.checked = checkAll
|
39
|
+
const row = checkbox.closest('tr')
|
40
|
+
|
41
|
+
if (checkAll) {
|
42
|
+
this.selectedIds.add(checkbox.value)
|
43
|
+
// Add selected styling to the row
|
44
|
+
row?.classList.add('bg-blue-50', 'border-blue-200')
|
45
|
+
row?.classList.remove('hover:bg-gray-100')
|
46
|
+
} else {
|
47
|
+
this.selectedIds.delete(checkbox.value)
|
48
|
+
// Remove selected styling from the row
|
49
|
+
row?.classList.remove('bg-blue-50', 'border-blue-200')
|
50
|
+
row?.classList.add('hover:bg-gray-100')
|
51
|
+
}
|
52
|
+
})
|
53
|
+
|
54
|
+
this.updateUI()
|
55
|
+
}
|
56
|
+
|
57
|
+
updateUI() {
|
58
|
+
const count = this.selectedIds.size
|
59
|
+
|
60
|
+
// Update counter
|
61
|
+
if (this.hasCounterTarget) {
|
62
|
+
this.counterTarget.textContent = count
|
63
|
+
}
|
64
|
+
|
65
|
+
// Show/hide action bar with iOS-style animation
|
66
|
+
if (this.hasActionBarContentTarget) {
|
67
|
+
if (count > 0) {
|
68
|
+
// Show the action bar with smooth animation
|
69
|
+
this.actionBarContentTarget.style.transform = 'translateY(0)'
|
70
|
+
this.actionBarContentTarget.style.opacity = '1'
|
71
|
+
} else {
|
72
|
+
// Hide with animation
|
73
|
+
this.actionBarContentTarget.style.transform = 'translateY(100%)'
|
74
|
+
this.actionBarContentTarget.style.opacity = '0'
|
75
|
+
}
|
76
|
+
}
|
77
|
+
|
78
|
+
// Update hidden input with selected IDs
|
79
|
+
if (this.hasSelectedIdsTarget) {
|
80
|
+
this.selectedIdsTarget.value = Array.from(this.selectedIds).join(',')
|
81
|
+
}
|
82
|
+
|
83
|
+
// Update select all checkbox state
|
84
|
+
if (this.hasSelectAllTarget) {
|
85
|
+
const itemCheckboxes = this.checkboxTargets.filter(cb => !cb.dataset.selectAll)
|
86
|
+
const allChecked = itemCheckboxes.length > 0 && itemCheckboxes.every(cb => cb.checked)
|
87
|
+
this.selectAllTarget.checked = allChecked
|
88
|
+
}
|
89
|
+
}
|
90
|
+
|
91
|
+
executeAction(event) {
|
92
|
+
const button = event.currentTarget
|
93
|
+
const actionClass = button.dataset.actionClass
|
94
|
+
const executionMode = button.dataset.executionMode
|
95
|
+
|
96
|
+
if (this.selectedIds.size === 0) {
|
97
|
+
alert('Please select at least one item')
|
98
|
+
return
|
99
|
+
}
|
100
|
+
|
101
|
+
if (executionMode === 'instant') {
|
102
|
+
this.executeInstantAction(actionClass, button)
|
103
|
+
} else if (executionMode === 'modal') {
|
104
|
+
this.openModalAction(actionClass)
|
105
|
+
}
|
106
|
+
}
|
107
|
+
|
108
|
+
async executeInstantAction(actionClass, button) {
|
109
|
+
// Always show confirmation for instant actions
|
110
|
+
const actionLabel = button.textContent.trim()
|
111
|
+
const selectedCount = this.selectedIds.size
|
112
|
+
const defaultMessage = `${selectedCount} selected item${selectedCount !== 1 ? 's' : ''}`
|
113
|
+
const confirmMessage = button.dataset.confirm || defaultMessage
|
114
|
+
|
115
|
+
// Determine if this is a dangerous action (delete, remove, etc.)
|
116
|
+
const isDangerous = actionLabel.toLowerCase().includes('delete') ||
|
117
|
+
actionLabel.toLowerCase().includes('remove') ||
|
118
|
+
actionLabel.toLowerCase().includes('destroy')
|
119
|
+
|
120
|
+
// Show confirmation modal via Turbo
|
121
|
+
const confirmed = await this.showConfirmationModal(actionLabel, confirmMessage, isDangerous)
|
122
|
+
|
123
|
+
if (!confirmed) {
|
124
|
+
return
|
125
|
+
}
|
126
|
+
|
127
|
+
// Show loading state
|
128
|
+
button.disabled = true
|
129
|
+
const originalText = button.textContent
|
130
|
+
button.textContent = 'Processing...'
|
131
|
+
|
132
|
+
// Prepare form data
|
133
|
+
const formData = new FormData()
|
134
|
+
|
135
|
+
// Add CSRF token
|
136
|
+
const csrfToken = document.querySelector('[name="csrf-token"]').content
|
137
|
+
formData.append('authenticity_token', csrfToken)
|
138
|
+
|
139
|
+
// Add action class and execution mode
|
140
|
+
formData.append('action_class', actionClass)
|
141
|
+
formData.append('execution_mode', 'instant')
|
142
|
+
|
143
|
+
// Add selected IDs
|
144
|
+
Array.from(this.selectedIds).forEach(id => {
|
145
|
+
formData.append('selected_ids[]', id)
|
146
|
+
})
|
147
|
+
|
148
|
+
// Submit via @rails/request.js
|
149
|
+
post(`/admin/${this.resourceNameValue}/batch_action`, {
|
150
|
+
body: formData,
|
151
|
+
responseKind: 'turbo-stream'
|
152
|
+
}).then(response => {
|
153
|
+
button.disabled = false
|
154
|
+
button.textContent = originalText
|
155
|
+
|
156
|
+
if (response.ok) {
|
157
|
+
// Dispatch event to clear selection
|
158
|
+
window.dispatchEvent(new CustomEvent('batch-action:completed'))
|
159
|
+
}
|
160
|
+
}).catch(error => {
|
161
|
+
button.disabled = false
|
162
|
+
button.textContent = originalText
|
163
|
+
console.error('Instant batch action failed:', error)
|
164
|
+
})
|
165
|
+
}
|
166
|
+
|
167
|
+
openModalAction(actionClass) {
|
168
|
+
// Open Turbo Frame modal for the batch action form
|
169
|
+
const url = `/admin/${this.resourceNameValue}/batch_action/${actionClass}/form`
|
170
|
+
const selectedIds = Array.from(this.selectedIds).join(',')
|
171
|
+
|
172
|
+
// Use @rails/request.js to load the modal
|
173
|
+
get(`${url}?selected_ids=${selectedIds}`, {
|
174
|
+
responseKind: 'turbo-stream'
|
175
|
+
})
|
176
|
+
}
|
177
|
+
|
178
|
+
clearSelection() {
|
179
|
+
this.selectedIds.clear()
|
180
|
+
this.checkboxTargets.forEach(cb => {
|
181
|
+
cb.checked = false
|
182
|
+
const row = cb.closest('tr')
|
183
|
+
// Remove selected styling from all rows
|
184
|
+
row?.classList.remove('bg-blue-50', 'border-blue-200')
|
185
|
+
row?.classList.add('hover:bg-gray-100')
|
186
|
+
})
|
187
|
+
this.updateUI()
|
188
|
+
}
|
189
|
+
|
190
|
+
async showConfirmationModal(title, message, isDangerous = false) {
|
191
|
+
return new Promise((resolve) => {
|
192
|
+
// Prepare the confirmation modal URL
|
193
|
+
const params = new URLSearchParams({
|
194
|
+
title: title,
|
195
|
+
message: message,
|
196
|
+
confirm_text: title,
|
197
|
+
cancel_text: 'Cancel',
|
198
|
+
danger: isDangerous
|
199
|
+
})
|
200
|
+
|
201
|
+
// Load the confirmation modal via Turbo
|
202
|
+
get(`/admin/confirmation_modal?${params.toString()}`, {
|
203
|
+
responseKind: 'turbo-stream'
|
204
|
+
})
|
205
|
+
|
206
|
+
// Listen for confirmation events
|
207
|
+
const handleConfirmed = () => {
|
208
|
+
document.removeEventListener('confirmation-modal:confirmed', handleConfirmed)
|
209
|
+
document.removeEventListener('confirmation-modal:cancelled', handleCancelled)
|
210
|
+
resolve(true)
|
211
|
+
}
|
212
|
+
|
213
|
+
const handleCancelled = () => {
|
214
|
+
document.removeEventListener('confirmation-modal:confirmed', handleConfirmed)
|
215
|
+
document.removeEventListener('confirmation-modal:cancelled', handleCancelled)
|
216
|
+
resolve(false)
|
217
|
+
}
|
218
|
+
|
219
|
+
document.addEventListener('confirmation-modal:confirmed', handleConfirmed)
|
220
|
+
document.addEventListener('confirmation-modal:cancelled', handleCancelled)
|
221
|
+
})
|
222
|
+
}
|
223
|
+
}
|
@@ -0,0 +1,216 @@
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
2
|
+
import { Chart, registerables } from "chart.js"
|
3
|
+
|
4
|
+
Chart.register(...registerables)
|
5
|
+
|
6
|
+
// iOS-style color palette
|
7
|
+
const iOSColors = {
|
8
|
+
blue: "#007AFF",
|
9
|
+
green: "#34C759",
|
10
|
+
red: "#FF3B30",
|
11
|
+
orange: "#FF9500",
|
12
|
+
yellow: "#FFCC00",
|
13
|
+
purple: "#AF52DE",
|
14
|
+
pink: "#FF2D92",
|
15
|
+
teal: "#5AC8FA",
|
16
|
+
indigo: "#5856D6",
|
17
|
+
gray: "#8E8E93"
|
18
|
+
}
|
19
|
+
|
20
|
+
const chartColors = [
|
21
|
+
iOSColors.blue,
|
22
|
+
iOSColors.green,
|
23
|
+
iOSColors.orange,
|
24
|
+
iOSColors.purple,
|
25
|
+
iOSColors.teal,
|
26
|
+
iOSColors.pink,
|
27
|
+
iOSColors.indigo,
|
28
|
+
iOSColors.yellow
|
29
|
+
]
|
30
|
+
|
31
|
+
export default class extends Controller {
|
32
|
+
static values = {
|
33
|
+
type: String,
|
34
|
+
data: Object,
|
35
|
+
options: Object
|
36
|
+
}
|
37
|
+
|
38
|
+
connect() {
|
39
|
+
this.initializeChart()
|
40
|
+
}
|
41
|
+
|
42
|
+
disconnect() {
|
43
|
+
if (this.chart) {
|
44
|
+
this.chart.destroy()
|
45
|
+
}
|
46
|
+
}
|
47
|
+
|
48
|
+
initializeChart() {
|
49
|
+
const ctx = this.element.querySelector('canvas')
|
50
|
+
if (!ctx) return
|
51
|
+
|
52
|
+
const chartData = this.prepareData()
|
53
|
+
const chartOptions = this.prepareOptions()
|
54
|
+
|
55
|
+
this.chart = new Chart(ctx, {
|
56
|
+
type: this.getChartJSType(),
|
57
|
+
data: chartData,
|
58
|
+
options: chartOptions
|
59
|
+
})
|
60
|
+
}
|
61
|
+
|
62
|
+
// Map dashboard chart types to Chart.js types
|
63
|
+
getChartJSType() {
|
64
|
+
const typeMapping = {
|
65
|
+
'horizontal_bar': 'bar',
|
66
|
+
'area': 'line',
|
67
|
+
'donut': 'doughnut',
|
68
|
+
'polar_area': 'polarArea'
|
69
|
+
}
|
70
|
+
|
71
|
+
return typeMapping[this.typeValue] || this.typeValue || 'line'
|
72
|
+
}
|
73
|
+
|
74
|
+
prepareData() {
|
75
|
+
const data = this.dataValue || {}
|
76
|
+
const chartJSType = this.getChartJSType()
|
77
|
+
|
78
|
+
// Apply iOS colors to datasets
|
79
|
+
if (data.datasets) {
|
80
|
+
data.datasets.forEach((dataset, index) => {
|
81
|
+
const color = chartColors[index % chartColors.length]
|
82
|
+
|
83
|
+
if (chartJSType === 'line') {
|
84
|
+
dataset.borderColor = color
|
85
|
+
dataset.backgroundColor = this.hexToRgba(color, this.typeValue === 'area' ? 0.3 : 0.1)
|
86
|
+
dataset.pointBackgroundColor = color
|
87
|
+
dataset.pointBorderColor = '#ffffff'
|
88
|
+
dataset.pointBorderWidth = 2
|
89
|
+
dataset.pointRadius = 4
|
90
|
+
dataset.pointHoverRadius = 6
|
91
|
+
dataset.fill = this.typeValue === 'area' // Fill area charts
|
92
|
+
dataset.tension = 0.4
|
93
|
+
} else if (chartJSType === 'bar') {
|
94
|
+
dataset.backgroundColor = color
|
95
|
+
dataset.borderColor = color
|
96
|
+
dataset.borderWidth = 0
|
97
|
+
dataset.borderRadius = 4
|
98
|
+
dataset.borderSkipped = false
|
99
|
+
} else if (chartJSType === 'doughnut' || chartJSType === 'pie') {
|
100
|
+
dataset.backgroundColor = chartColors.slice(0, data.labels?.length || 8)
|
101
|
+
dataset.borderWidth = 0
|
102
|
+
}
|
103
|
+
})
|
104
|
+
}
|
105
|
+
|
106
|
+
return data
|
107
|
+
}
|
108
|
+
|
109
|
+
prepareOptions() {
|
110
|
+
const chartJSType = this.getChartJSType()
|
111
|
+
|
112
|
+
const defaultOptions = {
|
113
|
+
responsive: true,
|
114
|
+
maintainAspectRatio: false,
|
115
|
+
interaction: {
|
116
|
+
intersect: false,
|
117
|
+
mode: 'index'
|
118
|
+
},
|
119
|
+
// Set indexAxis for horizontal bar charts
|
120
|
+
indexAxis: this.typeValue === 'horizontal_bar' ? 'y' : 'x',
|
121
|
+
plugins: {
|
122
|
+
legend: {
|
123
|
+
display: true,
|
124
|
+
position: 'bottom',
|
125
|
+
labels: {
|
126
|
+
usePointStyle: true,
|
127
|
+
pointStyle: 'circle',
|
128
|
+
padding: 20,
|
129
|
+
font: {
|
130
|
+
family: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
|
131
|
+
size: 12,
|
132
|
+
weight: '500'
|
133
|
+
},
|
134
|
+
color: '#1D1D1F'
|
135
|
+
}
|
136
|
+
},
|
137
|
+
tooltip: {
|
138
|
+
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
139
|
+
titleColor: '#ffffff',
|
140
|
+
bodyColor: '#ffffff',
|
141
|
+
borderColor: 'rgba(255, 255, 255, 0.1)',
|
142
|
+
borderWidth: 1,
|
143
|
+
cornerRadius: 8,
|
144
|
+
displayColors: true,
|
145
|
+
titleFont: {
|
146
|
+
family: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
|
147
|
+
size: 13,
|
148
|
+
weight: '600'
|
149
|
+
},
|
150
|
+
bodyFont: {
|
151
|
+
family: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
|
152
|
+
size: 12,
|
153
|
+
weight: '500'
|
154
|
+
}
|
155
|
+
}
|
156
|
+
},
|
157
|
+
scales: {
|
158
|
+
x: {
|
159
|
+
grid: {
|
160
|
+
display: true,
|
161
|
+
color: 'rgba(0, 0, 0, 0.05)',
|
162
|
+
lineWidth: 1
|
163
|
+
},
|
164
|
+
ticks: {
|
165
|
+
font: {
|
166
|
+
family: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
|
167
|
+
size: 11,
|
168
|
+
weight: '500'
|
169
|
+
},
|
170
|
+
color: 'rgba(60, 60, 67, 0.6)'
|
171
|
+
},
|
172
|
+
border: {
|
173
|
+
display: false
|
174
|
+
}
|
175
|
+
},
|
176
|
+
y: {
|
177
|
+
grid: {
|
178
|
+
display: true,
|
179
|
+
color: 'rgba(0, 0, 0, 0.05)',
|
180
|
+
lineWidth: 1
|
181
|
+
},
|
182
|
+
ticks: {
|
183
|
+
font: {
|
184
|
+
family: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
|
185
|
+
size: 11,
|
186
|
+
weight: '500'
|
187
|
+
},
|
188
|
+
color: 'rgba(60, 60, 67, 0.6)'
|
189
|
+
},
|
190
|
+
border: {
|
191
|
+
display: false
|
192
|
+
}
|
193
|
+
}
|
194
|
+
}
|
195
|
+
}
|
196
|
+
|
197
|
+
// Remove scales for pie/doughnut charts
|
198
|
+
if (chartJSType === 'doughnut' || chartJSType === 'pie') {
|
199
|
+
delete defaultOptions.scales
|
200
|
+
}
|
201
|
+
|
202
|
+
// Merge with custom options
|
203
|
+
return this.mergeOptions(defaultOptions, this.optionsValue || {})
|
204
|
+
}
|
205
|
+
|
206
|
+
mergeOptions(defaultOptions, customOptions) {
|
207
|
+
return Object.assign({}, defaultOptions, customOptions)
|
208
|
+
}
|
209
|
+
|
210
|
+
hexToRgba(hex, alpha) {
|
211
|
+
const r = parseInt(hex.slice(1, 3), 16)
|
212
|
+
const g = parseInt(hex.slice(3, 5), 16)
|
213
|
+
const b = parseInt(hex.slice(5, 7), 16)
|
214
|
+
return `rgba(${r}, ${g}, ${b}, ${alpha})`
|
215
|
+
}
|
216
|
+
}
|
@@ -0,0 +1,118 @@
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
2
|
+
|
3
|
+
// Connects to data-controller="collapsible-filters"
|
4
|
+
export default class extends Controller {
|
5
|
+
static targets = ["content", "icon"]
|
6
|
+
static values = {
|
7
|
+
expanded: { type: Boolean, default: true }
|
8
|
+
}
|
9
|
+
|
10
|
+
connect() {
|
11
|
+
// Load saved state
|
12
|
+
this.loadState()
|
13
|
+
|
14
|
+
// Set initial state
|
15
|
+
if (this.expandedValue) {
|
16
|
+
this.expand(false) // Don't animate on initial load
|
17
|
+
} else {
|
18
|
+
this.collapse(false) // Don't animate on initial load
|
19
|
+
}
|
20
|
+
|
21
|
+
// Ensure icon has correct initial rotation
|
22
|
+
this.updateIconRotation()
|
23
|
+
}
|
24
|
+
|
25
|
+
toggle(event) {
|
26
|
+
event.preventDefault()
|
27
|
+
|
28
|
+
if (this.expandedValue) {
|
29
|
+
this.collapse(true)
|
30
|
+
} else {
|
31
|
+
this.expand(true)
|
32
|
+
}
|
33
|
+
}
|
34
|
+
|
35
|
+
expand(animate = true) {
|
36
|
+
this.expandedValue = true
|
37
|
+
|
38
|
+
if (animate) {
|
39
|
+
// Animate expansion
|
40
|
+
this.contentTarget.style.maxHeight = this.contentTarget.scrollHeight + "px"
|
41
|
+
this.contentTarget.classList.remove("opacity-0")
|
42
|
+
this.contentTarget.classList.add("opacity-100")
|
43
|
+
} else {
|
44
|
+
// Instant expansion
|
45
|
+
this.contentTarget.style.maxHeight = "none"
|
46
|
+
this.contentTarget.classList.remove("opacity-0")
|
47
|
+
this.contentTarget.classList.add("opacity-100")
|
48
|
+
}
|
49
|
+
|
50
|
+
// Update icon rotation
|
51
|
+
this.updateIconRotation()
|
52
|
+
|
53
|
+
// Save state
|
54
|
+
this.saveState(true)
|
55
|
+
}
|
56
|
+
|
57
|
+
collapse(animate = true) {
|
58
|
+
this.expandedValue = false
|
59
|
+
|
60
|
+
if (animate) {
|
61
|
+
// Prepare for animation
|
62
|
+
this.contentTarget.style.maxHeight = this.contentTarget.scrollHeight + "px"
|
63
|
+
|
64
|
+
// Force reflow
|
65
|
+
this.contentTarget.offsetHeight
|
66
|
+
|
67
|
+
// Animate collapse
|
68
|
+
this.contentTarget.style.maxHeight = "0px"
|
69
|
+
this.contentTarget.classList.remove("opacity-100")
|
70
|
+
this.contentTarget.classList.add("opacity-0")
|
71
|
+
} else {
|
72
|
+
// Instant collapse
|
73
|
+
this.contentTarget.style.maxHeight = "0px"
|
74
|
+
this.contentTarget.classList.remove("opacity-100")
|
75
|
+
this.contentTarget.classList.add("opacity-0")
|
76
|
+
}
|
77
|
+
|
78
|
+
// Update icon rotation
|
79
|
+
this.updateIconRotation()
|
80
|
+
|
81
|
+
// Save state
|
82
|
+
this.saveState(false)
|
83
|
+
}
|
84
|
+
|
85
|
+
saveState(expanded) {
|
86
|
+
// Save the state for this specific page
|
87
|
+
const key = `filters_expanded_${window.location.pathname}`
|
88
|
+
localStorage.setItem(key, expanded ? "true" : "false")
|
89
|
+
}
|
90
|
+
|
91
|
+
loadState() {
|
92
|
+
// Load saved state for this specific page
|
93
|
+
const key = `filters_expanded_${window.location.pathname}`
|
94
|
+
const savedState = localStorage.getItem(key)
|
95
|
+
|
96
|
+
if (savedState !== null) {
|
97
|
+
this.expandedValue = savedState === "true"
|
98
|
+
}
|
99
|
+
// If no saved state exists, use the default value (true - expanded)
|
100
|
+
}
|
101
|
+
|
102
|
+
updateIconRotation() {
|
103
|
+
if (this.hasIconTarget) {
|
104
|
+
const svg = this.iconTarget.querySelector("svg")
|
105
|
+
if (svg) {
|
106
|
+
if (this.expandedValue) {
|
107
|
+
// Expanded state: icon points down (no rotation)
|
108
|
+
svg.classList.remove("-rotate-90")
|
109
|
+
svg.style.transform = "rotate(0deg)"
|
110
|
+
} else {
|
111
|
+
// Collapsed state: icon points right (rotated -90 degrees)
|
112
|
+
svg.classList.add("-rotate-90")
|
113
|
+
svg.style.transform = "rotate(-90deg)"
|
114
|
+
}
|
115
|
+
}
|
116
|
+
}
|
117
|
+
}
|
118
|
+
}
|
@@ -0,0 +1,64 @@
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
2
|
+
|
3
|
+
export default class extends Controller {
|
4
|
+
static targets = ["modal"]
|
5
|
+
|
6
|
+
connect() {
|
7
|
+
// Show modal with animation
|
8
|
+
this.element.style.opacity = '0'
|
9
|
+
this.element.style.display = 'block'
|
10
|
+
requestAnimationFrame(() => {
|
11
|
+
this.element.style.transition = 'opacity 0.3s ease-out'
|
12
|
+
this.element.style.opacity = '1'
|
13
|
+
})
|
14
|
+
|
15
|
+
// Scale animation for modal
|
16
|
+
this.modalTarget.style.transform = 'scale(0.9)'
|
17
|
+
this.modalTarget.style.transition = 'transform 0.3s ease-out'
|
18
|
+
requestAnimationFrame(() => {
|
19
|
+
this.modalTarget.style.transform = 'scale(1)'
|
20
|
+
})
|
21
|
+
|
22
|
+
// Prevent body scroll
|
23
|
+
document.body.style.overflow = 'hidden'
|
24
|
+
}
|
25
|
+
|
26
|
+
disconnect() {
|
27
|
+
// Restore body scroll
|
28
|
+
document.body.style.overflow = ''
|
29
|
+
}
|
30
|
+
|
31
|
+
confirm() {
|
32
|
+
// Dispatch confirmed event
|
33
|
+
this.dispatch('confirmed')
|
34
|
+
this.close()
|
35
|
+
}
|
36
|
+
|
37
|
+
cancel() {
|
38
|
+
// Dispatch cancelled event
|
39
|
+
this.dispatch('cancelled')
|
40
|
+
this.close()
|
41
|
+
}
|
42
|
+
|
43
|
+
clickOutside(event) {
|
44
|
+
// Close if clicked outside modal content
|
45
|
+
if (event.target === this.element) {
|
46
|
+
this.cancel()
|
47
|
+
}
|
48
|
+
}
|
49
|
+
|
50
|
+
close() {
|
51
|
+
// Hide with animation
|
52
|
+
this.element.style.transition = 'opacity 0.3s ease-out'
|
53
|
+
this.modalTarget.style.transform = 'scale(0.9)'
|
54
|
+
this.element.style.opacity = '0'
|
55
|
+
|
56
|
+
setTimeout(() => {
|
57
|
+
// Clear the modal turbo frame
|
58
|
+
const modalFrame = document.getElementById("modal")
|
59
|
+
if (modalFrame) {
|
60
|
+
modalFrame.innerHTML = ""
|
61
|
+
}
|
62
|
+
}, 300)
|
63
|
+
}
|
64
|
+
}
|