quick_admin 0.1.2 → 0.1.5
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 +4 -4
- data/app/assets/javascripts/quick_admin/application.js +149 -128
- data/app/assets/stylesheets/quick_admin/application.css +36 -0
- data/app/controllers/quick_admin/application_controller.rb +16 -4
- data/app/controllers/quick_admin/resources_controller.rb +7 -1
- data/app/helpers/quick_admin/application_helper.rb +96 -0
- data/app/views/layouts/quick_admin/application.html.erb +14 -0
- data/app/views/quick_admin/resources/_form.html.erb +3 -1
- data/app/views/quick_admin/resources/_resource_row.html.erb +5 -3
- data/app/views/quick_admin/resources/show.html.erb +3 -2
- data/lib/quick_admin/configuration.rb +47 -1
- data/lib/quick_admin/resource.rb +36 -2
- data/lib/quick_admin/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 63bdba520fa5bce41ca4dce1e5b4a8d20c3287d06b41b9d05e013ab36e7738e5
|
|
4
|
+
data.tar.gz: d076140889f8f98d75bb19e5b7ba922e8c4ed07f542287982096b5b738d95fb2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5c33376a983c0e6be53b301902d90237383976a29ccd209d731ad7e1598ed11ef441e0017c04d957eb0aba51c62873bba9f1c28d2564e790b85386c7f85c3041
|
|
7
|
+
data.tar.gz: 672e6998f1aa925e6fc49ce67c83d747458fe002e62172685f76e669dc8cc1f3b479d93ce19f8ae746c7931d84db4942849b4d537cc926c87a3c77a0ce650fba
|
|
@@ -1,152 +1,167 @@
|
|
|
1
1
|
// QuickAdmin JavaScript Application
|
|
2
2
|
|
|
3
3
|
// Simple vanilla JS approach to avoid import issues
|
|
4
|
-
document.addEventListener(
|
|
5
|
-
|
|
4
|
+
document.addEventListener("DOMContentLoaded", function () {
|
|
5
|
+
// Track if confirmation was already shown for a specific action
|
|
6
|
+
let confirmationShown = false;
|
|
7
|
+
|
|
6
8
|
// Global confirmation handler for delete and dangerous actions
|
|
7
|
-
//
|
|
8
|
-
document.addEventListener(
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
9
|
+
// Only handle links with data-turbo-method (delete links)
|
|
10
|
+
document.addEventListener(
|
|
11
|
+
"click",
|
|
12
|
+
function (e) {
|
|
13
|
+
const el = e.target.closest("a[data-turbo-confirm],a[data-confirm]");
|
|
14
|
+
if (!el) return;
|
|
15
|
+
// Only show confirmation for links with turbo-method (destructive actions)
|
|
16
|
+
if (!el.hasAttribute("data-turbo-method")) return;
|
|
17
|
+
|
|
18
|
+
const msg =
|
|
19
|
+
el.getAttribute("data-turbo-confirm") ||
|
|
20
|
+
el.getAttribute("data-confirm");
|
|
21
|
+
if (msg && !confirmationShown) {
|
|
22
|
+
confirmationShown = true;
|
|
23
|
+
if (!window.confirm(msg)) {
|
|
24
|
+
e.preventDefault();
|
|
25
|
+
e.stopImmediatePropagation();
|
|
26
|
+
}
|
|
27
|
+
// Reset after a short delay
|
|
28
|
+
setTimeout(function () {
|
|
29
|
+
confirmationShown = false;
|
|
30
|
+
}, 100);
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
true
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
// Form submit confirmation - only for bulk delete form
|
|
37
|
+
document.addEventListener(
|
|
38
|
+
"submit",
|
|
39
|
+
function (e) {
|
|
40
|
+
const form = e.target;
|
|
41
|
+
if (!(form instanceof HTMLFormElement)) return;
|
|
42
|
+
|
|
43
|
+
// Only handle bulk delete forms (forms with data-bulk-actions)
|
|
44
|
+
if (!form.hasAttribute("data-bulk-actions")) return;
|
|
45
|
+
|
|
46
|
+
// Check if the submit button was clicked (not auto-submit from filters)
|
|
47
|
+
const submitter = e.submitter;
|
|
48
|
+
if (!submitter || !submitter.hasAttribute("data-bulk-actions")) return;
|
|
49
|
+
|
|
50
|
+
const msg =
|
|
51
|
+
form.getAttribute("data-turbo-confirm") ||
|
|
52
|
+
form.getAttribute("data-confirm");
|
|
53
|
+
if (msg && !confirmationShown) {
|
|
54
|
+
confirmationShown = true;
|
|
55
|
+
if (!window.confirm(msg)) {
|
|
56
|
+
e.preventDefault();
|
|
57
|
+
e.stopImmediatePropagation();
|
|
58
|
+
}
|
|
59
|
+
setTimeout(function () {
|
|
60
|
+
confirmationShown = false;
|
|
61
|
+
}, 100);
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
true
|
|
65
|
+
);
|
|
66
|
+
|
|
57
67
|
// Modal functionality
|
|
58
|
-
document.addEventListener(
|
|
68
|
+
document.addEventListener("click", function (e) {
|
|
59
69
|
// Close modal when clicking overlay
|
|
60
|
-
if (e.target.classList.contains(
|
|
70
|
+
if (e.target.classList.contains("modal-overlay")) {
|
|
61
71
|
closeModal();
|
|
62
72
|
}
|
|
63
|
-
|
|
73
|
+
|
|
64
74
|
// Close modal when clicking close button
|
|
65
|
-
if (
|
|
66
|
-
|
|
75
|
+
if (
|
|
76
|
+
e.target.classList.contains("modal-close") ||
|
|
77
|
+
(e.target.hasAttribute("data-action") &&
|
|
78
|
+
e.target.getAttribute("data-action").includes("modal#close"))
|
|
79
|
+
) {
|
|
67
80
|
e.preventDefault();
|
|
68
81
|
closeModal();
|
|
69
82
|
}
|
|
70
83
|
});
|
|
71
|
-
|
|
84
|
+
|
|
72
85
|
// Close modal on Escape key
|
|
73
|
-
document.addEventListener(
|
|
74
|
-
if (e.key ===
|
|
86
|
+
document.addEventListener("keydown", function (e) {
|
|
87
|
+
if (e.key === "Escape") {
|
|
75
88
|
closeModal();
|
|
76
89
|
}
|
|
77
90
|
});
|
|
78
|
-
|
|
91
|
+
|
|
79
92
|
function closeModal() {
|
|
80
|
-
const modal = document.getElementById(
|
|
93
|
+
const modal = document.getElementById("modal");
|
|
81
94
|
if (modal) {
|
|
82
|
-
modal.innerHTML =
|
|
83
|
-
document.body.style.overflow =
|
|
95
|
+
modal.innerHTML = "";
|
|
96
|
+
document.body.style.overflow = "";
|
|
84
97
|
}
|
|
85
98
|
}
|
|
86
|
-
|
|
99
|
+
|
|
87
100
|
// Search functionality with debouncing
|
|
88
101
|
function initializeSearch() {
|
|
89
102
|
const searchInputs = document.querySelectorAll('[data-search="input"]');
|
|
90
|
-
searchInputs.forEach(function(input) {
|
|
91
|
-
if (!input.hasAttribute(
|
|
103
|
+
searchInputs.forEach(function (input) {
|
|
104
|
+
if (!input.hasAttribute("data-initialized")) {
|
|
92
105
|
let timeout;
|
|
93
|
-
input.addEventListener(
|
|
106
|
+
input.addEventListener("input", function () {
|
|
94
107
|
clearTimeout(timeout);
|
|
95
|
-
timeout = setTimeout(function() {
|
|
96
|
-
input.closest(
|
|
108
|
+
timeout = setTimeout(function () {
|
|
109
|
+
input.closest("form").submit();
|
|
97
110
|
}, 300);
|
|
98
111
|
});
|
|
99
|
-
input.setAttribute(
|
|
112
|
+
input.setAttribute("data-initialized", "true");
|
|
100
113
|
}
|
|
101
114
|
});
|
|
102
115
|
}
|
|
103
|
-
|
|
116
|
+
|
|
104
117
|
// Filter functionality
|
|
105
118
|
function initializeFilters() {
|
|
106
119
|
const filterSelects = document.querySelectorAll('[data-filter="select"]');
|
|
107
|
-
filterSelects.forEach(function(select) {
|
|
108
|
-
if (!select.hasAttribute(
|
|
109
|
-
select.addEventListener(
|
|
110
|
-
select.closest(
|
|
120
|
+
filterSelects.forEach(function (select) {
|
|
121
|
+
if (!select.hasAttribute("data-initialized")) {
|
|
122
|
+
select.addEventListener("change", function () {
|
|
123
|
+
select.closest("form").submit();
|
|
111
124
|
});
|
|
112
|
-
select.setAttribute(
|
|
125
|
+
select.setAttribute("data-initialized", "true");
|
|
113
126
|
}
|
|
114
127
|
});
|
|
115
128
|
}
|
|
116
|
-
|
|
129
|
+
|
|
117
130
|
// Initialize search and filters on page load
|
|
118
131
|
initializeSearch();
|
|
119
132
|
initializeFilters();
|
|
120
|
-
|
|
133
|
+
|
|
121
134
|
// Bulk actions functionality
|
|
122
135
|
function initializeBulkActions() {
|
|
123
136
|
const bulkForms = document.querySelectorAll('[data-bulk-actions="form"]');
|
|
124
|
-
bulkForms.forEach(function(form) {
|
|
137
|
+
bulkForms.forEach(function (form) {
|
|
125
138
|
const toggleAll = form.querySelector('[data-bulk-actions="toggle-all"]');
|
|
126
139
|
const items = form.querySelectorAll('[data-bulk-actions="item"]');
|
|
127
140
|
const submitBtn = form.querySelector('[data-bulk-actions="submit"]');
|
|
128
|
-
|
|
141
|
+
|
|
129
142
|
// Remove existing event listeners to prevent duplicates
|
|
130
|
-
if (toggleAll && !toggleAll.hasAttribute(
|
|
131
|
-
toggleAll.addEventListener(
|
|
143
|
+
if (toggleAll && !toggleAll.hasAttribute("data-initialized")) {
|
|
144
|
+
toggleAll.addEventListener("change", function () {
|
|
132
145
|
const checked = toggleAll.checked;
|
|
133
|
-
items.forEach(function(item) {
|
|
146
|
+
items.forEach(function (item) {
|
|
134
147
|
item.checked = checked;
|
|
135
148
|
});
|
|
136
149
|
updateSubmitButton();
|
|
137
150
|
});
|
|
138
|
-
toggleAll.setAttribute(
|
|
151
|
+
toggleAll.setAttribute("data-initialized", "true");
|
|
139
152
|
}
|
|
140
|
-
|
|
141
|
-
items.forEach(function(item) {
|
|
142
|
-
if (!item.hasAttribute(
|
|
143
|
-
item.addEventListener(
|
|
144
|
-
item.setAttribute(
|
|
153
|
+
|
|
154
|
+
items.forEach(function (item) {
|
|
155
|
+
if (!item.hasAttribute("data-initialized")) {
|
|
156
|
+
item.addEventListener("change", updateSubmitButton);
|
|
157
|
+
item.setAttribute("data-initialized", "true");
|
|
145
158
|
}
|
|
146
159
|
});
|
|
147
|
-
|
|
160
|
+
|
|
148
161
|
function updateSubmitButton() {
|
|
149
|
-
const checkedItems = form.querySelectorAll(
|
|
162
|
+
const checkedItems = form.querySelectorAll(
|
|
163
|
+
'[data-bulk-actions="item"]:checked'
|
|
164
|
+
);
|
|
150
165
|
if (submitBtn) {
|
|
151
166
|
submitBtn.disabled = checkedItems.length === 0;
|
|
152
167
|
if (checkedItems.length > 0) {
|
|
@@ -158,38 +173,43 @@ document.addEventListener('DOMContentLoaded', function() {
|
|
|
158
173
|
}
|
|
159
174
|
});
|
|
160
175
|
}
|
|
161
|
-
|
|
176
|
+
|
|
162
177
|
// Initialize bulk actions on page load
|
|
163
178
|
initializeBulkActions();
|
|
164
|
-
|
|
179
|
+
|
|
165
180
|
// Re-initialize all components after turbo updates
|
|
166
|
-
document.addEventListener(
|
|
167
|
-
console.log(
|
|
181
|
+
document.addEventListener("turbo:frame-load", function (event) {
|
|
182
|
+
console.log("Turbo frame loaded:", event.target.id);
|
|
168
183
|
initializeSearch();
|
|
169
184
|
initializeFilters();
|
|
170
185
|
initializeBulkActions();
|
|
171
186
|
initializeAlerts();
|
|
172
187
|
});
|
|
173
|
-
|
|
188
|
+
|
|
174
189
|
// Also reinitialize after turbo stream actions
|
|
175
|
-
document.addEventListener(
|
|
176
|
-
console.log(
|
|
190
|
+
document.addEventListener("turbo:before-stream-render", function (event) {
|
|
191
|
+
console.log("Turbo stream before render:", event.detail.newStream);
|
|
177
192
|
// Clear existing initializations so they can be re-added
|
|
178
|
-
document.querySelectorAll(
|
|
179
|
-
element.removeAttribute(
|
|
193
|
+
document.querySelectorAll("[data-initialized]").forEach(function (element) {
|
|
194
|
+
element.removeAttribute("data-initialized");
|
|
180
195
|
});
|
|
181
196
|
});
|
|
182
|
-
|
|
197
|
+
|
|
183
198
|
// Additional event listeners for debugging and ensuring functionality
|
|
184
|
-
document.addEventListener(
|
|
185
|
-
console.log(
|
|
199
|
+
document.addEventListener("turbo:submit-start", function (event) {
|
|
200
|
+
console.log("Form submission started:", event.target);
|
|
186
201
|
});
|
|
187
|
-
|
|
188
|
-
document.addEventListener(
|
|
189
|
-
console.log(
|
|
202
|
+
|
|
203
|
+
document.addEventListener("turbo:submit-end", function (event) {
|
|
204
|
+
console.log(
|
|
205
|
+
"Form submission ended:",
|
|
206
|
+
event.target,
|
|
207
|
+
"Success:",
|
|
208
|
+
event.detail.success
|
|
209
|
+
);
|
|
190
210
|
if (event.detail.success) {
|
|
191
211
|
// Reinitialize everything after successful form submission
|
|
192
|
-
setTimeout(function() {
|
|
212
|
+
setTimeout(function () {
|
|
193
213
|
initializeSearch();
|
|
194
214
|
initializeFilters();
|
|
195
215
|
initializeBulkActions();
|
|
@@ -197,25 +217,27 @@ document.addEventListener('DOMContentLoaded', function() {
|
|
|
197
217
|
}, 100);
|
|
198
218
|
}
|
|
199
219
|
});
|
|
200
|
-
|
|
220
|
+
|
|
201
221
|
// Handle turbo stream rendering completion
|
|
202
|
-
document.addEventListener(
|
|
222
|
+
document.addEventListener("turbo:before-stream-render", function () {
|
|
203
223
|
// Clear all flash messages before new ones are added
|
|
204
|
-
document.querySelectorAll(
|
|
224
|
+
document.querySelectorAll(".flash-message").forEach(function (msg) {
|
|
205
225
|
msg.remove();
|
|
206
226
|
});
|
|
207
227
|
});
|
|
208
|
-
|
|
228
|
+
|
|
209
229
|
// Auto-dismiss alerts function
|
|
210
230
|
function initializeAlerts() {
|
|
211
|
-
const alerts = document.querySelectorAll(
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
231
|
+
const alerts = document.querySelectorAll(
|
|
232
|
+
'[data-auto-dismiss="true"]:not([data-alert-initialized])'
|
|
233
|
+
);
|
|
234
|
+
alerts.forEach(function (alert) {
|
|
235
|
+
alert.setAttribute("data-alert-initialized", "true");
|
|
236
|
+
setTimeout(function () {
|
|
215
237
|
if (alert.parentNode) {
|
|
216
|
-
alert.style.opacity =
|
|
217
|
-
alert.style.transform =
|
|
218
|
-
setTimeout(function() {
|
|
238
|
+
alert.style.opacity = "0";
|
|
239
|
+
alert.style.transform = "translateY(-10px)";
|
|
240
|
+
setTimeout(function () {
|
|
219
241
|
if (alert.parentNode) {
|
|
220
242
|
alert.remove();
|
|
221
243
|
}
|
|
@@ -224,22 +246,21 @@ document.addEventListener('DOMContentLoaded', function() {
|
|
|
224
246
|
}, 3000);
|
|
225
247
|
});
|
|
226
248
|
}
|
|
227
|
-
|
|
249
|
+
|
|
228
250
|
// Initialize alerts on page load
|
|
229
251
|
initializeAlerts();
|
|
230
|
-
|
|
252
|
+
|
|
231
253
|
// Alert close buttons
|
|
232
|
-
document.addEventListener(
|
|
233
|
-
if (e.target.classList.contains(
|
|
234
|
-
const alert = e.target.closest(
|
|
254
|
+
document.addEventListener("click", function (e) {
|
|
255
|
+
if (e.target.classList.contains("alert-close")) {
|
|
256
|
+
const alert = e.target.closest(".alert");
|
|
235
257
|
if (alert) {
|
|
236
|
-
alert.style.opacity =
|
|
237
|
-
alert.style.transform =
|
|
238
|
-
setTimeout(function() {
|
|
258
|
+
alert.style.opacity = "0";
|
|
259
|
+
alert.style.transform = "translateY(-10px)";
|
|
260
|
+
setTimeout(function () {
|
|
239
261
|
alert.remove();
|
|
240
262
|
}, 300);
|
|
241
263
|
}
|
|
242
264
|
}
|
|
243
265
|
});
|
|
244
|
-
|
|
245
266
|
});
|
|
@@ -86,6 +86,42 @@ body {
|
|
|
86
86
|
padding-left: 0.5rem;
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
/* Navigation Footer */
|
|
90
|
+
.nav-footer {
|
|
91
|
+
position: absolute;
|
|
92
|
+
bottom: 0;
|
|
93
|
+
left: 0;
|
|
94
|
+
right: 0;
|
|
95
|
+
padding: 1rem;
|
|
96
|
+
border-top: 1px solid rgba(255,255,255,0.1);
|
|
97
|
+
background-color: var(--dark-color);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.user-info {
|
|
101
|
+
margin-bottom: 0.5rem;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.user-email {
|
|
105
|
+
color: rgba(255,255,255,0.7);
|
|
106
|
+
font-size: 0.85rem;
|
|
107
|
+
display: block;
|
|
108
|
+
overflow: hidden;
|
|
109
|
+
text-overflow: ellipsis;
|
|
110
|
+
white-space: nowrap;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.sign-out-link {
|
|
114
|
+
color: rgba(255,255,255,0.8);
|
|
115
|
+
text-decoration: none;
|
|
116
|
+
padding: 0.5rem 0;
|
|
117
|
+
display: block;
|
|
118
|
+
font-size: 0.9rem;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.sign-out-link:hover {
|
|
122
|
+
color: var(--danger-color);
|
|
123
|
+
}
|
|
124
|
+
|
|
89
125
|
/* Buttons */
|
|
90
126
|
.btn {
|
|
91
127
|
display: inline-block;
|
|
@@ -10,16 +10,28 @@ module QuickAdmin
|
|
|
10
10
|
private
|
|
11
11
|
|
|
12
12
|
def authenticate_admin_user!
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
auth_method = QuickAdmin.config.authenticate_method
|
|
14
|
+
|
|
15
|
+
if auth_method.present?
|
|
16
|
+
# Use explicitly configured method
|
|
17
|
+
send(auth_method)
|
|
18
|
+
elsif defined?(Devise)
|
|
19
|
+
# Auto-detect Devise authentication method
|
|
20
|
+
if respond_to?(:authenticate_admin!, true)
|
|
21
|
+
authenticate_admin!
|
|
22
|
+
elsif respond_to?(:authenticate_user!, true)
|
|
23
|
+
authenticate_user!
|
|
24
|
+
end
|
|
15
25
|
elsif QuickAdmin.config.authentication == :custom
|
|
16
|
-
#
|
|
26
|
+
# Custom authentication - override admin_authenticated? in your app
|
|
17
27
|
head :unauthorized unless admin_authenticated?
|
|
18
28
|
end
|
|
19
29
|
end
|
|
20
30
|
|
|
21
31
|
def authentication_required?
|
|
22
|
-
QuickAdmin.config.authentication.present?
|
|
32
|
+
QuickAdmin.config.authentication.present? ||
|
|
33
|
+
QuickAdmin.config.authenticate_method.present? ||
|
|
34
|
+
defined?(Devise)
|
|
23
35
|
end
|
|
24
36
|
|
|
25
37
|
def admin_authenticated?
|
|
@@ -113,7 +113,13 @@ module QuickAdmin
|
|
|
113
113
|
end
|
|
114
114
|
|
|
115
115
|
def set_resource
|
|
116
|
-
|
|
116
|
+
find_field = @resource_config.find_by_field
|
|
117
|
+
|
|
118
|
+
if find_field == 'id'
|
|
119
|
+
@resource = resource_class.find(params[:id])
|
|
120
|
+
else
|
|
121
|
+
@resource = resource_class.find_by!(find_field => params[:id])
|
|
122
|
+
end
|
|
117
123
|
end
|
|
118
124
|
|
|
119
125
|
def resource_name
|
|
@@ -1,6 +1,20 @@
|
|
|
1
1
|
module QuickAdmin
|
|
2
2
|
module ApplicationHelper
|
|
3
3
|
def display_field_value(resource, field)
|
|
4
|
+
# Check if this is a belongs_to association (field ends with _id)
|
|
5
|
+
if field.to_s.end_with?('_id')
|
|
6
|
+
association_name = field.to_s.sub(/_id$/, '')
|
|
7
|
+
if resource.class.reflect_on_association(association_name.to_sym)
|
|
8
|
+
associated = resource.send(association_name)
|
|
9
|
+
if associated
|
|
10
|
+
# Try common display methods
|
|
11
|
+
return associated.try(:name) || associated.try(:title) || associated.try(:to_s) || associated.id
|
|
12
|
+
else
|
|
13
|
+
return content_tag(:span, "—", class: "null-value")
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
4
18
|
value = resource.send(field)
|
|
5
19
|
|
|
6
20
|
case value
|
|
@@ -57,6 +71,26 @@ module QuickAdmin
|
|
|
57
71
|
end
|
|
58
72
|
|
|
59
73
|
def render_form_field(form, resource, field)
|
|
74
|
+
# Check if this is a belongs_to association (field ends with _id)
|
|
75
|
+
if field.to_s.end_with?('_id')
|
|
76
|
+
association_name = field.to_s.sub(/_id$/, '')
|
|
77
|
+
association = resource.class.reflect_on_association(association_name.to_sym)
|
|
78
|
+
if association
|
|
79
|
+
associated_class = association.klass
|
|
80
|
+
options = associated_class.all.map do |record|
|
|
81
|
+
display = record.try(:name) || record.try(:title) || record.to_s
|
|
82
|
+
[display, record.id]
|
|
83
|
+
end
|
|
84
|
+
return form.select(field, options, { include_blank: "Select #{association_name.humanize}" }, class: "form-control")
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Check if this is an enum field
|
|
89
|
+
if resource.class.respond_to?(:defined_enums) && resource.class.defined_enums.key?(field.to_s)
|
|
90
|
+
enum_options = resource.class.send(field.to_s.pluralize).keys.map { |k| [k.humanize, k] }
|
|
91
|
+
return form.select(field, enum_options, { include_blank: "Select #{field.humanize}" }, class: "form-control")
|
|
92
|
+
end
|
|
93
|
+
|
|
60
94
|
column = resource.class.columns_hash[field]
|
|
61
95
|
|
|
62
96
|
case column&.type
|
|
@@ -96,6 +130,20 @@ module QuickAdmin
|
|
|
96
130
|
def filter_options_for_field(model_class, field)
|
|
97
131
|
return [] unless model_class.respond_to?(:column_names)
|
|
98
132
|
|
|
133
|
+
# Check if this is a belongs_to association (field ends with _id)
|
|
134
|
+
if field.to_s.end_with?('_id')
|
|
135
|
+
association_name = field.to_s.sub(/_id$/, '')
|
|
136
|
+
association = model_class.reflect_on_association(association_name.to_sym)
|
|
137
|
+
if association
|
|
138
|
+
associated_class = association.klass
|
|
139
|
+
# Get all records from the associated model and display their name/title
|
|
140
|
+
return associated_class.all.map do |record|
|
|
141
|
+
display = record.try(:name) || record.try(:title) || record.to_s
|
|
142
|
+
[display, record.id.to_s]
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
99
147
|
column = model_class.columns_hash[field]
|
|
100
148
|
case column&.type
|
|
101
149
|
when :boolean
|
|
@@ -106,6 +154,14 @@ module QuickAdmin
|
|
|
106
154
|
dates = model_class.where.not(field => nil).order(field => :desc).limit(200).pluck(field)
|
|
107
155
|
days = dates.map { |d| d.to_date }.uniq.first(10)
|
|
108
156
|
days.map { |day| [day.strftime('%Y-%m-%d'), day.strftime('%Y-%m-%d')] }
|
|
157
|
+
when :integer
|
|
158
|
+
# Check if this is an enum field
|
|
159
|
+
enum_name = field.to_s.pluralize
|
|
160
|
+
if model_class.respond_to?(enum_name)
|
|
161
|
+
return model_class.send(enum_name).keys.map { |k| [k.humanize, k] }
|
|
162
|
+
end
|
|
163
|
+
values = model_class.distinct.pluck(field).compact.uniq
|
|
164
|
+
values.map { |v| [v.to_s, v.to_s] }
|
|
109
165
|
else
|
|
110
166
|
values = model_class.distinct.pluck(field).compact.uniq
|
|
111
167
|
values.map { |v| [v.to_s, v.to_s] }
|
|
@@ -137,5 +193,45 @@ module QuickAdmin
|
|
|
137
193
|
def current_resource_config
|
|
138
194
|
@resource_config
|
|
139
195
|
end
|
|
196
|
+
|
|
197
|
+
# Returns the param value to use in URLs for a resource.
|
|
198
|
+
# Uses the configured find_by_field to get the correct value.
|
|
199
|
+
#
|
|
200
|
+
# @param resource [ActiveRecord::Base] the resource record
|
|
201
|
+
# @return [String, Integer] the param value for URLs
|
|
202
|
+
def resource_param_value(resource)
|
|
203
|
+
return resource.id unless @resource_config
|
|
204
|
+
|
|
205
|
+
find_field = @resource_config.find_by_field
|
|
206
|
+
find_field == 'id' ? resource.id : resource.send(find_field)
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# Returns the current user using the configured method.
|
|
210
|
+
# @return [Object, nil] the current user or nil
|
|
211
|
+
def quick_admin_current_user
|
|
212
|
+
method_name = QuickAdmin.config.resolved_current_user_method
|
|
213
|
+
return nil unless respond_to?(method_name, true)
|
|
214
|
+
send(method_name)
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# Returns a display string for the current user.
|
|
218
|
+
# Tries email, name, username, or falls back to "Admin".
|
|
219
|
+
# @return [String] display name for the user
|
|
220
|
+
def quick_admin_user_display
|
|
221
|
+
user = quick_admin_current_user
|
|
222
|
+
return "Admin" unless user
|
|
223
|
+
|
|
224
|
+
user.try(:email) || user.try(:name) || user.try(:username) || "Admin"
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# Returns the sign out path if available.
|
|
228
|
+
# @return [String, nil] the sign out path or nil
|
|
229
|
+
def quick_admin_sign_out_path
|
|
230
|
+
path = QuickAdmin.config.resolved_sign_out_path
|
|
231
|
+
return path if path.is_a?(String)
|
|
232
|
+
|
|
233
|
+
# If it's a proc, call it
|
|
234
|
+
path.respond_to?(:call) ? path.call : nil
|
|
235
|
+
end
|
|
140
236
|
end
|
|
141
237
|
end
|
|
@@ -24,6 +24,20 @@
|
|
|
24
24
|
<li><%= link_to resource.display_name, quick_admin.resources_path(name), class: "nav-link" %></li>
|
|
25
25
|
<% end %>
|
|
26
26
|
</ul>
|
|
27
|
+
|
|
28
|
+
<div class="nav-footer">
|
|
29
|
+
<% if quick_admin_current_user %>
|
|
30
|
+
<div class="user-info">
|
|
31
|
+
<span class="user-email"><%= quick_admin_user_display %></span>
|
|
32
|
+
</div>
|
|
33
|
+
<% end %>
|
|
34
|
+
<% if quick_admin_sign_out_path %>
|
|
35
|
+
<%= link_to "Sign Out", quick_admin_sign_out_path,
|
|
36
|
+
method: QuickAdmin.config.sign_out_method,
|
|
37
|
+
data: { turbo_method: QuickAdmin.config.sign_out_method },
|
|
38
|
+
class: "nav-link sign-out-link" %>
|
|
39
|
+
<% end %>
|
|
40
|
+
</div>
|
|
27
41
|
</nav>
|
|
28
42
|
|
|
29
43
|
<main class="quick-admin-main">
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
<%
|
|
2
2
|
# Handle resource_name parameter
|
|
3
3
|
resource_name_param = defined?(resource_name) ? resource_name : @resource_config.model_name
|
|
4
|
+
# Get the correct param value for URLs (uses configured find_by_field)
|
|
5
|
+
param_value = resource_param_value(resource)
|
|
4
6
|
%>
|
|
5
7
|
|
|
6
8
|
<%= form_with model: [QuickAdmin::Engine, resource_name_param.singularize.to_sym, resource],
|
|
7
|
-
url: resource.persisted? ? quick_admin.resource_path(resource_name_param,
|
|
9
|
+
url: resource.persisted? ? quick_admin.resource_path(resource_name_param, param_value) : quick_admin.resources_path(resource_name_param),
|
|
8
10
|
method: resource.persisted? ? :patch : :post,
|
|
9
11
|
local: false,
|
|
10
12
|
class: "resource-form" do |form| %>
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
<%
|
|
2
2
|
# Handle resource_name parameter
|
|
3
3
|
resource_name_param = defined?(resource_name) ? resource_name : params[:resource_name]
|
|
4
|
+
# Get the correct param value for URLs (uses configured find_by_field)
|
|
5
|
+
param_value = resource_param_value(resource)
|
|
4
6
|
%>
|
|
5
7
|
|
|
6
8
|
<tr class="resource-row" id="resource_<%= resource.id %>">
|
|
@@ -14,13 +16,13 @@
|
|
|
14
16
|
</td>
|
|
15
17
|
<% end %>
|
|
16
18
|
<td class="actions">
|
|
17
|
-
<%= link_to "View", quick_admin.resource_path(resource_name_param,
|
|
19
|
+
<%= link_to "View", quick_admin.resource_path(resource_name_param, param_value),
|
|
18
20
|
class: "btn btn-sm btn-info",
|
|
19
21
|
data: { turbo_frame: "modal" } %>
|
|
20
|
-
<%= link_to "Edit", quick_admin.edit_resource_path(resource_name_param,
|
|
22
|
+
<%= link_to "Edit", quick_admin.edit_resource_path(resource_name_param, param_value),
|
|
21
23
|
class: "btn btn-sm btn-warning",
|
|
22
24
|
data: { turbo_frame: "modal" } %>
|
|
23
|
-
<%= link_to "Delete", quick_admin.resource_path(resource_name_param,
|
|
25
|
+
<%= link_to "Delete", quick_admin.resource_path(resource_name_param, param_value),
|
|
24
26
|
class: "btn btn-sm btn-danger",
|
|
25
27
|
data: {
|
|
26
28
|
turbo_method: :delete,
|
|
@@ -19,10 +19,11 @@
|
|
|
19
19
|
</div>
|
|
20
20
|
|
|
21
21
|
<div class="detail-actions">
|
|
22
|
-
|
|
22
|
+
<% param_value = resource_param_value(@resource) %>
|
|
23
|
+
<%= link_to "Edit", quick_admin.edit_resource_path(resource_name, param_value),
|
|
23
24
|
class: "btn btn-warning",
|
|
24
25
|
data: { turbo_frame: "modal" } %>
|
|
25
|
-
<%= button_to "Delete", quick_admin.resource_path(resource_name,
|
|
26
|
+
<%= button_to "Delete", quick_admin.resource_path(resource_name, param_value),
|
|
26
27
|
method: :delete,
|
|
27
28
|
class: "btn btn-danger",
|
|
28
29
|
form_class: "inline-form",
|
|
@@ -10,9 +10,14 @@ module QuickAdmin
|
|
|
10
10
|
# @attr per_page [Integer] number of records per page (default: 25)
|
|
11
11
|
# @attr mount_path [String] URL path where admin is mounted (default: '/admin')
|
|
12
12
|
# @attr app_name [String] application name shown in UI (default: 'QuickAdmin')
|
|
13
|
+
# @attr sign_out_path [String, Proc] path for sign out link (default: auto-detect)
|
|
14
|
+
# @attr sign_out_method [Symbol] HTTP method for sign out (default: :delete)
|
|
15
|
+
# @attr current_user_method [Symbol] method to get current user (default: :current_user)
|
|
13
16
|
class Configuration
|
|
14
17
|
attr_accessor :authentication, :css_framework, :text_editor, :turbo_enabled,
|
|
15
|
-
:stimulus_enabled, :per_page, :mount_path, :app_name
|
|
18
|
+
:stimulus_enabled, :per_page, :mount_path, :app_name,
|
|
19
|
+
:sign_out_path, :sign_out_method, :current_user_method,
|
|
20
|
+
:authenticate_method
|
|
16
21
|
|
|
17
22
|
# Initializes a new Configuration with default values.
|
|
18
23
|
def initialize
|
|
@@ -24,6 +29,10 @@ module QuickAdmin
|
|
|
24
29
|
@per_page = 25 # Reasonable default pagination
|
|
25
30
|
@mount_path = '/admin' # Standard admin path
|
|
26
31
|
@app_name = 'QuickAdmin' # Default app name
|
|
32
|
+
@sign_out_path = nil # Auto-detect from Devise or set manually
|
|
33
|
+
@sign_out_method = :delete # Default HTTP method for sign out
|
|
34
|
+
@current_user_method = :current_user # Method to get current user
|
|
35
|
+
@authenticate_method = nil # Auto-detect or set manually (e.g., :authenticate_admin!)
|
|
27
36
|
end
|
|
28
37
|
|
|
29
38
|
# Checks if Devise authentication is enabled and available.
|
|
@@ -55,5 +64,42 @@ module QuickAdmin
|
|
|
55
64
|
def lexical_enabled?
|
|
56
65
|
text_editor == :lexical
|
|
57
66
|
end
|
|
67
|
+
|
|
68
|
+
# Returns the resolved sign out path.
|
|
69
|
+
# Auto-detects Devise path if not explicitly set.
|
|
70
|
+
# @return [String, nil] the sign out path or nil if not available
|
|
71
|
+
def resolved_sign_out_path
|
|
72
|
+
return sign_out_path if sign_out_path.present?
|
|
73
|
+
|
|
74
|
+
# Auto-detect Devise sign out path (even if not explicitly configured)
|
|
75
|
+
if defined?(Devise)
|
|
76
|
+
# Check for Admin model first, then User
|
|
77
|
+
if defined?(Admin)
|
|
78
|
+
'/admins/sign_out'
|
|
79
|
+
else
|
|
80
|
+
'/users/sign_out'
|
|
81
|
+
end
|
|
82
|
+
else
|
|
83
|
+
nil
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Returns the resolved current user method.
|
|
88
|
+
# Auto-detects based on available Devise models.
|
|
89
|
+
# @return [Symbol] the method name to get current user
|
|
90
|
+
def resolved_current_user_method
|
|
91
|
+
return current_user_method if current_user_method != :current_user
|
|
92
|
+
|
|
93
|
+
# Auto-detect based on Devise models
|
|
94
|
+
if defined?(Devise)
|
|
95
|
+
if defined?(Admin)
|
|
96
|
+
:current_admin
|
|
97
|
+
else
|
|
98
|
+
:current_user
|
|
99
|
+
end
|
|
100
|
+
else
|
|
101
|
+
current_user_method
|
|
102
|
+
end
|
|
103
|
+
end
|
|
58
104
|
end
|
|
59
105
|
end
|
data/lib/quick_admin/resource.rb
CHANGED
|
@@ -7,9 +7,10 @@ module QuickAdmin
|
|
|
7
7
|
# @attr filterable_fields [Array<String>] fields that can be filtered
|
|
8
8
|
# @attr editable_fields [Array<String>] fields that can be edited
|
|
9
9
|
# @attr display_name [String] human-friendly name for the resource
|
|
10
|
+
# @attr param_key [String] the field to use for finding records (default: id)
|
|
10
11
|
class Resource
|
|
11
12
|
attr_accessor :model_name, :searchable_fields,
|
|
12
|
-
:filterable_fields, :editable_fields, :display_name
|
|
13
|
+
:filterable_fields, :editable_fields, :display_name, :param_key
|
|
13
14
|
|
|
14
15
|
# Initializes a new Resource.
|
|
15
16
|
#
|
|
@@ -21,6 +22,7 @@ module QuickAdmin
|
|
|
21
22
|
@filterable_fields = []
|
|
22
23
|
@editable_fields = []
|
|
23
24
|
@display_name = model_name.to_s.humanize.pluralize
|
|
25
|
+
@param_key = nil # Will auto-detect or default to :id
|
|
24
26
|
end
|
|
25
27
|
|
|
26
28
|
# Returns the actual model class for this resource.
|
|
@@ -86,6 +88,38 @@ module QuickAdmin
|
|
|
86
88
|
@display_name
|
|
87
89
|
end
|
|
88
90
|
|
|
91
|
+
# Gets or sets the param key used for finding records.
|
|
92
|
+
# Use this when your model overrides to_param (e.g., uses slug instead of id).
|
|
93
|
+
#
|
|
94
|
+
# @param field [Symbol, String, nil] optional field name to set
|
|
95
|
+
# @return [String] the param key field name
|
|
96
|
+
#
|
|
97
|
+
# @example
|
|
98
|
+
# r.param :slug # Use slug field for finding records
|
|
99
|
+
def param(field = nil)
|
|
100
|
+
@param_key = field.to_s if field
|
|
101
|
+
@param_key
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Returns the field to use for finding records.
|
|
105
|
+
# If param_key is set, uses that. Otherwise, auto-detects if model
|
|
106
|
+
# overrides to_param and tries to find the matching field.
|
|
107
|
+
#
|
|
108
|
+
# @return [String] the field name to use for finding records
|
|
109
|
+
def find_by_field
|
|
110
|
+
return @param_key if @param_key.present?
|
|
111
|
+
|
|
112
|
+
# Auto-detect: check if model overrides to_param
|
|
113
|
+
if model_class && model_class.instance_methods(false).include?(:to_param)
|
|
114
|
+
# Common patterns: slug, uuid, token, etc.
|
|
115
|
+
%w[slug uuid token permalink].each do |field|
|
|
116
|
+
return field if model_class.column_names.include?(field)
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
'id'
|
|
121
|
+
end
|
|
122
|
+
|
|
89
123
|
private
|
|
90
124
|
|
|
91
125
|
# Returns default fields from the model if no fields are explicitly set.
|
|
@@ -93,7 +127,7 @@ module QuickAdmin
|
|
|
93
127
|
#
|
|
94
128
|
# @return [Array<String>] list of column names
|
|
95
129
|
def default_fields
|
|
96
|
-
return [] unless model_class
|
|
130
|
+
return [] unless model_class && model_class.respond_to?(:column_names)
|
|
97
131
|
|
|
98
132
|
excluded_fields = %w[id created_at updated_at]
|
|
99
133
|
model_class.column_names.reject { |field| excluded_fields.include?(field) }
|
data/lib/quick_admin/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: quick_admin
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.5
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Nezir Zahirovic
|
|
@@ -142,7 +142,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
142
142
|
- !ruby/object:Gem::Version
|
|
143
143
|
version: 2.0.0
|
|
144
144
|
requirements: []
|
|
145
|
-
rubygems_version: 3.
|
|
145
|
+
rubygems_version: 3.6.9
|
|
146
146
|
specification_version: 4
|
|
147
147
|
summary: Simple, minimal Rails admin interface
|
|
148
148
|
test_files: []
|