@marktiderman/genesis-forms 0.1.1 → 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 +121 -17
- 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-LsPRTzt3.d.cts → engine-CPLtKVew.d.cts} +7 -6
- package/dist/{engine-Cp5t2_Am.d.ts → engine-DOrPgFDw.d.ts} +7 -6
- package/dist/index.cjs +410 -24
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +122 -18
- package/dist/index.d.ts +122 -18
- package/dist/index.js +399 -25
- package/dist/index.js.map +1 -1
- package/dist/server.cjs +350 -29
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +13 -12
- package/dist/server.d.ts +13 -12
- package/dist/server.js +350 -29
- package/dist/server.js.map +1 -1
- package/dist/testing.cjs +354 -29
- 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 +354 -29
- package/dist/testing.js.map +1 -1
- package/dist/{validate-j8Z188mF.d.cts → types-BBKBQ8sp.d.cts} +9 -29
- package/dist/{validate-j8Z188mF.d.ts → types-BBKBQ8sp.d.ts} +9 -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) {
|
|
@@ -397,7 +602,31 @@ function recallRefs(text) {
|
|
|
397
602
|
}
|
|
398
603
|
|
|
399
604
|
// src/parse.ts
|
|
400
|
-
|
|
605
|
+
function shown(v) {
|
|
606
|
+
let text;
|
|
607
|
+
try {
|
|
608
|
+
text = JSON.stringify(v);
|
|
609
|
+
} catch {
|
|
610
|
+
text = void 0;
|
|
611
|
+
}
|
|
612
|
+
text ??= typeof v === "bigint" ? `${v}n` : typeof v;
|
|
613
|
+
return text.length > 60 ? `${text.slice(0, 57)}...` : text;
|
|
614
|
+
}
|
|
615
|
+
var isObject2 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
616
|
+
var isInt = (v) => typeof v === "number" && Number.isInteger(v);
|
|
617
|
+
var LITERAL_TYPE = {
|
|
618
|
+
"short-text": "string",
|
|
619
|
+
"long-text": "string",
|
|
620
|
+
email: "string",
|
|
621
|
+
phone: "string",
|
|
622
|
+
rating: "number",
|
|
623
|
+
nps: "number",
|
|
624
|
+
"single-choice": "string",
|
|
625
|
+
"multi-choice": "string",
|
|
626
|
+
"yes-no": "boolean",
|
|
627
|
+
list: "string"
|
|
628
|
+
};
|
|
629
|
+
var ORDERING = /* @__PURE__ */ new Set(["lt", "lte", "gt", "gte"]);
|
|
401
630
|
var OPERATORS = /* @__PURE__ */ new Set([
|
|
402
631
|
"eq",
|
|
403
632
|
"neq",
|
|
@@ -437,7 +666,48 @@ function parseForm(input) {
|
|
|
437
666
|
}
|
|
438
667
|
return true;
|
|
439
668
|
};
|
|
440
|
-
|
|
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)) {
|
|
441
711
|
issue("", "A form definition must be an object.");
|
|
442
712
|
return fail();
|
|
443
713
|
}
|
|
@@ -446,19 +716,19 @@ function parseForm(input) {
|
|
|
446
716
|
text(input.title, "title");
|
|
447
717
|
optionalText(input.description, "description");
|
|
448
718
|
if (input.layout !== void 0) {
|
|
449
|
-
if (!
|
|
719
|
+
if (!isObject2(input.layout)) issue("layout", "Must be an object.");
|
|
450
720
|
else if (input.layout.mode !== "focus" && input.layout.mode !== "classic") {
|
|
451
721
|
issue("layout.mode", "Must be focus or classic.");
|
|
452
722
|
}
|
|
453
723
|
}
|
|
454
724
|
if (input.settings !== void 0) {
|
|
455
|
-
if (!
|
|
725
|
+
if (!isObject2(input.settings)) issue("settings", "Must be an object.");
|
|
456
726
|
else {
|
|
457
727
|
optionalBoolean(input.settings.anonymous, "settings.anonymous");
|
|
458
728
|
optionalBoolean(input.settings.clearHiddenValues, "settings.clearHiddenValues");
|
|
459
729
|
}
|
|
460
730
|
}
|
|
461
|
-
if (input.meta !== void 0 && (!
|
|
731
|
+
if (input.meta !== void 0 && (!isObject2(input.meta) || !isJson(input.meta))) {
|
|
462
732
|
issue("meta", "Must be a JSON object.");
|
|
463
733
|
}
|
|
464
734
|
if (!Array.isArray(input.blocks)) {
|
|
@@ -470,9 +740,10 @@ function parseForm(input) {
|
|
|
470
740
|
const keys = /* @__PURE__ */ new Map();
|
|
471
741
|
const fieldTypes = /* @__PURE__ */ new Map();
|
|
472
742
|
const order = /* @__PURE__ */ new Map();
|
|
743
|
+
const choices = /* @__PURE__ */ new Map();
|
|
473
744
|
input.blocks.forEach((block, i) => {
|
|
474
745
|
const path = `blocks[${i}]`;
|
|
475
|
-
if (!
|
|
746
|
+
if (!isObject2(block) || typeof block.id !== "string" || !block.id) {
|
|
476
747
|
issue(path, "Every block needs a string id.");
|
|
477
748
|
return;
|
|
478
749
|
}
|
|
@@ -493,18 +764,30 @@ function parseForm(input) {
|
|
|
493
764
|
issue(`${path}.type`, `Unknown field type "${String(block.type)}".`);
|
|
494
765
|
}
|
|
495
766
|
if (block.props !== void 0) {
|
|
496
|
-
if (!
|
|
767
|
+
if (!isObject2(block.props) || !isJson(block.props)) issue(`${path}.props`, "Must be a JSON object.");
|
|
497
768
|
else if (block.props.options !== void 0) {
|
|
498
769
|
const options = block.props.options;
|
|
499
770
|
if (!Array.isArray(options)) issue(`${path}.props.options`, "Must be an array.");
|
|
500
771
|
else {
|
|
501
772
|
options.forEach((o, j) => {
|
|
502
773
|
const at = `${path}.props.options[${j}]`;
|
|
503
|
-
if (!
|
|
774
|
+
if (!isObject2(o) || typeof o.value !== "string") issue(at, "Each option needs a string value.");
|
|
504
775
|
else optionalText(o.label, `${at}.label`);
|
|
505
776
|
});
|
|
777
|
+
choices.set(
|
|
778
|
+
block.id,
|
|
779
|
+
new Set(options.flatMap((o) => isObject2(o) && typeof o.value === "string" ? [o.value] : []))
|
|
780
|
+
);
|
|
506
781
|
}
|
|
507
782
|
}
|
|
783
|
+
const { max, maxLength: maxLength2 } = isObject2(block.props) ? block.props : {};
|
|
784
|
+
if (block.type === "rating" && max !== void 0 && !(isInt(max) && max >= 2 && max <= 10)) {
|
|
785
|
+
issue(`${path}.props.max`, "Must be a whole number from 2 to 10.");
|
|
786
|
+
}
|
|
787
|
+
if (maxLength2 !== void 0 && !(isInt(maxLength2) && maxLength2 > 0)) {
|
|
788
|
+
issue(`${path}.props.maxLength`, "Must be a whole number above 0.");
|
|
789
|
+
}
|
|
790
|
+
if (isObject2(block.props) && typeof block.type === "string") structuredProps(block.type, block.props, `${path}.props`);
|
|
508
791
|
}
|
|
509
792
|
if (validKey) {
|
|
510
793
|
if (keys.has(block.key)) issue(`${path}.key`, `Duplicate key "${String(block.key)}".`);
|
|
@@ -521,9 +804,10 @@ function parseForm(input) {
|
|
|
521
804
|
}
|
|
522
805
|
});
|
|
523
806
|
const hiddenNames = /* @__PURE__ */ new Set();
|
|
807
|
+
const hiddenTypes = /* @__PURE__ */ new Map();
|
|
524
808
|
if (input.hidden !== void 0 && !Array.isArray(input.hidden)) issue("hidden", "Must be an array.");
|
|
525
809
|
(Array.isArray(input.hidden) ? input.hidden : []).forEach((h, i) => {
|
|
526
|
-
if (!
|
|
810
|
+
if (!isObject2(h) || typeof h.name !== "string" || !h.name) {
|
|
527
811
|
issue(`hidden[${i}]`, "Each hidden input needs a name.");
|
|
528
812
|
return;
|
|
529
813
|
}
|
|
@@ -536,13 +820,15 @@ function parseForm(input) {
|
|
|
536
820
|
issue(`hidden[${i}].type`, "Must be string, number or boolean.");
|
|
537
821
|
}
|
|
538
822
|
optionalBoolean(h.sensitive, `hidden[${i}].sensitive`);
|
|
823
|
+
optionalBoolean(h.identifying, `hidden[${i}].identifying`);
|
|
539
824
|
hiddenNames.add(h.name);
|
|
825
|
+
if (typeof h.type === "string") hiddenTypes.set(h.name, h.type);
|
|
540
826
|
});
|
|
541
827
|
const endingIds = /* @__PURE__ */ new Set();
|
|
542
828
|
if (input.endings !== void 0 && !Array.isArray(input.endings)) issue("endings", "Must be an array.");
|
|
543
829
|
(Array.isArray(input.endings) ? input.endings : []).forEach((e, i) => {
|
|
544
830
|
const path = `endings[${i}]`;
|
|
545
|
-
if (!
|
|
831
|
+
if (!isObject2(e)) {
|
|
546
832
|
issue(path, "Each ending must be an object with an id and a title.");
|
|
547
833
|
return;
|
|
548
834
|
}
|
|
@@ -554,7 +840,7 @@ function parseForm(input) {
|
|
|
554
840
|
});
|
|
555
841
|
const checkOperand = (o, path, reads) => {
|
|
556
842
|
if (o === void 0) return;
|
|
557
|
-
if (!
|
|
843
|
+
if (!isObject2(o)) return issue(path, "An operand must be an object.");
|
|
558
844
|
if ("field" in o) {
|
|
559
845
|
if (typeof o.field !== "string" || !fieldTypes.has(o.field)) return issue(path, `No field "${String(o.field)}".`);
|
|
560
846
|
reads.push(o.field);
|
|
@@ -567,7 +853,7 @@ function parseForm(input) {
|
|
|
567
853
|
}
|
|
568
854
|
};
|
|
569
855
|
const checkCondition = (c, path, reads, depth = 0) => {
|
|
570
|
-
if (!
|
|
856
|
+
if (!isObject2(c)) return issue(path, "A condition must be an object.");
|
|
571
857
|
if (depth > MAX_CONDITION_DEPTH) return issue(path, "The condition is nested too deeply.");
|
|
572
858
|
if ("all" in c || "any" in c) {
|
|
573
859
|
const group = "all" in c ? "all" : "any";
|
|
@@ -576,7 +862,7 @@ function parseForm(input) {
|
|
|
576
862
|
return list.forEach((x, i) => checkCondition(x, `${path}.${group}[${i}]`, reads, depth + 1));
|
|
577
863
|
}
|
|
578
864
|
if ("not" in c) return checkCondition(c.not, `${path}.not`, reads, depth + 1);
|
|
579
|
-
if (typeof c.op !== "string" || !
|
|
865
|
+
if (typeof c.op !== "string" || !isObject2(c.left)) {
|
|
580
866
|
return issue(path, "A comparison needs an `op` and a `left` operand.");
|
|
581
867
|
}
|
|
582
868
|
if (!OPERATORS.has(c.op)) issue(`${path}.op`, `Unknown operator "${c.op}".`);
|
|
@@ -585,16 +871,53 @@ function parseForm(input) {
|
|
|
585
871
|
if (typeof c.left.field === "string") {
|
|
586
872
|
const manifest = getFieldType(fieldTypes.get(c.left.field) ?? "");
|
|
587
873
|
if (manifest && OPERATORS.has(c.op) && !manifest.operators.includes(c.op)) {
|
|
588
|
-
issue(`${path}.op`, `"${c.op}" does not apply to a ${manifest.type} field.`);
|
|
874
|
+
return issue(`${path}.op`, `"${c.op}" does not apply to a ${manifest.type} field.`);
|
|
589
875
|
}
|
|
590
876
|
}
|
|
877
|
+
checkLiterals(c.op, c.left, c.right, path);
|
|
878
|
+
};
|
|
879
|
+
const checkLiterals = (op, left, right, path) => {
|
|
880
|
+
if (op === "answered" || op === "empty" || !OPERATORS.has(op)) return;
|
|
881
|
+
const literal = isObject2(right) && "value" in right;
|
|
882
|
+
if (right !== void 0 && !literal) return;
|
|
883
|
+
let what = "the left side";
|
|
884
|
+
let type;
|
|
885
|
+
let options;
|
|
886
|
+
if (typeof left.field === "string" && fieldTypes.has(left.field)) {
|
|
887
|
+
const fieldType = fieldTypes.get(left.field);
|
|
888
|
+
what = `"${keyOf(left.field)}"`;
|
|
889
|
+
type = LITERAL_TYPE[fieldType];
|
|
890
|
+
if (fieldType === "single-choice" || fieldType === "multi-choice") options = choices.get(left.field);
|
|
891
|
+
} else if (typeof left.hidden === "string" && hiddenTypes.has(left.hidden)) {
|
|
892
|
+
what = `hidden input "${left.hidden}"`;
|
|
893
|
+
type = hiddenTypes.get(left.hidden);
|
|
894
|
+
}
|
|
895
|
+
const outcome = op === "neq" ? "always true" : "never true";
|
|
896
|
+
let values;
|
|
897
|
+
if (op === "in") {
|
|
898
|
+
if (!literal || !Array.isArray(right.value)) {
|
|
899
|
+
return void warnings.push({ path: `${path}.right`, message: "`in` needs a list of values; it never matches." });
|
|
900
|
+
}
|
|
901
|
+
values = right.value;
|
|
902
|
+
} else if (!literal) return;
|
|
903
|
+
else values = [right.value];
|
|
904
|
+
if (ORDERING.has(op)) {
|
|
905
|
+
if (type !== void 0 && type !== "number") {
|
|
906
|
+
return void warnings.push({ path: `${path}.op`, message: `"${op}" compares numbers, but ${what} holds a ${type}; it is never true.` });
|
|
907
|
+
}
|
|
908
|
+
type = "number";
|
|
909
|
+
}
|
|
910
|
+
for (const v of values) {
|
|
911
|
+
const message = type !== void 0 && typeof v !== type ? `${what} holds a ${type}, but ${shown(v)} is not one; this comparison is ${outcome}.` : options && typeof v === "string" && !options.has(v) ? `${shown(v)} is not an option of ${what}; this comparison is ${outcome}.` : null;
|
|
912
|
+
if (message) warnings.push({ path: `${path}.right`, message });
|
|
913
|
+
}
|
|
591
914
|
};
|
|
592
915
|
const keyOf = (id) => [...keys].find(([, v]) => v === id)?.[0] ?? id;
|
|
593
916
|
const ruleIds = /* @__PURE__ */ new Set();
|
|
594
917
|
if (input.logic !== void 0 && !Array.isArray(input.logic)) issue("logic", "Must be an array.");
|
|
595
918
|
(Array.isArray(input.logic) ? input.logic : []).forEach((rule, i) => {
|
|
596
919
|
const path = `logic[${i}]`;
|
|
597
|
-
if (!
|
|
920
|
+
if (!isObject2(rule) || typeof rule.id !== "string" || !isObject2(rule.when)) {
|
|
598
921
|
issue(path, "Each rule needs an id and a `when` condition.");
|
|
599
922
|
return;
|
|
600
923
|
}
|
|
@@ -613,7 +936,7 @@ function parseForm(input) {
|
|
|
613
936
|
at = rule.from;
|
|
614
937
|
inclusive = true;
|
|
615
938
|
}
|
|
616
|
-
if (
|
|
939
|
+
if (isObject2(rule.to)) {
|
|
617
940
|
if (typeof rule.to.ending !== "string" || !endingIds.has(rule.to.ending)) {
|
|
618
941
|
issue(`${path}.to`, `No ending "${String(rule.to.ending)}".`);
|
|
619
942
|
}
|
|
@@ -645,7 +968,7 @@ function parseForm(input) {
|
|
|
645
968
|
}
|
|
646
969
|
});
|
|
647
970
|
input.blocks.forEach((block, i) => {
|
|
648
|
-
if (!
|
|
971
|
+
if (!isObject2(block)) return;
|
|
649
972
|
const raw = block.kind === "field" ? [block.label, block.description] : block.kind === "content" ? [block.text] : [];
|
|
650
973
|
const texts = raw.filter((t) => typeof t === "string");
|
|
651
974
|
for (const t of texts) {
|
|
@@ -663,11 +986,23 @@ function parseForm(input) {
|
|
|
663
986
|
}
|
|
664
987
|
}
|
|
665
988
|
});
|
|
989
|
+
const extra = [["description", input.description]];
|
|
990
|
+
(Array.isArray(input.endings) ? input.endings : []).forEach((e, i) => {
|
|
991
|
+
if (isObject2(e)) extra.push([`endings[${i}]`, e.title], [`endings[${i}]`, e.description]);
|
|
992
|
+
});
|
|
993
|
+
for (const [path, t] of extra) {
|
|
994
|
+
if (typeof t !== "string") continue;
|
|
995
|
+
for (const ref of recallRefs(t)) {
|
|
996
|
+
if (ref.kind === "hidden" ? !hiddenNames.has(ref.name) : !keys.has(ref.name)) {
|
|
997
|
+
warnings.push({ path, message: `Recalls unknown ${ref.kind === "hidden" ? "hidden input" : "field"} "${ref.name}"; it shows its fallback text.` });
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
666
1001
|
return issues.length ? fail() : { ok: true, form: input, warnings };
|
|
667
1002
|
}
|
|
668
1003
|
|
|
669
1004
|
// src/define.ts
|
|
670
|
-
function field(type, key, options,
|
|
1005
|
+
function field(type, key, options, props2) {
|
|
671
1006
|
const block = {
|
|
672
1007
|
kind: "field",
|
|
673
1008
|
id: options.id ?? key,
|
|
@@ -680,16 +1015,18 @@ function field(type, key, options, props) {
|
|
|
680
1015
|
if (options.required) block.required = true;
|
|
681
1016
|
if (options.sensitive) block.sensitive = true;
|
|
682
1017
|
if (options.identifying) block.identifying = true;
|
|
683
|
-
if (
|
|
1018
|
+
if (props2 && Object.keys(props2).length) block.props = props2;
|
|
684
1019
|
return block;
|
|
685
1020
|
}
|
|
1021
|
+
var props = (all) => Object.fromEntries(Object.entries(all).filter((e) => e[1] !== void 0));
|
|
686
1022
|
var choiceProps = (options) => ({
|
|
687
1023
|
options: options.map((o) => o.label ? { value: o.value, label: o.label } : { value: o.value })
|
|
688
1024
|
});
|
|
689
1025
|
var hiddenSpec = (type, o) => ({
|
|
690
1026
|
type,
|
|
691
1027
|
source: o.source ?? "url",
|
|
692
|
-
...o.sensitive ? { sensitive: true } : {}
|
|
1028
|
+
...o.sensitive ? { sensitive: true } : {},
|
|
1029
|
+
...o.identifying ? { identifying: true } : {}
|
|
693
1030
|
});
|
|
694
1031
|
var f = {
|
|
695
1032
|
shortText: (key, o) => field("short-text", key, o, o.maxLength ? { maxLength: o.maxLength } : void 0),
|
|
@@ -701,6 +1038,41 @@ var f = {
|
|
|
701
1038
|
singleChoice: (key, o) => field("single-choice", key, o, choiceProps(o.options)),
|
|
702
1039
|
multiChoice: (key, o) => field("multi-choice", key, o, choiceProps(o.options)),
|
|
703
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
|
+
),
|
|
704
1076
|
/** Any registered field type, including plugins. */
|
|
705
1077
|
custom: (type, key, o) => field(type, key, o, o.props),
|
|
706
1078
|
heading: (id, text) => ({ kind: "content", id, type: "heading", text }),
|
|
@@ -784,7 +1156,8 @@ function defineForm(input) {
|
|
|
784
1156
|
name,
|
|
785
1157
|
type: spec.type,
|
|
786
1158
|
source: spec.source,
|
|
787
|
-
...spec.sensitive ? { sensitive: true } : {}
|
|
1159
|
+
...spec.sensitive ? { sensitive: true } : {},
|
|
1160
|
+
...spec.identifying ? { identifying: true } : {}
|
|
788
1161
|
}));
|
|
789
1162
|
}
|
|
790
1163
|
if (input.logic?.length) definition.logic = input.logic.map((rule, i) => rule.build(resolve2, `r${i + 1}`));
|
|
@@ -809,6 +1182,7 @@ ${list}`);
|
|
|
809
1182
|
|
|
810
1183
|
// src/hidden.ts
|
|
811
1184
|
var MAX_HIDDEN_STRING_LENGTH = 1e3;
|
|
1185
|
+
var DECIMAL = /^-?\d+(\.\d+)?$/;
|
|
812
1186
|
function coerceHiddenValue(value, type) {
|
|
813
1187
|
if (typeof value === "string" && value.length > MAX_HIDDEN_STRING_LENGTH) return void 0;
|
|
814
1188
|
if (type === "string") {
|
|
@@ -818,7 +1192,7 @@ function coerceHiddenValue(value, type) {
|
|
|
818
1192
|
}
|
|
819
1193
|
if (type === "number") {
|
|
820
1194
|
if (typeof value === "number") return Number.isFinite(value) ? value : void 0;
|
|
821
|
-
if (typeof value !== "string" ||
|
|
1195
|
+
if (typeof value !== "string" || !DECIMAL.test(value)) return void 0;
|
|
822
1196
|
const n = Number(value);
|
|
823
1197
|
return Number.isFinite(n) ? n : void 0;
|
|
824
1198
|
}
|
|
@@ -999,7 +1373,7 @@ function build(definition, options, replaying = false) {
|
|
|
999
1373
|
for (const id of state.path) {
|
|
1000
1374
|
const field2 = fields.get(id);
|
|
1001
1375
|
const value = own(state.values, id);
|
|
1002
|
-
if (field2 && value
|
|
1376
|
+
if (field2 && !isEmptyValue(value, field2)) out[id] = normalizeValue(value, field2);
|
|
1003
1377
|
}
|
|
1004
1378
|
return out;
|
|
1005
1379
|
}
|
|
@@ -1169,6 +1543,6 @@ function describeCondition(condition, def, fields = fieldMap(def)) {
|
|
|
1169
1543
|
return `${left} ${op} ${right}`;
|
|
1170
1544
|
}
|
|
1171
1545
|
|
|
1172
|
-
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 };
|
|
1173
1547
|
//# sourceMappingURL=index.js.map
|
|
1174
1548
|
//# sourceMappingURL=index.js.map
|