webspresso 0.0.90 → 0.0.93

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.
Files changed (125) hide show
  1. package/README.md +98 -2509
  2. package/bin/commands/admin-password.js +8 -2
  3. package/bin/commands/api.js +7 -1
  4. package/bin/commands/favicon-generate.js +6 -4
  5. package/bin/commands/mcp.js +162 -0
  6. package/bin/commands/new.js +7 -1
  7. package/bin/commands/page.js +7 -1
  8. package/bin/commands/seed.js +7 -1
  9. package/bin/commands/service.js +199 -0
  10. package/bin/commands/skill.js +7 -1
  11. package/bin/webspresso.js +4 -0
  12. package/core/auth/middleware.js +9 -0
  13. package/core/compileSchema.js +1 -5
  14. package/core/compression/compressible.js +63 -0
  15. package/core/compression/compression-stream.js +59 -0
  16. package/core/compression/index.js +25 -0
  17. package/core/compression/middleware.js +310 -0
  18. package/core/compression/negotiate.js +165 -0
  19. package/core/errors/base.js +31 -0
  20. package/core/errors/domain.js +193 -0
  21. package/core/errors/http.js +133 -0
  22. package/core/errors/index.js +77 -0
  23. package/core/errors/middleware.js +162 -0
  24. package/core/errors/normalize.js +171 -0
  25. package/core/kernel/app.js +2 -1
  26. package/core/kernel/index.js +5 -1
  27. package/core/orm/cache/fingerprint.js +23 -5
  28. package/core/orm/complexity.js +120 -0
  29. package/core/orm/eager-loader.js +54 -9
  30. package/core/orm/events.js +64 -14
  31. package/core/orm/index.js +83 -12
  32. package/core/orm/model.js +13 -2
  33. package/core/orm/query-builder.js +30 -4
  34. package/core/orm/repository.js +57 -25
  35. package/core/orm/transaction.js +39 -5
  36. package/core/orm/utils.js +6 -3
  37. package/core/queue/adapters/database.js +317 -0
  38. package/core/queue/adapters/memory.js +176 -0
  39. package/core/queue/adapters/redis.js +357 -0
  40. package/core/queue/index.js +21 -0
  41. package/core/queue/job.js +103 -0
  42. package/core/queue/manager.js +413 -0
  43. package/core/realtime/adapters/redis.js +431 -0
  44. package/core/realtime/adapters/socket-io.js +203 -0
  45. package/core/realtime/adapters/sse.js +210 -0
  46. package/core/realtime/adapters/websocket.js +337 -0
  47. package/core/realtime/auth-manager.js +140 -0
  48. package/core/realtime/core.js +359 -0
  49. package/core/realtime/emitter.js +98 -0
  50. package/core/realtime/errors.js +71 -0
  51. package/core/realtime/index.js +79 -0
  52. package/core/realtime/reconnect-manager.js +118 -0
  53. package/core/realtime/registry.js +243 -0
  54. package/core/shutdown/http-adapter.js +134 -0
  55. package/core/shutdown/index.js +12 -0
  56. package/core/shutdown/shutdown-manager.js +312 -0
  57. package/core/ssr/index.js +13 -0
  58. package/core/ssr/stream.js +174 -0
  59. package/core/validation/index.js +70 -0
  60. package/index.d.ts +924 -53
  61. package/index.js +110 -1
  62. package/package.json +30 -2
  63. package/plugins/admin-panel/api.js +37 -4
  64. package/plugins/admin-panel/app.js +57 -17
  65. package/plugins/admin-panel/client/parts/01-state-api-breadcrumb.js +10 -2
  66. package/plugins/admin-panel/client/parts/04-field-renderers.js +102 -58
  67. package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +51 -27
  68. package/plugins/admin-panel/client/parts/07-model-list.js +9 -4
  69. package/plugins/admin-panel/client/parts/08-record-list.js +37 -26
  70. package/plugins/admin-panel/client/parts/09-record-form.js +168 -19
  71. package/plugins/admin-panel/core/admin-module.js +2 -2
  72. package/plugins/admin-panel/core/api-extensions.js +149 -0
  73. package/plugins/admin-panel/field-renderers/rich-text.js +3 -2
  74. package/plugins/admin-panel/index.js +155 -23
  75. package/plugins/admin-panel/modules/bulk-actions.js +9 -2
  76. package/plugins/admin-panel/modules/custom-pages.js +144 -45
  77. package/plugins/admin-panel/modules/dashboard.js +8 -1
  78. package/plugins/admin-panel/modules/menu.js +85 -0
  79. package/plugins/admin-panel/modules/user-management.js +5 -2
  80. package/plugins/admin-panel/vendor/mithril.min.js +1 -0
  81. package/plugins/admin-panel/vendor/quill.min.js +7 -0
  82. package/plugins/admin-panel/vendor/quill.snow.min.css +945 -0
  83. package/plugins/admin-panel/vendor/tailwind.min.js +65 -0
  84. package/plugins/basic-auth/index.js +293 -0
  85. package/plugins/data-exchange/export-xlsx.js +28 -2
  86. package/plugins/data-exchange/import.js +55 -16
  87. package/plugins/data-exchange/index.js +19 -0
  88. package/plugins/data-exchange/parse-table.js +14 -5
  89. package/plugins/data-exchange/record-selection.js +2 -0
  90. package/plugins/email/index.js +14 -1
  91. package/plugins/index.js +33 -0
  92. package/plugins/mcp/auth.js +104 -0
  93. package/plugins/mcp/discovery.js +88 -0
  94. package/plugins/mcp/index.js +208 -0
  95. package/plugins/mcp/prompts.js +149 -0
  96. package/plugins/mcp/providers/orm.js +325 -0
  97. package/plugins/mcp/providers/services.js +190 -0
  98. package/plugins/mcp/providers/system.js +87 -0
  99. package/plugins/mcp/server.js +472 -0
  100. package/plugins/mcp/transports/sse.js +139 -0
  101. package/plugins/mcp/transports/stdio.js +108 -0
  102. package/plugins/queue/index.js +108 -0
  103. package/plugins/realtime/index.js +150 -0
  104. package/plugins/recaptcha/index.js +11 -0
  105. package/plugins/redirect/index.js +9 -1
  106. package/plugins/rest-resources/index.js +280 -11
  107. package/plugins/upload/index.js +16 -0
  108. package/src/app-context.js +46 -6
  109. package/src/file-router.js +366 -130
  110. package/src/helpers.js +20 -2
  111. package/src/njk-frontmatter.js +24 -14
  112. package/src/plugin-manager.js +124 -4
  113. package/src/server.js +332 -54
  114. package/src/services/builtins/auth.js +273 -0
  115. package/src/services/builtins/exchange.js +229 -0
  116. package/src/services/builtins/mail.js +134 -0
  117. package/src/services/builtins/media.js +196 -0
  118. package/src/services/builtins/system.js +221 -0
  119. package/src/services/discovery.js +141 -0
  120. package/src/services/executor.js +292 -0
  121. package/src/services/index.js +70 -0
  122. package/src/services/memoize.js +256 -0
  123. package/src/services/registry.js +290 -0
  124. package/src/services/validator.js +230 -0
  125. package/templates/skills/webspresso-usage/REFERENCE-framework.md +6 -4
