formanitor 0.0.8 → 0.0.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -181,10 +181,6 @@ var NO_OP_PROVIDER = {
181
181
  function applyPrefill(currentValues, fields, prefilledData = {}) {
182
182
  const newValues = { ...currentValues };
183
183
  fields.forEach((field) => {
184
- const currentValue = newValues[field.id];
185
- if (!isEmpty(currentValue)) {
186
- return;
187
- }
188
184
  if (field.prefill?.value !== void 0) {
189
185
  newValues[field.id] = field.prefill.value;
190
186
  return;
@@ -196,9 +192,6 @@ function applyPrefill(currentValues, fields, prefilledData = {}) {
196
192
  });
197
193
  return newValues;
198
194
  }
199
- function isEmpty(val) {
200
- return val === void 0 || val === null || val === "";
201
- }
202
195
 
203
196
  // src/core/store.ts
204
197
  var FormStore = class {
@@ -230,7 +223,8 @@ var FormStore = class {
230
223
  workflowState: initialWorkflowState,
231
224
  isValid: true,
232
225
  isSubmitting: false,
233
- submitAttempted: false
226
+ submitAttempted: false,
227
+ pendingUploads: 0
234
228
  };
235
229
  this.initializeDefaults();
236
230
  this.evaluate();
@@ -492,9 +486,52 @@ var FormStore = class {
492
486
  this.config.onEvent(event, finalPayload);
493
487
  }
494
488
  }
489
+ /**
490
+ * Update mutable config properties (callbacks, role) without recreating
491
+ * the store. This allows the host app to pass fresh closures (e.g. an
492
+ * upload handler whose auth token was initially undefined) after the
493
+ * initial render.
494
+ */
495
+ updateConfig(next) {
496
+ let needsReeval = false;
497
+ if (next.onUpload !== void 0) this.config.onUpload = next.onUpload;
498
+ if (next.onEvent !== void 0) this.config.onEvent = next.onEvent;
499
+ if (next.role !== void 0 && next.role !== this.config.role) {
500
+ this.config.role = next.role;
501
+ needsReeval = true;
502
+ }
503
+ if (needsReeval) {
504
+ this.evaluate();
505
+ this.notify();
506
+ }
507
+ }
495
508
  getUploadHandler() {
496
509
  return this.config.onUpload;
497
510
  }
511
+ preSubmitHandlers = /* @__PURE__ */ new Map();
512
+ registerPreSubmitHandler(fieldId, fn) {
513
+ this.preSubmitHandlers.set(fieldId, fn);
514
+ }
515
+ unregisterPreSubmitHandler(fieldId) {
516
+ this.preSubmitHandlers.delete(fieldId);
517
+ }
518
+ /**
519
+ * Runs all registered pre-submit handlers in parallel.
520
+ * Call this before getSerializedValuesForSubmission() to ensure any
521
+ * unconfirmed signatures (or similar) are uploaded first.
522
+ */
523
+ async flushPendingUploads() {
524
+ const handlers = Array.from(this.preSubmitHandlers.values());
525
+ await Promise.all(handlers.map((fn) => fn()));
526
+ }
527
+ incrementPendingUploads() {
528
+ this.state.pendingUploads = (this.state.pendingUploads ?? 0) + 1;
529
+ this.notify();
530
+ }
531
+ decrementPendingUploads() {
532
+ this.state.pendingUploads = Math.max(0, (this.state.pendingUploads ?? 0) - 1);
533
+ this.notify();
534
+ }
498
535
  // Mark that a submit attempt has been made so UI can decide when to show errors
499
536
  markSubmitAttempted() {
500
537
  if (!this.state.submitAttempted) {
@@ -526,6 +563,11 @@ var FormProvider = ({ schema, config, children }) => {
526
563
  if (!storeRef.current) {
527
564
  storeRef.current = new FormStore(schema, config);
528
565
  }
566
+ React11.useEffect(() => {
567
+ if (storeRef.current && config) {
568
+ storeRef.current.updateConfig(config);
569
+ }
570
+ });
529
571
  React11.useEffect(() => {
530
572
  storeRef.current?.load();
531
573
  }, []);
@@ -556,7 +598,8 @@ function useForm() {
556
598
  availableTransitions: store.getAvailableTransitions(),
557
599
  hasPersistence: store.hasPersistence(),
558
600
  getSerializedValues: store.getSerializedValuesForSubmission.bind(store),
559
- markSubmitAttempted: store.markSubmitAttempted.bind(store)
601
+ markSubmitAttempted: store.markSubmitAttempted.bind(store),
602
+ flushPendingUploads: store.flushPendingUploads.bind(store)
560
603
  };
561
604
  }
562
605
  function useField(fieldId) {
@@ -1464,6 +1507,7 @@ var ImageUploadWidget = ({ fieldId }) => {
1464
1507
  }
1465
1508
  setIsUploading(true);
1466
1509
  setTouched();
1510
+ store.incrementPendingUploads();
1467
1511
  try {
1468
1512
  const previewUrl = URL.createObjectURL(file);
1469
1513
  setPreview(previewUrl);
@@ -1476,6 +1520,7 @@ var ImageUploadWidget = ({ fieldId }) => {
1476
1520
  setPreview(null);
1477
1521
  } finally {
1478
1522
  setIsUploading(false);
1523
+ store.decrementPendingUploads();
1479
1524
  }
1480
1525
  };
1481
1526
  const handleRemove = () => {
@@ -1575,6 +1620,22 @@ var SignatureUploadWidget = ({ fieldId }) => {
1575
1620
  const [hasDrawing, setHasDrawing] = React11.useState(false);
1576
1621
  const [isUploading, setIsUploading] = React11.useState(false);
1577
1622
  const [isConfirmed, setIsConfirmed] = React11.useState(false);
1623
+ const [uploadError, setUploadError] = React11.useState(null);
1624
+ const confirmSignatureRef = React11.useRef(null);
1625
+ React11.useEffect(() => {
1626
+ if (hasDrawing && !isConfirmed) {
1627
+ store.registerPreSubmitHandler(fieldId, async () => {
1628
+ if (confirmSignatureRef.current) {
1629
+ await confirmSignatureRef.current();
1630
+ }
1631
+ });
1632
+ } else {
1633
+ store.unregisterPreSubmitHandler(fieldId);
1634
+ }
1635
+ return () => {
1636
+ store.unregisterPreSubmitHandler(fieldId);
1637
+ };
1638
+ }, [hasDrawing, isConfirmed, fieldId, store]);
1578
1639
  const showError = !!error && (touched || state.submitAttempted);
1579
1640
  const displayUrl = React11.useMemo(() => extractDisplayUrl(value), [value]);
1580
1641
  const showPreview = displayUrl !== null;
@@ -1613,6 +1674,7 @@ var SignatureUploadWidget = ({ fieldId }) => {
1613
1674
  if (disabled) return;
1614
1675
  setIsDrawing(true);
1615
1676
  setIsConfirmed(false);
1677
+ setUploadError(null);
1616
1678
  setTouched();
1617
1679
  const ctx = canvasRef.current?.getContext("2d");
1618
1680
  if (!ctx) return;
@@ -1693,7 +1755,9 @@ var SignatureUploadWidget = ({ fieldId }) => {
1693
1755
  const confirmSignature = async () => {
1694
1756
  if (!hasDrawing) return;
1695
1757
  setIsUploading(true);
1758
+ setUploadError(null);
1696
1759
  setTouched();
1760
+ store.incrementPendingUploads();
1697
1761
  try {
1698
1762
  const svg = generateSVG();
1699
1763
  const blob = new Blob([svg], { type: "image/svg+xml" });
@@ -1704,10 +1768,15 @@ var SignatureUploadWidget = ({ fieldId }) => {
1704
1768
  setHasDrawing(false);
1705
1769
  } catch (err) {
1706
1770
  console.error("[SignatureUpload] Upload failed:", err);
1771
+ setUploadError(
1772
+ err instanceof Error ? err.message : "Signature upload failed. Please try again."
1773
+ );
1707
1774
  } finally {
1708
1775
  setIsUploading(false);
1776
+ store.decrementPendingUploads();
1709
1777
  }
1710
1778
  };
1779
+ confirmSignatureRef.current = confirmSignature;
1711
1780
  const canClear = showPreview ? !disabled : hasDrawing || isConfirmed;
1712
1781
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-2", children: [
1713
1782
  /* @__PURE__ */ jsxRuntime.jsxs(Label, { children: [
@@ -1775,19 +1844,22 @@ var SignatureUploadWidget = ({ fieldId }) => {
1775
1844
  isConfirmed ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 text-sm text-green-600", children: [
1776
1845
  /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Check, { className: "h-4 w-4" }),
1777
1846
  /* @__PURE__ */ jsxRuntime.jsx("span", { children: "Signature saved" })
1778
- ] }) : /* @__PURE__ */ jsxRuntime.jsxs(
1779
- Button,
1780
- {
1781
- type: "button",
1782
- onClick: confirmSignature,
1783
- disabled: !hasDrawing || isUploading,
1784
- className: "text-xs",
1785
- children: [
1786
- /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Check, { className: "h-4 w-4 mr-1" }),
1787
- isUploading ? "Saving\u2026" : "Confirm"
1788
- ]
1789
- }
1790
- )
1847
+ ] }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1848
+ /* @__PURE__ */ jsxRuntime.jsxs(
1849
+ Button,
1850
+ {
1851
+ type: "button",
1852
+ onClick: confirmSignature,
1853
+ disabled: !hasDrawing || isUploading,
1854
+ className: "text-xs",
1855
+ children: [
1856
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Check, { className: "h-4 w-4 mr-1" }),
1857
+ isUploading ? "Saving\u2026" : "Confirm"
1858
+ ]
1859
+ }
1860
+ ),
1861
+ uploadError && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-red-500 mt-1", children: uploadError })
1862
+ ] })
1791
1863
  ] })
1792
1864
  ),
