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