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.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
@@ -155,10 +155,6 @@ var NO_OP_PROVIDER = {
155
155
  function applyPrefill(currentValues, fields, prefilledData = {}) {
156
156
  const newValues = { ...currentValues };
157
157
  fields.forEach((field) => {
158
- const currentValue = newValues[field.id];
159
- if (!isEmpty(currentValue)) {
160
- return;
161
- }
162
158
  if (field.prefill?.value !== void 0) {
163
159
  newValues[field.id] = field.prefill.value;
164
160
  return;
@@ -170,9 +166,6 @@ function applyPrefill(currentValues, fields, prefilledData = {}) {
170
166
  });
171
167
  return newValues;
172
168
  }
173
- function isEmpty(val) {
174
- return val === void 0 || val === null || val === "";
175
- }
176
169
 
177
170
  // src/core/store.ts
178
171
  var FormStore = class {
@@ -204,7 +197,8 @@ var FormStore = class {
204
197
  workflowState: initialWorkflowState,
205
198
  isValid: true,
206
199
  isSubmitting: false,
207
- submitAttempted: false
200
+ submitAttempted: false,
201
+ pendingUploads: 0
208
202
  };
209
203
  this.initializeDefaults();
210
204
  this.evaluate();
@@ -466,9 +460,52 @@ var FormStore = class {
466
460
  this.config.onEvent(event, finalPayload);
467
461
  }
468
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
+ }
469
482
  getUploadHandler() {
470
483
  return this.config.onUpload;
471
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
+ }
472
509
  // Mark that a submit attempt has been made so UI can decide when to show errors
473
510
  markSubmitAttempted() {
474
511
  if (!this.state.submitAttempted) {
@@ -500,6 +537,11 @@ var FormProvider = ({ schema, config, children }) => {
500
537
  if (!storeRef.current) {
501
538
  storeRef.current = new FormStore(schema, config);
502
539
  }
540
+ useEffect(() => {
541
+ if (storeRef.current && config) {
542
+ storeRef.current.updateConfig(config);
543
+ }
544
+ });
503
545
  useEffect(() => {
504
546
  storeRef.current?.load();
505
547
  }, []);
@@ -530,7 +572,8 @@ function useForm() {
530
572
  availableTransitions: store.getAvailableTransitions(),
531
573
  hasPersistence: store.hasPersistence(),
532
574
  getSerializedValues: store.getSerializedValuesForSubmission.bind(store),
533
- markSubmitAttempted: store.markSubmitAttempted.bind(store)
575
+ markSubmitAttempted: store.markSubmitAttempted.bind(store),
576
+ flushPendingUploads: store.flushPendingUploads.bind(store)
534
577
  };
535
578
  }
536
579
  function useField(fieldId) {
@@ -1438,6 +1481,7 @@ var ImageUploadWidget = ({ fieldId }) => {
1438
1481
  }
1439
1482
  setIsUploading(true);
1440
1483
  setTouched();
1484
+ store.incrementPendingUploads();
1441
1485
  try {
1442
1486
  const previewUrl = URL.createObjectURL(file);
1443
1487
  setPreview(previewUrl);
@@ -1450,6 +1494,7 @@ var ImageUploadWidget = ({ fieldId }) => {
1450
1494
  setPreview(null);
1451
1495
  } finally {
1452
1496
  setIsUploading(false);
1497
+ store.decrementPendingUploads();
1453
1498
  }
1454
1499
  };
1455
1500
  const handleRemove = () => {
@@ -1549,6 +1594,22 @@ var SignatureUploadWidget = ({ fieldId }) => {
1549
1594
  const [hasDrawing, setHasDrawing] = useState(false);
1550
1595
  const [isUploading, setIsUploading] = useState(false);
1551
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]);
1552
1613
  const showError = !!error && (touched || state.submitAttempted);
1553
1614
  const displayUrl = useMemo(() => extractDisplayUrl(value), [value]);
1554
1615
  const showPreview = displayUrl !== null;
@@ -1587,6 +1648,7 @@ var SignatureUploadWidget = ({ fieldId }) => {
1587
1648
  if (disabled) return;
1588
1649
  setIsDrawing(true);
1589
1650
  setIsConfirmed(false);
1651
+ setUploadError(null);
1590
1652
  setTouched();
1591
1653
  const ctx = canvasRef.current?.getContext("2d");
1592
1654
  if (!ctx) return;
@@ -1667,7 +1729,9 @@ var SignatureUploadWidget = ({ fieldId }) => {
1667
1729
  const confirmSignature = async () => {
1668
1730
  if (!hasDrawing) return;
1669
1731
  setIsUploading(true);
1732
+ setUploadError(null);
1670
1733
  setTouched();
1734
+ store.incrementPendingUploads();
1671
1735
  try {
1672
1736
  const svg = generateSVG();
1673
1737
  const blob = new Blob([svg], { type: "image/svg+xml" });
@@ -1678,10 +1742,15 @@ var SignatureUploadWidget = ({ fieldId }) => {
1678
1742
  setHasDrawing(false);
1679
1743
  } catch (err) {
1680
1744
  console.error("[SignatureUpload] Upload failed:", err);
1745
+ setUploadError(
1746
+ err instanceof Error ? err.message : "Signature upload failed. Please try again."
1747
+ );
1681
1748
  } finally {
1682
1749
  setIsUploading(false);
1750
+ store.decrementPendingUploads();
1683
1751
  }
1684
1752
  };
1753
+ confirmSignatureRef.current = confirmSignature;
1685
1754
  const canClear = showPreview ? !disabled : hasDrawing || isConfirmed;
1686
1755
  return /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
1687
1756
  /* @__PURE__ */ jsxs(Label, { children: [
@@ -1749,19 +1818,22 @@ var SignatureUploadWidget = ({ fieldId }) => {
1749
1818
  isConfirmed ? /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 text-sm text-green-600", children: [
1750
1819
  /* @__PURE__ */ jsx(Check, { className: "h-4 w-4" }),
1751
1820
  /* @__PURE__ */ jsx("span", { children: "Signature saved" })
1752
- ] }) : /* @__PURE__ */ jsxs(
1753
- Button,
1754
- {
1755
- type: "button",
1756
- onClick: confirmSignature,
1757
- disabled: !hasDrawing || isUploading,
1758
- className: "text-xs",
1759
- children: [
1760
- /* @__PURE__ */ jsx(Check, { className: "h-4 w-4 mr-1" }),
1761
- isUploading ? "Saving\u2026" : "Confirm"
1762
- ]
1763
- }
1764
- )
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
+ ] })
1765
1837
  ] })
1766
1838
  ),
1767
1839
  showError && /* @__PURE__ */ jsx("p", { className: "text-xs text-red-500", children: error })
@@ -2341,10 +2413,10 @@ var ReadOnlySelect = ({ fieldDef, value }) => {
2341
2413
  return String(opt.value) === String(value) || opt.value === value;
2342
2414
  });
2343
2415
  const displayValue = selectedOption ? selectedOption.label : value ?? "";
2344
- const isEmpty2 = !value && value !== 0;
2416
+ const isEmpty = !value && value !== 0;
2345
2417
  return /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
2346
2418
  /* @__PURE__ */ jsx(Label, { className: "text-sm font-medium text-gray-700", children: fieldDef.label }),
2347
- /* @__PURE__ */ 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__ */ jsx("span", { className: "text-gray-400 italic", children: "Loading..." }) : isEmpty2 ? /* @__PURE__ */ jsx("span", { className: "text-gray-400 italic", children: "No value" }) : displayValue })
2419
+ /* @__PURE__ */ 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__ */ jsx("span", { className: "text-gray-400 italic", children: "Loading..." }) : isEmpty ? /* @__PURE__ */ jsx("span", { className: "text-gray-400 italic", children: "No value" }) : displayValue })
2348
2420
  ] });
