millas 0.2.5 → 0.2.7
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 +1 -1
- package/src/admin/Admin.js +241 -62
- package/src/admin/AdminAuth.js +281 -0
- package/src/admin/index.js +6 -1
- package/src/admin/resources/AdminResource.js +180 -29
- package/src/admin/views/layouts/base.njk +39 -2
- package/src/admin/views/pages/detail.njk +322 -0
- package/src/admin/views/pages/form.njk +571 -125
- package/src/admin/views/pages/list.njk +454 -0
- package/src/admin/views/pages/login.njk +354 -0
- package/src/index.js +23 -10
- package/src/logger/Logger.js +341 -0
- package/src/logger/channels/ConsoleChannel.js +39 -0
- package/src/logger/channels/FileChannel.js +101 -0
- package/src/logger/channels/index.js +48 -0
- package/src/logger/formatters/JsonFormatter.js +52 -0
- package/src/logger/formatters/PrettyFormatter.js +95 -0
- package/src/logger/formatters/SimpleFormatter.js +37 -0
- package/src/logger/index.js +69 -0
- package/src/logger/levels.js +52 -0
- package/src/middleware/LogMiddleware.js +54 -28
- package/src/providers/LogServiceProvider.js +138 -0
- package/src/scaffold/templates.js +37 -26
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
{% extends "layouts/base.njk" %}
|
|
2
|
+
|
|
3
|
+
{% block title %}{{ resource.singular }} #{{ record.id }}{% endblock %}
|
|
4
|
+
{% block topbar_title %}
|
|
5
|
+
<span class="icon icon-16" style="color:var(--text-muted)">
|
|
6
|
+
<svg viewBox="0 0 24 24"><use href="#ic-eye"/></svg>
|
|
7
|
+
</span>
|
|
8
|
+
{{ resource.singular }} #{{ record.id }}
|
|
9
|
+
{% endblock %}
|
|
10
|
+
|
|
11
|
+
{% block topbar_actions %}
|
|
12
|
+
{% if resource.canEdit %}
|
|
13
|
+
<a href="{{ adminPrefix }}/{{ resource.slug }}/{{ record.id }}/edit" class="btn btn-primary">
|
|
14
|
+
<span class="icon icon-14"><svg viewBox="0 0 24 24"><use href="#ic-edit"/></svg></span>
|
|
15
|
+
Edit
|
|
16
|
+
</a>
|
|
17
|
+
{% endif %}
|
|
18
|
+
<a href="{{ adminPrefix }}/{{ resource.slug }}" class="btn btn-ghost">
|
|
19
|
+
<span class="icon icon-13"><svg viewBox="0 0 24 24"><use href="#ic-arrow-left"/></svg></span>
|
|
20
|
+
Back
|
|
21
|
+
</a>
|
|
22
|
+
{% endblock %}
|
|
23
|
+
|
|
24
|
+
{% block content %}
|
|
25
|
+
<div class="breadcrumb">
|
|
26
|
+
<a href="{{ adminPrefix }}/">
|
|
27
|
+
<span class="icon icon-12"><svg viewBox="0 0 24 24"><use href="#ic-grid"/></svg></span>
|
|
28
|
+
</a>
|
|
29
|
+
<span class="breadcrumb-sep">›</span>
|
|
30
|
+
<a href="{{ adminPrefix }}/{{ resource.slug }}">{{ resource.label }}</a>
|
|
31
|
+
<span class="breadcrumb-sep">›</span>
|
|
32
|
+
<span class="breadcrumb-current">#{{ record.id }}</span>
|
|
33
|
+
</div>
|
|
34
|
+
|
|
35
|
+
<div style="max-width:860px">
|
|
36
|
+
|
|
37
|
+
{# ── Header card with key identity fields ── #}
|
|
38
|
+
<div class="card mb-4">
|
|
39
|
+
<div class="card-header">
|
|
40
|
+
<div class="flex items-center gap-3">
|
|
41
|
+
<div style="width:42px;height:42px;background:var(--primary-soft);border-radius:10px;display:flex;align-items:center;justify-content:center;color:var(--primary)">
|
|
42
|
+
<span class="icon icon-20">
|
|
43
|
+
<svg viewBox="0 0 24 24"><use href="#ic-{{ resource.icon or 'file' }}"/></svg>
|
|
44
|
+
</span>
|
|
45
|
+
</div>
|
|
46
|
+
<div>
|
|
47
|
+
<div class="fw-600" style="font-size:15px">{{ resource.singular }} #{{ record.id }}</div>
|
|
48
|
+
<div class="text-muted text-sm">
|
|
49
|
+
{% if record.created_at %}Created {{ record.created_at }}{% endif %}
|
|
50
|
+
{% if record.updated_at %} · Updated {{ record.updated_at }}{% endif %}
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
<div class="flex items-center gap-2">
|
|
55
|
+
{% if resource.canEdit %}
|
|
56
|
+
<a href="{{ adminPrefix }}/{{ resource.slug }}/{{ record.id }}/edit" class="btn btn-ghost btn-sm">
|
|
57
|
+
<span class="icon icon-13"><svg viewBox="0 0 24 24"><use href="#ic-edit"/></svg></span>
|
|
58
|
+
Edit
|
|
59
|
+
</a>
|
|
60
|
+
{% endif %}
|
|
61
|
+
{% if resource.canDelete %}
|
|
62
|
+
<button class="btn btn-danger btn-sm"
|
|
63
|
+
onclick="confirmDelete('{{ adminPrefix }}/{{ resource.slug }}/{{ record.id }}/delete', '{{ resource.singular }} #{{ record.id }}')">
|
|
64
|
+
<span class="icon icon-13"><svg viewBox="0 0 24 24"><use href="#ic-trash"/></svg></span>
|
|
65
|
+
Delete
|
|
66
|
+
</button>
|
|
67
|
+
{% endif %}
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
|
|
72
|
+
{# ── Tabbed detail fields ── #}
|
|
73
|
+
{% if hasTabs %}
|
|
74
|
+
{# ── Tab nav ── #}
|
|
75
|
+
<div class="tab-nav mb-4" id="tab-nav">
|
|
76
|
+
{% for tab in tabs %}
|
|
77
|
+
<button
|
|
78
|
+
class="tab-btn {% if loop.first %}active{% endif %}"
|
|
79
|
+
data-tab="{{ loop.index0 }}"
|
|
80
|
+
onclick="switchTab({{ loop.index0 }})">
|
|
81
|
+
{{ tab.label or 'General' }}
|
|
82
|
+
</button>
|
|
83
|
+
{% endfor %}
|
|
84
|
+
</div>
|
|
85
|
+
|
|
86
|
+
{% for tab in tabs %}
|
|
87
|
+
<div class="tab-panel {% if loop.first %}active{% endif %}" id="tab-{{ loop.index0 }}">
|
|
88
|
+
<div class="card mb-4">
|
|
89
|
+
{% if tab.label %}
|
|
90
|
+
<div class="card-header">
|
|
91
|
+
<span class="card-title">{{ tab.label }}</span>
|
|
92
|
+
</div>
|
|
93
|
+
{% endif %}
|
|
94
|
+
<div class="card-body">
|
|
95
|
+
<div class="detail-grid">
|
|
96
|
+
{% for field in tab.fields %}
|
|
97
|
+
{% if not field.hidden and not field.listOnly %}
|
|
98
|
+
<div class="detail-item {% if field.span == 'full' %}full{% elif field.type == 'textarea' or field.type == 'json' or field.type == 'richtext' %}full{% endif %}">
|
|
99
|
+
<div class="detail-label">{{ field.label }}</div>
|
|
100
|
+
<div class="detail-value">
|
|
101
|
+
{% set val = record[field.name] %}
|
|
102
|
+
{% include "partials/detail-value.njk" ignore missing %}
|
|
103
|
+
{{ val | adminCell(field) | safe if val is not none else '<span class="cell-muted">—</span>' }}
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
{% endif %}
|
|
107
|
+
{% endfor %}
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
{% endfor %}
|
|
113
|
+
|
|
114
|
+
{% else %}
|
|
115
|
+
|
|
116
|
+
{# ── No tabs — single card ── #}
|
|
117
|
+
<div class="card">
|
|
118
|
+
<div class="card-body">
|
|
119
|
+
<div class="detail-grid">
|
|
120
|
+
{% for field in detailFields %}
|
|
121
|
+
{% if field._isFieldset %}
|
|
122
|
+
<div class="detail-fieldset-heading">{{ field.label }}</div>
|
|
123
|
+
{% elif not field.hidden and not field.listOnly %}
|
|
124
|
+
<div class="detail-item {% if field.span == 'full' %}full{% elif field.type == 'textarea' or field.type == 'json' or field.type == 'richtext' %}full{% endif %}">
|
|
125
|
+
<div class="detail-label">{{ field.label }}</div>
|
|
126
|
+
<div class="detail-value">
|
|
127
|
+
{% set val = record[field.name] %}
|
|
128
|
+
{{ val | adminDetail(field) | safe }}
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
{% endif %}
|
|
132
|
+
{% endfor %}
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
|
|
137
|
+
{% endif %}
|
|
138
|
+
|
|
139
|
+
{# ── Navigation footer ── #}
|
|
140
|
+
<div class="flex items-center gap-3" style="margin-top:16px;padding:0 2px">
|
|
141
|
+
<a href="{{ adminPrefix }}/{{ resource.slug }}" class="btn btn-ghost btn-sm">
|
|
142
|
+
<span class="icon icon-13"><svg viewBox="0 0 24 24"><use href="#ic-arrow-left"/></svg></span>
|
|
143
|
+
Back to {{ resource.label }}
|
|
144
|
+
</a>
|
|
145
|
+
{% if resource.canCreate %}
|
|
146
|
+
<a href="{{ adminPrefix }}/{{ resource.slug }}/create" class="btn btn-ghost btn-sm" style="margin-left:auto">
|
|
147
|
+
<span class="icon icon-13"><svg viewBox="0 0 24 24"><use href="#ic-plus"/></svg></span>
|
|
148
|
+
New {{ resource.singular }}
|
|
149
|
+
</a>
|
|
150
|
+
{% endif %}
|
|
151
|
+
</div>
|
|
152
|
+
|
|
153
|
+
{# ── Custom row actions ── #}
|
|
154
|
+
{% if resource.rowActions | length %}
|
|
155
|
+
<div class="flex items-center gap-2" style="margin-top:12px;padding:0 2px;flex-wrap:wrap">
|
|
156
|
+
{% for ra in resource.rowActions %}
|
|
157
|
+
{% if ra.href %}
|
|
158
|
+
<a href="{{ ra.href }}" target="_blank" rel="noopener" class="btn btn-ghost btn-sm">
|
|
159
|
+
<span class="icon icon-13"><svg viewBox="0 0 24 24"><use href="#ic-{{ ra.icon or 'eye' }}"/></svg></span>
|
|
160
|
+
{{ ra.label }}
|
|
161
|
+
</a>
|
|
162
|
+
{% elif ra.action %}
|
|
163
|
+
<form method="POST" action="{{ adminPrefix }}/{{ resource.slug }}/{{ record.id }}/action/{{ ra.action }}" style="display:inline">
|
|
164
|
+
<button type="submit" class="btn btn-ghost btn-sm">
|
|
165
|
+
<span class="icon icon-13"><svg viewBox="0 0 24 24"><use href="#ic-{{ ra.icon or 'check' }}"/></svg></span>
|
|
166
|
+
{{ ra.label }}
|
|
167
|
+
</button>
|
|
168
|
+
</form>
|
|
169
|
+
{% endif %}
|
|
170
|
+
{% endfor %}
|
|
171
|
+
</div>
|
|
172
|
+
{% endif %}
|
|
173
|
+
|
|
174
|
+
</div>
|
|
175
|
+
|
|
176
|
+
{# ════════════════════════════════════════
|
|
177
|
+
INLINE RELATED RECORDS
|
|
178
|
+
════════════════════════════════════════ #}
|
|
179
|
+
{% if inlines | length %}
|
|
180
|
+
<div style="max-width:860px;margin-top:24px">
|
|
181
|
+
{% for inline in inlines %}
|
|
182
|
+
<div class="card mb-5">
|
|
183
|
+
<div class="card-header">
|
|
184
|
+
<span class="card-title">
|
|
185
|
+
<span class="icon icon-15" style="color:var(--primary)">
|
|
186
|
+
<svg viewBox="0 0 24 24"><use href="#ic-table"/></svg>
|
|
187
|
+
</span>
|
|
188
|
+
{{ inline.label }}
|
|
189
|
+
{% if inline.rows | length %}
|
|
190
|
+
<span class="badge badge-gray" style="font-size:10.5px">{{ inline.rows | length }}</span>
|
|
191
|
+
{% endif %}
|
|
192
|
+
</span>
|
|
193
|
+
{% if inline.canCreate %}
|
|
194
|
+
<a href="{{ adminPrefix }}/{{ inline.label | lower | replace(' ', '-') }}/create?{{ inline.foreignKey }}={{ record.id }}"
|
|
195
|
+
class="btn btn-ghost btn-sm">
|
|
196
|
+
<span class="icon icon-13"><svg viewBox="0 0 24 24"><use href="#ic-plus"/></svg></span>
|
|
197
|
+
Add {{ inline.label | replace('s','') if inline.label | last == 's' else inline.label }}
|
|
198
|
+
</a>
|
|
199
|
+
{% endif %}
|
|
200
|
+
</div>
|
|
201
|
+
|
|
202
|
+
{% if inline.rows | length %}
|
|
203
|
+
<div class="table-wrap">
|
|
204
|
+
<table>
|
|
205
|
+
<thead>
|
|
206
|
+
<tr>
|
|
207
|
+
{% for field in inline.fields %}
|
|
208
|
+
<th>{{ field.label }}</th>
|
|
209
|
+
{% endfor %}
|
|
210
|
+
{% if inline.canDelete %}
|
|
211
|
+
<th class="col-actions">Actions</th>
|
|
212
|
+
{% endif %}
|
|
213
|
+
</tr>
|
|
214
|
+
</thead>
|
|
215
|
+
<tbody>
|
|
216
|
+
{% for row in inline.rows %}
|
|
217
|
+
<tr>
|
|
218
|
+
{% for field in inline.fields %}
|
|
219
|
+
<td {% if loop.first %}class="td-primary"{% endif %}>
|
|
220
|
+
{{ row[field.name] | adminCell(field) | safe if row[field.name] is not none else '<span class="cell-muted">—</span>' }}
|
|
221
|
+
</td>
|
|
222
|
+
{% endfor %}
|
|
223
|
+
{% if inline.canDelete %}
|
|
224
|
+
<td class="col-actions" style="text-align:right">
|
|
225
|
+
<button
|
|
226
|
+
onclick="confirmDelete('{{ adminPrefix }}/{{ inline.label | lower | replace(' ','-') }}/{{ row.id }}/delete','{{ inline.label | replace('s','') if inline.label | last == 's' else inline.label }} #{{ row.id }}')"
|
|
227
|
+
class="btn btn-danger btn-xs">
|
|
228
|
+
<span class="icon icon-12"><svg viewBox="0 0 24 24"><use href="#ic-trash"/></svg></span>
|
|
229
|
+
</button>
|
|
230
|
+
</td>
|
|
231
|
+
{% endif %}
|
|
232
|
+
</tr>
|
|
233
|
+
{% endfor %}
|
|
234
|
+
</tbody>
|
|
235
|
+
</table>
|
|
236
|
+
</div>
|
|
237
|
+
{% else %}
|
|
238
|
+
<div class="empty-state" style="padding:28px 20px">
|
|
239
|
+
<div class="empty-icon" style="width:32px;height:32px;margin:0 auto 10px">
|
|
240
|
+
<span class="icon icon-16"><svg viewBox="0 0 24 24"><use href="#ic-table"/></svg></span>
|
|
241
|
+
</div>
|
|
242
|
+
<div class="empty-title" style="font-size:13px">No {{ inline.label | lower }} yet</div>
|
|
243
|
+
{% if inline.canCreate %}
|
|
244
|
+
<a href="{{ adminPrefix }}/{{ inline.label | lower | replace(' ', '-') }}/create?{{ inline.foreignKey }}={{ record.id }}"
|
|
245
|
+
class="btn btn-ghost btn-sm" style="margin-top:10px">
|
|
246
|
+
<span class="icon icon-13"><svg viewBox="0 0 24 24"><use href="#ic-plus"/></svg></span>
|
|
247
|
+
Add first
|
|
248
|
+
</a>
|
|
249
|
+
{% endif %}
|
|
250
|
+
</div>
|
|
251
|
+
{% endif %}
|
|
252
|
+
</div>
|
|
253
|
+
{% endfor %}
|
|
254
|
+
</div>
|
|
255
|
+
{% endif %}
|
|
256
|
+
|
|
257
|
+
<style>
|
|
258
|
+
.tab-nav {
|
|
259
|
+
display: flex; gap: 0; overflow-x: auto;
|
|
260
|
+
border-bottom: 2px solid var(--border);
|
|
261
|
+
}
|
|
262
|
+
.tab-btn {
|
|
263
|
+
padding: 9px 16px; font-size: 13.5px; font-weight: 500;
|
|
264
|
+
color: var(--text-muted); background: none; border: none;
|
|
265
|
+
border-bottom: 2px solid transparent; margin-bottom: -2px;
|
|
266
|
+
cursor: pointer; font-family: inherit; white-space: nowrap;
|
|
267
|
+
transition: color .12s, border-color .12s;
|
|
268
|
+
}
|
|
269
|
+
.tab-btn:hover { color: var(--text-soft); }
|
|
270
|
+
.tab-btn.active { color: var(--primary); border-bottom-color: var(--primary); }
|
|
271
|
+
.tab-panel { display: none; }
|
|
272
|
+
.tab-panel.active { display: block; }
|
|
273
|
+
|
|
274
|
+
.detail-grid {
|
|
275
|
+
display: grid;
|
|
276
|
+
grid-template-columns: 1fr 1fr;
|
|
277
|
+
gap: 0;
|
|
278
|
+
}
|
|
279
|
+
.detail-item {
|
|
280
|
+
padding: 13px 0;
|
|
281
|
+
border-bottom: 1px solid var(--border-soft);
|
|
282
|
+
display: grid;
|
|
283
|
+
grid-template-columns: 155px 1fr;
|
|
284
|
+
gap: 16px;
|
|
285
|
+
align-items: start;
|
|
286
|
+
}
|
|
287
|
+
.detail-item:last-child { border-bottom: none; }
|
|
288
|
+
.detail-item.full { grid-column: 1 / -1; }
|
|
289
|
+
.detail-label {
|
|
290
|
+
font-size: 11.5px; font-weight: 600; color: var(--text-muted);
|
|
291
|
+
text-transform: uppercase; letter-spacing: .4px; padding-top: 1px;
|
|
292
|
+
}
|
|
293
|
+
.detail-value { font-size: 13.5px; color: var(--text-soft); word-break: break-word; }
|
|
294
|
+
.detail-value pre {
|
|
295
|
+
background: var(--surface2); border: 1px solid var(--border);
|
|
296
|
+
border-radius: var(--radius-sm); padding: 12px;
|
|
297
|
+
font-family: 'DM Mono', monospace; font-size: 12px;
|
|
298
|
+
overflow-x: auto; margin: 0; white-space: pre-wrap;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/* Fieldset heading in detail view */
|
|
302
|
+
.detail-fieldset-heading {
|
|
303
|
+
grid-column: 1 / -1;
|
|
304
|
+
font-size: 11px; font-weight: 700; text-transform: uppercase;
|
|
305
|
+
letter-spacing: .6px; color: var(--text-muted);
|
|
306
|
+
padding: 16px 0 8px;
|
|
307
|
+
border-bottom: 1px solid var(--border-soft);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
@media (max-width: 640px) {
|
|
311
|
+
.detail-grid { grid-template-columns: 1fr; }
|
|
312
|
+
.detail-item { grid-template-columns: 1fr; gap: 4px; }
|
|
313
|
+
}
|
|
314
|
+
</style>
|
|
315
|
+
|
|
316
|
+
<script>
|
|
317
|
+
function switchTab(idx) {
|
|
318
|
+
document.querySelectorAll('.tab-btn').forEach((b, i) => b.classList.toggle('active', i === idx));
|
|
319
|
+
document.querySelectorAll('.tab-panel').forEach((p, i) => p.classList.toggle('active', i === idx));
|
|
320
|
+
}
|
|
321
|
+
</script>
|
|
322
|
+
{% endblock %}
|