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
|
@@ -1,22 +1,23 @@
|
|
|
1
1
|
{% extends "layouts/base.njk" %}
|
|
2
2
|
|
|
3
3
|
{% block title %}{{ 'Edit' if isEdit else 'New' }} {{ resource.singular }}{% endblock %}
|
|
4
|
+
|
|
4
5
|
{% block topbar_title %}
|
|
5
6
|
<span class="icon icon-16" style="color:var(--text-muted)">
|
|
6
|
-
<svg viewBox="0 0 24 24"><use href="#ic-{{ 'edit' if isEdit else '
|
|
7
|
+
<svg viewBox="0 0 24 24"><use href="#ic-{{ 'edit' if isEdit else 'file' }}"/></svg>
|
|
7
8
|
</span>
|
|
8
|
-
{{ 'Edit' if isEdit else 'New'
|
|
9
|
+
{{ ('Edit ' + resource.singular) if isEdit else ('New ' + resource.singular) }}
|
|
9
10
|
{% endblock %}
|
|
10
11
|
|
|
11
12
|
{% block topbar_actions %}
|
|
12
13
|
{% if isEdit and resource.canView %}
|
|
13
14
|
<a href="{{ adminPrefix }}/{{ resource.slug }}/{{ record.id }}" class="btn btn-ghost btn-sm">
|
|
14
|
-
<span class="icon icon-
|
|
15
|
+
<span class="icon icon-14"><svg viewBox="0 0 24 24"><use href="#ic-eye"/></svg></span>
|
|
15
16
|
View
|
|
16
17
|
</a>
|
|
17
18
|
{% endif %}
|
|
18
19
|
<a href="{{ adminPrefix }}/{{ resource.slug }}" class="btn btn-ghost btn-sm">
|
|
19
|
-
<span class="icon icon-
|
|
20
|
+
<span class="icon icon-14"><svg viewBox="0 0 24 24"><use href="#ic-arrow-left"/></svg></span>
|
|
20
21
|
Back
|
|
21
22
|
</a>
|
|
22
23
|
{% endblock %}
|
|
@@ -34,7 +35,7 @@
|
|
|
34
35
|
|
|
35
36
|
<div style="max-width:780px">
|
|
36
37
|
|
|
37
|
-
{#
|
|
38
|
+
{# Build tab list from formFields #}
|
|
38
39
|
{% set tabNames = [] %}
|
|
39
40
|
{% for f in formFields %}
|
|
40
41
|
{% if f.tab and f.tab not in tabNames %}
|
|
@@ -44,11 +45,10 @@
|
|
|
44
45
|
{% set hasTabs = tabNames | length > 0 %}
|
|
45
46
|
|
|
46
47
|
<form method="POST" action="{{ formAction }}" id="record-form" novalidate>
|
|
47
|
-
{
|
|
48
|
-
<input type="hidden" name="_method" value="PUT">
|
|
49
|
-
{% endif %}
|
|
48
|
+
<input type="hidden" name="_csrf" value="{{ csrfToken }}">
|
|
49
|
+
{% if isEdit %}<input type="hidden" name="_method" value="PUT">{% endif %}
|
|
50
50
|
|
|
51
|
-
{#
|
|
51
|
+
{# Validation errors summary #}
|
|
52
52
|
{% if errors | length %}
|
|
53
53
|
<div class="alert alert-error mb-4" id="error-summary">
|
|
54
54
|
<span class="icon icon-16" style="flex-shrink:0"><svg viewBox="0 0 24 24"><use href="#ic-alert-circle"/></svg></span>
|
|
@@ -56,73 +56,56 @@
|
|
|
56
56
|
<div class="fw-600" style="margin-bottom:4px">Please fix the following errors:</div>
|
|
57
57
|
<ul style="padding-left:14px;list-style:disc">
|
|
58
58
|
{% for field, msgs in errors %}
|
|
59
|
-
<li style="font-size:12.5px;margin-top:2px">
|
|
60
|
-
{{ msgs[0] if msgs is iterable else msgs }}
|
|
61
|
-
</li>
|
|
59
|
+
<li style="font-size:12.5px;margin-top:2px">{{ msgs[0] if msgs is iterable else msgs }}</li>
|
|
62
60
|
{% endfor %}
|
|
63
61
|
</ul>
|
|
64
62
|
</div>
|
|
65
63
|
</div>
|
|
66
64
|
{% endif %}
|
|
67
65
|
|
|
68
|
-
{# ══════════════════════════════════════
|
|
69
|
-
TABBED LAYOUT
|
|
70
|
-
══════════════════════════════════════ #}
|
|
71
66
|
{% if hasTabs %}
|
|
72
|
-
|
|
73
|
-
<div class="tab-nav mb-0" style="
|
|
74
|
-
{# First tab is always "General" for ungrouped fields #}
|
|
67
|
+
{# Tab nav #}
|
|
68
|
+
<div class="tab-nav mb-0" style="border-radius:var(--radius-lg) var(--radius-lg) 0 0;background:var(--surface);border:1px solid var(--border);border-bottom:none;padding:0 4px">
|
|
75
69
|
{% set hasUngrouped = false %}
|
|
76
70
|
{% for f in formFields %}{% if not f.tab %}{% set hasUngrouped = true %}{% endif %}{% endfor %}
|
|
77
|
-
|
|
78
71
|
{% if hasUngrouped %}
|
|
79
|
-
<button type="button" class="tab-btn active" data-tab="__general__" onclick="switchFormTab('__general__', this)">
|
|
80
|
-
General
|
|
81
|
-
</button>
|
|
72
|
+
<button type="button" class="tab-btn active" data-tab="__general__" onclick="switchFormTab('__general__', this)">General</button>
|
|
82
73
|
{% endif %}
|
|
83
74
|
{% for name in tabNames %}
|
|
84
|
-
<button type="button" class="tab-btn {% if not hasUngrouped and loop.first %}active{% endif %}" data-tab="{{ name }}" onclick="switchFormTab('{{ name }}', this)">
|
|
85
|
-
{{ name }}
|
|
86
|
-
</button>
|
|
75
|
+
<button type="button" class="tab-btn {% if not hasUngrouped and loop.first %}active{% endif %}" data-tab="{{ name }}" onclick="switchFormTab('{{ name }}', this)">{{ name }}</button>
|
|
87
76
|
{% endfor %}
|
|
88
77
|
</div>
|
|
89
78
|
|
|
90
|
-
{# General (ungrouped)
|
|
79
|
+
{# General (ungrouped) tab panel #}
|
|
91
80
|
{% if hasUngrouped %}
|
|
92
81
|
<div class="card tab-form-panel active" id="fpanel-__general__" style="border-top-left-radius:0;border-top-right-radius:0">
|
|
93
|
-
<div class="card-body">
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
{% endfor %}
|
|
101
|
-
</div>
|
|
102
|
-
</div>
|
|
82
|
+
<div class="card-body"><div class="form-grid">
|
|
83
|
+
{% for field in formFields %}{% if not field.tab %}
|
|
84
|
+
{% set val = record[field.name] if record[field.name] is defined else '' %}
|
|
85
|
+
{% set hasError = errors[field.name] is defined %}
|
|
86
|
+
{% include "partials/form-field.njk" %}
|
|
87
|
+
{% endif %}{% endfor %}
|
|
88
|
+
</div></div>
|
|
103
89
|
</div>
|
|
104
90
|
{% endif %}
|
|
105
91
|
|
|
106
92
|
{# Named tab panels #}
|
|
107
93
|
{% for name in tabNames %}
|
|
108
|
-
<div class="card tab-form-panel {% if not hasUngrouped and loop.first %}active{% endif %}"
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
</div>
|
|
94
|
+
<div class="card tab-form-panel {% if not hasUngrouped and loop.first %}active{% endif %}"
|
|
95
|
+
id="fpanel-{{ name | tabId }}"
|
|
96
|
+
style="border-top-left-radius:0;border-top-right-radius:0;{% if not loop.first or hasUngrouped %}display:none{% endif %}">
|
|
97
|
+
<div class="card-body"><div class="form-grid">
|
|
98
|
+
{% for field in formFields %}{% if field.tab == name %}
|
|
99
|
+
{% set val = record[field.name] if record[field.name] is defined else '' %}
|
|
100
|
+
{% set hasError = errors[field.name] is defined %}
|
|
101
|
+
{% include "partials/form-field.njk" %}
|
|
102
|
+
{% endif %}{% endfor %}
|
|
103
|
+
</div></div>
|
|
118
104
|
</div>
|
|
119
105
|
{% endfor %}
|
|
120
106
|
|
|
121
|
-
{# ══════════════════════════════════════
|
|
122
|
-
FLAT LAYOUT (no tabs)
|
|
123
|
-
══════════════════════════════════════ #}
|
|
124
107
|
{% else %}
|
|
125
|
-
|
|
108
|
+
{# Flat layout — no tabs #}
|
|
126
109
|
<div class="card">
|
|
127
110
|
<div class="card-header">
|
|
128
111
|
<span class="card-title">
|
|
@@ -133,264 +116,20 @@
|
|
|
133
116
|
</span>
|
|
134
117
|
{% if isEdit %}<span class="badge badge-gray text-xs">ID: {{ record.id }}</span>{% endif %}
|
|
135
118
|
</div>
|
|
136
|
-
<div class="card-body">
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
<div class="fieldset-heading">{{ field.label }}</div>
|
|
144
|
-
</div>
|
|
145
|
-
|
|
146
|
-
{# ── Readonly field ── #}
|
|
147
|
-
{% elif field.isReadonly %}
|
|
148
|
-
<div class="form-group {% if field.span == 'full' or field.type == 'textarea' or field.type == 'json' or field.type == 'richtext' %}full{% endif %}">
|
|
149
|
-
<div class="form-label">{{ field.label }}</div>
|
|
150
|
-
<div class="readonly-value">
|
|
151
|
-
{% set val = record[field.name] %}
|
|
152
|
-
{% if val is not none and val != '' %}
|
|
153
|
-
{% if field.type == 'boolean' %}
|
|
154
|
-
{% if val %}<span class="badge badge-green">Yes</span>{% else %}<span class="badge badge-gray">No</span>{% endif %}
|
|
155
|
-
{% elif field.type == 'badge' %}
|
|
156
|
-
<span class="badge badge-blue">{{ val }}</span>
|
|
157
|
-
{% elif field.type == 'datetime' %}
|
|
158
|
-
{{ val }}
|
|
159
|
-
{% elif field.type == 'json' %}
|
|
160
|
-
<pre class="readonly-pre">{{ val | dump }}</pre>
|
|
161
|
-
{% elif field.type == 'image' %}
|
|
162
|
-
<img src="{{ val }}" class="cell-image" style="width:48px;height:48px">
|
|
163
|
-
{% elif field.type == 'url' %}
|
|
164
|
-
<a href="{{ val }}" target="_blank" rel="noopener" style="color:var(--primary)">{{ val }}</a>
|
|
165
|
-
{% elif field.type == 'color' %}
|
|
166
|
-
<span style="display:inline-flex;align-items:center;gap:8px"><span style="width:18px;height:18px;border-radius:4px;background:{{ val }};border:1px solid var(--border)"></span>{{ val }}</span>
|
|
167
|
-
{% else %}
|
|
168
|
-
{{ val }}
|
|
169
|
-
{% endif %}
|
|
170
|
-
{% else %}
|
|
171
|
-
<span class="cell-muted">—</span>
|
|
172
|
-
{% endif %}
|
|
173
|
-
</div>
|
|
174
|
-
</div>
|
|
175
|
-
|
|
176
|
-
{# ── Editable field ── #}
|
|
177
|
-
{% else %}
|
|
178
|
-
{% set val = record[field.name] if record[field.name] is defined else '' %}
|
|
179
|
-
{% set hasError = errors[field.name] is defined %}
|
|
180
|
-
<div class="form-group
|
|
181
|
-
{% if field.span == 'full' %}full
|
|
182
|
-
{% elif field.span == 'third' %}w-third
|
|
183
|
-
{% elif field.type == 'textarea' or field.type == 'json' or field.type == 'richtext' %}full
|
|
184
|
-
{% elif field.type == 'boolean' %}w-third
|
|
185
|
-
{% endif %}"
|
|
186
|
-
data-field="{{ field.name }}">
|
|
187
|
-
|
|
188
|
-
{% if field.type != 'boolean' %}
|
|
189
|
-
<label class="form-label" for="field-{{ field.name }}">
|
|
190
|
-
{{ field.label }}
|
|
191
|
-
{% if not field.nullable %}<span class="required">*</span>{% endif %}
|
|
192
|
-
</label>
|
|
193
|
-
{% endif %}
|
|
194
|
-
|
|
195
|
-
{# ── Select ── #}
|
|
196
|
-
{% if field.type == 'select' and field.options %}
|
|
197
|
-
<select id="field-{{ field.name }}" name="{{ field.name }}"
|
|
198
|
-
class="form-control{% if hasError %} error{% endif %}"
|
|
199
|
-
{% if not field.nullable %}data-required="true"{% endif %}>
|
|
200
|
-
<option value="">— Select —</option>
|
|
201
|
-
{% for opt in field.options %}
|
|
202
|
-
{% set optVal = opt.value if opt.value is defined else opt %}
|
|
203
|
-
{% set optLabel = opt.label if opt.label is defined else opt %}
|
|
204
|
-
<option value="{{ optVal }}" {% if val == optVal %}selected{% endif %}>{{ optLabel }}</option>
|
|
205
|
-
{% endfor %}
|
|
206
|
-
</select>
|
|
207
|
-
|
|
208
|
-
{# ── Boolean ── #}
|
|
209
|
-
{% elif field.type == 'boolean' %}
|
|
210
|
-
<div style="padding:8px 0">
|
|
211
|
-
<label class="check-group" for="field-{{ field.name }}" style="cursor:pointer">
|
|
212
|
-
<input type="checkbox" id="field-{{ field.name }}" name="{{ field.name }}"
|
|
213
|
-
class="check-input" value="1" {% if val %}checked{% endif %}>
|
|
214
|
-
<span class="check-label">{{ field.label }}</span>
|
|
215
|
-
</label>
|
|
216
|
-
</div>
|
|
217
|
-
|
|
218
|
-
{# ── Textarea ── #}
|
|
219
|
-
{% elif field.type == 'textarea' %}
|
|
220
|
-
<textarea id="field-{{ field.name }}" name="{{ field.name }}"
|
|
221
|
-
class="form-control{% if hasError %} error{% endif %}"
|
|
222
|
-
placeholder="{{ field.placeholder or '' }}"
|
|
223
|
-
{% if not field.nullable %}data-required="true"{% endif %}
|
|
224
|
-
rows="4">{{ val }}</textarea>
|
|
225
|
-
|
|
226
|
-
{# ── JSON ── #}
|
|
227
|
-
{% elif field.type == 'json' %}
|
|
228
|
-
<textarea id="field-{{ field.name }}" name="{{ field.name }}"
|
|
229
|
-
class="form-control{% if hasError %} error{% endif %}"
|
|
230
|
-
placeholder='{"key": "value"}'
|
|
231
|
-
style="font-family:'DM Mono',monospace;font-size:12px"
|
|
232
|
-
data-validate="json"
|
|
233
|
-
rows="5">{{ val | dump if val else '' }}</textarea>
|
|
234
|
-
|
|
235
|
-
{# ── Richtext ── #}
|
|
236
|
-
{% elif field.type == 'richtext' %}
|
|
237
|
-
<div class="richtext-wrap">
|
|
238
|
-
<div class="richtext-toolbar">
|
|
239
|
-
<button type="button" onclick="rtCmd('bold')" title="Bold"><b>B</b></button>
|
|
240
|
-
<button type="button" onclick="rtCmd('italic')" title="Italic"><i>I</i></button>
|
|
241
|
-
<button type="button" onclick="rtCmd('underline')" title="Underline"><u>U</u></button>
|
|
242
|
-
<span class="rt-sep"></span>
|
|
243
|
-
<button type="button" onclick="rtCmd('insertUnorderedList')" title="Bullet list">≡</button>
|
|
244
|
-
<button type="button" onclick="rtCmd('insertOrderedList')" title="Numbered list">1.</button>
|
|
245
|
-
<span class="rt-sep"></span>
|
|
246
|
-
<button type="button" onclick="rtLink()" title="Link">🔗</button>
|
|
247
|
-
</div>
|
|
248
|
-
<div id="rt-{{ field.name }}" class="richtext-editor form-control{% if hasError %} error{% endif %}"
|
|
249
|
-
contenteditable="true"
|
|
250
|
-
style="min-height:120px;height:auto">{{ val | safe }}</div>
|
|
251
|
-
<input type="hidden" id="field-{{ field.name }}" name="{{ field.name }}" value="{{ val }}">
|
|
252
|
-
</div>
|
|
253
|
-
|
|
254
|
-
{# ── Password ── #}
|
|
255
|
-
{% elif field.type == 'password' %}
|
|
256
|
-
<div style="position:relative">
|
|
257
|
-
<input type="password" id="field-{{ field.name }}" name="{{ field.name }}"
|
|
258
|
-
class="form-control{% if hasError %} error{% endif %}"
|
|
259
|
-
placeholder="{{ 'Leave blank to keep current' if isEdit else '' }}"
|
|
260
|
-
{% if not field.nullable and not isEdit %}data-required="true"{% endif %}
|
|
261
|
-
style="padding-right:40px">
|
|
262
|
-
<button type="button" class="pw-toggle"
|
|
263
|
-
onclick="this.previousElementSibling.type=this.previousElementSibling.type==='password'?'text':'password'">
|
|
264
|
-
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
265
|
-
<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"/>
|
|
266
|
-
</svg>
|
|
267
|
-
</button>
|
|
268
|
-
</div>
|
|
269
|
-
|
|
270
|
-
{# ── Email ── #}
|
|
271
|
-
{% elif field.type == 'email' %}
|
|
272
|
-
<input type="email" id="field-{{ field.name }}" name="{{ field.name }}"
|
|
273
|
-
value="{{ val }}"
|
|
274
|
-
class="form-control{% if hasError %} error{% endif %}"
|
|
275
|
-
placeholder="{{ field.placeholder or 'name@example.com' }}"
|
|
276
|
-
data-validate="email"
|
|
277
|
-
{% if not field.nullable %}data-required="true"{% endif %}>
|
|
278
|
-
|
|
279
|
-
{# ── URL ── #}
|
|
280
|
-
{% elif field.type == 'url' %}
|
|
281
|
-
<input type="url" id="field-{{ field.name }}" name="{{ field.name }}"
|
|
282
|
-
value="{{ val }}"
|
|
283
|
-
class="form-control{% if hasError %} error{% endif %}"
|
|
284
|
-
placeholder="{{ field.placeholder or 'https://' }}"
|
|
285
|
-
data-validate="url"
|
|
286
|
-
{% if not field.nullable %}data-required="true"{% endif %}>
|
|
287
|
-
|
|
288
|
-
{# ── Phone ── #}
|
|
289
|
-
{% elif field.type == 'phone' %}
|
|
290
|
-
<input type="tel" id="field-{{ field.name }}" name="{{ field.name }}"
|
|
291
|
-
value="{{ val }}"
|
|
292
|
-
class="form-control{% if hasError %} error{% endif %}"
|
|
293
|
-
placeholder="{{ field.placeholder or '+1 555 000 0000' }}"
|
|
294
|
-
{% if not field.nullable %}data-required="true"{% endif %}>
|
|
295
|
-
|
|
296
|
-
{# ── Color ── #}
|
|
297
|
-
{% elif field.type == 'color' %}
|
|
298
|
-
<div class="flex items-center gap-2">
|
|
299
|
-
<input type="color" id="field-{{ field.name }}-picker"
|
|
300
|
-
value="{{ val or '#000000' }}"
|
|
301
|
-
style="width:40px;height:36px;border:1px solid var(--border);border-radius:var(--radius-sm);cursor:pointer;padding:2px"
|
|
302
|
-
oninput="document.getElementById('field-{{ field.name }}').value=this.value">
|
|
303
|
-
<input type="text" id="field-{{ field.name }}" name="{{ field.name }}"
|
|
304
|
-
value="{{ val }}"
|
|
305
|
-
class="form-control{% if hasError %} error{% endif %}"
|
|
306
|
-
placeholder="#000000"
|
|
307
|
-
style="width:120px"
|
|
308
|
-
oninput="if(/^#[0-9a-fA-F]{6}$/.test(this.value))document.getElementById('field-{{ field.name }}-picker').value=this.value">
|
|
309
|
-
</div>
|
|
310
|
-
|
|
311
|
-
{# ── Number ── #}
|
|
312
|
-
{% elif field.type == 'number' %}
|
|
313
|
-
<input type="number" id="field-{{ field.name }}" name="{{ field.name }}"
|
|
314
|
-
value="{{ val }}"
|
|
315
|
-
class="form-control{% if hasError %} error{% endif %}"
|
|
316
|
-
placeholder="{{ field.placeholder or '0' }}"
|
|
317
|
-
{% if field.min is not none %}min="{{ field.min }}" data-min="{{ field.min }}"{% endif %}
|
|
318
|
-
{% if field.max is not none %}max="{{ field.max }}" data-max="{{ field.max }}"{% endif %}
|
|
319
|
-
{% if not field.nullable %}data-required="true"{% endif %}>
|
|
320
|
-
|
|
321
|
-
{# ── Date ── #}
|
|
322
|
-
{% elif field.type == 'date' %}
|
|
323
|
-
<input type="date" id="field-{{ field.name }}" name="{{ field.name }}"
|
|
324
|
-
value="{{ val }}"
|
|
325
|
-
class="form-control{% if hasError %} error{% endif %}"
|
|
326
|
-
{% if not field.nullable %}data-required="true"{% endif %}>
|
|
327
|
-
|
|
328
|
-
{# ── Datetime ── #}
|
|
329
|
-
{% elif field.type == 'datetime' %}
|
|
330
|
-
<input type="datetime-local" id="field-{{ field.name }}" name="{{ field.name }}"
|
|
331
|
-
value="{{ val }}"
|
|
332
|
-
class="form-control{% if hasError %} error{% endif %}"
|
|
333
|
-
{% if not field.nullable %}data-required="true"{% endif %}>
|
|
334
|
-
|
|
335
|
-
{# ── Default text ── #}
|
|
336
|
-
{% else %}
|
|
337
|
-
<input type="text" id="field-{{ field.name }}" name="{{ field.name }}"
|
|
338
|
-
value="{{ val }}"
|
|
339
|
-
class="form-control{% if hasError %} error{% endif %}"
|
|
340
|
-
placeholder="{{ field.placeholder or '' }}"
|
|
341
|
-
{% if field.max is not none %}maxlength="{{ field.max }}"{% endif %}
|
|
342
|
-
{% if not field.nullable %}data-required="true"{% endif %}
|
|
343
|
-
{% if field.prepopulate %}data-prepopulate="{{ field.prepopulate }}"{% endif %}>
|
|
344
|
-
{% endif %}
|
|
345
|
-
|
|
346
|
-
{# ── Error / help ── #}
|
|
347
|
-
<div class="field-feedback" id="feedback-{{ field.name }}">
|
|
348
|
-
{% if hasError %}
|
|
349
|
-
<span class="form-error">
|
|
350
|
-
<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>
|
|
351
|
-
{{ errors[field.name][0] if errors[field.name] is iterable else errors[field.name] }}
|
|
352
|
-
</span>
|
|
353
|
-
{% elif field.help %}
|
|
354
|
-
<span class="form-help">{{ field.help }}</span>
|
|
355
|
-
{% endif %}
|
|
356
|
-
</div>
|
|
357
|
-
</div>
|
|
358
|
-
{% endif %}
|
|
359
|
-
|
|
360
|
-
{% endfor %}
|
|
361
|
-
</div>
|
|
362
|
-
</div>
|
|
119
|
+
<div class="card-body"><div class="form-grid">
|
|
120
|
+
{% for field in formFields %}
|
|
121
|
+
{% set val = record[field.name] if record[field.name] is defined else '' %}
|
|
122
|
+
{% set hasError = errors[field.name] is defined %}
|
|
123
|
+
{% include "partials/form-field.njk" %}
|
|
124
|
+
{% endfor %}
|
|
125
|
+
</div></div>
|
|
363
126
|
</div>
|
|
364
127
|
{% endif %}
|
|
365
128
|
|
|
366
|
-
{
|
|
367
|
-
<div class="flex items-center gap-2" style="margin-top:16px;flex-wrap:wrap">
|
|
368
|
-
<button type="submit" name="_submit" value="save" class="btn btn-primary" id="submit-btn">
|
|
369
|
-
<span class="icon icon-14"><svg viewBox="0 0 24 24"><use href="#ic-save"/></svg></span>
|
|
370
|
-
{{ 'Save Changes' if isEdit else 'Create ' + resource.singular }}
|
|
371
|
-
</button>
|
|
372
|
-
<button type="submit" name="_submit" value="continue" class="btn btn-ghost btn-sm">
|
|
373
|
-
Save and continue editing
|
|
374
|
-
</button>
|
|
375
|
-
{% if not isEdit %}
|
|
376
|
-
<button type="submit" name="_submit" value="add_another" class="btn btn-ghost btn-sm">
|
|
377
|
-
Save and add another
|
|
378
|
-
</button>
|
|
379
|
-
{% endif %}
|
|
380
|
-
<a href="{{ adminPrefix }}/{{ resource.slug }}" class="btn btn-ghost">Cancel</a>
|
|
381
|
-
|
|
382
|
-
{% if isEdit and resource.canDelete %}
|
|
383
|
-
<button type="button" class="btn btn-danger" style="margin-left:auto"
|
|
384
|
-
onclick="confirmDelete('{{ adminPrefix }}/{{ resource.slug }}/{{ record.id }}/delete', '{{ resource.singular }} #{{ record.id }}')">
|
|
385
|
-
<span class="icon icon-14"><svg viewBox="0 0 24 24"><use href="#ic-trash"/></svg></span>
|
|
386
|
-
Delete
|
|
387
|
-
</button>
|
|
388
|
-
{% endif %}
|
|
389
|
-
</div>
|
|
390
|
-
|
|
129
|
+
{% include "partials/form-footer.njk" %}
|
|
391
130
|
</form>
|
|
392
131
|
|
|
393
|
-
{#
|
|
132
|
+
{# Timestamps #}
|
|
394
133
|
{% if isEdit and (record.created_at or record.updated_at) %}
|
|
395
134
|
<div class="flex gap-4" style="margin-top:12px;padding:0 2px">
|
|
396
135
|
{% if record.created_at %}
|
|
@@ -407,302 +146,11 @@
|
|
|
407
146
|
{% endif %}
|
|
408
147
|
</div>
|
|
409
148
|
{% endif %}
|
|
410
|
-
</div>
|
|
411
|
-
|
|
412
|
-
<style>
|
|
413
|
-
/* ── Tab nav (form) ── */
|
|
414
|
-
.tab-nav {
|
|
415
|
-
display: flex;
|
|
416
|
-
gap: 0;
|
|
417
|
-
overflow-x: auto;
|
|
418
|
-
}
|
|
419
|
-
.tab-btn {
|
|
420
|
-
padding: 10px 18px;
|
|
421
|
-
font-size: 13.5px;
|
|
422
|
-
font-weight: 500;
|
|
423
|
-
color: var(--text-muted);
|
|
424
|
-
background: none;
|
|
425
|
-
border: none;
|
|
426
|
-
border-bottom: 2px solid transparent;
|
|
427
|
-
margin-bottom: -2px;
|
|
428
|
-
cursor: pointer;
|
|
429
|
-
font-family: inherit;
|
|
430
|
-
white-space: nowrap;
|
|
431
|
-
transition: color .12s, border-color .12s;
|
|
432
|
-
}
|
|
433
|
-
.tab-btn:hover { color: var(--text-soft); }
|
|
434
|
-
.tab-btn.active { color: var(--primary); border-bottom-color: var(--primary); background: var(--primary-soft); }
|
|
435
|
-
|
|
436
|
-
.tab-form-panel { display: none; }
|
|
437
|
-
.tab-form-panel.active { display: block; }
|
|
438
|
-
|
|
439
|
-
/* ── Readonly ── */
|
|
440
|
-
.readonly-value {
|
|
441
|
-
font-size: 13.5px;
|
|
442
|
-
color: var(--text-soft);
|
|
443
|
-
padding: 8px 12px;
|
|
444
|
-
background: var(--surface2);
|
|
445
|
-
border: 1px solid var(--border-soft);
|
|
446
|
-
border-radius: var(--radius-sm);
|
|
447
|
-
min-height: 36px;
|
|
448
|
-
display: flex;
|
|
449
|
-
align-items: center;
|
|
450
|
-
gap: 8px;
|
|
451
|
-
}
|
|
452
|
-
.readonly-pre {
|
|
453
|
-
background: var(--surface2);
|
|
454
|
-
border: 1px solid var(--border);
|
|
455
|
-
border-radius: var(--radius-sm);
|
|
456
|
-
padding: 10px;
|
|
457
|
-
font-family: 'DM Mono', monospace;
|
|
458
|
-
font-size: 12px;
|
|
459
|
-
overflow-x: auto;
|
|
460
|
-
white-space: pre-wrap;
|
|
461
|
-
margin: 0;
|
|
462
|
-
color: var(--text-soft);
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
/* ── Password toggle ── */
|
|
466
|
-
.pw-toggle {
|
|
467
|
-
position: absolute;
|
|
468
|
-
right: 10px;
|
|
469
|
-
top: 50%;
|
|
470
|
-
transform: translateY(-50%);
|
|
471
|
-
background: none;
|
|
472
|
-
border: none;
|
|
473
|
-
cursor: pointer;
|
|
474
|
-
color: var(--text-muted);
|
|
475
|
-
display: flex;
|
|
476
|
-
align-items: center;
|
|
477
|
-
padding: 0;
|
|
478
|
-
}
|
|
479
|
-
.pw-toggle:hover { color: var(--text-soft); }
|
|
480
|
-
|
|
481
|
-
/* ── Richtext ── */
|
|
482
|
-
.richtext-wrap { border: 1px solid var(--border); border-radius: var(--radius-sm); overflow: hidden; }
|
|
483
|
-
.richtext-toolbar {
|
|
484
|
-
display: flex;
|
|
485
|
-
align-items: center;
|
|
486
|
-
gap: 2px;
|
|
487
|
-
padding: 6px 8px;
|
|
488
|
-
background: var(--surface2);
|
|
489
|
-
border-bottom: 1px solid var(--border);
|
|
490
|
-
}
|
|
491
|
-
.richtext-toolbar button {
|
|
492
|
-
width: 28px; height: 28px;
|
|
493
|
-
display: flex; align-items: center; justify-content: center;
|
|
494
|
-
background: none;
|
|
495
|
-
border: 1px solid transparent;
|
|
496
|
-
border-radius: 4px;
|
|
497
|
-
cursor: pointer;
|
|
498
|
-
font-size: 13px;
|
|
499
|
-
color: var(--text-soft);
|
|
500
|
-
font-family: inherit;
|
|
501
|
-
transition: all .1s;
|
|
502
|
-
}
|
|
503
|
-
.richtext-toolbar button:hover { background: var(--surface3); border-color: var(--border); }
|
|
504
|
-
.rt-sep { width: 1px; height: 18px; background: var(--border); margin: 0 4px; }
|
|
505
|
-
.richtext-editor {
|
|
506
|
-
border: none !important;
|
|
507
|
-
border-radius: 0 !important;
|
|
508
|
-
outline: none !important;
|
|
509
|
-
box-shadow: none !important;
|
|
510
|
-
padding: 12px;
|
|
511
|
-
line-height: 1.6;
|
|
512
|
-
}
|
|
513
|
-
.richtext-editor:focus { outline: none; }
|
|
514
|
-
|
|
515
|
-
/* ── Field feedback ── */
|
|
516
|
-
.field-feedback:empty { display: none; }
|
|
517
|
-
|
|
518
|
-
@keyframes spin { to { transform: rotate(360deg); } }
|
|
519
|
-
</style>
|
|
520
|
-
|
|
521
|
-
<script>
|
|
522
|
-
// ── Tab switching ──────────────────────────────────────────────
|
|
523
|
-
function switchFormTab(name, btn) {
|
|
524
|
-
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
|
|
525
|
-
document.querySelectorAll('.tab-form-panel').forEach(p => { p.classList.remove('active'); p.style.display = 'none'; });
|
|
526
|
-
btn.classList.add('active');
|
|
527
|
-
const safeId = name.replace(/\s+/g, '-');
|
|
528
|
-
const panel = document.getElementById(`fpanel-${safeId}`);
|
|
529
|
-
if (panel) { panel.classList.add('active'); panel.style.display = 'block'; }
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
// ── Richtext sync ─────────────────────────────────────────────
|
|
533
|
-
document.querySelectorAll('.richtext-editor').forEach(editor => {
|
|
534
|
-
const fieldName = editor.id.replace('rt-', '');
|
|
535
|
-
const hidden = document.getElementById(`field-${fieldName}`);
|
|
536
|
-
if (!hidden) return;
|
|
537
|
-
editor.addEventListener('input', () => { hidden.value = editor.innerHTML; });
|
|
538
|
-
});
|
|
539
|
-
function rtCmd(cmd) {
|
|
540
|
-
document.execCommand(cmd, false, null);
|
|
541
|
-
}
|
|
542
|
-
function rtLink() {
|
|
543
|
-
const url = prompt('Enter URL:');
|
|
544
|
-
if (url) document.execCommand('createLink', false, url);
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
// ── Client-side validation ─────────────────────────────────────
|
|
548
|
-
const form = document.getElementById('record-form');
|
|
549
|
-
|
|
550
|
-
function showFieldError(name, msg) {
|
|
551
|
-
const input = document.getElementById(`field-${name}`) || document.querySelector(`[name="${name}"]`);
|
|
552
|
-
if (input) input.classList.add('error');
|
|
553
|
-
const fb = document.getElementById(`feedback-${name}`);
|
|
554
|
-
if (fb) {
|
|
555
|
-
fb.innerHTML = `<span class="form-error">
|
|
556
|
-
<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>
|
|
557
|
-
${msg}
|
|
558
|
-
</span>`;
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
function clearFieldError(name) {
|
|
563
|
-
const input = document.getElementById(`field-${name}`) || document.querySelector(`[name="${name}"]`);
|
|
564
|
-
if (input) input.classList.remove('error');
|
|
565
|
-
const fb = document.getElementById(`feedback-${name}`);
|
|
566
|
-
if (fb) { const help = fb.dataset.help; fb.innerHTML = help ? `<span class="form-help">${help}</span>` : ''; }
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
// Live validation on blur
|
|
570
|
-
document.querySelectorAll('.form-control').forEach(input => {
|
|
571
|
-
input.addEventListener('blur', () => validateField(input));
|
|
572
|
-
input.addEventListener('input', () => {
|
|
573
|
-
if (input.classList.contains('error')) validateField(input);
|
|
574
|
-
});
|
|
575
|
-
});
|
|
576
|
-
|
|
577
|
-
function validateField(input) {
|
|
578
|
-
const name = input.name;
|
|
579
|
-
if (!name) return true;
|
|
580
|
-
const val = input.value.trim();
|
|
581
|
-
const required = input.dataset.required === 'true';
|
|
582
|
-
const validate = input.dataset.validate;
|
|
583
149
|
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
if (required && val === '') {
|
|
587
|
-
const label = document.querySelector(`label[for="field-${name}"]`)?.textContent?.replace('*','').trim() || name;
|
|
588
|
-
showFieldError(name, `${label} is required`);
|
|
589
|
-
return false;
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
if (val && validate === 'email') {
|
|
593
|
-
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(val)) {
|
|
594
|
-
showFieldError(name, 'Please enter a valid email address');
|
|
595
|
-
return false;
|
|
596
|
-
}
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
if (val && validate === 'url') {
|
|
600
|
-
try { new URL(val); } catch {
|
|
601
|
-
showFieldError(name, 'Please enter a valid URL (include https://)');
|
|
602
|
-
return false;
|
|
603
|
-
}
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
if (val && validate === 'json') {
|
|
607
|
-
try { JSON.parse(val); } catch {
|
|
608
|
-
showFieldError(name, 'Invalid JSON format');
|
|
609
|
-
return false;
|
|
610
|
-
}
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
if (val && input.dataset.min !== undefined && Number(val) < Number(input.dataset.min)) {
|
|
614
|
-
showFieldError(name, `Minimum value is ${input.dataset.min}`);
|
|
615
|
-
return false;
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
if (val && input.dataset.max !== undefined && Number(val) > Number(input.dataset.max)) {
|
|
619
|
-
showFieldError(name, `Maximum value is ${input.dataset.max}`);
|
|
620
|
-
return false;
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
return true;
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
// Full form validation on submit
|
|
627
|
-
form?.addEventListener('submit', function(e) {
|
|
628
|
-
let valid = true;
|
|
629
|
-
let firstError = null;
|
|
630
|
-
|
|
631
|
-
document.querySelectorAll('.form-control').forEach(input => {
|
|
632
|
-
if (!validateField(input)) {
|
|
633
|
-
valid = false;
|
|
634
|
-
if (!firstError) firstError = input;
|
|
635
|
-
}
|
|
636
|
-
});
|
|
637
|
-
|
|
638
|
-
if (!valid) {
|
|
639
|
-
e.preventDefault();
|
|
640
|
-
firstError?.focus();
|
|
641
|
-
firstError?.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
642
|
-
return;
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
// Prevent double-submit
|
|
646
|
-
const btn = document.getElementById('submit-btn');
|
|
647
|
-
if (btn) {
|
|
648
|
-
btn.disabled = true;
|
|
649
|
-
btn.innerHTML = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="animation:spin 1s linear infinite"><polyline points="23 4 23 10 17 10"/><polyline points="1 20 1 14 7 14"/><path d="M3.51 9a9 9 0 0114.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0020.49 15"/></svg> Saving…`;
|
|
650
|
-
}
|
|
651
|
-
});
|
|
652
|
-
|
|
653
|
-
// ── Auto-switch to the tab containing the first error
|
|
654
|
-
const firstErrorField = document.querySelector('.form-control.error');
|
|
655
|
-
if (firstErrorField) {
|
|
656
|
-
const group = firstErrorField.closest('[data-field]');
|
|
657
|
-
if (group) {
|
|
658
|
-
const panel = group.closest('.tab-form-panel');
|
|
659
|
-
if (panel) {
|
|
660
|
-
const panelId = panel.id.replace('fpanel-', '');
|
|
661
|
-
const btn = document.querySelector(`.tab-btn[data-tab="${panelId}"]`);
|
|
662
|
-
if (btn) switchFormTab(panelId, btn);
|
|
663
|
-
}
|
|
664
|
-
}
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
// ── Prepopulate (slug auto-fill from source field) ────────────
|
|
668
|
-
(function() {
|
|
669
|
-
const prepopMappings = {};
|
|
670
|
-
document.querySelectorAll('[data-prepopulate]').forEach(el => {
|
|
671
|
-
prepopMappings[el.name] = el.dataset.prepopulate;
|
|
672
|
-
});
|
|
673
|
-
|
|
674
|
-
function slugify(str) {
|
|
675
|
-
return str.toLowerCase()
|
|
676
|
-
.replace(/[^\w\s-]/g, '')
|
|
677
|
-
.replace(/[\s_]+/g, '-')
|
|
678
|
-
.replace(/^-+|-+$/g, '');
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
for (const [targetName, sourceName] of Object.entries(prepopMappings)) {
|
|
682
|
-
const srcEl = document.querySelector(`[name="${sourceName}"]`);
|
|
683
|
-
const tgtEl = document.querySelector(`[name="${targetName}"]`);
|
|
684
|
-
if (!srcEl || !tgtEl) continue;
|
|
685
|
-
|
|
686
|
-
let userEdited = !!tgtEl.value;
|
|
687
|
-
tgtEl.addEventListener('input', () => { userEdited = true; });
|
|
688
|
-
srcEl.addEventListener('input', () => {
|
|
689
|
-
if (!userEdited) tgtEl.value = slugify(srcEl.value);
|
|
690
|
-
});
|
|
691
|
-
}
|
|
692
|
-
})();
|
|
693
|
-
</script>
|
|
150
|
+
</div>
|
|
694
151
|
|
|
695
|
-
<
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
text-transform: uppercase;
|
|
700
|
-
letter-spacing: .6px;
|
|
701
|
-
color: var(--text-muted);
|
|
702
|
-
padding: 16px 0 10px;
|
|
703
|
-
border-bottom: 1px solid var(--border-soft);
|
|
704
|
-
margin-bottom: 2px;
|
|
705
|
-
grid-column: 1 / -1;
|
|
706
|
-
}
|
|
707
|
-
</style>
|
|
152
|
+
<script src="{{ adminPrefix }}/static/date-picker.js"></script>
|
|
153
|
+
{% include "partials/form-scripts.njk" %}
|
|
154
|
+
{% include "partials/json-dialog.njk" %}
|
|
155
|
+
<script src="{{ adminPrefix }}/static/json-editor.js"></script>
|
|
708
156
|
{% endblock %}
|