@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/testing.d.cts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { a as FormState, F as FormEngine, C as CreateFormOptions } from './engine-
|
|
2
|
-
export { d as compactEvents } from './engine-
|
|
3
|
-
import { J as Json,
|
|
1
|
+
import { a as FormState, F as FormEngine, C as CreateFormOptions } from './engine-CPLtKVew.cjs';
|
|
2
|
+
export { d as compactEvents } from './engine-CPLtKVew.cjs';
|
|
3
|
+
import { J as Json, F as FormDefinition, e as FormEvent } from './types-BBKBQ8sp.cjs';
|
|
4
|
+
import './validate-BXXqi0fq.cjs';
|
|
4
5
|
|
|
5
6
|
interface CoverageOptions {
|
|
6
7
|
/** Stop after this many explored states and report `truncated`. Default 5000. */
|
package/dist/testing.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { a as FormState, F as FormEngine, C as CreateFormOptions } from './engine-
|
|
2
|
-
export { d as compactEvents } from './engine-
|
|
3
|
-
import { J as Json,
|
|
1
|
+
import { a as FormState, F as FormEngine, C as CreateFormOptions } from './engine-DOrPgFDw.js';
|
|
2
|
+
export { d as compactEvents } from './engine-DOrPgFDw.js';
|
|
3
|
+
import { J as Json, F as FormDefinition, e as FormEvent } from './types-BBKBQ8sp.js';
|
|
4
|
+
import './validate-BjNwF1UE.js';
|
|
4
5
|
|
|
5
6
|
interface CoverageOptions {
|
|
6
7
|
/** Stop after this many explored states and report `truncated`. Default 5000. */
|
package/dist/testing.js
CHANGED
|
@@ -1,3 +1,185 @@
|
|
|
1
|
+
// src/structured.ts
|
|
2
|
+
var isObject = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
3
|
+
function intProp(field, name, fallback, floor = 1) {
|
|
4
|
+
const v = field.props?.[name];
|
|
5
|
+
return typeof v === "number" && Number.isInteger(v) && v >= floor ? v : fallback;
|
|
6
|
+
}
|
|
7
|
+
function optionalIntProp(field, name, floor = 0) {
|
|
8
|
+
const v = field.props?.[name];
|
|
9
|
+
return typeof v === "number" && Number.isInteger(v) && v >= floor ? v : void 0;
|
|
10
|
+
}
|
|
11
|
+
var blank = (v) => typeof v !== "string" || v.trim() === "";
|
|
12
|
+
var blankText = (v) => typeof v === "string" && v.trim() === "";
|
|
13
|
+
var emptyList = (value, isBlank) => value === void 0 || value === null || Array.isArray(value) && value.every(isBlank);
|
|
14
|
+
var plural = (n, one, many = `${one}s`) => `${n} ${n === 1 ? one : many}`;
|
|
15
|
+
function listLimits(field) {
|
|
16
|
+
return { min: optionalIntProp(field, "min") ?? 0, max: intProp(field, "max", 10), itemMaxLength: intProp(field, "itemMaxLength", 500) };
|
|
17
|
+
}
|
|
18
|
+
var listItems = (value) => Array.isArray(value) ? value.filter((v) => typeof v === "string" && v.trim() !== "") : [];
|
|
19
|
+
var MAX_TABLE_COLUMNS = 6;
|
|
20
|
+
var MAX_TABLE_CELL_LENGTH = 2e3;
|
|
21
|
+
function tableColumns(field) {
|
|
22
|
+
const raw = field.props?.columns;
|
|
23
|
+
if (!Array.isArray(raw)) return [];
|
|
24
|
+
const seen = /* @__PURE__ */ new Set();
|
|
25
|
+
const out = [];
|
|
26
|
+
for (const c of raw) {
|
|
27
|
+
if (!isObject(c) || typeof c.key !== "string" || !c.key || seen.has(c.key)) continue;
|
|
28
|
+
seen.add(c.key);
|
|
29
|
+
const column = { key: c.key, label: typeof c.label === "string" ? c.label : c.key };
|
|
30
|
+
if (c.long === true) column.long = true;
|
|
31
|
+
out.push(column);
|
|
32
|
+
}
|
|
33
|
+
return out;
|
|
34
|
+
}
|
|
35
|
+
function tableLimits(field) {
|
|
36
|
+
return { minRows: optionalIntProp(field, "minRows") ?? 0, maxRows: intProp(field, "maxRows", 3) };
|
|
37
|
+
}
|
|
38
|
+
var filledRows = (value) => Array.isArray(value) ? value.filter((r) => isObject(r) && Object.values(r).some((c) => !blank(c))) : [];
|
|
39
|
+
var MAX_TIMESTAMP_SECONDS = 86400;
|
|
40
|
+
function formatTimestamp(seconds) {
|
|
41
|
+
const total = Number.isFinite(seconds) && seconds > 0 ? Math.floor(seconds) : 0;
|
|
42
|
+
const h = Math.floor(total / 3600);
|
|
43
|
+
const m = Math.floor(total % 3600 / 60);
|
|
44
|
+
const s = String(total % 60).padStart(2, "0");
|
|
45
|
+
return h ? `${h}:${String(m).padStart(2, "0")}:${s}` : `${m}:${s}`;
|
|
46
|
+
}
|
|
47
|
+
var isTimestamp = (v) => typeof v === "number" && Number.isFinite(v) && v >= 0 && v <= MAX_TIMESTAMP_SECONDS;
|
|
48
|
+
var notesWithText = (value) => Array.isArray(value) ? value.filter((n) => isObject(n) && !blank(n.text)) : [];
|
|
49
|
+
var MAX_HIGHLIGHT_NOTE_LENGTH = 1e3;
|
|
50
|
+
var MAX_HIGHLIGHT_QUOTE_LENGTH = 2e3;
|
|
51
|
+
var HIGHLIGHT_NOTE_MESSAGE = "Add a note to each highlight.";
|
|
52
|
+
var sourceText = (field) => typeof field.props?.text === "string" && field.props.text ? field.props.text : void 0;
|
|
53
|
+
var structuredFieldTypes = [
|
|
54
|
+
{
|
|
55
|
+
type: "list",
|
|
56
|
+
label: "List",
|
|
57
|
+
category: "text",
|
|
58
|
+
value: { json: true },
|
|
59
|
+
// logic.ts `contains` on an array is exact item equality, same as multi-choice, so it is offered.
|
|
60
|
+
operators: ["contains", "answered", "empty"],
|
|
61
|
+
a11y: { semantics: "group" },
|
|
62
|
+
isEmpty: (value) => emptyList(value, blankText),
|
|
63
|
+
validate: (value, field) => {
|
|
64
|
+
if (!Array.isArray(value) || !value.every((v) => typeof v === "string")) return "Enter a list of answers.";
|
|
65
|
+
const { min, max, itemMaxLength } = listLimits(field);
|
|
66
|
+
if (value.some((v) => v.trim().length > itemMaxLength)) return `Keep each item under ${itemMaxLength} characters.`;
|
|
67
|
+
const count = listItems(value).length;
|
|
68
|
+
if (count < min) return `Add at least ${plural(min, "item")}.`;
|
|
69
|
+
if (count > max) return `Add at most ${plural(max, "item")}.`;
|
|
70
|
+
return null;
|
|
71
|
+
},
|
|
72
|
+
normalize: (value) => Array.isArray(value) ? listItems(value).map((v) => v.trim()) : value,
|
|
73
|
+
summarize: (value) => listItems(value).map((v) => v.trim()).join("; "),
|
|
74
|
+
samples: (field) => {
|
|
75
|
+
const { min, max } = listLimits(field);
|
|
76
|
+
return [Array.from({ length: Math.min(Math.max(1, min), max) }, (_, i) => `Item ${i + 1}`)];
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
type: "table",
|
|
81
|
+
label: "Table",
|
|
82
|
+
category: "advanced",
|
|
83
|
+
value: { json: true },
|
|
84
|
+
operators: ["answered", "empty"],
|
|
85
|
+
a11y: { semantics: "group" },
|
|
86
|
+
isEmpty: (value) => emptyList(value, (r) => isObject(r) && Object.values(r).every(blankText)),
|
|
87
|
+
validate: (value, field) => {
|
|
88
|
+
const columns = tableColumns(field);
|
|
89
|
+
if (!columns.length) return "This question has no columns.";
|
|
90
|
+
if (!Array.isArray(value) || !value.every(isObject)) return "Fill in the rows.";
|
|
91
|
+
const keys = new Set(columns.map((c) => c.key));
|
|
92
|
+
for (const row of value) {
|
|
93
|
+
for (const [key, cell] of Object.entries(row)) {
|
|
94
|
+
if (!keys.has(key)) return "An answer names a column this question doesn't have.";
|
|
95
|
+
if (typeof cell !== "string") return "Fill in the rows with text.";
|
|
96
|
+
if (cell.trim().length > MAX_TABLE_CELL_LENGTH) return `Keep each answer under ${MAX_TABLE_CELL_LENGTH} characters.`;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const { minRows, maxRows } = tableLimits(field);
|
|
100
|
+
const count = filledRows(value).length;
|
|
101
|
+
if (count < minRows) return `Fill in at least ${plural(minRows, "row")}.`;
|
|
102
|
+
if (count > maxRows) return `Fill in at most ${plural(maxRows, "row")}.`;
|
|
103
|
+
return null;
|
|
104
|
+
},
|
|
105
|
+
normalize: (value) => Array.isArray(value) ? filledRows(value).map((row) => {
|
|
106
|
+
const out = {};
|
|
107
|
+
for (const [key, cell] of Object.entries(row)) if (typeof cell === "string") out[key] = cell.trim();
|
|
108
|
+
return out;
|
|
109
|
+
}) : value,
|
|
110
|
+
summarize: (value, field) => {
|
|
111
|
+
const columns = tableColumns(field);
|
|
112
|
+
return filledRows(value).map((row) => columns.filter((c) => !blank(row[c.key])).map((c) => `${c.label}: ${row[c.key].trim()}`).join(" \xB7 ")).join(" / ");
|
|
113
|
+
},
|
|
114
|
+
samples: (field) => {
|
|
115
|
+
const columns = tableColumns(field);
|
|
116
|
+
if (!columns.length) return [];
|
|
117
|
+
const { minRows, maxRows } = tableLimits(field);
|
|
118
|
+
const row = Object.fromEntries(columns.map((c) => [c.key, "sample"]));
|
|
119
|
+
return [Array.from({ length: Math.min(Math.max(1, minRows), maxRows) }, () => ({ ...row }))];
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
type: "media-notes",
|
|
124
|
+
label: "Notes at moments",
|
|
125
|
+
category: "media",
|
|
126
|
+
value: { json: true },
|
|
127
|
+
operators: ["answered", "empty"],
|
|
128
|
+
a11y: { semantics: "group" },
|
|
129
|
+
isEmpty: (value) => emptyList(value, (n) => isObject(n) && blankText(n.text)),
|
|
130
|
+
validate: (value, field) => {
|
|
131
|
+
if (!Array.isArray(value)) return "Add notes.";
|
|
132
|
+
const textMaxLength = intProp(field, "textMaxLength", 1e3);
|
|
133
|
+
for (const note of value) {
|
|
134
|
+
if (!isObject(note) || typeof note.text !== "string") return "Add notes.";
|
|
135
|
+
if (!isTimestamp(note.at)) return "Each note needs a time.";
|
|
136
|
+
if (note.text.trim().length > textMaxLength) return `Keep each note under ${textMaxLength} characters.`;
|
|
137
|
+
}
|
|
138
|
+
const maxNotes = intProp(field, "maxNotes", 20);
|
|
139
|
+
return notesWithText(value).length > maxNotes ? `Add at most ${plural(maxNotes, "note")}.` : null;
|
|
140
|
+
},
|
|
141
|
+
normalize: (value) => Array.isArray(value) ? notesWithText(value).map((n) => ({ at: n.at, text: n.text.trim() })).sort((a, b) => a.at - b.at) : value,
|
|
142
|
+
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("; "),
|
|
143
|
+
samples: () => [[{ at: 0, text: "sample" }]]
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
type: "text-highlights",
|
|
147
|
+
label: "Highlights",
|
|
148
|
+
category: "media",
|
|
149
|
+
value: { json: true },
|
|
150
|
+
operators: ["answered", "empty"],
|
|
151
|
+
a11y: { semantics: "group" },
|
|
152
|
+
isEmpty: (value) => emptyList(value, () => false),
|
|
153
|
+
validate: (value, field) => {
|
|
154
|
+
if (!Array.isArray(value)) return "Highlight a passage.";
|
|
155
|
+
const maxHighlights = intProp(field, "maxHighlights", 20);
|
|
156
|
+
if (value.length > maxHighlights) return `Add at most ${plural(maxHighlights, "highlight")}.`;
|
|
157
|
+
const text = sourceText(field);
|
|
158
|
+
for (const h of value) {
|
|
159
|
+
if (!isObject(h) || !Number.isInteger(h.start) || !Number.isInteger(h.end) || typeof h.quote !== "string" || typeof h.note !== "string") {
|
|
160
|
+
return "Highlight a passage of the text.";
|
|
161
|
+
}
|
|
162
|
+
const start = h.start;
|
|
163
|
+
const end = h.end;
|
|
164
|
+
if (start < 0 || end <= start || h.quote.length !== end - start) return "Highlight a passage of the text.";
|
|
165
|
+
if (end - start > MAX_HIGHLIGHT_QUOTE_LENGTH) return `Highlight under ${MAX_HIGHLIGHT_QUOTE_LENGTH} characters at a time.`;
|
|
166
|
+
if (text !== void 0 && (end > text.length || text.slice(start, end) !== h.quote)) return "A highlight doesn't match the text.";
|
|
167
|
+
if (h.note.trim() === "") return HIGHLIGHT_NOTE_MESSAGE;
|
|
168
|
+
if (h.note.trim().length > MAX_HIGHLIGHT_NOTE_LENGTH) return `Keep each note under ${MAX_HIGHLIGHT_NOTE_LENGTH} characters.`;
|
|
169
|
+
}
|
|
170
|
+
return null;
|
|
171
|
+
},
|
|
172
|
+
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,
|
|
173
|
+
summarize: (value) => Array.isArray(value) ? value.filter(isObject).map((h) => `"${String(h.quote)}" \u2014 ${String(h.note).trim()}`).join("; ") : "",
|
|
174
|
+
samples: (field) => {
|
|
175
|
+
const text = sourceText(field);
|
|
176
|
+
if (text === void 0) return [[{ start: 0, end: 6, quote: "sample", note: "sample" }]];
|
|
177
|
+
const end = Math.min(text.length, 20);
|
|
178
|
+
return [[{ start: 0, end, quote: text.slice(0, end), note: "sample" }]];
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
];
|
|
182
|
+
|
|
1
183
|
// src/fields.ts
|
|
2
184
|
var REGISTRY = /* @__PURE__ */ Symbol.for("@marktiderman/genesis-forms/field-types");
|
|
3
185
|
var globalScope = globalThis;
|
|
@@ -179,7 +361,7 @@ var builtins = [
|
|
|
179
361
|
summarize: (value) => value ? "Yes" : "No"
|
|
180
362
|
}
|
|
181
363
|
];
|
|
182
|
-
for (const manifest of builtins) registerFieldType(manifest);
|
|
364
|
+
for (const manifest of [...builtins, ...structuredFieldTypes]) registerFieldType(manifest);
|
|
183
365
|
|
|
184
366
|
// src/events.ts
|
|
185
367
|
function isPlainObject(value) {
|
|
@@ -668,6 +850,10 @@ function domainOf(field, extra, maxOptions) {
|
|
|
668
850
|
extra = extra.map((v) => typeof v === "string" ? [v] : v);
|
|
669
851
|
break;
|
|
670
852
|
}
|
|
853
|
+
case "list":
|
|
854
|
+
base = getFieldType(field.type)?.samples?.(field) ?? [];
|
|
855
|
+
extra = extra.map((v) => typeof v === "string" ? [v] : v);
|
|
856
|
+
break;
|
|
671
857
|
case "email":
|
|
672
858
|
base = ["ada@example.com"];
|
|
673
859
|
break;
|
|
@@ -781,7 +967,7 @@ function shown(v) {
|
|
|
781
967
|
text ??= typeof v === "bigint" ? `${v}n` : typeof v;
|
|
782
968
|
return text.length > 60 ? `${text.slice(0, 57)}...` : text;
|
|
783
969
|
}
|
|
784
|
-
var
|
|
970
|
+
var isObject2 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
785
971
|
var isInt = (v) => typeof v === "number" && Number.isInteger(v);
|
|
786
972
|
var LITERAL_TYPE = {
|
|
787
973
|
"short-text": "string",
|
|
@@ -792,7 +978,8 @@ var LITERAL_TYPE = {
|
|
|
792
978
|
nps: "number",
|
|
793
979
|
"single-choice": "string",
|
|
794
980
|
"multi-choice": "string",
|
|
795
|
-
"yes-no": "boolean"
|
|
981
|
+
"yes-no": "boolean",
|
|
982
|
+
list: "string"
|
|
796
983
|
};
|
|
797
984
|
var ORDERING = /* @__PURE__ */ new Set(["lt", "lte", "gt", "gte"]);
|
|
798
985
|
var OPERATORS = /* @__PURE__ */ new Set([
|
|
@@ -834,7 +1021,48 @@ function parseForm(input) {
|
|
|
834
1021
|
}
|
|
835
1022
|
return true;
|
|
836
1023
|
};
|
|
837
|
-
|
|
1024
|
+
const optionalInt = (v, path, floor) => {
|
|
1025
|
+
if (v !== void 0 && !(isInt(v) && v >= floor)) issue(path, `Must be a whole number of ${floor} or more.`);
|
|
1026
|
+
};
|
|
1027
|
+
const ordered = (low, high, path, what) => {
|
|
1028
|
+
if (isInt(low) && isInt(high) && low > high) issue(path, `Can't be more than the ${what}.`);
|
|
1029
|
+
};
|
|
1030
|
+
const structuredProps = (type, props, path) => {
|
|
1031
|
+
if (type === "list") {
|
|
1032
|
+
optionalInt(props.min, `${path}.min`, 0);
|
|
1033
|
+
optionalInt(props.max, `${path}.max`, 1);
|
|
1034
|
+
optionalInt(props.itemMaxLength, `${path}.itemMaxLength`, 1);
|
|
1035
|
+
optionalText(props.placeholder, `${path}.placeholder`);
|
|
1036
|
+
ordered(props.min, props.max ?? 10, `${path}.min`, "most items");
|
|
1037
|
+
} else if (type === "table") {
|
|
1038
|
+
const columns = props.columns;
|
|
1039
|
+
if (!Array.isArray(columns) || columns.length < 1 || columns.length > MAX_TABLE_COLUMNS) {
|
|
1040
|
+
issue(`${path}.columns`, `Must be a list of 1 to ${MAX_TABLE_COLUMNS} columns.`);
|
|
1041
|
+
} else {
|
|
1042
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1043
|
+
columns.forEach((c, j) => {
|
|
1044
|
+
const at = `${path}.columns[${j}]`;
|
|
1045
|
+
if (!isObject2(c)) return issue(at, "Each column needs a key and a label.");
|
|
1046
|
+
if (name(c.key, `${at}.key`, "A column key")) {
|
|
1047
|
+
if (seen.has(c.key)) issue(`${at}.key`, `Duplicate column key "${c.key}".`);
|
|
1048
|
+
seen.add(c.key);
|
|
1049
|
+
}
|
|
1050
|
+
text(c.label, `${at}.label`);
|
|
1051
|
+
optionalBoolean(c.long, `${at}.long`);
|
|
1052
|
+
});
|
|
1053
|
+
}
|
|
1054
|
+
optionalInt(props.minRows, `${path}.minRows`, 0);
|
|
1055
|
+
optionalInt(props.maxRows, `${path}.maxRows`, 1);
|
|
1056
|
+
ordered(props.minRows, props.maxRows ?? 3, `${path}.minRows`, "most rows");
|
|
1057
|
+
} else if (type === "media-notes") {
|
|
1058
|
+
optionalInt(props.maxNotes, `${path}.maxNotes`, 1);
|
|
1059
|
+
optionalInt(props.textMaxLength, `${path}.textMaxLength`, 1);
|
|
1060
|
+
} else if (type === "text-highlights") {
|
|
1061
|
+
optionalText(props.text, `${path}.text`);
|
|
1062
|
+
optionalInt(props.maxHighlights, `${path}.maxHighlights`, 1);
|
|
1063
|
+
}
|
|
1064
|
+
};
|
|
1065
|
+
if (!isObject2(input)) {
|
|
838
1066
|
issue("", "A form definition must be an object.");
|
|
839
1067
|
return fail();
|
|
840
1068
|
}
|
|
@@ -843,19 +1071,19 @@ function parseForm(input) {
|
|
|
843
1071
|
text(input.title, "title");
|
|
844
1072
|
optionalText(input.description, "description");
|
|
845
1073
|
if (input.layout !== void 0) {
|
|
846
|
-
if (!
|
|
1074
|
+
if (!isObject2(input.layout)) issue("layout", "Must be an object.");
|
|
847
1075
|
else if (input.layout.mode !== "focus" && input.layout.mode !== "classic") {
|
|
848
1076
|
issue("layout.mode", "Must be focus or classic.");
|
|
849
1077
|
}
|
|
850
1078
|
}
|
|
851
1079
|
if (input.settings !== void 0) {
|
|
852
|
-
if (!
|
|
1080
|
+
if (!isObject2(input.settings)) issue("settings", "Must be an object.");
|
|
853
1081
|
else {
|
|
854
1082
|
optionalBoolean(input.settings.anonymous, "settings.anonymous");
|
|
855
1083
|
optionalBoolean(input.settings.clearHiddenValues, "settings.clearHiddenValues");
|
|
856
1084
|
}
|
|
857
1085
|
}
|
|
858
|
-
if (input.meta !== void 0 && (!
|
|
1086
|
+
if (input.meta !== void 0 && (!isObject2(input.meta) || !isJson(input.meta))) {
|
|
859
1087
|
issue("meta", "Must be a JSON object.");
|
|
860
1088
|
}
|
|
861
1089
|
if (!Array.isArray(input.blocks)) {
|
|
@@ -870,7 +1098,7 @@ function parseForm(input) {
|
|
|
870
1098
|
const choices = /* @__PURE__ */ new Map();
|
|
871
1099
|
input.blocks.forEach((block, i) => {
|
|
872
1100
|
const path = `blocks[${i}]`;
|
|
873
|
-
if (!
|
|
1101
|
+
if (!isObject2(block) || typeof block.id !== "string" || !block.id) {
|
|
874
1102
|
issue(path, "Every block needs a string id.");
|
|
875
1103
|
return;
|
|
876
1104
|
}
|
|
@@ -891,29 +1119,30 @@ function parseForm(input) {
|
|
|
891
1119
|
issue(`${path}.type`, `Unknown field type "${String(block.type)}".`);
|
|
892
1120
|
}
|
|
893
1121
|
if (block.props !== void 0) {
|
|
894
|
-
if (!
|
|
1122
|
+
if (!isObject2(block.props) || !isJson(block.props)) issue(`${path}.props`, "Must be a JSON object.");
|
|
895
1123
|
else if (block.props.options !== void 0) {
|
|
896
1124
|
const options = block.props.options;
|
|
897
1125
|
if (!Array.isArray(options)) issue(`${path}.props.options`, "Must be an array.");
|
|
898
1126
|
else {
|
|
899
1127
|
options.forEach((o, j) => {
|
|
900
1128
|
const at = `${path}.props.options[${j}]`;
|
|
901
|
-
if (!
|
|
1129
|
+
if (!isObject2(o) || typeof o.value !== "string") issue(at, "Each option needs a string value.");
|
|
902
1130
|
else optionalText(o.label, `${at}.label`);
|
|
903
1131
|
});
|
|
904
1132
|
choices.set(
|
|
905
1133
|
block.id,
|
|
906
|
-
new Set(options.flatMap((o) =>
|
|
1134
|
+
new Set(options.flatMap((o) => isObject2(o) && typeof o.value === "string" ? [o.value] : []))
|
|
907
1135
|
);
|
|
908
1136
|
}
|
|
909
1137
|
}
|
|
910
|
-
const { max, maxLength: maxLength2 } =
|
|
1138
|
+
const { max, maxLength: maxLength2 } = isObject2(block.props) ? block.props : {};
|
|
911
1139
|
if (block.type === "rating" && max !== void 0 && !(isInt(max) && max >= 2 && max <= 10)) {
|
|
912
1140
|
issue(`${path}.props.max`, "Must be a whole number from 2 to 10.");
|
|
913
1141
|
}
|
|
914
1142
|
if (maxLength2 !== void 0 && !(isInt(maxLength2) && maxLength2 > 0)) {
|
|
915
1143
|
issue(`${path}.props.maxLength`, "Must be a whole number above 0.");
|
|
916
1144
|
}
|
|
1145
|
+
if (isObject2(block.props) && typeof block.type === "string") structuredProps(block.type, block.props, `${path}.props`);
|
|
917
1146
|
}
|
|
918
1147
|
if (validKey) {
|
|
919
1148
|
if (keys.has(block.key)) issue(`${path}.key`, `Duplicate key "${String(block.key)}".`);
|
|
@@ -933,7 +1162,7 @@ function parseForm(input) {
|
|
|
933
1162
|
const hiddenTypes = /* @__PURE__ */ new Map();
|
|
934
1163
|
if (input.hidden !== void 0 && !Array.isArray(input.hidden)) issue("hidden", "Must be an array.");
|
|
935
1164
|
(Array.isArray(input.hidden) ? input.hidden : []).forEach((h, i) => {
|
|
936
|
-
if (!
|
|
1165
|
+
if (!isObject2(h) || typeof h.name !== "string" || !h.name) {
|
|
937
1166
|
issue(`hidden[${i}]`, "Each hidden input needs a name.");
|
|
938
1167
|
return;
|
|
939
1168
|
}
|
|
@@ -954,7 +1183,7 @@ function parseForm(input) {
|
|
|
954
1183
|
if (input.endings !== void 0 && !Array.isArray(input.endings)) issue("endings", "Must be an array.");
|
|
955
1184
|
(Array.isArray(input.endings) ? input.endings : []).forEach((e, i) => {
|
|
956
1185
|
const path = `endings[${i}]`;
|
|
957
|
-
if (!
|
|
1186
|
+
if (!isObject2(e)) {
|
|
958
1187
|
issue(path, "Each ending must be an object with an id and a title.");
|
|
959
1188
|
return;
|
|
960
1189
|
}
|
|
@@ -966,7 +1195,7 @@ function parseForm(input) {
|
|
|
966
1195
|
});
|
|
967
1196
|
const checkOperand = (o, path, reads) => {
|
|
968
1197
|
if (o === void 0) return;
|
|
969
|
-
if (!
|
|
1198
|
+
if (!isObject2(o)) return issue(path, "An operand must be an object.");
|
|
970
1199
|
if ("field" in o) {
|
|
971
1200
|
if (typeof o.field !== "string" || !fieldTypes.has(o.field)) return issue(path, `No field "${String(o.field)}".`);
|
|
972
1201
|
reads.push(o.field);
|
|
@@ -979,7 +1208,7 @@ function parseForm(input) {
|
|
|
979
1208
|
}
|
|
980
1209
|
};
|
|
981
1210
|
const checkCondition = (c, path, reads, depth = 0) => {
|
|
982
|
-
if (!
|
|
1211
|
+
if (!isObject2(c)) return issue(path, "A condition must be an object.");
|
|
983
1212
|
if (depth > MAX_CONDITION_DEPTH) return issue(path, "The condition is nested too deeply.");
|
|
984
1213
|
if ("all" in c || "any" in c) {
|
|
985
1214
|
const group = "all" in c ? "all" : "any";
|
|
@@ -988,7 +1217,7 @@ function parseForm(input) {
|
|
|
988
1217
|
return list.forEach((x, i) => checkCondition(x, `${path}.${group}[${i}]`, reads, depth + 1));
|
|
989
1218
|
}
|
|
990
1219
|
if ("not" in c) return checkCondition(c.not, `${path}.not`, reads, depth + 1);
|
|
991
|
-
if (typeof c.op !== "string" || !
|
|
1220
|
+
if (typeof c.op !== "string" || !isObject2(c.left)) {
|
|
992
1221
|
return issue(path, "A comparison needs an `op` and a `left` operand.");
|
|
993
1222
|
}
|
|
994
1223
|
if (!OPERATORS.has(c.op)) issue(`${path}.op`, `Unknown operator "${c.op}".`);
|
|
@@ -1004,7 +1233,7 @@ function parseForm(input) {
|
|
|
1004
1233
|
};
|
|
1005
1234
|
const checkLiterals = (op, left, right, path) => {
|
|
1006
1235
|
if (op === "answered" || op === "empty" || !OPERATORS.has(op)) return;
|
|
1007
|
-
const literal =
|
|
1236
|
+
const literal = isObject2(right) && "value" in right;
|
|
1008
1237
|
if (right !== void 0 && !literal) return;
|
|
1009
1238
|
let what = "the left side";
|
|
1010
1239
|
let type;
|
|
@@ -1043,7 +1272,7 @@ function parseForm(input) {
|
|
|
1043
1272
|
if (input.logic !== void 0 && !Array.isArray(input.logic)) issue("logic", "Must be an array.");
|
|
1044
1273
|
(Array.isArray(input.logic) ? input.logic : []).forEach((rule, i) => {
|
|
1045
1274
|
const path = `logic[${i}]`;
|
|
1046
|
-
if (!
|
|
1275
|
+
if (!isObject2(rule) || typeof rule.id !== "string" || !isObject2(rule.when)) {
|
|
1047
1276
|
issue(path, "Each rule needs an id and a `when` condition.");
|
|
1048
1277
|
return;
|
|
1049
1278
|
}
|
|
@@ -1062,7 +1291,7 @@ function parseForm(input) {
|
|
|
1062
1291
|
at = rule.from;
|
|
1063
1292
|
inclusive = true;
|
|
1064
1293
|
}
|
|
1065
|
-
if (
|
|
1294
|
+
if (isObject2(rule.to)) {
|
|
1066
1295
|
if (typeof rule.to.ending !== "string" || !endingIds.has(rule.to.ending)) {
|
|
1067
1296
|
issue(`${path}.to`, `No ending "${String(rule.to.ending)}".`);
|
|
1068
1297
|
}
|
|
@@ -1094,7 +1323,7 @@ function parseForm(input) {
|
|
|
1094
1323
|
}
|
|
1095
1324
|
});
|
|
1096
1325
|
input.blocks.forEach((block, i) => {
|
|
1097
|
-
if (!
|
|
1326
|
+
if (!isObject2(block)) return;
|
|
1098
1327
|
const raw = block.kind === "field" ? [block.label, block.description] : block.kind === "content" ? [block.text] : [];
|
|
1099
1328
|
const texts = raw.filter((t) => typeof t === "string");
|
|
1100
1329
|
for (const t of texts) {
|
|
@@ -1114,7 +1343,7 @@ function parseForm(input) {
|
|
|
1114
1343
|
});
|
|
1115
1344
|
const extra = [["description", input.description]];
|
|
1116
1345
|
(Array.isArray(input.endings) ? input.endings : []).forEach((e, i) => {
|
|
1117
|
-
if (
|
|
1346
|
+
if (isObject2(e)) extra.push([`endings[${i}]`, e.title], [`endings[${i}]`, e.description]);
|
|
1118
1347
|
});
|
|
1119
1348
|
for (const [path, t] of extra) {
|
|
1120
1349
|
if (typeof t !== "string") continue;
|