@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.d.cts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { J as Json,
|
|
1
|
+
import { J as Json, e as FormEvent, F as FormDefinition } from './types-BBKBQ8sp.cjs';
|
|
2
|
+
import { F as FieldValidator } from './validate-BXXqi0fq.cjs';
|
|
2
3
|
|
|
3
4
|
interface SubmissionInput {
|
|
4
5
|
/** Answers keyed by field id. Ignored when `events` is given. */
|
|
@@ -9,7 +10,7 @@ interface SubmissionInput {
|
|
|
9
10
|
hidden?: Record<string, Json> | undefined;
|
|
10
11
|
}
|
|
11
12
|
interface SubmissionOptions {
|
|
12
|
-
/** Values for `source: "server"` inputs, resolved by the server (
|
|
13
|
+
/** Values for `source: "server"` inputs, resolved by the server (account id, location, user id…). */
|
|
13
14
|
context?: Record<string, Json> | undefined;
|
|
14
15
|
/** `false` checks a draft: formats only, nothing required. Default true. */
|
|
15
16
|
complete?: boolean | undefined;
|
package/dist/server.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { J as Json,
|
|
1
|
+
import { J as Json, e as FormEvent, F as FormDefinition } from './types-BBKBQ8sp.js';
|
|
2
|
+
import { F as FieldValidator } from './validate-BjNwF1UE.js';
|
|
2
3
|
|
|
3
4
|
interface SubmissionInput {
|
|
4
5
|
/** Answers keyed by field id. Ignored when `events` is given. */
|
|
@@ -9,7 +10,7 @@ interface SubmissionInput {
|
|
|
9
10
|
hidden?: Record<string, Json> | undefined;
|
|
10
11
|
}
|
|
11
12
|
interface SubmissionOptions {
|
|
12
|
-
/** Values for `source: "server"` inputs, resolved by the server (
|
|
13
|
+
/** Values for `source: "server"` inputs, resolved by the server (account id, location, user id…). */
|
|
13
14
|
context?: Record<string, Json> | undefined;
|
|
14
15
|
/** `false` checks a draft: formats only, nothing required. Default true. */
|
|
15
16
|
complete?: boolean | undefined;
|
package/dist/server.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) {
|
|
@@ -608,7 +790,7 @@ function shown(v) {
|
|
|
608
790
|
text ??= typeof v === "bigint" ? `${v}n` : typeof v;
|
|
609
791
|
return text.length > 60 ? `${text.slice(0, 57)}...` : text;
|
|
610
792
|
}
|
|
611
|
-
var
|
|
793
|
+
var isObject2 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
612
794
|
var isInt = (v) => typeof v === "number" && Number.isInteger(v);
|
|
613
795
|
var LITERAL_TYPE = {
|
|
614
796
|
"short-text": "string",
|
|
@@ -619,7 +801,8 @@ var LITERAL_TYPE = {
|
|
|
619
801
|
nps: "number",
|
|
620
802
|
"single-choice": "string",
|
|
621
803
|
"multi-choice": "string",
|
|
622
|
-
"yes-no": "boolean"
|
|
804
|
+
"yes-no": "boolean",
|
|
805
|
+
list: "string"
|
|
623
806
|
};
|
|
624
807
|
var ORDERING = /* @__PURE__ */ new Set(["lt", "lte", "gt", "gte"]);
|
|
625
808
|
var OPERATORS = /* @__PURE__ */ new Set([
|
|
@@ -661,7 +844,48 @@ function parseForm(input) {
|
|
|
661
844
|
}
|
|
662
845
|
return true;
|
|
663
846
|
};
|
|
664
|
-
|
|
847
|
+
const optionalInt = (v, path, floor) => {
|
|
848
|
+
if (v !== void 0 && !(isInt(v) && v >= floor)) issue(path, `Must be a whole number of ${floor} or more.`);
|
|
849
|
+
};
|
|
850
|
+
const ordered = (low, high, path, what) => {
|
|
851
|
+
if (isInt(low) && isInt(high) && low > high) issue(path, `Can't be more than the ${what}.`);
|
|
852
|
+
};
|
|
853
|
+
const structuredProps = (type, props, path) => {
|
|
854
|
+
if (type === "list") {
|
|
855
|
+
optionalInt(props.min, `${path}.min`, 0);
|
|
856
|
+
optionalInt(props.max, `${path}.max`, 1);
|
|
857
|
+
optionalInt(props.itemMaxLength, `${path}.itemMaxLength`, 1);
|
|
858
|
+
optionalText(props.placeholder, `${path}.placeholder`);
|
|
859
|
+
ordered(props.min, props.max ?? 10, `${path}.min`, "most items");
|
|
860
|
+
} else if (type === "table") {
|
|
861
|
+
const columns = props.columns;
|
|
862
|
+
if (!Array.isArray(columns) || columns.length < 1 || columns.length > MAX_TABLE_COLUMNS) {
|
|
863
|
+
issue(`${path}.columns`, `Must be a list of 1 to ${MAX_TABLE_COLUMNS} columns.`);
|
|
864
|
+
} else {
|
|
865
|
+
const seen = /* @__PURE__ */ new Set();
|
|
866
|
+
columns.forEach((c, j) => {
|
|
867
|
+
const at = `${path}.columns[${j}]`;
|
|
868
|
+
if (!isObject2(c)) return issue(at, "Each column needs a key and a label.");
|
|
869
|
+
if (name(c.key, `${at}.key`, "A column key")) {
|
|
870
|
+
if (seen.has(c.key)) issue(`${at}.key`, `Duplicate column key "${c.key}".`);
|
|
871
|
+
seen.add(c.key);
|
|
872
|
+
}
|
|
873
|
+
text(c.label, `${at}.label`);
|
|
874
|
+
optionalBoolean(c.long, `${at}.long`);
|
|
875
|
+
});
|
|
876
|
+
}
|
|
877
|
+
optionalInt(props.minRows, `${path}.minRows`, 0);
|
|
878
|
+
optionalInt(props.maxRows, `${path}.maxRows`, 1);
|
|
879
|
+
ordered(props.minRows, props.maxRows ?? 3, `${path}.minRows`, "most rows");
|
|
880
|
+
} else if (type === "media-notes") {
|
|
881
|
+
optionalInt(props.maxNotes, `${path}.maxNotes`, 1);
|
|
882
|
+
optionalInt(props.textMaxLength, `${path}.textMaxLength`, 1);
|
|
883
|
+
} else if (type === "text-highlights") {
|
|
884
|
+
optionalText(props.text, `${path}.text`);
|
|
885
|
+
optionalInt(props.maxHighlights, `${path}.maxHighlights`, 1);
|
|
886
|
+
}
|
|
887
|
+
};
|
|
888
|
+
if (!isObject2(input)) {
|
|
665
889
|
issue("", "A form definition must be an object.");
|
|
666
890
|
return fail();
|
|
667
891
|
}
|
|
@@ -670,19 +894,19 @@ function parseForm(input) {
|
|
|
670
894
|
text(input.title, "title");
|
|
671
895
|
optionalText(input.description, "description");
|
|
672
896
|
if (input.layout !== void 0) {
|
|
673
|
-
if (!
|
|
897
|
+
if (!isObject2(input.layout)) issue("layout", "Must be an object.");
|
|
674
898
|
else if (input.layout.mode !== "focus" && input.layout.mode !== "classic") {
|
|
675
899
|
issue("layout.mode", "Must be focus or classic.");
|
|
676
900
|
}
|
|
677
901
|
}
|
|
678
902
|
if (input.settings !== void 0) {
|
|
679
|
-
if (!
|
|
903
|
+
if (!isObject2(input.settings)) issue("settings", "Must be an object.");
|
|
680
904
|
else {
|
|
681
905
|
optionalBoolean(input.settings.anonymous, "settings.anonymous");
|
|
682
906
|
optionalBoolean(input.settings.clearHiddenValues, "settings.clearHiddenValues");
|
|
683
907
|
}
|
|
684
908
|
}
|
|
685
|
-
if (input.meta !== void 0 && (!
|
|
909
|
+
if (input.meta !== void 0 && (!isObject2(input.meta) || !isJson(input.meta))) {
|
|
686
910
|
issue("meta", "Must be a JSON object.");
|
|
687
911
|
}
|
|
688
912
|
if (!Array.isArray(input.blocks)) {
|
|
@@ -697,7 +921,7 @@ function parseForm(input) {
|
|
|
697
921
|
const choices = /* @__PURE__ */ new Map();
|
|
698
922
|
input.blocks.forEach((block, i) => {
|
|
699
923
|
const path = `blocks[${i}]`;
|
|
700
|
-
if (!
|
|
924
|
+
if (!isObject2(block) || typeof block.id !== "string" || !block.id) {
|
|
701
925
|
issue(path, "Every block needs a string id.");
|
|
702
926
|
return;
|
|
703
927
|
}
|
|
@@ -718,23 +942,23 @@ function parseForm(input) {
|
|
|
718
942
|
issue(`${path}.type`, `Unknown field type "${String(block.type)}".`);
|
|
719
943
|
}
|
|
720
944
|
if (block.props !== void 0) {
|
|
721
|
-
if (!
|
|
945
|
+
if (!isObject2(block.props) || !isJson(block.props)) issue(`${path}.props`, "Must be a JSON object.");
|
|
722
946
|
else if (block.props.options !== void 0) {
|
|
723
947
|
const options = block.props.options;
|
|
724
948
|
if (!Array.isArray(options)) issue(`${path}.props.options`, "Must be an array.");
|
|
725
949
|
else {
|
|
726
950
|
options.forEach((o, j) => {
|
|
727
951
|
const at = `${path}.props.options[${j}]`;
|
|
728
|
-
if (!
|
|
952
|
+
if (!isObject2(o) || typeof o.value !== "string") issue(at, "Each option needs a string value.");
|
|
729
953
|
else optionalText(o.label, `${at}.label`);
|
|
730
954
|
});
|
|
731
955
|
choices.set(
|
|
732
956
|
block.id,
|
|
733
|
-
new Set(options.flatMap((o) =>
|
|
957
|
+
new Set(options.flatMap((o) => isObject2(o) && typeof o.value === "string" ? [o.value] : []))
|
|
734
958
|
);
|
|
735
959
|
}
|
|
736
960
|
}
|
|
737
|
-
const { max, maxLength: maxLength2 } =
|
|
961
|
+
const { max, maxLength: maxLength2 } = isObject2(block.props) ? block.props : {};
|
|
738
962
|
if (block.type === "rating" && max !== void 0 && !(isInt(max) && max >= 2 && max <= 10)) {
|
|
739
963
|
issue(`${path}.props.max`, "Must be a whole number from 2 to 10.");
|
|
740
964
|
}
|
|
@@ -742,6 +966,9 @@ function parseForm(input) {
|
|
|
742
966
|
issue(`${path}.props.maxLength`, "Must be a whole number above 0.");
|
|
743
967
|
}
|
|
744
968
|
}
|
|
969
|
+
if (typeof block.type === "string" && (block.props === void 0 || isObject2(block.props))) {
|
|
970
|
+
structuredProps(block.type, block.props ?? {}, `${path}.props`);
|
|
971
|
+
}
|
|
745
972
|
if (validKey) {
|
|
746
973
|
if (keys.has(block.key)) issue(`${path}.key`, `Duplicate key "${String(block.key)}".`);
|
|
747
974
|
keys.set(block.key, block.id);
|
|
@@ -760,7 +987,7 @@ function parseForm(input) {
|
|
|
760
987
|
const hiddenTypes = /* @__PURE__ */ new Map();
|
|
761
988
|
if (input.hidden !== void 0 && !Array.isArray(input.hidden)) issue("hidden", "Must be an array.");
|
|
762
989
|
(Array.isArray(input.hidden) ? input.hidden : []).forEach((h, i) => {
|
|
763
|
-
if (!
|
|
990
|
+
if (!isObject2(h) || typeof h.name !== "string" || !h.name) {
|
|
764
991
|
issue(`hidden[${i}]`, "Each hidden input needs a name.");
|
|
765
992
|
return;
|
|
766
993
|
}
|
|
@@ -781,7 +1008,7 @@ function parseForm(input) {
|
|
|
781
1008
|
if (input.endings !== void 0 && !Array.isArray(input.endings)) issue("endings", "Must be an array.");
|
|
782
1009
|
(Array.isArray(input.endings) ? input.endings : []).forEach((e, i) => {
|
|
783
1010
|
const path = `endings[${i}]`;
|
|
784
|
-
if (!
|
|
1011
|
+
if (!isObject2(e)) {
|
|
785
1012
|
issue(path, "Each ending must be an object with an id and a title.");
|
|
786
1013
|
return;
|
|
787
1014
|
}
|
|
@@ -793,7 +1020,7 @@ function parseForm(input) {
|
|
|
793
1020
|
});
|
|
794
1021
|
const checkOperand = (o, path, reads) => {
|
|
795
1022
|
if (o === void 0) return;
|
|
796
|
-
if (!
|
|
1023
|
+
if (!isObject2(o)) return issue(path, "An operand must be an object.");
|
|
797
1024
|
if ("field" in o) {
|
|
798
1025
|
if (typeof o.field !== "string" || !fieldTypes.has(o.field)) return issue(path, `No field "${String(o.field)}".`);
|
|
799
1026
|
reads.push(o.field);
|
|
@@ -806,7 +1033,7 @@ function parseForm(input) {
|
|
|
806
1033
|
}
|
|
807
1034
|
};
|
|
808
1035
|
const checkCondition = (c, path, reads, depth = 0) => {
|
|
809
|
-
if (!
|
|
1036
|
+
if (!isObject2(c)) return issue(path, "A condition must be an object.");
|
|
810
1037
|
if (depth > MAX_CONDITION_DEPTH) return issue(path, "The condition is nested too deeply.");
|
|
811
1038
|
if ("all" in c || "any" in c) {
|
|
812
1039
|
const group = "all" in c ? "all" : "any";
|
|
@@ -815,7 +1042,7 @@ function parseForm(input) {
|
|
|
815
1042
|
return list.forEach((x, i) => checkCondition(x, `${path}.${group}[${i}]`, reads, depth + 1));
|
|
816
1043
|
}
|
|
817
1044
|
if ("not" in c) return checkCondition(c.not, `${path}.not`, reads, depth + 1);
|
|
818
|
-
if (typeof c.op !== "string" || !
|
|
1045
|
+
if (typeof c.op !== "string" || !isObject2(c.left)) {
|
|
819
1046
|
return issue(path, "A comparison needs an `op` and a `left` operand.");
|
|
820
1047
|
}
|
|
821
1048
|
if (!OPERATORS.has(c.op)) issue(`${path}.op`, `Unknown operator "${c.op}".`);
|
|
@@ -831,7 +1058,7 @@ function parseForm(input) {
|
|
|
831
1058
|
};
|
|
832
1059
|
const checkLiterals = (op, left, right, path) => {
|
|
833
1060
|
if (op === "answered" || op === "empty" || !OPERATORS.has(op)) return;
|
|
834
|
-
const literal =
|
|
1061
|
+
const literal = isObject2(right) && "value" in right;
|
|
835
1062
|
if (right !== void 0 && !literal) return;
|
|
836
1063
|
let what = "the left side";
|
|
837
1064
|
let type;
|
|
@@ -870,7 +1097,7 @@ function parseForm(input) {
|
|
|
870
1097
|
if (input.logic !== void 0 && !Array.isArray(input.logic)) issue("logic", "Must be an array.");
|
|
871
1098
|
(Array.isArray(input.logic) ? input.logic : []).forEach((rule, i) => {
|
|
872
1099
|
const path = `logic[${i}]`;
|
|
873
|
-
if (!
|
|
1100
|
+
if (!isObject2(rule) || typeof rule.id !== "string" || !isObject2(rule.when)) {
|
|
874
1101
|
issue(path, "Each rule needs an id and a `when` condition.");
|
|
875
1102
|
return;
|
|
876
1103
|
}
|
|
@@ -889,7 +1116,7 @@ function parseForm(input) {
|
|
|
889
1116
|
at = rule.from;
|
|
890
1117
|
inclusive = true;
|
|
891
1118
|
}
|
|
892
|
-
if (
|
|
1119
|
+
if (isObject2(rule.to)) {
|
|
893
1120
|
if (typeof rule.to.ending !== "string" || !endingIds.has(rule.to.ending)) {
|
|
894
1121
|
issue(`${path}.to`, `No ending "${String(rule.to.ending)}".`);
|
|
895
1122
|
}
|
|
@@ -921,7 +1148,7 @@ function parseForm(input) {
|
|
|
921
1148
|
}
|
|
922
1149
|
});
|
|
923
1150
|
input.blocks.forEach((block, i) => {
|
|
924
|
-
if (!
|
|
1151
|
+
if (!isObject2(block)) return;
|
|
925
1152
|
const raw = block.kind === "field" ? [block.label, block.description] : block.kind === "content" ? [block.text] : [];
|
|
926
1153
|
const texts = raw.filter((t) => typeof t === "string");
|
|
927
1154
|
for (const t of texts) {
|
|
@@ -941,7 +1168,7 @@ function parseForm(input) {
|
|
|
941
1168
|
});
|
|
942
1169
|
const extra = [["description", input.description]];
|
|
943
1170
|
(Array.isArray(input.endings) ? input.endings : []).forEach((e, i) => {
|
|
944
|
-
if (
|
|
1171
|
+
if (isObject2(e)) extra.push([`endings[${i}]`, e.title], [`endings[${i}]`, e.description]);
|
|
945
1172
|
});
|
|
946
1173
|
for (const [path, t] of extra) {
|
|
947
1174
|
if (typeof t !== "string") continue;
|