cogsbox-state 0.5.470 → 0.5.472
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/README.md +2 -51
- package/dist/CogsState.d.ts +15 -5
- package/dist/CogsState.d.ts.map +1 -1
- package/dist/CogsState.jsx +915 -890
- package/dist/CogsState.jsx.map +1 -1
- package/dist/Components.d.ts.map +1 -1
- package/dist/Components.jsx +214 -223
- package/dist/Components.jsx.map +1 -1
- package/dist/store.d.ts +23 -15
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +476 -231
- package/dist/store.js.map +1 -1
- package/package.json +8 -13
- package/src/CogsState.tsx +221 -161
- package/src/Components.tsx +215 -169
- package/src/store.ts +476 -39
package/src/Components.tsx
CHANGED
|
@@ -222,11 +222,17 @@ export function FormElementWrapper({
|
|
|
222
222
|
formOpts?: FormOptsType;
|
|
223
223
|
setState: any;
|
|
224
224
|
}) {
|
|
225
|
-
const
|
|
225
|
+
const componentId = useRef(uuidv4()).current;
|
|
226
226
|
const [, forceUpdate] = useState({});
|
|
227
227
|
|
|
228
228
|
const stateKeyPathKey = [stateKey, ...path].join('.');
|
|
229
229
|
useRegisterComponent(stateKey, componentId, forceUpdate);
|
|
230
|
+
|
|
231
|
+
// Get the shadow node to access typeInfo and schema
|
|
232
|
+
const shadowNode = getGlobalStore.getState().getShadowNode(stateKey, path);
|
|
233
|
+
const typeInfo = shadowNode?._meta?.typeInfo;
|
|
234
|
+
const fieldSchema = typeInfo?.schema; // The actual Zod schema for this field
|
|
235
|
+
|
|
230
236
|
const globalStateValue = getShadowValue(stateKey, path);
|
|
231
237
|
const [localValue, setLocalValue] = useState<any>(globalStateValue);
|
|
232
238
|
const isCurrentlyDebouncing = useRef(false);
|
|
@@ -258,13 +264,204 @@ export function FormElementWrapper({
|
|
|
258
264
|
};
|
|
259
265
|
}, []);
|
|
260
266
|
|
|
267
|
+
// Separate validation function that uses the field's schema
|
|
268
|
+
const validateField = useCallback(
|
|
269
|
+
(value: any, trigger: 'onChange' | 'onBlur') => {
|
|
270
|
+
const rootMeta = getGlobalStore
|
|
271
|
+
.getState()
|
|
272
|
+
.getShadowMetadata(stateKey, []);
|
|
273
|
+
if (!rootMeta?.features?.validationEnabled) return;
|
|
274
|
+
|
|
275
|
+
const validationOptions = getInitialOptions(stateKey)?.validation;
|
|
276
|
+
|
|
277
|
+
if (!validationOptions) return;
|
|
278
|
+
|
|
279
|
+
const currentMeta = getShadowMetadata(stateKey, path) || {};
|
|
280
|
+
const currentStatus = currentMeta?.validation?.status;
|
|
281
|
+
|
|
282
|
+
let shouldValidate = false;
|
|
283
|
+
let severity: 'error' | 'warning' | undefined;
|
|
284
|
+
console.log('trigger', trigger, validationOptions);
|
|
285
|
+
if (trigger === 'onBlur' && validationOptions.onBlur) {
|
|
286
|
+
shouldValidate = true;
|
|
287
|
+
severity = validationOptions.onBlur ?? 'error';
|
|
288
|
+
} else if (trigger === 'onChange') {
|
|
289
|
+
if (validationOptions.onChange) {
|
|
290
|
+
shouldValidate = true;
|
|
291
|
+
severity = validationOptions.onChange;
|
|
292
|
+
} else if (currentStatus === 'INVALID') {
|
|
293
|
+
shouldValidate = true;
|
|
294
|
+
severity = 'warning';
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (!shouldValidate) return;
|
|
299
|
+
|
|
300
|
+
let validationResult: { success: boolean; message?: string } | null =
|
|
301
|
+
null;
|
|
302
|
+
console.log(
|
|
303
|
+
'shouldValidate 33',
|
|
304
|
+
path,
|
|
305
|
+
fieldSchema,
|
|
306
|
+
shouldValidate,
|
|
307
|
+
value,
|
|
308
|
+
typeof value
|
|
309
|
+
);
|
|
310
|
+
if (fieldSchema && shouldValidate) {
|
|
311
|
+
// Direct field validation using its own schema
|
|
312
|
+
const result = fieldSchema.safeParse(value);
|
|
313
|
+
|
|
314
|
+
if (!result.success) {
|
|
315
|
+
const errors =
|
|
316
|
+
'issues' in result.error
|
|
317
|
+
? result.error.issues
|
|
318
|
+
: (result.error as any).errors;
|
|
319
|
+
|
|
320
|
+
validationResult = {
|
|
321
|
+
success: false,
|
|
322
|
+
message: errors[0]?.message || 'Invalid value',
|
|
323
|
+
};
|
|
324
|
+
} else {
|
|
325
|
+
validationResult = { success: true };
|
|
326
|
+
}
|
|
327
|
+
} else {
|
|
328
|
+
// Fallback: validate using the entire schema
|
|
329
|
+
const zodSchema =
|
|
330
|
+
validationOptions.zodSchemaV4 || validationOptions.zodSchemaV3;
|
|
331
|
+
if (!zodSchema) return;
|
|
332
|
+
|
|
333
|
+
// Create a test state with the new value at the correct path
|
|
334
|
+
const fullState = getShadowValue(stateKey, []);
|
|
335
|
+
const testState = JSON.parse(JSON.stringify(fullState)); // Deep clone
|
|
336
|
+
|
|
337
|
+
// Set the value at the correct path
|
|
338
|
+
let current = testState;
|
|
339
|
+
for (let i = 0; i < path.length - 1; i++) {
|
|
340
|
+
if (!current[path[i]!]) current[path[i]!] = {};
|
|
341
|
+
current = current[path[i]!];
|
|
342
|
+
}
|
|
343
|
+
if (path.length > 0) {
|
|
344
|
+
current[path[path.length - 1]!] = value;
|
|
345
|
+
} else {
|
|
346
|
+
// Root level update
|
|
347
|
+
Object.assign(testState, value);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const result = zodSchema.safeParse(testState);
|
|
351
|
+
|
|
352
|
+
if (!result.success) {
|
|
353
|
+
const errors =
|
|
354
|
+
'issues' in result.error
|
|
355
|
+
? result.error.issues
|
|
356
|
+
: (result.error as any).errors;
|
|
357
|
+
|
|
358
|
+
// Find errors for this specific path
|
|
359
|
+
const pathErrors = errors.filter((error: any) => {
|
|
360
|
+
// Handle array paths with id: prefixes
|
|
361
|
+
if (path.some((p) => p.startsWith('id:'))) {
|
|
362
|
+
const parentPath = path[0]!.startsWith('id:')
|
|
363
|
+
? []
|
|
364
|
+
: path.slice(0, -1);
|
|
365
|
+
const arrayMeta = getGlobalStore
|
|
366
|
+
.getState()
|
|
367
|
+
.getShadowMetadata(stateKey, parentPath);
|
|
368
|
+
|
|
369
|
+
if (arrayMeta?.arrayKeys) {
|
|
370
|
+
const itemKey = path.slice(0, -1).join('.');
|
|
371
|
+
const itemIndex = arrayMeta.arrayKeys.findIndex(
|
|
372
|
+
(k) => k === path[path.length - 2]
|
|
373
|
+
);
|
|
374
|
+
const zodPath = [...parentPath, itemIndex, ...path.slice(-1)];
|
|
375
|
+
return JSON.stringify(error.path) === JSON.stringify(zodPath);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
return JSON.stringify(error.path) === JSON.stringify(path);
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
if (pathErrors.length > 0) {
|
|
383
|
+
validationResult = {
|
|
384
|
+
success: false,
|
|
385
|
+
message: pathErrors[0]?.message,
|
|
386
|
+
};
|
|
387
|
+
} else {
|
|
388
|
+
validationResult = { success: true };
|
|
389
|
+
}
|
|
390
|
+
} else {
|
|
391
|
+
validationResult = { success: true };
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Update validation state based on result
|
|
396
|
+
if (validationResult) {
|
|
397
|
+
if (!validationResult.success) {
|
|
398
|
+
setShadowMetadata(stateKey, path, {
|
|
399
|
+
...currentMeta,
|
|
400
|
+
validation: {
|
|
401
|
+
status: 'INVALID',
|
|
402
|
+
errors: [
|
|
403
|
+
{
|
|
404
|
+
source: 'client' as const,
|
|
405
|
+
message: validationResult.message!,
|
|
406
|
+
severity: severity!,
|
|
407
|
+
},
|
|
408
|
+
],
|
|
409
|
+
lastValidated: Date.now(),
|
|
410
|
+
validatedValue: value,
|
|
411
|
+
},
|
|
412
|
+
});
|
|
413
|
+
} else {
|
|
414
|
+
setShadowMetadata(stateKey, path, {
|
|
415
|
+
...currentMeta,
|
|
416
|
+
validation: {
|
|
417
|
+
status: 'VALID',
|
|
418
|
+
errors: [],
|
|
419
|
+
lastValidated: Date.now(),
|
|
420
|
+
validatedValue: value,
|
|
421
|
+
},
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
forceUpdate({});
|
|
426
|
+
},
|
|
427
|
+
[stateKey, path, fieldSchema]
|
|
428
|
+
);
|
|
429
|
+
|
|
261
430
|
const debouncedUpdate = useCallback(
|
|
262
431
|
(newValue: any) => {
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
432
|
+
// Use typeInfo to properly convert the value
|
|
433
|
+
|
|
434
|
+
if (typeInfo) {
|
|
435
|
+
if (typeInfo.type === 'number' && typeof newValue === 'string') {
|
|
436
|
+
newValue =
|
|
437
|
+
newValue === ''
|
|
438
|
+
? typeInfo.nullable
|
|
439
|
+
? null
|
|
440
|
+
: (typeInfo.default ?? 0)
|
|
441
|
+
: Number(newValue);
|
|
442
|
+
} else if (
|
|
443
|
+
typeInfo.type === 'boolean' &&
|
|
444
|
+
typeof newValue === 'string'
|
|
445
|
+
) {
|
|
446
|
+
newValue = newValue === 'true' || newValue === '1';
|
|
447
|
+
} else if (typeInfo.type === 'date' && typeof newValue === 'string') {
|
|
448
|
+
newValue = new Date(newValue);
|
|
449
|
+
}
|
|
450
|
+
} else {
|
|
451
|
+
// Fallback to old behavior if no typeInfo
|
|
452
|
+
|
|
453
|
+
const currentType = typeof globalStateValue;
|
|
454
|
+
|
|
455
|
+
if (currentType === 'number' && typeof newValue === 'string') {
|
|
456
|
+
newValue = newValue === '' ? 0 : Number(newValue);
|
|
457
|
+
}
|
|
266
458
|
}
|
|
459
|
+
|
|
267
460
|
setLocalValue(newValue);
|
|
461
|
+
|
|
462
|
+
// Validate immediately on change (will only run if configured or clearing errors)
|
|
463
|
+
validateField(newValue, 'onChange');
|
|
464
|
+
|
|
268
465
|
isCurrentlyDebouncing.current = true;
|
|
269
466
|
|
|
270
467
|
if (debounceTimeoutRef.current) {
|
|
@@ -273,184 +470,34 @@ export function FormElementWrapper({
|
|
|
273
470
|
|
|
274
471
|
const debounceTime = formOpts?.debounceTime ?? 200;
|
|
275
472
|
|
|
473
|
+
// Debounce only the state update, not the validation
|
|
276
474
|
debounceTimeoutRef.current = setTimeout(() => {
|
|
277
475
|
isCurrentlyDebouncing.current = false;
|
|
278
476
|
setState(newValue, path, { updateType: 'update' });
|
|
279
|
-
|
|
280
|
-
// NEW: Check if validation is enabled via features
|
|
281
|
-
const rootMeta = getGlobalStore
|
|
282
|
-
.getState()
|
|
283
|
-
.getShadowMetadata(stateKey, []);
|
|
284
|
-
if (!rootMeta?.features?.validationEnabled) return;
|
|
285
|
-
|
|
286
|
-
const validationOptions = getInitialOptions(stateKey)?.validation;
|
|
287
|
-
const zodSchema =
|
|
288
|
-
validationOptions?.zodSchemaV4 || validationOptions?.zodSchemaV3;
|
|
289
|
-
|
|
290
|
-
if (zodSchema) {
|
|
291
|
-
const fullState = getShadowValue(stateKey, []);
|
|
292
|
-
const result = zodSchema.safeParse(fullState);
|
|
293
|
-
const currentMeta = getShadowMetadata(stateKey, path) || {};
|
|
294
|
-
|
|
295
|
-
if (!result.success) {
|
|
296
|
-
const errors =
|
|
297
|
-
'issues' in result.error
|
|
298
|
-
? result.error.issues
|
|
299
|
-
: (result.error as any).errors;
|
|
300
|
-
|
|
301
|
-
const pathErrors = errors.filter(
|
|
302
|
-
(error: any) =>
|
|
303
|
-
JSON.stringify(error.path) === JSON.stringify(path)
|
|
304
|
-
);
|
|
305
|
-
|
|
306
|
-
if (pathErrors.length > 0) {
|
|
307
|
-
setShadowMetadata(stateKey, path, {
|
|
308
|
-
...currentMeta,
|
|
309
|
-
validation: {
|
|
310
|
-
status: 'INVALID',
|
|
311
|
-
errors: [
|
|
312
|
-
{
|
|
313
|
-
source: 'client',
|
|
314
|
-
message: pathErrors[0]?.message,
|
|
315
|
-
severity: 'warning', // Gentle error during typing
|
|
316
|
-
},
|
|
317
|
-
],
|
|
318
|
-
lastValidated: Date.now(),
|
|
319
|
-
validatedValue: newValue,
|
|
320
|
-
},
|
|
321
|
-
});
|
|
322
|
-
} else {
|
|
323
|
-
setShadowMetadata(stateKey, path, {
|
|
324
|
-
...currentMeta,
|
|
325
|
-
validation: {
|
|
326
|
-
status: 'VALID',
|
|
327
|
-
errors: [],
|
|
328
|
-
lastValidated: Date.now(),
|
|
329
|
-
validatedValue: newValue,
|
|
330
|
-
},
|
|
331
|
-
});
|
|
332
|
-
}
|
|
333
|
-
} else {
|
|
334
|
-
setShadowMetadata(stateKey, path, {
|
|
335
|
-
...currentMeta,
|
|
336
|
-
validation: {
|
|
337
|
-
status: 'VALID',
|
|
338
|
-
errors: [],
|
|
339
|
-
lastValidated: Date.now(),
|
|
340
|
-
validatedValue: newValue,
|
|
341
|
-
},
|
|
342
|
-
});
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
477
|
}, debounceTime);
|
|
346
|
-
forceUpdate({});
|
|
347
478
|
},
|
|
348
|
-
[
|
|
479
|
+
[
|
|
480
|
+
setState,
|
|
481
|
+
path,
|
|
482
|
+
formOpts?.debounceTime,
|
|
483
|
+
validateField,
|
|
484
|
+
typeInfo,
|
|
485
|
+
globalStateValue,
|
|
486
|
+
]
|
|
349
487
|
);
|
|
350
488
|
|
|
351
|
-
const handleBlur = useCallback(
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
// Commit any pending changes
|
|
489
|
+
const handleBlur = useCallback(() => {
|
|
490
|
+
// Commit any pending changes immediately
|
|
355
491
|
if (debounceTimeoutRef.current) {
|
|
356
492
|
clearTimeout(debounceTimeoutRef.current);
|
|
357
493
|
debounceTimeoutRef.current = null;
|
|
358
494
|
isCurrentlyDebouncing.current = false;
|
|
359
495
|
setState(localValue, path, { updateType: 'update' });
|
|
360
496
|
}
|
|
361
|
-
const rootMeta = getShadowMetadata(stateKey, []);
|
|
362
|
-
if (!rootMeta?.features?.validationEnabled) return;
|
|
363
|
-
const { getInitialOptions } = getGlobalStore.getState();
|
|
364
|
-
const validationOptions = getInitialOptions(stateKey)?.validation;
|
|
365
|
-
const zodSchema =
|
|
366
|
-
validationOptions?.zodSchemaV4 || validationOptions?.zodSchemaV3;
|
|
367
|
-
|
|
368
|
-
if (!zodSchema) return;
|
|
369
|
-
|
|
370
|
-
// Get the full path including stateKey
|
|
371
|
-
|
|
372
|
-
// Update validation state to "validating"
|
|
373
|
-
const currentMeta = getShadowMetadata(stateKey, path);
|
|
374
|
-
|
|
375
|
-
setShadowMetadata(stateKey, path, {
|
|
376
|
-
...currentMeta,
|
|
377
|
-
validation: {
|
|
378
|
-
status: 'VALIDATING',
|
|
379
|
-
errors: [],
|
|
380
|
-
lastValidated: Date.now(),
|
|
381
|
-
validatedValue: localValue,
|
|
382
|
-
},
|
|
383
|
-
});
|
|
384
497
|
|
|
385
|
-
// Validate
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
if (!result.success) {
|
|
390
|
-
const errors =
|
|
391
|
-
'issues' in result.error
|
|
392
|
-
? result.error.issues
|
|
393
|
-
: (result.error as any).errors;
|
|
394
|
-
|
|
395
|
-
// Find errors for this specific path
|
|
396
|
-
const pathErrors = errors.filter((error: any) => {
|
|
397
|
-
// For array paths, we need to translate indices to ULIDs
|
|
398
|
-
if (path.some((p) => p.startsWith('id:'))) {
|
|
399
|
-
// This is an array item path like ["id:xyz", "name"]
|
|
400
|
-
const parentPath = path[0]!.startsWith('id:')
|
|
401
|
-
? []
|
|
402
|
-
: path.slice(0, -1);
|
|
403
|
-
|
|
404
|
-
const arrayMeta = getGlobalStore
|
|
405
|
-
.getState()
|
|
406
|
-
.getShadowMetadata(stateKey, parentPath);
|
|
407
|
-
|
|
408
|
-
if (arrayMeta?.arrayKeys) {
|
|
409
|
-
const itemKey = [stateKey, ...path.slice(0, -1)].join('.');
|
|
410
|
-
const itemIndex = arrayMeta.arrayKeys.indexOf(itemKey);
|
|
411
|
-
|
|
412
|
-
// Compare with Zod path
|
|
413
|
-
const zodPath = [...parentPath, itemIndex, ...path.slice(-1)];
|
|
414
|
-
const match =
|
|
415
|
-
JSON.stringify(error.path) === JSON.stringify(zodPath);
|
|
416
|
-
|
|
417
|
-
return match;
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
const directMatch = JSON.stringify(error.path) === JSON.stringify(path);
|
|
422
|
-
|
|
423
|
-
return directMatch;
|
|
424
|
-
});
|
|
425
|
-
|
|
426
|
-
// Update shadow metadata with validation result
|
|
427
|
-
setShadowMetadata(stateKey, path, {
|
|
428
|
-
...currentMeta,
|
|
429
|
-
validation: {
|
|
430
|
-
status: 'INVALID',
|
|
431
|
-
errors: pathErrors.map((err: any) => ({
|
|
432
|
-
source: 'client' as const,
|
|
433
|
-
message: err.message,
|
|
434
|
-
severity: 'error' as const, // Hard error on blur
|
|
435
|
-
})),
|
|
436
|
-
lastValidated: Date.now(),
|
|
437
|
-
validatedValue: localValue,
|
|
438
|
-
},
|
|
439
|
-
});
|
|
440
|
-
} else {
|
|
441
|
-
// Validation passed
|
|
442
|
-
setShadowMetadata(stateKey, path, {
|
|
443
|
-
...currentMeta,
|
|
444
|
-
validation: {
|
|
445
|
-
status: 'VALID',
|
|
446
|
-
errors: [],
|
|
447
|
-
lastValidated: Date.now(),
|
|
448
|
-
validatedValue: localValue,
|
|
449
|
-
},
|
|
450
|
-
});
|
|
451
|
-
}
|
|
452
|
-
forceUpdate({});
|
|
453
|
-
}, [stateKey, path, localValue, setState]);
|
|
498
|
+
// Validate on blur
|
|
499
|
+
validateField(localValue, 'onBlur');
|
|
500
|
+
}, [localValue, setState, path, validateField]);
|
|
454
501
|
|
|
455
502
|
const baseState = rebuildStateShape({
|
|
456
503
|
path: path,
|
|
@@ -466,7 +513,6 @@ export function FormElementWrapper({
|
|
|
466
513
|
onChange: (e: any) => {
|
|
467
514
|
debouncedUpdate(e.target.value);
|
|
468
515
|
},
|
|
469
|
-
// 5. Wire the new onBlur handler to the input props.
|
|
470
516
|
onBlur: handleBlur,
|
|
471
517
|
ref: formRefStore
|
|
472
518
|
.getState()
|