diff --git a/Website/components/datamodelview/DatamodelView.tsx b/Website/components/datamodelview/DatamodelView.tsx index f570f7b..3115c48 100644 --- a/Website/components/datamodelview/DatamodelView.tsx +++ b/Website/components/datamodelview/DatamodelView.tsx @@ -34,6 +34,7 @@ function DatamodelViewContent() { const datamodelDataDispatch = useDatamodelDataDispatch(); const workerRef = useRef(null); const [currentSearchIndex, setCurrentSearchIndex] = useState(0); + const accumulatedResultsRef = useRef>([]); // Track all results during search // Calculate total search results const totalResults = filtered.length > 0 ? filtered.filter(item => item.type === 'entity').length : 0; @@ -114,20 +115,27 @@ function DatamodelViewContent() { // setSearchProgress(0); setCurrentSearchIndex(0); // Start with empty results to show loading state + accumulatedResultsRef.current = []; // Reset accumulated results datamodelDataDispatch({ type: "SET_FILTERED", payload: [] }); } else if (message.type === 'results') { // setSearchProgress(message.progress || 0); - // For chunked results, append to existing + // Accumulate results in ref for immediate access + accumulatedResultsRef.current = [...accumulatedResultsRef.current, ...message.data]; + + // For chunked results, always append to existing + datamodelDataDispatch({ type: "APPEND_FILTERED", payload: message.data }); + + // Only handle completion logic when all chunks are received if (message.complete) { - datamodelDataDispatch({ type: "SET_FILTERED", payload: message.data }); datamodelDispatch({ type: "SET_LOADING", payload: false }); // Set to first result if we have any and auto-navigate to it - const entityResults = message.data.filter((item: { type: string }) => item.type === 'entity'); - if (entityResults.length > 0) { + // Use accumulated results from ref for immediate access + const allFilteredResults = accumulatedResultsRef.current.filter((item: { type: string }) => item.type === 'entity'); + if (allFilteredResults.length > 0) { setCurrentSearchIndex(1); - const firstEntity = entityResults[0]; + const firstEntity = allFilteredResults[0]; datamodelDispatch({ type: "SET_CURRENT_SECTION", payload: firstEntity.entity.SchemaName }); datamodelDispatch({ type: "SET_CURRENT_GROUP", payload: firstEntity.group.Name }); // Small delay to ensure virtual list is ready @@ -135,8 +143,6 @@ function DatamodelViewContent() { scrollToSection(firstEntity.entity.SchemaName); }, 100); } - } else { - datamodelDataDispatch({ type: "APPEND_FILTERED", payload: message.data }); } } else { diff --git a/Website/components/datamodelview/List.tsx b/Website/components/datamodelview/List.tsx index 9d5d054..8a66f44 100644 --- a/Website/components/datamodelview/List.tsx +++ b/Website/components/datamodelview/List.tsx @@ -27,6 +27,11 @@ export const List = ({ }: IListProps) => { const scrollTimeoutRef = useRef(); const sectionRefs = useRef<{ [key: string]: HTMLDivElement | null }>({}); + // Track position before search for restoration + const positionBeforeSearch = useRef<{ section: string | null; scrollTop: number } | null>(null); + const isTabSwitching = useRef(false); + const isIntentionalScroll = useRef(false); + const getSectionRefCallback = (schemaName: string) => (el: HTMLDivElement | null) => { sectionRefs.current[schemaName] = el; }; @@ -78,6 +83,25 @@ export const List = ({ }: IListProps) => { if (!item) return 100; return item.type === 'group' ? 92 : 300; }, + // Override scroll behavior to prevent jumping during tab switches + scrollToFn: (offset, options) => { + // When switching tabs during search, don't change scroll position + if (isTabSwitching.current && !isIntentionalScroll.current) { + return; + } + + // Reset the intentional scroll flag after use + if (isIntentionalScroll.current) { + isIntentionalScroll.current = false; + } + + // Default scroll behavior for other cases + const scrollElement = parentRef.current; + if (scrollElement) { + console.log("setting scroll to: " + offset + " - " + options.adjustments) + scrollElement.scrollTop = offset; + } + }, }); const scrollToSection = useCallback((sectionId: string) => { @@ -109,6 +133,7 @@ export const List = ({ }: IListProps) => { } try { + isIntentionalScroll.current = true; // Mark this as intentional scroll rowVirtualizer.scrollToIndex(sectionIndex, { align: 'start' }); @@ -143,17 +168,50 @@ export const List = ({ }: IListProps) => { const currentSearchLength = search.length; const prevSearchLength = prevSearchLengthRef.current; - // If we just crossed from < 3 to >= 3 characters (starting a search) + // Store position before starting search (crossing from < 3 to >= 3 characters) if (prevSearchLength < 3 && currentSearchLength >= 3) { + positionBeforeSearch.current = { + section: datamodelView.currentSection, + scrollTop: parentRef.current?.scrollTop || 0 + }; + setTimeout(() => { if (parentRef.current) { parentRef.current.scrollTop = 0; } }, 50); // Small delay to ensure virtualizer has processed the new items } + // Restore position when stopping search (crossing from >= 3 to < 3 characters) + else if (prevSearchLength >= 3 && currentSearchLength < 3) { + if (positionBeforeSearch.current) { + const { section, scrollTop } = positionBeforeSearch.current; + + // Restore to the section where the user was before searching + if (section) { + setTimeout(() => { + const sectionIndex = flatItems.findIndex(item => + item.type === 'entity' && item.entity.SchemaName === section + ); + + if (sectionIndex !== -1) { + // Scroll to the section they were at before search + isIntentionalScroll.current = true; // Mark this as intentional scroll + rowVirtualizer.scrollToIndex(sectionIndex, { align: 'start' }); + } else { + // Fallback to original scroll position + if (parentRef.current) { + parentRef.current.scrollTop = scrollTop; + } + } + }, 100); // Delay to ensure flatItems is updated + } + + positionBeforeSearch.current = null; + } + } prevSearchLengthRef.current = currentSearchLength; - }, [search]); + }, [search, datamodelView.currentSection, flatItems, rowVirtualizer]); // Throttled scroll handler to reduce calculations const handleScroll = useCallback(() => { @@ -306,6 +364,15 @@ export const List = ({ }: IListProps) => { entity={item.entity} group={item.group} onContentChange={() => remeasureSection(item.entity.SchemaName)} + onTabChange={(isChanging: boolean) => { + isTabSwitching.current = isChanging; + if (isChanging) { + // Reset after a short delay to allow for the content change + setTimeout(() => { + isTabSwitching.current = false; + }, 100); + } + }} search={search} /> diff --git a/Website/components/datamodelview/Section.tsx b/Website/components/datamodelview/Section.tsx index 9536ec7..442560e 100644 --- a/Website/components/datamodelview/Section.tsx +++ b/Website/components/datamodelview/Section.tsx @@ -14,16 +14,25 @@ interface ISectionProps { entity: EntityType; group: GroupType; onContentChange?: () => void; + onTabChange?: (isChanging: boolean) => void; search?: string; } export const Section = React.memo( - ({ entity, group, onContentChange, search }: ISectionProps) => { + ({ entity, group, onContentChange, onTabChange, search }: ISectionProps) => { // Use useRef to track previous props for comparison const prevSearch = React.useRef(search); const [tab, setTab] = React.useState("attributes"); + // Handle tab changes to notify parent component + const handleTabChange = React.useCallback((value: string) => { + if (onTabChange) { + onTabChange(true); // Signal that tab switching is starting + } + setTab(value); + }, [onTabChange]); + // Only compute these counts when needed const visibleAttributeCount = React.useMemo(() => entity.Attributes.length, [entity.Attributes]); const visibleRelationshipCount = React.useMemo(() => entity.Relationships.length, [entity.Relationships]); @@ -51,7 +60,7 @@ export const Section = React.memo( )} - +