cogsbox-state 0.5.465 → 0.5.467
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 +3 -3
- package/dist/CogsState.d.ts +1 -0
- package/dist/CogsState.d.ts.map +1 -1
- package/dist/CogsState.jsx +1008 -1273
- package/dist/CogsState.jsx.map +1 -1
- package/dist/Components.d.ts +39 -0
- package/dist/Components.d.ts.map +1 -0
- package/dist/Components.jsx +281 -0
- package/dist/Components.jsx.map +1 -0
- package/dist/index.js +11 -12
- package/dist/store.d.ts +2 -6
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +274 -236
- package/dist/store.js.map +1 -1
- package/package.json +1 -1
- package/src/CogsState.tsx +144 -709
- package/src/Components.tsx +541 -0
- package/src/store.ts +293 -221
- package/dist/Functions.d.ts +0 -11
- package/dist/Functions.d.ts.map +0 -1
- package/dist/Functions.jsx +0 -29
- package/dist/Functions.jsx.map +0 -1
- package/src/Functions.tsx +0 -66
package/src/CogsState.tsx
CHANGED
|
@@ -14,30 +14,32 @@ import {
|
|
|
14
14
|
type ReactNode,
|
|
15
15
|
type RefObject,
|
|
16
16
|
} from 'react';
|
|
17
|
-
|
|
17
|
+
|
|
18
18
|
import {
|
|
19
19
|
getDifferences,
|
|
20
20
|
isArray,
|
|
21
21
|
isFunction,
|
|
22
22
|
type GenericObject,
|
|
23
23
|
} from './utility.js';
|
|
24
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
FormElementWrapper,
|
|
26
|
+
MemoizedCogsItemWrapper,
|
|
27
|
+
ValidationWrapper,
|
|
28
|
+
} from './Components.js';
|
|
25
29
|
import { isDeepEqual, transformStateFunc } from './utility.js';
|
|
26
30
|
import superjson from 'superjson';
|
|
27
31
|
import { v4 as uuidv4 } from 'uuid';
|
|
28
32
|
|
|
29
33
|
import {
|
|
30
|
-
buildShadowNode,
|
|
31
34
|
formRefStore,
|
|
32
35
|
getGlobalStore,
|
|
33
|
-
METADATA_KEYS,
|
|
34
36
|
ValidationError,
|
|
35
37
|
ValidationStatus,
|
|
36
38
|
type ComponentsType,
|
|
37
39
|
} from './store.js';
|
|
38
40
|
import { useCogsConfig } from './CogsStateClient.js';
|
|
39
41
|
import { Operation } from 'fast-json-patch';
|
|
40
|
-
|
|
42
|
+
|
|
41
43
|
import * as z3 from 'zod/v3';
|
|
42
44
|
import * as z4 from 'zod/v4';
|
|
43
45
|
|
|
@@ -488,6 +490,7 @@ const {
|
|
|
488
490
|
initializeShadowState,
|
|
489
491
|
updateShadowAtPath,
|
|
490
492
|
insertShadowArrayElement,
|
|
493
|
+
insertManyShadowArrayElements,
|
|
491
494
|
removeShadowArrayElement,
|
|
492
495
|
getSelectedIndex,
|
|
493
496
|
setInitialStateOptions,
|
|
@@ -513,8 +516,7 @@ function getArrayData(stateKey: string, path: string[], meta?: MetaData) {
|
|
|
513
516
|
const value = getGlobalStore.getState().getShadowValue(stateKey, path);
|
|
514
517
|
return { isArray: false, value, keys: [] };
|
|
515
518
|
}
|
|
516
|
-
|
|
517
|
-
const arrayPathKey = path.join('.');
|
|
519
|
+
const arrayPathKey = path.length > 0 ? path.join('.') : 'root';
|
|
518
520
|
const viewIds = meta?.arrayViews?.[arrayPathKey] ?? shadowMeta.arrayKeys;
|
|
519
521
|
|
|
520
522
|
// FIX: If the derived view is empty, return an empty array and keys.
|
|
@@ -699,7 +701,7 @@ export const createCogsState = <State extends Record<StateKeys, unknown>>(
|
|
|
699
701
|
Object.keys(statePart).forEach((key) => {
|
|
700
702
|
initializeShadowState(key, statePart[key]);
|
|
701
703
|
});
|
|
702
|
-
|
|
704
|
+
|
|
703
705
|
type StateKeys = keyof typeof statePart;
|
|
704
706
|
|
|
705
707
|
const useCogsState = <StateKey extends StateKeys>(
|
|
@@ -1467,11 +1469,6 @@ export function useCogsStateFn<TStateObject extends unknown>(
|
|
|
1467
1469
|
return; // Ignore if no valid data
|
|
1468
1470
|
}
|
|
1469
1471
|
|
|
1470
|
-
console.log(
|
|
1471
|
-
'✅ SERVER_STATE_UPDATE received with data:',
|
|
1472
|
-
serverStateData
|
|
1473
|
-
);
|
|
1474
|
-
|
|
1475
1472
|
setAndMergeOptions(thisKey, { serverState: serverStateData });
|
|
1476
1473
|
|
|
1477
1474
|
const mergeConfig =
|
|
@@ -1481,7 +1478,6 @@ export function useCogsStateFn<TStateObject extends unknown>(
|
|
|
1481
1478
|
? { strategy: 'append' }
|
|
1482
1479
|
: null;
|
|
1483
1480
|
|
|
1484
|
-
// ✅ FIX 1: The path for the root value is now `[]`.
|
|
1485
1481
|
const currentState = getShadowValue(thisKey, []);
|
|
1486
1482
|
const incomingData = serverStateData.data;
|
|
1487
1483
|
|
|
@@ -1499,7 +1495,7 @@ export function useCogsStateFn<TStateObject extends unknown>(
|
|
|
1499
1495
|
);
|
|
1500
1496
|
return;
|
|
1501
1497
|
}
|
|
1502
|
-
|
|
1498
|
+
|
|
1503
1499
|
const existingIds = new Set(
|
|
1504
1500
|
currentState.map((item: any) => item[keyField])
|
|
1505
1501
|
);
|
|
@@ -1509,9 +1505,7 @@ export function useCogsStateFn<TStateObject extends unknown>(
|
|
|
1509
1505
|
);
|
|
1510
1506
|
|
|
1511
1507
|
if (newUniqueItems.length > 0) {
|
|
1512
|
-
|
|
1513
|
-
insertShadowArrayElement(thisKey, [], item);
|
|
1514
|
-
});
|
|
1508
|
+
insertManyShadowArrayElements(thisKey, [], newUniqueItems);
|
|
1515
1509
|
}
|
|
1516
1510
|
|
|
1517
1511
|
// Mark the entire final state as synced
|
|
@@ -1698,27 +1692,16 @@ type MetaData = {
|
|
|
1698
1692
|
fn: Function;
|
|
1699
1693
|
path: string[]; // Which array this transform applies to
|
|
1700
1694
|
}>;
|
|
1695
|
+
serverStateIsUpStream?: boolean;
|
|
1701
1696
|
};
|
|
1702
1697
|
|
|
1703
|
-
function hashTransforms(transforms: any[]) {
|
|
1704
|
-
if (!transforms || transforms.length === 0) {
|
|
1705
|
-
return '';
|
|
1706
|
-
}
|
|
1707
|
-
return transforms
|
|
1708
|
-
.map(
|
|
1709
|
-
(transform) =>
|
|
1710
|
-
`${transform.type}${JSON.stringify(transform.dependencies || [])}`
|
|
1711
|
-
)
|
|
1712
|
-
.join('');
|
|
1713
|
-
}
|
|
1714
|
-
|
|
1715
1698
|
const applyTransforms = (
|
|
1716
1699
|
stateKey: string,
|
|
1717
1700
|
path: string[],
|
|
1718
|
-
|
|
1701
|
+
meta?: MetaData
|
|
1719
1702
|
): string[] => {
|
|
1720
1703
|
let ids = getShadowMetadata(stateKey, path)?.arrayKeys || [];
|
|
1721
|
-
|
|
1704
|
+
const transforms = meta?.transforms;
|
|
1722
1705
|
if (!transforms || transforms.length === 0) {
|
|
1723
1706
|
return ids;
|
|
1724
1707
|
}
|
|
@@ -1775,8 +1758,7 @@ const notifySelectionComponents = (
|
|
|
1775
1758
|
parentPath: string[],
|
|
1776
1759
|
currentSelected?: string | undefined
|
|
1777
1760
|
) => {
|
|
1778
|
-
const
|
|
1779
|
-
const rootMeta = store.getShadowMetadata(stateKey, []);
|
|
1761
|
+
const rootMeta = getShadowMetadata(stateKey, []);
|
|
1780
1762
|
const notifiedComponents = new Set<string>();
|
|
1781
1763
|
|
|
1782
1764
|
// Handle "all" reactive components first
|
|
@@ -1793,20 +1775,18 @@ const notifySelectionComponents = (
|
|
|
1793
1775
|
});
|
|
1794
1776
|
}
|
|
1795
1777
|
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1778
|
+
getShadowMetadata(stateKey, [
|
|
1779
|
+
...parentPath,
|
|
1780
|
+
'getSelected',
|
|
1781
|
+
])?.pathComponents?.forEach((componentId) => {
|
|
1782
|
+
const thisComp = rootMeta?.components?.get(componentId);
|
|
1783
|
+
thisComp?.forceUpdate();
|
|
1784
|
+
});
|
|
1802
1785
|
|
|
1803
|
-
const parentMeta =
|
|
1786
|
+
const parentMeta = getShadowMetadata(stateKey, parentPath);
|
|
1804
1787
|
for (let arrayKey of parentMeta?.arrayKeys || []) {
|
|
1805
1788
|
const key = arrayKey + '.selected';
|
|
1806
|
-
const selectedItem =
|
|
1807
|
-
stateKey,
|
|
1808
|
-
key.split('.').slice(1)
|
|
1809
|
-
);
|
|
1789
|
+
const selectedItem = getShadowMetadata(stateKey, key.split('.').slice(1));
|
|
1810
1790
|
if (arrayKey == currentSelected) {
|
|
1811
1791
|
selectedItem?.pathComponents?.forEach((componentId) => {
|
|
1812
1792
|
const thisComp = rootMeta?.components?.get(componentId);
|
|
@@ -1817,7 +1797,7 @@ const notifySelectionComponents = (
|
|
|
1817
1797
|
};
|
|
1818
1798
|
function getScopedData(stateKey: string, path: string[], meta?: MetaData) {
|
|
1819
1799
|
const shadowMeta = getShadowMetadata(stateKey, path);
|
|
1820
|
-
const arrayPathKey = path.join('.');
|
|
1800
|
+
const arrayPathKey = path.length > 0 ? path.join('.') : 'root';
|
|
1821
1801
|
const arrayKeys = meta?.arrayViews?.[arrayPathKey];
|
|
1822
1802
|
|
|
1823
1803
|
// FIX: If the derived view is empty, return an empty array directly.
|
|
@@ -2168,6 +2148,13 @@ function createProxyHandler<T>(
|
|
|
2168
2148
|
const [rerender, forceUpdate] = useState({});
|
|
2169
2149
|
const initialScrollRef = useRef(true);
|
|
2170
2150
|
|
|
2151
|
+
useEffect(() => {
|
|
2152
|
+
const interval = setInterval(() => {
|
|
2153
|
+
forceUpdate({});
|
|
2154
|
+
}, 1000);
|
|
2155
|
+
return () => clearInterval(interval);
|
|
2156
|
+
}, []);
|
|
2157
|
+
|
|
2171
2158
|
// Scroll state management
|
|
2172
2159
|
const scrollStateRef = useRef({
|
|
2173
2160
|
isUserScrolling: false,
|
|
@@ -2180,57 +2167,28 @@ function createProxyHandler<T>(
|
|
|
2180
2167
|
const measurementCache = useRef(
|
|
2181
2168
|
new Map<string, { height: number; offset: number }>()
|
|
2182
2169
|
);
|
|
2170
|
+
const { keys: arrayKeys } = getArrayData(stateKey, path, meta);
|
|
2183
2171
|
|
|
2184
|
-
//
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
behavior: initialScrollRef.current ? 'instant' : 'smooth',
|
|
2197
|
-
});
|
|
2198
|
-
}, [rerender, stickToBottom]);
|
|
2199
|
-
|
|
2200
|
-
const { arrayKeys = [] } = getScopedData(stateKey, path, meta);
|
|
2201
|
-
|
|
2202
|
-
// Calculate total height and offsets
|
|
2203
|
-
const { totalHeight, itemOffsets } = useMemo(() => {
|
|
2204
|
-
let runningOffset = 0;
|
|
2205
|
-
const offsets = new Map<
|
|
2206
|
-
string,
|
|
2207
|
-
{ height: number; offset: number }
|
|
2208
|
-
>();
|
|
2209
|
-
const allItemKeys =
|
|
2210
|
-
getGlobalStore.getState().getShadowMetadata(stateKey, path)
|
|
2211
|
-
?.arrayKeys || [];
|
|
2212
|
-
|
|
2213
|
-
allItemKeys.forEach((itemKey) => {
|
|
2214
|
-
const itemPath = itemKey.split('.').slice(1);
|
|
2215
|
-
const measuredHeight =
|
|
2216
|
-
getGlobalStore
|
|
2217
|
-
.getState()
|
|
2218
|
-
.getShadowMetadata(stateKey, itemPath)?.virtualizer
|
|
2219
|
-
?.itemHeight || itemHeight;
|
|
2220
|
-
|
|
2221
|
-
offsets.set(itemKey, {
|
|
2222
|
-
height: measuredHeight,
|
|
2223
|
-
offset: runningOffset,
|
|
2172
|
+
// Subscribe to state changes like stateList does
|
|
2173
|
+
useEffect(() => {
|
|
2174
|
+
const stateKeyPathKey = [stateKey, ...path].join('.');
|
|
2175
|
+
const unsubscribe = getGlobalStore
|
|
2176
|
+
.getState()
|
|
2177
|
+
.subscribeToPath(stateKeyPathKey, (e) => {
|
|
2178
|
+
if (e.type === 'GET_SELECTED') {
|
|
2179
|
+
return;
|
|
2180
|
+
}
|
|
2181
|
+
if (e.type === 'SERVER_STATE_UPDATE') {
|
|
2182
|
+
// forceUpdate({});
|
|
2183
|
+
}
|
|
2224
2184
|
});
|
|
2225
2185
|
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
return { totalHeight: runningOffset, itemOffsets: offsets };
|
|
2231
|
-
}, [arrayKeys.length, itemHeight]);
|
|
2186
|
+
return () => {
|
|
2187
|
+
unsubscribe();
|
|
2188
|
+
};
|
|
2189
|
+
}, [componentId, stateKey, path.join('.')]);
|
|
2232
2190
|
|
|
2233
|
-
//
|
|
2191
|
+
// YOUR ORIGINAL INITIAL POSITIONING - KEEPING EXACTLY AS IS
|
|
2234
2192
|
useLayoutEffect(() => {
|
|
2235
2193
|
if (
|
|
2236
2194
|
stickToBottom &&
|
|
@@ -2241,7 +2199,6 @@ function createProxyHandler<T>(
|
|
|
2241
2199
|
) {
|
|
2242
2200
|
const container = containerRef.current;
|
|
2243
2201
|
|
|
2244
|
-
// Wait for container to have dimensions
|
|
2245
2202
|
const waitForContainer = () => {
|
|
2246
2203
|
if (container.clientHeight > 0) {
|
|
2247
2204
|
const visibleCount = Math.ceil(
|
|
@@ -2255,13 +2212,11 @@ function createProxyHandler<T>(
|
|
|
2255
2212
|
|
|
2256
2213
|
setRange({ startIndex, endIndex });
|
|
2257
2214
|
|
|
2258
|
-
// Ensure scroll after range is set
|
|
2259
2215
|
requestAnimationFrame(() => {
|
|
2260
2216
|
scrollToBottom('instant');
|
|
2261
|
-
initialScrollRef.current = false;
|
|
2217
|
+
initialScrollRef.current = false;
|
|
2262
2218
|
});
|
|
2263
2219
|
} else {
|
|
2264
|
-
// Container not ready, try again
|
|
2265
2220
|
requestAnimationFrame(waitForContainer);
|
|
2266
2221
|
}
|
|
2267
2222
|
};
|
|
@@ -2270,7 +2225,16 @@ function createProxyHandler<T>(
|
|
|
2270
2225
|
}
|
|
2271
2226
|
}, [arrayKeys.length, stickToBottom, itemHeight, overscan]);
|
|
2272
2227
|
|
|
2273
|
-
|
|
2228
|
+
const rangeRef = useRef(range);
|
|
2229
|
+
useLayoutEffect(() => {
|
|
2230
|
+
rangeRef.current = range;
|
|
2231
|
+
}, [range]);
|
|
2232
|
+
|
|
2233
|
+
const arrayKeysRef = useRef(arrayKeys);
|
|
2234
|
+
useLayoutEffect(() => {
|
|
2235
|
+
arrayKeysRef.current = arrayKeys;
|
|
2236
|
+
}, [arrayKeys]);
|
|
2237
|
+
|
|
2274
2238
|
const handleScroll = useCallback(() => {
|
|
2275
2239
|
const container = containerRef.current;
|
|
2276
2240
|
if (!container) return;
|
|
@@ -2314,9 +2278,14 @@ function createProxyHandler<T>(
|
|
|
2314
2278
|
break;
|
|
2315
2279
|
}
|
|
2316
2280
|
}
|
|
2317
|
-
|
|
2281
|
+
console.log(
|
|
2282
|
+
'hadnlescroll ',
|
|
2283
|
+
measurementCache.current,
|
|
2284
|
+
newStartIndex,
|
|
2285
|
+
range
|
|
2286
|
+
);
|
|
2318
2287
|
// Only update if range actually changed
|
|
2319
|
-
if (newStartIndex !== range.startIndex) {
|
|
2288
|
+
if (newStartIndex !== range.startIndex && range.startIndex != 0) {
|
|
2320
2289
|
const visibleCount = Math.ceil(clientHeight / itemHeight);
|
|
2321
2290
|
setRange({
|
|
2322
2291
|
startIndex: Math.max(0, newStartIndex - overscan),
|
|
@@ -2337,36 +2306,34 @@ function createProxyHandler<T>(
|
|
|
2337
2306
|
// Set up scroll listener
|
|
2338
2307
|
useEffect(() => {
|
|
2339
2308
|
const container = containerRef.current;
|
|
2340
|
-
if (!container
|
|
2309
|
+
if (!container) return;
|
|
2341
2310
|
|
|
2342
2311
|
container.addEventListener('scroll', handleScroll, {
|
|
2343
2312
|
passive: true,
|
|
2344
2313
|
});
|
|
2345
|
-
|
|
2346
2314
|
return () => {
|
|
2347
2315
|
container.removeEventListener('scroll', handleScroll);
|
|
2348
2316
|
};
|
|
2349
2317
|
}, [handleScroll, stickToBottom]);
|
|
2318
|
+
|
|
2319
|
+
// YOUR ORIGINAL SCROLL TO BOTTOM FUNCTION - KEEPING EXACTLY AS IS
|
|
2350
2320
|
const scrollToBottom = useCallback(
|
|
2351
2321
|
(behavior: ScrollBehavior = 'smooth') => {
|
|
2352
2322
|
const container = containerRef.current;
|
|
2353
2323
|
if (!container) return;
|
|
2354
2324
|
|
|
2355
|
-
// Reset scroll state
|
|
2356
2325
|
scrollStateRef.current.isUserScrolling = false;
|
|
2357
2326
|
scrollStateRef.current.isNearBottom = true;
|
|
2358
2327
|
scrollStateRef.current.scrollUpCount = 0;
|
|
2359
2328
|
|
|
2360
2329
|
const performScroll = () => {
|
|
2361
|
-
// Multiple attempts to ensure we hit the bottom
|
|
2362
2330
|
const attemptScroll = (attempts = 0) => {
|
|
2363
|
-
if (attempts > 5) return;
|
|
2331
|
+
if (attempts > 5) return;
|
|
2364
2332
|
|
|
2365
2333
|
const currentHeight = container.scrollHeight;
|
|
2366
2334
|
const currentScroll = container.scrollTop;
|
|
2367
2335
|
const clientHeight = container.clientHeight;
|
|
2368
2336
|
|
|
2369
|
-
// Check if we're already at the bottom
|
|
2370
2337
|
if (currentScroll + clientHeight >= currentHeight - 1) {
|
|
2371
2338
|
return;
|
|
2372
2339
|
}
|
|
@@ -2376,12 +2343,10 @@ function createProxyHandler<T>(
|
|
|
2376
2343
|
behavior: behavior,
|
|
2377
2344
|
});
|
|
2378
2345
|
|
|
2379
|
-
// In slow environments, check again after a short delay
|
|
2380
2346
|
setTimeout(() => {
|
|
2381
2347
|
const newHeight = container.scrollHeight;
|
|
2382
2348
|
const newScroll = container.scrollTop;
|
|
2383
2349
|
|
|
2384
|
-
// If height changed or we're not at bottom, try again
|
|
2385
2350
|
if (
|
|
2386
2351
|
newHeight !== currentHeight ||
|
|
2387
2352
|
newScroll + clientHeight < newHeight - 1
|
|
@@ -2394,11 +2359,9 @@ function createProxyHandler<T>(
|
|
|
2394
2359
|
attemptScroll();
|
|
2395
2360
|
};
|
|
2396
2361
|
|
|
2397
|
-
// Use requestIdleCallback for better performance in slow environments
|
|
2398
2362
|
if ('requestIdleCallback' in window) {
|
|
2399
2363
|
requestIdleCallback(performScroll, { timeout: 100 });
|
|
2400
2364
|
} else {
|
|
2401
|
-
// Fallback to rAF chain
|
|
2402
2365
|
requestAnimationFrame(() => {
|
|
2403
2366
|
requestAnimationFrame(performScroll);
|
|
2404
2367
|
});
|
|
@@ -2406,15 +2369,14 @@ function createProxyHandler<T>(
|
|
|
2406
2369
|
},
|
|
2407
2370
|
[]
|
|
2408
2371
|
);
|
|
2409
|
-
|
|
2410
|
-
//
|
|
2372
|
+
|
|
2373
|
+
// YOUR ORIGINAL AUTO-SCROLL EFFECTS - KEEPING ALL OF THEM
|
|
2411
2374
|
useEffect(() => {
|
|
2412
2375
|
if (!stickToBottom || !containerRef.current) return;
|
|
2413
2376
|
|
|
2414
2377
|
const container = containerRef.current;
|
|
2415
2378
|
const scrollState = scrollStateRef.current;
|
|
2416
2379
|
|
|
2417
|
-
// Debounced scroll function
|
|
2418
2380
|
let scrollTimeout: NodeJS.Timeout;
|
|
2419
2381
|
const debouncedScrollToBottom = () => {
|
|
2420
2382
|
clearTimeout(scrollTimeout);
|
|
@@ -2430,7 +2392,6 @@ function createProxyHandler<T>(
|
|
|
2430
2392
|
}, 100);
|
|
2431
2393
|
};
|
|
2432
2394
|
|
|
2433
|
-
// Single MutationObserver for all DOM changes
|
|
2434
2395
|
const observer = new MutationObserver(() => {
|
|
2435
2396
|
if (!scrollState.isUserScrolling) {
|
|
2436
2397
|
debouncedScrollToBottom();
|
|
@@ -2441,24 +2402,10 @@ function createProxyHandler<T>(
|
|
|
2441
2402
|
childList: true,
|
|
2442
2403
|
subtree: true,
|
|
2443
2404
|
attributes: true,
|
|
2444
|
-
attributeFilter: ['style', 'class'],
|
|
2405
|
+
attributeFilter: ['style', 'class'],
|
|
2445
2406
|
});
|
|
2446
2407
|
|
|
2447
|
-
// Handle image loads with event delegation
|
|
2448
|
-
const handleImageLoad = (e: Event) => {
|
|
2449
|
-
if (
|
|
2450
|
-
e.target instanceof HTMLImageElement &&
|
|
2451
|
-
!scrollState.isUserScrolling
|
|
2452
|
-
) {
|
|
2453
|
-
debouncedScrollToBottom();
|
|
2454
|
-
}
|
|
2455
|
-
};
|
|
2456
|
-
|
|
2457
|
-
container.addEventListener('load', handleImageLoad, true);
|
|
2458
|
-
|
|
2459
|
-
// Initial scroll with proper timing
|
|
2460
2408
|
if (initialScrollRef.current) {
|
|
2461
|
-
// For initial load, wait for next tick to ensure DOM is ready
|
|
2462
2409
|
setTimeout(() => {
|
|
2463
2410
|
scrollToBottom('instant');
|
|
2464
2411
|
}, 0);
|
|
@@ -2469,31 +2416,28 @@ function createProxyHandler<T>(
|
|
|
2469
2416
|
return () => {
|
|
2470
2417
|
clearTimeout(scrollTimeout);
|
|
2471
2418
|
observer.disconnect();
|
|
2472
|
-
container.removeEventListener('load', handleImageLoad, true);
|
|
2473
2419
|
};
|
|
2474
2420
|
}, [stickToBottom, arrayKeys.length, scrollToBottom]);
|
|
2475
|
-
|
|
2421
|
+
|
|
2422
|
+
// Create virtual state - NO NEED to get values, only IDs!
|
|
2476
2423
|
const virtualState = useMemo(() => {
|
|
2477
|
-
|
|
2478
|
-
const
|
|
2479
|
-
|
|
2480
|
-
|
|
2424
|
+
// 2. Physically slice the corresponding keys.
|
|
2425
|
+
const slicedKeys = Array.isArray(arrayKeys)
|
|
2426
|
+
? arrayKeys.slice(range.startIndex, range.endIndex + 1)
|
|
2427
|
+
: [];
|
|
2481
2428
|
|
|
2482
|
-
|
|
2483
|
-
range.startIndex,
|
|
2484
|
-
range.endIndex + 1
|
|
2485
|
-
);
|
|
2486
|
-
const slicedIds = currentKeys.slice(
|
|
2487
|
-
range.startIndex,
|
|
2488
|
-
range.endIndex + 1
|
|
2489
|
-
);
|
|
2429
|
+
// Use the same keying as getArrayData (empty string for root)
|
|
2490
2430
|
const arrayPath = path.length > 0 ? path.join('.') : 'root';
|
|
2491
2431
|
return rebuildStateShape({
|
|
2492
2432
|
path,
|
|
2493
2433
|
componentId: componentId!,
|
|
2494
|
-
meta: {
|
|
2434
|
+
meta: {
|
|
2435
|
+
...meta,
|
|
2436
|
+
arrayViews: { [arrayPath]: slicedKeys },
|
|
2437
|
+
serverStateIsUpStream: true,
|
|
2438
|
+
},
|
|
2495
2439
|
});
|
|
2496
|
-
}, [range.startIndex, range.endIndex, arrayKeys
|
|
2440
|
+
}, [range.startIndex, range.endIndex, arrayKeys, meta]);
|
|
2497
2441
|
|
|
2498
2442
|
return {
|
|
2499
2443
|
virtualState,
|
|
@@ -2501,15 +2445,14 @@ function createProxyHandler<T>(
|
|
|
2501
2445
|
outer: {
|
|
2502
2446
|
ref: containerRef,
|
|
2503
2447
|
style: {
|
|
2504
|
-
overflowY: 'auto',
|
|
2448
|
+
overflowY: 'auto' as const,
|
|
2505
2449
|
height: '100%',
|
|
2506
|
-
position: 'relative',
|
|
2450
|
+
position: 'relative' as const,
|
|
2507
2451
|
},
|
|
2508
2452
|
},
|
|
2509
2453
|
inner: {
|
|
2510
2454
|
style: {
|
|
2511
|
-
|
|
2512
|
-
position: 'relative',
|
|
2455
|
+
position: 'relative' as const,
|
|
2513
2456
|
},
|
|
2514
2457
|
},
|
|
2515
2458
|
list: {
|
|
@@ -2623,7 +2566,7 @@ function createProxyHandler<T>(
|
|
|
2623
2566
|
}
|
|
2624
2567
|
if (prop === 'stateSort') {
|
|
2625
2568
|
return (compareFn: (a: any, b: any) => number) => {
|
|
2626
|
-
const arrayPathKey = path.join('.');
|
|
2569
|
+
const arrayPathKey = path.length > 0 ? path.join('.') : 'root';
|
|
2627
2570
|
|
|
2628
2571
|
// FIX: Use the more robust `getArrayData` which always correctly resolves the keys for a view.
|
|
2629
2572
|
const { value: currentArray, keys: currentViewIds } = getArrayData(
|
|
@@ -2772,28 +2715,25 @@ function createProxyHandler<T>(
|
|
|
2772
2715
|
arraySetter: any
|
|
2773
2716
|
) => ReactNode
|
|
2774
2717
|
) => {
|
|
2775
|
-
console.log('meta outside', JSON.stringify(meta));
|
|
2776
2718
|
const StateListWrapper = () => {
|
|
2777
2719
|
const componentIdsRef = useRef<Map<string, string>>(new Map());
|
|
2778
2720
|
|
|
2779
2721
|
const [updateTrigger, forceUpdate] = useState({});
|
|
2780
2722
|
|
|
2781
|
-
console.log('updateTrigger updateTrigger updateTrigger');
|
|
2782
|
-
|
|
2783
|
-
const validIds = applyTransforms(
|
|
2784
|
-
stateKey,
|
|
2785
|
-
path,
|
|
2786
|
-
meta?.transforms
|
|
2787
|
-
);
|
|
2788
|
-
//the above get the new coorect valid ids i need ot udpate the meta object with this info
|
|
2789
2723
|
const arrayPathKey = path.length > 0 ? path.join('.') : 'root';
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2724
|
+
|
|
2725
|
+
const validIds = applyTransforms(stateKey, path, meta);
|
|
2726
|
+
|
|
2727
|
+
// Memoize the updated meta to prevent creating new objects on every render
|
|
2728
|
+
const updatedMeta = useMemo(() => {
|
|
2729
|
+
return {
|
|
2730
|
+
...meta,
|
|
2731
|
+
arrayViews: {
|
|
2732
|
+
...(meta?.arrayViews || {}),
|
|
2733
|
+
[arrayPathKey]: validIds,
|
|
2734
|
+
},
|
|
2735
|
+
};
|
|
2736
|
+
}, [meta, arrayPathKey, validIds]);
|
|
2797
2737
|
|
|
2798
2738
|
// Now use the updated meta when getting array data
|
|
2799
2739
|
const { value: arrayValues } = getArrayData(
|
|
@@ -2802,15 +2742,10 @@ function createProxyHandler<T>(
|
|
|
2802
2742
|
updatedMeta
|
|
2803
2743
|
);
|
|
2804
2744
|
|
|
2805
|
-
console.log('validIds', validIds);
|
|
2806
|
-
console.log('arrayValues', arrayValues);
|
|
2807
|
-
|
|
2808
2745
|
useEffect(() => {
|
|
2809
2746
|
const unsubscribe = getGlobalStore
|
|
2810
2747
|
.getState()
|
|
2811
2748
|
.subscribeToPath(stateKeyPathKey, (e) => {
|
|
2812
|
-
// A data change has occurred for the source array.
|
|
2813
|
-
console.log('changed array statelist ', e);
|
|
2814
2749
|
if (e.type === 'GET_SELECTED') {
|
|
2815
2750
|
return;
|
|
2816
2751
|
}
|
|
@@ -2832,8 +2767,11 @@ function createProxyHandler<T>(
|
|
|
2832
2767
|
|
|
2833
2768
|
if (
|
|
2834
2769
|
e.type === 'INSERT' ||
|
|
2770
|
+
e.type === 'INSERT_MANY' ||
|
|
2835
2771
|
e.type === 'REMOVE' ||
|
|
2836
|
-
e.type === 'CLEAR_SELECTION'
|
|
2772
|
+
e.type === 'CLEAR_SELECTION' ||
|
|
2773
|
+
(e.type === 'SERVER_STATE_UPDATE' &&
|
|
2774
|
+
!meta?.serverStateIsUpStream)
|
|
2837
2775
|
) {
|
|
2838
2776
|
forceUpdate({});
|
|
2839
2777
|
}
|
|
@@ -2856,38 +2794,35 @@ function createProxyHandler<T>(
|
|
|
2856
2794
|
componentId: componentId!,
|
|
2857
2795
|
meta: updatedMeta, // Use updated meta here
|
|
2858
2796
|
});
|
|
2859
|
-
console.log('arrayValues', arrayValues);
|
|
2860
2797
|
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
{arrayValues.map((item, localIndex) => {
|
|
2864
|
-
const itemKey = validIds[localIndex];
|
|
2798
|
+
const returnValue = arrayValues.map((item, localIndex) => {
|
|
2799
|
+
const itemKey = validIds[localIndex];
|
|
2865
2800
|
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2801
|
+
if (!itemKey) {
|
|
2802
|
+
return null;
|
|
2803
|
+
}
|
|
2869
2804
|
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2805
|
+
let itemComponentId = componentIdsRef.current.get(itemKey);
|
|
2806
|
+
if (!itemComponentId) {
|
|
2807
|
+
itemComponentId = uuidv4();
|
|
2808
|
+
componentIdsRef.current.set(itemKey, itemComponentId);
|
|
2809
|
+
}
|
|
2875
2810
|
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2811
|
+
const itemPath = [...path, itemKey];
|
|
2812
|
+
|
|
2813
|
+
return createElement(MemoizedCogsItemWrapper, {
|
|
2814
|
+
key: itemKey,
|
|
2815
|
+
stateKey,
|
|
2816
|
+
itemComponentId,
|
|
2817
|
+
itemPath,
|
|
2818
|
+
localIndex,
|
|
2819
|
+
arraySetter,
|
|
2820
|
+
rebuildStateShape,
|
|
2821
|
+
renderFn: callbackfn,
|
|
2822
|
+
});
|
|
2823
|
+
});
|
|
2824
|
+
|
|
2825
|
+
return <>{returnValue}</>;
|
|
2891
2826
|
};
|
|
2892
2827
|
|
|
2893
2828
|
return <StateListWrapper />;
|
|
@@ -2896,7 +2831,7 @@ function createProxyHandler<T>(
|
|
|
2896
2831
|
if (prop === 'stateFlattenOn') {
|
|
2897
2832
|
return (fieldName: string) => {
|
|
2898
2833
|
// FIX: Get the definitive list of IDs for the current view from meta.arrayViews.
|
|
2899
|
-
const arrayPathKey = path.join('.');
|
|
2834
|
+
const arrayPathKey = path.length > 0 ? path.join('.') : 'root';
|
|
2900
2835
|
const viewIds = meta?.arrayViews?.[arrayPathKey];
|
|
2901
2836
|
|
|
2902
2837
|
const currentState = getGlobalStore
|
|
@@ -2916,7 +2851,7 @@ function createProxyHandler<T>(
|
|
|
2916
2851
|
}
|
|
2917
2852
|
if (prop === 'index') {
|
|
2918
2853
|
return (index: number) => {
|
|
2919
|
-
const arrayPathKey = path.join('.');
|
|
2854
|
+
const arrayPathKey = path.length > 0 ? path.join('.') : 'root';
|
|
2920
2855
|
const viewIds = meta?.arrayViews?.[arrayPathKey];
|
|
2921
2856
|
|
|
2922
2857
|
if (viewIds) {
|
|
@@ -2945,26 +2880,17 @@ function createProxyHandler<T>(
|
|
|
2945
2880
|
}
|
|
2946
2881
|
if (prop === 'last') {
|
|
2947
2882
|
return () => {
|
|
2948
|
-
// ✅ FIX: Use getArrayData to get the keys for the current view (filtered or not).
|
|
2949
2883
|
const { keys: currentViewIds } = getArrayData(stateKey, path, meta);
|
|
2950
|
-
|
|
2951
|
-
// If the array is empty, there is no last item.
|
|
2952
2884
|
if (!currentViewIds || currentViewIds.length === 0) {
|
|
2953
2885
|
return undefined;
|
|
2954
2886
|
}
|
|
2955
|
-
|
|
2956
|
-
// Get the unique ID of the last item in the current view.
|
|
2957
2887
|
const lastItemKey = currentViewIds[currentViewIds.length - 1];
|
|
2958
2888
|
|
|
2959
|
-
// If for some reason the key is invalid, return undefined.
|
|
2960
2889
|
if (!lastItemKey) {
|
|
2961
2890
|
return undefined;
|
|
2962
2891
|
}
|
|
2963
|
-
|
|
2964
|
-
// ✅ FIX: The new path uses the item's unique key, not its numerical index.
|
|
2965
2892
|
const newPath = [...path, lastItemKey];
|
|
2966
2893
|
|
|
2967
|
-
// Return a new proxy scoped to that specific item.
|
|
2968
2894
|
return rebuildStateShape({
|
|
2969
2895
|
path: newPath,
|
|
2970
2896
|
componentId: componentId!,
|
|
@@ -3059,6 +2985,7 @@ function createProxyHandler<T>(
|
|
|
3059
2985
|
return;
|
|
3060
2986
|
}
|
|
3061
2987
|
const selectedId = selectedItemKey.split('.').pop() as string;
|
|
2988
|
+
|
|
3062
2989
|
if (!(currentViewIds as any[]).includes(selectedId!)) {
|
|
3063
2990
|
return;
|
|
3064
2991
|
}
|
|
@@ -3129,17 +3056,14 @@ function createProxyHandler<T>(
|
|
|
3129
3056
|
(item) => item?.[searchKey] === searchValue
|
|
3130
3057
|
);
|
|
3131
3058
|
|
|
3132
|
-
// FIX: If found, return a proxy to the item by appending its key to the current path.
|
|
3133
3059
|
if (found) {
|
|
3134
3060
|
return rebuildStateShape({
|
|
3135
|
-
path: [...path, found.key],
|
|
3061
|
+
path: [...path, found.key],
|
|
3136
3062
|
componentId: componentId!,
|
|
3137
3063
|
meta,
|
|
3138
3064
|
});
|
|
3139
3065
|
}
|
|
3140
3066
|
|
|
3141
|
-
// If not found, return an 'empty' proxy that will resolve to undefined on .get()
|
|
3142
|
-
// This prevents "cannot read property 'get' of undefined" errors.
|
|
3143
3067
|
return rebuildStateShape({
|
|
3144
3068
|
path: [...path, `not_found_${uuidv4()}`],
|
|
3145
3069
|
componentId: componentId!,
|
|
@@ -3149,7 +3073,8 @@ function createProxyHandler<T>(
|
|
|
3149
3073
|
}
|
|
3150
3074
|
if (prop === 'cutThis') {
|
|
3151
3075
|
const { value: shadowValue } = getScopedData(stateKey, path, meta);
|
|
3152
|
-
|
|
3076
|
+
const parentPath = path.slice(0, -1);
|
|
3077
|
+
notifySelectionComponents(stateKey, parentPath);
|
|
3153
3078
|
return () => {
|
|
3154
3079
|
effectiveSetState(shadowValue, path, { updateType: 'cut' });
|
|
3155
3080
|
};
|
|
@@ -3172,7 +3097,6 @@ function createProxyHandler<T>(
|
|
|
3172
3097
|
_meta: meta,
|
|
3173
3098
|
});
|
|
3174
3099
|
}
|
|
3175
|
-
// in CogsState.ts -> createProxyHandler -> handler -> get
|
|
3176
3100
|
|
|
3177
3101
|
if (prop === '$get') {
|
|
3178
3102
|
return () =>
|
|
@@ -3190,7 +3114,6 @@ function createProxyHandler<T>(
|
|
|
3190
3114
|
const parentPathArray = path.slice(0, -1);
|
|
3191
3115
|
const parentMeta = getShadowMetadata(stateKey, parentPathArray);
|
|
3192
3116
|
|
|
3193
|
-
// FIX: Check if the parent is an array by looking for arrayKeys in its metadata.
|
|
3194
3117
|
if (parentMeta?.arrayKeys) {
|
|
3195
3118
|
const fullParentKey = stateKey + '.' + parentPathArray.join('.');
|
|
3196
3119
|
const selectedItemKey = getGlobalStore
|
|
@@ -3199,14 +3122,11 @@ function createProxyHandler<T>(
|
|
|
3199
3122
|
|
|
3200
3123
|
const fullItemKey = stateKey + '.' + path.join('.');
|
|
3201
3124
|
|
|
3202
|
-
// Logic remains the same.
|
|
3203
|
-
notifySelectionComponents(stateKey, parentPathArray, undefined);
|
|
3204
3125
|
return selectedItemKey === fullItemKey;
|
|
3205
3126
|
}
|
|
3206
3127
|
return undefined;
|
|
3207
3128
|
}
|
|
3208
3129
|
|
|
3209
|
-
// Then use it in both:
|
|
3210
3130
|
if (prop === 'setSelected') {
|
|
3211
3131
|
return (value: boolean) => {
|
|
3212
3132
|
const parentPath = path.slice(0, -1);
|
|
@@ -3246,6 +3166,7 @@ function createProxyHandler<T>(
|
|
|
3246
3166
|
.getState()
|
|
3247
3167
|
.setSelectedIndex(fullParentKey, fullItemKey);
|
|
3248
3168
|
}
|
|
3169
|
+
notifySelectionComponents(stateKey, parentPath);
|
|
3249
3170
|
};
|
|
3250
3171
|
}
|
|
3251
3172
|
if (prop === '_componentId') {
|
|
@@ -3423,17 +3344,9 @@ function createProxyHandler<T>(
|
|
|
3423
3344
|
if (prop === '_stateKey') return stateKey;
|
|
3424
3345
|
if (prop === '_path') return path;
|
|
3425
3346
|
if (prop === 'update') {
|
|
3426
|
-
// This method is now greatly simplified.
|
|
3427
|
-
// All the complex batching logic has been removed because our new,
|
|
3428
|
-
// universal `createEffectiveSetState` function handles it automatically for all operations.
|
|
3429
3347
|
return (payload: UpdateArg<T>) => {
|
|
3430
|
-
// Simply call effectiveSetState. It will automatically handle queuing
|
|
3431
|
-
// this operation in the batch for efficient processing.
|
|
3432
3348
|
effectiveSetState(payload as any, path, { updateType: 'update' });
|
|
3433
3349
|
|
|
3434
|
-
// The .synced() method is a useful feature that allows developers
|
|
3435
|
-
// to manually mark a piece of state as "synced with the server"
|
|
3436
|
-
// after an update. This part of the functionality is preserved.
|
|
3437
3350
|
return {
|
|
3438
3351
|
synced: () => {
|
|
3439
3352
|
const shadowMeta = getGlobalStore
|
|
@@ -3628,7 +3541,8 @@ function SignalRenderer({
|
|
|
3628
3541
|
const instanceIdRef = useRef<string | null>(null);
|
|
3629
3542
|
const isSetupRef = useRef(false);
|
|
3630
3543
|
const signalId = `${proxy._stateKey}-${proxy._path.join('.')}`;
|
|
3631
|
-
|
|
3544
|
+
|
|
3545
|
+
const arrayPathKey = proxy._path.length > 0 ? proxy._path.join('.') : 'root';
|
|
3632
3546
|
const viewIds = proxy._meta?.arrayViews?.[arrayPathKey];
|
|
3633
3547
|
|
|
3634
3548
|
const value = getShadowValue(proxy._stateKey, proxy._path, viewIds);
|
|
@@ -3721,482 +3635,3 @@ function SignalRenderer({
|
|
|
3721
3635
|
'data-signal-id': signalId,
|
|
3722
3636
|
});
|
|
3723
3637
|
}
|
|
3724
|
-
|
|
3725
|
-
const MemoizedCogsItemWrapper = memo(
|
|
3726
|
-
ListItemWrapper,
|
|
3727
|
-
(prevProps, nextProps) => {
|
|
3728
|
-
// Re-render if any of these change:
|
|
3729
|
-
return (
|
|
3730
|
-
prevProps.itemPath.join('.') === nextProps.itemPath.join('.') &&
|
|
3731
|
-
prevProps.stateKey === nextProps.stateKey &&
|
|
3732
|
-
prevProps.itemComponentId === nextProps.itemComponentId &&
|
|
3733
|
-
prevProps.localIndex === nextProps.localIndex
|
|
3734
|
-
);
|
|
3735
|
-
}
|
|
3736
|
-
);
|
|
3737
|
-
|
|
3738
|
-
const useImageLoaded = (ref: RefObject<HTMLElement>): boolean => {
|
|
3739
|
-
const [loaded, setLoaded] = useState(false);
|
|
3740
|
-
|
|
3741
|
-
useLayoutEffect(() => {
|
|
3742
|
-
if (!ref.current) {
|
|
3743
|
-
setLoaded(true);
|
|
3744
|
-
return;
|
|
3745
|
-
}
|
|
3746
|
-
|
|
3747
|
-
const images = Array.from(ref.current.querySelectorAll('img'));
|
|
3748
|
-
|
|
3749
|
-
// If there are no images, we are "loaded" immediately.
|
|
3750
|
-
if (images.length === 0) {
|
|
3751
|
-
setLoaded(true);
|
|
3752
|
-
return;
|
|
3753
|
-
}
|
|
3754
|
-
|
|
3755
|
-
let loadedCount = 0;
|
|
3756
|
-
const handleImageLoad = () => {
|
|
3757
|
-
loadedCount++;
|
|
3758
|
-
if (loadedCount === images.length) {
|
|
3759
|
-
setLoaded(true);
|
|
3760
|
-
}
|
|
3761
|
-
};
|
|
3762
|
-
|
|
3763
|
-
images.forEach((image) => {
|
|
3764
|
-
if (image.complete) {
|
|
3765
|
-
handleImageLoad();
|
|
3766
|
-
} else {
|
|
3767
|
-
image.addEventListener('load', handleImageLoad);
|
|
3768
|
-
image.addEventListener('error', handleImageLoad);
|
|
3769
|
-
}
|
|
3770
|
-
});
|
|
3771
|
-
|
|
3772
|
-
return () => {
|
|
3773
|
-
images.forEach((image) => {
|
|
3774
|
-
image.removeEventListener('load', handleImageLoad);
|
|
3775
|
-
image.removeEventListener('error', handleImageLoad);
|
|
3776
|
-
});
|
|
3777
|
-
};
|
|
3778
|
-
}, [ref.current]);
|
|
3779
|
-
|
|
3780
|
-
return loaded;
|
|
3781
|
-
};
|
|
3782
|
-
|
|
3783
|
-
function ListItemWrapper({
|
|
3784
|
-
stateKey,
|
|
3785
|
-
itemComponentId,
|
|
3786
|
-
itemPath,
|
|
3787
|
-
localIndex,
|
|
3788
|
-
arraySetter,
|
|
3789
|
-
rebuildStateShape,
|
|
3790
|
-
renderFn,
|
|
3791
|
-
}: {
|
|
3792
|
-
stateKey: string;
|
|
3793
|
-
itemComponentId: string;
|
|
3794
|
-
itemPath: string[];
|
|
3795
|
-
localIndex: number;
|
|
3796
|
-
arraySetter: any;
|
|
3797
|
-
|
|
3798
|
-
rebuildStateShape: (options: {
|
|
3799
|
-
currentState: any;
|
|
3800
|
-
path: string[];
|
|
3801
|
-
componentId: string;
|
|
3802
|
-
meta?: any;
|
|
3803
|
-
}) => any;
|
|
3804
|
-
renderFn: (
|
|
3805
|
-
setter: any,
|
|
3806
|
-
index: number,
|
|
3807
|
-
|
|
3808
|
-
arraySetter: any
|
|
3809
|
-
) => React.ReactNode;
|
|
3810
|
-
}) {
|
|
3811
|
-
const [, forceUpdate] = useState({});
|
|
3812
|
-
const { ref: inViewRef, inView } = useInView();
|
|
3813
|
-
const elementRef = useRef<HTMLDivElement | null>(null);
|
|
3814
|
-
|
|
3815
|
-
const imagesLoaded = useImageLoaded(elementRef);
|
|
3816
|
-
const hasReportedInitialHeight = useRef(false);
|
|
3817
|
-
const fullKey = [stateKey, ...itemPath].join('.');
|
|
3818
|
-
useRegisterComponent(stateKey, itemComponentId, forceUpdate);
|
|
3819
|
-
|
|
3820
|
-
const setRefs = useCallback(
|
|
3821
|
-
(element: HTMLDivElement | null) => {
|
|
3822
|
-
elementRef.current = element;
|
|
3823
|
-
inViewRef(element); // This is the ref from useInView
|
|
3824
|
-
},
|
|
3825
|
-
[inViewRef]
|
|
3826
|
-
);
|
|
3827
|
-
|
|
3828
|
-
useEffect(() => {
|
|
3829
|
-
subscribeToPath(fullKey, (e) => {
|
|
3830
|
-
forceUpdate({});
|
|
3831
|
-
});
|
|
3832
|
-
}, []);
|
|
3833
|
-
useEffect(() => {
|
|
3834
|
-
if (!inView || !imagesLoaded || hasReportedInitialHeight.current) {
|
|
3835
|
-
return;
|
|
3836
|
-
}
|
|
3837
|
-
|
|
3838
|
-
const element = elementRef.current;
|
|
3839
|
-
if (element && element.offsetHeight > 0) {
|
|
3840
|
-
hasReportedInitialHeight.current = true;
|
|
3841
|
-
const newHeight = element.offsetHeight;
|
|
3842
|
-
|
|
3843
|
-
setShadowMetadata(stateKey, itemPath, {
|
|
3844
|
-
virtualizer: {
|
|
3845
|
-
itemHeight: newHeight,
|
|
3846
|
-
domRef: element,
|
|
3847
|
-
},
|
|
3848
|
-
});
|
|
3849
|
-
|
|
3850
|
-
const arrayPath = itemPath.slice(0, -1);
|
|
3851
|
-
const arrayPathKey = [stateKey, ...arrayPath].join('.');
|
|
3852
|
-
notifyPathSubscribers(arrayPathKey, {
|
|
3853
|
-
type: 'ITEMHEIGHT',
|
|
3854
|
-
itemKey: itemPath.join('.'),
|
|
3855
|
-
|
|
3856
|
-
ref: elementRef.current,
|
|
3857
|
-
});
|
|
3858
|
-
}
|
|
3859
|
-
}, [inView, imagesLoaded, stateKey, itemPath]);
|
|
3860
|
-
|
|
3861
|
-
const itemValue = getShadowValue(stateKey, itemPath);
|
|
3862
|
-
|
|
3863
|
-
if (itemValue === undefined) {
|
|
3864
|
-
return null;
|
|
3865
|
-
}
|
|
3866
|
-
|
|
3867
|
-
const itemSetter = rebuildStateShape({
|
|
3868
|
-
currentState: itemValue,
|
|
3869
|
-
path: itemPath,
|
|
3870
|
-
componentId: itemComponentId,
|
|
3871
|
-
});
|
|
3872
|
-
const children = renderFn(itemSetter, localIndex, arraySetter);
|
|
3873
|
-
|
|
3874
|
-
return <div ref={setRefs}>{children}</div>;
|
|
3875
|
-
}
|
|
3876
|
-
|
|
3877
|
-
function FormElementWrapper({
|
|
3878
|
-
stateKey,
|
|
3879
|
-
path,
|
|
3880
|
-
rebuildStateShape,
|
|
3881
|
-
renderFn,
|
|
3882
|
-
formOpts,
|
|
3883
|
-
setState,
|
|
3884
|
-
}: {
|
|
3885
|
-
stateKey: string;
|
|
3886
|
-
path: string[];
|
|
3887
|
-
rebuildStateShape: (options: {
|
|
3888
|
-
path: string[];
|
|
3889
|
-
componentId: string;
|
|
3890
|
-
meta?: any;
|
|
3891
|
-
}) => any;
|
|
3892
|
-
renderFn: (params: FormElementParams<any>) => React.ReactNode;
|
|
3893
|
-
formOpts?: FormOptsType;
|
|
3894
|
-
setState: any;
|
|
3895
|
-
}) {
|
|
3896
|
-
const [componentId] = useState(() => uuidv4());
|
|
3897
|
-
const [, forceUpdate] = useState({});
|
|
3898
|
-
|
|
3899
|
-
const stateKeyPathKey = [stateKey, ...path].join('.');
|
|
3900
|
-
useRegisterComponent(stateKey, componentId, forceUpdate);
|
|
3901
|
-
const globalStateValue = getShadowValue(stateKey, path);
|
|
3902
|
-
const [localValue, setLocalValue] = useState<any>(globalStateValue);
|
|
3903
|
-
const isCurrentlyDebouncing = useRef(false);
|
|
3904
|
-
const debounceTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
|
3905
|
-
|
|
3906
|
-
useEffect(() => {
|
|
3907
|
-
if (
|
|
3908
|
-
!isCurrentlyDebouncing.current &&
|
|
3909
|
-
!isDeepEqual(globalStateValue, localValue)
|
|
3910
|
-
) {
|
|
3911
|
-
setLocalValue(globalStateValue);
|
|
3912
|
-
}
|
|
3913
|
-
}, [globalStateValue]);
|
|
3914
|
-
|
|
3915
|
-
useEffect(() => {
|
|
3916
|
-
const unsubscribe = getGlobalStore
|
|
3917
|
-
.getState()
|
|
3918
|
-
.subscribeToPath(stateKeyPathKey, (newValue) => {
|
|
3919
|
-
if (!isCurrentlyDebouncing.current && localValue !== newValue) {
|
|
3920
|
-
forceUpdate({});
|
|
3921
|
-
}
|
|
3922
|
-
});
|
|
3923
|
-
return () => {
|
|
3924
|
-
unsubscribe();
|
|
3925
|
-
if (debounceTimeoutRef.current) {
|
|
3926
|
-
clearTimeout(debounceTimeoutRef.current);
|
|
3927
|
-
isCurrentlyDebouncing.current = false;
|
|
3928
|
-
}
|
|
3929
|
-
};
|
|
3930
|
-
}, []);
|
|
3931
|
-
|
|
3932
|
-
const debouncedUpdate = useCallback(
|
|
3933
|
-
(newValue: any) => {
|
|
3934
|
-
const currentType = typeof globalStateValue;
|
|
3935
|
-
if (currentType === 'number' && typeof newValue === 'string') {
|
|
3936
|
-
newValue = newValue === '' ? 0 : Number(newValue);
|
|
3937
|
-
}
|
|
3938
|
-
setLocalValue(newValue);
|
|
3939
|
-
isCurrentlyDebouncing.current = true;
|
|
3940
|
-
|
|
3941
|
-
if (debounceTimeoutRef.current) {
|
|
3942
|
-
clearTimeout(debounceTimeoutRef.current);
|
|
3943
|
-
}
|
|
3944
|
-
|
|
3945
|
-
const debounceTime = formOpts?.debounceTime ?? 200;
|
|
3946
|
-
|
|
3947
|
-
debounceTimeoutRef.current = setTimeout(() => {
|
|
3948
|
-
isCurrentlyDebouncing.current = false;
|
|
3949
|
-
setState(newValue, path, { updateType: 'update' });
|
|
3950
|
-
|
|
3951
|
-
// NEW: Check if validation is enabled via features
|
|
3952
|
-
const rootMeta = getGlobalStore
|
|
3953
|
-
.getState()
|
|
3954
|
-
.getShadowMetadata(stateKey, []);
|
|
3955
|
-
if (!rootMeta?.features?.validationEnabled) return;
|
|
3956
|
-
|
|
3957
|
-
const validationOptions = getInitialOptions(stateKey)?.validation;
|
|
3958
|
-
const zodSchema =
|
|
3959
|
-
validationOptions?.zodSchemaV4 || validationOptions?.zodSchemaV3;
|
|
3960
|
-
|
|
3961
|
-
if (zodSchema) {
|
|
3962
|
-
const fullState = getShadowValue(stateKey, []);
|
|
3963
|
-
const result = zodSchema.safeParse(fullState);
|
|
3964
|
-
const currentMeta = getShadowMetadata(stateKey, path) || {};
|
|
3965
|
-
|
|
3966
|
-
if (!result.success) {
|
|
3967
|
-
const errors =
|
|
3968
|
-
'issues' in result.error
|
|
3969
|
-
? result.error.issues
|
|
3970
|
-
: (result.error as any).errors;
|
|
3971
|
-
|
|
3972
|
-
const pathErrors = errors.filter(
|
|
3973
|
-
(error: any) =>
|
|
3974
|
-
JSON.stringify(error.path) === JSON.stringify(path)
|
|
3975
|
-
);
|
|
3976
|
-
|
|
3977
|
-
if (pathErrors.length > 0) {
|
|
3978
|
-
setShadowMetadata(stateKey, path, {
|
|
3979
|
-
...currentMeta,
|
|
3980
|
-
validation: {
|
|
3981
|
-
status: 'INVALID',
|
|
3982
|
-
errors: [
|
|
3983
|
-
{
|
|
3984
|
-
source: 'client',
|
|
3985
|
-
message: pathErrors[0]?.message,
|
|
3986
|
-
severity: 'warning', // Gentle error during typing
|
|
3987
|
-
},
|
|
3988
|
-
],
|
|
3989
|
-
lastValidated: Date.now(),
|
|
3990
|
-
validatedValue: newValue,
|
|
3991
|
-
},
|
|
3992
|
-
});
|
|
3993
|
-
} else {
|
|
3994
|
-
setShadowMetadata(stateKey, path, {
|
|
3995
|
-
...currentMeta,
|
|
3996
|
-
validation: {
|
|
3997
|
-
status: 'VALID',
|
|
3998
|
-
errors: [],
|
|
3999
|
-
lastValidated: Date.now(),
|
|
4000
|
-
validatedValue: newValue,
|
|
4001
|
-
},
|
|
4002
|
-
});
|
|
4003
|
-
}
|
|
4004
|
-
} else {
|
|
4005
|
-
setShadowMetadata(stateKey, path, {
|
|
4006
|
-
...currentMeta,
|
|
4007
|
-
validation: {
|
|
4008
|
-
status: 'VALID',
|
|
4009
|
-
errors: [],
|
|
4010
|
-
lastValidated: Date.now(),
|
|
4011
|
-
validatedValue: newValue,
|
|
4012
|
-
},
|
|
4013
|
-
});
|
|
4014
|
-
}
|
|
4015
|
-
}
|
|
4016
|
-
}, debounceTime);
|
|
4017
|
-
forceUpdate({});
|
|
4018
|
-
},
|
|
4019
|
-
[setState, path, formOpts?.debounceTime, stateKey]
|
|
4020
|
-
);
|
|
4021
|
-
|
|
4022
|
-
// --- NEW onBlur HANDLER ---
|
|
4023
|
-
// This replaces the old commented-out method with a modern approach.
|
|
4024
|
-
const handleBlur = useCallback(async () => {
|
|
4025
|
-
console.log('handleBlur triggered');
|
|
4026
|
-
|
|
4027
|
-
// Commit any pending changes
|
|
4028
|
-
if (debounceTimeoutRef.current) {
|
|
4029
|
-
clearTimeout(debounceTimeoutRef.current);
|
|
4030
|
-
debounceTimeoutRef.current = null;
|
|
4031
|
-
isCurrentlyDebouncing.current = false;
|
|
4032
|
-
setState(localValue, path, { updateType: 'update' });
|
|
4033
|
-
}
|
|
4034
|
-
const rootMeta = getShadowMetadata(stateKey, []);
|
|
4035
|
-
if (!rootMeta?.features?.validationEnabled) return;
|
|
4036
|
-
const { getInitialOptions } = getGlobalStore.getState();
|
|
4037
|
-
const validationOptions = getInitialOptions(stateKey)?.validation;
|
|
4038
|
-
const zodSchema =
|
|
4039
|
-
validationOptions?.zodSchemaV4 || validationOptions?.zodSchemaV3;
|
|
4040
|
-
|
|
4041
|
-
if (!zodSchema) return;
|
|
4042
|
-
|
|
4043
|
-
// Get the full path including stateKey
|
|
4044
|
-
|
|
4045
|
-
// Update validation state to "validating"
|
|
4046
|
-
const currentMeta = getShadowMetadata(stateKey, path);
|
|
4047
|
-
|
|
4048
|
-
setShadowMetadata(stateKey, path, {
|
|
4049
|
-
...currentMeta,
|
|
4050
|
-
validation: {
|
|
4051
|
-
status: 'VALIDATING',
|
|
4052
|
-
errors: [],
|
|
4053
|
-
lastValidated: Date.now(),
|
|
4054
|
-
validatedValue: localValue,
|
|
4055
|
-
},
|
|
4056
|
-
});
|
|
4057
|
-
|
|
4058
|
-
// Validate full state
|
|
4059
|
-
const fullState = getShadowValue(stateKey, []);
|
|
4060
|
-
const result = zodSchema.safeParse(fullState);
|
|
4061
|
-
console.log('result ', result);
|
|
4062
|
-
if (!result.success) {
|
|
4063
|
-
const errors =
|
|
4064
|
-
'issues' in result.error
|
|
4065
|
-
? result.error.issues
|
|
4066
|
-
: (result.error as any).errors;
|
|
4067
|
-
|
|
4068
|
-
console.log('All validation errors:', errors);
|
|
4069
|
-
console.log('Current blur path:', path);
|
|
4070
|
-
|
|
4071
|
-
// Find errors for this specific path
|
|
4072
|
-
const pathErrors = errors.filter((error: any) => {
|
|
4073
|
-
console.log('Processing error:', error);
|
|
4074
|
-
|
|
4075
|
-
// For array paths, we need to translate indices to ULIDs
|
|
4076
|
-
if (path.some((p) => p.startsWith('id:'))) {
|
|
4077
|
-
console.log('Detected array path with ULID');
|
|
4078
|
-
|
|
4079
|
-
// This is an array item path like ["id:xyz", "name"]
|
|
4080
|
-
const parentPath = path[0]!.startsWith('id:')
|
|
4081
|
-
? []
|
|
4082
|
-
: path.slice(0, -1);
|
|
4083
|
-
|
|
4084
|
-
console.log('Parent path:', parentPath);
|
|
4085
|
-
|
|
4086
|
-
const arrayMeta = getGlobalStore
|
|
4087
|
-
.getState()
|
|
4088
|
-
.getShadowMetadata(stateKey, parentPath);
|
|
4089
|
-
|
|
4090
|
-
console.log('Array metadata:', arrayMeta);
|
|
4091
|
-
|
|
4092
|
-
if (arrayMeta?.arrayKeys) {
|
|
4093
|
-
const itemKey = [stateKey, ...path.slice(0, -1)].join('.');
|
|
4094
|
-
const itemIndex = arrayMeta.arrayKeys.indexOf(itemKey);
|
|
4095
|
-
|
|
4096
|
-
console.log('Item key:', itemKey, 'Index:', itemIndex);
|
|
4097
|
-
|
|
4098
|
-
// Compare with Zod path
|
|
4099
|
-
const zodPath = [...parentPath, itemIndex, ...path.slice(-1)];
|
|
4100
|
-
const match =
|
|
4101
|
-
JSON.stringify(error.path) === JSON.stringify(zodPath);
|
|
4102
|
-
|
|
4103
|
-
console.log('Zod path comparison:', {
|
|
4104
|
-
zodPath,
|
|
4105
|
-
errorPath: error.path,
|
|
4106
|
-
match,
|
|
4107
|
-
});
|
|
4108
|
-
return match;
|
|
4109
|
-
}
|
|
4110
|
-
}
|
|
4111
|
-
|
|
4112
|
-
const directMatch = JSON.stringify(error.path) === JSON.stringify(path);
|
|
4113
|
-
console.log('Direct path comparison:', {
|
|
4114
|
-
errorPath: error.path,
|
|
4115
|
-
currentPath: path,
|
|
4116
|
-
match: directMatch,
|
|
4117
|
-
});
|
|
4118
|
-
return directMatch;
|
|
4119
|
-
});
|
|
4120
|
-
|
|
4121
|
-
console.log('Filtered path errors:', pathErrors);
|
|
4122
|
-
// Update shadow metadata with validation result
|
|
4123
|
-
setShadowMetadata(stateKey, path, {
|
|
4124
|
-
...currentMeta,
|
|
4125
|
-
validation: {
|
|
4126
|
-
status: 'INVALID',
|
|
4127
|
-
errors: pathErrors.map((err: any) => ({
|
|
4128
|
-
source: 'client' as const,
|
|
4129
|
-
message: err.message,
|
|
4130
|
-
severity: 'error' as const, // Hard error on blur
|
|
4131
|
-
})),
|
|
4132
|
-
lastValidated: Date.now(),
|
|
4133
|
-
validatedValue: localValue,
|
|
4134
|
-
},
|
|
4135
|
-
});
|
|
4136
|
-
} else {
|
|
4137
|
-
// Validation passed
|
|
4138
|
-
setShadowMetadata(stateKey, path, {
|
|
4139
|
-
...currentMeta,
|
|
4140
|
-
validation: {
|
|
4141
|
-
status: 'VALID',
|
|
4142
|
-
errors: [],
|
|
4143
|
-
lastValidated: Date.now(),
|
|
4144
|
-
validatedValue: localValue,
|
|
4145
|
-
},
|
|
4146
|
-
});
|
|
4147
|
-
}
|
|
4148
|
-
forceUpdate({});
|
|
4149
|
-
}, [stateKey, path, localValue, setState]);
|
|
4150
|
-
|
|
4151
|
-
const baseState = rebuildStateShape({
|
|
4152
|
-
path: path,
|
|
4153
|
-
componentId: componentId,
|
|
4154
|
-
});
|
|
4155
|
-
|
|
4156
|
-
const stateWithInputProps = new Proxy(baseState, {
|
|
4157
|
-
get(target, prop) {
|
|
4158
|
-
if (prop === 'inputProps') {
|
|
4159
|
-
return {
|
|
4160
|
-
value: localValue ?? '',
|
|
4161
|
-
onChange: (e: any) => {
|
|
4162
|
-
debouncedUpdate(e.target.value);
|
|
4163
|
-
},
|
|
4164
|
-
// 5. Wire the new onBlur handler to the input props.
|
|
4165
|
-
onBlur: handleBlur,
|
|
4166
|
-
ref: formRefStore
|
|
4167
|
-
.getState()
|
|
4168
|
-
.getFormRef(stateKey + '.' + path.join('.')),
|
|
4169
|
-
};
|
|
4170
|
-
}
|
|
4171
|
-
|
|
4172
|
-
return target[prop];
|
|
4173
|
-
},
|
|
4174
|
-
});
|
|
4175
|
-
|
|
4176
|
-
return (
|
|
4177
|
-
<ValidationWrapper formOpts={formOpts} path={path} stateKey={stateKey}>
|
|
4178
|
-
{renderFn(stateWithInputProps)}
|
|
4179
|
-
</ValidationWrapper>
|
|
4180
|
-
);
|
|
4181
|
-
}
|
|
4182
|
-
function useRegisterComponent(
|
|
4183
|
-
stateKey: string,
|
|
4184
|
-
componentId: string,
|
|
4185
|
-
forceUpdate: (o: object) => void
|
|
4186
|
-
) {
|
|
4187
|
-
const fullComponentId = `${stateKey}////${componentId}`;
|
|
4188
|
-
|
|
4189
|
-
useLayoutEffect(() => {
|
|
4190
|
-
// Call the safe, centralized function to register
|
|
4191
|
-
registerComponent(stateKey, fullComponentId, {
|
|
4192
|
-
forceUpdate: () => forceUpdate({}),
|
|
4193
|
-
paths: new Set(),
|
|
4194
|
-
reactiveType: ['component'],
|
|
4195
|
-
});
|
|
4196
|
-
|
|
4197
|
-
// The cleanup now calls the safe, centralized unregister function
|
|
4198
|
-
return () => {
|
|
4199
|
-
unregisterComponent(stateKey, fullComponentId);
|
|
4200
|
-
};
|
|
4201
|
-
}, [stateKey, fullComponentId]); // Dependencies are stable and correct
|
|
4202
|
-
}
|