1793
1865
  showError && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-red-500", children: error })
@@ -2367,10 +2439,10 @@ var ReadOnlySelect = ({ fieldDef, value }) => {
2367
2439
  return String(opt.value) === String(value) || opt.value === value;
2368
2440
  });
2369
2441
  const displayValue = selectedOption ? selectedOption.label : value ?? "";
2370
- const isEmpty2 = !value && value !== 0;
2442
+ const isEmpty = !value && value !== 0;
2371
2443
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-2", children: [
2372
2444
  /* @__PURE__ */ jsxRuntime.jsx(Label, { className: "text-sm font-medium text-gray-700", children: fieldDef.label }),
2373
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-gray-900 border border-gray-200 rounded-md p-3 bg-gray-50 min-h-[2.5rem] flex items-center", children: loading ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-gray-400 italic", children: "Loading..." }) : isEmpty2 ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-gray-400 italic", children: "No value" }) : displayValue })
2445
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-gray-900 border border-gray-200 rounded-md p-3 bg-gray-50 min-h-[2.5rem] flex items-center", children: loading ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-gray-400 italic", children: "Loading..." }) : isEmpty ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-gray-400 italic", children: "No value" }) : displayValue })
2374
2446
  ] });
2375
2447
  };
2376
2448
  var ReadOnlyMultiSelect = ({ fieldDef, value }) => {
@@ -2394,10 +2466,10 @@ var ReadOnlyMultiSelect = ({ fieldDef, value }) => {
2394
2466
  });
