@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.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,29 +942,30 @@ 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
|
}
|
|
741
965
|
if (maxLength2 !== void 0 && !(isInt(maxLength2) && maxLength2 > 0)) {
|
|
742
966
|
issue(`${path}.props.maxLength`, "Must be a whole number above 0.");
|
|
743
967
|
}
|
|
968
|
+
if (isObject2(block.props) && typeof block.type === "string") structuredProps(block.type, block.props, `${path}.props`);
|
|
744
969
|
}
|
|
745
970
|
if (validKey) {
|
|
746
971
|
if (keys.has(block.key)) issue(`${path}.key`, `Duplicate key "${String(block.key)}".`);
|
|
@@ -760,7 +985,7 @@ function parseForm(input) {
|
|
|
760
985
|
const hiddenTypes = /* @__PURE__ */ new Map();
|
|
761
986
|
if (input.hidden !== void 0 && !Array.isArray(input.hidden)) issue("hidden", "Must be an array.");
|
|
762
987
|
(Array.isArray(input.hidden) ? input.hidden : []).forEach((h, i) => {
|
|
763
|
-
if (!
|
|
988
|
+
if (!isObject2(h) || typeof h.name !== "string" || !h.name) {
|
|
764
989
|
issue(`hidden[${i}]`, "Each hidden input needs a name.");
|
|
765
990
|
return;
|
|
766
991
|
}
|
|
@@ -781,7 +1006,7 @@ function parseForm(input) {
|
|
|
781
1006
|
if (input.endings !== void 0 && !Array.isArray(input.endings)) issue("endings", "Must be an array.");
|
|
782
1007
|
(Array.isArray(input.endings) ? input.endings : []).forEach((e, i) => {
|
|
783
1008
|
const path = `endings[${i}]`;
|
|
784
|
-
if (!
|
|
1009
|
+
if (!isObject2(e)) {
|
|
785
1010
|
issue(path, "Each ending must be an object with an id and a title.");
|
|
786
1011
|
return;
|
|
787
1012
|
}
|
|
@@ -793,7 +1018,7 @@ function parseForm(input) {
|
|
|
793
1018
|
});
|
|
794
1019
|
const checkOperand = (o, path, reads) => {
|
|
795
1020
|
if (o === void 0) return;
|
|
796
|
-
if (!
|
|
1021
|
+
if (!isObject2(o)) return issue(path, "An operand must be an object.");
|
|
797
1022
|
if ("field" in o) {
|
|
798
1023
|
if (typeof o.field !== "string" || !fieldTypes.has(o.field)) return issue(path, `No field "${String(o.field)}".`);
|
|
799
1024
|
reads.push(o.field);
|
|
@@ -806,7 +1031,7 @@ function parseForm(input) {
|
|
|
806
1031
|
}
|
|
807
1032
|
};
|
|
808
1033
|
const checkCondition = (c, path, reads, depth = 0) => {
|
|
809
|
-
if (!
|
|
1034
|
+
if (!isObject2(c)) return issue(path, "A condition must be an object.");
|
|
810
1035
|
if (depth > MAX_CONDITION_DEPTH) return issue(path, "The condition is nested too deeply.");
|
|
811
1036
|
if ("all" in c || "any" in c) {
|
|
812
1037
|
const group = "all" in c ? "all" : "any";
|
|
@@ -815,7 +1040,7 @@ function parseForm(input) {
|
|
|
815
1040
|
return list.forEach((x, i) => checkCondition(x, `${path}.${group}[${i}]`, reads, depth + 1));
|
|
816
1041
|
}
|
|
817
1042
|
if ("not" in c) return checkCondition(c.not, `${path}.not`, reads, depth + 1);
|
|
818
|
-
if (typeof c.op !== "string" || !
|
|
1043
|
+
if (typeof c.op !== "string" || !isObject2(c.left)) {
|
|
819
1044
|
return issue(path, "A comparison needs an `op` and a `left` operand.");
|
|
820
1045
|
}
|
|
821
1046
|
if (!OPERATORS.has(c.op)) issue(`${path}.op`, `Unknown operator "${c.op}".`);
|
|
@@ -831,7 +1056,7 @@ function parseForm(input) {
|
|
|
831
1056
|
};
|
|
832
1057
|
const checkLiterals = (op, left, right, path) => {
|
|
833
1058
|
if (op === "answered" || op === "empty" || !OPERATORS.has(op)) return;
|
|
834
|
-
const literal =
|
|
1059
|
+
const literal = isObject2(right) && "value" in right;
|
|
835
1060
|
if (right !== void 0 && !literal) return;
|
|
836
1061
|
let what = "the left side";
|
|
837
1062
|
let type;
|
|
@@ -870,7 +1095,7 @@ function parseForm(input) {
|
|
|
870
1095
|
if (input.logic !== void 0 && !Array.isArray(input.logic)) issue("logic", "Must be an array.");
|
|
871
1096
|
(Array.isArray(input.logic) ? input.logic : []).forEach((rule, i) => {
|
|
872
1097
|
const path = `logic[${i}]`;
|
|
873
|
-
if (!
|
|
1098
|
+
if (!isObject2(rule) || typeof rule.id !== "string" || !isObject2(rule.when)) {
|
|
874
1099
|
issue(path, "Each rule needs an id and a `when` condition.");
|
|
875
1100
|
return;
|
|
876
1101
|
}
|
|
@@ -889,7 +1114,7 @@ function parseForm(input) {
|
|
|
889
1114
|
at = rule.from;
|
|
890
1115
|
inclusive = true;
|
|
891
1116
|
}
|
|
892
|
-
if (
|
|
1117
|
+
if (isObject2(rule.to)) {
|
|
893
1118
|
if (typeof rule.to.ending !== "string" || !endingIds.has(rule.to.ending)) {
|
|
894
1119
|
issue(`${path}.to`, `No ending "${String(rule.to.ending)}".`);
|
|
895
1120
|
}
|
|
@@ -921,7 +1146,7 @@ function parseForm(input) {
|
|
|
921
1146
|
}
|
|
922
1147
|
});
|
|
923
1148
|
input.blocks.forEach((block, i) => {
|
|
924
|
-
if (!
|
|
1149
|
+
if (!isObject2(block)) return;
|
|
925
1150
|
const raw = block.kind === "field" ? [block.label, block.description] : block.kind === "content" ? [block.text] : [];
|
|
926
1151
|
const texts = raw.filter((t) => typeof t === "string");
|
|
927
1152
|
for (const t of texts) {
|
|
@@ -941,7 +1166,7 @@ function parseForm(input) {
|
|
|
941
1166
|
});
|
|
942
1167
|
const extra = [["description", input.description]];
|
|
943
1168
|
(Array.isArray(input.endings) ? input.endings : []).forEach((e, i) => {
|
|
944
|
-
if (
|
|
1169
|
+
if (isObject2(e)) extra.push([`endings[${i}]`, e.title], [`endings[${i}]`, e.description]);
|
|
945
1170
|
});
|
|
946
1171
|
for (const [path, t] of extra) {
|
|
947
1172
|
if (typeof t !== "string") continue;
|