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
|
+
}
|