2395
2467
  return option ? option.label : String(val);
2396
2468
  }).filter(Boolean);
2397
- const isEmpty2 = selectedLabels.length === 0;
2469
+ const isEmpty = selectedLabels.length === 0;
2398
2470
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-2", children: [
2399
2471
  /* @__PURE__ */ jsxRuntime.jsx(Label, { className: "text-sm font-medium text-gray-700", children: fieldDef.label }),
2400
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-gray-900 border border-gray-200 rounded-md p-3 bg-gray-50 min-h-[2.5rem]", children: loading ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-gray-400 italic", children: "Loading..." }) : isEmpty2 ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-gray-400 italic", children: "No values selected" }) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-wrap gap-2", children: selectedLabels.map((label, idx) => /* @__PURE__ */ jsxRuntime.jsx(
2472
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-gray-900 border border-gray-200 rounded-md p-3 bg-gray-50 min-h-[2.5rem]", children: loading ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-gray-400 italic", children: "Loading..." }) : isEmpty ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-gray-400 italic", children: "No values selected" }) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-wrap gap-2", children: selectedLabels.map((label, idx) => /* @__PURE__ */ jsxRuntime.jsx(
2401
2473
  "span",
2402
2474
  {
2403
2475
  className: "inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800",
@@ -2600,10 +2672,10 @@ var ReadOnlyRadio = ({ fieldDef, value }) => {
2600
2672
  (opt) => String(opt.value) === String(value) || opt.value === value
2601
2673
  );
2602
2674
  const displayValue = selectedOption ? selectedOption.label : value ?? "";
2603
- const isEmpty2 = value == null || value === "";
2675
+ const isEmpty = value == null || value === "";
2604
2676
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-2", children: [
2605
2677
  /* @__PURE__ */ jsxRuntime.jsx(Label, { className: "text-sm font-medium text-gray-700", children: fieldDef.label }),
2606
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-gray-900 border border-gray-200 rounded-md p-3 bg-gray-50 min-h-[2.5rem] flex items-center", children: loading ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-gray-400 italic", children: "Loading..." }) : isEmpty2 ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-gray-400 italic", children: "No value" }) : displayValue })
2678
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-gray-900 border border-gray-200 rounded-md p-3 bg-gray-50 min-h-[2.5rem] flex items-center", children: loading ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-gray-400 italic", children: "Loading..." }) : isEmpty ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-gray-400 italic", children: "No value" }) : displayValue })
2607
2679
  ] });
