formanitor 0.0.9 → 0.0.11

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
@@ -204,6 +204,7 @@ var FormStore = class {
204
204
  config;
205
205
  // Cache for debounce
206
206
  saveTimeout = null;
207
+ draftCallbackTimeout = null;
207
208
  constructor(schema, config = {}) {
208
209
  this.schema = schema;
209
210
  this.config = config;
@@ -223,7 +224,8 @@ var FormStore = class {
223
224
  workflowState: initialWorkflowState,
224
225
  isValid: true,
225
226
  isSubmitting: false,
226
- submitAttempted: false
227
+ submitAttempted: false,
228
+ pendingUploads: 0
227
229
  };
228
230
  this.initializeDefaults();
229
231
  this.evaluate();
@@ -272,7 +274,10 @@ var FormStore = class {
272
274
  this.checkEvents(fieldId, "onValueChange", value);
273
275
  this.evaluate();
274
276
  this.notify();
275
- this.scheduleSave();
277
+ this.scheduleDraftCallback();
278
+ if (!this.config.onDraftChange) {
279
+ this.scheduleSave();
280
+ }
276
281
  }
277
282
  setTouched(fieldId) {
278
283
  if (this.state.touched[fieldId]) return;
@@ -308,6 +313,26 @@ var FormStore = class {
308
313
  this.save();
309
314
  }, 1e3);
310
315
  }
316
+ scheduleDraftCallback() {
317
+ if (!this.config.onDraftChange) return;
318
+ if (this.draftCallbackTimeout) {
319
+ clearTimeout(this.draftCallbackTimeout);
320
+ }
321
+ this.draftCallbackTimeout = setTimeout(() => {
322
+ const payload = {
323
+ // For consumer callbacks, send values in the same
324
+ // serialized shape used for outbound submissions so
325
+ // media/signature fields are wrapped consistently.
326
+ values: this.serializeValuesForSubmission(this.state.values),
327
+ meta: {
328
+ updatedAt: Date.now(),
329
+ workflowState: this.state.workflowState,
330
+ version: this.schema.version
331
+ }
332
+ };
333
+ this.config.onDraftChange?.(payload);
334
+ }, 2500);
335
+ }
311
336
  async save() {
312
337
  const payload = {
313
338
  values: this.state.values,
@@ -485,9 +510,53 @@ var FormStore = class {
485
510
  this.config.onEvent(event, finalPayload);
486
511
  }
487
512
  }
513
+ /**
514
+ * Update mutable config properties (callbacks, role) without recreating
515
+ * the store. This allows the host app to pass fresh closures (e.g. an
516
+ * upload handler whose auth token was initially undefined) after the
517
+ * initial render.
518
+ */
519
+ updateConfig(next) {
520
+ let needsReeval = false;
521
+ if (next.onUpload !== void 0) this.config.onUpload = next.onUpload;
522
+ if (next.onEvent !== void 0) this.config.onEvent = next.onEvent;
523
+ if (next.onDraftChange !== void 0) this.config.onDraftChange = next.onDraftChange;
524
+ if (next.role !== void 0 && next.role !== this.config.role) {
525
+ this.config.role = next.role;
526
+ needsReeval = true;
527
+ }
528
+ if (needsReeval) {
529
+ this.evaluate();
530
+ this.notify();
531
+ }
532
+ }
488
533
  getUploadHandler() {
489
534
  return this.config.onUpload;
490
535
  }
536
+ preSubmitHandlers = /* @__PURE__ */ new Map();
537
+ registerPreSubmitHandler(fieldId, fn) {
538
+ this.preSubmitHandlers.set(fieldId, fn);
539
+ }
540
+ unregisterPreSubmitHandler(fieldId) {
541
+ this.preSubmitHandlers.delete(fieldId);
542
+ }
543
+ /**
544
+ * Runs all registered pre-submit handlers in parallel.
545
+ * Call this before getSerializedValuesForSubmission() to ensure any
546
+ * unconfirmed signatures (or similar) are uploaded first.
547
+ */
548
+ async flushPendingUploads() {
549
+ const handlers = Array.from(this.preSubmitHandlers.values());
550
+ await Promise.all(handlers.map((fn) => fn()));
551
+ }
552
+ incrementPendingUploads() {
553
+ this.state.pendingUploads = (this.state.pendingUploads ?? 0) + 1;
554
+ this.notify();
555
+ }
556
+ decrementPendingUploads() {
557
+ this.state.pendingUploads = Math.max(0, (this.state.pendingUploads ?? 0) - 1);
558
+ this.notify();
559
+ }
491
560
  // Mark that a submit attempt has been made so UI can decide when to show errors
492
561
  markSubmitAttempted() {
493
562
  if (!this.state.submitAttempted) {
@@ -519,6 +588,11 @@ var FormProvider = ({ schema, config, children }) => {
519
588
  if (!storeRef.current) {
520
589
  storeRef.current = new FormStore(schema, config);
521
590
  }
591
+ React11.useEffect(() => {
592
+ if (storeRef.current && config) {
593
+ storeRef.current.updateConfig(config);
594
+ }
595
+ });
522
596
  React11.useEffect(() => {
523
597
  storeRef.current?.load();
524
598
  }, []);
@@ -549,7 +623,8 @@ function useForm() {
549
623
  availableTransitions: store.getAvailableTransitions(),
550
624
  hasPersistence: store.hasPersistence(),
551
625
  getSerializedValues: store.getSerializedValuesForSubmission.bind(store),
552
- markSubmitAttempted: store.markSubmitAttempted.bind(store)
626
+ markSubmitAttempted: store.markSubmitAttempted.bind(store),
627
+ flushPendingUploads: store.flushPendingUploads.bind(store)
553
628
  };
554
629
  }
555
630
  function useField(fieldId) {
@@ -1457,6 +1532,7 @@ var ImageUploadWidget = ({ fieldId }) => {
1457
1532
  }
1458
1533
  setIsUploading(true);
1459
1534
  setTouched();
1535
+ store.incrementPendingUploads();
1460
1536
  try {
1461
1537
  const previewUrl = URL.createObjectURL(file);
1462
1538
  setPreview(previewUrl);
@@ -1469,6 +1545,7 @@ var ImageUploadWidget = ({ fieldId }) => {
1469
1545
  setPreview(null);
1470
1546
  } finally {
1471
1547
  setIsUploading(false);
1548
+ store.decrementPendingUploads();
1472
1549
  }
1473
1550
  };
1474
1551
  const handleRemove = () => {
@@ -1568,6 +1645,22 @@ var SignatureUploadWidget = ({ fieldId }) => {
1568
1645
  const [hasDrawing, setHasDrawing] = React11.useState(false);
1569
1646
  const [isUploading, setIsUploading] = React11.useState(false);
1570
1647
  const [isConfirmed, setIsConfirmed] = React11.useState(false);
1648
+ const [uploadError, setUploadError] = React11.useState(null);
1649
+ const confirmSignatureRef = React11.useRef(null);
1650
+ React11.useEffect(() => {
1651
+ if (hasDrawing && !isConfirmed) {
1652
+ store.registerPreSubmitHandler(fieldId, async () => {
1653
+ if (confirmSignatureRef.current) {
1654
+ await confirmSignatureRef.current();
1655
+ }
1656
+ });
1657
+ } else {
1658
+ store.unregisterPreSubmitHandler(fieldId);
1659
+ }
1660
+ return () => {
1661
+ store.unregisterPreSubmitHandler(fieldId);
1662
+ };
1663
+ }, [hasDrawing, isConfirmed, fieldId, store]);
1571
1664
  const showError = !!error && (touched || state.submitAttempted);
1572
1665
  const displayUrl = React11.useMemo(() => extractDisplayUrl(value), [value]);
1573
1666
  const showPreview = displayUrl !== null;
@@ -1606,6 +1699,7 @@ var SignatureUploadWidget = ({ fieldId }) => {
1606
1699
  if (disabled) return;
1607
1700
  setIsDrawing(true);
1608
1701
  setIsConfirmed(false);
1702
+ setUploadError(null);
1609
1703
  setTouched();
1610
1704
  const ctx = canvasRef.current?.getContext("2d");
1611
1705
  if (!ctx) return;
@@ -1686,7 +1780,9 @@ var SignatureUploadWidget = ({ fieldId }) => {
1686
1780
  const confirmSignature = async () => {
1687
1781
  if (!hasDrawing) return;
1688
1782
  setIsUploading(true);
1783
+ setUploadError(null);
1689
1784
  setTouched();
1785
+ store.incrementPendingUploads();
1690
1786
  try {
1691
1787
  const svg = generateSVG();
1692
1788
  const blob = new Blob([svg], { type: "image/svg+xml" });
@@ -1697,10 +1793,15 @@ var SignatureUploadWidget = ({ fieldId }) => {
1697
1793
  setHasDrawing(false);
1698
1794
  } catch (err) {
1699
1795
  console.error("[SignatureUpload] Upload failed:", err);
1796
+ setUploadError(
1797
+ err instanceof Error ? err.message : "Signature upload failed. Please try again."
1798
+ );
1700
1799
  } finally {
1701
1800
  setIsUploading(false);
1801
+ store.decrementPendingUploads();
1702
1802
  }
1703
1803
  };
1804
+ confirmSignatureRef.current = confirmSignature;
1704
1805
  const canClear = showPreview ? !disabled : hasDrawing || isConfirmed;
1705
1806
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-2", children: [
1706
1807
  /* @__PURE__ */ jsxRuntime.jsxs(Label, { children: [
@@ -1768,19 +1869,22 @@ var SignatureUploadWidget = ({ fieldId }) => {
1768
1869
  isConfirmed ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 text-sm text-green-600", children: [
1769
1870
  /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Check, { className: "h-4 w-4" }),
1770
1871
  /* @__PURE__ */ jsxRuntime.jsx("span", { children: "Signature saved" })
1771
- ] }) : /* @__PURE__ */ jsxRuntime.jsxs(
1772
- Button,
1773
- {
1774
- type: "button",
1775
- onClick: confirmSignature,
1776
- disabled: !hasDrawing || isUploading,
1777
- className: "text-xs",
1778
- children: [
1779
- /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Check, { className: "h-4 w-4 mr-1" }),
1780
- isUploading ? "Saving\u2026" : "Confirm"
1781
- ]
1782
- }
1783
- )
1872
+ ] }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1873
+ /* @__PURE__ */ jsxRuntime.jsxs(
1874
+ Button,
1875
+ {
1876
+ type: "button",
1877
+ onClick: confirmSignature,
1878
+ disabled: !hasDrawing || isUploading,
1879
+ className: "text-xs",
1880
+ children: [
1881
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Check, { className: "h-4 w-4 mr-1" }),
1882
+ isUploading ? "Saving\u2026" : "Confirm"
1883
+ ]
1884
+ }
1885
+ ),
1886
+ uploadError && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-red-500 mt-1", children: uploadError })
1887
+ ] })
1784
1888
  ] })
