@aimeloic/monkey-tester 5.0.4 → 5.0.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.
- package/htmlTemplate.js +112 -103
- package/package.json +1 -1
package/htmlTemplate.js
CHANGED
|
@@ -1,23 +1,13 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
// ─── Unicode-safe base64 helpers
|
|
4
|
-
// Use these on the CALLER side to produce endpointsJsonB64:
|
|
5
|
-
// import { encodePayload } from './ui.js';
|
|
6
|
-
// const endpointsJsonB64 = encodePayload(endpointsData);
|
|
7
|
-
//
|
|
3
|
+
// ─── Unicode-safe base64 helpers ─────────────────────────────────────────────
|
|
8
4
|
function encodePayload(obj) {
|
|
9
5
|
const jsonStr = JSON.stringify(obj);
|
|
10
|
-
// Convert string to UTF-8 bytes
|
|
11
6
|
const bytes = new TextEncoder().encode(jsonStr);
|
|
12
|
-
// Convert bytes to binary string for btoa
|
|
13
7
|
const binStr = Array.from(bytes, byte => String.fromCharCode(byte)).join('');
|
|
14
8
|
return btoa(binStr);
|
|
15
9
|
}
|
|
16
10
|
|
|
17
|
-
// The reciprocal decode runs inside each HTML template (see _decode below).
|
|
18
|
-
// It is injected as a one-liner so every template is self-contained.
|
|
19
|
-
|
|
20
|
-
// Replace line 18 with this version:
|
|
21
11
|
const _decode = `function _decode(b64) {
|
|
22
12
|
const binStr = atob(b64.replace(/\\s+/g, ''));
|
|
23
13
|
const bytes = new Uint8Array(binStr.length);
|
|
@@ -27,22 +17,20 @@ const _decode = `function _decode(b64) {
|
|
|
27
17
|
return JSON.parse(new TextDecoder().decode(bytes));
|
|
28
18
|
}`;
|
|
29
19
|
|
|
30
|
-
// ─── Runtime sandbox (injected into tester page)
|
|
20
|
+
// ─── Runtime sandbox (injected into tester page) ──────────────────────────────
|
|
31
21
|
function runtimeClientSandbox() {
|
|
32
|
-
// Unicode-safe decode (whitespace-stripped for safety)
|
|
33
22
|
function _decode(b64) {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
23
|
+
const binStr = atob(b64.replace(/\s+/g, ''));
|
|
24
|
+
const bytes = new Uint8Array(binStr.length);
|
|
25
|
+
for (let i = 0; i < binStr.length; i++) {
|
|
26
|
+
bytes[i] = binStr.charCodeAt(i);
|
|
27
|
+
}
|
|
28
|
+
return JSON.parse(new TextDecoder().decode(bytes));
|
|
38
29
|
}
|
|
39
|
-
const jsonStr = new TextDecoder().decode(bytes);
|
|
40
|
-
return JSON.parse(jsonStr);
|
|
41
|
-
}
|
|
42
30
|
|
|
43
31
|
const ENDPOINTS = _decode(
|
|
44
|
-
|
|
45
|
-
);
|
|
32
|
+
document.getElementById('__monkey_data__').textContent.trim()
|
|
33
|
+
);
|
|
46
34
|
|
|
47
35
|
let currentKey = null;
|
|
48
36
|
|
|
@@ -218,10 +206,11 @@ function runtimeClientSandbox() {
|
|
|
218
206
|
const UI = {
|
|
219
207
|
|
|
220
208
|
// ── Tester sandbox ──────────────────────────────────────────────────────────
|
|
221
|
-
|
|
209
|
+
// appName: displayed in the header logo
|
|
210
|
+
tester: (endpointsJsonB64, appName = 'App') => `<!DOCTYPE html>
|
|
222
211
|
<html lang="en">
|
|
223
212
|
<head>
|
|
224
|
-
<meta charset="UTF-8"><title
|
|
213
|
+
<meta charset="UTF-8"><title>${appName} — API Tester</title>
|
|
225
214
|
<link href="https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;700&family=DM+Mono:wght@400;500&family=DM+Sans:wght@300;400;500&display=swap" rel="stylesheet">
|
|
226
215
|
<style>
|
|
227
216
|
:root { --bg:#0e0c09; --surface:#181510; --surface2:#221d14; --border:#3a3020; --accent:#e8a838; --text:#f0e8d8; --text-dim:#9a8c78; --red:#d45c3c; --green:#6ba05a; --blue:#5a86c0; --radius:8px; }
|
|
@@ -250,7 +239,7 @@ const UI = {
|
|
|
250
239
|
.form-section-title { font-size:11px; font-family:'DM Mono',monospace; color:var(--text-dim); text-transform:uppercase; margin-bottom:16px; border-bottom:1px solid var(--border); padding-bottom:6px; }
|
|
251
240
|
.field-row { display:grid; grid-template-columns:150px 1fr; align-items:center; gap:16px; margin-bottom:14px; }
|
|
252
241
|
.field-label { font-family:'DM Mono',monospace; font-size:12px; color:var(--text-dim); text-align:right; }
|
|
253
|
-
input[type=text],input[type=password],input[type=number] { background:var(--surface2); border:1px solid var(--border); color:var(--text); font-size:13px; padding:8px 12px; border-radius:var(--radius); width:100%; outline:none; }
|
|
242
|
+
input[type=text],input[type=password],input[type=number],input[type=email],input[type=date],input[type=tel],input[type=url] { background:var(--surface2); border:1px solid var(--border); color:var(--text); font-size:13px; padding:8px 12px; border-radius:var(--radius); width:100%; outline:none; }
|
|
254
243
|
.btn-row { margin-top:24px; display:flex; gap:12px; }
|
|
255
244
|
.btn { background:var(--accent); color:#0e0c09; border:none; padding:10px 24px; border-radius:var(--radius); font-size:13px; font-weight:500; cursor:pointer; }
|
|
256
245
|
.btn-secondary { background:var(--surface2); color:var(--text-dim); border:1px solid var(--border); }
|
|
@@ -268,10 +257,9 @@ const UI = {
|
|
|
268
257
|
</style>
|
|
269
258
|
</head>
|
|
270
259
|
<body>
|
|
271
|
-
<!-- Replace the old hidden div with this -->
|
|
272
260
|
<script id="__monkey_data__" type="text/plain">${endpointsJsonB64}</script>
|
|
273
261
|
<header>
|
|
274
|
-
<div class="logo"
|
|
262
|
+
<div class="logo">${appName} <span>API Tester</span></div>
|
|
275
263
|
<div class="header-right">
|
|
276
264
|
<div class="base-url-wrap"><label>HOST</label><input id="base-url" type="text" value=""></div>
|
|
277
265
|
<div class="jwt-wrap"><label>BEARER AUTH</label><input id="jwt-input" type="text" placeholder="Token value..."></div>
|
|
@@ -291,19 +279,24 @@ const UI = {
|
|
|
291
279
|
</html>`,
|
|
292
280
|
|
|
293
281
|
// ── Login page ──────────────────────────────────────────────────────────────
|
|
294
|
-
|
|
282
|
+
// appName: shown as the page title and card heading
|
|
283
|
+
// loginPath: the API endpoint to POST credentials to (default: /api/v1/auth/login)
|
|
284
|
+
// redirectTo: where to send the user after a successful login (default: /dashboard)
|
|
285
|
+
login: (appName = 'App', loginPath = '/api/v1/auth/login', redirectTo = '/dashboard') => `<!DOCTYPE html>
|
|
295
286
|
<html>
|
|
296
287
|
<head>
|
|
297
|
-
<title
|
|
288
|
+
<title>${appName} — Sign In</title>
|
|
298
289
|
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;700&display=swap" rel="stylesheet">
|
|
299
290
|
<style>
|
|
300
291
|
body { background:#0e0c09; color:#f0e8d8; font-family:'DM Sans',sans-serif; display:flex; justify-content:center; align-items:center; height:100vh; margin:0; }
|
|
301
292
|
.card { background:#181510; border:1px solid #3a3020; padding:40px; border-radius:12px; width:340px; }
|
|
302
|
-
h2 { color:#e8a838; margin:0 0
|
|
293
|
+
h2 { color:#e8a838; margin:0 0 8px; text-align:center; }
|
|
294
|
+
.subtitle { color:#9a8c78; font-size:13px; text-align:center; margin:0 0 28px; }
|
|
303
295
|
.field { margin-bottom:20px; }
|
|
304
296
|
label { display:block; font-size:11px; color:#9a8c78; text-transform:uppercase; margin-bottom:8px; }
|
|
305
|
-
input { background:#221d14; border:1px solid #3a3020; color:#f0e8d8; padding:12px; width:100%; box-sizing:border-box; border-radius:6px; outline:none; }
|
|
306
|
-
|
|
297
|
+
input { background:#221d14; border:1px solid #3a3020; color:#f0e8d8; padding:12px; width:100%; box-sizing:border-box; border-radius:6px; outline:none; font-size:14px; }
|
|
298
|
+
input::placeholder { color:#5a5040; }
|
|
299
|
+
button { background:#e8a838; color:#0e0c09; border:none; padding:12px; width:100%; border-radius:6px; font-weight:600; cursor:pointer; margin-top:10px; font-size:14px; }
|
|
307
300
|
.footer { text-align:center; margin-top:20px; font-size:13px; color:#9a8c78; }
|
|
308
301
|
a { color:#e8a838; text-decoration:none; }
|
|
309
302
|
#err { color:#d45c3c; font-size:13px; margin-bottom:15px; text-align:center; min-height:18px; }
|
|
@@ -311,48 +304,61 @@ const UI = {
|
|
|
311
304
|
</head>
|
|
312
305
|
<body>
|
|
313
306
|
<div class="card">
|
|
314
|
-
<h2
|
|
307
|
+
<h2>${appName}</h2>
|
|
308
|
+
<p class="subtitle">Sign in to your account</p>
|
|
315
309
|
<div id="err"></div>
|
|
316
|
-
<div class="field"><label>Email</label><input type="email" id="email"
|
|
317
|
-
<div class="field"><label>Password</label><input type="password" id="password"
|
|
318
|
-
<button onclick="handleLogin()">
|
|
310
|
+
<div class="field"><label>Email</label><input type="email" id="email" placeholder="you@example.com" autocomplete="email"></div>
|
|
311
|
+
<div class="field"><label>Password</label><input type="password" id="password" placeholder="Your password" autocomplete="current-password"></div>
|
|
312
|
+
<button onclick="handleLogin()">Sign In</button>
|
|
319
313
|
<div class="footer">Need an account? <a href="/signup">Sign up</a></div>
|
|
320
314
|
</div>
|
|
321
315
|
<script>
|
|
322
316
|
async function handleLogin() {
|
|
323
|
-
const email = document.getElementById('email').value;
|
|
317
|
+
const email = document.getElementById('email').value.trim();
|
|
324
318
|
const password = document.getElementById('password').value;
|
|
325
|
-
const
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
319
|
+
const errDiv = document.getElementById('err');
|
|
320
|
+
errDiv.textContent = '';
|
|
321
|
+
if (!email || !password) { errDiv.textContent = 'Please fill in all fields.'; return; }
|
|
322
|
+
try {
|
|
323
|
+
const res = await fetch('${loginPath}', {
|
|
324
|
+
method: 'POST',
|
|
325
|
+
headers: { 'Content-Type': 'application/json' },
|
|
326
|
+
body: JSON.stringify({ email, password })
|
|
327
|
+
});
|
|
328
|
+
const data = await res.json();
|
|
329
|
+
if (res.ok && data.token) {
|
|
330
|
+
localStorage.setItem('__auth_token__', data.token);
|
|
331
|
+
window.location.href = '${redirectTo}';
|
|
332
|
+
} else {
|
|
333
|
+
errDiv.textContent = data.error || data.message || 'Login failed.';
|
|
334
|
+
}
|
|
335
|
+
} catch (e) {
|
|
336
|
+
errDiv.textContent = 'Network error — could not reach the server.';
|
|
336
337
|
}
|
|
337
338
|
}
|
|
339
|
+
document.addEventListener('keydown', e => { if (e.key === 'Enter') handleLogin(); });
|
|
338
340
|
</script>
|
|
339
341
|
</body>
|
|
340
342
|
</html>`,
|
|
341
343
|
|
|
342
344
|
// ── Signup page ─────────────────────────────────────────────────────────────
|
|
343
|
-
|
|
345
|
+
// appName: shown as the page title and card heading
|
|
346
|
+
// registerPath: the API endpoint to POST to (default: /api/v1/auth/register)
|
|
347
|
+
signup: (appName = 'App', registerPath = '/api/v1/auth/register') => `<!DOCTYPE html>
|
|
344
348
|
<html>
|
|
345
349
|
<head>
|
|
346
|
-
<title
|
|
350
|
+
<title>${appName} — Create Account</title>
|
|
347
351
|
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;700&display=swap" rel="stylesheet">
|
|
348
352
|
<style>
|
|
349
353
|
body { background:#0e0c09; color:#f0e8d8; font-family:'DM Sans',sans-serif; display:flex; justify-content:center; align-items:center; height:100vh; margin:0; }
|
|
350
354
|
.card { background:#181510; border:1px solid #3a3020; padding:40px; border-radius:12px; width:340px; }
|
|
351
|
-
h2 { color:#e8a838; margin:0 0
|
|
355
|
+
h2 { color:#e8a838; margin:0 0 8px; text-align:center; }
|
|
356
|
+
.subtitle { color:#9a8c78; font-size:13px; text-align:center; margin:0 0 28px; }
|
|
352
357
|
.field { margin-bottom:20px; }
|
|
353
358
|
label { display:block; font-size:11px; color:#9a8c78; text-transform:uppercase; margin-bottom:8px; }
|
|
354
|
-
input { background:#221d14; border:1px solid #3a3020; color:#f0e8d8; padding:12px; width:100%; box-sizing:border-box; border-radius:6px; outline:none; }
|
|
355
|
-
|
|
359
|
+
input { background:#221d14; border:1px solid #3a3020; color:#f0e8d8; padding:12px; width:100%; box-sizing:border-box; border-radius:6px; outline:none; font-size:14px; }
|
|
360
|
+
input::placeholder { color:#5a5040; }
|
|
361
|
+
button { background:#e8a838; color:#0e0c09; border:none; padding:12px; width:100%; border-radius:6px; font-weight:600; cursor:pointer; margin-top:10px; font-size:14px; }
|
|
356
362
|
.footer { text-align:center; margin-top:20px; font-size:13px; color:#9a8c78; }
|
|
357
363
|
a { color:#e8a838; text-decoration:none; }
|
|
358
364
|
#msg { font-size:13px; margin-bottom:15px; text-align:center; min-height:18px; }
|
|
@@ -360,44 +366,55 @@ async function handleLogin() {
|
|
|
360
366
|
</head>
|
|
361
367
|
<body>
|
|
362
368
|
<div class="card">
|
|
363
|
-
<h2
|
|
369
|
+
<h2>${appName}</h2>
|
|
370
|
+
<p class="subtitle">Create your account</p>
|
|
364
371
|
<div id="msg"></div>
|
|
365
|
-
<div class="field"><label>Username</label><input type="text" id="username" placeholder="
|
|
366
|
-
<div class="field"><label>Email Address</label><input type="email" id="email" placeholder="
|
|
367
|
-
<div class="field"><label>Password</label><input type="password" id="password"></div>
|
|
368
|
-
<button onclick="handleRegister()">
|
|
369
|
-
<div class="footer">Have an account? <a href="/login">Sign
|
|
372
|
+
<div class="field"><label>Username</label><input type="text" id="username" placeholder="Choose a username" autocomplete="username"></div>
|
|
373
|
+
<div class="field"><label>Email Address</label><input type="email" id="email" placeholder="you@example.com" autocomplete="email"></div>
|
|
374
|
+
<div class="field"><label>Password</label><input type="password" id="password" placeholder="Choose a password" autocomplete="new-password"></div>
|
|
375
|
+
<button onclick="handleRegister()">Create Account</button>
|
|
376
|
+
<div class="footer">Have an account? <a href="/login">Sign in</a></div>
|
|
370
377
|
</div>
|
|
371
378
|
<script>
|
|
372
379
|
async function handleRegister() {
|
|
373
|
-
const username = document.getElementById('username').value;
|
|
374
|
-
const email = document.getElementById('email').value;
|
|
380
|
+
const username = document.getElementById('username').value.trim();
|
|
381
|
+
const email = document.getElementById('email').value.trim();
|
|
375
382
|
const password = document.getElementById('password').value;
|
|
376
383
|
const msgDiv = document.getElementById('msg');
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
384
|
+
msgDiv.textContent = '';
|
|
385
|
+
if (!username || !email || !password) { msgDiv.style.color = '#d45c3c'; msgDiv.textContent = 'Please fill in all fields.'; return; }
|
|
386
|
+
try {
|
|
387
|
+
const res = await fetch('${registerPath}', {
|
|
388
|
+
method: 'POST',
|
|
389
|
+
headers: { 'Content-Type': 'application/json' },
|
|
390
|
+
body: JSON.stringify({ username, email, password })
|
|
391
|
+
});
|
|
392
|
+
const data = await res.json();
|
|
393
|
+
if (res.ok) {
|
|
394
|
+
msgDiv.style.color = '#6ba05a';
|
|
395
|
+
msgDiv.textContent = 'Account created! Redirecting to login…';
|
|
396
|
+
setTimeout(() => window.location.href = '/login', 1200);
|
|
397
|
+
} else {
|
|
398
|
+
msgDiv.style.color = '#d45c3c';
|
|
399
|
+
msgDiv.textContent = data.error || data.message || 'Registration failed.';
|
|
400
|
+
}
|
|
401
|
+
} catch (e) {
|
|
388
402
|
msgDiv.style.color = '#d45c3c';
|
|
389
|
-
msgDiv.textContent =
|
|
403
|
+
msgDiv.textContent = 'Network error — could not reach the server.';
|
|
390
404
|
}
|
|
391
405
|
}
|
|
406
|
+
document.addEventListener('keydown', e => { if (e.key === 'Enter') handleRegister(); });
|
|
392
407
|
</script>
|
|
393
408
|
</body>
|
|
394
409
|
</html>`,
|
|
395
410
|
|
|
396
411
|
// ── Dashboard ────────────────────────────────────────────────────────────────
|
|
397
|
-
|
|
412
|
+
// endpointsJsonB64: base64-encoded endpoints payload from encodePayload()
|
|
413
|
+
// appName: shown in the page title and header h1
|
|
414
|
+
dashboard: (endpointsJsonB64, appName = 'App') => `<!DOCTYPE html>
|
|
398
415
|
<html>
|
|
399
416
|
<head>
|
|
400
|
-
<title
|
|
417
|
+
<title>${appName} — Admin Dashboard</title>
|
|
401
418
|
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;700&display=swap" rel="stylesheet">
|
|
402
419
|
<style>
|
|
403
420
|
:root { --bg:#0e0c09; --surface:#181510; --surface2:#221d14; --border:#3a3020; --accent:#e8a838; --text:#f0e8d8; --text-dim:#9a8c78; --red:#d45c3c; --green:#6ba05a; }
|
|
@@ -429,19 +446,18 @@ async function handleRegister() {
|
|
|
429
446
|
</style>
|
|
430
447
|
</head>
|
|
431
448
|
<body>
|
|
432
|
-
<!-- Replace the old hidden div with this -->
|
|
433
449
|
<script id="__monkey_data__" type="text/plain">${endpointsJsonB64}</script>
|
|
434
450
|
|
|
435
451
|
<header>
|
|
436
|
-
<h1
|
|
452
|
+
<h1>${appName}</h1>
|
|
437
453
|
<div class="nav-links">
|
|
438
|
-
<a href="/api/tester" target="_blank">🛠
|
|
454
|
+
<a href="/api/tester" target="_blank">🛠 API Tester</a>
|
|
439
455
|
<button class="logout-btn" onclick="localStorage.removeItem('__auth_token__'); window.location.href='/login'">Log Out</button>
|
|
440
456
|
</div>
|
|
441
457
|
</header>
|
|
442
458
|
|
|
443
459
|
<div class="selector-banner">
|
|
444
|
-
<label style="margin:0;">
|
|
460
|
+
<label style="margin:0;">Resource collection:</label>
|
|
445
461
|
<select id="route-selector" onchange="switchCollection()"></select>
|
|
446
462
|
</div>
|
|
447
463
|
|
|
@@ -449,8 +465,8 @@ async function handleRegister() {
|
|
|
449
465
|
<div class="panel">
|
|
450
466
|
<h3 id="form-title">Add Entry</h3>
|
|
451
467
|
<div id="dynamic-fields-container"></div>
|
|
452
|
-
<button class="btn" id="btn-submit" onclick="submitDataForm()">
|
|
453
|
-
<button class="btn btn-sm btn-cancel" id="btn-cancel" style="display:none; margin-top:10px;" onclick="resetDataForm()">Cancel
|
|
468
|
+
<button class="btn" id="btn-submit" onclick="submitDataForm()">Submit</button>
|
|
469
|
+
<button class="btn btn-sm btn-cancel" id="btn-cancel" style="display:none; margin-top:10px;" onclick="resetDataForm()">Cancel</button>
|
|
454
470
|
</div>
|
|
455
471
|
<div class="table-wrap">
|
|
456
472
|
<table id="dynamic-table">
|
|
@@ -461,22 +477,14 @@ async function handleRegister() {
|
|
|
461
477
|
</div>
|
|
462
478
|
|
|
463
479
|
<script>
|
|
464
|
-
// ── Unicode-safe decode ──────────────────────────────────────────────────────
|
|
465
|
-
// Replace the old _decode function inside UI.dashboard with this:
|
|
466
480
|
function _decode(b64) {
|
|
467
481
|
const binStr = atob(b64.replace(/\s+/g, ''));
|
|
468
482
|
const bytes = new Uint8Array(binStr.length);
|
|
469
|
-
for (let i = 0; i < binStr.length; i++)
|
|
470
|
-
bytes[i] = binStr.charCodeAt(i);
|
|
471
|
-
}
|
|
483
|
+
for (let i = 0; i < binStr.length; i++) bytes[i] = binStr.charCodeAt(i);
|
|
472
484
|
return JSON.parse(new TextDecoder().decode(bytes));
|
|
473
485
|
}
|
|
474
486
|
|
|
475
|
-
|
|
476
|
-
// AFTER (Safe from HTML parsing artifacts)
|
|
477
|
-
const ENDPOINTS = _decode(
|
|
478
|
-
document.getElementById('__monkey_data__').textContent.trim()
|
|
479
|
-
);
|
|
487
|
+
const ENDPOINTS = _decode(document.getElementById('__monkey_data__').textContent.trim());
|
|
480
488
|
|
|
481
489
|
const token = localStorage.getItem('__auth_token__');
|
|
482
490
|
if (!token) window.location.href = '/login';
|
|
@@ -511,7 +519,9 @@ function resolveRoutes() {
|
|
|
511
519
|
if (dynamicCollections[path].get) {
|
|
512
520
|
const opt = document.createElement('option');
|
|
513
521
|
opt.value = path;
|
|
514
|
-
|
|
522
|
+
// Derive a readable label from the path: /api/v1/students → Students
|
|
523
|
+
const label = path.split('/').filter(Boolean).pop();
|
|
524
|
+
opt.textContent = label.charAt(0).toUpperCase() + label.slice(1);
|
|
515
525
|
selector.appendChild(opt);
|
|
516
526
|
}
|
|
517
527
|
});
|
|
@@ -533,7 +543,7 @@ function renderFormFields() {
|
|
|
533
543
|
const fields = dynamicCollections[activeCollectionPath].modelFields;
|
|
534
544
|
|
|
535
545
|
if (!fields || fields.length === 0) {
|
|
536
|
-
container.innerHTML = '<p style="font-size:12px;color:var(--text-dim);">No
|
|
546
|
+
container.innerHTML = '<p style="font-size:12px;color:var(--text-dim);">No writable fields detected for this endpoint.</p>';
|
|
537
547
|
return;
|
|
538
548
|
}
|
|
539
549
|
|
|
@@ -551,7 +561,7 @@ async function fetchData() {
|
|
|
551
561
|
const head = document.getElementById('table-head');
|
|
552
562
|
const body = document.getElementById('table-body');
|
|
553
563
|
head.innerHTML = '';
|
|
554
|
-
body.innerHTML = '<tr><td style="padding:20px;color:var(--text-dim)">Loading
|
|
564
|
+
body.innerHTML = '<tr><td style="padding:20px;color:var(--text-dim)">Loading…</td></tr>';
|
|
555
565
|
|
|
556
566
|
try {
|
|
557
567
|
const res = await fetch(activeCollectionPath, { headers: { 'Authorization': 'Bearer ' + token } });
|
|
@@ -566,7 +576,7 @@ async function fetchData() {
|
|
|
566
576
|
}
|
|
567
577
|
|
|
568
578
|
if (!list || list.length === 0 || list[0] === null) {
|
|
569
|
-
body.innerHTML = '<tr><td style="padding:30px;color:var(--text-dim)">No records found
|
|
579
|
+
body.innerHTML = '<tr><td style="padding:30px;color:var(--text-dim)">No records found.</td></tr>';
|
|
570
580
|
return;
|
|
571
581
|
}
|
|
572
582
|
|
|
@@ -592,7 +602,7 @@ async function fetchData() {
|
|
|
592
602
|
});
|
|
593
603
|
|
|
594
604
|
} catch (e) {
|
|
595
|
-
body.innerHTML = '<tr><td style="padding:20px;color:var(--red)">Failed
|
|
605
|
+
body.innerHTML = '<tr><td style="padding:20px;color:var(--red)">Failed to load data from this endpoint.</td></tr>';
|
|
596
606
|
console.error(e);
|
|
597
607
|
}
|
|
598
608
|
}
|
|
@@ -622,24 +632,23 @@ async function submitDataForm() {
|
|
|
622
632
|
});
|
|
623
633
|
|
|
624
634
|
if (res.ok) { resetDataForm(); fetchData(); }
|
|
625
|
-
else { alert('
|
|
635
|
+
else { alert('Request failed — check the console for details.'); }
|
|
626
636
|
}
|
|
627
637
|
|
|
628
638
|
async function deleteRow(id) {
|
|
629
|
-
if (!confirm('
|
|
639
|
+
if (!confirm('Delete this record?')) return;
|
|
630
640
|
const delTemplate = dynamicCollections[activeCollectionPath].del;
|
|
631
641
|
const paramName = delTemplate.split('/:')[1];
|
|
632
642
|
const url = delTemplate.replace(\`:\${paramName}\`, id);
|
|
633
643
|
const res = await fetch(url, { method: 'DELETE', headers: { 'Authorization': 'Bearer ' + token } });
|
|
634
|
-
if (res.ok) fetchData(); else alert('Delete
|
|
644
|
+
if (res.ok) fetchData(); else alert('Delete failed.');
|
|
635
645
|
}
|
|
636
646
|
|
|
637
647
|
function startRowEdit(id, encodedJson) {
|
|
638
648
|
activeEditId = id;
|
|
639
|
-
// Unicode-safe decode for row data encoded above with btoa(unescape(encodeURIComponent(...)))
|
|
640
649
|
const data = JSON.parse(decodeURIComponent(escape(atob(encodedJson))));
|
|
641
|
-
document.getElementById('form-title').textContent = \`
|
|
642
|
-
document.getElementById('btn-submit').textContent = '
|
|
650
|
+
document.getElementById('form-title').textContent = \`Edit record #\${id}\`;
|
|
651
|
+
document.getElementById('btn-submit').textContent = 'Save Changes';
|
|
643
652
|
document.getElementById('btn-cancel').style.display = 'block';
|
|
644
653
|
const fields = dynamicCollections[activeCollectionPath].modelFields;
|
|
645
654
|
fields.forEach(f => {
|
|
@@ -651,7 +660,7 @@ function startRowEdit(id, encodedJson) {
|
|
|
651
660
|
function resetDataForm() {
|
|
652
661
|
activeEditId = null;
|
|
653
662
|
document.getElementById('form-title').textContent = 'Add Entry';
|
|
654
|
-
document.getElementById('btn-submit').textContent = '
|
|
663
|
+
document.getElementById('btn-submit').textContent = 'Submit';
|
|
655
664
|
document.getElementById('btn-cancel').style.display = 'none';
|
|
656
665
|
(dynamicCollections[activeCollectionPath]?.modelFields || []).forEach(f => {
|
|
657
666
|
const el = document.getElementById(\`input-\${f.name}\`);
|