json-object-editor 0.10.660 → 0.10.662
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/CHANGELOG.md +10 -0
- package/css/joe-styles.css +1 -1
- package/css/joe.css +2 -2
- package/css/joe.min.css +1 -1
- package/docs/JOE_AI_Overview.md +73 -17
- package/docs/React_Form_Integration_Example.md +299 -0
- package/docs/React_Form_Integration_Strategy.md +5 -6
- package/dummy +9 -1
- package/js/joe-ai.js +26 -0
- package/js/joe-react-form.js +608 -0
- package/js/joe.js +1 -1
- package/package.json +1 -1
- package/readme.md +8 -0
- package/server/fields/core.js +48 -1
- package/server/modules/MCP.js +4 -0
- package/server/modules/Sites.js +28 -2
- package/server/plugins/chatgpt.js +169 -21
- package/server/plugins/formBuilder.js +98 -0
- package/server/schemas/ai_assistant.js +43 -44
- package/server/schemas/ai_prompt.js +4 -58
- package/server/schemas/include.js +8 -3
- package/server/schemas/page.js +6 -0
|
@@ -0,0 +1,608 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JOE React Form Renderer
|
|
3
|
+
* Vanilla JS form renderer using React from CDN (no build step)
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* 1. Include React from CDN in your page
|
|
7
|
+
* 2. Load this script
|
|
8
|
+
* 3. Call: joeReactForm.init({ rootId: 'react-form-root', formDefinitionUrl: '/_include/{json_include_id}', formId: '{joe_form_id}' })
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
(function(window) {
|
|
12
|
+
'use strict';
|
|
13
|
+
|
|
14
|
+
// Wait for React/ReactDOM to load if not immediately available
|
|
15
|
+
function waitForReact(callback, maxAttempts) {
|
|
16
|
+
maxAttempts = maxAttempts || 50; // 50 attempts = ~2.5 seconds if 50ms intervals
|
|
17
|
+
var attempts = 0;
|
|
18
|
+
|
|
19
|
+
function check() {
|
|
20
|
+
if (typeof window.React !== 'undefined' && typeof window.ReactDOM !== 'undefined') {
|
|
21
|
+
callback();
|
|
22
|
+
} else {
|
|
23
|
+
attempts++;
|
|
24
|
+
if (attempts < maxAttempts) {
|
|
25
|
+
setTimeout(check, 50);
|
|
26
|
+
} else {
|
|
27
|
+
console.error('JOE React Form: React and ReactDOM failed to load after ' + (maxAttempts * 50) + 'ms. Make sure React CDN scripts are included before this script.');
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
check();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Test data generators for auto-fill
|
|
36
|
+
function generateTestValue(field, allFields) {
|
|
37
|
+
switch (field.type) {
|
|
38
|
+
case 'text':
|
|
39
|
+
if (field.id.toLowerCase().includes('name')) {
|
|
40
|
+
return field.id.toLowerCase().includes('first') ? 'John' : 'Doe';
|
|
41
|
+
}
|
|
42
|
+
if (field.id.toLowerCase().includes('email')) {
|
|
43
|
+
return 'test@example.com';
|
|
44
|
+
}
|
|
45
|
+
if (field.id.toLowerCase().includes('phone')) {
|
|
46
|
+
return '(555) 555-5555';
|
|
47
|
+
}
|
|
48
|
+
return 'Test ' + field.label;
|
|
49
|
+
case 'email':
|
|
50
|
+
return 'test@example.com';
|
|
51
|
+
case 'phone':
|
|
52
|
+
return '(555) 555-5555';
|
|
53
|
+
case 'number':
|
|
54
|
+
return field.min !== undefined ? field.min : 0;
|
|
55
|
+
case 'textarea':
|
|
56
|
+
return 'Test content for ' + field.label + '. This is sample text to fill the field.';
|
|
57
|
+
case 'select':
|
|
58
|
+
if (field.options && field.options.length > 0) {
|
|
59
|
+
// Prefer first non-blank option, or first option if all have values
|
|
60
|
+
var firstOption = field.options[0];
|
|
61
|
+
return firstOption.value || (field.options.length > 1 ? field.options[1].value : '');
|
|
62
|
+
}
|
|
63
|
+
return '';
|
|
64
|
+
case 'boolean':
|
|
65
|
+
return false; // Default to false for booleans
|
|
66
|
+
case 'scale':
|
|
67
|
+
var min = field.min !== undefined ? field.min : 0;
|
|
68
|
+
var max = field.max !== undefined ? field.max : 5;
|
|
69
|
+
return Math.floor((min + max) / 2); // Middle value
|
|
70
|
+
default:
|
|
71
|
+
return 'test';
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Create the public API immediately - this ensures joeReactForm exists even if React isn't loaded yet
|
|
76
|
+
window.joeReactForm = {
|
|
77
|
+
init: function(options) {
|
|
78
|
+
var rootId = options.rootId || 'react-form-root';
|
|
79
|
+
var formDefinitionUrl = options.formDefinitionUrl;
|
|
80
|
+
var formId = options.formId;
|
|
81
|
+
|
|
82
|
+
if (!formDefinitionUrl) {
|
|
83
|
+
console.error('JOE React Form: formDefinitionUrl is required');
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (!formId) {
|
|
88
|
+
console.error('JOE React Form: formId is required');
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Wait for React if needed, then initialize
|
|
93
|
+
function doInit() {
|
|
94
|
+
if (typeof window.React === 'undefined' || typeof window.ReactDOM === 'undefined') {
|
|
95
|
+
waitForReact(doInit);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// React is loaded - proceed with initialization
|
|
100
|
+
var React = window.React;
|
|
101
|
+
var ReactDOM = window.ReactDOM;
|
|
102
|
+
var createElement = React.createElement;
|
|
103
|
+
var useState = React.useState;
|
|
104
|
+
var useMemo = React.useMemo;
|
|
105
|
+
|
|
106
|
+
// Helper function for className
|
|
107
|
+
function clsx() {
|
|
108
|
+
var classes = [];
|
|
109
|
+
for (var i = 0; i < arguments.length; i++) {
|
|
110
|
+
if (arguments[i]) classes.push(arguments[i]);
|
|
111
|
+
}
|
|
112
|
+
return classes.join(' ');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Condition evaluation
|
|
116
|
+
function evalCondition(cond, values) {
|
|
117
|
+
var v = values && values[cond.field];
|
|
118
|
+
switch (cond.op) {
|
|
119
|
+
case 'eq':
|
|
120
|
+
return v === cond.value;
|
|
121
|
+
case 'neq':
|
|
122
|
+
return v !== cond.value;
|
|
123
|
+
case 'contains':
|
|
124
|
+
return typeof v === 'string' ? v.toLowerCase().indexOf(String(cond.value).toLowerCase()) !== -1 : false;
|
|
125
|
+
case 'gt':
|
|
126
|
+
return Number(v) > Number(cond.value);
|
|
127
|
+
case 'gte':
|
|
128
|
+
return Number(v) >= Number(cond.value);
|
|
129
|
+
case 'lt':
|
|
130
|
+
return Number(v) < Number(cond.value);
|
|
131
|
+
case 'lte':
|
|
132
|
+
return Number(v) <= Number(cond.value);
|
|
133
|
+
case 'truthy':
|
|
134
|
+
return Boolean(v);
|
|
135
|
+
default:
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Visibility check
|
|
141
|
+
function isVisible(field, values) {
|
|
142
|
+
var vis = field.visibility;
|
|
143
|
+
if (!vis) return true;
|
|
144
|
+
if (vis.whenAll) {
|
|
145
|
+
return vis.whenAll.every(function(c) { return evalCondition(c, values); });
|
|
146
|
+
}
|
|
147
|
+
if (vis.whenAny) {
|
|
148
|
+
return vis.whenAny.some(function(c) { return evalCondition(c, values); });
|
|
149
|
+
}
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Field component
|
|
154
|
+
function Field(props) {
|
|
155
|
+
var field = props.field;
|
|
156
|
+
var value = props.value;
|
|
157
|
+
var onChange = props.onChange;
|
|
158
|
+
var error = props.error;
|
|
159
|
+
|
|
160
|
+
var commonProps = {
|
|
161
|
+
id: field.id,
|
|
162
|
+
name: field.id,
|
|
163
|
+
className: 'w-full rounded-xl border border-slate-200 bg-white px-3 py-2 text-sm shadow-sm focus:border-slate-400 focus:outline-none focus:ring-2 focus:ring-slate-200',
|
|
164
|
+
value: value !== undefined && value !== null ? value : '',
|
|
165
|
+
onChange: function(e) { onChange(field.id, e.target.value); }
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
var label = createElement('label', {
|
|
169
|
+
htmlFor: field.id,
|
|
170
|
+
className: 'text-sm font-medium text-slate-900'
|
|
171
|
+
}, field.label, field.required ? createElement('span', { className: 'ml-1 text-rose-600' }, '*') : null);
|
|
172
|
+
|
|
173
|
+
var inputElement;
|
|
174
|
+
|
|
175
|
+
if (field.type === 'textarea') {
|
|
176
|
+
inputElement = createElement('textarea', Object.assign({}, commonProps, {
|
|
177
|
+
rows: 4,
|
|
178
|
+
placeholder: field.placeholder || ''
|
|
179
|
+
}));
|
|
180
|
+
} else if (field.type === 'select') {
|
|
181
|
+
var options = (field.options || []).map(function(o) {
|
|
182
|
+
return createElement('option', { key: o.value, value: o.value }, o.label);
|
|
183
|
+
});
|
|
184
|
+
inputElement = createElement('select', Object.assign({}, commonProps, {
|
|
185
|
+
value: value !== undefined && value !== null ? value : '',
|
|
186
|
+
onChange: function(e) { onChange(field.id, e.target.value); }
|
|
187
|
+
}), createElement('option', { value: '' }, 'Select…'), options);
|
|
188
|
+
} else if (field.type === 'boolean') {
|
|
189
|
+
inputElement = createElement('div', { className: 'flex items-center gap-3' },
|
|
190
|
+
createElement('label', { className: 'inline-flex items-center gap-2 rounded-xl border border-slate-200 bg-white px-3 py-2 text-sm shadow-sm' },
|
|
191
|
+
createElement('input', {
|
|
192
|
+
type: 'radio',
|
|
193
|
+
name: field.id,
|
|
194
|
+
checked: value === true,
|
|
195
|
+
onChange: function() { onChange(field.id, true); }
|
|
196
|
+
}),
|
|
197
|
+
'Yes'
|
|
198
|
+
),
|
|
199
|
+
createElement('label', { className: 'inline-flex items-center gap-2 rounded-xl border border-slate-200 bg-white px-3 py-2 text-sm shadow-sm' },
|
|
200
|
+
createElement('input', {
|
|
201
|
+
type: 'radio',
|
|
202
|
+
name: field.id,
|
|
203
|
+
checked: value === false,
|
|
204
|
+
onChange: function() { onChange(field.id, false); }
|
|
205
|
+
}),
|
|
206
|
+
'No'
|
|
207
|
+
)
|
|
208
|
+
);
|
|
209
|
+
} else if (field.type === 'scale') {
|
|
210
|
+
var min = field.min !== undefined ? field.min : 0;
|
|
211
|
+
var max = field.max !== undefined ? field.max : 5;
|
|
212
|
+
var currentValue = value !== undefined && value !== null ? value : 0;
|
|
213
|
+
inputElement = createElement('div', { className: 'space-y-2' },
|
|
214
|
+
createElement('input', {
|
|
215
|
+
type: 'range',
|
|
216
|
+
min: min,
|
|
217
|
+
max: max,
|
|
218
|
+
step: 1,
|
|
219
|
+
value: currentValue,
|
|
220
|
+
onChange: function(e) { onChange(field.id, Number(e.target.value)); },
|
|
221
|
+
className: 'w-full'
|
|
222
|
+
}),
|
|
223
|
+
createElement('div', { className: 'flex items-center justify-between text-xs text-slate-500' },
|
|
224
|
+
createElement('span', null, field.minLabel !== undefined ? field.minLabel : min),
|
|
225
|
+
createElement('span', { className: 'rounded-lg bg-slate-100 px-2 py-1 text-slate-700' }, String(currentValue)),
|
|
226
|
+
createElement('span', null, field.maxLabel !== undefined ? field.maxLabel : max)
|
|
227
|
+
)
|
|
228
|
+
);
|
|
229
|
+
} else {
|
|
230
|
+
var inputType = field.type === 'number' ? 'number' : (field.type === 'email' ? 'email' : 'text');
|
|
231
|
+
inputElement = createElement('input', Object.assign({}, commonProps, {
|
|
232
|
+
type: inputType,
|
|
233
|
+
min: field.min,
|
|
234
|
+
max: field.max,
|
|
235
|
+
step: field.step,
|
|
236
|
+
placeholder: field.placeholder || ''
|
|
237
|
+
}));
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return createElement('div', { className: 'space-y-1' },
|
|
241
|
+
createElement('div', { className: 'flex items-start justify-between gap-3' }, label),
|
|
242
|
+
inputElement,
|
|
243
|
+
error ? createElement('p', { className: 'text-xs text-rose-600' }, error) : null
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Validation
|
|
248
|
+
function validateSection(section, values) {
|
|
249
|
+
var errors = {};
|
|
250
|
+
for (var i = 0; i < section.fields.length; i++) {
|
|
251
|
+
var f = section.fields[i];
|
|
252
|
+
if (!isVisible(f, values)) continue;
|
|
253
|
+
if (!f.required) continue;
|
|
254
|
+
|
|
255
|
+
var v = values[f.id];
|
|
256
|
+
var empty = v === undefined || v === null || v === '';
|
|
257
|
+
|
|
258
|
+
if (f.type === 'boolean') {
|
|
259
|
+
if (v !== true && v !== false) {
|
|
260
|
+
errors[f.id] = 'Please select Yes or No.';
|
|
261
|
+
}
|
|
262
|
+
} else if (f.type === 'scale') {
|
|
263
|
+
if (v === undefined || v === null) {
|
|
264
|
+
errors[f.id] = 'Please choose a value.';
|
|
265
|
+
}
|
|
266
|
+
} else if (empty) {
|
|
267
|
+
errors[f.id] = 'This field is required.';
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return errors;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Check for test mode from query string
|
|
274
|
+
var testMode = false;
|
|
275
|
+
var skipValidation = false;
|
|
276
|
+
if (typeof window !== 'undefined' && window.location) {
|
|
277
|
+
var params = new URLSearchParams(window.location.search);
|
|
278
|
+
testMode = params.get('test') === '1' || params.get('test') === 'true';
|
|
279
|
+
skipValidation = params.get('skipValidation') === '1' || params.get('skipValidation') === 'true';
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Main App component
|
|
283
|
+
function App(props) {
|
|
284
|
+
var formDefinition = props.formDefinition;
|
|
285
|
+
var formId = props.formId;
|
|
286
|
+
|
|
287
|
+
if (!formDefinition || !formDefinition.sections || formDefinition.sections.length === 0) {
|
|
288
|
+
return createElement('div', { style: { padding: '20px' } }, 'Loading form...');
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
var sections = formDefinition.sections;
|
|
292
|
+
|
|
293
|
+
var stepState = useState(0);
|
|
294
|
+
var step = stepState[0];
|
|
295
|
+
var setStep = stepState[1];
|
|
296
|
+
|
|
297
|
+
var valuesState = useState({});
|
|
298
|
+
var values = valuesState[0];
|
|
299
|
+
var setValues = valuesState[1];
|
|
300
|
+
|
|
301
|
+
var errorsState = useState({});
|
|
302
|
+
var errors = errorsState[0];
|
|
303
|
+
var setErrors = errorsState[1];
|
|
304
|
+
|
|
305
|
+
var showReviewState = useState(false);
|
|
306
|
+
var showReview = showReviewState[0];
|
|
307
|
+
var setShowReview = showReviewState[1];
|
|
308
|
+
|
|
309
|
+
var current = sections[step];
|
|
310
|
+
|
|
311
|
+
var visibleFields = useMemo(function() {
|
|
312
|
+
if (!current) return [];
|
|
313
|
+
return current.fields.filter(function(f) { return isVisible(f, values); });
|
|
314
|
+
}, [current, values]);
|
|
315
|
+
|
|
316
|
+
// Collect all fields from all sections for auto-fill
|
|
317
|
+
var allFields = useMemo(function() {
|
|
318
|
+
var fields = [];
|
|
319
|
+
sections.forEach(function(section) {
|
|
320
|
+
if (section.fields) {
|
|
321
|
+
section.fields.forEach(function(f) {
|
|
322
|
+
fields.push(f);
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
return fields;
|
|
327
|
+
}, [sections]);
|
|
328
|
+
|
|
329
|
+
// Auto-fill function for testing
|
|
330
|
+
function autoFillForm() {
|
|
331
|
+
var newValues = {};
|
|
332
|
+
allFields.forEach(function(field) {
|
|
333
|
+
if (isVisible(field, newValues)) {
|
|
334
|
+
var testValue = generateTestValue(field, allFields);
|
|
335
|
+
if (testValue !== null && testValue !== undefined) {
|
|
336
|
+
newValues[field.id] = testValue;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
setValues(newValues);
|
|
341
|
+
setErrors({});
|
|
342
|
+
console.log('JOE React Form: Auto-filled form with test data', newValues);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Force submission bypassing validation
|
|
346
|
+
function forceSubmit() {
|
|
347
|
+
submitForm();
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Expose testing functions to window in test mode
|
|
351
|
+
if (testMode && typeof window !== 'undefined') {
|
|
352
|
+
window.__joeReactFormTest = {
|
|
353
|
+
autoFill: autoFillForm,
|
|
354
|
+
forceSubmit: forceSubmit,
|
|
355
|
+
getValues: function() { return values; },
|
|
356
|
+
setValues: setValues,
|
|
357
|
+
submit: submitForm,
|
|
358
|
+
setStep: setStep,
|
|
359
|
+
goToReview: function() { setShowReview(true); },
|
|
360
|
+
formDefinition: formDefinition
|
|
361
|
+
};
|
|
362
|
+
console.log('JOE React Form: Test mode enabled. Use window.__joeReactFormTest for testing.');
|
|
363
|
+
console.log('Available functions:', Object.keys(window.__joeReactFormTest));
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function setValue(id, v) {
|
|
367
|
+
setValues(function(prev) {
|
|
368
|
+
var next = Object.assign({}, prev);
|
|
369
|
+
next[id] = v;
|
|
370
|
+
return next;
|
|
371
|
+
});
|
|
372
|
+
setErrors(function(prev) {
|
|
373
|
+
if (!prev[id]) return prev;
|
|
374
|
+
var next = Object.assign({}, prev);
|
|
375
|
+
delete next[id];
|
|
376
|
+
return next;
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function next() {
|
|
381
|
+
if (!current) return;
|
|
382
|
+
var e = validateSection(current, values);
|
|
383
|
+
setErrors(e);
|
|
384
|
+
// Skip validation check if in skipValidation mode
|
|
385
|
+
if (!skipValidation && Object.keys(e).length) return;
|
|
386
|
+
|
|
387
|
+
if (step < sections.length - 1) {
|
|
388
|
+
setStep(step + 1);
|
|
389
|
+
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
390
|
+
} else {
|
|
391
|
+
setShowReview(true);
|
|
392
|
+
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function back() {
|
|
397
|
+
if (showReview) {
|
|
398
|
+
setShowReview(false);
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
setStep(Math.max(0, step - 1));
|
|
402
|
+
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function submitForm() {
|
|
406
|
+
// Submit to JOE
|
|
407
|
+
var submissionData = {
|
|
408
|
+
formid: formId,
|
|
409
|
+
submission: values
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
fetch('/API/plugin/formBuilder/submission', {
|
|
413
|
+
method: 'POST',
|
|
414
|
+
headers: { 'Content-Type': 'application/json' },
|
|
415
|
+
body: JSON.stringify(submissionData)
|
|
416
|
+
})
|
|
417
|
+
.then(function(response) { return response.json(); })
|
|
418
|
+
.then(function(data) {
|
|
419
|
+
if (data.errors) {
|
|
420
|
+
alert('Submission error: ' + (typeof data.errors === 'string' ? data.errors : JSON.stringify(data.errors)));
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
alert('Form submitted successfully!');
|
|
424
|
+
// Could redirect or show success message
|
|
425
|
+
})
|
|
426
|
+
.catch(function(error) {
|
|
427
|
+
console.error('Submission error:', error);
|
|
428
|
+
alert('Error submitting form: ' + error.message);
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// Already checked formDefinition above, just check current
|
|
433
|
+
if (!current) {
|
|
434
|
+
return createElement('div', { style: { padding: '20px' } }, 'Loading form...');
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
var progressPercent = showReview ? 100 : ((step + 1) / sections.length) * 100;
|
|
438
|
+
|
|
439
|
+
return createElement('div', { className: 'min-h-screen bg-gradient-to-b from-slate-50 to-white' },
|
|
440
|
+
createElement('div', { className: 'mx-auto max-w-3xl px-4 py-8' },
|
|
441
|
+
createElement('header', { className: 'mb-6' },
|
|
442
|
+
createElement('div', { className: 'flex flex-col gap-2' },
|
|
443
|
+
createElement('h1', { className: 'text-2xl font-semibold text-slate-900' }, formDefinition.formName || 'Form'),
|
|
444
|
+
createElement('p', { className: 'text-sm text-slate-600' },
|
|
445
|
+
'Version ' + (formDefinition.version || '1.0') + ' • Interactive form'
|
|
446
|
+
)
|
|
447
|
+
),
|
|
448
|
+
createElement('div', { className: 'mt-4 h-2 w-full overflow-hidden rounded-full bg-slate-100' },
|
|
449
|
+
createElement('div', {
|
|
450
|
+
className: 'h-full rounded-full bg-slate-900 transition-all',
|
|
451
|
+
style: { width: progressPercent + '%' }
|
|
452
|
+
})
|
|
453
|
+
),
|
|
454
|
+
createElement('div', { className: 'mt-2 flex items-center justify-between text-xs text-slate-500' },
|
|
455
|
+
createElement('span', null, showReview ? 'Review' : 'Section ' + (step + 1) + ' of ' + sections.length),
|
|
456
|
+
createElement('span', null, showReview ? 'Ready' : current.title)
|
|
457
|
+
)
|
|
458
|
+
),
|
|
459
|
+
createElement('main', { className: 'rounded-2xl border border-slate-200 bg-white p-5 shadow-sm' },
|
|
460
|
+
!showReview ? createElement('div', null,
|
|
461
|
+
createElement('div', { className: 'mb-4' },
|
|
462
|
+
createElement('h2', { className: 'text-lg font-semibold text-slate-900' }, current.title),
|
|
463
|
+
current.description ? createElement('p', { className: 'mt-1 text-sm text-slate-600' }, current.description) : null
|
|
464
|
+
),
|
|
465
|
+
createElement('div', { className: 'grid gap-4' },
|
|
466
|
+
visibleFields.map(function(f) {
|
|
467
|
+
return createElement(Field, {
|
|
468
|
+
key: f.id,
|
|
469
|
+
field: f,
|
|
470
|
+
value: values[f.id],
|
|
471
|
+
onChange: setValue,
|
|
472
|
+
error: errors[f.id]
|
|
473
|
+
});
|
|
474
|
+
})
|
|
475
|
+
),
|
|
476
|
+
createElement('div', { className: 'mt-6 flex items-center justify-between' },
|
|
477
|
+
createElement('div', { className: 'flex items-center gap-2' },
|
|
478
|
+
createElement('button', {
|
|
479
|
+
onClick: back,
|
|
480
|
+
disabled: step === 0,
|
|
481
|
+
className: clsx(
|
|
482
|
+
'rounded-xl px-4 py-2 text-sm font-medium',
|
|
483
|
+
step === 0
|
|
484
|
+
? 'cursor-not-allowed bg-slate-100 text-slate-400'
|
|
485
|
+
: 'bg-slate-100 text-slate-800 hover:bg-slate-200'
|
|
486
|
+
)
|
|
487
|
+
}, 'Back'),
|
|
488
|
+
testMode ? createElement('button', {
|
|
489
|
+
onClick: autoFillForm,
|
|
490
|
+
className: 'rounded-xl bg-amber-500 px-3 py-2 text-xs font-medium text-white hover:bg-amber-600',
|
|
491
|
+
title: 'Auto-fill all fields with test data'
|
|
492
|
+
}, '🧪 Auto-fill') : null
|
|
493
|
+
),
|
|
494
|
+
createElement('button', {
|
|
495
|
+
onClick: next,
|
|
496
|
+
className: 'rounded-xl bg-slate-900 px-4 py-2 text-sm font-medium text-white hover:bg-slate-800'
|
|
497
|
+
}, step === sections.length - 1 ? 'Review' : 'Next')
|
|
498
|
+
)
|
|
499
|
+
) : createElement('div', null,
|
|
500
|
+
createElement('div', { className: 'mb-4' },
|
|
501
|
+
createElement('h2', { className: 'text-lg font-semibold text-slate-900' }, 'Review & Submit'),
|
|
502
|
+
createElement('p', { className: 'mt-1 text-sm text-slate-600' },
|
|
503
|
+
'Please review your answers before submitting.'
|
|
504
|
+
)
|
|
505
|
+
),
|
|
506
|
+
createElement('div', { className: 'rounded-xl border border-slate-200 bg-slate-50 p-3' },
|
|
507
|
+
createElement('pre', { className: 'max-h-[55vh] overflow-auto text-xs text-slate-800' },
|
|
508
|
+
JSON.stringify(values, null, 2)
|
|
509
|
+
)
|
|
510
|
+
),
|
|
511
|
+
createElement('div', { className: 'mt-6 flex items-center justify-between' },
|
|
512
|
+
createElement('div', { className: 'flex items-center gap-2' },
|
|
513
|
+
createElement('button', {
|
|
514
|
+
onClick: back,
|
|
515
|
+
className: 'rounded-xl bg-slate-100 px-4 py-2 text-sm font-medium text-slate-800 hover:bg-slate-200'
|
|
516
|
+
}, 'Back'),
|
|
517
|
+
testMode ? createElement('button', {
|
|
518
|
+
onClick: forceSubmit,
|
|
519
|
+
className: 'rounded-xl bg-red-500 px-3 py-2 text-xs font-medium text-white hover:bg-red-600',
|
|
520
|
+
title: 'Force submit (bypass validation)'
|
|
521
|
+
}, '🚀 Force Submit') : null
|
|
522
|
+
),
|
|
523
|
+
createElement('button', {
|
|
524
|
+
onClick: submitForm,
|
|
525
|
+
className: 'rounded-xl bg-slate-900 px-4 py-2 text-sm font-medium text-white hover:bg-slate-800'
|
|
526
|
+
}, 'Submit')
|
|
527
|
+
)
|
|
528
|
+
)
|
|
529
|
+
)
|
|
530
|
+
)
|
|
531
|
+
);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// Fetch form definition and render
|
|
535
|
+
console.log('JOE React Form: Fetching form definition from:', formDefinitionUrl);
|
|
536
|
+
fetch(formDefinitionUrl)
|
|
537
|
+
.then(function(response) {
|
|
538
|
+
if (!response.ok) {
|
|
539
|
+
throw new Error('HTTP error! status: ' + response.status);
|
|
540
|
+
}
|
|
541
|
+
return response.json();
|
|
542
|
+
})
|
|
543
|
+
.then(function(responseData) {
|
|
544
|
+
console.log('JOE React Form: Raw response received:', responseData);
|
|
545
|
+
console.log('JOE React Form: Response type:', typeof responseData);
|
|
546
|
+
console.log('JOE React Form: Has sections?', responseData && responseData.sections);
|
|
547
|
+
|
|
548
|
+
// Handle potential response wrapping (some APIs wrap in {data: {...}})
|
|
549
|
+
var formDefinition = responseData;
|
|
550
|
+
if (responseData && responseData.data && responseData.data.sections) {
|
|
551
|
+
formDefinition = responseData.data;
|
|
552
|
+
console.log('JOE React Form: Unwrapped response.data');
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// If response has errors, show them
|
|
556
|
+
if (responseData && (responseData.errors || responseData.error)) {
|
|
557
|
+
console.error('JOE React Form: API returned error:', responseData.errors || responseData.error);
|
|
558
|
+
var root = document.getElementById(rootId);
|
|
559
|
+
if (root) {
|
|
560
|
+
var errorMsg = responseData.errors || responseData.error;
|
|
561
|
+
root.innerHTML = '<div style="padding: 20px; color: red;">Error loading form: ' +
|
|
562
|
+
(typeof errorMsg === 'string' ? errorMsg : JSON.stringify(errorMsg)) + '</div>';
|
|
563
|
+
}
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
console.log('JOE React Form: Processing form definition:', formDefinition);
|
|
568
|
+
console.log('JOE React Form: Sections count:', formDefinition && formDefinition.sections ? formDefinition.sections.length : 'N/A');
|
|
569
|
+
|
|
570
|
+
if (!formDefinition || !formDefinition.sections || formDefinition.sections.length === 0) {
|
|
571
|
+
console.error('JOE React Form: Invalid form definition');
|
|
572
|
+
console.error('JOE React Form: formDefinition:', formDefinition);
|
|
573
|
+
console.error('JOE React Form: formDefinition.sections:', formDefinition && formDefinition.sections);
|
|
574
|
+
var root = document.getElementById(rootId);
|
|
575
|
+
if (root) {
|
|
576
|
+
root.innerHTML = '<div style="padding: 20px; color: red;">Error: Form definition is invalid or has no sections.<br><pre style="font-size:11px; overflow:auto; max-height:200px;">' +
|
|
577
|
+
JSON.stringify(formDefinition, null, 2) + '</pre></div>';
|
|
578
|
+
}
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
var root = document.getElementById(rootId);
|
|
583
|
+
if (!root) {
|
|
584
|
+
console.error('JOE React Form: root element #' + rootId + ' not found');
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
console.log('JOE React Form: Rendering form with', formDefinition.sections.length, 'sections');
|
|
589
|
+
ReactDOM.render(
|
|
590
|
+
createElement(App, { formDefinition: formDefinition, formId: formId }),
|
|
591
|
+
root
|
|
592
|
+
);
|
|
593
|
+
})
|
|
594
|
+
.catch(function(error) {
|
|
595
|
+
console.error('JOE React Form: Error loading form definition:', error);
|
|
596
|
+
var root = document.getElementById(rootId);
|
|
597
|
+
if (root) {
|
|
598
|
+
root.innerHTML = '<div style="padding: 20px; color: red;">Error loading form: ' + error.message + '</div>';
|
|
599
|
+
}
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// Start initialization (will wait for React if needed)
|
|
604
|
+
doInit();
|
|
605
|
+
}
|
|
606
|
+
};
|
|
607
|
+
|
|
608
|
+
})(window);
|
package/js/joe.js
CHANGED
package/package.json
CHANGED
package/readme.md
CHANGED
|
@@ -4,6 +4,14 @@
|
|
|
4
4
|
JOE is software that allows you to manage data models via JSON objects. There are two flavors, the client-side version and nodejs server platform.
|
|
5
5
|
|
|
6
6
|
|
|
7
|
+
## What's new in 0.10.662 (brief)
|
|
8
|
+
- React Form Integration with JSON Definitions:
|
|
9
|
+
- **JSON includes**: `include` schema now supports `filetype: 'json'` for storing JSON form definitions (served at `/_include/{id}` with proper content-type).
|
|
10
|
+
- **Form-page linking**: Added `form` reference field to `page` schema to link pages to JOE forms.
|
|
11
|
+
- **Form definition API**: New `/API/plugin/formBuilder/definition` endpoint (merged from `formDefinition` plugin) serves JSON form definitions. Automatically finds JSON includes from form metadata or page includes.
|
|
12
|
+
- **React form renderer**: New `joe-react-form.js` client library renders multi-step React forms from JSON definitions with conditional visibility, validation, and submission to JOE's form submission system.
|
|
13
|
+
- **Page rendering fix**: Enhanced template variable processing to preserve newlines in page content, fixing JavaScript comment issues in `code` and `module` content types.
|
|
14
|
+
|
|
7
15
|
|
|
8
16
|
## What’s new in 0.10.660 (brief)
|
|
9
17
|
- MCP everywhere (prompts, autofill, widget):
|