2608
2680
  };
2609
2681
  function isOptionSelected2(selected, optValue) {
@@ -2773,6 +2845,7 @@ var FormControls = ({
2773
2845
  onSubmit
2774
2846
  }) => {
2775
2847
  const { state, availableTransitions, submit, transition, hasPersistence, markSubmitAttempted } = useForm();
2848
+ const hasUploadsInFlight = (state.pendingUploads ?? 0) > 0;
2776
2849
  const draftAvailable = onSaveDraft || hasPersistence && submit;
2777
2850
  const submitAvailable = onSubmit || availableTransitions.length > 0 && transition;
2778
2851
  if (!draftAvailable && !submitAvailable) {
@@ -2793,7 +2866,8 @@ var FormControls = ({
2793
2866
  onClick: onSaveDraft || submit,
2794
2867
  variant: "outline",
2795
2868
  size: "sm",
2796
- children: "Save Draft"
2869
+ disabled: hasUploadsInFlight,
2870
+ children: hasUploadsInFlight ? "Upload in progress\u2026" : "Save Draft"
2797
2871
  }
2798
2872
  ),
2799
2873
  submitAvailable && /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: availableTransitions.length > 0 ? availableTransitions.map((t) => /* @__PURE__ */ jsxRuntime.jsx(
@@ -2807,9 +2881,9 @@ var FormControls = ({
2807
2881
  transition(t.to);
2808
2882
  }
2809
2883
  },
2810
- disabled: !state.isValid,
2884
+ disabled: !state.isValid || hasUploadsInFlight,
2811
2885
  size: "sm",
2812
- children: availableTransitions.length > 1 ? `Submit to ${t.to}` : "Submit"
2886
+ children: hasUploadsInFlight ? "Upload in progress\u2026" : availableTransitions.length > 1 ? `Submit to ${t.to}` : "Submit"
2813
2887
  },
2814
2888
  t.to
2815
2889
  )) : onSubmit ? /* @__PURE__ */ jsxRuntime.jsx(
@@ -2819,9 +2893,9 @@ var FormControls = ({
2819
2893
  markSubmitAttempted();
2820
2894
  onSubmit(state.workflowState || "submit");
2821
2895
  },
2822
- disabled: !state.isValid,
2896
+ disabled: !state.isValid || hasUploadsInFlight,
2823
2897
  size: "sm",
2824
- children: "Submit"
2898
+ children: hasUploadsInFlight ? "Upload in progress\u2026" : "Submit"
2825
2899
  }
2826
2900
  ) : null })
2827
2901
  ] })