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