2349
2421
  };
2350
2422
  var ReadOnlyMultiSelect = ({ fieldDef, value }) => {
@@ -2368,10 +2440,10 @@ var ReadOnlyMultiSelect = ({ fieldDef, value }) => {
2368
2440
  });
2369
2441
  return option ? option.label : String(val);
2370
2442
  }).filter(Boolean);
2371
- const isEmpty2 = selectedLabels.length === 0;
2443
+ const isEmpty = selectedLabels.length === 0;
2372
2444
  return /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
2373
2445
  /* @__PURE__ */ jsx(Label, { className: "text-sm font-medium text-gray-700", children: fieldDef.label }),
2374
- /* @__PURE__ */ jsx("div", { className: "text-gray-900 border border-gray-200 rounded-md p-3 bg-gray-50 min-h-[2.5rem]", children: loading ? /* @__PURE__ */ jsx("span", { className: "text-gray-400 italic", children: "Loading..." }) : isEmpty2 ? /* @__PURE__ */ jsx("span", { className: "text-gray-400 italic", children: "No values selected" }) : /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-2", children: selectedLabels.map((label, idx) => /* @__PURE__ */ jsx(
2446
+ /* @__PURE__ */ jsx("div", { className: "text-gray-900 border border-gray-200 rounded-md p-3 bg-gray-50 min-h-[2.5rem]", children: loading ? /* @__PURE__ */ jsx("span", { className: "text-gray-400 italic", children: "Loading..." }) : isEmpty ? /* @__PURE__ */ jsx("span", { className: "text-gray-400 italic", children: "No values selected" }) : /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-2", children: selectedLabels.map((label, idx) => /* @__PURE__ */ jsx(
2375
2447
  "span",
2376
2448
  {
2377
2449
  className: "inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800",
@@ -2574,10 +2646,10 @@ var ReadOnlyRadio = ({ fieldDef, value }) => {
2574
2646
  (opt) => String(opt.value) === String(value) || opt.value === value
2575
2647
  );
2576
2648
  const displayValue = selectedOption ? selectedOption.label : value ?? "";
2577
- const isEmpty2 = value == null || value === "";
2649
+ const isEmpty = value == null || value === "";
2578
2650
  return /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
2579
2651
  /* @__PURE__ */ jsx(Label, { className: "text-sm font-medium text-gray-700", children: fieldDef.label }),
2580
- /* @__PURE__ */ 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__ */ jsx("span", { className: "text-gray-400 italic", children: "Loading..." }) : isEmpty2 ? /* @__PURE__ */ jsx("span", { className: "text-gray-400 italic", children: "No value" }) : displayValue })
2652
+ /* @__PURE__ */ 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__ */ jsx("span", { className: "text-gray-400 italic", children: "Loading..." }) : isEmpty ? /* @__PURE__ */ jsx("span", { className: "text-gray-400 italic", children: "No value" }) : displayValue })
2581
2653
  ] });
