@marktiderman/genesis-forms 0.2.0 → 0.3.0
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 +1211 -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 +1169 -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 +320 -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 +309 -25
- package/dist/index.js.map +1 -1
- package/dist/server.cjs +247 -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 +247 -22
- package/dist/server.js.map +1 -1
- package/dist/testing.cjs +251 -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 +251 -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,29 +944,30 @@ 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
|
}
|
|
743
967
|
if (maxLength2 !== void 0 && !(isInt(maxLength2) && maxLength2 > 0)) {
|
|
744
968
|
issue(`${path}.props.maxLength`, "Must be a whole number above 0.");
|
|
745
969
|
}
|
|
970
|
+
if (isObject2(block.props) && typeof block.type === "string") structuredProps(block.type, block.props, `${path}.props`);
|
|
746
971
|
}
|
|
747
972
|
if (validKey) {
|
|
748
973
|
if (keys.has(block.key)) issue(`${path}.key`, `Duplicate key "${String(block.key)}".`);
|
|
@@ -762,7 +987,7 @@ function parseForm(input) {
|
|
|
762
987
|
const hiddenTypes = /* @__PURE__ */ new Map();
|
|
763
988
|
if (input.hidden !== void 0 && !Array.isArray(input.hidden)) issue("hidden", "Must be an array.");
|
|
764
989
|
(Array.isArray(input.hidden) ? input.hidden : []).forEach((h, i) => {
|
|
765
|
-
if (!
|
|
990
|
+
if (!isObject2(h) || typeof h.name !== "string" || !h.name) {
|
|
766
991
|
issue(`hidden[${i}]`, "Each hidden input needs a name.");
|
|
767
992
|
return;
|
|
768
993
|
}
|
|
@@ -783,7 +1008,7 @@ function parseForm(input) {
|
|
|
783
1008
|
if (input.endings !== void 0 && !Array.isArray(input.endings)) issue("endings", "Must be an array.");
|
|
784
1009
|
(Array.isArray(input.endings) ? input.endings : []).forEach((e, i) => {
|
|
785
1010
|
const path = `endings[${i}]`;
|
|
786
|
-
if (!
|
|
1011
|
+
if (!isObject2(e)) {
|
|
787
1012
|
issue(path, "Each ending must be an object with an id and a title.");
|
|
788
1013
|
return;
|
|
789
1014
|
}
|
|
@@ -795,7 +1020,7 @@ function parseForm(input) {
|
|
|
795
1020
|
});
|
|
796
1021
|
const checkOperand = (o, path, reads) => {
|
|
797
1022
|
if (o === void 0) return;
|
|
798
|
-
if (!
|
|
1023
|
+
if (!isObject2(o)) return issue(path, "An operand must be an object.");
|
|
799
1024
|
if ("field" in o) {
|
|
800
1025
|
if (typeof o.field !== "string" || !fieldTypes.has(o.field)) return issue(path, `No field "${String(o.field)}".`);
|
|
801
1026
|
reads.push(o.field);
|
|
@@ -808,7 +1033,7 @@ function parseForm(input) {
|
|
|
808
1033
|
}
|
|
809
1034
|
};
|
|
810
1035
|
const checkCondition = (c, path, reads, depth = 0) => {
|
|
811
|
-
if (!
|
|
1036
|
+
if (!isObject2(c)) return issue(path, "A condition must be an object.");
|
|
812
1037
|
if (depth > MAX_CONDITION_DEPTH) return issue(path, "The condition is nested too deeply.");
|
|
813
1038
|
if ("all" in c || "any" in c) {
|
|
814
1039
|
const group = "all" in c ? "all" : "any";
|
|
@@ -817,7 +1042,7 @@ function parseForm(input) {
|
|
|
817
1042
|
return list.forEach((x, i) => checkCondition(x, `${path}.${group}[${i}]`, reads, depth + 1));
|
|
818
1043
|
}
|
|
819
1044
|
if ("not" in c) return checkCondition(c.not, `${path}.not`, reads, depth + 1);
|
|
820
|
-
if (typeof c.op !== "string" || !
|
|
1045
|
+
if (typeof c.op !== "string" || !isObject2(c.left)) {
|
|
821
1046
|
return issue(path, "A comparison needs an `op` and a `left` operand.");
|
|
822
1047
|
}
|
|
823
1048
|
if (!OPERATORS.has(c.op)) issue(`${path}.op`, `Unknown operator "${c.op}".`);
|
|
@@ -833,7 +1058,7 @@ function parseForm(input) {
|
|
|
833
1058
|
};
|
|
834
1059
|
const checkLiterals = (op, left, right, path) => {
|
|
835
1060
|
if (op === "answered" || op === "empty" || !OPERATORS.has(op)) return;
|
|
836
|
-
const literal =
|
|
1061
|
+
const literal = isObject2(right) && "value" in right;
|
|
837
1062
|
if (right !== void 0 && !literal) return;
|
|
838
1063
|
let what = "the left side";
|
|
839
1064
|
let type;
|
|
@@ -872,7 +1097,7 @@ function parseForm(input) {
|
|
|
872
1097
|
if (input.logic !== void 0 && !Array.isArray(input.logic)) issue("logic", "Must be an array.");
|
|
873
1098
|
(Array.isArray(input.logic) ? input.logic : []).forEach((rule, i) => {
|
|
874
1099
|
const path = `logic[${i}]`;
|
|
875
|
-
if (!
|
|
1100
|
+
if (!isObject2(rule) || typeof rule.id !== "string" || !isObject2(rule.when)) {
|
|
876
1101
|
issue(path, "Each rule needs an id and a `when` condition.");
|
|
877
1102
|
return;
|
|
878
1103
|
}
|
|
@@ -891,7 +1116,7 @@ function parseForm(input) {
|
|
|
891
1116
|
at = rule.from;
|
|
892
1117
|
inclusive = true;
|
|
893
1118
|
}
|
|
894
|
-
if (
|
|
1119
|
+
if (isObject2(rule.to)) {
|
|
895
1120
|
if (typeof rule.to.ending !== "string" || !endingIds.has(rule.to.ending)) {
|
|
896
1121
|
issue(`${path}.to`, `No ending "${String(rule.to.ending)}".`);
|
|
897
1122
|
}
|
|
@@ -923,7 +1148,7 @@ function parseForm(input) {
|
|
|
923
1148
|
}
|
|
924
1149
|
});
|
|
925
1150
|
input.blocks.forEach((block, i) => {
|
|
926
|
-
if (!
|
|
1151
|
+
if (!isObject2(block)) return;
|
|
927
1152
|
const raw = block.kind === "field" ? [block.label, block.description] : block.kind === "content" ? [block.text] : [];
|
|
928
1153
|
const texts = raw.filter((t) => typeof t === "string");
|
|
929
1154
|
for (const t of texts) {
|
|
@@ -943,7 +1168,7 @@ function parseForm(input) {
|
|
|
943
1168
|
});
|
|
944
1169
|
const extra = [["description", input.description]];
|
|
945
1170
|
(Array.isArray(input.endings) ? input.endings : []).forEach((e, i) => {
|
|
946
|
-
if (
|
|
1171
|
+
if (isObject2(e)) extra.push([`endings[${i}]`, e.title], [`endings[${i}]`, e.description]);
|
|
947
1172
|
});
|
|
948
1173
|
for (const [path, t] of extra) {
|
|
949
1174
|
if (typeof t !== "string") continue;
|