@embeddables/forms 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -8
- package/dist/{form-7wmC3G_q.js → form-CR5xJ_nQ.js} +164 -60
- package/dist/form-CR5xJ_nQ.js.map +1 -0
- package/dist/{form-x4EJcR9A.d.ts → form-DKPfOrTG.d.ts} +31 -10
- package/dist/form-DKPfOrTG.d.ts.map +1 -0
- package/dist/{form-CdIfHqNc.cjs → form-lmr-O7j7.cjs} +164 -60
- package/dist/form-lmr-O7j7.cjs.map +1 -0
- package/dist/{index-CmcgIxTH.d.ts → index-FePzwfQE.d.ts} +2 -2
- package/dist/{index-CmcgIxTH.d.ts.map → index-FePzwfQE.d.ts.map} +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +1 -1
- package/dist/react.cjs +1 -1
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.ts +4 -4
- package/dist/react.js +1 -1
- package/dist/react.js.map +1 -1
- package/dist/server.cjs +1 -1
- package/dist/server.d.ts +1 -1
- package/dist/server.js +1 -1
- package/package.json +3 -3
- package/dist/form-7wmC3G_q.js.map +0 -1
- package/dist/form-CdIfHqNc.cjs.map +0 -1
- package/dist/form-x4EJcR9A.d.ts.map +0 -1
package/README.md
CHANGED
|
@@ -87,7 +87,7 @@ export function App() {
|
|
|
87
87
|
|
|
88
88
|
### File inputs
|
|
89
89
|
|
|
90
|
-
For `
|
|
90
|
+
For `value_type: file` fields, use `useFormFileUpload`. It uploads through the publishable-key API, commits the returned `FormFileRef`, and exposes `inputProps` for a native `<input type="file">`.
|
|
91
91
|
|
|
92
92
|
Example:
|
|
93
93
|
|
|
@@ -127,7 +127,8 @@ const schema = {
|
|
|
127
127
|
{
|
|
128
128
|
key: 'email',
|
|
129
129
|
label: 'Email',
|
|
130
|
-
|
|
130
|
+
value_type: 'email',
|
|
131
|
+
field_type: 'user_input',
|
|
131
132
|
validations: { required: true },
|
|
132
133
|
},
|
|
133
134
|
],
|
|
@@ -155,9 +156,20 @@ from `embeddables/_dist` are already typed.
|
|
|
155
156
|
|
|
156
157
|
## Schema
|
|
157
158
|
|
|
158
|
-
Each form is one object: `id`, optional `name`, and `fields`.
|
|
159
|
-
|
|
160
|
-
`file
|
|
159
|
+
Each form is one object: `id`, optional `name`, and `fields`. Each field has a
|
|
160
|
+
`value_type` (`text`, `email`, `number`, `boolean`, `select`, `multiselect`,
|
|
161
|
+
`json`, `file`).
|
|
162
|
+
|
|
163
|
+
Use `getFieldByKey(fieldKey)` for presentation metadata (`display_text`,
|
|
164
|
+
`display_description`, option `display_text`, `field_type`) instead of
|
|
165
|
+
duplicating copy in markup. `local_only` values still persist but are omitted
|
|
166
|
+
from `data:updated` / `field:updated` analytics patches.
|
|
167
|
+
|
|
168
|
+
```ts
|
|
169
|
+
const plan = form.getFieldByKey('plan')
|
|
170
|
+
plan?.display_text
|
|
171
|
+
plan?.options?.[0]?.display_text
|
|
172
|
+
```
|
|
161
173
|
|
|
162
174
|
Declarative rules: `required`, `minLength`, `maxLength`, `min`, `max`,
|
|
163
175
|
`pattern`, `oneOf`, and optional synchronous `validations.custom`. File fields
|
|
@@ -167,8 +179,9 @@ protocol question. `json` fields cannot. `FormFileRef` is exported for typed
|
|
|
167
179
|
file values.
|
|
168
180
|
|
|
169
181
|
A `select` or `multiselect` field declares its choices in a field-level `options`
|
|
170
|
-
array of `{
|
|
171
|
-
a rule — decides whether one or many of them may
|
|
182
|
+
array of `{ key, display_text, display_description?, exclusive? }`. The field's
|
|
183
|
+
`value_type` — not the name of a rule — decides whether one or many of them may
|
|
184
|
+
be selected. An option marked
|
|
172
185
|
`exclusive` on a `multiselect` cannot coexist with any other value: selecting it
|
|
173
186
|
clears the rest, and selecting a regular option afterwards clears it. Those two
|
|
174
187
|
types no longer accept `validations.oneOf`, which stays available on every other
|
|
@@ -190,10 +203,11 @@ on `initForms` / `forms()`.
|
|
|
190
203
|
| ------ | ------- |
|
|
191
204
|
| `set({ … })` | Validate and persist a patch atomically; optional analytics; best-effort durable R2 write when configured |
|
|
192
205
|
| `get(key)` / `getAll()` | Read declared fields from storage |
|
|
206
|
+
| `getFieldByKey(key)` | Read resolved schema metadata for one declared field |
|
|
193
207
|
| `getValueByProtocolFieldId(protocolFieldId)` | Read by schema `protocolFieldId` (typed like `get()` for the backing field; `undefined` if unknown or unset) |
|
|
194
208
|
| `validate({ … })` | Check values without writing or tracking |
|
|
195
209
|
| `submit()` | Validate all fields, best-effort durable R2 write, and emit `form:submitted` when analytics is configured |
|
|
196
|
-
| `uploadFile({ key, file, fileName? })` | Upload bytes for a `
|
|
210
|
+
| `uploadFile({ key, file, fileName? })` | Upload bytes for a `value_type: file` field; returns `FormFileRef` (caller commits with `.set()`) |
|
|
197
211
|
| `errors()` | Current validation messages |
|
|
198
212
|
| `clear()` | Remove this form's stored values |
|
|
199
213
|
|
|
@@ -72,9 +72,9 @@ function validateValue({ field, value, values, pattern, validator }) {
|
|
|
72
72
|
const isBlank = isAbsent || value === "" || Array.isArray(value) && value.length === 0;
|
|
73
73
|
if (rules?.required === true && isBlank) messages.push(`${field.label} is required`);
|
|
74
74
|
if (isAbsent) return messages;
|
|
75
|
-
if (!FIELD_TYPE_PREDICATES[field.
|
|
75
|
+
if (!FIELD_TYPE_PREDICATES[field.value_type](value)) messages.push(`${field.label} expects ${/^[aeiou]/.test(field.value_type) ? "an" : "a"} ${field.value_type} value`);
|
|
76
76
|
if (typeof value === "string") {
|
|
77
|
-
if (field.
|
|
77
|
+
if (field.value_type === "email" && value !== "" && !EMAIL_PATTERN.test(value)) messages.push(`${field.label} must be a valid email address`);
|
|
78
78
|
if (rules?.minLength !== void 0 && value.length < rules.minLength) messages.push(`${field.label} must be at least ${rules.minLength} characters`);
|
|
79
79
|
if (rules?.maxLength !== void 0 && value.length > rules.maxLength) messages.push(`${field.label} must be at most ${rules.maxLength} characters`);
|
|
80
80
|
if (pattern && !pattern.test(value)) messages.push(`${field.label} is not in the expected format`);
|
|
@@ -87,12 +87,12 @@ function validateValue({ field, value, values, pattern, validator }) {
|
|
|
87
87
|
if (rules?.min !== void 0 && value < rules.min) messages.push(`${field.label} must be at least ${rules.min}`);
|
|
88
88
|
if (rules?.max !== void 0 && value > rules.max) messages.push(`${field.label} must be at most ${rules.max}`);
|
|
89
89
|
}
|
|
90
|
-
if (field.
|
|
90
|
+
if (field.value_type === "select" || field.value_type === "multiselect") {
|
|
91
91
|
const options = field.options;
|
|
92
92
|
if (options) {
|
|
93
|
-
const allowed = new Set(options.map((option) => option.
|
|
94
|
-
if (field.
|
|
95
|
-
if (field.
|
|
93
|
+
const allowed = new Set(options.map((option) => option.key));
|
|
94
|
+
if (field.value_type === "select" && typeof value === "string" && !allowed.has(value)) messages.push(`${field.label} must be one of the allowed options`);
|
|
95
|
+
if (field.value_type === "multiselect" && Array.isArray(value)) {
|
|
96
96
|
if (value.some((entry) => typeof entry !== "string" || !allowed.has(entry))) messages.push(`${field.label} has values that are not allowed options`);
|
|
97
97
|
}
|
|
98
98
|
}
|
|
@@ -101,7 +101,7 @@ function validateValue({ field, value, values, pattern, validator }) {
|
|
|
101
101
|
const encoded = canonicalize(value);
|
|
102
102
|
if (!rules.oneOf.some((option) => canonicalize(option) === encoded)) messages.push(`${field.label} must be one of the allowed options`);
|
|
103
103
|
}
|
|
104
|
-
if (field.
|
|
104
|
+
if (field.value_type === "file" && isFormFileRef(value)) {
|
|
105
105
|
if (value.status !== "done") messages.push(`${field.label} upload is not complete`);
|
|
106
106
|
if (rules?.maxSize !== void 0 && value.size > rules.maxSize) messages.push(`${field.label} must be at most ${rules.maxSize} bytes`);
|
|
107
107
|
if (rules?.accept !== void 0 && !contentTypeMatchesAccept({
|
|
@@ -223,7 +223,7 @@ function writeFields({ storage, formKey, fields, fieldDefinitions }) {
|
|
|
223
223
|
if (value === void 0) continue;
|
|
224
224
|
nextForm[field.key] = {
|
|
225
225
|
value,
|
|
226
|
-
type: field.
|
|
226
|
+
type: field.value_type,
|
|
227
227
|
label: field.label,
|
|
228
228
|
...field.registryId === void 0 ? {} : { registryId: field.registryId },
|
|
229
229
|
...field.protocolFieldId === void 0 ? {} : { protocolFieldId: field.protocolFieldId }
|
|
@@ -305,7 +305,7 @@ function buildCookiePayloadFromBag({ bag, fieldDefinitions }) {
|
|
|
305
305
|
if (value === void 0) continue;
|
|
306
306
|
payload[field.key] = {
|
|
307
307
|
value,
|
|
308
|
-
type: field.
|
|
308
|
+
type: field.value_type,
|
|
309
309
|
label: field.label,
|
|
310
310
|
...field.registryId === void 0 ? {} : { registryId: field.registryId },
|
|
311
311
|
...field.protocolFieldId === void 0 ? {} : { protocolFieldId: field.protocolFieldId }
|
|
@@ -325,10 +325,10 @@ function isStoredField(value) {
|
|
|
325
325
|
return true;
|
|
326
326
|
}
|
|
327
327
|
function entryMatchesFieldDefinition(entry, field) {
|
|
328
|
-
if (entry.type !== field.
|
|
328
|
+
if (entry.type !== field.value_type || entry.label !== field.label) return false;
|
|
329
329
|
if ((entry.registryId ?? void 0) !== (field.registryId ?? void 0)) return false;
|
|
330
330
|
if ((entry.protocolFieldId ?? void 0) !== (field.protocolFieldId ?? void 0)) return false;
|
|
331
|
-
return FIELD_TYPE_PREDICATES[field.
|
|
331
|
+
return FIELD_TYPE_PREDICATES[field.value_type](entry.value);
|
|
332
332
|
}
|
|
333
333
|
function serializeBrowserCookie(key, value) {
|
|
334
334
|
const locationRef = globalThis.location;
|
|
@@ -589,9 +589,22 @@ async function uploadFormFile(config, input) {
|
|
|
589
589
|
const MAX_VALUE_LENGTH = 1024;
|
|
590
590
|
/** The ingest bound on a `data:updated` entry's `label`. */
|
|
591
591
|
const MAX_LABEL_LENGTH = 256;
|
|
592
|
-
/** Maps a form field's declared type to the analytics `field:updated` class. */
|
|
593
|
-
function mapFieldUpdatedType(
|
|
594
|
-
return
|
|
592
|
+
/** Maps a form field's declared value type to the analytics `field:updated` class. */
|
|
593
|
+
function mapFieldUpdatedType(valueType) {
|
|
594
|
+
return valueType;
|
|
595
|
+
}
|
|
596
|
+
function resolvedFieldClassification(field) {
|
|
597
|
+
return field.field_type ?? "user_input";
|
|
598
|
+
}
|
|
599
|
+
function isAnalyticsExcludedField(field) {
|
|
600
|
+
return resolvedFieldClassification(field) === "local_only";
|
|
601
|
+
}
|
|
602
|
+
function analyticsPatchEntries({ fields, patch }) {
|
|
603
|
+
const byKey = new Map(fields.map((field) => [field.key, field]));
|
|
604
|
+
return Object.entries(patch).filter(([key]) => {
|
|
605
|
+
const field = byKey.get(key);
|
|
606
|
+
return field === void 0 || !isAnalyticsExcludedField(field);
|
|
607
|
+
});
|
|
595
608
|
}
|
|
596
609
|
/**
|
|
597
610
|
* Stringifies values for `data:updated` entries. `field:updated` carries the
|
|
@@ -604,9 +617,13 @@ function formatFieldValue({ value }) {
|
|
|
604
617
|
/** One event carrying every key in one `.set()` call. */
|
|
605
618
|
function buildDataUpdatedEvent({ fields, patch, formId }) {
|
|
606
619
|
const byKey = new Map(fields.map((field) => [field.key, field]));
|
|
620
|
+
const entries = analyticsPatchEntries({
|
|
621
|
+
fields,
|
|
622
|
+
patch
|
|
623
|
+
});
|
|
607
624
|
return {
|
|
608
625
|
event_name: "data:updated",
|
|
609
|
-
data: Object.fromEntries(
|
|
626
|
+
data: Object.fromEntries(entries.map(([key, value]) => {
|
|
610
627
|
const field = byKey.get(key);
|
|
611
628
|
return [key, {
|
|
612
629
|
value: formatFieldValue({
|
|
@@ -622,12 +639,15 @@ function buildDataUpdatedEvent({ fields, patch, formId }) {
|
|
|
622
639
|
/** One `field:updated` per changed key, emitted alongside `data:updated`. */
|
|
623
640
|
function buildFieldUpdatedEvents({ fields, patch, formId }) {
|
|
624
641
|
const byKey = new Map(fields.map((field) => [field.key, field]));
|
|
625
|
-
return
|
|
642
|
+
return analyticsPatchEntries({
|
|
643
|
+
fields,
|
|
644
|
+
patch
|
|
645
|
+
}).map(([key, value]) => {
|
|
626
646
|
const field = byKey.get(key);
|
|
627
647
|
const event = {
|
|
628
648
|
event_name: "field:updated",
|
|
629
649
|
field_key: key,
|
|
630
|
-
field_type: mapFieldUpdatedType(field?.
|
|
650
|
+
field_type: mapFieldUpdatedType(field?.value_type ?? "text"),
|
|
631
651
|
form_id: formId
|
|
632
652
|
};
|
|
633
653
|
if (value !== void 0) event.field_value = value;
|
|
@@ -645,7 +665,7 @@ function buildFieldUpdatedEvents({ fields, patch, formId }) {
|
|
|
645
665
|
* values passes through.
|
|
646
666
|
*/
|
|
647
667
|
function normalizeExclusiveSelection({ next, previous, options }) {
|
|
648
|
-
const exclusive = new Set(options.filter((option) => option.exclusive === true).map((option) => option.
|
|
668
|
+
const exclusive = new Set(options.filter((option) => option.exclusive === true).map((option) => option.key));
|
|
649
669
|
if (exclusive.size === 0) return next;
|
|
650
670
|
const previousValues = new Set(previous);
|
|
651
671
|
const added = next.filter((entry) => !previousValues.has(entry));
|
|
@@ -686,9 +706,17 @@ const NUMERIC_RULES = [
|
|
|
686
706
|
"maxSize"
|
|
687
707
|
];
|
|
688
708
|
const FILE_ONLY_RULES = ["accept", "maxSize"];
|
|
689
|
-
const
|
|
690
|
-
"
|
|
691
|
-
"
|
|
709
|
+
const FIELD_CLASSIFICATIONS = [
|
|
710
|
+
"user_input",
|
|
711
|
+
"computed",
|
|
712
|
+
"local_only"
|
|
713
|
+
];
|
|
714
|
+
const SNAKE_CASE_FIELD_KEY = /^[a-z][a-z0-9_]*$/;
|
|
715
|
+
/** Keys allowed on a raw option object before normalization. */
|
|
716
|
+
const OPTION_INPUT_KEYS = [
|
|
717
|
+
"key",
|
|
718
|
+
"display_text",
|
|
719
|
+
"display_description",
|
|
692
720
|
"exclusive"
|
|
693
721
|
];
|
|
694
722
|
/** The two field types whose choices are declared through a field-level `options`. */
|
|
@@ -724,78 +752,141 @@ function validateAndCompile({ schema }) {
|
|
|
724
752
|
if (fields.length === 0) throw new SchemaError("schema.fields: must declare at least one field");
|
|
725
753
|
const patterns = /* @__PURE__ */ new Map();
|
|
726
754
|
const seenKeys = /* @__PURE__ */ new Set();
|
|
755
|
+
const compiledFields = [];
|
|
727
756
|
fields.forEach((field, index) => {
|
|
728
|
-
|
|
757
|
+
compiledFields.push(compileField({
|
|
729
758
|
field,
|
|
730
759
|
path: `schema.fields[${index}]`,
|
|
731
760
|
seenKeys,
|
|
732
761
|
patterns
|
|
733
|
-
});
|
|
762
|
+
}));
|
|
734
763
|
});
|
|
735
764
|
return {
|
|
736
765
|
formKey,
|
|
737
|
-
fields:
|
|
766
|
+
fields: compiledFields,
|
|
738
767
|
patterns
|
|
739
768
|
};
|
|
740
769
|
}
|
|
741
|
-
function
|
|
770
|
+
function compileField({ field, path, seenKeys, patterns }) {
|
|
742
771
|
if (!isObjectLike(field)) throw new SchemaError(`${path}: must be an object`);
|
|
743
772
|
const key = field["key"];
|
|
744
773
|
if (typeof key !== "string" || key.trim() === "") throw new SchemaError(`${path}.key: must be a non-empty string`);
|
|
774
|
+
if (!SNAKE_CASE_FIELD_KEY.test(key)) throw new SchemaError(`${path}.key: must be lowercase snake_case`);
|
|
745
775
|
if (key.length > MAX_FIELD_KEY_LENGTH) throw new SchemaError(`${path}.key: must be at most ${MAX_FIELD_KEY_LENGTH} characters (received ${key.length})`);
|
|
746
776
|
if (seenKeys.has(key)) throw new SchemaError(`${path}.key: duplicate field key "${key}" in this form`);
|
|
747
777
|
seenKeys.add(key);
|
|
748
778
|
const label = field["label"];
|
|
749
779
|
if (typeof label !== "string" || label.trim() === "") throw new SchemaError(`${path}.label: must be a non-empty string`);
|
|
750
|
-
|
|
751
|
-
|
|
780
|
+
if (field["type"] !== void 0) throw new SchemaError(`${path}.type: is not supported; use value_type`);
|
|
781
|
+
const valueTypeRaw = field["value_type"];
|
|
782
|
+
if (typeof valueTypeRaw !== "string" || valueTypeRaw.trim() === "") throw new SchemaError(`${path}.value_type: must be a non-empty string`);
|
|
783
|
+
const valueType = valueTypeRaw;
|
|
784
|
+
if (!FIELD_TYPES.includes(valueType)) throw new SchemaError(`${path}.value_type: must be one of ${FIELD_TYPES.join(", ")}`);
|
|
785
|
+
const displayText = field["display_text"];
|
|
786
|
+
if (displayText !== void 0) {
|
|
787
|
+
if (typeof displayText !== "string" || displayText.trim() === "") throw new SchemaError(`${path}.display_text: must be a non-empty string`);
|
|
788
|
+
}
|
|
789
|
+
const displayDescription = field["display_description"];
|
|
790
|
+
if (displayDescription !== void 0) {
|
|
791
|
+
if (typeof displayDescription !== "string" || displayDescription.trim() === "") throw new SchemaError(`${path}.display_description: must be a non-empty string`);
|
|
792
|
+
}
|
|
793
|
+
const fieldTypeRaw = field["field_type"];
|
|
794
|
+
let fieldType = "user_input";
|
|
795
|
+
if (fieldTypeRaw !== void 0) {
|
|
796
|
+
if (typeof fieldTypeRaw !== "string" || !FIELD_CLASSIFICATIONS.includes(fieldTypeRaw)) throw new SchemaError(`${path}.field_type: must be one of ${FIELD_CLASSIFICATIONS.join(", ")}`);
|
|
797
|
+
fieldType = fieldTypeRaw;
|
|
798
|
+
}
|
|
752
799
|
const registryId = field["registryId"];
|
|
753
800
|
if (registryId !== void 0 && typeof registryId !== "string") throw new SchemaError(`${path}.registryId: must be a string`);
|
|
754
801
|
const protocolFieldId = field["protocolFieldId"];
|
|
755
802
|
if (protocolFieldId !== void 0 && typeof protocolFieldId !== "string") throw new SchemaError(`${path}.protocolFieldId: must be a string`);
|
|
756
803
|
const includeInCookies = field["includeInCookies"];
|
|
757
804
|
if (includeInCookies !== void 0 && typeof includeInCookies !== "boolean") throw new SchemaError(`${path}.includeInCookies: must be a boolean`);
|
|
758
|
-
|
|
805
|
+
const normalizedOptions = normalizeOptions({
|
|
759
806
|
field,
|
|
760
|
-
|
|
807
|
+
valueType,
|
|
761
808
|
path
|
|
762
809
|
});
|
|
763
810
|
const validations = field["validations"];
|
|
764
|
-
if (validations
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
811
|
+
if (validations !== void 0) {
|
|
812
|
+
validateValidations({
|
|
813
|
+
validations,
|
|
814
|
+
type: valueType,
|
|
815
|
+
path: `${path}.validations`
|
|
816
|
+
});
|
|
817
|
+
if (isObjectLike(validations)) {
|
|
818
|
+
const pattern = validations["pattern"];
|
|
819
|
+
if (typeof pattern === "string") patterns.set(key, compilePattern({
|
|
820
|
+
pattern,
|
|
821
|
+
path: `${path}.validations.pattern`
|
|
822
|
+
}));
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
const compiled = {
|
|
826
|
+
...field,
|
|
827
|
+
key,
|
|
828
|
+
label,
|
|
829
|
+
value_type: valueType
|
|
830
|
+
};
|
|
831
|
+
delete compiled["type"];
|
|
832
|
+
compiled["field_type"] = fieldType;
|
|
833
|
+
if (displayText !== void 0) compiled["display_text"] = displayText;
|
|
834
|
+
if (displayDescription !== void 0) compiled["display_description"] = displayDescription;
|
|
835
|
+
if (normalizedOptions !== void 0) compiled["options"] = normalizedOptions;
|
|
836
|
+
if (validations !== void 0) compiled["validations"] = cloneValidations(validations);
|
|
837
|
+
return compiled;
|
|
838
|
+
}
|
|
839
|
+
function cloneValidations(validations) {
|
|
840
|
+
if (!isObjectLike(validations)) return validations;
|
|
841
|
+
const cloned = {};
|
|
842
|
+
for (const [ruleKey, ruleValue] of Object.entries(validations)) {
|
|
843
|
+
if (ruleKey === "custom") {
|
|
844
|
+
cloned[ruleKey] = ruleValue;
|
|
845
|
+
continue;
|
|
846
|
+
}
|
|
847
|
+
if (ruleKey === "oneOf" || ruleKey === "accept") {
|
|
848
|
+
cloned[ruleKey] = Array.isArray(ruleValue) ? [...ruleValue] : ruleValue;
|
|
849
|
+
continue;
|
|
850
|
+
}
|
|
851
|
+
cloned[ruleKey] = ruleValue;
|
|
852
|
+
}
|
|
853
|
+
return cloned;
|
|
777
854
|
}
|
|
778
|
-
function
|
|
855
|
+
function normalizeOptions({ field, valueType, path }) {
|
|
779
856
|
const options = field["options"];
|
|
780
|
-
if (options === void 0) return;
|
|
781
|
-
if (!OPTION_FIELD_TYPES.includes(
|
|
857
|
+
if (options === void 0) return void 0;
|
|
858
|
+
if (!OPTION_FIELD_TYPES.includes(valueType)) throw new SchemaError(`${path}.options: only a select or multiselect field may declare options`);
|
|
782
859
|
if (!(Array.isArray(options) && options.length > 0)) throw new SchemaError(`${path}.options: must be a non-empty array`);
|
|
783
860
|
const entries = options;
|
|
784
|
-
const
|
|
861
|
+
const seenKeys = /* @__PURE__ */ new Set();
|
|
862
|
+
const normalized = [];
|
|
785
863
|
entries.forEach((option, index) => {
|
|
786
864
|
const at = `${path}.options[${index}]`;
|
|
787
865
|
if (!isObjectLike(option)) throw new SchemaError(`${at}: must be an object`);
|
|
788
|
-
for (const optionKey of Object.keys(option)) if (!
|
|
789
|
-
const
|
|
790
|
-
|
|
791
|
-
const
|
|
792
|
-
if (
|
|
866
|
+
for (const optionKey of Object.keys(option)) if (!OPTION_INPUT_KEYS.includes(optionKey)) throw new SchemaError(`${at}.${optionKey}: unknown option key; expected one of ${OPTION_INPUT_KEYS.join(", ")}`);
|
|
867
|
+
const optionKey = option["key"];
|
|
868
|
+
const displayTextRaw = option["display_text"];
|
|
869
|
+
const displayDescription = option["display_description"];
|
|
870
|
+
if (typeof optionKey !== "string" || optionKey.trim() === "") throw new SchemaError(`${at}.key: must be a non-empty string`);
|
|
871
|
+
if (typeof displayTextRaw !== "string" || displayTextRaw.trim() === "") throw new SchemaError(`${at}.display_text: must be a non-empty string`);
|
|
872
|
+
const key = optionKey;
|
|
873
|
+
const displayText = displayTextRaw;
|
|
874
|
+
if (displayDescription !== void 0) {
|
|
875
|
+
if (typeof displayDescription !== "string" || displayDescription.trim() === "") throw new SchemaError(`${at}.display_description: must be a non-empty string`);
|
|
876
|
+
}
|
|
793
877
|
const exclusive = option["exclusive"];
|
|
794
878
|
if (exclusive !== void 0 && typeof exclusive !== "boolean") throw new SchemaError(`${at}.exclusive: must be a boolean`);
|
|
795
|
-
if (exclusive
|
|
796
|
-
if (
|
|
797
|
-
|
|
879
|
+
if (exclusive !== void 0 && valueType === "select") throw new SchemaError(`${at}.exclusive: only a multiselect field may declare an exclusive option`);
|
|
880
|
+
if (seenKeys.has(key)) throw new SchemaError(`${at}.key: duplicate option key "${key}" on this field`);
|
|
881
|
+
seenKeys.add(key);
|
|
882
|
+
normalized.push({
|
|
883
|
+
key,
|
|
884
|
+
display_text: displayText,
|
|
885
|
+
...displayDescription === void 0 ? {} : { display_description: displayDescription },
|
|
886
|
+
...exclusive === void 0 ? {} : { exclusive }
|
|
887
|
+
});
|
|
798
888
|
});
|
|
889
|
+
return normalized;
|
|
799
890
|
}
|
|
800
891
|
function validateValidations({ validations, type, path }) {
|
|
801
892
|
if (!isObjectLike(validations)) throw new SchemaError(`${path}: must be an object`);
|
|
@@ -1205,7 +1296,7 @@ function createFormInstance({ analyticsInstance, storage, persistence, uploadCon
|
|
|
1205
1296
|
const previousBag = readBag();
|
|
1206
1297
|
for (const [key, value] of Object.entries(changes)) {
|
|
1207
1298
|
const field = declared.get(key);
|
|
1208
|
-
if (!field || field.
|
|
1299
|
+
if (!field || field.value_type !== "multiselect" || !field.options) continue;
|
|
1209
1300
|
if (!Array.isArray(value) || !value.every((entry) => typeof entry === "string")) continue;
|
|
1210
1301
|
const stored = previousBag[key];
|
|
1211
1302
|
changes[key] = [...normalizeExclusiveSelection({
|
|
@@ -1291,15 +1382,22 @@ function createFormInstance({ analyticsInstance, storage, persistence, uploadCon
|
|
|
1291
1382
|
ok: true,
|
|
1292
1383
|
errors: noErrors()
|
|
1293
1384
|
});
|
|
1294
|
-
|
|
1385
|
+
const dataUpdated = buildDataUpdatedEvent({
|
|
1295
1386
|
fields: resolved.fields,
|
|
1296
1387
|
patch: changes,
|
|
1297
1388
|
formId: schema.id
|
|
1298
|
-
})
|
|
1389
|
+
});
|
|
1390
|
+
const fieldUpdated = buildFieldUpdatedEvents({
|
|
1299
1391
|
fields: resolved.fields,
|
|
1300
1392
|
patch: changes,
|
|
1301
1393
|
formId: schema.id
|
|
1302
|
-
})
|
|
1394
|
+
});
|
|
1395
|
+
const analyticsEvents = [...Object.keys(dataUpdated.data).length > 0 ? [dataUpdated] : [], ...fieldUpdated];
|
|
1396
|
+
if (analyticsEvents.length === 0) return Promise.resolve({
|
|
1397
|
+
ok: true,
|
|
1398
|
+
errors: noErrors()
|
|
1399
|
+
});
|
|
1400
|
+
return analyticsInstance.trackEvent(analyticsEvents).then(() => ({
|
|
1303
1401
|
ok: true,
|
|
1304
1402
|
errors: noErrors()
|
|
1305
1403
|
})).catch((error) => ({
|
|
@@ -1333,6 +1431,11 @@ function createFormInstance({ analyticsInstance, storage, persistence, uploadCon
|
|
|
1333
1431
|
return get(fieldKey);
|
|
1334
1432
|
});
|
|
1335
1433
|
const getAll = () => narrow(state.bag);
|
|
1434
|
+
const getFieldByKey = (key) => {
|
|
1435
|
+
const fieldKey = key;
|
|
1436
|
+
if (!declared.has(fieldKey)) return void 0;
|
|
1437
|
+
return declared.get(fieldKey);
|
|
1438
|
+
};
|
|
1336
1439
|
const submit = () => {
|
|
1337
1440
|
const snapshot = narrow(readBag());
|
|
1338
1441
|
const values = snapshot;
|
|
@@ -1478,7 +1581,7 @@ function createFormInstance({ analyticsInstance, storage, persistence, uploadCon
|
|
|
1478
1581
|
const uploadFile = async ({ key, file, fileName }) => {
|
|
1479
1582
|
const field = declared.get(key);
|
|
1480
1583
|
if (!field) throw new FormsError(`Unknown field: ${key}`);
|
|
1481
|
-
if (field.
|
|
1584
|
+
if (field.value_type !== "file") throw new FormsError(`Field "${key}" is not a file field`);
|
|
1482
1585
|
if (!uploadConfig) throw new FormsError("File uploads require a valid publishable key and initialized Embeddables core instance.");
|
|
1483
1586
|
const rules = field.validations;
|
|
1484
1587
|
const contentType = file.type || "application/octet-stream";
|
|
@@ -1500,6 +1603,7 @@ function createFormInstance({ analyticsInstance, storage, persistence, uploadCon
|
|
|
1500
1603
|
key: schema.id,
|
|
1501
1604
|
set,
|
|
1502
1605
|
get,
|
|
1606
|
+
getFieldByKey,
|
|
1503
1607
|
getValueByProtocolFieldId,
|
|
1504
1608
|
getAll,
|
|
1505
1609
|
submit,
|
|
@@ -1513,4 +1617,4 @@ function createFormInstance({ analyticsInstance, storage, persistence, uploadCon
|
|
|
1513
1617
|
//#endregion
|
|
1514
1618
|
export { FORM_DATA_KEY as a, SchemaError as c, seedServerFormsStorageFromCookies as i, ValidatorError as l, initForms as n, createMemoryFormsStorage as o, createNoopPersistence as r, FormsError as s, createFormsClient as t };
|
|
1515
1619
|
|
|
1516
|
-
//# sourceMappingURL=form-
|
|
1620
|
+
//# sourceMappingURL=form-CR5xJ_nQ.js.map
|