@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.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,23 +766,23 @@ 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
|
}
|
|
@@ -543,6 +790,9 @@ function parseForm(input) {
|
|
|
543
790
|
issue(`${path}.props.maxLength`, "Must be a whole number above 0.");
|
|
544
791
|
}
|
|
545
792
|
}
|
|
793
|
+
if (typeof block.type === "string" && (block.props === void 0 || isObject2(block.props))) {
|
|
794
|
+
structuredProps(block.type, block.props ?? {}, `${path}.props`);
|
|
795
|
+
}
|
|
546
796
|
if (validKey) {
|
|
547
797
|
if (keys.has(block.key)) issue(`${path}.key`, `Duplicate key "${String(block.key)}".`);
|
|
548
798
|
keys.set(block.key, block.id);
|
|
@@ -561,7 +811,7 @@ function parseForm(input) {
|
|
|
561
811
|
const hiddenTypes = /* @__PURE__ */ new Map();
|
|
562
812
|
if (input.hidden !== void 0 && !Array.isArray(input.hidden)) issue("hidden", "Must be an array.");
|
|
563
813
|
(Array.isArray(input.hidden) ? input.hidden : []).forEach((h, i) => {
|
|
564
|
-
if (!
|
|
814
|
+
if (!isObject2(h) || typeof h.name !== "string" || !h.name) {
|
|
565
815
|
issue(`hidden[${i}]`, "Each hidden input needs a name.");
|
|
566
816
|
return;
|
|
567
817
|
}
|
|
@@ -582,7 +832,7 @@ function parseForm(input) {
|
|
|
582
832
|
if (input.endings !== void 0 && !Array.isArray(input.endings)) issue("endings", "Must be an array.");
|
|
583
833
|
(Array.isArray(input.endings) ? input.endings : []).forEach((e, i) => {
|
|
584
834
|
const path = `endings[${i}]`;
|
|
585
|
-
if (!
|
|
835
|
+
if (!isObject2(e)) {
|
|
586
836
|
issue(path, "Each ending must be an object with an id and a title.");
|
|
587
837
|
return;
|
|
588
838
|
}
|
|
@@ -594,7 +844,7 @@ function parseForm(input) {
|
|
|
594
844
|
});
|
|
595
845
|
const checkOperand = (o, path, reads) => {
|
|
596
846
|
if (o === void 0) return;
|
|
597
|
-
if (!
|
|
847
|
+
if (!isObject2(o)) return issue(path, "An operand must be an object.");
|
|
598
848
|
if ("field" in o) {
|
|
599
849
|
if (typeof o.field !== "string" || !fieldTypes.has(o.field)) return issue(path, `No field "${String(o.field)}".`);
|
|
600
850
|
reads.push(o.field);
|
|
@@ -607,7 +857,7 @@ function parseForm(input) {
|
|
|
607
857
|
}
|
|
608
858
|
};
|
|
609
859
|
const checkCondition = (c, path, reads, depth = 0) => {
|
|
610
|
-
if (!
|
|
860
|
+
if (!isObject2(c)) return issue(path, "A condition must be an object.");
|
|
611
861
|
if (depth > MAX_CONDITION_DEPTH) return issue(path, "The condition is nested too deeply.");
|
|
612
862
|
if ("all" in c || "any" in c) {
|
|
613
863
|
const group = "all" in c ? "all" : "any";
|
|
@@ -616,7 +866,7 @@ function parseForm(input) {
|
|
|
616
866
|
return list.forEach((x, i) => checkCondition(x, `${path}.${group}[${i}]`, reads, depth + 1));
|
|
617
867
|
}
|
|
618
868
|
if ("not" in c) return checkCondition(c.not, `${path}.not`, reads, depth + 1);
|
|
619
|
-
if (typeof c.op !== "string" || !
|
|
869
|
+
if (typeof c.op !== "string" || !isObject2(c.left)) {
|
|
620
870
|
return issue(path, "A comparison needs an `op` and a `left` operand.");
|
|
621
871
|
}
|
|
622
872
|
if (!OPERATORS.has(c.op)) issue(`${path}.op`, `Unknown operator "${c.op}".`);
|
|
@@ -632,7 +882,7 @@ function parseForm(input) {
|
|
|
632
882
|
};
|
|
633
883
|
const checkLiterals = (op, left, right, path) => {
|
|
634
884
|
if (op === "answered" || op === "empty" || !OPERATORS.has(op)) return;
|
|
635
|
-
const literal =
|
|
885
|
+
const literal = isObject2(right) && "value" in right;
|
|
636
886
|
if (right !== void 0 && !literal) return;
|
|
637
887
|
let what = "the left side";
|
|
638
888
|
let type;
|
|
@@ -671,7 +921,7 @@ function parseForm(input) {
|
|
|
671
921
|
if (input.logic !== void 0 && !Array.isArray(input.logic)) issue("logic", "Must be an array.");
|
|
672
922
|
(Array.isArray(input.logic) ? input.logic : []).forEach((rule, i) => {
|
|
673
923
|
const path = `logic[${i}]`;
|
|
674
|
-
if (!
|
|
924
|
+
if (!isObject2(rule) || typeof rule.id !== "string" || !isObject2(rule.when)) {
|
|
675
925
|
issue(path, "Each rule needs an id and a `when` condition.");
|
|
676
926
|
return;
|
|
677
927
|
}
|
|
@@ -690,7 +940,7 @@ function parseForm(input) {
|
|
|
690
940
|
at = rule.from;
|
|
691
941
|
inclusive = true;
|
|
692
942
|
}
|
|
693
|
-
if (
|
|
943
|
+
if (isObject2(rule.to)) {
|
|
694
944
|
if (typeof rule.to.ending !== "string" || !endingIds.has(rule.to.ending)) {
|
|
695
945
|
issue(`${path}.to`, `No ending "${String(rule.to.ending)}".`);
|
|
696
946
|
}
|
|
@@ -722,7 +972,7 @@ function parseForm(input) {
|
|
|
722
972
|
}
|
|
723
973
|
});
|
|
724
974
|
input.blocks.forEach((block, i) => {
|
|
725
|
-
if (!
|
|
975
|
+
if (!isObject2(block)) return;
|
|
726
976
|
const raw = block.kind === "field" ? [block.label, block.description] : block.kind === "content" ? [block.text] : [];
|
|
727
977
|
const texts = raw.filter((t) => typeof t === "string");
|
|
728
978
|
for (const t of texts) {
|
|
@@ -742,7 +992,7 @@ function parseForm(input) {
|
|
|
742
992
|
});
|
|
743
993
|
const extra = [["description", input.description]];
|
|
744
994
|
(Array.isArray(input.endings) ? input.endings : []).forEach((e, i) => {
|
|
745
|
-
if (
|
|
995
|
+
if (isObject2(e)) extra.push([`endings[${i}]`, e.title], [`endings[${i}]`, e.description]);
|
|
746
996
|
});
|
|
747
997
|
for (const [path, t] of extra) {
|
|
748
998
|
if (typeof t !== "string") continue;
|
|
@@ -756,7 +1006,7 @@ function parseForm(input) {
|
|
|
756
1006
|
}
|
|
757
1007
|
|
|
758
1008
|
// src/define.ts
|
|
759
|
-
function field(type, key, options,
|
|
1009
|
+
function field(type, key, options, props2) {
|
|
760
1010
|
const block = {
|
|
761
1011
|
kind: "field",
|
|
762
1012
|
id: options.id ?? key,
|
|
@@ -769,9 +1019,10 @@ function field(type, key, options, props) {
|
|
|
769
1019
|
if (options.required) block.required = true;
|
|
770
1020
|
if (options.sensitive) block.sensitive = true;
|
|
771
1021
|
if (options.identifying) block.identifying = true;
|
|
772
|
-
if (
|
|
1022
|
+
if (props2 && Object.keys(props2).length) block.props = props2;
|
|
773
1023
|
return block;
|
|
774
1024
|
}
|
|
1025
|
+
var props = (all) => Object.fromEntries(Object.entries(all).filter((e) => e[1] !== void 0));
|
|
775
1026
|
var choiceProps = (options) => ({
|
|
776
1027
|
options: options.map((o) => o.label ? { value: o.value, label: o.label } : { value: o.value })
|
|
777
1028
|
});
|
|
@@ -791,6 +1042,41 @@ var f = {
|
|
|
791
1042
|
singleChoice: (key, o) => field("single-choice", key, o, choiceProps(o.options)),
|
|
792
1043
|
multiChoice: (key, o) => field("multi-choice", key, o, choiceProps(o.options)),
|
|
793
1044
|
yesNo: (key, o) => field("yes-no", key, o),
|
|
1045
|
+
/** Several short answers, one per line: goals, action items, three things that went well. */
|
|
1046
|
+
list: (key, o) => field(
|
|
1047
|
+
"list",
|
|
1048
|
+
key,
|
|
1049
|
+
o,
|
|
1050
|
+
props({ min: o.min, max: o.max, itemMaxLength: o.itemMaxLength, placeholder: o.itemPlaceholder })
|
|
1051
|
+
),
|
|
1052
|
+
/**
|
|
1053
|
+
* A small table each answer fills row by row, one answer per column: an
|
|
1054
|
+
* action plan (What / Who / By when), pros and cons.
|
|
1055
|
+
*/
|
|
1056
|
+
table: (key, o) => field("table", key, o, {
|
|
1057
|
+
columns: o.columns.map((c) => c.long ? { key: c.key, label: c.label, long: true } : { key: c.key, label: c.label }),
|
|
1058
|
+
...props({ minRows: o.minRows, maxRows: o.maxRows })
|
|
1059
|
+
}),
|
|
1060
|
+
/**
|
|
1061
|
+
* Notes pinned to times in a video or audio (lecture notes, a recorded call
|
|
1062
|
+
* review): `at` is seconds from its start.
|
|
1063
|
+
*/
|
|
1064
|
+
mediaNotes: (key, o) => field(
|
|
1065
|
+
"media-notes",
|
|
1066
|
+
key,
|
|
1067
|
+
o,
|
|
1068
|
+
props({ maxNotes: o.maxNotes, textMaxLength: o.textMaxLength })
|
|
1069
|
+
),
|
|
1070
|
+
/**
|
|
1071
|
+
* Highlight passages of a text and note on each (annotating an article, a
|
|
1072
|
+
* policy or a contract): `text` here, or supplied by the host app.
|
|
1073
|
+
*/
|
|
1074
|
+
textHighlights: (key, o) => field(
|
|
1075
|
+
"text-highlights",
|
|
1076
|
+
key,
|
|
1077
|
+
o,
|
|
1078
|
+
props({ text: o.text, maxHighlights: o.maxHighlights })
|
|
1079
|
+
),
|
|
794
1080
|
/** Any registered field type, including plugins. */
|
|
795
1081
|
custom: (type, key, o) => field(type, key, o, o.props),
|
|
796
1082
|
heading: (id, text) => ({ kind: "content", id, type: "heading", text }),
|
|
@@ -1261,8 +1547,15 @@ function describeCondition(condition, def, fields = fieldMap(def)) {
|
|
|
1261
1547
|
return `${left} ${op} ${right}`;
|
|
1262
1548
|
}
|
|
1263
1549
|
|
|
1550
|
+
exports.HIGHLIGHT_NOTE_MESSAGE = HIGHLIGHT_NOTE_MESSAGE;
|
|
1264
1551
|
exports.MAX_HIDDEN_STRING_LENGTH = MAX_HIDDEN_STRING_LENGTH;
|
|
1552
|
+
exports.MAX_HIGHLIGHT_NOTE_LENGTH = MAX_HIGHLIGHT_NOTE_LENGTH;
|
|
1553
|
+
exports.MAX_HIGHLIGHT_QUOTE_LENGTH = MAX_HIGHLIGHT_QUOTE_LENGTH;
|
|
1554
|
+
exports.MAX_TABLE_CELL_LENGTH = MAX_TABLE_CELL_LENGTH;
|
|
1555
|
+
exports.MAX_TABLE_COLUMNS = MAX_TABLE_COLUMNS;
|
|
1556
|
+
exports.MAX_TIMESTAMP_SECONDS = MAX_TIMESTAMP_SECONDS;
|
|
1265
1557
|
exports.REQUIRED_MESSAGE = REQUIRED_MESSAGE;
|
|
1558
|
+
exports.STRUCTURED_FIELD_TYPES = STRUCTURED_FIELD_TYPES;
|
|
1266
1559
|
exports.canonicalJson = canonicalJson;
|
|
1267
1560
|
exports.choiceOptions = choiceOptions;
|
|
1268
1561
|
exports.coerceHidden = coerceHidden;
|
|
@@ -1275,20 +1568,25 @@ exports.evaluate = evaluate;
|
|
|
1275
1568
|
exports.explain = explain;
|
|
1276
1569
|
exports.f = f;
|
|
1277
1570
|
exports.fingerprint = fingerprint;
|
|
1571
|
+
exports.formatTimestamp = formatTimestamp;
|
|
1278
1572
|
exports.getFieldType = getFieldType;
|
|
1279
1573
|
exports.isEmptyValue = isEmptyValue;
|
|
1280
1574
|
exports.isFormEvent = isFormEvent;
|
|
1281
1575
|
exports.isJson = isJson;
|
|
1282
1576
|
exports.isSensitive = isSensitive;
|
|
1283
1577
|
exports.listFieldTypes = listFieldTypes;
|
|
1578
|
+
exports.listLimits = listLimits;
|
|
1284
1579
|
exports.normalizeValue = normalizeValue;
|
|
1285
1580
|
exports.parseForm = parseForm;
|
|
1581
|
+
exports.parseTimestamp = parseTimestamp;
|
|
1286
1582
|
exports.recall = recall;
|
|
1287
1583
|
exports.registerFieldType = registerFieldType;
|
|
1288
1584
|
exports.replay = replay;
|
|
1289
1585
|
exports.ruleVisible = ruleVisible;
|
|
1290
1586
|
exports.steps = steps;
|
|
1291
1587
|
exports.summarizeValue = summarizeValue;
|
|
1588
|
+
exports.tableColumns = tableColumns;
|
|
1589
|
+
exports.tableLimits = tableLimits;
|
|
1292
1590
|
exports.validateField = validateField;
|
|
1293
1591
|
exports.when = when;
|
|
1294
1592
|
//# sourceMappingURL=index.cjs.map
|