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.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 {
@@ -243,12 +244,14 @@ interface PersistenceProvider {
243
244
  type Listener = (state: FormState) => void;
244
245
  type EventListener = (event: string, payload?: any) => void;
245
246
  type UploadHandler = (file: File | Blob, filename: string) => Promise<string>;
247
+ type DraftChangeHandler = (payload: SavedState) => void | Promise<void>;
246
248
  interface StoreConfig {
247
249
  persistence?: PersistenceProvider;
248
250
  prefillData?: Record<string, any>;
249
251
  role?: string;
250
252
  onEvent?: EventListener;
251
253
  onUpload?: UploadHandler;
254
+ onDraftChange?: DraftChangeHandler;
252
255
  }
253
256
  declare class FormStore {
254
257
  private state;
@@ -259,6 +262,7 @@ declare class FormStore {
259
262
  private prefillData;
260
263
  private config;
261
264
  private saveTimeout;
265
+ private draftCallbackTimeout;
262
266
  constructor(schema: FormDef, config?: StoreConfig);
263
267
  private initializeDefaults;
264
268
  getState(): FormState;
@@ -277,6 +281,7 @@ declare class FormStore {
277
281
  setRole(role: string): void;
278
282
  load(): Promise<void>;
279
283
  private scheduleSave;
284
+ private scheduleDraftCallback;
280
285
  save(): Promise<void>;
281
286
  /**
282
287
  * Prepare values for outbound submission to the backend.
@@ -298,7 +303,25 @@ declare class FormStore {
298
303
  transition(toState: string): void;
299
304
  private checkEvents;
300
305
  private emit;
306
+ /**
307
+ * Update mutable config properties (callbacks, role) without recreating
308
+ * the store. This allows the host app to pass fresh closures (e.g. an
309
+ * upload handler whose auth token was initially undefined) after the
310
+ * initial render.
311
+ */
312
+ updateConfig(next: Partial<StoreConfig>): void;
301
313
  getUploadHandler(): UploadHandler | undefined;
314
+ private preSubmitHandlers;
315
+ registerPreSubmitHandler(fieldId: string, fn: () => Promise<void>): void;
316
+ unregisterPreSubmitHandler(fieldId: string): void;
317
+ /**
318
+ * Runs all registered pre-submit handlers in parallel.
319
+ * Call this before getSerializedValuesForSubmission() to ensure any
320
+ * unconfirmed signatures (or similar) are uploaded first.
321
+ */
322
+ flushPendingUploads(): Promise<void>;
323
+ incrementPendingUploads(): void;
324
+ decrementPendingUploads(): void;
302
325
  markSubmitAttempted(): void;
303
326
  subscribe(listener: Listener): () => void;
304
327
  private notify;
@@ -332,6 +355,7 @@ declare function useForm(): {
332
355
  hasPersistence: boolean;
333
356
  getSerializedValues: () => FormValues;
334
357
  markSubmitAttempted: () => void;
358
+ flushPendingUploads: () => Promise<void>;
335
359
  };
336
360
 
337
361
  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 {
@@ -243,12 +244,14 @@ interface PersistenceProvider {
243
244
  type Listener = (state: FormState) => void;
244
245
  type EventListener = (event: string, payload?: any) => void;
245
246
  type UploadHandler = (file: File | Blob, filename: string) => Promise<string>;
247
+ type DraftChangeHandler = (payload: SavedState) => void | Promise<void>;
246
248
  interface StoreConfig {
247
249
  persistence?: PersistenceProvider;
248
250
  prefillData?: Record<string, any>;
249
251
  role?: string;
250
252
  onEvent?: EventListener;
251
253
  onUpload?: UploadHandler;
254
+ onDraftChange?: DraftChangeHandler;
252
255
  }
253
256
  declare class FormStore {
254
257
  private state;
@@ -259,6 +262,7 @@ declare class FormStore {
259
262
  private prefillData;
260
263
  private config;
261
264
  private saveTimeout;
265
+ private draftCallbackTimeout;
262
266
  constructor(schema: FormDef, config?: StoreConfig);
263
267
  private initializeDefaults;
264
268
  getState(): FormState;
@@ -277,6 +281,7 @@ declare class FormStore {
277
281
  setRole(role: string): void;
278
282
  load(): Promise<void>;
279
283
  private scheduleSave;
284
+ private scheduleDraftCallback;
280
285
  save(): Promise<void>;
281
286
  /**
282
287
  * Prepare values for outbound submission to the backend.
@@ -298,7 +303,25 @@ declare class FormStore {
298
303
  transition(toState: string): void;
299
304
  private checkEvents;
300
305
  private emit;
306
+ /**
307
+ * Update mutable config properties (callbacks, role) without recreating
308
+ * the store. This allows the host app to pass fresh closures (e.g. an
309
+ * upload handler whose auth token was initially undefined) after the
310
+ * initial render.
311
+ */
312
+ updateConfig(next: Partial<StoreConfig>): void;
301
313
  getUploadHandler(): UploadHandler | undefined;
314
+ private preSubmitHandlers;
315
+ registerPreSubmitHandler(fieldId: string, fn: () => Promise<void>): void;
316
+ unregisterPreSubmitHandler(fieldId: string): void;
317
+ /**
318
+ * Runs all registered pre-submit handlers in parallel.
319
+ * Call this before getSerializedValuesForSubmission() to ensure any
320
+ * unconfirmed signatures (or similar) are uploaded first.
321
+ */
322
+ flushPendingUploads(): Promise<void>;
323
+ incrementPendingUploads(): void;
324
+ decrementPendingUploads(): void;
302
325
  markSubmitAttempted(): void;
303
326
  subscribe(listener: Listener): () => void;
304
327
  private notify;
@@ -332,6 +355,7 @@ declare function useForm(): {
332
355
  hasPersistence: boolean;
333
356
  getSerializedValues: () => FormValues;
334
357
  markSubmitAttempted: () => void;
358
+ flushPendingUploads: () => Promise<void>;
335
359
  };
336
360
 
337
361
  declare function useField(fieldId: string): {
package/dist/index.mjs CHANGED
@@ -178,6 +178,7 @@ var FormStore = class {
178
178
  config;
179
179
  // Cache for debounce
180
180
  saveTimeout = null;
181
+ draftCallbackTimeout = null;
181
182
  constructor(schema, config = {}) {
182
183
  this.schema = schema;
183
184
  this.config = config;
@@ -197,7 +198,8 @@ var FormStore = class {
197
198
  workflowState: initialWorkflowState,
198
199
  isValid: true,
199
200
  isSubmitting: false,
200
- submitAttempted: false
201
+ submitAttempted: false,
202
+ pendingUploads: 0
201
203
  };
202
204
  this.initializeDefaults();
203
205
  this.evaluate();
@@ -246,7 +248,10 @@ var FormStore = class {
246
248
  this.checkEvents(fieldId, "onValueChange", value);
247
249
  this.evaluate();
248
250
  this.notify();
249
- this.scheduleSave();
251
+ this.scheduleDraftCallback();
252
+ if (!this.config.onDraftChange) {
253
+ this.scheduleSave();
254
+ }
250
255
  }
251
256
  setTouched(fieldId) {
252
257
  if (this.state.touched[fieldId]) return;
@@ -282,6 +287,26 @@ var FormStore = class {
282
287
  this.save();
283
288
  }, 1e3);
284
289
  }
290
+ scheduleDraftCallback() {
291
+ if (!this.config.onDraftChange) return;
292
+ if (this.draftCallbackTimeout) {
293
+ clearTimeout(this.draftCallbackTimeout);
294
+ }
295
+ this.draftCallbackTimeout = setTimeout(() => {
296
+ const payload = {
297
+ // For consumer callbacks, send values in the same
298
+ // serialized shape used for outbound submissions so
299
+ // media/signature fields are wrapped consistently.
300
+ values: this.serializeValuesForSubmission(this.state.values),
301
+ meta: {
302
+ updatedAt: Date.now(),
303
+ workflowState: this.state.workflowState,
304
+ version: this.schema.version
305
+ }
306
+ };
307
+ this.config.onDraftChange?.(payload);
308
+ }, 2500);
309
+ }
285
310
  async save() {
286
311
  const payload = {
287
312
  values: this.state.values,
@@ -459,9 +484,53 @@ var FormStore = class {
459
484
  this.config.onEvent(event, finalPayload);
460
485
  }
461
486
  }
487
+ /**
488
+ * Update mutable config properties (callbacks, role) without recreating
489
+ * the store. This allows the host app to pass fresh closures (e.g. an
490
+ * upload handler whose auth token was initially undefined) after the
491
+ * initial render.
492
+ */
493
+ updateConfig(next) {
494
+ let needsReeval = false;
495
+ if (next.onUpload !== void 0) this.config.onUpload = next.onUpload;
496
+ if (next.onEvent !== void 0) this.config.onEvent = next.onEvent;
497
+ if (next.onDraftChange !== void 0) this.config.onDraftChange = next.onDraftChange;
498
+ if (next.role !== void 0 && next.role !== this.config.role) {
499
+ this.config.role = next.role;
500
+ needsReeval = true;
501
+ }
502
+ if (needsReeval) {
503
+ this.evaluate();
504
+ this.notify();
505
+ }
506
+ }
462
507
  getUploadHandler() {
463
508
  return this.config.onUpload;
464
509
  }
510
+ preSubmitHandlers = /* @__PURE__ */ new Map();
511
+ registerPreSubmitHandler(fieldId, fn) {
512
+ this.preSubmitHandlers.set(fieldId, fn);
513
+ }
514
+ unregisterPreSubmitHandler(fieldId) {
515
+ this.preSubmitHandlers.delete(fieldId);
516
+ }
517
+ /**
518
+ * Runs all registered pre-submit handlers in parallel.
519
+ * Call this before getSerializedValuesForSubmission() to ensure any
520
+ * unconfirmed signatures (or similar) are uploaded first.
521
+ */
522
+ async flushPendingUploads() {
523
+ const handlers = Array.from(this.preSubmitHandlers.values());
524
+ await Promise.all(handlers.map((fn) => fn()));
525
+ }
526
+ incrementPendingUploads() {
527
+ this.state.pendingUploads = (this.state.pendingUploads ?? 0) + 1;
528
+ this.notify();
529
+ }
530
+ decrementPendingUploads() {
531
+ this.state.pendingUploads = Math.max(0, (this.state.pendingUploads ?? 0) - 1);
532
+ this.notify();
533
+ }
465
534
  // Mark that a submit attempt has been made so UI can decide when to show errors
466
535
  markSubmitAttempted() {
467
536
  if (!this.state.submitAttempted) {
@@ -493,6 +562,11 @@ var FormProvider = ({ schema, config, children }) => {
493
562
  if (!storeRef.current) {
494
563
  storeRef.current = new FormStore(schema, config);
495
564
  }
565
+ useEffect(() => {
566
+ if (storeRef.current && config) {
567
+ storeRef.current.updateConfig(config);
568
+ }
569
+ });
496
570
  useEffect(() => {
497
571
  storeRef.current?.load();
498
572
  }, []);
@@ -523,7 +597,8 @@ function useForm() {
523
597
  availableTransitions: store.getAvailableTransitions(),
524
598
  hasPersistence: store.hasPersistence(),
525
599
  getSerializedValues: store.getSerializedValuesForSubmission.bind(store),
526
- markSubmitAttempted: store.markSubmitAttempted.bind(store)
600
+ markSubmitAttempted: store.markSubmitAttempted.bind(store),
601
+ flushPendingUploads: store.flushPendingUploads.bind(store)
527
602
  };
528
603
  }
529
604
  function useField(fieldId) {
@@ -1431,6 +1506,7 @@ var ImageUploadWidget = ({ fieldId }) => {
1431
1506
  }
1432
1507
  setIsUploading(true);
1433
1508
  setTouched();
1509
+ store.incrementPendingUploads();
1434
1510
  try {
1435
1511
  const previewUrl = URL.createObjectURL(file);
1436
1512
  setPreview(previewUrl);
@@ -1443,6 +1519,7 @@ var ImageUploadWidget = ({ fieldId }) => {
1443
1519
  setPreview(null);
1444
1520
  } finally {
1445
1521
  setIsUploading(false);
1522
+ store.decrementPendingUploads();
1446
1523
  }
1447
1524
  };
1448
1525
  const handleRemove = () => {
@@ -1542,6 +1619,22 @@ var SignatureUploadWidget = ({ fieldId }) => {
1542
1619
  const [hasDrawing, setHasDrawing] = useState(false);
1543
1620
  const [isUploading, setIsUploading] = useState(false);
1544
1621
  const [isConfirmed, setIsConfirmed] = useState(false);
1622
+ const [uploadError, setUploadError] = useState(null);
1623
+ const confirmSignatureRef = useRef(null);
1624
+ useEffect(() => {
1625
+ if (hasDrawing && !isConfirmed) {
1626
+ store.registerPreSubmitHandler(fieldId, async () => {
1627
+ if (confirmSignatureRef.current) {
1628
+ await confirmSignatureRef.current();
1629
+ }
1630
+ });
1631
+ } else {
1632
+ store.unregisterPreSubmitHandler(fieldId);
1633
+ }
1634
+ return () => {
1635
+ store.unregisterPreSubmitHandler(fieldId);
1636
+ };
1637
+ }, [hasDrawing, isConfirmed, fieldId, store]);
1545
1638
  const showError = !!error && (touched || state.submitAttempted);
1546
1639
  const displayUrl = useMemo(() => extractDisplayUrl(value), [value]);
1547
1640
  const showPreview = displayUrl !== null;
@@ -1580,6 +1673,7 @@ var SignatureUploadWidget = ({ fieldId }) => {
1580
1673
  if (disabled) return;
1581
1674
  setIsDrawing(true);
1582
1675
  setIsConfirmed(false);
1676
+ setUploadError(null);
1583
1677
  setTouched();
1584
1678
  const ctx = canvasRef.current?.getContext("2d");
1585
1679
  if (!ctx) return;
@@ -1660,7 +1754,9 @@ var SignatureUploadWidget = ({ fieldId }) => {
1660
1754
  const confirmSignature = async () => {
1661
1755
  if (!hasDrawing) return;
1662
1756
  setIsUploading(true);
1757
+ setUploadError(null);
1663
1758
  setTouched();
1759
+ store.incrementPendingUploads();
1664
1760
  try {
1665
1761
  const svg = generateSVG();
1666
1762
  const blob = new Blob([svg], { type: "image/svg+xml" });
@@ -1671,10 +1767,15 @@ var SignatureUploadWidget = ({ fieldId }) => {
1671
1767
  setHasDrawing(false);
1672
1768
  } catch (err) {
1673
1769
  console.error("[SignatureUpload] Upload failed:", err);
1770
+ setUploadError(
1771
+ err instanceof Error ? err.message : "Signature upload failed. Please try again."
1772
+ );
1674
1773
  } finally {
1675
1774
  setIsUploading(false);
1775
+ store.decrementPendingUploads();
1676
1776
  }
1677
1777
  };
1778
+ confirmSignatureRef.current = confirmSignature;
1678
1779
  const canClear = showPreview ? !disabled : hasDrawing || isConfirmed;
1679
1780
  return /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
1680
1781
  /* @__PURE__ */ jsxs(Label, { children: [
@@ -1742,19 +1843,22 @@ var SignatureUploadWidget = ({ fieldId }) => {
1742
1843
  isConfirmed ? /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 text-sm text-green-600", children: [
1743
1844
  /* @__PURE__ */ jsx(Check, { className: "h-4 w-4" }),
1744
1845
  /* @__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
- )
1846
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
1847
+ /* @__PURE__ */ jsxs(
1848
+ Button,
1849
+ {
1850
+ type: "button",
1851
+ onClick: confirmSignature,
1852
+ disabled: !hasDrawing || isUploading,
1853
+ className: "text-xs",
1854
+ children: [
1855
+ /* @__PURE__ */ jsx(Check, { className: "h-4 w-4 mr-1" }),
1856
+ isUploading ? "Saving\u2026" : "Confirm"
1857
+ ]
1858
+ }
1859
+ ),
1860
+ uploadError && /* @__PURE__ */ jsx("p", { className: "text-xs text-red-500 mt-1", children: uploadError })
1861
+ ] })
1758
1862
  ] })
1759
1863
  ),
1760
1864
  showError && /* @__PURE__ */ jsx("p", { className: "text-xs text-red-500", children: error })
@@ -2740,6 +2844,7 @@ var FormControls = ({
2740
2844
  onSubmit
2741
2845
  }) => {
2742
2846
  const { state, availableTransitions, submit, transition, hasPersistence, markSubmitAttempted } = useForm();
2847
+ const hasUploadsInFlight = (state.pendingUploads ?? 0) > 0;
2743
2848
  const draftAvailable = onSaveDraft || hasPersistence && submit;
2744
2849
  const submitAvailable = onSubmit || availableTransitions.length > 0 && transition;
2745
2850
  if (!draftAvailable && !submitAvailable) {
@@ -2760,7 +2865,8 @@ var FormControls = ({
2760
2865
  onClick: onSaveDraft || submit,
2761
2866
  variant: "outline",
2762
2867
  size: "sm",
2763
- children: "Save Draft"
2868
+ disabled: hasUploadsInFlight,
2869
+ children: hasUploadsInFlight ? "Upload in progress\u2026" : "Save Draft"
2764
2870
  }
2765
2871
  ),
2766
2872
  submitAvailable && /* @__PURE__ */ jsx(Fragment, { children: availableTransitions.length > 0 ? availableTransitions.map((t) => /* @__PURE__ */ jsx(
@@ -2774,9 +2880,9 @@ var FormControls = ({
2774
2880
  transition(t.to);
2775
2881
  }
2776
2882
  },
2777
- disabled: !state.isValid,
2883
+ disabled: !state.isValid || hasUploadsInFlight,
2778
2884
  size: "sm",
2779
- children: availableTransitions.length > 1 ? `Submit to ${t.to}` : "Submit"
2885
+ children: hasUploadsInFlight ? "Upload in progress\u2026" : availableTransitions.length > 1 ? `Submit to ${t.to}` : "Submit"
2780
2886
  },
2781
2887
  t.to
2782
2888
  )) : onSubmit ? /* @__PURE__ */ jsx(
@@ -2786,9 +2892,9 @@ var FormControls = ({
2786
2892
  markSubmitAttempted();
2787
2893
  onSubmit(state.workflowState || "submit");
2788
2894
  },
2789
- disabled: !state.isValid,
2895
+ disabled: !state.isValid || hasUploadsInFlight,
2790
2896
  size: "sm",
2791
- children: "Submit"
2897
+ children: hasUploadsInFlight ? "Upload in progress\u2026" : "Submit"
2792
2898
  }
2793
2899
  ) : null })
2794
2900
  ] })