millas 0.2.8 → 0.2.10
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/views/pages/list.njk +0 -454
- package/src/container/MillasApp.js +307 -0
- package/src/container/index.js +2 -0
- package/src/index.js +2 -22
- package/src/scaffold/templates.js +19 -114
package/package.json
CHANGED
|
@@ -1,459 +1,5 @@
|
|
|
1
1
|
{% extends "layouts/base.njk" %}
|
|
2
2
|
|
|
3
|
-
{% block title %}{{ resource.label }}{% 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-{{ resource.icon or 'table' }}"/></svg>
|
|
7
|
-
</span>
|
|
8
|
-
{{ resource.label }}
|
|
9
|
-
<span class="badge badge-gray" style="font-size:11px;font-weight:500">{{ total }}</span>
|
|
10
|
-
{% endblock %}
|
|
11
|
-
|
|
12
|
-
{% block topbar_actions %}
|
|
13
|
-
{% if resource.canCreate %}
|
|
14
|
-
<a href="{{ adminPrefix }}/{{ resource.slug }}/create" class="btn btn-primary">
|
|
15
|
-
<span class="icon icon-14"><svg viewBox="0 0 24 24"><use href="#ic-plus"/></svg></span>
|
|
16
|
-
New {{ resource.singular }}
|
|
17
|
-
</a>
|
|
18
|
-
{% endif %}
|
|
19
|
-
{% endblock %}
|
|
20
|
-
|
|
21
|
-
{% block content %}
|
|
22
|
-
<div class="breadcrumb">
|
|
23
|
-
<a href="{{ adminPrefix }}/">
|
|
24
|
-
<span class="icon icon-12"><svg viewBox="0 0 24 24"><use href="#ic-grid"/></svg></span>
|
|
25
|
-
</a>
|
|
26
|
-
<span class="breadcrumb-sep">›</span>
|
|
27
|
-
<span class="breadcrumb-current">{{ resource.label }}</span>
|
|
28
|
-
{% if year %}
|
|
29
|
-
<span class="breadcrumb-sep">›</span>
|
|
30
|
-
<span class="breadcrumb-current">{{ year }}</span>
|
|
31
|
-
{% endif %}
|
|
32
|
-
{% if month %}
|
|
33
|
-
<span class="breadcrumb-sep">›</span>
|
|
34
|
-
<span class="breadcrumb-current">{{ month }}</span>
|
|
35
|
-
{% endif %}
|
|
36
|
-
</div>
|
|
37
|
-
|
|
38
|
-
{# ── Date hierarchy ── #}
|
|
39
|
-
{% if resource.dateHierarchy %}
|
|
40
|
-
<div class="date-hierarchy mb-4">
|
|
41
|
-
<a href="{{ adminPrefix }}/{{ resource.slug }}" class="dh-btn {% if not year %}active{% endif %}">All</a>
|
|
42
|
-
{% for y in dateYears %}
|
|
43
|
-
<a href="{{ adminPrefix }}/{{ resource.slug }}?year={{ y }}" class="dh-btn {% if year == y %}active{% endif %}">{{ y }}</a>
|
|
44
|
-
{% endfor %}
|
|
45
|
-
{% if year %}
|
|
46
|
-
<span class="dh-sep">›</span>
|
|
47
|
-
{% set monthNames = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'] %}
|
|
48
|
-
{% for m in range(1, 13) %}
|
|
49
|
-
<a href="{{ adminPrefix }}/{{ resource.slug }}?year={{ year }}&month={{ m }}" class="dh-btn {% if month == m %}active{% endif %}">{{ monthNames[m-1] }}</a>
|
|
50
|
-
{% endfor %}
|
|
51
|
-
{% endif %}
|
|
52
|
-
</div>
|
|
53
|
-
{% endif %}
|
|
54
|
-
|
|
55
|
-
<div class="card">
|
|
56
|
-
|
|
57
|
-
{# ── Toolbar ── #}
|
|
58
|
-
<div class="toolbar">
|
|
59
|
-
<div class="toolbar-left">
|
|
60
|
-
<form method="GET" action="{{ adminPrefix }}/{{ resource.slug }}" id="search-form" style="display:flex;gap:8px;align-items:center">
|
|
61
|
-
<input type="hidden" name="sort" value="{{ sort }}">
|
|
62
|
-
<input type="hidden" name="order" value="{{ order }}">
|
|
63
|
-
<input type="hidden" name="perPage" value="{{ perPage }}">
|
|
64
|
-
{% if year %}<input type="hidden" name="year" value="{{ year }}">{% endif %}
|
|
65
|
-
{% if month %}<input type="hidden" name="month" value="{{ month }}">{% endif %}
|
|
66
|
-
{% for key, val in activeFilters %}
|
|
67
|
-
<input type="hidden" name="filter[{{ key }}]" value="{{ val }}">
|
|
68
|
-
{% endfor %}
|
|
69
|
-
<div class="search-wrap">
|
|
70
|
-
<span class="search-icon-inner">
|
|
71
|
-
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
72
|
-
<circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/>
|
|
73
|
-
</svg>
|
|
74
|
-
</span>
|
|
75
|
-
<input type="text" name="search" value="{{ search }}"
|
|
76
|
-
placeholder="Search {{ resource.label | lower }}…"
|
|
77
|
-
class="form-control search-input" autocomplete="off">
|
|
78
|
-
</div>
|
|
79
|
-
{% if search %}
|
|
80
|
-
<a href="{{ adminPrefix }}/{{ resource.slug }}" class="btn btn-ghost btn-sm">
|
|
81
|
-
<span class="icon icon-13"><svg viewBox="0 0 24 24"><use href="#ic-x"/></svg></span>
|
|
82
|
-
Clear
|
|
83
|
-
</a>
|
|
84
|
-
{% endif %}
|
|
85
|
-
</form>
|
|
86
|
-
</div>
|
|
87
|
-
|
|
88
|
-
<div class="toolbar-right">
|
|
89
|
-
{# Per-page selector #}
|
|
90
|
-
<select class="filter-control" style="min-width:80px" onchange="changePerPage(this.value)" title="Rows per page">
|
|
91
|
-
{% for n in [20, 50, 100] %}
|
|
92
|
-
<option value="{{ n }}" {% if perPage == n %}selected{% endif %}>{{ n }} / page</option>
|
|
93
|
-
{% endfor %}
|
|
94
|
-
</select>
|
|
95
|
-
|
|
96
|
-
{% if filters | length %}
|
|
97
|
-
<button class="btn btn-ghost btn-sm" onclick="toggleFilters()" id="filter-toggle">
|
|
98
|
-
<span class="icon icon-13"><svg viewBox="0 0 24 24"><use href="#ic-filter"/></svg></span>
|
|
99
|
-
Filters
|
|
100
|
-
{% if activeFilters | length %}
|
|
101
|
-
<span class="badge badge-blue" style="font-size:10px;padding:1px 5px">{{ activeFilters | length }}</span>
|
|
102
|
-
{% endif %}
|
|
103
|
-
</button>
|
|
104
|
-
{% endif %}
|
|
105
|
-
|
|
106
|
-
{# Export dropdown #}
|
|
107
|
-
<div class="action-menu">
|
|
108
|
-
<button class="btn btn-ghost btn-sm action-menu-btn">
|
|
109
|
-
<span class="icon icon-13"><svg viewBox="0 0 24 24"><use href="#ic-download"/></svg></span>
|
|
110
|
-
Export
|
|
111
|
-
<span class="icon icon-12"><svg viewBox="0 0 24 24"><use href="#ic-chevron-down"/></svg></span>
|
|
112
|
-
</button>
|
|
113
|
-
<div class="action-dropdown" style="right:0;min-width:160px">
|
|
114
|
-
<a href="{{ adminPrefix }}/{{ resource.slug }}/export.csv?search={{ search }}&sort={{ sort }}&order={{ order }}{% for key, val in activeFilters %}&filter[{{ key }}]={{ val }}{% endfor %}">
|
|
115
|
-
<span class="icon icon-13"><svg viewBox="0 0 24 24"><use href="#ic-download"/></svg></span>
|
|
116
|
-
Export CSV
|
|
117
|
-
</a>
|
|
118
|
-
<a href="{{ adminPrefix }}/{{ resource.slug }}/export.json?search={{ search }}&sort={{ sort }}&order={{ order }}{% for key, val in activeFilters %}&filter[{{ key }}]={{ val }}{% endfor %}">
|
|
119
|
-
<span class="icon icon-13"><svg viewBox="0 0 24 24"><use href="#ic-download"/></svg></span>
|
|
120
|
-
Export JSON
|
|
121
|
-
</a>
|
|
122
|
-
</div>
|
|
123
|
-
</div>
|
|
124
|
-
</div>
|
|
125
|
-
</div>
|
|
126
|
-
|
|
127
|
-
{# ── Bulk action bar ── #}
|
|
128
|
-
<div class="bulk-bar" id="bulk-bar">
|
|
129
|
-
<span class="bulk-count" id="bulk-count">0 selected</span>
|
|
130
|
-
<span class="text-muted" style="font-size:12px">—</span>
|
|
131
|
-
|
|
132
|
-
{# Built-in delete #}
|
|
133
|
-
{% if resource.canDelete %}
|
|
134
|
-
<button class="btn btn-ghost btn-sm btn-danger" onclick="bulkDelete()">
|
|
135
|
-
<span class="icon icon-13"><svg viewBox="0 0 24 24"><use href="#ic-trash"/></svg></span>
|
|
136
|
-
Delete selected
|
|
137
|
-
</button>
|
|
138
|
-
{% endif %}
|
|
139
|
-
|
|
140
|
-
{# Custom bulk actions #}
|
|
141
|
-
{% for action in resource.actions %}
|
|
142
|
-
<button class="btn btn-ghost btn-sm" onclick="submitBulkAction({{ action.index }})">
|
|
143
|
-
<span class="icon icon-13"><svg viewBox="0 0 24 24"><use href="#ic-{{ action.icon or 'check' }}"/></svg></span>
|
|
144
|
-
{{ action.label }}
|
|
145
|
-
</button>
|
|
146
|
-
{% endfor %}
|
|
147
|
-
|
|
148
|
-
<button class="btn btn-ghost btn-sm" style="margin-left:auto" onclick="clearSelection()">
|
|
149
|
-
<span class="icon icon-13"><svg viewBox="0 0 24 24"><use href="#ic-x"/></svg></span>
|
|
150
|
-
Cancel
|
|
151
|
-
</button>
|
|
152
|
-
</div>
|
|
153
|
-
|
|
154
|
-
{# ── Filter panel ── #}
|
|
155
|
-
{% if filters | length %}
|
|
156
|
-
<div class="filter-row" id="filter-panel" style="display:none">
|
|
157
|
-
<form method="GET" action="{{ adminPrefix }}/{{ resource.slug }}" id="filter-form" style="display:flex;gap:10px;flex-wrap:wrap;align-items:flex-end;width:100%">
|
|
158
|
-
<input type="hidden" name="search" value="{{ search }}">
|
|
159
|
-
<input type="hidden" name="sort" value="{{ sort }}">
|
|
160
|
-
<input type="hidden" name="order" value="{{ order }}">
|
|
161
|
-
<input type="hidden" name="perPage" value="{{ perPage }}">
|
|
162
|
-
|
|
163
|
-
{% for filter in filters %}
|
|
164
|
-
<div class="filter-group">
|
|
165
|
-
<label class="filter-label">{{ filter.label }}</label>
|
|
166
|
-
{% if filter.type == 'select' %}
|
|
167
|
-
<select name="filter[{{ filter.name }}]" class="filter-control" style="min-width:120px" onchange="this.form.submit()">
|
|
168
|
-
<option value="">All</option>
|
|
169
|
-
{% for opt in filter.options %}
|
|
170
|
-
<option value="{{ opt }}" {% if activeFilters[filter.name] == opt %}selected{% endif %}>{{ opt }}</option>
|
|
171
|
-
{% endfor %}
|
|
172
|
-
</select>
|
|
173
|
-
{% elif filter.type == 'boolean' %}
|
|
174
|
-
<select name="filter[{{ filter.name }}]" class="filter-control" style="min-width:100px" onchange="this.form.submit()">
|
|
175
|
-
<option value="">All</option>
|
|
176
|
-
<option value="1" {% if activeFilters[filter.name] == '1' %}selected{% endif %}>Yes</option>
|
|
177
|
-
<option value="0" {% if activeFilters[filter.name] == '0' %}selected{% endif %}>No</option>
|
|
178
|
-
</select>
|
|
179
|
-
{% elif filter.type == 'text' %}
|
|
180
|
-
<input type="text" name="filter[{{ filter.name }}]" class="filter-control" style="min-width:130px"
|
|
181
|
-
value="{{ activeFilters[filter.name] or '' }}" placeholder="Filter…">
|
|
182
|
-
{% elif filter.type == 'number' %}
|
|
183
|
-
<input type="number" name="filter[{{ filter.name }}]" class="filter-control" style="min-width:100px"
|
|
184
|
-
value="{{ activeFilters[filter.name] or '' }}" placeholder="0">
|
|
185
|
-
{% elif filter.type == 'dateRange' %}
|
|
186
|
-
<div style="display:flex;gap:6px;align-items:center">
|
|
187
|
-
<input type="date" name="filter[{{ filter.name }}__gte]" class="filter-control"
|
|
188
|
-
value="{{ activeFilters[filter.name + '__gte'] or '' }}" placeholder="From">
|
|
189
|
-
<span style="color:var(--text-muted);font-size:12px">→</span>
|
|
190
|
-
<input type="date" name="filter[{{ filter.name }}__lte]" class="filter-control"
|
|
191
|
-
value="{{ activeFilters[filter.name + '__lte'] or '' }}" placeholder="To">
|
|
192
|
-
</div>
|
|
193
|
-
{% endif %}
|
|
194
|
-
</div>
|
|
195
|
-
{% endfor %}
|
|
196
|
-
|
|
197
|
-
<div class="filter-group" style="flex-direction:row;align-items:flex-end;gap:6px;margin-left:auto">
|
|
198
|
-
<button type="submit" class="btn btn-ghost btn-sm">Apply</button>
|
|
199
|
-
{% if activeFilters | length %}
|
|
200
|
-
<a href="{{ adminPrefix }}/{{ resource.slug }}{% if search %}?search={{ search }}{% endif %}" class="btn btn-ghost btn-sm">
|
|
201
|
-
<span class="icon icon-12"><svg viewBox="0 0 24 24"><use href="#ic-x"/></svg></span>
|
|
202
|
-
Reset
|
|
203
|
-
</a>
|
|
204
|
-
{% endif %}
|
|
205
|
-
</div>
|
|
206
|
-
</form>
|
|
207
|
-
</div>
|
|
208
|
-
{% endif %}
|
|
209
|
-
|
|
210
|
-
{# ── Table ── #}
|
|
211
|
-
<div class="table-wrap">
|
|
212
|
-
{% if rows | length %}
|
|
213
|
-
<table id="data-table">
|
|
214
|
-
<thead>
|
|
215
|
-
<tr>
|
|
216
|
-
<th class="col-check">
|
|
217
|
-
<input type="checkbox" class="row-check" id="check-all" onchange="toggleAll(this)">
|
|
218
|
-
</th>
|
|
219
|
-
{% for field in listFields %}
|
|
220
|
-
{% set isSorted = sort == field.name %}
|
|
221
|
-
<th class="{% if field.name in sortable %}sortable{% endif %} {% if isSorted %}sort-{{ order }} sort-active{% endif %}">
|
|
222
|
-
{% if field.name in sortable %}
|
|
223
|
-
<a href="{{ adminPrefix }}/{{ resource.slug }}?sort={{ field.name }}&order={{ 'desc' if (isSorted and order == 'asc') else 'asc' }}&search={{ search }}&page={{ page }}&perPage={{ perPage }}" style="color:inherit;text-decoration:none;display:flex;align-items:center;gap:4px">
|
|
224
|
-
{{ field.label }}
|
|
225
|
-
<span class="sort-indicator"><span class="up"></span><span class="down"></span></span>
|
|
226
|
-
</a>
|
|
227
|
-
{% else %}
|
|
228
|
-
{{ field.label }}
|
|
229
|
-
{% endif %}
|
|
230
|
-
</th>
|
|
231
|
-
{% endfor %}
|
|
232
|
-
{% if resource.canEdit or resource.canDelete or resource.rowActions | length %}
|
|
233
|
-
<th class="col-actions">Actions</th>
|
|
234
|
-
{% endif %}
|
|
235
|
-
</tr>
|
|
236
|
-
</thead>
|
|
237
|
-
<tbody>
|
|
238
|
-
{% for row in rows %}
|
|
239
|
-
<tr data-id="{{ row.id }}">
|
|
240
|
-
<td class="col-check">
|
|
241
|
-
<input type="checkbox" class="row-check item-check" value="{{ row.id }}" onchange="updateBulkBar()">
|
|
242
|
-
</td>
|
|
243
|
-
{% for field in listFields %}
|
|
244
|
-
<td class="{% if loop.first %}td-primary{% endif %}">
|
|
245
|
-
{# listDisplayLinks: make column value a link to detail page #}
|
|
246
|
-
{% if loop.first or field.name in resource.listDisplayLinks or field.isLink %}
|
|
247
|
-
{% if resource.canView %}
|
|
248
|
-
<a href="{{ adminPrefix }}/{{ resource.slug }}/{{ row.id }}" style="color:inherit;text-decoration:none">
|
|
249
|
-
{{ row[field.name] | adminCell(field) | safe }}
|
|
250
|
-
</a>
|
|
251
|
-
{% else %}
|
|
252
|
-
{{ row[field.name] | adminCell(field) | safe }}
|
|
253
|
-
{% endif %}
|
|
254
|
-
{% else %}
|
|
255
|
-
{{ row[field.name] | adminCell(field) | safe }}
|
|
256
|
-
{% endif %}
|
|
257
|
-
</td>
|
|
258
|
-
{% endfor %}
|
|
259
|
-
{% if resource.canEdit or resource.canDelete or resource.rowActions | length %}
|
|
260
|
-
<td class="col-actions">
|
|
261
|
-
<div class="action-menu">
|
|
262
|
-
<button class="action-menu-btn">
|
|
263
|
-
<span class="icon icon-14">
|
|
264
|
-
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
265
|
-
<circle cx="12" cy="5" r="1.2" fill="currentColor" stroke="none"/>
|
|
266
|
-
<circle cx="12" cy="12" r="1.2" fill="currentColor" stroke="none"/>
|
|
267
|
-
<circle cx="12" cy="19" r="1.2" fill="currentColor" stroke="none"/>
|
|
268
|
-
</svg>
|
|
269
|
-
</span>
|
|
270
|
-
</button>
|
|
271
|
-
<div class="action-dropdown">
|
|
272
|
-
{% if resource.canView %}
|
|
273
|
-
<a href="{{ adminPrefix }}/{{ resource.slug }}/{{ row.id }}">
|
|
274
|
-
<span class="icon icon-13"><svg viewBox="0 0 24 24"><use href="#ic-eye"/></svg></span>
|
|
275
|
-
View
|
|
276
|
-
</a>
|
|
277
|
-
{% endif %}
|
|
278
|
-
{% if resource.canEdit %}
|
|
279
|
-
<a href="{{ adminPrefix }}/{{ resource.slug }}/{{ row.id }}/edit">
|
|
280
|
-
<span class="icon icon-13"><svg viewBox="0 0 24 24"><use href="#ic-edit"/></svg></span>
|
|
281
|
-
Edit
|
|
282
|
-
</a>
|
|
283
|
-
{% endif %}
|
|
284
|
-
{# Custom row actions #}
|
|
285
|
-
{% for ra in resource.rowActions %}
|
|
286
|
-
{% if ra.href %}
|
|
287
|
-
<a href="{{ ra.href }}" target="_blank" rel="noopener">
|
|
288
|
-
<span class="icon icon-13"><svg viewBox="0 0 24 24"><use href="#ic-{{ ra.icon or 'arrow-right' }}"/></svg></span>
|
|
289
|
-
{{ ra.label }}
|
|
290
|
-
</a>
|
|
291
|
-
{% elif ra.action %}
|
|
292
|
-
<form method="POST" action="{{ adminPrefix }}/{{ resource.slug }}/{{ row.id }}/action/{{ ra.action }}" style="display:contents">
|
|
293
|
-
<button type="submit">
|
|
294
|
-
<span class="icon icon-13"><svg viewBox="0 0 24 24"><use href="#ic-{{ ra.icon or 'check' }}"/></svg></span>
|
|
295
|
-
{{ ra.label }}
|
|
296
|
-
</button>
|
|
297
|
-
</form>
|
|
298
|
-
{% endif %}
|
|
299
|
-
{% endfor %}
|
|
300
|
-
{% if resource.canDelete %}
|
|
301
|
-
<div class="sep"></div>
|
|
302
|
-
<button class="danger" onclick="confirmDelete('{{ adminPrefix }}/{{ resource.slug }}/{{ row.id }}/delete', '#{{ row.id }}')">
|
|
303
|
-
<span class="icon icon-13"><svg viewBox="0 0 24 24"><use href="#ic-trash"/></svg></span>
|
|
304
|
-
Delete
|
|
305
|
-
</button>
|
|
306
|
-
{% endif %}
|
|
307
|
-
</div>
|
|
308
|
-
</div>
|
|
309
|
-
</td>
|
|
310
|
-
{% endif %}
|
|
311
|
-
</tr>
|
|
312
|
-
{% endfor %}
|
|
313
|
-
</tbody>
|
|
314
|
-
</table>
|
|
315
|
-
|
|
316
|
-
{# ── Pagination ── #}
|
|
317
|
-
{% if lastPage > 1 %}
|
|
318
|
-
<div class="pagination">
|
|
319
|
-
<a class="page-btn {% if page <= 1 %}disabled{% endif %}"
|
|
320
|
-
href="{{ adminPrefix }}/{{ resource.slug }}?page={{ page - 1 }}&sort={{ sort }}&order={{ order }}&search={{ search }}&perPage={{ perPage }}"
|
|
321
|
-
{% if page <= 1 %}tabindex="-1" aria-disabled="true"{% endif %}>
|
|
322
|
-
<span class="icon icon-13"><svg viewBox="0 0 24 24"><use href="#ic-chevron-left"/></svg></span>
|
|
323
|
-
</a>
|
|
324
|
-
{% for p in range(1, lastPage + 1) %}
|
|
325
|
-
{% if p == 1 or p == lastPage or (p >= page - 2 and p <= page + 2) %}
|
|
326
|
-
<a class="page-btn {% if p == page %}active{% endif %}"
|
|
327
|
-
href="{{ adminPrefix }}/{{ resource.slug }}?page={{ p }}&sort={{ sort }}&order={{ order }}&search={{ search }}&perPage={{ perPage }}">{{ p }}</a>
|
|
328
|
-
{% elif p == 2 and page > 4 %}
|
|
329
|
-
<span class="page-ellipsis">…</span>
|
|
330
|
-
{% elif p == lastPage - 1 and page < lastPage - 3 %}
|
|
331
|
-
<span class="page-ellipsis">…</span>
|
|
332
|
-
{% endif %}
|
|
333
|
-
{% endfor %}
|
|
334
|
-
<a class="page-btn {% if page >= lastPage %}disabled{% endif %}"
|
|
335
|
-
href="{{ adminPrefix }}/{{ resource.slug }}?page={{ page + 1 }}&sort={{ sort }}&order={{ order }}&search={{ search }}&perPage={{ perPage }}"
|
|
336
|
-
{% if page >= lastPage %}tabindex="-1" aria-disabled="true"{% endif %}>
|
|
337
|
-
<span class="icon icon-13"><svg viewBox="0 0 24 24"><use href="#ic-chevron-right"/></svg></span>
|
|
338
|
-
</a>
|
|
339
|
-
<span class="page-info">
|
|
340
|
-
{{ (page - 1) * perPage + 1 }}–{{ [page * perPage, total] | min }} of {{ total }}
|
|
341
|
-
</span>
|
|
342
|
-
</div>
|
|
343
|
-
{% endif %}
|
|
344
|
-
|
|
345
|
-
{% else %}
|
|
346
|
-
<div class="empty-state">
|
|
347
|
-
<div class="empty-icon">
|
|
348
|
-
<span class="icon icon-22"><svg viewBox="0 0 24 24"><use href="#ic-table"/></svg></span>
|
|
349
|
-
</div>
|
|
350
|
-
<div class="empty-title">No {{ resource.label }} found</div>
|
|
351
|
-
<div class="empty-desc">
|
|
352
|
-
{% if search %}No results for "<strong>{{ search }}</strong>".
|
|
353
|
-
{% elif activeFilters | length %}No records match the active filters.
|
|
354
|
-
{% else %}Get started by creating your first record.{% endif %}
|
|
355
|
-
</div>
|
|
356
|
-
{% if resource.canCreate %}
|
|
357
|
-
<a href="{{ adminPrefix }}/{{ resource.slug }}/create" class="btn btn-primary">
|
|
358
|
-
<span class="icon icon-14"><svg viewBox="0 0 24 24"><use href="#ic-plus"/></svg></span>
|
|
359
|
-
New {{ resource.singular }}
|
|
360
|
-
</a>
|
|
361
|
-
{% endif %}
|
|
362
|
-
</div>
|
|
363
|
-
{% endif %}
|
|
364
|
-
</div>
|
|
365
|
-
</div>
|
|
366
|
-
|
|
367
|
-
{# Hidden bulk action form #}
|
|
368
|
-
<form method="POST" action="{{ adminPrefix }}/{{ resource.slug }}/bulk-action" id="bulk-action-form" style="display:none">
|
|
369
|
-
<input type="hidden" name="actionIndex" id="bulk-action-index" value="">
|
|
370
|
-
<div id="bulk-action-ids"></div>
|
|
371
|
-
</form>
|
|
372
|
-
|
|
373
|
-
<style>
|
|
374
|
-
/* ── Date hierarchy ── */
|
|
375
|
-
.date-hierarchy {
|
|
376
|
-
display: flex; align-items: center; gap: 4px; flex-wrap: wrap;
|
|
377
|
-
padding: 10px 0;
|
|
378
|
-
}
|
|
379
|
-
.dh-btn {
|
|
380
|
-
padding: 4px 10px; border-radius: 99px; font-size: 12.5px;
|
|
381
|
-
border: 1px solid var(--border); background: var(--surface);
|
|
382
|
-
color: var(--text-muted); text-decoration: none; font-weight: 500;
|
|
383
|
-
transition: all .12s;
|
|
384
|
-
}
|
|
385
|
-
.dh-btn:hover { border-color: var(--primary); color: var(--primary); }
|
|
386
|
-
.dh-btn.active { background: var(--primary); border-color: var(--primary); color: #fff; }
|
|
387
|
-
.dh-sep { color: var(--text-xmuted); font-size: 13px; padding: 0 2px; }
|
|
388
|
-
</style>
|
|
389
|
-
|
|
390
|
-
<script>
|
|
391
|
-
// ── Filter panel ──────────────────────────────────────────────
|
|
392
|
-
const filterPanel = document.getElementById('filter-panel');
|
|
393
|
-
{% if activeFilters | length %}
|
|
394
|
-
if (filterPanel) filterPanel.style.display = 'flex';
|
|
395
|
-
{% endif %}
|
|
396
|
-
function toggleFilters() {
|
|
397
|
-
if (!filterPanel) return;
|
|
398
|
-
filterPanel.style.display = filterPanel.style.display === 'none' ? 'flex' : 'none';
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
// ── Per-page selector ─────────────────────────────────────────
|
|
402
|
-
function changePerPage(val) {
|
|
403
|
-
const url = new URL(window.location.href);
|
|
404
|
-
url.searchParams.set('perPage', val);
|
|
405
|
-
url.searchParams.set('page', '1');
|
|
406
|
-
window.location.href = url.toString();
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
// ── Bulk selection ────────────────────────────────────────────
|
|
410
|
-
const bulkBar = document.getElementById('bulk-bar');
|
|
411
|
-
const bulkCount = document.getElementById('bulk-count');
|
|
412
|
-
|
|
413
|
-
function updateBulkBar() {
|
|
414
|
-
const checked = document.querySelectorAll('.item-check:checked');
|
|
415
|
-
const n = checked.length;
|
|
416
|
-
bulkBar.classList.toggle('visible', n > 0);
|
|
417
|
-
if (bulkCount) bulkCount.textContent = `${n} selected`;
|
|
418
|
-
const all = document.getElementById('check-all');
|
|
419
|
-
if (all) {
|
|
420
|
-
const total = document.querySelectorAll('.item-check').length;
|
|
421
|
-
all.indeterminate = n > 0 && n < total;
|
|
422
|
-
all.checked = n > 0 && n === total;
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
function toggleAll(master) {
|
|
427
|
-
document.querySelectorAll('.item-check').forEach(c => { c.checked = master.checked; });
|
|
428
|
-
updateBulkBar();
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
function clearSelection() {
|
|
432
|
-
document.querySelectorAll('.item-check, #check-all').forEach(c => { c.checked = false; c.indeterminate = false; });
|
|
433
|
-
updateBulkBar();
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
function bulkDelete() {
|
|
437
|
-
const ids = [...document.querySelectorAll('.item-check:checked')].map(c => c.value);
|
|
438
|
-
if (!ids.length) return;
|
|
439
|
-
confirmDelete(
|
|
440
|
-
'{{ adminPrefix }}/{{ resource.slug }}/bulk-delete',
|
|
441
|
-
`${ids.length} record${ids.length > 1 ? 's' : ''}`
|
|
442
|
-
);
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
function submitBulkAction(actionIndex) {
|
|
446
|
-
const ids = [...document.querySelectorAll('.item-check:checked')].map(c => c.value);
|
|
447
|
-
if (!ids.length) return;
|
|
448
|
-
const form = document.getElementById('bulk-action-form');
|
|
449
|
-
const container = document.getElementById('bulk-action-ids');
|
|
450
|
-
document.getElementById('bulk-action-index').value = actionIndex;
|
|
451
|
-
container.innerHTML = ids.map(id => `<input type="hidden" name="ids[]" value="${id}">`).join('');
|
|
452
|
-
form.submit();
|
|
453
|
-
}
|
|
454
|
-
</script>
|
|
455
|
-
{% endblock %}
|
|
456
|
-
|
|
457
3
|
{% block title %}{{ resource.label }}{% endblock %}
|
|
458
4
|
{% block topbar_title %}
|
|
459
5
|
<span class="icon icon-16" style="color:var(--text-muted)">
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const express = require('express');
|
|
4
|
+
const Application = require('./Application');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* MillasApp
|
|
8
|
+
*
|
|
9
|
+
* A clean factory wrapper around Application that wires up Express,
|
|
10
|
+
* core providers, routes, and the admin panel automatically.
|
|
11
|
+
*
|
|
12
|
+
* Reduces bootstrap/app.js from ~60 lines to ~8.
|
|
13
|
+
*
|
|
14
|
+
* ── Usage ───────────────────────────────────────────────────────────────────
|
|
15
|
+
*
|
|
16
|
+
* require('dotenv').config();
|
|
17
|
+
* const { MillasApp } = require('millas');
|
|
18
|
+
* const AppServiceProvider = require('../providers/AppServiceProvider');
|
|
19
|
+
*
|
|
20
|
+
* const app = MillasApp.create()
|
|
21
|
+
* .providers([AppServiceProvider])
|
|
22
|
+
* .routes(Route => {
|
|
23
|
+
* require('../routes/web')(Route);
|
|
24
|
+
* require('../routes/api')(Route);
|
|
25
|
+
* })
|
|
26
|
+
* .withAdmin();
|
|
27
|
+
*
|
|
28
|
+
* module.exports = app.start();
|
|
29
|
+
*
|
|
30
|
+
* ── Escape hatches ──────────────────────────────────────────────────────────
|
|
31
|
+
*
|
|
32
|
+
* // Access the raw Express app
|
|
33
|
+
* app.express.use(someExpressMiddleware());
|
|
34
|
+
*
|
|
35
|
+
* // Access the DI container
|
|
36
|
+
* app.container.make('db');
|
|
37
|
+
*
|
|
38
|
+
* // Access the underlying Application kernel
|
|
39
|
+
* app.kernel
|
|
40
|
+
*
|
|
41
|
+
* ── Advanced ────────────────────────────────────────────────────────────────
|
|
42
|
+
*
|
|
43
|
+
* MillasApp.create({
|
|
44
|
+
* // Disable a core provider
|
|
45
|
+
* mail: false,
|
|
46
|
+
* queue: false,
|
|
47
|
+
*
|
|
48
|
+
* // Custom admin prefix
|
|
49
|
+
* admin: { prefix: '/cms', title: 'My CMS' },
|
|
50
|
+
*
|
|
51
|
+
* // Add Express middleware before routes
|
|
52
|
+
* expressMiddleware: [cors(), helmet()],
|
|
53
|
+
*
|
|
54
|
+
* // Called once the app is listening
|
|
55
|
+
* onStart: (port, host) => console.log(`Running on ${host}:${port}`),
|
|
56
|
+
* })
|
|
57
|
+
*/
|
|
58
|
+
class MillasApp {
|
|
59
|
+
|
|
60
|
+
// ─── Factory ──────────────────────────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Create a new MillasApp instance.
|
|
64
|
+
* @param {object} [options]
|
|
65
|
+
* @param {boolean} [options.database=true] — register DatabaseServiceProvider
|
|
66
|
+
* @param {boolean} [options.cache=true] — register CacheServiceProvider
|
|
67
|
+
* @param {boolean} [options.storage=true] — register StorageServiceProvider
|
|
68
|
+
* @param {boolean} [options.mail=true] — register MailServiceProvider
|
|
69
|
+
* @param {boolean} [options.queue=true] — register QueueServiceProvider
|
|
70
|
+
* @param {boolean} [options.events=true] — register EventServiceProvider
|
|
71
|
+
* @param {boolean} [options.logging=true] — register LogServiceProvider
|
|
72
|
+
* @param {Array} [options.expressMiddleware] — Express middleware applied before routes
|
|
73
|
+
* @param {Function}[options.onStart] — callback(port, host) after listen
|
|
74
|
+
*/
|
|
75
|
+
static create(options = {}) {
|
|
76
|
+
return new MillasApp(options);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
constructor(options = {}) {
|
|
80
|
+
this._options = options;
|
|
81
|
+
this._userProviders = [];
|
|
82
|
+
this._routesCb = null;
|
|
83
|
+
this._adminOptions = null; // null = no admin; object/true = mount admin
|
|
84
|
+
this._expressApp = null;
|
|
85
|
+
this._kernel = null;
|
|
86
|
+
this._started = false;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ─── Fluent API ───────────────────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Register application service providers.
|
|
93
|
+
*
|
|
94
|
+
* .providers([AppServiceProvider, PaymentServiceProvider])
|
|
95
|
+
*/
|
|
96
|
+
providers(list = []) {
|
|
97
|
+
this._userProviders.push(...list);
|
|
98
|
+
return this;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Define application routes.
|
|
103
|
+
*
|
|
104
|
+
* .routes(Route => {
|
|
105
|
+
* require('../routes/web')(Route);
|
|
106
|
+
* require('../routes/api')(Route);
|
|
107
|
+
* })
|
|
108
|
+
*/
|
|
109
|
+
routes(callback) {
|
|
110
|
+
this._routesCb = callback;
|
|
111
|
+
return this;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Register a named middleware alias.
|
|
116
|
+
* Must be called before start().
|
|
117
|
+
*
|
|
118
|
+
* .middleware('verified', EmailVerifiedMiddleware)
|
|
119
|
+
*/
|
|
120
|
+
middleware(alias, handler) {
|
|
121
|
+
if (!this._pendingMiddleware) this._pendingMiddleware = [];
|
|
122
|
+
this._pendingMiddleware.push({ alias, handler });
|
|
123
|
+
return this;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Mount the admin panel.
|
|
128
|
+
*
|
|
129
|
+
* .withAdmin() // default /admin
|
|
130
|
+
* .withAdmin({ prefix: '/cms' }) // custom prefix
|
|
131
|
+
* .withAdmin({ prefix: '/admin', auth: { users: [...] } })
|
|
132
|
+
*/
|
|
133
|
+
withAdmin(options = {}) {
|
|
134
|
+
this._adminOptions = options;
|
|
135
|
+
return this;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ─── Start ────────────────────────────────────────────────────────────────
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Boot all providers, mount routes, and start listening.
|
|
142
|
+
* Returns an object with { app, expressApp, route } for module.exports.
|
|
143
|
+
*
|
|
144
|
+
* Safe to await if you need to run code after boot:
|
|
145
|
+
* const { app } = await MillasApp.create()...start();
|
|
146
|
+
*/
|
|
147
|
+
start() {
|
|
148
|
+
// Build Express + Application kernel
|
|
149
|
+
this._expressApp = express();
|
|
150
|
+
this._expressApp.use(express.json());
|
|
151
|
+
this._expressApp.use(express.urlencoded({ extended: true }));
|
|
152
|
+
|
|
153
|
+
// Apply any extra Express middleware (e.g. helmet, cors)
|
|
154
|
+
for (const mw of (this._options.expressMiddleware || [])) {
|
|
155
|
+
this._expressApp.use(mw);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
this._kernel = new Application(this._expressApp);
|
|
159
|
+
|
|
160
|
+
// Register pending middleware aliases
|
|
161
|
+
for (const { alias, handler } of (this._pendingMiddleware || [])) {
|
|
162
|
+
this._kernel.middleware(alias, handler);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Register core providers (auto, unless disabled)
|
|
166
|
+
const coreProviders = this._buildCoreProviders();
|
|
167
|
+
this._kernel.providers([...coreProviders, ...this._userProviders]);
|
|
168
|
+
|
|
169
|
+
// Register routes
|
|
170
|
+
if (this._routesCb) {
|
|
171
|
+
this._kernel.routes(this._routesCb);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Boot asynchronously
|
|
175
|
+
const self = this;
|
|
176
|
+
const boot = (async () => {
|
|
177
|
+
await self._kernel.boot();
|
|
178
|
+
|
|
179
|
+
if (!process.env.MILLAS_ROUTE_LIST) {
|
|
180
|
+
self._kernel.mountRoutes();
|
|
181
|
+
|
|
182
|
+
// Admin panel (mounted between routes and fallbacks)
|
|
183
|
+
if (self._adminOptions !== null) {
|
|
184
|
+
const adminOpts = typeof self._adminOptions === 'object'
|
|
185
|
+
? self._adminOptions
|
|
186
|
+
: {};
|
|
187
|
+
|
|
188
|
+
try {
|
|
189
|
+
const Admin = require('../admin/Admin');
|
|
190
|
+
if (Object.keys(adminOpts).length) {
|
|
191
|
+
Admin.configure(adminOpts);
|
|
192
|
+
}
|
|
193
|
+
Admin.mount(self._expressApp);
|
|
194
|
+
} catch (err) {
|
|
195
|
+
process.stderr.write(`[millas] Admin mount failed: ${err.message}\n`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
self._kernel.mountFallbacks();
|
|
200
|
+
|
|
201
|
+
const onStart = self._options.onStart || null;
|
|
202
|
+
self._kernel.listen(undefined, undefined, onStart || undefined);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
self._started = true;
|
|
206
|
+
})();
|
|
207
|
+
|
|
208
|
+
// Expose the promise for testing / programmatic use
|
|
209
|
+
this._bootPromise = boot;
|
|
210
|
+
|
|
211
|
+
return this;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Await the boot promise. Useful in tests or scripts.
|
|
216
|
+
*
|
|
217
|
+
* const app = await MillasApp.create().routes(...).start().ready();
|
|
218
|
+
*/
|
|
219
|
+
async ready() {
|
|
220
|
+
await (this._bootPromise || Promise.resolve());
|
|
221
|
+
return this;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// ─── Escape hatches ───────────────────────────────────────────────────────
|
|
225
|
+
|
|
226
|
+
/** The raw Express application instance. */
|
|
227
|
+
get express() { return this._expressApp; }
|
|
228
|
+
|
|
229
|
+
/** The underlying Application kernel. */
|
|
230
|
+
get kernel() { return this._kernel; }
|
|
231
|
+
|
|
232
|
+
/** The DI container. */
|
|
233
|
+
get container() { return this._kernel?._container; }
|
|
234
|
+
|
|
235
|
+
/** The Route instance (for programmatic route registration). */
|
|
236
|
+
get route() { return this._kernel?._route; }
|
|
237
|
+
|
|
238
|
+
// ─── module.exports helper ────────────────────────────────────────────────
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Returns a plain object suitable for module.exports.
|
|
242
|
+
* Matches the shape existing code expects.
|
|
243
|
+
*/
|
|
244
|
+
toModule() {
|
|
245
|
+
const self = this;
|
|
246
|
+
return {
|
|
247
|
+
app: this._kernel,
|
|
248
|
+
expressApp: this._expressApp,
|
|
249
|
+
get route() { return self._kernel?._route; },
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// ─── Internal ─────────────────────────────────────────────────────────────
|
|
254
|
+
|
|
255
|
+
_buildCoreProviders() {
|
|
256
|
+
const opts = this._options;
|
|
257
|
+
const providers = [];
|
|
258
|
+
|
|
259
|
+
// Helper — require a provider, silently skip if not found
|
|
260
|
+
const load = (path) => {
|
|
261
|
+
try { return require(path); } catch { return null; }
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
// Logging — first so all other providers can use Log
|
|
265
|
+
if (opts.logging !== false) {
|
|
266
|
+
const p = load('../providers/LogServiceProvider');
|
|
267
|
+
if (p) providers.push(p);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Database
|
|
271
|
+
if (opts.database !== false) {
|
|
272
|
+
const p = load('../providers/DatabaseServiceProvider');
|
|
273
|
+
if (p) providers.push(p);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Cache + Storage
|
|
277
|
+
if (opts.cache !== false || opts.storage !== false) {
|
|
278
|
+
const p = load('../providers/CacheStorageServiceProvider');
|
|
279
|
+
if (p) {
|
|
280
|
+
if (opts.cache !== false && p.CacheServiceProvider) providers.push(p.CacheServiceProvider);
|
|
281
|
+
if (opts.storage !== false && p.StorageServiceProvider) providers.push(p.StorageServiceProvider);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Mail
|
|
286
|
+
if (opts.mail !== false) {
|
|
287
|
+
const p = load('../providers/MailServiceProvider');
|
|
288
|
+
if (p) providers.push(p);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Queue
|
|
292
|
+
if (opts.queue !== false) {
|
|
293
|
+
const p = load('../providers/QueueServiceProvider');
|
|
294
|
+
if (p) providers.push(p);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Events
|
|
298
|
+
if (opts.events !== false) {
|
|
299
|
+
const p = load('../providers/EventServiceProvider');
|
|
300
|
+
if (p) providers.push(p);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return providers;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
module.exports = MillasApp;
|
package/src/container/index.js
CHANGED
|
@@ -2,12 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
const Container = require('./Container');
|
|
4
4
|
const Application = require('./Application');
|
|
5
|
+
const MillasApp = require('./MillasApp');
|
|
5
6
|
const ServiceProvider = require('../providers/ServiceProvider');
|
|
6
7
|
const ProviderRegistry = require('../providers/ProviderRegistry');
|
|
7
8
|
|
|
8
9
|
module.exports = {
|
|
9
10
|
Container,
|
|
10
11
|
Application,
|
|
12
|
+
MillasApp,
|
|
11
13
|
ServiceProvider,
|
|
12
14
|
ProviderRegistry,
|
|
13
15
|
};
|
package/src/index.js
CHANGED
|
@@ -1,21 +1,5 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
// ── Logger ────────────────────────────────────────────────────────
|
|
4
|
-
const {
|
|
5
|
-
Log,
|
|
6
|
-
Logger,
|
|
7
|
-
LEVELS,
|
|
8
|
-
LEVEL_NAMES,
|
|
9
|
-
PrettyFormatter,
|
|
10
|
-
JsonFormatter,
|
|
11
|
-
SimpleFormatter,
|
|
12
|
-
ConsoleChannel,
|
|
13
|
-
FileChannel,
|
|
14
|
-
NullChannel,
|
|
15
|
-
StackChannel,
|
|
16
|
-
} = require('./logger/index');
|
|
17
|
-
const LogServiceProvider = require('./providers/LogServiceProvider');
|
|
18
|
-
|
|
19
3
|
// ── HTTP Layer ────────────────────────────────────────────────────
|
|
20
4
|
const Controller = require('./controller/Controller');
|
|
21
5
|
const Middleware = require('./middleware/Middleware');
|
|
@@ -28,6 +12,7 @@ const HttpError = require('./errors/HttpError');
|
|
|
28
12
|
// ── DI Container ─────────────────────────────────────────────────
|
|
29
13
|
const Container = require('./container/Container');
|
|
30
14
|
const Application = require('./container/Application');
|
|
15
|
+
const MillasApp = require('./container/MillasApp');
|
|
31
16
|
const ServiceProvider = require('./providers/ServiceProvider');
|
|
32
17
|
const ProviderRegistry = require('./providers/ProviderRegistry');
|
|
33
18
|
|
|
@@ -76,16 +61,11 @@ const Storage = require('./storage/Storage');
|
|
|
76
61
|
const LocalDriver = require('./storage/drivers/LocalDriver');
|
|
77
62
|
|
|
78
63
|
module.exports = {
|
|
79
|
-
// Logger
|
|
80
|
-
Log, Logger, LEVELS, LEVEL_NAMES,
|
|
81
|
-
PrettyFormatter, JsonFormatter, SimpleFormatter,
|
|
82
|
-
ConsoleChannel, FileChannel, NullChannel, StackChannel,
|
|
83
|
-
LogServiceProvider,
|
|
84
64
|
// HTTP
|
|
85
65
|
Controller, Middleware, MiddlewarePipeline,
|
|
86
66
|
CorsMiddleware, ThrottleMiddleware, LogMiddleware, HttpError,
|
|
87
67
|
// DI
|
|
88
|
-
Container, Application, ServiceProvider, ProviderRegistry,
|
|
68
|
+
Container, Application, MillasApp, ServiceProvider, ProviderRegistry,
|
|
89
69
|
// ORM
|
|
90
70
|
Model, fields, QueryBuilder, DatabaseManager, SchemaBuilder,
|
|
91
71
|
MigrationRunner, ModelInspector, DatabaseServiceProvider,
|
|
@@ -88,77 +88,18 @@ module.exports = {
|
|
|
88
88
|
|
|
89
89
|
require('dotenv').config();
|
|
90
90
|
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
function resolveMillas() {
|
|
94
|
-
try { return require('millas/src'); } catch {}
|
|
95
|
-
try {
|
|
96
|
-
const path = require('path');
|
|
97
|
-
const cliPath = require.resolve('millas/bin/millas.js');
|
|
98
|
-
const millasSrc = path.join(path.dirname(cliPath), '..', 'src', 'index.js');
|
|
99
|
-
return require(millasSrc);
|
|
100
|
-
} catch {}
|
|
101
|
-
throw new Error('Cannot find millas. Run: npm install millas');
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
const {
|
|
105
|
-
Application,
|
|
106
|
-
Admin,
|
|
107
|
-
LogServiceProvider,
|
|
108
|
-
CacheServiceProvider,
|
|
109
|
-
StorageServiceProvider,
|
|
110
|
-
MailServiceProvider,
|
|
111
|
-
QueueServiceProvider,
|
|
112
|
-
EventServiceProvider,
|
|
113
|
-
} = resolveMillas();
|
|
114
|
-
|
|
91
|
+
const { MillasApp } = require('millas');
|
|
115
92
|
const AppServiceProvider = require('../providers/AppServiceProvider');
|
|
116
93
|
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
// ── Register service providers ──────────────────────────────────
|
|
125
|
-
app.providers([
|
|
126
|
-
LogServiceProvider, // first — so Log works in all other providers
|
|
127
|
-
CacheServiceProvider,
|
|
128
|
-
StorageServiceProvider,
|
|
129
|
-
MailServiceProvider,
|
|
130
|
-
QueueServiceProvider,
|
|
131
|
-
EventServiceProvider,
|
|
132
|
-
AppServiceProvider,
|
|
133
|
-
]);
|
|
134
|
-
|
|
135
|
-
// ── Define routes ────────────────────────────────────────────────
|
|
136
|
-
app.routes(Route => {
|
|
137
|
-
require('../routes/web')(Route);
|
|
138
|
-
require('../routes/api')(Route);
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
// ── Boot + mount + listen ────────────────────────────────────────
|
|
142
|
-
(async () => {
|
|
143
|
-
await app.boot();
|
|
144
|
-
|
|
145
|
-
if (!process.env.MILLAS_ROUTE_LIST) {
|
|
146
|
-
// Register app routes first (without 404 handler yet)
|
|
147
|
-
app.mountRoutes();
|
|
148
|
-
|
|
149
|
-
// Admin panel mounts here — before the 404 fallback
|
|
150
|
-
// To disable: comment out this line
|
|
151
|
-
// To change path: Admin.configure({ prefix: '/cms' });
|
|
152
|
-
Admin.mount(expressApp);
|
|
153
|
-
|
|
154
|
-
// Add 404 + error handlers LAST
|
|
155
|
-
app.mountFallbacks();
|
|
156
|
-
|
|
157
|
-
app.listen();
|
|
158
|
-
}
|
|
159
|
-
})();
|
|
94
|
+
const app = MillasApp.create()
|
|
95
|
+
.providers([AppServiceProvider])
|
|
96
|
+
.routes(Route => {
|
|
97
|
+
require('../routes/web')(Route);
|
|
98
|
+
require('../routes/api')(Route);
|
|
99
|
+
})
|
|
100
|
+
.withAdmin();
|
|
160
101
|
|
|
161
|
-
module.exports =
|
|
102
|
+
module.exports = app.start();
|
|
162
103
|
`,
|
|
163
104
|
|
|
164
105
|
// ─── routes/web.js ────────────────────────────────────────────
|
|
@@ -198,38 +139,6 @@ module.exports = function (Route) {
|
|
|
198
139
|
|
|
199
140
|
});
|
|
200
141
|
};
|
|
201
|
-
`,
|
|
202
|
-
|
|
203
|
-
// ─── config/logging.js ────────────────────────────────────────
|
|
204
|
-
'config/logging.js': `'use strict';
|
|
205
|
-
|
|
206
|
-
/**
|
|
207
|
-
* Logging Configuration
|
|
208
|
-
*
|
|
209
|
-
* Levels (lowest → highest): verbose | debug | info | warn | error | wtf
|
|
210
|
-
* Channels: 'console' | 'file' | 'null'
|
|
211
|
-
* Formats: 'pretty' (coloured) | 'simple' (plain text) | 'json' (structured)
|
|
212
|
-
*/
|
|
213
|
-
module.exports = {
|
|
214
|
-
level: process.env.LOG_LEVEL || (process.env.NODE_ENV === 'production' ? 'info' : 'debug'),
|
|
215
|
-
|
|
216
|
-
defaultTag: process.env.APP_NAME || 'App',
|
|
217
|
-
|
|
218
|
-
channels: [
|
|
219
|
-
{
|
|
220
|
-
driver: 'console',
|
|
221
|
-
format: 'pretty',
|
|
222
|
-
},
|
|
223
|
-
{
|
|
224
|
-
driver: 'file',
|
|
225
|
-
format: 'simple',
|
|
226
|
-
path: 'storage/logs',
|
|
227
|
-
prefix: '${projectName}',
|
|
228
|
-
level: 'warn',
|
|
229
|
-
maxFiles: 30,
|
|
230
|
-
},
|
|
231
|
-
],
|
|
232
|
-
};
|
|
233
142
|
`,
|
|
234
143
|
|
|
235
144
|
// ─── config/app.js ────────────────────────────────────────────
|
|
@@ -332,25 +241,15 @@ module.exports = {
|
|
|
332
241
|
// ─── providers/AppServiceProvider.js ──────────────────────────
|
|
333
242
|
'providers/AppServiceProvider.js': `'use strict';
|
|
334
243
|
|
|
335
|
-
|
|
336
|
-
try { return require('millas/src'); } catch {}
|
|
337
|
-
try {
|
|
338
|
-
const path = require('path');
|
|
339
|
-
const cli = require.resolve('millas/bin/millas.js');
|
|
340
|
-
return require(path.join(path.dirname(cli), '..', 'src', 'index.js'));
|
|
341
|
-
} catch {}
|
|
342
|
-
throw new Error('Cannot find millas. Run: npm install millas');
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
const { ServiceProvider } = resolveMillas();
|
|
244
|
+
const { ServiceProvider } = require('millas');
|
|
346
245
|
|
|
347
246
|
/**
|
|
348
247
|
* AppServiceProvider
|
|
349
248
|
*
|
|
350
249
|
* Register and bootstrap your application services here.
|
|
351
250
|
*
|
|
352
|
-
* register() — bind things into the container
|
|
353
|
-
* boot() — called after all providers have registered
|
|
251
|
+
* register() — bind things into the container (synchronous)
|
|
252
|
+
* boot() — called after all providers have registered (async OK)
|
|
354
253
|
*/
|
|
355
254
|
class AppServiceProvider extends ServiceProvider {
|
|
356
255
|
register(container) {
|
|
@@ -360,7 +259,13 @@ class AppServiceProvider extends ServiceProvider {
|
|
|
360
259
|
}
|
|
361
260
|
|
|
362
261
|
async boot(container, app) {
|
|
363
|
-
// const
|
|
262
|
+
// const { Log } = require('millas');
|
|
263
|
+
// Log.tag('AppServiceProvider').i('Booted');
|
|
264
|
+
|
|
265
|
+
// Register resources in the Admin panel:
|
|
266
|
+
// const { Admin, AdminResource } = require('millas');
|
|
267
|
+
// const Post = require('../app/models/Post');
|
|
268
|
+
// Admin.register(Post);
|
|
364
269
|
}
|
|
365
270
|
}
|
|
366
271
|
|