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.
Files changed (203) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +28 -0
  4. data/Rakefile +8 -0
  5. data/app/assets/builds/easy_admin.base.js +43505 -0
  6. data/app/assets/builds/easy_admin.base.js.map +7 -0
  7. data/app/assets/builds/easy_admin.css +6141 -0
  8. data/app/assets/config/easy_admin_manifest.js +1 -0
  9. data/app/assets/images/jsoneditor-icons.svg +749 -0
  10. data/app/assets/stylesheets/easy_admin/application.tailwind.css +390 -0
  11. data/app/components/easy_admin/base_component.rb +35 -0
  12. data/app/components/easy_admin/batch_action_bar_component.rb +125 -0
  13. data/app/components/easy_admin/batch_action_form_component.rb +124 -0
  14. data/app/components/easy_admin/combined_filters_component.rb +232 -0
  15. data/app/components/easy_admin/confirmation_modal_component.rb +61 -0
  16. data/app/components/easy_admin/context_menu_component.rb +161 -0
  17. data/app/components/easy_admin/dashboards/base_card_component.rb +152 -0
  18. data/app/components/easy_admin/dashboards/card_error_component.rb +23 -0
  19. data/app/components/easy_admin/dashboards/card_factory.rb +90 -0
  20. data/app/components/easy_admin/dashboards/card_stream_component.rb +22 -0
  21. data/app/components/easy_admin/dashboards/cards/base_card_component.rb +54 -0
  22. data/app/components/easy_admin/dashboards/cards/chart_card_component.rb +175 -0
  23. data/app/components/easy_admin/dashboards/cards/custom_card_component.rb +50 -0
  24. data/app/components/easy_admin/dashboards/cards/metric_card_component.rb +164 -0
  25. data/app/components/easy_admin/dashboards/cards/table_card_component.rb +148 -0
  26. data/app/components/easy_admin/dashboards/chart_card_component.rb +44 -0
  27. data/app/components/easy_admin/dashboards/metric_card_component.rb +56 -0
  28. data/app/components/easy_admin/dashboards/refresh_stream_component.rb +279 -0
  29. data/app/components/easy_admin/dashboards/show_component.rb +163 -0
  30. data/app/components/easy_admin/dashboards/table_card_component.rb +52 -0
  31. data/app/components/easy_admin/date_picker_component.rb +188 -0
  32. data/app/components/easy_admin/fields/base_component.rb +101 -0
  33. data/app/components/easy_admin/fields/belongs_to_edit_modal_component.rb +117 -0
  34. data/app/components/easy_admin/fields/form/belongs_to_component.rb +82 -0
  35. data/app/components/easy_admin/fields/form/boolean_component.rb +100 -0
  36. data/app/components/easy_admin/fields/form/date_component.rb +55 -0
  37. data/app/components/easy_admin/fields/form/datetime_component.rb +55 -0
  38. data/app/components/easy_admin/fields/form/email_component.rb +55 -0
  39. data/app/components/easy_admin/fields/form/file_component.rb +190 -0
  40. data/app/components/easy_admin/fields/form/has_many_component.rb +416 -0
  41. data/app/components/easy_admin/fields/form/json_component.rb +81 -0
  42. data/app/components/easy_admin/fields/form/number_component.rb +55 -0
  43. data/app/components/easy_admin/fields/form/select_component.rb +326 -0
  44. data/app/components/easy_admin/fields/form/text_component.rb +55 -0
  45. data/app/components/easy_admin/fields/form/textarea_component.rb +54 -0
  46. data/app/components/easy_admin/fields/index/belongs_to_component.rb +93 -0
  47. data/app/components/easy_admin/fields/index/boolean_component.rb +29 -0
  48. data/app/components/easy_admin/fields/index/date_component.rb +13 -0
  49. data/app/components/easy_admin/fields/index/datetime_component.rb +13 -0
  50. data/app/components/easy_admin/fields/index/email_component.rb +24 -0
  51. data/app/components/easy_admin/fields/index/filters/base_component.rb +48 -0
  52. data/app/components/easy_admin/fields/index/filters/boolean_component.rb +96 -0
  53. data/app/components/easy_admin/fields/index/filters/date_component.rb +182 -0
  54. data/app/components/easy_admin/fields/index/filters/number_component.rb +30 -0
  55. data/app/components/easy_admin/fields/index/filters/select_component.rb +101 -0
  56. data/app/components/easy_admin/fields/index/filters/string_component.rb +32 -0
  57. data/app/components/easy_admin/fields/index/json_component.rb +23 -0
  58. data/app/components/easy_admin/fields/index/number_component.rb +20 -0
  59. data/app/components/easy_admin/fields/index/select_component.rb +25 -0
  60. data/app/components/easy_admin/fields/index/text_component.rb +20 -0
  61. data/app/components/easy_admin/fields/inline_edit_modal_component.rb +135 -0
  62. data/app/components/easy_admin/fields/inline_edit_trigger_component.rb +144 -0
  63. data/app/components/easy_admin/fields/show/belongs_to_component.rb +93 -0
  64. data/app/components/easy_admin/fields/show/boolean_component.rb +21 -0
  65. data/app/components/easy_admin/fields/show/date_component.rb +13 -0
  66. data/app/components/easy_admin/fields/show/datetime_component.rb +13 -0
  67. data/app/components/easy_admin/fields/show/email_component.rb +19 -0
  68. data/app/components/easy_admin/fields/show/file_component.rb +304 -0
  69. data/app/components/easy_admin/fields/show/has_many_component.rb +192 -0
  70. data/app/components/easy_admin/fields/show/json_component.rb +45 -0
  71. data/app/components/easy_admin/fields/show/number_component.rb +20 -0
  72. data/app/components/easy_admin/fields/show/select_component.rb +25 -0
  73. data/app/components/easy_admin/fields/show/text_component.rb +17 -0
  74. data/app/components/easy_admin/fields/show/textarea_component.rb +26 -0
  75. data/app/components/easy_admin/filters_component.rb +120 -0
  76. data/app/components/easy_admin/form_tabs_component.rb +166 -0
  77. data/app/components/easy_admin/infinite_scroll_component.rb +82 -0
  78. data/app/components/easy_admin/lazy_chart_card_component.rb +128 -0
  79. data/app/components/easy_admin/lazy_metric_card_component.rb +76 -0
  80. data/app/components/easy_admin/modal_frame_component.rb +26 -0
  81. data/app/components/easy_admin/navbar_component.rb +226 -0
  82. data/app/components/easy_admin/notification_component.rb +83 -0
  83. data/app/components/easy_admin/pagination_component.rb +188 -0
  84. data/app/components/easy_admin/quick_filters_component.rb +65 -0
  85. data/app/components/easy_admin/resource_pagination_component.rb +14 -0
  86. data/app/components/easy_admin/resources/index_component.rb +211 -0
  87. data/app/components/easy_admin/resources/index_frame_component.rb +88 -0
  88. data/app/components/easy_admin/resources/show_page_actions_component.rb +324 -0
  89. data/app/components/easy_admin/resources/table_cell_component.rb +145 -0
  90. data/app/components/easy_admin/resources/table_component.rb +206 -0
  91. data/app/components/easy_admin/resources/table_row_component.rb +160 -0
  92. data/app/components/easy_admin/row_action_form_component.rb +127 -0
  93. data/app/components/easy_admin/scopes_component.rb +224 -0
  94. data/app/components/easy_admin/settings_sidebar_component.rb +140 -0
  95. data/app/components/easy_admin/show_layout_component.rb +600 -0
  96. data/app/components/easy_admin/sidebar_component.rb +174 -0
  97. data/app/components/easy_admin/turbo/response_component.rb +40 -0
  98. data/app/components/easy_admin/turbo/stream_component.rb +28 -0
  99. data/app/controllers/easy_admin/application_controller.rb +66 -0
  100. data/app/controllers/easy_admin/batch_actions_controller.rb +166 -0
  101. data/app/controllers/easy_admin/confirmation_modal_controller.rb +20 -0
  102. data/app/controllers/easy_admin/dashboard_controller.rb +6 -0
  103. data/app/controllers/easy_admin/dashboards_controller.rb +123 -0
  104. data/app/controllers/easy_admin/passwords_controller.rb +15 -0
  105. data/app/controllers/easy_admin/registrations_controller.rb +52 -0
  106. data/app/controllers/easy_admin/resources_controller.rb +907 -0
  107. data/app/controllers/easy_admin/row_actions_controller.rb +216 -0
  108. data/app/controllers/easy_admin/sessions_controller.rb +32 -0
  109. data/app/controllers/easy_admin/settings_controller.rb +94 -0
  110. data/app/helpers/easy_admin/application_helper.rb +4 -0
  111. data/app/helpers/easy_admin/dashboards_helper.rb +121 -0
  112. data/app/helpers/easy_admin/fields_helper.rb +27 -0
  113. data/app/helpers/easy_admin/pagy_helper.rb +30 -0
  114. data/app/helpers/easy_admin/resources_helper.rb +39 -0
  115. data/app/javascript/easy_admin/application.js +12 -0
  116. data/app/javascript/easy_admin/controllers/batch_modal_controller.js +66 -0
  117. data/app/javascript/easy_admin/controllers/batch_selection_controller.js +223 -0
  118. data/app/javascript/easy_admin/controllers/chart_controller.js +216 -0
  119. data/app/javascript/easy_admin/controllers/collapsible_filters_controller.js +118 -0
  120. data/app/javascript/easy_admin/controllers/confirmation_modal_controller.js +64 -0
  121. data/app/javascript/easy_admin/controllers/context_menu_controller.js +227 -0
  122. data/app/javascript/easy_admin/controllers/date_picker_controller.js +309 -0
  123. data/app/javascript/easy_admin/controllers/dropdown_controller.js +63 -0
  124. data/app/javascript/easy_admin/controllers/event_emitter_controller.js +19 -0
  125. data/app/javascript/easy_admin/controllers/file_controller.js +121 -0
  126. data/app/javascript/easy_admin/controllers/form_tabs_controller.js +100 -0
  127. data/app/javascript/easy_admin/controllers/has_many_search_controller.js +76 -0
  128. data/app/javascript/easy_admin/controllers/infinite_scroll_controller.js +174 -0
  129. data/app/javascript/easy_admin/controllers/ios_alert_controller.js +195 -0
  130. data/app/javascript/easy_admin/controllers/jsoneditor_controller.js +88 -0
  131. data/app/javascript/easy_admin/controllers/modal_controller.js +75 -0
  132. data/app/javascript/easy_admin/controllers/navbar_scroll_controller.js +76 -0
  133. data/app/javascript/easy_admin/controllers/notification_controller.js +48 -0
  134. data/app/javascript/easy_admin/controllers/row_action_controller.js +124 -0
  135. data/app/javascript/easy_admin/controllers/row_modal_controller.js +59 -0
  136. data/app/javascript/easy_admin/controllers/select_field_controller.js +618 -0
  137. data/app/javascript/easy_admin/controllers/settings_button_controller.js +8 -0
  138. data/app/javascript/easy_admin/controllers/settings_sidebar_controller.js +186 -0
  139. data/app/javascript/easy_admin/controllers/sidebar_controller.js +102 -0
  140. data/app/javascript/easy_admin/controllers/sidebar_mobile_controller.js +23 -0
  141. data/app/javascript/easy_admin/controllers/sidebar_nav_controller.js +96 -0
  142. data/app/javascript/easy_admin/controllers/table_controller.js +28 -0
  143. data/app/javascript/easy_admin/controllers/table_row_controller.js +16 -0
  144. data/app/javascript/easy_admin/controllers/toggle_switch_controller.js +22 -0
  145. data/app/javascript/easy_admin/controllers/turbo_stream_redirect.js +9 -0
  146. data/app/javascript/easy_admin/controllers.js +54 -0
  147. data/app/javascript/easy_admin.base.js +4 -0
  148. data/app/models/easy_admin/admin_user.rb +53 -0
  149. data/app/models/easy_admin/application_record.rb +5 -0
  150. data/app/views/easy_admin/dashboard/index.html.erb +3 -0
  151. data/app/views/easy_admin/dashboards/show.html.erb +7 -0
  152. data/app/views/easy_admin/passwords/edit.html.erb +42 -0
  153. data/app/views/easy_admin/passwords/new.html.erb +41 -0
  154. data/app/views/easy_admin/registrations/new.html.erb +65 -0
  155. data/app/views/easy_admin/resources/_redirect.turbo_stream.erb +3 -0
  156. data/app/views/easy_admin/resources/_table_rows.html.erb +46 -0
  157. data/app/views/easy_admin/resources/edit.html.erb +151 -0
  158. data/app/views/easy_admin/resources/index.html.erb +12 -0
  159. data/app/views/easy_admin/resources/index.turbo_stream.erb +139 -0
  160. data/app/views/easy_admin/resources/index_frame.html.erb +142 -0
  161. data/app/views/easy_admin/resources/new.html.erb +100 -0
  162. data/app/views/easy_admin/resources/show.html.erb +31 -0
  163. data/app/views/easy_admin/sessions/new.html.erb +55 -0
  164. data/app/views/easy_admin/settings/_form.html.erb +51 -0
  165. data/app/views/easy_admin/settings/index.html.erb +53 -0
  166. data/app/views/layouts/easy_admin/application.html.erb +48 -0
  167. data/app/views/layouts/easy_admin/auth.html.erb +34 -0
  168. data/config/initializers/easy_admin_card_factory.rb +27 -0
  169. data/config/initializers/pagy.rb +15 -0
  170. data/config/initializers/rack_mini_profiler.rb +67 -0
  171. data/config/routes.rb +70 -0
  172. data/db/migrate/20250101000001_create_easy_admin_admin_users.rb +45 -0
  173. data/lib/easy-admin.rb +32 -0
  174. data/lib/easy_admin/action.rb +159 -0
  175. data/lib/easy_admin/batch_action.rb +134 -0
  176. data/lib/easy_admin/configuration.rb +75 -0
  177. data/lib/easy_admin/dashboard.rb +110 -0
  178. data/lib/easy_admin/dashboard_registry.rb +30 -0
  179. data/lib/easy_admin/delete_action.rb +22 -0
  180. data/lib/easy_admin/engine.rb +54 -0
  181. data/lib/easy_admin/field.rb +118 -0
  182. data/lib/easy_admin/resource.rb +806 -0
  183. data/lib/easy_admin/resource_registry.rb +22 -0
  184. data/lib/easy_admin/types/json_type.rb +25 -0
  185. data/lib/easy_admin/version.rb +3 -0
  186. data/lib/generators/easy_admin/auth_generator.rb +69 -0
  187. data/lib/generators/easy_admin/card/card_generator.rb +94 -0
  188. data/lib/generators/easy_admin/card/templates/card_component.rb.erb +127 -0
  189. data/lib/generators/easy_admin/card/templates/card_component_spec.rb.erb +122 -0
  190. data/lib/generators/easy_admin/install/templates/easy_admin.rb +31 -0
  191. data/lib/generators/easy_admin/install_generator.rb +25 -0
  192. data/lib/generators/easy_admin/rbac/rbac_generator.rb +244 -0
  193. data/lib/generators/easy_admin/rbac/templates/add_rbac_to_admin_users.rb +23 -0
  194. data/lib/generators/easy_admin/rbac/templates/super_admin.rb +34 -0
  195. data/lib/generators/easy_admin/resource_generator.rb +43 -0
  196. data/lib/generators/easy_admin/templates/AUTH_README +35 -0
  197. data/lib/generators/easy_admin/templates/README +27 -0
  198. data/lib/generators/easy_admin/templates/create_easy_admin_admin_users.rb +45 -0
  199. data/lib/generators/easy_admin/templates/devise.rb +267 -0
  200. data/lib/generators/easy_admin/templates/easy_admin.rb +24 -0
  201. data/lib/generators/easy_admin/templates/resource.rb +29 -0
  202. data/lib/tasks/easy_admin_tasks.rake +4 -0
  203. 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
+ }