fix(ui): завершить основу таблиц и фильтров серверов

This commit is contained in:
Denozordec committed 2026-09-13 14:46:17 +07:00
1 parent f9a4a07996
commit 3d3c45e4d2
24 files changed
+385 -263

No files matched your search

+32 -23
View File
@@ -925,23 +925,30 @@ function Cascader<T>({
// Remembers every selected node, so the trigger keeps a label after it
// leaves the loaded set (or was never in it).
const labelCacheRef = React.useRef(new Map<string, CascaderNode<T>>())
const [labelCache, setLabelCache] = React.useState(() => new Map<string, CascaderNode<T>>())
// Populated during render, since the trigger reads it while rendering.
// Idempotent and prop-derived, so a discarded render is harmless.
for (const selected of selectedValues) {
if (selectedValues.some(selected => {
const node = index.byValue.get(selected)
if (node) labelCacheRef.current.set(selected, node)
return node && labelCache.get(selected) !== node
})) {
const nextCache = new Map(labelCache)
for (const selected of selectedValues) {
const node = index.byValue.get(selected)
if (node) nextCache.set(selected, node)
}
setLabelCache(nextCache)
}
const resolveNode = React.useCallback(
(nodeValue: string): CascaderNode<T> =>
index.byValue.get(nodeValue) ??
labelCacheRef.current.get(nodeValue) ?? {
labelCache.get(nodeValue) ?? {
value: nodeValue,
label: nodeValue,
},
[index]
[index, labelCache]
)
/* ------------------------------ derived view ----------------------------- */
@@ -1086,7 +1093,9 @@ function Cascader<T>({
const swapping = swap.active && swap.key === levelKey
React.useLayoutEffect(() => {
if (swap.active) setSwap((prev) => ({ ...prev, active: false }))
if (!swap.active) return
const frame = requestAnimationFrame(() => setSwap(prev => ({ ...prev, active: false })))
return () => cancelAnimationFrame(frame)
}, [swap.active])
/* ----------------------------- virtualization ---------------------------- */
@@ -1139,7 +1148,8 @@ function Cascader<T>({
React.useEffect(() => {
if (!virtualized) return
setVirtualSyncs((count) => count + 1)
const frame = requestAnimationFrame(() => setVirtualSyncs(count => count + 1))
return () => cancelAnimationFrame(frame)
}, [virtualized])
React.useEffect(() => {
@@ -1669,24 +1679,23 @@ function Cascader<T>({
const state = loadStates.get(pendingNavigation.value)
if (!state || state.loading || state.error) return
setPendingNavigation(null)
if (pendingNavigation.kind === "expand") {
toggleExpanded(pendingNavigation.value)
return
}
if (pendingNavigation.kind === "at") {
goToLevelAt(pendingNavigation.value, pendingNavigation.depth)
return
}
pushLevel(pendingNavigation.value)
}, [pendingNavigation, loadStates, toggleExpanded, goToLevelAt, pushLevel])
if (!open && !inline) return
// Navigation notifies the controlled parent; dispatch only after this
// commit and discard the intent if the popup/context changes first.
let cancelled = false
queueMicrotask(() => {
if (cancelled) return
setPendingNavigation(null)
if (pendingNavigation.kind === "expand") toggleExpanded(pendingNavigation.value)
else if (pendingNavigation.kind === "at") goToLevelAt(pendingNavigation.value, pendingNavigation.depth)
else pushLevel(pendingNavigation.value)
})
return () => { cancelled = true }
}, [pendingNavigation, loadStates, toggleExpanded, goToLevelAt, pushLevel, open, inline])
// A popup that closes mid-flight must not drill in when the answer arrives;
// the loader aborts the request, this drops the intent.
React.useEffect(() => {
if (!open && !inline) setPendingNavigation(null)
}, [open, inline])
if (!open && !inline && pendingNavigation !== null) setPendingNavigation(null)
/* ------------------------------- selection ------------------------------ */
@@ -2931,4 +2940,4 @@ export type {
CascaderItemState,
CascaderRenderContextValue,
CascaderStateContextValue,
}
}