solidcrud 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 (70) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +1285 -0
  4. data/app/assets/javascripts/controllers/dashboard_controller.js +96 -0
  5. data/app/assets/javascripts/controllers/modal_controller.js +217 -0
  6. data/app/assets/javascripts/controllers/navigation_controller.js +117 -0
  7. data/app/assets/javascripts/controllers/notification_controller.js +85 -0
  8. data/app/assets/javascripts/controllers/search_controller.js +189 -0
  9. data/app/assets/javascripts/controllers/table_controller.js +272 -0
  10. data/app/assets/javascripts/solidcrud/application.js +9475 -0
  11. data/app/assets/stylesheets/solidcrud/_components.scss +267 -0
  12. data/app/assets/stylesheets/solidcrud/_forms.scss +69 -0
  13. data/app/assets/stylesheets/solidcrud/_layout.scss +149 -0
  14. data/app/assets/stylesheets/solidcrud/_tables.scss +90 -0
  15. data/app/assets/stylesheets/solidcrud/_variables.scss +21 -0
  16. data/app/assets/stylesheets/solidcrud/application.css +10 -0
  17. data/app/assets/stylesheets/solidcrud/application.css.map +1 -0
  18. data/app/assets/stylesheets/solidcrud/application.scss +10 -0
  19. data/app/assets/stylesheets/solidcrud/temp.css.map +1 -0
  20. data/app/assets/stylesheets/solidcrud/temp2.css.map +1 -0
  21. data/app/assets/stylesheets/solidcrud/webfonts/fa-brands-400.ttf +0 -0
  22. data/app/assets/stylesheets/solidcrud/webfonts/fa-brands-400.woff2 +0 -0
  23. data/app/assets/stylesheets/solidcrud/webfonts/fa-regular-400.ttf +0 -0
  24. data/app/assets/stylesheets/solidcrud/webfonts/fa-regular-400.woff2 +0 -0
  25. data/app/assets/stylesheets/solidcrud/webfonts/fa-solid-900.ttf +0 -0
  26. data/app/assets/stylesheets/solidcrud/webfonts/fa-solid-900.woff2 +0 -0
  27. data/app/assets/stylesheets/solidcrud/webfonts/fa-v4compatibility.ttf +0 -0
  28. data/app/assets/stylesheets/solidcrud/webfonts/fa-v4compatibility.woff2 +0 -0
  29. data/app/assets/stylesheets/webfonts/fa-brands-400.ttf +0 -0
  30. data/app/assets/stylesheets/webfonts/fa-brands-400.woff2 +0 -0
  31. data/app/assets/stylesheets/webfonts/fa-regular-400.ttf +0 -0
  32. data/app/assets/stylesheets/webfonts/fa-regular-400.woff2 +0 -0
  33. data/app/assets/stylesheets/webfonts/fa-solid-900.ttf +0 -0
  34. data/app/assets/stylesheets/webfonts/fa-solid-900.woff2 +0 -0
  35. data/app/assets/stylesheets/webfonts/fa-v4compatibility.ttf +0 -0
  36. data/app/assets/stylesheets/webfonts/fa-v4compatibility.woff2 +0 -0
  37. data/app/controllers/solidcrud/admin_controller.rb +215 -0
  38. data/app/controllers/solidcrud/application_controller.rb +19 -0
  39. data/app/controllers/solidcrud/assets_controller.rb +59 -0
  40. data/app/controllers/solidcrud/sessions_controller.rb +84 -0
  41. data/app/helpers/solidcrud/application_helper.rb +153 -0
  42. data/app/javascript/solidcrud/application.js +14 -0
  43. data/app/javascript/solidcrud/controllers/crud_controller.js +64 -0
  44. data/app/javascript/solidcrud/controllers/index.js +33 -0
  45. data/app/views/layouts/solidcrud/application.html.erb +70 -0
  46. data/app/views/solidcrud/admin/edit.html.erb +294 -0
  47. data/app/views/solidcrud/admin/index.html.erb +128 -0
  48. data/app/views/solidcrud/admin/model.html.erb +353 -0
  49. data/app/views/solidcrud/admin/new.html.erb +275 -0
  50. data/app/views/solidcrud/admin/shared/_dashboard_stats.html.erb +49 -0
  51. data/app/views/solidcrud/admin/shared/_edit_form_sidebar.html.erb +9 -0
  52. data/app/views/solidcrud/admin/shared/_flash_messages.html.erb +27 -0
  53. data/app/views/solidcrud/admin/shared/_full_sidebar.html.erb +56 -0
  54. data/app/views/solidcrud/admin/shared/_modal.html.erb +45 -0
  55. data/app/views/solidcrud/admin/shared/_new_form_sidebar.html.erb +6 -0
  56. data/app/views/solidcrud/admin/shared/_record_row.html.erb +35 -0
  57. data/app/views/solidcrud/admin/shared/_records_table.html.erb +85 -0
  58. data/app/views/solidcrud/sessions/new.html.erb +262 -0
  59. data/config/routes.rb +24 -0
  60. data/lib/generators/solidcrud/install/install_generator.rb +21 -0
  61. data/lib/generators/solidcrud/install/templates/INSTALL.md +80 -0
  62. data/lib/generators/solidcrud/install/templates/solidcrud.rb +31 -0
  63. data/lib/generators/solidcrud/install_generator.rb +17 -0
  64. data/lib/generators/solidcrud/templates/solidcrud.rb +4 -0
  65. data/lib/solidcrud/authentication.rb +143 -0
  66. data/lib/solidcrud/configuration.rb +64 -0
  67. data/lib/solidcrud/engine.rb +49 -0
  68. data/lib/solidcrud/version.rb +5 -0
  69. data/lib/solidcrud.rb +10 -0
  70. metadata +177 -0
