elysia-rack 0.4.0 → 0.5.2
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.
- package/README.md +61 -3
- package/dist/Dashboard-2raba6xf.js +212 -0
- package/dist/Panel-5wx7qpqt.js +1159 -0
- package/dist/Panel-9k5vambv.js +1160 -0
- package/dist/index-9k12apt9.js +157 -0
- package/dist/index-vq4gh509.js +40 -0
- package/dist/panel-app.js +322 -83
- package/dist/panel.css +1 -1
- package/dist/rack/index.js +10 -6
- package/dist/rack/page.d.ts +2 -0
- package/dist/rack/query.d.ts +14 -12
- package/dist/rack/types/actions.d.ts +27 -0
- package/dist/rack/types/fields.d.ts +48 -0
- package/dist/rack/types/index.d.ts +15 -0
- package/dist/rack/types/metadata.d.ts +11 -0
- package/dist/react/index.js +28 -9
- package/dist/react/page/Panel.d.ts +6 -23
- package/dist/react/page/components/DataTable.d.ts +9 -2
- package/dist/react/page/components/Toolbar.d.ts +5 -1
- package/dist/react/page/components/panelFields.d.ts +39 -0
- package/package.json +1 -1
package/dist/panel-app.js
CHANGED
|
@@ -71,6 +71,72 @@ function fillForm(form, row) {
|
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
// src/react/page/components/panelFields.ts
|
|
75
|
+
function isNumericField(field) {
|
|
76
|
+
return field.format === "number" || field.format === "currency" || field.kind === "integer" || field.kind === "number";
|
|
77
|
+
}
|
|
78
|
+
function formatDate(value, withTime) {
|
|
79
|
+
const date = new Date(String(value));
|
|
80
|
+
if (Number.isNaN(date.getTime()))
|
|
81
|
+
return String(value);
|
|
82
|
+
return withTime ? date.toLocaleString("en-US") : date.toLocaleDateString("en-US");
|
|
83
|
+
}
|
|
84
|
+
function displayValue(field, value) {
|
|
85
|
+
if (value === null || value === undefined)
|
|
86
|
+
return "";
|
|
87
|
+
if (field?.relation) {
|
|
88
|
+
const match = field.relation.find((o) => String(o.value) === String(value));
|
|
89
|
+
if (match)
|
|
90
|
+
return match.label;
|
|
91
|
+
}
|
|
92
|
+
switch (field?.format) {
|
|
93
|
+
case "boolean":
|
|
94
|
+
return value === true || value === "true" || value === 1 ? "Yes" : "No";
|
|
95
|
+
case "date":
|
|
96
|
+
return formatDate(value, false);
|
|
97
|
+
case "datetime":
|
|
98
|
+
return formatDate(value, true);
|
|
99
|
+
case "currency": {
|
|
100
|
+
const amount = Number(value);
|
|
101
|
+
if (Number.isNaN(amount))
|
|
102
|
+
return String(value);
|
|
103
|
+
return new Intl.NumberFormat("en-US", {
|
|
104
|
+
style: "currency",
|
|
105
|
+
currency: field.currency ?? "USD"
|
|
106
|
+
}).format(amount);
|
|
107
|
+
}
|
|
108
|
+
case "number":
|
|
109
|
+
return Number.isNaN(Number(value)) ? String(value) : Number(value).toLocaleString("en-US");
|
|
110
|
+
case "json":
|
|
111
|
+
return typeof value === "object" ? JSON.stringify(value) : String(value);
|
|
112
|
+
default:
|
|
113
|
+
return typeof value === "object" ? JSON.stringify(value) : String(value);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
function coerceFieldValue(field, raw) {
|
|
117
|
+
if (!field)
|
|
118
|
+
return { value: raw };
|
|
119
|
+
switch (field.kind) {
|
|
120
|
+
case "integer":
|
|
121
|
+
case "number": {
|
|
122
|
+
const n = Number(raw);
|
|
123
|
+
if (raw.trim() === "" || Number.isNaN(n))
|
|
124
|
+
return { error: `${field.label} must be a number` };
|
|
125
|
+
return { value: n };
|
|
126
|
+
}
|
|
127
|
+
case "boolean":
|
|
128
|
+
return { value: raw === "true" || raw === "1" };
|
|
129
|
+
case "json":
|
|
130
|
+
try {
|
|
131
|
+
return { value: JSON.parse(raw) };
|
|
132
|
+
} catch {
|
|
133
|
+
return { error: `${field.label} is not valid JSON` };
|
|
134
|
+
}
|
|
135
|
+
default:
|
|
136
|
+
return { value: raw };
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
74
140
|
// src/react/page/client/panel.ts
|
|
75
141
|
function debounce(fn, wait) {
|
|
76
142
|
let id;
|
|
@@ -82,23 +148,6 @@ function debounce(fn, wait) {
|
|
|
82
148
|
function esc(value) {
|
|
83
149
|
return String(value ?? "").split("&").join("&").split("<").join("<").split(">").join(">").split('"').join(""");
|
|
84
150
|
}
|
|
85
|
-
function cellText(value) {
|
|
86
|
-
if (value === null || value === undefined)
|
|
87
|
-
return "";
|
|
88
|
-
if (typeof value === "object")
|
|
89
|
-
return JSON.stringify(value);
|
|
90
|
-
return String(value);
|
|
91
|
-
}
|
|
92
|
-
function badgeClass(col, value) {
|
|
93
|
-
if (col !== "status" || typeof value !== "string")
|
|
94
|
-
return null;
|
|
95
|
-
const text = value.toLowerCase();
|
|
96
|
-
if (text === "active")
|
|
97
|
-
return "bg-success-muted text-success-muted-foreground";
|
|
98
|
-
if (text === "archived")
|
|
99
|
-
return "bg-warning-muted text-warning-muted-foreground";
|
|
100
|
-
return "bg-badge-background text-badge-foreground";
|
|
101
|
-
}
|
|
102
151
|
function toParams(sp) {
|
|
103
152
|
const out = {};
|
|
104
153
|
sp.forEach((v, k) => {
|
|
@@ -122,11 +171,87 @@ function setErr(form, msg) {
|
|
|
122
171
|
if (el)
|
|
123
172
|
el.textContent = msg;
|
|
124
173
|
}
|
|
174
|
+
function setBusy(el, busy) {
|
|
175
|
+
if (!el)
|
|
176
|
+
return;
|
|
177
|
+
el.disabled = busy;
|
|
178
|
+
if (busy)
|
|
179
|
+
el.setAttribute("aria-busy", "true");
|
|
180
|
+
else
|
|
181
|
+
el.removeAttribute("aria-busy");
|
|
182
|
+
}
|
|
183
|
+
function toast(root, message, error = false) {
|
|
184
|
+
const box = root.querySelector("[data-rack-toasts]");
|
|
185
|
+
if (!box)
|
|
186
|
+
return;
|
|
187
|
+
const el = document.createElement("div");
|
|
188
|
+
el.className = error ? "bg-destructive text-destructive-foreground pointer-events-auto rounded-md px-3.5 py-2 text-sm shadow-lg" : "bg-card text-card-foreground border-border pointer-events-auto rounded-md border px-3.5 py-2 text-sm shadow-lg";
|
|
189
|
+
el.textContent = message;
|
|
190
|
+
box.appendChild(el);
|
|
191
|
+
window.setTimeout(() => el.remove(), 4000);
|
|
192
|
+
}
|
|
125
193
|
function refresh(root) {
|
|
126
194
|
run(root, new URLSearchParams(window.location.search));
|
|
127
195
|
}
|
|
128
|
-
function
|
|
129
|
-
|
|
196
|
+
function parse(raw, fallback) {
|
|
197
|
+
try {
|
|
198
|
+
return raw ? JSON.parse(raw) : fallback;
|
|
199
|
+
} catch {
|
|
200
|
+
return fallback;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
function tableOf(root) {
|
|
204
|
+
return root.querySelector("table");
|
|
205
|
+
}
|
|
206
|
+
function tableConfig(root) {
|
|
207
|
+
const tbl = tableOf(root);
|
|
208
|
+
const fields = parse(tbl?.getAttribute("data-rack-fields"), []);
|
|
209
|
+
return {
|
|
210
|
+
fields,
|
|
211
|
+
byName: new Map(fields.map((f) => [f.name, f])),
|
|
212
|
+
actions: parse(tbl?.getAttribute("data-rack-actions"), []),
|
|
213
|
+
pk: tbl?.getAttribute("data-pk") || "id",
|
|
214
|
+
selectable: !!tbl?.hasAttribute("data-selectable"),
|
|
215
|
+
editable: !!tbl?.hasAttribute("data-editable"),
|
|
216
|
+
viewable: !!tbl?.hasAttribute("data-viewable"),
|
|
217
|
+
softDelete: !!tbl?.hasAttribute("data-soft-delete"),
|
|
218
|
+
restorable: !!tbl?.hasAttribute("data-restorable"),
|
|
219
|
+
deletedAt: tbl?.getAttribute("data-deleted-at") || "deletedAt"
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
function badgeClass(value) {
|
|
223
|
+
if (typeof value !== "string")
|
|
224
|
+
return "bg-badge-background text-badge-foreground";
|
|
225
|
+
const text = value.toLowerCase();
|
|
226
|
+
if (text === "active")
|
|
227
|
+
return "bg-success-muted text-success-muted-foreground";
|
|
228
|
+
if (text === "archived")
|
|
229
|
+
return "bg-warning-muted text-warning-muted-foreground";
|
|
230
|
+
return "bg-badge-background text-badge-foreground";
|
|
231
|
+
}
|
|
232
|
+
function actionButton(action, id) {
|
|
233
|
+
const target = esc(action.href.replaceAll(":id", encodeURIComponent(id)));
|
|
234
|
+
const cls = "text-link hover:text-link-hover ml-3 px-1 text-sm font-medium hover:underline";
|
|
235
|
+
if (action.method) {
|
|
236
|
+
const confirm = action.confirm ? ` data-rack-confirm="${esc(action.confirm)}"` : "";
|
|
237
|
+
return `<button type="button" data-rack-href="${target}" data-rack-method="${esc(action.method)}"${confirm} class="${cls}">${esc(action.label)}</button>`;
|
|
238
|
+
}
|
|
239
|
+
return `<a href="${target}" class="${cls}">${esc(action.label)}</a>`;
|
|
240
|
+
}
|
|
241
|
+
function actionCell(id, cfg, deleted) {
|
|
242
|
+
let inner = "";
|
|
243
|
+
if (deleted) {
|
|
244
|
+
if (cfg.restorable)
|
|
245
|
+
inner = `<button type="button" data-restore-id="${esc(id)}" class="text-link hover:text-link-hover px-1 text-sm font-medium hover:underline">Restore</button>`;
|
|
246
|
+
} else {
|
|
247
|
+
if (cfg.viewable)
|
|
248
|
+
inner += `<button type="button" data-view-id="${esc(id)}" class="text-link hover:text-link-hover px-1 text-sm font-medium hover:underline">View</button>`;
|
|
249
|
+
if (cfg.editable)
|
|
250
|
+
inner += `<button type="button" data-edit-id="${esc(id)}" class="text-link hover:text-link-hover ${cfg.viewable ? "ml-3 " : ""}px-1 text-sm font-medium hover:underline">Edit</button>` + `<button type="button" data-delete-id="${esc(id)}" class="text-destructive ml-3 px-1 text-sm font-medium hover:underline">Delete</button>`;
|
|
251
|
+
for (const action of cfg.actions)
|
|
252
|
+
inner += actionButton(action, id);
|
|
253
|
+
}
|
|
254
|
+
return `<td class="border-table-border border-t px-3 py-2.5 whitespace-nowrap text-right">${inner}</td>`;
|
|
130
255
|
}
|
|
131
256
|
function numericCols(rows, cols) {
|
|
132
257
|
const out = new Set;
|
|
@@ -138,26 +263,35 @@ function numericCols(rows, cols) {
|
|
|
138
263
|
}
|
|
139
264
|
return out;
|
|
140
265
|
}
|
|
141
|
-
function
|
|
142
|
-
|
|
266
|
+
function cellHtml(field, value) {
|
|
267
|
+
if (value === null || value === undefined)
|
|
268
|
+
return "";
|
|
269
|
+
const text = esc(displayValue(field, value));
|
|
270
|
+
if (field?.format === "badge" || field?.name === "status")
|
|
271
|
+
return `<span class="rounded-sm px-1.5 py-0.5 text-xs font-medium ${badgeClass(value)}">${text}</span>`;
|
|
272
|
+
return text;
|
|
273
|
+
}
|
|
274
|
+
function paintRows(tbody, cols, rows, cfg) {
|
|
275
|
+
const showActions = cfg.editable || cfg.viewable || cfg.softDelete || cfg.actions.length > 0;
|
|
276
|
+
const span = cols.length + (cfg.selectable ? 1 : 0) + (showActions ? 1 : 0);
|
|
143
277
|
if (!rows.length) {
|
|
144
278
|
tbody.innerHTML = `<tr><td colspan="${span || 1}" class="text-text-muted px-4 py-10 text-center">No records found.</td></tr>`;
|
|
145
279
|
return;
|
|
146
280
|
}
|
|
147
281
|
const numeric = numericCols(rows, cols);
|
|
148
282
|
tbody.innerHTML = rows.map((row) => {
|
|
149
|
-
const id = row[pk] == null ? "" : String(row[pk]);
|
|
283
|
+
const id = row[cfg.pk] == null ? "" : String(row[cfg.pk]);
|
|
284
|
+
const deleted = cfg.softDelete && row[cfg.deletedAt] != null;
|
|
150
285
|
let html = `<tr class="hover:bg-table-row-hover">`;
|
|
151
|
-
if (selectable)
|
|
286
|
+
if (cfg.selectable)
|
|
152
287
|
html += `<td class="border-table-border border-t px-3 py-2.5"><input type="checkbox" data-select-row value="${esc(id)}" aria-label="Select row ${esc(id)}" class="accent-primary size-4"></td>`;
|
|
153
288
|
html += cols.map((col) => {
|
|
154
|
-
const
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
return `<td class="border-table-border border-t px-3 py-2.5${align}">${inner}</td>`;
|
|
289
|
+
const field = cfg.byName.get(col);
|
|
290
|
+
const align = numeric.has(col) || field && isNumericField(field) ? " text-right tabular-nums" : "";
|
|
291
|
+
return `<td class="border-table-border border-t px-3 py-2.5${align}">${cellHtml(field, row[col])}</td>`;
|
|
158
292
|
}).join("");
|
|
159
|
-
if (
|
|
160
|
-
html += actionCell(id);
|
|
293
|
+
if (showActions)
|
|
294
|
+
html += actionCell(id, cfg, Boolean(deleted));
|
|
161
295
|
return html + "</tr>";
|
|
162
296
|
}).join("");
|
|
163
297
|
}
|
|
@@ -187,13 +321,10 @@ function paint(root, json, sp) {
|
|
|
187
321
|
const limit = q.limit ?? 20;
|
|
188
322
|
const total = json.total ?? 0;
|
|
189
323
|
const totalPages = Math.max(1, Math.ceil(total / limit));
|
|
190
|
-
const
|
|
191
|
-
const selectable = !!tbl?.hasAttribute("data-selectable");
|
|
192
|
-
const editable = !!tbl?.hasAttribute("data-editable");
|
|
193
|
-
const pk = tbl?.getAttribute("data-pk") || "id";
|
|
324
|
+
const cfg = tableConfig(root);
|
|
194
325
|
const tbody = root.querySelector("[data-panel-rows]");
|
|
195
326
|
if (tbody)
|
|
196
|
-
paintRows(tbody, cols, json.data ?? [],
|
|
327
|
+
paintRows(tbody, cols, json.data ?? [], cfg);
|
|
197
328
|
const count = root.querySelector("[data-panel-count]");
|
|
198
329
|
if (count) {
|
|
199
330
|
const from = total === 0 ? 0 : (page - 1) * limit + 1;
|
|
@@ -253,14 +384,25 @@ function syncBulk(root) {
|
|
|
253
384
|
}
|
|
254
385
|
async function createRecord(root, form) {
|
|
255
386
|
setErr(form, "");
|
|
256
|
-
const
|
|
257
|
-
|
|
387
|
+
const submit = form.querySelector('[type="submit"]');
|
|
388
|
+
setBusy(submit, true);
|
|
389
|
+
try {
|
|
390
|
+
const raw = form.querySelector("[data-create-json]");
|
|
258
391
|
let body;
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
392
|
+
if (raw) {
|
|
393
|
+
try {
|
|
394
|
+
body = JSON.parse(raw.value);
|
|
395
|
+
} catch {
|
|
396
|
+
setErr(form, "Invalid JSON body");
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
} else {
|
|
400
|
+
const collected = collectFormBody(form);
|
|
401
|
+
if (collected.error) {
|
|
402
|
+
setErr(form, collected.error);
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
body = collected.body;
|
|
264
406
|
}
|
|
265
407
|
const res = await fetch(baseUrl(root), {
|
|
266
408
|
method: "POST",
|
|
@@ -268,64 +410,65 @@ async function createRecord(root, form) {
|
|
|
268
410
|
body: JSON.stringify(body)
|
|
269
411
|
});
|
|
270
412
|
if (!res.ok) {
|
|
271
|
-
|
|
413
|
+
const message = `Error ${res.status}: ${(await res.text()).slice(0, 200)}`;
|
|
414
|
+
setErr(form, message);
|
|
415
|
+
toast(root, message, true);
|
|
272
416
|
return;
|
|
273
417
|
}
|
|
274
|
-
|
|
418
|
+
const dlg = form.closest("dialog");
|
|
419
|
+
form.reset();
|
|
420
|
+
if (dlg)
|
|
421
|
+
dlg.close();
|
|
422
|
+
toast(root, "Record created");
|
|
423
|
+
refresh(root);
|
|
424
|
+
} finally {
|
|
425
|
+
setBusy(submit, false);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
async function updateRecord(root, form) {
|
|
429
|
+
setErr(form, "");
|
|
430
|
+
const id = form.dataset?.id || "";
|
|
431
|
+
if (!id) {
|
|
432
|
+
setErr(form, "Missing record id");
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
const submit = form.querySelector('[type="submit"]');
|
|
436
|
+
setBusy(submit, true);
|
|
437
|
+
try {
|
|
275
438
|
const collected = collectFormBody(form);
|
|
276
439
|
if (collected.error) {
|
|
277
440
|
setErr(form, collected.error);
|
|
278
441
|
return;
|
|
279
442
|
}
|
|
280
|
-
const res = await fetch(baseUrl(root)
|
|
281
|
-
method: "
|
|
282
|
-
headers: { "Content-Type": "application/json"
|
|
443
|
+
const res = await fetch(`${baseUrl(root)}/${encodeURIComponent(id)}`, {
|
|
444
|
+
method: "PATCH",
|
|
445
|
+
headers: { "Content-Type": "application/json" },
|
|
283
446
|
body: JSON.stringify(collected.body)
|
|
284
447
|
});
|
|
285
448
|
if (!res.ok) {
|
|
286
|
-
|
|
449
|
+
const message = `Error ${res.status}: ${(await res.text()).slice(0, 200)}`;
|
|
450
|
+
setErr(form, message);
|
|
451
|
+
toast(root, message, true);
|
|
287
452
|
return;
|
|
288
453
|
}
|
|
454
|
+
const dlg = form.closest("dialog");
|
|
455
|
+
if (dlg)
|
|
456
|
+
dlg.close();
|
|
457
|
+
toast(root, "Record saved");
|
|
458
|
+
refresh(root);
|
|
459
|
+
} finally {
|
|
460
|
+
setBusy(submit, false);
|
|
289
461
|
}
|
|
290
|
-
const dlg = form.closest("dialog");
|
|
291
|
-
form.reset();
|
|
292
|
-
if (dlg)
|
|
293
|
-
dlg.close();
|
|
294
|
-
refresh(root);
|
|
295
|
-
}
|
|
296
|
-
async function updateRecord(root, form) {
|
|
297
|
-
setErr(form, "");
|
|
298
|
-
const id = form.dataset?.id || "";
|
|
299
|
-
if (!id) {
|
|
300
|
-
setErr(form, "Missing record id");
|
|
301
|
-
return;
|
|
302
|
-
}
|
|
303
|
-
const collected = collectFormBody(form);
|
|
304
|
-
if (collected.error) {
|
|
305
|
-
setErr(form, collected.error);
|
|
306
|
-
return;
|
|
307
|
-
}
|
|
308
|
-
const res = await fetch(`${baseUrl(root)}/${encodeURIComponent(id)}`, {
|
|
309
|
-
method: "PATCH",
|
|
310
|
-
headers: { "Content-Type": "application/json" },
|
|
311
|
-
body: JSON.stringify(collected.body)
|
|
312
|
-
});
|
|
313
|
-
if (!res.ok) {
|
|
314
|
-
setErr(form, `Error ${res.status}: ${(await res.text()).slice(0, 200)}`);
|
|
315
|
-
return;
|
|
316
|
-
}
|
|
317
|
-
const dlg = form.closest("dialog");
|
|
318
|
-
if (dlg)
|
|
319
|
-
dlg.close();
|
|
320
|
-
refresh(root);
|
|
321
462
|
}
|
|
322
463
|
async function openEdit(root, id) {
|
|
323
464
|
const res = await fetch(`${baseUrl(root)}/data/${encodeURIComponent(id)}`, {
|
|
324
465
|
method: "QUERY",
|
|
325
466
|
headers: { Accept: "application/json" }
|
|
326
467
|
});
|
|
327
|
-
if (!res.ok)
|
|
468
|
+
if (!res.ok) {
|
|
469
|
+
toast(root, `Could not load record ${id}`, true);
|
|
328
470
|
return;
|
|
471
|
+
}
|
|
329
472
|
const row = (await res.json()).data ?? {};
|
|
330
473
|
const dlg = document.getElementById("rack-edit");
|
|
331
474
|
if (!dlg)
|
|
@@ -341,14 +484,58 @@ async function openEdit(root, id) {
|
|
|
341
484
|
setErr(form, "");
|
|
342
485
|
dlg.showModal?.();
|
|
343
486
|
}
|
|
487
|
+
async function openDetail(root, id) {
|
|
488
|
+
const res = await fetch(`${baseUrl(root)}/data/${encodeURIComponent(id)}`, {
|
|
489
|
+
method: "QUERY",
|
|
490
|
+
headers: { Accept: "application/json" }
|
|
491
|
+
});
|
|
492
|
+
if (!res.ok) {
|
|
493
|
+
toast(root, `Could not load record ${id}`, true);
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
const row = (await res.json()).data ?? {};
|
|
497
|
+
const dlg = document.getElementById("rack-detail");
|
|
498
|
+
if (!dlg)
|
|
499
|
+
return;
|
|
500
|
+
const label = dlg.querySelector("[data-detail-id-label]");
|
|
501
|
+
if (label)
|
|
502
|
+
label.textContent = `#${id}`;
|
|
503
|
+
const body = dlg.querySelector("[data-detail-body]");
|
|
504
|
+
if (body)
|
|
505
|
+
body.innerHTML = renderDetail(root, row);
|
|
506
|
+
dlg.showModal?.();
|
|
507
|
+
}
|
|
508
|
+
function renderDetail(root, row) {
|
|
509
|
+
const cfg = tableConfig(root);
|
|
510
|
+
const items = cfg.fields.filter((field) => row[field.name] !== undefined && !field.hideInTable).map((field) => `<div class="flex flex-col gap-0.5"><dt class="text-text-muted text-xs font-medium">${esc(field.label)}</dt><dd class="text-foreground text-sm">${esc(displayValue(field, row[field.name])) || "—"}</dd></div>`);
|
|
511
|
+
return items.join("") || `<p class="text-text-muted text-sm">No fields</p>`;
|
|
512
|
+
}
|
|
344
513
|
async function deleteOne(root, id) {
|
|
345
514
|
if (!window.confirm(`Delete record ${id}?`))
|
|
346
515
|
return;
|
|
347
516
|
const res = await fetch(`${baseUrl(root)}/${encodeURIComponent(id)}`, {
|
|
348
517
|
method: "DELETE"
|
|
349
518
|
});
|
|
350
|
-
if (res.ok)
|
|
519
|
+
if (res.ok) {
|
|
520
|
+
toast(root, "Record deleted");
|
|
351
521
|
refresh(root);
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
toast(root, `Error ${res.status}: ${(await res.text()).slice(0, 200)}`, true);
|
|
525
|
+
}
|
|
526
|
+
async function restoreOne(root, id) {
|
|
527
|
+
const cfg = tableConfig(root);
|
|
528
|
+
const res = await fetch(`${baseUrl(root)}/${encodeURIComponent(id)}`, {
|
|
529
|
+
method: "PATCH",
|
|
530
|
+
headers: { "Content-Type": "application/json" },
|
|
531
|
+
body: JSON.stringify({ [cfg.deletedAt]: null })
|
|
532
|
+
});
|
|
533
|
+
if (res.ok) {
|
|
534
|
+
toast(root, "Record restored");
|
|
535
|
+
refresh(root);
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
toast(root, `Error ${res.status}: ${(await res.text()).slice(0, 200)}`, true);
|
|
352
539
|
}
|
|
353
540
|
async function bulkDelete(root) {
|
|
354
541
|
const ids = selectedIds(root);
|
|
@@ -357,9 +544,16 @@ async function bulkDelete(root) {
|
|
|
357
544
|
if (!window.confirm(`Delete ${ids.length} records?`))
|
|
358
545
|
return;
|
|
359
546
|
const base = baseUrl(root);
|
|
547
|
+
let failed = 0;
|
|
360
548
|
for (const id of ids) {
|
|
361
|
-
await fetch(`${base}/${encodeURIComponent(id)}`, { method: "DELETE" });
|
|
549
|
+
const res = await fetch(`${base}/${encodeURIComponent(id)}`, { method: "DELETE" });
|
|
550
|
+
if (!res.ok)
|
|
551
|
+
failed++;
|
|
362
552
|
}
|
|
553
|
+
if (failed > 0)
|
|
554
|
+
toast(root, `${failed} of ${ids.length} deletes failed`, true);
|
|
555
|
+
else
|
|
556
|
+
toast(root, `${ids.length} record${ids.length === 1 ? "" : "s"} deleted`);
|
|
363
557
|
refresh(root);
|
|
364
558
|
}
|
|
365
559
|
async function bulkApply(root) {
|
|
@@ -368,18 +562,47 @@ async function bulkApply(root) {
|
|
|
368
562
|
const ids = selectedIds(root);
|
|
369
563
|
if (!field?.value || !input || !ids.length)
|
|
370
564
|
return;
|
|
565
|
+
const cfg = tableConfig(root);
|
|
566
|
+
const meta = cfg.byName.get(field.value);
|
|
567
|
+
const coerced = coerceFieldValue(meta, input.value);
|
|
568
|
+
if (coerced.error) {
|
|
569
|
+
toast(root, coerced.error, true);
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
371
572
|
const base = baseUrl(root);
|
|
372
|
-
const payload = JSON.stringify({ [field.value]:
|
|
573
|
+
const payload = JSON.stringify({ [field.value]: coerced.value });
|
|
574
|
+
let failed = 0;
|
|
373
575
|
for (const id of ids) {
|
|
374
|
-
await fetch(`${base}/${encodeURIComponent(id)}`, {
|
|
576
|
+
const res = await fetch(`${base}/${encodeURIComponent(id)}`, {
|
|
375
577
|
method: "PATCH",
|
|
376
578
|
headers: { "Content-Type": "application/json" },
|
|
377
579
|
body: payload
|
|
378
580
|
});
|
|
581
|
+
if (!res.ok)
|
|
582
|
+
failed++;
|
|
379
583
|
}
|
|
380
584
|
input.value = "";
|
|
585
|
+
if (failed > 0)
|
|
586
|
+
toast(root, `${failed} of ${ids.length} updates failed`, true);
|
|
587
|
+
else
|
|
588
|
+
toast(root, `${ids.length} record${ids.length === 1 ? "" : "s"} updated`);
|
|
381
589
|
refresh(root);
|
|
382
590
|
}
|
|
591
|
+
async function runCustomAction(root, el) {
|
|
592
|
+
const target = el.getAttribute("data-rack-href");
|
|
593
|
+
if (!target)
|
|
594
|
+
return;
|
|
595
|
+
const confirmMsg = el.getAttribute("data-rack-confirm");
|
|
596
|
+
if (confirmMsg && !window.confirm(confirmMsg))
|
|
597
|
+
return;
|
|
598
|
+
const res = await fetch(target, {
|
|
599
|
+
method: el.getAttribute("data-rack-method") || "GET"
|
|
600
|
+
});
|
|
601
|
+
if (res.ok)
|
|
602
|
+
refresh(root);
|
|
603
|
+
else
|
|
604
|
+
toast(root, `Error ${res.status}: ${(await res.text()).slice(0, 200)}`, true);
|
|
605
|
+
}
|
|
383
606
|
document.addEventListener("click", (e) => {
|
|
384
607
|
const t = e.target;
|
|
385
608
|
if (!t?.closest)
|
|
@@ -392,6 +615,12 @@ document.addEventListener("click", (e) => {
|
|
|
392
615
|
closer.closest("dialog")?.close();
|
|
393
616
|
return;
|
|
394
617
|
}
|
|
618
|
+
const custom = t.closest("[data-rack-href]");
|
|
619
|
+
if (custom) {
|
|
620
|
+
e.preventDefault();
|
|
621
|
+
runCustomAction(root, custom);
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
395
624
|
if (t.closest("[data-open-create]")) {
|
|
396
625
|
const dlg = document.getElementById("rack-create");
|
|
397
626
|
if (dlg) {
|
|
@@ -404,6 +633,16 @@ document.addEventListener("click", (e) => {
|
|
|
404
633
|
}
|
|
405
634
|
return;
|
|
406
635
|
}
|
|
636
|
+
const view = t.closest("[data-view-id]");
|
|
637
|
+
if (view) {
|
|
638
|
+
openDetail(root, view.getAttribute("data-view-id"));
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
const restore = t.closest("[data-restore-id]");
|
|
642
|
+
if (restore) {
|
|
643
|
+
restoreOne(root, restore.getAttribute("data-restore-id"));
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
407
646
|
const eb = t.closest("[data-edit-id]");
|
|
408
647
|
if (eb) {
|
|
409
648
|
openEdit(root, eb.getAttribute("data-edit-id"));
|