@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 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
- async get(formId, formVersion) {
2408
- const storage = browserStorage();
2409
- if (storage === null) return null;
2410
- try {
2411
- const serialized = storage.getItem(receiptKey(namespace, formId, formVersion));
2412
- if (serialized === null) return null;
2413
- const receipt = parseReceipt(serialized);
2414
- return receipt?.formId === formId && receipt.formVersion === formVersion ? receipt : null;
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 import_react4 = require("react");
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, import_react4.useId)().replace(/:/g, "");
2663
- const formRef = (0, import_react4.useRef)(null);
2664
- const loadedDraftKey = (0, import_react4.useRef)(null);
2665
- const [draftRestored, setDraftRestored] = (0, import_react4.useState)(false);
2666
- const [currentPageIndex, setCurrentPageIndex] = (0, import_react4.useState)(0);
2667
- const [focusFieldId, setFocusFieldId] = (0, import_react4.useState)(null);
2668
- const [confirmation, setConfirmation] = (0, import_react4.useState)(null);
2669
- const [guardMessage, setGuardMessage] = (0, import_react4.useState)(null);
2670
- const [receipt, setReceipt] = (0, import_react4.useState)(null);
2671
- const [receiptLoaded, setReceiptLoaded] = (0, import_react4.useState)(receiptStore === void 0);
2672
- const rendererSubmissionInFlight = (0, import_react4.useRef)(false);
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, import_react4.useMemo)(
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, import_react4.useEffect)(() => {
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, import_react4.useEffect)(() => {
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, import_react4.useEffect)(() => {
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, import_react4.useEffect)(() => {
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, import_react4.useEffect)(() => {
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, (0, import_core4.selectVisibleAnswers)(form.schema, form.values));
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 ?? "Submission blocked because sensitive data was detected.");
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
- "Already submitted.",
2873
- receiptStore === void 0 ? null : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", onClick: () => void resetReceipt(), children: "Submit another response" })
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)(import_react4.Fragment, { children: slots.renderField({
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 ?? "Sensitive data may be included. Confirm before submitting." }),
2946
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", onClick: confirmSubmission, children: "Confirm submission" }),
2947
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", onClick: cancelSubmission, children: "Cancel" })
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, FormValues, AnswerValidationResult, FieldType } from '@form-engine-ts/core';
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: (values: FormValues) => void | Promise<void>;
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: (answers: FormValues) => Promise<void> | void;
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, FormValues, AnswerValidationResult, FieldType } from '@form-engine-ts/core';
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: (values: FormValues) => void | Promise<void>;
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: (answers: FormValues) => Promise<void> | void;
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
- async get(formId, formVersion) {
2389
- const storage = browserStorage();
2390
- if (storage === null) return null;
2391
- try {
2392
- const serialized = storage.getItem(receiptKey(namespace, formId, formVersion));
2393
- if (serialized === null) return null;
2394
- const receipt = parseReceipt(serialized);
2395
- return receipt?.formId === formId && receipt.formVersion === formVersion ? receipt : null;
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 useEffect2,
2511
+ useEffect as useEffect3,
2421
2512
  useId,
2422
- useMemo as useMemo3,
2513
+ useMemo as useMemo4,
2423
2514
  useRef as useRef2,
2424
- useState as useState3
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] = useState3(false);
2657
- const [currentPageIndex, setCurrentPageIndex] = useState3(0);
2658
- const [focusFieldId, setFocusFieldId] = useState3(null);
2659
- const [confirmation, setConfirmation] = useState3(null);
2660
- const [guardMessage, setGuardMessage] = useState3(null);
2661
- const [receipt, setReceipt] = useState3(null);
2662
- const [receiptLoaded, setReceiptLoaded] = useState3(receiptStore === void 0);
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 = useMemo3(
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
- useEffect2(() => {
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
- useEffect2(() => {
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
- useEffect2(() => {
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
- useEffect2(() => {
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
- useEffect2(() => {
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, selectVisibleAnswers2(form.schema, form.values));
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 ?? "Submission blocked because sensitive data was detected.");
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
- "Already submitted.",
2864
- receiptStore === void 0 ? null : /* @__PURE__ */ jsx3("button", { type: "button", onClick: () => void resetReceipt(), children: "Submit another response" })
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 ?? "Sensitive data may be included. Confirm before submitting." }),
2937
- /* @__PURE__ */ jsx3("button", { type: "button", onClick: confirmSubmission, children: "Confirm submission" }),
2938
- /* @__PURE__ */ jsx3("button", { type: "button", onClick: cancelSubmission, children: "Cancel" })
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.7.0",
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.7.0",
46
- "@form-engine-ts/privacy": "2.7.0"
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",