cogsbox-state 0.5.465 → 0.5.466
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 +1011 -1270
- 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 -5
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +261 -219
- package/dist/store.js.map +1 -1
- package/package.json +1 -1
- package/src/CogsState.tsx +151 -707
- package/src/Components.tsx +541 -0
- package/src/store.ts +178 -141
- 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.
|
|
@@ -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,33 @@ 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
|
-
|
|
2723
|
+
const arrayPathKey = path.length > 0 ? path.join('.') : 'root';
|
|
2782
2724
|
|
|
2783
|
-
const validIds =
|
|
2725
|
+
const validIds = useMemo(() => {
|
|
2726
|
+
return applyTransforms(stateKey, path, meta);
|
|
2727
|
+
}, [
|
|
2784
2728
|
stateKey,
|
|
2785
|
-
path,
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2729
|
+
path.join('.'),
|
|
2730
|
+
// Only recalculate if the underlying array keys or transforms change
|
|
2731
|
+
getShadowMetadata(stateKey, path)?.arrayKeys,
|
|
2732
|
+
meta?.transforms,
|
|
2733
|
+
]);
|
|
2734
|
+
|
|
2735
|
+
// Memoize the updated meta to prevent creating new objects on every render
|
|
2736
|
+
const updatedMeta = useMemo(() => {
|
|
2737
|
+
return {
|
|
2738
|
+
...meta,
|
|
2739
|
+
arrayViews: {
|
|
2740
|
+
...(meta?.arrayViews || {}),
|
|
2741
|
+
[arrayPathKey]: validIds,
|
|
2742
|
+
},
|
|
2743
|
+
};
|
|
2744
|
+
}, [meta, arrayPathKey, validIds]);
|
|
2797
2745
|
|
|
2798
2746
|
// Now use the updated meta when getting array data
|
|
2799
2747
|
const { value: arrayValues } = getArrayData(
|
|
@@ -2802,15 +2750,10 @@ function createProxyHandler<T>(
|
|
|
2802
2750
|
updatedMeta
|
|
2803
2751
|
);
|
|
2804
2752
|
|
|
2805
|
-
console.log('validIds', validIds);
|
|
2806
|
-
console.log('arrayValues', arrayValues);
|
|
2807
|
-
|
|
2808
2753
|
useEffect(() => {
|
|
2809
2754
|
const unsubscribe = getGlobalStore
|
|
2810
2755
|
.getState()
|
|
2811
2756
|
.subscribeToPath(stateKeyPathKey, (e) => {
|
|
2812
|
-
// A data change has occurred for the source array.
|
|
2813
|
-
console.log('changed array statelist ', e);
|
|
2814
2757
|
if (e.type === 'GET_SELECTED') {
|
|
2815
2758
|
return;
|
|
2816
2759
|
}
|
|
@@ -2832,8 +2775,11 @@ function createProxyHandler<T>(
|
|
|
2832
2775
|
|
|
2833
2776
|
if (
|
|
2834
2777
|
e.type === 'INSERT' ||
|
|
2778
|
+
e.type === 'INSERT_MANY' ||
|
|
2835
2779
|
e.type === 'REMOVE' ||
|
|
2836
|
-
e.type === 'CLEAR_SELECTION'
|
|
2780
|
+
e.type === 'CLEAR_SELECTION' ||
|
|
2781
|
+
(e.type === 'SERVER_STATE_UPDATE' &&
|
|
2782
|
+
!meta?.serverStateIsUpStream)
|
|
2837
2783
|
) {
|
|
2838
2784
|
forceUpdate({});
|
|
2839
2785
|
}
|
|
@@ -2856,38 +2802,35 @@ function createProxyHandler<T>(
|
|
|
2856
2802
|
componentId: componentId!,
|
|
2857
2803
|
meta: updatedMeta, // Use updated meta here
|
|
2858
2804
|
});
|
|
2859
|
-
console.log('arrayValues', arrayValues);
|
|
2860
2805
|
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
{arrayValues.map((item, localIndex) => {
|
|
2864
|
-
const itemKey = validIds[localIndex];
|
|
2806
|
+
const returnValue = arrayValues.map((item, localIndex) => {
|
|
2807
|
+
const itemKey = validIds[localIndex];
|
|
2865
2808
|
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2809
|
+
if (!itemKey) {
|
|
2810
|
+
return null;
|
|
2811
|
+
}
|
|
2869
2812
|
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2813
|
+
let itemComponentId = componentIdsRef.current.get(itemKey);
|
|
2814
|
+
if (!itemComponentId) {
|
|
2815
|
+
itemComponentId = uuidv4();
|
|
2816
|
+
componentIdsRef.current.set(itemKey, itemComponentId);
|
|
2817
|
+
}
|
|
2875
2818
|
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2819
|
+
const itemPath = [...path, itemKey];
|
|
2820
|
+
|
|
2821
|
+
return createElement(MemoizedCogsItemWrapper, {
|
|
2822
|
+
key: itemKey,
|
|
2823
|
+
stateKey,
|
|
2824
|
+
itemComponentId,
|
|
2825
|
+
itemPath,
|
|
2826
|
+
localIndex,
|
|
2827
|
+
arraySetter,
|
|
2828
|
+
rebuildStateShape,
|
|
2829
|
+
renderFn: callbackfn,
|
|
2830
|
+
});
|
|
2831
|
+
});
|
|
2832
|
+
|
|
2833
|
+
return <>{returnValue}</>;
|
|
2891
2834
|
};
|
|
2892
2835
|
|
|
2893
2836
|
return <StateListWrapper />;
|
|
@@ -2896,7 +2839,7 @@ function createProxyHandler<T>(
|
|
|
2896
2839
|
if (prop === 'stateFlattenOn') {
|
|
2897
2840
|
return (fieldName: string) => {
|
|
2898
2841
|
// FIX: Get the definitive list of IDs for the current view from meta.arrayViews.
|
|
2899
|
-
const arrayPathKey = path.join('.');
|
|
2842
|
+
const arrayPathKey = path.length > 0 ? path.join('.') : 'root';
|
|
2900
2843
|
const viewIds = meta?.arrayViews?.[arrayPathKey];
|
|
2901
2844
|
|
|
2902
2845
|
const currentState = getGlobalStore
|
|
@@ -2916,7 +2859,7 @@ function createProxyHandler<T>(
|
|
|
2916
2859
|
}
|
|
2917
2860
|
if (prop === 'index') {
|
|
2918
2861
|
return (index: number) => {
|
|
2919
|
-
const arrayPathKey = path.join('.');
|
|
2862
|
+
const arrayPathKey = path.length > 0 ? path.join('.') : 'root';
|
|
2920
2863
|
const viewIds = meta?.arrayViews?.[arrayPathKey];
|
|
2921
2864
|
|
|
2922
2865
|
if (viewIds) {
|
|
@@ -2945,26 +2888,17 @@ function createProxyHandler<T>(
|
|
|
2945
2888
|
}
|
|
2946
2889
|
if (prop === 'last') {
|
|
2947
2890
|
return () => {
|
|
2948
|
-
// ✅ FIX: Use getArrayData to get the keys for the current view (filtered or not).
|
|
2949
2891
|
const { keys: currentViewIds } = getArrayData(stateKey, path, meta);
|
|
2950
|
-
|
|
2951
|
-
// If the array is empty, there is no last item.
|
|
2952
2892
|
if (!currentViewIds || currentViewIds.length === 0) {
|
|
2953
2893
|
return undefined;
|
|
2954
2894
|
}
|
|
2955
|
-
|
|
2956
|
-
// Get the unique ID of the last item in the current view.
|
|
2957
2895
|
const lastItemKey = currentViewIds[currentViewIds.length - 1];
|
|
2958
2896
|
|
|
2959
|
-
// If for some reason the key is invalid, return undefined.
|
|
2960
2897
|
if (!lastItemKey) {
|
|
2961
2898
|
return undefined;
|
|
2962
2899
|
}
|
|
2963
|
-
|
|
2964
|
-
// ✅ FIX: The new path uses the item's unique key, not its numerical index.
|
|
2965
2900
|
const newPath = [...path, lastItemKey];
|
|
2966
2901
|
|
|
2967
|
-
// Return a new proxy scoped to that specific item.
|
|
2968
2902
|
return rebuildStateShape({
|
|
2969
2903
|
path: newPath,
|
|
2970
2904
|
componentId: componentId!,
|
|
@@ -3059,6 +2993,7 @@ function createProxyHandler<T>(
|
|
|
3059
2993
|
return;
|
|
3060
2994
|
}
|
|
3061
2995
|
const selectedId = selectedItemKey.split('.').pop() as string;
|
|
2996
|
+
|
|
3062
2997
|
if (!(currentViewIds as any[]).includes(selectedId!)) {
|
|
3063
2998
|
return;
|
|
3064
2999
|
}
|
|
@@ -3129,17 +3064,14 @@ function createProxyHandler<T>(
|
|
|
3129
3064
|
(item) => item?.[searchKey] === searchValue
|
|
3130
3065
|
);
|
|
3131
3066
|
|
|
3132
|
-
// FIX: If found, return a proxy to the item by appending its key to the current path.
|
|
3133
3067
|
if (found) {
|
|
3134
3068
|
return rebuildStateShape({
|
|
3135
|
-
path: [...path, found.key],
|
|
3069
|
+
path: [...path, found.key],
|
|
3136
3070
|
componentId: componentId!,
|
|
3137
3071
|
meta,
|
|
3138
3072
|
});
|
|
3139
3073
|
}
|
|
3140
3074
|
|
|
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
3075
|
return rebuildStateShape({
|
|
3144
3076
|
path: [...path, `not_found_${uuidv4()}`],
|
|
3145
3077
|
componentId: componentId!,
|
|
@@ -3149,7 +3081,8 @@ function createProxyHandler<T>(
|
|
|
3149
3081
|
}
|
|
3150
3082
|
if (prop === 'cutThis') {
|
|
3151
3083
|
const { value: shadowValue } = getScopedData(stateKey, path, meta);
|
|
3152
|
-
|
|
3084
|
+
const parentPath = path.slice(0, -1);
|
|
3085
|
+
notifySelectionComponents(stateKey, parentPath);
|
|
3153
3086
|
return () => {
|
|
3154
3087
|
effectiveSetState(shadowValue, path, { updateType: 'cut' });
|
|
3155
3088
|
};
|
|
@@ -3172,7 +3105,6 @@ function createProxyHandler<T>(
|
|
|
3172
3105
|
_meta: meta,
|
|
3173
3106
|
});
|
|
3174
3107
|
}
|
|
3175
|
-
// in CogsState.ts -> createProxyHandler -> handler -> get
|
|
3176
3108
|
|
|
3177
3109
|
if (prop === '$get') {
|
|
3178
3110
|
return () =>
|
|
@@ -3190,7 +3122,6 @@ function createProxyHandler<T>(
|
|
|
3190
3122
|
const parentPathArray = path.slice(0, -1);
|
|
3191
3123
|
const parentMeta = getShadowMetadata(stateKey, parentPathArray);
|
|
3192
3124
|
|
|
3193
|
-
// FIX: Check if the parent is an array by looking for arrayKeys in its metadata.
|
|
3194
3125
|
if (parentMeta?.arrayKeys) {
|
|
3195
3126
|
const fullParentKey = stateKey + '.' + parentPathArray.join('.');
|
|
3196
3127
|
const selectedItemKey = getGlobalStore
|
|
@@ -3199,14 +3130,11 @@ function createProxyHandler<T>(
|
|
|
3199
3130
|
|
|
3200
3131
|
const fullItemKey = stateKey + '.' + path.join('.');
|
|
3201
3132
|
|
|
3202
|
-
// Logic remains the same.
|
|
3203
|
-
notifySelectionComponents(stateKey, parentPathArray, undefined);
|
|
3204
3133
|
return selectedItemKey === fullItemKey;
|
|
3205
3134
|
}
|
|
3206
3135
|
return undefined;
|
|
3207
3136
|
}
|
|
3208
3137
|
|
|
3209
|
-
// Then use it in both:
|
|
3210
3138
|
if (prop === 'setSelected') {
|
|
3211
3139
|
return (value: boolean) => {
|
|
3212
3140
|
const parentPath = path.slice(0, -1);
|
|
@@ -3246,6 +3174,7 @@ function createProxyHandler<T>(
|
|
|
3246
3174
|
.getState()
|
|
3247
3175
|
.setSelectedIndex(fullParentKey, fullItemKey);
|
|
3248
3176
|
}
|
|
3177
|
+
notifySelectionComponents(stateKey, parentPath);
|
|
3249
3178
|
};
|
|
3250
3179
|
}
|
|
3251
3180
|
if (prop === '_componentId') {
|
|
@@ -3423,17 +3352,10 @@ function createProxyHandler<T>(
|
|
|
3423
3352
|
if (prop === '_stateKey') return stateKey;
|
|
3424
3353
|
if (prop === '_path') return path;
|
|
3425
3354
|
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
3355
|
return (payload: UpdateArg<T>) => {
|
|
3430
|
-
|
|
3431
|
-
// this operation in the batch for efficient processing.
|
|
3356
|
+
console.log('udpating', payload, path);
|
|
3432
3357
|
effectiveSetState(payload as any, path, { updateType: 'update' });
|
|
3433
3358
|
|
|
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
3359
|
return {
|
|
3438
3360
|
synced: () => {
|
|
3439
3361
|
const shadowMeta = getGlobalStore
|
|
@@ -3628,7 +3550,8 @@ function SignalRenderer({
|
|
|
3628
3550
|
const instanceIdRef = useRef<string | null>(null);
|
|
3629
3551
|
const isSetupRef = useRef(false);
|
|
3630
3552
|
const signalId = `${proxy._stateKey}-${proxy._path.join('.')}`;
|
|
3631
|
-
|
|
3553
|
+
|
|
3554
|
+
const arrayPathKey = proxy._path.length > 0 ? proxy._path.join('.') : 'root';
|
|
3632
3555
|
const viewIds = proxy._meta?.arrayViews?.[arrayPathKey];
|
|
3633
3556
|
|
|
3634
3557
|
const value = getShadowValue(proxy._stateKey, proxy._path, viewIds);
|
|
@@ -3721,482 +3644,3 @@ function SignalRenderer({
|
|
|
3721
3644
|
'data-signal-id': signalId,
|
|
3722
3645
|
});
|
|
3723
3646
|
}
|
|
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
|
-
}
|