@marktiderman/genesis-forms 0.5.0 → 0.6.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 +128 -0
- package/dist/destinations.cjs +55 -3
- package/dist/destinations.cjs.map +1 -1
- package/dist/destinations.d.cts +1 -1
- package/dist/destinations.d.ts +1 -1
- package/dist/destinations.js +55 -3
- package/dist/destinations.js.map +1 -1
- package/dist/{engine-BDq0XJ21.d.cts → engine-CpfOz8FN.d.ts} +10 -3
- package/dist/{engine-Qzp6bX6O.d.ts → engine-DC4YTPdr.d.cts} +10 -3
- package/dist/index.cjs +484 -62
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +29 -7
- package/dist/index.d.ts +29 -7
- package/dist/index.js +461 -63
- package/dist/index.js.map +1 -1
- package/dist/server.cjs +541 -21
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +193 -3
- package/dist/server.d.ts +193 -3
- package/dist/server.js +522 -22
- package/dist/server.js.map +1 -1
- package/dist/testing.cjs +314 -21
- package/dist/testing.cjs.map +1 -1
- package/dist/testing.d.cts +4 -4
- package/dist/testing.d.ts +4 -4
- package/dist/testing.js +314 -21
- package/dist/testing.js.map +1 -1
- package/dist/{types-BhYUbLJ-.d.cts → types-oAckGR_Z.d.cts} +10 -0
- package/dist/{types-BhYUbLJ-.d.ts → types-oAckGR_Z.d.ts} +10 -0
- package/dist/validate-B5WKHbst.d.cts +322 -0
- package/dist/validate-GpTXPwyl.d.ts +322 -0
- package/package.json +1 -1
- package/dist/validate-Bs1lyoOl.d.ts +0 -131
- package/dist/validate-BsZ0YTQS.d.cts +0 -131
package/dist/testing.cjs
CHANGED
|
@@ -831,6 +831,65 @@ function summarizeValue(value, field) {
|
|
|
831
831
|
if (Array.isArray(value)) return value.join(", ");
|
|
832
832
|
return String(value);
|
|
833
833
|
}
|
|
834
|
+
var OPTION_LIMITS = {
|
|
835
|
+
/** Characters in a description. */
|
|
836
|
+
description: 500,
|
|
837
|
+
/** Characters in an image URL. */
|
|
838
|
+
image: 2048,
|
|
839
|
+
/** Attributes on one option. */
|
|
840
|
+
attributes: 20,
|
|
841
|
+
/** Characters in an attribute's name. */
|
|
842
|
+
attributeName: 64,
|
|
843
|
+
/** Characters in one attribute value. */
|
|
844
|
+
attributeValue: 200,
|
|
845
|
+
/** Values in one attribute's list. */
|
|
846
|
+
attributeItems: 50
|
|
847
|
+
};
|
|
848
|
+
function isHttpsUrl(url) {
|
|
849
|
+
return typeof url === "string" && url.length <= OPTION_LIMITS.image && /^https:\/\/[^\s/?#@\\]+(?:[/?#][^\s]*)?$/i.test(url) && // No control characters anywhere (a line break would end a CSV row or an HTML attribute).
|
|
850
|
+
![...url].some((c) => c.charCodeAt(0) < 32 || c.charCodeAt(0) === 127);
|
|
851
|
+
}
|
|
852
|
+
function attributeName(name) {
|
|
853
|
+
return name.trim() !== "" && name.length <= OPTION_LIMITS.attributeName && !(name in Object.prototype);
|
|
854
|
+
}
|
|
855
|
+
function readAttributes(raw) {
|
|
856
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return void 0;
|
|
857
|
+
const out = {};
|
|
858
|
+
let count = 0;
|
|
859
|
+
for (const name of Object.keys(raw)) {
|
|
860
|
+
if (count >= OPTION_LIMITS.attributes) break;
|
|
861
|
+
if (!attributeName(name)) continue;
|
|
862
|
+
const v = raw[name];
|
|
863
|
+
const fits = (x) => typeof x === "string" && x.length <= OPTION_LIMITS.attributeValue;
|
|
864
|
+
if (fits(v)) out[name] = v;
|
|
865
|
+
else if (Array.isArray(v) && v.length <= OPTION_LIMITS.attributeItems && v.every(fits)) out[name] = [...v];
|
|
866
|
+
else continue;
|
|
867
|
+
count++;
|
|
868
|
+
}
|
|
869
|
+
return count ? out : void 0;
|
|
870
|
+
}
|
|
871
|
+
function readOption(raw) {
|
|
872
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
873
|
+
const o = raw;
|
|
874
|
+
if (typeof o.value !== "string") return null;
|
|
875
|
+
const out = { value: o.value };
|
|
876
|
+
if (typeof o.label === "string") out.label = o.label;
|
|
877
|
+
if (typeof o.description === "string" && o.description.trim() && o.description.length <= OPTION_LIMITS.description) {
|
|
878
|
+
out.description = o.description;
|
|
879
|
+
}
|
|
880
|
+
if (isHttpsUrl(o.image)) out.image = o.image;
|
|
881
|
+
const attributes = readAttributes(o.attributes);
|
|
882
|
+
if (attributes) out.attributes = attributes;
|
|
883
|
+
return out;
|
|
884
|
+
}
|
|
885
|
+
function optionJson(option) {
|
|
886
|
+
const out = { value: option.value };
|
|
887
|
+
if (option.label !== void 0) out.label = option.label;
|
|
888
|
+
if (option.description !== void 0) out.description = option.description;
|
|
889
|
+
if (option.image !== void 0) out.image = option.image;
|
|
890
|
+
if (option.attributes !== void 0) out.attributes = { ...option.attributes };
|
|
891
|
+
return out;
|
|
892
|
+
}
|
|
834
893
|
var OTHER_MAX_LENGTH = 200;
|
|
835
894
|
function otherOption(field) {
|
|
836
895
|
const other = field.props?.other;
|
|
@@ -848,14 +907,21 @@ var typedOther = (field, value) => isOtherValue(field, value) && value.trim() !=
|
|
|
848
907
|
function choiceOptions(field) {
|
|
849
908
|
const raw = field.props?.options;
|
|
850
909
|
if (!Array.isArray(raw)) return [];
|
|
851
|
-
return raw.flatMap(
|
|
852
|
-
|
|
853
|
-
|
|
910
|
+
return raw.flatMap((o) => {
|
|
911
|
+
const option = readOption(o);
|
|
912
|
+
return option ? [option] : [];
|
|
913
|
+
});
|
|
854
914
|
}
|
|
855
915
|
function optionsSource(field) {
|
|
856
916
|
const raw = field.props?.optionsFrom;
|
|
857
917
|
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return void 0;
|
|
858
|
-
|
|
918
|
+
if (raw.source !== "host" || typeof raw.key !== "string" || !raw.key) return void 0;
|
|
919
|
+
const out = { source: "host", key: raw.key };
|
|
920
|
+
const filter = raw.filter;
|
|
921
|
+
if (filter && typeof filter === "object" && !Array.isArray(filter) && typeof filter.attribute === "string" && filter.attribute && typeof filter.field === "string" && filter.field) {
|
|
922
|
+
out.filter = { attribute: filter.attribute, field: filter.field };
|
|
923
|
+
}
|
|
924
|
+
return out;
|
|
859
925
|
}
|
|
860
926
|
function hostList(hostOptions, key) {
|
|
861
927
|
if (!hostOptions || !Object.prototype.hasOwnProperty.call(hostOptions, key)) return [];
|
|
@@ -864,11 +930,10 @@ function hostList(hostOptions, key) {
|
|
|
864
930
|
const seen = /* @__PURE__ */ new Set();
|
|
865
931
|
const out = [];
|
|
866
932
|
for (const o of list) {
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
out.push(typeof label === "string" ? { value, label } : { value });
|
|
933
|
+
const option = readOption(o);
|
|
934
|
+
if (!option || !option.value || seen.has(option.value)) continue;
|
|
935
|
+
seen.add(option.value);
|
|
936
|
+
out.push(option);
|
|
872
937
|
}
|
|
873
938
|
return out;
|
|
874
939
|
}
|
|
@@ -883,7 +948,7 @@ function withHostOptions(definition, hostOptions) {
|
|
|
883
948
|
const added = hostList(hostOptions, from.key).filter((o) => !taken.has(o.value));
|
|
884
949
|
if (!added.length) return block;
|
|
885
950
|
changed = true;
|
|
886
|
-
const options = [...own2, ...added].map(
|
|
951
|
+
const options = [...own2, ...added].map(optionJson);
|
|
887
952
|
return { ...block, props: { ...block.props, options } };
|
|
888
953
|
});
|
|
889
954
|
return changed ? { ...definition, blocks } : definition;
|
|
@@ -1363,6 +1428,150 @@ function fieldMap(def) {
|
|
|
1363
1428
|
return map;
|
|
1364
1429
|
}
|
|
1365
1430
|
|
|
1431
|
+
// src/options.ts
|
|
1432
|
+
var CHOICE_FIELD_TYPES = /* @__PURE__ */ new Set(["single-choice", "multi-choice", "dropdown"]);
|
|
1433
|
+
var isChoice = (block) => !!block && block.kind === "field" && CHOICE_FIELD_TYPES.has(block.type);
|
|
1434
|
+
function put(record, key, value) {
|
|
1435
|
+
Object.defineProperty(record, key, { value, enumerable: true, writable: true, configurable: true });
|
|
1436
|
+
}
|
|
1437
|
+
function optionFilter(field) {
|
|
1438
|
+
return optionsSource(field)?.filter;
|
|
1439
|
+
}
|
|
1440
|
+
function answerValues(answer) {
|
|
1441
|
+
const one = (v) => typeof v === "string" ? [v] : typeof v === "number" || typeof v === "boolean" ? [String(v)] : [];
|
|
1442
|
+
if (answer === void 0 || answer === null) return [];
|
|
1443
|
+
return Array.isArray(answer) ? answer.flatMap(one) : one(answer);
|
|
1444
|
+
}
|
|
1445
|
+
function optionMatches(option, attribute, answer) {
|
|
1446
|
+
const have = option.attributes && Object.prototype.hasOwnProperty.call(option.attributes, attribute) ? option.attributes[attribute] : void 0;
|
|
1447
|
+
if (have === void 0) return false;
|
|
1448
|
+
const wanted = answerValues(answer);
|
|
1449
|
+
return (Array.isArray(have) ? have : [have]).some((v) => wanted.includes(v));
|
|
1450
|
+
}
|
|
1451
|
+
function filterSource(definition, key) {
|
|
1452
|
+
return definition.blocks.find((b) => b.kind === "field" && b.key === key);
|
|
1453
|
+
}
|
|
1454
|
+
function visibleOptions(definition, field, values) {
|
|
1455
|
+
const options = choiceOptions(field);
|
|
1456
|
+
const filter = optionFilter(field);
|
|
1457
|
+
if (!filter) return options;
|
|
1458
|
+
const source = filterSource(definition, filter.field);
|
|
1459
|
+
if (!source || source.id === field.id) return options;
|
|
1460
|
+
const answer = own(values, source.id);
|
|
1461
|
+
if (isEmptyValue(answer, source)) return options;
|
|
1462
|
+
return options.filter((o) => optionMatches(o, filter.attribute, answer));
|
|
1463
|
+
}
|
|
1464
|
+
function answersOnPath(state) {
|
|
1465
|
+
const out = /* @__PURE__ */ Object.create(null);
|
|
1466
|
+
for (const id of state.path) out[id] = own(state.values, id);
|
|
1467
|
+
return out;
|
|
1468
|
+
}
|
|
1469
|
+
var OUTSIDE_FILTER = "Choose one of the options.";
|
|
1470
|
+
function filteredChoiceError(definition, field, value, values) {
|
|
1471
|
+
if (!optionFilter(field) || isEmptyValue(value, field)) return null;
|
|
1472
|
+
const all = new Set(choiceOptions(field).map((o) => o.value));
|
|
1473
|
+
const shown2 = new Set(visibleOptions(definition, field, values).map((o) => o.value));
|
|
1474
|
+
const picked = answerValues(value);
|
|
1475
|
+
return picked.some((v) => all.has(v) && !shown2.has(v)) ? OUTSIDE_FILTER : null;
|
|
1476
|
+
}
|
|
1477
|
+
function hasOptionFilters(definition) {
|
|
1478
|
+
return definition.blocks.some((b) => isChoice(b) && optionFilter(b) !== void 0);
|
|
1479
|
+
}
|
|
1480
|
+
function clearFilteredAnswers(definition, values, changedId) {
|
|
1481
|
+
const changed = definition.blocks.find((b) => b.kind === "field" && b.id === changedId);
|
|
1482
|
+
if (!changed) return values;
|
|
1483
|
+
const keys = /* @__PURE__ */ new Set([changed.key]);
|
|
1484
|
+
let out = values;
|
|
1485
|
+
for (const block of definition.blocks) {
|
|
1486
|
+
if (!isChoice(block)) continue;
|
|
1487
|
+
const filter = optionFilter(block);
|
|
1488
|
+
if (!filter || !keys.has(filter.field)) continue;
|
|
1489
|
+
const value = own(out, block.id);
|
|
1490
|
+
if (value === void 0 || isEmptyValue(value, block)) continue;
|
|
1491
|
+
const all = new Set(choiceOptions(block).map((o) => o.value));
|
|
1492
|
+
const shown2 = new Set(visibleOptions(definition, block, out).map((o) => o.value));
|
|
1493
|
+
const keep = (v) => typeof v !== "string" || !all.has(v) || shown2.has(v);
|
|
1494
|
+
let next = value;
|
|
1495
|
+
if (Array.isArray(value)) {
|
|
1496
|
+
const kept = value.filter(keep);
|
|
1497
|
+
if (kept.length !== value.length) next = kept.length ? kept : void 0;
|
|
1498
|
+
} else if (!keep(value)) next = void 0;
|
|
1499
|
+
if (next === value) continue;
|
|
1500
|
+
if (out === values) out = { ...values };
|
|
1501
|
+
put(out, block.id, next);
|
|
1502
|
+
keys.add(block.key);
|
|
1503
|
+
}
|
|
1504
|
+
return out;
|
|
1505
|
+
}
|
|
1506
|
+
var MAX_PREFILL_PARAM_LENGTH = 64;
|
|
1507
|
+
function isPrefillParam(param) {
|
|
1508
|
+
return typeof param === "string" && param.length <= MAX_PREFILL_PARAM_LENGTH && /^[A-Za-z0-9_.-]+$/.test(param);
|
|
1509
|
+
}
|
|
1510
|
+
function prefillSetting(field) {
|
|
1511
|
+
if (!CHOICE_FIELD_TYPES.has(field.type)) return void 0;
|
|
1512
|
+
const raw = field.props?.prefill;
|
|
1513
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw) || !isPrefillParam(raw.param)) return void 0;
|
|
1514
|
+
return { param: raw.param, hideWhenPrefilled: raw.hideWhenPrefilled === true };
|
|
1515
|
+
}
|
|
1516
|
+
function paramValues(params, name) {
|
|
1517
|
+
let raw;
|
|
1518
|
+
if (typeof params.getAll === "function") {
|
|
1519
|
+
raw = params.getAll(name);
|
|
1520
|
+
} else if (Object.prototype.hasOwnProperty.call(params, name)) {
|
|
1521
|
+
raw = params[name];
|
|
1522
|
+
}
|
|
1523
|
+
const list = Array.isArray(raw) ? raw : raw === void 0 ? [] : [raw];
|
|
1524
|
+
return list.filter((v) => typeof v === "string" && v !== "" && v.length <= MAX_HIDDEN_STRING_LENGTH);
|
|
1525
|
+
}
|
|
1526
|
+
function prefillFromLink(definition, params, values = {}) {
|
|
1527
|
+
const out = {};
|
|
1528
|
+
if (!params || typeof params !== "object") return out;
|
|
1529
|
+
const seen = {};
|
|
1530
|
+
for (const id of Object.keys(values)) put(seen, id, values[id]);
|
|
1531
|
+
for (const block of definition.blocks) {
|
|
1532
|
+
if (!isChoice(block)) continue;
|
|
1533
|
+
const setting = prefillSetting(block);
|
|
1534
|
+
if (!setting || own(seen, block.id) !== void 0) continue;
|
|
1535
|
+
const raw = paramValues(params, setting.param);
|
|
1536
|
+
if (!raw.length) continue;
|
|
1537
|
+
const shown2 = new Set(visibleOptions(definition, block, seen).map((o) => o.value));
|
|
1538
|
+
let value;
|
|
1539
|
+
if (block.type === "multi-choice") {
|
|
1540
|
+
const picked = [];
|
|
1541
|
+
for (const r of raw) {
|
|
1542
|
+
for (const v of shown2.has(r) ? [r] : r.split(",").map((s) => s.trim())) {
|
|
1543
|
+
if (shown2.has(v) && !picked.includes(v)) picked.push(v);
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
if (picked.length) value = picked;
|
|
1547
|
+
} else value = raw.find((v) => shown2.has(v));
|
|
1548
|
+
if (value === void 0) continue;
|
|
1549
|
+
put(out, block.id, value);
|
|
1550
|
+
put(seen, block.id, value);
|
|
1551
|
+
}
|
|
1552
|
+
return out;
|
|
1553
|
+
}
|
|
1554
|
+
function hiddenByPrefill(definition, state) {
|
|
1555
|
+
const hideable = /* @__PURE__ */ new Set();
|
|
1556
|
+
for (const b of definition.blocks) {
|
|
1557
|
+
if (isChoice(b) && prefillSetting(b)?.hideWhenPrefilled) hideable.add(b.id);
|
|
1558
|
+
}
|
|
1559
|
+
if (!hideable.size) return [];
|
|
1560
|
+
const prefilled = /* @__PURE__ */ new Set();
|
|
1561
|
+
for (const e of state.events) {
|
|
1562
|
+
if (e.type !== "answer" || !hideable.has(e.field)) continue;
|
|
1563
|
+
if (e.source === "prefill") prefilled.add(e.field);
|
|
1564
|
+
else prefilled.delete(e.field);
|
|
1565
|
+
}
|
|
1566
|
+
if (!prefilled.size) return [];
|
|
1567
|
+
const fields = new Map(definition.blocks.map((b) => [b.id, b]));
|
|
1568
|
+
return state.path.filter((id) => {
|
|
1569
|
+
if (!prefilled.has(id) || own(state.errors, id) !== void 0) return false;
|
|
1570
|
+
const field = fields.get(id);
|
|
1571
|
+
return !isEmptyValue(own(state.values, id), field);
|
|
1572
|
+
});
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1366
1575
|
// src/recall.ts
|
|
1367
1576
|
var TOKEN = /\{\{\s*(field|hidden):([A-Za-z0-9_-]+)\s*(?:\|([^}]*))?\}\}/g;
|
|
1368
1577
|
function recall(text, scope) {
|
|
@@ -1484,16 +1693,34 @@ function build(given, options, replaying = false, budget) {
|
|
|
1484
1693
|
return at === void 0 ? event : { ...event, at };
|
|
1485
1694
|
};
|
|
1486
1695
|
const isVisibleIn = (s, id) => s.path.includes(id);
|
|
1696
|
+
const filters = hasOptionFilters(definition);
|
|
1697
|
+
const clearFiltered = (values, id) => filters ? clearFilteredAnswers(definition, values, id) : values;
|
|
1487
1698
|
const errorsFor = (s, ids) => {
|
|
1488
1699
|
const errors = {};
|
|
1700
|
+
let shown2;
|
|
1489
1701
|
for (const id of ids) {
|
|
1490
1702
|
const field = fields.get(id);
|
|
1491
1703
|
if (!field || !isVisibleIn(s, id)) continue;
|
|
1492
|
-
const
|
|
1704
|
+
const value = own(s.values, id);
|
|
1705
|
+
let message = validateField(field, value, { custom: options.validators?.[id] });
|
|
1706
|
+
if (!message && filters) message = filteredChoiceError(definition, field, value, shown2 ??= answersOnPath(s));
|
|
1493
1707
|
if (message) errors[id] = message;
|
|
1494
1708
|
}
|
|
1495
1709
|
return errors;
|
|
1496
1710
|
};
|
|
1711
|
+
const skipped = (s) => new Set(hiddenByPrefill(definition, s));
|
|
1712
|
+
const shownFrom = (s, from, skip) => {
|
|
1713
|
+
for (let i = from; i < s.path.length; i++) if (!skip.has(s.path[i])) return s.path[i];
|
|
1714
|
+
return void 0;
|
|
1715
|
+
};
|
|
1716
|
+
const showCurrent = () => {
|
|
1717
|
+
if (!state.current) return;
|
|
1718
|
+
const skip = skipped(state);
|
|
1719
|
+
if (!skip.has(state.current)) return;
|
|
1720
|
+
const at = state.path.indexOf(state.current);
|
|
1721
|
+
const shown2 = at < 0 ? void 0 : shownFrom(state, at, skip);
|
|
1722
|
+
if (shown2) state = { ...state, current: shown2, furthest: state.furthest === state.current ? shown2 : state.furthest };
|
|
1723
|
+
};
|
|
1497
1724
|
let unsettled = false;
|
|
1498
1725
|
let formulasStale = false;
|
|
1499
1726
|
const settleNow = (needValues = true) => {
|
|
@@ -1522,13 +1749,13 @@ function build(given, options, replaying = false, budget) {
|
|
|
1522
1749
|
const errors = { ...state.errors };
|
|
1523
1750
|
delete errors[id];
|
|
1524
1751
|
if (batch) {
|
|
1525
|
-
state.values = { ...state.values, [id]: value };
|
|
1752
|
+
state.values = clearFiltered({ ...state.values, [id]: value }, id);
|
|
1526
1753
|
state.errors = errors;
|
|
1527
1754
|
log(event);
|
|
1528
1755
|
unsettled = true;
|
|
1529
1756
|
return;
|
|
1530
1757
|
}
|
|
1531
|
-
const { values, path } = settle({ ...state.values, [id]: value }, state.hidden);
|
|
1758
|
+
const { values, path } = settle(clearFiltered({ ...state.values, [id]: value }, id), state.hidden);
|
|
1532
1759
|
emit({ ...state, values, path, errors, events: log(event) });
|
|
1533
1760
|
};
|
|
1534
1761
|
const engine = {
|
|
@@ -1559,7 +1786,7 @@ function build(given, options, replaying = false, budget) {
|
|
|
1559
1786
|
return false;
|
|
1560
1787
|
}
|
|
1561
1788
|
const at = state.path.indexOf(state.current);
|
|
1562
|
-
const target = state
|
|
1789
|
+
const target = shownFrom(state, at + 1, skipped(state));
|
|
1563
1790
|
if (!target) {
|
|
1564
1791
|
emit({ ...state, events });
|
|
1565
1792
|
return true;
|
|
@@ -1571,8 +1798,9 @@ function build(given, options, replaying = false, budget) {
|
|
|
1571
1798
|
back() {
|
|
1572
1799
|
if (state.status === "completed") return;
|
|
1573
1800
|
const history = [...state.history];
|
|
1801
|
+
const skip = skipped(state);
|
|
1574
1802
|
let previous = history.pop();
|
|
1575
|
-
while (previous && !state.path.includes(previous)) previous = history.pop();
|
|
1803
|
+
while (previous && (!state.path.includes(previous) || skip.has(previous))) previous = history.pop();
|
|
1576
1804
|
if (!previous) return;
|
|
1577
1805
|
emit({ ...state, current: previous, history, events: log(stamp({ type: "back" })) });
|
|
1578
1806
|
},
|
|
@@ -1600,15 +1828,18 @@ function build(given, options, replaying = false, budget) {
|
|
|
1600
1828
|
return out;
|
|
1601
1829
|
}
|
|
1602
1830
|
};
|
|
1603
|
-
|
|
1831
|
+
const fromLink = options.linkParams ? prefillFromLink(definition, options.linkParams, options.initialValues) : {};
|
|
1832
|
+
const initialValues = Object.keys(fromLink).length ? { ...fromLink, ...options.initialValues } : options.initialValues;
|
|
1833
|
+
if (initialValues) {
|
|
1604
1834
|
const wasBatch = batch;
|
|
1605
1835
|
batch = true;
|
|
1606
|
-
for (const [id, value] of Object.entries(
|
|
1836
|
+
for (const [id, value] of Object.entries(initialValues)) {
|
|
1607
1837
|
if (!fields.has(id) || value === void 0) continue;
|
|
1608
1838
|
answer(id, value, stamp({ type: "answer", field: id, value, source: "prefill" }));
|
|
1609
1839
|
}
|
|
1610
1840
|
settleNow();
|
|
1611
1841
|
batch = wasBatch;
|
|
1842
|
+
showCurrent();
|
|
1612
1843
|
}
|
|
1613
1844
|
const resumeAt = options.resumeAt;
|
|
1614
1845
|
if (resumeAt && !replaying && state.path.includes(resumeAt)) {
|
|
@@ -1627,18 +1858,22 @@ function build(given, options, replaying = false, budget) {
|
|
|
1627
1858
|
}
|
|
1628
1859
|
} else if (event.type === "next") {
|
|
1629
1860
|
settleNow(false);
|
|
1861
|
+
showCurrent();
|
|
1630
1862
|
if (formulasStale && isCalculated(fields.get(state.current ?? ""))) settleNow(true);
|
|
1631
1863
|
engine.next();
|
|
1632
1864
|
} else if (event.type === "back") {
|
|
1633
1865
|
settleNow(false);
|
|
1866
|
+
showCurrent();
|
|
1634
1867
|
engine.back();
|
|
1635
1868
|
} else if (event.type === "submit") {
|
|
1636
1869
|
settleNow();
|
|
1870
|
+
showCurrent();
|
|
1637
1871
|
engine.submit();
|
|
1638
1872
|
}
|
|
1639
1873
|
};
|
|
1640
1874
|
const finish = () => {
|
|
1641
1875
|
settleNow();
|
|
1876
|
+
showCurrent();
|
|
1642
1877
|
batch = false;
|
|
1643
1878
|
state = { ...state, events: [...state.events] };
|
|
1644
1879
|
};
|
|
@@ -1649,7 +1884,12 @@ function replay(definition, events, options = {}) {
|
|
|
1649
1884
|
}
|
|
1650
1885
|
function replayWithin(definition, events, options, maxWork) {
|
|
1651
1886
|
const budget = maxWork === void 0 ? void 0 : { used: 0, max: maxWork };
|
|
1652
|
-
const { engine, apply, finish } = build(
|
|
1887
|
+
const { engine, apply, finish } = build(
|
|
1888
|
+
definition,
|
|
1889
|
+
{ ...options, initialValues: void 0, linkParams: void 0, clock: () => void 0 },
|
|
1890
|
+
true,
|
|
1891
|
+
budget
|
|
1892
|
+
);
|
|
1653
1893
|
try {
|
|
1654
1894
|
for (const event of events) apply(event);
|
|
1655
1895
|
finish();
|
|
@@ -1900,6 +2140,31 @@ function parseForm(input) {
|
|
|
1900
2140
|
const optionalBoolean = (v, path) => {
|
|
1901
2141
|
if (v !== void 0 && typeof v !== "boolean") issue(path, "Must be true or false.");
|
|
1902
2142
|
};
|
|
2143
|
+
const optionDetails = (o, at) => {
|
|
2144
|
+
if (o.description !== void 0 && (typeof o.description !== "string" || o.description.length > OPTION_LIMITS.description)) {
|
|
2145
|
+
issue(`${at}.description`, `Must be text of at most ${OPTION_LIMITS.description} characters.`);
|
|
2146
|
+
}
|
|
2147
|
+
if (o.image !== void 0 && !isHttpsUrl(o.image)) {
|
|
2148
|
+
issue(`${at}.image`, `Must be an https:// link of at most ${OPTION_LIMITS.image} characters.`);
|
|
2149
|
+
}
|
|
2150
|
+
if (o.attributes === void 0) return;
|
|
2151
|
+
const attributes = o.attributes;
|
|
2152
|
+
if (!isObject2(attributes) || Object.keys(attributes).length > OPTION_LIMITS.attributes) {
|
|
2153
|
+
issue(`${at}.attributes`, `Must be an object of at most ${OPTION_LIMITS.attributes} attributes.`);
|
|
2154
|
+
return;
|
|
2155
|
+
}
|
|
2156
|
+
const fits = (x) => typeof x === "string" && x.length <= OPTION_LIMITS.attributeValue;
|
|
2157
|
+
for (const [name2, v] of Object.entries(attributes)) {
|
|
2158
|
+
if (!name2.trim() || name2.length > OPTION_LIMITS.attributeName || RESERVED.has(name2)) {
|
|
2159
|
+
issue(`${at}.attributes`, `${shown(name2)} isn't a usable attribute name (at most ${OPTION_LIMITS.attributeName} characters).`);
|
|
2160
|
+
} else if (!fits(v) && !(Array.isArray(v) && v.length <= OPTION_LIMITS.attributeItems && v.every(fits))) {
|
|
2161
|
+
issue(
|
|
2162
|
+
`${at}.attributes.${name2}`,
|
|
2163
|
+
`Must be text, or a list of at most ${OPTION_LIMITS.attributeItems} texts, each at most ${OPTION_LIMITS.attributeValue} characters.`
|
|
2164
|
+
);
|
|
2165
|
+
}
|
|
2166
|
+
}
|
|
2167
|
+
};
|
|
1903
2168
|
const name = (v, path, what) => {
|
|
1904
2169
|
if (typeof v !== "string" || !v) {
|
|
1905
2170
|
issue(path, `${what} must be a non-empty string.`);
|
|
@@ -2072,7 +2337,31 @@ function parseForm(input) {
|
|
|
2072
2337
|
} else if (typeof block.type === "string" && !CHOICE_TYPES.has(block.type)) {
|
|
2073
2338
|
warnings.push({ path: `${path}.props.optionsFrom`, message: "Only single choice, multiple choice and dropdown questions take options from the app." });
|
|
2074
2339
|
}
|
|
2340
|
+
const filter = isObject2(from) ? from.filter : void 0;
|
|
2341
|
+
if (filter !== void 0) {
|
|
2342
|
+
const at = `${path}.props.optionsFrom.filter`;
|
|
2343
|
+
if (!isObject2(filter) || typeof filter.attribute !== "string" || !filter.attribute || filter.attribute.length > OPTION_LIMITS.attributeName || typeof filter.field !== "string" || !filter.field) {
|
|
2344
|
+
issue(at, `Must be { attribute, field }: an attribute name (at most ${OPTION_LIMITS.attributeName} characters) and an earlier question's key.`);
|
|
2345
|
+
} else if (!keys.has(filter.field)) {
|
|
2346
|
+
issue(`${at}.field`, `"${filter.field}" must be the key of an earlier question.`);
|
|
2347
|
+
}
|
|
2348
|
+
}
|
|
2349
|
+
}
|
|
2350
|
+
const prefill = isObject2(block.props) ? block.props.prefill : void 0;
|
|
2351
|
+
if (prefill !== void 0) {
|
|
2352
|
+
if (!isObject2(prefill) || !isPrefillParam(prefill.param)) {
|
|
2353
|
+
issue(
|
|
2354
|
+
`${path}.props.prefill`,
|
|
2355
|
+
`Must be { param, hideWhenPrefilled? }: a link parameter of letters, digits, "_", "-" or "." (at most ${MAX_PREFILL_PARAM_LENGTH} characters).`
|
|
2356
|
+
);
|
|
2357
|
+
} else {
|
|
2358
|
+
optionalBoolean(prefill.hideWhenPrefilled, `${path}.props.prefill.hideWhenPrefilled`);
|
|
2359
|
+
if (typeof block.type === "string" && !CHOICE_TYPES.has(block.type)) {
|
|
2360
|
+
warnings.push({ path: `${path}.props.prefill`, message: "Only single choice, multiple choice and dropdown questions are filled in from a link." });
|
|
2361
|
+
}
|
|
2362
|
+
}
|
|
2075
2363
|
}
|
|
2364
|
+
if (isObject2(block.props)) optionalBoolean(block.props.searchable, `${path}.props.searchable`);
|
|
2076
2365
|
if (!isObject2(block.props) || !isJson(block.props)) issue(`${path}.props`, "Must be a JSON object.");
|
|
2077
2366
|
else if (block.props.options !== void 0) {
|
|
2078
2367
|
const options = block.props.options;
|
|
@@ -2081,7 +2370,10 @@ function parseForm(input) {
|
|
|
2081
2370
|
options.forEach((o, j) => {
|
|
2082
2371
|
const at = `${path}.props.options[${j}]`;
|
|
2083
2372
|
if (!isObject2(o) || typeof o.value !== "string") issue(at, "Each option needs a string value.");
|
|
2084
|
-
else
|
|
2373
|
+
else {
|
|
2374
|
+
optionalText(o.label, `${at}.label`);
|
|
2375
|
+
optionDetails(o, at);
|
|
2376
|
+
}
|
|
2085
2377
|
});
|
|
2086
2378
|
if (from === void 0) {
|
|
2087
2379
|
choices.set(
|
|
@@ -2511,11 +2803,12 @@ function validateSubmission(published, submission, options = {}) {
|
|
|
2511
2803
|
}
|
|
2512
2804
|
} else {
|
|
2513
2805
|
const errors = {};
|
|
2514
|
-
|
|
2806
|
+
const kept = engine.answers();
|
|
2807
|
+
for (const [id, value] of Object.entries(kept)) {
|
|
2515
2808
|
const message = validateField(fields.get(id), value, {
|
|
2516
2809
|
requireAnswers: false,
|
|
2517
2810
|
custom: options.validators?.[id]
|
|
2518
|
-
});
|
|
2811
|
+
}) ?? filteredChoiceError(definition, fields.get(id), value, kept);
|
|
2519
2812
|
if (message) errors[id] = message;
|
|
2520
2813
|
}
|
|
2521
2814
|
if (Object.keys(errors).length) return { ok: false, errors };
|