@marktiderman/genesis-forms 0.4.2 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +67 -0
- package/dist/destinations.cjs +92 -4
- package/dist/destinations.cjs.map +1 -1
- package/dist/destinations.d.cts +1 -1
- package/dist/destinations.d.ts +1 -1
- package/dist/destinations.js +92 -4
- package/dist/destinations.js.map +1 -1
- package/dist/{engine-Dz9J5ej2.d.ts → engine-BDq0XJ21.d.cts} +25 -2
- package/dist/{engine-BkNuzlna.d.cts → engine-Qzp6bX6O.d.ts} +25 -2
- package/dist/index.cjs +259 -16
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +120 -75
- package/dist/index.d.ts +120 -75
- package/dist/index.js +236 -17
- package/dist/index.js.map +1 -1
- package/dist/server.cjs +187 -12
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +10 -3
- package/dist/server.d.ts +10 -3
- package/dist/server.js +187 -12
- package/dist/server.js.map +1 -1
- package/dist/testing.cjs +187 -12
- 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 +187 -12
- package/dist/testing.js.map +1 -1
- package/dist/{types-BBKBQ8sp.d.cts → types-BhYUbLJ-.d.cts} +21 -1
- package/dist/{types-BBKBQ8sp.d.ts → types-BhYUbLJ-.d.ts} +21 -1
- package/dist/validate-Bs1lyoOl.d.ts +131 -0
- package/dist/validate-BsZ0YTQS.d.cts +131 -0
- package/package.json +1 -1
- package/dist/validate-BXXqi0fq.d.cts +0 -30
- package/dist/validate-BjNwF1UE.d.ts +0 -30
package/dist/server.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { F as
|
|
1
|
+
import { F as FieldValidator, H as HostOptions } from './validate-BsZ0YTQS.cjs';
|
|
2
|
+
import { J as Json, e as FormEvent, F as FormDefinition } from './types-BhYUbLJ-.cjs';
|
|
3
3
|
|
|
4
4
|
interface SubmissionInput {
|
|
5
5
|
/** Answers keyed by field id. Ignored when `events` is given. */
|
|
@@ -35,6 +35,13 @@ interface SubmissionOptions {
|
|
|
35
35
|
* replay counts: the submit that follows it does not.
|
|
36
36
|
*/
|
|
37
37
|
maxReplayWork?: number | undefined;
|
|
38
|
+
/**
|
|
39
|
+
* The option lists the app supplies for `optionsFrom: { source: "host" }`
|
|
40
|
+
* choice fields, loaded here on the server (never taken from the client):
|
|
41
|
+
* an answer must be one of the field's own options or of these. Pass what
|
|
42
|
+
* the form was shown with, loaded afresh, such as the current staff.
|
|
43
|
+
*/
|
|
44
|
+
hostOptions?: HostOptions | undefined;
|
|
38
45
|
}
|
|
39
46
|
interface CheckedSubmission {
|
|
40
47
|
status: "draft" | "completed";
|
|
@@ -74,7 +81,7 @@ declare const RAW_EVENTS_PER_EVENT = 10;
|
|
|
74
81
|
* Check a submission against the published definition. Never throws on a
|
|
75
82
|
* malformed submission: it returns `{ ok: false, errors: { "": … } }`.
|
|
76
83
|
*/
|
|
77
|
-
declare function validateSubmission(
|
|
84
|
+
declare function validateSubmission(published: FormDefinition, submission: SubmissionInput, options?: SubmissionOptions): SubmissionResult;
|
|
78
85
|
/**
|
|
79
86
|
* The definition as respondents may see it. R1 forms hold nothing secret;
|
|
80
87
|
* assessment answer keys (R4) will be stripped here.
|
package/dist/server.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { F as
|
|
1
|
+
import { F as FieldValidator, H as HostOptions } from './validate-Bs1lyoOl.js';
|
|
2
|
+
import { J as Json, e as FormEvent, F as FormDefinition } from './types-BhYUbLJ-.js';
|
|
3
3
|
|
|
4
4
|
interface SubmissionInput {
|
|
5
5
|
/** Answers keyed by field id. Ignored when `events` is given. */
|
|
@@ -35,6 +35,13 @@ interface SubmissionOptions {
|
|
|
35
35
|
* replay counts: the submit that follows it does not.
|
|
36
36
|
*/
|
|
37
37
|
maxReplayWork?: number | undefined;
|
|
38
|
+
/**
|
|
39
|
+
* The option lists the app supplies for `optionsFrom: { source: "host" }`
|
|
40
|
+
* choice fields, loaded here on the server (never taken from the client):
|
|
41
|
+
* an answer must be one of the field's own options or of these. Pass what
|
|
42
|
+
* the form was shown with, loaded afresh, such as the current staff.
|
|
43
|
+
*/
|
|
44
|
+
hostOptions?: HostOptions | undefined;
|
|
38
45
|
}
|
|
39
46
|
interface CheckedSubmission {
|
|
40
47
|
status: "draft" | "completed";
|
|
@@ -74,7 +81,7 @@ declare const RAW_EVENTS_PER_EVENT = 10;
|
|
|
74
81
|
* Check a submission against the published definition. Never throws on a
|
|
75
82
|
* malformed submission: it returns `{ ok: false, errors: { "": … } }`.
|
|
76
83
|
*/
|
|
77
|
-
declare function validateSubmission(
|
|
84
|
+
declare function validateSubmission(published: FormDefinition, submission: SubmissionInput, options?: SubmissionOptions): SubmissionResult;
|
|
78
85
|
/**
|
|
79
86
|
* The definition as respondents may see it. R1 forms hold nothing secret;
|
|
80
87
|
* assessment answer keys (R4) will be stripped here.
|
package/dist/server.js
CHANGED
|
@@ -719,6 +719,80 @@ var structuredFieldTypes = [
|
|
|
719
719
|
}
|
|
720
720
|
];
|
|
721
721
|
|
|
722
|
+
// src/photo.ts
|
|
723
|
+
var PHOTO_TYPE = "photo";
|
|
724
|
+
var PHOTO_FORMATS = {
|
|
725
|
+
jpeg: ["image/jpeg"],
|
|
726
|
+
png: ["image/png"],
|
|
727
|
+
heic: ["image/heic", "image/heif"],
|
|
728
|
+
webp: ["image/webp"]
|
|
729
|
+
};
|
|
730
|
+
var FORMAT_NAMES = { jpeg: "JPEG", png: "PNG", heic: "HEIC", webp: "WebP" };
|
|
731
|
+
var ALL_FORMATS = Object.keys(PHOTO_FORMATS);
|
|
732
|
+
var DEFAULT_PHOTO_FILES = 3;
|
|
733
|
+
var MAX_PHOTO_FILES = 5;
|
|
734
|
+
var DEFAULT_PHOTO_SIZE_MB = 10;
|
|
735
|
+
var MAX_PHOTO_SIZE_MB = 25;
|
|
736
|
+
var MAX_PHOTO_ID_LENGTH = 200;
|
|
737
|
+
var MAX_PHOTO_NAME_LENGTH = 255;
|
|
738
|
+
var isPhotoFormat = (v) => typeof v === "string" && Object.prototype.hasOwnProperty.call(PHOTO_FORMATS, v);
|
|
739
|
+
var wholeIn = (v, min, max) => typeof v === "number" && Number.isInteger(v) && v >= min && v <= max;
|
|
740
|
+
function photoLimits(field) {
|
|
741
|
+
const maxFiles = wholeIn(field.props?.maxFiles, 1, MAX_PHOTO_FILES) ? field.props.maxFiles : DEFAULT_PHOTO_FILES;
|
|
742
|
+
const maxSizeMb = wholeIn(field.props?.maxSizeMb, 1, MAX_PHOTO_SIZE_MB) ? field.props.maxSizeMb : DEFAULT_PHOTO_SIZE_MB;
|
|
743
|
+
const raw = field.props?.formats;
|
|
744
|
+
const picked = Array.isArray(raw) ? ALL_FORMATS.filter((f) => raw.includes(f)) : [];
|
|
745
|
+
const formats = picked.length ? picked : [...ALL_FORMATS];
|
|
746
|
+
return {
|
|
747
|
+
maxFiles,
|
|
748
|
+
maxSizeMb,
|
|
749
|
+
maxBytes: maxSizeMb * 1024 * 1024,
|
|
750
|
+
formats,
|
|
751
|
+
mimeTypes: formats.flatMap((f) => PHOTO_FORMATS[f])
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
function isPhotoRef(v) {
|
|
755
|
+
if (!v || typeof v !== "object" || Array.isArray(v)) return false;
|
|
756
|
+
const r = v;
|
|
757
|
+
return typeof r.id === "string" && r.id.length > 0 && r.id.length <= MAX_PHOTO_ID_LENGTH && typeof r.name === "string" && r.name.length <= MAX_PHOTO_NAME_LENGTH && typeof r.type === "string" && typeof r.size === "number" && Number.isInteger(r.size) && r.size > 0;
|
|
758
|
+
}
|
|
759
|
+
function photoRefs(value) {
|
|
760
|
+
if (!Array.isArray(value)) return [];
|
|
761
|
+
const refs = [];
|
|
762
|
+
for (const r of value) if (isPhotoRef(r)) refs.push({ id: r.id, name: r.name, type: r.type, size: r.size });
|
|
763
|
+
return refs;
|
|
764
|
+
}
|
|
765
|
+
var plural2 = (n, one, many = `${one}s`) => `${n} ${n === 1 ? one : many}`;
|
|
766
|
+
function photoFormatList(formats) {
|
|
767
|
+
const names = formats.map((f) => FORMAT_NAMES[f]);
|
|
768
|
+
return names.length > 1 ? `${names.slice(0, -1).join(", ")} or ${names[names.length - 1]}` : names[0] ?? "";
|
|
769
|
+
}
|
|
770
|
+
var photoTypeMessage = (field) => `Use a ${photoFormatList(photoLimits(field).formats)} photo.`;
|
|
771
|
+
var photoSizeMessage = (field) => `Each photo must be ${photoLimits(field).maxSizeMb} MB or smaller.`;
|
|
772
|
+
var photoCountMessage = (field) => `Add at most ${plural2(photoLimits(field).maxFiles, "photo")}.`;
|
|
773
|
+
var photoFieldType = {
|
|
774
|
+
type: PHOTO_TYPE,
|
|
775
|
+
label: "Photo",
|
|
776
|
+
category: "media",
|
|
777
|
+
value: { json: true },
|
|
778
|
+
operators: ["answered", "empty"],
|
|
779
|
+
a11y: { semantics: "group" },
|
|
780
|
+
isEmpty: (value) => value === void 0 || value === null || Array.isArray(value) && value.length === 0,
|
|
781
|
+
validate: (value, field) => {
|
|
782
|
+
if (!Array.isArray(value) || !value.every(isPhotoRef)) return "Add a photo.";
|
|
783
|
+
const limits = photoLimits(field);
|
|
784
|
+
if (value.length > limits.maxFiles) return photoCountMessage(field);
|
|
785
|
+
const refs = value;
|
|
786
|
+
if (new Set(refs.map((r) => r.id)).size !== refs.length) return "Each photo can be added once.";
|
|
787
|
+
if (refs.some((r) => !limits.mimeTypes.includes(r.type.toLowerCase()))) return photoTypeMessage(field);
|
|
788
|
+
if (refs.some((r) => r.size > limits.maxBytes)) return photoSizeMessage(field);
|
|
789
|
+
return null;
|
|
790
|
+
},
|
|
791
|
+
normalize: (value) => Array.isArray(value) && value.every(isPhotoRef) ? photoRefs(value) : value,
|
|
792
|
+
summarize: (value) => photoRefs(value).map((r) => r.name || "Photo").join(", "),
|
|
793
|
+
samples: () => [[{ id: "sample-photo", name: "photo.jpg", type: "image/jpeg", size: 1024 }]]
|
|
794
|
+
};
|
|
795
|
+
|
|
722
796
|
// src/fields.ts
|
|
723
797
|
var REGISTRY = /* @__PURE__ */ Symbol.for("@marktiderman/genesis-forms/field-types");
|
|
724
798
|
var globalScope = globalThis;
|
|
@@ -755,6 +829,20 @@ function summarizeValue(value, field) {
|
|
|
755
829
|
if (Array.isArray(value)) return value.join(", ");
|
|
756
830
|
return String(value);
|
|
757
831
|
}
|
|
832
|
+
var OTHER_MAX_LENGTH = 200;
|
|
833
|
+
function otherOption(field) {
|
|
834
|
+
const other = field.props?.other;
|
|
835
|
+
if (other === true) return { label: "Other" };
|
|
836
|
+
if (other && typeof other === "object" && !Array.isArray(other)) {
|
|
837
|
+
const label = typeof other.label === "string" && other.label.trim() ? other.label.trim() : "Other";
|
|
838
|
+
return { label };
|
|
839
|
+
}
|
|
840
|
+
return null;
|
|
841
|
+
}
|
|
842
|
+
function isOtherValue(field, value) {
|
|
843
|
+
return otherOption(field) !== null && typeof value === "string" && !choiceOptions(field).some((o) => o.value === value);
|
|
844
|
+
}
|
|
845
|
+
var typedOther = (field, value) => isOtherValue(field, value) && value.trim() !== "" && value.length <= OTHER_MAX_LENGTH;
|
|
758
846
|
function choiceOptions(field) {
|
|
759
847
|
const raw = field.props?.options;
|
|
760
848
|
if (!Array.isArray(raw)) return [];
|
|
@@ -762,6 +850,42 @@ function choiceOptions(field) {
|
|
|
762
850
|
(o) => o && typeof o === "object" && !Array.isArray(o) && typeof o.value === "string" ? [{ value: o.value, label: typeof o.label === "string" ? o.label : void 0 }] : []
|
|
763
851
|
);
|
|
764
852
|
}
|
|
853
|
+
function optionsSource(field) {
|
|
854
|
+
const raw = field.props?.optionsFrom;
|
|
855
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return void 0;
|
|
856
|
+
return raw.source === "host" && typeof raw.key === "string" && raw.key ? { source: "host", key: raw.key } : void 0;
|
|
857
|
+
}
|
|
858
|
+
function hostList(hostOptions, key) {
|
|
859
|
+
if (!hostOptions || !Object.prototype.hasOwnProperty.call(hostOptions, key)) return [];
|
|
860
|
+
const list = hostOptions[key];
|
|
861
|
+
if (!Array.isArray(list)) return [];
|
|
862
|
+
const seen = /* @__PURE__ */ new Set();
|
|
863
|
+
const out = [];
|
|
864
|
+
for (const o of list) {
|
|
865
|
+
if (!o || typeof o !== "object" || Array.isArray(o)) continue;
|
|
866
|
+
const { value, label } = o;
|
|
867
|
+
if (typeof value !== "string" || !value || seen.has(value)) continue;
|
|
868
|
+
seen.add(value);
|
|
869
|
+
out.push(typeof label === "string" ? { value, label } : { value });
|
|
870
|
+
}
|
|
871
|
+
return out;
|
|
872
|
+
}
|
|
873
|
+
function withHostOptions(definition, hostOptions) {
|
|
874
|
+
let changed = false;
|
|
875
|
+
const blocks = definition.blocks.map((block) => {
|
|
876
|
+
if (block.kind !== "field") return block;
|
|
877
|
+
const from = optionsSource(block);
|
|
878
|
+
if (!from) return block;
|
|
879
|
+
const own2 = choiceOptions(block);
|
|
880
|
+
const taken = new Set(own2.map((o) => o.value));
|
|
881
|
+
const added = hostList(hostOptions, from.key).filter((o) => !taken.has(o.value));
|
|
882
|
+
if (!added.length) return block;
|
|
883
|
+
changed = true;
|
|
884
|
+
const options = [...own2, ...added].map((o) => o.label === void 0 ? { value: o.value } : { value: o.value, label: o.label });
|
|
885
|
+
return { ...block, props: { ...block.props, options } };
|
|
886
|
+
});
|
|
887
|
+
return changed ? { ...definition, blocks } : definition;
|
|
888
|
+
}
|
|
765
889
|
function optionLabel(field, value) {
|
|
766
890
|
const option = choiceOptions(field).find((o) => o.value === value);
|
|
767
891
|
return option?.label ?? value;
|
|
@@ -880,7 +1004,7 @@ var builtins = [
|
|
|
880
1004
|
value: { json: true },
|
|
881
1005
|
operators: ["eq", "neq", "in", "answered", "empty"],
|
|
882
1006
|
a11y: { semantics: "group" },
|
|
883
|
-
validate: (value, field) => typeof value === "string" && choiceOptions(field).some((o) => o.value === value) ? null : "Choose one of the options.",
|
|
1007
|
+
validate: (value, field) => typeof value === "string" && (choiceOptions(field).some((o) => o.value === value) || typedOther(field, value)) ? null : "Choose one of the options.",
|
|
884
1008
|
summarize: (value, field) => optionLabel(field, String(value))
|
|
885
1009
|
},
|
|
886
1010
|
{
|
|
@@ -892,7 +1016,8 @@ var builtins = [
|
|
|
892
1016
|
a11y: { semantics: "group" },
|
|
893
1017
|
validate: (value, field) => {
|
|
894
1018
|
const allowed = new Set(choiceOptions(field).map((o) => o.value));
|
|
895
|
-
|
|
1019
|
+
const others = Array.isArray(value) ? value.filter((v) => typeof v === "string" && !allowed.has(v)) : [];
|
|
1020
|
+
return Array.isArray(value) && new Set(value).size === value.length && value.every((v) => typeof v === "string") && (others.length === 0 || others.length === 1 && typedOther(field, others[0])) ? null : "Choose from the options.";
|
|
896
1021
|
},
|
|
897
1022
|
summarize: (value, field) => Array.isArray(value) ? value.map((v) => optionLabel(field, String(v))).join(", ") : String(value)
|
|
898
1023
|
},
|
|
@@ -913,7 +1038,7 @@ var builtins = [
|
|
|
913
1038
|
value: { json: true },
|
|
914
1039
|
operators: ["eq", "neq", "in", "answered", "empty"],
|
|
915
1040
|
a11y: { semantics: "control" },
|
|
916
|
-
validate: (value, field) => typeof value === "string" && choiceOptions(field).some((o) => o.value === value) ? null : "Choose one of the options.",
|
|
1041
|
+
validate: (value, field) => typeof value === "string" && (choiceOptions(field).some((o) => o.value === value) || typedOther(field, value)) ? null : "Choose one of the options.",
|
|
917
1042
|
summarize: (value, field) => optionLabel(field, String(value))
|
|
918
1043
|
},
|
|
919
1044
|
{
|
|
@@ -1020,7 +1145,7 @@ function dateLimits(field) {
|
|
|
1020
1145
|
if (typeof max === "string" && isIsoDate(max)) out.max = max;
|
|
1021
1146
|
return out;
|
|
1022
1147
|
}
|
|
1023
|
-
for (const manifest of [...builtins, ...structuredFieldTypes, calculatedFieldType]) registerFieldType(manifest);
|
|
1148
|
+
for (const manifest of [...builtins, ...structuredFieldTypes, calculatedFieldType, photoFieldType]) registerFieldType(manifest);
|
|
1024
1149
|
|
|
1025
1150
|
// src/events.ts
|
|
1026
1151
|
function isPlainObject(value) {
|
|
@@ -1289,7 +1414,8 @@ function readsAny(node, ids) {
|
|
|
1289
1414
|
function createForm(definition, options = {}) {
|
|
1290
1415
|
return build(definition, options).engine;
|
|
1291
1416
|
}
|
|
1292
|
-
function build(
|
|
1417
|
+
function build(given, options, replaying = false, budget) {
|
|
1418
|
+
const definition = withHostOptions(given, options.hostOptions);
|
|
1293
1419
|
const fields = fieldMap(definition);
|
|
1294
1420
|
const byKey = new Map([...fields.values()].map((f) => [f.key, f]));
|
|
1295
1421
|
const blocks = new Map(definition.blocks.map((b) => [b.id, b]));
|
|
@@ -1335,6 +1461,7 @@ function build(definition, options, replaying = false, budget) {
|
|
|
1335
1461
|
path: first.path,
|
|
1336
1462
|
current: first.path[0] ?? null,
|
|
1337
1463
|
history: [],
|
|
1464
|
+
furthest: first.path[0] ?? null,
|
|
1338
1465
|
errors: {},
|
|
1339
1466
|
status: "in_progress",
|
|
1340
1467
|
ending: null,
|
|
@@ -1435,7 +1562,8 @@ function build(definition, options, replaying = false, budget) {
|
|
|
1435
1562
|
emit({ ...state, events });
|
|
1436
1563
|
return true;
|
|
1437
1564
|
}
|
|
1438
|
-
|
|
1565
|
+
const furthest = state.furthest && state.path.indexOf(state.furthest) >= state.path.indexOf(target) ? state.furthest : target;
|
|
1566
|
+
emit({ ...state, current: target, history: [...state.history, state.current], furthest, events });
|
|
1439
1567
|
return true;
|
|
1440
1568
|
},
|
|
1441
1569
|
back() {
|
|
@@ -1480,6 +1608,15 @@ function build(definition, options, replaying = false, budget) {
|
|
|
1480
1608
|
settleNow();
|
|
1481
1609
|
batch = wasBatch;
|
|
1482
1610
|
}
|
|
1611
|
+
const resumeAt = options.resumeAt;
|
|
1612
|
+
if (resumeAt && !replaying && state.path.includes(resumeAt)) {
|
|
1613
|
+
for (let i = 0; i < state.path.length && state.current !== resumeAt; i++) {
|
|
1614
|
+
if (!engine.next()) {
|
|
1615
|
+
state = { ...state, errors: {} };
|
|
1616
|
+
break;
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1483
1620
|
const apply = (event) => {
|
|
1484
1621
|
if (event.type === "answer") {
|
|
1485
1622
|
if (fields.has(event.field)) {
|
|
@@ -1667,6 +1804,22 @@ function parseForm(input) {
|
|
|
1667
1804
|
}
|
|
1668
1805
|
}
|
|
1669
1806
|
if (type === "consent") optionalText(props.text, `${path}.text`);
|
|
1807
|
+
if (type === PHOTO_TYPE) {
|
|
1808
|
+
if (props.maxFiles !== void 0 && !(isInt(props.maxFiles) && props.maxFiles >= 1 && props.maxFiles <= MAX_PHOTO_FILES)) {
|
|
1809
|
+
issue(`${path}.maxFiles`, `Must be a whole number from 1 to ${MAX_PHOTO_FILES}.`);
|
|
1810
|
+
}
|
|
1811
|
+
if (props.maxSizeMb !== void 0 && !(isInt(props.maxSizeMb) && props.maxSizeMb >= 1 && props.maxSizeMb <= MAX_PHOTO_SIZE_MB)) {
|
|
1812
|
+
issue(`${path}.maxSizeMb`, `Must be a whole number from 1 to ${MAX_PHOTO_SIZE_MB}.`);
|
|
1813
|
+
}
|
|
1814
|
+
if (props.formats !== void 0) {
|
|
1815
|
+
const formats = props.formats;
|
|
1816
|
+
if (!Array.isArray(formats) || formats.length === 0 || !formats.every(isPhotoFormat)) {
|
|
1817
|
+
issue(`${path}.formats`, "Must be a list of one or more of jpeg, png, heic and webp.");
|
|
1818
|
+
} else if (new Set(formats).size !== formats.length) {
|
|
1819
|
+
issue(`${path}.formats`, "Name each format once.");
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1670
1823
|
};
|
|
1671
1824
|
if (!isObject2(input)) {
|
|
1672
1825
|
issue("", "A form definition must be an object.");
|
|
@@ -1687,6 +1840,7 @@ function parseForm(input) {
|
|
|
1687
1840
|
else {
|
|
1688
1841
|
optionalBoolean(input.settings.anonymous, "settings.anonymous");
|
|
1689
1842
|
optionalBoolean(input.settings.clearHiddenValues, "settings.clearHiddenValues");
|
|
1843
|
+
optionalBoolean(input.settings.saveProgress, "settings.saveProgress");
|
|
1690
1844
|
}
|
|
1691
1845
|
}
|
|
1692
1846
|
if (input.meta !== void 0 && (!isObject2(input.meta) || !isJson(input.meta))) {
|
|
@@ -1727,6 +1881,14 @@ function parseForm(input) {
|
|
|
1727
1881
|
issue(`${path}.type`, `Unknown field type "${String(block.type)}".`);
|
|
1728
1882
|
}
|
|
1729
1883
|
if (block.props !== void 0) {
|
|
1884
|
+
const from = isObject2(block.props) ? block.props.optionsFrom : void 0;
|
|
1885
|
+
if (from !== void 0) {
|
|
1886
|
+
if (!isObject2(from) || from.source !== "host" || typeof from.key !== "string" || !from.key) {
|
|
1887
|
+
issue(`${path}.props.optionsFrom`, 'Must be { source: "host", key } with a non-empty key.');
|
|
1888
|
+
} else if (typeof block.type === "string" && !CHOICE_TYPES.has(block.type)) {
|
|
1889
|
+
warnings.push({ path: `${path}.props.optionsFrom`, message: "Only single choice, multiple choice and dropdown questions take options from the app." });
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1730
1892
|
if (!isObject2(block.props) || !isJson(block.props)) issue(`${path}.props`, "Must be a JSON object.");
|
|
1731
1893
|
else if (block.props.options !== void 0) {
|
|
1732
1894
|
const options = block.props.options;
|
|
@@ -1737,10 +1899,20 @@ function parseForm(input) {
|
|
|
1737
1899
|
if (!isObject2(o) || typeof o.value !== "string") issue(at, "Each option needs a string value.");
|
|
1738
1900
|
else optionalText(o.label, `${at}.label`);
|
|
1739
1901
|
});
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1902
|
+
if (from === void 0) {
|
|
1903
|
+
choices.set(
|
|
1904
|
+
block.id,
|
|
1905
|
+
new Set(options.flatMap((o) => isObject2(o) && typeof o.value === "string" ? [o.value] : []))
|
|
1906
|
+
);
|
|
1907
|
+
}
|
|
1908
|
+
}
|
|
1909
|
+
}
|
|
1910
|
+
const other = isObject2(block.props) ? block.props.other : void 0;
|
|
1911
|
+
if (other !== void 0) {
|
|
1912
|
+
if (other !== true && other !== false && !(isObject2(other) && (other.label === void 0 || typeof other.label === "string"))) {
|
|
1913
|
+
issue(`${path}.props.other`, "Must be true, false or { label }.");
|
|
1914
|
+
} else if (other !== false && typeof block.type === "string" && !CHOICE_TYPES.has(block.type)) {
|
|
1915
|
+
warnings.push({ path: `${path}.props.other`, message: 'Only single choice, multiple choice and dropdown questions take an "Other" answer.' });
|
|
1744
1916
|
}
|
|
1745
1917
|
}
|
|
1746
1918
|
const { max, maxLength: maxLength2 } = isObject2(block.props) ? block.props : {};
|
|
@@ -1815,6 +1987,8 @@ function parseForm(input) {
|
|
|
1815
1987
|
} else if (ref.column !== void 0) issue(path, `"${ref.key}" is not a table, so it has no column "${ref.column}".`);
|
|
1816
1988
|
else if (targetType === "media-notes" || targetType === "text-highlights") {
|
|
1817
1989
|
warnings.push({ path, message: `"${ref.key}" holds notes, not numbers; the formula reads it as blank.` });
|
|
1990
|
+
} else if (targetType === PHOTO_TYPE) {
|
|
1991
|
+
warnings.push({ path, message: `"${ref.key}" holds photos, not numbers; the formula reads it as blank.` });
|
|
1818
1992
|
}
|
|
1819
1993
|
}
|
|
1820
1994
|
});
|
|
@@ -2079,9 +2253,10 @@ function wellFormed(input) {
|
|
|
2079
2253
|
}
|
|
2080
2254
|
return { answers, events: known, hidden };
|
|
2081
2255
|
}
|
|
2082
|
-
function validateSubmission(
|
|
2083
|
-
const invalid = checkDefinition(
|
|
2256
|
+
function validateSubmission(published, submission, options = {}) {
|
|
2257
|
+
const invalid = checkDefinition(published);
|
|
2084
2258
|
if (invalid) return { ok: false, errors: { "": invalid } };
|
|
2259
|
+
const definition = withHostOptions(published, options.hostOptions);
|
|
2085
2260
|
if (!isPlainObject(submission)) return MALFORMED;
|
|
2086
2261
|
const maxEvents = options.maxEvents ?? MAX_EVENTS;
|
|
2087
2262
|
let sent = submission;
|