quick_admin 0.1.1 → 0.1.3
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/controllers/quick_admin/resources_controller.rb +7 -1
- data/app/helpers/quick_admin/application_helper.rb +68 -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/resource.rb +36 -2
- data/lib/quick_admin/version.rb +1 -1
- metadata +4 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: fc9beb45bd6b82af84edf71a0d43eb3d0527c94ad3dfc81e588e9366c1e220c5
|
|
4
|
+
data.tar.gz: 1343ec8088cb48f43f640c039295132b278690410361c0623ed721024a758a2f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 30f12d2e5a83062e9e308a47ce74884060cf857a457ed51fb2153b13753ef53da085c0a2d2d63fea130ca5ab82a12141e8954185113895a7f7223d43cd58a738
|
|
7
|
+
data.tar.gz: e6e92a9acba49e5cba6600579b4d3371bef5af2ab05635af6388a36b91f0cdb25fe3dea82c6cd4332e611b67cac342df685daa86a0d7b231177dead4572b0e05
|
|
@@ -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
|
});
|
|
@@ -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,17 @@ 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
|
|
140
208
|
end
|
|
141
209
|
end
|
|
@@ -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",
|
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.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Nezir Zahirovic
|
|
@@ -120,11 +120,11 @@ files:
|
|
|
120
120
|
- lib/quick_admin/engine.rb
|
|
121
121
|
- lib/quick_admin/resource.rb
|
|
122
122
|
- lib/quick_admin/version.rb
|
|
123
|
-
homepage: https://
|
|
123
|
+
homepage: https://blog.rubyonrails.ba/articles/introducing-quickadmin-a-lightweight-modern-admin-interface-for-rails
|
|
124
124
|
licenses:
|
|
125
125
|
- MIT
|
|
126
126
|
metadata:
|
|
127
|
-
homepage_uri: https://
|
|
127
|
+
homepage_uri: https://blog.rubyonrails.ba/articles/introducing-quickadmin-a-lightweight-modern-admin-interface-for-rails
|
|
128
128
|
source_code_uri: https://github.com/nezirz/quick_admin
|
|
129
129
|
changelog_uri: https://github.com/nezirz/quick_admin/blob/main/CHANGELOG.md
|
|
130
130
|
bug_tracker_uri: https://github.com/nezirz/quick_admin/issues
|
|
@@ -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: []
|