millas 0.2.13 → 0.2.14
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/package.json +6 -3
- package/src/admin/Admin.js +107 -1027
- package/src/admin/AdminAuth.js +1 -1
- package/src/admin/ViewContext.js +1 -1
- package/src/admin/handlers/ActionHandler.js +103 -0
- package/src/admin/handlers/ApiHandler.js +113 -0
- package/src/admin/handlers/AuthHandler.js +76 -0
- package/src/admin/handlers/ExportHandler.js +70 -0
- package/src/admin/handlers/InlineHandler.js +71 -0
- package/src/admin/handlers/PageHandler.js +351 -0
- package/src/admin/resources/AdminResource.js +22 -1
- package/src/admin/static/SelectFilter2.js +34 -0
- package/src/admin/static/actions.js +201 -0
- package/src/admin/static/admin.css +7 -0
- package/src/admin/static/change_form.js +585 -0
- package/src/admin/static/core.js +128 -0
- package/src/admin/static/login.js +76 -0
- package/src/admin/static/vendor/bi/bootstrap-icons.min.css +5 -0
- package/src/admin/static/vendor/bi/fonts/bootstrap-icons.woff +0 -0
- package/src/admin/static/vendor/bi/fonts/bootstrap-icons.woff2 +0 -0
- package/src/admin/static/vendor/jquery.min.js +2 -0
- package/src/admin/views/layouts/base.njk +30 -113
- package/src/admin/views/pages/detail.njk +10 -9
- package/src/admin/views/pages/form.njk +4 -4
- package/src/admin/views/pages/list.njk +11 -193
- package/src/admin/views/pages/login.njk +19 -64
- package/src/admin/views/partials/form-field.njk +1 -1
- package/src/admin/views/partials/form-scripts.njk +4 -478
- package/src/admin/views/partials/form-widget.njk +10 -10
- package/src/ai/AITokenBudget.js +1 -1
- package/src/auth/Auth.js +112 -3
- package/src/auth/AuthMiddleware.js +18 -15
- package/src/auth/Hasher.js +15 -43
- package/src/cli.js +3 -0
- package/src/commands/call.js +190 -0
- package/src/commands/createsuperuser.js +3 -4
- package/src/commands/key.js +97 -0
- package/src/commands/make.js +16 -2
- package/src/commands/new.js +16 -1
- package/src/commands/serve.js +5 -5
- package/src/console/Command.js +337 -0
- package/src/console/CommandLoader.js +165 -0
- package/src/console/index.js +6 -0
- package/src/container/AppInitializer.js +48 -1
- package/src/container/Application.js +3 -1
- package/src/container/HttpServer.js +0 -1
- package/src/container/MillasConfig.js +48 -0
- package/src/controller/Controller.js +13 -11
- package/src/core/docs.js +6 -0
- package/src/core/foundation.js +8 -0
- package/src/core/http.js +20 -10
- package/src/core/validation.js +58 -27
- package/src/docs/Docs.js +268 -0
- package/src/docs/DocsServiceProvider.js +80 -0
- package/src/docs/SchemaInferrer.js +131 -0
- package/src/docs/handlers/ApiHandler.js +305 -0
- package/src/docs/handlers/PageHandler.js +47 -0
- package/src/docs/index.js +13 -0
- package/src/docs/resources/ApiResource.js +402 -0
- package/src/docs/static/docs.css +723 -0
- package/src/docs/static/docs.js +1181 -0
- package/src/encryption/Encrypter.js +381 -0
- package/src/facades/Auth.js +5 -2
- package/src/facades/Crypt.js +166 -0
- package/src/facades/Docs.js +43 -0
- package/src/facades/Mail.js +1 -1
- package/src/http/MillasRequest.js +7 -31
- package/src/http/RequestContext.js +11 -7
- package/src/http/SecurityBootstrap.js +24 -2
- package/src/http/Shape.js +168 -0
- package/src/http/adapters/ExpressAdapter.js +9 -5
- package/src/middleware/CorsMiddleware.js +3 -0
- package/src/middleware/ThrottleMiddleware.js +10 -7
- package/src/orm/model/Model.js +14 -1
- package/src/providers/EncryptionServiceProvider.js +66 -0
- package/src/router/MiddlewareRegistry.js +79 -54
- package/src/router/Route.js +9 -4
- package/src/router/RouteEntry.js +91 -0
- package/src/router/Router.js +71 -1
- package/src/scaffold/maker.js +138 -1
- package/src/scaffold/templates.js +12 -0
- package/src/serializer/Serializer.js +239 -0
- package/src/support/Str.js +1080 -0
- package/src/validation/BaseValidator.js +45 -5
- package/src/validation/Validator.js +67 -61
- package/src/validation/types.js +490 -0
- package/src/middleware/AuthMiddleware.js +0 -46
- package/src/middleware/MiddlewareRegistry.js +0 -106
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
|
|
66
66
|
<div class="toolbar-right">
|
|
67
67
|
{% if filters | length %}
|
|
68
|
-
<button class="btn btn-ghost btn-sm"
|
|
68
|
+
<button class="btn btn-ghost btn-sm" id="filter-toggle">
|
|
69
69
|
<span class="icon icon-13"><svg viewBox="0 0 24 24"><use href="#ic-filter"/></svg></span>
|
|
70
70
|
Filters
|
|
71
71
|
{% if activeFilters | length %}
|
|
@@ -100,18 +100,18 @@
|
|
|
100
100
|
<span class="bulk-count" id="bulk-count">0 selected</span>
|
|
101
101
|
<span class="text-muted" style="font-size:12px">—</span>
|
|
102
102
|
{% for action in resource.actions %}
|
|
103
|
-
<button class="btn btn-ghost btn-sm"
|
|
103
|
+
<button class="btn btn-ghost btn-sm" data-bulk-action="{{ action.index }}">
|
|
104
104
|
{% if action.icon %}<span class="icon icon-13"><svg viewBox="0 0 24 24"><use href="#ic-{{ action.icon }}"/></svg></span>{% endif %}
|
|
105
105
|
{{ action.label }}
|
|
106
106
|
</button>
|
|
107
107
|
{% endfor %}
|
|
108
108
|
{% if resource.canDelete %}
|
|
109
|
-
<button class="btn btn-ghost btn-sm btn-danger"
|
|
109
|
+
<button class="btn btn-ghost btn-sm btn-danger" id="bulk-delete-btn">
|
|
110
110
|
<span class="icon icon-13"><svg viewBox="0 0 24 24"><use href="#ic-trash"/></svg></span>
|
|
111
111
|
Delete selected
|
|
112
112
|
</button>
|
|
113
113
|
{% endif %}
|
|
114
|
-
<button class="btn btn-ghost btn-sm" style="margin-left:auto"
|
|
114
|
+
<button class="btn btn-ghost btn-sm" style="margin-left:auto" id="clear-selection">
|
|
115
115
|
<span class="icon icon-13"><svg viewBox="0 0 24 24"><use href="#ic-x"/></svg></span>
|
|
116
116
|
Cancel
|
|
117
117
|
</button>
|
|
@@ -171,7 +171,7 @@
|
|
|
171
171
|
<thead>
|
|
172
172
|
<tr>
|
|
173
173
|
<th class="col-check">
|
|
174
|
-
<input type="checkbox" class="row-check" id="check-all"
|
|
174
|
+
<input type="checkbox" class="row-check" id="check-all" title="Select all">
|
|
175
175
|
</th>
|
|
176
176
|
{% for field in listFields %}
|
|
177
177
|
{% set isSorted = sort == field.name %}
|
|
@@ -323,196 +323,14 @@
|
|
|
323
323
|
</div>
|
|
324
324
|
</div>
|
|
325
325
|
|
|
326
|
+
{% block scripts %}
|
|
326
327
|
<script>
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
// ── Filter panel ─────────────────────────────────────────────────────────
|
|
332
|
-
{% if activeFilters | length %}$('#filter-panel').show();{% endif %}
|
|
333
|
-
|
|
334
|
-
window.toggleFilters = function() {
|
|
335
|
-
$('#filter-panel').toggle();
|
|
336
|
-
};
|
|
337
|
-
|
|
338
|
-
// ── Live search on Enter ──────────────────────────────────────────────────
|
|
339
|
-
$('#search-form input[name="search"]').on('keydown', function(e) {
|
|
340
|
-
if (e.key === 'Enter') $('#search-form').submit();
|
|
341
|
-
});
|
|
342
|
-
|
|
343
|
-
// ── Bulk selection ────────────────────────────────────────────────────────
|
|
344
|
-
function updateBulkBar() {
|
|
345
|
-
var $checked = $('.item-check:checked');
|
|
346
|
-
var n = $checked.length;
|
|
347
|
-
var total = $('.item-check').length;
|
|
348
|
-
$('#bulk-bar').toggleClass('visible', n > 0);
|
|
349
|
-
$('#bulk-count').text(n + ' selected');
|
|
350
|
-
$('#check-all').prop('indeterminate', n > 0 && n < total)
|
|
351
|
-
.prop('checked', n > 0 && n === total);
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
window.toggleAll = function(master) {
|
|
355
|
-
$('.item-check').prop('checked', master.checked);
|
|
356
|
-
updateBulkBar();
|
|
357
|
-
};
|
|
358
|
-
|
|
359
|
-
window.clearSelection = function() {
|
|
360
|
-
$('.item-check, #check-all').prop('checked', false).prop('indeterminate', false);
|
|
361
|
-
updateBulkBar();
|
|
362
|
-
};
|
|
363
|
-
|
|
364
|
-
$(document).on('change', '.item-check', updateBulkBar);
|
|
365
|
-
|
|
366
|
-
window.bulkDelete = function() {
|
|
367
|
-
var ids = $('.item-check:checked').map(function() { return this.value; }).get();
|
|
368
|
-
if (!ids.length) return;
|
|
369
|
-
var label = ids.length + ' record' + (ids.length > 1 ? 's' : '');
|
|
370
|
-
confirmDeleteBulk(PREFIX + '/' + SLUG + '/bulk-delete', label, ids);
|
|
371
|
-
};
|
|
372
|
-
|
|
373
|
-
window.confirmDeleteBulk = function(url, label, ids) {
|
|
374
|
-
UI.Confirm.show({
|
|
375
|
-
title: 'Delete ' + label,
|
|
376
|
-
message: 'Delete <strong>' + label + '</strong>? This cannot be undone.',
|
|
377
|
-
confirm: 'Delete',
|
|
378
|
-
danger: true,
|
|
379
|
-
}).then(function(ok) {
|
|
380
|
-
if (!ok) return;
|
|
381
|
-
var csrf = $('meta[name="csrf-token"]').attr('content') || '';
|
|
382
|
-
var $form = $('<form method="POST">').attr('action', url);
|
|
383
|
-
$form.append('<input name="_csrf" value="' + csrf + '">');
|
|
384
|
-
$.each(ids, function(_, id) {
|
|
385
|
-
$form.append('<input type="hidden" name="ids[]" value="' + id + '">');
|
|
386
|
-
});
|
|
387
|
-
$form.appendTo('body').submit();
|
|
388
|
-
});
|
|
389
|
-
};
|
|
390
|
-
|
|
391
|
-
window.bulkAction = function(actionIndex) {
|
|
392
|
-
var ids = $('.item-check:checked').map(function() { return this.value; }).get();
|
|
393
|
-
if (!ids.length) return;
|
|
394
|
-
var csrf = $('meta[name="csrf-token"]').attr('content') || '';
|
|
395
|
-
var $form = $('<form method="POST">').attr('action', PREFIX + '/' + SLUG + '/bulk-action');
|
|
396
|
-
$form.append('<input name="_csrf" value="' + csrf + '">');
|
|
397
|
-
$form.append('<input type="hidden" name="actionIndex" value="' + actionIndex + '">');
|
|
398
|
-
$.each(ids, function(_, id) {
|
|
399
|
-
$form.append('<input type="hidden" name="ids[]" value="' + id + '">');
|
|
400
|
-
});
|
|
401
|
-
$form.appendTo('body').submit();
|
|
402
|
-
};
|
|
403
|
-
|
|
404
|
-
// ── UI.Dropdown — portal-rendered action menus ────────────────────────────
|
|
405
|
-
// Wire export menu
|
|
406
|
-
var $exportBtn = $('#export-menu-btn');
|
|
407
|
-
var $exportPanel = $('#export-menu-panel');
|
|
408
|
-
if ($exportBtn.length && $exportPanel.length) {
|
|
409
|
-
var exportDd = UI.Dropdown.create({
|
|
410
|
-
anchor: $exportBtn[0],
|
|
411
|
-
content: $exportPanel[0],
|
|
412
|
-
placement: 'bottom-end',
|
|
413
|
-
offset: 4,
|
|
414
|
-
});
|
|
415
|
-
$exportBtn.on('click', function() { exportDd.toggle(); });
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
// Wire per-row action menus
|
|
419
|
-
$('.ui-menu').each(function() {
|
|
420
|
-
var $menu = $(this);
|
|
421
|
-
var $btn = $menu.find('.ui-menu-trigger');
|
|
422
|
-
var $panel = $menu.find('.ui-menu-panel');
|
|
423
|
-
if (!$btn.length || !$panel.length) return;
|
|
424
|
-
|
|
425
|
-
var dd = UI.Dropdown.create({
|
|
426
|
-
anchor: $btn[0],
|
|
427
|
-
content: $panel[0],
|
|
428
|
-
placement: 'bottom-end',
|
|
429
|
-
offset: 4,
|
|
430
|
-
});
|
|
431
|
-
|
|
432
|
-
$btn.on('click', function(e) {
|
|
433
|
-
e.stopPropagation();
|
|
434
|
-
dd.toggle();
|
|
435
|
-
});
|
|
436
|
-
|
|
437
|
-
$panel.find('[data-confirm-delete]').on('click', function() {
|
|
438
|
-
dd.close();
|
|
439
|
-
confirmDelete($(this).data('confirm-delete'), $(this).data('confirm-label'));
|
|
440
|
-
});
|
|
441
|
-
|
|
442
|
-
$panel.find('[data-row-action]').on('click', function() {
|
|
443
|
-
dd.close();
|
|
444
|
-
var url = $(this).data('row-action');
|
|
445
|
-
var label = $(this).data('row-action-label');
|
|
446
|
-
UI.Confirm.show({
|
|
447
|
-
title: label,
|
|
448
|
-
message: 'Run <strong>' + label + '</strong> on this record?',
|
|
449
|
-
confirm: label,
|
|
450
|
-
}).then(function(ok) {
|
|
451
|
-
if (!ok) return;
|
|
452
|
-
var csrf = $('meta[name="csrf-token"]').attr('content') || '';
|
|
453
|
-
var $form = $('<form method="POST">').attr('action', url);
|
|
454
|
-
$form.append('<input name="_csrf" value="' + csrf + '">');
|
|
455
|
-
$form.appendTo('body').submit();
|
|
456
|
-
});
|
|
457
|
-
});
|
|
458
|
-
|
|
459
|
-
// ── Right-click on the row opens a context menu at the cursor ──────────
|
|
460
|
-
(function($menuPanel) {
|
|
461
|
-
var $row = $menuPanel.closest('tr');
|
|
462
|
-
|
|
463
|
-
$row.on('contextmenu', function(e) {
|
|
464
|
-
e.preventDefault();
|
|
465
|
-
e.stopPropagation();
|
|
466
|
-
|
|
467
|
-
// Remove any existing portal
|
|
468
|
-
$('.context-menu-portal').remove();
|
|
469
|
-
|
|
470
|
-
// Build a fresh portal from the panel's inner HTML (avoids clone display:none)
|
|
471
|
-
var $p = $('<div class="ui-menu-panel context-menu-portal"></div>');
|
|
472
|
-
$p.html($menuPanel.html());
|
|
473
|
-
$p.css({
|
|
474
|
-
position: 'fixed',
|
|
475
|
-
top: Math.min(e.clientY, window.innerHeight - 240) + 'px',
|
|
476
|
-
left: Math.min(e.clientX, window.innerWidth - 190) + 'px',
|
|
477
|
-
zIndex: 9999,
|
|
478
|
-
});
|
|
479
|
-
$('body').append($p);
|
|
480
|
-
|
|
481
|
-
// Wire handlers on the portal
|
|
482
|
-
$p.find('[data-confirm-delete]').on('click', function() {
|
|
483
|
-
$p.remove();
|
|
484
|
-
confirmDelete($(this).data('confirm-delete'), $(this).data('confirm-label'));
|
|
485
|
-
});
|
|
486
|
-
$p.find('[data-row-action]').on('click', function() {
|
|
487
|
-
$p.remove();
|
|
488
|
-
var _url = $(this).data('row-action');
|
|
489
|
-
var _label = $(this).data('row-action-label');
|
|
490
|
-
UI.Confirm.show({
|
|
491
|
-
title: _label,
|
|
492
|
-
message: 'Run <strong>' + _label + '</strong> on this record?',
|
|
493
|
-
confirm: _label,
|
|
494
|
-
}).then(function(ok) {
|
|
495
|
-
if (!ok) return;
|
|
496
|
-
var csrf = $('meta[name="csrf-token"]').attr('content') || '';
|
|
497
|
-
var $form = $('<form method="POST">').attr('action', _url);
|
|
498
|
-
$form.append('<input name="_csrf" value="' + csrf + '">');
|
|
499
|
-
$form.appendTo('body').submit();
|
|
500
|
-
});
|
|
501
|
-
});
|
|
502
|
-
$p.find('a.ui-menu-item').on('click', function() { $p.remove(); });
|
|
503
|
-
|
|
504
|
-
// Dismiss on next click anywhere or Escape
|
|
505
|
-
setTimeout(function() {
|
|
506
|
-
$(document).one('click.ctxmenu', function() { $p.remove(); });
|
|
507
|
-
}, 0);
|
|
508
|
-
$(document).one('keydown.ctxmenu', function(ev) {
|
|
509
|
-
if (ev.key === 'Escape') { $p.remove(); }
|
|
510
|
-
});
|
|
511
|
-
});
|
|
512
|
-
})($panel);
|
|
513
|
-
});
|
|
514
|
-
});
|
|
328
|
+
window.MILLAS_ADMIN_PREFIX = '{{ adminPrefix }}';
|
|
329
|
+
window.MILLAS_RESOURCE_SLUG = '{{ resource.slug }}';
|
|
330
|
+
window.MILLAS_HAS_ACTIVE_FILTERS = {% if activeFilters | length %}true{% else %}false{% endif %};
|
|
515
331
|
</script>
|
|
332
|
+
<script src="{{ adminPrefix }}/static/actions.js?v=2"></script>
|
|
333
|
+
{% endblock %}
|
|
516
334
|
|
|
517
335
|
<style>
|
|
518
336
|
/* ── UI Menu (portal dropdown) ── */
|
|
@@ -4,22 +4,19 @@
|
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
6
|
<title>Sign In — {{ adminTitle }}</title>
|
|
7
|
-
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
8
|
-
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
9
|
-
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:opsz,wght@9..40,300;9..40,400;9..40,500;9..40,600;9..40,700&family=DM+Mono:wght@400;500&display=swap" rel="stylesheet">
|
|
10
7
|
<style>
|
|
11
8
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
12
9
|
:root {
|
|
13
|
-
--bg: #
|
|
10
|
+
--bg: #f6f7f8;
|
|
14
11
|
--surface: #ffffff;
|
|
15
12
|
--border: #e3e6ec;
|
|
16
13
|
--border-soft: #edf0f5;
|
|
17
|
-
--primary: #
|
|
18
|
-
--primary-h: #
|
|
19
|
-
--primary-soft: #
|
|
20
|
-
--primary-dim: #
|
|
21
|
-
--text: #
|
|
22
|
-
|
|
14
|
+
--primary: #f6821f;
|
|
15
|
+
--primary-h: #e06d1a;
|
|
16
|
+
--primary-soft: #fff4e6;
|
|
17
|
+
--primary-dim: #ffe4c4;
|
|
18
|
+
--text: #172b4d;
|
|
19
|
+
--text-soft: #2c3e50;
|
|
23
20
|
--text-muted: #6b7280;
|
|
24
21
|
--text-xmuted: #9ca3af;
|
|
25
22
|
--danger: #dc2626;
|
|
@@ -51,7 +48,7 @@
|
|
|
51
48
|
.login-card {
|
|
52
49
|
background: var(--surface);
|
|
53
50
|
border: 1px solid var(--border);
|
|
54
|
-
border-radius:
|
|
51
|
+
border-radius: 6px;
|
|
55
52
|
box-shadow: var(--shadow);
|
|
56
53
|
width: 100%;
|
|
57
54
|
max-width: 400px;
|
|
@@ -203,6 +200,15 @@
|
|
|
203
200
|
radial-gradient(ellipse 60% 40% at 80% 90%, rgba(99,102,241,.05) 0%, transparent 60%);
|
|
204
201
|
pointer-events: none;
|
|
205
202
|
}
|
|
203
|
+
input:-webkit-autofill,
|
|
204
|
+
input:-webkit-autofill:hover,
|
|
205
|
+
input:-webkit-autofill:focus,
|
|
206
|
+
input:-webkit-autofill:active {
|
|
207
|
+
-webkit-box-shadow: 0 0 0 100px white inset !important;
|
|
208
|
+
-webkit-text-fill-color: #000 !important;
|
|
209
|
+
caret-color: #000;
|
|
210
|
+
transition: background-color 5000s ease-in-out 0s;
|
|
211
|
+
}
|
|
206
212
|
</style>
|
|
207
213
|
</head>
|
|
208
214
|
<body>
|
|
@@ -210,13 +216,6 @@
|
|
|
210
216
|
<div class="login-card">
|
|
211
217
|
|
|
212
218
|
<div class="login-header">
|
|
213
|
-
<div class="login-logo">
|
|
214
|
-
<svg viewBox="0 0 24 24">
|
|
215
|
-
<ellipse cx="12" cy="5" rx="9" ry="3"/>
|
|
216
|
-
<path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3"/>
|
|
217
|
-
<path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"/>
|
|
218
|
-
</svg>
|
|
219
|
-
</div>
|
|
220
219
|
<div class="login-title">{{ adminTitle }}</div>
|
|
221
220
|
<div class="login-subtitle">Sign in to your admin panel</div>
|
|
222
221
|
</div>
|
|
@@ -270,7 +269,7 @@
|
|
|
270
269
|
autocomplete="current-password"
|
|
271
270
|
required
|
|
272
271
|
style="padding-right: 40px">
|
|
273
|
-
<button type="button" class="pw-toggle"
|
|
272
|
+
<button type="button" class="pw-toggle" aria-label="Toggle password visibility">
|
|
274
273
|
<svg id="pw-icon" viewBox="0 0 24 24">
|
|
275
274
|
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/>
|
|
276
275
|
<circle cx="12" cy="12" r="3"/>
|
|
@@ -304,51 +303,7 @@
|
|
|
304
303
|
|
|
305
304
|
</div>
|
|
306
305
|
|
|
307
|
-
<script>
|
|
308
|
-
function togglePw() {
|
|
309
|
-
const input = document.getElementById('password');
|
|
310
|
-
const icon = document.getElementById('pw-icon');
|
|
311
|
-
const isHidden = input.type === 'password';
|
|
312
|
-
input.type = isHidden ? 'text' : 'password';
|
|
313
|
-
icon.innerHTML = isHidden
|
|
314
|
-
? `<path d="M17.94 17.94A10.07 10.07 0 0112 20c-7 0-11-8-11-8a18.45 18.45 0 015.06-5.94"/><path d="M9.9 4.24A9.12 9.12 0 0112 4c7 0 11 8 11 8a18.5 18.5 0 01-2.16 3.19"/><line x1="1" y1="1" x2="23" y2="23"/>`
|
|
315
|
-
: `<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/>`;
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
// Submit loading state + basic client validation
|
|
319
|
-
document.getElementById('login-form').addEventListener('submit', function(e) {
|
|
320
|
-
const email = document.getElementById('email').value.trim();
|
|
321
|
-
const password = document.getElementById('password').value;
|
|
322
|
-
|
|
323
|
-
if (!email || !password) {
|
|
324
|
-
e.preventDefault();
|
|
325
|
-
const existing = document.getElementById('login-error');
|
|
326
|
-
const msg = !email ? 'Email is required.' : 'Password is required.';
|
|
327
|
-
if (existing) {
|
|
328
|
-
existing.querySelector('svg + *')
|
|
329
|
-
? (existing.lastChild.textContent = msg)
|
|
330
|
-
: (existing.innerHTML += ` ${msg}`);
|
|
331
|
-
} else {
|
|
332
|
-
const el = document.createElement('div');
|
|
333
|
-
el.className = 'alert alert-error';
|
|
334
|
-
el.id = 'login-error';
|
|
335
|
-
el.innerHTML = `<svg viewBox="0 0 24 24" width="15" height="15"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg> ${msg}`;
|
|
336
|
-
this.insertBefore(el, this.firstChild);
|
|
337
|
-
}
|
|
338
|
-
return;
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
const btn = document.getElementById('login-btn');
|
|
342
|
-
const icon = document.getElementById('login-icon');
|
|
343
|
-
btn.disabled = true;
|
|
344
|
-
icon.innerHTML = `<circle cx="12" cy="12" r="10" stroke-dasharray="32" stroke-dashoffset="32" class="spin"/>`;
|
|
345
|
-
btn.lastChild.textContent = ' Signing in…';
|
|
346
|
-
});
|
|
347
|
-
|
|
348
|
-
// Auto-focus password if email is pre-filled
|
|
349
|
-
const emailVal = document.getElementById('email').value;
|
|
350
|
-
if (emailVal) document.getElementById('password').focus();
|
|
351
|
-
</script>
|
|
306
|
+
<script src="{{ adminPrefix }}/static/login.js?v=1"></script>
|
|
352
307
|
|
|
353
308
|
</body>
|
|
354
309
|
</html>
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
{% if hasError %}
|
|
43
43
|
<span class="form-error">
|
|
44
44
|
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
|
|
45
|
-
{{ errors[field.name]
|
|
45
|
+
{{ errors[field.name] if errors[field.name] is string else errors[field.name][0] }}
|
|
46
46
|
</span>
|
|
47
47
|
{% elif field.help %}
|
|
48
48
|
<span class="form-help">{{ field.help }}</span>
|