@usefillo/core 0.12.0 → 0.13.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/dist/index.d.ts +130 -9
- package/dist/index.js +402 -13
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -20,7 +20,7 @@ interface JumpRule {
|
|
|
20
20
|
/** Target page id, or "end" to finish the form. */
|
|
21
21
|
to: string | "end";
|
|
22
22
|
}
|
|
23
|
-
type FieldKind = "short_text" | "long_text" | "email" | "url" | "phone" | "number" | "select" | "multi_select" | "dropdown" | "checkbox" | "rating" | "linear_scale" | "ranking" | "matrix" | "signature" | "date" | "file_upload" | "hidden" | "calculated" | "custom";
|
|
23
|
+
type FieldKind = "short_text" | "long_text" | "email" | "url" | "phone" | "number" | "select" | "multi_select" | "dropdown" | "checkbox" | "rating" | "linear_scale" | "ranking" | "matrix" | "signature" | "date" | "file_upload" | "hidden" | "calculated" | "repeating_group" | "custom";
|
|
24
24
|
type ContentKind = "heading" | "paragraph" | "divider";
|
|
25
25
|
type BlockKind = FieldKind | ContentKind;
|
|
26
26
|
interface SelectOption {
|
|
@@ -198,6 +198,53 @@ interface CalculatedField extends BaseField {
|
|
|
198
198
|
prefix?: string;
|
|
199
199
|
suffix?: string;
|
|
200
200
|
}
|
|
201
|
+
/**
|
|
202
|
+
* One repeated instance's answers: childId → that child's canonical value,
|
|
203
|
+
* the exact same shapes a top-level field would carry (trimmed strings,
|
|
204
|
+
* wire-format numerics coerced, empty answers absent — see canonical.ts). A
|
|
205
|
+
* key absent from the record means that child is unanswered in this instance,
|
|
206
|
+
* mirroring how a key absent from {@link ResponseData} means a top-level field
|
|
207
|
+
* is unanswered.
|
|
208
|
+
*/
|
|
209
|
+
type GroupInstanceValue = Record<string, FieldValue>;
|
|
210
|
+
/**
|
|
211
|
+
* A repeated block of fields (logic depth P3) — "one row per guest/item",
|
|
212
|
+
* respondent-controlled. Bounded by construction: `maxInstances` is required
|
|
213
|
+
* (1–20); `minInstances` clamps to 0..maxInstances (default 1). The template
|
|
214
|
+
* (`fields`) is capped at 12 children, drawn from a restricted v1 allowlist
|
|
215
|
+
* (see schema-validation's GROUP_CHILD_ALLOWED_KINDS) — notably NOT
|
|
216
|
+
* `repeating_group` itself (nesting is out of scope for v1) and NOT
|
|
217
|
+
* `calculated` (no per-instance calc plumbing yet). Child ids are unique only
|
|
218
|
+
* WITHIN the group (the select-option precedent, not the global block
|
|
219
|
+
* namespace), so the same child id can recur across two different groups.
|
|
220
|
+
*
|
|
221
|
+
* Value shape (locked, roadmap decision 4):
|
|
222
|
+
* `data[groupId]: GroupInstanceValue[]`, instances in respondent order. Key
|
|
223
|
+
* absent ⇔ zero instances (unanswered iff `minInstances` is 0).
|
|
224
|
+
*
|
|
225
|
+
* Scope walls (v1, strict): a child's `visibleIf` may reference ONLY
|
|
226
|
+
* same-group sibling ids, evaluated per instance over that instance's
|
|
227
|
+
* canonical values (see logic.ts's `visibleGroupChildren`). Nothing outside
|
|
228
|
+
* the group — top-level visibility, jumps, calc operands, piping — can
|
|
229
|
+
* reference a child id; both directions are schema hard errors.
|
|
230
|
+
*/
|
|
231
|
+
interface RepeatingGroupField extends BaseField {
|
|
232
|
+
kind: "repeating_group";
|
|
233
|
+
/** The repeated template. Child ids are unique WITHIN the group (the
|
|
234
|
+
* select-option precedent, not the global block namespace). */
|
|
235
|
+
fields: Field[];
|
|
236
|
+
/** Respondent-controlled instance count, clamped server-side. 0..maxInstances,
|
|
237
|
+
* default 1. */
|
|
238
|
+
minInstances?: number;
|
|
239
|
+
/** REQUIRED, 1..20 — bounded by design (also bounds the authoring-time
|
|
240
|
+
* worst-case payload-size estimate). */
|
|
241
|
+
maxInstances: number;
|
|
242
|
+
/** "Add another" default via strings. */
|
|
243
|
+
addLabel?: string;
|
|
244
|
+
/** Names one instance — "Guest" → headings "Guest 2 of 3", announcements,
|
|
245
|
+
* CSV column prefixes. Defaults to the field label. */
|
|
246
|
+
itemLabel?: string;
|
|
247
|
+
}
|
|
201
248
|
/**
|
|
202
249
|
* A field type Fillo doesn't ship. You define it in code and supply the
|
|
203
250
|
* renderer via the SDK's `customComponents` map, keyed by `component`. The
|
|
@@ -212,7 +259,7 @@ interface CustomField extends BaseField {
|
|
|
212
259
|
/** Arbitrary options handed to your component. */
|
|
213
260
|
config?: Record<string, unknown>;
|
|
214
261
|
}
|
|
215
|
-
type Field = TextField | PhoneField | NumberField | ChoiceField | CheckboxField | RatingField | LinearScaleField | RankingField | MatrixField | SignatureField | DateField | FileUploadField | HiddenField | CalculatedField | CustomField;
|
|
262
|
+
type Field = TextField | PhoneField | NumberField | ChoiceField | CheckboxField | RatingField | LinearScaleField | RankingField | MatrixField | SignatureField | DateField | FileUploadField | HiddenField | CalculatedField | RepeatingGroupField | CustomField;
|
|
216
263
|
interface HeadingBlock extends BaseBlock {
|
|
217
264
|
kind: "heading";
|
|
218
265
|
text: string;
|
|
@@ -389,6 +436,10 @@ type JsonValue = string | number | boolean | null | JsonValue[] | {
|
|
|
389
436
|
type FieldValue = string | number | boolean | string[] | FileValue[]
|
|
390
437
|
/** Matrix answers: rowId → columnId. */
|
|
391
438
|
| Record<string, string>
|
|
439
|
+
/** Repeating-group answers: one record per instance, in respondent order
|
|
440
|
+
* (roadmap decision 4). A NAMED member — never smuggled through
|
|
441
|
+
* JsonValue — so the discriminated union stays exhaustiveness-checkable. */
|
|
442
|
+
| GroupInstanceValue[]
|
|
392
443
|
/** Custom fields hold arbitrary JSON. */
|
|
393
444
|
| JsonValue | null | undefined;
|
|
394
445
|
type ResponseData = Record<string, FieldValue>;
|
|
@@ -491,6 +542,27 @@ declare function allFields(form: FormSchema): Field[];
|
|
|
491
542
|
declare function responseScopeValue(settings: FormSettings, data: ResponseData): string | null;
|
|
492
543
|
/** Fields currently visible given the response data. */
|
|
493
544
|
declare function visibleFields(form: FormSchema, data: ResponseData): Field[];
|
|
545
|
+
/**
|
|
546
|
+
* THE scoped per-instance visibility resolver for a repeating group's
|
|
547
|
+
* children (contract decisions 3 + 4): given ONE instance's values, the
|
|
548
|
+
* template children currently visible in that instance, in template order.
|
|
549
|
+
* Both renderers (which rows to draw in instance n) and validateResponse
|
|
550
|
+
* (which children to require/keep in instance n) consume THIS one helper —
|
|
551
|
+
* no second engine. It literally IS the shared engine: the children run
|
|
552
|
+
* through the same {@link resolveLogicState} fixpoint a page of top-level
|
|
553
|
+
* fields would, over the instance's values — so canonical-value discipline
|
|
554
|
+
* (trim/coerce/empty-drop before any condition reads a sibling) and
|
|
555
|
+
* hidden-sibling cascading (a hidden controller reads as unanswered) behave
|
|
556
|
+
* exactly like top level, one scope down. Scope walls are enforced at schema
|
|
557
|
+
* time: a child's visibleIf can only name same-group siblings, so the
|
|
558
|
+
* instance's own values are the complete condition universe — outer answers
|
|
559
|
+
* can never matter here, and child values never leak out (the group is one
|
|
560
|
+
* opaque field in the top-level fixpoint). No calc pass runs because
|
|
561
|
+
* `calculated` children are schema-rejected (decision 2). A logic-hidden
|
|
562
|
+
* child is unanswered for its siblings AND skipped by per-instance
|
|
563
|
+
* validation, mirroring the top-level discipline.
|
|
564
|
+
*/
|
|
565
|
+
declare function visibleGroupChildren(group: RepeatingGroupField, instanceValues: GroupInstanceValue): Field[];
|
|
494
566
|
/** Where navigation goes when leaving a page. */
|
|
495
567
|
type NextPage = {
|
|
496
568
|
to: string;
|
|
@@ -637,6 +709,16 @@ declare const FILLO_CHALLENGE_MIN_SDK_VERSION = "0.9.0";
|
|
|
637
709
|
* applicable floors).
|
|
638
710
|
*/
|
|
639
711
|
declare const FILLO_CALC_MIN_SDK_VERSION = "0.11.0";
|
|
712
|
+
/**
|
|
713
|
+
* The floor served INSTEAD of FILLO_MIN_SDK_VERSION for forms whose live
|
|
714
|
+
* schema contains a repeating_group field: the first release that ships the
|
|
715
|
+
* kind. Stricter than the calc case (recon risk 5): an old SDK's zod enum
|
|
716
|
+
* doesn't just strip one row, it drops the WHOLE templated section silently —
|
|
717
|
+
* worse than a blank row, because there is no visual trace a question ever
|
|
718
|
+
* existed. Non-negotiable; the form GET picks the max of the applicable
|
|
719
|
+
* floors (the Turnstile/calc precedent).
|
|
720
|
+
*/
|
|
721
|
+
declare const FILLO_GROUP_MIN_SDK_VERSION = "0.13.0";
|
|
640
722
|
declare function normalizeSettings(value: unknown): FormSettings;
|
|
641
723
|
interface SchemaValidationResult {
|
|
642
724
|
ok: boolean;
|
|
@@ -899,6 +981,14 @@ declare function createEmptyForm(title?: string): FormSchema;
|
|
|
899
981
|
* first two are code-only concerns and signature is too niche to draft well)
|
|
900
982
|
* and calculated (an LLM can't reliably wire a calc AST to stable field ids —
|
|
901
983
|
* owners add calculations deliberately, in the builder or in code).
|
|
984
|
+
* Also excludes repeating_group (contract decision 11): the flat FieldSpec →
|
|
985
|
+
* createBlock-defaults assembly has no nested-template channel, and a group
|
|
986
|
+
* needs structural choices an LLM gets wrong in ways the assembler can't
|
|
987
|
+
* repair — per-group-unique child ids, the v1 child-kind allowlist, and a
|
|
988
|
+
* deliberate maxInstances bound (required, 1–20). A malformed group is a
|
|
989
|
+
* schema HARD error, so a hallucinated one would brick the whole draft rather
|
|
990
|
+
* than degrade a field. Owners add groups deliberately, in the builder or in
|
|
991
|
+
* code — the calculated precedent.
|
|
902
992
|
*/
|
|
903
993
|
declare const DRAFT_KINDS: readonly BlockKind[];
|
|
904
994
|
/** The LLM-friendly intermediate shape. Everything optional but `kind`. */
|
|
@@ -1696,8 +1786,8 @@ declare function resolveThemeAppearance(theme: FormTheme | null): FormTheme | nu
|
|
|
1696
1786
|
/**
|
|
1697
1787
|
* The default English validation message for a required field left empty.
|
|
1698
1788
|
* `validateField` returns this exact sentinel (core has no strings context),
|
|
1699
|
-
* and
|
|
1700
|
-
*
|
|
1789
|
+
* and renderer layers swap it for field-aware/localized copy. Keep it
|
|
1790
|
+
* identical to `DEFAULT_FIELD_STRINGS.required`.
|
|
1701
1791
|
*/
|
|
1702
1792
|
declare const REQUIRED_FIELD_MESSAGE = "This field is required";
|
|
1703
1793
|
/**
|
|
@@ -1763,8 +1853,12 @@ declare const DEFAULT_STRINGS: FilloStrings;
|
|
|
1763
1853
|
* an exhaustively-enumerated, closed set.
|
|
1764
1854
|
*/
|
|
1765
1855
|
interface FilloFieldStrings {
|
|
1766
|
-
/**
|
|
1856
|
+
/** Generic/legacy validation copy for a required field left empty. Mirrors
|
|
1857
|
+
* REQUIRED_FIELD_MESSAGE and remains the fallback for custom/hidden fields. */
|
|
1767
1858
|
required: string;
|
|
1859
|
+
/** Optional field-aware required copy. Existing `required` overrides keep
|
|
1860
|
+
* their legacy generic behavior unless this function is also supplied. */
|
|
1861
|
+
requiredForField?: (field: Field) => string;
|
|
1768
1862
|
/** Notice when a spent/expired resume link couldn't restore progress. */
|
|
1769
1863
|
resumeLinkExpired: string;
|
|
1770
1864
|
/** Success-screen message when a verified identity re-submits and the form
|
|
@@ -1772,6 +1866,14 @@ interface FilloFieldStrings {
|
|
|
1772
1866
|
alreadyAnswered: string;
|
|
1773
1867
|
/** Retry control on a failed upload row. */
|
|
1774
1868
|
uploadRetry: string;
|
|
1869
|
+
/** Accessible action names for upload rows. The filename is appended by the renderer. */
|
|
1870
|
+
uploadCancel: string;
|
|
1871
|
+
uploadRemove: string;
|
|
1872
|
+
uploadDismiss: string;
|
|
1873
|
+
/** Visible secondary status for an upload in progress. */
|
|
1874
|
+
uploadingFile: (percent: number, size: string) => string;
|
|
1875
|
+
/** Visible secondary status for a completed upload. */
|
|
1876
|
+
uploadedFile: (size: string) => string;
|
|
1775
1877
|
/** Dropzone copy when uploads can't run because the form isn't connected. */
|
|
1776
1878
|
uploadsDisabled: string;
|
|
1777
1879
|
/** Dropzone copy for an explicit render-only preview, which never has transport. */
|
|
@@ -1797,11 +1899,21 @@ interface FilloFieldStrings {
|
|
|
1797
1899
|
/** Live-region announcement while a submit is in flight — for auto-submit
|
|
1798
1900
|
* forms, which have no footer/button to show `submitting` on. */
|
|
1799
1901
|
submittingAnnouncement: string;
|
|
1800
|
-
/**
|
|
1801
|
-
*
|
|
1902
|
+
/** @deprecated Retained for localization compatibility. Default renderers
|
|
1903
|
+
* now focus the first invalid control and show field-aware inline guidance. */
|
|
1802
1904
|
errorSummaryTitle: string;
|
|
1803
1905
|
/** Live-region announcement after a ranking move: "«label», position n of m". */
|
|
1804
1906
|
rankingPosition: (label: string, position: number, count: number) => string;
|
|
1907
|
+
/** Repeating group: the Add button's default label. */
|
|
1908
|
+
groupAdd: string;
|
|
1909
|
+
/** Repeating group instance heading — "«item» n of m". */
|
|
1910
|
+
groupInstanceLabel: (item: string, position: number, count: number) => string;
|
|
1911
|
+
/** Repeating group per-instance Remove button accessible label. */
|
|
1912
|
+
groupRemoveLabel: (item: string, position: number) => string;
|
|
1913
|
+
/** Live-region announcement after adding an instance. */
|
|
1914
|
+
groupInstanceAdded: (item: string, position: number, count: number) => string;
|
|
1915
|
+
/** Live-region announcement after removing an instance. */
|
|
1916
|
+
groupInstanceRemoved: (item: string, count: number) => string;
|
|
1805
1917
|
/** Live-region announcement once a phone country-picker selection commits
|
|
1806
1918
|
* (focus moves straight to the national input, so nothing else announces it). */
|
|
1807
1919
|
phoneCountrySelected: (name: string) => string;
|
|
@@ -1816,8 +1928,12 @@ interface FilloFieldStrings {
|
|
|
1816
1928
|
declare const DEFAULT_FIELD_STRINGS: FilloFieldStrings;
|
|
1817
1929
|
/** Everything the default renderers can localize — chrome + field/validation. */
|
|
1818
1930
|
type FilloRendererStrings = FilloStrings & FilloFieldStrings;
|
|
1931
|
+
/** Resolve the renderer copy for core's stable required-field sentinel. */
|
|
1932
|
+
declare function requiredFieldMessage(field: Field, strings: Pick<FilloFieldStrings, "required" | "requiredForField">): string;
|
|
1819
1933
|
/** Merge overrides over the built-in chrome + field defaults. Accepts a partial
|
|
1820
|
-
* of the full renderer surface so the `strings` prop can override either set.
|
|
1934
|
+
* of the full renderer surface so the `strings` prop can override either set.
|
|
1935
|
+
* A legacy generic `required` override disables the new field-aware default
|
|
1936
|
+
* unless the caller explicitly supplies `requiredForField` too. */
|
|
1821
1937
|
declare function resolveStrings(overrides?: Partial<FilloRendererStrings>): FilloRendererStrings;
|
|
1822
1938
|
|
|
1823
1939
|
/** Result of syncing a code-defined form: its canonical id, slug, and branding. */
|
|
@@ -1938,6 +2054,11 @@ interface BlockSpec {
|
|
|
1938
2054
|
optionChildren?: boolean;
|
|
1939
2055
|
/** String children become the `text` prop (heading/paragraph). */
|
|
1940
2056
|
textChildren?: boolean;
|
|
2057
|
+
/** Accepts Fillo FIELD elements as children, compiled into the `fields`
|
|
2058
|
+
* template (repeating groups). The walk's flatten/classify/buildProps trio
|
|
2059
|
+
* is reentrant, so children build through the exact same path as top-level
|
|
2060
|
+
* blocks. Child ids are per-GROUP (checked here), not global. */
|
|
2061
|
+
blockChildren?: boolean;
|
|
1941
2062
|
/** Content blocks carry no label/required/etc. */
|
|
1942
2063
|
content?: boolean;
|
|
1943
2064
|
}
|
|
@@ -2058,4 +2179,4 @@ declare class Sha1 {
|
|
|
2058
2179
|
}
|
|
2059
2180
|
declare const sha1Base64: (bytes: Uint8Array) => string;
|
|
2060
2181
|
|
|
2061
|
-
export { type AutoSubmitContext, BLOCK_KIND_META, type BaseField, type Block, type BlockKind, CONTENT_KINDS, type CalcExpr, type CalculatedField, type ChallengeConfig, type CheckboxField, type ChoiceField, type CodeForm, type Condition, type ConditionOp, type ContentBlock, type ContentKind, type CreatedDraft, type CustomField, DEFAULT_FIELD_STRINGS, DEFAULT_STRINGS, DRAFT_KINDS, type DateField, type DividerBlock, FILLO_CALC_MIN_SDK_VERSION, FILLO_CHALLENGE_MIN_SDK_VERSION, FILLO_DATA_ATTRS, FILLO_MIN_SDK_VERSION, FILLO_SCHEMA_VERSION, FILLO_SDK_VERSION, FILLO_SLOTS, FILLO_THEME_VARS, type Field, type FieldKind, type FieldSpec, type FieldValue, type FileUploadField, type FileValue, type FilloAppearance, FilloClient, type FilloClientOptions, FilloError, type FilloFieldStrings, FilloJsxError, type FilloRendererStrings, type FilloRespondent, type FilloSlot, type FilloStrings, type FormBranding, type FormController, type FormControllerOptions, type FormControllerState, type FormDraftSpec, type FormPage, type FormSchema, type FormSettings, type FormStatus, type FormTheme, type HeadingBlock, type HiddenField, JSX_BLOCK_COMPONENTS, JSX_BLOCK_SPECS, type JsonValue, type JsxFormMeta, type JumpRule, type LinearScaleField, type MatrixField, type NextPage, type NumberField, PHONE_COUNTRIES, PHONE_PICKER_COUNTRIES, PHONE_POPOVER_VIEWPORT_GAP, type ParagraphBlock, type ParsePhoneOptions, type ParsedPhone, type PhoneCountry, type PhoneField, type PhonePopoverPlacement, type ProvisionWorkspaceResult, type PublishedForm, REQUIRED_FIELD_MESSAGE, type RankingField, type RatingField, type ResponseData, type ResponseDraft, type ResponseLimit, type SchemaValidationResult, type SelectOption, Sha1, type SignatureField, type SlotClass, type SlotState, type SubmitMeta, type SubmitResult, type SyncFormResult, type SyncedForm, type TextField, type TrustPolicy, type UploadFileOptions, type UploadProgress, type UploadSession, type UploadStatus, type UploadTransport, type ValidationResult, type WhenBuilder, allFields, assembleForm, codeFormFromJsx, computeCalculated, conditionsMet, contentHash, countryByDialCode, countryByIso, countryByTimeZone, createBlock, createClient, createEmptyForm, createFormController, createId, defineForm, digitsOnly, evaluateCalc, flagEmoji, formSchemasEqual, formatAnswer, formatGroupedNumber, formatNational, isAutoSubmitBlock, isBlockVisible, isBuildTimeDevEnv, isCodeForm, isField, isFilloError, isLikelyDevEnv, isPossiblePhone, isTerminalPage, isValidPartialNumberText, localeForNotation, needsExplicitSubmit, normalizeFormSchema, normalizeFormTheme, normalizeSettings, parseGroupedNumber, parsePhone, pipeBlock, positionPhonePopover, prefillFromParams, provisionWorkspace, radioGroupStep, reachableFieldIds, reachableFields, reachablePageIds, reachablePageSequence, resolveNextPage, resolveSlotClass, resolveStrings, resolveText, resolveThemeAppearance, responseScopeValue, schemaFromJsx, sha1Base64, shouldAutoSubmit, slotClass, syncCodeForm, toE164, validateField, validateFormSchema, validateResponse, visibleBlocks, visibleFields, visiblePageBlocks, when };
|
|
2182
|
+
export { type AutoSubmitContext, BLOCK_KIND_META, type BaseField, type Block, type BlockKind, CONTENT_KINDS, type CalcExpr, type CalculatedField, type ChallengeConfig, type CheckboxField, type ChoiceField, type CodeForm, type Condition, type ConditionOp, type ContentBlock, type ContentKind, type CreatedDraft, type CustomField, DEFAULT_FIELD_STRINGS, DEFAULT_STRINGS, DRAFT_KINDS, type DateField, type DividerBlock, FILLO_CALC_MIN_SDK_VERSION, FILLO_CHALLENGE_MIN_SDK_VERSION, FILLO_DATA_ATTRS, FILLO_GROUP_MIN_SDK_VERSION, FILLO_MIN_SDK_VERSION, FILLO_SCHEMA_VERSION, FILLO_SDK_VERSION, FILLO_SLOTS, FILLO_THEME_VARS, type Field, type FieldKind, type FieldSpec, type FieldValue, type FileUploadField, type FileValue, type FilloAppearance, FilloClient, type FilloClientOptions, FilloError, type FilloFieldStrings, FilloJsxError, type FilloRendererStrings, type FilloRespondent, type FilloSlot, type FilloStrings, type FormBranding, type FormController, type FormControllerOptions, type FormControllerState, type FormDraftSpec, type FormPage, type FormSchema, type FormSettings, type FormStatus, type FormTheme, type GroupInstanceValue, type HeadingBlock, type HiddenField, JSX_BLOCK_COMPONENTS, JSX_BLOCK_SPECS, type JsonValue, type JsxFormMeta, type JumpRule, type LinearScaleField, type MatrixField, type NextPage, type NumberField, PHONE_COUNTRIES, PHONE_PICKER_COUNTRIES, PHONE_POPOVER_VIEWPORT_GAP, type ParagraphBlock, type ParsePhoneOptions, type ParsedPhone, type PhoneCountry, type PhoneField, type PhonePopoverPlacement, type ProvisionWorkspaceResult, type PublishedForm, REQUIRED_FIELD_MESSAGE, type RankingField, type RatingField, type RepeatingGroupField, type ResponseData, type ResponseDraft, type ResponseLimit, type SchemaValidationResult, type SelectOption, Sha1, type SignatureField, type SlotClass, type SlotState, type SubmitMeta, type SubmitResult, type SyncFormResult, type SyncedForm, type TextField, type TrustPolicy, type UploadFileOptions, type UploadProgress, type UploadSession, type UploadStatus, type UploadTransport, type ValidationResult, type WhenBuilder, allFields, assembleForm, codeFormFromJsx, computeCalculated, conditionsMet, contentHash, countryByDialCode, countryByIso, countryByTimeZone, createBlock, createClient, createEmptyForm, createFormController, createId, defineForm, digitsOnly, evaluateCalc, flagEmoji, formSchemasEqual, formatAnswer, formatGroupedNumber, formatNational, isAutoSubmitBlock, isBlockVisible, isBuildTimeDevEnv, isCodeForm, isField, isFilloError, isLikelyDevEnv, isPossiblePhone, isTerminalPage, isValidPartialNumberText, localeForNotation, needsExplicitSubmit, normalizeFormSchema, normalizeFormTheme, normalizeSettings, parseGroupedNumber, parsePhone, pipeBlock, positionPhonePopover, prefillFromParams, provisionWorkspace, radioGroupStep, reachableFieldIds, reachableFields, reachablePageIds, reachablePageSequence, requiredFieldMessage, resolveNextPage, resolveSlotClass, resolveStrings, resolveText, resolveThemeAppearance, responseScopeValue, schemaFromJsx, sha1Base64, shouldAutoSubmit, slotClass, syncCodeForm, toE164, validateField, validateFormSchema, validateResponse, visibleBlocks, visibleFields, visibleGroupChildren, visiblePageBlocks, when };
|
package/dist/index.js
CHANGED
|
@@ -13,9 +13,38 @@ function isEmpty(value) {
|
|
|
13
13
|
if (typeof value === "object") return Object.keys(value).length === 0;
|
|
14
14
|
return false;
|
|
15
15
|
}
|
|
16
|
+
function normalizeGroupValue(field2, value) {
|
|
17
|
+
if (!Array.isArray(value)) return void 0;
|
|
18
|
+
const childById = new Map(field2.fields.map((child) => [child.id, child]));
|
|
19
|
+
let changed = false;
|
|
20
|
+
const instances = value.map((instance) => {
|
|
21
|
+
if (!instance || typeof instance !== "object" || Array.isArray(instance)) return instance;
|
|
22
|
+
let instanceChanged = false;
|
|
23
|
+
const out = {};
|
|
24
|
+
for (const [key, raw] of Object.entries(instance)) {
|
|
25
|
+
const child = childById.get(key);
|
|
26
|
+
if (!child) {
|
|
27
|
+
instanceChanged = true;
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
const canonical = canonicalValue(child, raw);
|
|
31
|
+
if (canonical === void 0) {
|
|
32
|
+
instanceChanged = true;
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
out[key] = canonical;
|
|
36
|
+
if (canonical !== raw) instanceChanged = true;
|
|
37
|
+
}
|
|
38
|
+
if (!instanceChanged) return instance;
|
|
39
|
+
changed = true;
|
|
40
|
+
return out;
|
|
41
|
+
});
|
|
42
|
+
return changed ? instances : value;
|
|
43
|
+
}
|
|
16
44
|
function normalizeValue(field2, value) {
|
|
17
45
|
if (value === void 0 || value === null) return value;
|
|
18
46
|
if (field2.kind === "custom") return value;
|
|
47
|
+
if (field2.kind === "repeating_group") return normalizeGroupValue(field2, value);
|
|
19
48
|
if (field2.kind === "number" && typeof value === "string" && value.trim() !== "") {
|
|
20
49
|
const n = Number(value);
|
|
21
50
|
return Number.isNaN(n) ? value : n;
|
|
@@ -190,7 +219,12 @@ function resolveLogicState(fields, data) {
|
|
|
190
219
|
for (const f of fields) {
|
|
191
220
|
if (f.kind === "calculated") continue;
|
|
192
221
|
const value = canonicalValue(f, data[f.id]);
|
|
193
|
-
if (value
|
|
222
|
+
if (value === void 0) continue;
|
|
223
|
+
if (f.kind === "repeating_group") {
|
|
224
|
+
const count = Array.isArray(value) ? value.length : 0;
|
|
225
|
+
if (count < Math.max(1, f.minInstances ?? 1)) continue;
|
|
226
|
+
}
|
|
227
|
+
canonical[f.id] = value;
|
|
194
228
|
}
|
|
195
229
|
let visible = scoped;
|
|
196
230
|
let calc = {};
|
|
@@ -257,6 +291,11 @@ function visibleFields(form, data) {
|
|
|
257
291
|
const visible = visibleFieldIds(fields, data);
|
|
258
292
|
return fields.filter((f) => visible.has(f.id));
|
|
259
293
|
}
|
|
294
|
+
function visibleGroupChildren(group, instanceValues) {
|
|
295
|
+
const values = instanceValues && typeof instanceValues === "object" && !Array.isArray(instanceValues) ? instanceValues : {};
|
|
296
|
+
const state = resolveLogicState(group.fields, values);
|
|
297
|
+
return group.fields.filter((f) => state.visible.has(f.id));
|
|
298
|
+
}
|
|
260
299
|
function formResolver(form, data) {
|
|
261
300
|
return resolveLogicState(allFields(form), data).resolve;
|
|
262
301
|
}
|
|
@@ -707,11 +746,39 @@ var DEFAULT_STRINGS = {
|
|
|
707
746
|
resumeStartOver: "Start over",
|
|
708
747
|
challengeUnavailable: "The verification check couldn't load. Refresh the page, or check that challenges.cloudflare.com isn't blocked."
|
|
709
748
|
};
|
|
749
|
+
var REQUIRED_BY_KIND = {
|
|
750
|
+
short_text: "Enter your answer",
|
|
751
|
+
long_text: "Enter your answer",
|
|
752
|
+
email: "Enter an email address",
|
|
753
|
+
url: "Enter a URL",
|
|
754
|
+
phone: "Enter a phone number",
|
|
755
|
+
number: "Enter a number",
|
|
756
|
+
select: "Choose an option",
|
|
757
|
+
dropdown: "Choose an option",
|
|
758
|
+
multi_select: "Select at least one option",
|
|
759
|
+
checkbox: "Select this checkbox",
|
|
760
|
+
rating: "Choose a rating",
|
|
761
|
+
linear_scale: "Choose a value",
|
|
762
|
+
ranking: "Rank every option",
|
|
763
|
+
matrix: "Answer every row",
|
|
764
|
+
signature: "Add your signature",
|
|
765
|
+
date: "Enter a date",
|
|
766
|
+
file_upload: "Add a file"
|
|
767
|
+
};
|
|
768
|
+
function defaultRequiredForField(field2) {
|
|
769
|
+
return REQUIRED_BY_KIND[field2.kind] ?? REQUIRED_FIELD_MESSAGE;
|
|
770
|
+
}
|
|
710
771
|
var DEFAULT_FIELD_STRINGS = {
|
|
711
772
|
required: REQUIRED_FIELD_MESSAGE,
|
|
773
|
+
requiredForField: defaultRequiredForField,
|
|
712
774
|
resumeLinkExpired: "That saved-progress link expired or was already used \u2014 start again below.",
|
|
713
775
|
alreadyAnswered: "You've already answered this form.",
|
|
714
776
|
uploadRetry: "Retry",
|
|
777
|
+
uploadCancel: "Cancel",
|
|
778
|
+
uploadRemove: "Remove",
|
|
779
|
+
uploadDismiss: "Dismiss",
|
|
780
|
+
uploadingFile: (percent, size) => `Uploading \xB7 ${percent}% \xB7 ${size}`,
|
|
781
|
+
uploadedFile: (size) => `Uploaded \xB7 ${size}`,
|
|
715
782
|
uploadsDisabled: "Connect this form to Fillo to enable uploads",
|
|
716
783
|
uploadsRenderOnly: "Uploads are unavailable in this render-only preview",
|
|
717
784
|
uploadsUnavailable: "Uploads are temporarily unavailable",
|
|
@@ -724,15 +791,27 @@ var DEFAULT_FIELD_STRINGS = {
|
|
|
724
791
|
uploadsFailed: (count) => `${count} ${count === 1 ? "upload" : "uploads"} failed.`,
|
|
725
792
|
filesUploaded: (count) => `${count} ${count === 1 ? "file" : "files"} uploaded.`,
|
|
726
793
|
submittingAnnouncement: "Submitting your response\u2026",
|
|
727
|
-
errorSummaryTitle: "
|
|
794
|
+
errorSummaryTitle: "Check these fields",
|
|
728
795
|
rankingPosition: (label, position, count) => `${label}, position ${position} of ${count}`,
|
|
796
|
+
groupAdd: "Add another",
|
|
797
|
+
groupInstanceLabel: (item, position, count) => `${item} ${position} of ${count}`,
|
|
798
|
+
groupRemoveLabel: (item, position) => `Remove ${item} ${position}`,
|
|
799
|
+
groupInstanceAdded: (item, position, count) => `${item} ${position} added, ${position} of ${count}`,
|
|
800
|
+
groupInstanceRemoved: (item, count) => count === 0 ? `${item} removed, none left` : `${item} removed, ${count} remaining`,
|
|
729
801
|
phoneCountrySelected: (name) => `${name} selected`,
|
|
730
802
|
phoneResultsCount: (count) => `${count} ${count === 1 ? "result" : "results"}`,
|
|
731
803
|
signatureEmpty: "No signature yet",
|
|
732
804
|
signatureSigned: "Signature saved"
|
|
733
805
|
};
|
|
806
|
+
function requiredFieldMessage(field2, strings) {
|
|
807
|
+
return strings.requiredForField?.(field2) ?? strings.required;
|
|
808
|
+
}
|
|
734
809
|
function resolveStrings(overrides) {
|
|
735
|
-
|
|
810
|
+
const resolved = overrides ? { ...DEFAULT_STRINGS, ...DEFAULT_FIELD_STRINGS, ...overrides } : { ...DEFAULT_STRINGS, ...DEFAULT_FIELD_STRINGS };
|
|
811
|
+
if (overrides && Object.prototype.hasOwnProperty.call(overrides, "required") && !Object.prototype.hasOwnProperty.call(overrides, "requiredForField")) {
|
|
812
|
+
delete resolved.requiredForField;
|
|
813
|
+
}
|
|
814
|
+
return resolved;
|
|
736
815
|
}
|
|
737
816
|
|
|
738
817
|
// src/validation.ts
|
|
@@ -743,9 +822,17 @@ var fileValueSchema = z.object({
|
|
|
743
822
|
mime: z.string(),
|
|
744
823
|
url: z.optional(z.string())
|
|
745
824
|
});
|
|
825
|
+
function groupCountError(field2, count) {
|
|
826
|
+
const min = Math.max(0, field2.minInstances ?? 1);
|
|
827
|
+
if (count < min) return `Add at least ${min} ${min === 1 ? "entry" : "entries"}`;
|
|
828
|
+
if (Number.isFinite(field2.maxInstances) && count > field2.maxInstances)
|
|
829
|
+
return `At most ${field2.maxInstances} ${field2.maxInstances === 1 ? "entry" : "entries"}`;
|
|
830
|
+
return null;
|
|
831
|
+
}
|
|
746
832
|
function validateField(field2, value) {
|
|
747
833
|
if (isEmpty(value)) {
|
|
748
834
|
if (field2.kind === "calculated") return null;
|
|
835
|
+
if (field2.kind === "repeating_group") return groupCountError(field2, 0);
|
|
749
836
|
return field2.required ? REQUIRED_FIELD_MESSAGE : null;
|
|
750
837
|
}
|
|
751
838
|
switch (field2.kind) {
|
|
@@ -865,6 +952,17 @@ function validateField(field2, value) {
|
|
|
865
952
|
case "calculated": {
|
|
866
953
|
return null;
|
|
867
954
|
}
|
|
955
|
+
case "repeating_group": {
|
|
956
|
+
if (!Array.isArray(value)) {
|
|
957
|
+
return groupCountError(field2, 0);
|
|
958
|
+
}
|
|
959
|
+
const instances = value;
|
|
960
|
+
for (const instance of instances) {
|
|
961
|
+
if (!instance || typeof instance !== "object" || Array.isArray(instance))
|
|
962
|
+
return "Invalid value";
|
|
963
|
+
}
|
|
964
|
+
return groupCountError(field2, instances.length);
|
|
965
|
+
}
|
|
868
966
|
case "custom": {
|
|
869
967
|
return null;
|
|
870
968
|
}
|
|
@@ -894,6 +992,10 @@ function validateResponse(form, data) {
|
|
|
894
992
|
for (const field2 of fields) {
|
|
895
993
|
if (field2.kind === "calculated") continue;
|
|
896
994
|
const value = normalizeValue(field2, data[field2.id]);
|
|
995
|
+
if (field2.kind === "repeating_group") {
|
|
996
|
+
validateGroupResponse(field2, value, errors, cleaned);
|
|
997
|
+
continue;
|
|
998
|
+
}
|
|
897
999
|
const error = validateField(field2, value);
|
|
898
1000
|
if (error) {
|
|
899
1001
|
errors[field2.id] = error;
|
|
@@ -903,6 +1005,29 @@ function validateResponse(form, data) {
|
|
|
903
1005
|
}
|
|
904
1006
|
return { ok: Object.keys(errors).length === 0, errors, data: cleaned };
|
|
905
1007
|
}
|
|
1008
|
+
function validateGroupResponse(field2, value, errors, cleaned) {
|
|
1009
|
+
const groupError = validateField(field2, value);
|
|
1010
|
+
if (groupError) errors[field2.id] = groupError;
|
|
1011
|
+
if (!Array.isArray(value)) return;
|
|
1012
|
+
const instances = value;
|
|
1013
|
+
const kept = [];
|
|
1014
|
+
for (let index = 0; index < instances.length; index++) {
|
|
1015
|
+
const instance = instances[index];
|
|
1016
|
+
if (!instance || typeof instance !== "object" || Array.isArray(instance)) continue;
|
|
1017
|
+
const keptInstance = {};
|
|
1018
|
+
for (const child of visibleGroupChildren(field2, instance)) {
|
|
1019
|
+
const childValue = instance[child.id];
|
|
1020
|
+
const childError = validateField(child, childValue);
|
|
1021
|
+
if (childError) {
|
|
1022
|
+
errors[`${field2.id}.${index}.${child.id}`] = childError;
|
|
1023
|
+
} else if (!isEmpty(childValue)) {
|
|
1024
|
+
keptInstance[child.id] = childValue;
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
kept.push(keptInstance);
|
|
1028
|
+
}
|
|
1029
|
+
if (!groupError && kept.length > 0) cleaned[field2.id] = kept;
|
|
1030
|
+
}
|
|
906
1031
|
|
|
907
1032
|
// src/schema-validation.ts
|
|
908
1033
|
import * as z2 from "zod/mini";
|
|
@@ -926,6 +1051,7 @@ var FIELD_KINDS = [
|
|
|
926
1051
|
"file_upload",
|
|
927
1052
|
"hidden",
|
|
928
1053
|
"calculated",
|
|
1054
|
+
"repeating_group",
|
|
929
1055
|
"custom"
|
|
930
1056
|
];
|
|
931
1057
|
var CONTENT_KINDS2 = ["heading", "paragraph", "divider"];
|
|
@@ -962,10 +1088,11 @@ var schemaShape = z2.object({
|
|
|
962
1088
|
});
|
|
963
1089
|
var MAX_SCHEMA_VERSION = 1;
|
|
964
1090
|
var FILLO_SCHEMA_VERSION = 1;
|
|
965
|
-
var FILLO_SDK_VERSION = true ? "0.
|
|
1091
|
+
var FILLO_SDK_VERSION = true ? "0.13.0" : "0.0.0-dev";
|
|
966
1092
|
var FILLO_MIN_SDK_VERSION = "0.4.0";
|
|
967
1093
|
var FILLO_CHALLENGE_MIN_SDK_VERSION = "0.9.0";
|
|
968
1094
|
var FILLO_CALC_MIN_SDK_VERSION = "0.11.0";
|
|
1095
|
+
var FILLO_GROUP_MIN_SDK_VERSION = "0.13.0";
|
|
969
1096
|
function str(value, max, fallback = "") {
|
|
970
1097
|
return typeof value === "string" ? value.trim().slice(0, max) : fallback;
|
|
971
1098
|
}
|
|
@@ -1135,7 +1262,7 @@ function collectCalcRefs(expr, valueRefs, whenRefs) {
|
|
|
1135
1262
|
return;
|
|
1136
1263
|
}
|
|
1137
1264
|
}
|
|
1138
|
-
function validateCalculatedFields(pages) {
|
|
1265
|
+
function validateCalculatedFields(pages, childToGroup) {
|
|
1139
1266
|
const kinds = /* @__PURE__ */ new Map();
|
|
1140
1267
|
for (const page of pages) {
|
|
1141
1268
|
for (const block of page.blocks) if (isField(block)) kinds.set(block.id, block.kind);
|
|
@@ -1155,6 +1282,10 @@ function validateCalculatedFields(pages) {
|
|
|
1155
1282
|
for (const ref of valueRefs) {
|
|
1156
1283
|
const kind = kinds.get(ref);
|
|
1157
1284
|
if (kind === void 0) {
|
|
1285
|
+
const owningGroup = childToGroup.get(ref);
|
|
1286
|
+
if (owningGroup !== void 0) {
|
|
1287
|
+
return `Calculated field ${block.id} references ${ref}, which is a child of repeating group ${owningGroup} \u2014 calculations can't reach inside a repeating group (calc operands must be top-level fields)`;
|
|
1288
|
+
}
|
|
1158
1289
|
return `Calculated field ${block.id} references a missing field: ${ref} \u2014 remove the reference or restore that field`;
|
|
1159
1290
|
}
|
|
1160
1291
|
if (!CALC_OPERAND_KINDS.includes(kind)) {
|
|
@@ -1228,6 +1359,151 @@ function calcComplexityError(pages) {
|
|
|
1228
1359
|
}
|
|
1229
1360
|
return null;
|
|
1230
1361
|
}
|
|
1362
|
+
var GROUP_CHILD_ALLOWED_KINDS = [
|
|
1363
|
+
"short_text",
|
|
1364
|
+
"long_text",
|
|
1365
|
+
"email",
|
|
1366
|
+
"url",
|
|
1367
|
+
"phone",
|
|
1368
|
+
"number",
|
|
1369
|
+
"select",
|
|
1370
|
+
"multi_select",
|
|
1371
|
+
"dropdown",
|
|
1372
|
+
"checkbox",
|
|
1373
|
+
"date",
|
|
1374
|
+
"rating",
|
|
1375
|
+
"linear_scale"
|
|
1376
|
+
];
|
|
1377
|
+
var MAX_GROUP_TEMPLATE_CHILDREN = 12;
|
|
1378
|
+
var MAX_GROUP_ESTIMATED_BYTES = 18e4;
|
|
1379
|
+
function worstCaseChildBytes(child) {
|
|
1380
|
+
const overhead = child.id.length + 20;
|
|
1381
|
+
switch (child.kind) {
|
|
1382
|
+
case "short_text":
|
|
1383
|
+
case "email":
|
|
1384
|
+
case "url":
|
|
1385
|
+
return overhead + (child.maxLength ?? 2e3);
|
|
1386
|
+
case "long_text":
|
|
1387
|
+
return overhead + (child.maxLength ?? 2e4);
|
|
1388
|
+
case "phone":
|
|
1389
|
+
return overhead + 32;
|
|
1390
|
+
case "number":
|
|
1391
|
+
return overhead + 32;
|
|
1392
|
+
case "select":
|
|
1393
|
+
case "dropdown": {
|
|
1394
|
+
const longestOption = child.options.reduce((max, o) => Math.max(max, o.id.length), 0);
|
|
1395
|
+
return overhead + Math.max(longestOption, child.allowOther ? 500 : 0);
|
|
1396
|
+
}
|
|
1397
|
+
case "multi_select": {
|
|
1398
|
+
const everyOption = child.options.reduce((sum, o) => sum + o.id.length + 3, 0);
|
|
1399
|
+
return overhead + everyOption + (child.allowOther ? 500 : 0);
|
|
1400
|
+
}
|
|
1401
|
+
case "checkbox":
|
|
1402
|
+
return overhead + 8;
|
|
1403
|
+
case "date":
|
|
1404
|
+
return overhead + 16;
|
|
1405
|
+
case "rating":
|
|
1406
|
+
case "linear_scale":
|
|
1407
|
+
return overhead + 8;
|
|
1408
|
+
default:
|
|
1409
|
+
return overhead + 64;
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
function groupSizeEstimate(field2) {
|
|
1413
|
+
const perInstance = field2.fields.reduce((sum, child) => sum + worstCaseChildBytes(child), 24);
|
|
1414
|
+
return perInstance * (Number.isFinite(field2.maxInstances) ? field2.maxInstances : 0);
|
|
1415
|
+
}
|
|
1416
|
+
function validateRepeatingGroups(pages) {
|
|
1417
|
+
for (const page of pages) {
|
|
1418
|
+
for (const block of page.blocks) {
|
|
1419
|
+
if (block.kind !== "repeating_group") continue;
|
|
1420
|
+
if (!Number.isInteger(block.maxInstances) || block.maxInstances < 1 || block.maxInstances > 20) {
|
|
1421
|
+
return `Repeating group ${block.id} needs maxInstances to be a whole number between 1 and 20`;
|
|
1422
|
+
}
|
|
1423
|
+
const minInstances = block.minInstances ?? 1;
|
|
1424
|
+
if (!Number.isInteger(minInstances) || minInstances < 0 || minInstances > block.maxInstances) {
|
|
1425
|
+
return `Repeating group ${block.id} needs minInstances to be a whole number between 0 and maxInstances (${block.maxInstances})`;
|
|
1426
|
+
}
|
|
1427
|
+
block.minInstances = minInstances;
|
|
1428
|
+
if (block.fields.length === 0) {
|
|
1429
|
+
return `Repeating group ${block.id} has no template fields \u2014 add at least one field to repeat`;
|
|
1430
|
+
}
|
|
1431
|
+
if (block.fields.length > MAX_GROUP_TEMPLATE_CHILDREN) {
|
|
1432
|
+
return `Repeating group ${block.id} has ${block.fields.length} template fields \u2014 the maximum is ${MAX_GROUP_TEMPLATE_CHILDREN}; split it into fewer fields`;
|
|
1433
|
+
}
|
|
1434
|
+
for (const child of block.fields) {
|
|
1435
|
+
if (!GROUP_CHILD_ALLOWED_KINDS.includes(child.kind)) {
|
|
1436
|
+
return `Repeating group ${block.id} has a "${child.kind}" child (${child.id}) \u2014 allowed child kinds are ${GROUP_CHILD_ALLOWED_KINDS.join(", ")}`;
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
const duplicateChild = duplicateOptionId(block.fields);
|
|
1440
|
+
if (duplicateChild) {
|
|
1441
|
+
return `Duplicate child id in repeating group ${block.id}: ${duplicateChild}`;
|
|
1442
|
+
}
|
|
1443
|
+
const siblingIds = new Set(block.fields.map((f) => f.id));
|
|
1444
|
+
for (const child of block.fields) {
|
|
1445
|
+
for (const cond of child.visibleIf ?? []) {
|
|
1446
|
+
if (!siblingIds.has(cond.fieldId)) {
|
|
1447
|
+
return `Field ${child.id} in repeating group ${block.id} has a visibleIf referencing "${cond.fieldId}" \u2014 a child's visibleIf can only reference another field in the SAME group`;
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
const estimate = groupSizeEstimate(block);
|
|
1452
|
+
if (estimate > MAX_GROUP_ESTIMATED_BYTES) {
|
|
1453
|
+
return `Repeating group ${block.id} could reach ~${Math.ceil(estimate / 1e3)}KB at ${block.maxInstances} instances \u2014 the maximum is ~${Math.round(MAX_GROUP_ESTIMATED_BYTES / 1e3)}KB; lower maxInstances, shorten text limits, or trim the template`;
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
return null;
|
|
1458
|
+
}
|
|
1459
|
+
function collectGroupChildIds(pages) {
|
|
1460
|
+
const topLevelIds = /* @__PURE__ */ new Set();
|
|
1461
|
+
for (const page of pages) {
|
|
1462
|
+
for (const block of page.blocks) if (isField(block)) topLevelIds.add(block.id);
|
|
1463
|
+
}
|
|
1464
|
+
const childToGroup = /* @__PURE__ */ new Map();
|
|
1465
|
+
for (const page of pages) {
|
|
1466
|
+
for (const block of page.blocks) {
|
|
1467
|
+
if (block.kind !== "repeating_group") continue;
|
|
1468
|
+
for (const child of block.fields) {
|
|
1469
|
+
if (!topLevelIds.has(child.id) && !childToGroup.has(child.id)) {
|
|
1470
|
+
childToGroup.set(child.id, block.id);
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
return childToGroup;
|
|
1476
|
+
}
|
|
1477
|
+
function groupScopeWallError(pages, rawPages, childToGroup) {
|
|
1478
|
+
for (const page of pages) {
|
|
1479
|
+
for (const block of page.blocks) {
|
|
1480
|
+
for (const cond of block.visibleIf ?? []) {
|
|
1481
|
+
const owner = childToGroup.get(cond.fieldId);
|
|
1482
|
+
if (owner !== void 0) {
|
|
1483
|
+
return `Field ${block.id}'s visibleIf references "${cond.fieldId}", which is a child of repeating group ${owner} \u2014 fields outside a repeating group can't reference its children`;
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
for (const page of rawPages) {
|
|
1489
|
+
if (!Array.isArray(page.next)) continue;
|
|
1490
|
+
for (const rule of page.next) {
|
|
1491
|
+
if (!rule || typeof rule !== "object") continue;
|
|
1492
|
+
const when2 = rule.when;
|
|
1493
|
+
if (!Array.isArray(when2)) continue;
|
|
1494
|
+
for (const cond of when2) {
|
|
1495
|
+
if (!cond || typeof cond !== "object") continue;
|
|
1496
|
+
const fieldId = cond.fieldId;
|
|
1497
|
+
if (typeof fieldId !== "string") continue;
|
|
1498
|
+
const owner = childToGroup.get(fieldId);
|
|
1499
|
+
if (owner !== void 0) {
|
|
1500
|
+
return `A jump rule on page ${page.id} references "${fieldId}", which is a child of repeating group ${owner} \u2014 jumps can't reference a repeating group's children`;
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
return null;
|
|
1506
|
+
}
|
|
1231
1507
|
function baseBlock(rec) {
|
|
1232
1508
|
return {
|
|
1233
1509
|
id: str(rec.id, 128),
|
|
@@ -1441,6 +1717,33 @@ function normalizeBlock(input) {
|
|
|
1441
1717
|
return item ? [item] : [];
|
|
1442
1718
|
}).slice(0, 50) : void 0
|
|
1443
1719
|
});
|
|
1720
|
+
case "repeating_group": {
|
|
1721
|
+
const rawFields = Array.isArray(rec.fields) ? rec.fields : [];
|
|
1722
|
+
const fields = rawFields.slice(0, 50).flatMap((raw) => {
|
|
1723
|
+
const child = normalizeBlock(raw);
|
|
1724
|
+
return child && isField(child) ? [child] : [];
|
|
1725
|
+
});
|
|
1726
|
+
return cleanObject({
|
|
1727
|
+
...baseField(rec, kind),
|
|
1728
|
+
// A container's OWN completeness is governed by minInstances (0 is a
|
|
1729
|
+
// legitimate "this group is optional"), not the generic required
|
|
1730
|
+
// flag — a separately-true `required` would be redundant at best and
|
|
1731
|
+
// self-contradictory at worst (the hidden/calculated precedent).
|
|
1732
|
+
required: false,
|
|
1733
|
+
fields,
|
|
1734
|
+
// Bounds become authoritative in validateRepeatingGroups as HARD
|
|
1735
|
+
// ERRORS, not silent clamps (unlike rating.max/linear_scale.min-max)
|
|
1736
|
+
// — instances are respondent-controlled wire shape, not cosmetic
|
|
1737
|
+
// display config. maxInstances is schema-REQUIRED, so a missing/
|
|
1738
|
+
// non-finite raw value becomes NaN here (fails every bounds check on
|
|
1739
|
+
// its own) rather than a fabricated fallback the post-pass would
|
|
1740
|
+
// have to override anyway.
|
|
1741
|
+
maxInstances: finite(rec.maxInstances) ?? Number.NaN,
|
|
1742
|
+
minInstances: finite(rec.minInstances),
|
|
1743
|
+
addLabel: optionalStr(rec.addLabel, 100),
|
|
1744
|
+
itemLabel: optionalStr(rec.itemLabel, 100)
|
|
1745
|
+
});
|
|
1746
|
+
}
|
|
1444
1747
|
case "custom":
|
|
1445
1748
|
return cleanObject({
|
|
1446
1749
|
...baseField(rec, kind),
|
|
@@ -1529,7 +1832,12 @@ function normalizeFormSchema(input) {
|
|
|
1529
1832
|
}
|
|
1530
1833
|
}
|
|
1531
1834
|
}
|
|
1532
|
-
const
|
|
1835
|
+
const groupError = validateRepeatingGroups(pages);
|
|
1836
|
+
if (groupError) return { ok: false, error: groupError };
|
|
1837
|
+
const childToGroup = collectGroupChildIds(pages);
|
|
1838
|
+
const wallError = groupScopeWallError(pages, rebuilt, childToGroup);
|
|
1839
|
+
if (wallError) return { ok: false, error: wallError };
|
|
1840
|
+
const calcError = validateCalculatedFields(pages, childToGroup);
|
|
1533
1841
|
if (calcError) return { ok: false, error: calcError };
|
|
1534
1842
|
const calcComplexity = calcComplexityError(pages);
|
|
1535
1843
|
if (calcComplexity) return { ok: false, error: calcComplexity };
|
|
@@ -1618,6 +1926,19 @@ function formatAnswer(field2, value) {
|
|
|
1618
1926
|
const text = field2.decimals === void 0 ? String(n) : n.toFixed(Math.max(0, Math.min(6, field2.decimals)));
|
|
1619
1927
|
return `${field2.prefix ?? ""}${text}${field2.suffix ?? ""}`;
|
|
1620
1928
|
}
|
|
1929
|
+
case "repeating_group": {
|
|
1930
|
+
if (!Array.isArray(value)) return String(value);
|
|
1931
|
+
const instances = value;
|
|
1932
|
+
if (instances.length === 0) return "";
|
|
1933
|
+
const noun = field2.itemLabel ?? field2.label;
|
|
1934
|
+
const summary = `${instances.length} \xD7 ${noun}`;
|
|
1935
|
+
const firstChild = field2.fields[0];
|
|
1936
|
+
if (!firstChild) return summary;
|
|
1937
|
+
const previews = instances.map(
|
|
1938
|
+
(instance) => instance && typeof instance === "object" && !Array.isArray(instance) ? formatAnswer(firstChild, instance[firstChild.id]) : ""
|
|
1939
|
+
).filter((text) => text !== "");
|
|
1940
|
+
return previews.length > 0 ? `${summary}: ${previews.join(", ")}` : summary;
|
|
1941
|
+
}
|
|
1621
1942
|
case "phone": {
|
|
1622
1943
|
const { country, national, e164 } = parsePhone(String(value));
|
|
1623
1944
|
if (!e164) return String(value);
|
|
@@ -1780,6 +2101,7 @@ var BLOCK_KIND_META = {
|
|
|
1780
2101
|
file_upload: { label: "File upload", hint: "Direct to connected storage" },
|
|
1781
2102
|
hidden: { label: "Hidden field", hint: "Filled from the URL" },
|
|
1782
2103
|
calculated: { label: "Calculated value", hint: "Computed from other answers" },
|
|
2104
|
+
repeating_group: { label: "Repeating group", hint: "A set of fields respondents can repeat" },
|
|
1783
2105
|
custom: { label: "Custom", hint: "Your own component (code only)" },
|
|
1784
2106
|
heading: { label: "Heading", hint: "Section title" },
|
|
1785
2107
|
paragraph: { label: "Paragraph", hint: "Explanatory text" },
|
|
@@ -1851,6 +2173,14 @@ function createBlock(kind) {
|
|
|
1851
2173
|
return { id, kind, label: "Hidden field" };
|
|
1852
2174
|
case "calculated":
|
|
1853
2175
|
return { id, kind, label: "Calculated value", calc: { op: "const", value: 0 } };
|
|
2176
|
+
case "repeating_group":
|
|
2177
|
+
return {
|
|
2178
|
+
id,
|
|
2179
|
+
kind,
|
|
2180
|
+
label: "Repeating group",
|
|
2181
|
+
maxInstances: 5,
|
|
2182
|
+
fields: [{ id: createId(6), kind: "short_text", label: "Short text" }]
|
|
2183
|
+
};
|
|
1854
2184
|
case "custom":
|
|
1855
2185
|
return { id, kind, label: "Custom field", component: "" };
|
|
1856
2186
|
case "file_upload":
|
|
@@ -2884,6 +3214,8 @@ function prefillFromParams(form, params) {
|
|
|
2884
3214
|
case "calculated":
|
|
2885
3215
|
break;
|
|
2886
3216
|
// engine-computed, never prefillable — a crafted URL can't seed it
|
|
3217
|
+
case "repeating_group":
|
|
3218
|
+
break;
|
|
2887
3219
|
default:
|
|
2888
3220
|
data[field2.id] = raw;
|
|
2889
3221
|
}
|
|
@@ -3312,15 +3644,24 @@ function createFormController(options2) {
|
|
|
3312
3644
|
data = computeCalculated(form, { ...data, [fieldId]: value });
|
|
3313
3645
|
onChange?.(data);
|
|
3314
3646
|
submitError = void 0;
|
|
3315
|
-
|
|
3316
|
-
|
|
3647
|
+
const prefix = `${fieldId}.`;
|
|
3648
|
+
if (fieldId in errors || Object.keys(errors).some((k) => k.startsWith(prefix))) {
|
|
3649
|
+
errors = Object.fromEntries(
|
|
3650
|
+
Object.entries(errors).filter(([k]) => k !== fieldId && !k.startsWith(prefix))
|
|
3651
|
+
);
|
|
3317
3652
|
}
|
|
3318
3653
|
notify();
|
|
3319
3654
|
scheduleDraftSave();
|
|
3320
3655
|
}
|
|
3321
3656
|
function setUploading(fieldId, busy) {
|
|
3322
|
-
if (busy)
|
|
3323
|
-
|
|
3657
|
+
if (busy) {
|
|
3658
|
+
uploadingFields.add(fieldId);
|
|
3659
|
+
if (fieldId in errors) {
|
|
3660
|
+
errors = Object.fromEntries(Object.entries(errors).filter(([k]) => k !== fieldId));
|
|
3661
|
+
}
|
|
3662
|
+
} else {
|
|
3663
|
+
uploadingFields.delete(fieldId);
|
|
3664
|
+
}
|
|
3324
3665
|
notify();
|
|
3325
3666
|
}
|
|
3326
3667
|
function currentSeqPosition(seq) {
|
|
@@ -3820,12 +4161,16 @@ function syncCodeForm(client, form, opts) {
|
|
|
3820
4161
|
const content2 = JSON.stringify(form.schema) + JSON.stringify(form.theme ?? null);
|
|
3821
4162
|
const hash = contentHash(content2);
|
|
3822
4163
|
const key = `${client.baseUrl}:${client.key}:${form.id}:${hash}`;
|
|
4164
|
+
const revalidateUploadState = allFields(form.schema).some(
|
|
4165
|
+
(field2) => field2.kind === "file_upload"
|
|
4166
|
+
);
|
|
3823
4167
|
const cached = syncCache.get(key);
|
|
3824
4168
|
if (cached && !opts?.bypassCache) {
|
|
3825
|
-
if (cached.expiresAt === null || cached.expiresAt > Date.now())
|
|
4169
|
+
if (cached.expiresAt === null || !revalidateUploadState && cached.expiresAt > Date.now())
|
|
4170
|
+
return cached.promise;
|
|
3826
4171
|
syncCache.delete(key);
|
|
3827
4172
|
}
|
|
3828
|
-
if (!opts?.bypassCache) {
|
|
4173
|
+
if (!opts?.bypassCache && !revalidateUploadState) {
|
|
3829
4174
|
const stored = readStoredSync(client, form.id, hash);
|
|
3830
4175
|
if (stored) {
|
|
3831
4176
|
const promise2 = Promise.resolve({
|
|
@@ -3847,7 +4192,7 @@ function syncCodeForm(client, form, opts) {
|
|
|
3847
4192
|
promise.then(
|
|
3848
4193
|
(result) => {
|
|
3849
4194
|
if (syncCache.get(key) !== entry) return;
|
|
3850
|
-
if (isVolatileSync(result)) {
|
|
4195
|
+
if (revalidateUploadState || isVolatileSync(result)) {
|
|
3851
4196
|
syncCache.delete(key);
|
|
3852
4197
|
return;
|
|
3853
4198
|
}
|
|
@@ -3907,6 +4252,9 @@ var JSX_BLOCK_SPECS = [
|
|
|
3907
4252
|
field("FileUpload", "file_upload", ["maxFiles", "maxFileSizeMb", "accept"]),
|
|
3908
4253
|
field("Hidden", "hidden", ["paramName", "defaultValue"]),
|
|
3909
4254
|
field("Calculated", "calculated", ["calc", "decimals", "prefix", "suffix"]),
|
|
4255
|
+
field("RepeatingGroup", "repeating_group", ["minInstances", "maxInstances", "addLabel", "itemLabel"], {
|
|
4256
|
+
blockChildren: true
|
|
4257
|
+
}),
|
|
3910
4258
|
field("Custom", "custom", ["component", "config"]),
|
|
3911
4259
|
content("Heading", "heading", true),
|
|
3912
4260
|
content("Paragraph", "paragraph", true),
|
|
@@ -4010,6 +4358,29 @@ function buildProps(spec, el) {
|
|
|
4010
4358
|
);
|
|
4011
4359
|
}
|
|
4012
4360
|
out.__childText = parts.join("");
|
|
4361
|
+
} else if (spec.blockChildren && props.children !== void 0) {
|
|
4362
|
+
const childBlocks = flatten(props.children).map((child) => {
|
|
4363
|
+
const childSpec = classify(child);
|
|
4364
|
+
if (childSpec.kind === "__page" || childSpec.kind === "__option" || childSpec.content) {
|
|
4365
|
+
throw new FilloJsxError(
|
|
4366
|
+
"NON_FILLO_CHILD",
|
|
4367
|
+
`Fillo.${spec.name} "${id}" children must be field elements, found Fillo.${childSpec.name} \u2014 a repeating group's template holds answerable fields only`
|
|
4368
|
+
);
|
|
4369
|
+
}
|
|
4370
|
+
return buildProps(childSpec, child);
|
|
4371
|
+
});
|
|
4372
|
+
const childIds = /* @__PURE__ */ new Set();
|
|
4373
|
+
for (const child of childBlocks) {
|
|
4374
|
+
const childId = child.id;
|
|
4375
|
+
if (childIds.has(childId)) {
|
|
4376
|
+
throw new FilloJsxError(
|
|
4377
|
+
"DUPLICATE_ID",
|
|
4378
|
+
`Two fields in repeating group "${id}" share the id "${childId}" \u2014 child ids key instance answers and must be unique within the group`
|
|
4379
|
+
);
|
|
4380
|
+
}
|
|
4381
|
+
childIds.add(childId);
|
|
4382
|
+
}
|
|
4383
|
+
out.__childBlocks = childBlocks;
|
|
4013
4384
|
} else if (props.children !== void 0) {
|
|
4014
4385
|
throw new FilloJsxError(
|
|
4015
4386
|
"NON_FILLO_CHILD",
|
|
@@ -4032,6 +4403,16 @@ function buildProps(spec, el) {
|
|
|
4032
4403
|
if (spec.textChildren && out.text === void 0) {
|
|
4033
4404
|
throw new FilloJsxError("TEXT_CHILD_REQUIRED", `Fillo.${spec.name} "${id}" has no text`);
|
|
4034
4405
|
}
|
|
4406
|
+
if (spec.blockChildren) {
|
|
4407
|
+
if (out.__childBlocks === void 0) {
|
|
4408
|
+
throw new FilloJsxError(
|
|
4409
|
+
"TEXT_CHILD_REQUIRED",
|
|
4410
|
+
`Fillo.${spec.name} "${id}" has no fields \u2014 a repeating group needs at least one field element as a child (empty templates are invalid)`
|
|
4411
|
+
);
|
|
4412
|
+
}
|
|
4413
|
+
out.fields = out.__childBlocks;
|
|
4414
|
+
delete out.__childBlocks;
|
|
4415
|
+
}
|
|
4035
4416
|
if (props.visibleIf !== void 0) {
|
|
4036
4417
|
const conditions2 = Array.isArray(props.visibleIf) ? props.visibleIf : [props.visibleIf];
|
|
4037
4418
|
out.visibleIf = conditions2;
|
|
@@ -4139,6 +4520,11 @@ function schemaFromJsx(children, meta) {
|
|
|
4139
4520
|
assertUniqueOptionIds(block.id, block.rows, "matrix rows");
|
|
4140
4521
|
assertUniqueOptionIds(block.id, block.columns, "matrix columns");
|
|
4141
4522
|
}
|
|
4523
|
+
if (block.kind === "repeating_group") {
|
|
4524
|
+
for (const child of block.fields) {
|
|
4525
|
+
if ("options" in child) assertUniqueOptionIds(child.id, child.options, "options");
|
|
4526
|
+
}
|
|
4527
|
+
}
|
|
4142
4528
|
}
|
|
4143
4529
|
}
|
|
4144
4530
|
return {
|
|
@@ -4236,6 +4622,7 @@ export {
|
|
|
4236
4622
|
FILLO_CALC_MIN_SDK_VERSION,
|
|
4237
4623
|
FILLO_CHALLENGE_MIN_SDK_VERSION,
|
|
4238
4624
|
FILLO_DATA_ATTRS,
|
|
4625
|
+
FILLO_GROUP_MIN_SDK_VERSION,
|
|
4239
4626
|
FILLO_MIN_SDK_VERSION,
|
|
4240
4627
|
FILLO_SCHEMA_VERSION,
|
|
4241
4628
|
FILLO_SDK_VERSION,
|
|
@@ -4299,6 +4686,7 @@ export {
|
|
|
4299
4686
|
reachableFields,
|
|
4300
4687
|
reachablePageIds,
|
|
4301
4688
|
reachablePageSequence,
|
|
4689
|
+
requiredFieldMessage,
|
|
4302
4690
|
resolveNextPage,
|
|
4303
4691
|
resolveSlotClass,
|
|
4304
4692
|
resolveStrings,
|
|
@@ -4316,6 +4704,7 @@ export {
|
|
|
4316
4704
|
validateResponse,
|
|
4317
4705
|
visibleBlocks,
|
|
4318
4706
|
visibleFields,
|
|
4707
|
+
visibleGroupChildren,
|
|
4319
4708
|
visiblePageBlocks,
|
|
4320
4709
|
when
|
|
4321
4710
|
};
|