@esheet/adapters 0.0.3
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 +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/lib/mcp.d.ts +128 -0
- package/dist/lib/mcp.d.ts.map +1 -0
- package/dist/lib/mcp.js +408 -0
- package/dist/lib/surveyjs-converter.d.ts +120 -0
- package/dist/lib/surveyjs-converter.d.ts.map +1 -0
- package/dist/lib/surveyjs-converter.js +698 -0
- package/package.json +39 -0
|
@@ -0,0 +1,698 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Convert SurveyJS schema to eSheet FormDefinition.
|
|
3
|
+
*
|
|
4
|
+
* SurveyJS is well-known with lots of training data, so AI models
|
|
5
|
+
* generate better SurveyJS output. We leverage this by having AI
|
|
6
|
+
* generate SurveyJS, then converting to eSheet format.
|
|
7
|
+
*/
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// Type Mapping
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
const TYPE_MAP = {
|
|
12
|
+
text: 'text',
|
|
13
|
+
comment: 'longtext',
|
|
14
|
+
radiogroup: 'radio',
|
|
15
|
+
checkbox: 'check',
|
|
16
|
+
dropdown: 'dropdown',
|
|
17
|
+
tagbox: 'multiselectdropdown',
|
|
18
|
+
boolean: 'boolean',
|
|
19
|
+
rating: 'rating',
|
|
20
|
+
ranking: 'ranking',
|
|
21
|
+
matrix: 'singlematrix',
|
|
22
|
+
matrixdropdown: 'multimatrix',
|
|
23
|
+
matrixdynamic: 'multimatrix',
|
|
24
|
+
signaturepad: 'signature',
|
|
25
|
+
image: 'image',
|
|
26
|
+
html: 'html',
|
|
27
|
+
expression: 'html',
|
|
28
|
+
imagepicker: 'radio',
|
|
29
|
+
file: 'text',
|
|
30
|
+
panel: 'section',
|
|
31
|
+
paneldynamic: 'section',
|
|
32
|
+
multipletext: 'multitext',
|
|
33
|
+
};
|
|
34
|
+
const INPUT_TYPE_MAP = {
|
|
35
|
+
// Direct matches
|
|
36
|
+
text: 'string',
|
|
37
|
+
string: 'string',
|
|
38
|
+
number: 'number',
|
|
39
|
+
email: 'email',
|
|
40
|
+
tel: 'tel',
|
|
41
|
+
date: 'date',
|
|
42
|
+
'datetime-local': 'datetime-local',
|
|
43
|
+
month: 'month',
|
|
44
|
+
time: 'time',
|
|
45
|
+
url: 'url',
|
|
46
|
+
// SurveyJS aliases
|
|
47
|
+
color: 'string',
|
|
48
|
+
password: 'string',
|
|
49
|
+
range: 'number',
|
|
50
|
+
week: 'date',
|
|
51
|
+
datetime: 'datetime-local',
|
|
52
|
+
datetimelocal: 'datetime-local',
|
|
53
|
+
};
|
|
54
|
+
function mapInputType(surveyInputType) {
|
|
55
|
+
if (typeof surveyInputType !== 'string')
|
|
56
|
+
return 'string';
|
|
57
|
+
const normalized = surveyInputType.toLowerCase().replace(/[_\s]/g, '');
|
|
58
|
+
return INPUT_TYPE_MAP[normalized] ?? 'string';
|
|
59
|
+
}
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
// Converters
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
function toKebabCase(str) {
|
|
64
|
+
if (typeof str !== 'string')
|
|
65
|
+
return '';
|
|
66
|
+
return str
|
|
67
|
+
.replace(/([a-z])([A-Z])/g, '$1-$2')
|
|
68
|
+
.replace(/[\s_]+/g, '-')
|
|
69
|
+
.toLowerCase();
|
|
70
|
+
}
|
|
71
|
+
function convertChoice(choice, index) {
|
|
72
|
+
if (typeof choice === 'string') {
|
|
73
|
+
return { id: toKebabCase(choice) || `opt-${index}`, value: choice };
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
id: toKebabCase(choice.value) || `opt-${index}`,
|
|
77
|
+
value: choice.value,
|
|
78
|
+
text: choice.text,
|
|
79
|
+
score: choice.score,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
function convertMatrixRow(item, index) {
|
|
83
|
+
if (typeof item === 'string') {
|
|
84
|
+
return { id: toKebabCase(item) || `item-${index}`, value: item };
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
id: toKebabCase(item.value) || `item-${index}`,
|
|
88
|
+
value: item.text ?? item.value,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
function convertMatrixColumn(item, index) {
|
|
92
|
+
if (typeof item === 'string') {
|
|
93
|
+
return { id: toKebabCase(item) || `item-${index}`, value: item };
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
id: toKebabCase(item.value) || `item-${index}`,
|
|
97
|
+
value: item.text ?? item.value,
|
|
98
|
+
score: item.score,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Parse SurveyJS visibleIf expression into eSheet rules.
|
|
103
|
+
*
|
|
104
|
+
* Simple expressions like `{gender} = 'female'` become field conditions.
|
|
105
|
+
* Complex expressions with arithmetic or multiple fields become expression conditions.
|
|
106
|
+
*/
|
|
107
|
+
function parseVisibleIf(expr, effect) {
|
|
108
|
+
if (typeof expr !== 'string')
|
|
109
|
+
return null;
|
|
110
|
+
const trimmed = expr.trim();
|
|
111
|
+
if (!trimmed)
|
|
112
|
+
return null;
|
|
113
|
+
// Count field references - if more than one, it's a complex expression
|
|
114
|
+
const fieldRefs = trimmed.match(/\{[^}]+\}/g) ?? [];
|
|
115
|
+
// Check for arithmetic operators between field refs
|
|
116
|
+
const hasArithmetic = /\{[^}]+\}\s*[+\-*/]\s*\{/.test(trimmed);
|
|
117
|
+
// Simple pattern: {fieldName} op 'value' (single field, no arithmetic)
|
|
118
|
+
if (fieldRefs.length === 1 && !hasArithmetic) {
|
|
119
|
+
// Note: >= and <= must come before > and < in the alternation (regex is first-match)
|
|
120
|
+
const simpleMatch = trimmed.match(/^\{([^}]+)\}\s*(==|!=|<>|>=|<=|=|>|<|empty|notempty)\s*'?([^']*)'?$/i);
|
|
121
|
+
if (simpleMatch) {
|
|
122
|
+
const [, targetId, op, rawExpected] = simpleMatch;
|
|
123
|
+
const expected = rawExpected.trim();
|
|
124
|
+
const operatorMap = {
|
|
125
|
+
'==': 'equals',
|
|
126
|
+
'!=': 'notEquals',
|
|
127
|
+
'<>': 'notEquals',
|
|
128
|
+
'>=': 'greaterThanOrEqual',
|
|
129
|
+
'<=': 'lessThanOrEqual',
|
|
130
|
+
'=': 'equals',
|
|
131
|
+
'>': 'greaterThan',
|
|
132
|
+
'<': 'lessThan',
|
|
133
|
+
empty: 'empty',
|
|
134
|
+
notempty: 'notEmpty',
|
|
135
|
+
};
|
|
136
|
+
const operator = operatorMap[op.toLowerCase()] ?? 'equals';
|
|
137
|
+
// Normalize expected value for boolean comparisons
|
|
138
|
+
// SurveyJS uses true/false, eSheet uses yes/no option IDs
|
|
139
|
+
let normalizedExpected = expected;
|
|
140
|
+
if (operator === 'empty' || operator === 'notEmpty') {
|
|
141
|
+
normalizedExpected = '';
|
|
142
|
+
}
|
|
143
|
+
else if (expected.toLowerCase() === 'true') {
|
|
144
|
+
normalizedExpected = 'yes';
|
|
145
|
+
}
|
|
146
|
+
else if (expected.toLowerCase() === 'false') {
|
|
147
|
+
normalizedExpected = 'no';
|
|
148
|
+
}
|
|
149
|
+
const condition = {
|
|
150
|
+
conditionType: 'field',
|
|
151
|
+
targetId: toKebabCase(targetId),
|
|
152
|
+
operator,
|
|
153
|
+
expected: normalizedExpected,
|
|
154
|
+
};
|
|
155
|
+
return {
|
|
156
|
+
effect,
|
|
157
|
+
logic: 'AND',
|
|
158
|
+
conditions: [condition],
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
// Complex expression: use expression condition type
|
|
163
|
+
// Convert SurveyJS field syntax {fieldName} to eSheet syntax
|
|
164
|
+
const eSheetExpr = trimmed.replace(/\{([^}]+)\}/g, (_, fieldName) => `{${toKebabCase(fieldName)}}`);
|
|
165
|
+
return {
|
|
166
|
+
effect,
|
|
167
|
+
logic: 'AND',
|
|
168
|
+
conditions: [
|
|
169
|
+
{
|
|
170
|
+
conditionType: 'expression',
|
|
171
|
+
expression: eSheetExpr,
|
|
172
|
+
},
|
|
173
|
+
],
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
function convertElement(element, ancestorIds = new Set()) {
|
|
177
|
+
const fieldType = TYPE_MAP[element.type] ?? 'text';
|
|
178
|
+
const id = toKebabCase(element.name);
|
|
179
|
+
// Preserve original SurveyJS metadata for lossless round-trip export.
|
|
180
|
+
const _sourceData = {
|
|
181
|
+
surveyType: element.type,
|
|
182
|
+
surveyName: element.name,
|
|
183
|
+
...(element.visibleIf ? { visibleIf: element.visibleIf } : {}),
|
|
184
|
+
...(element.enableIf ? { enableIf: element.enableIf } : {}),
|
|
185
|
+
...(element.requiredIf ? { requiredIf: element.requiredIf } : {}),
|
|
186
|
+
...(element.placeholder !== undefined
|
|
187
|
+
? { placeholder: element.placeholder }
|
|
188
|
+
: {}),
|
|
189
|
+
...(element.defaultValue !== undefined
|
|
190
|
+
? { defaultValue: element.defaultValue }
|
|
191
|
+
: {}),
|
|
192
|
+
...(element.defaultValueExpression !== undefined
|
|
193
|
+
? { defaultValueExpression: element.defaultValueExpression }
|
|
194
|
+
: {}),
|
|
195
|
+
...(element.min !== undefined ? { min: element.min } : {}),
|
|
196
|
+
...(element.max !== undefined ? { max: element.max } : {}),
|
|
197
|
+
...(element.step !== undefined ? { step: element.step } : {}),
|
|
198
|
+
...(element.rateMin !== undefined ? { rateMin: element.rateMin } : {}),
|
|
199
|
+
...(element.rateMax !== undefined ? { rateMax: element.rateMax } : {}),
|
|
200
|
+
...(element.rateStep !== undefined ? { rateStep: element.rateStep } : {}),
|
|
201
|
+
...(element.minRateDescription !== undefined
|
|
202
|
+
? { minRateDescription: element.minRateDescription }
|
|
203
|
+
: {}),
|
|
204
|
+
...(element.maxRateDescription !== undefined
|
|
205
|
+
? { maxRateDescription: element.maxRateDescription }
|
|
206
|
+
: {}),
|
|
207
|
+
...(element.showOtherItem !== undefined
|
|
208
|
+
? { showOtherItem: element.showOtherItem }
|
|
209
|
+
: {}),
|
|
210
|
+
...(element.otherText !== undefined
|
|
211
|
+
? { otherText: element.otherText }
|
|
212
|
+
: {}),
|
|
213
|
+
...(element.showNoneItem !== undefined
|
|
214
|
+
? { showNoneItem: element.showNoneItem }
|
|
215
|
+
: {}),
|
|
216
|
+
...(element.noneText !== undefined ? { noneText: element.noneText } : {}),
|
|
217
|
+
...(element.showSelectAllItem !== undefined
|
|
218
|
+
? { showSelectAllItem: element.showSelectAllItem }
|
|
219
|
+
: {}),
|
|
220
|
+
...(element.selectAllText !== undefined
|
|
221
|
+
? { selectAllText: element.selectAllText }
|
|
222
|
+
: {}),
|
|
223
|
+
...(element.storeOthersAsComment !== undefined
|
|
224
|
+
? { storeOthersAsComment: element.storeOthersAsComment }
|
|
225
|
+
: {}),
|
|
226
|
+
...(element.colCount !== undefined ? { colCount: element.colCount } : {}),
|
|
227
|
+
...(element.allowMultiple !== undefined
|
|
228
|
+
? { allowMultiple: element.allowMultiple }
|
|
229
|
+
: {}),
|
|
230
|
+
...(element.acceptedTypes !== undefined
|
|
231
|
+
? { acceptedTypes: element.acceptedTypes }
|
|
232
|
+
: {}),
|
|
233
|
+
};
|
|
234
|
+
// Build rules from visibleIf/enableIf/requiredIf
|
|
235
|
+
const rules = [];
|
|
236
|
+
if (element.visibleIf) {
|
|
237
|
+
const rule = parseVisibleIf(element.visibleIf, 'visible');
|
|
238
|
+
if (rule)
|
|
239
|
+
rules.push(rule);
|
|
240
|
+
}
|
|
241
|
+
if (element.enableIf) {
|
|
242
|
+
const rule = parseVisibleIf(element.enableIf, 'enable');
|
|
243
|
+
if (rule)
|
|
244
|
+
rules.push(rule);
|
|
245
|
+
}
|
|
246
|
+
if (element.requiredIf) {
|
|
247
|
+
const rule = parseVisibleIf(element.requiredIf, 'required');
|
|
248
|
+
if (rule)
|
|
249
|
+
rules.push(rule);
|
|
250
|
+
}
|
|
251
|
+
// Base field
|
|
252
|
+
const base = {
|
|
253
|
+
id,
|
|
254
|
+
question: element.title ?? element.name,
|
|
255
|
+
required: element.isRequired ?? false,
|
|
256
|
+
rules,
|
|
257
|
+
_sourceData,
|
|
258
|
+
};
|
|
259
|
+
// Type-specific properties
|
|
260
|
+
switch (fieldType) {
|
|
261
|
+
case 'text':
|
|
262
|
+
return {
|
|
263
|
+
...base,
|
|
264
|
+
fieldType: 'text',
|
|
265
|
+
inputType: mapInputType(element.inputType),
|
|
266
|
+
};
|
|
267
|
+
case 'longtext':
|
|
268
|
+
return {
|
|
269
|
+
...base,
|
|
270
|
+
fieldType: 'longtext',
|
|
271
|
+
inputType: 'string',
|
|
272
|
+
};
|
|
273
|
+
case 'radio':
|
|
274
|
+
case 'dropdown':
|
|
275
|
+
case 'check':
|
|
276
|
+
case 'multiselectdropdown':
|
|
277
|
+
return {
|
|
278
|
+
...base,
|
|
279
|
+
fieldType,
|
|
280
|
+
options: (element.choices ?? []).map(convertChoice),
|
|
281
|
+
};
|
|
282
|
+
case 'boolean':
|
|
283
|
+
return {
|
|
284
|
+
...base,
|
|
285
|
+
fieldType: 'boolean',
|
|
286
|
+
options: [
|
|
287
|
+
{ id: 'yes', value: 'Yes' },
|
|
288
|
+
{ id: 'no', value: 'No' },
|
|
289
|
+
],
|
|
290
|
+
};
|
|
291
|
+
case 'rating': {
|
|
292
|
+
let options;
|
|
293
|
+
if (element.choices && element.choices.length > 0) {
|
|
294
|
+
options = element.choices.map(convertChoice);
|
|
295
|
+
}
|
|
296
|
+
else {
|
|
297
|
+
// Generate from rateMin/rateMax/rateStep
|
|
298
|
+
const min = typeof element.rateMin === 'number' ? element.rateMin : 1;
|
|
299
|
+
const max = typeof element.rateMax === 'number' ? element.rateMax : 5;
|
|
300
|
+
const step = typeof element.rateStep === 'number' && element.rateStep > 0
|
|
301
|
+
? element.rateStep
|
|
302
|
+
: 1;
|
|
303
|
+
options = [];
|
|
304
|
+
for (let n = min; n <= max; n += step) {
|
|
305
|
+
options.push({ id: String(n), value: String(n) });
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
return { ...base, fieldType: 'rating', options };
|
|
309
|
+
}
|
|
310
|
+
case 'ranking':
|
|
311
|
+
return {
|
|
312
|
+
...base,
|
|
313
|
+
fieldType: 'ranking',
|
|
314
|
+
options: (element.choices ?? []).map(convertChoice),
|
|
315
|
+
};
|
|
316
|
+
case 'singlematrix':
|
|
317
|
+
case 'multimatrix': {
|
|
318
|
+
const rows = (element.rows ?? []).map(convertMatrixRow);
|
|
319
|
+
const columns = (element.columns ?? []).map(convertMatrixColumn);
|
|
320
|
+
// Check if any column has an explicit score
|
|
321
|
+
const hasScores = columns.some((col) => col.score !== undefined);
|
|
322
|
+
return {
|
|
323
|
+
...base,
|
|
324
|
+
fieldType,
|
|
325
|
+
rows,
|
|
326
|
+
columns,
|
|
327
|
+
// Enable scored mode if any column has scores
|
|
328
|
+
...(hasScores ? { scored: true } : {}),
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
case 'signature':
|
|
332
|
+
return {
|
|
333
|
+
...base,
|
|
334
|
+
fieldType: 'signature',
|
|
335
|
+
};
|
|
336
|
+
case 'image':
|
|
337
|
+
return {
|
|
338
|
+
...base,
|
|
339
|
+
fieldType: 'image',
|
|
340
|
+
imageUri: element.imageLink ?? '',
|
|
341
|
+
altText: element.altText,
|
|
342
|
+
};
|
|
343
|
+
case 'html':
|
|
344
|
+
return {
|
|
345
|
+
...base,
|
|
346
|
+
fieldType: 'html',
|
|
347
|
+
// 'expression' type: render the expression string as HTML placeholder
|
|
348
|
+
htmlContent: element.type === 'expression'
|
|
349
|
+
? element.expression ?? ''
|
|
350
|
+
: element.html ?? '',
|
|
351
|
+
};
|
|
352
|
+
case 'multitext':
|
|
353
|
+
return {
|
|
354
|
+
...base,
|
|
355
|
+
fieldType: 'multitext',
|
|
356
|
+
options: (element.items ?? []).map((item, idx) => ({
|
|
357
|
+
id: toKebabCase(item.name) || `item-${idx}`,
|
|
358
|
+
value: item.name,
|
|
359
|
+
text: item.title ?? item.name,
|
|
360
|
+
})),
|
|
361
|
+
};
|
|
362
|
+
case 'section': {
|
|
363
|
+
// Guard against circular nesting (AI can generate panels referencing ancestor IDs)
|
|
364
|
+
if (ancestorIds.has(id)) {
|
|
365
|
+
return { ...base, fieldType: 'text', inputType: 'string' };
|
|
366
|
+
}
|
|
367
|
+
const childAncestors = new Set(ancestorIds).add(id);
|
|
368
|
+
return {
|
|
369
|
+
...base,
|
|
370
|
+
fieldType: 'section',
|
|
371
|
+
title: element.title ?? element.name,
|
|
372
|
+
fields: (element.elements ?? []).map((el) => convertElement(el, childAncestors)),
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
default:
|
|
376
|
+
return {
|
|
377
|
+
...base,
|
|
378
|
+
fieldType: 'text',
|
|
379
|
+
inputType: 'string',
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Convert a SurveyJS schema to eSheet FormDefinition.
|
|
385
|
+
*/
|
|
386
|
+
export function convertSurveyJSToESheet(survey) {
|
|
387
|
+
const fields = [];
|
|
388
|
+
// Collect survey-level metadata for round-trip fidelity
|
|
389
|
+
const surveyMeta = {};
|
|
390
|
+
if (survey.locale !== undefined)
|
|
391
|
+
surveyMeta.locale = survey.locale;
|
|
392
|
+
if (survey.logo !== undefined)
|
|
393
|
+
surveyMeta.logo = survey.logo;
|
|
394
|
+
if (survey.logoPosition !== undefined)
|
|
395
|
+
surveyMeta.logoPosition = survey.logoPosition;
|
|
396
|
+
if (survey.showProgressBar !== undefined)
|
|
397
|
+
surveyMeta.showProgressBar = survey.showProgressBar;
|
|
398
|
+
if (survey.progressBarType !== undefined)
|
|
399
|
+
surveyMeta.progressBarType = survey.progressBarType;
|
|
400
|
+
if (survey.completedHtml !== undefined)
|
|
401
|
+
surveyMeta.completedHtml = survey.completedHtml;
|
|
402
|
+
if (survey.showQuestionNumbers !== undefined)
|
|
403
|
+
surveyMeta.showQuestionNumbers = survey.showQuestionNumbers;
|
|
404
|
+
if (survey.questionTitleLocation !== undefined)
|
|
405
|
+
surveyMeta.questionTitleLocation = survey.questionTitleLocation;
|
|
406
|
+
if (survey.calculatedValues !== undefined)
|
|
407
|
+
surveyMeta.calculatedValues = survey.calculatedValues;
|
|
408
|
+
if (survey.triggers !== undefined)
|
|
409
|
+
surveyMeta.triggers = survey.triggers;
|
|
410
|
+
const hasSurveyMeta = Object.keys(surveyMeta).length > 0;
|
|
411
|
+
// Handle pages (convert each page to a section)
|
|
412
|
+
if (survey.pages && survey.pages.length > 0) {
|
|
413
|
+
for (const page of survey.pages) {
|
|
414
|
+
if (!page.elements || page.elements.length === 0)
|
|
415
|
+
continue;
|
|
416
|
+
// If only one page, flatten elements directly
|
|
417
|
+
if (survey.pages.length === 1) {
|
|
418
|
+
fields.push(...page.elements.map((el) => convertElement(el)));
|
|
419
|
+
}
|
|
420
|
+
else {
|
|
421
|
+
// Multiple pages become sections
|
|
422
|
+
const sectionId = toKebabCase(page.name ?? page.title ?? 'page');
|
|
423
|
+
const sectionAncestors = new Set([sectionId]);
|
|
424
|
+
fields.push({
|
|
425
|
+
id: sectionId,
|
|
426
|
+
fieldType: 'section',
|
|
427
|
+
question: '',
|
|
428
|
+
required: false,
|
|
429
|
+
rules: [],
|
|
430
|
+
title: page.title ?? page.name ?? 'Section',
|
|
431
|
+
fields: page.elements.map((el) => convertElement(el, sectionAncestors)),
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
// Handle top-level elements (no pages)
|
|
437
|
+
if (survey.elements) {
|
|
438
|
+
fields.push(...survey.elements.map((el) => convertElement(el)));
|
|
439
|
+
}
|
|
440
|
+
return {
|
|
441
|
+
id: toKebabCase(survey.title ?? 'form'),
|
|
442
|
+
title: survey.title ?? 'Untitled Form',
|
|
443
|
+
description: survey.description,
|
|
444
|
+
fields,
|
|
445
|
+
...(hasSurveyMeta ? { _sourceData: { surveyMeta } } : {}),
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
/**
|
|
449
|
+
* System prompt for generating SurveyJS format.
|
|
450
|
+
* AI models have better training data for SurveyJS.
|
|
451
|
+
*/
|
|
452
|
+
export const SURVEYJS_SYSTEM_PROMPT = `You are an expert form designer. Output ONLY a valid SurveyJS JSON schema — no markdown, no explanation, no code blocks, raw JSON only. STRUCTURE: {"title":"...","pages":[{"name":"page1","title":"...","elements":[...]}]} FIELD TYPES — always pick most specific: short text→text (add inputType:"email"/"tel"/"date"/"number"/"url"), long text→comment, single choice→radiogroup (NOT text), multiple choice→checkbox (NOT text), long list single→dropdown, long list multi→tagbox, yes/no→boolean (NOT radiogroup), 1-N scale→rating (set rateMin/rateMax/minRateDescription/maxRateDescription), ordered preference→ranking, grid single answer→matrix (rows=items columns=scale labels), grid multi answer→matrixdropdown, signature→signaturepad (NEVER use text), file upload→file, image pick→imagepicker, multiple short inputs→multipletext. RULES: use pages only to group related fields — never use panel type; every page must have a title; add "isRequired":true on important fields; use "visibleIf" for follow-up questions: "{field} = 'value'"; choices format: [{"value":"id","text":"Label"}]. For scored surveys: use numeric strings as column values ("0","1","2","3"), add calculatedValues at top level. OUTPUT: raw JSON only, no prose.`;
|
|
453
|
+
// ---------------------------------------------------------------------------
|
|
454
|
+
// Export: FormDefinition → SurveyJS schema
|
|
455
|
+
// ---------------------------------------------------------------------------
|
|
456
|
+
const FIELD_TYPE_TO_SURVEY = {
|
|
457
|
+
text: 'text',
|
|
458
|
+
longtext: 'comment',
|
|
459
|
+
radio: 'radiogroup',
|
|
460
|
+
check: 'checkbox',
|
|
461
|
+
dropdown: 'dropdown',
|
|
462
|
+
multiselectdropdown: 'tagbox',
|
|
463
|
+
boolean: 'boolean',
|
|
464
|
+
rating: 'rating',
|
|
465
|
+
ranking: 'ranking',
|
|
466
|
+
singlematrix: 'matrix',
|
|
467
|
+
multimatrix: 'matrixdropdown',
|
|
468
|
+
signature: 'signaturepad',
|
|
469
|
+
image: 'image',
|
|
470
|
+
html: 'html',
|
|
471
|
+
multitext: 'multipletext',
|
|
472
|
+
section: 'panel',
|
|
473
|
+
};
|
|
474
|
+
const ESHEET_INPUT_TO_SURVEY = {
|
|
475
|
+
string: 'text',
|
|
476
|
+
number: 'number',
|
|
477
|
+
email: 'email',
|
|
478
|
+
tel: 'tel',
|
|
479
|
+
date: 'date',
|
|
480
|
+
'datetime-local': 'datetime-local',
|
|
481
|
+
month: 'month',
|
|
482
|
+
time: 'time',
|
|
483
|
+
url: 'url',
|
|
484
|
+
};
|
|
485
|
+
const OPERATOR_TO_SURVEY = {
|
|
486
|
+
equals: '=',
|
|
487
|
+
notEquals: '<>',
|
|
488
|
+
greaterThan: '>',
|
|
489
|
+
lessThan: '<',
|
|
490
|
+
greaterThanOrEqual: '>=',
|
|
491
|
+
lessThanOrEqual: '<=',
|
|
492
|
+
empty: 'empty',
|
|
493
|
+
notEmpty: 'notempty',
|
|
494
|
+
};
|
|
495
|
+
function conditionToSurveyExpr(condition) {
|
|
496
|
+
if (condition.conditionType === 'expression') {
|
|
497
|
+
return condition.expression ?? '';
|
|
498
|
+
}
|
|
499
|
+
const op = OPERATOR_TO_SURVEY[condition.operator ?? 'equals'] ?? '=';
|
|
500
|
+
if (op === 'empty' || op === 'notempty') {
|
|
501
|
+
return `{${condition.targetId}} ${op}`;
|
|
502
|
+
}
|
|
503
|
+
return `{${condition.targetId}} ${op} '${condition.expected}'`;
|
|
504
|
+
}
|
|
505
|
+
function ruleToSurveyExpr(rule) {
|
|
506
|
+
const sep = rule.logic === 'OR' ? ' or ' : ' and ';
|
|
507
|
+
return rule.conditions.map(conditionToSurveyExpr).join(sep);
|
|
508
|
+
}
|
|
509
|
+
function fieldToSurveyElement(field) {
|
|
510
|
+
const meta = field._sourceData;
|
|
511
|
+
// Restore original SurveyJS name if available; otherwise convert id back
|
|
512
|
+
const name = meta?.surveyName ?? field.id;
|
|
513
|
+
// Restore original SurveyJS type if available; otherwise use reverse map
|
|
514
|
+
const type = meta?.surveyType ?? FIELD_TYPE_TO_SURVEY[field.fieldType] ?? 'text';
|
|
515
|
+
const el = { type, name };
|
|
516
|
+
if (field.question !== undefined)
|
|
517
|
+
el.title = field.question;
|
|
518
|
+
if (field.required)
|
|
519
|
+
el.isRequired = true;
|
|
520
|
+
// Restore conditional expressions verbatim from _sourceData for lossless round-trip.
|
|
521
|
+
// Fall back to re-constructing from parsed rules.
|
|
522
|
+
const visibleRule = field.rules?.find((r) => r.effect === 'visible');
|
|
523
|
+
const enableRule = field.rules?.find((r) => r.effect === 'enable');
|
|
524
|
+
const requiredRule = field.rules?.find((r) => r.effect === 'required');
|
|
525
|
+
if (meta?.visibleIf)
|
|
526
|
+
el.visibleIf = meta.visibleIf;
|
|
527
|
+
else if (visibleRule)
|
|
528
|
+
el.visibleIf = ruleToSurveyExpr(visibleRule);
|
|
529
|
+
if (meta?.enableIf)
|
|
530
|
+
el.enableIf = meta.enableIf;
|
|
531
|
+
else if (enableRule)
|
|
532
|
+
el.enableIf = ruleToSurveyExpr(enableRule);
|
|
533
|
+
if (meta?.requiredIf)
|
|
534
|
+
el.requiredIf = meta.requiredIf;
|
|
535
|
+
else if (requiredRule)
|
|
536
|
+
el.requiredIf = ruleToSurveyExpr(requiredRule);
|
|
537
|
+
// Restore extra element props from _sourceData for round-trip fidelity
|
|
538
|
+
if (meta?.placeholder !== undefined)
|
|
539
|
+
el.placeholder = meta.placeholder;
|
|
540
|
+
if (meta?.defaultValue !== undefined)
|
|
541
|
+
el.defaultValue = meta.defaultValue;
|
|
542
|
+
if (meta?.defaultValueExpression !== undefined)
|
|
543
|
+
el.defaultValueExpression = meta.defaultValueExpression;
|
|
544
|
+
if (meta?.min !== undefined)
|
|
545
|
+
el.min = meta.min;
|
|
546
|
+
if (meta?.max !== undefined)
|
|
547
|
+
el.max = meta.max;
|
|
548
|
+
if (meta?.step !== undefined)
|
|
549
|
+
el.step = meta.step;
|
|
550
|
+
if (meta?.rateMin !== undefined)
|
|
551
|
+
el.rateMin = meta.rateMin;
|
|
552
|
+
if (meta?.rateMax !== undefined)
|
|
553
|
+
el.rateMax = meta.rateMax;
|
|
554
|
+
if (meta?.rateStep !== undefined)
|
|
555
|
+
el.rateStep = meta.rateStep;
|
|
556
|
+
if (meta?.minRateDescription !== undefined)
|
|
557
|
+
el.minRateDescription = meta.minRateDescription;
|
|
558
|
+
if (meta?.maxRateDescription !== undefined)
|
|
559
|
+
el.maxRateDescription = meta.maxRateDescription;
|
|
560
|
+
if (meta?.showOtherItem !== undefined)
|
|
561
|
+
el.showOtherItem = meta.showOtherItem;
|
|
562
|
+
if (meta?.otherText !== undefined)
|
|
563
|
+
el.otherText = meta.otherText;
|
|
564
|
+
if (meta?.showNoneItem !== undefined)
|
|
565
|
+
el.showNoneItem = meta.showNoneItem;
|
|
566
|
+
if (meta?.noneText !== undefined)
|
|
567
|
+
el.noneText = meta.noneText;
|
|
568
|
+
if (meta?.showSelectAllItem !== undefined)
|
|
569
|
+
el.showSelectAllItem = meta.showSelectAllItem;
|
|
570
|
+
if (meta?.selectAllText !== undefined)
|
|
571
|
+
el.selectAllText = meta.selectAllText;
|
|
572
|
+
if (meta?.storeOthersAsComment !== undefined)
|
|
573
|
+
el.storeOthersAsComment = meta.storeOthersAsComment;
|
|
574
|
+
if (meta?.colCount !== undefined)
|
|
575
|
+
el.colCount = meta.colCount;
|
|
576
|
+
if (meta?.allowMultiple !== undefined)
|
|
577
|
+
el.allowMultiple = meta.allowMultiple;
|
|
578
|
+
if (meta?.acceptedTypes !== undefined)
|
|
579
|
+
el.acceptedTypes = meta.acceptedTypes;
|
|
580
|
+
// Type-specific properties
|
|
581
|
+
switch (field.fieldType) {
|
|
582
|
+
case 'text':
|
|
583
|
+
case 'longtext': {
|
|
584
|
+
const inputType = field.inputType;
|
|
585
|
+
if (inputType && inputType !== 'string') {
|
|
586
|
+
el.inputType = ESHEET_INPUT_TO_SURVEY[inputType] ?? inputType;
|
|
587
|
+
}
|
|
588
|
+
break;
|
|
589
|
+
}
|
|
590
|
+
case 'radio':
|
|
591
|
+
case 'check':
|
|
592
|
+
case 'dropdown':
|
|
593
|
+
case 'multiselectdropdown':
|
|
594
|
+
case 'ranking':
|
|
595
|
+
case 'rating':
|
|
596
|
+
el.choices = (field.options ?? []).map((o) => o.text !== undefined
|
|
597
|
+
? {
|
|
598
|
+
value: o.value,
|
|
599
|
+
text: o.text,
|
|
600
|
+
...(o.score !== undefined ? { score: o.score } : {}),
|
|
601
|
+
}
|
|
602
|
+
: o.value);
|
|
603
|
+
break;
|
|
604
|
+
case 'singlematrix':
|
|
605
|
+
case 'multimatrix':
|
|
606
|
+
el.rows = (field.rows ?? []).map((r) => ({ value: r.id, text: r.value }));
|
|
607
|
+
el.columns = (field.columns ?? []).map((c) => ({
|
|
608
|
+
value: c.id,
|
|
609
|
+
text: c.value,
|
|
610
|
+
...(c.score !== undefined ? { score: c.score } : {}),
|
|
611
|
+
}));
|
|
612
|
+
break;
|
|
613
|
+
case 'image':
|
|
614
|
+
if (field.imageUri)
|
|
615
|
+
el.imageLink = field.imageUri;
|
|
616
|
+
if (field.altText)
|
|
617
|
+
el.altText = field.altText;
|
|
618
|
+
break;
|
|
619
|
+
case 'html':
|
|
620
|
+
// expression type: restore expression property instead of html
|
|
621
|
+
if (type === 'expression') {
|
|
622
|
+
el.expression = field.htmlContent ?? '';
|
|
623
|
+
}
|
|
624
|
+
else {
|
|
625
|
+
el.html = field.htmlContent ?? '';
|
|
626
|
+
}
|
|
627
|
+
break;
|
|
628
|
+
case 'multitext':
|
|
629
|
+
el.items = (field.options ?? []).map((o) => ({
|
|
630
|
+
name: o.value,
|
|
631
|
+
title: o.text ?? o.value,
|
|
632
|
+
}));
|
|
633
|
+
break;
|
|
634
|
+
case 'section':
|
|
635
|
+
el.elements = (field.fields ?? []).map(fieldToSurveyElement);
|
|
636
|
+
break;
|
|
637
|
+
}
|
|
638
|
+
return el;
|
|
639
|
+
}
|
|
640
|
+
/**
|
|
641
|
+
* Convert an eSheet FormDefinition back to a SurveyJS schema.
|
|
642
|
+
*
|
|
643
|
+
* Preserves original SurveyJS element names, types, and conditional expressions
|
|
644
|
+
* stored in `_sourceData` during import, enabling a lossless round-trip.
|
|
645
|
+
* Fields without `_sourceData` are reverse-mapped from eSheet types.
|
|
646
|
+
*
|
|
647
|
+
* Section fields become pages; non-section fields are placed in a single page.
|
|
648
|
+
*/
|
|
649
|
+
export function exportToSurveyJS(form) {
|
|
650
|
+
// Restore survey-level metadata preserved during import
|
|
651
|
+
const surveyMeta = form._sourceData
|
|
652
|
+
?.surveyMeta ?? {};
|
|
653
|
+
const hasSections = form.fields.some((f) => f.fieldType === 'section');
|
|
654
|
+
if (hasSections) {
|
|
655
|
+
// Each top-level section becomes a page; non-section fields go into a leading page.
|
|
656
|
+
const pages = [];
|
|
657
|
+
const topLevelFields = form.fields.filter((f) => f.fieldType !== 'section');
|
|
658
|
+
if (topLevelFields.length > 0) {
|
|
659
|
+
pages.push({
|
|
660
|
+
name: 'page1',
|
|
661
|
+
elements: topLevelFields.map(fieldToSurveyElement),
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
for (const field of form.fields) {
|
|
665
|
+
if (field.fieldType !== 'section')
|
|
666
|
+
continue;
|
|
667
|
+
const meta = field._sourceData;
|
|
668
|
+
pages.push({
|
|
669
|
+
name: meta?.surveyName ?? field.id,
|
|
670
|
+
title: field.title ?? field.question,
|
|
671
|
+
elements: (field.fields ?? []).map(fieldToSurveyElement),
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
return {
|
|
675
|
+
title: form.title,
|
|
676
|
+
...(form.description ? { description: form.description } : {}),
|
|
677
|
+
...surveyMeta,
|
|
678
|
+
pages,
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
// No sections — single page
|
|
682
|
+
return {
|
|
683
|
+
title: form.title,
|
|
684
|
+
...(form.description ? { description: form.description } : {}),
|
|
685
|
+
...surveyMeta,
|
|
686
|
+
pages: [{ name: 'page1', elements: form.fields.map(fieldToSurveyElement) }],
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
/**
|
|
690
|
+
* Convert a SurveyJS schema to eSheet FormDefinition.
|
|
691
|
+
* Primary named export matching the importFromMcp / exportToMcp naming convention.
|
|
692
|
+
*/
|
|
693
|
+
export const importFromSurveyJS = convertSurveyJSToESheet;
|
|
694
|
+
/**
|
|
695
|
+
* Convert a SurveyJS schema to eSheet FormDefinition.
|
|
696
|
+
* Alias for importFromSurveyJS / convertSurveyJSToESheet.
|
|
697
|
+
*/
|
|
698
|
+
export const convertSurveyJS = convertSurveyJSToESheet;
|