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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +1285 -0
- data/app/assets/javascripts/controllers/dashboard_controller.js +96 -0
- data/app/assets/javascripts/controllers/modal_controller.js +217 -0
- data/app/assets/javascripts/controllers/navigation_controller.js +117 -0
- data/app/assets/javascripts/controllers/notification_controller.js +85 -0
- data/app/assets/javascripts/controllers/search_controller.js +189 -0
- data/app/assets/javascripts/controllers/table_controller.js +272 -0
- data/app/assets/javascripts/solidcrud/application.js +9475 -0
- data/app/assets/stylesheets/solidcrud/_components.scss +267 -0
- data/app/assets/stylesheets/solidcrud/_forms.scss +69 -0
- data/app/assets/stylesheets/solidcrud/_layout.scss +149 -0
- data/app/assets/stylesheets/solidcrud/_tables.scss +90 -0
- data/app/assets/stylesheets/solidcrud/_variables.scss +21 -0
- data/app/assets/stylesheets/solidcrud/application.css +10 -0
- data/app/assets/stylesheets/solidcrud/application.css.map +1 -0
- data/app/assets/stylesheets/solidcrud/application.scss +10 -0
- data/app/assets/stylesheets/solidcrud/temp.css.map +1 -0
- data/app/assets/stylesheets/solidcrud/temp2.css.map +1 -0
- data/app/assets/stylesheets/solidcrud/webfonts/fa-brands-400.ttf +0 -0
- data/app/assets/stylesheets/solidcrud/webfonts/fa-brands-400.woff2 +0 -0
- data/app/assets/stylesheets/solidcrud/webfonts/fa-regular-400.ttf +0 -0
- data/app/assets/stylesheets/solidcrud/webfonts/fa-regular-400.woff2 +0 -0
- data/app/assets/stylesheets/solidcrud/webfonts/fa-solid-900.ttf +0 -0
- data/app/assets/stylesheets/solidcrud/webfonts/fa-solid-900.woff2 +0 -0
- data/app/assets/stylesheets/solidcrud/webfonts/fa-v4compatibility.ttf +0 -0
- data/app/assets/stylesheets/solidcrud/webfonts/fa-v4compatibility.woff2 +0 -0
- data/app/assets/stylesheets/webfonts/fa-brands-400.ttf +0 -0
- data/app/assets/stylesheets/webfonts/fa-brands-400.woff2 +0 -0
- data/app/assets/stylesheets/webfonts/fa-regular-400.ttf +0 -0
- data/app/assets/stylesheets/webfonts/fa-regular-400.woff2 +0 -0
- data/app/assets/stylesheets/webfonts/fa-solid-900.ttf +0 -0
- data/app/assets/stylesheets/webfonts/fa-solid-900.woff2 +0 -0
- data/app/assets/stylesheets/webfonts/fa-v4compatibility.ttf +0 -0
- data/app/assets/stylesheets/webfonts/fa-v4compatibility.woff2 +0 -0
- data/app/controllers/solidcrud/admin_controller.rb +215 -0
- data/app/controllers/solidcrud/application_controller.rb +19 -0
- data/app/controllers/solidcrud/assets_controller.rb +59 -0
- data/app/controllers/solidcrud/sessions_controller.rb +84 -0
- data/app/helpers/solidcrud/application_helper.rb +153 -0
- data/app/javascript/solidcrud/application.js +14 -0
- data/app/javascript/solidcrud/controllers/crud_controller.js +64 -0
- data/app/javascript/solidcrud/controllers/index.js +33 -0
- data/app/views/layouts/solidcrud/application.html.erb +70 -0
- data/app/views/solidcrud/admin/edit.html.erb +294 -0
- data/app/views/solidcrud/admin/index.html.erb +128 -0
- data/app/views/solidcrud/admin/model.html.erb +353 -0
- data/app/views/solidcrud/admin/new.html.erb +275 -0
- data/app/views/solidcrud/admin/shared/_dashboard_stats.html.erb +49 -0
- data/app/views/solidcrud/admin/shared/_edit_form_sidebar.html.erb +9 -0
- data/app/views/solidcrud/admin/shared/_flash_messages.html.erb +27 -0
- data/app/views/solidcrud/admin/shared/_full_sidebar.html.erb +56 -0
- data/app/views/solidcrud/admin/shared/_modal.html.erb +45 -0
- data/app/views/solidcrud/admin/shared/_new_form_sidebar.html.erb +6 -0
- data/app/views/solidcrud/admin/shared/_record_row.html.erb +35 -0
- data/app/views/solidcrud/admin/shared/_records_table.html.erb +85 -0
- data/app/views/solidcrud/sessions/new.html.erb +262 -0
- data/config/routes.rb +24 -0
- data/lib/generators/solidcrud/install/install_generator.rb +21 -0
- data/lib/generators/solidcrud/install/templates/INSTALL.md +80 -0
- data/lib/generators/solidcrud/install/templates/solidcrud.rb +31 -0
- data/lib/generators/solidcrud/install_generator.rb +17 -0
- data/lib/generators/solidcrud/templates/solidcrud.rb +4 -0
- data/lib/solidcrud/authentication.rb +143 -0
- data/lib/solidcrud/configuration.rb +64 -0
- data/lib/solidcrud/engine.rb +49 -0
- data/lib/solidcrud/version.rb +5 -0
- data/lib/solidcrud.rb +10 -0
- 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
|
+
}
|