1785
1889
  ),
1786
1890
  showError && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-red-500", children: error })
@@ -2766,6 +2870,7 @@ var FormControls = ({
2766
2870
  onSubmit
2767
2871
  }) => {
2768
2872
  const { state, availableTransitions, submit, transition, hasPersistence, markSubmitAttempted } = useForm();
2873
+ const hasUploadsInFlight = (state.pendingUploads ?? 0) > 0;
2769
2874
  const draftAvailable = onSaveDraft || hasPersistence && submit;
2770
2875
  const submitAvailable = onSubmit || availableTransitions.length > 0 && transition;
2771
2876
  if (!draftAvailable && !submitAvailable) {
@@ -2786,7 +2891,8 @@ var FormControls = ({
2786
2891
  onClick: onSaveDraft || submit,
2787
2892
  variant: "outline",
2788
2893
  size: "sm",
2789
- children: "Save Draft"
2894
+ disabled: hasUploadsInFlight,
2895
+ children: hasUploadsInFlight ? "Upload in progress\u2026" : "Save Draft"
2790
2896
  }
2791
2897
  ),
2792
2898
  submitAvailable && /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: availableTransitions.length > 0 ? availableTransitions.map((t) => /* @__PURE__ */ jsxRuntime.jsx(
@@ -2800,9 +2906,9 @@ var FormControls = ({
2800
2906
  transition(t.to);
2801
2907
  }
2802
2908
  },
2803
- disabled: !state.isValid,
2909
+ disabled: !state.isValid || hasUploadsInFlight,
2804
2910
  size: "sm",
2805
- children: availableTransitions.length > 1 ? `Submit to ${t.to}` : "Submit"
2911
+ children: hasUploadsInFlight ? "Upload in progress\u2026" : availableTransitions.length > 1 ? `Submit to ${t.to}` : "Submit"
2806
2912
  },
2807
2913
  t.to
2808
2914
  )) : onSubmit ? /* @__PURE__ */ jsxRuntime.jsx(
@@ -2812,9 +2918,9 @@ var FormControls = ({
2812
2918
  markSubmitAttempted();
2813
2919
  onSubmit(state.workflowState || "submit");
2814
2920
  },
2815
- disabled: !state.isValid,
2921
+ disabled: !state.isValid || hasUploadsInFlight,
2816
2922
  size: "sm",
2817
- children: "Submit"
2923
+ children: hasUploadsInFlight ? "Upload in progress\u2026" : "Submit"
2818
2924
  }
2819
2925
  ) : null })
2820
2926
  ] })