2582
2654
  };
2583
2655
  function isOptionSelected2(selected, optValue) {
@@ -2747,6 +2819,7 @@ var FormControls = ({
2747
2819
  onSubmit
2748
2820
  }) => {
2749
2821
  const { state, availableTransitions, submit, transition, hasPersistence, markSubmitAttempted } = useForm();
2822
+ const hasUploadsInFlight = (state.pendingUploads ?? 0) > 0;
2750
2823
  const draftAvailable = onSaveDraft || hasPersistence && submit;
2751
2824
  const submitAvailable = onSubmit || availableTransitions.length > 0 && transition;
2752
2825
  if (!draftAvailable && !submitAvailable) {
@@ -2767,7 +2840,8 @@ var FormControls = ({
2767
2840
  onClick: onSaveDraft || submit,
2768
2841
  variant: "outline",
2769
2842
  size: "sm",
2770
- children: "Save Draft"
2843
+ disabled: hasUploadsInFlight,
2844
+ children: hasUploadsInFlight ? "Upload in progress\u2026" : "Save Draft"
2771
2845
  }
2772
2846
  ),
2773
2847
  submitAvailable && /* @__PURE__ */ jsx(Fragment, { children: availableTransitions.length > 0 ? availableTransitions.map((t) => /* @__PURE__ */ jsx(
@@ -2781,9 +2855,9 @@ var FormControls = ({
2781
2855
  transition(t.to);
2782
2856
  }
2783
2857
  },
2784
- disabled: !state.isValid,
2858
+ disabled: !state.isValid || hasUploadsInFlight,
2785
2859
  size: "sm",
2786
- children: availableTransitions.length > 1 ? `Submit to ${t.to}` : "Submit"
2860
+ children: hasUploadsInFlight ? "Upload in progress\u2026" : availableTransitions.length > 1 ? `Submit to ${t.to}` : "Submit"
2787
2861
  },
2788
2862
  t.to
2789
2863
  )) : onSubmit ? /* @__PURE__ */ jsx(
@@ -2793,9 +2867,9 @@ var FormControls = ({
2793
2867
  markSubmitAttempted();
2794
2868
  onSubmit(state.workflowState || "submit");
2795
2869
  },
2796
- disabled: !state.isValid,
2870
+ disabled: !state.isValid || hasUploadsInFlight,
2797
2871
  size: "sm",
2798
- children: "Submit"
2872
+ children: hasUploadsInFlight ? "Upload in progress\u2026" : "Submit"
2799
2873
  }
2800
2874
  ) : null })
2801
2875
  ] })