formanitor 0.0.9 → 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.d.cts CHANGED
@@ -224,6 +224,7 @@ interface FormState {
224
224
  isValid: boolean;
225
225
  isSubmitting: boolean;
226
226
  submitAttempted?: boolean;
227
+ pendingUploads: number;
227
228
  }
228
229
 
229
230
  interface SavedState {
@@ -298,7 +299,25 @@ declare class FormStore {
298
299
  transition(toState: string): void;
299
300
  private checkEvents;
300
301
  private emit;
302
+ /**
303
+ * Update mutable config properties (callbacks, role) without recreating
304
+ * the store. This allows the host app to pass fresh closures (e.g. an
305
+ * upload handler whose auth token was initially undefined) after the
306
+ * initial render.
307
+ */
308
+ updateConfig(next: Partial<StoreConfig>): void;
301
309
  getUploadHandler(): UploadHandler | undefined;
310
+ private preSubmitHandlers;
311
+ registerPreSubmitHandler(fieldId: string, fn: () => Promise<void>): void;
312
+ unregisterPreSubmitHandler(fieldId: string): void;
313
+ /**
314
+ * Runs all registered pre-submit handlers in parallel.
315
+ * Call this before getSerializedValuesForSubmission() to ensure any
316
+ * unconfirmed signatures (or similar) are uploaded first.
317
+ */
318
+ flushPendingUploads(): Promise<void>;
319
+ incrementPendingUploads(): void;
320
+ decrementPendingUploads(): void;
302
321
  markSubmitAttempted(): void;
303
322
  subscribe(listener: Listener): () => void;
304
323
  private notify;
@@ -332,6 +351,7 @@ declare function useForm(): {
332
351
  hasPersistence: boolean;
333
352
  getSerializedValues: () => FormValues;
334
353
  markSubmitAttempted: () => void;
354
+ flushPendingUploads: () => Promise<void>;
335
355
  };
336
356
 
337
357
  declare function useField(fieldId: string): {
package/dist/index.d.ts CHANGED
@@ -224,6 +224,7 @@ interface FormState {
224
224
  isValid: boolean;
225
225
  isSubmitting: boolean;
226
226
  submitAttempted?: boolean;
227
+ pendingUploads: number;
227
228
  }
228
229
 
229
230
  interface SavedState {
@@ -298,7 +299,25 @@ declare class FormStore {
298
299
  transition(toState: string): void;
299
300
  private checkEvents;
300
301
  private emit;
302
+ /**
303
+ * Update mutable config properties (callbacks, role) without recreating
304
+ * the store. This allows the host app to pass fresh closures (e.g. an
305
+ * upload handler whose auth token was initially undefined) after the
306
+ * initial render.
307
+ */
308
+ updateConfig(next: Partial<StoreConfig>): void;
301
309
  getUploadHandler(): UploadHandler | undefined;
310
+ private preSubmitHandlers;
311
+ registerPreSubmitHandler(fieldId: string, fn: () => Promise<void>): void;
312
+ unregisterPreSubmitHandler(fieldId: string): void;
313
+ /**
314
+ * Runs all registered pre-submit handlers in parallel.
315
+ * Call this before getSerializedValuesForSubmission() to ensure any
316
+ * unconfirmed signatures (or similar) are uploaded first.
317
+ */
318
+ flushPendingUploads(): Promise<void>;
319
+ incrementPendingUploads(): void;
320
+ decrementPendingUploads(): void;
302
321
  markSubmitAttempted(): void;
303
322
  subscribe(listener: Listener): () => void;
304
323
  private notify;
@@ -332,6 +351,7 @@ declare function useForm(): {
332
351
  hasPersistence: boolean;
333
352
  getSerializedValues: () => FormValues;
334
353
  markSubmitAttempted: () => void;
354
+ flushPendingUploads: () => Promise<void>;
335
355
  };
336
356
 
337
357
  declare function useField(fieldId: string): {
package/dist/index.mjs CHANGED
@@ -197,7 +197,8 @@ var FormStore = class {
197
197
  workflowState: initialWorkflowState,
198
198
  isValid: true,
199
199
  isSubmitting: false,
200
- submitAttempted: false
200
+ submitAttempted: false,
201
+ pendingUploads: 0
201
202
  };
202
203
  this.initializeDefaults();
203
204
  this.evaluate();
@@ -459,9 +460,52 @@ var FormStore = class {
459
460
  this.config.onEvent(event, finalPayload);
460
461
  }
461
462
  }
463
+ /**
464
+ * Update mutable config properties (callbacks, role) without recreating
465
+ * the store. This allows the host app to pass fresh closures (e.g. an
466
+ * upload handler whose auth token was initially undefined) after the
467
+ * initial render.
468
+ */
469
+ updateConfig(next) {
470
+ let needsReeval = false;
471
+ if (next.onUpload !== void 0) this.config.onUpload = next.onUpload;
472
+ if (next.onEvent !== void 0) this.config.onEvent = next.onEvent;
473
+ if (next.role !== void 0 && next.role !== this.config.role) {
474
+ this.config.role = next.role;
475
+ needsReeval = true;
476
+ }
477
+ if (needsReeval) {
478
+ this.evaluate();
479
+ this.notify();
480
+ }
481
+ }
462
482
  getUploadHandler() {
463
483
  return this.config.onUpload;
464
484
  }
485
+ preSubmitHandlers = /* @__PURE__ */ new Map();
486
+ registerPreSubmitHandler(fieldId, fn) {
487
+ this.preSubmitHandlers.set(fieldId, fn);
488
+ }
489
+ unregisterPreSubmitHandler(fieldId) {
490
+ this.preSubmitHandlers.delete(fieldId);
491
+ }
492
+ /**
493
+ * Runs all registered pre-submit handlers in parallel.
494
+ * Call this before getSerializedValuesForSubmission() to ensure any
495
+ * unconfirmed signatures (or similar) are uploaded first.
496
+ */
497
+ async flushPendingUploads() {
498
+ const handlers = Array.from(this.preSubmitHandlers.values());
499
+ await Promise.all(handlers.map((fn) => fn()));
500
+ }
501
+ incrementPendingUploads() {
502
+ this.state.pendingUploads = (this.state.pendingUploads ?? 0) + 1;
503
+ this.notify();
504
+ }
505
+ decrementPendingUploads() {
506
+ this.state.pendingUploads = Math.max(0, (this.state.pendingUploads ?? 0) - 1);
507
+ this.notify();
508
+ }
465
509
  // Mark that a submit attempt has been made so UI can decide when to show errors
466
510
  markSubmitAttempted() {
467
511
  if (!this.state.submitAttempted) {
@@ -493,6 +537,11 @@ var FormProvider = ({ schema, config, children }) => {
493
537
  if (!storeRef.current) {
494
538
  storeRef.current = new FormStore(schema, config);
495
539
  }
540
+ useEffect(() => {
541
+ if (storeRef.current && config) {
542
+ storeRef.current.updateConfig(config);
543
+ }
544
+ });
496
545
  useEffect(() => {
497
546
  storeRef.current?.load();
498
547
  }, []);
@@ -523,7 +572,8 @@ function useForm() {
523
572
  availableTransitions: store.getAvailableTransitions(),
524
573
  hasPersistence: store.hasPersistence(),
525
574
  getSerializedValues: store.getSerializedValuesForSubmission.bind(store),
526
- markSubmitAttempted: store.markSubmitAttempted.bind(store)
575
+ markSubmitAttempted: store.markSubmitAttempted.bind(store),
576
+ flushPendingUploads: store.flushPendingUploads.bind(store)
527
577
  };
528
578
  }
529
579
  function useField(fieldId) {
@@ -1431,6 +1481,7 @@ var ImageUploadWidget = ({ fieldId }) => {
1431
1481
  }
1432
1482
  setIsUploading(true);
1433
1483
  setTouched();
1484
+ store.incrementPendingUploads();
1434
1485
  try {
1435
1486
  const previewUrl = URL.createObjectURL(file);
1436
1487
  setPreview(previewUrl);
@@ -1443,6 +1494,7 @@ var ImageUploadWidget = ({ fieldId }) => {
1443
1494
  setPreview(null);
1444
1495
  } finally {
1445
1496
  setIsUploading(false);
1497
+ store.decrementPendingUploads();
1446
1498
  }
1447
1499
  };
1448
1500
  const handleRemove = () => {
@@ -1542,6 +1594,22 @@ var SignatureUploadWidget = ({ fieldId }) => {
1542
1594
  const [hasDrawing, setHasDrawing] = useState(false);
1543
1595
  const [isUploading, setIsUploading] = useState(false);
1544
1596
  const [isConfirmed, setIsConfirmed] = useState(false);
1597
+ const [uploadError, setUploadError] = useState(null);
1598
+ const confirmSignatureRef = useRef(null);
1599
+ useEffect(() => {
1600
+ if (hasDrawing && !isConfirmed) {
1601
+ store.registerPreSubmitHandler(fieldId, async () => {
1602
+ if (confirmSignatureRef.current) {
1603
+ await confirmSignatureRef.current();
1604
+ }
1605
+ });
1606
+ } else {
1607
+ store.unregisterPreSubmitHandler(fieldId);
1608
+ }
1609
+ return () => {
1610
+ store.unregisterPreSubmitHandler(fieldId);
1611
+ };
1612
+ }, [hasDrawing, isConfirmed, fieldId, store]);
1545
1613
  const showError = !!error && (touched || state.submitAttempted);
1546
1614
  const displayUrl = useMemo(() => extractDisplayUrl(value), [value]);
1547
1615
  const showPreview = displayUrl !== null;
@@ -1580,6 +1648,7 @@ var SignatureUploadWidget = ({ fieldId }) => {
1580
1648
  if (disabled) return;
1581
1649
  setIsDrawing(true);
1582
1650
  setIsConfirmed(false);
1651
+ setUploadError(null);
1583
1652
  setTouched();
1584
1653
  const ctx = canvasRef.current?.getContext("2d");
1585
1654
  if (!ctx) return;
@@ -1660,7 +1729,9 @@ var SignatureUploadWidget = ({ fieldId }) => {
1660
1729
  const confirmSignature = async () => {
1661
1730
  if (!hasDrawing) return;
1662
1731
  setIsUploading(true);
1732
+ setUploadError(null);
1663
1733
  setTouched();
1734
+ store.incrementPendingUploads();
1664
1735
  try {
1665
1736
  const svg = generateSVG();
1666
1737
  const blob = new Blob([svg], { type: "image/svg+xml" });
@@ -1671,10 +1742,15 @@ var SignatureUploadWidget = ({ fieldId }) => {
1671
1742
  setHasDrawing(false);
1672
1743
  } catch (err) {
1673
1744
  console.error("[SignatureUpload] Upload failed:", err);
1745
+ setUploadError(
1746
+ err instanceof Error ? err.message : "Signature upload failed. Please try again."
1747
+ );
1674
1748
  } finally {
1675
1749
  setIsUploading(false);
1750
+ store.decrementPendingUploads();
1676
1751
  }
1677
1752
  };
1753
+ confirmSignatureRef.current = confirmSignature;
1678
1754
  const canClear = showPreview ? !disabled : hasDrawing || isConfirmed;
1679
1755
  return /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
1680
1756
  /* @__PURE__ */ jsxs(Label, { children: [
@@ -1742,19 +1818,22 @@ var SignatureUploadWidget = ({ fieldId }) => {
1742
1818
  isConfirmed ? /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 text-sm text-green-600", children: [
1743
1819
  /* @__PURE__ */ jsx(Check, { className: "h-4 w-4" }),
1744
1820
  /* @__PURE__ */ jsx("span", { children: "Signature saved" })
1745
- ] }) : /* @__PURE__ */ jsxs(
1746
- Button,
1747
- {
1748
- type: "button",
1749
- onClick: confirmSignature,
1750
- disabled: !hasDrawing || isUploading,
1751
- className: "text-xs",
1752
- children: [
1753
- /* @__PURE__ */ jsx(Check, { className: "h-4 w-4 mr-1" }),
1754
- isUploading ? "Saving\u2026" : "Confirm"
1755
- ]
1756
- }
1757
- )
1821
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
1822
+ /* @__PURE__ */ jsxs(
1823
+ Button,
1824
+ {
1825
+ type: "button",
1826
+ onClick: confirmSignature,
1827
+ disabled: !hasDrawing || isUploading,
1828
+ className: "text-xs",
1829
+ children: [
1830
+ /* @__PURE__ */ jsx(Check, { className: "h-4 w-4 mr-1" }),
1831
+ isUploading ? "Saving\u2026" : "Confirm"
1832
+ ]
1833
+ }
1834
+ ),
1835
+ uploadError && /* @__PURE__ */ jsx("p", { className: "text-xs text-red-500 mt-1", children: uploadError })
1836
+ ] })
1758
1837
  ] })
1759
1838
  ),
1760
1839
  showError && /* @__PURE__ */ jsx("p", { className: "text-xs text-red-500", children: error })
@@ -2740,6 +2819,7 @@ var FormControls = ({
2740
2819
  onSubmit
2741
2820
  }) => {
2742
2821
  const { state, availableTransitions, submit, transition, hasPersistence, markSubmitAttempted } = useForm();
2822
+ const hasUploadsInFlight = (state.pendingUploads ?? 0) > 0;
2743
2823
  const draftAvailable = onSaveDraft || hasPersistence && submit;
2744
2824
  const submitAvailable = onSubmit || availableTransitions.length > 0 && transition;
2745
2825
  if (!draftAvailable && !submitAvailable) {
@@ -2760,7 +2840,8 @@ var FormControls = ({
2760
2840
  onClick: onSaveDraft || submit,
2761
2841
  variant: "outline",
2762
2842
  size: "sm",
2763
- children: "Save Draft"
2843
+ disabled: hasUploadsInFlight,
2844
+ children: hasUploadsInFlight ? "Upload in progress\u2026" : "Save Draft"
2764
2845
  }
2765
2846
  ),
2766
2847
  submitAvailable && /* @__PURE__ */ jsx(Fragment, { children: availableTransitions.length > 0 ? availableTransitions.map((t) => /* @__PURE__ */ jsx(
@@ -2774,9 +2855,9 @@ var FormControls = ({
2774
2855
  transition(t.to);
2775
2856
  }
2776
2857
  },
2777
- disabled: !state.isValid,
2858
+ disabled: !state.isValid || hasUploadsInFlight,
2778
2859
  size: "sm",
2779
- children: availableTransitions.length > 1 ? `Submit to ${t.to}` : "Submit"
2860
+ children: hasUploadsInFlight ? "Upload in progress\u2026" : availableTransitions.length > 1 ? `Submit to ${t.to}` : "Submit"
2780
2861
  },
2781
2862
  t.to
2782
2863
  )) : onSubmit ? /* @__PURE__ */ jsx(
@@ -2786,9 +2867,9 @@ var FormControls = ({
2786
2867
  markSubmitAttempted();
2787
2868
  onSubmit(state.workflowState || "submit");
2788
2869
  },
2789
- disabled: !state.isValid,
2870
+ disabled: !state.isValid || hasUploadsInFlight,
2790
2871
  size: "sm",
2791
- children: "Submit"
2872
+ children: hasUploadsInFlight ? "Upload in progress\u2026" : "Submit"
2792
2873
  }
2793
2874
  ) : null })
2794
2875
  ] })