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,121 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
export default class extends Controller {
|
|
4
|
+
static targets = ["input", "preview", "currentFile"]
|
|
5
|
+
|
|
6
|
+
connect() {
|
|
7
|
+
this.setupFileInput()
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
setupFileInput() {
|
|
11
|
+
if (this.hasInputTarget) {
|
|
12
|
+
this.inputTarget.addEventListener('change', this.handleFileChange.bind(this))
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
handleFileChange(event) {
|
|
17
|
+
const files = event.target.files
|
|
18
|
+
if (files.length > 0) {
|
|
19
|
+
this.updatePreview(files)
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
updatePreview(files) {
|
|
24
|
+
if (!this.hasPreviewTarget) return
|
|
25
|
+
|
|
26
|
+
this.previewTarget.innerHTML = ''
|
|
27
|
+
|
|
28
|
+
Array.from(files).forEach(file => {
|
|
29
|
+
const previewElement = this.createPreviewElement(file)
|
|
30
|
+
this.previewTarget.appendChild(previewElement)
|
|
31
|
+
})
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
createPreviewElement(file) {
|
|
35
|
+
const div = document.createElement('div')
|
|
36
|
+
div.className = 'flex items-center p-4 bg-gray-50 border border-gray-200 rounded-lg mb-3'
|
|
37
|
+
|
|
38
|
+
const isImage = file.type.startsWith('image/')
|
|
39
|
+
|
|
40
|
+
div.innerHTML = `
|
|
41
|
+
<div class="mr-4">
|
|
42
|
+
${isImage ?
|
|
43
|
+
`<img src="${URL.createObjectURL(file)}" alt="${file.name}" class="w-16 h-16 object-cover rounded-lg border border-gray-300">` :
|
|
44
|
+
`<div class="flex items-center justify-center w-16 h-16 bg-blue-100 text-blue-600 rounded-lg">
|
|
45
|
+
<svg class="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
46
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
|
|
47
|
+
</svg>
|
|
48
|
+
</div>`
|
|
49
|
+
}
|
|
50
|
+
</div>
|
|
51
|
+
<div class="flex-1">
|
|
52
|
+
<div class="text-sm font-medium text-gray-900">${file.name}</div>
|
|
53
|
+
<div class="text-xs text-gray-500">${this.formatFileSize(file.size)}</div>
|
|
54
|
+
</div>
|
|
55
|
+
<div>
|
|
56
|
+
<button type="button" class="inline-flex items-center p-2 border border-red-300 text-red-700 bg-red-50 hover:bg-red-100 rounded-lg focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500" data-action="click->file#removePreview">
|
|
57
|
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
58
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
|
59
|
+
</svg>
|
|
60
|
+
</button>
|
|
61
|
+
</div>
|
|
62
|
+
`
|
|
63
|
+
|
|
64
|
+
return div
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
removePreview(event) {
|
|
68
|
+
const previewElement = event.target.closest('.flex')
|
|
69
|
+
if (previewElement) {
|
|
70
|
+
previewElement.remove()
|
|
71
|
+
// Also clear the file input
|
|
72
|
+
if (this.hasInputTarget) {
|
|
73
|
+
this.inputTarget.value = ''
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
removeFile(event) {
|
|
79
|
+
// For removing existing files in show component
|
|
80
|
+
const fileElement = event.target.closest('.bg-gray-50')
|
|
81
|
+
if (fileElement && confirm('Are you sure you want to remove this file?')) {
|
|
82
|
+
fileElement.style.opacity = '0.5'
|
|
83
|
+
// You could emit a custom event here to handle actual removal
|
|
84
|
+
this.dispatch('fileRemoved', {
|
|
85
|
+
detail: { element: fileElement }
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
formatFileSize(bytes) {
|
|
91
|
+
if (bytes === 0) return '0 bytes'
|
|
92
|
+
|
|
93
|
+
const k = 1024
|
|
94
|
+
const sizes = ['bytes', 'KB', 'MB', 'GB']
|
|
95
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
|
96
|
+
|
|
97
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Drag and drop support
|
|
101
|
+
dragOver(event) {
|
|
102
|
+
event.preventDefault()
|
|
103
|
+
event.currentTarget.classList.add('border-blue-500', 'bg-blue-50')
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
dragLeave(event) {
|
|
107
|
+
event.preventDefault()
|
|
108
|
+
event.currentTarget.classList.remove('border-blue-500', 'bg-blue-50')
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
drop(event) {
|
|
112
|
+
event.preventDefault()
|
|
113
|
+
event.currentTarget.classList.remove('border-blue-500', 'bg-blue-50')
|
|
114
|
+
|
|
115
|
+
const files = event.dataTransfer.files
|
|
116
|
+
if (files.length > 0 && this.hasInputTarget) {
|
|
117
|
+
// Set the files to the input (note: this is limited in some browsers)
|
|
118
|
+
this.updatePreview(files)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
export default class extends Controller {
|
|
4
|
+
static targets = ["tabButton", "tabPanel", "mobileButtonText"]
|
|
5
|
+
|
|
6
|
+
connect() {
|
|
7
|
+
this.showFirstTab()
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
switchTab(event) {
|
|
11
|
+
const clickedButton = event.currentTarget
|
|
12
|
+
const tabId = clickedButton.dataset.tabId
|
|
13
|
+
|
|
14
|
+
this.hideAllTabs()
|
|
15
|
+
this.deactivateAllButtons()
|
|
16
|
+
|
|
17
|
+
this.showTab(tabId)
|
|
18
|
+
this.activateButton(clickedButton)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Emit custom event when dropdown item is clicked
|
|
22
|
+
emitTabSelection(event) {
|
|
23
|
+
event.preventDefault()
|
|
24
|
+
event.stopPropagation()
|
|
25
|
+
|
|
26
|
+
const tabId = event.currentTarget.dataset.tabId
|
|
27
|
+
const tabLabel = event.currentTarget.dataset.tabLabel
|
|
28
|
+
|
|
29
|
+
window.dispatchEvent(new CustomEvent('tab:selected', {
|
|
30
|
+
detail: { tabId, tabLabel }
|
|
31
|
+
}))
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Handle the custom event
|
|
35
|
+
handleTabSelection(event) {
|
|
36
|
+
const { tabId, tabLabel } = event.detail
|
|
37
|
+
|
|
38
|
+
this.hideAllTabs()
|
|
39
|
+
this.deactivateAllButtons()
|
|
40
|
+
|
|
41
|
+
this.showTab(tabId)
|
|
42
|
+
|
|
43
|
+
// Update mobile button text
|
|
44
|
+
if (this.hasMobileButtonTextTarget) {
|
|
45
|
+
this.mobileButtonTextTarget.textContent = tabLabel
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Update desktop tab button state
|
|
49
|
+
const matchingButton = this.tabButtonTargets.find(button =>
|
|
50
|
+
button.dataset.tabId === tabId
|
|
51
|
+
)
|
|
52
|
+
if (matchingButton) {
|
|
53
|
+
this.activateButton(matchingButton)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
showFirstTab() {
|
|
58
|
+
if (this.tabButtonTargets.length > 0 && this.tabPanelTargets.length > 0) {
|
|
59
|
+
const firstButton = this.tabButtonTargets[0]
|
|
60
|
+
const firstTabId = firstButton.dataset.tabId
|
|
61
|
+
|
|
62
|
+
this.activateButton(firstButton)
|
|
63
|
+
this.showTab(firstTabId)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
hideAllTabs() {
|
|
68
|
+
this.tabPanelTargets.forEach(panel => {
|
|
69
|
+
panel.classList.add('hidden')
|
|
70
|
+
panel.classList.remove('block')
|
|
71
|
+
})
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
showTab(tabId) {
|
|
75
|
+
const targetPanel = this.tabPanelTargets.find(panel =>
|
|
76
|
+
panel.id === `${tabId}-panel`
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
if (targetPanel) {
|
|
80
|
+
targetPanel.classList.remove('hidden')
|
|
81
|
+
targetPanel.classList.add('block')
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
deactivateAllButtons() {
|
|
86
|
+
this.tabButtonTargets.forEach(button => {
|
|
87
|
+
// Remove active classes
|
|
88
|
+
button.classList.remove('text-blue-600', 'border-blue-600')
|
|
89
|
+
// Add inactive classes
|
|
90
|
+
button.classList.add('text-gray-500', 'border-transparent')
|
|
91
|
+
})
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
activateButton(button) {
|
|
95
|
+
// Remove inactive classes
|
|
96
|
+
button.classList.remove('text-gray-500', 'border-transparent')
|
|
97
|
+
// Add active classes
|
|
98
|
+
button.classList.add('text-blue-600', 'border-blue-600')
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
import { get } from "@rails/request.js"
|
|
3
|
+
|
|
4
|
+
export default class extends Controller {
|
|
5
|
+
static targets = ["search"]
|
|
6
|
+
static values = {
|
|
7
|
+
url: String
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
connect() {
|
|
11
|
+
this.searchTimeout = null
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
disconnect() {
|
|
15
|
+
if (this.searchTimeout) {
|
|
16
|
+
clearTimeout(this.searchTimeout)
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Handle search input with debouncing
|
|
21
|
+
search(event) {
|
|
22
|
+
const query = event.target.value.trim()
|
|
23
|
+
|
|
24
|
+
// Clear previous timeout
|
|
25
|
+
if (this.searchTimeout) {
|
|
26
|
+
clearTimeout(this.searchTimeout)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Debounce search requests
|
|
30
|
+
this.searchTimeout = setTimeout(() => {
|
|
31
|
+
if (query.length >= 2) {
|
|
32
|
+
this.performSearch(query)
|
|
33
|
+
} else {
|
|
34
|
+
this.clearResults()
|
|
35
|
+
}
|
|
36
|
+
}, 300)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Perform search using Request.js - Turbo Stream will handle DOM updates
|
|
40
|
+
async performSearch(query) {
|
|
41
|
+
const response = await get(this.urlValue, {
|
|
42
|
+
query: { q: query },
|
|
43
|
+
responseKind: "turbo-stream"
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Select an item - Turbo Stream will handle DOM updates
|
|
48
|
+
async selectItem(event) {
|
|
49
|
+
const itemId = event.params.id
|
|
50
|
+
|
|
51
|
+
const response = await get(`${this.urlValue}/select`, {
|
|
52
|
+
query: { id: itemId },
|
|
53
|
+
responseKind: "turbo-stream"
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
// Clear search input after selection
|
|
57
|
+
this.searchTarget.value = ''
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Remove an item - Turbo Stream will handle DOM updates
|
|
61
|
+
async removeItem(event) {
|
|
62
|
+
const itemId = event.params.id
|
|
63
|
+
|
|
64
|
+
const response = await get(`${this.urlValue}/remove`, {
|
|
65
|
+
query: { id: itemId },
|
|
66
|
+
responseKind: "turbo-stream"
|
|
67
|
+
})
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Clear search results - Turbo Stream will handle this
|
|
71
|
+
clearResults() {
|
|
72
|
+
get(`${this.urlValue}/clear`, {
|
|
73
|
+
responseKind: "turbo-stream"
|
|
74
|
+
})
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
import { get } from "@rails/request.js"
|
|
3
|
+
|
|
4
|
+
// Connects to data-controller="infinite-scroll"
|
|
5
|
+
export default class extends Controller {
|
|
6
|
+
static targets = ["loading", "loadMore", "end"]
|
|
7
|
+
static values = {
|
|
8
|
+
url: String,
|
|
9
|
+
hasMore: Boolean,
|
|
10
|
+
threshold: { type: Number, default: 100 } // Distance from bottom to trigger load
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
connect() {
|
|
14
|
+
this.isLoading = false
|
|
15
|
+
this.setupIntersectionObserver()
|
|
16
|
+
this.initializeUI()
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Called when the element is replaced by turbo stream
|
|
20
|
+
urlValueChanged() {
|
|
21
|
+
if (this.observer && this.sentinel) {
|
|
22
|
+
// Re-setup observer with new URL
|
|
23
|
+
this.setupIntersectionObserver()
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
hasMoreValueChanged() {
|
|
28
|
+
if (!this.hasMoreValue) {
|
|
29
|
+
this.showEndMessage()
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
disconnect() {
|
|
34
|
+
if (this.observer) {
|
|
35
|
+
this.observer.disconnect()
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
setupIntersectionObserver() {
|
|
40
|
+
// Clean up existing observer and sentinel
|
|
41
|
+
if (this.observer) {
|
|
42
|
+
this.observer.disconnect()
|
|
43
|
+
}
|
|
44
|
+
if (this.sentinel) {
|
|
45
|
+
this.sentinel.remove()
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Create a sentinel element at the bottom to trigger infinite scroll
|
|
49
|
+
this.sentinel = document.createElement('div')
|
|
50
|
+
this.sentinel.classList.add('infinite-scroll-sentinel')
|
|
51
|
+
this.sentinel.style.height = '1px'
|
|
52
|
+
this.element.appendChild(this.sentinel)
|
|
53
|
+
|
|
54
|
+
// Set up intersection observer
|
|
55
|
+
this.observer = new IntersectionObserver(
|
|
56
|
+
(entries) => {
|
|
57
|
+
entries.forEach(entry => {
|
|
58
|
+
if (entry.isIntersecting && this.hasMoreValue && !this.isLoading) {
|
|
59
|
+
this.loadNextPage()
|
|
60
|
+
}
|
|
61
|
+
})
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
rootMargin: `${this.thresholdValue}px`
|
|
65
|
+
}
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
this.observer.observe(this.sentinel)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
initializeUI() {
|
|
72
|
+
// Initially hide the loading spinner until actually loading
|
|
73
|
+
this.hideLoading()
|
|
74
|
+
|
|
75
|
+
// Show the load more button as the primary interaction
|
|
76
|
+
if (this.hasLoadMoreTarget) {
|
|
77
|
+
this.loadMoreTarget.style.display = 'block'
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async loadMore(event) {
|
|
82
|
+
// Handle manual "Load More" button clicks
|
|
83
|
+
event.preventDefault()
|
|
84
|
+
if (!this.isLoading && this.hasMoreValue) {
|
|
85
|
+
await this.loadNextPage()
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async loadNextPage() {
|
|
90
|
+
if (this.isLoading || !this.hasMoreValue) {
|
|
91
|
+
return
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
console.log('Loading next page:', this.urlValue)
|
|
95
|
+
this.isLoading = true
|
|
96
|
+
this.showLoading()
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
const response = await get(this.urlValue, {
|
|
100
|
+
responseKind: "turbo-stream"
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
if (response.ok) {
|
|
104
|
+
// Turbo streams will be automatically processed
|
|
105
|
+
// Dispatch event to let other controllers know more content was loaded
|
|
106
|
+
this.dispatch("loaded", { detail: { url: this.urlValue } })
|
|
107
|
+
console.log('Loaded successfully, new state will be:', {
|
|
108
|
+
currentUrl: this.urlValue,
|
|
109
|
+
hasMore: this.hasMoreValue
|
|
110
|
+
})
|
|
111
|
+
} else {
|
|
112
|
+
// Let the server handle error responses via turbo streams
|
|
113
|
+
this.dispatch("error", { detail: { response } })
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
} catch (error) {
|
|
117
|
+
console.error('Error loading more content:', error)
|
|
118
|
+
// Dispatch error event so server can handle it
|
|
119
|
+
this.dispatch("error", { detail: { error } })
|
|
120
|
+
} finally {
|
|
121
|
+
this.isLoading = false
|
|
122
|
+
this.hideLoading()
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
showLoading() {
|
|
127
|
+
if (this.hasLoadingTarget) {
|
|
128
|
+
this.loadingTarget.classList.remove('hidden')
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
hideLoading() {
|
|
133
|
+
if (this.hasLoadingTarget) {
|
|
134
|
+
this.loadingTarget.classList.add('hidden')
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Called by turbo streams to update pagination state
|
|
139
|
+
updateState(event) {
|
|
140
|
+
const { url, hasMore } = event.detail
|
|
141
|
+
this.urlValue = url
|
|
142
|
+
this.hasMoreValue = hasMore
|
|
143
|
+
|
|
144
|
+
if (!hasMore) {
|
|
145
|
+
this.showEndMessage()
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Debug method to log current state
|
|
150
|
+
logState() {
|
|
151
|
+
console.log('InfiniteScroll State:', {
|
|
152
|
+
urlValue: this.urlValue,
|
|
153
|
+
hasMoreValue: this.hasMoreValue,
|
|
154
|
+
isLoading: this.isLoading
|
|
155
|
+
})
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
showEndMessage() {
|
|
159
|
+
if (this.hasEndTarget) {
|
|
160
|
+
this.endTarget.style.display = 'block'
|
|
161
|
+
}
|
|
162
|
+
// Remove sentinel since we're done
|
|
163
|
+
if (this.sentinel && this.observer) {
|
|
164
|
+
this.observer.unobserve(this.sentinel)
|
|
165
|
+
this.sentinel.remove()
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
retry(event) {
|
|
170
|
+
event.preventDefault()
|
|
171
|
+
this.hideLoading()
|
|
172
|
+
setTimeout(() => this.loadNextPage(), 100)
|
|
173
|
+
}
|
|
174
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
import Swal from "sweetalert2"
|
|
3
|
+
|
|
4
|
+
export default class extends Controller {
|
|
5
|
+
static values = {
|
|
6
|
+
type: String, // 'danger', 'warning', 'confirm', 'loading', 'success'
|
|
7
|
+
title: String,
|
|
8
|
+
message: String,
|
|
9
|
+
confirmText: String,
|
|
10
|
+
cancelText: String
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
show() {
|
|
14
|
+
const alertType = this.typeValue || 'confirm'
|
|
15
|
+
|
|
16
|
+
switch(alertType) {
|
|
17
|
+
case 'danger':
|
|
18
|
+
this.showDangerAlert()
|
|
19
|
+
break
|
|
20
|
+
case 'warning':
|
|
21
|
+
this.showWarningAlert()
|
|
22
|
+
break
|
|
23
|
+
case 'loading':
|
|
24
|
+
this.showLoadingAlert()
|
|
25
|
+
break
|
|
26
|
+
case 'success':
|
|
27
|
+
this.showSuccessAlert()
|
|
28
|
+
break
|
|
29
|
+
default:
|
|
30
|
+
this.showConfirmAlert()
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
showConfirmAlert() {
|
|
35
|
+
Swal.fire({
|
|
36
|
+
title: this.titleValue || 'Confirm',
|
|
37
|
+
text: this.messageValue || 'Are you sure?',
|
|
38
|
+
icon: 'question',
|
|
39
|
+
showCancelButton: true,
|
|
40
|
+
confirmButtonText: this.confirmTextValue || 'Yes',
|
|
41
|
+
cancelButtonText: this.cancelTextValue || 'Cancel',
|
|
42
|
+
reverseButtons: true,
|
|
43
|
+
...this.getIOSStyles()
|
|
44
|
+
}).then((result) => {
|
|
45
|
+
if (result.isConfirmed) {
|
|
46
|
+
this.dispatch('confirmed', { detail: { type: 'confirm' } })
|
|
47
|
+
} else {
|
|
48
|
+
this.dispatch('cancelled', { detail: { type: 'confirm' } })
|
|
49
|
+
}
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
showDangerAlert() {
|
|
54
|
+
Swal.fire({
|
|
55
|
+
title: this.titleValue || 'Danger',
|
|
56
|
+
text: this.messageValue || 'This action cannot be undone.',
|
|
57
|
+
icon: 'error',
|
|
58
|
+
showCancelButton: true,
|
|
59
|
+
confirmButtonText: this.confirmTextValue || 'Delete',
|
|
60
|
+
cancelButtonText: this.cancelTextValue || 'Cancel',
|
|
61
|
+
reverseButtons: true,
|
|
62
|
+
...this.getIOSStyles()
|
|
63
|
+
}).then((result) => {
|
|
64
|
+
if (result.isConfirmed) {
|
|
65
|
+
this.dispatch('confirmed', { detail: { type: 'danger' } })
|
|
66
|
+
} else {
|
|
67
|
+
this.dispatch('cancelled', { detail: { type: 'danger' } })
|
|
68
|
+
}
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
showWarningAlert() {
|
|
73
|
+
Swal.fire({
|
|
74
|
+
title: this.titleValue || 'Warning',
|
|
75
|
+
text: this.messageValue || 'Please review before continuing.',
|
|
76
|
+
icon: 'warning',
|
|
77
|
+
showCancelButton: true,
|
|
78
|
+
confirmButtonText: this.confirmTextValue || 'Continue',
|
|
79
|
+
cancelButtonText: this.cancelTextValue || 'Cancel',
|
|
80
|
+
reverseButtons: true,
|
|
81
|
+
...this.getIOSStyles()
|
|
82
|
+
}).then((result) => {
|
|
83
|
+
if (result.isConfirmed) {
|
|
84
|
+
this.dispatch('confirmed', { detail: { type: 'warning' } })
|
|
85
|
+
} else {
|
|
86
|
+
this.dispatch('cancelled', { detail: { type: 'warning' } })
|
|
87
|
+
}
|
|
88
|
+
})
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
showLoadingAlert() {
|
|
92
|
+
Swal.fire({
|
|
93
|
+
title: this.titleValue || 'Loading...',
|
|
94
|
+
text: this.messageValue || 'Please wait...',
|
|
95
|
+
allowOutsideClick: false,
|
|
96
|
+
allowEscapeKey: false,
|
|
97
|
+
showConfirmButton: false,
|
|
98
|
+
didOpen: () => {
|
|
99
|
+
Swal.showLoading()
|
|
100
|
+
},
|
|
101
|
+
...this.getIOSStyles()
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
this.dispatch('loading', { detail: { type: 'loading' } })
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
showSuccessAlert() {
|
|
108
|
+
Swal.fire({
|
|
109
|
+
title: this.titleValue || 'Success',
|
|
110
|
+
text: this.messageValue || 'Operation completed successfully.',
|
|
111
|
+
icon: 'success',
|
|
112
|
+
confirmButtonText: this.confirmTextValue || 'OK',
|
|
113
|
+
timer: 3000,
|
|
114
|
+
timerProgressBar: true,
|
|
115
|
+
...this.getIOSStyles()
|
|
116
|
+
}).then(() => {
|
|
117
|
+
this.dispatch('confirmed', { detail: { type: 'success' } })
|
|
118
|
+
})
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
getIOSStyles() {
|
|
122
|
+
return {
|
|
123
|
+
buttonsStyling: false,
|
|
124
|
+
width: '100%',
|
|
125
|
+
padding: '0',
|
|
126
|
+
background: '#ffffff',
|
|
127
|
+
backdrop: 'rgba(0,0,0,0.4)',
|
|
128
|
+
allowOutsideClick: true,
|
|
129
|
+
allowEscapeKey: true,
|
|
130
|
+
position: 'bottom',
|
|
131
|
+
showClass: {
|
|
132
|
+
popup: 'swal2-ios-bottom-show',
|
|
133
|
+
backdrop: 'swal2-backdrop-show'
|
|
134
|
+
},
|
|
135
|
+
hideClass: {
|
|
136
|
+
popup: 'swal2-ios-bottom-hide',
|
|
137
|
+
backdrop: 'swal2-backdrop-hide'
|
|
138
|
+
},
|
|
139
|
+
customClass: {
|
|
140
|
+
popup: 'swal2-ios-bottom-popup',
|
|
141
|
+
title: 'swal2-ios-bottom-title',
|
|
142
|
+
htmlContainer: 'swal2-ios-bottom-content',
|
|
143
|
+
actions: 'swal2-ios-bottom-actions',
|
|
144
|
+
confirmButton: 'swal2-ios-bottom-confirm-button',
|
|
145
|
+
cancelButton: 'swal2-ios-bottom-cancel-button'
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Static helper methods
|
|
151
|
+
static showConfirm(title, message, confirmText = 'Yes', cancelText = 'Cancel') {
|
|
152
|
+
return this.createAndShow('confirm', title, message, confirmText, cancelText)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
static showDanger(title, message, confirmText = 'Delete', cancelText = 'Cancel') {
|
|
156
|
+
return this.createAndShow('danger', title, message, confirmText, cancelText)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
static showWarning(title, message, confirmText = 'Continue', cancelText = 'Cancel') {
|
|
160
|
+
return this.createAndShow('warning', title, message, confirmText, cancelText)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
static showLoading(title = 'Loading...', message = 'Please wait...') {
|
|
164
|
+
return this.createAndShow('loading', title, message)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
static showSuccess(title = 'Success', message = 'Operation completed successfully.', confirmText = 'OK') {
|
|
168
|
+
return this.createAndShow('success', title, message, confirmText)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
static createAndShow(type, title, message, confirmText, cancelText) {
|
|
172
|
+
const element = document.createElement('div')
|
|
173
|
+
element.dataset.controller = 'ios-alert'
|
|
174
|
+
element.dataset.iosAlertTypeValue = type
|
|
175
|
+
element.dataset.iosAlertTitleValue = title
|
|
176
|
+
element.dataset.iosAlertMessageValue = message
|
|
177
|
+
if (confirmText) element.dataset.iosAlertConfirmTextValue = confirmText
|
|
178
|
+
if (cancelText) element.dataset.iosAlertCancelTextValue = cancelText
|
|
179
|
+
|
|
180
|
+
document.body.appendChild(element)
|
|
181
|
+
|
|
182
|
+
// Trigger the show method
|
|
183
|
+
const controller = this.application.getControllerForElementAndIdentifier(element, 'ios-alert')
|
|
184
|
+
if (controller) {
|
|
185
|
+
controller.show()
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return element
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Method to close loading alert
|
|
192
|
+
static closeLoading() {
|
|
193
|
+
Swal.close()
|
|
194
|
+
}
|
|
195
|
+
}
|