@form-engine-ts/react 2.7.0 → 2.9.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 +11 -0
- package/dist/index.cjs +294 -50
- package/dist/index.d.cts +49 -10
- package/dist/index.d.ts +49 -10
- package/dist/index.js +287 -46
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -123,3 +123,14 @@ be created with the SSR-safe `createLocalStorageSubmissionReceiptStore`; `render
|
|
|
123
123
|
state. Text controls forward schema `minLength`, `maxLength`, and `pattern` constraints to the DOM, and
|
|
124
124
|
`renderCharacterCount` can replace the default count. Guard evaluation, confirmation, receipt persistence, and provider
|
|
125
125
|
submission share an in-flight lock so rapid clicks cannot submit twice.
|
|
126
|
+
|
|
127
|
+
The Builder basic-settings section edits source `title` and `description` through the same policy-aware action pipeline.
|
|
128
|
+
Submission confirmation slots receive the effective message, localized schema, and visible answers. An `onSubmit` result
|
|
129
|
+
may provide `submissionId` and `submittedAt`, which Renderer copies into its receipt. Receipt stores support `getBatch`,
|
|
130
|
+
and `useSubmissionReceipts` loads multiple form/version receipts for list and dashboard surfaces.
|
|
131
|
+
|
|
132
|
+
Receipt persistence is best-effort: `onReceiptError` observes storage failures while the successful completion screen is
|
|
133
|
+
preserved. Pass an SSR-safe `createLocalStorageSubmissionAttemptStore()` as `attemptStore` to reserve an ID immediately
|
|
134
|
+
before submission. Renderer injects it as `attemptId` and `submissionId`, retains it after a failed request, promotes it
|
|
135
|
+
to the receipt after success, and then clears the attempt. Custom receipt stores may omit `getBatch`; the hook falls back
|
|
136
|
+
to concurrent `get` calls.
|
package/dist/index.cjs
CHANGED
|
@@ -23,14 +23,116 @@ __export(index_exports, {
|
|
|
23
23
|
FormBuilder: () => FormBuilder,
|
|
24
24
|
FormProvider: () => FormProvider,
|
|
25
25
|
FormRenderer: () => FormRenderer,
|
|
26
|
+
createLocalStorageSubmissionAttemptStore: () => createLocalStorageSubmissionAttemptStore,
|
|
26
27
|
createLocalStorageSubmissionReceiptStore: () => createLocalStorageSubmissionReceiptStore,
|
|
27
28
|
resolveInitialFieldType: () => resolveInitialFieldType,
|
|
29
|
+
submissionReceiptQueryKey: () => submissionReceiptQueryKey,
|
|
28
30
|
useField: () => useField,
|
|
29
31
|
useForm: () => useForm,
|
|
30
|
-
useFormBuilder: () => useFormBuilder
|
|
32
|
+
useFormBuilder: () => useFormBuilder,
|
|
33
|
+
useSubmissionReceipts: () => useSubmissionReceipts
|
|
31
34
|
});
|
|
32
35
|
module.exports = __toCommonJS(index_exports);
|
|
33
36
|
|
|
37
|
+
// src/attempt.ts
|
|
38
|
+
function attemptKey(namespace, formId, formVersion) {
|
|
39
|
+
return `${namespace}:${formId}:v${formVersion}`;
|
|
40
|
+
}
|
|
41
|
+
function browserStorage() {
|
|
42
|
+
if (typeof window === "undefined") return null;
|
|
43
|
+
try {
|
|
44
|
+
return window.localStorage;
|
|
45
|
+
} catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function parseAttempt(serialized) {
|
|
50
|
+
try {
|
|
51
|
+
const value = JSON.parse(serialized);
|
|
52
|
+
if (typeof value !== "object" || value === null || !("attemptId" in value) || typeof value.attemptId !== "string" || value.attemptId.length === 0 || !("formId" in value) || typeof value.formId !== "string" || !("formVersion" in value) || typeof value.formVersion !== "number" || !Number.isSafeInteger(value.formVersion) || !("createdAt" in value) || typeof value.createdAt !== "string" || !Number.isFinite(Date.parse(value.createdAt))) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
attemptId: value.attemptId,
|
|
57
|
+
formId: value.formId,
|
|
58
|
+
formVersion: value.formVersion,
|
|
59
|
+
createdAt: value.createdAt
|
|
60
|
+
};
|
|
61
|
+
} catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function defaultAttemptId() {
|
|
66
|
+
const randomUuid = globalThis.crypto?.randomUUID;
|
|
67
|
+
if (typeof randomUuid === "function") return randomUuid.call(globalThis.crypto);
|
|
68
|
+
return `attempt-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
|
|
69
|
+
}
|
|
70
|
+
function createLocalStorageSubmissionAttemptStore(options = {}) {
|
|
71
|
+
const namespace = options.namespace ?? "form_engine_attempt";
|
|
72
|
+
if (namespace.trim().length === 0) throw new TypeError("Attempt namespace must not be empty.");
|
|
73
|
+
const memory = /* @__PURE__ */ new Map();
|
|
74
|
+
const inFlight = /* @__PURE__ */ new Map();
|
|
75
|
+
const get = async (formId, formVersion) => {
|
|
76
|
+
const key = attemptKey(namespace, formId, formVersion);
|
|
77
|
+
const remembered = memory.get(key);
|
|
78
|
+
if (remembered !== void 0) return remembered;
|
|
79
|
+
const storage = browserStorage();
|
|
80
|
+
if (storage === null) return null;
|
|
81
|
+
try {
|
|
82
|
+
const serialized = storage.getItem(key);
|
|
83
|
+
if (serialized === null) return null;
|
|
84
|
+
const attempt = parseAttempt(serialized);
|
|
85
|
+
if (attempt === null || attempt.formId !== formId || attempt.formVersion !== formVersion) return null;
|
|
86
|
+
memory.set(key, attempt);
|
|
87
|
+
return attempt;
|
|
88
|
+
} catch {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
return {
|
|
93
|
+
get,
|
|
94
|
+
async getOrCreate(formId, formVersion, idFactory = defaultAttemptId) {
|
|
95
|
+
const key = attemptKey(namespace, formId, formVersion);
|
|
96
|
+
const pending = inFlight.get(key);
|
|
97
|
+
if (pending !== void 0) return pending;
|
|
98
|
+
const create = (async () => {
|
|
99
|
+
const existing = await get(formId, formVersion);
|
|
100
|
+
if (existing !== null) return existing;
|
|
101
|
+
const attemptId = idFactory();
|
|
102
|
+
if (typeof attemptId !== "string" || attemptId.trim().length === 0) {
|
|
103
|
+
throw new TypeError("Attempt idFactory must return a non-empty string.");
|
|
104
|
+
}
|
|
105
|
+
const attempt = {
|
|
106
|
+
attemptId,
|
|
107
|
+
formId,
|
|
108
|
+
formVersion,
|
|
109
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
110
|
+
};
|
|
111
|
+
memory.set(key, attempt);
|
|
112
|
+
try {
|
|
113
|
+
browserStorage()?.setItem(key, JSON.stringify(attempt));
|
|
114
|
+
} catch {
|
|
115
|
+
}
|
|
116
|
+
return attempt;
|
|
117
|
+
})();
|
|
118
|
+
inFlight.set(key, create);
|
|
119
|
+
try {
|
|
120
|
+
return await create;
|
|
121
|
+
} finally {
|
|
122
|
+
inFlight.delete(key);
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
async clear(formId, formVersion) {
|
|
126
|
+
const key = attemptKey(namespace, formId, formVersion);
|
|
127
|
+
memory.delete(key);
|
|
128
|
+
try {
|
|
129
|
+
browserStorage()?.removeItem(key);
|
|
130
|
+
} catch {
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
34
136
|
// src/builder.tsx
|
|
35
137
|
var import_core2 = require("@form-engine-ts/core");
|
|
36
138
|
var import_react2 = require("react");
|
|
@@ -972,6 +1074,9 @@ var FIELD_TYPES = [
|
|
|
972
1074
|
];
|
|
973
1075
|
var BUILDER_DEFAULTS = {
|
|
974
1076
|
"builder.formBuilder": "Form builder",
|
|
1077
|
+
"builder.basicSettings": "Basic settings",
|
|
1078
|
+
"builder.formTitle": "Form title",
|
|
1079
|
+
"builder.formDescription": "Form description",
|
|
975
1080
|
"builder.moveUp": "Move {{title}} up",
|
|
976
1081
|
"builder.moveDown": "Move {{title}} down",
|
|
977
1082
|
"builder.delete": "Delete {{title}}",
|
|
@@ -1137,7 +1242,7 @@ function FormBuilder({
|
|
|
1137
1242
|
}) {
|
|
1138
1243
|
const resolvedComponents = { ...DEFAULT_COMPONENTS, ...componentOverrides };
|
|
1139
1244
|
const components = GUARDED_COMPONENTS;
|
|
1140
|
-
const { Button, Checkbox, ErrorMessage, Fieldset, IconButton, Section, Select, TextInput } = components;
|
|
1245
|
+
const { Button, Checkbox, ErrorMessage, Fieldset, IconButton, Section, Select, TextArea, TextInput } = components;
|
|
1141
1246
|
const ToolbarSlot = slots?.toolbar;
|
|
1142
1247
|
const FieldEditorSlot = slots?.fieldEditor;
|
|
1143
1248
|
const OptionEditorSlot = slots?.optionEditor;
|
|
@@ -1440,6 +1545,42 @@ function FormBuilder({
|
|
|
1440
1545
|
}
|
|
1441
1546
|
},
|
|
1442
1547
|
children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Fieldset, { className: "form-engine-builder__controls", disabled: readOnly, children: [
|
|
1548
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1549
|
+
Section,
|
|
1550
|
+
{
|
|
1551
|
+
className: "form-engine-builder__basic-settings",
|
|
1552
|
+
headingId: "builder-basic-settings-heading",
|
|
1553
|
+
title: translate("builder.basicSettings"),
|
|
1554
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "form-engine-builder__grid", children: [
|
|
1555
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { children: [
|
|
1556
|
+
translate("builder.formTitle"),
|
|
1557
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1558
|
+
TextInput,
|
|
1559
|
+
{
|
|
1560
|
+
name: "title",
|
|
1561
|
+
required: true,
|
|
1562
|
+
error: schema.title.trim().length === 0,
|
|
1563
|
+
helperText: schema.title.trim().length === 0 ? translate("builder.required") : "",
|
|
1564
|
+
value: schema.title,
|
|
1565
|
+
onChange: (value) => setSourceText({ kind: "form" }, "title", value)
|
|
1566
|
+
}
|
|
1567
|
+
)
|
|
1568
|
+
] }),
|
|
1569
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { children: [
|
|
1570
|
+
translate("builder.formDescription"),
|
|
1571
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1572
|
+
TextArea,
|
|
1573
|
+
{
|
|
1574
|
+
name: "description",
|
|
1575
|
+
rows: 3,
|
|
1576
|
+
value: schema.description ?? "",
|
|
1577
|
+
onChange: (value) => setSourceText({ kind: "form" }, "description", value)
|
|
1578
|
+
}
|
|
1579
|
+
)
|
|
1580
|
+
] })
|
|
1581
|
+
] })
|
|
1582
|
+
}
|
|
1583
|
+
),
|
|
1443
1584
|
pagesEnabled ? PagesSlot === void 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1444
1585
|
Section,
|
|
1445
1586
|
{
|
|
@@ -2279,7 +2420,7 @@ function FormProvider({
|
|
|
2279
2420
|
setSubmitError(null);
|
|
2280
2421
|
}, [initialValues]);
|
|
2281
2422
|
const submit = (0, import_react3.useCallback)(
|
|
2282
|
-
async (beforeSubmit) => {
|
|
2423
|
+
async (beforeSubmit, prepareSubmission) => {
|
|
2283
2424
|
if (submissionInFlight.current) return { status: "cancelled" };
|
|
2284
2425
|
const validation = (0, import_core3.validateAnswers)(validSchema, values);
|
|
2285
2426
|
if (!validation.valid) {
|
|
@@ -2300,10 +2441,11 @@ function FormProvider({
|
|
|
2300
2441
|
setSubmitStatus("idle");
|
|
2301
2442
|
return { status: "cancelled" };
|
|
2302
2443
|
}
|
|
2303
|
-
await
|
|
2444
|
+
const submissionValues = prepareSubmission === void 0 ? visibleValues : await prepareSubmission(visibleValues);
|
|
2445
|
+
const response = await onSubmit(submissionValues);
|
|
2304
2446
|
if (resetOnSuccess) setValues({ ...initialValues });
|
|
2305
2447
|
setSubmitStatus("success");
|
|
2306
|
-
return { status: "success" };
|
|
2448
|
+
return response === void 0 ? { status: "success" } : { status: "success", response };
|
|
2307
2449
|
} catch (cause) {
|
|
2308
2450
|
const error = cause instanceof Error ? cause : new Error(String(cause));
|
|
2309
2451
|
setSubmitError(error);
|
|
@@ -2372,10 +2514,14 @@ function useField(fieldId) {
|
|
|
2372
2514
|
}
|
|
2373
2515
|
|
|
2374
2516
|
// src/receipt.ts
|
|
2517
|
+
var import_react4 = require("react");
|
|
2518
|
+
function submissionReceiptQueryKey(formId, formVersion) {
|
|
2519
|
+
return `${formId}:v${formVersion}`;
|
|
2520
|
+
}
|
|
2375
2521
|
function receiptKey(namespace, formId, formVersion) {
|
|
2376
2522
|
return `${namespace}:${formId}:v${formVersion}`;
|
|
2377
2523
|
}
|
|
2378
|
-
function
|
|
2524
|
+
function browserStorage2() {
|
|
2379
2525
|
if (typeof window === "undefined") return null;
|
|
2380
2526
|
try {
|
|
2381
2527
|
return window.localStorage;
|
|
@@ -2403,35 +2549,90 @@ function parseReceipt(serialized) {
|
|
|
2403
2549
|
function createLocalStorageSubmissionReceiptStore(options = {}) {
|
|
2404
2550
|
const namespace = options.namespace ?? "form_engine_receipt";
|
|
2405
2551
|
if (namespace.trim().length === 0) throw new TypeError("Receipt namespace must not be empty.");
|
|
2552
|
+
const get = async (formId, formVersion) => {
|
|
2553
|
+
const storage = browserStorage2();
|
|
2554
|
+
if (storage === null) return null;
|
|
2555
|
+
try {
|
|
2556
|
+
const serialized = storage.getItem(receiptKey(namespace, formId, formVersion));
|
|
2557
|
+
if (serialized === null) return null;
|
|
2558
|
+
const receipt = parseReceipt(serialized);
|
|
2559
|
+
return receipt?.formId === formId && receipt.formVersion === formVersion ? receipt : null;
|
|
2560
|
+
} catch {
|
|
2561
|
+
return null;
|
|
2562
|
+
}
|
|
2563
|
+
};
|
|
2406
2564
|
return {
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
} catch {
|
|
2416
|
-
return null;
|
|
2417
|
-
}
|
|
2565
|
+
get,
|
|
2566
|
+
async getBatch(queries) {
|
|
2567
|
+
const receipts = await Promise.all(queries.map((query) => get(query.formId, query.formVersion)));
|
|
2568
|
+
return new Map(
|
|
2569
|
+
receipts.flatMap(
|
|
2570
|
+
(receipt) => receipt === null ? [] : [[submissionReceiptQueryKey(receipt.formId, receipt.formVersion), receipt]]
|
|
2571
|
+
)
|
|
2572
|
+
);
|
|
2418
2573
|
},
|
|
2419
2574
|
async save(receipt) {
|
|
2420
|
-
const storage =
|
|
2575
|
+
const storage = browserStorage2();
|
|
2421
2576
|
if (storage === null) return;
|
|
2422
2577
|
storage.setItem(receiptKey(namespace, receipt.formId, receipt.formVersion), JSON.stringify(receipt));
|
|
2423
2578
|
},
|
|
2424
2579
|
async remove(formId, formVersion) {
|
|
2425
|
-
const storage =
|
|
2580
|
+
const storage = browserStorage2();
|
|
2426
2581
|
if (storage === null) return;
|
|
2427
2582
|
storage.removeItem(receiptKey(namespace, formId, formVersion));
|
|
2428
2583
|
}
|
|
2429
2584
|
};
|
|
2430
2585
|
}
|
|
2586
|
+
function useSubmissionReceipts(store, queries) {
|
|
2587
|
+
const querySignature = JSON.stringify(queries.map(({ formId, formVersion }) => [formId, formVersion]));
|
|
2588
|
+
const stableQueries = (0, import_react4.useMemo)(() => {
|
|
2589
|
+
const parsed = JSON.parse(querySignature);
|
|
2590
|
+
if (!Array.isArray(parsed)) return [];
|
|
2591
|
+
return parsed.flatMap(
|
|
2592
|
+
(entry) => Array.isArray(entry) && typeof entry[0] === "string" && typeof entry[1] === "number" && Number.isSafeInteger(entry[1]) ? [{ formId: entry[0], formVersion: entry[1] }] : []
|
|
2593
|
+
);
|
|
2594
|
+
}, [querySignature]);
|
|
2595
|
+
const [state, setState] = (0, import_react4.useState)({
|
|
2596
|
+
receipts: /* @__PURE__ */ new Map(),
|
|
2597
|
+
isLoading: stableQueries.length > 0,
|
|
2598
|
+
error: null
|
|
2599
|
+
});
|
|
2600
|
+
(0, import_react4.useEffect)(() => {
|
|
2601
|
+
let active = true;
|
|
2602
|
+
if (stableQueries.length === 0) {
|
|
2603
|
+
setState({ receipts: /* @__PURE__ */ new Map(), isLoading: false, error: null });
|
|
2604
|
+
return () => {
|
|
2605
|
+
active = false;
|
|
2606
|
+
};
|
|
2607
|
+
}
|
|
2608
|
+
setState((current) => ({ ...current, isLoading: true, error: null }));
|
|
2609
|
+
const load = store.getBatch?.(stableQueries) ?? Promise.all(stableQueries.map((query) => store.get(query.formId, query.formVersion))).then(
|
|
2610
|
+
(receipts) => new Map(
|
|
2611
|
+
receipts.flatMap(
|
|
2612
|
+
(receipt) => receipt === null ? [] : [[submissionReceiptQueryKey(receipt.formId, receipt.formVersion), receipt]]
|
|
2613
|
+
)
|
|
2614
|
+
)
|
|
2615
|
+
);
|
|
2616
|
+
void load.then((receipts) => {
|
|
2617
|
+
if (active) setState({ receipts, isLoading: false, error: null });
|
|
2618
|
+
}).catch((cause) => {
|
|
2619
|
+
if (!active) return;
|
|
2620
|
+
setState({
|
|
2621
|
+
receipts: /* @__PURE__ */ new Map(),
|
|
2622
|
+
isLoading: false,
|
|
2623
|
+
error: cause instanceof Error ? cause : new Error(String(cause))
|
|
2624
|
+
});
|
|
2625
|
+
});
|
|
2626
|
+
return () => {
|
|
2627
|
+
active = false;
|
|
2628
|
+
};
|
|
2629
|
+
}, [stableQueries, store]);
|
|
2630
|
+
return state;
|
|
2631
|
+
}
|
|
2431
2632
|
|
|
2432
2633
|
// src/renderer.tsx
|
|
2433
2634
|
var import_core4 = require("@form-engine-ts/core");
|
|
2434
|
-
var
|
|
2635
|
+
var import_react5 = require("react");
|
|
2435
2636
|
var import_jsx_runtime3 = require("react/jsx-runtime");
|
|
2436
2637
|
function describedBy(field, error, helpId, errorId) {
|
|
2437
2638
|
const ids = [field.description === void 0 ? void 0 : helpId, error === void 0 ? void 0 : errorId].filter(
|
|
@@ -2656,31 +2857,34 @@ function ContextFormRenderer({
|
|
|
2656
2857
|
onDraftSave,
|
|
2657
2858
|
submissionGuards = [],
|
|
2658
2859
|
receiptStore,
|
|
2860
|
+
attemptStore,
|
|
2861
|
+
onReceiptError,
|
|
2659
2862
|
slots = {}
|
|
2660
2863
|
}) {
|
|
2661
2864
|
const form = useForm();
|
|
2662
|
-
const prefix = (0,
|
|
2663
|
-
const formRef = (0,
|
|
2664
|
-
const loadedDraftKey = (0,
|
|
2665
|
-
const [draftRestored, setDraftRestored] = (0,
|
|
2666
|
-
const [currentPageIndex, setCurrentPageIndex] = (0,
|
|
2667
|
-
const [focusFieldId, setFocusFieldId] = (0,
|
|
2668
|
-
const [confirmation, setConfirmation] = (0,
|
|
2669
|
-
const [guardMessage, setGuardMessage] = (0,
|
|
2670
|
-
const [receipt, setReceipt] = (0,
|
|
2671
|
-
const [receiptLoaded, setReceiptLoaded] = (0,
|
|
2672
|
-
const rendererSubmissionInFlight = (0,
|
|
2865
|
+
const prefix = (0, import_react5.useId)().replace(/:/g, "");
|
|
2866
|
+
const formRef = (0, import_react5.useRef)(null);
|
|
2867
|
+
const loadedDraftKey = (0, import_react5.useRef)(null);
|
|
2868
|
+
const [draftRestored, setDraftRestored] = (0, import_react5.useState)(false);
|
|
2869
|
+
const [currentPageIndex, setCurrentPageIndex] = (0, import_react5.useState)(0);
|
|
2870
|
+
const [focusFieldId, setFocusFieldId] = (0, import_react5.useState)(null);
|
|
2871
|
+
const [confirmation, setConfirmation] = (0, import_react5.useState)(null);
|
|
2872
|
+
const [guardMessage, setGuardMessage] = (0, import_react5.useState)(null);
|
|
2873
|
+
const [receipt, setReceipt] = (0, import_react5.useState)(null);
|
|
2874
|
+
const [receiptLoaded, setReceiptLoaded] = (0, import_react5.useState)(receiptStore === void 0);
|
|
2875
|
+
const rendererSubmissionInFlight = (0, import_react5.useRef)(false);
|
|
2673
2876
|
const pages = form.schema.pages;
|
|
2674
|
-
const visiblePageIndexes = (0,
|
|
2877
|
+
const visiblePageIndexes = (0, import_react5.useMemo)(
|
|
2675
2878
|
() => pages === void 0 ? [] : pages.flatMap((page, index) => form.pageVisibility[page.id] === true ? [index] : []),
|
|
2676
2879
|
[form.pageVisibility, pages]
|
|
2677
2880
|
);
|
|
2678
2881
|
const activePage = pages?.[currentPageIndex];
|
|
2679
2882
|
const activeVisibleIndex = visiblePageIndexes.indexOf(currentPageIndex);
|
|
2680
2883
|
const fieldIds = activePage === void 0 ? void 0 : new Set(activePage.questionIds);
|
|
2884
|
+
const visibleValues = (0, import_react5.useMemo)(() => (0, import_core4.selectVisibleAnswers)(form.schema, form.values), [form.schema, form.values]);
|
|
2681
2885
|
const submitState = confirmation === null ? form.submitStatus : "confirming";
|
|
2682
2886
|
const interactionLocked = submitState === "confirming" || submitState === "submitting";
|
|
2683
|
-
(0,
|
|
2887
|
+
(0, import_react5.useEffect)(() => {
|
|
2684
2888
|
let active = true;
|
|
2685
2889
|
if (receiptStore === void 0) {
|
|
2686
2890
|
setReceipt(null);
|
|
@@ -2701,14 +2905,14 @@ function ContextFormRenderer({
|
|
|
2701
2905
|
active = false;
|
|
2702
2906
|
};
|
|
2703
2907
|
}, [form.schema.id, form.schema.version, receiptStore]);
|
|
2704
|
-
(0,
|
|
2908
|
+
(0, import_react5.useEffect)(() => {
|
|
2705
2909
|
if (pages === void 0 || visiblePageIndexes.length === 0) {
|
|
2706
2910
|
setCurrentPageIndex(0);
|
|
2707
2911
|
return;
|
|
2708
2912
|
}
|
|
2709
2913
|
if (!visiblePageIndexes.includes(currentPageIndex)) setCurrentPageIndex(visiblePageIndexes[0] ?? 0);
|
|
2710
2914
|
}, [currentPageIndex, pages, visiblePageIndexes]);
|
|
2711
|
-
(0,
|
|
2915
|
+
(0, import_react5.useEffect)(() => {
|
|
2712
2916
|
if (focusFieldId === null) return;
|
|
2713
2917
|
const fieldContainer = [...formRef.current?.querySelectorAll("[data-field-id]") ?? []].find(
|
|
2714
2918
|
(element) => element.dataset.fieldId === focusFieldId
|
|
@@ -2719,7 +2923,7 @@ function ContextFormRenderer({
|
|
|
2719
2923
|
setFocusFieldId(null);
|
|
2720
2924
|
}
|
|
2721
2925
|
}, [focusFieldId]);
|
|
2722
|
-
(0,
|
|
2926
|
+
(0, import_react5.useEffect)(() => {
|
|
2723
2927
|
if (autoSaveKey === void 0 || typeof globalThis.localStorage === "undefined") return;
|
|
2724
2928
|
const loadIdentity = `${autoSaveKey}:${form.schema.id}:${form.schema.version}`;
|
|
2725
2929
|
if (loadedDraftKey.current === loadIdentity) return;
|
|
@@ -2731,7 +2935,7 @@ function ContextFormRenderer({
|
|
|
2731
2935
|
form.restoreValues(draft.values);
|
|
2732
2936
|
setDraftRestored(true);
|
|
2733
2937
|
}, [autoSaveKey, form.restoreValues, form.schema.id, form.schema.version]);
|
|
2734
|
-
(0,
|
|
2938
|
+
(0, import_react5.useEffect)(() => {
|
|
2735
2939
|
if (form.submitStatus === "success") return;
|
|
2736
2940
|
const timeout = globalThis.setTimeout(() => {
|
|
2737
2941
|
onDraftSave?.(form.values);
|
|
@@ -2764,7 +2968,7 @@ function ContextFormRenderer({
|
|
|
2764
2968
|
let confirmationMessage;
|
|
2765
2969
|
let requiresConfirmation = false;
|
|
2766
2970
|
for (const guard of guards) {
|
|
2767
|
-
const result = await guard(form.schema,
|
|
2971
|
+
const result = await guard(form.schema, visibleValues);
|
|
2768
2972
|
if (result.status === "allow") continue;
|
|
2769
2973
|
findings.push(...result.findings);
|
|
2770
2974
|
if (result.status === "block") {
|
|
@@ -2794,7 +2998,7 @@ function ContextFormRenderer({
|
|
|
2794
2998
|
try {
|
|
2795
2999
|
const guardResult = await runSubmissionGuards(submissionGuards);
|
|
2796
3000
|
if (guardResult.status === "block") {
|
|
2797
|
-
setGuardMessage(guardResult.message ?? "
|
|
3001
|
+
setGuardMessage(guardResult.message ?? form.translate("form.submissionBlocked"));
|
|
2798
3002
|
return { status: "cancelled" };
|
|
2799
3003
|
}
|
|
2800
3004
|
if (guardResult.status === "confirm") {
|
|
@@ -2812,7 +3016,18 @@ function ContextFormRenderer({
|
|
|
2812
3016
|
setGuardMessage(null);
|
|
2813
3017
|
rendererSubmissionInFlight.current = true;
|
|
2814
3018
|
try {
|
|
2815
|
-
|
|
3019
|
+
let submissionAttempt;
|
|
3020
|
+
const result = await form.submit(
|
|
3021
|
+
beforeSubmit,
|
|
3022
|
+
attemptStore === void 0 ? void 0 : async (values) => {
|
|
3023
|
+
submissionAttempt = await attemptStore.getOrCreate(form.schema.id, form.schema.version);
|
|
3024
|
+
return {
|
|
3025
|
+
...values,
|
|
3026
|
+
attemptId: submissionAttempt.attemptId,
|
|
3027
|
+
submissionId: submissionAttempt.attemptId
|
|
3028
|
+
};
|
|
3029
|
+
}
|
|
3030
|
+
);
|
|
2816
3031
|
if (result.status === "invalid") {
|
|
2817
3032
|
const invalidPageIndex = pages?.findIndex(
|
|
2818
3033
|
(page) => firstInvalidFieldId === void 0 ? false : page.questionIds.includes(firstInvalidFieldId)
|
|
@@ -2823,13 +3038,30 @@ function ContextFormRenderer({
|
|
|
2823
3038
|
}
|
|
2824
3039
|
if (result.status !== "success") return result;
|
|
2825
3040
|
if (receiptStore !== void 0) {
|
|
3041
|
+
const response = result.response;
|
|
3042
|
+
const submissionId = response?.submissionId ?? submissionAttempt?.attemptId;
|
|
2826
3043
|
const storedReceipt = {
|
|
2827
3044
|
formId: form.schema.id,
|
|
2828
3045
|
formVersion: form.schema.version,
|
|
2829
|
-
submittedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3046
|
+
submittedAt: response?.submittedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
3047
|
+
...submissionId === void 0 ? {} : { submissionId }
|
|
2830
3048
|
};
|
|
2831
|
-
|
|
2832
|
-
|
|
3049
|
+
try {
|
|
3050
|
+
await receiptStore.save(storedReceipt);
|
|
3051
|
+
setReceipt(storedReceipt);
|
|
3052
|
+
} catch (cause) {
|
|
3053
|
+
const error = cause instanceof Error ? cause : new Error(String(cause));
|
|
3054
|
+
try {
|
|
3055
|
+
onReceiptError?.(error, storedReceipt);
|
|
3056
|
+
} catch {
|
|
3057
|
+
}
|
|
3058
|
+
}
|
|
3059
|
+
}
|
|
3060
|
+
if (attemptStore !== void 0 && submissionAttempt !== void 0) {
|
|
3061
|
+
try {
|
|
3062
|
+
await attemptStore.clear(form.schema.id, form.schema.version);
|
|
3063
|
+
} catch {
|
|
3064
|
+
}
|
|
2833
3065
|
}
|
|
2834
3066
|
if (autoSaveKey !== void 0 && typeof globalThis.localStorage !== "undefined") {
|
|
2835
3067
|
globalThis.localStorage.removeItem(autoSaveKey);
|
|
@@ -2869,8 +3101,8 @@ function ContextFormRenderer({
|
|
|
2869
3101
|
receipt,
|
|
2870
3102
|
...receiptStore === void 0 ? {} : { onReset: () => void resetReceipt() }
|
|
2871
3103
|
}) ?? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { role: "status", children: [
|
|
2872
|
-
"
|
|
2873
|
-
receiptStore === void 0 ? null : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", onClick: () => void resetReceipt(), children: "
|
|
3104
|
+
form.translate("form.alreadySubmitted"),
|
|
3105
|
+
receiptStore === void 0 ? null : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", onClick: () => void resetReceipt(), children: form.translate("form.submitAnother") })
|
|
2874
3106
|
] }) });
|
|
2875
3107
|
}
|
|
2876
3108
|
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("form", { ref: formRef, className: `fe-form ${className}`.trim(), noValidate: true, onSubmit: handleSubmit, children: [
|
|
@@ -2924,7 +3156,7 @@ function ContextFormRenderer({
|
|
|
2924
3156
|
...slots.renderCharacterCount === void 0 ? {} : { renderCharacterCount: slots.renderCharacterCount }
|
|
2925
3157
|
};
|
|
2926
3158
|
if (slots.renderField !== void 0) {
|
|
2927
|
-
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
3159
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react5.Fragment, { children: slots.renderField({
|
|
2928
3160
|
question: field,
|
|
2929
3161
|
value: form.values[field.id],
|
|
2930
3162
|
onChange: (value) => {
|
|
@@ -2939,12 +3171,15 @@ function ContextFormRenderer({
|
|
|
2939
3171
|
guardMessage === null ? null : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { role: "alert", children: guardMessage }),
|
|
2940
3172
|
confirmation === null ? null : slots.renderSubmissionConfirmation?.({
|
|
2941
3173
|
findings: confirmation.findings,
|
|
3174
|
+
message: confirmation.message ?? form.translate("form.confirmSensitiveData"),
|
|
3175
|
+
schema: form.schema,
|
|
3176
|
+
visibleValues,
|
|
2942
3177
|
onConfirm: confirmSubmission,
|
|
2943
3178
|
onCancel: cancelSubmission
|
|
2944
3179
|
}) ?? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "fe-submission-confirmation", role: "dialog", "aria-modal": "true", children: [
|
|
2945
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { children: confirmation.message ?? "
|
|
2946
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", onClick: confirmSubmission, children: "
|
|
2947
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", onClick: cancelSubmission, children: "
|
|
3180
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { children: confirmation.message ?? form.translate("form.confirmSensitiveData") }),
|
|
3181
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", onClick: confirmSubmission, children: form.translate("form.confirmSubmission") }),
|
|
3182
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", onClick: cancelSubmission, children: form.translate("form.cancelSubmission") })
|
|
2948
3183
|
] }),
|
|
2949
3184
|
validationIssues.length === 0 ? null : slots.renderValidationSummary?.({ issues: validationIssues }) ?? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "fe-validation-summary", role: "alert", children: [
|
|
2950
3185
|
validationIssues.length,
|
|
@@ -3001,6 +3236,12 @@ var RENDERER_MESSAGES = {
|
|
|
3001
3236
|
"form.next": "Next",
|
|
3002
3237
|
"form.step": "Step {{current}} / {{total}}",
|
|
3003
3238
|
"form.draftRestored": "Draft restored",
|
|
3239
|
+
"form.submissionBlocked": "Submission blocked because sensitive data was detected.",
|
|
3240
|
+
"form.confirmSensitiveData": "Sensitive data may be included. Confirm before submitting.",
|
|
3241
|
+
"form.confirmSubmission": "Confirm submission",
|
|
3242
|
+
"form.cancelSubmission": "Cancel",
|
|
3243
|
+
"form.alreadySubmitted": "Already submitted.",
|
|
3244
|
+
"form.submitAnother": "Submit another response",
|
|
3004
3245
|
"validation.required": "This field is required."
|
|
3005
3246
|
};
|
|
3006
3247
|
var defaultRendererTranslator = {
|
|
@@ -3040,9 +3281,12 @@ function FormRenderer(props) {
|
|
|
3040
3281
|
FormBuilder,
|
|
3041
3282
|
FormProvider,
|
|
3042
3283
|
FormRenderer,
|
|
3284
|
+
createLocalStorageSubmissionAttemptStore,
|
|
3043
3285
|
createLocalStorageSubmissionReceiptStore,
|
|
3044
3286
|
resolveInitialFieldType,
|
|
3287
|
+
submissionReceiptQueryKey,
|
|
3045
3288
|
useField,
|
|
3046
3289
|
useForm,
|
|
3047
|
-
useFormBuilder
|
|
3290
|
+
useFormBuilder,
|
|
3291
|
+
useSubmissionReceipts
|
|
3048
3292
|
});
|