@form-engine-ts/react 2.7.0 → 2.8.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 +5 -0
- package/dist/index.cjs +150 -43
- package/dist/index.d.cts +31 -9
- package/dist/index.d.ts +31 -9
- package/dist/index.js +144 -39
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -123,3 +123,8 @@ 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.
|
package/dist/index.cjs
CHANGED
|
@@ -25,9 +25,11 @@ __export(index_exports, {
|
|
|
25
25
|
FormRenderer: () => FormRenderer,
|
|
26
26
|
createLocalStorageSubmissionReceiptStore: () => createLocalStorageSubmissionReceiptStore,
|
|
27
27
|
resolveInitialFieldType: () => resolveInitialFieldType,
|
|
28
|
+
submissionReceiptQueryKey: () => submissionReceiptQueryKey,
|
|
28
29
|
useField: () => useField,
|
|
29
30
|
useForm: () => useForm,
|
|
30
|
-
useFormBuilder: () => useFormBuilder
|
|
31
|
+
useFormBuilder: () => useFormBuilder,
|
|
32
|
+
useSubmissionReceipts: () => useSubmissionReceipts
|
|
31
33
|
});
|
|
32
34
|
module.exports = __toCommonJS(index_exports);
|
|
33
35
|
|
|
@@ -972,6 +974,9 @@ var FIELD_TYPES = [
|
|
|
972
974
|
];
|
|
973
975
|
var BUILDER_DEFAULTS = {
|
|
974
976
|
"builder.formBuilder": "Form builder",
|
|
977
|
+
"builder.basicSettings": "Basic settings",
|
|
978
|
+
"builder.formTitle": "Form title",
|
|
979
|
+
"builder.formDescription": "Form description",
|
|
975
980
|
"builder.moveUp": "Move {{title}} up",
|
|
976
981
|
"builder.moveDown": "Move {{title}} down",
|
|
977
982
|
"builder.delete": "Delete {{title}}",
|
|
@@ -1137,7 +1142,7 @@ function FormBuilder({
|
|
|
1137
1142
|
}) {
|
|
1138
1143
|
const resolvedComponents = { ...DEFAULT_COMPONENTS, ...componentOverrides };
|
|
1139
1144
|
const components = GUARDED_COMPONENTS;
|
|
1140
|
-
const { Button, Checkbox, ErrorMessage, Fieldset, IconButton, Section, Select, TextInput } = components;
|
|
1145
|
+
const { Button, Checkbox, ErrorMessage, Fieldset, IconButton, Section, Select, TextArea, TextInput } = components;
|
|
1141
1146
|
const ToolbarSlot = slots?.toolbar;
|
|
1142
1147
|
const FieldEditorSlot = slots?.fieldEditor;
|
|
1143
1148
|
const OptionEditorSlot = slots?.optionEditor;
|
|
@@ -1440,6 +1445,42 @@ function FormBuilder({
|
|
|
1440
1445
|
}
|
|
1441
1446
|
},
|
|
1442
1447
|
children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Fieldset, { className: "form-engine-builder__controls", disabled: readOnly, children: [
|
|
1448
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1449
|
+
Section,
|
|
1450
|
+
{
|
|
1451
|
+
className: "form-engine-builder__basic-settings",
|
|
1452
|
+
headingId: "builder-basic-settings-heading",
|
|
1453
|
+
title: translate("builder.basicSettings"),
|
|
1454
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "form-engine-builder__grid", children: [
|
|
1455
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { children: [
|
|
1456
|
+
translate("builder.formTitle"),
|
|
1457
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1458
|
+
TextInput,
|
|
1459
|
+
{
|
|
1460
|
+
name: "title",
|
|
1461
|
+
required: true,
|
|
1462
|
+
error: schema.title.trim().length === 0,
|
|
1463
|
+
helperText: schema.title.trim().length === 0 ? translate("builder.required") : "",
|
|
1464
|
+
value: schema.title,
|
|
1465
|
+
onChange: (value) => setSourceText({ kind: "form" }, "title", value)
|
|
1466
|
+
}
|
|
1467
|
+
)
|
|
1468
|
+
] }),
|
|
1469
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { children: [
|
|
1470
|
+
translate("builder.formDescription"),
|
|
1471
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1472
|
+
TextArea,
|
|
1473
|
+
{
|
|
1474
|
+
name: "description",
|
|
1475
|
+
rows: 3,
|
|
1476
|
+
value: schema.description ?? "",
|
|
1477
|
+
onChange: (value) => setSourceText({ kind: "form" }, "description", value)
|
|
1478
|
+
}
|
|
1479
|
+
)
|
|
1480
|
+
] })
|
|
1481
|
+
] })
|
|
1482
|
+
}
|
|
1483
|
+
),
|
|
1443
1484
|
pagesEnabled ? PagesSlot === void 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1444
1485
|
Section,
|
|
1445
1486
|
{
|
|
@@ -2300,10 +2341,10 @@ function FormProvider({
|
|
|
2300
2341
|
setSubmitStatus("idle");
|
|
2301
2342
|
return { status: "cancelled" };
|
|
2302
2343
|
}
|
|
2303
|
-
await onSubmit(visibleValues);
|
|
2344
|
+
const response = await onSubmit(visibleValues);
|
|
2304
2345
|
if (resetOnSuccess) setValues({ ...initialValues });
|
|
2305
2346
|
setSubmitStatus("success");
|
|
2306
|
-
return { status: "success" };
|
|
2347
|
+
return response === void 0 ? { status: "success" } : { status: "success", response };
|
|
2307
2348
|
} catch (cause) {
|
|
2308
2349
|
const error = cause instanceof Error ? cause : new Error(String(cause));
|
|
2309
2350
|
setSubmitError(error);
|
|
@@ -2372,6 +2413,10 @@ function useField(fieldId) {
|
|
|
2372
2413
|
}
|
|
2373
2414
|
|
|
2374
2415
|
// src/receipt.ts
|
|
2416
|
+
var import_react4 = require("react");
|
|
2417
|
+
function submissionReceiptQueryKey(formId, formVersion) {
|
|
2418
|
+
return `${formId}:v${formVersion}`;
|
|
2419
|
+
}
|
|
2375
2420
|
function receiptKey(namespace, formId, formVersion) {
|
|
2376
2421
|
return `${namespace}:${formId}:v${formVersion}`;
|
|
2377
2422
|
}
|
|
@@ -2403,18 +2448,27 @@ function parseReceipt(serialized) {
|
|
|
2403
2448
|
function createLocalStorageSubmissionReceiptStore(options = {}) {
|
|
2404
2449
|
const namespace = options.namespace ?? "form_engine_receipt";
|
|
2405
2450
|
if (namespace.trim().length === 0) throw new TypeError("Receipt namespace must not be empty.");
|
|
2451
|
+
const get = async (formId, formVersion) => {
|
|
2452
|
+
const storage = browserStorage();
|
|
2453
|
+
if (storage === null) return null;
|
|
2454
|
+
try {
|
|
2455
|
+
const serialized = storage.getItem(receiptKey(namespace, formId, formVersion));
|
|
2456
|
+
if (serialized === null) return null;
|
|
2457
|
+
const receipt = parseReceipt(serialized);
|
|
2458
|
+
return receipt?.formId === formId && receipt.formVersion === formVersion ? receipt : null;
|
|
2459
|
+
} catch {
|
|
2460
|
+
return null;
|
|
2461
|
+
}
|
|
2462
|
+
};
|
|
2406
2463
|
return {
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
} catch {
|
|
2416
|
-
return null;
|
|
2417
|
-
}
|
|
2464
|
+
get,
|
|
2465
|
+
async getBatch(queries) {
|
|
2466
|
+
const receipts = await Promise.all(queries.map((query) => get(query.formId, query.formVersion)));
|
|
2467
|
+
return new Map(
|
|
2468
|
+
receipts.flatMap(
|
|
2469
|
+
(receipt) => receipt === null ? [] : [[submissionReceiptQueryKey(receipt.formId, receipt.formVersion), receipt]]
|
|
2470
|
+
)
|
|
2471
|
+
);
|
|
2418
2472
|
},
|
|
2419
2473
|
async save(receipt) {
|
|
2420
2474
|
const storage = browserStorage();
|
|
@@ -2428,10 +2482,49 @@ function createLocalStorageSubmissionReceiptStore(options = {}) {
|
|
|
2428
2482
|
}
|
|
2429
2483
|
};
|
|
2430
2484
|
}
|
|
2485
|
+
function useSubmissionReceipts(store, queries) {
|
|
2486
|
+
const querySignature = JSON.stringify(queries.map(({ formId, formVersion }) => [formId, formVersion]));
|
|
2487
|
+
const stableQueries = (0, import_react4.useMemo)(() => {
|
|
2488
|
+
const parsed = JSON.parse(querySignature);
|
|
2489
|
+
if (!Array.isArray(parsed)) return [];
|
|
2490
|
+
return parsed.flatMap(
|
|
2491
|
+
(entry) => Array.isArray(entry) && typeof entry[0] === "string" && typeof entry[1] === "number" && Number.isSafeInteger(entry[1]) ? [{ formId: entry[0], formVersion: entry[1] }] : []
|
|
2492
|
+
);
|
|
2493
|
+
}, [querySignature]);
|
|
2494
|
+
const [state, setState] = (0, import_react4.useState)({
|
|
2495
|
+
receipts: /* @__PURE__ */ new Map(),
|
|
2496
|
+
isLoading: stableQueries.length > 0,
|
|
2497
|
+
error: null
|
|
2498
|
+
});
|
|
2499
|
+
(0, import_react4.useEffect)(() => {
|
|
2500
|
+
let active = true;
|
|
2501
|
+
if (stableQueries.length === 0) {
|
|
2502
|
+
setState({ receipts: /* @__PURE__ */ new Map(), isLoading: false, error: null });
|
|
2503
|
+
return () => {
|
|
2504
|
+
active = false;
|
|
2505
|
+
};
|
|
2506
|
+
}
|
|
2507
|
+
setState((current) => ({ ...current, isLoading: true, error: null }));
|
|
2508
|
+
void store.getBatch(stableQueries).then((receipts) => {
|
|
2509
|
+
if (active) setState({ receipts, isLoading: false, error: null });
|
|
2510
|
+
}).catch((cause) => {
|
|
2511
|
+
if (!active) return;
|
|
2512
|
+
setState({
|
|
2513
|
+
receipts: /* @__PURE__ */ new Map(),
|
|
2514
|
+
isLoading: false,
|
|
2515
|
+
error: cause instanceof Error ? cause : new Error(String(cause))
|
|
2516
|
+
});
|
|
2517
|
+
});
|
|
2518
|
+
return () => {
|
|
2519
|
+
active = false;
|
|
2520
|
+
};
|
|
2521
|
+
}, [stableQueries, store]);
|
|
2522
|
+
return state;
|
|
2523
|
+
}
|
|
2431
2524
|
|
|
2432
2525
|
// src/renderer.tsx
|
|
2433
2526
|
var import_core4 = require("@form-engine-ts/core");
|
|
2434
|
-
var
|
|
2527
|
+
var import_react5 = require("react");
|
|
2435
2528
|
var import_jsx_runtime3 = require("react/jsx-runtime");
|
|
2436
2529
|
function describedBy(field, error, helpId, errorId) {
|
|
2437
2530
|
const ids = [field.description === void 0 ? void 0 : helpId, error === void 0 ? void 0 : errorId].filter(
|
|
@@ -2659,28 +2752,29 @@ function ContextFormRenderer({
|
|
|
2659
2752
|
slots = {}
|
|
2660
2753
|
}) {
|
|
2661
2754
|
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,
|
|
2755
|
+
const prefix = (0, import_react5.useId)().replace(/:/g, "");
|
|
2756
|
+
const formRef = (0, import_react5.useRef)(null);
|
|
2757
|
+
const loadedDraftKey = (0, import_react5.useRef)(null);
|
|
2758
|
+
const [draftRestored, setDraftRestored] = (0, import_react5.useState)(false);
|
|
2759
|
+
const [currentPageIndex, setCurrentPageIndex] = (0, import_react5.useState)(0);
|
|
2760
|
+
const [focusFieldId, setFocusFieldId] = (0, import_react5.useState)(null);
|
|
2761
|
+
const [confirmation, setConfirmation] = (0, import_react5.useState)(null);
|
|
2762
|
+
const [guardMessage, setGuardMessage] = (0, import_react5.useState)(null);
|
|
2763
|
+
const [receipt, setReceipt] = (0, import_react5.useState)(null);
|
|
2764
|
+
const [receiptLoaded, setReceiptLoaded] = (0, import_react5.useState)(receiptStore === void 0);
|
|
2765
|
+
const rendererSubmissionInFlight = (0, import_react5.useRef)(false);
|
|
2673
2766
|
const pages = form.schema.pages;
|
|
2674
|
-
const visiblePageIndexes = (0,
|
|
2767
|
+
const visiblePageIndexes = (0, import_react5.useMemo)(
|
|
2675
2768
|
() => pages === void 0 ? [] : pages.flatMap((page, index) => form.pageVisibility[page.id] === true ? [index] : []),
|
|
2676
2769
|
[form.pageVisibility, pages]
|
|
2677
2770
|
);
|
|
2678
2771
|
const activePage = pages?.[currentPageIndex];
|
|
2679
2772
|
const activeVisibleIndex = visiblePageIndexes.indexOf(currentPageIndex);
|
|
2680
2773
|
const fieldIds = activePage === void 0 ? void 0 : new Set(activePage.questionIds);
|
|
2774
|
+
const visibleValues = (0, import_react5.useMemo)(() => (0, import_core4.selectVisibleAnswers)(form.schema, form.values), [form.schema, form.values]);
|
|
2681
2775
|
const submitState = confirmation === null ? form.submitStatus : "confirming";
|
|
2682
2776
|
const interactionLocked = submitState === "confirming" || submitState === "submitting";
|
|
2683
|
-
(0,
|
|
2777
|
+
(0, import_react5.useEffect)(() => {
|
|
2684
2778
|
let active = true;
|
|
2685
2779
|
if (receiptStore === void 0) {
|
|
2686
2780
|
setReceipt(null);
|
|
@@ -2701,14 +2795,14 @@ function ContextFormRenderer({
|
|
|
2701
2795
|
active = false;
|
|
2702
2796
|
};
|
|
2703
2797
|
}, [form.schema.id, form.schema.version, receiptStore]);
|
|
2704
|
-
(0,
|
|
2798
|
+
(0, import_react5.useEffect)(() => {
|
|
2705
2799
|
if (pages === void 0 || visiblePageIndexes.length === 0) {
|
|
2706
2800
|
setCurrentPageIndex(0);
|
|
2707
2801
|
return;
|
|
2708
2802
|
}
|
|
2709
2803
|
if (!visiblePageIndexes.includes(currentPageIndex)) setCurrentPageIndex(visiblePageIndexes[0] ?? 0);
|
|
2710
2804
|
}, [currentPageIndex, pages, visiblePageIndexes]);
|
|
2711
|
-
(0,
|
|
2805
|
+
(0, import_react5.useEffect)(() => {
|
|
2712
2806
|
if (focusFieldId === null) return;
|
|
2713
2807
|
const fieldContainer = [...formRef.current?.querySelectorAll("[data-field-id]") ?? []].find(
|
|
2714
2808
|
(element) => element.dataset.fieldId === focusFieldId
|
|
@@ -2719,7 +2813,7 @@ function ContextFormRenderer({
|
|
|
2719
2813
|
setFocusFieldId(null);
|
|
2720
2814
|
}
|
|
2721
2815
|
}, [focusFieldId]);
|
|
2722
|
-
(0,
|
|
2816
|
+
(0, import_react5.useEffect)(() => {
|
|
2723
2817
|
if (autoSaveKey === void 0 || typeof globalThis.localStorage === "undefined") return;
|
|
2724
2818
|
const loadIdentity = `${autoSaveKey}:${form.schema.id}:${form.schema.version}`;
|
|
2725
2819
|
if (loadedDraftKey.current === loadIdentity) return;
|
|
@@ -2731,7 +2825,7 @@ function ContextFormRenderer({
|
|
|
2731
2825
|
form.restoreValues(draft.values);
|
|
2732
2826
|
setDraftRestored(true);
|
|
2733
2827
|
}, [autoSaveKey, form.restoreValues, form.schema.id, form.schema.version]);
|
|
2734
|
-
(0,
|
|
2828
|
+
(0, import_react5.useEffect)(() => {
|
|
2735
2829
|
if (form.submitStatus === "success") return;
|
|
2736
2830
|
const timeout = globalThis.setTimeout(() => {
|
|
2737
2831
|
onDraftSave?.(form.values);
|
|
@@ -2764,7 +2858,7 @@ function ContextFormRenderer({
|
|
|
2764
2858
|
let confirmationMessage;
|
|
2765
2859
|
let requiresConfirmation = false;
|
|
2766
2860
|
for (const guard of guards) {
|
|
2767
|
-
const result = await guard(form.schema,
|
|
2861
|
+
const result = await guard(form.schema, visibleValues);
|
|
2768
2862
|
if (result.status === "allow") continue;
|
|
2769
2863
|
findings.push(...result.findings);
|
|
2770
2864
|
if (result.status === "block") {
|
|
@@ -2794,7 +2888,7 @@ function ContextFormRenderer({
|
|
|
2794
2888
|
try {
|
|
2795
2889
|
const guardResult = await runSubmissionGuards(submissionGuards);
|
|
2796
2890
|
if (guardResult.status === "block") {
|
|
2797
|
-
setGuardMessage(guardResult.message ?? "
|
|
2891
|
+
setGuardMessage(guardResult.message ?? form.translate("form.submissionBlocked"));
|
|
2798
2892
|
return { status: "cancelled" };
|
|
2799
2893
|
}
|
|
2800
2894
|
if (guardResult.status === "confirm") {
|
|
@@ -2823,10 +2917,12 @@ function ContextFormRenderer({
|
|
|
2823
2917
|
}
|
|
2824
2918
|
if (result.status !== "success") return result;
|
|
2825
2919
|
if (receiptStore !== void 0) {
|
|
2920
|
+
const response = result.response;
|
|
2826
2921
|
const storedReceipt = {
|
|
2827
2922
|
formId: form.schema.id,
|
|
2828
2923
|
formVersion: form.schema.version,
|
|
2829
|
-
submittedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2924
|
+
submittedAt: response?.submittedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
2925
|
+
...response?.submissionId === void 0 ? {} : { submissionId: response.submissionId }
|
|
2830
2926
|
};
|
|
2831
2927
|
await receiptStore.save(storedReceipt);
|
|
2832
2928
|
setReceipt(storedReceipt);
|
|
@@ -2869,8 +2965,8 @@ function ContextFormRenderer({
|
|
|
2869
2965
|
receipt,
|
|
2870
2966
|
...receiptStore === void 0 ? {} : { onReset: () => void resetReceipt() }
|
|
2871
2967
|
}) ?? /* @__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: "
|
|
2968
|
+
form.translate("form.alreadySubmitted"),
|
|
2969
|
+
receiptStore === void 0 ? null : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", onClick: () => void resetReceipt(), children: form.translate("form.submitAnother") })
|
|
2874
2970
|
] }) });
|
|
2875
2971
|
}
|
|
2876
2972
|
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("form", { ref: formRef, className: `fe-form ${className}`.trim(), noValidate: true, onSubmit: handleSubmit, children: [
|
|
@@ -2924,7 +3020,7 @@ function ContextFormRenderer({
|
|
|
2924
3020
|
...slots.renderCharacterCount === void 0 ? {} : { renderCharacterCount: slots.renderCharacterCount }
|
|
2925
3021
|
};
|
|
2926
3022
|
if (slots.renderField !== void 0) {
|
|
2927
|
-
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
3023
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react5.Fragment, { children: slots.renderField({
|
|
2928
3024
|
question: field,
|
|
2929
3025
|
value: form.values[field.id],
|
|
2930
3026
|
onChange: (value) => {
|
|
@@ -2939,12 +3035,15 @@ function ContextFormRenderer({
|
|
|
2939
3035
|
guardMessage === null ? null : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { role: "alert", children: guardMessage }),
|
|
2940
3036
|
confirmation === null ? null : slots.renderSubmissionConfirmation?.({
|
|
2941
3037
|
findings: confirmation.findings,
|
|
3038
|
+
message: confirmation.message ?? form.translate("form.confirmSensitiveData"),
|
|
3039
|
+
schema: form.schema,
|
|
3040
|
+
visibleValues,
|
|
2942
3041
|
onConfirm: confirmSubmission,
|
|
2943
3042
|
onCancel: cancelSubmission
|
|
2944
3043
|
}) ?? /* @__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: "
|
|
3044
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { children: confirmation.message ?? form.translate("form.confirmSensitiveData") }),
|
|
3045
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", onClick: confirmSubmission, children: form.translate("form.confirmSubmission") }),
|
|
3046
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", onClick: cancelSubmission, children: form.translate("form.cancelSubmission") })
|
|
2948
3047
|
] }),
|
|
2949
3048
|
validationIssues.length === 0 ? null : slots.renderValidationSummary?.({ issues: validationIssues }) ?? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "fe-validation-summary", role: "alert", children: [
|
|
2950
3049
|
validationIssues.length,
|
|
@@ -3001,6 +3100,12 @@ var RENDERER_MESSAGES = {
|
|
|
3001
3100
|
"form.next": "Next",
|
|
3002
3101
|
"form.step": "Step {{current}} / {{total}}",
|
|
3003
3102
|
"form.draftRestored": "Draft restored",
|
|
3103
|
+
"form.submissionBlocked": "Submission blocked because sensitive data was detected.",
|
|
3104
|
+
"form.confirmSensitiveData": "Sensitive data may be included. Confirm before submitting.",
|
|
3105
|
+
"form.confirmSubmission": "Confirm submission",
|
|
3106
|
+
"form.cancelSubmission": "Cancel",
|
|
3107
|
+
"form.alreadySubmitted": "Already submitted.",
|
|
3108
|
+
"form.submitAnother": "Submit another response",
|
|
3004
3109
|
"validation.required": "This field is required."
|
|
3005
3110
|
};
|
|
3006
3111
|
var defaultRendererTranslator = {
|
|
@@ -3042,7 +3147,9 @@ function FormRenderer(props) {
|
|
|
3042
3147
|
FormRenderer,
|
|
3043
3148
|
createLocalStorageSubmissionReceiptStore,
|
|
3044
3149
|
resolveInitialFieldType,
|
|
3150
|
+
submissionReceiptQueryKey,
|
|
3045
3151
|
useField,
|
|
3046
3152
|
useForm,
|
|
3047
|
-
useFormBuilder
|
|
3153
|
+
useFormBuilder,
|
|
3154
|
+
useSubmissionReceipts
|
|
3048
3155
|
});
|
package/dist/index.d.cts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
2
|
import { ReactNode, ComponentType, MouseEvent } from 'react';
|
|
3
|
-
import { QuestionType, FormField, ChoiceOption, FormPage, FormSchema, DisplayCondition, JsonValue, SchemaIssue, FormPolicy, FieldOption, TranslationReport, ValidationError, TranslationAdapter, AsyncTranslationAdapter, PopulateTranslationOptions, FormValue, ValidationIssue,
|
|
3
|
+
import { QuestionType, FormField, ChoiceOption, FormPage, FormSchema, DisplayCondition, JsonValue, SchemaIssue, FormPolicy, FieldOption, TranslationReport, ValidationError, FormValues, TranslationAdapter, AsyncTranslationAdapter, PopulateTranslationOptions, FormValue, ValidationIssue, AnswerValidationResult, FieldType } from '@form-engine-ts/core';
|
|
4
4
|
import { SensitiveDataFinding } from '@form-engine-ts/privacy';
|
|
5
5
|
|
|
6
6
|
/** @deprecated Import FormPolicy from @form-engine-ts/core instead. */
|
|
@@ -91,14 +91,26 @@ interface SubmissionReceipt {
|
|
|
91
91
|
readonly submissionId?: string;
|
|
92
92
|
readonly submittedAt: string;
|
|
93
93
|
}
|
|
94
|
+
interface SubmissionReceiptQuery {
|
|
95
|
+
readonly formId: string;
|
|
96
|
+
readonly formVersion: number;
|
|
97
|
+
}
|
|
94
98
|
interface SubmissionReceiptStore {
|
|
95
99
|
get(formId: string, formVersion: number): Promise<SubmissionReceipt | null>;
|
|
100
|
+
getBatch(queries: readonly SubmissionReceiptQuery[]): Promise<Map<string, SubmissionReceipt>>;
|
|
96
101
|
save(receipt: SubmissionReceipt): Promise<void>;
|
|
97
102
|
remove(formId: string, formVersion: number): Promise<void>;
|
|
98
103
|
}
|
|
104
|
+
interface UseSubmissionReceiptsResult {
|
|
105
|
+
readonly receipts: ReadonlyMap<string, SubmissionReceipt>;
|
|
106
|
+
readonly isLoading: boolean;
|
|
107
|
+
readonly error: Error | null;
|
|
108
|
+
}
|
|
109
|
+
declare function submissionReceiptQueryKey(formId: string, formVersion: number): string;
|
|
99
110
|
declare function createLocalStorageSubmissionReceiptStore(options?: {
|
|
100
111
|
readonly namespace?: string;
|
|
101
112
|
}): SubmissionReceiptStore;
|
|
113
|
+
declare function useSubmissionReceipts(store: SubmissionReceiptStore, queries: readonly SubmissionReceiptQuery[]): UseSubmissionReceiptsResult;
|
|
102
114
|
|
|
103
115
|
interface ComponentBaseProps {
|
|
104
116
|
readonly id?: string;
|
|
@@ -261,10 +273,16 @@ type SubmitResult = {
|
|
|
261
273
|
readonly status: "cancelled";
|
|
262
274
|
} | {
|
|
263
275
|
readonly status: "success";
|
|
276
|
+
readonly response?: SubmitResponse;
|
|
264
277
|
} | {
|
|
265
278
|
readonly status: "error";
|
|
266
279
|
readonly error: Error;
|
|
267
280
|
};
|
|
281
|
+
interface SubmitResponse {
|
|
282
|
+
readonly submissionId?: string;
|
|
283
|
+
readonly submittedAt?: string;
|
|
284
|
+
}
|
|
285
|
+
type FormSubmitHandler = (answers: FormValues) => SubmitResponse | void | Promise<SubmitResponse | undefined> | Promise<void>;
|
|
268
286
|
type SubmissionGuardResult = {
|
|
269
287
|
readonly status: "allow";
|
|
270
288
|
} | {
|
|
@@ -278,6 +296,14 @@ type SubmissionGuardResult = {
|
|
|
278
296
|
};
|
|
279
297
|
type SubmissionGuard = (schema: FormSchema, values: Record<string, unknown>) => SubmissionGuardResult | Promise<SubmissionGuardResult>;
|
|
280
298
|
type FormSubmitState = "idle" | "submitting" | "confirming" | "success" | "error";
|
|
299
|
+
interface SubmissionConfirmationSlotProps {
|
|
300
|
+
readonly findings: readonly SensitiveDataFinding[];
|
|
301
|
+
readonly message?: string;
|
|
302
|
+
readonly schema: FormSchema;
|
|
303
|
+
readonly visibleValues: Record<string, unknown>;
|
|
304
|
+
readonly onConfirm: () => void;
|
|
305
|
+
readonly onCancel: () => void;
|
|
306
|
+
}
|
|
281
307
|
interface FormRendererSlots {
|
|
282
308
|
readonly renderHeader?: (props: {
|
|
283
309
|
readonly title: string;
|
|
@@ -316,11 +342,7 @@ interface FormRendererSlots {
|
|
|
316
342
|
readonly error: Error;
|
|
317
343
|
readonly onRetry?: () => void;
|
|
318
344
|
}) => ReactNode;
|
|
319
|
-
readonly renderSubmissionConfirmation?: (props:
|
|
320
|
-
readonly findings: readonly SensitiveDataFinding[];
|
|
321
|
-
readonly onConfirm: () => void;
|
|
322
|
-
readonly onCancel: () => void;
|
|
323
|
-
}) => ReactNode;
|
|
345
|
+
readonly renderSubmissionConfirmation?: (props: SubmissionConfirmationSlotProps) => ReactNode;
|
|
324
346
|
readonly renderAlreadySubmitted?: (props: {
|
|
325
347
|
readonly receipt: SubmissionReceipt;
|
|
326
348
|
readonly onReset?: () => void;
|
|
@@ -390,7 +412,7 @@ interface FormProviderProps {
|
|
|
390
412
|
readonly translator: TranslationAdapter;
|
|
391
413
|
readonly initialValues?: FormValues;
|
|
392
414
|
readonly resetOnSuccess?: boolean;
|
|
393
|
-
readonly onSubmit:
|
|
415
|
+
readonly onSubmit: FormSubmitHandler;
|
|
394
416
|
readonly children: ReactNode;
|
|
395
417
|
}
|
|
396
418
|
declare function FormProvider({ schema, locale, translator, initialValues, resetOnSuccess, onSubmit, children }: FormProviderProps): react.JSX.Element;
|
|
@@ -431,9 +453,9 @@ interface StandaloneFormRendererProps extends FormRendererPresentationProps {
|
|
|
431
453
|
readonly translator?: TranslationAdapter;
|
|
432
454
|
readonly initialValues?: FormValues;
|
|
433
455
|
readonly resetOnSuccess?: boolean;
|
|
434
|
-
readonly onSubmit:
|
|
456
|
+
readonly onSubmit: FormSubmitHandler;
|
|
435
457
|
}
|
|
436
458
|
type FormRendererProps = FormRendererPresentationProps | StandaloneFormRendererProps;
|
|
437
459
|
declare function FormRenderer(props: FormRendererProps): react.JSX.Element;
|
|
438
460
|
|
|
439
|
-
export { type BeforeSubmit, type BuilderActionContext, type BuilderActionError, type BuilderActionResult, type BuilderButtonProps, type BuilderCheckboxProps, type BuilderFactories, type BuilderFieldEditorSlotProps, type BuilderFieldsetProps, type BuilderIconButtonProps, type BuilderIdKind, type BuilderLocalizationSlotProps, type BuilderOptionEditorSlotProps, type BuilderPagesSlotProps, type BuilderPolicy, type BuilderSectionProps, type BuilderSelectOption, type BuilderSelectProps, type BuilderTextAreaProps, type BuilderTextInputProps, type BuilderTextTarget, type BuilderToolbarSlotProps, type BuilderTranslationActionsSlotProps, type ComponentBaseProps, type FieldComponentProps, type FieldComponents, type FieldState, FormBuilder, type FormBuilderActions, type FormBuilderComponents, type FormBuilderFeatures, type FormBuilderOptions, type FormBuilderProps, type FormBuilderResult, type FormBuilderSlots, type FormContextValue, FormProvider, type FormProviderProps, FormRenderer, type FormRendererPresentationProps, type FormRendererProps, type FormRendererSlots, type FormSubmitState, type InputComponentProps, type ManualTranslationContext, type StandaloneFormRendererProps, type SubmissionGuard, type SubmissionGuardResult, type SubmissionProtectionProps, type SubmissionReceipt, type SubmissionReceiptStore, type SubmitResult, type SubmitStatus, createLocalStorageSubmissionReceiptStore, resolveInitialFieldType, useField, useForm, useFormBuilder };
|
|
461
|
+
export { type BeforeSubmit, type BuilderActionContext, type BuilderActionError, type BuilderActionResult, type BuilderButtonProps, type BuilderCheckboxProps, type BuilderFactories, type BuilderFieldEditorSlotProps, type BuilderFieldsetProps, type BuilderIconButtonProps, type BuilderIdKind, type BuilderLocalizationSlotProps, type BuilderOptionEditorSlotProps, type BuilderPagesSlotProps, type BuilderPolicy, type BuilderSectionProps, type BuilderSelectOption, type BuilderSelectProps, type BuilderTextAreaProps, type BuilderTextInputProps, type BuilderTextTarget, type BuilderToolbarSlotProps, type BuilderTranslationActionsSlotProps, type ComponentBaseProps, type FieldComponentProps, type FieldComponents, type FieldState, FormBuilder, type FormBuilderActions, type FormBuilderComponents, type FormBuilderFeatures, type FormBuilderOptions, type FormBuilderProps, type FormBuilderResult, type FormBuilderSlots, type FormContextValue, FormProvider, type FormProviderProps, FormRenderer, type FormRendererPresentationProps, type FormRendererProps, type FormRendererSlots, type FormSubmitHandler, type FormSubmitState, type InputComponentProps, type ManualTranslationContext, type StandaloneFormRendererProps, type SubmissionConfirmationSlotProps, type SubmissionGuard, type SubmissionGuardResult, type SubmissionProtectionProps, type SubmissionReceipt, type SubmissionReceiptQuery, type SubmissionReceiptStore, type SubmitResponse, type SubmitResult, type SubmitStatus, type UseSubmissionReceiptsResult, createLocalStorageSubmissionReceiptStore, resolveInitialFieldType, submissionReceiptQueryKey, useField, useForm, useFormBuilder, useSubmissionReceipts };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
2
|
import { ReactNode, ComponentType, MouseEvent } from 'react';
|
|
3
|
-
import { QuestionType, FormField, ChoiceOption, FormPage, FormSchema, DisplayCondition, JsonValue, SchemaIssue, FormPolicy, FieldOption, TranslationReport, ValidationError, TranslationAdapter, AsyncTranslationAdapter, PopulateTranslationOptions, FormValue, ValidationIssue,
|
|
3
|
+
import { QuestionType, FormField, ChoiceOption, FormPage, FormSchema, DisplayCondition, JsonValue, SchemaIssue, FormPolicy, FieldOption, TranslationReport, ValidationError, FormValues, TranslationAdapter, AsyncTranslationAdapter, PopulateTranslationOptions, FormValue, ValidationIssue, AnswerValidationResult, FieldType } from '@form-engine-ts/core';
|
|
4
4
|
import { SensitiveDataFinding } from '@form-engine-ts/privacy';
|
|
5
5
|
|
|
6
6
|
/** @deprecated Import FormPolicy from @form-engine-ts/core instead. */
|
|
@@ -91,14 +91,26 @@ interface SubmissionReceipt {
|
|
|
91
91
|
readonly submissionId?: string;
|
|
92
92
|
readonly submittedAt: string;
|
|
93
93
|
}
|
|
94
|
+
interface SubmissionReceiptQuery {
|
|
95
|
+
readonly formId: string;
|
|
96
|
+
readonly formVersion: number;
|
|
97
|
+
}
|
|
94
98
|
interface SubmissionReceiptStore {
|
|
95
99
|
get(formId: string, formVersion: number): Promise<SubmissionReceipt | null>;
|
|
100
|
+
getBatch(queries: readonly SubmissionReceiptQuery[]): Promise<Map<string, SubmissionReceipt>>;
|
|
96
101
|
save(receipt: SubmissionReceipt): Promise<void>;
|
|
97
102
|
remove(formId: string, formVersion: number): Promise<void>;
|
|
98
103
|
}
|
|
104
|
+
interface UseSubmissionReceiptsResult {
|
|
105
|
+
readonly receipts: ReadonlyMap<string, SubmissionReceipt>;
|
|
106
|
+
readonly isLoading: boolean;
|
|
107
|
+
readonly error: Error | null;
|
|
108
|
+
}
|
|
109
|
+
declare function submissionReceiptQueryKey(formId: string, formVersion: number): string;
|
|
99
110
|
declare function createLocalStorageSubmissionReceiptStore(options?: {
|
|
100
111
|
readonly namespace?: string;
|
|
101
112
|
}): SubmissionReceiptStore;
|
|
113
|
+
declare function useSubmissionReceipts(store: SubmissionReceiptStore, queries: readonly SubmissionReceiptQuery[]): UseSubmissionReceiptsResult;
|
|
102
114
|
|
|
103
115
|
interface ComponentBaseProps {
|
|
104
116
|
readonly id?: string;
|
|
@@ -261,10 +273,16 @@ type SubmitResult = {
|
|
|
261
273
|
readonly status: "cancelled";
|
|
262
274
|
} | {
|
|
263
275
|
readonly status: "success";
|
|
276
|
+
readonly response?: SubmitResponse;
|
|
264
277
|
} | {
|
|
265
278
|
readonly status: "error";
|
|
266
279
|
readonly error: Error;
|
|
267
280
|
};
|
|
281
|
+
interface SubmitResponse {
|
|
282
|
+
readonly submissionId?: string;
|
|
283
|
+
readonly submittedAt?: string;
|
|
284
|
+
}
|
|
285
|
+
type FormSubmitHandler = (answers: FormValues) => SubmitResponse | void | Promise<SubmitResponse | undefined> | Promise<void>;
|
|
268
286
|
type SubmissionGuardResult = {
|
|
269
287
|
readonly status: "allow";
|
|
270
288
|
} | {
|
|
@@ -278,6 +296,14 @@ type SubmissionGuardResult = {
|
|
|
278
296
|
};
|
|
279
297
|
type SubmissionGuard = (schema: FormSchema, values: Record<string, unknown>) => SubmissionGuardResult | Promise<SubmissionGuardResult>;
|
|
280
298
|
type FormSubmitState = "idle" | "submitting" | "confirming" | "success" | "error";
|
|
299
|
+
interface SubmissionConfirmationSlotProps {
|
|
300
|
+
readonly findings: readonly SensitiveDataFinding[];
|
|
301
|
+
readonly message?: string;
|
|
302
|
+
readonly schema: FormSchema;
|
|
303
|
+
readonly visibleValues: Record<string, unknown>;
|
|
304
|
+
readonly onConfirm: () => void;
|
|
305
|
+
readonly onCancel: () => void;
|
|
306
|
+
}
|
|
281
307
|
interface FormRendererSlots {
|
|
282
308
|
readonly renderHeader?: (props: {
|
|
283
309
|
readonly title: string;
|
|
@@ -316,11 +342,7 @@ interface FormRendererSlots {
|
|
|
316
342
|
readonly error: Error;
|
|
317
343
|
readonly onRetry?: () => void;
|
|
318
344
|
}) => ReactNode;
|
|
319
|
-
readonly renderSubmissionConfirmation?: (props:
|
|
320
|
-
readonly findings: readonly SensitiveDataFinding[];
|
|
321
|
-
readonly onConfirm: () => void;
|
|
322
|
-
readonly onCancel: () => void;
|
|
323
|
-
}) => ReactNode;
|
|
345
|
+
readonly renderSubmissionConfirmation?: (props: SubmissionConfirmationSlotProps) => ReactNode;
|
|
324
346
|
readonly renderAlreadySubmitted?: (props: {
|
|
325
347
|
readonly receipt: SubmissionReceipt;
|
|
326
348
|
readonly onReset?: () => void;
|
|
@@ -390,7 +412,7 @@ interface FormProviderProps {
|
|
|
390
412
|
readonly translator: TranslationAdapter;
|
|
391
413
|
readonly initialValues?: FormValues;
|
|
392
414
|
readonly resetOnSuccess?: boolean;
|
|
393
|
-
readonly onSubmit:
|
|
415
|
+
readonly onSubmit: FormSubmitHandler;
|
|
394
416
|
readonly children: ReactNode;
|
|
395
417
|
}
|
|
396
418
|
declare function FormProvider({ schema, locale, translator, initialValues, resetOnSuccess, onSubmit, children }: FormProviderProps): react.JSX.Element;
|
|
@@ -431,9 +453,9 @@ interface StandaloneFormRendererProps extends FormRendererPresentationProps {
|
|
|
431
453
|
readonly translator?: TranslationAdapter;
|
|
432
454
|
readonly initialValues?: FormValues;
|
|
433
455
|
readonly resetOnSuccess?: boolean;
|
|
434
|
-
readonly onSubmit:
|
|
456
|
+
readonly onSubmit: FormSubmitHandler;
|
|
435
457
|
}
|
|
436
458
|
type FormRendererProps = FormRendererPresentationProps | StandaloneFormRendererProps;
|
|
437
459
|
declare function FormRenderer(props: FormRendererProps): react.JSX.Element;
|
|
438
460
|
|
|
439
|
-
export { type BeforeSubmit, type BuilderActionContext, type BuilderActionError, type BuilderActionResult, type BuilderButtonProps, type BuilderCheckboxProps, type BuilderFactories, type BuilderFieldEditorSlotProps, type BuilderFieldsetProps, type BuilderIconButtonProps, type BuilderIdKind, type BuilderLocalizationSlotProps, type BuilderOptionEditorSlotProps, type BuilderPagesSlotProps, type BuilderPolicy, type BuilderSectionProps, type BuilderSelectOption, type BuilderSelectProps, type BuilderTextAreaProps, type BuilderTextInputProps, type BuilderTextTarget, type BuilderToolbarSlotProps, type BuilderTranslationActionsSlotProps, type ComponentBaseProps, type FieldComponentProps, type FieldComponents, type FieldState, FormBuilder, type FormBuilderActions, type FormBuilderComponents, type FormBuilderFeatures, type FormBuilderOptions, type FormBuilderProps, type FormBuilderResult, type FormBuilderSlots, type FormContextValue, FormProvider, type FormProviderProps, FormRenderer, type FormRendererPresentationProps, type FormRendererProps, type FormRendererSlots, type FormSubmitState, type InputComponentProps, type ManualTranslationContext, type StandaloneFormRendererProps, type SubmissionGuard, type SubmissionGuardResult, type SubmissionProtectionProps, type SubmissionReceipt, type SubmissionReceiptStore, type SubmitResult, type SubmitStatus, createLocalStorageSubmissionReceiptStore, resolveInitialFieldType, useField, useForm, useFormBuilder };
|
|
461
|
+
export { type BeforeSubmit, type BuilderActionContext, type BuilderActionError, type BuilderActionResult, type BuilderButtonProps, type BuilderCheckboxProps, type BuilderFactories, type BuilderFieldEditorSlotProps, type BuilderFieldsetProps, type BuilderIconButtonProps, type BuilderIdKind, type BuilderLocalizationSlotProps, type BuilderOptionEditorSlotProps, type BuilderPagesSlotProps, type BuilderPolicy, type BuilderSectionProps, type BuilderSelectOption, type BuilderSelectProps, type BuilderTextAreaProps, type BuilderTextInputProps, type BuilderTextTarget, type BuilderToolbarSlotProps, type BuilderTranslationActionsSlotProps, type ComponentBaseProps, type FieldComponentProps, type FieldComponents, type FieldState, FormBuilder, type FormBuilderActions, type FormBuilderComponents, type FormBuilderFeatures, type FormBuilderOptions, type FormBuilderProps, type FormBuilderResult, type FormBuilderSlots, type FormContextValue, FormProvider, type FormProviderProps, FormRenderer, type FormRendererPresentationProps, type FormRendererProps, type FormRendererSlots, type FormSubmitHandler, type FormSubmitState, type InputComponentProps, type ManualTranslationContext, type StandaloneFormRendererProps, type SubmissionConfirmationSlotProps, type SubmissionGuard, type SubmissionGuardResult, type SubmissionProtectionProps, type SubmissionReceipt, type SubmissionReceiptQuery, type SubmissionReceiptStore, type SubmitResponse, type SubmitResult, type SubmitStatus, type UseSubmissionReceiptsResult, createLocalStorageSubmissionReceiptStore, resolveInitialFieldType, submissionReceiptQueryKey, useField, useForm, useFormBuilder, useSubmissionReceipts };
|
package/dist/index.js
CHANGED
|
@@ -945,6 +945,9 @@ var FIELD_TYPES = [
|
|
|
945
945
|
];
|
|
946
946
|
var BUILDER_DEFAULTS = {
|
|
947
947
|
"builder.formBuilder": "Form builder",
|
|
948
|
+
"builder.basicSettings": "Basic settings",
|
|
949
|
+
"builder.formTitle": "Form title",
|
|
950
|
+
"builder.formDescription": "Form description",
|
|
948
951
|
"builder.moveUp": "Move {{title}} up",
|
|
949
952
|
"builder.moveDown": "Move {{title}} down",
|
|
950
953
|
"builder.delete": "Delete {{title}}",
|
|
@@ -1110,7 +1113,7 @@ function FormBuilder({
|
|
|
1110
1113
|
}) {
|
|
1111
1114
|
const resolvedComponents = { ...DEFAULT_COMPONENTS, ...componentOverrides };
|
|
1112
1115
|
const components = GUARDED_COMPONENTS;
|
|
1113
|
-
const { Button, Checkbox, ErrorMessage, Fieldset, IconButton, Section, Select, TextInput } = components;
|
|
1116
|
+
const { Button, Checkbox, ErrorMessage, Fieldset, IconButton, Section, Select, TextArea, TextInput } = components;
|
|
1114
1117
|
const ToolbarSlot = slots?.toolbar;
|
|
1115
1118
|
const FieldEditorSlot = slots?.fieldEditor;
|
|
1116
1119
|
const OptionEditorSlot = slots?.optionEditor;
|
|
@@ -1413,6 +1416,42 @@ function FormBuilder({
|
|
|
1413
1416
|
}
|
|
1414
1417
|
},
|
|
1415
1418
|
children: /* @__PURE__ */ jsxs(Fieldset, { className: "form-engine-builder__controls", disabled: readOnly, children: [
|
|
1419
|
+
/* @__PURE__ */ jsx(
|
|
1420
|
+
Section,
|
|
1421
|
+
{
|
|
1422
|
+
className: "form-engine-builder__basic-settings",
|
|
1423
|
+
headingId: "builder-basic-settings-heading",
|
|
1424
|
+
title: translate("builder.basicSettings"),
|
|
1425
|
+
children: /* @__PURE__ */ jsxs("div", { className: "form-engine-builder__grid", children: [
|
|
1426
|
+
/* @__PURE__ */ jsxs("label", { children: [
|
|
1427
|
+
translate("builder.formTitle"),
|
|
1428
|
+
/* @__PURE__ */ jsx(
|
|
1429
|
+
TextInput,
|
|
1430
|
+
{
|
|
1431
|
+
name: "title",
|
|
1432
|
+
required: true,
|
|
1433
|
+
error: schema.title.trim().length === 0,
|
|
1434
|
+
helperText: schema.title.trim().length === 0 ? translate("builder.required") : "",
|
|
1435
|
+
value: schema.title,
|
|
1436
|
+
onChange: (value) => setSourceText({ kind: "form" }, "title", value)
|
|
1437
|
+
}
|
|
1438
|
+
)
|
|
1439
|
+
] }),
|
|
1440
|
+
/* @__PURE__ */ jsxs("label", { children: [
|
|
1441
|
+
translate("builder.formDescription"),
|
|
1442
|
+
/* @__PURE__ */ jsx(
|
|
1443
|
+
TextArea,
|
|
1444
|
+
{
|
|
1445
|
+
name: "description",
|
|
1446
|
+
rows: 3,
|
|
1447
|
+
value: schema.description ?? "",
|
|
1448
|
+
onChange: (value) => setSourceText({ kind: "form" }, "description", value)
|
|
1449
|
+
}
|
|
1450
|
+
)
|
|
1451
|
+
] })
|
|
1452
|
+
] })
|
|
1453
|
+
}
|
|
1454
|
+
),
|
|
1416
1455
|
pagesEnabled ? PagesSlot === void 0 ? /* @__PURE__ */ jsx(
|
|
1417
1456
|
Section,
|
|
1418
1457
|
{
|
|
@@ -2281,10 +2320,10 @@ function FormProvider({
|
|
|
2281
2320
|
setSubmitStatus("idle");
|
|
2282
2321
|
return { status: "cancelled" };
|
|
2283
2322
|
}
|
|
2284
|
-
await onSubmit(visibleValues);
|
|
2323
|
+
const response = await onSubmit(visibleValues);
|
|
2285
2324
|
if (resetOnSuccess) setValues({ ...initialValues });
|
|
2286
2325
|
setSubmitStatus("success");
|
|
2287
|
-
return { status: "success" };
|
|
2326
|
+
return response === void 0 ? { status: "success" } : { status: "success", response };
|
|
2288
2327
|
} catch (cause) {
|
|
2289
2328
|
const error = cause instanceof Error ? cause : new Error(String(cause));
|
|
2290
2329
|
setSubmitError(error);
|
|
@@ -2353,6 +2392,10 @@ function useField(fieldId) {
|
|
|
2353
2392
|
}
|
|
2354
2393
|
|
|
2355
2394
|
// src/receipt.ts
|
|
2395
|
+
import { useEffect as useEffect2, useMemo as useMemo3, useState as useState3 } from "react";
|
|
2396
|
+
function submissionReceiptQueryKey(formId, formVersion) {
|
|
2397
|
+
return `${formId}:v${formVersion}`;
|
|
2398
|
+
}
|
|
2356
2399
|
function receiptKey(namespace, formId, formVersion) {
|
|
2357
2400
|
return `${namespace}:${formId}:v${formVersion}`;
|
|
2358
2401
|
}
|
|
@@ -2384,18 +2427,27 @@ function parseReceipt(serialized) {
|
|
|
2384
2427
|
function createLocalStorageSubmissionReceiptStore(options = {}) {
|
|
2385
2428
|
const namespace = options.namespace ?? "form_engine_receipt";
|
|
2386
2429
|
if (namespace.trim().length === 0) throw new TypeError("Receipt namespace must not be empty.");
|
|
2430
|
+
const get = async (formId, formVersion) => {
|
|
2431
|
+
const storage = browserStorage();
|
|
2432
|
+
if (storage === null) return null;
|
|
2433
|
+
try {
|
|
2434
|
+
const serialized = storage.getItem(receiptKey(namespace, formId, formVersion));
|
|
2435
|
+
if (serialized === null) return null;
|
|
2436
|
+
const receipt = parseReceipt(serialized);
|
|
2437
|
+
return receipt?.formId === formId && receipt.formVersion === formVersion ? receipt : null;
|
|
2438
|
+
} catch {
|
|
2439
|
+
return null;
|
|
2440
|
+
}
|
|
2441
|
+
};
|
|
2387
2442
|
return {
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
} catch {
|
|
2397
|
-
return null;
|
|
2398
|
-
}
|
|
2443
|
+
get,
|
|
2444
|
+
async getBatch(queries) {
|
|
2445
|
+
const receipts = await Promise.all(queries.map((query) => get(query.formId, query.formVersion)));
|
|
2446
|
+
return new Map(
|
|
2447
|
+
receipts.flatMap(
|
|
2448
|
+
(receipt) => receipt === null ? [] : [[submissionReceiptQueryKey(receipt.formId, receipt.formVersion), receipt]]
|
|
2449
|
+
)
|
|
2450
|
+
);
|
|
2399
2451
|
},
|
|
2400
2452
|
async save(receipt) {
|
|
2401
2453
|
const storage = browserStorage();
|
|
@@ -2409,6 +2461,45 @@ function createLocalStorageSubmissionReceiptStore(options = {}) {
|
|
|
2409
2461
|
}
|
|
2410
2462
|
};
|
|
2411
2463
|
}
|
|
2464
|
+
function useSubmissionReceipts(store, queries) {
|
|
2465
|
+
const querySignature = JSON.stringify(queries.map(({ formId, formVersion }) => [formId, formVersion]));
|
|
2466
|
+
const stableQueries = useMemo3(() => {
|
|
2467
|
+
const parsed = JSON.parse(querySignature);
|
|
2468
|
+
if (!Array.isArray(parsed)) return [];
|
|
2469
|
+
return parsed.flatMap(
|
|
2470
|
+
(entry) => Array.isArray(entry) && typeof entry[0] === "string" && typeof entry[1] === "number" && Number.isSafeInteger(entry[1]) ? [{ formId: entry[0], formVersion: entry[1] }] : []
|
|
2471
|
+
);
|
|
2472
|
+
}, [querySignature]);
|
|
2473
|
+
const [state, setState] = useState3({
|
|
2474
|
+
receipts: /* @__PURE__ */ new Map(),
|
|
2475
|
+
isLoading: stableQueries.length > 0,
|
|
2476
|
+
error: null
|
|
2477
|
+
});
|
|
2478
|
+
useEffect2(() => {
|
|
2479
|
+
let active = true;
|
|
2480
|
+
if (stableQueries.length === 0) {
|
|
2481
|
+
setState({ receipts: /* @__PURE__ */ new Map(), isLoading: false, error: null });
|
|
2482
|
+
return () => {
|
|
2483
|
+
active = false;
|
|
2484
|
+
};
|
|
2485
|
+
}
|
|
2486
|
+
setState((current) => ({ ...current, isLoading: true, error: null }));
|
|
2487
|
+
void store.getBatch(stableQueries).then((receipts) => {
|
|
2488
|
+
if (active) setState({ receipts, isLoading: false, error: null });
|
|
2489
|
+
}).catch((cause) => {
|
|
2490
|
+
if (!active) return;
|
|
2491
|
+
setState({
|
|
2492
|
+
receipts: /* @__PURE__ */ new Map(),
|
|
2493
|
+
isLoading: false,
|
|
2494
|
+
error: cause instanceof Error ? cause : new Error(String(cause))
|
|
2495
|
+
});
|
|
2496
|
+
});
|
|
2497
|
+
return () => {
|
|
2498
|
+
active = false;
|
|
2499
|
+
};
|
|
2500
|
+
}, [stableQueries, store]);
|
|
2501
|
+
return state;
|
|
2502
|
+
}
|
|
2412
2503
|
|
|
2413
2504
|
// src/renderer.tsx
|
|
2414
2505
|
import {
|
|
@@ -2417,11 +2508,11 @@ import {
|
|
|
2417
2508
|
} from "@form-engine-ts/core";
|
|
2418
2509
|
import {
|
|
2419
2510
|
Fragment as Fragment2,
|
|
2420
|
-
useEffect as
|
|
2511
|
+
useEffect as useEffect3,
|
|
2421
2512
|
useId,
|
|
2422
|
-
useMemo as
|
|
2513
|
+
useMemo as useMemo4,
|
|
2423
2514
|
useRef as useRef2,
|
|
2424
|
-
useState as
|
|
2515
|
+
useState as useState4
|
|
2425
2516
|
} from "react";
|
|
2426
2517
|
import { Fragment as Fragment3, jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
2427
2518
|
function describedBy(field, error, helpId, errorId) {
|
|
@@ -2653,25 +2744,26 @@ function ContextFormRenderer({
|
|
|
2653
2744
|
const prefix = useId().replace(/:/g, "");
|
|
2654
2745
|
const formRef = useRef2(null);
|
|
2655
2746
|
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] =
|
|
2747
|
+
const [draftRestored, setDraftRestored] = useState4(false);
|
|
2748
|
+
const [currentPageIndex, setCurrentPageIndex] = useState4(0);
|
|
2749
|
+
const [focusFieldId, setFocusFieldId] = useState4(null);
|
|
2750
|
+
const [confirmation, setConfirmation] = useState4(null);
|
|
2751
|
+
const [guardMessage, setGuardMessage] = useState4(null);
|
|
2752
|
+
const [receipt, setReceipt] = useState4(null);
|
|
2753
|
+
const [receiptLoaded, setReceiptLoaded] = useState4(receiptStore === void 0);
|
|
2663
2754
|
const rendererSubmissionInFlight = useRef2(false);
|
|
2664
2755
|
const pages = form.schema.pages;
|
|
2665
|
-
const visiblePageIndexes =
|
|
2756
|
+
const visiblePageIndexes = useMemo4(
|
|
2666
2757
|
() => pages === void 0 ? [] : pages.flatMap((page, index) => form.pageVisibility[page.id] === true ? [index] : []),
|
|
2667
2758
|
[form.pageVisibility, pages]
|
|
2668
2759
|
);
|
|
2669
2760
|
const activePage = pages?.[currentPageIndex];
|
|
2670
2761
|
const activeVisibleIndex = visiblePageIndexes.indexOf(currentPageIndex);
|
|
2671
2762
|
const fieldIds = activePage === void 0 ? void 0 : new Set(activePage.questionIds);
|
|
2763
|
+
const visibleValues = useMemo4(() => selectVisibleAnswers2(form.schema, form.values), [form.schema, form.values]);
|
|
2672
2764
|
const submitState = confirmation === null ? form.submitStatus : "confirming";
|
|
2673
2765
|
const interactionLocked = submitState === "confirming" || submitState === "submitting";
|
|
2674
|
-
|
|
2766
|
+
useEffect3(() => {
|
|
2675
2767
|
let active = true;
|
|
2676
2768
|
if (receiptStore === void 0) {
|
|
2677
2769
|
setReceipt(null);
|
|
@@ -2692,14 +2784,14 @@ function ContextFormRenderer({
|
|
|
2692
2784
|
active = false;
|
|
2693
2785
|
};
|
|
2694
2786
|
}, [form.schema.id, form.schema.version, receiptStore]);
|
|
2695
|
-
|
|
2787
|
+
useEffect3(() => {
|
|
2696
2788
|
if (pages === void 0 || visiblePageIndexes.length === 0) {
|
|
2697
2789
|
setCurrentPageIndex(0);
|
|
2698
2790
|
return;
|
|
2699
2791
|
}
|
|
2700
2792
|
if (!visiblePageIndexes.includes(currentPageIndex)) setCurrentPageIndex(visiblePageIndexes[0] ?? 0);
|
|
2701
2793
|
}, [currentPageIndex, pages, visiblePageIndexes]);
|
|
2702
|
-
|
|
2794
|
+
useEffect3(() => {
|
|
2703
2795
|
if (focusFieldId === null) return;
|
|
2704
2796
|
const fieldContainer = [...formRef.current?.querySelectorAll("[data-field-id]") ?? []].find(
|
|
2705
2797
|
(element) => element.dataset.fieldId === focusFieldId
|
|
@@ -2710,7 +2802,7 @@ function ContextFormRenderer({
|
|
|
2710
2802
|
setFocusFieldId(null);
|
|
2711
2803
|
}
|
|
2712
2804
|
}, [focusFieldId]);
|
|
2713
|
-
|
|
2805
|
+
useEffect3(() => {
|
|
2714
2806
|
if (autoSaveKey === void 0 || typeof globalThis.localStorage === "undefined") return;
|
|
2715
2807
|
const loadIdentity = `${autoSaveKey}:${form.schema.id}:${form.schema.version}`;
|
|
2716
2808
|
if (loadedDraftKey.current === loadIdentity) return;
|
|
@@ -2722,7 +2814,7 @@ function ContextFormRenderer({
|
|
|
2722
2814
|
form.restoreValues(draft.values);
|
|
2723
2815
|
setDraftRestored(true);
|
|
2724
2816
|
}, [autoSaveKey, form.restoreValues, form.schema.id, form.schema.version]);
|
|
2725
|
-
|
|
2817
|
+
useEffect3(() => {
|
|
2726
2818
|
if (form.submitStatus === "success") return;
|
|
2727
2819
|
const timeout = globalThis.setTimeout(() => {
|
|
2728
2820
|
onDraftSave?.(form.values);
|
|
@@ -2755,7 +2847,7 @@ function ContextFormRenderer({
|
|
|
2755
2847
|
let confirmationMessage;
|
|
2756
2848
|
let requiresConfirmation = false;
|
|
2757
2849
|
for (const guard of guards) {
|
|
2758
|
-
const result = await guard(form.schema,
|
|
2850
|
+
const result = await guard(form.schema, visibleValues);
|
|
2759
2851
|
if (result.status === "allow") continue;
|
|
2760
2852
|
findings.push(...result.findings);
|
|
2761
2853
|
if (result.status === "block") {
|
|
@@ -2785,7 +2877,7 @@ function ContextFormRenderer({
|
|
|
2785
2877
|
try {
|
|
2786
2878
|
const guardResult = await runSubmissionGuards(submissionGuards);
|
|
2787
2879
|
if (guardResult.status === "block") {
|
|
2788
|
-
setGuardMessage(guardResult.message ?? "
|
|
2880
|
+
setGuardMessage(guardResult.message ?? form.translate("form.submissionBlocked"));
|
|
2789
2881
|
return { status: "cancelled" };
|
|
2790
2882
|
}
|
|
2791
2883
|
if (guardResult.status === "confirm") {
|
|
@@ -2814,10 +2906,12 @@ function ContextFormRenderer({
|
|
|
2814
2906
|
}
|
|
2815
2907
|
if (result.status !== "success") return result;
|
|
2816
2908
|
if (receiptStore !== void 0) {
|
|
2909
|
+
const response = result.response;
|
|
2817
2910
|
const storedReceipt = {
|
|
2818
2911
|
formId: form.schema.id,
|
|
2819
2912
|
formVersion: form.schema.version,
|
|
2820
|
-
submittedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2913
|
+
submittedAt: response?.submittedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
2914
|
+
...response?.submissionId === void 0 ? {} : { submissionId: response.submissionId }
|
|
2821
2915
|
};
|
|
2822
2916
|
await receiptStore.save(storedReceipt);
|
|
2823
2917
|
setReceipt(storedReceipt);
|
|
@@ -2860,8 +2954,8 @@ function ContextFormRenderer({
|
|
|
2860
2954
|
receipt,
|
|
2861
2955
|
...receiptStore === void 0 ? {} : { onReset: () => void resetReceipt() }
|
|
2862
2956
|
}) ?? /* @__PURE__ */ jsxs2("div", { role: "status", children: [
|
|
2863
|
-
"
|
|
2864
|
-
receiptStore === void 0 ? null : /* @__PURE__ */ jsx3("button", { type: "button", onClick: () => void resetReceipt(), children: "
|
|
2957
|
+
form.translate("form.alreadySubmitted"),
|
|
2958
|
+
receiptStore === void 0 ? null : /* @__PURE__ */ jsx3("button", { type: "button", onClick: () => void resetReceipt(), children: form.translate("form.submitAnother") })
|
|
2865
2959
|
] }) });
|
|
2866
2960
|
}
|
|
2867
2961
|
return /* @__PURE__ */ jsxs2("form", { ref: formRef, className: `fe-form ${className}`.trim(), noValidate: true, onSubmit: handleSubmit, children: [
|
|
@@ -2930,12 +3024,15 @@ function ContextFormRenderer({
|
|
|
2930
3024
|
guardMessage === null ? null : /* @__PURE__ */ jsx3("div", { role: "alert", children: guardMessage }),
|
|
2931
3025
|
confirmation === null ? null : slots.renderSubmissionConfirmation?.({
|
|
2932
3026
|
findings: confirmation.findings,
|
|
3027
|
+
message: confirmation.message ?? form.translate("form.confirmSensitiveData"),
|
|
3028
|
+
schema: form.schema,
|
|
3029
|
+
visibleValues,
|
|
2933
3030
|
onConfirm: confirmSubmission,
|
|
2934
3031
|
onCancel: cancelSubmission
|
|
2935
3032
|
}) ?? /* @__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: "
|
|
3033
|
+
/* @__PURE__ */ jsx3("p", { children: confirmation.message ?? form.translate("form.confirmSensitiveData") }),
|
|
3034
|
+
/* @__PURE__ */ jsx3("button", { type: "button", onClick: confirmSubmission, children: form.translate("form.confirmSubmission") }),
|
|
3035
|
+
/* @__PURE__ */ jsx3("button", { type: "button", onClick: cancelSubmission, children: form.translate("form.cancelSubmission") })
|
|
2939
3036
|
] }),
|
|
2940
3037
|
validationIssues.length === 0 ? null : slots.renderValidationSummary?.({ issues: validationIssues }) ?? /* @__PURE__ */ jsxs2("div", { className: "fe-validation-summary", role: "alert", children: [
|
|
2941
3038
|
validationIssues.length,
|
|
@@ -2992,6 +3089,12 @@ var RENDERER_MESSAGES = {
|
|
|
2992
3089
|
"form.next": "Next",
|
|
2993
3090
|
"form.step": "Step {{current}} / {{total}}",
|
|
2994
3091
|
"form.draftRestored": "Draft restored",
|
|
3092
|
+
"form.submissionBlocked": "Submission blocked because sensitive data was detected.",
|
|
3093
|
+
"form.confirmSensitiveData": "Sensitive data may be included. Confirm before submitting.",
|
|
3094
|
+
"form.confirmSubmission": "Confirm submission",
|
|
3095
|
+
"form.cancelSubmission": "Cancel",
|
|
3096
|
+
"form.alreadySubmitted": "Already submitted.",
|
|
3097
|
+
"form.submitAnother": "Submit another response",
|
|
2995
3098
|
"validation.required": "This field is required."
|
|
2996
3099
|
};
|
|
2997
3100
|
var defaultRendererTranslator = {
|
|
@@ -3032,7 +3135,9 @@ export {
|
|
|
3032
3135
|
FormRenderer,
|
|
3033
3136
|
createLocalStorageSubmissionReceiptStore,
|
|
3034
3137
|
resolveInitialFieldType,
|
|
3138
|
+
submissionReceiptQueryKey,
|
|
3035
3139
|
useField,
|
|
3036
3140
|
useForm,
|
|
3037
|
-
useFormBuilder
|
|
3141
|
+
useFormBuilder,
|
|
3142
|
+
useSubmissionReceipts
|
|
3038
3143
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@form-engine-ts/react",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.8.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.8.0",
|
|
46
|
+
"@form-engine-ts/privacy": "2.8.0"
|
|
47
47
|
},
|
|
48
48
|
"peerDependencies": {
|
|
49
49
|
"react": ">=18.2 <20",
|