cogsbox-state 0.5.434 → 0.5.436
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/CogsState.d.ts +40 -42
- package/dist/CogsState.jsx +1358 -1155
- package/dist/CogsState.jsx.map +1 -1
- package/dist/CogsStateClient.jsx.map +1 -1
- package/dist/Functions.d.ts +1 -5
- package/dist/Functions.jsx +17 -49
- package/dist/Functions.jsx.map +1 -1
- package/dist/TRPCValidationLink.js.map +1 -1
- package/dist/store.d.ts +11 -0
- package/dist/store.js +247 -185
- package/dist/store.js.map +1 -1
- package/dist/utility.js.map +1 -1
- package/package.json +6 -5
- package/src/CogsState.tsx +816 -211
- package/src/Functions.tsx +8 -241
- package/src/TRPCValidationLink.ts +12 -12
- package/src/store.ts +209 -43
- package/dist/useValidateZodPath.d.ts +0 -34
- package/src/useValidateZodPath.ts +0 -231
package/src/CogsState.tsx
CHANGED
|
@@ -26,12 +26,18 @@ import { ValidationWrapper } from './Functions.js';
|
|
|
26
26
|
import { isDeepEqual, transformStateFunc } from './utility.js';
|
|
27
27
|
import superjson from 'superjson';
|
|
28
28
|
import { v4 as uuidv4 } from 'uuid';
|
|
29
|
-
import { z } from 'zod';
|
|
30
29
|
|
|
31
|
-
import {
|
|
30
|
+
import {
|
|
31
|
+
formRefStore,
|
|
32
|
+
getGlobalStore,
|
|
33
|
+
ValidationStatus,
|
|
34
|
+
type ComponentsType,
|
|
35
|
+
} from './store.js';
|
|
32
36
|
import { useCogsConfig } from './CogsStateClient.js';
|
|
33
|
-
import { applyPatch } from 'fast-json-patch';
|
|
37
|
+
import { applyPatch, compare, Operation } from 'fast-json-patch';
|
|
34
38
|
import { useInView } from 'react-intersection-observer';
|
|
39
|
+
import * as z3 from 'zod/v3';
|
|
40
|
+
import * as z4 from 'zod/v4';
|
|
35
41
|
|
|
36
42
|
type Prettify<T> = T extends any ? { [K in keyof T]: T[K] } : never;
|
|
37
43
|
|
|
@@ -217,12 +223,15 @@ export type FormOptsType = {
|
|
|
217
223
|
validation?: {
|
|
218
224
|
hideMessage?: boolean;
|
|
219
225
|
message?: string;
|
|
220
|
-
|
|
226
|
+
|
|
221
227
|
props?: GenericObject;
|
|
222
228
|
disable?: boolean;
|
|
223
229
|
};
|
|
224
230
|
|
|
225
231
|
debounceTime?: number;
|
|
232
|
+
sync?: {
|
|
233
|
+
allowInvalidValues?: boolean; // default: false
|
|
234
|
+
};
|
|
226
235
|
};
|
|
227
236
|
|
|
228
237
|
export type FormControl<T> = (obj: FormElementParams<T>) => JSX.Element;
|
|
@@ -231,7 +240,7 @@ export type UpdateArg<S> = S | ((prevState: S) => S);
|
|
|
231
240
|
export type InsertParams<S> =
|
|
232
241
|
| S
|
|
233
242
|
| ((prevState: { state: S; uuid: string }) => S);
|
|
234
|
-
export type UpdateType<T> = (payload: UpdateArg<T>) => void;
|
|
243
|
+
export type UpdateType<T> = (payload: UpdateArg<T>) => { synced: () => void };
|
|
235
244
|
|
|
236
245
|
export type InsertType<T> = (payload: InsertParams<T>, index?: number) => void;
|
|
237
246
|
export type InsertTypeObj<T> = (payload: InsertParams<T>) => void;
|
|
@@ -248,6 +257,7 @@ export type EndType<T, IsArrayElement = false> = {
|
|
|
248
257
|
_stateKey: string;
|
|
249
258
|
formElement: (control: FormControl<T>, opts?: FormOptsType) => JSX.Element;
|
|
250
259
|
get: () => T;
|
|
260
|
+
getState: () => T;
|
|
251
261
|
$get: () => T;
|
|
252
262
|
$derive: <R>(fn: EffectFunction<T, R>) => R;
|
|
253
263
|
|
|
@@ -317,13 +327,17 @@ type EffectiveSetStateArg<
|
|
|
317
327
|
? InsertParams<InferArrayElement<T>>
|
|
318
328
|
: never
|
|
319
329
|
: UpdateArg<T>;
|
|
330
|
+
type UpdateOptions = {
|
|
331
|
+
updateType: 'insert' | 'cut' | 'update';
|
|
320
332
|
|
|
333
|
+
sync?: boolean;
|
|
334
|
+
};
|
|
321
335
|
type EffectiveSetState<TStateObject> = (
|
|
322
336
|
newStateOrFunction:
|
|
323
337
|
| EffectiveSetStateArg<TStateObject, 'update'>
|
|
324
338
|
| EffectiveSetStateArg<TStateObject, 'insert'>,
|
|
325
339
|
path: string[],
|
|
326
|
-
updateObj:
|
|
340
|
+
updateObj: UpdateOptions,
|
|
327
341
|
validationKey?: string
|
|
328
342
|
) => void;
|
|
329
343
|
|
|
@@ -346,16 +360,24 @@ export type ReactivityType =
|
|
|
346
360
|
| 'all'
|
|
347
361
|
| Array<Prettify<'none' | 'component' | 'deps' | 'all'>>;
|
|
348
362
|
|
|
363
|
+
// Define the return type of the sync hook locally
|
|
364
|
+
type SyncApi = {
|
|
365
|
+
updateState: (data: { operation: any }) => void;
|
|
366
|
+
connected: boolean;
|
|
367
|
+
clientId: string | null;
|
|
368
|
+
subscribers: string[];
|
|
369
|
+
};
|
|
349
370
|
type ValidationOptionsType = {
|
|
350
371
|
key?: string;
|
|
351
|
-
|
|
372
|
+
zodSchemaV3?: z3.ZodType<any, any, any>;
|
|
373
|
+
zodSchemaV4?: z4.ZodType<any, any, any>;
|
|
374
|
+
|
|
352
375
|
onBlur?: boolean;
|
|
353
376
|
};
|
|
354
|
-
|
|
355
377
|
export type OptionsType<T extends unknown = unknown> = {
|
|
356
378
|
log?: boolean;
|
|
357
379
|
componentId?: string;
|
|
358
|
-
|
|
380
|
+
cogsSync?: (stateObject: StateObject<T>) => SyncApi;
|
|
359
381
|
validation?: ValidationOptionsType;
|
|
360
382
|
|
|
361
383
|
serverState?: {
|
|
@@ -396,7 +418,7 @@ export type OptionsType<T extends unknown = unknown> = {
|
|
|
396
418
|
key: string | ((state: T) => string);
|
|
397
419
|
onChange?: (state: T) => void;
|
|
398
420
|
};
|
|
399
|
-
formElements?: FormsElementsType
|
|
421
|
+
formElements?: FormsElementsType<T>;
|
|
400
422
|
|
|
401
423
|
reactiveDeps?: (state: T) => any[] | true;
|
|
402
424
|
reactiveType?: ReactivityType;
|
|
@@ -405,29 +427,7 @@ export type OptionsType<T extends unknown = unknown> = {
|
|
|
405
427
|
defaultState?: T;
|
|
406
428
|
dependencies?: any[];
|
|
407
429
|
};
|
|
408
|
-
export type ServerSyncType<T> = {
|
|
409
|
-
testKey?: string;
|
|
410
|
-
syncKey: (({ state }: { state: T }) => string) | string;
|
|
411
|
-
syncFunction: ({ state }: { state: T }) => void;
|
|
412
|
-
debounce?: number;
|
|
413
|
-
|
|
414
|
-
snapshot?: {
|
|
415
|
-
name: (({ state }: { state: T }) => string) | string;
|
|
416
|
-
stateKeys: StateKeys[];
|
|
417
|
-
currentUrl: string;
|
|
418
|
-
currentParams?: URLSearchParams;
|
|
419
|
-
};
|
|
420
|
-
};
|
|
421
430
|
|
|
422
|
-
export type ValidationWrapperOptions<T extends unknown = unknown> = {
|
|
423
|
-
children: React.ReactNode;
|
|
424
|
-
active: boolean;
|
|
425
|
-
stretch?: boolean;
|
|
426
|
-
path: string[];
|
|
427
|
-
message?: string;
|
|
428
|
-
data?: T;
|
|
429
|
-
key?: string;
|
|
430
|
-
};
|
|
431
431
|
export type SyncRenderOptions<T extends unknown = unknown> = {
|
|
432
432
|
children: React.ReactNode;
|
|
433
433
|
time: number;
|
|
@@ -435,8 +435,16 @@ export type SyncRenderOptions<T extends unknown = unknown> = {
|
|
|
435
435
|
key?: string;
|
|
436
436
|
};
|
|
437
437
|
|
|
438
|
-
type FormsElementsType<T
|
|
439
|
-
validation?: (options:
|
|
438
|
+
type FormsElementsType<T> = {
|
|
439
|
+
validation?: (options: {
|
|
440
|
+
children: React.ReactNode;
|
|
441
|
+
status: ValidationStatus; // Instead of 'active' boolean
|
|
442
|
+
|
|
443
|
+
path: string[];
|
|
444
|
+
message?: string;
|
|
445
|
+
data?: T;
|
|
446
|
+
key?: string;
|
|
447
|
+
}) => React.ReactNode;
|
|
440
448
|
syncRender?: (options: SyncRenderOptions<T>) => React.ReactNode;
|
|
441
449
|
};
|
|
442
450
|
|
|
@@ -525,9 +533,32 @@ export function addStateOptions<T extends unknown>(
|
|
|
525
533
|
) {
|
|
526
534
|
return { initialState: initialState, formElements, validation } as T;
|
|
527
535
|
}
|
|
536
|
+
type UseCogsStateHook<T extends Record<string, any>> = <
|
|
537
|
+
StateKey extends keyof TransformedStateType<T>,
|
|
538
|
+
>(
|
|
539
|
+
stateKey: StateKey,
|
|
540
|
+
options?: Prettify<OptionsType<TransformedStateType<T>[StateKey]>>
|
|
541
|
+
) => StateObject<TransformedStateType<T>[StateKey]>;
|
|
542
|
+
|
|
543
|
+
// Define the type for the options setter using the Transformed state
|
|
544
|
+
type SetCogsOptionsFunc<T extends Record<string, any>> = <
|
|
545
|
+
StateKey extends keyof TransformedStateType<T>,
|
|
546
|
+
>(
|
|
547
|
+
stateKey: StateKey,
|
|
548
|
+
options: OptionsType<TransformedStateType<T>[StateKey]>
|
|
549
|
+
) => void;
|
|
550
|
+
|
|
551
|
+
// Define the final API object shape
|
|
552
|
+
type CogsApi<T extends Record<string, any>> = {
|
|
553
|
+
useCogsState: UseCogsStateHook<T>;
|
|
554
|
+
setCogsOptions: SetCogsOptionsFunc<T>;
|
|
555
|
+
};
|
|
528
556
|
export const createCogsState = <State extends Record<StateKeys, unknown>>(
|
|
529
557
|
initialState: State,
|
|
530
|
-
opt?: {
|
|
558
|
+
opt?: {
|
|
559
|
+
formElements?: FormsElementsType<State>;
|
|
560
|
+
validation?: ValidationOptionsType;
|
|
561
|
+
}
|
|
531
562
|
) => {
|
|
532
563
|
let newInitialState = initialState;
|
|
533
564
|
|
|
@@ -582,12 +613,12 @@ export const createCogsState = <State extends Record<StateKeys, unknown>>(
|
|
|
582
613
|
|
|
583
614
|
const useCogsState = <StateKey extends StateKeys>(
|
|
584
615
|
stateKey: StateKey,
|
|
585
|
-
options?: OptionsType<(typeof statePart)[StateKey]
|
|
616
|
+
options?: Prettify<OptionsType<(typeof statePart)[StateKey]>>
|
|
586
617
|
) => {
|
|
587
618
|
const [componentId] = useState(options?.componentId ?? uuidv4());
|
|
588
619
|
setOptions({
|
|
589
620
|
stateKey,
|
|
590
|
-
options,
|
|
621
|
+
options: options as any,
|
|
591
622
|
initialOptionsPart,
|
|
592
623
|
});
|
|
593
624
|
|
|
@@ -628,7 +659,7 @@ export const createCogsState = <State extends Record<StateKeys, unknown>>(
|
|
|
628
659
|
notifyComponents(stateKey as string);
|
|
629
660
|
}
|
|
630
661
|
|
|
631
|
-
return { useCogsState, setCogsOptions }
|
|
662
|
+
return { useCogsState, setCogsOptions } as CogsApi<State>;
|
|
632
663
|
};
|
|
633
664
|
|
|
634
665
|
const {
|
|
@@ -782,12 +813,172 @@ export const notifyComponent = (stateKey: string, componentId: string) => {
|
|
|
782
813
|
}
|
|
783
814
|
}
|
|
784
815
|
};
|
|
816
|
+
function markEntireStateAsServerSynced(
|
|
817
|
+
stateKey: string,
|
|
818
|
+
path: string[],
|
|
819
|
+
data: any,
|
|
820
|
+
timestamp: number
|
|
821
|
+
) {
|
|
822
|
+
const store = getGlobalStore.getState();
|
|
823
|
+
|
|
824
|
+
// Mark current path as synced
|
|
825
|
+
const currentMeta = store.getShadowMetadata(stateKey, path);
|
|
826
|
+
store.setShadowMetadata(stateKey, path, {
|
|
827
|
+
...currentMeta,
|
|
828
|
+
isDirty: false,
|
|
829
|
+
stateSource: 'server',
|
|
830
|
+
lastServerSync: timestamp || Date.now(),
|
|
831
|
+
});
|
|
832
|
+
|
|
833
|
+
// If it's an array, mark each item as synced
|
|
834
|
+
if (Array.isArray(data)) {
|
|
835
|
+
const arrayMeta = store.getShadowMetadata(stateKey, path);
|
|
836
|
+
if (arrayMeta?.arrayKeys) {
|
|
837
|
+
arrayMeta.arrayKeys.forEach((itemKey, index) => {
|
|
838
|
+
const itemPath = itemKey.split('.').slice(1);
|
|
839
|
+
const itemData = data[index];
|
|
840
|
+
if (itemData !== undefined) {
|
|
841
|
+
markEntireStateAsServerSynced(
|
|
842
|
+
stateKey,
|
|
843
|
+
itemPath,
|
|
844
|
+
itemData,
|
|
845
|
+
timestamp
|
|
846
|
+
);
|
|
847
|
+
}
|
|
848
|
+
});
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
// If it's an object, mark each field as synced
|
|
852
|
+
else if (data && typeof data === 'object' && data.constructor === Object) {
|
|
853
|
+
Object.keys(data).forEach((key) => {
|
|
854
|
+
const fieldPath = [...path, key];
|
|
855
|
+
const fieldData = data[key];
|
|
856
|
+
markEntireStateAsServerSynced(stateKey, fieldPath, fieldData, timestamp);
|
|
857
|
+
});
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
const _notifySubscribedComponents = (
|
|
862
|
+
stateKey: string,
|
|
863
|
+
path: string[],
|
|
864
|
+
updateType: 'update' | 'insert' | 'cut',
|
|
865
|
+
oldValue: any,
|
|
866
|
+
newValue: any
|
|
867
|
+
) => {
|
|
868
|
+
const store = getGlobalStore.getState();
|
|
869
|
+
const rootMeta = store.getShadowMetadata(stateKey, []);
|
|
870
|
+
if (!rootMeta?.components) {
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
const notifiedComponents = new Set<string>();
|
|
875
|
+
const shadowMeta = store.getShadowMetadata(stateKey, path);
|
|
876
|
+
|
|
877
|
+
// --- PASS 1: Notify specific subscribers based on update type ---
|
|
878
|
+
|
|
879
|
+
if (updateType === 'update') {
|
|
880
|
+
if (shadowMeta?.pathComponents) {
|
|
881
|
+
shadowMeta.pathComponents.forEach((componentId) => {
|
|
882
|
+
if (notifiedComponents.has(componentId)) return;
|
|
883
|
+
const component = rootMeta.components?.get(componentId);
|
|
884
|
+
if (component) {
|
|
885
|
+
const reactiveTypes = Array.isArray(component.reactiveType)
|
|
886
|
+
? component.reactiveType
|
|
887
|
+
: [component.reactiveType || 'component'];
|
|
888
|
+
if (!reactiveTypes.includes('none')) {
|
|
889
|
+
component.forceUpdate();
|
|
890
|
+
notifiedComponents.add(componentId);
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
});
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
if (
|
|
897
|
+
newValue &&
|
|
898
|
+
typeof newValue === 'object' &&
|
|
899
|
+
!isArray(newValue) &&
|
|
900
|
+
oldValue &&
|
|
901
|
+
typeof oldValue === 'object' &&
|
|
902
|
+
!isArray(oldValue)
|
|
903
|
+
) {
|
|
904
|
+
const changedSubPaths = getDifferences(newValue, oldValue);
|
|
905
|
+
changedSubPaths.forEach((subPathString) => {
|
|
906
|
+
const subPath = subPathString.split('.');
|
|
907
|
+
const fullSubPath = [...path, ...subPath];
|
|
908
|
+
const subPathMeta = store.getShadowMetadata(stateKey, fullSubPath);
|
|
909
|
+
if (subPathMeta?.pathComponents) {
|
|
910
|
+
subPathMeta.pathComponents.forEach((componentId) => {
|
|
911
|
+
if (notifiedComponents.has(componentId)) return;
|
|
912
|
+
const component = rootMeta.components?.get(componentId);
|
|
913
|
+
if (component) {
|
|
914
|
+
const reactiveTypes = Array.isArray(component.reactiveType)
|
|
915
|
+
? component.reactiveType
|
|
916
|
+
: [component.reactiveType || 'component'];
|
|
917
|
+
if (!reactiveTypes.includes('none')) {
|
|
918
|
+
component.forceUpdate();
|
|
919
|
+
notifiedComponents.add(componentId);
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
});
|
|
923
|
+
}
|
|
924
|
+
});
|
|
925
|
+
}
|
|
926
|
+
} else if (updateType === 'insert' || updateType === 'cut') {
|
|
927
|
+
const parentArrayPath = updateType === 'insert' ? path : path.slice(0, -1);
|
|
928
|
+
const parentMeta = store.getShadowMetadata(stateKey, parentArrayPath);
|
|
929
|
+
if (parentMeta?.pathComponents) {
|
|
930
|
+
parentMeta.pathComponents.forEach((componentId) => {
|
|
931
|
+
if (!notifiedComponents.has(componentId)) {
|
|
932
|
+
const component = rootMeta.components?.get(componentId);
|
|
933
|
+
if (component) {
|
|
934
|
+
component.forceUpdate();
|
|
935
|
+
notifiedComponents.add(componentId);
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
});
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
// --- PASS 2: Notify global subscribers ('all', 'deps') ---
|
|
943
|
+
|
|
944
|
+
rootMeta.components.forEach((component, componentId) => {
|
|
945
|
+
if (notifiedComponents.has(componentId)) {
|
|
946
|
+
return;
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
const reactiveTypes = Array.isArray(component.reactiveType)
|
|
950
|
+
? component.reactiveType
|
|
951
|
+
: [component.reactiveType || 'component'];
|
|
952
|
+
|
|
953
|
+
if (reactiveTypes.includes('all')) {
|
|
954
|
+
component.forceUpdate();
|
|
955
|
+
notifiedComponents.add(componentId);
|
|
956
|
+
return;
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
if (reactiveTypes.includes('deps')) {
|
|
960
|
+
if (component.depsFunction) {
|
|
961
|
+
const currentState = store.getShadowValue(stateKey);
|
|
962
|
+
const newDeps = component.depsFunction(currentState);
|
|
963
|
+
let shouldUpdate = false;
|
|
964
|
+
if (newDeps === true || !isDeepEqual(component.prevDeps, newDeps)) {
|
|
965
|
+
if (Array.isArray(newDeps)) component.prevDeps = newDeps;
|
|
966
|
+
shouldUpdate = true;
|
|
967
|
+
}
|
|
968
|
+
if (shouldUpdate) {
|
|
969
|
+
component.forceUpdate();
|
|
970
|
+
notifiedComponents.add(componentId);
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
});
|
|
975
|
+
};
|
|
785
976
|
|
|
786
977
|
export function useCogsStateFn<TStateObject extends unknown>(
|
|
787
978
|
stateObject: TStateObject,
|
|
788
979
|
{
|
|
789
980
|
stateKey,
|
|
790
|
-
|
|
981
|
+
|
|
791
982
|
localStorage,
|
|
792
983
|
formElements,
|
|
793
984
|
reactiveDeps,
|
|
@@ -901,7 +1092,6 @@ export function useCogsStateFn<TStateObject extends unknown>(
|
|
|
901
1092
|
const unsubscribe = getGlobalStore
|
|
902
1093
|
.getState()
|
|
903
1094
|
.subscribeToPath(thisKey, (event) => {
|
|
904
|
-
// REPLACEMENT STARTS HERE
|
|
905
1095
|
if (event?.type === 'SERVER_STATE_UPDATE') {
|
|
906
1096
|
const serverStateData = event.serverState;
|
|
907
1097
|
|
|
@@ -912,7 +1102,6 @@ export function useCogsStateFn<TStateObject extends unknown>(
|
|
|
912
1102
|
const newOptions = { serverState: serverStateData };
|
|
913
1103
|
setAndMergeOptions(thisKey, newOptions);
|
|
914
1104
|
|
|
915
|
-
// Check for a merge request.
|
|
916
1105
|
const mergeConfig =
|
|
917
1106
|
typeof serverStateData.merge === 'object'
|
|
918
1107
|
? serverStateData.merge
|
|
@@ -920,20 +1109,17 @@ export function useCogsStateFn<TStateObject extends unknown>(
|
|
|
920
1109
|
? { strategy: 'append' }
|
|
921
1110
|
: null;
|
|
922
1111
|
|
|
923
|
-
// Get the current array and the new data.
|
|
924
1112
|
const currentState = getGlobalStore
|
|
925
1113
|
.getState()
|
|
926
1114
|
.getShadowValue(thisKey);
|
|
927
1115
|
const incomingData = serverStateData.data;
|
|
928
1116
|
|
|
929
|
-
// *** THE REAL FIX: PERFORM INCREMENTAL INSERTS ***
|
|
930
1117
|
if (
|
|
931
1118
|
mergeConfig &&
|
|
932
1119
|
Array.isArray(currentState) &&
|
|
933
1120
|
Array.isArray(incomingData)
|
|
934
1121
|
) {
|
|
935
1122
|
const keyField = mergeConfig.key || 'id';
|
|
936
|
-
|
|
937
1123
|
const existingIds = new Set(
|
|
938
1124
|
currentState.map((item: any) => item[keyField])
|
|
939
1125
|
);
|
|
@@ -941,21 +1127,72 @@ export function useCogsStateFn<TStateObject extends unknown>(
|
|
|
941
1127
|
const newUniqueItems = incomingData.filter((item: any) => {
|
|
942
1128
|
return !existingIds.has(item[keyField]);
|
|
943
1129
|
});
|
|
944
|
-
|
|
1130
|
+
|
|
945
1131
|
if (newUniqueItems.length > 0) {
|
|
946
1132
|
newUniqueItems.forEach((item) => {
|
|
947
1133
|
getGlobalStore
|
|
948
1134
|
.getState()
|
|
949
1135
|
.insertShadowArrayElement(thisKey, [], item);
|
|
1136
|
+
|
|
1137
|
+
// MARK NEW SERVER ITEMS AS SYNCED
|
|
1138
|
+
const arrayMeta = getGlobalStore
|
|
1139
|
+
.getState()
|
|
1140
|
+
.getShadowMetadata(thisKey, []);
|
|
1141
|
+
|
|
1142
|
+
if (arrayMeta?.arrayKeys) {
|
|
1143
|
+
const newItemKey =
|
|
1144
|
+
arrayMeta.arrayKeys[arrayMeta.arrayKeys.length - 1];
|
|
1145
|
+
if (newItemKey) {
|
|
1146
|
+
const newItemPath = newItemKey.split('.').slice(1);
|
|
1147
|
+
|
|
1148
|
+
// Mark the new server item as synced, not dirty
|
|
1149
|
+
getGlobalStore
|
|
1150
|
+
.getState()
|
|
1151
|
+
.setShadowMetadata(thisKey, newItemPath, {
|
|
1152
|
+
isDirty: false,
|
|
1153
|
+
stateSource: 'server',
|
|
1154
|
+
lastServerSync:
|
|
1155
|
+
serverStateData.timestamp || Date.now(),
|
|
1156
|
+
});
|
|
1157
|
+
|
|
1158
|
+
// Also mark all its child fields as synced if it's an object
|
|
1159
|
+
const itemValue = getGlobalStore
|
|
1160
|
+
.getState()
|
|
1161
|
+
.getShadowValue(newItemKey);
|
|
1162
|
+
if (
|
|
1163
|
+
itemValue &&
|
|
1164
|
+
typeof itemValue === 'object' &&
|
|
1165
|
+
!Array.isArray(itemValue)
|
|
1166
|
+
) {
|
|
1167
|
+
Object.keys(itemValue).forEach((fieldKey) => {
|
|
1168
|
+
const fieldPath = [...newItemPath, fieldKey];
|
|
1169
|
+
getGlobalStore
|
|
1170
|
+
.getState()
|
|
1171
|
+
.setShadowMetadata(thisKey, fieldPath, {
|
|
1172
|
+
isDirty: false,
|
|
1173
|
+
stateSource: 'server',
|
|
1174
|
+
lastServerSync:
|
|
1175
|
+
serverStateData.timestamp || Date.now(),
|
|
1176
|
+
});
|
|
1177
|
+
});
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
950
1181
|
});
|
|
951
|
-
} else {
|
|
952
|
-
// No new items, no need to do anything.
|
|
953
|
-
return;
|
|
954
1182
|
}
|
|
955
1183
|
} else {
|
|
1184
|
+
// For replace strategy or initial load
|
|
956
1185
|
getGlobalStore
|
|
957
1186
|
.getState()
|
|
958
1187
|
.initializeShadowState(thisKey, incomingData);
|
|
1188
|
+
|
|
1189
|
+
// Mark the entire state tree as synced from server
|
|
1190
|
+
markEntireStateAsServerSynced(
|
|
1191
|
+
thisKey,
|
|
1192
|
+
[],
|
|
1193
|
+
incomingData,
|
|
1194
|
+
serverStateData.timestamp
|
|
1195
|
+
);
|
|
959
1196
|
}
|
|
960
1197
|
|
|
961
1198
|
const meta = getGlobalStore
|
|
@@ -1012,7 +1249,6 @@ export function useCogsStateFn<TStateObject extends unknown>(
|
|
|
1012
1249
|
useLayoutEffect(() => {
|
|
1013
1250
|
if (noStateKey) {
|
|
1014
1251
|
setAndMergeOptions(thisKey as string, {
|
|
1015
|
-
serverSync,
|
|
1016
1252
|
formElements,
|
|
1017
1253
|
defaultState,
|
|
1018
1254
|
localStorage,
|
|
@@ -1043,9 +1279,7 @@ export function useCogsStateFn<TStateObject extends unknown>(
|
|
|
1043
1279
|
...rootMeta,
|
|
1044
1280
|
components,
|
|
1045
1281
|
});
|
|
1046
|
-
|
|
1047
1282
|
forceUpdate({});
|
|
1048
|
-
|
|
1049
1283
|
return () => {
|
|
1050
1284
|
const meta = getGlobalStore.getState().getShadowMetadata(thisKey, []);
|
|
1051
1285
|
const component = meta?.components?.get(componentKey);
|
|
@@ -1079,11 +1313,12 @@ export function useCogsStateFn<TStateObject extends unknown>(
|
|
|
1079
1313
|
}
|
|
1080
1314
|
};
|
|
1081
1315
|
}, []);
|
|
1316
|
+
|
|
1317
|
+
const syncApiRef = useRef<SyncApi | null>(null);
|
|
1082
1318
|
const effectiveSetState = (
|
|
1083
1319
|
newStateOrFunction: UpdateArg<TStateObject> | InsertParams<TStateObject>,
|
|
1084
1320
|
path: string[],
|
|
1085
|
-
updateObj:
|
|
1086
|
-
validationKey?: string
|
|
1321
|
+
updateObj: UpdateOptions
|
|
1087
1322
|
) => {
|
|
1088
1323
|
const fullPath = [thisKey, ...path].join('.');
|
|
1089
1324
|
if (Array.isArray(path)) {
|
|
@@ -1122,24 +1357,38 @@ export function useCogsStateFn<TStateObject extends unknown>(
|
|
|
1122
1357
|
store.insertShadowArrayElement(thisKey, path, newUpdate.newValue);
|
|
1123
1358
|
// The array at `path` has been modified. Mark it AND all its parents as dirty.
|
|
1124
1359
|
store.markAsDirty(thisKey, path, { bubble: true });
|
|
1360
|
+
|
|
1361
|
+
// ALSO mark the newly inserted item itself as dirty
|
|
1362
|
+
// Get the new item's path and mark it as dirty
|
|
1363
|
+
const arrayMeta = store.getShadowMetadata(thisKey, path);
|
|
1364
|
+
if (arrayMeta?.arrayKeys) {
|
|
1365
|
+
const newItemKey =
|
|
1366
|
+
arrayMeta.arrayKeys[arrayMeta.arrayKeys.length - 1];
|
|
1367
|
+
if (newItemKey) {
|
|
1368
|
+
const newItemPath = newItemKey.split('.').slice(1); // Remove stateKey
|
|
1369
|
+
store.markAsDirty(thisKey, newItemPath, { bubble: false });
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1125
1372
|
break;
|
|
1126
1373
|
}
|
|
1127
1374
|
case 'cut': {
|
|
1128
|
-
// The item is at `path`, so the parent array is at `path.slice(0, -1)`
|
|
1129
1375
|
const parentArrayPath = path.slice(0, -1);
|
|
1376
|
+
|
|
1130
1377
|
store.removeShadowArrayElement(thisKey, path);
|
|
1131
|
-
// The parent array has been modified. Mark it AND all its parents as dirty.
|
|
1132
1378
|
store.markAsDirty(thisKey, parentArrayPath, { bubble: true });
|
|
1133
1379
|
break;
|
|
1134
1380
|
}
|
|
1135
1381
|
case 'update': {
|
|
1136
1382
|
store.updateShadowAtPath(thisKey, path, newUpdate.newValue);
|
|
1137
|
-
// The item at `path` was updated. Mark it AND all its parents as dirty.
|
|
1138
1383
|
store.markAsDirty(thisKey, path, { bubble: true });
|
|
1139
1384
|
break;
|
|
1140
1385
|
}
|
|
1141
1386
|
}
|
|
1387
|
+
const shouldSync = updateObj.sync !== false;
|
|
1142
1388
|
|
|
1389
|
+
if (shouldSync && syncApiRef.current && syncApiRef.current.connected) {
|
|
1390
|
+
syncApiRef.current.updateState({ operation: newUpdate });
|
|
1391
|
+
}
|
|
1143
1392
|
// Handle signals - reuse shadowMeta from the beginning
|
|
1144
1393
|
if (shadowMeta?.signals && shadowMeta.signals.length > 0) {
|
|
1145
1394
|
// Use updatedShadowValue if we need the new value, otherwise use payload
|
|
@@ -1305,59 +1554,18 @@ export function useCogsStateFn<TStateObject extends unknown>(
|
|
|
1305
1554
|
});
|
|
1306
1555
|
}
|
|
1307
1556
|
}
|
|
1308
|
-
if (
|
|
1309
|
-
updateObj.updateType === 'update' &&
|
|
1310
|
-
(validationKey || latestInitialOptionsRef.current?.validation?.key) &&
|
|
1311
|
-
path
|
|
1312
|
-
) {
|
|
1313
|
-
removeValidationError(
|
|
1314
|
-
(validationKey || latestInitialOptionsRef.current?.validation?.key) +
|
|
1315
|
-
'.' +
|
|
1316
|
-
path.join('.')
|
|
1317
|
-
);
|
|
1318
|
-
}
|
|
1319
|
-
const arrayWithoutIndex = path.slice(0, path.length - 1);
|
|
1320
|
-
if (
|
|
1321
|
-
updateObj.updateType === 'cut' &&
|
|
1322
|
-
latestInitialOptionsRef.current?.validation?.key
|
|
1323
|
-
) {
|
|
1324
|
-
removeValidationError(
|
|
1325
|
-
latestInitialOptionsRef.current?.validation?.key +
|
|
1326
|
-
'.' +
|
|
1327
|
-
arrayWithoutIndex.join('.')
|
|
1328
|
-
);
|
|
1329
|
-
}
|
|
1330
|
-
if (
|
|
1331
|
-
updateObj.updateType === 'insert' &&
|
|
1332
|
-
latestInitialOptionsRef.current?.validation?.key
|
|
1333
|
-
) {
|
|
1334
|
-
const getValidation = getValidationErrors(
|
|
1335
|
-
latestInitialOptionsRef.current?.validation?.key +
|
|
1336
|
-
'.' +
|
|
1337
|
-
arrayWithoutIndex.join('.')
|
|
1338
|
-
);
|
|
1339
|
-
|
|
1340
|
-
getValidation.filter((k) => {
|
|
1341
|
-
let length = k?.split('.').length;
|
|
1342
|
-
const v = ''; // Placeholder as `v` is not used from getValidationErrors
|
|
1343
1557
|
|
|
1344
|
-
if (
|
|
1345
|
-
k == arrayWithoutIndex.join('.') &&
|
|
1346
|
-
length == arrayWithoutIndex.length - 1
|
|
1347
|
-
) {
|
|
1348
|
-
let newKey = k + '.' + arrayWithoutIndex;
|
|
1349
|
-
removeValidationError(k!);
|
|
1350
|
-
addValidationError(newKey, v!);
|
|
1351
|
-
}
|
|
1352
|
-
});
|
|
1353
|
-
}
|
|
1354
1558
|
// Assumes `isDeepEqual` is available in this scope.
|
|
1355
1559
|
// Assumes `isDeepEqual` is available in this scope.
|
|
1356
1560
|
|
|
1357
|
-
const newState =
|
|
1358
|
-
const rootMeta =
|
|
1561
|
+
const newState = getGlobalStore.getState().getShadowValue(thisKey);
|
|
1562
|
+
const rootMeta = getGlobalStore.getState().getShadowMetadata(thisKey, []);
|
|
1359
1563
|
const notifiedComponents = new Set<string>();
|
|
1360
|
-
|
|
1564
|
+
console.log(
|
|
1565
|
+
'rootMeta',
|
|
1566
|
+
thisKey,
|
|
1567
|
+
getGlobalStore.getState().shadowStateStore
|
|
1568
|
+
);
|
|
1361
1569
|
if (!rootMeta?.components) {
|
|
1362
1570
|
return newState;
|
|
1363
1571
|
}
|
|
@@ -1365,28 +1573,39 @@ export function useCogsStateFn<TStateObject extends unknown>(
|
|
|
1365
1573
|
// --- PASS 1: Notify specific subscribers based on update type ---
|
|
1366
1574
|
|
|
1367
1575
|
if (updateObj.updateType === 'update') {
|
|
1368
|
-
//
|
|
1369
|
-
//
|
|
1370
|
-
//
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
: [component.reactiveType || 'component'];
|
|
1382
|
-
|
|
1383
|
-
// Check if the component has reactivity enabled
|
|
1384
|
-
if (!reactiveTypes.includes('none')) {
|
|
1385
|
-
component.forceUpdate();
|
|
1386
|
-
notifiedComponents.add(componentId);
|
|
1576
|
+
// --- Bubble-up Notification ---
|
|
1577
|
+
// When a nested property changes, notify components listening at that exact path,
|
|
1578
|
+
// and also "bubble up" to notify components listening on parent paths.
|
|
1579
|
+
// e.g., an update to `user.address.street` notifies listeners of `street`, `address`, and `user`.
|
|
1580
|
+
let currentPath = [...path]; // Create a mutable copy of the path
|
|
1581
|
+
|
|
1582
|
+
while (true) {
|
|
1583
|
+
const currentPathMeta = store.getShadowMetadata(thisKey, currentPath);
|
|
1584
|
+
|
|
1585
|
+
if (currentPathMeta?.pathComponents) {
|
|
1586
|
+
currentPathMeta.pathComponents.forEach((componentId) => {
|
|
1587
|
+
if (notifiedComponents.has(componentId)) {
|
|
1588
|
+
return; // Avoid sending redundant notifications
|
|
1387
1589
|
}
|
|
1388
|
-
|
|
1389
|
-
|
|
1590
|
+
const component = rootMeta.components?.get(componentId);
|
|
1591
|
+
if (component) {
|
|
1592
|
+
const reactiveTypes = Array.isArray(component.reactiveType)
|
|
1593
|
+
? component.reactiveType
|
|
1594
|
+
: [component.reactiveType || 'component'];
|
|
1595
|
+
|
|
1596
|
+
// This notification logic applies to components that depend on object structures.
|
|
1597
|
+
if (!reactiveTypes.includes('none')) {
|
|
1598
|
+
component.forceUpdate();
|
|
1599
|
+
notifiedComponents.add(componentId);
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
});
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
if (currentPath.length === 0) {
|
|
1606
|
+
break; // We've reached the root, stop bubbling.
|
|
1607
|
+
}
|
|
1608
|
+
currentPath.pop(); // Go up one level for the next iteration.
|
|
1390
1609
|
}
|
|
1391
1610
|
|
|
1392
1611
|
// ADDITIONALLY, if the payload is an object, perform a deep-check and
|
|
@@ -1581,6 +1800,11 @@ export function useCogsStateFn<TStateObject extends unknown>(
|
|
|
1581
1800
|
);
|
|
1582
1801
|
}, [thisKey, sessionId]);
|
|
1583
1802
|
|
|
1803
|
+
const cogsSyncFn = latestInitialOptionsRef.current?.cogsSync;
|
|
1804
|
+
if (cogsSyncFn) {
|
|
1805
|
+
syncApiRef.current = cogsSyncFn(updaterFinal);
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1584
1808
|
return updaterFinal;
|
|
1585
1809
|
}
|
|
1586
1810
|
|
|
@@ -1661,12 +1885,16 @@ const registerComponentDependency = (
|
|
|
1661
1885
|
dependencyPath: string[]
|
|
1662
1886
|
) => {
|
|
1663
1887
|
const fullComponentId = `${stateKey}////${componentId}`;
|
|
1664
|
-
const
|
|
1888
|
+
const { addPathComponent, getShadowMetadata } = getGlobalStore.getState();
|
|
1889
|
+
|
|
1890
|
+
// First, check if the component should even be registered.
|
|
1891
|
+
// This check is safe to do outside the setter.
|
|
1892
|
+
const rootMeta = getShadowMetadata(stateKey, []);
|
|
1665
1893
|
const component = rootMeta?.components?.get(fullComponentId);
|
|
1666
1894
|
|
|
1667
1895
|
if (
|
|
1668
1896
|
!component ||
|
|
1669
|
-
component.reactiveType
|
|
1897
|
+
component.reactiveType === 'none' ||
|
|
1670
1898
|
!(
|
|
1671
1899
|
Array.isArray(component.reactiveType)
|
|
1672
1900
|
? component.reactiveType
|
|
@@ -1676,23 +1904,9 @@ const registerComponentDependency = (
|
|
|
1676
1904
|
return;
|
|
1677
1905
|
}
|
|
1678
1906
|
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
// Add to component's paths (existing logic)
|
|
1682
|
-
component.paths.add(pathKey);
|
|
1683
|
-
|
|
1684
|
-
// NEW: Also store componentId at the path level
|
|
1685
|
-
const pathMeta =
|
|
1686
|
-
getGlobalStore.getState().getShadowMetadata(stateKey, dependencyPath) || {};
|
|
1687
|
-
const pathComponents = pathMeta.pathComponents || new Set<string>();
|
|
1688
|
-
pathComponents.add(fullComponentId);
|
|
1689
|
-
|
|
1690
|
-
getGlobalStore.getState().setShadowMetadata(stateKey, dependencyPath, {
|
|
1691
|
-
...pathMeta,
|
|
1692
|
-
pathComponents,
|
|
1693
|
-
});
|
|
1907
|
+
// Now, call the single, safe, atomic function to perform the update.
|
|
1908
|
+
addPathComponent(stateKey, dependencyPath, fullComponentId);
|
|
1694
1909
|
};
|
|
1695
|
-
|
|
1696
1910
|
const notifySelectionComponents = (
|
|
1697
1911
|
stateKey: string,
|
|
1698
1912
|
parentPath: string[],
|
|
@@ -1896,21 +2110,56 @@ function createProxyHandler<T>(
|
|
|
1896
2110
|
}
|
|
1897
2111
|
};
|
|
1898
2112
|
}
|
|
2113
|
+
// Fixed getStatus function in createProxyHandler
|
|
1899
2114
|
if (prop === '_status' || prop === 'getStatus') {
|
|
1900
2115
|
const getStatusFunc = () => {
|
|
1901
2116
|
const shadowMeta = getGlobalStore
|
|
1902
2117
|
.getState()
|
|
1903
|
-
.getShadowMetadata(stateKey,
|
|
2118
|
+
.getShadowMetadata(stateKey, path);
|
|
2119
|
+
const value = getGlobalStore
|
|
2120
|
+
.getState()
|
|
2121
|
+
.getShadowValue(stateKeyPathKey);
|
|
1904
2122
|
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
} else if (shadowMeta?.isDirty) {
|
|
2123
|
+
// Priority 1: Explicitly dirty items
|
|
2124
|
+
if (shadowMeta?.isDirty === true) {
|
|
1908
2125
|
return 'dirty';
|
|
1909
|
-
}
|
|
2126
|
+
}
|
|
2127
|
+
|
|
2128
|
+
// Priority 2: Explicitly synced items (isDirty: false)
|
|
2129
|
+
if (shadowMeta?.isDirty === false) {
|
|
2130
|
+
return 'synced';
|
|
2131
|
+
}
|
|
2132
|
+
|
|
2133
|
+
// Priority 3: Items from server source (should be synced even without explicit isDirty flag)
|
|
2134
|
+
if (shadowMeta?.stateSource === 'server') {
|
|
2135
|
+
return 'synced';
|
|
2136
|
+
}
|
|
2137
|
+
|
|
2138
|
+
// Priority 4: Items restored from localStorage
|
|
2139
|
+
if (shadowMeta?.stateSource === 'localStorage') {
|
|
1910
2140
|
return 'restored';
|
|
1911
|
-
}
|
|
2141
|
+
}
|
|
2142
|
+
|
|
2143
|
+
// Priority 5: Items from default/initial state
|
|
2144
|
+
if (shadowMeta?.stateSource === 'default') {
|
|
2145
|
+
return 'fresh';
|
|
2146
|
+
}
|
|
2147
|
+
|
|
2148
|
+
// Priority 6: Check if this is part of initial server load
|
|
2149
|
+
// Look up the tree to see if parent has server source
|
|
2150
|
+
const rootMeta = getGlobalStore
|
|
2151
|
+
.getState()
|
|
2152
|
+
.getShadowMetadata(stateKey, []);
|
|
2153
|
+
if (rootMeta?.stateSource === 'server' && !shadowMeta?.isDirty) {
|
|
2154
|
+
return 'synced';
|
|
2155
|
+
}
|
|
2156
|
+
|
|
2157
|
+
// Priority 7: If no metadata exists but value exists, it's probably fresh
|
|
2158
|
+
if (value !== undefined && !shadowMeta) {
|
|
1912
2159
|
return 'fresh';
|
|
1913
2160
|
}
|
|
2161
|
+
|
|
2162
|
+
// Fallback
|
|
1914
2163
|
return 'unknown';
|
|
1915
2164
|
};
|
|
1916
2165
|
|
|
@@ -1930,13 +2179,16 @@ function createProxyHandler<T>(
|
|
|
1930
2179
|
}
|
|
1931
2180
|
if (prop === 'showValidationErrors') {
|
|
1932
2181
|
return () => {
|
|
1933
|
-
const
|
|
1934
|
-
.getState()
|
|
1935
|
-
.getInitialOptions(stateKey)?.validation;
|
|
1936
|
-
if (!init?.key) throw new Error('Validation key not found');
|
|
1937
|
-
return getGlobalStore
|
|
2182
|
+
const meta = getGlobalStore
|
|
1938
2183
|
.getState()
|
|
1939
|
-
.
|
|
2184
|
+
.getShadowMetadata(stateKey, path);
|
|
2185
|
+
if (
|
|
2186
|
+
meta?.validation?.status === 'VALIDATION_FAILED' &&
|
|
2187
|
+
meta.validation.message
|
|
2188
|
+
) {
|
|
2189
|
+
return [meta.validation.message];
|
|
2190
|
+
}
|
|
2191
|
+
return [];
|
|
1940
2192
|
};
|
|
1941
2193
|
}
|
|
1942
2194
|
if (Array.isArray(currentState)) {
|
|
@@ -2457,6 +2709,49 @@ function createProxyHandler<T>(
|
|
|
2457
2709
|
},
|
|
2458
2710
|
rebuildStateShape,
|
|
2459
2711
|
});
|
|
2712
|
+
} // In createProxyHandler -> handler -> get -> if (Array.isArray(currentState))
|
|
2713
|
+
|
|
2714
|
+
if (prop === 'stateFind') {
|
|
2715
|
+
return (
|
|
2716
|
+
callbackfn: (value: any, index: number) => boolean
|
|
2717
|
+
): StateObject<any> | undefined => {
|
|
2718
|
+
// 1. Use the correct set of keys: filtered/sorted from meta, or all keys from the store.
|
|
2719
|
+
const arrayKeys =
|
|
2720
|
+
meta?.validIds ??
|
|
2721
|
+
getGlobalStore.getState().getShadowMetadata(stateKey, path)
|
|
2722
|
+
?.arrayKeys;
|
|
2723
|
+
|
|
2724
|
+
if (!arrayKeys) {
|
|
2725
|
+
return undefined;
|
|
2726
|
+
}
|
|
2727
|
+
|
|
2728
|
+
// 2. Iterate through the keys, get the value for each, and run the callback.
|
|
2729
|
+
for (let i = 0; i < arrayKeys.length; i++) {
|
|
2730
|
+
const itemKey = arrayKeys[i];
|
|
2731
|
+
if (!itemKey) continue; // Safety check
|
|
2732
|
+
|
|
2733
|
+
const itemValue = getGlobalStore
|
|
2734
|
+
.getState()
|
|
2735
|
+
.getShadowValue(itemKey);
|
|
2736
|
+
|
|
2737
|
+
// 3. If the callback returns true, we've found our item.
|
|
2738
|
+
if (callbackfn(itemValue, i)) {
|
|
2739
|
+
// Get the item's path relative to the stateKey (e.g., ['messages', '42'] -> ['42'])
|
|
2740
|
+
const itemPath = itemKey.split('.').slice(1);
|
|
2741
|
+
|
|
2742
|
+
// 4. Rebuild a new, fully functional StateObject for just that item and return it.
|
|
2743
|
+
return rebuildStateShape({
|
|
2744
|
+
currentState: itemValue,
|
|
2745
|
+
path: itemPath,
|
|
2746
|
+
componentId: componentId,
|
|
2747
|
+
meta, // Pass along meta for potential further chaining
|
|
2748
|
+
});
|
|
2749
|
+
}
|
|
2750
|
+
}
|
|
2751
|
+
|
|
2752
|
+
// 5. If the loop finishes without finding anything, return undefined.
|
|
2753
|
+
return undefined;
|
|
2754
|
+
};
|
|
2460
2755
|
}
|
|
2461
2756
|
if (prop === 'stateFilter') {
|
|
2462
2757
|
return (callbackfn: (value: any, index: number) => boolean) => {
|
|
@@ -2691,13 +2986,13 @@ function createProxyHandler<T>(
|
|
|
2691
2986
|
arrayValues: freshValues || [],
|
|
2692
2987
|
};
|
|
2693
2988
|
}, [cacheKey, updateTrigger]);
|
|
2694
|
-
|
|
2989
|
+
console.log('freshValues', validIds, arrayValues);
|
|
2695
2990
|
useEffect(() => {
|
|
2696
2991
|
const unsubscribe = getGlobalStore
|
|
2697
2992
|
.getState()
|
|
2698
2993
|
.subscribeToPath(stateKeyPathKey, (e) => {
|
|
2699
2994
|
// A data change has occurred for the source array.
|
|
2700
|
-
|
|
2995
|
+
|
|
2701
2996
|
if (e.type === 'GET_SELECTED') {
|
|
2702
2997
|
return;
|
|
2703
2998
|
}
|
|
@@ -2716,7 +3011,12 @@ function createProxyHandler<T>(
|
|
|
2716
3011
|
}
|
|
2717
3012
|
}
|
|
2718
3013
|
}
|
|
2719
|
-
|
|
3014
|
+
|
|
3015
|
+
if (
|
|
3016
|
+
e.type === 'INSERT' ||
|
|
3017
|
+
e.type === 'REMOVE' ||
|
|
3018
|
+
e.type === 'CLEAR_SELECTION'
|
|
3019
|
+
) {
|
|
2720
3020
|
forceUpdate({});
|
|
2721
3021
|
}
|
|
2722
3022
|
});
|
|
@@ -2741,7 +3041,7 @@ function createProxyHandler<T>(
|
|
|
2741
3041
|
validIds: validIds,
|
|
2742
3042
|
},
|
|
2743
3043
|
});
|
|
2744
|
-
|
|
3044
|
+
console.log('sssssssssssssssssssssssssssss', arraySetter);
|
|
2745
3045
|
return (
|
|
2746
3046
|
<>
|
|
2747
3047
|
{arrayValues.map((item, localIndex) => {
|
|
@@ -2918,15 +3218,12 @@ function createProxyHandler<T>(
|
|
|
2918
3218
|
}
|
|
2919
3219
|
if (prop === 'cutSelected') {
|
|
2920
3220
|
return () => {
|
|
2921
|
-
const baseArrayKeys =
|
|
2922
|
-
getGlobalStore.getState().getShadowMetadata(stateKey, path)
|
|
2923
|
-
?.arrayKeys || [];
|
|
2924
3221
|
const validKeys = applyTransforms(
|
|
2925
3222
|
stateKey,
|
|
2926
3223
|
path,
|
|
2927
3224
|
meta?.transforms
|
|
2928
3225
|
);
|
|
2929
|
-
|
|
3226
|
+
|
|
2930
3227
|
if (!validKeys || validKeys.length === 0) return;
|
|
2931
3228
|
|
|
2932
3229
|
const indexKeyToCut = getGlobalStore
|
|
@@ -2936,13 +3233,15 @@ function createProxyHandler<T>(
|
|
|
2936
3233
|
let indexToCut = validKeys.findIndex(
|
|
2937
3234
|
(key) => key === indexKeyToCut
|
|
2938
3235
|
);
|
|
2939
|
-
|
|
3236
|
+
|
|
2940
3237
|
const pathForCut = validKeys[
|
|
2941
3238
|
indexToCut == -1 ? validKeys.length - 1 : indexToCut
|
|
2942
3239
|
]
|
|
2943
3240
|
?.split('.')
|
|
2944
3241
|
.slice(1);
|
|
2945
|
-
|
|
3242
|
+
getGlobalStore
|
|
3243
|
+
.getState()
|
|
3244
|
+
.clearSelectedIndex({ arrayKey: stateKeyPathKey });
|
|
2946
3245
|
effectiveSetState(currentState, pathForCut!, {
|
|
2947
3246
|
updateType: 'cut',
|
|
2948
3247
|
});
|
|
@@ -3068,6 +3367,14 @@ function createProxyHandler<T>(
|
|
|
3068
3367
|
.getShadowValue(stateKeyPathKey, meta?.validIds);
|
|
3069
3368
|
};
|
|
3070
3369
|
}
|
|
3370
|
+
if (prop === 'getState') {
|
|
3371
|
+
return () => {
|
|
3372
|
+
return getGlobalStore
|
|
3373
|
+
.getState()
|
|
3374
|
+
.getShadowValue(stateKeyPathKey, meta?.validIds);
|
|
3375
|
+
};
|
|
3376
|
+
}
|
|
3377
|
+
|
|
3071
3378
|
if (prop === '$derive') {
|
|
3072
3379
|
return (fn: any) =>
|
|
3073
3380
|
$cogsSignal({
|
|
@@ -3177,13 +3484,93 @@ function createProxyHandler<T>(
|
|
|
3177
3484
|
};
|
|
3178
3485
|
}
|
|
3179
3486
|
if (prop === 'applyJsonPatch') {
|
|
3180
|
-
return (patches:
|
|
3181
|
-
const
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3487
|
+
return (patches: Operation[]) => {
|
|
3488
|
+
const store = getGlobalStore.getState();
|
|
3489
|
+
const rootMeta = store.getShadowMetadata(stateKey, []);
|
|
3490
|
+
if (!rootMeta?.components) return;
|
|
3491
|
+
|
|
3492
|
+
const convertPath = (jsonPath: string): string[] => {
|
|
3493
|
+
if (!jsonPath || jsonPath === '/') return [];
|
|
3494
|
+
return jsonPath
|
|
3495
|
+
.split('/')
|
|
3496
|
+
.slice(1)
|
|
3497
|
+
.map((p) => p.replace(/~1/g, '/').replace(/~0/g, '~'));
|
|
3498
|
+
};
|
|
3185
3499
|
|
|
3186
|
-
|
|
3500
|
+
const notifiedComponents = new Set<string>();
|
|
3501
|
+
|
|
3502
|
+
for (const patch of patches) {
|
|
3503
|
+
const relativePath = convertPath(patch.path);
|
|
3504
|
+
|
|
3505
|
+
switch (patch.op) {
|
|
3506
|
+
case 'add':
|
|
3507
|
+
case 'replace': {
|
|
3508
|
+
const { value } = patch as {
|
|
3509
|
+
op: 'add' | 'replace';
|
|
3510
|
+
path: string;
|
|
3511
|
+
value: any;
|
|
3512
|
+
};
|
|
3513
|
+
store.updateShadowAtPath(stateKey, relativePath, value);
|
|
3514
|
+
store.markAsDirty(stateKey, relativePath, { bubble: true });
|
|
3515
|
+
|
|
3516
|
+
// Bubble up - notify components at this path and all parent paths
|
|
3517
|
+
let currentPath = [...relativePath];
|
|
3518
|
+
while (true) {
|
|
3519
|
+
const pathMeta = store.getShadowMetadata(
|
|
3520
|
+
stateKey,
|
|
3521
|
+
currentPath
|
|
3522
|
+
);
|
|
3523
|
+
console.log('pathMeta', pathMeta);
|
|
3524
|
+
if (pathMeta?.pathComponents) {
|
|
3525
|
+
pathMeta.pathComponents.forEach((componentId) => {
|
|
3526
|
+
if (!notifiedComponents.has(componentId)) {
|
|
3527
|
+
const component =
|
|
3528
|
+
rootMeta.components?.get(componentId);
|
|
3529
|
+
if (component) {
|
|
3530
|
+
component.forceUpdate();
|
|
3531
|
+
notifiedComponents.add(componentId);
|
|
3532
|
+
}
|
|
3533
|
+
}
|
|
3534
|
+
});
|
|
3535
|
+
}
|
|
3536
|
+
|
|
3537
|
+
if (currentPath.length === 0) break;
|
|
3538
|
+
currentPath.pop(); // Go up one level
|
|
3539
|
+
}
|
|
3540
|
+
break;
|
|
3541
|
+
}
|
|
3542
|
+
case 'remove': {
|
|
3543
|
+
const parentPath = relativePath.slice(0, -1);
|
|
3544
|
+
store.removeShadowArrayElement(stateKey, relativePath);
|
|
3545
|
+
store.markAsDirty(stateKey, parentPath, { bubble: true });
|
|
3546
|
+
|
|
3547
|
+
// Bubble up from parent path
|
|
3548
|
+
let currentPath = [...parentPath];
|
|
3549
|
+
while (true) {
|
|
3550
|
+
const pathMeta = store.getShadowMetadata(
|
|
3551
|
+
stateKey,
|
|
3552
|
+
currentPath
|
|
3553
|
+
);
|
|
3554
|
+
if (pathMeta?.pathComponents) {
|
|
3555
|
+
pathMeta.pathComponents.forEach((componentId) => {
|
|
3556
|
+
if (!notifiedComponents.has(componentId)) {
|
|
3557
|
+
const component =
|
|
3558
|
+
rootMeta.components?.get(componentId);
|
|
3559
|
+
if (component) {
|
|
3560
|
+
component.forceUpdate();
|
|
3561
|
+
notifiedComponents.add(componentId);
|
|
3562
|
+
}
|
|
3563
|
+
}
|
|
3564
|
+
});
|
|
3565
|
+
}
|
|
3566
|
+
|
|
3567
|
+
if (currentPath.length === 0) break;
|
|
3568
|
+
currentPath.pop();
|
|
3569
|
+
}
|
|
3570
|
+
break;
|
|
3571
|
+
}
|
|
3572
|
+
}
|
|
3573
|
+
}
|
|
3187
3574
|
};
|
|
3188
3575
|
}
|
|
3189
3576
|
if (prop === 'validateZodSchema') {
|
|
@@ -3191,20 +3578,40 @@ function createProxyHandler<T>(
|
|
|
3191
3578
|
const init = getGlobalStore
|
|
3192
3579
|
.getState()
|
|
3193
3580
|
.getInitialOptions(stateKey)?.validation;
|
|
3194
|
-
|
|
3195
|
-
|
|
3581
|
+
|
|
3582
|
+
// UPDATED: Select v4 schema, with a fallback to v3
|
|
3583
|
+
const zodSchema = init?.zodSchemaV4 || init?.zodSchemaV3;
|
|
3584
|
+
|
|
3585
|
+
if (!zodSchema || !init?.key) {
|
|
3586
|
+
throw new Error(
|
|
3587
|
+
'Zod schema (v3 or v4) or validation key not found'
|
|
3588
|
+
);
|
|
3589
|
+
}
|
|
3196
3590
|
|
|
3197
3591
|
removeValidationError(init.key);
|
|
3198
3592
|
const thisObject = getGlobalStore
|
|
3199
3593
|
.getState()
|
|
3200
3594
|
.getShadowValue(stateKey);
|
|
3201
|
-
|
|
3595
|
+
|
|
3596
|
+
// Use the selected schema for parsing
|
|
3597
|
+
const result = zodSchema.safeParse(thisObject);
|
|
3202
3598
|
|
|
3203
3599
|
if (!result.success) {
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3600
|
+
// This logic already handles both v3 and v4 error types correctly
|
|
3601
|
+
if ('issues' in result.error) {
|
|
3602
|
+
// Zod v4 error
|
|
3603
|
+
result.error.issues.forEach((error) => {
|
|
3604
|
+
const fullErrorPath = [init.key, ...error.path].join('.');
|
|
3605
|
+
addValidationError(fullErrorPath, error.message);
|
|
3606
|
+
});
|
|
3607
|
+
} else {
|
|
3608
|
+
// Zod v3 error
|
|
3609
|
+
(result.error as any).errors.forEach((error: any) => {
|
|
3610
|
+
const fullErrorPath = [init.key, ...error.path].join('.');
|
|
3611
|
+
addValidationError(fullErrorPath, error.message);
|
|
3612
|
+
});
|
|
3613
|
+
}
|
|
3614
|
+
|
|
3208
3615
|
notifyComponents(stateKey);
|
|
3209
3616
|
return false;
|
|
3210
3617
|
}
|
|
@@ -3247,9 +3654,38 @@ function createProxyHandler<T>(
|
|
|
3247
3654
|
if (prop === '_path') return path;
|
|
3248
3655
|
if (prop === 'update') {
|
|
3249
3656
|
return (payload: UpdateArg<T>) => {
|
|
3657
|
+
// Step 1: This is the same. It performs the data update.
|
|
3250
3658
|
effectiveSetState(payload as any, path, { updateType: 'update' });
|
|
3659
|
+
|
|
3660
|
+
return {
|
|
3661
|
+
/**
|
|
3662
|
+
* Marks this specific item, which was just updated, as 'synced' (not dirty).
|
|
3663
|
+
*/
|
|
3664
|
+
synced: () => {
|
|
3665
|
+
// This function "remembers" the path of the item that was just updated.
|
|
3666
|
+
const shadowMeta = getGlobalStore
|
|
3667
|
+
.getState()
|
|
3668
|
+
.getShadowMetadata(stateKey, path);
|
|
3669
|
+
|
|
3670
|
+
// It updates ONLY the metadata for that specific item.
|
|
3671
|
+
getGlobalStore.getState().setShadowMetadata(stateKey, path, {
|
|
3672
|
+
...shadowMeta,
|
|
3673
|
+
isDirty: false, // EXPLICITLY set to false, not just undefined
|
|
3674
|
+
stateSource: 'server', // Mark as coming from server
|
|
3675
|
+
lastServerSync: Date.now(), // Add timestamp
|
|
3676
|
+
});
|
|
3677
|
+
|
|
3678
|
+
// Force a re-render for components watching this path
|
|
3679
|
+
const fullPath = [stateKey, ...path].join('.');
|
|
3680
|
+
getGlobalStore.getState().notifyPathSubscribers(fullPath, {
|
|
3681
|
+
type: 'SYNC_STATUS_CHANGE',
|
|
3682
|
+
isDirty: false,
|
|
3683
|
+
});
|
|
3684
|
+
},
|
|
3685
|
+
};
|
|
3251
3686
|
};
|
|
3252
3687
|
}
|
|
3688
|
+
|
|
3253
3689
|
if (prop === 'toggle') {
|
|
3254
3690
|
const currentValueAtPath = getGlobalStore
|
|
3255
3691
|
.getState()
|
|
@@ -3268,20 +3704,14 @@ function createProxyHandler<T>(
|
|
|
3268
3704
|
if (prop === 'formElement') {
|
|
3269
3705
|
return (child: FormControl<T>, formOpts?: FormOptsType) => {
|
|
3270
3706
|
return (
|
|
3271
|
-
<
|
|
3272
|
-
formOpts={formOpts}
|
|
3273
|
-
path={path}
|
|
3707
|
+
<FormElementWrapper
|
|
3274
3708
|
stateKey={stateKey}
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
formOpts={formOpts}
|
|
3282
|
-
renderFn={child as any}
|
|
3283
|
-
/>
|
|
3284
|
-
</ValidationWrapper>
|
|
3709
|
+
path={path}
|
|
3710
|
+
rebuildStateShape={rebuildStateShape}
|
|
3711
|
+
setState={effectiveSetState}
|
|
3712
|
+
formOpts={formOpts}
|
|
3713
|
+
renderFn={child as any}
|
|
3714
|
+
/>
|
|
3285
3715
|
);
|
|
3286
3716
|
};
|
|
3287
3717
|
}
|
|
@@ -3782,6 +4212,7 @@ function ListItemWrapper({
|
|
|
3782
4212
|
const hasReportedInitialHeight = useRef(false);
|
|
3783
4213
|
const fullKey = [stateKey, ...itemPath].join('.');
|
|
3784
4214
|
useRegisterComponent(stateKey, itemComponentId, forceUpdate);
|
|
4215
|
+
|
|
3785
4216
|
const setRefs = useCallback(
|
|
3786
4217
|
(element: HTMLDivElement | null) => {
|
|
3787
4218
|
elementRef.current = element;
|
|
@@ -3839,6 +4270,7 @@ function ListItemWrapper({
|
|
|
3839
4270
|
|
|
3840
4271
|
return <div ref={setRefs}>{children}</div>;
|
|
3841
4272
|
}
|
|
4273
|
+
|
|
3842
4274
|
function FormElementWrapper({
|
|
3843
4275
|
stateKey,
|
|
3844
4276
|
path,
|
|
@@ -3884,7 +4316,9 @@ function FormElementWrapper({
|
|
|
3884
4316
|
const unsubscribe = getGlobalStore
|
|
3885
4317
|
.getState()
|
|
3886
4318
|
.subscribeToPath(stateKeyPathKey, (newValue) => {
|
|
3887
|
-
|
|
4319
|
+
if (!isCurrentlyDebouncing.current && localValue !== newValue) {
|
|
4320
|
+
forceUpdate({});
|
|
4321
|
+
}
|
|
3888
4322
|
});
|
|
3889
4323
|
return () => {
|
|
3890
4324
|
unsubscribe();
|
|
@@ -3897,6 +4331,10 @@ function FormElementWrapper({
|
|
|
3897
4331
|
|
|
3898
4332
|
const debouncedUpdate = useCallback(
|
|
3899
4333
|
(newValue: any) => {
|
|
4334
|
+
const currentType = typeof globalStateValue;
|
|
4335
|
+
if (currentType === 'number' && typeof newValue === 'string') {
|
|
4336
|
+
newValue = newValue === '' ? 0 : Number(newValue);
|
|
4337
|
+
}
|
|
3900
4338
|
setLocalValue(newValue);
|
|
3901
4339
|
isCurrentlyDebouncing.current = true;
|
|
3902
4340
|
|
|
@@ -3908,19 +4346,188 @@ function FormElementWrapper({
|
|
|
3908
4346
|
|
|
3909
4347
|
debounceTimeoutRef.current = setTimeout(() => {
|
|
3910
4348
|
isCurrentlyDebouncing.current = false;
|
|
4349
|
+
|
|
4350
|
+
// Update state
|
|
3911
4351
|
setState(newValue, path, { updateType: 'update' });
|
|
4352
|
+
|
|
4353
|
+
// Perform LIVE validation (gentle)
|
|
4354
|
+
const { getInitialOptions, setShadowMetadata, getShadowMetadata } =
|
|
4355
|
+
getGlobalStore.getState();
|
|
4356
|
+
const validationOptions = getInitialOptions(stateKey)?.validation;
|
|
4357
|
+
const zodSchema =
|
|
4358
|
+
validationOptions?.zodSchemaV4 || validationOptions?.zodSchemaV3;
|
|
4359
|
+
|
|
4360
|
+
if (zodSchema) {
|
|
4361
|
+
const fullState = getGlobalStore.getState().getShadowValue(stateKey);
|
|
4362
|
+
const result = zodSchema.safeParse(fullState);
|
|
4363
|
+
|
|
4364
|
+
const currentMeta = getShadowMetadata(stateKey, path) || {};
|
|
4365
|
+
|
|
4366
|
+
if (!result.success) {
|
|
4367
|
+
const errors =
|
|
4368
|
+
'issues' in result.error
|
|
4369
|
+
? result.error.issues
|
|
4370
|
+
: (result.error as any).errors;
|
|
4371
|
+
const pathErrors = errors.filter(
|
|
4372
|
+
(error: any) =>
|
|
4373
|
+
JSON.stringify(error.path) === JSON.stringify(path)
|
|
4374
|
+
);
|
|
4375
|
+
|
|
4376
|
+
if (pathErrors.length > 0) {
|
|
4377
|
+
setShadowMetadata(stateKey, path, {
|
|
4378
|
+
...currentMeta,
|
|
4379
|
+
validation: {
|
|
4380
|
+
status: 'INVALID_LIVE',
|
|
4381
|
+
message: pathErrors[0]?.message,
|
|
4382
|
+
validatedValue: newValue,
|
|
4383
|
+
},
|
|
4384
|
+
});
|
|
4385
|
+
} else {
|
|
4386
|
+
// This field has no errors - clear validation
|
|
4387
|
+
setShadowMetadata(stateKey, path, {
|
|
4388
|
+
...currentMeta,
|
|
4389
|
+
validation: {
|
|
4390
|
+
status: 'VALID_LIVE',
|
|
4391
|
+
validatedValue: newValue,
|
|
4392
|
+
},
|
|
4393
|
+
});
|
|
4394
|
+
}
|
|
4395
|
+
} else {
|
|
4396
|
+
// Validation passed - clear any existing errors
|
|
4397
|
+
setShadowMetadata(stateKey, path, {
|
|
4398
|
+
...currentMeta,
|
|
4399
|
+
validation: {
|
|
4400
|
+
status: 'VALID_LIVE',
|
|
4401
|
+
validatedValue: newValue,
|
|
4402
|
+
},
|
|
4403
|
+
});
|
|
4404
|
+
}
|
|
4405
|
+
}
|
|
3912
4406
|
}, debounceTime);
|
|
4407
|
+
forceUpdate({});
|
|
3913
4408
|
},
|
|
3914
|
-
[setState, path, formOpts?.debounceTime]
|
|
4409
|
+
[setState, path, formOpts?.debounceTime, stateKey]
|
|
3915
4410
|
);
|
|
3916
4411
|
|
|
3917
|
-
|
|
4412
|
+
// --- NEW onBlur HANDLER ---
|
|
4413
|
+
// This replaces the old commented-out method with a modern approach.
|
|
4414
|
+
const handleBlur = useCallback(async () => {
|
|
4415
|
+
console.log('handleBlur triggered');
|
|
4416
|
+
|
|
4417
|
+
// Commit any pending changes
|
|
3918
4418
|
if (debounceTimeoutRef.current) {
|
|
3919
4419
|
clearTimeout(debounceTimeoutRef.current);
|
|
4420
|
+
debounceTimeoutRef.current = null;
|
|
3920
4421
|
isCurrentlyDebouncing.current = false;
|
|
3921
4422
|
setState(localValue, path, { updateType: 'update' });
|
|
3922
4423
|
}
|
|
3923
|
-
|
|
4424
|
+
|
|
4425
|
+
const { getInitialOptions } = getGlobalStore.getState();
|
|
4426
|
+
const validationOptions = getInitialOptions(stateKey)?.validation;
|
|
4427
|
+
const zodSchema =
|
|
4428
|
+
validationOptions?.zodSchemaV4 || validationOptions?.zodSchemaV3;
|
|
4429
|
+
|
|
4430
|
+
if (!zodSchema) return;
|
|
4431
|
+
|
|
4432
|
+
// Get the full path including stateKey
|
|
4433
|
+
|
|
4434
|
+
// Update validation state to "validating"
|
|
4435
|
+
const currentMeta = getGlobalStore
|
|
4436
|
+
.getState()
|
|
4437
|
+
.getShadowMetadata(stateKey, path);
|
|
4438
|
+
getGlobalStore.getState().setShadowMetadata(stateKey, path, {
|
|
4439
|
+
...currentMeta,
|
|
4440
|
+
validation: {
|
|
4441
|
+
status: 'DIRTY',
|
|
4442
|
+
validatedValue: localValue,
|
|
4443
|
+
},
|
|
4444
|
+
});
|
|
4445
|
+
|
|
4446
|
+
// Validate full state
|
|
4447
|
+
const fullState = getGlobalStore.getState().getShadowValue(stateKey);
|
|
4448
|
+
const result = zodSchema.safeParse(fullState);
|
|
4449
|
+
console.log('result ', result);
|
|
4450
|
+
if (!result.success) {
|
|
4451
|
+
const errors =
|
|
4452
|
+
'issues' in result.error
|
|
4453
|
+
? result.error.issues
|
|
4454
|
+
: (result.error as any).errors;
|
|
4455
|
+
|
|
4456
|
+
console.log('All validation errors:', errors);
|
|
4457
|
+
console.log('Current blur path:', path);
|
|
4458
|
+
|
|
4459
|
+
// Find errors for this specific path
|
|
4460
|
+
const pathErrors = errors.filter((error: any) => {
|
|
4461
|
+
console.log('Processing error:', error);
|
|
4462
|
+
|
|
4463
|
+
// For array paths, we need to translate indices to ULIDs
|
|
4464
|
+
if (path.some((p) => p.startsWith('id:'))) {
|
|
4465
|
+
console.log('Detected array path with ULID');
|
|
4466
|
+
|
|
4467
|
+
// This is an array item path like ["id:xyz", "name"]
|
|
4468
|
+
const parentPath = path[0]!.startsWith('id:')
|
|
4469
|
+
? []
|
|
4470
|
+
: path.slice(0, -1);
|
|
4471
|
+
|
|
4472
|
+
console.log('Parent path:', parentPath);
|
|
4473
|
+
|
|
4474
|
+
const arrayMeta = getGlobalStore
|
|
4475
|
+
.getState()
|
|
4476
|
+
.getShadowMetadata(stateKey, parentPath);
|
|
4477
|
+
|
|
4478
|
+
console.log('Array metadata:', arrayMeta);
|
|
4479
|
+
|
|
4480
|
+
if (arrayMeta?.arrayKeys) {
|
|
4481
|
+
const itemKey = [stateKey, ...path.slice(0, -1)].join('.');
|
|
4482
|
+
const itemIndex = arrayMeta.arrayKeys.indexOf(itemKey);
|
|
4483
|
+
|
|
4484
|
+
console.log('Item key:', itemKey, 'Index:', itemIndex);
|
|
4485
|
+
|
|
4486
|
+
// Compare with Zod path
|
|
4487
|
+
const zodPath = [...parentPath, itemIndex, ...path.slice(-1)];
|
|
4488
|
+
const match =
|
|
4489
|
+
JSON.stringify(error.path) === JSON.stringify(zodPath);
|
|
4490
|
+
|
|
4491
|
+
console.log('Zod path comparison:', {
|
|
4492
|
+
zodPath,
|
|
4493
|
+
errorPath: error.path,
|
|
4494
|
+
match,
|
|
4495
|
+
});
|
|
4496
|
+
return match;
|
|
4497
|
+
}
|
|
4498
|
+
}
|
|
4499
|
+
|
|
4500
|
+
const directMatch = JSON.stringify(error.path) === JSON.stringify(path);
|
|
4501
|
+
console.log('Direct path comparison:', {
|
|
4502
|
+
errorPath: error.path,
|
|
4503
|
+
currentPath: path,
|
|
4504
|
+
match: directMatch,
|
|
4505
|
+
});
|
|
4506
|
+
return directMatch;
|
|
4507
|
+
});
|
|
4508
|
+
|
|
4509
|
+
console.log('Filtered path errors:', pathErrors);
|
|
4510
|
+
// Update shadow metadata with validation result
|
|
4511
|
+
getGlobalStore.getState().setShadowMetadata(stateKey, path, {
|
|
4512
|
+
...currentMeta,
|
|
4513
|
+
validation: {
|
|
4514
|
+
status: 'VALIDATION_FAILED',
|
|
4515
|
+
message: pathErrors[0]?.message,
|
|
4516
|
+
validatedValue: localValue,
|
|
4517
|
+
},
|
|
4518
|
+
});
|
|
4519
|
+
} else {
|
|
4520
|
+
// Validation passed
|
|
4521
|
+
getGlobalStore.getState().setShadowMetadata(stateKey, path, {
|
|
4522
|
+
...currentMeta,
|
|
4523
|
+
validation: {
|
|
4524
|
+
status: 'VALID_PENDING_SYNC',
|
|
4525
|
+
validatedValue: localValue,
|
|
4526
|
+
},
|
|
4527
|
+
});
|
|
4528
|
+
}
|
|
4529
|
+
forceUpdate({});
|
|
4530
|
+
}, [stateKey, path, localValue, setState]);
|
|
3924
4531
|
|
|
3925
4532
|
const baseState = rebuildStateShape({
|
|
3926
4533
|
currentState: globalStateValue,
|
|
@@ -3936,7 +4543,8 @@ function FormElementWrapper({
|
|
|
3936
4543
|
onChange: (e: any) => {
|
|
3937
4544
|
debouncedUpdate(e.target.value);
|
|
3938
4545
|
},
|
|
3939
|
-
onBlur
|
|
4546
|
+
// 5. Wire the new onBlur handler to the input props.
|
|
4547
|
+
onBlur: handleBlur,
|
|
3940
4548
|
ref: formRefStore
|
|
3941
4549
|
.getState()
|
|
3942
4550
|
.getFormRef(stateKey + '.' + path.join('.')),
|
|
@@ -3947,9 +4555,12 @@ function FormElementWrapper({
|
|
|
3947
4555
|
},
|
|
3948
4556
|
});
|
|
3949
4557
|
|
|
3950
|
-
return
|
|
4558
|
+
return (
|
|
4559
|
+
<ValidationWrapper formOpts={formOpts} path={path} stateKey={stateKey}>
|
|
4560
|
+
{renderFn(stateWithInputProps)}
|
|
4561
|
+
</ValidationWrapper>
|
|
4562
|
+
);
|
|
3951
4563
|
}
|
|
3952
|
-
|
|
3953
4564
|
function useRegisterComponent(
|
|
3954
4565
|
stateKey: string,
|
|
3955
4566
|
componentId: string,
|
|
@@ -3958,25 +4569,19 @@ function useRegisterComponent(
|
|
|
3958
4569
|
const fullComponentId = `${stateKey}////${componentId}`;
|
|
3959
4570
|
|
|
3960
4571
|
useLayoutEffect(() => {
|
|
3961
|
-
const
|
|
3962
|
-
|
|
4572
|
+
const { registerComponent, unregisterComponent } =
|
|
4573
|
+
getGlobalStore.getState();
|
|
3963
4574
|
|
|
3964
|
-
|
|
4575
|
+
// Call the safe, centralized function to register
|
|
4576
|
+
registerComponent(stateKey, fullComponentId, {
|
|
3965
4577
|
forceUpdate: () => forceUpdate({}),
|
|
3966
4578
|
paths: new Set(),
|
|
3967
4579
|
reactiveType: ['component'],
|
|
3968
4580
|
});
|
|
3969
4581
|
|
|
3970
|
-
|
|
3971
|
-
...rootMeta,
|
|
3972
|
-
components,
|
|
3973
|
-
});
|
|
3974
|
-
|
|
4582
|
+
// The cleanup now calls the safe, centralized unregister function
|
|
3975
4583
|
return () => {
|
|
3976
|
-
|
|
3977
|
-
if (meta?.components) {
|
|
3978
|
-
meta.components.delete(fullComponentId);
|
|
3979
|
-
}
|
|
4584
|
+
unregisterComponent(stateKey, fullComponentId);
|
|
3980
4585
|
};
|
|
3981
|
-
}, [stateKey, fullComponentId]);
|
|
4586
|
+
}, [stateKey, fullComponentId]); // Dependencies are stable and correct
|
|
3982
4587
|
}
|