webspresso 0.0.90 → 0.0.91
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/README.md +358 -8
- package/bin/commands/service.js +193 -0
- package/bin/webspresso.js +2 -0
- package/core/auth/middleware.js +9 -0
- package/core/compression/compressible.js +51 -0
- package/core/compression/compression-stream.js +59 -0
- package/core/compression/index.js +25 -0
- package/core/compression/middleware.js +301 -0
- package/core/compression/negotiate.js +153 -0
- package/core/errors/base.js +31 -0
- package/core/errors/domain.js +193 -0
- package/core/errors/http.js +133 -0
- package/core/errors/index.js +77 -0
- package/core/errors/middleware.js +162 -0
- package/core/errors/normalize.js +171 -0
- package/core/orm/complexity.js +120 -0
- package/core/orm/events.js +64 -14
- package/core/orm/index.js +45 -10
- package/core/orm/model.js +2 -0
- package/core/orm/query-builder.js +30 -4
- package/core/orm/repository.js +57 -25
- package/core/orm/transaction.js +39 -5
- package/core/orm/utils.js +6 -3
- package/core/queue/adapters/database.js +284 -0
- package/core/queue/adapters/memory.js +155 -0
- package/core/queue/adapters/redis.js +336 -0
- package/core/queue/index.js +21 -0
- package/core/queue/job.js +103 -0
- package/core/queue/manager.js +387 -0
- package/core/realtime/adapters/redis.js +431 -0
- package/core/realtime/adapters/socket-io.js +203 -0
- package/core/realtime/adapters/sse.js +210 -0
- package/core/realtime/adapters/websocket.js +337 -0
- package/core/realtime/auth-manager.js +140 -0
- package/core/realtime/core.js +359 -0
- package/core/realtime/emitter.js +98 -0
- package/core/realtime/errors.js +71 -0
- package/core/realtime/index.js +79 -0
- package/core/realtime/reconnect-manager.js +118 -0
- package/core/realtime/registry.js +243 -0
- package/core/shutdown/http-adapter.js +134 -0
- package/core/shutdown/index.js +12 -0
- package/core/shutdown/shutdown-manager.js +312 -0
- package/core/ssr/index.js +13 -0
- package/core/ssr/stream.js +174 -0
- package/index.d.ts +829 -52
- package/index.js +103 -0
- package/package.json +22 -2
- package/plugins/admin-panel/api.js +37 -4
- package/plugins/admin-panel/app.js +57 -17
- package/plugins/admin-panel/client/parts/01-state-api-breadcrumb.js +4 -1
- package/plugins/admin-panel/client/parts/04-field-renderers.js +102 -58
- package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +37 -23
- package/plugins/admin-panel/client/parts/08-record-list.js +17 -24
- package/plugins/admin-panel/client/parts/09-record-form.js +168 -19
- package/plugins/admin-panel/core/admin-module.js +2 -2
- package/plugins/admin-panel/core/api-extensions.js +149 -0
- package/plugins/admin-panel/field-renderers/rich-text.js +3 -2
- package/plugins/admin-panel/index.js +90 -17
- package/plugins/admin-panel/modules/custom-pages.js +144 -45
- package/plugins/admin-panel/modules/menu.js +85 -0
- package/plugins/admin-panel/modules/user-management.js +5 -2
- package/plugins/admin-panel/vendor/mithril.min.js +1 -0
- package/plugins/admin-panel/vendor/quill.min.js +7 -0
- package/plugins/admin-panel/vendor/quill.snow.min.css +945 -0
- package/plugins/admin-panel/vendor/tailwind.min.js +65 -0
- package/plugins/basic-auth/index.js +293 -0
- package/plugins/data-exchange/export-xlsx.js +5 -1
- package/plugins/data-exchange/import.js +16 -2
- package/plugins/data-exchange/index.js +14 -0
- package/plugins/email/index.js +14 -1
- package/plugins/index.js +29 -0
- package/plugins/queue/index.js +108 -0
- package/plugins/realtime/index.js +150 -0
- package/plugins/recaptcha/index.js +11 -0
- package/plugins/redirect/index.js +9 -1
- package/plugins/rest-resources/index.js +280 -11
- package/plugins/upload/index.js +16 -0
- package/src/app-context.js +46 -6
- package/src/file-router.js +196 -21
- package/src/helpers.js +20 -2
- package/src/plugin-manager.js +124 -4
- package/src/server.js +304 -44
- package/src/services/builtins/auth.js +273 -0
- package/src/services/builtins/exchange.js +229 -0
- package/src/services/builtins/mail.js +134 -0
- package/src/services/builtins/media.js +196 -0
- package/src/services/builtins/system.js +221 -0
- package/src/services/discovery.js +140 -0
- package/src/services/executor.js +292 -0
- package/src/services/index.js +70 -0
- package/src/services/memoize.js +256 -0
- package/src/services/registry.js +288 -0
- package/src/services/validator.js +281 -0
|
@@ -23,7 +23,7 @@ const BelongsToField = {
|
|
|
23
23
|
});
|
|
24
24
|
},
|
|
25
25
|
view: (vnode) => {
|
|
26
|
-
const { col, value, onChange, readonly } = vnode.attrs;
|
|
26
|
+
const { col, value, onChange, readonly, error } = vnode.attrs;
|
|
27
27
|
const customField = col.customField || {};
|
|
28
28
|
const label = col.ui?.label || formatColumnLabel(col.name);
|
|
29
29
|
const hint = col.ui?.hint || '';
|
|
@@ -38,18 +38,22 @@ const BelongsToField = {
|
|
|
38
38
|
m('.text-xs.text-gray-500.dark:text-slate-400', 'Loading options...')
|
|
39
39
|
]);
|
|
40
40
|
}
|
|
41
|
+
|
|
42
|
+
const borderClass = error
|
|
43
|
+
? 'border-red-500 dark:border-red-500 focus:ring-red-500'
|
|
44
|
+
: 'border-gray-300 dark:border-slate-600 focus:ring-blue-500 dark:focus:ring-blue-400';
|
|
41
45
|
|
|
42
46
|
return m('.mb-4', [
|
|
43
47
|
m('label.block.text-sm.font-medium.text-gray-700.dark:text-slate-300.mb-1', { for: col.name }, [
|
|
44
48
|
label,
|
|
45
49
|
!col.nullable && !readonly ? m('span.text-red-500', ' *') : null
|
|
46
50
|
]),
|
|
47
|
-
m('select.w-full.px-3.py-2.border.
|
|
51
|
+
m('select.w-full.px-3.py-2.border.rounded-md.bg-white.dark:bg-slate-900/70.text-gray-900.dark:text-slate-100.focus:outline-none.focus:ring-2', {
|
|
48
52
|
id: col.name,
|
|
49
53
|
name: col.name,
|
|
50
54
|
value: value !== null && value !== undefined ? String(value) : '',
|
|
51
55
|
disabled: readonly,
|
|
52
|
-
class: readonly ? 'bg-gray-100 dark:bg-slate-800 cursor-not-allowed' : '',
|
|
56
|
+
class: (readonly ? 'bg-gray-100 dark:bg-slate-800 cursor-not-allowed ' : '') + borderClass,
|
|
53
57
|
onchange: (e) => {
|
|
54
58
|
const selectedValue = e.target.value === '' ? null : e.target.value;
|
|
55
59
|
const typedValue = (col.type === 'integer' || col.type === 'bigint' || col.type === 'float' || col.type === 'decimal') && selectedValue !== null
|
|
@@ -68,7 +72,7 @@ const BelongsToField = {
|
|
|
68
72
|
}, itemDisplay);
|
|
69
73
|
}),
|
|
70
74
|
]),
|
|
71
|
-
hint ? m('p.text-xs.text-gray-500.dark:text-slate-400.mt-1', hint) : null,
|
|
75
|
+
error ? m('p.text-xs.text-red-600.dark:text-red-400.mt-1.font-medium', error) : (hint ? m('p.text-xs.text-gray-500.dark:text-slate-400.mt-1', hint) : null),
|
|
72
76
|
]);
|
|
73
77
|
}
|
|
74
78
|
};
|
|
@@ -96,7 +100,7 @@ const HasManyField = {
|
|
|
96
100
|
});
|
|
97
101
|
},
|
|
98
102
|
view: (vnode) => {
|
|
99
|
-
const { col, value = [], onChange, readonly } = vnode.attrs;
|
|
103
|
+
const { col, value = [], onChange, readonly, error } = vnode.attrs;
|
|
100
104
|
const customField = col.customField || {};
|
|
101
105
|
const label = col.ui?.label || formatColumnLabel(col.name);
|
|
102
106
|
const hint = col.ui?.hint || '';
|
|
@@ -112,13 +116,17 @@ const HasManyField = {
|
|
|
112
116
|
m('.text-xs.text-gray-500.dark:text-slate-400', 'Loading options...')
|
|
113
117
|
]);
|
|
114
118
|
}
|
|
119
|
+
|
|
120
|
+
const borderClass = error
|
|
121
|
+
? 'border-red-500 dark:border-red-500'
|
|
122
|
+
: 'border-gray-300 dark:border-slate-600';
|
|
115
123
|
|
|
116
124
|
return m('.mb-4', [
|
|
117
125
|
m('label.block.text-sm.font-medium.text-gray-700.dark:text-slate-300.mb-1', [
|
|
118
126
|
label,
|
|
119
127
|
!col.nullable && !readonly ? m('span.text-red-500', ' *') : null
|
|
120
128
|
]),
|
|
121
|
-
m('.border.
|
|
129
|
+
m('.border.rounded-md.p-3.max-h-48.overflow-y-auto.bg-white.dark:bg-slate-900/70', { class: borderClass }, [
|
|
122
130
|
vnode.state.options.map(item => {
|
|
123
131
|
const itemValue = String(item[valueKey]);
|
|
124
132
|
const itemDisplay = item[displayKey] || itemValue;
|
|
@@ -150,24 +158,28 @@ const HasManyField = {
|
|
|
150
158
|
]);
|
|
151
159
|
}),
|
|
152
160
|
]),
|
|
153
|
-
hint ? m('p.text-xs.text-gray-500.dark:text-slate-400.mt-1', hint) : null,
|
|
161
|
+
error ? m('p.text-xs.text-red-600.dark:text-red-400.mt-1.font-medium', error) : (hint ? m('p.text-xs.text-gray-500.dark:text-slate-400.mt-1', hint) : null),
|
|
154
162
|
]);
|
|
155
163
|
}
|
|
156
164
|
};
|
|
157
165
|
|
|
158
166
|
const FieldRenderers = {
|
|
159
167
|
// Text input (string)
|
|
160
|
-
string: (col, value, onChange, readonly) => {
|
|
168
|
+
string: (col, value, onChange, readonly, error) => {
|
|
161
169
|
const validations = col.validations || {};
|
|
162
170
|
const ui = col.ui || {};
|
|
163
171
|
const label = ui.label || formatColumnLabel(col.name);
|
|
164
172
|
const inputType = ui.inputType || (validations.email ? 'email' : validations.url ? 'url' : 'text');
|
|
165
173
|
const placeholder = ui.placeholder || '';
|
|
166
174
|
const hint = ui.hint || '';
|
|
175
|
+
const borderClass = error ? 'border-red-500 dark:border-red-500 focus:ring-red-500' : 'border-gray-300 dark:border-slate-600 focus:ring-blue-500 dark:focus:ring-blue-400';
|
|
167
176
|
|
|
168
177
|
return m('.mb-4', [
|
|
169
|
-
m('label.block.text-sm.font-medium.text-gray-700
|
|
170
|
-
|
|
178
|
+
m('label.block.text-sm.font-medium.text-gray-700.dark:text-slate-300.mb-1', { for: col.name }, [
|
|
179
|
+
label,
|
|
180
|
+
!col.nullable && !readonly ? m('span.text-red-500', ' *') : null,
|
|
181
|
+
]),
|
|
182
|
+
m('input.w-full.px-3.py-2.border.rounded-md.bg-white.dark:bg-slate-900/70.text-gray-900.dark:text-slate-100.placeholder-gray-400.dark:placeholder-slate-500.focus:outline-none.focus:ring-2', {
|
|
171
183
|
id: col.name,
|
|
172
184
|
name: col.name,
|
|
173
185
|
type: inputType,
|
|
@@ -179,25 +191,29 @@ const FieldRenderers = {
|
|
|
179
191
|
required: !col.nullable && !readonly,
|
|
180
192
|
readonly: readonly,
|
|
181
193
|
disabled: readonly,
|
|
182
|
-
class: readonly ? 'bg-gray-100 dark:bg-slate-800 cursor-not-allowed' : '',
|
|
194
|
+
class: (readonly ? 'bg-gray-100 dark:bg-slate-800 cursor-not-allowed ' : '') + borderClass,
|
|
183
195
|
oninput: (e) => onChange(e.target.value),
|
|
184
196
|
}),
|
|
185
|
-
hint ? m('p.text-xs.text-gray-500
|
|
197
|
+
error ? m('p.text-xs.text-red-600.dark:text-red-400.mt-1.font-medium', error) : (hint ? m('p.text-xs.text-gray-500.dark:text-slate-400.mt-1', hint) : null),
|
|
186
198
|
]);
|
|
187
199
|
},
|
|
188
200
|
|
|
189
201
|
// Textarea (text)
|
|
190
|
-
text: (col, value, onChange, readonly) => {
|
|
202
|
+
text: (col, value, onChange, readonly, error) => {
|
|
191
203
|
const validations = col.validations || {};
|
|
192
204
|
const ui = col.ui || {};
|
|
193
205
|
const label = ui.label || formatColumnLabel(col.name);
|
|
194
206
|
const placeholder = ui.placeholder || '';
|
|
195
207
|
const hint = ui.hint || '';
|
|
196
208
|
const rows = ui.rows || 4;
|
|
209
|
+
const borderClass = error ? 'border-red-500 dark:border-red-500 focus:ring-red-500' : 'border-gray-300 dark:border-slate-600 focus:ring-blue-500 dark:focus:ring-blue-400';
|
|
197
210
|
|
|
198
211
|
return m('.mb-4', [
|
|
199
|
-
m('label.block.text-sm.font-medium.text-gray-700
|
|
200
|
-
|
|
212
|
+
m('label.block.text-sm.font-medium.text-gray-700.dark:text-slate-300.mb-1', { for: col.name }, [
|
|
213
|
+
label,
|
|
214
|
+
!col.nullable && !readonly ? m('span.text-red-500', ' *') : null,
|
|
215
|
+
]),
|
|
216
|
+
m('textarea.w-full.px-3.py-2.border.rounded-md.bg-white.dark:bg-slate-900/70.text-gray-900.dark:text-slate-100.placeholder-gray-400.dark:placeholder-slate-500.focus:outline-none.focus:ring-2', {
|
|
201
217
|
id: col.name,
|
|
202
218
|
name: col.name,
|
|
203
219
|
rows: rows,
|
|
@@ -207,24 +223,28 @@ const FieldRenderers = {
|
|
|
207
223
|
required: !col.nullable && !readonly,
|
|
208
224
|
readonly: readonly,
|
|
209
225
|
disabled: readonly,
|
|
210
|
-
class: readonly ? 'bg-gray-100 dark:bg-slate-800 cursor-not-allowed' : '',
|
|
226
|
+
class: (readonly ? 'bg-gray-100 dark:bg-slate-800 cursor-not-allowed ' : '') + borderClass,
|
|
211
227
|
oninput: (e) => onChange(e.target.value),
|
|
212
228
|
}, value || ''),
|
|
213
|
-
hint ? m('p.text-xs.text-gray-500
|
|
229
|
+
error ? m('p.text-xs.text-red-600.dark:text-red-400.mt-1.font-medium', error) : (hint ? m('p.text-xs.text-gray-500.dark:text-slate-400.mt-1', hint) : null),
|
|
214
230
|
]);
|
|
215
231
|
},
|
|
216
232
|
|
|
217
233
|
// Number input (integer, bigint)
|
|
218
|
-
integer: (col, value, onChange, readonly) => {
|
|
234
|
+
integer: (col, value, onChange, readonly, error) => {
|
|
219
235
|
const validations = col.validations || {};
|
|
220
236
|
const ui = col.ui || {};
|
|
221
237
|
const label = ui.label || formatColumnLabel(col.name);
|
|
222
238
|
const placeholder = ui.placeholder || '';
|
|
223
239
|
const hint = ui.hint || '';
|
|
240
|
+
const borderClass = error ? 'border-red-500 dark:border-red-500 focus:ring-red-500' : 'border-gray-300 dark:border-slate-600 focus:ring-blue-500 dark:focus:ring-blue-400';
|
|
224
241
|
|
|
225
242
|
return m('.mb-4', [
|
|
226
|
-
m('label.block.text-sm.font-medium.text-gray-700
|
|
227
|
-
|
|
243
|
+
m('label.block.text-sm.font-medium.text-gray-700.dark:text-slate-300.mb-1', { for: col.name }, [
|
|
244
|
+
label,
|
|
245
|
+
!col.nullable && !readonly ? m('span.text-red-500', ' *') : null,
|
|
246
|
+
]),
|
|
247
|
+
m('input.w-full.px-3.py-2.border.rounded-md.bg-white.dark:bg-slate-900/70.text-gray-900.dark:text-slate-100.placeholder-gray-400.dark:placeholder-slate-500.focus:outline-none.focus:ring-2', {
|
|
228
248
|
id: col.name,
|
|
229
249
|
name: col.name,
|
|
230
250
|
type: 'number',
|
|
@@ -236,24 +256,28 @@ const FieldRenderers = {
|
|
|
236
256
|
required: !col.nullable && !readonly,
|
|
237
257
|
readonly: readonly,
|
|
238
258
|
disabled: readonly,
|
|
239
|
-
class: readonly ? 'bg-gray-100 dark:bg-slate-800 cursor-not-allowed' : '',
|
|
259
|
+
class: (readonly ? 'bg-gray-100 dark:bg-slate-800 cursor-not-allowed ' : '') + borderClass,
|
|
240
260
|
oninput: (e) => onChange(e.target.value === '' ? null : parseInt(e.target.value, 10)),
|
|
241
261
|
}),
|
|
242
|
-
hint ? m('p.text-xs.text-gray-500
|
|
262
|
+
error ? m('p.text-xs.text-red-600.dark:text-red-400.mt-1.font-medium', error) : (hint ? m('p.text-xs.text-gray-500.dark:text-slate-400.mt-1', hint) : null),
|
|
243
263
|
]);
|
|
244
264
|
},
|
|
245
265
|
|
|
246
266
|
// Float/Decimal input
|
|
247
|
-
float: (col, value, onChange, readonly) => {
|
|
267
|
+
float: (col, value, onChange, readonly, error) => {
|
|
248
268
|
const validations = col.validations || {};
|
|
249
269
|
const ui = col.ui || {};
|
|
250
270
|
const label = ui.label || formatColumnLabel(col.name);
|
|
251
271
|
const placeholder = ui.placeholder || '';
|
|
252
272
|
const hint = ui.hint || '';
|
|
273
|
+
const borderClass = error ? 'border-red-500 dark:border-red-500 focus:ring-red-500' : 'border-gray-300 dark:border-slate-600 focus:ring-blue-500 dark:focus:ring-blue-400';
|
|
253
274
|
|
|
254
275
|
return m('.mb-4', [
|
|
255
|
-
m('label.block.text-sm.font-medium.text-gray-700
|
|
256
|
-
|
|
276
|
+
m('label.block.text-sm.font-medium.text-gray-700.dark:text-slate-300.mb-1', { for: col.name }, [
|
|
277
|
+
label,
|
|
278
|
+
!col.nullable && !readonly ? m('span.text-red-500', ' *') : null,
|
|
279
|
+
]),
|
|
280
|
+
m('input.w-full.px-3.py-2.border.rounded-md.bg-white.dark:bg-slate-900/70.text-gray-900.dark:text-slate-100.placeholder-gray-400.dark:placeholder-slate-500.focus:outline-none.focus:ring-2', {
|
|
257
281
|
id: col.name,
|
|
258
282
|
name: col.name,
|
|
259
283
|
type: 'number',
|
|
@@ -265,15 +289,15 @@ const FieldRenderers = {
|
|
|
265
289
|
required: !col.nullable && !readonly,
|
|
266
290
|
readonly: readonly,
|
|
267
291
|
disabled: readonly,
|
|
268
|
-
class: readonly ? 'bg-gray-100 dark:bg-slate-800 cursor-not-allowed' : '',
|
|
292
|
+
class: (readonly ? 'bg-gray-100 dark:bg-slate-800 cursor-not-allowed ' : '') + borderClass,
|
|
269
293
|
oninput: (e) => onChange(e.target.value === '' ? null : parseFloat(e.target.value)),
|
|
270
294
|
}),
|
|
271
|
-
hint ? m('p.text-xs.text-gray-500
|
|
295
|
+
error ? m('p.text-xs.text-red-600.dark:text-red-400.mt-1.font-medium', error) : (hint ? m('p.text-xs.text-gray-500.dark:text-slate-400.mt-1', hint) : null),
|
|
272
296
|
]);
|
|
273
297
|
},
|
|
274
298
|
|
|
275
299
|
// Boolean checkbox
|
|
276
|
-
boolean: (col, value, onChange, readonly) => {
|
|
300
|
+
boolean: (col, value, onChange, readonly, error) => {
|
|
277
301
|
const ui = col.ui || {};
|
|
278
302
|
const label = ui.label || formatColumnLabel(col.name);
|
|
279
303
|
const hint = ui.hint || '';
|
|
@@ -289,17 +313,18 @@ const FieldRenderers = {
|
|
|
289
313
|
}),
|
|
290
314
|
m('span.text-sm.font-medium.text-gray-700.dark:text-slate-300', label),
|
|
291
315
|
]),
|
|
292
|
-
hint ? m('p.text-xs.text-gray-500
|
|
316
|
+
error ? m('p.text-xs.text-red-600.dark:text-red-400.mt-1.font-medium', error) : (hint ? m('p.text-xs.text-gray-500.dark:text-slate-400.mt-1', hint) : null),
|
|
293
317
|
]);
|
|
294
318
|
},
|
|
295
319
|
|
|
296
320
|
// Date input
|
|
297
|
-
date: (col, value, onChange, readonly) => {
|
|
321
|
+
date: (col, value, onChange, readonly, error) => {
|
|
298
322
|
const validations = col.validations || {};
|
|
299
323
|
const ui = col.ui || {};
|
|
300
324
|
const label = ui.label || formatColumnLabel(col.name);
|
|
301
325
|
const placeholder = ui.placeholder || '';
|
|
302
326
|
const hint = ui.hint || '';
|
|
327
|
+
const borderClass = error ? 'border-red-500 dark:border-red-500 focus:ring-red-500' : 'border-gray-300 dark:border-slate-600 focus:ring-blue-500 dark:focus:ring-blue-400';
|
|
303
328
|
let dateValue = '';
|
|
304
329
|
if (value) {
|
|
305
330
|
try {
|
|
@@ -311,8 +336,11 @@ const FieldRenderers = {
|
|
|
311
336
|
}
|
|
312
337
|
|
|
313
338
|
return m('.mb-4', [
|
|
314
|
-
m('label.block.text-sm.font-medium.text-gray-700
|
|
315
|
-
|
|
339
|
+
m('label.block.text-sm.font-medium.text-gray-700.dark:text-slate-300.mb-1', { for: col.name }, [
|
|
340
|
+
label,
|
|
341
|
+
!col.nullable && !readonly ? m('span.text-red-500', ' *') : null,
|
|
342
|
+
]),
|
|
343
|
+
m('input.w-full.px-3.py-2.border.rounded-md.bg-white.dark:bg-slate-900/70.text-gray-900.dark:text-slate-100.placeholder-gray-400.dark:placeholder-slate-500.focus:outline-none.focus:ring-2', {
|
|
316
344
|
id: col.name,
|
|
317
345
|
name: col.name,
|
|
318
346
|
type: 'date',
|
|
@@ -323,20 +351,21 @@ const FieldRenderers = {
|
|
|
323
351
|
required: !col.nullable && !readonly,
|
|
324
352
|
readonly: readonly,
|
|
325
353
|
disabled: readonly,
|
|
326
|
-
class: readonly ? 'bg-gray-100 dark:bg-slate-800 cursor-not-allowed' : '',
|
|
354
|
+
class: (readonly ? 'bg-gray-100 dark:bg-slate-800 cursor-not-allowed ' : '') + borderClass,
|
|
327
355
|
oninput: (e) => onChange(e.target.value),
|
|
328
356
|
}),
|
|
329
|
-
hint ? m('p.text-xs.text-gray-500
|
|
357
|
+
error ? m('p.text-xs.text-red-600.dark:text-red-400.mt-1.font-medium', error) : (hint ? m('p.text-xs.text-gray-500.dark:text-slate-400.mt-1', hint) : null),
|
|
330
358
|
]);
|
|
331
359
|
},
|
|
332
360
|
|
|
333
361
|
// DateTime input (datetime, timestamp)
|
|
334
|
-
datetime: (col, value, onChange, readonly) => {
|
|
362
|
+
datetime: (col, value, onChange, readonly, error) => {
|
|
335
363
|
const validations = col.validations || {};
|
|
336
364
|
const ui = col.ui || {};
|
|
337
365
|
const label = ui.label || formatColumnLabel(col.name);
|
|
338
366
|
const placeholder = ui.placeholder || '';
|
|
339
367
|
const hint = ui.hint || '';
|
|
368
|
+
const borderClass = error ? 'border-red-500 dark:border-red-500 focus:ring-red-500' : 'border-gray-300 dark:border-slate-600 focus:ring-blue-500 dark:focus:ring-blue-400';
|
|
340
369
|
let dateTimeValue = '';
|
|
341
370
|
if (value) {
|
|
342
371
|
try {
|
|
@@ -348,8 +377,11 @@ const FieldRenderers = {
|
|
|
348
377
|
}
|
|
349
378
|
|
|
350
379
|
return m('.mb-4', [
|
|
351
|
-
m('label.block.text-sm.font-medium.text-gray-700
|
|
352
|
-
|
|
380
|
+
m('label.block.text-sm.font-medium.text-gray-700.dark:text-slate-300.mb-1', { for: col.name }, [
|
|
381
|
+
label,
|
|
382
|
+
!col.nullable && !readonly ? m('span.text-red-500', ' *') : null,
|
|
383
|
+
]),
|
|
384
|
+
m('input.w-full.px-3.py-2.border.rounded-md.bg-white.dark:bg-slate-900/70.text-gray-900.dark:text-slate-100.placeholder-gray-400.dark:placeholder-slate-500.focus:outline-none.focus:ring-2', {
|
|
353
385
|
id: col.name,
|
|
354
386
|
name: col.name,
|
|
355
387
|
type: 'datetime-local',
|
|
@@ -360,50 +392,58 @@ const FieldRenderers = {
|
|
|
360
392
|
required: !col.nullable && !readonly,
|
|
361
393
|
readonly: readonly,
|
|
362
394
|
disabled: readonly,
|
|
363
|
-
class: readonly ? 'bg-gray-100 dark:bg-slate-800 cursor-not-allowed' : '',
|
|
395
|
+
class: (readonly ? 'bg-gray-100 dark:bg-slate-800 cursor-not-allowed ' : '') + borderClass,
|
|
364
396
|
oninput: (e) => onChange(e.target.value),
|
|
365
397
|
}),
|
|
366
|
-
hint ? m('p.text-xs.text-gray-500
|
|
398
|
+
error ? m('p.text-xs.text-red-600.dark:text-red-400.mt-1.font-medium', error) : (hint ? m('p.text-xs.text-gray-500.dark:text-slate-400.mt-1', hint) : null),
|
|
367
399
|
]);
|
|
368
400
|
},
|
|
369
401
|
|
|
370
402
|
// Enum select
|
|
371
|
-
enum: (col, value, onChange, readonly) => {
|
|
403
|
+
enum: (col, value, onChange, readonly, error) => {
|
|
372
404
|
const ui = col.ui || {};
|
|
373
405
|
const label = ui.label || formatColumnLabel(col.name);
|
|
374
406
|
const hint = ui.hint || '';
|
|
375
407
|
const options = col.enumValues || [];
|
|
408
|
+
const borderClass = error ? 'border-red-500 dark:border-red-500 focus:ring-red-500' : 'border-gray-300 dark:border-slate-600 focus:ring-blue-500 dark:focus:ring-blue-400';
|
|
376
409
|
|
|
377
410
|
return m('.mb-4', [
|
|
378
|
-
m('label.block.text-sm.font-medium.text-gray-700
|
|
379
|
-
|
|
411
|
+
m('label.block.text-sm.font-medium.text-gray-700.dark:text-slate-300.mb-1', { for: col.name }, [
|
|
412
|
+
label,
|
|
413
|
+
!col.nullable && !readonly ? m('span.text-red-500', ' *') : null,
|
|
414
|
+
]),
|
|
415
|
+
m('select.w-full.px-3.py-2.border.rounded-md.bg-white.dark:bg-slate-900/70.text-gray-900.dark:text-slate-100.focus:outline-none.focus:ring-2', {
|
|
380
416
|
id: col.name,
|
|
381
417
|
name: col.name,
|
|
382
418
|
value: value || '',
|
|
383
419
|
required: !col.nullable && !readonly,
|
|
384
420
|
disabled: readonly,
|
|
385
|
-
class: readonly ? 'bg-gray-100 dark:bg-slate-800 cursor-not-allowed' : '',
|
|
421
|
+
class: (readonly ? 'bg-gray-100 dark:bg-slate-800 cursor-not-allowed ' : '') + borderClass,
|
|
386
422
|
onchange: (e) => onChange(e.target.value),
|
|
387
423
|
}, [
|
|
388
424
|
col.nullable ? m('option', { value: '' }, '-- Select --') : null,
|
|
389
425
|
...options.map(opt => m('option', { value: opt, selected: value === opt }, opt)),
|
|
390
426
|
]),
|
|
391
|
-
hint ? m('p.text-xs.text-gray-500
|
|
427
|
+
error ? m('p.text-xs.text-red-600.dark:text-red-400.mt-1.font-medium', error) : (hint ? m('p.text-xs.text-gray-500.dark:text-slate-400.mt-1', hint) : null),
|
|
392
428
|
]);
|
|
393
429
|
},
|
|
394
430
|
|
|
395
431
|
// JSON textarea
|
|
396
|
-
json: (col, value, onChange, readonly) => {
|
|
432
|
+
json: (col, value, onChange, readonly, error) => {
|
|
397
433
|
const ui = col.ui || {};
|
|
398
434
|
const label = ui.label || formatColumnLabel(col.name);
|
|
399
435
|
const placeholder = ui.placeholder || '';
|
|
400
436
|
const hint = ui.hint || '';
|
|
401
437
|
const rows = ui.rows || 6;
|
|
402
438
|
const jsonString = value ? (typeof value === 'string' ? value : JSON.stringify(value, null, 2)) : '';
|
|
439
|
+
const borderClass = error ? 'border-red-500 dark:border-red-500 focus:ring-red-500' : 'border-gray-300 dark:border-slate-600 focus:ring-blue-500 dark:focus:ring-blue-400';
|
|
403
440
|
|
|
404
441
|
return m('.mb-4', [
|
|
405
|
-
m('label.block.text-sm.font-medium.text-gray-700
|
|
406
|
-
|
|
442
|
+
m('label.block.text-sm.font-medium.text-gray-700.dark:text-slate-300.mb-1', { for: col.name }, [
|
|
443
|
+
label,
|
|
444
|
+
!col.nullable && !readonly ? m('span.text-red-500', ' *') : null,
|
|
445
|
+
]),
|
|
446
|
+
m('textarea.w-full.px-3.py-2.border.rounded-md.bg-white.dark:bg-slate-900/70.text-gray-900.dark:text-slate-100.placeholder-gray-400.dark:placeholder-slate-500.font-mono.text-sm.focus:outline-none.focus:ring-2', {
|
|
407
447
|
id: col.name,
|
|
408
448
|
name: col.name,
|
|
409
449
|
rows: rows,
|
|
@@ -411,7 +451,7 @@ const FieldRenderers = {
|
|
|
411
451
|
required: !col.nullable && !readonly,
|
|
412
452
|
readonly: readonly,
|
|
413
453
|
disabled: readonly,
|
|
414
|
-
class: readonly ? 'bg-gray-100 dark:bg-slate-800 cursor-not-allowed' : '',
|
|
454
|
+
class: (readonly ? 'bg-gray-100 dark:bg-slate-800 cursor-not-allowed ' : '') + borderClass,
|
|
415
455
|
oninput: (e) => {
|
|
416
456
|
try {
|
|
417
457
|
const parsed = JSON.parse(e.target.value);
|
|
@@ -421,22 +461,26 @@ const FieldRenderers = {
|
|
|
421
461
|
}
|
|
422
462
|
},
|
|
423
463
|
}, jsonString),
|
|
424
|
-
hint ? m('p.text-xs.text-gray-500
|
|
464
|
+
error ? m('p.text-xs.text-red-600.dark:text-red-400.mt-1.font-medium', error) : (hint ? m('p.text-xs.text-gray-500.dark:text-slate-400.mt-1', hint) : null),
|
|
425
465
|
]);
|
|
426
466
|
},
|
|
427
467
|
|
|
428
468
|
// Array (as JSON or tags)
|
|
429
|
-
array: (col, value, onChange, readonly) => {
|
|
469
|
+
array: (col, value, onChange, readonly, error) => {
|
|
430
470
|
const validations = col.validations || {};
|
|
431
471
|
const ui = col.ui || {};
|
|
432
472
|
const label = ui.label || formatColumnLabel(col.name);
|
|
433
473
|
const placeholder = ui.placeholder || 'Comma-separated values';
|
|
434
474
|
const hint = ui.hint || 'Enter comma-separated values';
|
|
435
475
|
const arrayValue = Array.isArray(value) ? value.join(', ') : (value || '');
|
|
476
|
+
const borderClass = error ? 'border-red-500 dark:border-red-500 focus:ring-red-500' : 'border-gray-300 dark:border-slate-600 focus:ring-blue-500 dark:focus:ring-blue-400';
|
|
436
477
|
|
|
437
478
|
return m('.mb-4', [
|
|
438
|
-
m('label.block.text-sm.font-medium.text-gray-700
|
|
439
|
-
|
|
479
|
+
m('label.block.text-sm.font-medium.text-gray-700.dark:text-slate-300.mb-1', { for: col.name }, [
|
|
480
|
+
label,
|
|
481
|
+
!col.nullable && !readonly ? m('span.text-red-500', ' *') : null,
|
|
482
|
+
]),
|
|
483
|
+
m('input.w-full.px-3.py-2.border.rounded-md.bg-white.dark:bg-slate-900/70.text-gray-900.dark:text-slate-100.placeholder-gray-400.dark:placeholder-slate-500.focus:outline-none.focus:ring-2', {
|
|
440
484
|
id: col.name,
|
|
441
485
|
name: col.name,
|
|
442
486
|
type: 'text',
|
|
@@ -447,23 +491,23 @@ const FieldRenderers = {
|
|
|
447
491
|
required: !col.nullable && !readonly,
|
|
448
492
|
readonly: readonly,
|
|
449
493
|
disabled: readonly,
|
|
450
|
-
class: readonly ? 'bg-gray-100 dark:bg-slate-800 cursor-not-allowed' : '',
|
|
494
|
+
class: (readonly ? 'bg-gray-100 dark:bg-slate-800 cursor-not-allowed ' : '') + borderClass,
|
|
451
495
|
oninput: (e) => {
|
|
452
496
|
const arr = e.target.value.split(',').map(s => s.trim()).filter(s => s);
|
|
453
497
|
onChange(arr);
|
|
454
498
|
},
|
|
455
499
|
}),
|
|
456
|
-
hint ? m('p.text-xs.text-gray-500
|
|
500
|
+
error ? m('p.text-xs.text-red-600.dark:text-red-400.mt-1.font-medium', error) : (hint ? m('p.text-xs.text-gray-500.dark:text-slate-400.mt-1', hint) : null),
|
|
457
501
|
]);
|
|
458
502
|
},
|
|
459
503
|
|
|
460
504
|
// belongsTo relation dropdown
|
|
461
|
-
belongsTo: (col, value, onChange, readonly) => {
|
|
462
|
-
return m(BelongsToField, { col, value, onChange, readonly });
|
|
505
|
+
belongsTo: (col, value, onChange, readonly, error) => {
|
|
506
|
+
return m(BelongsToField, { col, value, onChange, readonly, error });
|
|
463
507
|
},
|
|
464
508
|
|
|
465
509
|
// hasMany relation checklist
|
|
466
|
-
hasMany: (col, value, onChange, readonly) => {
|
|
467
|
-
return m(HasManyField, { col, value, onChange, readonly });
|
|
510
|
+
hasMany: (col, value, onChange, readonly, error) => {
|
|
511
|
+
return m(HasManyField, { col, value, onChange, readonly, error });
|
|
468
512
|
},
|
|
469
513
|
};
|
|
@@ -5,13 +5,14 @@ const RichTextField = {
|
|
|
5
5
|
|
|
6
6
|
// Load Quill if not already loaded
|
|
7
7
|
if (typeof window.Quill === 'undefined') {
|
|
8
|
+
const adminPath = window.__ADMIN_PATH__ || '/_admin';
|
|
8
9
|
const link = document.createElement('link');
|
|
9
10
|
link.rel = 'stylesheet';
|
|
10
|
-
link.href = '
|
|
11
|
+
link.href = adminPath + '/vendor/quill.snow.min.css';
|
|
11
12
|
document.head.appendChild(link);
|
|
12
13
|
|
|
13
14
|
const script = document.createElement('script');
|
|
14
|
-
script.src = '
|
|
15
|
+
script.src = adminPath + '/vendor/quill.min.js';
|
|
15
16
|
script.onload = () => {
|
|
16
17
|
initEditor(vnode);
|
|
17
18
|
};
|
|
@@ -87,21 +88,22 @@ const RichTextField = {
|
|
|
87
88
|
},
|
|
88
89
|
|
|
89
90
|
view: (vnode) => {
|
|
90
|
-
const { name, col, value = '', onChange, readonly } = vnode.attrs;
|
|
91
|
+
const { name, col, value = '', onChange, readonly, error } = vnode.attrs;
|
|
91
92
|
const ui = col.ui || {};
|
|
92
93
|
const label = ui.label || formatColumnLabel(col.name);
|
|
93
94
|
const hint = ui.hint || '';
|
|
94
95
|
const editorId = 'quill-editor-' + name;
|
|
95
96
|
const required = !col.nullable && !readonly;
|
|
97
|
+
const borderClass = error ? 'border-red-500 dark:border-red-500' : 'border-gray-300 dark:border-slate-600';
|
|
96
98
|
|
|
97
99
|
return m('.mb-4', [
|
|
98
100
|
m('label.block.text-sm.font-medium.text-gray-700 dark:text-slate-300.mb-1', { for: name },
|
|
99
101
|
label,
|
|
100
102
|
required ? m('span.text-red-500', ' *') : null
|
|
101
103
|
),
|
|
102
|
-
m('div.border.
|
|
104
|
+
m('div.border.rounded.bg-white.dark:bg-slate-900/50', {
|
|
103
105
|
id: editorId,
|
|
104
|
-
class: readonly ? 'bg-gray-100 dark:bg-slate-800 opacity-50' : '',
|
|
106
|
+
class: (readonly ? 'bg-gray-100 dark:bg-slate-800 opacity-50 ' : '') + borderClass,
|
|
105
107
|
style: 'min-height: 200px;'
|
|
106
108
|
}),
|
|
107
109
|
m('input[type=hidden]', {
|
|
@@ -109,7 +111,7 @@ const RichTextField = {
|
|
|
109
111
|
id: name + '-value',
|
|
110
112
|
value: value || '',
|
|
111
113
|
}),
|
|
112
|
-
hint ? m('p.text-xs.text-gray-500
|
|
114
|
+
error ? m('p.text-xs.text-red-600.dark:text-red-400.mt-1.font-medium', error) : (hint ? m('p.text-xs.text-gray-500.dark:text-slate-400.mt-1', hint) : null),
|
|
113
115
|
]);
|
|
114
116
|
}
|
|
115
117
|
};
|
|
@@ -251,38 +253,47 @@ const FileUploadField = {
|
|
|
251
253
|
revokeLocalPreview(state);
|
|
252
254
|
if (onChange) onChange('');
|
|
253
255
|
};
|
|
256
|
+
var error = vnode.attrs.error;
|
|
257
|
+
var borderClass = error ? 'border-red-500 dark:border-red-500' : 'border-gray-300 dark:border-slate-600';
|
|
254
258
|
if (readonly) {
|
|
255
259
|
return m('.mb-4', [
|
|
256
|
-
m('label.block.text-sm.font-medium.text-gray-700.dark:text-slate-300.mb-1', label
|
|
260
|
+
m('label.block.text-sm.font-medium.text-gray-700.dark:text-slate-300.mb-1', label),
|
|
257
261
|
value
|
|
258
262
|
? renderUploadedFilePreview(value, { accept: accept, readonly: true, label: label })
|
|
259
263
|
: m('span.text-gray-400.dark:text-slate-500', '—'),
|
|
260
|
-
hint ? m('p.text-xs.text-gray-500.dark:text-slate-400.mt-1', hint) : null,
|
|
264
|
+
error ? m('p.text-xs.text-red-600.dark:text-red-400.mt-1.font-medium', error) : (hint ? m('p.text-xs.text-gray-500.dark:text-slate-400.mt-1', hint) : null),
|
|
261
265
|
]);
|
|
262
266
|
}
|
|
263
267
|
if (!uploadUrl) {
|
|
264
268
|
return m('.mb-4', [
|
|
265
|
-
m('label.block.text-sm.font-medium.text-gray-700.dark:text-slate-300.mb-1', { for: col.name },
|
|
269
|
+
m('label.block.text-sm.font-medium.text-gray-700.dark:text-slate-300.mb-1', { for: col.name }, [
|
|
270
|
+
label,
|
|
271
|
+
required ? m('span.text-red-500', ' *') : null,
|
|
272
|
+
]),
|
|
266
273
|
m('p.text-xs.text-amber-700.dark:text-amber-400.mb-2', 'Upload URL is not configured. Enter a public URL or path manually.'),
|
|
267
|
-
m('input.w-full.px-3.py-2.border.
|
|
274
|
+
m('input.w-full.px-3.py-2.border.rounded-md.bg-white.dark:bg-slate-900/70.text-gray-900.dark:text-slate-100.placeholder-gray-400.dark:placeholder-slate-500', {
|
|
268
275
|
type: 'text',
|
|
269
276
|
id: col.name,
|
|
270
277
|
name: col.name,
|
|
271
278
|
value: value,
|
|
272
279
|
placeholder: 'https://… or /uploads/…',
|
|
273
280
|
required: required,
|
|
281
|
+
class: borderClass,
|
|
274
282
|
oninput: function (e) { if (onChange) onChange(e.target.value); },
|
|
275
283
|
}),
|
|
276
284
|
value ? renderUploadedFilePreview(value, { accept: accept, readonly: false, onRemove: clearValue, label: label }) : null,
|
|
277
|
-
hint ? m('p.text-xs.text-gray-500.dark:text-slate-400.mt-1', hint) : null,
|
|
285
|
+
error ? m('p.text-xs.text-red-600.dark:text-red-400.mt-1.font-medium', error) : (hint ? m('p.text-xs.text-gray-500.dark:text-slate-400.mt-1', hint) : null),
|
|
278
286
|
]);
|
|
279
287
|
}
|
|
280
288
|
return m('.mb-4', [
|
|
281
|
-
m('label.block.text-sm.font-medium.text-gray-700.dark:text-slate-300.mb-1',
|
|
289
|
+
m('label.block.text-sm.font-medium.text-gray-700.dark:text-slate-300.mb-1', [
|
|
290
|
+
label,
|
|
291
|
+
required ? m('span.text-red-500', ' *') : null,
|
|
292
|
+
]),
|
|
282
293
|
displayUrl
|
|
283
294
|
? renderUploadedFilePreview(displayUrl, { accept: accept, readonly: false, onRemove: clearValue, label: label })
|
|
284
295
|
: null,
|
|
285
|
-
m('div#' + dropZoneId + '.border-2.border-dashed.
|
|
296
|
+
m('div#' + dropZoneId + '.border-2.border-dashed.rounded-lg.p-6.text-center.bg-gray-50.dark:bg-slate-900/40', { class: borderClass, style: 'cursor: pointer;' }, [
|
|
286
297
|
m('input[type=file].hidden', {
|
|
287
298
|
id: 'file-input-' + col.name,
|
|
288
299
|
accept: accept,
|
|
@@ -299,7 +310,7 @@ const FileUploadField = {
|
|
|
299
310
|
]),
|
|
300
311
|
m('input[type=hidden]', { name: col.name, value: typeof value === 'string' ? value : '' }),
|
|
301
312
|
m('p.text-xs.text-gray-500.dark:text-slate-400.mt-1', 'Max ' + Math.round(maxSize / 1024 / 1024) + ' MB (server enforces limits)'),
|
|
302
|
-
hint ? m('p.text-xs.text-gray-500.dark:text-slate-400.mt-1', hint) : null,
|
|
313
|
+
error ? m('p.text-xs.text-red-600.dark:text-red-400.mt-1.font-medium', error) : (hint ? m('p.text-xs.text-gray-500.dark:text-slate-400.mt-1', hint) : null),
|
|
303
314
|
]);
|
|
304
315
|
},
|
|
305
316
|
};
|
|
@@ -352,23 +363,25 @@ function getFieldRenderer(col, modelMeta) {
|
|
|
352
363
|
// Check for custom field first
|
|
353
364
|
if (col.customField && col.customField.type) {
|
|
354
365
|
if (col.customField.type === 'rich-text') {
|
|
355
|
-
return (col, value, onChange, readonly) => {
|
|
366
|
+
return (col, value, onChange, readonly, error) => {
|
|
356
367
|
return m(RichTextField, {
|
|
357
368
|
name: col.name,
|
|
358
369
|
col,
|
|
359
370
|
value: value || '',
|
|
360
371
|
onChange,
|
|
361
372
|
readonly: readonly || false,
|
|
373
|
+
error,
|
|
362
374
|
});
|
|
363
375
|
};
|
|
364
376
|
}
|
|
365
377
|
if (col.customField.type === 'file-upload') {
|
|
366
|
-
return (col, value, onChange, readonly) => {
|
|
378
|
+
return (col, value, onChange, readonly, error) => {
|
|
367
379
|
return m(FileUploadField, {
|
|
368
380
|
col,
|
|
369
381
|
value: value || '',
|
|
370
382
|
onChange,
|
|
371
383
|
readonly: readonly || false,
|
|
384
|
+
error,
|
|
372
385
|
});
|
|
373
386
|
};
|
|
374
387
|
}
|
|
@@ -382,11 +395,11 @@ function getFieldRenderer(col, modelMeta) {
|
|
|
382
395
|
// Check registered custom field renderers in browser
|
|
383
396
|
const custom = window.__customFieldRenderers && window.__customFieldRenderers[col.customField.type];
|
|
384
397
|
if (custom) {
|
|
385
|
-
return (col, value, onChange, readonly) => {
|
|
398
|
+
return (col, value, onChange, readonly, error) => {
|
|
386
399
|
if (custom.edit && typeof custom.edit === 'object' && custom.edit.view) {
|
|
387
|
-
return m(custom.edit, { col, value, onChange, readonly });
|
|
400
|
+
return m(custom.edit, { col, value, onChange, readonly, error });
|
|
388
401
|
} else if (typeof custom.edit === 'function') {
|
|
389
|
-
return custom.edit(value, onChange, col);
|
|
402
|
+
return custom.edit(value, onChange, col, readonly, error);
|
|
390
403
|
}
|
|
391
404
|
};
|
|
392
405
|
}
|
|
@@ -395,22 +408,23 @@ function getFieldRenderer(col, modelMeta) {
|
|
|
395
408
|
// Check schema type custom field renderers
|
|
396
409
|
if (col.type && window.__customFieldRenderers && window.__customFieldRenderers[col.type]) {
|
|
397
410
|
const custom = window.__customFieldRenderers[col.type];
|
|
398
|
-
return (col, value, onChange, readonly) => {
|
|
411
|
+
return (col, value, onChange, readonly, error) => {
|
|
399
412
|
if (custom.edit && typeof custom.edit === 'object' && custom.edit.view) {
|
|
400
|
-
return m(custom.edit, { col, value, onChange, readonly });
|
|
413
|
+
return m(custom.edit, { col, value, onChange, readonly, error });
|
|
401
414
|
} else if (typeof custom.edit === 'function') {
|
|
402
|
-
return custom.edit(value, onChange, col);
|
|
415
|
+
return custom.edit(value, onChange, col, readonly, error);
|
|
403
416
|
}
|
|
404
417
|
};
|
|
405
418
|
}
|
|
406
419
|
|
|
407
420
|
if (col.type === 'file') {
|
|
408
|
-
return (col, value, onChange, readonly) => {
|
|
421
|
+
return (col, value, onChange, readonly, error) => {
|
|
409
422
|
return m(FileUploadField, {
|
|
410
423
|
col,
|
|
411
424
|
value: value || '',
|
|
412
425
|
onChange,
|
|
413
426
|
readonly: readonly || false,
|
|
427
|
+
error,
|
|
414
428
|
});
|
|
415
429
|
};
|
|
416
430
|
}
|