@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/index.js
CHANGED
|
@@ -60,6 +60,211 @@ function isFormEvent(value) {
|
|
|
60
60
|
return typeof value.field === "string" && (value.value === void 0 || isJson(value.value)) && (value.source === void 0 || value.source === "prefill");
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
// src/structured.ts
|
|
64
|
+
var isObject = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
65
|
+
function intProp(field2, name, fallback, floor = 1) {
|
|
66
|
+
const v = field2.props?.[name];
|
|
67
|
+
return typeof v === "number" && Number.isInteger(v) && v >= floor ? v : fallback;
|
|
68
|
+
}
|
|
69
|
+
function optionalIntProp(field2, name, floor = 0) {
|
|
70
|
+
const v = field2.props?.[name];
|
|
71
|
+
return typeof v === "number" && Number.isInteger(v) && v >= floor ? v : void 0;
|
|
72
|
+
}
|
|
73
|
+
var blank = (v) => typeof v !== "string" || v.trim() === "";
|
|
74
|
+
var blankText = (v) => typeof v === "string" && v.trim() === "";
|
|
75
|
+
var emptyList = (value, isBlank) => value === void 0 || value === null || Array.isArray(value) && value.every(isBlank);
|
|
76
|
+
var plural = (n, one, many = `${one}s`) => `${n} ${n === 1 ? one : many}`;
|
|
77
|
+
function listLimits(field2) {
|
|
78
|
+
return { min: optionalIntProp(field2, "min") ?? 0, max: intProp(field2, "max", 10), itemMaxLength: intProp(field2, "itemMaxLength", 500) };
|
|
79
|
+
}
|
|
80
|
+
var listItems = (value) => Array.isArray(value) ? value.filter((v) => typeof v === "string" && v.trim() !== "") : [];
|
|
81
|
+
var MAX_TABLE_COLUMNS = 6;
|
|
82
|
+
var MAX_TABLE_CELL_LENGTH = 2e3;
|
|
83
|
+
function tableColumns(field2) {
|
|
84
|
+
const raw = field2.props?.columns;
|
|
85
|
+
if (!Array.isArray(raw)) return [];
|
|
86
|
+
const seen = /* @__PURE__ */ new Set();
|
|
87
|
+
const out = [];
|
|
88
|
+
for (const c of raw) {
|
|
89
|
+
if (!isObject(c) || typeof c.key !== "string" || !c.key || seen.has(c.key)) continue;
|
|
90
|
+
seen.add(c.key);
|
|
91
|
+
const column = { key: c.key, label: typeof c.label === "string" ? c.label : c.key };
|
|
92
|
+
if (c.long === true) column.long = true;
|
|
93
|
+
out.push(column);
|
|
94
|
+
}
|
|
95
|
+
return out;
|
|
96
|
+
}
|
|
97
|
+
function tableLimits(field2) {
|
|
98
|
+
return { minRows: optionalIntProp(field2, "minRows") ?? 0, maxRows: intProp(field2, "maxRows", 3) };
|
|
99
|
+
}
|
|
100
|
+
var filledRows = (value) => Array.isArray(value) ? value.filter((r) => isObject(r) && Object.values(r).some((c) => !blank(c))) : [];
|
|
101
|
+
var MAX_TIMESTAMP_SECONDS = 86400;
|
|
102
|
+
function formatTimestamp(seconds) {
|
|
103
|
+
const total = Number.isFinite(seconds) && seconds > 0 ? Math.floor(seconds) : 0;
|
|
104
|
+
const h = Math.floor(total / 3600);
|
|
105
|
+
const m = Math.floor(total % 3600 / 60);
|
|
106
|
+
const s = String(total % 60).padStart(2, "0");
|
|
107
|
+
return h ? `${h}:${String(m).padStart(2, "0")}:${s}` : `${m}:${s}`;
|
|
108
|
+
}
|
|
109
|
+
function parseTimestamp(text) {
|
|
110
|
+
const t = text.trim();
|
|
111
|
+
if (!t) return null;
|
|
112
|
+
const parts = t.split(":");
|
|
113
|
+
if (parts.length > 3) return null;
|
|
114
|
+
if (parts.length === 1) {
|
|
115
|
+
if (!/^\d+(\.\d+)?$/.test(t)) return null;
|
|
116
|
+
const n = Number(t);
|
|
117
|
+
return n <= MAX_TIMESTAMP_SECONDS ? n : null;
|
|
118
|
+
}
|
|
119
|
+
const last = parts.length - 1;
|
|
120
|
+
let total = 0;
|
|
121
|
+
for (let i = 0; i < parts.length; i++) {
|
|
122
|
+
const p = parts[i];
|
|
123
|
+
const ok = i === 0 ? /^\d+$/.test(p) : i === last ? /^\d{2}(\.\d+)?$/.test(p) : /^\d{2}$/.test(p);
|
|
124
|
+
if (!ok) return null;
|
|
125
|
+
const n = Number(p);
|
|
126
|
+
if (i > 0 && n >= 60) return null;
|
|
127
|
+
total = total * 60 + n;
|
|
128
|
+
}
|
|
129
|
+
return total <= MAX_TIMESTAMP_SECONDS ? total : null;
|
|
130
|
+
}
|
|
131
|
+
var isTimestamp = (v) => typeof v === "number" && Number.isFinite(v) && v >= 0 && v <= MAX_TIMESTAMP_SECONDS;
|
|
132
|
+
var notesWithText = (value) => Array.isArray(value) ? value.filter((n) => isObject(n) && !blank(n.text)) : [];
|
|
133
|
+
var MAX_HIGHLIGHT_NOTE_LENGTH = 1e3;
|
|
134
|
+
var MAX_HIGHLIGHT_QUOTE_LENGTH = 2e3;
|
|
135
|
+
var HIGHLIGHT_NOTE_MESSAGE = "Add a note to each highlight.";
|
|
136
|
+
var sourceText = (field2) => typeof field2.props?.text === "string" && field2.props.text ? field2.props.text : void 0;
|
|
137
|
+
var STRUCTURED_FIELD_TYPES = ["list", "table", "media-notes", "text-highlights"];
|
|
138
|
+
var structuredFieldTypes = [
|
|
139
|
+
{
|
|
140
|
+
type: "list",
|
|
141
|
+
label: "List",
|
|
142
|
+
category: "text",
|
|
143
|
+
value: { json: true },
|
|
144
|
+
// logic.ts `contains` on an array is exact item equality, same as multi-choice, so it is offered.
|
|
145
|
+
operators: ["contains", "answered", "empty"],
|
|
146
|
+
a11y: { semantics: "group" },
|
|
147
|
+
isEmpty: (value) => emptyList(value, blankText),
|
|
148
|
+
validate: (value, field2) => {
|
|
149
|
+
if (!Array.isArray(value) || !value.every((v) => typeof v === "string")) return "Enter a list of answers.";
|
|
150
|
+
const { min, max, itemMaxLength } = listLimits(field2);
|
|
151
|
+
if (value.some((v) => v.trim().length > itemMaxLength)) return `Keep each item under ${itemMaxLength} characters.`;
|
|
152
|
+
const count = listItems(value).length;
|
|
153
|
+
if (count < min) return `Add at least ${plural(min, "item")}.`;
|
|
154
|
+
if (count > max) return `Add at most ${plural(max, "item")}.`;
|
|
155
|
+
return null;
|
|
156
|
+
},
|
|
157
|
+
normalize: (value) => Array.isArray(value) ? listItems(value).map((v) => v.trim()) : value,
|
|
158
|
+
summarize: (value) => listItems(value).map((v) => v.trim()).join("; "),
|
|
159
|
+
samples: (field2) => {
|
|
160
|
+
const { min, max } = listLimits(field2);
|
|
161
|
+
return [Array.from({ length: Math.min(Math.max(1, min), max) }, (_, i) => `Item ${i + 1}`)];
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
type: "table",
|
|
166
|
+
label: "Table",
|
|
167
|
+
category: "advanced",
|
|
168
|
+
value: { json: true },
|
|
169
|
+
operators: ["answered", "empty"],
|
|
170
|
+
a11y: { semantics: "group" },
|
|
171
|
+
isEmpty: (value) => emptyList(value, (r) => isObject(r) && Object.values(r).every(blankText)),
|
|
172
|
+
validate: (value, field2) => {
|
|
173
|
+
const columns = tableColumns(field2);
|
|
174
|
+
if (!columns.length) return "This question has no columns.";
|
|
175
|
+
if (!Array.isArray(value) || !value.every(isObject)) return "Fill in the rows.";
|
|
176
|
+
const keys = new Set(columns.map((c) => c.key));
|
|
177
|
+
for (const row of value) {
|
|
178
|
+
for (const [key, cell] of Object.entries(row)) {
|
|
179
|
+
if (!keys.has(key)) return "An answer names a column this question doesn't have.";
|
|
180
|
+
if (typeof cell !== "string") return "Fill in the rows with text.";
|
|
181
|
+
if (cell.trim().length > MAX_TABLE_CELL_LENGTH) return `Keep each answer under ${MAX_TABLE_CELL_LENGTH} characters.`;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
const { minRows, maxRows } = tableLimits(field2);
|
|
185
|
+
const count = filledRows(value).length;
|
|
186
|
+
if (count < minRows) return `Fill in at least ${plural(minRows, "row")}.`;
|
|
187
|
+
if (count > maxRows) return `Fill in at most ${plural(maxRows, "row")}.`;
|
|
188
|
+
return null;
|
|
189
|
+
},
|
|
190
|
+
normalize: (value) => Array.isArray(value) ? filledRows(value).map((row) => {
|
|
191
|
+
const out = {};
|
|
192
|
+
for (const [key, cell] of Object.entries(row)) if (typeof cell === "string") out[key] = cell.trim();
|
|
193
|
+
return out;
|
|
194
|
+
}) : value,
|
|
195
|
+
summarize: (value, field2) => {
|
|
196
|
+
const columns = tableColumns(field2);
|
|
197
|
+
return filledRows(value).map((row) => columns.filter((c) => !blank(row[c.key])).map((c) => `${c.label}: ${row[c.key].trim()}`).join(" \xB7 ")).join(" / ");
|
|
198
|
+
},
|
|
199
|
+
samples: (field2) => {
|
|
200
|
+
const columns = tableColumns(field2);
|
|
201
|
+
if (!columns.length) return [];
|
|
202
|
+
const { minRows, maxRows } = tableLimits(field2);
|
|
203
|
+
const row = Object.fromEntries(columns.map((c) => [c.key, "sample"]));
|
|
204
|
+
return [Array.from({ length: Math.min(Math.max(1, minRows), maxRows) }, () => ({ ...row }))];
|
|
205
|
+
}
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
type: "media-notes",
|
|
209
|
+
label: "Notes at moments",
|
|
210
|
+
category: "media",
|
|
211
|
+
value: { json: true },
|
|
212
|
+
operators: ["answered", "empty"],
|
|
213
|
+
a11y: { semantics: "group" },
|
|
214
|
+
isEmpty: (value) => emptyList(value, (n) => isObject(n) && blankText(n.text)),
|
|
215
|
+
validate: (value, field2) => {
|
|
216
|
+
if (!Array.isArray(value)) return "Add notes.";
|
|
217
|
+
const textMaxLength = intProp(field2, "textMaxLength", 1e3);
|
|
218
|
+
for (const note of value) {
|
|
219
|
+
if (!isObject(note) || typeof note.text !== "string") return "Add notes.";
|
|
220
|
+
if (!isTimestamp(note.at)) return "Each note needs a time.";
|
|
221
|
+
if (note.text.trim().length > textMaxLength) return `Keep each note under ${textMaxLength} characters.`;
|
|
222
|
+
}
|
|
223
|
+
const maxNotes = intProp(field2, "maxNotes", 20);
|
|
224
|
+
return notesWithText(value).length > maxNotes ? `Add at most ${plural(maxNotes, "note")}.` : null;
|
|
225
|
+
},
|
|
226
|
+
normalize: (value) => Array.isArray(value) ? notesWithText(value).map((n) => ({ at: n.at, text: n.text.trim() })).sort((a, b) => a.at - b.at) : value,
|
|
227
|
+
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("; "),
|
|
228
|
+
samples: () => [[{ at: 0, text: "sample" }]]
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
type: "text-highlights",
|
|
232
|
+
label: "Highlights",
|
|
233
|
+
category: "media",
|
|
234
|
+
value: { json: true },
|
|
235
|
+
operators: ["answered", "empty"],
|
|
236
|
+
a11y: { semantics: "group" },
|
|
237
|
+
isEmpty: (value) => emptyList(value, () => false),
|
|
238
|
+
validate: (value, field2) => {
|
|
239
|
+
if (!Array.isArray(value)) return "Highlight a passage.";
|
|
240
|
+
const maxHighlights = intProp(field2, "maxHighlights", 20);
|
|
241
|
+
if (value.length > maxHighlights) return `Add at most ${plural(maxHighlights, "highlight")}.`;
|
|
242
|
+
const text = sourceText(field2);
|
|
243
|
+
for (const h of value) {
|
|
244
|
+
if (!isObject(h) || !Number.isInteger(h.start) || !Number.isInteger(h.end) || typeof h.quote !== "string" || typeof h.note !== "string") {
|
|
245
|
+
return "Highlight a passage of the text.";
|
|
246
|
+
}
|
|
247
|
+
const start = h.start;
|
|
248
|
+
const end = h.end;
|
|
249
|
+
if (start < 0 || end <= start || h.quote.length !== end - start) return "Highlight a passage of the text.";
|
|
250
|
+
if (end - start > MAX_HIGHLIGHT_QUOTE_LENGTH) return `Highlight under ${MAX_HIGHLIGHT_QUOTE_LENGTH} characters at a time.`;
|
|
251
|
+
if (text !== void 0 && (end > text.length || text.slice(start, end) !== h.quote)) return "A highlight doesn't match the text.";
|
|
252
|
+
if (h.note.trim() === "") return HIGHLIGHT_NOTE_MESSAGE;
|
|
253
|
+
if (h.note.trim().length > MAX_HIGHLIGHT_NOTE_LENGTH) return `Keep each note under ${MAX_HIGHLIGHT_NOTE_LENGTH} characters.`;
|
|
254
|
+
}
|
|
255
|
+
return null;
|
|
256
|
+
},
|
|
257
|
+
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,
|
|
258
|
+
summarize: (value) => Array.isArray(value) ? value.filter(isObject).map((h) => `"${String(h.quote)}" \u2014 ${String(h.note).trim()}`).join("; ") : "",
|
|
259
|
+
samples: (field2) => {
|
|
260
|
+
const text = sourceText(field2);
|
|
261
|
+
if (text === void 0) return [[{ start: 0, end: 6, quote: "sample", note: "sample" }]];
|
|
262
|
+
const end = Math.min(text.length, 20);
|
|
263
|
+
return [[{ start: 0, end, quote: text.slice(0, end), note: "sample" }]];
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
];
|
|
267
|
+
|
|
63
268
|
// src/fields.ts
|
|
64
269
|
var REGISTRY = /* @__PURE__ */ Symbol.for("@marktiderman/genesis-forms/field-types");
|
|
65
270
|
var globalScope = globalThis;
|
|
@@ -247,7 +452,7 @@ var builtins = [
|
|
|
247
452
|
summarize: (value) => value ? "Yes" : "No"
|
|
248
453
|
}
|
|
249
454
|
];
|
|
250
|
-
for (const manifest of builtins) registerFieldType(manifest);
|
|
455
|
+
for (const manifest of [...builtins, ...structuredFieldTypes]) registerFieldType(manifest);
|
|
251
456
|
|
|
252
457
|
// src/logic.ts
|
|
253
458
|
function own(record, key) {
|
|
@@ -407,7 +612,7 @@ function shown(v) {
|
|
|
407
612
|
text ??= typeof v === "bigint" ? `${v}n` : typeof v;
|
|
408
613
|
return text.length > 60 ? `${text.slice(0, 57)}...` : text;
|
|
409
614
|
}
|
|
410
|
-
var
|
|
615
|
+
var isObject2 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
411
616
|
var isInt = (v) => typeof v === "number" && Number.isInteger(v);
|
|
412
617
|
var LITERAL_TYPE = {
|
|
413
618
|
"short-text": "string",
|
|
@@ -418,7 +623,8 @@ var LITERAL_TYPE = {
|
|
|
418
623
|
nps: "number",
|
|
419
624
|
"single-choice": "string",
|
|
420
625
|
"multi-choice": "string",
|
|
421
|
-
"yes-no": "boolean"
|
|
626
|
+
"yes-no": "boolean",
|
|
627
|
+
list: "string"
|
|
422
628
|
};
|
|
423
629
|
var ORDERING = /* @__PURE__ */ new Set(["lt", "lte", "gt", "gte"]);
|
|
424
630
|
var OPERATORS = /* @__PURE__ */ new Set([
|
|
@@ -460,7 +666,48 @@ function parseForm(input) {
|
|
|
460
666
|
}
|
|
461
667
|
return true;
|
|
462
668
|
};
|
|
463
|
-
|
|
669
|
+
const optionalInt = (v, path, floor) => {
|
|
670
|
+
if (v !== void 0 && !(isInt(v) && v >= floor)) issue(path, `Must be a whole number of ${floor} or more.`);
|
|
671
|
+
};
|
|
672
|
+
const ordered = (low, high, path, what) => {
|
|
673
|
+
if (isInt(low) && isInt(high) && low > high) issue(path, `Can't be more than the ${what}.`);
|
|
674
|
+
};
|
|
675
|
+
const structuredProps = (type, props2, path) => {
|
|
676
|
+
if (type === "list") {
|
|
677
|
+
optionalInt(props2.min, `${path}.min`, 0);
|
|
678
|
+
optionalInt(props2.max, `${path}.max`, 1);
|
|
679
|
+
optionalInt(props2.itemMaxLength, `${path}.itemMaxLength`, 1);
|
|
680
|
+
optionalText(props2.placeholder, `${path}.placeholder`);
|
|
681
|
+
ordered(props2.min, props2.max ?? 10, `${path}.min`, "most items");
|
|
682
|
+
} else if (type === "table") {
|
|
683
|
+
const columns = props2.columns;
|
|
684
|
+
if (!Array.isArray(columns) || columns.length < 1 || columns.length > MAX_TABLE_COLUMNS) {
|
|
685
|
+
issue(`${path}.columns`, `Must be a list of 1 to ${MAX_TABLE_COLUMNS} columns.`);
|
|
686
|
+
} else {
|
|
687
|
+
const seen = /* @__PURE__ */ new Set();
|
|
688
|
+
columns.forEach((c, j) => {
|
|
689
|
+
const at = `${path}.columns[${j}]`;
|
|
690
|
+
if (!isObject2(c)) return issue(at, "Each column needs a key and a label.");
|
|
691
|
+
if (name(c.key, `${at}.key`, "A column key")) {
|
|
692
|
+
if (seen.has(c.key)) issue(`${at}.key`, `Duplicate column key "${c.key}".`);
|
|
693
|
+
seen.add(c.key);
|
|
694
|
+
}
|
|
695
|
+
text(c.label, `${at}.label`);
|
|
696
|
+
optionalBoolean(c.long, `${at}.long`);
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
optionalInt(props2.minRows, `${path}.minRows`, 0);
|
|
700
|
+
optionalInt(props2.maxRows, `${path}.maxRows`, 1);
|
|
701
|
+
ordered(props2.minRows, props2.maxRows ?? 3, `${path}.minRows`, "most rows");
|
|
702
|
+
} else if (type === "media-notes") {
|
|
703
|
+
optionalInt(props2.maxNotes, `${path}.maxNotes`, 1);
|
|
704
|
+
optionalInt(props2.textMaxLength, `${path}.textMaxLength`, 1);
|
|
705
|
+
} else if (type === "text-highlights") {
|
|
706
|
+
optionalText(props2.text, `${path}.text`);
|
|
707
|
+
optionalInt(props2.maxHighlights, `${path}.maxHighlights`, 1);
|
|
708
|
+
}
|
|
709
|
+
};
|
|
710
|
+
if (!isObject2(input)) {
|
|
464
711
|
issue("", "A form definition must be an object.");
|
|
465
712
|
return fail();
|
|
466
713
|
}
|
|
@@ -469,19 +716,19 @@ function parseForm(input) {
|
|
|
469
716
|
text(input.title, "title");
|
|
470
717
|
optionalText(input.description, "description");
|
|
471
718
|
if (input.layout !== void 0) {
|
|
472
|
-
if (!
|
|
719
|
+
if (!isObject2(input.layout)) issue("layout", "Must be an object.");
|
|
473
720
|
else if (input.layout.mode !== "focus" && input.layout.mode !== "classic") {
|
|
474
721
|
issue("layout.mode", "Must be focus or classic.");
|
|
475
722
|
}
|
|
476
723
|
}
|
|
477
724
|
if (input.settings !== void 0) {
|
|
478
|
-
if (!
|
|
725
|
+
if (!isObject2(input.settings)) issue("settings", "Must be an object.");
|
|
479
726
|
else {
|
|
480
727
|
optionalBoolean(input.settings.anonymous, "settings.anonymous");
|
|
481
728
|
optionalBoolean(input.settings.clearHiddenValues, "settings.clearHiddenValues");
|
|
482
729
|
}
|
|
483
730
|
}
|
|
484
|
-
if (input.meta !== void 0 && (!
|
|
731
|
+
if (input.meta !== void 0 && (!isObject2(input.meta) || !isJson(input.meta))) {
|
|
485
732
|
issue("meta", "Must be a JSON object.");
|
|
486
733
|
}
|
|
487
734
|
if (!Array.isArray(input.blocks)) {
|
|
@@ -496,7 +743,7 @@ function parseForm(input) {
|
|
|
496
743
|
const choices = /* @__PURE__ */ new Map();
|
|
497
744
|
input.blocks.forEach((block, i) => {
|
|
498
745
|
const path = `blocks[${i}]`;
|
|
499
|
-
if (!
|
|
746
|
+
if (!isObject2(block) || typeof block.id !== "string" || !block.id) {
|
|
500
747
|
issue(path, "Every block needs a string id.");
|
|
501
748
|
return;
|
|
502
749
|
}
|
|
@@ -517,23 +764,23 @@ function parseForm(input) {
|
|
|
517
764
|
issue(`${path}.type`, `Unknown field type "${String(block.type)}".`);
|
|
518
765
|
}
|
|
519
766
|
if (block.props !== void 0) {
|
|
520
|
-
if (!
|
|
767
|
+
if (!isObject2(block.props) || !isJson(block.props)) issue(`${path}.props`, "Must be a JSON object.");
|
|
521
768
|
else if (block.props.options !== void 0) {
|
|
522
769
|
const options = block.props.options;
|
|
523
770
|
if (!Array.isArray(options)) issue(`${path}.props.options`, "Must be an array.");
|
|
524
771
|
else {
|
|
525
772
|
options.forEach((o, j) => {
|
|
526
773
|
const at = `${path}.props.options[${j}]`;
|
|
527
|
-
if (!
|
|
774
|
+
if (!isObject2(o) || typeof o.value !== "string") issue(at, "Each option needs a string value.");
|
|
528
775
|
else optionalText(o.label, `${at}.label`);
|
|
529
776
|
});
|
|
530
777
|
choices.set(
|
|
531
778
|
block.id,
|
|
532
|
-
new Set(options.flatMap((o) =>
|
|
779
|
+
new Set(options.flatMap((o) => isObject2(o) && typeof o.value === "string" ? [o.value] : []))
|
|
533
780
|
);
|
|
534
781
|
}
|
|
535
782
|
}
|
|
536
|
-
const { max, maxLength: maxLength2 } =
|
|
783
|
+
const { max, maxLength: maxLength2 } = isObject2(block.props) ? block.props : {};
|
|
537
784
|
if (block.type === "rating" && max !== void 0 && !(isInt(max) && max >= 2 && max <= 10)) {
|
|
538
785
|
issue(`${path}.props.max`, "Must be a whole number from 2 to 10.");
|
|
539
786
|
}
|
|
@@ -541,6 +788,9 @@ function parseForm(input) {
|
|
|
541
788
|
issue(`${path}.props.maxLength`, "Must be a whole number above 0.");
|
|
542
789
|
}
|
|
543
790
|
}
|
|
791
|
+
if (typeof block.type === "string" && (block.props === void 0 || isObject2(block.props))) {
|
|
792
|
+
structuredProps(block.type, block.props ?? {}, `${path}.props`);
|
|
793
|
+
}
|
|
544
794
|
if (validKey) {
|
|
545
795
|
if (keys.has(block.key)) issue(`${path}.key`, `Duplicate key "${String(block.key)}".`);
|
|
546
796
|
keys.set(block.key, block.id);
|
|
@@ -559,7 +809,7 @@ function parseForm(input) {
|
|
|
559
809
|
const hiddenTypes = /* @__PURE__ */ new Map();
|
|
560
810
|
if (input.hidden !== void 0 && !Array.isArray(input.hidden)) issue("hidden", "Must be an array.");
|
|
561
811
|
(Array.isArray(input.hidden) ? input.hidden : []).forEach((h, i) => {
|
|
562
|
-
if (!
|
|
812
|
+
if (!isObject2(h) || typeof h.name !== "string" || !h.name) {
|
|
563
813
|
issue(`hidden[${i}]`, "Each hidden input needs a name.");
|
|
564
814
|
return;
|
|
565
815
|
}
|
|
@@ -580,7 +830,7 @@ function parseForm(input) {
|
|
|
580
830
|
if (input.endings !== void 0 && !Array.isArray(input.endings)) issue("endings", "Must be an array.");
|
|
581
831
|
(Array.isArray(input.endings) ? input.endings : []).forEach((e, i) => {
|
|
582
832
|
const path = `endings[${i}]`;
|
|
583
|
-
if (!
|
|
833
|
+
if (!isObject2(e)) {
|
|
584
834
|
issue(path, "Each ending must be an object with an id and a title.");
|
|
585
835
|
return;
|
|
586
836
|
}
|
|
@@ -592,7 +842,7 @@ function parseForm(input) {
|
|
|
592
842
|
});
|
|
593
843
|
const checkOperand = (o, path, reads) => {
|
|
594
844
|
if (o === void 0) return;
|
|
595
|
-
if (!
|
|
845
|
+
if (!isObject2(o)) return issue(path, "An operand must be an object.");
|
|
596
846
|
if ("field" in o) {
|
|
597
847
|
if (typeof o.field !== "string" || !fieldTypes.has(o.field)) return issue(path, `No field "${String(o.field)}".`);
|
|
598
848
|
reads.push(o.field);
|
|
@@ -605,7 +855,7 @@ function parseForm(input) {
|
|
|
605
855
|
}
|
|
606
856
|
};
|
|
607
857
|
const checkCondition = (c, path, reads, depth = 0) => {
|
|
608
|
-
if (!
|
|
858
|
+
if (!isObject2(c)) return issue(path, "A condition must be an object.");
|
|
609
859
|
if (depth > MAX_CONDITION_DEPTH) return issue(path, "The condition is nested too deeply.");
|
|
610
860
|
if ("all" in c || "any" in c) {
|
|
611
861
|
const group = "all" in c ? "all" : "any";
|
|
@@ -614,7 +864,7 @@ function parseForm(input) {
|
|
|
614
864
|
return list.forEach((x, i) => checkCondition(x, `${path}.${group}[${i}]`, reads, depth + 1));
|
|
615
865
|
}
|
|
616
866
|
if ("not" in c) return checkCondition(c.not, `${path}.not`, reads, depth + 1);
|
|
617
|
-
if (typeof c.op !== "string" || !
|
|
867
|
+
if (typeof c.op !== "string" || !isObject2(c.left)) {
|
|
618
868
|
return issue(path, "A comparison needs an `op` and a `left` operand.");
|
|
619
869
|
}
|
|
620
870
|
if (!OPERATORS.has(c.op)) issue(`${path}.op`, `Unknown operator "${c.op}".`);
|
|
@@ -630,7 +880,7 @@ function parseForm(input) {
|
|
|
630
880
|
};
|
|
631
881
|
const checkLiterals = (op, left, right, path) => {
|
|
632
882
|
if (op === "answered" || op === "empty" || !OPERATORS.has(op)) return;
|
|
633
|
-
const literal =
|
|
883
|
+
const literal = isObject2(right) && "value" in right;
|
|
634
884
|
if (right !== void 0 && !literal) return;
|
|
635
885
|
let what = "the left side";
|
|
636
886
|
let type;
|
|
@@ -669,7 +919,7 @@ function parseForm(input) {
|
|
|
669
919
|
if (input.logic !== void 0 && !Array.isArray(input.logic)) issue("logic", "Must be an array.");
|
|
670
920
|
(Array.isArray(input.logic) ? input.logic : []).forEach((rule, i) => {
|
|
671
921
|
const path = `logic[${i}]`;
|
|
672
|
-
if (!
|
|
922
|
+
if (!isObject2(rule) || typeof rule.id !== "string" || !isObject2(rule.when)) {
|
|
673
923
|
issue(path, "Each rule needs an id and a `when` condition.");
|
|
674
924
|
return;
|
|
675
925
|
}
|
|
@@ -688,7 +938,7 @@ function parseForm(input) {
|
|
|
688
938
|
at = rule.from;
|
|
689
939
|
inclusive = true;
|
|
690
940
|
}
|
|
691
|
-
if (
|
|
941
|
+
if (isObject2(rule.to)) {
|
|
692
942
|
if (typeof rule.to.ending !== "string" || !endingIds.has(rule.to.ending)) {
|
|
693
943
|
issue(`${path}.to`, `No ending "${String(rule.to.ending)}".`);
|
|
694
944
|
}
|
|
@@ -720,7 +970,7 @@ function parseForm(input) {
|
|
|
720
970
|
}
|
|
721
971
|
});
|
|
722
972
|
input.blocks.forEach((block, i) => {
|
|
723
|
-
if (!
|
|
973
|
+
if (!isObject2(block)) return;
|
|
724
974
|
const raw = block.kind === "field" ? [block.label, block.description] : block.kind === "content" ? [block.text] : [];
|
|
725
975
|
const texts = raw.filter((t) => typeof t === "string");
|
|
726
976
|
for (const t of texts) {
|
|
@@ -740,7 +990,7 @@ function parseForm(input) {
|
|
|
740
990
|
});
|
|
741
991
|
const extra = [["description", input.description]];
|
|
742
992
|
(Array.isArray(input.endings) ? input.endings : []).forEach((e, i) => {
|
|
743
|
-
if (
|
|
993
|
+
if (isObject2(e)) extra.push([`endings[${i}]`, e.title], [`endings[${i}]`, e.description]);
|
|
744
994
|
});
|
|
745
995
|
for (const [path, t] of extra) {
|
|
746
996
|
if (typeof t !== "string") continue;
|
|
@@ -754,7 +1004,7 @@ function parseForm(input) {
|
|
|
754
1004
|
}
|
|
755
1005
|
|
|
756
1006
|
// src/define.ts
|
|
757
|
-
function field(type, key, options,
|
|
1007
|
+
function field(type, key, options, props2) {
|
|
758
1008
|
const block = {
|
|
759
1009
|
kind: "field",
|
|
760
1010
|
id: options.id ?? key,
|
|
@@ -767,9 +1017,10 @@ function field(type, key, options, props) {
|
|
|
767
1017
|
if (options.required) block.required = true;
|
|
768
1018
|
if (options.sensitive) block.sensitive = true;
|
|
769
1019
|
if (options.identifying) block.identifying = true;
|
|
770
|
-
if (
|
|
1020
|
+
if (props2 && Object.keys(props2).length) block.props = props2;
|
|
771
1021
|
return block;
|
|
772
1022
|
}
|
|
1023
|
+
var props = (all) => Object.fromEntries(Object.entries(all).filter((e) => e[1] !== void 0));
|
|
773
1024
|
var choiceProps = (options) => ({
|
|
774
1025
|
options: options.map((o) => o.label ? { value: o.value, label: o.label } : { value: o.value })
|
|
775
1026
|
});
|
|
@@ -789,6 +1040,41 @@ var f = {
|
|
|
789
1040
|
singleChoice: (key, o) => field("single-choice", key, o, choiceProps(o.options)),
|
|
790
1041
|
multiChoice: (key, o) => field("multi-choice", key, o, choiceProps(o.options)),
|
|
791
1042
|
yesNo: (key, o) => field("yes-no", key, o),
|
|
1043
|
+
/** Several short answers, one per line: goals, action items, three things that went well. */
|
|
1044
|
+
list: (key, o) => field(
|
|
1045
|
+
"list",
|
|
1046
|
+
key,
|
|
1047
|
+
o,
|
|
1048
|
+
props({ min: o.min, max: o.max, itemMaxLength: o.itemMaxLength, placeholder: o.itemPlaceholder })
|
|
1049
|
+
),
|
|
1050
|
+
/**
|
|
1051
|
+
* A small table each answer fills row by row, one answer per column: an
|
|
1052
|
+
* action plan (What / Who / By when), pros and cons.
|
|
1053
|
+
*/
|
|
1054
|
+
table: (key, o) => field("table", key, o, {
|
|
1055
|
+
columns: o.columns.map((c) => c.long ? { key: c.key, label: c.label, long: true } : { key: c.key, label: c.label }),
|
|
1056
|
+
...props({ minRows: o.minRows, maxRows: o.maxRows })
|
|
1057
|
+
}),
|
|
1058
|
+
/**
|
|
1059
|
+
* Notes pinned to times in a video or audio (lecture notes, a recorded call
|
|
1060
|
+
* review): `at` is seconds from its start.
|
|
1061
|
+
*/
|
|
1062
|
+
mediaNotes: (key, o) => field(
|
|
1063
|
+
"media-notes",
|
|
1064
|
+
key,
|
|
1065
|
+
o,
|
|
1066
|
+
props({ maxNotes: o.maxNotes, textMaxLength: o.textMaxLength })
|
|
1067
|
+
),
|
|
1068
|
+
/**
|
|
1069
|
+
* Highlight passages of a text and note on each (annotating an article, a
|
|
1070
|
+
* policy or a contract): `text` here, or supplied by the host app.
|
|
1071
|
+
*/
|
|
1072
|
+
textHighlights: (key, o) => field(
|
|
1073
|
+
"text-highlights",
|
|
1074
|
+
key,
|
|
1075
|
+
o,
|
|
1076
|
+
props({ text: o.text, maxHighlights: o.maxHighlights })
|
|
1077
|
+
),
|
|
792
1078
|
/** Any registered field type, including plugins. */
|
|
793
1079
|
custom: (type, key, o) => field(type, key, o, o.props),
|
|
794
1080
|
heading: (id, text) => ({ kind: "content", id, type: "heading", text }),
|
|
@@ -1259,6 +1545,6 @@ function describeCondition(condition, def, fields = fieldMap(def)) {
|
|
|
1259
1545
|
return `${left} ${op} ${right}`;
|
|
1260
1546
|
}
|
|
1261
1547
|
|
|
1262
|
-
export { MAX_HIDDEN_STRING_LENGTH, REQUIRED_MESSAGE, canonicalJson, choiceOptions, coerceHidden, coerceHiddenValue, computePath, createForm, defineForm, describeCondition, evaluate, explain, f, fingerprint, getFieldType, isEmptyValue, isFormEvent, isJson, isSensitive, listFieldTypes, normalizeValue, parseForm, recall, registerFieldType, replay, ruleVisible, steps, summarizeValue, validateField, when };
|
|
1548
|
+
export { HIGHLIGHT_NOTE_MESSAGE, MAX_HIDDEN_STRING_LENGTH, MAX_HIGHLIGHT_NOTE_LENGTH, MAX_HIGHLIGHT_QUOTE_LENGTH, MAX_TABLE_CELL_LENGTH, MAX_TABLE_COLUMNS, MAX_TIMESTAMP_SECONDS, REQUIRED_MESSAGE, STRUCTURED_FIELD_TYPES, canonicalJson, choiceOptions, coerceHidden, coerceHiddenValue, computePath, createForm, defineForm, describeCondition, evaluate, explain, f, fingerprint, formatTimestamp, getFieldType, isEmptyValue, isFormEvent, isJson, isSensitive, listFieldTypes, listLimits, normalizeValue, parseForm, parseTimestamp, recall, registerFieldType, replay, ruleVisible, steps, summarizeValue, tableColumns, tableLimits, validateField, when };
|
|
1263
1549
|
//# sourceMappingURL=index.js.map
|
|
1264
1550
|
//# sourceMappingURL=index.js.map
|