@@ -0,0 +1,272 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ // Connects to data-controller="table"
4
+ export default class extends Controller {
5
+ static targets = ["row", "checkbox", "selectAll", "column", "header"]
6
+
7
+ connect() {
8
+ console.log("Table controller connected")
9
+ this.setupColumnVisibility()
10
+ this.setupRowSelection()
11
+ }
12
+
13
+ // Column visibility management
14
+ setupColumnVisibility() {
15
+ const modelName = this.getModelName()
16
+ if (!modelName) return
17
+
18
+ // Load saved column preferences
19
+ this.loadColumnPreferences()
20
+
21
+ // Setup column toggle handlers
22
+ this.checkboxTargets.forEach(checkbox => {
23
+ checkbox.addEventListener("change", () => {
24
+ this.updateColumnVisibility()
25
+ this.saveColumnPreferences()
26
+ })
27
+ })
28
+ }
29
+
30
+ loadColumnPreferences() {
31
+ const modelName = this.getModelName()
32
+ const saved = localStorage.getItem(`solidcrud_columns_${modelName}`)
33
+
34
+ if (saved) {
35
+ const visibleColumns = JSON.parse(saved)
36
+ this.checkboxTargets.forEach(checkbox => {
37
+ checkbox.checked = visibleColumns.includes(checkbox.value)
38
+ })
39
+ } else {
40
+ // Default: all columns visible
41
+ this.checkboxTargets.forEach(checkbox => {
42
+ checkbox.checked = true
43
+ })
44
+ }
45
+
46
+ this.updateColumnVisibility()
47
+ }
48
+
49
+ saveColumnPreferences() {
50
+ const modelName = this.getModelName()
51
+ const visibleColumns = this.checkboxTargets
52
+ .filter(checkbox => checkbox.checked)
53
+ .map(checkbox => checkbox.value)
54
+
55
+ localStorage.setItem(`solidcrud_columns_${modelName}`, JSON.stringify(visibleColumns))
56
+ }
57
+
58
+ updateColumnVisibility() {
59
+ const visibleColumns = this.checkboxTargets
60
+ .filter(checkbox => checkbox.checked)
61
+ .map(checkbox => checkbox.value)
62
+
63
+ // Update headers
64
+ this.headerTargets.forEach(header => {
65
+ const column = header.getAttribute("data-column")
66
+ if (visibleColumns.includes(column)) {
67
+ header.classList.remove("hidden")
68
+ } else {
69
+ header.classList.add("hidden")
70
+ }
71
+ })
72
+
73
+ // Update cells
74
+ this.columnTargets.forEach(cell => {
75
+ const column = cell.getAttribute("data-column")
76
+ if (visibleColumns.includes(column)) {
77
+ cell.classList.remove("hidden")
78
+ } else {
79
+ cell.classList.add("hidden")
80
+ }
81
+ })
82
+ }
83
+
84
+ selectAllColumns() {
85
+ this.checkboxTargets.forEach(checkbox => {
86
+ checkbox.checked = true
87
+ })
88
+ this.updateColumnVisibility()
89
+ this.saveColumnPreferences()
90
+ }
91
+
92
+ deselectAllColumns() {
93
+ this.checkboxTargets.forEach(checkbox => {
94
+ checkbox.checked = false
95
+ })
96
+ this.updateColumnVisibility()
97
+ this.saveColumnPreferences()
98
+ }
99
+
100
+ // Row selection management
101
+ setupRowSelection() {
102
+ if (this.hasSelectAllTarget) {
103
+ this.selectAllTarget.addEventListener("change", this.handleSelectAll.bind(this))
104
+ }
105
+
106
+ this.rowTargets.forEach(row => {
107
+ const checkbox = row.querySelector('input[type="checkbox"]')
108
+ if (checkbox) {
109
+ checkbox.addEventListener("change", this.updateSelectAllState.bind(this))
110
+ }
111
+ })
112
+ }
113
+
114
+ handleSelectAll(event) {
115
+ const isChecked = event.target.checked
116
+ this.rowTargets.forEach(row => {
117
+ const checkbox = row.querySelector('input[type="checkbox"]')
118
+ if (checkbox) {
119
+ checkbox.checked = isChecked
120
+ row.classList.toggle("selected", isChecked)
121
+ }
122
+ })
123
+ }
124
+
125
+ updateSelectAllState() {
126
+ if (!this.hasSelectAllTarget) return
127
+
128
+ const checkedBoxes = this.rowTargets.filter(row => {
129
+ const checkbox = row.querySelector('input[type="checkbox"]')
130
+ return checkbox && checkbox.checked
131
+ })
132
+
133
+ const totalBoxes = this.rowTargets.length
134
+
135
+ if (checkedBoxes.length === 0) {
136
+ this.selectAllTarget.indeterminate = false
137
+ this.selectAllTarget.checked = false
138
+ } else if (checkedBoxes.length === totalBoxes) {
139
+ this.selectAllTarget.indeterminate = false
140
+ this.selectAllTarget.checked = true
141
+ } else {
142
+ this.selectAllTarget.indeterminate = true
143
+ this.selectAllTarget.checked = false
144
+ }
145
+ }
146
+
147
+ // Bulk actions
148
+ performBulkAction(action) {
149
+ const selectedIds = this.getSelectedIds()
150
+
151
+ if (selectedIds.length === 0) {
152
+ this.showNotification("Please select items to perform this action.", "warning")
153
+ return
154
+ }
155
+
156
+ switch (action) {
157
+ case "delete":
158
+ this.confirmBulkDelete(selectedIds)
159
+ break
160
+ case "export":
161
+ this.exportSelected(selectedIds)
162
+ break
163
+ default:
164
+ console.log(`Unknown bulk action: ${action}`)
165
+ }
166
+ }
167
+
168
+ getSelectedIds() {
169
+ return this.rowTargets
170
+ .filter(row => {
171
+ const checkbox = row.querySelector('input[type="checkbox"]')
172
+ return checkbox && checkbox.checked
173
+ })
174
+ .map(row => row.getAttribute("data-id"))
175
+ }
176
+
177
+ confirmBulkDelete(ids) {
178
+ const count = ids.length
179
+ const message = `Are you sure you want to delete ${count} ${count === 1 ? "item" : "items"}? This action cannot be undone.`
180
+
181
+ if (confirm(message)) {
182
+ this.deleteSelected(ids)
183
+ }
184
+ }
185
+
186
+ async deleteSelected(ids) {
187
+ try {
188
+ const response = await fetch("/admin/bulk_delete", {
189
+ method: "DELETE",
190
+ headers: {
191
+ "Content-Type": "application/json",
192
+ "X-CSRF-Token": document.querySelector('meta[name="csrf-token"]').content
193
+ },
194
+ body: JSON.stringify({ ids: ids, model: this.getModelName() })
195
+ })
196
+
197
+ if (response.ok) {
198
+ // Remove deleted rows from table
199
+ ids.forEach(id => {
200
+ const row = this.element.querySelector(`[data-id="${id}"]`)
201
+ if (row) {
202
+ row.remove()
203
+ }
204
+ })
205
+
206
+ this.showNotification(`Successfully deleted ${ids.length} items.`, "success")
207
+ } else {
208
+ this.showNotification("Failed to delete selected items.", "error")
209
+ }
210
+ } catch (error) {
211
+ console.error("Bulk delete error:", error)
212
+ this.showNotification("Network error during deletion.", "error")
213
+ }
214
+ }
215
+
216
+ exportSelected(ids) {
217
+ const url = new URL("/admin/export", window.location.origin)
218
+ url.searchParams.set("ids", ids.join(","))
219
+ url.searchParams.set("model", this.getModelName())
220
+
221
+ // Trigger download
222
+ const link = document.createElement("a")
223
+ link.href = url.toString()
224
+ link.download = `${this.getModelName()}_export.csv`
225
+ document.body.appendChild(link)
226
+ link.click()
227
+ document.body.removeChild(link)
228
+ }
229
+
230
+ showNotification(message, type = "info") {
231
+ // Create notification element
232
+ const notification = document.createElement("div")
233
+ notification.className = `fixed top-4 right-4 z-50 p-4 rounded-lg shadow-lg max-w-sm ${
234
+ type === "success" ? "bg-green-500" :
235
+ type === "error" ? "bg-red-500" :
236
+ type === "warning" ? "bg-yellow-500" : "bg-blue-500"
237
+ } text-white`
238
+
239
+ notification.innerHTML = `
240
+ <div class="flex items-center space-x-3">
241
+ <div class="flex-shrink-0">
242
+ <i class="fas ${
243
+ type === "success" ? "fa-check-circle" :
244
+ type === "error" ? "fa-exclamation-circle" :
245
+ type === "warning" ? "fa-exclamation-triangle" : "fa-info-circle"
246
+ }"></i>
247
+ </div>
248
+ <div class="flex-1">
249
+ <p class="font-medium">${message}</p>
250
+ </div>
251
+ <button class="flex-shrink-0 hover:opacity-75" onclick="this.parentElement.parentElement.remove()">
252
+ <i class="fas fa-times"></i>
253
+ </button>
254
+ </div>
255
+ `
256
+
257
+ document.body.appendChild(notification)
258
+
259
+ // Auto-remove after 5 seconds
260
+ setTimeout(() => {
261
+ if (notification.parentNode) {
262
+ notification.remove()
263
+ }
264
+ }, 5000)
265
+ }
266
+
267
+ getModelName() {
268
+ // Extract model name from URL or data attribute
269
+ const urlMatch = window.location.pathname.match(/\/admin\/([^\/]+)/)
270
+ return urlMatch ? urlMatch[1] : ""
271
+ }
272
+ }