millas 0.2.12-beta → 0.2.12-beta-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/package.json +3 -16
- package/src/admin/ActivityLog.js +153 -52
- package/src/admin/Admin.js +400 -167
- package/src/admin/AdminAuth.js +213 -98
- package/src/admin/FormGenerator.js +372 -0
- package/src/admin/HookRegistry.js +256 -0
- package/src/admin/QueryEngine.js +263 -0
- package/src/admin/ViewContext.js +309 -0
- package/src/admin/WidgetRegistry.js +406 -0
- package/src/admin/index.js +17 -0
- package/src/admin/resources/AdminResource.js +383 -97
- package/src/admin/static/admin.css +1341 -0
- package/src/admin/static/date-picker.css +157 -0
- package/src/admin/static/date-picker.js +316 -0
- package/src/admin/static/json-editor.css +649 -0
- package/src/admin/static/json-editor.js +1429 -0
- package/src/admin/static/ui.js +1044 -0
- package/src/admin/views/layouts/base.njk +65 -1013
- package/src/admin/views/pages/detail.njk +40 -16
- package/src/admin/views/pages/form.njk +47 -599
- package/src/admin/views/pages/list.njk +145 -62
- package/src/admin/views/partials/form-field.njk +53 -0
- package/src/admin/views/partials/form-footer.njk +28 -0
- package/src/admin/views/partials/form-readonly.njk +114 -0
- package/src/admin/views/partials/form-scripts.njk +476 -0
- package/src/admin/views/partials/form-widget.njk +296 -0
- package/src/admin/views/partials/json-dialog.njk +80 -0
- package/src/admin/views/partials/json-editor.njk +37 -0
- package/src/admin.zip +0 -0
- package/src/auth/Auth.js +31 -10
- package/src/auth/AuthController.js +3 -1
- package/src/auth/AuthUser.js +119 -0
- package/src/cli.js +4 -2
- package/src/commands/createsuperuser.js +254 -0
- package/src/commands/lang.js +589 -0
- package/src/commands/migrate.js +154 -81
- package/src/commands/serve.js +82 -110
- package/src/container/AppInitializer.js +215 -0
- package/src/container/Application.js +278 -253
- package/src/container/HttpServer.js +156 -0
- package/src/container/MillasApp.js +29 -279
- package/src/container/MillasConfig.js +192 -0
- package/src/core/admin.js +5 -0
- package/src/core/auth.js +9 -0
- package/src/core/db.js +9 -0
- package/src/core/foundation.js +59 -0
- package/src/core/http.js +11 -0
- package/src/core/lang.js +1 -0
- package/src/core/mail.js +6 -0
- package/src/core/queue.js +7 -0
- package/src/core/validation.js +29 -0
- package/src/facades/Admin.js +1 -1
- package/src/facades/Auth.js +22 -39
- package/src/facades/Cache.js +21 -10
- package/src/facades/Database.js +1 -1
- package/src/facades/Events.js +18 -17
- package/src/facades/Facade.js +197 -0
- package/src/facades/Http.js +42 -45
- package/src/facades/Log.js +25 -49
- package/src/facades/Mail.js +27 -32
- package/src/facades/Queue.js +22 -15
- package/src/facades/Storage.js +18 -10
- package/src/facades/Url.js +53 -0
- package/src/http/HttpClient.js +673 -0
- package/src/http/ResponseDispatcher.js +18 -111
- package/src/http/UrlGenerator.js +375 -0
- package/src/http/WelcomePage.js +273 -0
- package/src/http/adapters/ExpressAdapter.js +315 -0
- package/src/http/adapters/HttpAdapter.js +168 -0
- package/src/http/adapters/index.js +9 -0
- package/src/i18n/I18nServiceProvider.js +91 -0
- package/src/i18n/Translator.js +635 -0
- package/src/i18n/defaults.js +122 -0
- package/src/i18n/index.js +164 -0
- package/src/i18n/locales/en.js +55 -0
- package/src/i18n/locales/sw.js +48 -0
- package/src/index.js +5 -144
- package/src/logger/formatters/PrettyFormatter.js +103 -57
- package/src/logger/internal.js +2 -2
- package/src/logger/patchConsole.js +91 -81
- package/src/middleware/MiddlewareRegistry.js +62 -82
- package/src/migrations/system/0001_users.js +21 -0
- package/src/migrations/system/0002_admin_log.js +25 -0
- package/src/migrations/system/0003_sessions.js +23 -0
- package/src/orm/fields/index.js +210 -188
- package/src/orm/migration/DefaultValueParser.js +325 -0
- package/src/orm/migration/InteractiveResolver.js +191 -0
- package/src/orm/migration/Makemigrations.js +312 -0
- package/src/orm/migration/MigrationGraph.js +227 -0
- package/src/orm/migration/MigrationRunner.js +202 -108
- package/src/orm/migration/MigrationWriter.js +463 -0
- package/src/orm/migration/ModelInspector.js +412 -344
- package/src/orm/migration/ModelScanner.js +225 -0
- package/src/orm/migration/ProjectState.js +213 -0
- package/src/orm/migration/RenameDetector.js +175 -0
- package/src/orm/migration/SchemaBuilder.js +8 -81
- package/src/orm/migration/operations/base.js +57 -0
- package/src/orm/migration/operations/column.js +191 -0
- package/src/orm/migration/operations/fields.js +252 -0
- package/src/orm/migration/operations/index.js +55 -0
- package/src/orm/migration/operations/models.js +152 -0
- package/src/orm/migration/operations/registry.js +131 -0
- package/src/orm/migration/operations/special.js +51 -0
- package/src/orm/migration/utils.js +208 -0
- package/src/orm/model/Model.js +81 -13
- package/src/providers/AdminServiceProvider.js +66 -9
- package/src/providers/AuthServiceProvider.js +46 -7
- package/src/providers/CacheStorageServiceProvider.js +5 -3
- package/src/providers/DatabaseServiceProvider.js +3 -2
- package/src/providers/EventServiceProvider.js +2 -1
- package/src/providers/LogServiceProvider.js +7 -3
- package/src/providers/MailServiceProvider.js +4 -3
- package/src/providers/QueueServiceProvider.js +4 -3
- package/src/router/Router.js +119 -152
- package/src/scaffold/templates.js +83 -26
- package/src/facades/Validation.js +0 -69
|
@@ -74,19 +74,19 @@
|
|
|
74
74
|
</button>
|
|
75
75
|
{% endif %}
|
|
76
76
|
|
|
77
|
-
{# Export dropdown #}
|
|
78
|
-
<div class="
|
|
79
|
-
<button class="btn btn-ghost btn-sm
|
|
77
|
+
{# Export dropdown — portal-rendered via UI.Dropdown #}
|
|
78
|
+
<div class="ui-menu" id="export-menu-wrap">
|
|
79
|
+
<button class="btn btn-ghost btn-sm" id="export-menu-btn" type="button">
|
|
80
80
|
<span class="icon icon-13"><svg viewBox="0 0 24 24"><use href="#ic-download"/></svg></span>
|
|
81
81
|
Export
|
|
82
82
|
<span class="icon icon-12"><svg viewBox="0 0 24 24"><use href="#ic-chevron-down"/></svg></span>
|
|
83
83
|
</button>
|
|
84
|
-
<div class="
|
|
85
|
-
<a href="{{ adminPrefix }}/{{ resource.slug }}/export.csv?search={{ search }}&sort={{ sort }}&order={{ order }}{% for key, val in activeFilters %}&filter[{{ key }}]={{ val }}{% endfor %}">
|
|
84
|
+
<div class="ui-menu-panel" id="export-menu-panel" style="display:none">
|
|
85
|
+
<a class="ui-menu-item" href="{{ adminPrefix }}/{{ resource.slug }}/export.csv?search={{ search }}&sort={{ sort }}&order={{ order }}{% for key, val in activeFilters %}&filter[{{ key }}]={{ val }}{% endfor %}">
|
|
86
86
|
<span class="icon icon-13"><svg viewBox="0 0 24 24"><use href="#ic-download"/></svg></span>
|
|
87
87
|
Export CSV
|
|
88
88
|
</a>
|
|
89
|
-
<a href="{{ adminPrefix }}/{{ resource.slug }}/export.json?search={{ search }}&sort={{ sort }}&order={{ order }}{% for key, val in activeFilters %}&filter[{{ key }}]={{ val }}{% endfor %}">
|
|
89
|
+
<a class="ui-menu-item" href="{{ adminPrefix }}/{{ resource.slug }}/export.json?search={{ search }}&sort={{ sort }}&order={{ order }}{% for key, val in activeFilters %}&filter[{{ key }}]={{ val }}{% endfor %}">
|
|
90
90
|
<span class="icon icon-13"><svg viewBox="0 0 24 24"><use href="#ic-download"/></svg></span>
|
|
91
91
|
Export JSON
|
|
92
92
|
</a>
|
|
@@ -202,8 +202,8 @@
|
|
|
202
202
|
{% endfor %}
|
|
203
203
|
{% if resource.canEdit or resource.canDelete %}
|
|
204
204
|
<td class="col-actions">
|
|
205
|
-
<div class="
|
|
206
|
-
<button class="action-menu-btn">
|
|
205
|
+
<div class="ui-menu">
|
|
206
|
+
<button class="action-menu-btn ui-menu-trigger" type="button" aria-label="Row actions">
|
|
207
207
|
<span class="icon icon-14">
|
|
208
208
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
209
209
|
<circle cx="12" cy="5" r="1.2" fill="currentColor" stroke="none"/>
|
|
@@ -212,20 +212,18 @@
|
|
|
212
212
|
</svg>
|
|
213
213
|
</span>
|
|
214
214
|
</button>
|
|
215
|
-
<div class="
|
|
215
|
+
<div class="ui-menu-panel" style="display:none">
|
|
216
216
|
{% if resource.canEdit %}
|
|
217
|
-
<a href="{{ adminPrefix }}/{{ resource.slug }}/{{ row.id }}/edit">
|
|
217
|
+
<a class="ui-menu-item" href="{{ adminPrefix }}/{{ resource.slug }}/{{ row.id }}/edit">
|
|
218
218
|
<span class="icon icon-13"><svg viewBox="0 0 24 24"><use href="#ic-edit"/></svg></span>
|
|
219
219
|
Edit
|
|
220
220
|
</a>
|
|
221
221
|
{% endif %}
|
|
222
|
-
<a href="{{ adminPrefix }}/{{ resource.slug }}/{{ row.id }}" style="display:none">
|
|
223
|
-
<span class="icon icon-13"><svg viewBox="0 0 24 24"><use href="#ic-eye"/></svg></span>
|
|
224
|
-
View
|
|
225
|
-
</a>
|
|
226
222
|
{% if resource.canDelete %}
|
|
227
|
-
<div class="sep"></div>
|
|
228
|
-
<button class="danger"
|
|
223
|
+
<div class="ui-menu-sep"></div>
|
|
224
|
+
<button class="ui-menu-item ui-menu-danger" type="button"
|
|
225
|
+
data-confirm-delete="{{ adminPrefix }}/{{ resource.slug }}/{{ row.id }}/delete"
|
|
226
|
+
data-confirm-label="#{{ row.id }}">
|
|
229
227
|
<span class="icon icon-13"><svg viewBox="0 0 24 24"><use href="#ic-trash"/></svg></span>
|
|
230
228
|
Delete
|
|
231
229
|
</button>
|
|
@@ -300,60 +298,145 @@
|
|
|
300
298
|
</div>
|
|
301
299
|
|
|
302
300
|
<script>
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
if (filterPanel) filterPanel.style.display = 'flex';
|
|
307
|
-
{% endif %}
|
|
301
|
+
$(function() {
|
|
302
|
+
var PREFIX = '{{ adminPrefix }}';
|
|
303
|
+
var SLUG = '{{ resource.slug }}';
|
|
308
304
|
|
|
309
|
-
|
|
310
|
-
if (
|
|
311
|
-
const shown = filterPanel.style.display !== 'none';
|
|
312
|
-
filterPanel.style.display = shown ? 'none' : 'flex';
|
|
313
|
-
}
|
|
305
|
+
// ── Filter panel ─────────────────────────────────────────────────────────
|
|
306
|
+
{% if activeFilters | length %}$('#filter-panel').show();{% endif %}
|
|
314
307
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
308
|
+
window.toggleFilters = function() {
|
|
309
|
+
$('#filter-panel').toggle();
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
// ── Live search on Enter ──────────────────────────────────────────────────
|
|
313
|
+
$('#search-form input[name="search"]').on('keydown', function(e) {
|
|
314
|
+
if (e.key === 'Enter') $('#search-form').submit();
|
|
320
315
|
});
|
|
321
|
-
}
|
|
322
316
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
const all = document.getElementById('check-all');
|
|
333
|
-
if (all) {
|
|
334
|
-
const total = document.querySelectorAll('.item-check').length;
|
|
335
|
-
all.indeterminate = n > 0 && n < total;
|
|
336
|
-
all.checked = n > 0 && n === total;
|
|
317
|
+
// ── Bulk selection ────────────────────────────────────────────────────────
|
|
318
|
+
function updateBulkBar() {
|
|
319
|
+
var $checked = $('.item-check:checked');
|
|
320
|
+
var n = $checked.length;
|
|
321
|
+
var total = $('.item-check').length;
|
|
322
|
+
$('#bulk-bar').toggleClass('visible', n > 0);
|
|
323
|
+
$('#bulk-count').text(n + ' selected');
|
|
324
|
+
$('#check-all').prop('indeterminate', n > 0 && n < total)
|
|
325
|
+
.prop('checked', n > 0 && n === total);
|
|
337
326
|
}
|
|
338
|
-
}
|
|
339
327
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
328
|
+
window.toggleAll = function(master) {
|
|
329
|
+
$('.item-check').prop('checked', master.checked);
|
|
330
|
+
updateBulkBar();
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
window.clearSelection = function() {
|
|
334
|
+
$('.item-check, #check-all').prop('checked', false).prop('indeterminate', false);
|
|
335
|
+
updateBulkBar();
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
$(document).on('change', '.item-check', updateBulkBar);
|
|
344
339
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
340
|
+
window.bulkDelete = function() {
|
|
341
|
+
var ids = $('.item-check:checked').map(function() { return this.value; }).get();
|
|
342
|
+
if (!ids.length) return;
|
|
343
|
+
var label = ids.length + ' record' + (ids.length > 1 ? 's' : '');
|
|
344
|
+
confirmDeleteBulk(PREFIX + '/' + SLUG + '/bulk-delete', label, ids);
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
window.confirmDeleteBulk = function(url, label, ids) {
|
|
348
|
+
UI.Confirm.show({
|
|
349
|
+
title: 'Delete ' + label,
|
|
350
|
+
message: 'Delete <strong>' + label + '</strong>? This cannot be undone.',
|
|
351
|
+
confirm: 'Delete',
|
|
352
|
+
danger: true,
|
|
353
|
+
}).then(function(ok) {
|
|
354
|
+
if (!ok) return;
|
|
355
|
+
var csrf = $('meta[name="csrf-token"]').attr('content') || '';
|
|
356
|
+
var $form = $('<form method="POST">').attr('action', url);
|
|
357
|
+
$form.append('<input name="_csrf" value="' + csrf + '">');
|
|
358
|
+
$.each(ids, function(_, id) {
|
|
359
|
+
$form.append('<input type="hidden" name="ids[]" value="' + id + '">');
|
|
360
|
+
});
|
|
361
|
+
$form.appendTo('body').submit();
|
|
362
|
+
});
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
// ── UI.Dropdown — portal-rendered action menus ────────────────────────────
|
|
366
|
+
// Wire export menu
|
|
367
|
+
var $exportBtn = $('#export-menu-btn');
|
|
368
|
+
var $exportPanel = $('#export-menu-panel');
|
|
369
|
+
if ($exportBtn.length && $exportPanel.length) {
|
|
370
|
+
var exportDd = UI.Dropdown.create({
|
|
371
|
+
anchor: $exportBtn[0],
|
|
372
|
+
content: $exportPanel[0],
|
|
373
|
+
placement: 'bottom-end',
|
|
374
|
+
offset: 4,
|
|
375
|
+
});
|
|
376
|
+
$exportBtn.on('click', function() { exportDd.toggle(); });
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Wire per-row action menus
|
|
380
|
+
$('.ui-menu').each(function() {
|
|
381
|
+
var $menu = $(this);
|
|
382
|
+
var $btn = $menu.find('.ui-menu-trigger');
|
|
383
|
+
var $panel = $menu.find('.ui-menu-panel');
|
|
384
|
+
if (!$btn.length || !$panel.length) return;
|
|
385
|
+
|
|
386
|
+
var dd = UI.Dropdown.create({
|
|
387
|
+
anchor: $btn[0],
|
|
388
|
+
content: $panel[0],
|
|
389
|
+
placement: 'bottom-end',
|
|
390
|
+
offset: 4,
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
$btn.on('click', function(e) {
|
|
394
|
+
e.stopPropagation();
|
|
395
|
+
dd.toggle();
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
$panel.find('[data-confirm-delete]').on('click', function() {
|
|
399
|
+
dd.close();
|
|
400
|
+
confirmDelete($(this).data('confirm-delete'), $(this).data('confirm-label'));
|
|
401
|
+
});
|
|
402
|
+
});
|
|
403
|
+
});
|
|
404
|
+
</script>
|
|
405
|
+
|
|
406
|
+
<style>
|
|
407
|
+
/* ── UI Menu (portal dropdown) ── */
|
|
408
|
+
.ui-menu { position: relative; display: inline-block; }
|
|
409
|
+
|
|
410
|
+
.ui-menu-panel {
|
|
411
|
+
background: var(--surface);
|
|
412
|
+
border: 1px solid var(--border);
|
|
413
|
+
border-radius: var(--radius);
|
|
414
|
+
min-width: 150px;
|
|
415
|
+
overflow: hidden;
|
|
348
416
|
}
|
|
349
417
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
);
|
|
418
|
+
.ui-menu-item {
|
|
419
|
+
display: flex;
|
|
420
|
+
align-items: center;
|
|
421
|
+
gap: 8px;
|
|
422
|
+
padding: 8px 14px;
|
|
423
|
+
font-size: 13px;
|
|
424
|
+
color: var(--text-soft);
|
|
425
|
+
text-decoration: none;
|
|
426
|
+
background: none;
|
|
427
|
+
border: none;
|
|
428
|
+
width: 100%;
|
|
429
|
+
text-align: left;
|
|
430
|
+
cursor: pointer;
|
|
431
|
+
font-family: inherit;
|
|
432
|
+
transition: background .1s;
|
|
433
|
+
white-space: nowrap;
|
|
357
434
|
}
|
|
358
|
-
|
|
435
|
+
.ui-menu-item:hover { background: var(--surface2); color: var(--text); }
|
|
436
|
+
.ui-menu-danger { color: var(--danger); }
|
|
437
|
+
.ui-menu-danger:hover { background: var(--danger-bg); color: var(--danger); }
|
|
438
|
+
|
|
439
|
+
.ui-menu-sep { height: 1px; background: var(--border-soft); margin: 3px 0; }
|
|
440
|
+
</style>
|
|
441
|
+
|
|
359
442
|
{% endblock %}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{#
|
|
2
|
+
partials/form-field.njk
|
|
3
|
+
Renders a single editable form field — all widget types.
|
|
4
|
+
Expects in scope: field, val, hasError, isEdit, record, errors, adminPrefix
|
|
5
|
+
#}
|
|
6
|
+
|
|
7
|
+
{# ── Fieldset heading ── #}
|
|
8
|
+
{% if field._isFieldset %}
|
|
9
|
+
<div class="full" style="grid-column:1/-1;margin-top:8px">
|
|
10
|
+
<div class="fieldset-heading">{{ field.label }}</div>
|
|
11
|
+
</div>
|
|
12
|
+
|
|
13
|
+
{# ── Readonly ── #}
|
|
14
|
+
{% elif field.isReadonly %}
|
|
15
|
+
{% include "partials/form-readonly.njk" %}
|
|
16
|
+
|
|
17
|
+
{# ── Editable ── #}
|
|
18
|
+
{% else %}
|
|
19
|
+
{% set val = record[field.name] if record[field.name] is defined else '' %}
|
|
20
|
+
{% set hasError = errors[field.name] is defined %}
|
|
21
|
+
<div class="form-group
|
|
22
|
+
{% if field.span == 'full' %}full
|
|
23
|
+
{% elif field.span == 'third' %}w-third
|
|
24
|
+
{% elif field.type == 'textarea' or field.type == 'json' or field.type == 'richtext' %}full
|
|
25
|
+
{% elif field.type == 'checkbox' %}w-third
|
|
26
|
+
{% endif %}"
|
|
27
|
+
data-field="{{ field.name }}">
|
|
28
|
+
|
|
29
|
+
{# Label (not shown for checkbox — toggle has its own) #}
|
|
30
|
+
{% if field.type != 'checkbox' %}
|
|
31
|
+
<label class="form-label" for="field-{{ field.name }}">
|
|
32
|
+
{{ field.label }}
|
|
33
|
+
{% if not field.nullable %}<span class="required">*</span>{% endif %}
|
|
34
|
+
</label>
|
|
35
|
+
{% endif %}
|
|
36
|
+
|
|
37
|
+
{% include "partials/form-widget.njk" %}
|
|
38
|
+
|
|
39
|
+
{# Error / help #}
|
|
40
|
+
<div class="field-feedback" id="feedback-{{ field.name }}"
|
|
41
|
+
{% if field.help %}data-help="{{ field.help }}"{% endif %}>
|
|
42
|
+
{% if hasError %}
|
|
43
|
+
<span class="form-error">
|
|
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][0] if errors[field.name] is iterable else errors[field.name] }}
|
|
46
|
+
</span>
|
|
47
|
+
{% elif field.help %}
|
|
48
|
+
<span class="form-help">{{ field.help }}</span>
|
|
49
|
+
{% endif %}
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
</div>
|
|
53
|
+
{% endif %}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{#
|
|
2
|
+
partials/form-footer.njk
|
|
3
|
+
Submit buttons, cancel, delete, and timestamps.
|
|
4
|
+
Expects in scope: isEdit, resource, record, adminPrefix
|
|
5
|
+
#}
|
|
6
|
+
<div class="flex items-center gap-2" style="margin-top:16px;flex-wrap:wrap">
|
|
7
|
+
<button type="submit" name="_submit" value="save" class="btn btn-primary" id="submit-btn">
|
|
8
|
+
<span class="icon icon-14"><svg viewBox="0 0 24 24"><use href="#ic-save"/></svg></span>
|
|
9
|
+
{{ 'Save Changes' if isEdit else 'Create ' + resource.singular }}
|
|
10
|
+
</button>
|
|
11
|
+
<button type="submit" name="_submit" value="continue" class="btn btn-ghost btn-sm">
|
|
12
|
+
Save and continue editing
|
|
13
|
+
</button>
|
|
14
|
+
{% if not isEdit %}
|
|
15
|
+
<button type="submit" name="_submit" value="add_another" class="btn btn-ghost btn-sm">
|
|
16
|
+
Save and add another
|
|
17
|
+
</button>
|
|
18
|
+
{% endif %}
|
|
19
|
+
<a href="{{ adminPrefix }}/{{ resource.slug }}" class="btn btn-ghost">Cancel</a>
|
|
20
|
+
|
|
21
|
+
{% if isEdit and resource.canDelete %}
|
|
22
|
+
<button type="button" class="btn btn-danger" style="margin-left:auto"
|
|
23
|
+
onclick="confirmDelete('{{ adminPrefix }}/{{ resource.slug }}/{{ record.id }}/delete', '{{ resource.singular }} #{{ record.id }}')">
|
|
24
|
+
<span class="icon icon-14"><svg viewBox="0 0 24 24"><use href="#ic-trash"/></svg></span>
|
|
25
|
+
Delete
|
|
26
|
+
</button>
|
|
27
|
+
{% endif %}
|
|
28
|
+
</div>
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
{#
|
|
2
|
+
partials/form-readonly.njk
|
|
3
|
+
Renders a single readonly field as disabled input.
|
|
4
|
+
Expects in scope: field, record
|
|
5
|
+
#}
|
|
6
|
+
|
|
7
|
+
<div class="form-group {% if field.span == 'full' or field.type == 'textarea' or field.type == 'json' or field.type == 'richtext' %}full{% elif field.span == 'third' %}w-third{% endif %}">
|
|
8
|
+
<label class="form-label" for="field-{{ field.name }}">{{ field.label }}</label>
|
|
9
|
+
|
|
10
|
+
{# ── Select ── #}
|
|
11
|
+
{% if field.type == 'select' and field.options %}
|
|
12
|
+
<select class="form-control" disabled>
|
|
13
|
+
{% if val %}
|
|
14
|
+
{% for opt in field.options %}
|
|
15
|
+
{% set optVal = opt.value if opt.value is defined else opt %}
|
|
16
|
+
{% set optLabel = opt.label if opt.label is defined else opt %}
|
|
17
|
+
<option {% if val == optVal %}selected{% endif %}>{{ optLabel }}</option>
|
|
18
|
+
{% endfor %}
|
|
19
|
+
{% else %}
|
|
20
|
+
<option>— Select —</option>
|
|
21
|
+
{% endif %}
|
|
22
|
+
</select>
|
|
23
|
+
|
|
24
|
+
{# ── Checkbox ── #}
|
|
25
|
+
{% elif field.type == 'checkbox' %}
|
|
26
|
+
<div class="toggle-field">
|
|
27
|
+
<label class="toggle-wrap" style="opacity:0.6;cursor:not-allowed">
|
|
28
|
+
<input type="checkbox" disabled {{ 'checked' if val else '' }} class="toggle-input">
|
|
29
|
+
<span class="toggle-track"><span class="toggle-thumb"></span></span>
|
|
30
|
+
<span class="toggle-label">{{ field.label }}</span>
|
|
31
|
+
</label>
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
{# ── Badge ── #}
|
|
35
|
+
{% elif field.type == 'badge' %}
|
|
36
|
+
{% set _bc = field.colors[val] if (field.colors and val and field.colors[val]) else 'gray' %}
|
|
37
|
+
<span class="badge badge-{{ _bc if val else 'gray' }}">{{ val if val else 'Not set' }}</span>
|
|
38
|
+
|
|
39
|
+
{# ── Textarea ── #}
|
|
40
|
+
{% elif field.type == 'textarea' %}
|
|
41
|
+
<textarea class="form-control" disabled readonly rows="4" placeholder="{{ field.placeholder or '' }}">{{ val }}</textarea>
|
|
42
|
+
|
|
43
|
+
{# ── JSON ── #}
|
|
44
|
+
{% elif field.type == 'json' %}
|
|
45
|
+
<textarea class="form-control" disabled readonly rows="6" style="font-family:monospace;font-size:12px">{{ val | dump if val else '' }}</textarea>
|
|
46
|
+
|
|
47
|
+
{# ── Richtext ── #}
|
|
48
|
+
{% elif field.type == 'richtext' %}
|
|
49
|
+
<div class="form-control" style="min-height:120px;background:var(--surface-muted);cursor:not-allowed" disabled>{{ val | safe if val else '' }}</div>
|
|
50
|
+
|
|
51
|
+
{# ── Date ── #}
|
|
52
|
+
{% elif field.type == 'date' %}
|
|
53
|
+
<div class="dp-wrap">
|
|
54
|
+
<input type="text" class="form-control dp-trigger" value="{{ val }}" placeholder="Select date…" disabled readonly>
|
|
55
|
+
<span class="dp-icon" style="opacity:0.5">
|
|
56
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
57
|
+
<rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/>
|
|
58
|
+
</svg>
|
|
59
|
+
</span>
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
{# ── Datetime ── #}
|
|
63
|
+
{% elif field.type == 'datetime' or field.type == 'datetime-local' %}
|
|
64
|
+
<div class="dp-wrap">
|
|
65
|
+
<input type="text" class="form-control dp-trigger" value="{{ val }}" placeholder="Select date and time…" disabled readonly>
|
|
66
|
+
<span class="dp-icon" style="opacity:0.5">
|
|
67
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
68
|
+
<rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/>
|
|
69
|
+
</svg>
|
|
70
|
+
</span>
|
|
71
|
+
</div>
|
|
72
|
+
|
|
73
|
+
{# ── Email ── #}
|
|
74
|
+
{% elif field.type == 'email' %}
|
|
75
|
+
<input type="email" class="form-control" value="{{ val }}" placeholder="{{ field.placeholder or 'name@example.com' }}" disabled readonly>
|
|
76
|
+
|
|
77
|
+
{# ── URL ── #}
|
|
78
|
+
{% elif field.type == 'url' %}
|
|
79
|
+
<input type="url" class="form-control" value="{{ val }}" placeholder="{{ field.placeholder or 'https://' }}" disabled readonly>
|
|
80
|
+
|
|
81
|
+
{# ── Phone ── #}
|
|
82
|
+
{% elif field.type == 'phone' %}
|
|
83
|
+
<input type="tel" class="form-control" value="{{ val }}" placeholder="{{ field.placeholder or '+1 555 000 0000' }}" disabled readonly>
|
|
84
|
+
|
|
85
|
+
{# ── Color ── #}
|
|
86
|
+
{% elif field.type == 'color' %}
|
|
87
|
+
<div class="flex items-center gap-2">
|
|
88
|
+
<input type="color" value="{{ val or '#000000' }}" disabled style="width:40px;height:36px;border:1px solid var(--border);border-radius:var(--radius-sm);cursor:not-allowed;padding:2px;opacity:0.6">
|
|
89
|
+
<input type="text" class="form-control" value="{{ val }}" placeholder="#000000" style="width:120px" disabled readonly>
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
{# ── Number ── #}
|
|
93
|
+
{% elif field.type == 'number' %}
|
|
94
|
+
<input type="number" class="form-control" value="{{ val }}" placeholder="{{ field.placeholder or '0' }}" disabled readonly>
|
|
95
|
+
|
|
96
|
+
{# ── Password ── #}
|
|
97
|
+
{% elif field.type == 'password' %}
|
|
98
|
+
<input type="password" class="form-control" value="{{ '••••••••' if val else '' }}" placeholder="{{ field.placeholder or '' }}" disabled readonly>
|
|
99
|
+
|
|
100
|
+
{# ── Image ── #}
|
|
101
|
+
{% elif field.type == 'image' %}
|
|
102
|
+
<div>
|
|
103
|
+
{% if val %}
|
|
104
|
+
<img src="{{ val }}" style="width:64px;height:64px;object-fit:cover;border-radius:var(--radius-sm);border:1px solid var(--border);display:block;margin-bottom:8px" alt="">
|
|
105
|
+
{% endif %}
|
|
106
|
+
<input type="text" class="form-control" value="{{ val }}" placeholder="https://example.com/image.jpg" disabled readonly>
|
|
107
|
+
</div>
|
|
108
|
+
|
|
109
|
+
{# ── Default text ── #}
|
|
110
|
+
{% else %}
|
|
111
|
+
<input type="text" class="form-control" value="{{ val }}" placeholder="{{ field.placeholder or '' }}" disabled readonly>
|
|
112
|
+
|
|
113
|
+
{% endif %}
|
|
114
|
+
</div>
|