@marktiderman/genesis-forms 0.2.0 → 0.3.1
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 +113 -13
- package/conformance/{after-class-back-after-a-jump.scenario.json → event-feedback-back-after-a-jump.scenario.json} +21 -21
- package/conformance/{after-class-contact-yes-completed.scenario.json → event-feedback-contact-yes-completed.scenario.json} +21 -21
- package/conformance/{after-class-contact-yes-requires-name-and-email.scenario.json → event-feedback-contact-yes-requires-name-and-email.scenario.json} +19 -19
- package/conformance/{after-class-happy-path.scenario.json → event-feedback-happy-path.scenario.json} +24 -24
- package/conformance/{after-class-invalid-email-rejected.scenario.json → event-feedback-invalid-email-rejected.scenario.json} +19 -19
- package/conformance/{after-class-low-rating-skips-went-well.scenario.json → event-feedback-low-rating-skips-went-well.scenario.json} +21 -21
- package/conformance/{after-class-server-drops-unknown-and-hidden-answers.scenario.json → event-feedback-server-drops-unknown-and-hidden-answers.scenario.json} +22 -22
- package/conformance/{after-class-server-ignores-a-client-sent-coach.scenario.json → event-feedback-server-ignores-a-client-sent-owner.scenario.json} +24 -24
- package/conformance/{monthly-pulse-default-ending.scenario.json → monthly-check-in-default-ending.scenario.json} +6 -6
- package/conformance/{monthly-pulse-promoter-ending-via-jump.scenario.json → monthly-check-in-promoter-ending-via-jump.scenario.json} +6 -6
- package/dist/destinations.cjs +1235 -0
- package/dist/destinations.cjs.map +1 -0
- package/dist/destinations.d.cts +465 -0
- package/dist/destinations.d.ts +465 -0
- package/dist/destinations.js +1193 -0
- package/dist/destinations.js.map +1 -0
- package/dist/{engine-Coc-gQIB.d.ts → engine-CPLtKVew.d.cts} +2 -1
- package/dist/{engine-Bj8-mMVb.d.cts → engine-DOrPgFDw.d.ts} +2 -1
- package/dist/index.cjs +322 -24
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +102 -5
- package/dist/index.d.ts +102 -5
- package/dist/index.js +311 -25
- package/dist/index.js.map +1 -1
- package/dist/server.cjs +249 -22
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +3 -2
- package/dist/server.d.ts +3 -2
- package/dist/server.js +249 -22
- package/dist/server.js.map +1 -1
- package/dist/testing.cjs +253 -22
- package/dist/testing.cjs.map +1 -1
- package/dist/testing.d.cts +4 -3
- package/dist/testing.d.ts +4 -3
- package/dist/testing.js +253 -22
- package/dist/testing.js.map +1 -1
- package/dist/{validate-BbackAjB.d.cts → types-BBKBQ8sp.d.cts} +2 -29
- package/dist/{validate-BbackAjB.d.ts → types-BBKBQ8sp.d.ts} +2 -29
- package/dist/validate-BXXqi0fq.d.cts +30 -0
- package/dist/validate-BjNwF1UE.d.ts +30 -0
- package/package.json +14 -2
package/dist/testing.cjs
CHANGED
|
@@ -1,5 +1,187 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
// src/structured.ts
|
|
4
|
+
var isObject = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
5
|
+
function intProp(field, name, fallback, floor = 1) {
|
|
6
|
+
const v = field.props?.[name];
|
|
7
|
+
return typeof v === "number" && Number.isInteger(v) && v >= floor ? v : fallback;
|
|
8
|
+
}
|
|
9
|
+
function optionalIntProp(field, name, floor = 0) {
|
|
10
|
+
const v = field.props?.[name];
|
|
11
|
+
return typeof v === "number" && Number.isInteger(v) && v >= floor ? v : void 0;
|
|
12
|
+
}
|
|
13
|
+
var blank = (v) => typeof v !== "string" || v.trim() === "";
|
|
14
|
+
var blankText = (v) => typeof v === "string" && v.trim() === "";
|
|
15
|
+
var emptyList = (value, isBlank) => value === void 0 || value === null || Array.isArray(value) && value.every(isBlank);
|
|
16
|
+
var plural = (n, one, many = `${one}s`) => `${n} ${n === 1 ? one : many}`;
|
|
17
|
+
function listLimits(field) {
|
|
18
|
+
return { min: optionalIntProp(field, "min") ?? 0, max: intProp(field, "max", 10), itemMaxLength: intProp(field, "itemMaxLength", 500) };
|
|
19
|
+
}
|
|
20
|
+
var listItems = (value) => Array.isArray(value) ? value.filter((v) => typeof v === "string" && v.trim() !== "") : [];
|
|
21
|
+
var MAX_TABLE_COLUMNS = 6;
|
|
22
|
+
var MAX_TABLE_CELL_LENGTH = 2e3;
|
|
23
|
+
function tableColumns(field) {
|
|
24
|
+
const raw = field.props?.columns;
|
|
25
|
+
if (!Array.isArray(raw)) return [];
|
|
26
|
+
const seen = /* @__PURE__ */ new Set();
|
|
27
|
+
const out = [];
|
|
28
|
+
for (const c of raw) {
|
|
29
|
+
if (!isObject(c) || typeof c.key !== "string" || !c.key || seen.has(c.key)) continue;
|
|
30
|
+
seen.add(c.key);
|
|
31
|
+
const column = { key: c.key, label: typeof c.label === "string" ? c.label : c.key };
|
|
32
|
+
if (c.long === true) column.long = true;
|
|
33
|
+
out.push(column);
|
|
34
|
+
}
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
function tableLimits(field) {
|
|
38
|
+
return { minRows: optionalIntProp(field, "minRows") ?? 0, maxRows: intProp(field, "maxRows", 3) };
|
|
39
|
+
}
|
|
40
|
+
var filledRows = (value) => Array.isArray(value) ? value.filter((r) => isObject(r) && Object.values(r).some((c) => !blank(c))) : [];
|
|
41
|
+
var MAX_TIMESTAMP_SECONDS = 86400;
|
|
42
|
+
function formatTimestamp(seconds) {
|
|
43
|
+
const total = Number.isFinite(seconds) && seconds > 0 ? Math.floor(seconds) : 0;
|
|
44
|
+
const h = Math.floor(total / 3600);
|
|
45
|
+
const m = Math.floor(total % 3600 / 60);
|
|
46
|
+
const s = String(total % 60).padStart(2, "0");
|
|
47
|
+
return h ? `${h}:${String(m).padStart(2, "0")}:${s}` : `${m}:${s}`;
|
|
48
|
+
}
|
|
49
|
+
var isTimestamp = (v) => typeof v === "number" && Number.isFinite(v) && v >= 0 && v <= MAX_TIMESTAMP_SECONDS;
|
|
50
|
+
var notesWithText = (value) => Array.isArray(value) ? value.filter((n) => isObject(n) && !blank(n.text)) : [];
|
|
51
|
+
var MAX_HIGHLIGHT_NOTE_LENGTH = 1e3;
|
|
52
|
+
var MAX_HIGHLIGHT_QUOTE_LENGTH = 2e3;
|
|
53
|
+
var HIGHLIGHT_NOTE_MESSAGE = "Add a note to each highlight.";
|
|
54
|
+
var sourceText = (field) => typeof field.props?.text === "string" && field.props.text ? field.props.text : void 0;
|
|
55
|
+
var structuredFieldTypes = [
|
|
56
|
+
{
|
|
57
|
+
type: "list",
|
|
58
|
+
label: "List",
|
|
59
|
+
category: "text",
|
|
60
|
+
value: { json: true },
|
|
61
|
+
// logic.ts `contains` on an array is exact item equality, same as multi-choice, so it is offered.
|
|
62
|
+
operators: ["contains", "answered", "empty"],
|
|
63
|
+
a11y: { semantics: "group" },
|
|
64
|
+
isEmpty: (value) => emptyList(value, blankText),
|
|
65
|
+
validate: (value, field) => {
|
|
66
|
+
if (!Array.isArray(value) || !value.every((v) => typeof v === "string")) return "Enter a list of answers.";
|
|
67
|
+
const { min, max, itemMaxLength } = listLimits(field);
|
|
68
|
+
if (value.some((v) => v.trim().length > itemMaxLength)) return `Keep each item under ${itemMaxLength} characters.`;
|
|
69
|
+
const count = listItems(value).length;
|
|
70
|
+
if (count < min) return `Add at least ${plural(min, "item")}.`;
|
|
71
|
+
if (count > max) return `Add at most ${plural(max, "item")}.`;
|
|
72
|
+
return null;
|
|
73
|
+
},
|
|
74
|
+
normalize: (value) => Array.isArray(value) ? listItems(value).map((v) => v.trim()) : value,
|
|
75
|
+
summarize: (value) => listItems(value).map((v) => v.trim()).join("; "),
|
|
76
|
+
samples: (field) => {
|
|
77
|
+
const { min, max } = listLimits(field);
|
|
78
|
+
return [Array.from({ length: Math.min(Math.max(1, min), max) }, (_, i) => `Item ${i + 1}`)];
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
type: "table",
|
|
83
|
+
label: "Table",
|
|
84
|
+
category: "advanced",
|
|
85
|
+
value: { json: true },
|
|
86
|
+
operators: ["answered", "empty"],
|
|
87
|
+
a11y: { semantics: "group" },
|
|
88
|
+
isEmpty: (value) => emptyList(value, (r) => isObject(r) && Object.values(r).every(blankText)),
|
|
89
|
+
validate: (value, field) => {
|
|
90
|
+
const columns = tableColumns(field);
|
|
91
|
+
if (!columns.length) return "This question has no columns.";
|
|
92
|
+
if (!Array.isArray(value) || !value.every(isObject)) return "Fill in the rows.";
|
|
93
|
+
const keys = new Set(columns.map((c) => c.key));
|
|
94
|
+
for (const row of value) {
|
|
95
|
+
for (const [key, cell] of Object.entries(row)) {
|
|
96
|
+
if (!keys.has(key)) return "An answer names a column this question doesn't have.";
|
|
97
|
+
if (typeof cell !== "string") return "Fill in the rows with text.";
|
|
98
|
+
if (cell.trim().length > MAX_TABLE_CELL_LENGTH) return `Keep each answer under ${MAX_TABLE_CELL_LENGTH} characters.`;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const { minRows, maxRows } = tableLimits(field);
|
|
102
|
+
const count = filledRows(value).length;
|
|
103
|
+
if (count < minRows) return `Fill in at least ${plural(minRows, "row")}.`;
|
|
104
|
+
if (count > maxRows) return `Fill in at most ${plural(maxRows, "row")}.`;
|
|
105
|
+
return null;
|
|
106
|
+
},
|
|
107
|
+
normalize: (value) => Array.isArray(value) ? filledRows(value).map((row) => {
|
|
108
|
+
const out = {};
|
|
109
|
+
for (const [key, cell] of Object.entries(row)) if (typeof cell === "string") out[key] = cell.trim();
|
|
110
|
+
return out;
|
|
111
|
+
}) : value,
|
|
112
|
+
summarize: (value, field) => {
|
|
113
|
+
const columns = tableColumns(field);
|
|
114
|
+
return filledRows(value).map((row) => columns.filter((c) => !blank(row[c.key])).map((c) => `${c.label}: ${row[c.key].trim()}`).join(" \xB7 ")).join(" / ");
|
|
115
|
+
},
|
|
116
|
+
samples: (field) => {
|
|
117
|
+
const columns = tableColumns(field);
|
|
118
|
+
if (!columns.length) return [];
|
|
119
|
+
const { minRows, maxRows } = tableLimits(field);
|
|
120
|
+
const row = Object.fromEntries(columns.map((c) => [c.key, "sample"]));
|
|
121
|
+
return [Array.from({ length: Math.min(Math.max(1, minRows), maxRows) }, () => ({ ...row }))];
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
type: "media-notes",
|
|
126
|
+
label: "Notes at moments",
|
|
127
|
+
category: "media",
|
|
128
|
+
value: { json: true },
|
|
129
|
+
operators: ["answered", "empty"],
|
|
130
|
+
a11y: { semantics: "group" },
|
|
131
|
+
isEmpty: (value) => emptyList(value, (n) => isObject(n) && blankText(n.text)),
|
|
132
|
+
validate: (value, field) => {
|
|
133
|
+
if (!Array.isArray(value)) return "Add notes.";
|
|
134
|
+
const textMaxLength = intProp(field, "textMaxLength", 1e3);
|
|
135
|
+
for (const note of value) {
|
|
136
|
+
if (!isObject(note) || typeof note.text !== "string") return "Add notes.";
|
|
137
|
+
if (!isTimestamp(note.at)) return "Each note needs a time.";
|
|
138
|
+
if (note.text.trim().length > textMaxLength) return `Keep each note under ${textMaxLength} characters.`;
|
|
139
|
+
}
|
|
140
|
+
const maxNotes = intProp(field, "maxNotes", 20);
|
|
141
|
+
return notesWithText(value).length > maxNotes ? `Add at most ${plural(maxNotes, "note")}.` : null;
|
|
142
|
+
},
|
|
143
|
+
normalize: (value) => Array.isArray(value) ? notesWithText(value).map((n) => ({ at: n.at, text: n.text.trim() })).sort((a, b) => a.at - b.at) : value,
|
|
144
|
+
summarize: (value) => notesWithText(value).map((n) => ({ at: typeof n.at === "number" ? n.at : 0, text: n.text.trim() })).sort((a, b) => a.at - b.at).map((n) => `${formatTimestamp(n.at)} ${n.text}`).join("; "),
|
|
145
|
+
samples: () => [[{ at: 0, text: "sample" }]]
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
type: "text-highlights",
|
|
149
|
+
label: "Highlights",
|
|
150
|
+
category: "media",
|
|
151
|
+
value: { json: true },
|
|
152
|
+
operators: ["answered", "empty"],
|
|
153
|
+
a11y: { semantics: "group" },
|
|
154
|
+
isEmpty: (value) => emptyList(value, () => false),
|
|
155
|
+
validate: (value, field) => {
|
|
156
|
+
if (!Array.isArray(value)) return "Highlight a passage.";
|
|
157
|
+
const maxHighlights = intProp(field, "maxHighlights", 20);
|
|
158
|
+
if (value.length > maxHighlights) return `Add at most ${plural(maxHighlights, "highlight")}.`;
|
|
159
|
+
const text = sourceText(field);
|
|
160
|
+
for (const h of value) {
|
|
161
|
+
if (!isObject(h) || !Number.isInteger(h.start) || !Number.isInteger(h.end) || typeof h.quote !== "string" || typeof h.note !== "string") {
|
|
162
|
+
return "Highlight a passage of the text.";
|
|
163
|
+
}
|
|
164
|
+
const start = h.start;
|
|
165
|
+
const end = h.end;
|
|
166
|
+
if (start < 0 || end <= start || h.quote.length !== end - start) return "Highlight a passage of the text.";
|
|
167
|
+
if (end - start > MAX_HIGHLIGHT_QUOTE_LENGTH) return `Highlight under ${MAX_HIGHLIGHT_QUOTE_LENGTH} characters at a time.`;
|
|
168
|
+
if (text !== void 0 && (end > text.length || text.slice(start, end) !== h.quote)) return "A highlight doesn't match the text.";
|
|
169
|
+
if (h.note.trim() === "") return HIGHLIGHT_NOTE_MESSAGE;
|
|
170
|
+
if (h.note.trim().length > MAX_HIGHLIGHT_NOTE_LENGTH) return `Keep each note under ${MAX_HIGHLIGHT_NOTE_LENGTH} characters.`;
|
|
171
|
+
}
|
|
172
|
+
return null;
|
|
173
|
+
},
|
|
174
|
+
normalize: (value) => Array.isArray(value) ? value.filter(isObject).map((h) => ({ start: h.start, end: h.end, quote: h.quote, note: String(h.note).trim() })).sort((a, b) => a.start - b.start || a.end - b.end) : value,
|
|
175
|
+
summarize: (value) => Array.isArray(value) ? value.filter(isObject).map((h) => `"${String(h.quote)}" \u2014 ${String(h.note).trim()}`).join("; ") : "",
|
|
176
|
+
samples: (field) => {
|
|
177
|
+
const text = sourceText(field);
|
|
178
|
+
if (text === void 0) return [[{ start: 0, end: 6, quote: "sample", note: "sample" }]];
|
|
179
|
+
const end = Math.min(text.length, 20);
|
|
180
|
+
return [[{ start: 0, end, quote: text.slice(0, end), note: "sample" }]];
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
];
|
|
184
|
+
|
|
3
185
|
// src/fields.ts
|
|
4
186
|
var REGISTRY = /* @__PURE__ */ Symbol.for("@marktiderman/genesis-forms/field-types");
|
|
5
187
|
var globalScope = globalThis;
|
|
@@ -181,7 +363,7 @@ var builtins = [
|
|
|
181
363
|
summarize: (value) => value ? "Yes" : "No"
|
|
182
364
|
}
|
|
183
365
|
];
|
|
184
|
-
for (const manifest of builtins) registerFieldType(manifest);
|
|
366
|
+
for (const manifest of [...builtins, ...structuredFieldTypes]) registerFieldType(manifest);
|
|
185
367
|
|
|
186
368
|
// src/events.ts
|
|
187
369
|
function isPlainObject(value) {
|
|
@@ -670,6 +852,10 @@ function domainOf(field, extra, maxOptions) {
|
|
|
670
852
|
extra = extra.map((v) => typeof v === "string" ? [v] : v);
|
|
671
853
|
break;
|
|
672
854
|
}
|
|
855
|
+
case "list":
|
|
856
|
+
base = getFieldType(field.type)?.samples?.(field) ?? [];
|
|
857
|
+
extra = extra.map((v) => typeof v === "string" ? [v] : v);
|
|
858
|
+
break;
|
|
673
859
|
case "email":
|
|
674
860
|
base = ["ada@example.com"];
|
|
675
861
|
break;
|
|
@@ -783,7 +969,7 @@ function shown(v) {
|
|
|
783
969
|
text ??= typeof v === "bigint" ? `${v}n` : typeof v;
|
|
784
970
|
return text.length > 60 ? `${text.slice(0, 57)}...` : text;
|
|
785
971
|
}
|
|
786
|
-
var
|
|
972
|
+
var isObject2 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
787
973
|
var isInt = (v) => typeof v === "number" && Number.isInteger(v);
|
|
788
974
|
var LITERAL_TYPE = {
|
|
789
975
|
"short-text": "string",
|
|
@@ -794,7 +980,8 @@ var LITERAL_TYPE = {
|
|
|
794
980
|
nps: "number",
|
|
795
981
|
"single-choice": "string",
|
|
796
982
|
"multi-choice": "string",
|
|
797
|
-
"yes-no": "boolean"
|
|
983
|
+
"yes-no": "boolean",
|
|
984
|
+
list: "string"
|
|
798
985
|
};
|
|
799
986
|
var ORDERING = /* @__PURE__ */ new Set(["lt", "lte", "gt", "gte"]);
|
|
800
987
|
var OPERATORS = /* @__PURE__ */ new Set([
|
|
@@ -836,7 +1023,48 @@ function parseForm(input) {
|
|
|
836
1023
|
}
|
|
837
1024
|
return true;
|
|
838
1025
|
};
|
|
839
|
-
|
|
1026
|
+
const optionalInt = (v, path, floor) => {
|
|
1027
|
+
if (v !== void 0 && !(isInt(v) && v >= floor)) issue(path, `Must be a whole number of ${floor} or more.`);
|
|
1028
|
+
};
|
|
1029
|
+
const ordered = (low, high, path, what) => {
|
|
1030
|
+
if (isInt(low) && isInt(high) && low > high) issue(path, `Can't be more than the ${what}.`);
|
|
1031
|
+
};
|
|
1032
|
+
const structuredProps = (type, props, path) => {
|
|
1033
|
+
if (type === "list") {
|
|
1034
|
+
optionalInt(props.min, `${path}.min`, 0);
|
|
1035
|
+
optionalInt(props.max, `${path}.max`, 1);
|
|
1036
|
+
optionalInt(props.itemMaxLength, `${path}.itemMaxLength`, 1);
|
|
1037
|
+
optionalText(props.placeholder, `${path}.placeholder`);
|
|
1038
|
+
ordered(props.min, props.max ?? 10, `${path}.min`, "most items");
|
|
1039
|
+
} else if (type === "table") {
|
|
1040
|
+
const columns = props.columns;
|
|
1041
|
+
if (!Array.isArray(columns) || columns.length < 1 || columns.length > MAX_TABLE_COLUMNS) {
|
|
1042
|
+
issue(`${path}.columns`, `Must be a list of 1 to ${MAX_TABLE_COLUMNS} columns.`);
|
|
1043
|
+
} else {
|
|
1044
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1045
|
+
columns.forEach((c, j) => {
|
|
1046
|
+
const at = `${path}.columns[${j}]`;
|
|
1047
|
+
if (!isObject2(c)) return issue(at, "Each column needs a key and a label.");
|
|
1048
|
+
if (name(c.key, `${at}.key`, "A column key")) {
|
|
1049
|
+
if (seen.has(c.key)) issue(`${at}.key`, `Duplicate column key "${c.key}".`);
|
|
1050
|
+
seen.add(c.key);
|
|
1051
|
+
}
|
|
1052
|
+
text(c.label, `${at}.label`);
|
|
1053
|
+
optionalBoolean(c.long, `${at}.long`);
|
|
1054
|
+
});
|
|
1055
|
+
}
|
|
1056
|
+
optionalInt(props.minRows, `${path}.minRows`, 0);
|
|
1057
|
+
optionalInt(props.maxRows, `${path}.maxRows`, 1);
|
|
1058
|
+
ordered(props.minRows, props.maxRows ?? 3, `${path}.minRows`, "most rows");
|
|
1059
|
+
} else if (type === "media-notes") {
|
|
1060
|
+
optionalInt(props.maxNotes, `${path}.maxNotes`, 1);
|
|
1061
|
+
optionalInt(props.textMaxLength, `${path}.textMaxLength`, 1);
|
|
1062
|
+
} else if (type === "text-highlights") {
|
|
1063
|
+
optionalText(props.text, `${path}.text`);
|
|
1064
|
+
optionalInt(props.maxHighlights, `${path}.maxHighlights`, 1);
|
|
1065
|
+
}
|
|
1066
|
+
};
|
|
1067
|
+
if (!isObject2(input)) {
|
|
840
1068
|
issue("", "A form definition must be an object.");
|
|
841
1069
|
return fail();
|
|
842
1070
|
}
|
|
@@ -845,19 +1073,19 @@ function parseForm(input) {
|
|
|
845
1073
|
text(input.title, "title");
|
|
846
1074
|
optionalText(input.description, "description");
|
|
847
1075
|
if (input.layout !== void 0) {
|
|
848
|
-
if (!
|
|
1076
|
+
if (!isObject2(input.layout)) issue("layout", "Must be an object.");
|
|
849
1077
|
else if (input.layout.mode !== "focus" && input.layout.mode !== "classic") {
|
|
850
1078
|
issue("layout.mode", "Must be focus or classic.");
|
|
851
1079
|
}
|
|
852
1080
|
}
|
|
853
1081
|
if (input.settings !== void 0) {
|
|
854
|
-
if (!
|
|
1082
|
+
if (!isObject2(input.settings)) issue("settings", "Must be an object.");
|
|
855
1083
|
else {
|
|
856
1084
|
optionalBoolean(input.settings.anonymous, "settings.anonymous");
|
|
857
1085
|
optionalBoolean(input.settings.clearHiddenValues, "settings.clearHiddenValues");
|
|
858
1086
|
}
|
|
859
1087
|
}
|
|
860
|
-
if (input.meta !== void 0 && (!
|
|
1088
|
+
if (input.meta !== void 0 && (!isObject2(input.meta) || !isJson(input.meta))) {
|
|
861
1089
|
issue("meta", "Must be a JSON object.");
|
|
862
1090
|
}
|
|
863
1091
|
if (!Array.isArray(input.blocks)) {
|
|
@@ -872,7 +1100,7 @@ function parseForm(input) {
|
|
|
872
1100
|
const choices = /* @__PURE__ */ new Map();
|
|
873
1101
|
input.blocks.forEach((block, i) => {
|
|
874
1102
|
const path = `blocks[${i}]`;
|
|
875
|
-
if (!
|
|
1103
|
+
if (!isObject2(block) || typeof block.id !== "string" || !block.id) {
|
|
876
1104
|
issue(path, "Every block needs a string id.");
|
|
877
1105
|
return;
|
|
878
1106
|
}
|
|
@@ -893,23 +1121,23 @@ function parseForm(input) {
|
|
|
893
1121
|
issue(`${path}.type`, `Unknown field type "${String(block.type)}".`);
|
|
894
1122
|
}
|
|
895
1123
|
if (block.props !== void 0) {
|
|
896
|
-
if (!
|
|
1124
|
+
if (!isObject2(block.props) || !isJson(block.props)) issue(`${path}.props`, "Must be a JSON object.");
|
|
897
1125
|
else if (block.props.options !== void 0) {
|
|
898
1126
|
const options = block.props.options;
|
|
899
1127
|
if (!Array.isArray(options)) issue(`${path}.props.options`, "Must be an array.");
|
|
900
1128
|
else {
|
|
901
1129
|
options.forEach((o, j) => {
|
|
902
1130
|
const at = `${path}.props.options[${j}]`;
|
|
903
|
-
if (!
|
|
1131
|
+
if (!isObject2(o) || typeof o.value !== "string") issue(at, "Each option needs a string value.");
|
|
904
1132
|
else optionalText(o.label, `${at}.label`);
|
|
905
1133
|
});
|
|
906
1134
|
choices.set(
|
|
907
1135
|
block.id,
|
|
908
|
-
new Set(options.flatMap((o) =>
|
|
1136
|
+
new Set(options.flatMap((o) => isObject2(o) && typeof o.value === "string" ? [o.value] : []))
|
|
909
1137
|
);
|
|
910
1138
|
}
|
|
911
1139
|
}
|
|
912
|
-
const { max, maxLength: maxLength2 } =
|
|
1140
|
+
const { max, maxLength: maxLength2 } = isObject2(block.props) ? block.props : {};
|
|
913
1141
|
if (block.type === "rating" && max !== void 0 && !(isInt(max) && max >= 2 && max <= 10)) {
|
|
914
1142
|
issue(`${path}.props.max`, "Must be a whole number from 2 to 10.");
|
|
915
1143
|
}
|
|
@@ -917,6 +1145,9 @@ function parseForm(input) {
|
|
|
917
1145
|
issue(`${path}.props.maxLength`, "Must be a whole number above 0.");
|
|
918
1146
|
}
|
|
919
1147
|
}
|
|
1148
|
+
if (typeof block.type === "string" && (block.props === void 0 || isObject2(block.props))) {
|
|
1149
|
+
structuredProps(block.type, block.props ?? {}, `${path}.props`);
|
|
1150
|
+
}
|
|
920
1151
|
if (validKey) {
|
|
921
1152
|
if (keys.has(block.key)) issue(`${path}.key`, `Duplicate key "${String(block.key)}".`);
|
|
922
1153
|
keys.set(block.key, block.id);
|
|
@@ -935,7 +1166,7 @@ function parseForm(input) {
|
|
|
935
1166
|
const hiddenTypes = /* @__PURE__ */ new Map();
|
|
936
1167
|
if (input.hidden !== void 0 && !Array.isArray(input.hidden)) issue("hidden", "Must be an array.");
|
|
937
1168
|
(Array.isArray(input.hidden) ? input.hidden : []).forEach((h, i) => {
|
|
938
|
-
if (!
|
|
1169
|
+
if (!isObject2(h) || typeof h.name !== "string" || !h.name) {
|
|
939
1170
|
issue(`hidden[${i}]`, "Each hidden input needs a name.");
|
|
940
1171
|
return;
|
|
941
1172
|
}
|
|
@@ -956,7 +1187,7 @@ function parseForm(input) {
|
|
|
956
1187
|
if (input.endings !== void 0 && !Array.isArray(input.endings)) issue("endings", "Must be an array.");
|
|
957
1188
|
(Array.isArray(input.endings) ? input.endings : []).forEach((e, i) => {
|
|
958
1189
|
const path = `endings[${i}]`;
|
|
959
|
-
if (!
|
|
1190
|
+
if (!isObject2(e)) {
|
|
960
1191
|
issue(path, "Each ending must be an object with an id and a title.");
|
|
961
1192
|
return;
|
|
962
1193
|
}
|
|
@@ -968,7 +1199,7 @@ function parseForm(input) {
|
|
|
968
1199
|
});
|
|
969
1200
|
const checkOperand = (o, path, reads) => {
|
|
970
1201
|
if (o === void 0) return;
|
|
971
|
-
if (!
|
|
1202
|
+
if (!isObject2(o)) return issue(path, "An operand must be an object.");
|
|
972
1203
|
if ("field" in o) {
|
|
973
1204
|
if (typeof o.field !== "string" || !fieldTypes.has(o.field)) return issue(path, `No field "${String(o.field)}".`);
|
|
974
1205
|
reads.push(o.field);
|
|
@@ -981,7 +1212,7 @@ function parseForm(input) {
|
|
|
981
1212
|
}
|
|
982
1213
|
};
|
|
983
1214
|
const checkCondition = (c, path, reads, depth = 0) => {
|
|
984
|
-
if (!
|
|
1215
|
+
if (!isObject2(c)) return issue(path, "A condition must be an object.");
|
|
985
1216
|
if (depth > MAX_CONDITION_DEPTH) return issue(path, "The condition is nested too deeply.");
|
|
986
1217
|
if ("all" in c || "any" in c) {
|
|
987
1218
|
const group = "all" in c ? "all" : "any";
|
|
@@ -990,7 +1221,7 @@ function parseForm(input) {
|
|
|
990
1221
|
return list.forEach((x, i) => checkCondition(x, `${path}.${group}[${i}]`, reads, depth + 1));
|
|
991
1222
|
}
|
|
992
1223
|
if ("not" in c) return checkCondition(c.not, `${path}.not`, reads, depth + 1);
|
|
993
|
-
if (typeof c.op !== "string" || !
|
|
1224
|
+
if (typeof c.op !== "string" || !isObject2(c.left)) {
|
|
994
1225
|
return issue(path, "A comparison needs an `op` and a `left` operand.");
|
|
995
1226
|
}
|
|
996
1227
|
if (!OPERATORS.has(c.op)) issue(`${path}.op`, `Unknown operator "${c.op}".`);
|
|
@@ -1006,7 +1237,7 @@ function parseForm(input) {
|
|
|
1006
1237
|
};
|
|
1007
1238
|
const checkLiterals = (op, left, right, path) => {
|
|
1008
1239
|
if (op === "answered" || op === "empty" || !OPERATORS.has(op)) return;
|
|
1009
|
-
const literal =
|
|
1240
|
+
const literal = isObject2(right) && "value" in right;
|
|
1010
1241
|
if (right !== void 0 && !literal) return;
|
|
1011
1242
|
let what = "the left side";
|
|
1012
1243
|
let type;
|
|
@@ -1045,7 +1276,7 @@ function parseForm(input) {
|
|
|
1045
1276
|
if (input.logic !== void 0 && !Array.isArray(input.logic)) issue("logic", "Must be an array.");
|
|
1046
1277
|
(Array.isArray(input.logic) ? input.logic : []).forEach((rule, i) => {
|
|
1047
1278
|
const path = `logic[${i}]`;
|
|
1048
|
-
if (!
|
|
1279
|
+
if (!isObject2(rule) || typeof rule.id !== "string" || !isObject2(rule.when)) {
|
|
1049
1280
|
issue(path, "Each rule needs an id and a `when` condition.");
|
|
1050
1281
|
return;
|
|
1051
1282
|
}
|
|
@@ -1064,7 +1295,7 @@ function parseForm(input) {
|
|
|
1064
1295
|
at = rule.from;
|
|
1065
1296
|
inclusive = true;
|
|
1066
1297
|
}
|
|
1067
|
-
if (
|
|
1298
|
+
if (isObject2(rule.to)) {
|
|
1068
1299
|
if (typeof rule.to.ending !== "string" || !endingIds.has(rule.to.ending)) {
|
|
1069
1300
|
issue(`${path}.to`, `No ending "${String(rule.to.ending)}".`);
|
|
1070
1301
|
}
|
|
@@ -1096,7 +1327,7 @@ function parseForm(input) {
|
|
|
1096
1327
|
}
|
|
1097
1328
|
});
|
|
1098
1329
|
input.blocks.forEach((block, i) => {
|
|
1099
|
-
if (!
|
|
1330
|
+
if (!isObject2(block)) return;
|
|
1100
1331
|
const raw = block.kind === "field" ? [block.label, block.description] : block.kind === "content" ? [block.text] : [];
|
|
1101
1332
|
const texts = raw.filter((t) => typeof t === "string");
|
|
1102
1333
|
for (const t of texts) {
|
|
@@ -1116,7 +1347,7 @@ function parseForm(input) {
|
|
|
1116
1347
|
});
|
|
1117
1348
|
const extra = [["description", input.description]];
|
|
1118
1349
|
(Array.isArray(input.endings) ? input.endings : []).forEach((e, i) => {
|
|
1119
|
-
if (
|
|
1350
|
+
if (isObject2(e)) extra.push([`endings[${i}]`, e.title], [`endings[${i}]`, e.description]);
|
|
1120
1351
|
});
|
|
1121
1352
|
for (const [path, t] of extra) {
|
|
1122
1353
|
if (typeof t !== "string") continue;
|