@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/dist/index.js
CHANGED
|
@@ -1,3 +1,102 @@
|
|
|
1
|
+
// src/attempt.ts
|
|
2
|
+
function attemptKey(namespace, formId, formVersion) {
|
|
3
|
+
return `${namespace}:${formId}:v${formVersion}`;
|
|
4
|
+
}
|
|
5
|
+
function browserStorage() {
|
|
6
|
+
if (typeof window === "undefined") return null;
|
|
7
|
+
try {
|
|
8
|
+
return window.localStorage;
|
|
9
|
+
} catch {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
function parseAttempt(serialized) {
|
|
14
|
+
try {
|
|
15
|
+
const value = JSON.parse(serialized);
|
|
16
|
+
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))) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
return {
|
|
20
|
+
attemptId: value.attemptId,
|
|
21
|
+
formId: value.formId,
|
|
22
|
+
formVersion: value.formVersion,
|
|
23
|
+
createdAt: value.createdAt
|
|
24
|
+
};
|
|
25
|
+
} catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function defaultAttemptId() {
|
|
30
|
+
const randomUuid = globalThis.crypto?.randomUUID;
|
|
31
|
+
if (typeof randomUuid === "function") return randomUuid.call(globalThis.crypto);
|
|
32
|
+
return `attempt-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
|
|
33
|
+
}
|
|
34
|
+
function createLocalStorageSubmissionAttemptStore(options = {}) {
|
|
35
|
+
const namespace = options.namespace ?? "form_engine_attempt";
|
|
36
|
+
if (namespace.trim().length === 0) throw new TypeError("Attempt namespace must not be empty.");
|
|
37
|
+
const memory = /* @__PURE__ */ new Map();
|
|
38
|
+
const inFlight = /* @__PURE__ */ new Map();
|
|
39
|
+
const get = async (formId, formVersion) => {
|
|
40
|
+
const key = attemptKey(namespace, formId, formVersion);
|
|
41
|
+
const remembered = memory.get(key);
|
|
42
|
+
if (remembered !== void 0) return remembered;
|
|
43
|
+
const storage = browserStorage();
|
|
44
|
+
if (storage === null) return null;
|
|
45
|
+
try {
|
|
46
|
+
const serialized = storage.getItem(key);
|
|
47
|
+
if (serialized === null) return null;
|
|
48
|
+
const attempt = parseAttempt(serialized);
|
|
49
|
+
if (attempt === null || attempt.formId !== formId || attempt.formVersion !== formVersion) return null;
|
|
50
|
+
memory.set(key, attempt);
|
|
51
|
+
return attempt;
|
|
52
|
+
} catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
return {
|
|
57
|
+
get,
|
|
58
|
+
async getOrCreate(formId, formVersion, idFactory = defaultAttemptId) {
|
|
59
|
+
const key = attemptKey(namespace, formId, formVersion);
|
|
60
|
+
const pending = inFlight.get(key);
|
|
61
|
+
if (pending !== void 0) return pending;
|
|
62
|
+
const create = (async () => {
|
|
63
|
+
const existing = await get(formId, formVersion);
|
|
64
|
+
if (existing !== null) return existing;
|
|
65
|
+
const attemptId = idFactory();
|
|
66
|
+
if (typeof attemptId !== "string" || attemptId.trim().length === 0) {
|
|
67
|
+
throw new TypeError("Attempt idFactory must return a non-empty string.");
|
|
68
|
+
}
|
|
69
|
+
const attempt = {
|
|
70
|
+
attemptId,
|
|
71
|
+
formId,
|
|
72
|
+
formVersion,
|
|
73
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
74
|
+
};
|
|
75
|
+
memory.set(key, attempt);
|
|
76
|
+
try {
|
|
77
|
+
browserStorage()?.setItem(key, JSON.stringify(attempt));
|
|
78
|
+
} catch {
|
|
79
|
+
}
|
|
80
|
+
return attempt;
|
|
81
|
+
})();
|
|
82
|
+
inFlight.set(key, create);
|
|
83
|
+
try {
|
|
84
|
+
return await create;
|
|
85
|
+
} finally {
|
|
86
|
+
inFlight.delete(key);
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
async clear(formId, formVersion) {
|
|
90
|
+
const key = attemptKey(namespace, formId, formVersion);
|
|
91
|
+
memory.delete(key);
|
|
92
|
+
try {
|
|
93
|
+
browserStorage()?.removeItem(key);
|
|
94
|
+
} catch {
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
1
100
|
// src/builder.tsx
|
|
2
101
|
import {
|
|
3
102
|
populateSchemaTranslations
|
|
@@ -945,6 +1044,9 @@ var FIELD_TYPES = [
|
|
|
945
1044
|
];
|
|
946
1045
|
var BUILDER_DEFAULTS = {
|
|
947
1046
|
"builder.formBuilder": "Form builder",
|
|
1047
|
+
"builder.basicSettings": "Basic settings",
|
|
1048
|
+
"builder.formTitle": "Form title",
|
|
1049
|
+
"builder.formDescription": "Form description",
|
|
948
1050
|
"builder.moveUp": "Move {{title}} up",
|
|
949
1051
|
"builder.moveDown": "Move {{title}} down",
|
|
950
1052
|
"builder.delete": "Delete {{title}}",
|
|
@@ -1110,7 +1212,7 @@ function FormBuilder({
|
|
|
1110
1212
|
}) {
|
|
1111
1213
|
const resolvedComponents = { ...DEFAULT_COMPONENTS, ...componentOverrides };
|
|
1112
1214
|
const components = GUARDED_COMPONENTS;
|
|
1113
|
-
const { Button, Checkbox, ErrorMessage, Fieldset, IconButton, Section, Select, TextInput } = components;
|
|
1215
|
+
const { Button, Checkbox, ErrorMessage, Fieldset, IconButton, Section, Select, TextArea, TextInput } = components;
|
|
1114
1216
|
const ToolbarSlot = slots?.toolbar;
|
|
1115
1217
|
const FieldEditorSlot = slots?.fieldEditor;
|
|
1116
1218
|
const OptionEditorSlot = slots?.optionEditor;
|
|
@@ -1413,6 +1515,42 @@ function FormBuilder({
|
|
|
1413
1515
|
}
|
|
1414
1516
|
},
|
|
1415
1517
|
children: /* @__PURE__ */ jsxs(Fieldset, { className: "form-engine-builder__controls", disabled: readOnly, children: [
|
|
1518
|
+
/* @__PURE__ */ jsx(
|
|
1519
|
+
Section,
|
|
1520
|
+
{
|
|
1521
|
+
className: "form-engine-builder__basic-settings",
|
|
1522
|
+
headingId: "builder-basic-settings-heading",
|
|
1523
|
+
title: translate("builder.basicSettings"),
|
|
1524
|
+
children: /* @__PURE__ */ jsxs("div", { className: "form-engine-builder__grid", children: [
|
|
1525
|
+
/* @__PURE__ */ jsxs("label", { children: [
|
|
1526
|
+
translate("builder.formTitle"),
|
|
1527
|
+
/* @__PURE__ */ jsx(
|
|
1528
|
+
TextInput,
|
|
1529
|
+
{
|
|
1530
|
+
name: "title",
|
|
1531
|
+
required: true,
|
|
1532
|
+
error: schema.title.trim().length === 0,
|
|
1533
|
+
helperText: schema.title.trim().length === 0 ? translate("builder.required") : "",
|
|
1534
|
+
value: schema.title,
|
|
1535
|
+
onChange: (value) => setSourceText({ kind: "form" }, "title", value)
|
|
1536
|
+
}
|
|
1537
|
+
)
|
|
1538
|
+
] }),
|
|
1539
|
+
/* @__PURE__ */ jsxs("label", { children: [
|
|
1540
|
+
translate("builder.formDescription"),
|
|
1541
|
+
/* @__PURE__ */ jsx(
|
|
1542
|
+
TextArea,
|
|
1543
|
+
{
|
|
1544
|
+
name: "description",
|
|
1545
|
+
rows: 3,
|
|
1546
|
+
value: schema.description ?? "",
|
|
1547
|
+
onChange: (value) => setSourceText({ kind: "form" }, "description", value)
|
|
1548
|
+
}
|
|
1549
|
+
)
|
|
1550
|
+
] })
|
|
1551
|
+
] })
|
|
1552
|
+
}
|
|
1553
|
+
),
|
|
1416
1554
|
pagesEnabled ? PagesSlot === void 0 ? /* @__PURE__ */ jsx(
|
|
1417
1555
|
Section,
|
|
1418
1556
|
{
|
|
@@ -2260,7 +2398,7 @@ function FormProvider({
|
|
|
2260
2398
|
setSubmitError(null);
|
|
2261
2399
|
}, [initialValues]);
|
|
2262
2400
|
const submit = useCallback2(
|
|
2263
|
-
async (beforeSubmit) => {
|
|
2401
|
+
async (beforeSubmit, prepareSubmission) => {
|
|
2264
2402
|
if (submissionInFlight.current) return { status: "cancelled" };
|
|
2265
2403
|
const validation = validateAnswers(validSchema, values);
|
|
2266
2404
|
if (!validation.valid) {
|
|
@@ -2281,10 +2419,11 @@ function FormProvider({
|
|
|
2281
2419
|
setSubmitStatus("idle");
|
|
2282
2420
|
return { status: "cancelled" };
|
|
2283
2421
|
}
|
|
2284
|
-
await
|
|
2422
|
+
const submissionValues = prepareSubmission === void 0 ? visibleValues : await prepareSubmission(visibleValues);
|
|
2423
|
+
const response = await onSubmit(submissionValues);
|
|
2285
2424
|
if (resetOnSuccess) setValues({ ...initialValues });
|
|
2286
2425
|
setSubmitStatus("success");
|
|
2287
|
-
return { status: "success" };
|
|
2426
|
+
return response === void 0 ? { status: "success" } : { status: "success", response };
|
|
2288
2427
|
} catch (cause) {
|
|
2289
2428
|
const error = cause instanceof Error ? cause : new Error(String(cause));
|
|
2290
2429
|
setSubmitError(error);
|
|
@@ -2353,10 +2492,14 @@ function useField(fieldId) {
|
|
|
2353
2492
|
}
|
|
2354
2493
|
|
|
2355
2494
|
// src/receipt.ts
|
|
2495
|
+
import { useEffect as useEffect2, useMemo as useMemo3, useState as useState3 } from "react";
|
|
2496
|
+
function submissionReceiptQueryKey(formId, formVersion) {
|
|
2497
|
+
return `${formId}:v${formVersion}`;
|
|
2498
|
+
}
|
|
2356
2499
|
function receiptKey(namespace, formId, formVersion) {
|
|
2357
2500
|
return `${namespace}:${formId}:v${formVersion}`;
|
|
2358
2501
|
}
|
|
2359
|
-
function
|
|
2502
|
+
function browserStorage2() {
|
|
2360
2503
|
if (typeof window === "undefined") return null;
|
|
2361
2504
|
try {
|
|
2362
2505
|
return window.localStorage;
|
|
@@ -2384,31 +2527,86 @@ function parseReceipt(serialized) {
|
|
|
2384
2527
|
function createLocalStorageSubmissionReceiptStore(options = {}) {
|
|
2385
2528
|
const namespace = options.namespace ?? "form_engine_receipt";
|
|
2386
2529
|
if (namespace.trim().length === 0) throw new TypeError("Receipt namespace must not be empty.");
|
|
2530
|
+
const get = async (formId, formVersion) => {
|
|
2531
|
+
const storage = browserStorage2();
|
|
2532
|
+
if (storage === null) return null;
|
|
2533
|
+
try {
|
|
2534
|
+
const serialized = storage.getItem(receiptKey(namespace, formId, formVersion));
|
|
2535
|
+
if (serialized === null) return null;
|
|
2536
|
+
const receipt = parseReceipt(serialized);
|
|
2537
|
+
return receipt?.formId === formId && receipt.formVersion === formVersion ? receipt : null;
|
|
2538
|
+
} catch {
|
|
2539
|
+
return null;
|
|
2540
|
+
}
|
|
2541
|
+
};
|
|
2387
2542
|
return {
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
} catch {
|
|
2397
|
-
return null;
|
|
2398
|
-
}
|
|
2543
|
+
get,
|
|
2544
|
+
async getBatch(queries) {
|
|
2545
|
+
const receipts = await Promise.all(queries.map((query) => get(query.formId, query.formVersion)));
|
|
2546
|
+
return new Map(
|
|
2547
|
+
receipts.flatMap(
|
|
2548
|
+
(receipt) => receipt === null ? [] : [[submissionReceiptQueryKey(receipt.formId, receipt.formVersion), receipt]]
|
|
2549
|
+
)
|
|
2550
|
+
);
|
|
2399
2551
|
},
|
|
2400
2552
|
async save(receipt) {
|
|
2401
|
-
const storage =
|
|
2553
|
+
const storage = browserStorage2();
|
|
2402
2554
|
if (storage === null) return;
|
|
2403
2555
|
storage.setItem(receiptKey(namespace, receipt.formId, receipt.formVersion), JSON.stringify(receipt));
|
|
2404
2556
|
},
|
|
2405
2557
|
async remove(formId, formVersion) {
|
|
2406
|
-
const storage =
|
|
2558
|
+
const storage = browserStorage2();
|
|
2407
2559
|
if (storage === null) return;
|
|
2408
2560
|
storage.removeItem(receiptKey(namespace, formId, formVersion));
|
|
2409
2561
|
}
|
|
2410
2562
|
};
|
|
2411
2563
|
}
|
|
2564
|
+
function useSubmissionReceipts(store, queries) {
|
|
2565
|
+
const querySignature = JSON.stringify(queries.map(({ formId, formVersion }) => [formId, formVersion]));
|
|
2566
|
+
const stableQueries = useMemo3(() => {
|
|
2567
|
+
const parsed = JSON.parse(querySignature);
|
|
2568
|
+
if (!Array.isArray(parsed)) return [];
|
|
2569
|
+
return parsed.flatMap(
|
|
2570
|
+
(entry) => Array.isArray(entry) && typeof entry[0] === "string" && typeof entry[1] === "number" && Number.isSafeInteger(entry[1]) ? [{ formId: entry[0], formVersion: entry[1] }] : []
|
|
2571
|
+
);
|
|
2572
|
+
}, [querySignature]);
|
|
2573
|
+
const [state, setState] = useState3({
|
|
2574
|
+
receipts: /* @__PURE__ */ new Map(),
|
|
2575
|
+
isLoading: stableQueries.length > 0,
|
|
2576
|
+
error: null
|
|
2577
|
+
});
|
|
2578
|
+
useEffect2(() => {
|
|
2579
|
+
let active = true;
|
|
2580
|
+
if (stableQueries.length === 0) {
|
|
2581
|
+
setState({ receipts: /* @__PURE__ */ new Map(), isLoading: false, error: null });
|
|
2582
|
+
return () => {
|
|
2583
|
+
active = false;
|
|
2584
|
+
};
|
|
2585
|
+
}
|
|
2586
|
+
setState((current) => ({ ...current, isLoading: true, error: null }));
|
|
2587
|
+
const load = store.getBatch?.(stableQueries) ?? Promise.all(stableQueries.map((query) => store.get(query.formId, query.formVersion))).then(
|
|
2588
|
+
(receipts) => new Map(
|
|
2589
|
+
receipts.flatMap(
|
|
2590
|
+
(receipt) => receipt === null ? [] : [[submissionReceiptQueryKey(receipt.formId, receipt.formVersion), receipt]]
|
|
2591
|
+
)
|
|
2592
|
+
)
|
|
2593
|
+
);
|
|
2594
|
+
void load.then((receipts) => {
|
|
2595
|
+
if (active) setState({ receipts, isLoading: false, error: null });
|
|
2596
|
+
}).catch((cause) => {
|
|
2597
|
+
if (!active) return;
|
|
2598
|
+
setState({
|
|
2599
|
+
receipts: /* @__PURE__ */ new Map(),
|
|
2600
|
+
isLoading: false,
|
|
2601
|
+
error: cause instanceof Error ? cause : new Error(String(cause))
|
|
2602
|
+
});
|
|
2603
|
+
});
|
|
2604
|
+
return () => {
|
|
2605
|
+
active = false;
|
|
2606
|
+
};
|
|
2607
|
+
}, [stableQueries, store]);
|
|
2608
|
+
return state;
|
|
2609
|
+
}
|
|
2412
2610
|
|
|
2413
2611
|
// src/renderer.tsx
|
|
2414
2612
|
import {
|
|
@@ -2417,11 +2615,11 @@ import {
|
|
|
2417
2615
|
} from "@form-engine-ts/core";
|
|
2418
2616
|
import {
|
|
2419
2617
|
Fragment as Fragment2,
|
|
2420
|
-
useEffect as
|
|
2618
|
+
useEffect as useEffect3,
|
|
2421
2619
|
useId,
|
|
2422
|
-
useMemo as
|
|
2620
|
+
useMemo as useMemo4,
|
|
2423
2621
|
useRef as useRef2,
|
|
2424
|
-
useState as
|
|
2622
|
+
useState as useState4
|
|
2425
2623
|
} from "react";
|
|
2426
2624
|
import { Fragment as Fragment3, jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
2427
2625
|
function describedBy(field, error, helpId, errorId) {
|
|
@@ -2647,31 +2845,34 @@ function ContextFormRenderer({
|
|
|
2647
2845
|
onDraftSave,
|
|
2648
2846
|
submissionGuards = [],
|
|
2649
2847
|
receiptStore,
|
|
2848
|
+
attemptStore,
|
|
2849
|
+
onReceiptError,
|
|
2650
2850
|
slots = {}
|
|
2651
2851
|
}) {
|
|
2652
2852
|
const form = useForm();
|
|
2653
2853
|
const prefix = useId().replace(/:/g, "");
|
|
2654
2854
|
const formRef = useRef2(null);
|
|
2655
2855
|
const loadedDraftKey = useRef2(null);
|
|
2656
|
-
const [draftRestored, setDraftRestored] =
|
|
2657
|
-
const [currentPageIndex, setCurrentPageIndex] =
|
|
2658
|
-
const [focusFieldId, setFocusFieldId] =
|
|
2659
|
-
const [confirmation, setConfirmation] =
|
|
2660
|
-
const [guardMessage, setGuardMessage] =
|
|
2661
|
-
const [receipt, setReceipt] =
|
|
2662
|
-
const [receiptLoaded, setReceiptLoaded] =
|
|
2856
|
+
const [draftRestored, setDraftRestored] = useState4(false);
|
|
2857
|
+
const [currentPageIndex, setCurrentPageIndex] = useState4(0);
|
|
2858
|
+
const [focusFieldId, setFocusFieldId] = useState4(null);
|
|
2859
|
+
const [confirmation, setConfirmation] = useState4(null);
|
|
2860
|
+
const [guardMessage, setGuardMessage] = useState4(null);
|
|
2861
|
+
const [receipt, setReceipt] = useState4(null);
|
|
2862
|
+
const [receiptLoaded, setReceiptLoaded] = useState4(receiptStore === void 0);
|
|
2663
2863
|
const rendererSubmissionInFlight = useRef2(false);
|
|
2664
2864
|
const pages = form.schema.pages;
|
|
2665
|
-
const visiblePageIndexes =
|
|
2865
|
+
const visiblePageIndexes = useMemo4(
|
|
2666
2866
|
() => pages === void 0 ? [] : pages.flatMap((page, index) => form.pageVisibility[page.id] === true ? [index] : []),
|
|
2667
2867
|
[form.pageVisibility, pages]
|
|
2668
2868
|
);
|
|
2669
2869
|
const activePage = pages?.[currentPageIndex];
|
|
2670
2870
|
const activeVisibleIndex = visiblePageIndexes.indexOf(currentPageIndex);
|
|
2671
2871
|
const fieldIds = activePage === void 0 ? void 0 : new Set(activePage.questionIds);
|
|
2872
|
+
const visibleValues = useMemo4(() => selectVisibleAnswers2(form.schema, form.values), [form.schema, form.values]);
|
|
2672
2873
|
const submitState = confirmation === null ? form.submitStatus : "confirming";
|
|
2673
2874
|
const interactionLocked = submitState === "confirming" || submitState === "submitting";
|
|
2674
|
-
|
|
2875
|
+
useEffect3(() => {
|
|
2675
2876
|
let active = true;
|
|
2676
2877
|
if (receiptStore === void 0) {
|
|
2677
2878
|
setReceipt(null);
|
|
@@ -2692,14 +2893,14 @@ function ContextFormRenderer({
|
|
|
2692
2893
|
active = false;
|
|
2693
2894
|
};
|
|
2694
2895
|
}, [form.schema.id, form.schema.version, receiptStore]);
|
|
2695
|
-
|
|
2896
|
+
useEffect3(() => {
|
|
2696
2897
|
if (pages === void 0 || visiblePageIndexes.length === 0) {
|
|
2697
2898
|
setCurrentPageIndex(0);
|
|
2698
2899
|
return;
|
|
2699
2900
|
}
|
|
2700
2901
|
if (!visiblePageIndexes.includes(currentPageIndex)) setCurrentPageIndex(visiblePageIndexes[0] ?? 0);
|
|
2701
2902
|
}, [currentPageIndex, pages, visiblePageIndexes]);
|
|
2702
|
-
|
|
2903
|
+
useEffect3(() => {
|
|
2703
2904
|
if (focusFieldId === null) return;
|
|
2704
2905
|
const fieldContainer = [...formRef.current?.querySelectorAll("[data-field-id]") ?? []].find(
|
|
2705
2906
|
(element) => element.dataset.fieldId === focusFieldId
|
|
@@ -2710,7 +2911,7 @@ function ContextFormRenderer({
|
|
|
2710
2911
|
setFocusFieldId(null);
|
|
2711
2912
|
}
|
|
2712
2913
|
}, [focusFieldId]);
|
|
2713
|
-
|
|
2914
|
+
useEffect3(() => {
|
|
2714
2915
|
if (autoSaveKey === void 0 || typeof globalThis.localStorage === "undefined") return;
|
|
2715
2916
|
const loadIdentity = `${autoSaveKey}:${form.schema.id}:${form.schema.version}`;
|
|
2716
2917
|
if (loadedDraftKey.current === loadIdentity) return;
|
|
@@ -2722,7 +2923,7 @@ function ContextFormRenderer({
|
|
|
2722
2923
|
form.restoreValues(draft.values);
|
|
2723
2924
|
setDraftRestored(true);
|
|
2724
2925
|
}, [autoSaveKey, form.restoreValues, form.schema.id, form.schema.version]);
|
|
2725
|
-
|
|
2926
|
+
useEffect3(() => {
|
|
2726
2927
|
if (form.submitStatus === "success") return;
|
|
2727
2928
|
const timeout = globalThis.setTimeout(() => {
|
|
2728
2929
|
onDraftSave?.(form.values);
|
|
@@ -2755,7 +2956,7 @@ function ContextFormRenderer({
|
|
|
2755
2956
|
let confirmationMessage;
|
|
2756
2957
|
let requiresConfirmation = false;
|
|
2757
2958
|
for (const guard of guards) {
|
|
2758
|
-
const result = await guard(form.schema,
|
|
2959
|
+
const result = await guard(form.schema, visibleValues);
|
|
2759
2960
|
if (result.status === "allow") continue;
|
|
2760
2961
|
findings.push(...result.findings);
|
|
2761
2962
|
if (result.status === "block") {
|
|
@@ -2785,7 +2986,7 @@ function ContextFormRenderer({
|
|
|
2785
2986
|
try {
|
|
2786
2987
|
const guardResult = await runSubmissionGuards(submissionGuards);
|
|
2787
2988
|
if (guardResult.status === "block") {
|
|
2788
|
-
setGuardMessage(guardResult.message ?? "
|
|
2989
|
+
setGuardMessage(guardResult.message ?? form.translate("form.submissionBlocked"));
|
|
2789
2990
|
return { status: "cancelled" };
|
|
2790
2991
|
}
|
|
2791
2992
|
if (guardResult.status === "confirm") {
|
|
@@ -2803,7 +3004,18 @@ function ContextFormRenderer({
|
|
|
2803
3004
|
setGuardMessage(null);
|
|
2804
3005
|
rendererSubmissionInFlight.current = true;
|
|
2805
3006
|
try {
|
|
2806
|
-
|
|
3007
|
+
let submissionAttempt;
|
|
3008
|
+
const result = await form.submit(
|
|
3009
|
+
beforeSubmit,
|
|
3010
|
+
attemptStore === void 0 ? void 0 : async (values) => {
|
|
3011
|
+
submissionAttempt = await attemptStore.getOrCreate(form.schema.id, form.schema.version);
|
|
3012
|
+
return {
|
|
3013
|
+
...values,
|
|
3014
|
+
attemptId: submissionAttempt.attemptId,
|
|
3015
|
+
submissionId: submissionAttempt.attemptId
|
|
3016
|
+
};
|
|
3017
|
+
}
|
|
3018
|
+
);
|
|
2807
3019
|
if (result.status === "invalid") {
|
|
2808
3020
|
const invalidPageIndex = pages?.findIndex(
|
|
2809
3021
|
(page) => firstInvalidFieldId === void 0 ? false : page.questionIds.includes(firstInvalidFieldId)
|
|
@@ -2814,13 +3026,30 @@ function ContextFormRenderer({
|
|
|
2814
3026
|
}
|
|
2815
3027
|
if (result.status !== "success") return result;
|
|
2816
3028
|
if (receiptStore !== void 0) {
|
|
3029
|
+
const response = result.response;
|
|
3030
|
+
const submissionId = response?.submissionId ?? submissionAttempt?.attemptId;
|
|
2817
3031
|
const storedReceipt = {
|
|
2818
3032
|
formId: form.schema.id,
|
|
2819
3033
|
formVersion: form.schema.version,
|
|
2820
|
-
submittedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3034
|
+
submittedAt: response?.submittedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
3035
|
+
...submissionId === void 0 ? {} : { submissionId }
|
|
2821
3036
|
};
|
|
2822
|
-
|
|
2823
|
-
|
|
3037
|
+
try {
|
|
3038
|
+
await receiptStore.save(storedReceipt);
|
|
3039
|
+
setReceipt(storedReceipt);
|
|
3040
|
+
} catch (cause) {
|
|
3041
|
+
const error = cause instanceof Error ? cause : new Error(String(cause));
|
|
3042
|
+
try {
|
|
3043
|
+
onReceiptError?.(error, storedReceipt);
|
|
3044
|
+
} catch {
|
|
3045
|
+
}
|
|
3046
|
+
}
|
|
3047
|
+
}
|
|
3048
|
+
if (attemptStore !== void 0 && submissionAttempt !== void 0) {
|
|
3049
|
+
try {
|
|
3050
|
+
await attemptStore.clear(form.schema.id, form.schema.version);
|
|
3051
|
+
} catch {
|
|
3052
|
+
}
|
|
2824
3053
|
}
|
|
2825
3054
|
if (autoSaveKey !== void 0 && typeof globalThis.localStorage !== "undefined") {
|
|
2826
3055
|
globalThis.localStorage.removeItem(autoSaveKey);
|
|
@@ -2860,8 +3089,8 @@ function ContextFormRenderer({
|
|
|
2860
3089
|
receipt,
|
|
2861
3090
|
...receiptStore === void 0 ? {} : { onReset: () => void resetReceipt() }
|
|
2862
3091
|
}) ?? /* @__PURE__ */ jsxs2("div", { role: "status", children: [
|
|
2863
|
-
"
|
|
2864
|
-
receiptStore === void 0 ? null : /* @__PURE__ */ jsx3("button", { type: "button", onClick: () => void resetReceipt(), children: "
|
|
3092
|
+
form.translate("form.alreadySubmitted"),
|
|
3093
|
+
receiptStore === void 0 ? null : /* @__PURE__ */ jsx3("button", { type: "button", onClick: () => void resetReceipt(), children: form.translate("form.submitAnother") })
|
|
2865
3094
|
] }) });
|
|
2866
3095
|
}
|
|
2867
3096
|
return /* @__PURE__ */ jsxs2("form", { ref: formRef, className: `fe-form ${className}`.trim(), noValidate: true, onSubmit: handleSubmit, children: [
|
|
@@ -2930,12 +3159,15 @@ function ContextFormRenderer({
|
|
|
2930
3159
|
guardMessage === null ? null : /* @__PURE__ */ jsx3("div", { role: "alert", children: guardMessage }),
|
|
2931
3160
|
confirmation === null ? null : slots.renderSubmissionConfirmation?.({
|
|
2932
3161
|
findings: confirmation.findings,
|
|
3162
|
+
message: confirmation.message ?? form.translate("form.confirmSensitiveData"),
|
|
3163
|
+
schema: form.schema,
|
|
3164
|
+
visibleValues,
|
|
2933
3165
|
onConfirm: confirmSubmission,
|
|
2934
3166
|
onCancel: cancelSubmission
|
|
2935
3167
|
}) ?? /* @__PURE__ */ jsxs2("div", { className: "fe-submission-confirmation", role: "dialog", "aria-modal": "true", children: [
|
|
2936
|
-
/* @__PURE__ */ jsx3("p", { children: confirmation.message ?? "
|
|
2937
|
-
/* @__PURE__ */ jsx3("button", { type: "button", onClick: confirmSubmission, children: "
|
|
2938
|
-
/* @__PURE__ */ jsx3("button", { type: "button", onClick: cancelSubmission, children: "
|
|
3168
|
+
/* @__PURE__ */ jsx3("p", { children: confirmation.message ?? form.translate("form.confirmSensitiveData") }),
|
|
3169
|
+
/* @__PURE__ */ jsx3("button", { type: "button", onClick: confirmSubmission, children: form.translate("form.confirmSubmission") }),
|
|
3170
|
+
/* @__PURE__ */ jsx3("button", { type: "button", onClick: cancelSubmission, children: form.translate("form.cancelSubmission") })
|
|
2939
3171
|
] }),
|
|
2940
3172
|
validationIssues.length === 0 ? null : slots.renderValidationSummary?.({ issues: validationIssues }) ?? /* @__PURE__ */ jsxs2("div", { className: "fe-validation-summary", role: "alert", children: [
|
|
2941
3173
|
validationIssues.length,
|
|
@@ -2992,6 +3224,12 @@ var RENDERER_MESSAGES = {
|
|
|
2992
3224
|
"form.next": "Next",
|
|
2993
3225
|
"form.step": "Step {{current}} / {{total}}",
|
|
2994
3226
|
"form.draftRestored": "Draft restored",
|
|
3227
|
+
"form.submissionBlocked": "Submission blocked because sensitive data was detected.",
|
|
3228
|
+
"form.confirmSensitiveData": "Sensitive data may be included. Confirm before submitting.",
|
|
3229
|
+
"form.confirmSubmission": "Confirm submission",
|
|
3230
|
+
"form.cancelSubmission": "Cancel",
|
|
3231
|
+
"form.alreadySubmitted": "Already submitted.",
|
|
3232
|
+
"form.submitAnother": "Submit another response",
|
|
2995
3233
|
"validation.required": "This field is required."
|
|
2996
3234
|
};
|
|
2997
3235
|
var defaultRendererTranslator = {
|
|
@@ -3030,9 +3268,12 @@ export {
|
|
|
3030
3268
|
FormBuilder,
|
|
3031
3269
|
FormProvider,
|
|
3032
3270
|
FormRenderer,
|
|
3271
|
+
createLocalStorageSubmissionAttemptStore,
|
|
3033
3272
|
createLocalStorageSubmissionReceiptStore,
|
|
3034
3273
|
resolveInitialFieldType,
|
|
3274
|
+
submissionReceiptQueryKey,
|
|
3035
3275
|
useField,
|
|
3036
3276
|
useForm,
|
|
3037
|
-
useFormBuilder
|
|
3277
|
+
useFormBuilder,
|
|
3278
|
+
useSubmissionReceipts
|
|
3038
3279
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@form-engine-ts/react",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.9.0",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -42,8 +42,8 @@
|
|
|
42
42
|
"typescript"
|
|
43
43
|
],
|
|
44
44
|
"dependencies": {
|
|
45
|
-
"@form-engine-ts/core": "2.
|
|
46
|
-
"@form-engine-ts/privacy": "2.
|
|
45
|
+
"@form-engine-ts/core": "2.9.0",
|
|
46
|
+
"@form-engine-ts/privacy": "2.9.0"
|
|
47
47
|
},
|
|
48
48
|
"peerDependencies": {
|
|
49
49
|
"react": ">=18.2 <20",
|