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

+2 -2
View File
@@ -765,6 +765,7 @@ export function useCascaderLoader<T = unknown>({
const hasSearch = typeof onSearch === "function"
const hasResolve = typeof resolveValue === "function"
const trimmed = query.trim()
if ((!enabled || !trimmed) && search !== null) setSearch(null)
// Serialised so the effects key on CONTENT, not on the array identity.
const levelsKey = JSON.stringify(levels)
@@ -783,7 +784,6 @@ export function useCascaderLoader<T = unknown>({
if (!hasSearch) return undefined
if (!enabled || !trimmed) {
abortKey(SEARCH_KEY)
setSearch((prev) => (prev === null ? prev : null))
return undefined
}
const timer = setTimeout(() => runSearch(trimmed), searchDebounce)
@@ -863,4 +863,4 @@ export function useCascaderLoadState(
): CascaderLoadState | null {
const { loadStates } = useCascaderState()
return loadStates.get(parent ?? CASCADER_ROOT_KEY) ?? null
}
}
@@ -125,6 +125,7 @@ function useCascaderVirtualGutter(
React.useLayoutEffect(() => {
if (!contentElement) return
const measure = () => {
const styles = getComputedStyle(contentElement)
const block = Number.parseFloat(styles.paddingTop) || 0
const inline =
@@ -134,6 +135,10 @@ function useCascaderVirtualGutter(
? previous
: { block, inline }
)
}
const observer = new ResizeObserver(measure)
observer.observe(contentElement)
return () => observer.disconnect()
}, [contentElement])
return gutter
@@ -455,4 +460,4 @@ function CascaderVirtualColumnRows({
)
}
export { CascaderVirtualColumn, CascaderVirtualItems }
export { CascaderVirtualColumn, CascaderVirtualItems }
+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,
}
}