@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/server.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) {
|
|
@@ -610,7 +792,7 @@ function shown(v) {
|
|
|
610
792
|
text ??= typeof v === "bigint" ? `${v}n` : typeof v;
|
|
611
793
|
return text.length > 60 ? `${text.slice(0, 57)}...` : text;
|
|
612
794
|
}
|
|
613
|
-
var
|
|
795
|
+
var isObject2 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
614
796
|
var isInt = (v) => typeof v === "number" && Number.isInteger(v);
|
|
615
797
|
var LITERAL_TYPE = {
|
|
616
798
|
"short-text": "string",
|
|
@@ -621,7 +803,8 @@ var LITERAL_TYPE = {
|
|
|
621
803
|
nps: "number",
|
|
622
804
|
"single-choice": "string",
|
|
623
805
|
"multi-choice": "string",
|
|
624
|
-
"yes-no": "boolean"
|
|
806
|
+
"yes-no": "boolean",
|
|
807
|
+
list: "string"
|
|
625
808
|
};
|
|
626
809
|
var ORDERING = /* @__PURE__ */ new Set(["lt", "lte", "gt", "gte"]);
|
|
627
810
|
var OPERATORS = /* @__PURE__ */ new Set([
|
|
@@ -663,7 +846,48 @@ function parseForm(input) {
|
|
|
663
846
|
}
|
|
664
847
|
return true;
|
|
665
848
|
};
|
|
666
|
-
|
|
849
|
+
const optionalInt = (v, path, floor) => {
|
|
850
|
+
if (v !== void 0 && !(isInt(v) && v >= floor)) issue(path, `Must be a whole number of ${floor} or more.`);
|
|
851
|
+
};
|
|
852
|
+
const ordered = (low, high, path, what) => {
|
|
853
|
+
if (isInt(low) && isInt(high) && low > high) issue(path, `Can't be more than the ${what}.`);
|
|
854
|
+
};
|
|
855
|
+
const structuredProps = (type, props, path) => {
|
|
856
|
+
if (type === "list") {
|
|
857
|
+
optionalInt(props.min, `${path}.min`, 0);
|
|
858
|
+
optionalInt(props.max, `${path}.max`, 1);
|
|
859
|
+
optionalInt(props.itemMaxLength, `${path}.itemMaxLength`, 1);
|
|
860
|
+
optionalText(props.placeholder, `${path}.placeholder`);
|
|
861
|
+
ordered(props.min, props.max ?? 10, `${path}.min`, "most items");
|
|
862
|
+
} else if (type === "table") {
|
|
863
|
+
const columns = props.columns;
|
|
864
|
+
if (!Array.isArray(columns) || columns.length < 1 || columns.length > MAX_TABLE_COLUMNS) {
|
|
865
|
+
issue(`${path}.columns`, `Must be a list of 1 to ${MAX_TABLE_COLUMNS} columns.`);
|
|
866
|
+
} else {
|
|
867
|
+
const seen = /* @__PURE__ */ new Set();
|
|
868
|
+
columns.forEach((c, j) => {
|
|
869
|
+
const at = `${path}.columns[${j}]`;
|
|
870
|
+
if (!isObject2(c)) return issue(at, "Each column needs a key and a label.");
|
|
871
|
+
if (name(c.key, `${at}.key`, "A column key")) {
|
|
872
|
+
if (seen.has(c.key)) issue(`${at}.key`, `Duplicate column key "${c.key}".`);
|
|
873
|
+
seen.add(c.key);
|
|
874
|
+
}
|
|
875
|
+
text(c.label, `${at}.label`);
|
|
876
|
+
optionalBoolean(c.long, `${at}.long`);
|
|
877
|
+
});
|
|
878
|
+
}
|
|
879
|
+
optionalInt(props.minRows, `${path}.minRows`, 0);
|
|
880
|
+
optionalInt(props.maxRows, `${path}.maxRows`, 1);
|
|
881
|
+
ordered(props.minRows, props.maxRows ?? 3, `${path}.minRows`, "most rows");
|
|
882
|
+
} else if (type === "media-notes") {
|
|
883
|
+
optionalInt(props.maxNotes, `${path}.maxNotes`, 1);
|
|
884
|
+
optionalInt(props.textMaxLength, `${path}.textMaxLength`, 1);
|
|
885
|
+
} else if (type === "text-highlights") {
|
|
886
|
+
optionalText(props.text, `${path}.text`);
|
|
887
|
+
optionalInt(props.maxHighlights, `${path}.maxHighlights`, 1);
|
|
888
|
+
}
|
|
889
|
+
};
|
|
890
|
+
if (!isObject2(input)) {
|
|
667
891
|
issue("", "A form definition must be an object.");
|
|
668
892
|
return fail();
|
|
669
893
|
}
|
|
@@ -672,19 +896,19 @@ function parseForm(input) {
|
|
|
672
896
|
text(input.title, "title");
|
|
673
897
|
optionalText(input.description, "description");
|
|
674
898
|
if (input.layout !== void 0) {
|
|
675
|
-
if (!
|
|
899
|
+
if (!isObject2(input.layout)) issue("layout", "Must be an object.");
|
|
676
900
|
else if (input.layout.mode !== "focus" && input.layout.mode !== "classic") {
|
|
677
901
|
issue("layout.mode", "Must be focus or classic.");
|
|
678
902
|
}
|
|
679
903
|
}
|
|
680
904
|
if (input.settings !== void 0) {
|
|
681
|
-
if (!
|
|
905
|
+
if (!isObject2(input.settings)) issue("settings", "Must be an object.");
|
|
682
906
|
else {
|
|
683
907
|
optionalBoolean(input.settings.anonymous, "settings.anonymous");
|
|
684
908
|
optionalBoolean(input.settings.clearHiddenValues, "settings.clearHiddenValues");
|
|
685
909
|
}
|
|
686
910
|
}
|
|
687
|
-
if (input.meta !== void 0 && (!
|
|
911
|
+
if (input.meta !== void 0 && (!isObject2(input.meta) || !isJson(input.meta))) {
|
|
688
912
|
issue("meta", "Must be a JSON object.");
|
|
689
913
|
}
|
|
690
914
|
if (!Array.isArray(input.blocks)) {
|
|
@@ -699,7 +923,7 @@ function parseForm(input) {
|
|
|
699
923
|
const choices = /* @__PURE__ */ new Map();
|
|
700
924
|
input.blocks.forEach((block, i) => {
|
|
701
925
|
const path = `blocks[${i}]`;
|
|
702
|
-
if (!
|
|
926
|
+
if (!isObject2(block) || typeof block.id !== "string" || !block.id) {
|
|
703
927
|
issue(path, "Every block needs a string id.");
|
|
704
928
|
return;
|
|
705
929
|
}
|
|
@@ -720,23 +944,23 @@ function parseForm(input) {
|
|
|
720
944
|
issue(`${path}.type`, `Unknown field type "${String(block.type)}".`);
|
|
721
945
|
}
|
|
722
946
|
if (block.props !== void 0) {
|
|
723
|
-
if (!
|
|
947
|
+
if (!isObject2(block.props) || !isJson(block.props)) issue(`${path}.props`, "Must be a JSON object.");
|
|
724
948
|
else if (block.props.options !== void 0) {
|
|
725
949
|
const options = block.props.options;
|
|
726
950
|
if (!Array.isArray(options)) issue(`${path}.props.options`, "Must be an array.");
|
|
727
951
|
else {
|
|
728
952
|
options.forEach((o, j) => {
|
|
729
953
|
const at = `${path}.props.options[${j}]`;
|
|
730
|
-
if (!
|
|
954
|
+
if (!isObject2(o) || typeof o.value !== "string") issue(at, "Each option needs a string value.");
|
|
731
955
|
else optionalText(o.label, `${at}.label`);
|
|
732
956
|
});
|
|
733
957
|
choices.set(
|
|
734
958
|
block.id,
|
|
735
|
-
new Set(options.flatMap((o) =>
|
|
959
|
+
new Set(options.flatMap((o) => isObject2(o) && typeof o.value === "string" ? [o.value] : []))
|
|
736
960
|
);
|
|
737
961
|
}
|
|
738
962
|
}
|
|
739
|
-
const { max, maxLength: maxLength2 } =
|
|
963
|
+
const { max, maxLength: maxLength2 } = isObject2(block.props) ? block.props : {};
|
|
740
964
|
if (block.type === "rating" && max !== void 0 && !(isInt(max) && max >= 2 && max <= 10)) {
|
|
741
965
|
issue(`${path}.props.max`, "Must be a whole number from 2 to 10.");
|
|
742
966
|
}
|
|
@@ -744,6 +968,9 @@ function parseForm(input) {
|
|
|
744
968
|
issue(`${path}.props.maxLength`, "Must be a whole number above 0.");
|
|
745
969
|
}
|
|
746
970
|
}
|
|
971
|
+
if (typeof block.type === "string" && (block.props === void 0 || isObject2(block.props))) {
|
|
972
|
+
structuredProps(block.type, block.props ?? {}, `${path}.props`);
|
|
973
|
+
}
|
|
747
974
|
if (validKey) {
|
|
748
975
|
if (keys.has(block.key)) issue(`${path}.key`, `Duplicate key "${String(block.key)}".`);
|
|
749
976
|
keys.set(block.key, block.id);
|
|
@@ -762,7 +989,7 @@ function parseForm(input) {
|
|
|
762
989
|
const hiddenTypes = /* @__PURE__ */ new Map();
|
|
763
990
|
if (input.hidden !== void 0 && !Array.isArray(input.hidden)) issue("hidden", "Must be an array.");
|
|
764
991
|
(Array.isArray(input.hidden) ? input.hidden : []).forEach((h, i) => {
|
|
765
|
-
if (!
|
|
992
|
+
if (!isObject2(h) || typeof h.name !== "string" || !h.name) {
|
|
766
993
|
issue(`hidden[${i}]`, "Each hidden input needs a name.");
|
|
767
994
|
return;
|
|
768
995
|
}
|
|
@@ -783,7 +1010,7 @@ function parseForm(input) {
|
|
|
783
1010
|
if (input.endings !== void 0 && !Array.isArray(input.endings)) issue("endings", "Must be an array.");
|
|
784
1011
|
(Array.isArray(input.endings) ? input.endings : []).forEach((e, i) => {
|
|
785
1012
|
const path = `endings[${i}]`;
|
|
786
|
-
if (!
|
|
1013
|
+
if (!isObject2(e)) {
|
|
787
1014
|
issue(path, "Each ending must be an object with an id and a title.");
|
|
788
1015
|
return;
|
|
789
1016
|
}
|
|
@@ -795,7 +1022,7 @@ function parseForm(input) {
|
|
|
795
1022
|
});
|
|
796
1023
|
const checkOperand = (o, path, reads) => {
|
|
797
1024
|
if (o === void 0) return;
|
|
798
|
-
if (!
|
|
1025
|
+
if (!isObject2(o)) return issue(path, "An operand must be an object.");
|
|
799
1026
|
if ("field" in o) {
|
|
800
1027
|
if (typeof o.field !== "string" || !fieldTypes.has(o.field)) return issue(path, `No field "${String(o.field)}".`);
|
|
801
1028
|
reads.push(o.field);
|
|
@@ -808,7 +1035,7 @@ function parseForm(input) {
|
|
|
808
1035
|
}
|
|
809
1036
|
};
|
|
810
1037
|
const checkCondition = (c, path, reads, depth = 0) => {
|
|
811
|
-
if (!
|
|
1038
|
+
if (!isObject2(c)) return issue(path, "A condition must be an object.");
|
|
812
1039
|
if (depth > MAX_CONDITION_DEPTH) return issue(path, "The condition is nested too deeply.");
|
|
813
1040
|
if ("all" in c || "any" in c) {
|
|
814
1041
|
const group = "all" in c ? "all" : "any";
|
|
@@ -817,7 +1044,7 @@ function parseForm(input) {
|
|
|
817
1044
|
return list.forEach((x, i) => checkCondition(x, `${path}.${group}[${i}]`, reads, depth + 1));
|
|
818
1045
|
}
|
|
819
1046
|
if ("not" in c) return checkCondition(c.not, `${path}.not`, reads, depth + 1);
|
|
820
|
-
if (typeof c.op !== "string" || !
|
|
1047
|
+
if (typeof c.op !== "string" || !isObject2(c.left)) {
|
|
821
1048
|
return issue(path, "A comparison needs an `op` and a `left` operand.");
|
|
822
1049
|
}
|
|
823
1050
|
if (!OPERATORS.has(c.op)) issue(`${path}.op`, `Unknown operator "${c.op}".`);
|
|
@@ -833,7 +1060,7 @@ function parseForm(input) {
|
|
|
833
1060
|
};
|
|
834
1061
|
const checkLiterals = (op, left, right, path) => {
|
|
835
1062
|
if (op === "answered" || op === "empty" || !OPERATORS.has(op)) return;
|
|
836
|
-
const literal =
|
|
1063
|
+
const literal = isObject2(right) && "value" in right;
|
|
837
1064
|
if (right !== void 0 && !literal) return;
|
|
838
1065
|
let what = "the left side";
|
|
839
1066
|
let type;
|
|
@@ -872,7 +1099,7 @@ function parseForm(input) {
|
|
|
872
1099
|
if (input.logic !== void 0 && !Array.isArray(input.logic)) issue("logic", "Must be an array.");
|
|
873
1100
|
(Array.isArray(input.logic) ? input.logic : []).forEach((rule, i) => {
|
|
874
1101
|
const path = `logic[${i}]`;
|
|
875
|
-
if (!
|
|
1102
|
+
if (!isObject2(rule) || typeof rule.id !== "string" || !isObject2(rule.when)) {
|
|
876
1103
|
issue(path, "Each rule needs an id and a `when` condition.");
|
|
877
1104
|
return;
|
|
878
1105
|
}
|
|
@@ -891,7 +1118,7 @@ function parseForm(input) {
|
|
|
891
1118
|
at = rule.from;
|
|
892
1119
|
inclusive = true;
|
|
893
1120
|
}
|
|
894
|
-
if (
|
|
1121
|
+
if (isObject2(rule.to)) {
|
|
895
1122
|
if (typeof rule.to.ending !== "string" || !endingIds.has(rule.to.ending)) {
|
|
896
1123
|
issue(`${path}.to`, `No ending "${String(rule.to.ending)}".`);
|
|
897
1124
|
}
|
|
@@ -923,7 +1150,7 @@ function parseForm(input) {
|
|
|
923
1150
|
}
|
|
924
1151
|
});
|
|
925
1152
|
input.blocks.forEach((block, i) => {
|
|
926
|
-
if (!
|
|
1153
|
+
if (!isObject2(block)) return;
|
|
927
1154
|
const raw = block.kind === "field" ? [block.label, block.description] : block.kind === "content" ? [block.text] : [];
|
|
928
1155
|
const texts = raw.filter((t) => typeof t === "string");
|
|
929
1156
|
for (const t of texts) {
|
|
@@ -943,7 +1170,7 @@ function parseForm(input) {
|
|
|
943
1170
|
});
|
|
944
1171
|
const extra = [["description", input.description]];
|
|
945
1172
|
(Array.isArray(input.endings) ? input.endings : []).forEach((e, i) => {
|
|
946
|
-
if (
|
|
1173
|
+
if (isObject2(e)) extra.push([`endings[${i}]`, e.title], [`endings[${i}]`, e.description]);
|
|
947
1174
|
});
|
|
948
1175
|
for (const [path, t] of extra) {
|
|
949
1176
|
if (typeof t !== "string") continue;
|