@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.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,29 +764,30 @@ 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
|
}
|
|
540
787
|
if (maxLength2 !== void 0 && !(isInt(maxLength2) && maxLength2 > 0)) {
|
|
541
788
|
issue(`${path}.props.maxLength`, "Must be a whole number above 0.");
|
|
542
789
|
}
|
|
790
|
+
if (isObject2(block.props) && typeof block.type === "string") structuredProps(block.type, block.props, `${path}.props`);
|
|
543
791
|
}
|
|
544
792
|
if (validKey) {
|
|
545
793
|
if (keys.has(block.key)) issue(`${path}.key`, `Duplicate key "${String(block.key)}".`);
|
|
@@ -559,7 +807,7 @@ function parseForm(input) {
|
|
|
559
807
|
const hiddenTypes = /* @__PURE__ */ new Map();
|
|
560
808
|
if (input.hidden !== void 0 && !Array.isArray(input.hidden)) issue("hidden", "Must be an array.");
|
|
561
809
|
(Array.isArray(input.hidden) ? input.hidden : []).forEach((h, i) => {
|
|
562
|
-
if (!
|
|
810
|
+
if (!isObject2(h) || typeof h.name !== "string" || !h.name) {
|
|
563
811
|
issue(`hidden[${i}]`, "Each hidden input needs a name.");
|
|
564
812
|
return;
|
|
565
813
|
}
|
|
@@ -580,7 +828,7 @@ function parseForm(input) {
|
|
|
580
828
|
if (input.endings !== void 0 && !Array.isArray(input.endings)) issue("endings", "Must be an array.");
|
|
581
829
|
(Array.isArray(input.endings) ? input.endings : []).forEach((e, i) => {
|
|
582
830
|
const path = `endings[${i}]`;
|
|
583
|
-
if (!
|
|
831
|
+
if (!isObject2(e)) {
|
|
584
832
|
issue(path, "Each ending must be an object with an id and a title.");
|
|
585
833
|
return;
|
|
586
834
|
}
|
|
@@ -592,7 +840,7 @@ function parseForm(input) {
|
|
|
592
840
|
});
|
|
593
841
|
const checkOperand = (o, path, reads) => {
|
|
594
842
|
if (o === void 0) return;
|
|
595
|
-
if (!
|
|
843
|
+
if (!isObject2(o)) return issue(path, "An operand must be an object.");
|
|
596
844
|
if ("field" in o) {
|
|
597
845
|
if (typeof o.field !== "string" || !fieldTypes.has(o.field)) return issue(path, `No field "${String(o.field)}".`);
|
|
598
846
|
reads.push(o.field);
|
|
@@ -605,7 +853,7 @@ function parseForm(input) {
|
|
|
605
853
|
}
|
|
606
854
|
};
|
|
607
855
|
const checkCondition = (c, path, reads, depth = 0) => {
|
|
608
|
-
if (!
|
|
856
|
+
if (!isObject2(c)) return issue(path, "A condition must be an object.");
|
|
609
857
|
if (depth > MAX_CONDITION_DEPTH) return issue(path, "The condition is nested too deeply.");
|
|
610
858
|
if ("all" in c || "any" in c) {
|
|
611
859
|
const group = "all" in c ? "all" : "any";
|
|
@@ -614,7 +862,7 @@ function parseForm(input) {
|
|
|
614
862
|
return list.forEach((x, i) => checkCondition(x, `${path}.${group}[${i}]`, reads, depth + 1));
|
|
615
863
|
}
|
|
616
864
|
if ("not" in c) return checkCondition(c.not, `${path}.not`, reads, depth + 1);
|
|
617
|
-
if (typeof c.op !== "string" || !
|
|
865
|
+
if (typeof c.op !== "string" || !isObject2(c.left)) {
|
|
618
866
|
return issue(path, "A comparison needs an `op` and a `left` operand.");
|
|
619
867
|
}
|
|
620
868
|
if (!OPERATORS.has(c.op)) issue(`${path}.op`, `Unknown operator "${c.op}".`);
|
|
@@ -630,7 +878,7 @@ function parseForm(input) {
|
|
|
630
878
|
};
|
|
631
879
|
const checkLiterals = (op, left, right, path) => {
|
|
632
880
|
if (op === "answered" || op === "empty" || !OPERATORS.has(op)) return;
|
|
633
|
-
const literal =
|
|
881
|
+
const literal = isObject2(right) && "value" in right;
|
|
634
882
|
if (right !== void 0 && !literal) return;
|
|
635
883
|
let what = "the left side";
|
|
636
884
|
let type;
|
|
@@ -669,7 +917,7 @@ function parseForm(input) {
|
|
|
669
917
|
if (input.logic !== void 0 && !Array.isArray(input.logic)) issue("logic", "Must be an array.");
|
|
670
918
|
(Array.isArray(input.logic) ? input.logic : []).forEach((rule, i) => {
|
|
671
919
|
const path = `logic[${i}]`;
|
|
672
|
-
if (!
|
|
920
|
+
if (!isObject2(rule) || typeof rule.id !== "string" || !isObject2(rule.when)) {
|
|
673
921
|
issue(path, "Each rule needs an id and a `when` condition.");
|
|
674
922
|
return;
|
|
675
923
|
}
|
|
@@ -688,7 +936,7 @@ function parseForm(input) {
|
|
|
688
936
|
at = rule.from;
|
|
689
937
|
inclusive = true;
|
|
690
938
|
}
|
|
691
|
-
if (
|
|
939
|
+
if (isObject2(rule.to)) {
|
|
692
940
|
if (typeof rule.to.ending !== "string" || !endingIds.has(rule.to.ending)) {
|
|
693
941
|
issue(`${path}.to`, `No ending "${String(rule.to.ending)}".`);
|
|
694
942
|
}
|
|
@@ -720,7 +968,7 @@ function parseForm(input) {
|
|
|
720
968
|
}
|
|
721
969
|
});
|
|
722
970
|
input.blocks.forEach((block, i) => {
|
|
723
|
-
if (!
|
|
971
|
+
if (!isObject2(block)) return;
|
|
724
972
|
const raw = block.kind === "field" ? [block.label, block.description] : block.kind === "content" ? [block.text] : [];
|
|
725
973
|
const texts = raw.filter((t) => typeof t === "string");
|
|
726
974
|
for (const t of texts) {
|
|
@@ -740,7 +988,7 @@ function parseForm(input) {
|
|
|
740
988
|
});
|
|
741
989
|
const extra = [["description", input.description]];
|
|
742
990
|
(Array.isArray(input.endings) ? input.endings : []).forEach((e, i) => {
|
|
743
|
-
if (
|
|
991
|
+
if (isObject2(e)) extra.push([`endings[${i}]`, e.title], [`endings[${i}]`, e.description]);
|
|
744
992
|
});
|
|
745
993
|
for (const [path, t] of extra) {
|
|
746
994
|
if (typeof t !== "string") continue;
|
|
@@ -754,7 +1002,7 @@ function parseForm(input) {
|
|
|
754
1002
|
}
|
|
755
1003
|
|
|
756
1004
|
// src/define.ts
|
|
757
|
-
function field(type, key, options,
|
|
1005
|
+
function field(type, key, options, props2) {
|
|
758
1006
|
const block = {
|
|
759
1007
|
kind: "field",
|
|
760
1008
|
id: options.id ?? key,
|
|
@@ -767,9 +1015,10 @@ function field(type, key, options, props) {
|
|
|
767
1015
|
if (options.required) block.required = true;
|
|
768
1016
|
if (options.sensitive) block.sensitive = true;
|
|
769
1017
|
if (options.identifying) block.identifying = true;
|
|
770
|
-
if (
|
|
1018
|
+
if (props2 && Object.keys(props2).length) block.props = props2;
|
|
771
1019
|
return block;
|
|
772
1020
|
}
|
|
1021
|
+
var props = (all) => Object.fromEntries(Object.entries(all).filter((e) => e[1] !== void 0));
|
|
773
1022
|
var choiceProps = (options) => ({
|
|
774
1023
|
options: options.map((o) => o.label ? { value: o.value, label: o.label } : { value: o.value })
|
|
775
1024
|
});
|
|
@@ -789,6 +1038,41 @@ var f = {
|
|
|
789
1038
|
singleChoice: (key, o) => field("single-choice", key, o, choiceProps(o.options)),
|
|
790
1039
|
multiChoice: (key, o) => field("multi-choice", key, o, choiceProps(o.options)),
|
|
791
1040
|
yesNo: (key, o) => field("yes-no", key, o),
|
|
1041
|
+
/** Several short answers, one per line: goals, action items, three things that went well. */
|
|
1042
|
+
list: (key, o) => field(
|
|
1043
|
+
"list",
|
|
1044
|
+
key,
|
|
1045
|
+
o,
|
|
1046
|
+
props({ min: o.min, max: o.max, itemMaxLength: o.itemMaxLength, placeholder: o.itemPlaceholder })
|
|
1047
|
+
),
|
|
1048
|
+
/**
|
|
1049
|
+
* A small table each answer fills row by row, one answer per column: an
|
|
1050
|
+
* action plan (What / Who / By when), pros and cons.
|
|
1051
|
+
*/
|
|
1052
|
+
table: (key, o) => field("table", key, o, {
|
|
1053
|
+
columns: o.columns.map((c) => c.long ? { key: c.key, label: c.label, long: true } : { key: c.key, label: c.label }),
|
|
1054
|
+
...props({ minRows: o.minRows, maxRows: o.maxRows })
|
|
1055
|
+
}),
|
|
1056
|
+
/**
|
|
1057
|
+
* Notes pinned to times in a video or audio (lecture notes, a recorded call
|
|
1058
|
+
* review): `at` is seconds from its start.
|
|
1059
|
+
*/
|
|
1060
|
+
mediaNotes: (key, o) => field(
|
|
1061
|
+
"media-notes",
|
|
1062
|
+
key,
|
|
1063
|
+
o,
|
|
1064
|
+
props({ maxNotes: o.maxNotes, textMaxLength: o.textMaxLength })
|
|
1065
|
+
),
|
|
1066
|
+
/**
|
|
1067
|
+
* Highlight passages of a text and note on each (annotating an article, a
|
|
1068
|
+
* policy or a contract): `text` here, or supplied by the host app.
|
|
1069
|
+
*/
|
|
1070
|
+
textHighlights: (key, o) => field(
|
|
1071
|
+
"text-highlights",
|
|
1072
|
+
key,
|
|
1073
|
+
o,
|
|
1074
|
+
props({ text: o.text, maxHighlights: o.maxHighlights })
|
|
1075
|
+
),
|
|
792
1076
|
/** Any registered field type, including plugins. */
|
|
793
1077
|
custom: (type, key, o) => field(type, key, o, o.props),
|
|
794
1078
|
heading: (id, text) => ({ kind: "content", id, type: "heading", text }),
|
|
@@ -1259,6 +1543,6 @@ function describeCondition(condition, def, fields = fieldMap(def)) {
|
|
|
1259
1543
|
return `${left} ${op} ${right}`;
|
|
1260
1544
|
}
|
|
1261
1545
|
|
|
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 };
|
|
1546
|
+
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
1547
|
//# sourceMappingURL=index.js.map
|
|
1264
1548
|
//# sourceMappingURL=index.js.map
|