@marktiderman/genesis-forms 0.8.0 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +299 -10
- package/dist/destinations.cjs +331 -114
- package/dist/destinations.cjs.map +1 -1
- package/dist/destinations.d.cts +67 -45
- package/dist/destinations.d.ts +67 -45
- package/dist/destinations.js +327 -111
- package/dist/destinations.js.map +1 -1
- package/dist/{engine-Bj23YJlB.d.cts → engine-BaHPC2-1.d.cts} +2 -2
- package/dist/{engine-CzinTJph.d.ts → engine-Da99Tv-E.d.ts} +2 -2
- package/dist/index.cjs +996 -167
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +288 -11
- package/dist/index.d.ts +288 -11
- package/dist/index.js +975 -168
- package/dist/index.js.map +1 -1
- package/dist/server.cjs +563 -173
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +3 -474
- package/dist/server.d.ts +3 -474
- package/dist/server.js +558 -174
- package/dist/server.js.map +1 -1
- package/dist/submit-protocol-CFi-0Qd0.d.ts +840 -0
- package/dist/submit-protocol-D0cOckmK.d.cts +840 -0
- package/dist/testing.cjs +250 -154
- 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 +250 -154
- package/dist/testing.js.map +1 -1
- package/dist/{types-jqDKcmP4.d.cts → types-C0pUfew3.d.cts} +7 -1
- package/dist/{types-jqDKcmP4.d.ts → types-C0pUfew3.d.ts} +7 -1
- package/dist/{validate-DvfQHYnu.d.ts → validate-DpmBssFk.d.ts} +258 -140
- package/dist/{validate-irIPubxc.d.cts → validate-U5LoW3eR.d.cts} +258 -140
- package/package.json +3 -2
- package/dist/upload-protocol-BOe018kL.d.cts +0 -122
- package/dist/upload-protocol-BUO74WkS.d.ts +0 -122
package/dist/index.cjs
CHANGED
|
@@ -30,6 +30,45 @@ function fingerprint(value) {
|
|
|
30
30
|
return (4294967296 * (2097151 & h2) + (h1 >>> 0)).toString(16).padStart(14, "0");
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
// src/props-check.ts
|
|
34
|
+
var RESERVED_NAMES = new Set(Object.getOwnPropertyNames(Object.prototype));
|
|
35
|
+
var isPlainRecord = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
36
|
+
var isWholeNumber = (v) => typeof v === "number" && Number.isInteger(v);
|
|
37
|
+
var isFiniteNumber = (v) => typeof v === "number" && Number.isFinite(v);
|
|
38
|
+
function propsChecker(prefix, report) {
|
|
39
|
+
const at = (path) => path ? `${prefix}.${path}` : prefix;
|
|
40
|
+
const issue = (path, message2) => report(at(path), message2);
|
|
41
|
+
return {
|
|
42
|
+
issue,
|
|
43
|
+
text(v, path, nonEmpty = false) {
|
|
44
|
+
if (typeof v !== "string" || nonEmpty && !v) issue(path, nonEmpty ? "Required." : "Required text.");
|
|
45
|
+
},
|
|
46
|
+
optionalText(v, path) {
|
|
47
|
+
if (v !== void 0 && typeof v !== "string") issue(path, "Must be text.");
|
|
48
|
+
},
|
|
49
|
+
optionalBoolean(v, path) {
|
|
50
|
+
if (v !== void 0 && typeof v !== "boolean") issue(path, "Must be true or false.");
|
|
51
|
+
},
|
|
52
|
+
optionalInt(v, path, floor) {
|
|
53
|
+
if (v !== void 0 && !(isWholeNumber(v) && v >= floor)) issue(path, `Must be a whole number of ${floor} or more.`);
|
|
54
|
+
},
|
|
55
|
+
ordered(low, high, path, what) {
|
|
56
|
+
if (isWholeNumber(low) && isWholeNumber(high) && low > high) issue(path, `Can't be more than the ${what}.`);
|
|
57
|
+
},
|
|
58
|
+
name(v, path, what) {
|
|
59
|
+
if (typeof v !== "string" || !v) {
|
|
60
|
+
issue(path, `${what} must be a non-empty string.`);
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
if (RESERVED_NAMES.has(v)) {
|
|
64
|
+
issue(path, `"${v}" is reserved and cannot be used as ${what.toLowerCase()}.`);
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
33
72
|
// src/formula.ts
|
|
34
73
|
var FORMULA_FUNCTIONS = ["sum", "avg", "min", "max"];
|
|
35
74
|
var MAX_FORMULA_LENGTH = 2e3;
|
|
@@ -39,8 +78,8 @@ var TWO_CHAR = /* @__PURE__ */ new Set(["==", "!=", "<=", ">="]);
|
|
|
39
78
|
var ONE_CHAR = /* @__PURE__ */ new Set(["+", "-", "*", "/", "<", ">", "(", ")", "?", ":", ",", ".", "=", "\u2212", "\xD7", "\xF7", "\u2260", "\u2264", "\u2265"]);
|
|
40
79
|
var COMPARISONS = /* @__PURE__ */ new Set(["==", "!=", "<", "<=", ">", ">="]);
|
|
41
80
|
var FormulaError = class extends Error {
|
|
42
|
-
constructor(
|
|
43
|
-
super(
|
|
81
|
+
constructor(message2, at) {
|
|
82
|
+
super(message2);
|
|
44
83
|
this.at = at;
|
|
45
84
|
}
|
|
46
85
|
at;
|
|
@@ -118,8 +157,8 @@ function parseFormula(source) {
|
|
|
118
157
|
const tok = peek();
|
|
119
158
|
return tok.t === "op" && tok.v === v;
|
|
120
159
|
};
|
|
121
|
-
const expect = (v,
|
|
122
|
-
if (!isOp(v)) throw new FormulaError(
|
|
160
|
+
const expect = (v, message2) => {
|
|
161
|
+
if (!isOp(v)) throw new FormulaError(message2, peek().at);
|
|
123
162
|
pos++;
|
|
124
163
|
};
|
|
125
164
|
const describe = (tok) => tok.t === "end" ? "the formula ends there" : `found "${String(tok.v)}"`;
|
|
@@ -593,6 +632,17 @@ var calculatedFieldType = {
|
|
|
593
632
|
value: { json: true },
|
|
594
633
|
operators: numberOps,
|
|
595
634
|
a11y: { semantics: "control" },
|
|
635
|
+
// A number, unless the format says text; without a format it can hold either, so it isn't known.
|
|
636
|
+
literal: (props2) => props2.format === "text" ? "string" : props2.format === "number" || props2.format === "percent" ? "number" : void 0,
|
|
637
|
+
// The formula itself is checked once every key is known (`parseForm`).
|
|
638
|
+
checkProps: (props2, check) => {
|
|
639
|
+
if (props2.format !== void 0 && props2.format !== "number" && props2.format !== "percent" && props2.format !== "text") {
|
|
640
|
+
check.issue("format", "Must be number, percent or text.");
|
|
641
|
+
}
|
|
642
|
+
if (props2.decimals !== void 0 && !(isWholeNumber(props2.decimals) && props2.decimals >= 0 && props2.decimals <= MAX_CALCULATED_DECIMALS)) {
|
|
643
|
+
check.issue("decimals", `Must be a whole number from 0 to ${MAX_CALCULATED_DECIMALS}.`);
|
|
644
|
+
}
|
|
645
|
+
},
|
|
596
646
|
// The engine works the value out; any number, text or true / false it made is valid.
|
|
597
647
|
validate: (value) => typeof value === "number" || typeof value === "string" || typeof value === "boolean" ? null : "This value is worked out from other answers.",
|
|
598
648
|
summarize: (value, field2) => formatCalculated(value, field2)
|
|
@@ -705,12 +755,42 @@ var MAX_HIGHLIGHT_QUOTE_LENGTH = 2e3;
|
|
|
705
755
|
var HIGHLIGHT_NOTE_MESSAGE = "Add a note to each highlight.";
|
|
706
756
|
var sourceText = (field2) => typeof field2.props?.text === "string" && field2.props.text ? field2.props.text : void 0;
|
|
707
757
|
var STRUCTURED_FIELD_TYPES = ["list", "table", "media-notes", "text-highlights"];
|
|
758
|
+
var listProps = (props2, check) => {
|
|
759
|
+
check.optionalInt(props2.min, "min", 0);
|
|
760
|
+
check.optionalInt(props2.max, "max", 1);
|
|
761
|
+
check.optionalInt(props2.itemMaxLength, "itemMaxLength", 1);
|
|
762
|
+
check.optionalText(props2.placeholder, "placeholder");
|
|
763
|
+
check.ordered(props2.min, props2.max ?? 10, "min", "most items");
|
|
764
|
+
};
|
|
765
|
+
var tableProps = (props2, check) => {
|
|
766
|
+
const columns = props2.columns;
|
|
767
|
+
if (!Array.isArray(columns) || columns.length < 1 || columns.length > MAX_TABLE_COLUMNS) {
|
|
768
|
+
check.issue("columns", `Must be a list of 1 to ${MAX_TABLE_COLUMNS} columns.`);
|
|
769
|
+
} else {
|
|
770
|
+
const seen = /* @__PURE__ */ new Set();
|
|
771
|
+
columns.forEach((c, j) => {
|
|
772
|
+
const at = `columns[${j}]`;
|
|
773
|
+
if (!isPlainRecord(c)) return check.issue(at, "Each column needs a key and a label.");
|
|
774
|
+
if (check.name(c.key, `${at}.key`, "A column key")) {
|
|
775
|
+
if (seen.has(c.key)) check.issue(`${at}.key`, `Duplicate column key "${c.key}".`);
|
|
776
|
+
seen.add(c.key);
|
|
777
|
+
}
|
|
778
|
+
check.text(c.label, `${at}.label`);
|
|
779
|
+
check.optionalBoolean(c.long, `${at}.long`);
|
|
780
|
+
});
|
|
781
|
+
}
|
|
782
|
+
check.optionalInt(props2.minRows, "minRows", 0);
|
|
783
|
+
check.optionalInt(props2.maxRows, "maxRows", 1);
|
|
784
|
+
check.ordered(props2.minRows, props2.maxRows ?? 3, "minRows", "most rows");
|
|
785
|
+
};
|
|
708
786
|
var structuredFieldTypes = [
|
|
709
787
|
{
|
|
710
788
|
type: "list",
|
|
711
789
|
label: "List",
|
|
712
790
|
category: "text",
|
|
713
791
|
value: { json: true },
|
|
792
|
+
literal: "string",
|
|
793
|
+
checkProps: listProps,
|
|
714
794
|
// logic.ts `contains` on an array is exact item equality, same as multi-choice, so it is offered.
|
|
715
795
|
operators: ["contains", "answered", "empty"],
|
|
716
796
|
a11y: { semantics: "group" },
|
|
@@ -736,6 +816,7 @@ var structuredFieldTypes = [
|
|
|
736
816
|
label: "Table",
|
|
737
817
|
category: "advanced",
|
|
738
818
|
value: { json: true },
|
|
819
|
+
checkProps: tableProps,
|
|
739
820
|
operators: ["answered", "empty"],
|
|
740
821
|
a11y: { semantics: "group" },
|
|
741
822
|
isEmpty: (value) => emptyList(value, (r) => isObject(r) && Object.values(r).every(blankText)),
|
|
@@ -779,6 +860,10 @@ var structuredFieldTypes = [
|
|
|
779
860
|
label: "Notes at moments",
|
|
780
861
|
category: "media",
|
|
781
862
|
value: { json: true },
|
|
863
|
+
checkProps: (props2, check) => {
|
|
864
|
+
check.optionalInt(props2.maxNotes, "maxNotes", 1);
|
|
865
|
+
check.optionalInt(props2.textMaxLength, "textMaxLength", 1);
|
|
866
|
+
},
|
|
782
867
|
operators: ["answered", "empty"],
|
|
783
868
|
a11y: { semantics: "group" },
|
|
784
869
|
isEmpty: (value) => emptyList(value, (n) => isObject(n) && blankText(n.text)),
|
|
@@ -802,6 +887,10 @@ var structuredFieldTypes = [
|
|
|
802
887
|
label: "Highlights",
|
|
803
888
|
category: "media",
|
|
804
889
|
value: { json: true },
|
|
890
|
+
checkProps: (props2, check) => {
|
|
891
|
+
check.optionalText(props2.text, "text");
|
|
892
|
+
check.optionalInt(props2.maxHighlights, "maxHighlights", 1);
|
|
893
|
+
},
|
|
805
894
|
operators: ["answered", "empty"],
|
|
806
895
|
a11y: { semantics: "group" },
|
|
807
896
|
isEmpty: (value) => emptyList(value, () => false),
|
|
@@ -894,6 +983,22 @@ var photoFieldType = {
|
|
|
894
983
|
value: { json: true },
|
|
895
984
|
operators: ["answered", "empty"],
|
|
896
985
|
a11y: { semantics: "group" },
|
|
986
|
+
checkProps: (props2, check) => {
|
|
987
|
+
if (props2.maxFiles !== void 0 && !(isWholeNumber(props2.maxFiles) && props2.maxFiles >= 1 && props2.maxFiles <= MAX_PHOTO_FILES)) {
|
|
988
|
+
check.issue("maxFiles", `Must be a whole number from 1 to ${MAX_PHOTO_FILES}.`);
|
|
989
|
+
}
|
|
990
|
+
if (props2.maxSizeMb !== void 0 && !(isWholeNumber(props2.maxSizeMb) && props2.maxSizeMb >= 1 && props2.maxSizeMb <= MAX_PHOTO_SIZE_MB)) {
|
|
991
|
+
check.issue("maxSizeMb", `Must be a whole number from 1 to ${MAX_PHOTO_SIZE_MB}.`);
|
|
992
|
+
}
|
|
993
|
+
if (props2.formats !== void 0) {
|
|
994
|
+
const formats = props2.formats;
|
|
995
|
+
if (!Array.isArray(formats) || formats.length === 0 || !formats.every(isPhotoFormat)) {
|
|
996
|
+
check.issue("formats", "Must be a list of one or more of jpeg, png, heic and webp.");
|
|
997
|
+
} else if (new Set(formats).size !== formats.length) {
|
|
998
|
+
check.issue("formats", "Name each format once.");
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
},
|
|
897
1002
|
isEmpty: (value) => value === void 0 || value === null || Array.isArray(value) && value.length === 0,
|
|
898
1003
|
validate: (value, field2) => {
|
|
899
1004
|
if (!Array.isArray(value) || !value.every(isPhotoRef)) return "Add a photo.";
|
|
@@ -911,18 +1016,48 @@ var photoFieldType = {
|
|
|
911
1016
|
};
|
|
912
1017
|
|
|
913
1018
|
// src/fields.ts
|
|
914
|
-
var
|
|
1019
|
+
var VALIDATOR_FAILED_MESSAGE = "This answer couldn't be checked.";
|
|
1020
|
+
var REGISTRY = /* @__PURE__ */ Symbol.for("@marktiderman/genesis-forms/field-types@2");
|
|
1021
|
+
var BUILT_IN = /* @__PURE__ */ Symbol.for("@marktiderman/genesis-forms/built-in-field-types@2");
|
|
915
1022
|
var globalScope = globalThis;
|
|
916
1023
|
var registry = globalScope[REGISTRY] ??= /* @__PURE__ */ new Map();
|
|
917
|
-
|
|
918
|
-
|
|
1024
|
+
var builtIn = globalScope[BUILT_IN] ??= /* @__PURE__ */ new Set();
|
|
1025
|
+
var freezeRecord = (v) => v && typeof v === "object" && !Array.isArray(v) ? Object.freeze({ ...v }) : v;
|
|
1026
|
+
function frozen(manifest) {
|
|
1027
|
+
const copy = { ...manifest };
|
|
1028
|
+
for (const key of ["choice", "a11y", "value"]) if (key in copy) copy[key] = freezeRecord(copy[key]);
|
|
1029
|
+
if (Array.isArray(copy.operators)) {
|
|
1030
|
+
const operators = Object.freeze([...copy.operators]);
|
|
1031
|
+
Object.defineProperty(copy, "operators", { get: () => [...operators], enumerable: true });
|
|
1032
|
+
}
|
|
1033
|
+
return Object.freeze(copy);
|
|
1034
|
+
}
|
|
1035
|
+
function registerFieldType(manifest, options = {}) {
|
|
1036
|
+
if (!manifest || typeof manifest.type !== "string" || !manifest.type) {
|
|
1037
|
+
throw new Error("registerFieldType: a field type needs a `type`.");
|
|
1038
|
+
}
|
|
919
1039
|
const value = manifest.value;
|
|
920
|
-
if (!value.json && (typeof value.serialize !== "function" || typeof value.deserialize !== "function")) {
|
|
1040
|
+
if (value && !value.json && (typeof value.serialize !== "function" || typeof value.deserialize !== "function")) {
|
|
921
1041
|
throw new Error(
|
|
922
1042
|
`registerFieldType: "${manifest.type}" stores a non-JSON value, so it must provide both serialize and deserialize.`
|
|
923
1043
|
);
|
|
924
1044
|
}
|
|
925
|
-
|
|
1045
|
+
if (!value || !Array.isArray(manifest.operators)) {
|
|
1046
|
+
throw new Error(`registerFieldType: "${manifest.type}" needs a \`value\` and a list of \`operators\`.`);
|
|
1047
|
+
}
|
|
1048
|
+
if (manifest.choice !== void 0 && typeof manifest.choice?.multiple !== "boolean") {
|
|
1049
|
+
throw new Error(`registerFieldType: "${manifest.type}" has a \`choice\` without \`multiple: true | false\`.`);
|
|
1050
|
+
}
|
|
1051
|
+
if (builtIn.has(manifest.type) && options.replace !== true) {
|
|
1052
|
+
throw new Error(
|
|
1053
|
+
`registerFieldType: "${manifest.type}" is a built-in field type; pass { replace: true } to replace it for every form in this process.`
|
|
1054
|
+
);
|
|
1055
|
+
}
|
|
1056
|
+
registry.set(manifest.type, frozen(withChoiceDefaults(manifest)));
|
|
1057
|
+
}
|
|
1058
|
+
function registerBuiltIn(manifest) {
|
|
1059
|
+
builtIn.add(manifest.type);
|
|
1060
|
+
if (!registry.has(manifest.type)) registry.set(manifest.type, frozen(manifest));
|
|
926
1061
|
}
|
|
927
1062
|
function getFieldType(type) {
|
|
928
1063
|
return registry.get(type);
|
|
@@ -930,6 +1065,67 @@ function getFieldType(type) {
|
|
|
930
1065
|
function listFieldTypes() {
|
|
931
1066
|
return [...registry.values()];
|
|
932
1067
|
}
|
|
1068
|
+
function isBuiltInFieldType(type) {
|
|
1069
|
+
return builtIn.has(type);
|
|
1070
|
+
}
|
|
1071
|
+
function choiceKind(type) {
|
|
1072
|
+
return registry.get(type)?.choice;
|
|
1073
|
+
}
|
|
1074
|
+
function isChoiceField(block) {
|
|
1075
|
+
return !!block && block.kind === "field" && typeof block.type === "string" && choiceKind(block.type) !== void 0;
|
|
1076
|
+
}
|
|
1077
|
+
function literalType(type, props2) {
|
|
1078
|
+
const literal = registry.get(type)?.literal;
|
|
1079
|
+
if (typeof literal !== "function") return literal;
|
|
1080
|
+
try {
|
|
1081
|
+
return literal(props2);
|
|
1082
|
+
} catch {
|
|
1083
|
+
return void 0;
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
var CONTENT_REGISTRY = /* @__PURE__ */ Symbol.for("@marktiderman/genesis-forms/content-types@1");
|
|
1087
|
+
var contentScope = globalThis;
|
|
1088
|
+
var contentRegistry = contentScope[CONTENT_REGISTRY] ??= /* @__PURE__ */ new Map();
|
|
1089
|
+
var BUILT_IN_CONTENT = /* @__PURE__ */ new Set(["heading", "text"]);
|
|
1090
|
+
function registerContentType(manifest, options = {}) {
|
|
1091
|
+
if (!manifest || typeof manifest.type !== "string" || !manifest.type) {
|
|
1092
|
+
throw new Error("registerContentType: a content type needs a `type`.");
|
|
1093
|
+
}
|
|
1094
|
+
if (typeof manifest.label !== "string") throw new Error(`registerContentType: "${manifest.type}" needs a \`label\`.`);
|
|
1095
|
+
for (const hook of ["checkProps", "text"]) {
|
|
1096
|
+
if (manifest[hook] !== void 0 && typeof manifest[hook] !== "function") {
|
|
1097
|
+
throw new Error(`registerContentType: "${manifest.type}" has a \`${hook}\` that isn't a function.`);
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
if (BUILT_IN_CONTENT.has(manifest.type) && options.replace !== true) {
|
|
1101
|
+
throw new Error(
|
|
1102
|
+
`registerContentType: "${manifest.type}" is a built-in content type; pass { replace: true } to replace it for every form in this process.`
|
|
1103
|
+
);
|
|
1104
|
+
}
|
|
1105
|
+
contentRegistry.set(manifest.type, frozen(manifest));
|
|
1106
|
+
}
|
|
1107
|
+
function getContentType(type) {
|
|
1108
|
+
return contentRegistry.get(type);
|
|
1109
|
+
}
|
|
1110
|
+
function listContentTypes() {
|
|
1111
|
+
return [...contentRegistry.values()];
|
|
1112
|
+
}
|
|
1113
|
+
function contentText(block) {
|
|
1114
|
+
const text = contentRegistry.get(block.type)?.text;
|
|
1115
|
+
if (!text) return block.text;
|
|
1116
|
+
try {
|
|
1117
|
+
const shown2 = text(block);
|
|
1118
|
+
return typeof shown2 === "string" ? shown2 : block.text;
|
|
1119
|
+
} catch {
|
|
1120
|
+
return block.text;
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
for (const manifest of [
|
|
1124
|
+
{ type: "heading", label: "Heading" },
|
|
1125
|
+
{ type: "text", label: "Text" }
|
|
1126
|
+
]) {
|
|
1127
|
+
if (!contentRegistry.has(manifest.type)) contentRegistry.set(manifest.type, frozen(manifest));
|
|
1128
|
+
}
|
|
933
1129
|
function isEmptyValue(value, field2) {
|
|
934
1130
|
const manifest = field2 ? registry.get(field2.type) : void 0;
|
|
935
1131
|
if (manifest?.isEmpty) return manifest.isEmpty(value);
|
|
@@ -1115,6 +1311,27 @@ function isPhoneNumber(value) {
|
|
|
1115
1311
|
}
|
|
1116
1312
|
return digits >= 7 && digits <= 15;
|
|
1117
1313
|
}
|
|
1314
|
+
var oneOptionValidate = (value, field2) => typeof value === "string" && (choiceOptions(field2).some((o) => o.value === value) || typedOther(field2, value)) ? null : "Choose one of the options.";
|
|
1315
|
+
var oneOptionSummary = (value, field2) => optionLabel(field2, String(value));
|
|
1316
|
+
var optionsValidate = (value, field2) => {
|
|
1317
|
+
const allowed = new Set(choiceOptions(field2).map((o) => o.value));
|
|
1318
|
+
const others = Array.isArray(value) ? value.filter((v) => typeof v === "string" && !allowed.has(v)) : [];
|
|
1319
|
+
return Array.isArray(value) && new Set(value).size === value.length && value.every((v) => typeof v === "string") && (others.length === 0 || others.length === 1 && typedOther(field2, others[0])) ? null : "Choose from the options.";
|
|
1320
|
+
};
|
|
1321
|
+
var optionsSummary = (value, field2) => Array.isArray(value) ? value.map((v) => optionLabel(field2, String(v))).join(", ") : String(value);
|
|
1322
|
+
function withChoiceDefaults(manifest) {
|
|
1323
|
+
if (!manifest.choice || manifest.validate && manifest.summarize) return manifest;
|
|
1324
|
+
const multiple = manifest.choice.multiple;
|
|
1325
|
+
return {
|
|
1326
|
+
...manifest,
|
|
1327
|
+
validate: manifest.validate ?? (multiple ? optionsValidate : oneOptionValidate),
|
|
1328
|
+
summarize: manifest.summarize ?? (multiple ? optionsSummary : oneOptionSummary)
|
|
1329
|
+
};
|
|
1330
|
+
}
|
|
1331
|
+
var scaleProps = (props2, check) => {
|
|
1332
|
+
check.optionalText(props2.lowLabel, "lowLabel");
|
|
1333
|
+
check.optionalText(props2.highLabel, "highLabel");
|
|
1334
|
+
};
|
|
1118
1335
|
var builtins = [
|
|
1119
1336
|
{
|
|
1120
1337
|
type: "short-text",
|
|
@@ -1123,6 +1340,7 @@ var builtins = [
|
|
|
1123
1340
|
value: { json: true },
|
|
1124
1341
|
operators: textOps,
|
|
1125
1342
|
a11y: { semantics: "control" },
|
|
1343
|
+
literal: "string",
|
|
1126
1344
|
validate: textValidate(500)
|
|
1127
1345
|
},
|
|
1128
1346
|
{
|
|
@@ -1132,6 +1350,7 @@ var builtins = [
|
|
|
1132
1350
|
value: { json: true },
|
|
1133
1351
|
operators: textOps,
|
|
1134
1352
|
a11y: { semantics: "control" },
|
|
1353
|
+
literal: "string",
|
|
1135
1354
|
validate: textValidate(5e3)
|
|
1136
1355
|
},
|
|
1137
1356
|
{
|
|
@@ -1141,6 +1360,7 @@ var builtins = [
|
|
|
1141
1360
|
value: { json: true },
|
|
1142
1361
|
operators: textOps,
|
|
1143
1362
|
a11y: { semantics: "control", autocomplete: "email", inputMode: "email" },
|
|
1363
|
+
literal: "string",
|
|
1144
1364
|
validate: (value) => typeof value === "string" && isEmailAddress(value.trim()) ? null : "Enter an email address like name@example.com.",
|
|
1145
1365
|
normalize: (value) => typeof value === "string" ? value.trim() : value
|
|
1146
1366
|
},
|
|
@@ -1151,6 +1371,7 @@ var builtins = [
|
|
|
1151
1371
|
value: { json: true },
|
|
1152
1372
|
operators: ["eq", "neq", "answered", "empty"],
|
|
1153
1373
|
a11y: { semantics: "control", autocomplete: "tel", inputMode: "tel" },
|
|
1374
|
+
literal: "string",
|
|
1154
1375
|
validate: (value) => typeof value === "string" && isPhoneNumber(value.trim()) ? null : "Enter a phone number with 7 to 15 digits.",
|
|
1155
1376
|
normalize: (value) => typeof value === "string" ? value.trim() : value,
|
|
1156
1377
|
summarize: (value) => String(value)
|
|
@@ -1162,6 +1383,13 @@ var builtins = [
|
|
|
1162
1383
|
value: { json: true },
|
|
1163
1384
|
operators: numberOps2,
|
|
1164
1385
|
a11y: { semantics: "group" },
|
|
1386
|
+
literal: "number",
|
|
1387
|
+
checkProps: (props2, check) => {
|
|
1388
|
+
scaleProps(props2, check);
|
|
1389
|
+
if (props2.display !== void 0 && props2.display !== "numbers" && props2.display !== "stars") {
|
|
1390
|
+
check.issue("display", "Must be numbers or stars.");
|
|
1391
|
+
}
|
|
1392
|
+
},
|
|
1165
1393
|
validate: (value, field2) => {
|
|
1166
1394
|
const { min, max } = intRange(field2, 1, 5);
|
|
1167
1395
|
return Number.isInteger(value) && value >= min && value <= max ? null : `Choose a rating from ${min} to ${max}.`;
|
|
@@ -1179,6 +1407,8 @@ var builtins = [
|
|
|
1179
1407
|
value: { json: true },
|
|
1180
1408
|
operators: numberOps2,
|
|
1181
1409
|
a11y: { semantics: "group" },
|
|
1410
|
+
literal: "number",
|
|
1411
|
+
checkProps: scaleProps,
|
|
1182
1412
|
validate: (value) => Number.isInteger(value) && value >= 0 && value <= 10 ? null : "Choose a score from 0 to 10.",
|
|
1183
1413
|
summarize: (value, field2) => {
|
|
1184
1414
|
const end = scaleEndLabel(field2, value, 0, 10);
|
|
@@ -1192,8 +1422,10 @@ var builtins = [
|
|
|
1192
1422
|
value: { json: true },
|
|
1193
1423
|
operators: ["eq", "neq", "in", "answered", "empty"],
|
|
1194
1424
|
a11y: { semantics: "group" },
|
|
1195
|
-
|
|
1196
|
-
|
|
1425
|
+
literal: "string",
|
|
1426
|
+
choice: { multiple: false },
|
|
1427
|
+
validate: oneOptionValidate,
|
|
1428
|
+
summarize: oneOptionSummary
|
|
1197
1429
|
},
|
|
1198
1430
|
{
|
|
1199
1431
|
type: "multi-choice",
|
|
@@ -1202,12 +1434,10 @@ var builtins = [
|
|
|
1202
1434
|
value: { json: true },
|
|
1203
1435
|
operators: ["contains", "answered", "empty"],
|
|
1204
1436
|
a11y: { semantics: "group" },
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
},
|
|
1210
|
-
summarize: (value, field2) => Array.isArray(value) ? value.map((v) => optionLabel(field2, String(v))).join(", ") : String(value)
|
|
1437
|
+
literal: "string",
|
|
1438
|
+
choice: { multiple: true },
|
|
1439
|
+
validate: optionsValidate,
|
|
1440
|
+
summarize: optionsSummary
|
|
1211
1441
|
},
|
|
1212
1442
|
{
|
|
1213
1443
|
type: "yes-no",
|
|
@@ -1216,6 +1446,7 @@ var builtins = [
|
|
|
1216
1446
|
value: { json: true },
|
|
1217
1447
|
operators: ["eq", "neq", "answered", "empty"],
|
|
1218
1448
|
a11y: { semantics: "group" },
|
|
1449
|
+
literal: "boolean",
|
|
1219
1450
|
validate: (value) => typeof value === "boolean" ? null : "Choose yes or no.",
|
|
1220
1451
|
summarize: (value) => value ? "Yes" : "No"
|
|
1221
1452
|
},
|
|
@@ -1226,8 +1457,10 @@ var builtins = [
|
|
|
1226
1457
|
value: { json: true },
|
|
1227
1458
|
operators: ["eq", "neq", "in", "answered", "empty"],
|
|
1228
1459
|
a11y: { semantics: "control" },
|
|
1229
|
-
|
|
1230
|
-
|
|
1460
|
+
literal: "string",
|
|
1461
|
+
choice: { multiple: false },
|
|
1462
|
+
validate: oneOptionValidate,
|
|
1463
|
+
summarize: oneOptionSummary
|
|
1231
1464
|
},
|
|
1232
1465
|
{
|
|
1233
1466
|
type: "number",
|
|
@@ -1236,6 +1469,16 @@ var builtins = [
|
|
|
1236
1469
|
value: { json: true },
|
|
1237
1470
|
operators: numberOps2,
|
|
1238
1471
|
a11y: { semantics: "control", inputMode: "decimal" },
|
|
1472
|
+
literal: "number",
|
|
1473
|
+
checkProps: (props2, check) => {
|
|
1474
|
+
for (const k of ["min", "max", "step"]) {
|
|
1475
|
+
if (props2[k] !== void 0 && !isFiniteNumber(props2[k])) check.issue(k, "Must be a number.");
|
|
1476
|
+
}
|
|
1477
|
+
if (isFiniteNumber(props2.step) && props2.step <= 0) check.issue("step", "Must be above 0.");
|
|
1478
|
+
if (isFiniteNumber(props2.min) && isFiniteNumber(props2.max) && props2.min > props2.max) {
|
|
1479
|
+
check.issue("min", "Can't be more than the largest number.");
|
|
1480
|
+
}
|
|
1481
|
+
},
|
|
1239
1482
|
validate: (value, field2) => {
|
|
1240
1483
|
if (typeof value !== "number" || !Number.isFinite(value)) return "Enter a number.";
|
|
1241
1484
|
const { min, max, step } = numberLimits(field2);
|
|
@@ -1262,6 +1505,17 @@ var builtins = [
|
|
|
1262
1505
|
value: { json: true },
|
|
1263
1506
|
operators: ["eq", "neq", "answered", "empty"],
|
|
1264
1507
|
a11y: { semantics: "control" },
|
|
1508
|
+
literal: "string",
|
|
1509
|
+
checkProps: (props2, check) => {
|
|
1510
|
+
for (const k of ["min", "max"]) {
|
|
1511
|
+
if (props2[k] !== void 0 && !(typeof props2[k] === "string" && isIsoDate(props2[k]))) {
|
|
1512
|
+
check.issue(k, "Must be a date like 2026-10-22.");
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
if (typeof props2.min === "string" && typeof props2.max === "string" && isIsoDate(props2.min) && isIsoDate(props2.max) && props2.min > props2.max) {
|
|
1516
|
+
check.issue("min", "Can't be after the latest date.");
|
|
1517
|
+
}
|
|
1518
|
+
},
|
|
1265
1519
|
validate: (value, field2) => {
|
|
1266
1520
|
if (typeof value !== "string" || !isIsoDate(value.trim())) return "Enter a date like 2026-10-22.";
|
|
1267
1521
|
const date = value.trim();
|
|
@@ -1284,6 +1538,8 @@ var builtins = [
|
|
|
1284
1538
|
value: { json: true },
|
|
1285
1539
|
operators: ["answered", "empty"],
|
|
1286
1540
|
a11y: { semantics: "group" },
|
|
1541
|
+
literal: "boolean",
|
|
1542
|
+
checkProps: (props2, check) => check.optionalText(props2.text, "text"),
|
|
1287
1543
|
// Unchecked is unanswered, so a required consent must be checked.
|
|
1288
1544
|
isEmpty: (value) => value === void 0 || value === null || value === false,
|
|
1289
1545
|
validate: (value) => value === true ? null : "Check the box to agree.",
|
|
@@ -1336,7 +1592,7 @@ function dateLimits(field2) {
|
|
|
1336
1592
|
if (typeof max === "string" && isIsoDate(max)) out.max = max;
|
|
1337
1593
|
return out;
|
|
1338
1594
|
}
|
|
1339
|
-
for (const manifest of [...builtins, ...structuredFieldTypes, calculatedFieldType, photoFieldType])
|
|
1595
|
+
for (const manifest of [...builtins, ...structuredFieldTypes, calculatedFieldType, photoFieldType]) registerBuiltIn(manifest);
|
|
1340
1596
|
|
|
1341
1597
|
// src/hidden.ts
|
|
1342
1598
|
var MAX_HIDDEN_STRING_LENGTH = 1e3;
|
|
@@ -1512,7 +1768,8 @@ function fieldMap(def) {
|
|
|
1512
1768
|
|
|
1513
1769
|
// src/options.ts
|
|
1514
1770
|
var CHOICE_FIELD_TYPES = /* @__PURE__ */ new Set(["single-choice", "multi-choice", "dropdown"]);
|
|
1515
|
-
var isChoice =
|
|
1771
|
+
var isChoice = isChoiceField;
|
|
1772
|
+
var isMultiple = (field2) => choiceKind(field2.type)?.multiple === true;
|
|
1516
1773
|
function put(record, key, value) {
|
|
1517
1774
|
Object.defineProperty(record, key, { value, enumerable: true, writable: true, configurable: true });
|
|
1518
1775
|
}
|
|
@@ -1607,7 +1864,7 @@ function isPrefillParam(param) {
|
|
|
1607
1864
|
return typeof param === "string" && param.length <= MAX_PREFILL_PARAM_LENGTH && /^[A-Za-z0-9_.-]+$/.test(param);
|
|
1608
1865
|
}
|
|
1609
1866
|
function prefillSetting(field2) {
|
|
1610
|
-
if (!
|
|
1867
|
+
if (!choiceKind(field2.type)) return void 0;
|
|
1611
1868
|
const raw = field2.props?.prefill;
|
|
1612
1869
|
if (!raw || typeof raw !== "object" || Array.isArray(raw) || !isPrefillParam(raw.param)) return void 0;
|
|
1613
1870
|
return { param: raw.param, hideWhenPrefilled: raw.hideWhenPrefilled === true };
|
|
@@ -1635,7 +1892,7 @@ function prefillFromLink(definition, params, values = {}) {
|
|
|
1635
1892
|
if (!raw.length) continue;
|
|
1636
1893
|
const shown2 = new Set(visibleOptions(definition, block, seen).map((o) => o.value));
|
|
1637
1894
|
let value;
|
|
1638
|
-
if (block
|
|
1895
|
+
if (isMultiple(block)) {
|
|
1639
1896
|
const picked = [];
|
|
1640
1897
|
for (const r of raw) {
|
|
1641
1898
|
for (const v of shown2.has(r) ? [r] : r.split(",").map((s) => s.trim())) {
|
|
@@ -1684,6 +1941,21 @@ function snapshotOf(byValue, value) {
|
|
|
1684
1941
|
function answerSnapshots(definition, answers) {
|
|
1685
1942
|
const out = {};
|
|
1686
1943
|
for (const block of definition.blocks) {
|
|
1944
|
+
if (block.kind !== "field") continue;
|
|
1945
|
+
const manifest = getFieldType(block.type);
|
|
1946
|
+
const custom = manifest?.choice ? void 0 : manifest?.snapshot;
|
|
1947
|
+
if (custom) {
|
|
1948
|
+
const value2 = own(answers, block.id);
|
|
1949
|
+
if (isEmptyValue(value2, block)) continue;
|
|
1950
|
+
let snapshot2;
|
|
1951
|
+
try {
|
|
1952
|
+
snapshot2 = custom(value2, block);
|
|
1953
|
+
} catch {
|
|
1954
|
+
snapshot2 = void 0;
|
|
1955
|
+
}
|
|
1956
|
+
if (snapshot2 !== void 0) put(out, block.id, snapshot2);
|
|
1957
|
+
continue;
|
|
1958
|
+
}
|
|
1687
1959
|
if (!isChoice(block) || !optionsSource(block)) continue;
|
|
1688
1960
|
const value = own(answers, block.id);
|
|
1689
1961
|
if (isEmptyValue(value, block)) continue;
|
|
@@ -1701,6 +1973,22 @@ function answersFromSnapshots(definition, snapshots) {
|
|
|
1701
1973
|
const answers = {};
|
|
1702
1974
|
const missing = {};
|
|
1703
1975
|
for (const block of definition.blocks) {
|
|
1976
|
+
if (block.kind !== "field") continue;
|
|
1977
|
+
const manifest = getFieldType(block.type);
|
|
1978
|
+
const custom = manifest?.choice ? void 0 : manifest?.fromSnapshot;
|
|
1979
|
+
if (custom) {
|
|
1980
|
+
const raw2 = own(snapshots, block.id);
|
|
1981
|
+
if (raw2 === void 0) continue;
|
|
1982
|
+
let restored;
|
|
1983
|
+
try {
|
|
1984
|
+
restored = custom(raw2, block);
|
|
1985
|
+
} catch {
|
|
1986
|
+
restored = void 0;
|
|
1987
|
+
}
|
|
1988
|
+
if (restored?.value !== void 0) put(answers, block.id, restored.value);
|
|
1989
|
+
if (Array.isArray(restored?.missing) && restored.missing.length) put(missing, block.id, restored.missing);
|
|
1990
|
+
continue;
|
|
1991
|
+
}
|
|
1704
1992
|
if (!isChoice(block)) continue;
|
|
1705
1993
|
const raw = own(snapshots, block.id);
|
|
1706
1994
|
const list = (Array.isArray(raw) ? raw : raw === void 0 ? [] : [raw]).flatMap((s) => {
|
|
@@ -1723,7 +2011,7 @@ function answersFromSnapshots(definition, snapshots) {
|
|
|
1723
2011
|
if (value === void 0) lost.push(s);
|
|
1724
2012
|
else if (!found.includes(value)) found.push(value);
|
|
1725
2013
|
}
|
|
1726
|
-
if (found.length) put(answers, block.id, block
|
|
2014
|
+
if (found.length) put(answers, block.id, isMultiple(block) ? found : found[0]);
|
|
1727
2015
|
if (lost.length) put(missing, block.id, lost);
|
|
1728
2016
|
}
|
|
1729
2017
|
return { answers, missing };
|
|
@@ -1760,25 +2048,6 @@ function shown(v) {
|
|
|
1760
2048
|
}
|
|
1761
2049
|
var isObject2 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
1762
2050
|
var isInt = (v) => typeof v === "number" && Number.isInteger(v);
|
|
1763
|
-
var LITERAL_TYPE = {
|
|
1764
|
-
"short-text": "string",
|
|
1765
|
-
"long-text": "string",
|
|
1766
|
-
email: "string",
|
|
1767
|
-
phone: "string",
|
|
1768
|
-
rating: "number",
|
|
1769
|
-
nps: "number",
|
|
1770
|
-
"single-choice": "string",
|
|
1771
|
-
"multi-choice": "string",
|
|
1772
|
-
"yes-no": "boolean",
|
|
1773
|
-
list: "string",
|
|
1774
|
-
dropdown: "string",
|
|
1775
|
-
number: "number",
|
|
1776
|
-
date: "string",
|
|
1777
|
-
consent: "boolean",
|
|
1778
|
-
// A calculated field holds a number unless its format says text; see `literalType`.
|
|
1779
|
-
calculated: "number"
|
|
1780
|
-
};
|
|
1781
|
-
var CHOICE_TYPES = /* @__PURE__ */ new Set(["single-choice", "multi-choice", "dropdown"]);
|
|
1782
2051
|
var ORDERING = /* @__PURE__ */ new Set(["lt", "lte", "gt", "gte"]);
|
|
1783
2052
|
var OPERATORS = /* @__PURE__ */ new Set([
|
|
1784
2053
|
"eq",
|
|
@@ -1792,13 +2061,16 @@ var OPERATORS = /* @__PURE__ */ new Set([
|
|
|
1792
2061
|
"answered",
|
|
1793
2062
|
"empty"
|
|
1794
2063
|
]);
|
|
1795
|
-
var RESERVED = new Set(Object.getOwnPropertyNames(Object.prototype));
|
|
1796
2064
|
var MAX_CONDITION_DEPTH = 32;
|
|
2065
|
+
function contentKinds() {
|
|
2066
|
+
const names = listContentTypes().map((c) => c.type);
|
|
2067
|
+
return names.length > 1 ? `${names.slice(0, -1).join(", ")} or ${names[names.length - 1]}` : names[0] ?? "a registered content type";
|
|
2068
|
+
}
|
|
1797
2069
|
function parseForm(input) {
|
|
1798
2070
|
const issues = [];
|
|
1799
2071
|
const warnings = [];
|
|
1800
2072
|
const fail = () => ({ ok: false, issues, warnings });
|
|
1801
|
-
const issue = (path,
|
|
2073
|
+
const issue = (path, message2) => void issues.push({ path, message: message2 });
|
|
1802
2074
|
const text = (v, path, nonEmpty = false) => {
|
|
1803
2075
|
if (typeof v !== "string" || nonEmpty && !v) issue(path, nonEmpty ? "Required." : "Required text.");
|
|
1804
2076
|
};
|
|
@@ -1823,7 +2095,7 @@ function parseForm(input) {
|
|
|
1823
2095
|
}
|
|
1824
2096
|
const fits = (x) => typeof x === "string" && x.length <= OPTION_LIMITS.attributeValue;
|
|
1825
2097
|
for (const [name2, v] of Object.entries(attributes)) {
|
|
1826
|
-
if (!name2.trim() || name2.length > OPTION_LIMITS.attributeName ||
|
|
2098
|
+
if (!name2.trim() || name2.length > OPTION_LIMITS.attributeName || RESERVED_NAMES.has(name2)) {
|
|
1827
2099
|
issue(`${at}.attributes`, `${shown(name2)} isn't a usable attribute name (at most ${OPTION_LIMITS.attributeName} characters).`);
|
|
1828
2100
|
} else if (!fits(v) && !(Array.isArray(v) && v.length <= OPTION_LIMITS.attributeItems && v.every(fits))) {
|
|
1829
2101
|
issue(
|
|
@@ -1838,104 +2110,18 @@ function parseForm(input) {
|
|
|
1838
2110
|
issue(path, `${what} must be a non-empty string.`);
|
|
1839
2111
|
return false;
|
|
1840
2112
|
}
|
|
1841
|
-
if (
|
|
2113
|
+
if (RESERVED_NAMES.has(v)) {
|
|
1842
2114
|
issue(path, `"${v}" is reserved and cannot be used as ${what.toLowerCase()}.`);
|
|
1843
2115
|
return false;
|
|
1844
2116
|
}
|
|
1845
2117
|
return true;
|
|
1846
2118
|
};
|
|
1847
|
-
const
|
|
1848
|
-
if (
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
const structuredProps = (type, props2, path) => {
|
|
1854
|
-
if (type === "list") {
|
|
1855
|
-
optionalInt(props2.min, `${path}.min`, 0);
|
|
1856
|
-
optionalInt(props2.max, `${path}.max`, 1);
|
|
1857
|
-
optionalInt(props2.itemMaxLength, `${path}.itemMaxLength`, 1);
|
|
1858
|
-
optionalText(props2.placeholder, `${path}.placeholder`);
|
|
1859
|
-
ordered(props2.min, props2.max ?? 10, `${path}.min`, "most items");
|
|
1860
|
-
} else if (type === "table") {
|
|
1861
|
-
const columns = props2.columns;
|
|
1862
|
-
if (!Array.isArray(columns) || columns.length < 1 || columns.length > MAX_TABLE_COLUMNS) {
|
|
1863
|
-
issue(`${path}.columns`, `Must be a list of 1 to ${MAX_TABLE_COLUMNS} columns.`);
|
|
1864
|
-
} else {
|
|
1865
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1866
|
-
columns.forEach((c, j) => {
|
|
1867
|
-
const at = `${path}.columns[${j}]`;
|
|
1868
|
-
if (!isObject2(c)) return issue(at, "Each column needs a key and a label.");
|
|
1869
|
-
if (name(c.key, `${at}.key`, "A column key")) {
|
|
1870
|
-
if (seen.has(c.key)) issue(`${at}.key`, `Duplicate column key "${c.key}".`);
|
|
1871
|
-
seen.add(c.key);
|
|
1872
|
-
}
|
|
1873
|
-
text(c.label, `${at}.label`);
|
|
1874
|
-
optionalBoolean(c.long, `${at}.long`);
|
|
1875
|
-
});
|
|
1876
|
-
}
|
|
1877
|
-
optionalInt(props2.minRows, `${path}.minRows`, 0);
|
|
1878
|
-
optionalInt(props2.maxRows, `${path}.maxRows`, 1);
|
|
1879
|
-
ordered(props2.minRows, props2.maxRows ?? 3, `${path}.minRows`, "most rows");
|
|
1880
|
-
} else if (type === "media-notes") {
|
|
1881
|
-
optionalInt(props2.maxNotes, `${path}.maxNotes`, 1);
|
|
1882
|
-
optionalInt(props2.textMaxLength, `${path}.textMaxLength`, 1);
|
|
1883
|
-
} else if (type === "text-highlights") {
|
|
1884
|
-
optionalText(props2.text, `${path}.text`);
|
|
1885
|
-
optionalInt(props2.maxHighlights, `${path}.maxHighlights`, 1);
|
|
1886
|
-
} else if (type === CALCULATED_TYPE) {
|
|
1887
|
-
if (props2.format !== void 0 && props2.format !== "number" && props2.format !== "percent" && props2.format !== "text") {
|
|
1888
|
-
issue(`${path}.format`, "Must be number, percent or text.");
|
|
1889
|
-
}
|
|
1890
|
-
if (props2.decimals !== void 0 && !(isInt(props2.decimals) && props2.decimals >= 0 && props2.decimals <= MAX_CALCULATED_DECIMALS)) {
|
|
1891
|
-
issue(`${path}.decimals`, `Must be a whole number from 0 to ${MAX_CALCULATED_DECIMALS}.`);
|
|
1892
|
-
}
|
|
1893
|
-
}
|
|
1894
|
-
};
|
|
1895
|
-
const isFiniteNumber = (v) => typeof v === "number" && Number.isFinite(v);
|
|
1896
|
-
const fieldProps = (type, props2, path) => {
|
|
1897
|
-
if (type === "rating" || type === "nps") {
|
|
1898
|
-
optionalText(props2.lowLabel, `${path}.lowLabel`);
|
|
1899
|
-
optionalText(props2.highLabel, `${path}.highLabel`);
|
|
1900
|
-
}
|
|
1901
|
-
if (type === "rating" && props2.display !== void 0 && props2.display !== "numbers" && props2.display !== "stars") {
|
|
1902
|
-
issue(`${path}.display`, "Must be numbers or stars.");
|
|
1903
|
-
}
|
|
1904
|
-
if (type === "number") {
|
|
1905
|
-
for (const k of ["min", "max", "step"]) {
|
|
1906
|
-
if (props2[k] !== void 0 && !isFiniteNumber(props2[k])) issue(`${path}.${k}`, "Must be a number.");
|
|
1907
|
-
}
|
|
1908
|
-
if (isFiniteNumber(props2.step) && props2.step <= 0) issue(`${path}.step`, "Must be above 0.");
|
|
1909
|
-
if (isFiniteNumber(props2.min) && isFiniteNumber(props2.max) && props2.min > props2.max) {
|
|
1910
|
-
issue(`${path}.min`, "Can't be more than the largest number.");
|
|
1911
|
-
}
|
|
1912
|
-
}
|
|
1913
|
-
if (type === "date") {
|
|
1914
|
-
for (const k of ["min", "max"]) {
|
|
1915
|
-
if (props2[k] !== void 0 && !(typeof props2[k] === "string" && isIsoDate(props2[k]))) {
|
|
1916
|
-
issue(`${path}.${k}`, "Must be a date like 2026-10-22.");
|
|
1917
|
-
}
|
|
1918
|
-
}
|
|
1919
|
-
if (typeof props2.min === "string" && typeof props2.max === "string" && isIsoDate(props2.min) && isIsoDate(props2.max) && props2.min > props2.max) {
|
|
1920
|
-
issue(`${path}.min`, "Can't be after the latest date.");
|
|
1921
|
-
}
|
|
1922
|
-
}
|
|
1923
|
-
if (type === "consent") optionalText(props2.text, `${path}.text`);
|
|
1924
|
-
if (type === PHOTO_TYPE) {
|
|
1925
|
-
if (props2.maxFiles !== void 0 && !(isInt(props2.maxFiles) && props2.maxFiles >= 1 && props2.maxFiles <= MAX_PHOTO_FILES)) {
|
|
1926
|
-
issue(`${path}.maxFiles`, `Must be a whole number from 1 to ${MAX_PHOTO_FILES}.`);
|
|
1927
|
-
}
|
|
1928
|
-
if (props2.maxSizeMb !== void 0 && !(isInt(props2.maxSizeMb) && props2.maxSizeMb >= 1 && props2.maxSizeMb <= MAX_PHOTO_SIZE_MB)) {
|
|
1929
|
-
issue(`${path}.maxSizeMb`, `Must be a whole number from 1 to ${MAX_PHOTO_SIZE_MB}.`);
|
|
1930
|
-
}
|
|
1931
|
-
if (props2.formats !== void 0) {
|
|
1932
|
-
const formats = props2.formats;
|
|
1933
|
-
if (!Array.isArray(formats) || formats.length === 0 || !formats.every(isPhotoFormat)) {
|
|
1934
|
-
issue(`${path}.formats`, "Must be a list of one or more of jpeg, png, heic and webp.");
|
|
1935
|
-
} else if (new Set(formats).size !== formats.length) {
|
|
1936
|
-
issue(`${path}.formats`, "Name each format once.");
|
|
1937
|
-
}
|
|
1938
|
-
}
|
|
2119
|
+
const kindProps = (check, props2, path) => {
|
|
2120
|
+
if (!check) return;
|
|
2121
|
+
try {
|
|
2122
|
+
check(props2, propsChecker(path, issue));
|
|
2123
|
+
} catch {
|
|
2124
|
+
issue(path, "These settings couldn't be checked.");
|
|
1939
2125
|
}
|
|
1940
2126
|
};
|
|
1941
2127
|
if (!isObject2(input)) {
|
|
@@ -1974,7 +2160,7 @@ function parseForm(input) {
|
|
|
1974
2160
|
const order = /* @__PURE__ */ new Map();
|
|
1975
2161
|
const tableColumnKeys = /* @__PURE__ */ new Map();
|
|
1976
2162
|
const choices = /* @__PURE__ */ new Map();
|
|
1977
|
-
const
|
|
2163
|
+
const literals = /* @__PURE__ */ new Map();
|
|
1978
2164
|
input.blocks.forEach((block, i) => {
|
|
1979
2165
|
const path = `blocks[${i}]`;
|
|
1980
2166
|
if (!isObject2(block) || typeof block.id !== "string" || !block.id) {
|
|
@@ -2002,7 +2188,7 @@ function parseForm(input) {
|
|
|
2002
2188
|
if (from !== void 0) {
|
|
2003
2189
|
if (!isObject2(from) || from.source !== "host" || typeof from.key !== "string" || !from.key) {
|
|
2004
2190
|
issue(`${path}.props.optionsFrom`, 'Must be { source: "host", key } with a non-empty key.');
|
|
2005
|
-
} else if (typeof block.type === "string" && !
|
|
2191
|
+
} else if (typeof block.type === "string" && !choiceKind(block.type)) {
|
|
2006
2192
|
warnings.push({ path: `${path}.props.optionsFrom`, message: "Only single choice, multiple choice and dropdown questions take options from the app." });
|
|
2007
2193
|
}
|
|
2008
2194
|
const filter = isObject2(from) ? from.filter : void 0;
|
|
@@ -2024,7 +2210,7 @@ function parseForm(input) {
|
|
|
2024
2210
|
);
|
|
2025
2211
|
} else {
|
|
2026
2212
|
optionalBoolean(prefill.hideWhenPrefilled, `${path}.props.prefill.hideWhenPrefilled`);
|
|
2027
|
-
if (typeof block.type === "string" && !
|
|
2213
|
+
if (typeof block.type === "string" && !choiceKind(block.type)) {
|
|
2028
2214
|
warnings.push({ path: `${path}.props.prefill`, message: "Only single choice, multiple choice and dropdown questions are filled in from a link." });
|
|
2029
2215
|
}
|
|
2030
2216
|
}
|
|
@@ -2055,7 +2241,7 @@ function parseForm(input) {
|
|
|
2055
2241
|
if (other !== void 0) {
|
|
2056
2242
|
if (other !== true && other !== false && !(isObject2(other) && (other.label === void 0 || typeof other.label === "string"))) {
|
|
2057
2243
|
issue(`${path}.props.other`, "Must be true, false or { label }.");
|
|
2058
|
-
} else if (other !== false && typeof block.type === "string" && !
|
|
2244
|
+
} else if (other !== false && typeof block.type === "string" && !choiceKind(block.type)) {
|
|
2059
2245
|
warnings.push({ path: `${path}.props.other`, message: 'Only single choice, multiple choice and dropdown questions take an "Other" answer.' });
|
|
2060
2246
|
}
|
|
2061
2247
|
}
|
|
@@ -2068,19 +2254,14 @@ function parseForm(input) {
|
|
|
2068
2254
|
}
|
|
2069
2255
|
}
|
|
2070
2256
|
if (typeof block.type === "string" && (block.props === void 0 || isObject2(block.props))) {
|
|
2071
|
-
|
|
2072
|
-
fieldProps(block.type, block.props ?? {}, `${path}.props`);
|
|
2257
|
+
kindProps(getFieldType(block.type)?.checkProps, block.props ?? {}, `${path}.props`);
|
|
2073
2258
|
}
|
|
2074
2259
|
if (validKey) {
|
|
2075
2260
|
if (keys.has(block.key)) issue(`${path}.key`, `Duplicate key "${String(block.key)}".`);
|
|
2076
2261
|
keys.set(block.key, block.id);
|
|
2077
2262
|
}
|
|
2078
2263
|
fieldTypes.set(block.id, typeof block.type === "string" ? block.type : "");
|
|
2079
|
-
|
|
2080
|
-
const format = isObject2(block.props) ? block.props.format : void 0;
|
|
2081
|
-
if (format === "text") calculatedLiteral.set(block.id, "string");
|
|
2082
|
-
else if (format !== "number" && format !== "percent") calculatedLiteral.set(block.id, void 0);
|
|
2083
|
-
}
|
|
2264
|
+
literals.set(block.id, typeof block.type === "string" ? literalType(block.type, isObject2(block.props) ? block.props : {}) : void 0);
|
|
2084
2265
|
if (block.type === "table" && isObject2(block.props) && Array.isArray(block.props.columns)) {
|
|
2085
2266
|
tableColumnKeys.set(
|
|
2086
2267
|
block.id,
|
|
@@ -2088,8 +2269,13 @@ function parseForm(input) {
|
|
|
2088
2269
|
);
|
|
2089
2270
|
}
|
|
2090
2271
|
} else if (block.kind === "content") {
|
|
2091
|
-
|
|
2272
|
+
const content = typeof block.type === "string" ? getContentType(block.type) : void 0;
|
|
2273
|
+
if (!content) issue(`${path}.type`, `Must be ${contentKinds()}.`);
|
|
2092
2274
|
text(block.text, `${path}.text`);
|
|
2275
|
+
if (content?.checkProps) {
|
|
2276
|
+
if (block.props === void 0 || isObject2(block.props) && isJson(block.props)) kindProps(content.checkProps, block.props ?? {}, `${path}.props`);
|
|
2277
|
+
else issue(`${path}.props`, "Must be a JSON object.");
|
|
2278
|
+
}
|
|
2093
2279
|
} else if (block.kind === "page-break") {
|
|
2094
2280
|
optionalText(block.title, `${path}.title`);
|
|
2095
2281
|
} else {
|
|
@@ -2219,8 +2405,8 @@ function parseForm(input) {
|
|
|
2219
2405
|
if (typeof left.field === "string" && fieldTypes.has(left.field)) {
|
|
2220
2406
|
const fieldType = fieldTypes.get(left.field);
|
|
2221
2407
|
what = `"${keyOf(left.field)}"`;
|
|
2222
|
-
type =
|
|
2223
|
-
if (
|
|
2408
|
+
type = literals.get(left.field);
|
|
2409
|
+
if (choiceKind(fieldType)) options = choices.get(left.field);
|
|
2224
2410
|
} else if (typeof left.hidden === "string" && hiddenTypes.has(left.hidden)) {
|
|
2225
2411
|
what = `hidden input "${left.hidden}"`;
|
|
2226
2412
|
type = hiddenTypes.get(left.hidden);
|
|
@@ -2241,8 +2427,8 @@ function parseForm(input) {
|
|
|
2241
2427
|
type = "number";
|
|
2242
2428
|
}
|
|
2243
2429
|
for (const v of values) {
|
|
2244
|
-
const
|
|
2245
|
-
if (
|
|
2430
|
+
const message2 = 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;
|
|
2431
|
+
if (message2) warnings.push({ path: `${path}.right`, message: message2 });
|
|
2246
2432
|
}
|
|
2247
2433
|
};
|
|
2248
2434
|
const keyOf = (id) => [...keys].find(([, v]) => v === id)?.[0] ?? id;
|
|
@@ -2557,17 +2743,17 @@ function defineForm(input) {
|
|
|
2557
2743
|
if (input.endings?.length) definition.endings = input.endings;
|
|
2558
2744
|
if (input.settings) definition.settings = input.settings;
|
|
2559
2745
|
if (input.meta) definition.meta = input.meta;
|
|
2560
|
-
const
|
|
2561
|
-
const parsed = parseForm(
|
|
2746
|
+
const frozen2 = JSON.parse(JSON.stringify(definition));
|
|
2747
|
+
const parsed = parseForm(frozen2);
|
|
2562
2748
|
if (!parsed.ok) {
|
|
2563
2749
|
const list = parsed.issues.map((i) => ` - ${i.path ? `${i.path}: ` : ""}${i.message}`).join("\n");
|
|
2564
2750
|
throw new Error(`defineForm "${input.id}" is not a valid form:
|
|
2565
2751
|
${list}`);
|
|
2566
2752
|
}
|
|
2567
2753
|
return {
|
|
2568
|
-
definition:
|
|
2569
|
-
toJSON: () => JSON.parse(JSON.stringify(
|
|
2570
|
-
hash: () => fingerprint(
|
|
2754
|
+
definition: frozen2,
|
|
2755
|
+
toJSON: () => JSON.parse(JSON.stringify(frozen2)),
|
|
2756
|
+
hash: () => fingerprint(frozen2),
|
|
2571
2757
|
$answers: void 0,
|
|
2572
2758
|
$hidden: void 0
|
|
2573
2759
|
};
|
|
@@ -2590,8 +2776,13 @@ function validateField(field2, value, options = {}) {
|
|
|
2590
2776
|
}
|
|
2591
2777
|
const manifest = getFieldType(field2.type);
|
|
2592
2778
|
if (!manifest) return `Unknown field type "${field2.type}".`;
|
|
2593
|
-
|
|
2594
|
-
|
|
2779
|
+
let builtIn2;
|
|
2780
|
+
try {
|
|
2781
|
+
builtIn2 = manifest.validate?.(value, field2) ?? null;
|
|
2782
|
+
} catch {
|
|
2783
|
+
builtIn2 = VALIDATOR_FAILED_MESSAGE;
|
|
2784
|
+
}
|
|
2785
|
+
if (builtIn2) return builtIn2;
|
|
2595
2786
|
return options.custom ? runCustom(options.custom, value) : null;
|
|
2596
2787
|
}
|
|
2597
2788
|
|
|
@@ -2685,9 +2876,9 @@ function build(given, options, replaying = false, budget) {
|
|
|
2685
2876
|
const field2 = fields.get(id);
|
|
2686
2877
|
if (!field2 || !isVisibleIn(s, id)) continue;
|
|
2687
2878
|
const value = own(s.values, id);
|
|
2688
|
-
let
|
|
2689
|
-
if (!
|
|
2690
|
-
if (
|
|
2879
|
+
let message2 = validateField(field2, value, { custom: options.validators?.[id] });
|
|
2880
|
+
if (!message2 && filters) message2 = filteredChoiceError(definition, field2, value, shown2 ??= answersOnPath(s));
|
|
2881
|
+
if (message2) errors[id] = message2;
|
|
2691
2882
|
}
|
|
2692
2883
|
return errors;
|
|
2693
2884
|
};
|
|
@@ -2911,6 +3102,40 @@ function formProgress(engine) {
|
|
|
2911
3102
|
// src/upload-protocol.ts
|
|
2912
3103
|
var MAX_UPLOAD_REQUEST_BYTES = 4e3;
|
|
2913
3104
|
|
|
3105
|
+
// src/merge.ts
|
|
3106
|
+
function put2(record, key, value) {
|
|
3107
|
+
Object.defineProperty(record, key, { value, enumerable: true, writable: true, configurable: true });
|
|
3108
|
+
}
|
|
3109
|
+
function mergeResponses(definition, base, patch, options = {}) {
|
|
3110
|
+
const def = withHostOptions(definition, options.hostOptions);
|
|
3111
|
+
const fields = fieldMap(def);
|
|
3112
|
+
const keepable = (field2) => !!field2 && !isCalculated(field2) && !withheld(def, field2);
|
|
3113
|
+
let values = /* @__PURE__ */ Object.create(null);
|
|
3114
|
+
const record = (r) => r && typeof r === "object" && !Array.isArray(r) ? r : {};
|
|
3115
|
+
const from = record(base);
|
|
3116
|
+
const changes = record(patch);
|
|
3117
|
+
for (const [id, field2] of fields) {
|
|
3118
|
+
if (!keepable(field2)) continue;
|
|
3119
|
+
const changed = Object.prototype.hasOwnProperty.call(changes, id);
|
|
3120
|
+
const value = changed ? own(changes, id) : own(from, id);
|
|
3121
|
+
if (value === void 0 || value === null || isEmptyValue(value, field2)) continue;
|
|
3122
|
+
values[id] = value;
|
|
3123
|
+
}
|
|
3124
|
+
for (const id of Object.keys(values)) values = clearFilteredAnswers(def, values, id);
|
|
3125
|
+
const initial = {};
|
|
3126
|
+
for (const id of Object.keys(values)) if (values[id] !== void 0) put2(initial, id, values[id]);
|
|
3127
|
+
const state = createForm(def, { initialValues: initial, hidden: options.hidden, clock: () => void 0 }).getState();
|
|
3128
|
+
const keepOffPath = (options.offPath ?? (def.settings?.clearHiddenValues === false ? "keep" : "drop")) === "keep";
|
|
3129
|
+
const onPath = new Set(state.path);
|
|
3130
|
+
const out = {};
|
|
3131
|
+
for (const id of fields.keys()) {
|
|
3132
|
+
if (!keepOffPath && !onPath.has(id)) continue;
|
|
3133
|
+
const value = own(initial, id);
|
|
3134
|
+
if (value !== void 0) put2(out, id, value);
|
|
3135
|
+
}
|
|
3136
|
+
return out;
|
|
3137
|
+
}
|
|
3138
|
+
|
|
2914
3139
|
// src/explain.ts
|
|
2915
3140
|
function explain(source, blockId, options = {}) {
|
|
2916
3141
|
const input = "getState" in source ? { definition: source.definition, values: source.getState().values, hidden: source.getState().hidden } : source;
|
|
@@ -2943,8 +3168,8 @@ function explain(source, blockId, options = {}) {
|
|
|
2943
3168
|
const value = own(input.values, id);
|
|
2944
3169
|
if (!visible) validation = { status: "not-checked" };
|
|
2945
3170
|
else {
|
|
2946
|
-
const
|
|
2947
|
-
if (
|
|
3171
|
+
const message2 = validateField(block, value, { custom: options.validators?.[id] });
|
|
3172
|
+
if (message2) validation = { status: "invalid", message: message2 };
|
|
2948
3173
|
else validation = { status: isEmptyValue(value, block) ? "unanswered" : "valid" };
|
|
2949
3174
|
}
|
|
2950
3175
|
}
|
|
@@ -3063,13 +3288,598 @@ function carryAnswers(from, to, answers, extra = {}) {
|
|
|
3063
3288
|
return out;
|
|
3064
3289
|
}
|
|
3065
3290
|
|
|
3291
|
+
// src/submit-protocol.ts
|
|
3292
|
+
var IDEMPOTENCY_KEY_PATTERN = /^[A-Za-z0-9_-]{16,128}$/;
|
|
3293
|
+
function isIdempotencyKey(value) {
|
|
3294
|
+
return typeof value === "string" && IDEMPOTENCY_KEY_PATTERN.test(value);
|
|
3295
|
+
}
|
|
3296
|
+
function submitOutcome(result) {
|
|
3297
|
+
if (!isSubmitResult(result)) return "retry";
|
|
3298
|
+
if (result.ok || result.reason === "duplicate") return "delivered";
|
|
3299
|
+
if (result.reason === "invalid" || result.reason === "conflict" || result.reason === "closed") return "rejected";
|
|
3300
|
+
return "retry";
|
|
3301
|
+
}
|
|
3302
|
+
function isSubmitResult(value) {
|
|
3303
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
3304
|
+
const v = value;
|
|
3305
|
+
const stored = typeof v.id === "string" && v.id !== "" && typeof v.receivedAt === "string";
|
|
3306
|
+
if (v.ok === true) return stored;
|
|
3307
|
+
if (v.ok !== false) return false;
|
|
3308
|
+
switch (v.reason) {
|
|
3309
|
+
case "duplicate":
|
|
3310
|
+
return stored;
|
|
3311
|
+
case "invalid":
|
|
3312
|
+
return !!v.errors && typeof v.errors === "object" && !Array.isArray(v.errors);
|
|
3313
|
+
case "rate_limited":
|
|
3314
|
+
return v.retryAfterSeconds === void 0 || typeof v.retryAfterSeconds === "number" && Number.isFinite(v.retryAfterSeconds);
|
|
3315
|
+
case "conflict":
|
|
3316
|
+
case "closed":
|
|
3317
|
+
case "store_failed":
|
|
3318
|
+
return true;
|
|
3319
|
+
default:
|
|
3320
|
+
return false;
|
|
3321
|
+
}
|
|
3322
|
+
}
|
|
3323
|
+
|
|
3324
|
+
// src/storage.ts
|
|
3325
|
+
function memoryStorage() {
|
|
3326
|
+
const map = /* @__PURE__ */ new Map();
|
|
3327
|
+
return {
|
|
3328
|
+
getItem: (key) => map.get(key) ?? null,
|
|
3329
|
+
setItem: (key, value) => void map.set(key, value),
|
|
3330
|
+
removeItem: (key) => void map.delete(key),
|
|
3331
|
+
keys: () => [...map.keys()]
|
|
3332
|
+
};
|
|
3333
|
+
}
|
|
3334
|
+
function webStorage(storage) {
|
|
3335
|
+
const get = () => {
|
|
3336
|
+
const s = storage ? storage() : globalThis.localStorage;
|
|
3337
|
+
if (!s) throw new Error("Web storage isn't available here.");
|
|
3338
|
+
return s;
|
|
3339
|
+
};
|
|
3340
|
+
return {
|
|
3341
|
+
getItem: (key) => get().getItem(key),
|
|
3342
|
+
setItem: (key, value) => get().setItem(key, value),
|
|
3343
|
+
removeItem: (key) => get().removeItem(key)
|
|
3344
|
+
};
|
|
3345
|
+
}
|
|
3346
|
+
function isThenable(value) {
|
|
3347
|
+
return typeof value?.then === "function";
|
|
3348
|
+
}
|
|
3349
|
+
function andThen(value, next) {
|
|
3350
|
+
return isThenable(value) ? Promise.resolve(value).then(next) : next(value);
|
|
3351
|
+
}
|
|
3352
|
+
|
|
3353
|
+
// src/drafts.ts
|
|
3354
|
+
var DRAFT_KEY_PREFIX = "genesis-forms:draft:";
|
|
3355
|
+
function isRecord(value) {
|
|
3356
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
3357
|
+
}
|
|
3358
|
+
function readDraft(raw, key) {
|
|
3359
|
+
if (typeof raw !== "string") return null;
|
|
3360
|
+
let value;
|
|
3361
|
+
try {
|
|
3362
|
+
value = JSON.parse(raw);
|
|
3363
|
+
} catch {
|
|
3364
|
+
return null;
|
|
3365
|
+
}
|
|
3366
|
+
if (!isRecord(value)) return null;
|
|
3367
|
+
const { formId, version, respondent, values, types, resumeAt, savedAt } = value;
|
|
3368
|
+
if (formId !== key.formId || respondent !== key.respondent || typeof version !== "string") return null;
|
|
3369
|
+
if (!isRecord(values) || !isRecord(types) || typeof savedAt !== "number") return null;
|
|
3370
|
+
if (resumeAt !== null && typeof resumeAt !== "string") return null;
|
|
3371
|
+
return value;
|
|
3372
|
+
}
|
|
3373
|
+
function createDraftStore(storage, options = {}) {
|
|
3374
|
+
const prefix = options.prefix ?? DRAFT_KEY_PREFIX;
|
|
3375
|
+
const storageKey = (key) => `${prefix}${encodeURIComponent(key.formId)}:${encodeURIComponent(key.respondent)}`;
|
|
3376
|
+
return {
|
|
3377
|
+
get: (key) => andThen(storage.getItem(storageKey(key)), (raw) => readDraft(raw, key)),
|
|
3378
|
+
set: (draft) => storage.setItem(storageKey(draft), JSON.stringify(draft)),
|
|
3379
|
+
remove: (key) => storage.removeItem(storageKey(key))
|
|
3380
|
+
};
|
|
3381
|
+
}
|
|
3382
|
+
function draftOf(engine, key, options = {}) {
|
|
3383
|
+
const definition = engine.definition;
|
|
3384
|
+
const state = engine.getState();
|
|
3385
|
+
const values = {};
|
|
3386
|
+
const types = {};
|
|
3387
|
+
for (const [id, field2] of fieldMap(definition)) {
|
|
3388
|
+
if (isCalculated(field2) || withheld(definition, field2)) continue;
|
|
3389
|
+
if (!options.keepSensitive && isSensitive(field2)) continue;
|
|
3390
|
+
const value = own(state.values, id);
|
|
3391
|
+
if (value === void 0 || isEmptyValue(value, field2)) continue;
|
|
3392
|
+
values[id] = value;
|
|
3393
|
+
types[id] = field2.type;
|
|
3394
|
+
}
|
|
3395
|
+
if (Object.keys(values).length === 0) return null;
|
|
3396
|
+
return {
|
|
3397
|
+
formId: key.formId,
|
|
3398
|
+
version: key.version,
|
|
3399
|
+
respondent: key.respondent,
|
|
3400
|
+
values,
|
|
3401
|
+
types,
|
|
3402
|
+
resumeAt: formProgress(engine).fieldId,
|
|
3403
|
+
savedAt: (options.now ?? Date.now)()
|
|
3404
|
+
};
|
|
3405
|
+
}
|
|
3406
|
+
var DRAFT_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
3407
|
+
function restoreDraft(definition, draft, options) {
|
|
3408
|
+
const now = (options.now ?? Date.now)();
|
|
3409
|
+
if (now - draft.savedAt > (options.maxAgeMs ?? DRAFT_MAX_AGE_MS)) return { ok: false, reason: "expired" };
|
|
3410
|
+
const values = { ...draft.values };
|
|
3411
|
+
if (draft.version !== options.version) {
|
|
3412
|
+
const fields = fieldMap(definition);
|
|
3413
|
+
for (const [id, value] of Object.entries(values)) {
|
|
3414
|
+
const field2 = fields.get(id);
|
|
3415
|
+
if (!field2 || field2.type !== own(draft.types, id) || isCalculated(field2) || withheld(definition, field2)) {
|
|
3416
|
+
return { ok: false, reason: "version" };
|
|
3417
|
+
}
|
|
3418
|
+
if (validateField(field2, value, { requireAnswers: false }) !== null) return { ok: false, reason: "version" };
|
|
3419
|
+
}
|
|
3420
|
+
}
|
|
3421
|
+
const resumeAt = draft.resumeAt && fieldMap(definition).has(draft.resumeAt) ? draft.resumeAt : null;
|
|
3422
|
+
return { ok: true, values, resumeAt };
|
|
3423
|
+
}
|
|
3424
|
+
function createDraftAutosave(engine, store, key, options = {}) {
|
|
3425
|
+
const delay = options.delayMs ?? 800;
|
|
3426
|
+
const onError = options.onError ?? ((e) => console.warn("[forms] The draft could not be saved.", e));
|
|
3427
|
+
let timer = null;
|
|
3428
|
+
let dirty = false;
|
|
3429
|
+
let cleared = false;
|
|
3430
|
+
let listening = true;
|
|
3431
|
+
let stored = options.stored === true;
|
|
3432
|
+
let chain = Promise.resolve();
|
|
3433
|
+
const run = (step) => {
|
|
3434
|
+
chain = chain.then(async () => {
|
|
3435
|
+
try {
|
|
3436
|
+
const done = step();
|
|
3437
|
+
if (isThenable(done)) await done;
|
|
3438
|
+
} catch (e) {
|
|
3439
|
+
onError(e);
|
|
3440
|
+
}
|
|
3441
|
+
});
|
|
3442
|
+
return chain;
|
|
3443
|
+
};
|
|
3444
|
+
const write = () => {
|
|
3445
|
+
if (timer) clearTimeout(timer);
|
|
3446
|
+
timer = null;
|
|
3447
|
+
if (!dirty || cleared) return chain;
|
|
3448
|
+
dirty = false;
|
|
3449
|
+
const draft = draftOf(engine, key, options);
|
|
3450
|
+
return run(() => {
|
|
3451
|
+
if (cleared) return;
|
|
3452
|
+
if (!draft) {
|
|
3453
|
+
if (!stored) return;
|
|
3454
|
+
return andThen(store.remove(key), () => {
|
|
3455
|
+
stored = false;
|
|
3456
|
+
});
|
|
3457
|
+
}
|
|
3458
|
+
return andThen(store.set(draft), () => {
|
|
3459
|
+
stored = true;
|
|
3460
|
+
options.onSaved?.(draft);
|
|
3461
|
+
});
|
|
3462
|
+
});
|
|
3463
|
+
};
|
|
3464
|
+
const unsubscribe = engine.subscribe(() => {
|
|
3465
|
+
if (!listening) return;
|
|
3466
|
+
dirty = true;
|
|
3467
|
+
if (timer) clearTimeout(timer);
|
|
3468
|
+
timer = setTimeout(() => void write(), delay);
|
|
3469
|
+
});
|
|
3470
|
+
return {
|
|
3471
|
+
flush: () => write(),
|
|
3472
|
+
clear: () => {
|
|
3473
|
+
cleared = true;
|
|
3474
|
+
listening = false;
|
|
3475
|
+
dirty = false;
|
|
3476
|
+
if (timer) clearTimeout(timer);
|
|
3477
|
+
timer = null;
|
|
3478
|
+
unsubscribe();
|
|
3479
|
+
return run(() => store.remove(key));
|
|
3480
|
+
},
|
|
3481
|
+
dispose: () => {
|
|
3482
|
+
const done = write();
|
|
3483
|
+
listening = false;
|
|
3484
|
+
unsubscribe();
|
|
3485
|
+
return done;
|
|
3486
|
+
}
|
|
3487
|
+
};
|
|
3488
|
+
}
|
|
3489
|
+
|
|
3490
|
+
// src/outbox.ts
|
|
3491
|
+
var DEFAULT_PREFIX = "genesis-forms:outbox";
|
|
3492
|
+
function defaultKey() {
|
|
3493
|
+
const c = globalThis.crypto;
|
|
3494
|
+
if (c && typeof c.randomUUID === "function") return () => c.randomUUID();
|
|
3495
|
+
throw new Error("createSubmitOutbox: crypto.randomUUID isn't available here; pass createKey (expo-crypto's randomUUID, say).");
|
|
3496
|
+
}
|
|
3497
|
+
var MAX_TIMER_MS = 2 ** 31 - 1;
|
|
3498
|
+
var MAX_WAIT_MS = 24 * 60 * 60 * 1e3;
|
|
3499
|
+
var indexChains = /* @__PURE__ */ new WeakMap();
|
|
3500
|
+
function withIndexLock(storage, prefix, step) {
|
|
3501
|
+
let chains = indexChains.get(storage);
|
|
3502
|
+
if (!chains) indexChains.set(storage, chains = /* @__PURE__ */ new Map());
|
|
3503
|
+
const locks = globalThis.navigator?.locks;
|
|
3504
|
+
const locked = () => locks && typeof locks.request === "function" ? locks.request(`genesis-forms-outbox:${prefix}`, step) : step();
|
|
3505
|
+
const previous = chains.get(prefix) ?? Promise.resolve();
|
|
3506
|
+
const run = previous.then(locked, locked);
|
|
3507
|
+
chains.set(prefix, run.catch(() => void 0));
|
|
3508
|
+
return run;
|
|
3509
|
+
}
|
|
3510
|
+
var message = (e) => e instanceof Error ? e.message : String(e);
|
|
3511
|
+
function readEntry(raw, id) {
|
|
3512
|
+
if (typeof raw !== "string") return null;
|
|
3513
|
+
let v;
|
|
3514
|
+
try {
|
|
3515
|
+
v = JSON.parse(raw);
|
|
3516
|
+
} catch {
|
|
3517
|
+
return null;
|
|
3518
|
+
}
|
|
3519
|
+
if (!v || typeof v !== "object") return null;
|
|
3520
|
+
const e = v;
|
|
3521
|
+
if (e.id !== id || typeof e.formId !== "string" || !e.request || typeof e.request !== "object") return null;
|
|
3522
|
+
if (e.request.idempotencyKey !== id) return null;
|
|
3523
|
+
return {
|
|
3524
|
+
id,
|
|
3525
|
+
formId: e.formId,
|
|
3526
|
+
request: e.request,
|
|
3527
|
+
meta: e.meta ?? null,
|
|
3528
|
+
createdAt: typeof e.createdAt === "number" ? e.createdAt : 0,
|
|
3529
|
+
attempts: typeof e.attempts === "number" ? e.attempts : 0,
|
|
3530
|
+
nextAttemptAt: typeof e.nextAttemptAt === "number" ? e.nextAttemptAt : 0,
|
|
3531
|
+
state: e.state === "failed" ? "failed" : "pending",
|
|
3532
|
+
lastError: typeof e.lastError === "string" ? e.lastError : null,
|
|
3533
|
+
rejection: e.rejection ?? null
|
|
3534
|
+
};
|
|
3535
|
+
}
|
|
3536
|
+
function readIndex(raw) {
|
|
3537
|
+
if (typeof raw !== "string") return [];
|
|
3538
|
+
try {
|
|
3539
|
+
const v = JSON.parse(raw);
|
|
3540
|
+
return Array.isArray(v) ? v.filter((x) => typeof x === "string") : [];
|
|
3541
|
+
} catch {
|
|
3542
|
+
return [];
|
|
3543
|
+
}
|
|
3544
|
+
}
|
|
3545
|
+
function createSubmitOutbox(options) {
|
|
3546
|
+
const { storage, send } = options;
|
|
3547
|
+
const now = options.now ?? Date.now;
|
|
3548
|
+
const prefix = options.prefix ?? DEFAULT_PREFIX;
|
|
3549
|
+
const maxAttempts = Math.max(1, options.maxAttempts ?? 10);
|
|
3550
|
+
const baseMs = options.backoff?.baseMs ?? 2e3;
|
|
3551
|
+
const maxMs = options.backoff?.maxMs ?? 15 * 6e4;
|
|
3552
|
+
const timeoutMs = options.timeoutMs ?? 3e4;
|
|
3553
|
+
const createKey = options.createKey ?? defaultKey();
|
|
3554
|
+
const random = options.random ?? Math.random;
|
|
3555
|
+
const log = options.log ?? ((m) => console.warn(`[forms outbox] ${m}`));
|
|
3556
|
+
const indexKey = `${prefix}:index`;
|
|
3557
|
+
const itemKey = (id) => `${prefix}:item:${id}`;
|
|
3558
|
+
const entries = /* @__PURE__ */ new Map();
|
|
3559
|
+
const unreadable = /* @__PURE__ */ new Set();
|
|
3560
|
+
let sendingId = null;
|
|
3561
|
+
let online = options.online ?? true;
|
|
3562
|
+
let loaded = false;
|
|
3563
|
+
let storageError = null;
|
|
3564
|
+
let running = false;
|
|
3565
|
+
let timer = null;
|
|
3566
|
+
let flushing = null;
|
|
3567
|
+
let again = false;
|
|
3568
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
3569
|
+
let snapshot = build2();
|
|
3570
|
+
function build2() {
|
|
3571
|
+
const items = [...entries.values()].sort((a, b) => a.createdAt - b.createdAt || (a.id < b.id ? -1 : 1)).map((e) => ({ ...e, sending: e.id === sendingId }));
|
|
3572
|
+
return {
|
|
3573
|
+
loaded,
|
|
3574
|
+
online,
|
|
3575
|
+
items,
|
|
3576
|
+
pending: items.filter((i) => i.state === "pending").length,
|
|
3577
|
+
failed: items.filter((i) => i.state === "failed").length,
|
|
3578
|
+
storageError,
|
|
3579
|
+
unreadable: [...unreadable]
|
|
3580
|
+
};
|
|
3581
|
+
}
|
|
3582
|
+
function changed() {
|
|
3583
|
+
snapshot = build2();
|
|
3584
|
+
for (const l of [...listeners]) l();
|
|
3585
|
+
}
|
|
3586
|
+
let writes = Promise.resolve();
|
|
3587
|
+
function serial(step) {
|
|
3588
|
+
const run = writes.then(step, step);
|
|
3589
|
+
writes = run.catch(() => void 0);
|
|
3590
|
+
return run;
|
|
3591
|
+
}
|
|
3592
|
+
function updateIndex(change) {
|
|
3593
|
+
return withIndexLock(storage, prefix, async () => {
|
|
3594
|
+
const ids = new Set(readIndex(await storage.getItem(indexKey)));
|
|
3595
|
+
for (const id of entries.keys()) ids.add(id);
|
|
3596
|
+
for (const id of unreadable) ids.add(id);
|
|
3597
|
+
change(ids);
|
|
3598
|
+
await storage.setItem(indexKey, JSON.stringify([...ids]));
|
|
3599
|
+
});
|
|
3600
|
+
}
|
|
3601
|
+
function storageOk() {
|
|
3602
|
+
if (storageError !== null) {
|
|
3603
|
+
storageError = null;
|
|
3604
|
+
changed();
|
|
3605
|
+
}
|
|
3606
|
+
}
|
|
3607
|
+
function storageFailed(what, e) {
|
|
3608
|
+
storageError = `${what}: ${message(e)}`;
|
|
3609
|
+
log(storageError);
|
|
3610
|
+
changed();
|
|
3611
|
+
}
|
|
3612
|
+
async function persist(entry) {
|
|
3613
|
+
try {
|
|
3614
|
+
await serial(() => Promise.resolve(storage.setItem(itemKey(entry.id), JSON.stringify(entry))));
|
|
3615
|
+
storageOk();
|
|
3616
|
+
} catch (e) {
|
|
3617
|
+
storageFailed("Couldn't save the outbox", e);
|
|
3618
|
+
}
|
|
3619
|
+
}
|
|
3620
|
+
async function forget(id) {
|
|
3621
|
+
await serial(async () => {
|
|
3622
|
+
await updateIndex((ids) => void ids.delete(id));
|
|
3623
|
+
await storage.removeItem(itemKey(id));
|
|
3624
|
+
});
|
|
3625
|
+
}
|
|
3626
|
+
async function load() {
|
|
3627
|
+
try {
|
|
3628
|
+
const ids = readIndex(await storage.getItem(indexKey));
|
|
3629
|
+
const gone = [];
|
|
3630
|
+
for (const id of ids) {
|
|
3631
|
+
if (entries.has(id)) continue;
|
|
3632
|
+
const raw = await storage.getItem(itemKey(id));
|
|
3633
|
+
if (raw === null || raw === void 0) {
|
|
3634
|
+
gone.push(id);
|
|
3635
|
+
continue;
|
|
3636
|
+
}
|
|
3637
|
+
const entry = readEntry(raw, id);
|
|
3638
|
+
if (entry) entries.set(id, entry);
|
|
3639
|
+
else if (!unreadable.has(id)) {
|
|
3640
|
+
unreadable.add(id);
|
|
3641
|
+
log(`A stored submission (${id}) couldn't be read; it was left in storage.`);
|
|
3642
|
+
}
|
|
3643
|
+
}
|
|
3644
|
+
if (gone.length) await serial(() => updateIndex((set) => gone.forEach((id) => set.delete(id))));
|
|
3645
|
+
loaded = true;
|
|
3646
|
+
storageError = null;
|
|
3647
|
+
} catch (e) {
|
|
3648
|
+
storageError = `Couldn't read the outbox: ${message(e)}`;
|
|
3649
|
+
log(storageError);
|
|
3650
|
+
}
|
|
3651
|
+
changed();
|
|
3652
|
+
}
|
|
3653
|
+
let ready = load();
|
|
3654
|
+
const isDue = (e, t) => e.state === "pending" && (e.nextAttemptAt <= t || e.nextAttemptAt - t > MAX_WAIT_MS);
|
|
3655
|
+
function revive() {
|
|
3656
|
+
const at = now();
|
|
3657
|
+
const revived = [];
|
|
3658
|
+
for (const [id, e] of entries) {
|
|
3659
|
+
if (e.state === "failed" && e.rejection === null) {
|
|
3660
|
+
const next = { ...e, state: "pending", attempts: 0, nextAttemptAt: at };
|
|
3661
|
+
entries.set(id, next);
|
|
3662
|
+
revived.push(next);
|
|
3663
|
+
}
|
|
3664
|
+
}
|
|
3665
|
+
return revived;
|
|
3666
|
+
}
|
|
3667
|
+
function delayFor(attempts) {
|
|
3668
|
+
const full = Math.min(maxMs, baseMs * 2 ** Math.max(0, attempts - 1));
|
|
3669
|
+
return Math.round(full / 2 + random() * full / 2);
|
|
3670
|
+
}
|
|
3671
|
+
function schedule() {
|
|
3672
|
+
if (timer) clearTimeout(timer);
|
|
3673
|
+
timer = null;
|
|
3674
|
+
if (!running || !online) return;
|
|
3675
|
+
let next = Infinity;
|
|
3676
|
+
const t = now();
|
|
3677
|
+
for (const e of entries.values()) if (e.state === "pending") next = Math.min(next, isDue(e, t) ? t : e.nextAttemptAt);
|
|
3678
|
+
if (next === Infinity) return;
|
|
3679
|
+
timer = setTimeout(() => void outbox.flush(), Math.min(MAX_TIMER_MS, Math.max(0, next - t)));
|
|
3680
|
+
}
|
|
3681
|
+
async function attempt(entry) {
|
|
3682
|
+
sendingId = entry.id;
|
|
3683
|
+
changed();
|
|
3684
|
+
const controller = new AbortController();
|
|
3685
|
+
let timeout = null;
|
|
3686
|
+
let result;
|
|
3687
|
+
let error = null;
|
|
3688
|
+
try {
|
|
3689
|
+
result = await Promise.race([
|
|
3690
|
+
send({ ...entry, sending: true }, { signal: controller.signal }),
|
|
3691
|
+
new Promise((_, reject) => {
|
|
3692
|
+
timeout = setTimeout(() => {
|
|
3693
|
+
controller.abort();
|
|
3694
|
+
reject(new Error(`No answer after ${timeoutMs} ms.`));
|
|
3695
|
+
}, timeoutMs);
|
|
3696
|
+
})
|
|
3697
|
+
]);
|
|
3698
|
+
} catch (e) {
|
|
3699
|
+
error = message(e);
|
|
3700
|
+
} finally {
|
|
3701
|
+
if (timeout) clearTimeout(timeout);
|
|
3702
|
+
sendingId = null;
|
|
3703
|
+
}
|
|
3704
|
+
if (!entries.has(entry.id)) return changed();
|
|
3705
|
+
const outcome = error === null ? submitOutcome(result) : "retry";
|
|
3706
|
+
if (outcome === "delivered") {
|
|
3707
|
+
entries.delete(entry.id);
|
|
3708
|
+
changed();
|
|
3709
|
+
try {
|
|
3710
|
+
await forget(entry.id);
|
|
3711
|
+
storageOk();
|
|
3712
|
+
} catch (e) {
|
|
3713
|
+
storageFailed("Couldn't remove a delivered submission from the outbox", e);
|
|
3714
|
+
}
|
|
3715
|
+
options.onDelivered?.({ ...entry, sending: false }, result);
|
|
3716
|
+
return;
|
|
3717
|
+
}
|
|
3718
|
+
const next = { ...entry, attempts: entry.attempts + 1 };
|
|
3719
|
+
if (outcome === "rejected") {
|
|
3720
|
+
const r = result;
|
|
3721
|
+
next.state = "failed";
|
|
3722
|
+
next.rejection = r.reason === "invalid" ? { reason: r.reason, errors: r.errors } : { reason: r.reason };
|
|
3723
|
+
next.lastError = r.reason === "closed" ? "The form isn't taking responses." : r.reason === "conflict" ? "The server holds another response under this submission's key." : "The server refused the answers.";
|
|
3724
|
+
} else {
|
|
3725
|
+
next.lastError = error ?? (isSubmitResult(result) && !result.ok ? `The server answered ${result.reason}.` : "The server's answer wasn't understood.");
|
|
3726
|
+
if (next.attempts >= maxAttempts) {
|
|
3727
|
+
next.state = "failed";
|
|
3728
|
+
next.lastError = `Gave up after ${next.attempts} tries. Last: ${next.lastError}`;
|
|
3729
|
+
} else {
|
|
3730
|
+
let wait = delayFor(next.attempts);
|
|
3731
|
+
if (isSubmitResult(result) && !result.ok && result.reason === "rate_limited" && result.retryAfterSeconds !== void 0) {
|
|
3732
|
+
wait = Math.max(wait, Math.min(MAX_WAIT_MS, result.retryAfterSeconds * 1e3));
|
|
3733
|
+
}
|
|
3734
|
+
next.nextAttemptAt = now() + wait;
|
|
3735
|
+
}
|
|
3736
|
+
}
|
|
3737
|
+
entries.set(entry.id, next);
|
|
3738
|
+
changed();
|
|
3739
|
+
await persist(next);
|
|
3740
|
+
}
|
|
3741
|
+
async function drain() {
|
|
3742
|
+
do {
|
|
3743
|
+
again = false;
|
|
3744
|
+
await ready;
|
|
3745
|
+
if (!loaded) {
|
|
3746
|
+
ready = load();
|
|
3747
|
+
await ready;
|
|
3748
|
+
}
|
|
3749
|
+
while (online) {
|
|
3750
|
+
const t = now();
|
|
3751
|
+
const due = [...entries.values()].filter((e) => isDue(e, t)).sort((a, b) => a.createdAt - b.createdAt || (a.id < b.id ? -1 : 1))[0];
|
|
3752
|
+
if (!due) break;
|
|
3753
|
+
await attempt(due);
|
|
3754
|
+
}
|
|
3755
|
+
} while (again);
|
|
3756
|
+
}
|
|
3757
|
+
const outbox = {
|
|
3758
|
+
get ready() {
|
|
3759
|
+
return ready;
|
|
3760
|
+
},
|
|
3761
|
+
async enqueue(input) {
|
|
3762
|
+
await ready;
|
|
3763
|
+
const id = input.idempotencyKey ?? createKey();
|
|
3764
|
+
const existing = entries.get(id);
|
|
3765
|
+
if (existing) return { ...existing, sending: existing.id === sendingId };
|
|
3766
|
+
const submission = JSON.parse(JSON.stringify(input.submission ?? {}));
|
|
3767
|
+
const at = now();
|
|
3768
|
+
const entry = {
|
|
3769
|
+
id,
|
|
3770
|
+
formId: input.formId,
|
|
3771
|
+
request: {
|
|
3772
|
+
...submission,
|
|
3773
|
+
idempotencyKey: id,
|
|
3774
|
+
clientSubmittedAt: input.clientSubmittedAt ?? at,
|
|
3775
|
+
...input.attemptId !== void 0 ? { attemptId: input.attemptId } : {}
|
|
3776
|
+
},
|
|
3777
|
+
meta: input.meta ? JSON.parse(JSON.stringify(input.meta)) : null,
|
|
3778
|
+
createdAt: at,
|
|
3779
|
+
attempts: 0,
|
|
3780
|
+
nextAttemptAt: at,
|
|
3781
|
+
state: "pending",
|
|
3782
|
+
lastError: null,
|
|
3783
|
+
rejection: null
|
|
3784
|
+
};
|
|
3785
|
+
try {
|
|
3786
|
+
await serial(async () => {
|
|
3787
|
+
await storage.setItem(itemKey(id), JSON.stringify(entry));
|
|
3788
|
+
await updateIndex((ids) => void ids.add(id));
|
|
3789
|
+
});
|
|
3790
|
+
} catch (e) {
|
|
3791
|
+
storageFailed("Couldn't queue the submission", e);
|
|
3792
|
+
throw e;
|
|
3793
|
+
}
|
|
3794
|
+
storageOk();
|
|
3795
|
+
entries.set(id, entry);
|
|
3796
|
+
changed();
|
|
3797
|
+
if (running) void outbox.flush();
|
|
3798
|
+
return { ...entry, sending: false };
|
|
3799
|
+
},
|
|
3800
|
+
flush() {
|
|
3801
|
+
if (flushing) {
|
|
3802
|
+
again = true;
|
|
3803
|
+
return flushing;
|
|
3804
|
+
}
|
|
3805
|
+
flushing = drain().finally(() => {
|
|
3806
|
+
flushing = null;
|
|
3807
|
+
schedule();
|
|
3808
|
+
});
|
|
3809
|
+
return flushing;
|
|
3810
|
+
},
|
|
3811
|
+
async retry(id) {
|
|
3812
|
+
await ready;
|
|
3813
|
+
const entry = entries.get(id);
|
|
3814
|
+
if (!entry || entry.state !== "failed") return;
|
|
3815
|
+
const next = { ...entry, state: "pending", attempts: 0, nextAttemptAt: now(), lastError: null, rejection: null };
|
|
3816
|
+
entries.set(id, next);
|
|
3817
|
+
changed();
|
|
3818
|
+
await persist(next);
|
|
3819
|
+
await outbox.flush();
|
|
3820
|
+
},
|
|
3821
|
+
async discard(id) {
|
|
3822
|
+
await ready;
|
|
3823
|
+
const entry = entries.get(id);
|
|
3824
|
+
if (!entry || sendingId === id) return false;
|
|
3825
|
+
entries.delete(id);
|
|
3826
|
+
changed();
|
|
3827
|
+
schedule();
|
|
3828
|
+
try {
|
|
3829
|
+
await forget(id);
|
|
3830
|
+
storageOk();
|
|
3831
|
+
} catch (e) {
|
|
3832
|
+
entries.set(id, entry);
|
|
3833
|
+
storageFailed("Couldn't discard the submission", e);
|
|
3834
|
+
throw e;
|
|
3835
|
+
}
|
|
3836
|
+
return true;
|
|
3837
|
+
},
|
|
3838
|
+
setOnline(value) {
|
|
3839
|
+
if (online === value) return;
|
|
3840
|
+
online = value;
|
|
3841
|
+
if (online) {
|
|
3842
|
+
const at = now();
|
|
3843
|
+
for (const [id, e] of entries) if (e.state === "pending" && e.nextAttemptAt > at) entries.set(id, { ...e, nextAttemptAt: at });
|
|
3844
|
+
for (const e of revive()) void persist(e);
|
|
3845
|
+
}
|
|
3846
|
+
changed();
|
|
3847
|
+
if (online && running) void outbox.flush();
|
|
3848
|
+
else schedule();
|
|
3849
|
+
},
|
|
3850
|
+
start() {
|
|
3851
|
+
if (running) return;
|
|
3852
|
+
running = true;
|
|
3853
|
+
void ready.then(() => {
|
|
3854
|
+
const revived = revive();
|
|
3855
|
+
if (revived.length) changed();
|
|
3856
|
+
for (const e of revived) void persist(e);
|
|
3857
|
+
return outbox.flush();
|
|
3858
|
+
});
|
|
3859
|
+
},
|
|
3860
|
+
stop() {
|
|
3861
|
+
running = false;
|
|
3862
|
+
schedule();
|
|
3863
|
+
},
|
|
3864
|
+
getSnapshot: () => snapshot,
|
|
3865
|
+
subscribe(listener) {
|
|
3866
|
+
listeners.add(listener);
|
|
3867
|
+
return () => void listeners.delete(listener);
|
|
3868
|
+
}
|
|
3869
|
+
};
|
|
3870
|
+
return outbox;
|
|
3871
|
+
}
|
|
3872
|
+
|
|
3066
3873
|
exports.CALCULATED_TYPE = CALCULATED_TYPE;
|
|
3067
3874
|
exports.CHOICE_FIELD_TYPES = CHOICE_FIELD_TYPES;
|
|
3068
3875
|
exports.COMPUTED_FIELD_TYPES = COMPUTED_FIELD_TYPES;
|
|
3069
3876
|
exports.DEFAULT_PHOTO_FILES = DEFAULT_PHOTO_FILES;
|
|
3070
3877
|
exports.DEFAULT_PHOTO_SIZE_MB = DEFAULT_PHOTO_SIZE_MB;
|
|
3878
|
+
exports.DRAFT_KEY_PREFIX = DRAFT_KEY_PREFIX;
|
|
3879
|
+
exports.DRAFT_MAX_AGE_MS = DRAFT_MAX_AGE_MS;
|
|
3071
3880
|
exports.FORMULA_FUNCTIONS = FORMULA_FUNCTIONS;
|
|
3072
3881
|
exports.HIGHLIGHT_NOTE_MESSAGE = HIGHLIGHT_NOTE_MESSAGE;
|
|
3882
|
+
exports.IDEMPOTENCY_KEY_PATTERN = IDEMPOTENCY_KEY_PATTERN;
|
|
3073
3883
|
exports.MAX_CALCULATED_DECIMALS = MAX_CALCULATED_DECIMALS;
|
|
3074
3884
|
exports.MAX_FORMULA_ITEMS = MAX_FORMULA_ITEMS;
|
|
3075
3885
|
exports.MAX_FORMULA_LENGTH = MAX_FORMULA_LENGTH;
|
|
@@ -3095,6 +3905,7 @@ exports.REQUIRED_MESSAGE = REQUIRED_MESSAGE;
|
|
|
3095
3905
|
exports.SEARCHABLE_OPTION_COUNT = SEARCHABLE_OPTION_COUNT;
|
|
3096
3906
|
exports.STRUCTURED_FIELD_TYPES = STRUCTURED_FIELD_TYPES;
|
|
3097
3907
|
exports.UPLOAD_FIELD_TYPES = UPLOAD_FIELD_TYPES;
|
|
3908
|
+
exports.VALIDATOR_FAILED_MESSAGE = VALIDATOR_FAILED_MESSAGE;
|
|
3098
3909
|
exports.answerSnapshots = answerSnapshots;
|
|
3099
3910
|
exports.answersFromSnapshots = answersFromSnapshots;
|
|
3100
3911
|
exports.answersOnPath = answersOnPath;
|
|
@@ -3102,15 +3913,21 @@ exports.calculatedProps = calculatedProps;
|
|
|
3102
3913
|
exports.calculatedValue = calculatedValue;
|
|
3103
3914
|
exports.canonicalJson = canonicalJson;
|
|
3104
3915
|
exports.carryAnswers = carryAnswers;
|
|
3916
|
+
exports.choiceKind = choiceKind;
|
|
3105
3917
|
exports.choiceOptions = choiceOptions;
|
|
3106
3918
|
exports.clearFilteredAnswers = clearFilteredAnswers;
|
|
3107
3919
|
exports.coerceHidden = coerceHidden;
|
|
3108
3920
|
exports.coerceHiddenValue = coerceHiddenValue;
|
|
3109
3921
|
exports.computePath = computePath;
|
|
3922
|
+
exports.contentText = contentText;
|
|
3923
|
+
exports.createDraftAutosave = createDraftAutosave;
|
|
3924
|
+
exports.createDraftStore = createDraftStore;
|
|
3110
3925
|
exports.createForm = createForm;
|
|
3926
|
+
exports.createSubmitOutbox = createSubmitOutbox;
|
|
3111
3927
|
exports.dateLimits = dateLimits;
|
|
3112
3928
|
exports.defineForm = defineForm;
|
|
3113
3929
|
exports.describeCondition = describeCondition;
|
|
3930
|
+
exports.draftOf = draftOf;
|
|
3114
3931
|
exports.evaluate = evaluate;
|
|
3115
3932
|
exports.evaluateFormula = evaluateFormula;
|
|
3116
3933
|
exports.explain = explain;
|
|
@@ -3124,12 +3941,16 @@ exports.formulaInput = formulaInput;
|
|
|
3124
3941
|
exports.formulaNumber = formulaNumber;
|
|
3125
3942
|
exports.formulaRefs = formulaRefs;
|
|
3126
3943
|
exports.furthestReached = furthestReached;
|
|
3944
|
+
exports.getContentType = getContentType;
|
|
3127
3945
|
exports.getFieldType = getFieldType;
|
|
3128
3946
|
exports.hasOptionFilters = hasOptionFilters;
|
|
3129
3947
|
exports.hiddenByPrefill = hiddenByPrefill;
|
|
3948
|
+
exports.isBuiltInFieldType = isBuiltInFieldType;
|
|
3949
|
+
exports.isChoiceField = isChoiceField;
|
|
3130
3950
|
exports.isEmptyValue = isEmptyValue;
|
|
3131
3951
|
exports.isFormEvent = isFormEvent;
|
|
3132
3952
|
exports.isHttpsUrl = isHttpsUrl;
|
|
3953
|
+
exports.isIdempotencyKey = isIdempotencyKey;
|
|
3133
3954
|
exports.isIsoDate = isIsoDate;
|
|
3134
3955
|
exports.isJson = isJson;
|
|
3135
3956
|
exports.isOtherValue = isOtherValue;
|
|
@@ -3138,8 +3959,12 @@ exports.isPhotoRef = isPhotoRef;
|
|
|
3138
3959
|
exports.isPrefillParam = isPrefillParam;
|
|
3139
3960
|
exports.isSearchable = isSearchable;
|
|
3140
3961
|
exports.isSensitive = isSensitive;
|
|
3962
|
+
exports.isSubmitResult = isSubmitResult;
|
|
3963
|
+
exports.listContentTypes = listContentTypes;
|
|
3141
3964
|
exports.listFieldTypes = listFieldTypes;
|
|
3142
3965
|
exports.listLimits = listLimits;
|
|
3966
|
+
exports.memoryStorage = memoryStorage;
|
|
3967
|
+
exports.mergeResponses = mergeResponses;
|
|
3143
3968
|
exports.normalizeValue = normalizeValue;
|
|
3144
3969
|
exports.numberLimits = numberLimits;
|
|
3145
3970
|
exports.optionFilter = optionFilter;
|
|
@@ -3161,19 +3986,23 @@ exports.prefillSetting = prefillSetting;
|
|
|
3161
3986
|
exports.ratingDisplay = ratingDisplay;
|
|
3162
3987
|
exports.readOption = readOption;
|
|
3163
3988
|
exports.recall = recall;
|
|
3989
|
+
exports.registerContentType = registerContentType;
|
|
3164
3990
|
exports.registerFieldType = registerFieldType;
|
|
3165
3991
|
exports.renameFormulaKey = renameFormulaKey;
|
|
3166
3992
|
exports.replay = replay;
|
|
3167
3993
|
exports.respondentPath = respondentPath;
|
|
3994
|
+
exports.restoreDraft = restoreDraft;
|
|
3168
3995
|
exports.ruleVisible = ruleVisible;
|
|
3169
3996
|
exports.scaleLabels = scaleLabels;
|
|
3170
3997
|
exports.searchOptions = searchOptions;
|
|
3171
3998
|
exports.steps = steps;
|
|
3999
|
+
exports.submitOutcome = submitOutcome;
|
|
3172
4000
|
exports.summarizeValue = summarizeValue;
|
|
3173
4001
|
exports.tableColumns = tableColumns;
|
|
3174
4002
|
exports.tableLimits = tableLimits;
|
|
3175
4003
|
exports.validateField = validateField;
|
|
3176
4004
|
exports.visibleOptions = visibleOptions;
|
|
4005
|
+
exports.webStorage = webStorage;
|
|
3177
4006
|
exports.when = when;
|
|
3178
4007
|
exports.withHostOptions = withHostOptions;
|
|
3179
4008
|
//# sourceMappingURL=index.cjs.map
|