cogsbox-state 0.5.483 → 0.5.485

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.
@@ -336,21 +336,28 @@ export function FormElementWrapper({
336
336
 
337
337
  const debouncedUpdate = useCallback(
338
338
  (newValue: any) => {
339
+ const liveTypeInfo =
340
+ getGlobalStore.getState().getShadowNode(stateKey, path)?._meta
341
+ ?.typeInfo ?? typeInfo;
342
+
339
343
  // Type conversion logic (keep existing)
340
- if (typeInfo) {
341
- if (typeInfo.type === 'number' && typeof newValue === 'string') {
344
+ if (liveTypeInfo) {
345
+ if (liveTypeInfo.type === 'number' && typeof newValue === 'string') {
342
346
  newValue =
343
347
  newValue === ''
344
- ? typeInfo.nullable
348
+ ? liveTypeInfo.nullable
345
349
  ? null
346
- : (typeInfo.default ?? 0)
350
+ : (liveTypeInfo.default ?? 0)
347
351
  : Number(newValue);
348
352
  } else if (
349
- typeInfo.type === 'boolean' &&
353
+ liveTypeInfo.type === 'boolean' &&
350
354
  typeof newValue === 'string'
351
355
  ) {
352
356
  newValue = newValue === 'true' || newValue === '1';
353
- } else if (typeInfo.type === 'date' && typeof newValue === 'string') {
357
+ } else if (
358
+ liveTypeInfo.type === 'date' &&
359
+ typeof newValue === 'string'
360
+ ) {
354
361
  newValue = new Date(newValue);
355
362
  }
356
363
  } else {
@@ -476,10 +483,7 @@ export function FormElementWrapper({
476
483
  clearTimeout(debounceTimeoutRef.current);
477
484
  debounceTimeoutRef.current = null;
478
485
  isCurrentlyDebouncing.current = false;
479
- setState(localValue, path, {
480
- updateType: 'update',
481
- validationTrigger: 'onBlur',
482
- });
486
+ setState(localValue, path, { updateType: 'update' });
483
487
  }
484
488
 
485
489
  // Clear element's current activity
@@ -493,10 +497,26 @@ export function FormElementWrapper({
493
497
  meta?.clientActivityState?.elements?.get(componentId)?.currentActivity
494
498
  ?.startTime;
495
499
 
496
- // Notify plugins
500
+ const validationOptions = getInitialOptions(stateKey)?.validation;
501
+ if (validationOptions?.onBlur) {
502
+ runValidation(
503
+ {
504
+ stateKey,
505
+ path,
506
+ newValue: localValue,
507
+ updateType: 'update',
508
+ timeStamp: Date.now(),
509
+ status: 'new',
510
+ oldValue: globalStateValue,
511
+ },
512
+ 'onBlur'
513
+ );
514
+ }
515
+
516
+ // Refine / plugin validation runs after field validation so it wins on conflict
497
517
  notifyFormUpdate({
498
518
  stateKey,
499
- activityType: 'blur', // Changed from 'type'
519
+ activityType: 'blur',
500
520
  path,
501
521
  timestamp: Date.now(),
502
522
  duration: focusStartTime ? Date.now() - focusStartTime : undefined,
@@ -504,21 +524,6 @@ export function FormElementWrapper({
504
524
  duration: focusStartTime ? Date.now() - focusStartTime : 0,
505
525
  },
506
526
  });
507
-
508
- // Run validation if configured
509
- const validationOptions = getInitialOptions(stateKey)?.validation;
510
- if (validationOptions?.onBlur) {
511
- const virtualOperation: UpdateTypeDetail = {
512
- stateKey,
513
- path,
514
- newValue: localValue,
515
- updateType: 'update',
516
- timeStamp: Date.now(),
517
- status: 'new',
518
- oldValue: globalStateValue,
519
- };
520
- runValidation(virtualOperation, 'onBlur');
521
- }
522
527
  }, [localValue, setState, path, stateKey, componentId, globalStateValue]);
523
528
 
524
529
  const baseState = rebuildStateShape({
@@ -1,5 +1,6 @@
1
1
  import React, {
2
2
  useEffect,
3
+ useLayoutEffect,
3
4
  useMemo,
4
5
  useState,
5
6
  useRef,
@@ -75,7 +76,7 @@ const PluginInstance = React.memo(
75
76
  const lastProcessedOptionsRef = useRef<any>();
76
77
  const [isInitialTransform, setIsInitialTransform] = useState(true);
77
78
 
78
- useEffect(() => {
79
+ useLayoutEffect(() => {
79
80
  if (plugin.transformState) {
80
81
  if (!isDeepEqual(options, lastProcessedOptionsRef.current)) {
81
82
  plugin.transformState({
package/src/plugins.ts CHANGED
@@ -89,6 +89,7 @@ type DeconstructedCogsMethods<TStateSlice = any> = {
89
89
  initialiseShadowState: (data: any) => void;
90
90
  applyOperation: (patch: any, meta?: { dontUpdate?: boolean }) => void;
91
91
  addZodErrors: (errors: any[]) => void;
92
+ clearZodErrors: (paths: string[][]) => void;
92
93
  getState: () => TStateSlice;
93
94
  setOptions: (options: any) => void;
94
95
  };
@@ -104,6 +105,8 @@ export function toDeconstructedMethods(stateHandler: StateObject<any>) {
104
105
  applyOperation: (patch: any, meta?: { dontUpdate?: boolean }) =>
105
106
  stateHandler.$applyOperation(patch, meta),
106
107
  addZodErrors: (errors: any[]) => stateHandler.$addZodValidation(errors),
108
+ clearZodErrors: (paths: string[][]) =>
109
+ stateHandler.$clearZodValidationPaths(paths),
107
110
  getState: () => stateHandler.$get(),
108
111
  setOptions: (opts: any) => {
109
112
  stateHandler.$setOptions(opts);
package/src/store.ts CHANGED
@@ -316,16 +316,25 @@ function getTypeFromZodSchema(
316
316
 
317
317
  const typeIdentifier = def.typeName || def.type || current._type;
318
318
 
319
- // --- START: THE CRITICAL FIX FOR ZodUnion ---
320
319
  if (typeIdentifier === 'ZodUnion' || typeIdentifier === 'union') {
321
320
  if (def.options && def.options.length > 0) {
322
- current = def.options[0]; // Proceed by analyzing the FIRST option of the union
323
- continue; // Restart the loop with the new schema
321
+ const preferredOption = def.options.find((option: any) => {
322
+ const optionDef = option?.def || option?._def;
323
+ const optionType =
324
+ optionDef?.typeName || optionDef?.type || option?._type;
325
+ return (
326
+ optionType !== 'ZodNull' &&
327
+ optionType !== 'null' &&
328
+ optionType !== 'ZodUndefined' &&
329
+ optionType !== 'undefined'
330
+ );
331
+ });
332
+ current = preferredOption ?? def.options[0];
333
+ continue;
324
334
  } else {
325
- break; // Union with no options, cannot determine type
335
+ break;
326
336
  }
327
337
  }
328
- // --- END: THE CRITICAL FIX ---
329
338
 
330
339
  if (typeIdentifier === 'ZodOptional' || typeIdentifier === 'optional') {
331
340
  isOptional = true;
@@ -684,9 +693,10 @@ function getSchemaAtPath(schema: any, path: string[]): any {
684
693
  const typeIdentifier = def?.typeName || def?.type || containerSchema._type;
685
694
 
686
695
  if (typeIdentifier === 'ZodObject' || typeIdentifier === 'object') {
687
- // VITAL FIX: Check for `shape` inside `def` first, then on the schema itself.
696
+ const rawShape =
697
+ def?.shape ?? containerSchema.shape ?? containerSchema._shape;
688
698
  const shape =
689
- def?.shape || containerSchema.shape || containerSchema._shape;
699
+ typeof rawShape === 'function' ? rawShape() : rawShape;
690
700
  currentSchema = shape?.[segment];
691
701
  } else if (typeIdentifier === 'ZodArray' || typeIdentifier === 'array') {
692
702
  // For arrays, the next schema is always the element's schema.
@@ -1006,6 +1016,13 @@ export const getGlobalStore = create<CogsGlobalState>((set, get) => ({
1006
1016
 
1007
1017
  const storageKey = Array.isArray(initialState) ? `[${key}` : key;
1008
1018
  shadowStateStore.set(storageKey, newRoot);
1019
+
1020
+ const latestOptions = get().getInitialOptions(key);
1021
+ if (latestOptions?.validation?.zodSchemaV4) {
1022
+ updateShadowTypeInfo(key, latestOptions.validation.zodSchemaV4, 'zod4');
1023
+ } else if (latestOptions?.validation?.zodSchemaV3) {
1024
+ updateShadowTypeInfo(key, latestOptions.validation.zodSchemaV3, 'zod3');
1025
+ }
1009
1026
  },
1010
1027
  getShadowNode: (key: string, path: string[]): ShadowNode | undefined => {
1011
1028
  let current: any =