feat(assets): add dimension analysis and authorized user previews
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/push/woodpecker Pipeline was successful
Refactor asset drilldowns with combined dimension filters, parent summaries and progressive loading. Add the hidden /who read-only permission preview with scoped authorization, identity mapping and audit logging. Refine inventory counts and AI问数 presentation.
This commit is contained in:
@@ -0,0 +1,48 @@
|
||||
import { useCallback, useEffect, useMemo, useState, type RefObject } from 'react';
|
||||
|
||||
interface ProgressiveRowsOptions {
|
||||
total: number;
|
||||
rootRef: RefObject<HTMLElement | null>;
|
||||
resetKey: unknown;
|
||||
enabled?: boolean;
|
||||
batchSize?: number;
|
||||
}
|
||||
|
||||
/** Render a cumulative prefix, loading another batch near the actual scroll body's bottom. */
|
||||
export function useProgressiveRows({ total, rootRef, resetKey, enabled = true, batchSize = 40 }: ProgressiveRowsOptions) {
|
||||
const context = useMemo(() => ({ resetKey, enabled, batchSize }), [resetKey, enabled, batchSize]);
|
||||
const [progress, setProgress] = useState(() => ({ context, limit: batchSize }));
|
||||
const [sentinel, setSentinel] = useState<HTMLDivElement | null>(null);
|
||||
const visibleCount = Math.min(total, progress.context === context ? progress.limit : batchSize);
|
||||
const hasMore = visibleCount < total;
|
||||
const loadMore = useCallback(() => {
|
||||
if (!enabled) return;
|
||||
setProgress(previous => ({ context, limit: Math.min(total, (previous.context === context ? previous.limit : batchSize) + batchSize) }));
|
||||
}, [enabled, context, total, batchSize]);
|
||||
|
||||
useEffect(() => {
|
||||
if (enabled && rootRef.current) rootRef.current.scrollTop = 0;
|
||||
}, [context, enabled, rootRef]);
|
||||
|
||||
useEffect(() => {
|
||||
const root = rootRef.current;
|
||||
if (!enabled || !hasMore || !root || !sentinel || typeof IntersectionObserver === 'undefined') return;
|
||||
let active = true;
|
||||
let queued = false;
|
||||
let frame: number | undefined;
|
||||
const observer = new IntersectionObserver(entries => {
|
||||
if (!active || queued || !entries.some(entry => entry.isIntersecting)) return;
|
||||
queued = true;
|
||||
// One append per observer: let layout settle before deciding whether more rows fit.
|
||||
frame = requestAnimationFrame(loadMore);
|
||||
}, { root, rootMargin: '0px 0px 240px 0px', threshold: 0 });
|
||||
observer.observe(sentinel);
|
||||
return () => {
|
||||
active = false;
|
||||
observer.disconnect();
|
||||
if (frame !== undefined) cancelAnimationFrame(frame);
|
||||
};
|
||||
}, [enabled, hasMore, rootRef, sentinel, visibleCount, loadMore]);
|
||||
|
||||
return { visibleCount, total, hasMore, loadMore, sentinelRef: setSentinel };
|
||||
}
|
||||
Reference in New Issue
Block a user