elysia-rack 0.1.2 → 0.2.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.
- package/README.md +183 -27
- package/dist/Dashboard-s8nfyf7s.js +39 -0
- package/dist/Panel-3yrk3krj.js +914 -0
- package/dist/Panel-b7jhgvnm.js +916 -0
- package/dist/Panel-pkvqf5v8.js +912 -0
- package/dist/panel-app.js +498 -1
- package/dist/panel-theme.js +38 -1
- package/dist/rack/index.js +13 -1
- package/dist/react/index.d.ts +1 -1
- package/dist/react/index.js +12 -4
- package/dist/react/page/Dashboard.d.ts +0 -3
- package/dist/react/types.d.ts +1 -1
- package/package.json +3 -2
package/dist/panel-app.js
CHANGED
|
@@ -1 +1,498 @@
|
|
|
1
|
-
|
|
1
|
+
// src/react/page/components/href.ts
|
|
2
|
+
function href(params, over = {}) {
|
|
3
|
+
const query = new URLSearchParams;
|
|
4
|
+
for (const [key, value] of Object.entries({ ...params, ...over })) {
|
|
5
|
+
if (value === undefined || value === null || value === "")
|
|
6
|
+
continue;
|
|
7
|
+
query.set(key, Array.isArray(value) ? value.join(",") : String(value));
|
|
8
|
+
}
|
|
9
|
+
const str = query.toString();
|
|
10
|
+
return str ? `?${str}` : "?";
|
|
11
|
+
}
|
|
12
|
+
function pageWindow(page, total) {
|
|
13
|
+
const keep = new Set([1, total, page - 1, page, page + 1]);
|
|
14
|
+
const sorted = [...keep].filter((p) => p >= 1 && p <= total).sort((a, b) => a - b);
|
|
15
|
+
const out = [];
|
|
16
|
+
let prev = 0;
|
|
17
|
+
for (const p of sorted) {
|
|
18
|
+
if (p - prev > 1)
|
|
19
|
+
out.push("…");
|
|
20
|
+
out.push(p);
|
|
21
|
+
prev = p;
|
|
22
|
+
}
|
|
23
|
+
return out;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// src/react/page/components/panelForm.ts
|
|
27
|
+
function collectFormBody(form) {
|
|
28
|
+
const body = {};
|
|
29
|
+
const els = form.elements || [];
|
|
30
|
+
for (let i = 0;i < els.length; i++) {
|
|
31
|
+
const el = els[i];
|
|
32
|
+
if (!el || !el.name || el.disabled)
|
|
33
|
+
continue;
|
|
34
|
+
if (el.type === "checkbox") {
|
|
35
|
+
if (el.checked)
|
|
36
|
+
body[el.name] = true;
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (el.value === "" || el.value === undefined || el.value === null)
|
|
40
|
+
continue;
|
|
41
|
+
if (el.hasAttribute && el.hasAttribute("data-json")) {
|
|
42
|
+
try {
|
|
43
|
+
body[el.name] = JSON.parse(el.value);
|
|
44
|
+
} catch {
|
|
45
|
+
return { error: `Invalid JSON in ${el.name}` };
|
|
46
|
+
}
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
body[el.name] = el.value;
|
|
50
|
+
}
|
|
51
|
+
return { body };
|
|
52
|
+
}
|
|
53
|
+
function fillForm(form, row) {
|
|
54
|
+
const els = form.elements || [];
|
|
55
|
+
for (let i = 0;i < els.length; i++) {
|
|
56
|
+
const el = els[i];
|
|
57
|
+
if (!el || !el.name)
|
|
58
|
+
continue;
|
|
59
|
+
const v = row[el.name];
|
|
60
|
+
if (v === undefined || v === null)
|
|
61
|
+
continue;
|
|
62
|
+
if (el.type === "checkbox") {
|
|
63
|
+
el.checked = v === true || v === "true" || v === 1;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (el.type === "datetime-local" && typeof v === "string") {
|
|
67
|
+
el.value = v.slice(0, 16);
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
el.value = typeof v === "object" ? JSON.stringify(v) : String(v);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// src/react/page/client/panel.ts
|
|
75
|
+
function debounce(fn, wait) {
|
|
76
|
+
let id;
|
|
77
|
+
return (...args) => {
|
|
78
|
+
clearTimeout(id);
|
|
79
|
+
id = setTimeout(() => fn(...args), wait);
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
function esc(value) {
|
|
83
|
+
return String(value ?? "").split("&").join("&").split("<").join("<").split(">").join(">").split('"').join(""");
|
|
84
|
+
}
|
|
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
|
+
function toParams(sp) {
|
|
103
|
+
const out = {};
|
|
104
|
+
sp.forEach((v, k) => {
|
|
105
|
+
out[k] = v;
|
|
106
|
+
});
|
|
107
|
+
return out;
|
|
108
|
+
}
|
|
109
|
+
function baseUrl(root) {
|
|
110
|
+
const q = root.getAttribute("data-query-url") || "";
|
|
111
|
+
return q.slice(-5) === "/data" ? q.slice(0, -5) : q || "?";
|
|
112
|
+
}
|
|
113
|
+
function uid() {
|
|
114
|
+
try {
|
|
115
|
+
if (window.crypto?.randomUUID)
|
|
116
|
+
return window.crypto.randomUUID();
|
|
117
|
+
} catch {}
|
|
118
|
+
return `k-${Date.now().toString(36)}-${Math.floor(Math.random() * 1e9).toString(36)}`;
|
|
119
|
+
}
|
|
120
|
+
function setErr(form, msg) {
|
|
121
|
+
const el = form.querySelector("[data-form-error]");
|
|
122
|
+
if (el)
|
|
123
|
+
el.textContent = msg;
|
|
124
|
+
}
|
|
125
|
+
function refresh(root) {
|
|
126
|
+
run(root, new URLSearchParams(window.location.search));
|
|
127
|
+
}
|
|
128
|
+
function actionCell(id) {
|
|
129
|
+
return `<td class="border-table-border border-t px-4 py-2 whitespace-nowrap">` + `<div class="border-table-border inline-flex overflow-hidden rounded-md border">` + `<button type="button" data-edit-id="${esc(id)}" class="text-link hover:bg-table-row-hover px-2 py-1 text-sm font-semibold">Edit</button>` + `<button type="button" data-delete-id="${esc(id)}" class="border-table-border text-destructive hover:bg-table-row-hover border-l px-2 py-1 text-sm font-semibold">Delete</button>` + `</div></td>`;
|
|
130
|
+
}
|
|
131
|
+
function paintRows(tbody, cols, rows, pk, selectable, editable) {
|
|
132
|
+
if (!rows.length) {
|
|
133
|
+
tbody.innerHTML = `<tr><td colspan="${cols.length}" class="text-text-muted px-4 py-10 text-center">No records found.</td></tr>`;
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
tbody.innerHTML = rows.map((row) => {
|
|
137
|
+
const id = row[pk] == null ? "" : String(row[pk]);
|
|
138
|
+
let html = `<tr class="hover:bg-table-row-hover">`;
|
|
139
|
+
if (selectable)
|
|
140
|
+
html += `<td class="border-table-border border-t px-4 py-2"><input type="checkbox" data-select-row value="${esc(id)}" aria-label="Select row ${esc(id)}"></td>`;
|
|
141
|
+
html += cols.map((col) => {
|
|
142
|
+
const cls = badgeClass(col, row[col]);
|
|
143
|
+
const inner = cls ? `<span class="rounded-full px-2 py-0.5 text-xs font-semibold ${cls}">${esc(cellText(row[col]))}</span>` : esc(cellText(row[col]));
|
|
144
|
+
return `<td class="border-table-border border-t px-4 py-2">${inner}</td>`;
|
|
145
|
+
}).join("");
|
|
146
|
+
if (editable)
|
|
147
|
+
html += actionCell(id);
|
|
148
|
+
return html + "</tr>";
|
|
149
|
+
}).join("");
|
|
150
|
+
}
|
|
151
|
+
function paintPages(box, params, page, totalPages) {
|
|
152
|
+
const linkCls = "border-border rounded-md border px-3 py-1 text-sm hover:bg-navigation-hover";
|
|
153
|
+
const offCls = "border-border text-text-disabled rounded-md border px-3 py-1 text-sm";
|
|
154
|
+
let html = page > 1 ? `<a class="${linkCls}" data-qlink href="${esc(href(params, { page: page - 1 }))}">← Previous</a>` : `<span class="${offCls}">← Previous</span>`;
|
|
155
|
+
for (const p of pageWindow(page, totalPages)) {
|
|
156
|
+
if (p === "…")
|
|
157
|
+
html += `<span class="text-text-muted px-1">…</span>`;
|
|
158
|
+
else if (p === page)
|
|
159
|
+
html += `<span class="bg-primary text-primary-foreground rounded-md px-3 py-1 text-sm font-bold">${p}</span>`;
|
|
160
|
+
else
|
|
161
|
+
html += `<a class="${linkCls}" data-qlink href="${esc(href(params, { page: p }))}">${p}</a>`;
|
|
162
|
+
}
|
|
163
|
+
html += page < totalPages ? `<a class="${linkCls}" data-qlink href="${esc(href(params, { page: page + 1 }))}">Next →</a>` : `<span class="${offCls}">Next →</span>`;
|
|
164
|
+
box.innerHTML = html;
|
|
165
|
+
}
|
|
166
|
+
function paint(root, json, sp) {
|
|
167
|
+
const params = toParams(sp);
|
|
168
|
+
const cols = [];
|
|
169
|
+
Array.prototype.forEach.call(root.querySelectorAll("thead th[data-col]"), (th) => {
|
|
170
|
+
cols.push(th.getAttribute("data-col"));
|
|
171
|
+
});
|
|
172
|
+
const q = json.query ?? {};
|
|
173
|
+
const page = q.page ?? 1;
|
|
174
|
+
const limit = q.limit ?? 20;
|
|
175
|
+
const total = json.total ?? 0;
|
|
176
|
+
const totalPages = Math.max(1, Math.ceil(total / limit));
|
|
177
|
+
const tbl = root.querySelector("table");
|
|
178
|
+
const selectable = !!tbl?.hasAttribute("data-selectable");
|
|
179
|
+
const editable = !!tbl?.hasAttribute("data-editable");
|
|
180
|
+
const pk = tbl?.getAttribute("data-pk") || "id";
|
|
181
|
+
const tbody = root.querySelector("[data-panel-rows]");
|
|
182
|
+
if (tbody)
|
|
183
|
+
paintRows(tbody, cols, json.data ?? [], pk, selectable, editable);
|
|
184
|
+
const count = root.querySelector("[data-panel-count]");
|
|
185
|
+
if (count) {
|
|
186
|
+
const from = total === 0 ? 0 : (page - 1) * limit + 1;
|
|
187
|
+
count.textContent = `Page ${page} of ${totalPages} — ${from}–${Math.min(page * limit, total)} of ${total} records`;
|
|
188
|
+
}
|
|
189
|
+
const box = root.querySelector("[data-panel-pages]");
|
|
190
|
+
if (box)
|
|
191
|
+
paintPages(box, params, page, totalPages);
|
|
192
|
+
const sort = q.sort ?? {};
|
|
193
|
+
Array.prototype.forEach.call(root.querySelectorAll("a[data-sort-link]"), (a) => {
|
|
194
|
+
const col = a.getAttribute("data-sort-link");
|
|
195
|
+
const active = sort.field === col;
|
|
196
|
+
const order = active && sort.direction === "asc" ? "desc" : "asc";
|
|
197
|
+
a.setAttribute("href", href(params, { sort: col, order }));
|
|
198
|
+
const ind = a.querySelector("[data-sort-ind]");
|
|
199
|
+
if (ind)
|
|
200
|
+
ind.textContent = active ? sort.direction === "asc" ? " ▲" : " ▼" : "";
|
|
201
|
+
});
|
|
202
|
+
try {
|
|
203
|
+
window.history.replaceState(null, "", href(params, {}));
|
|
204
|
+
} catch {}
|
|
205
|
+
syncBulk(root);
|
|
206
|
+
}
|
|
207
|
+
async function run(root, sp) {
|
|
208
|
+
const base = root.getAttribute("data-query-url") || "";
|
|
209
|
+
const search = sp.toString();
|
|
210
|
+
const res = await fetch(base + (search ? `?${search}` : ""), {
|
|
211
|
+
method: "QUERY",
|
|
212
|
+
headers: { Accept: "application/json" }
|
|
213
|
+
});
|
|
214
|
+
if (!res.ok)
|
|
215
|
+
return;
|
|
216
|
+
paint(root, await res.json(), sp);
|
|
217
|
+
}
|
|
218
|
+
function selectedIds(root) {
|
|
219
|
+
const ids = [];
|
|
220
|
+
Array.prototype.forEach.call(root.querySelectorAll("[data-select-row]:checked"), (c) => {
|
|
221
|
+
ids.push(c.value);
|
|
222
|
+
});
|
|
223
|
+
return ids;
|
|
224
|
+
}
|
|
225
|
+
function syncBulk(root) {
|
|
226
|
+
const ids = selectedIds(root);
|
|
227
|
+
const bar = root.querySelector("[data-bulk-bar]");
|
|
228
|
+
if (bar)
|
|
229
|
+
bar.style.display = ids.length ? "" : "none";
|
|
230
|
+
const count = root.querySelector("[data-bulk-count]");
|
|
231
|
+
if (count)
|
|
232
|
+
count.textContent = `${ids.length} selected`;
|
|
233
|
+
const boxes = root.querySelectorAll("[data-select-row]");
|
|
234
|
+
const all = root.querySelector("[data-select-all]");
|
|
235
|
+
if (all)
|
|
236
|
+
all.checked = boxes.length > 0 && ids.length === boxes.length;
|
|
237
|
+
}
|
|
238
|
+
async function createRecord(root, form) {
|
|
239
|
+
setErr(form, "");
|
|
240
|
+
const raw = form.querySelector("[data-create-json]");
|
|
241
|
+
if (raw) {
|
|
242
|
+
let body;
|
|
243
|
+
try {
|
|
244
|
+
body = JSON.parse(raw.value);
|
|
245
|
+
} catch {
|
|
246
|
+
setErr(form, "Invalid JSON body");
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
const res = await fetch(baseUrl(root), {
|
|
250
|
+
method: "POST",
|
|
251
|
+
headers: { "Content-Type": "application/json", "Idempotency-Key": uid() },
|
|
252
|
+
body: JSON.stringify(body)
|
|
253
|
+
});
|
|
254
|
+
if (!res.ok) {
|
|
255
|
+
setErr(form, `Error ${res.status}: ${(await res.text()).slice(0, 200)}`);
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
} else {
|
|
259
|
+
const collected = collectFormBody(form);
|
|
260
|
+
if (collected.error) {
|
|
261
|
+
setErr(form, collected.error);
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
const res = await fetch(baseUrl(root), {
|
|
265
|
+
method: "POST",
|
|
266
|
+
headers: { "Content-Type": "application/json", "Idempotency-Key": uid() },
|
|
267
|
+
body: JSON.stringify(collected.body)
|
|
268
|
+
});
|
|
269
|
+
if (!res.ok) {
|
|
270
|
+
setErr(form, `Error ${res.status}: ${(await res.text()).slice(0, 200)}`);
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
const dlg = form.closest("dialog");
|
|
275
|
+
form.reset();
|
|
276
|
+
if (dlg)
|
|
277
|
+
dlg.close();
|
|
278
|
+
refresh(root);
|
|
279
|
+
}
|
|
280
|
+
async function updateRecord(root, form) {
|
|
281
|
+
setErr(form, "");
|
|
282
|
+
const id = form.dataset?.id || "";
|
|
283
|
+
if (!id) {
|
|
284
|
+
setErr(form, "Missing record id");
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
const collected = collectFormBody(form);
|
|
288
|
+
if (collected.error) {
|
|
289
|
+
setErr(form, collected.error);
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
const res = await fetch(`${baseUrl(root)}/${encodeURIComponent(id)}`, {
|
|
293
|
+
method: "PATCH",
|
|
294
|
+
headers: { "Content-Type": "application/json" },
|
|
295
|
+
body: JSON.stringify(collected.body)
|
|
296
|
+
});
|
|
297
|
+
if (!res.ok) {
|
|
298
|
+
setErr(form, `Error ${res.status}: ${(await res.text()).slice(0, 200)}`);
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
const dlg = form.closest("dialog");
|
|
302
|
+
if (dlg)
|
|
303
|
+
dlg.close();
|
|
304
|
+
refresh(root);
|
|
305
|
+
}
|
|
306
|
+
async function openEdit(root, id) {
|
|
307
|
+
const res = await fetch(`${baseUrl(root)}/data/${encodeURIComponent(id)}`, {
|
|
308
|
+
method: "QUERY",
|
|
309
|
+
headers: { Accept: "application/json" }
|
|
310
|
+
});
|
|
311
|
+
if (!res.ok)
|
|
312
|
+
return;
|
|
313
|
+
const row = (await res.json()).data ?? {};
|
|
314
|
+
const dlg = document.getElementById("rack-edit");
|
|
315
|
+
if (!dlg)
|
|
316
|
+
return;
|
|
317
|
+
const form = dlg.querySelector("[data-edit-form]");
|
|
318
|
+
if (!form)
|
|
319
|
+
return;
|
|
320
|
+
form.dataset.id = id;
|
|
321
|
+
const label = dlg.querySelector("[data-edit-id-label]");
|
|
322
|
+
if (label)
|
|
323
|
+
label.textContent = `#${id}`;
|
|
324
|
+
fillForm(form, row);
|
|
325
|
+
setErr(form, "");
|
|
326
|
+
dlg.showModal?.();
|
|
327
|
+
}
|
|
328
|
+
async function deleteOne(root, id) {
|
|
329
|
+
if (!window.confirm(`Delete record ${id}?`))
|
|
330
|
+
return;
|
|
331
|
+
const res = await fetch(`${baseUrl(root)}/${encodeURIComponent(id)}`, {
|
|
332
|
+
method: "DELETE"
|
|
333
|
+
});
|
|
334
|
+
if (res.ok)
|
|
335
|
+
refresh(root);
|
|
336
|
+
}
|
|
337
|
+
async function bulkDelete(root) {
|
|
338
|
+
const ids = selectedIds(root);
|
|
339
|
+
if (!ids.length)
|
|
340
|
+
return;
|
|
341
|
+
if (!window.confirm(`Delete ${ids.length} records?`))
|
|
342
|
+
return;
|
|
343
|
+
const base = baseUrl(root);
|
|
344
|
+
for (const id of ids) {
|
|
345
|
+
await fetch(`${base}/${encodeURIComponent(id)}`, { method: "DELETE" });
|
|
346
|
+
}
|
|
347
|
+
refresh(root);
|
|
348
|
+
}
|
|
349
|
+
async function bulkApply(root) {
|
|
350
|
+
const field = root.querySelector("[data-bulk-field]");
|
|
351
|
+
const input = root.querySelector("[data-bulk-value]");
|
|
352
|
+
const ids = selectedIds(root);
|
|
353
|
+
if (!field?.value || !input || !ids.length)
|
|
354
|
+
return;
|
|
355
|
+
const base = baseUrl(root);
|
|
356
|
+
const payload = JSON.stringify({ [field.value]: input.value });
|
|
357
|
+
for (const id of ids) {
|
|
358
|
+
await fetch(`${base}/${encodeURIComponent(id)}`, {
|
|
359
|
+
method: "PATCH",
|
|
360
|
+
headers: { "Content-Type": "application/json" },
|
|
361
|
+
body: payload
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
input.value = "";
|
|
365
|
+
refresh(root);
|
|
366
|
+
}
|
|
367
|
+
document.addEventListener("click", (e) => {
|
|
368
|
+
const t = e.target;
|
|
369
|
+
if (!t?.closest)
|
|
370
|
+
return;
|
|
371
|
+
const root = t.closest("[data-query-url]");
|
|
372
|
+
if (!root)
|
|
373
|
+
return;
|
|
374
|
+
const closer = t.closest("[data-close]");
|
|
375
|
+
if (closer) {
|
|
376
|
+
closer.closest("dialog")?.close();
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
if (t.closest("[data-open-create]")) {
|
|
380
|
+
const dlg = document.getElementById("rack-create");
|
|
381
|
+
if (dlg) {
|
|
382
|
+
const cf = dlg.querySelector("[data-create-form]");
|
|
383
|
+
if (cf) {
|
|
384
|
+
cf.reset();
|
|
385
|
+
setErr(cf, "");
|
|
386
|
+
}
|
|
387
|
+
dlg.showModal?.();
|
|
388
|
+
}
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
const eb = t.closest("[data-edit-id]");
|
|
392
|
+
if (eb) {
|
|
393
|
+
openEdit(root, eb.getAttribute("data-edit-id"));
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
const dbtn = t.closest("[data-delete-id]");
|
|
397
|
+
if (dbtn) {
|
|
398
|
+
deleteOne(root, dbtn.getAttribute("data-delete-id"));
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
if (t.closest("[data-bulk-delete]")) {
|
|
402
|
+
bulkDelete(root);
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
if (t.closest("[data-bulk-apply]")) {
|
|
406
|
+
bulkApply(root);
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
if (t.closest("[data-bulk-clear]")) {
|
|
410
|
+
Array.prototype.forEach.call(root.querySelectorAll("[data-select-row],[data-select-all]"), (c) => {
|
|
411
|
+
c.checked = false;
|
|
412
|
+
});
|
|
413
|
+
syncBulk(root);
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
const a = t.closest("a[data-qlink]");
|
|
417
|
+
if (!a)
|
|
418
|
+
return;
|
|
419
|
+
e.preventDefault();
|
|
420
|
+
run(root, new URLSearchParams(a.getAttribute("href").replace(/^\?/, "")));
|
|
421
|
+
});
|
|
422
|
+
document.addEventListener("submit", (e) => {
|
|
423
|
+
const f = e.target;
|
|
424
|
+
if (!f || f.tagName !== "FORM")
|
|
425
|
+
return;
|
|
426
|
+
const root = f.closest?.("[data-query-url]");
|
|
427
|
+
if (!root)
|
|
428
|
+
return;
|
|
429
|
+
if (f.hasAttribute("data-create-form")) {
|
|
430
|
+
e.preventDefault();
|
|
431
|
+
createRecord(root, f);
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
if (f.hasAttribute("data-edit-form")) {
|
|
435
|
+
e.preventDefault();
|
|
436
|
+
updateRecord(root, f);
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
e.preventDefault();
|
|
440
|
+
const clean = new URLSearchParams;
|
|
441
|
+
const els = f.elements ?? [];
|
|
442
|
+
for (let i = 0;i < els.length; i++) {
|
|
443
|
+
const el = els[i];
|
|
444
|
+
if (!el.name || el.disabled)
|
|
445
|
+
continue;
|
|
446
|
+
if ((el.type === "checkbox" || el.type === "radio") && !el.checked)
|
|
447
|
+
continue;
|
|
448
|
+
if (el.value === "")
|
|
449
|
+
continue;
|
|
450
|
+
clean.append(el.name, el.value);
|
|
451
|
+
}
|
|
452
|
+
run(root, clean);
|
|
453
|
+
});
|
|
454
|
+
var debouncedRunForSearch = debounce((form, root) => {
|
|
455
|
+
const clean = new URLSearchParams;
|
|
456
|
+
const els = form.elements ?? [];
|
|
457
|
+
for (let i = 0;i < els.length; i++) {
|
|
458
|
+
const el = els[i];
|
|
459
|
+
if (!el.name || el.disabled)
|
|
460
|
+
continue;
|
|
461
|
+
if ((el.type === "checkbox" || el.type === "radio") && !el.checked)
|
|
462
|
+
continue;
|
|
463
|
+
if (el.value === "")
|
|
464
|
+
continue;
|
|
465
|
+
clean.append(el.name, el.value);
|
|
466
|
+
}
|
|
467
|
+
run(root, clean);
|
|
468
|
+
}, 350);
|
|
469
|
+
document.addEventListener("input", (e) => {
|
|
470
|
+
const t = e.target;
|
|
471
|
+
if (!t || !t.closest)
|
|
472
|
+
return;
|
|
473
|
+
const isSearch = t.getAttribute?.("data-search-input") !== null || t.getAttribute?.("name") === "search";
|
|
474
|
+
if (!isSearch)
|
|
475
|
+
return;
|
|
476
|
+
const form = t.closest("form");
|
|
477
|
+
const root = t.closest("[data-query-url]") || form?.closest?.("[data-query-url]");
|
|
478
|
+
if (!form || !root)
|
|
479
|
+
return;
|
|
480
|
+
debouncedRunForSearch(form, root);
|
|
481
|
+
});
|
|
482
|
+
document.addEventListener("change", (e) => {
|
|
483
|
+
const t = e.target;
|
|
484
|
+
if (!t?.closest)
|
|
485
|
+
return;
|
|
486
|
+
const root = t.closest("[data-query-url]");
|
|
487
|
+
if (!root)
|
|
488
|
+
return;
|
|
489
|
+
const all = t.hasAttribute("data-select-all");
|
|
490
|
+
if (!all && !t.hasAttribute("data-select-row"))
|
|
491
|
+
return;
|
|
492
|
+
if (all) {
|
|
493
|
+
Array.prototype.forEach.call(root.querySelectorAll("[data-select-row]"), (c) => {
|
|
494
|
+
c.checked = t.checked;
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
syncBulk(root);
|
|
498
|
+
});
|
package/dist/panel-theme.js
CHANGED
|
@@ -1 +1,38 @@
|
|
|
1
|
-
|
|
1
|
+
// src/react/page/client/theme.ts
|
|
2
|
+
var STORAGE_KEY = "koran-theme";
|
|
3
|
+
function storedTheme() {
|
|
4
|
+
try {
|
|
5
|
+
const value = localStorage.getItem(STORAGE_KEY);
|
|
6
|
+
return value === "dark" || value === "light" ? value : null;
|
|
7
|
+
} catch {
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
function systemDark() {
|
|
12
|
+
try {
|
|
13
|
+
return typeof window.matchMedia === "function" && window.matchMedia("(prefers-color-scheme: dark)").matches;
|
|
14
|
+
} catch {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function toggleTheme() {
|
|
19
|
+
const root = document.documentElement;
|
|
20
|
+
const dark = root.classList.toggle("dark");
|
|
21
|
+
root.classList.toggle("light", !dark);
|
|
22
|
+
try {
|
|
23
|
+
localStorage.setItem(STORAGE_KEY, dark ? "dark" : "light");
|
|
24
|
+
} catch {}
|
|
25
|
+
}
|
|
26
|
+
function init() {
|
|
27
|
+
const stored = storedTheme();
|
|
28
|
+
if (stored)
|
|
29
|
+
document.documentElement.classList.add(stored);
|
|
30
|
+
else if (systemDark())
|
|
31
|
+
document.documentElement.classList.add("dark");
|
|
32
|
+
document.getElementById("koran-theme-toggle")?.addEventListener("click", toggleTheme);
|
|
33
|
+
window.__koranToggle = toggleTheme;
|
|
34
|
+
}
|
|
35
|
+
init();
|
|
36
|
+
export {
|
|
37
|
+
toggleTheme
|
|
38
|
+
};
|
package/dist/rack/index.js
CHANGED
|
@@ -686,6 +686,9 @@ function isNull(value) {
|
|
|
686
686
|
function like(column, value) {
|
|
687
687
|
return sql`${column} like ${value}`;
|
|
688
688
|
}
|
|
689
|
+
function ilike(column, value) {
|
|
690
|
+
return sql`${column} ilike ${value}`;
|
|
691
|
+
}
|
|
689
692
|
|
|
690
693
|
// node_modules/drizzle-orm/sql/expressions/select.js
|
|
691
694
|
function asc(column) {
|
|
@@ -763,7 +766,16 @@ function whereFor(table, query, opts, withSoftDelete) {
|
|
|
763
766
|
}
|
|
764
767
|
if (query.search && query.search.fields.length > 0) {
|
|
765
768
|
const pattern = `%${escapeLike(query.search.value)}%`;
|
|
766
|
-
const ors = query.search.fields.map((f) =>
|
|
769
|
+
const ors = query.search.fields.map((f) => {
|
|
770
|
+
const col = table[f];
|
|
771
|
+
if (!col)
|
|
772
|
+
return;
|
|
773
|
+
try {
|
|
774
|
+
return typeof ilike === "function" ? ilike(col, pattern) : like(col, pattern);
|
|
775
|
+
} catch {
|
|
776
|
+
return like(col, pattern);
|
|
777
|
+
}
|
|
778
|
+
}).filter((c) => c !== undefined);
|
|
767
779
|
if (ors.length > 0)
|
|
768
780
|
conds.push(or(...ors));
|
|
769
781
|
}
|
package/dist/react/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import Elysia from "elysia";
|
|
|
2
2
|
import { ReactRack } from "./types";
|
|
3
3
|
export declare function page<Props = Record<string, never>>(path: string, props?: Props): ReactRack.PageDescriptor<Props>;
|
|
4
4
|
export declare const pages: ReactRack.PageRegistry;
|
|
5
|
-
export declare function reactPlugin(options
|
|
5
|
+
export declare function reactPlugin(options?: ReactRack.ReactPluginOptions): Elysia<"", {
|
|
6
6
|
decorator: {
|
|
7
7
|
react: {
|
|
8
8
|
render: (path: string, props: unknown) => Promise<Response>;
|
package/dist/react/index.js
CHANGED
|
@@ -152,11 +152,19 @@ function page(path, props) {
|
|
|
152
152
|
};
|
|
153
153
|
}
|
|
154
154
|
var pages = {
|
|
155
|
-
"/dashboard": () => import("../Dashboard-
|
|
156
|
-
"/panel": () => import("../Panel-
|
|
155
|
+
"/dashboard": () => import("../Dashboard-s8nfyf7s.js"),
|
|
156
|
+
"/panel": () => import("../Panel-b7jhgvnm.js")
|
|
157
157
|
};
|
|
158
|
-
|
|
159
|
-
|
|
158
|
+
var defaultPages = pages;
|
|
159
|
+
function mergeRegistries(base, extra) {
|
|
160
|
+
if (!extra)
|
|
161
|
+
return { ...base };
|
|
162
|
+
const list = Array.isArray(extra) ? extra : [extra];
|
|
163
|
+
return list.reduce((acc, r) => ({ ...acc, ...r }), { ...base });
|
|
164
|
+
}
|
|
165
|
+
function reactPlugin(options = {}) {
|
|
166
|
+
const registry = mergeRegistries(defaultPages, options.pages);
|
|
167
|
+
const render = createRenderer(registry);
|
|
160
168
|
const panelCssOverride = pickPanelCssOption(options);
|
|
161
169
|
let cachedCustomCss = null;
|
|
162
170
|
let cachedCustomCssLoaded = false;
|
package/dist/react/types.d.ts
CHANGED
|
@@ -16,7 +16,7 @@ export declare namespace ReactRack {
|
|
|
16
16
|
content?: string;
|
|
17
17
|
};
|
|
18
18
|
interface ReactPluginOptions {
|
|
19
|
-
pages
|
|
19
|
+
pages?: PageRegistry | PageRegistry[];
|
|
20
20
|
/**
|
|
21
21
|
* Override `panel.css` served at `/__rack/panel.css`.
|
|
22
22
|
* - `string`: file path if the file exists, otherwise raw CSS string.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "elysia-rack",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Model-driven CRUD rack for Elysia: REST + QUERY routes, validation, authorization, OpenAPI metadata, and React panel rendering.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -54,8 +54,9 @@
|
|
|
54
54
|
"dev": "bun run src/playground.ts",
|
|
55
55
|
"test": "bun test --coverage",
|
|
56
56
|
"typecheck": "tsc --noEmit",
|
|
57
|
-
"build": "bun run build:js && bun run build:dts &&
|
|
57
|
+
"build": "bun run build:js && bun run build:dts && bun run build:client",
|
|
58
58
|
"build:js": "bun build ./src/rack/index.ts ./src/react/index.ts --outdir ./dist --target node --format esm --splitting --external elysia --external react --external react-dom",
|
|
59
|
+
"build:client": "bun build src/react/page/client/panel.ts --outfile dist/panel-app.js --target browser --format esm && bun build src/react/page/client/theme.ts --outfile dist/panel-theme.js --target browser --format esm && mkdir -p dist && cp src/react/app.html dist/app.html 2>/dev/null || true",
|
|
59
60
|
"build:dts": "tsc -p tsconfig.build.json",
|
|
60
61
|
"prepublishOnly": "bun run typecheck && bun run build"
|
|
61
62
|
},
|