@@ -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.border-gray-300.dark:border-slate-600.rounded-md.bg-white.dark:bg-slate-900/70.text-gray-900.dark:text-slate-100.focus:outline-none.focus:ring-2.focus:ring-blue-500.dark:focus:ring-blue-400', {
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.border-gray-300.dark:border-slate-600.rounded-md.p-3.max-h-48.overflow-y-auto.bg-white.dark:bg-slate-900/70', [
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 dark:text-slate-300.mb-1', { for: col.name }, label),
170
- m('input.w-full.px-3.py-2.border.border-gray-300.dark:border-slate-600.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.focus:ring-blue-500.dark:focus:ring-blue-400', {
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 dark:text-slate-400.mt-1', hint) : null,
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 dark:text-slate-300.mb-1', { for: col.name }, label),
200
- m('textarea.w-full.px-3.py-2.border.border-gray-300.dark:border-slate-600.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.focus:ring-blue-500.dark:focus:ring-blue-400', {
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 dark:text-slate-400.mt-1', hint) : null,
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 dark:text-slate-300.mb-1', { for: col.name }, label),
227
- m('input.w-full.px-3.py-2.border.border-gray-300.dark:border-slate-600.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.focus:ring-blue-500.dark:focus:ring-blue-400', {
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 dark:text-slate-400.mt-1', hint) : null,
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 dark:text-slate-300.mb-1', { for: col.name }, label),
256
- m('input.w-full.px-3.py-2.border.border-gray-300.dark:border-slate-600.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.focus:ring-blue-500.dark:focus:ring-blue-400', {
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 dark:text-slate-400.mt-1', hint) : null,
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 dark:text-slate-400.mt-1', hint) : null,
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 dark:text-slate-300.mb-1', { for: col.name }, label),
315
- m('input.w-full.px-3.py-2.border.border-gray-300.dark:border-slate-600.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.focus:ring-blue-500.dark:focus:ring-blue-400', {
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 dark:text-slate-400.mt-1', hint) : null,
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 dark:text-slate-300.mb-1', { for: col.name }, label),
352
- m('input.w-full.px-3.py-2.border.border-gray-300.dark:border-slate-600.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.focus:ring-blue-500.dark:focus:ring-blue-400', {
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 dark:text-slate-400.mt-1', hint) : null,
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 dark:text-slate-300.mb-1', { for: col.name }, label),
379
- m('select.w-full.px-3.py-2.border.border-gray-300.dark:border-slate-600.rounded-md.bg-white.dark:bg-slate-900/70.text-gray-900.dark:text-slate-100.focus:outline-none.focus:ring-2.focus:ring-blue-500.dark:focus:ring-blue-400', {
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 dark:text-slate-400.mt-1', hint) : null,
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 dark:text-slate-300.mb-1', { for: col.name }, label),
406
- m('textarea.w-full.px-3.py-2.border.border-gray-300.dark:border-slate-600.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.focus:ring-blue-500.dark:focus:ring-blue-400', {
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 dark:text-slate-400.mt-1', hint) : null,
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 dark:text-slate-300.mb-1', { for: col.name }, label),
439
- m('input.w-full.px-3.py-2.border.border-gray-300.dark:border-slate-600.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.focus:ring-blue-500.dark:focus:ring-blue-400', {
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 dark:text-slate-400.mt-1', hint) : null,
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 = 'https://cdn.quilljs.com/1.3.6/quill.snow.css';
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 = 'https://cdn.quilljs.com/1.3.6/quill.js';
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.border-gray-300.dark:border-slate-600.rounded.bg-white.dark:bg-slate-900/50', {
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 dark:text-slate-400.mt-1', hint) : null,
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, required ? m('span.text-red-500', ' *') : null),
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 }, label, required ? m('span.text-red-500', ' *') : null),
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.border-gray-300.dark:border-slate-600.rounded-md.bg-white.dark:bg-slate-900/70.text-gray-900.dark:text-slate-100.placeholder-gray-400.dark:placeholder-slate-500', {
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', label, required ? m('span.text-red-500', ' *') : null),
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.border-gray-300.dark:border-slate-600.rounded-lg.p-6.text-center.bg-gray-50.dark:bg-slate-900/40', { style: 'cursor: pointer;' }, [
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,16 @@ 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) => {
386
- if (custom.edit && typeof custom.edit === 'object' && custom.edit.view) {
387
- return m(custom.edit, { col, value, onChange, readonly });
388
- } else if (typeof custom.edit === 'function') {
389
- return custom.edit(value, onChange, col);
398
+ return (col, value, onChange, readonly, error) => {
399
+ try {
400
+ if (custom.edit && typeof custom.edit === 'object' && custom.edit.view) {
401
+ return m(custom.edit, { col, value, onChange, readonly, error });
402
+ } else if (typeof custom.edit === 'function') {
403
+ return custom.edit(value, onChange, col, readonly, error);
404
+ }
405
+ } catch (err) {
406
+ console.error('[Admin Panel] Error rendering custom field edit for "' + col.customField.type + '":', err);
407
+ return m('div.text-red-500.text-xs', 'Edit render error: ' + (err.message || String(err)));
390
408
  }
391
409
  };
392
410
  }
@@ -395,22 +413,28 @@ function getFieldRenderer(col, modelMeta) {
395
413
  // Check schema type custom field renderers
396
414
  if (col.type && window.__customFieldRenderers && window.__customFieldRenderers[col.type]) {
397
415
  const custom = window.__customFieldRenderers[col.type];
398
- return (col, value, onChange, readonly) => {
399
- if (custom.edit && typeof custom.edit === 'object' && custom.edit.view) {
400
- return m(custom.edit, { col, value, onChange, readonly });
401
- } else if (typeof custom.edit === 'function') {
402
- return custom.edit(value, onChange, col);
416
+ return (col, value, onChange, readonly, error) => {
417
+ try {
418
+ if (custom.edit && typeof custom.edit === 'object' && custom.edit.view) {
419
+ return m(custom.edit, { col, value, onChange, readonly, error });
420
+ } else if (typeof custom.edit === 'function') {
421
+ return custom.edit(value, onChange, col, readonly, error);
422
+ }
423
+ } catch (err) {
424
+ console.error('[Admin Panel] Error rendering custom field edit for "' + col.type + '":', err);
425
+ return m('div.text-red-500.text-xs', 'Edit render error: ' + (err.message || String(err)));
403
426
  }
404
427
  };
405
428
  }
406
429
 
407
430
  if (col.type === 'file') {
408
- return (col, value, onChange, readonly) => {
431
+ return (col, value, onChange, readonly, error) => {
409
432
  return m(FileUploadField, {
410
433
  col,
411
434
  value: value || '',
412
435
  onChange,
413
436
  readonly: readonly || false,
437
+ error,
414
438
  });
415
439
  };
416
440
  }