diff --git a/Website/components/datamodelview/DatamodelView.tsx b/Website/components/datamodelview/DatamodelView.tsx index afa64af..b28b7e5 100644 --- a/Website/components/datamodelview/DatamodelView.tsx +++ b/Website/components/datamodelview/DatamodelView.tsx @@ -27,7 +27,7 @@ export function DatamodelView() { } function DatamodelViewContent() { - const { scrollToSection } = useDatamodelView(); + const { scrollToSection, restoreSection } = useDatamodelView(); const datamodelDispatch = useDatamodelViewDispatch(); const { groups, filtered } = useDatamodelData(); const datamodelDataDispatch = useDatamodelDataDispatch(); @@ -47,6 +47,8 @@ function DatamodelViewContent() { } else { // Clear search - reset to show all groups datamodelDataDispatch({ type: "SET_FILTERED", payload: [] }); + // Relocate section + restoreSection(); } } updateURL({ query: { globalsearch: searchValue.length >= 3 ? searchValue : "" } }) @@ -210,7 +212,7 @@ function DatamodelViewContent() { currentIndex={currentSearchIndex} totalResults={totalResults} /> - + ); diff --git a/Website/components/datamodelview/List.tsx b/Website/components/datamodelview/List.tsx index 05e7565..7770d34 100644 --- a/Website/components/datamodelview/List.tsx +++ b/Website/components/datamodelview/List.tsx @@ -1,16 +1,17 @@ import { useEffect, useMemo, useRef, useCallback, useState } from "react"; import { useDatamodelView, useDatamodelViewDispatch } from "@/contexts/DatamodelViewContext"; import React from "react"; -import { useVirtualizer } from '@tanstack/react-virtual'; +import { elementScroll, useVirtualizer, VirtualizerOptions } from '@tanstack/react-virtual'; import { Section } from "./Section"; import { useDatamodelData } from "@/contexts/DatamodelDataContext"; import { AttributeType, EntityType, GroupType } from "@/lib/Types"; import { updateURL } from "@/lib/url-utils"; import { copyToClipboard, generateGroupLink } from "@/lib/clipboard-utils"; import { useSnackbar } from "@/contexts/SnackbarContext"; -import { Tooltip } from '@mui/material'; +import { debounce, Tooltip } from '@mui/material'; interface IListProps { + setCurrentIndex: (index: number) => void; } // Helper to highlight search matches @@ -21,31 +22,20 @@ export function highlightMatch(text: string, search: string) { return <>{text.slice(0, idx)}{text.slice(idx, idx + search.length)}{text.slice(idx + search.length)}; } -export const List = ({ }: IListProps) => { +export const List = ({ setCurrentIndex }: IListProps) => { const dispatch = useDatamodelViewDispatch(); - const datamodelView = useDatamodelView(); - const [isScrollingToSection, setIsScrollingToSection] = useState(false); + const { currentSection, loading } = useDatamodelView(); const { groups, filtered, search } = useDatamodelData(); const { showSnackbar } = useSnackbar(); const parentRef = useRef(null); - const lastScrollHandleTime = useRef(0); const scrollTimeoutRef = useRef(); - const sectionRefs = useRef<{ [key: string]: HTMLDivElement | null }>({}); + const scrollingRef = React.useRef() + // used to relocate section after search/filter + const [sectionVirtualItem, setSectionVirtualItem] = useState(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; - }; - - const remeasureSection = (schemaName: string) => { - const el = sectionRefs.current[schemaName]; - if (el) rowVirtualizer.measureElement(el); - }; - const handleCopyGroupLink = useCallback(async (groupName: string) => { const link = generateGroupLink(groupName); const success = await copyToClipboard(link); @@ -89,35 +79,107 @@ export const List = ({ }: IListProps) => { return items; }, [filtered, search, groups]); + function easeInOutQuint(t: number) { + return t < 0.5 ? 16 * t * t * t * t * t : 1 + 16 * --t * t * t * t * t + } + + const scrollToFn: VirtualizerOptions['scrollToFn'] = + React.useCallback((offset, canSmooth, instance) => { + const duration = 2000 + const start = parentRef.current?.scrollTop || 0 + const startTime = (scrollingRef.current = Date.now()) + + const run = () => { + if (scrollingRef.current !== startTime) return + const now = Date.now() + const elapsed = now - startTime + const progress = easeInOutQuint(Math.min(elapsed / duration, 1)) + const interpolated = start + (offset - start) * progress + + if (elapsed < duration) { + elementScroll(interpolated, canSmooth, instance) + requestAnimationFrame(run) + } else { + elementScroll(interpolated, canSmooth, instance) + } + } + + requestAnimationFrame(run) + }, []) + + const debouncedOnChange = debounce((instance, sync) => { + if (!sync) { + dispatch({ type: 'SET_LOADING_SECTION', payload: null }); + } + + const virtualItems = instance.getVirtualItems(); + if (virtualItems.length === 0) return; + + const scrollOffset = instance.scrollOffset; + const scrollRect = instance.scrollRect; + if (!scrollOffset || !scrollRect) return; + + const viewportTop = scrollOffset; + const viewportBottom = scrollOffset + scrollRect.height; + + let mostVisibleEntity: { + entity: EntityType; + group: GroupType; + index: number; + visibleArea: number; + } | null = null; + + let actualIndex = 0; + for (const vi of virtualItems) { + const item = flatItems[vi.index]; + if (!item || item.type !== 'entity') continue; + actualIndex++; + + const itemTop = vi.start; + const itemBottom = vi.end; + + // Calculate intersection + const intersectionTop = Math.max(itemTop, viewportTop); + const intersectionBottom = Math.min(itemBottom, viewportBottom); + + // Skip if no intersection + if (intersectionTop >= intersectionBottom) continue; + + const visibleArea = intersectionBottom - intersectionTop; + + // Update most visible entity without array operations + if (!mostVisibleEntity || visibleArea > mostVisibleEntity.visibleArea) { + mostVisibleEntity = { + entity: item.entity, + group: item.group, + index: actualIndex, + visibleArea + }; + } + } + + if (mostVisibleEntity && currentSection !== mostVisibleEntity.entity.SchemaName) { + setSectionVirtualItem(mostVisibleEntity.entity.SchemaName); + updateURL({ query: { group: mostVisibleEntity.group.Name, section: mostVisibleEntity.entity.SchemaName } }); + dispatch({ type: "SET_CURRENT_GROUP", payload: mostVisibleEntity.group.Name }); + dispatch({ type: "SET_CURRENT_SECTION", payload: mostVisibleEntity.entity.SchemaName }); + setCurrentIndex(mostVisibleEntity.index); + } + }, 100); + const rowVirtualizer = useVirtualizer({ count: flatItems.length, getScrollElement: () => parentRef.current, - overscan: 5, // Reduce overscan to improve performance + overscan: 5, estimateSize: (index) => { const item = flatItems[index]; - if (!item) return 100; - return item.type === 'group' ? 92 : 300; - }, - // Override scroll behavior to prevent jumping during tab switches - scrollToFn: (offset) => { - // 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) { - scrollElement.scrollTop = offset; - } + if (!item) return 200; + return item.type === 'group' ? 100 : 500; }, + scrollToFn, + onChange: debouncedOnChange, }); - + const scrollToSection = useCallback((sectionId: string) => { if (scrollTimeoutRef.current) { clearTimeout(scrollTimeoutRef.current); @@ -132,51 +194,11 @@ export const List = ({ }: IListProps) => { return; } - const currentIndex = rowVirtualizer.getVirtualItems()[0]?.index || 0; - const isLargeJump = Math.abs(sectionIndex - currentIndex) > 10; - - if (isLargeJump) { - setIsScrollingToSection(true); - } - - scrollTimeoutRef.current = setTimeout(() => { - if (!rowVirtualizer || sectionIndex >= flatItems.length) { - console.warn(`Invalid index ${sectionIndex} for section ${sectionId}`); - setIsScrollingToSection(false); - return; - } - - try { - isIntentionalScroll.current = true; // Mark this as intentional scroll - rowVirtualizer.scrollToIndex(sectionIndex, { - align: 'start' - }); - - setTimeout(() => { - setIsScrollingToSection(false); - dispatch({ type: 'SET_LOADING_SECTION', payload: null }); - // Reset intentional scroll flag after scroll is complete - setTimeout(() => { - isIntentionalScroll.current = false; - }, 100); - }, 500); - } catch (error) { - console.warn(`Failed to scroll to section ${sectionId}:`, error); - - const estimatedOffset = sectionIndex * 300; - if (parentRef.current) { - isIntentionalScroll.current = true; - parentRef.current.scrollTop = estimatedOffset; - // Reset flags for fallback scroll - setTimeout(() => { - isIntentionalScroll.current = false; - }, 600); - } - setIsScrollingToSection(false); - } - }, 20); + rowVirtualizer.scrollToIndex(sectionIndex, { + align: 'start' + }); - }, [flatItems, rowVirtualizer]); + }, [flatItems]); const scrollToGroup = useCallback((groupName: string) => { if (scrollTimeoutRef.current) { @@ -192,164 +214,21 @@ export const List = ({ }: IListProps) => { return; } - const currentIndex = rowVirtualizer.getVirtualItems()[0]?.index || 0; - const isLargeJump = Math.abs(groupIndex - currentIndex) > 10; - - if (isLargeJump) { - setIsScrollingToSection(true); - } - - scrollTimeoutRef.current = setTimeout(() => { - if (!rowVirtualizer || groupIndex >= flatItems.length) { - console.warn(`Invalid index ${groupIndex} for group ${groupName}`); - setIsScrollingToSection(false); - return; - } - - try { - isIntentionalScroll.current = true; // Mark this as intentional scroll - rowVirtualizer.scrollToIndex(groupIndex, { - align: 'start' - }); - - setTimeout(() => { - setIsScrollingToSection(false); - // Reset intentional scroll flag after scroll is complete - setTimeout(() => { - isIntentionalScroll.current = false; - }, 100); - }, 500); - } catch (error) { - console.warn(`Failed to scroll to group ${groupName}:`, error); - - const estimatedOffset = groupIndex * 300; - if (parentRef.current) { - isIntentionalScroll.current = true; - parentRef.current.scrollTop = estimatedOffset; - // Reset flags for fallback scroll - setTimeout(() => { - isIntentionalScroll.current = false; - }, 600); - } - setIsScrollingToSection(false); - } - }, 20); - }, [flatItems, rowVirtualizer]); - - useEffect(() => { - // Only measure if we're not filtering - let the virtualizer handle filtered states naturally - if (!search || search.length < 3) { - requestAnimationFrame(() => { - rowVirtualizer.measure(); - }); - } - }, [flatItems, search, rowVirtualizer]); - - // Handle scrolling to top when starting a search - const prevSearchLengthRef = useRef(search.length); - useEffect(() => { - const currentSearchLength = search.length; - const prevSearchLength = prevSearchLengthRef.current; - - // 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, datamodelView.currentSection, flatItems, rowVirtualizer]); - - // Throttled scroll handler to reduce calculations - const handleScroll = useCallback(() => { - const now = Date.now(); - if (now - lastScrollHandleTime.current < 100) return; // Only process every 100ms - lastScrollHandleTime.current = now; - - const scrollElement = parentRef.current; - if (!scrollElement || isScrollingToSection || isIntentionalScroll.current) return; - - const scrollOffset = scrollElement.scrollTop; - const virtualItems = rowVirtualizer.getVirtualItems(); - - // Find the first visible item - const padding = 32; - const firstVisibleItem = virtualItems.find(v => { - return (v.start - padding) <= scrollOffset && v.end >= (scrollOffset + padding); + rowVirtualizer.scrollToIndex(groupIndex, { + align: 'start' }); - - if (firstVisibleItem) { - const item = flatItems[firstVisibleItem.index]; - if (item?.type === 'entity') { - if (item.entity.SchemaName !== datamodelView.currentSection) { - updateURL({ query: { group: item.group.Name, section: item.entity.SchemaName } }); - dispatch({ type: "SET_CURRENT_GROUP", payload: item.group.Name }); - dispatch({ type: "SET_CURRENT_SECTION", payload: item.entity.SchemaName }); - } - } - } - }, [dispatch, flatItems, rowVirtualizer, datamodelView.currentSection, isScrollingToSection]); + }, [flatItems]); - // Throttled scroll event listener - useEffect(() => { - const scrollElement = parentRef.current; - if (!scrollElement) return; - - let scrollTimeout: number; - const throttledScrollHandler = () => { - if (scrollTimeout) return; - scrollTimeout = window.setTimeout(() => { - handleScroll(); - scrollTimeout = 0; - }, 100); - }; - - scrollElement.addEventListener("scroll", throttledScrollHandler, { passive: true }); - return () => { - scrollElement.removeEventListener("scroll", throttledScrollHandler); - clearTimeout(scrollTimeout); - }; - }, [handleScroll]); + const restoreSection = useCallback(() => { + if (sectionVirtualItem) { + scrollToSection(sectionVirtualItem); + } + }, [sectionVirtualItem]); useEffect(() => { dispatch({ type: 'SET_SCROLL_TO_SECTION', payload: scrollToSection }); dispatch({ type: 'SET_SCROLL_TO_GROUP', payload: scrollToGroup }); + dispatch({ type: 'SET_RESTORE_SECTION', payload: restoreSection }); return () => { if (scrollTimeoutRef.current) { @@ -358,25 +237,21 @@ export const List = ({ }: IListProps) => { }; }, [dispatch, scrollToSection, scrollToGroup]); - useEffect(() => { - // When the current section is in view, set loading to false - if (datamodelView.currentSection) { - // Check if the current section is rendered in the virtualizer - const isInView = rowVirtualizer.getVirtualItems().some(vi => { - const item = flatItems[vi.index]; - return item.type === 'entity' && item.entity.SchemaName === datamodelView.currentSection; - }); - if (isInView) { - dispatch({ type: 'SET_LOADING', payload: false }); + // Callback to handle section content changes (for tab switches, expansions, etc.) + const handleSectionResize = useCallback((index: number) => { + if (index !== -1) { + const containerElement = document.querySelector(`[data-index="${index}"]`) as HTMLElement; + if (containerElement) { + rowVirtualizer.measureElement(containerElement); } } - }, [datamodelView.currentSection, flatItems, rowVirtualizer, dispatch]); + }, [rowVirtualizer]); return (
{/* Show skeleton loading state only when initially loading */} - {flatItems.length === 0 && datamodelView.loading && (!search || search.length < 3) && ( + {flatItems.length === 0 && loading && (!search || search.length < 3) && (
{[...Array(5)].map((_, i) => (
@@ -413,62 +288,61 @@ export const List = ({ }: IListProps) => { }} > {rowVirtualizer.getVirtualItems().map((virtualItem) => { - const item = flatItems[virtualItem.index]; - const sectionRef = item.type === 'entity' ? getSectionRefCallback(item.entity.SchemaName) : undefined; + const item = flatItems[virtualItem.index]; - return ( -
{ - if (sectionRef) sectionRef(el); - if (el) rowVirtualizer.measureElement(el); - } - : rowVirtualizer.measureElement - } - style={{ - position: 'absolute', - top: 0, - left: 0, - width: '100%', - transform: `translateY(${virtualItem.start}px)`, - }} - > - {item.type === 'group' ? ( -
-
- -
handleCopyGroupLink(item.group.Name)} - > - {item.group.Name} -
-
-
-
- ) : ( -
-
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} - /> -
- )} -
- ); + return ( +
{ + if (el) { + // trigger remeasurement when content changes and load + requestAnimationFrame(() => { + handleSectionResize(virtualItem.index); + }); + } + }} + > + {item.type === 'group' ? ( +
+
+ +
handleCopyGroupLink(item.group.Name)} + > + {item.group.Name} +
+
+
+
+ ) : ( +
+
{ + 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/SidebarDatamodelView.tsx b/Website/components/datamodelview/SidebarDatamodelView.tsx index b849ebd..ebd7503 100644 --- a/Website/components/datamodelview/SidebarDatamodelView.tsx +++ b/Website/components/datamodelview/SidebarDatamodelView.tsx @@ -27,7 +27,7 @@ export const SidebarDatamodelView = ({ }: ISidebarDatamodelViewProps) => { const dataModelDispatch = useDatamodelViewDispatch(); - const { groups } = useDatamodelData(); + const { groups, filtered, search } = useDatamodelData(); const [searchTerm, setSearchTerm] = useState(""); const [displaySearchTerm, setDisplaySearchTerm] = useState(""); @@ -35,16 +35,17 @@ export const SidebarDatamodelView = ({ }: ISidebarDatamodelViewProps) => { // Memoize search results to prevent recalculation on every render const filteredGroups = useMemo(() => { - if (!searchTerm.trim()) return groups; + if (!searchTerm.trim() && !search) return groups; return groups.map(group => ({ ...group, Entities: group.Entities.filter(entity => - entity.SchemaName.toLowerCase().includes(searchTerm.toLowerCase()) || - entity.DisplayName.toLowerCase().includes(searchTerm.toLowerCase()) + (entity.SchemaName.toLowerCase().includes(searchTerm.toLowerCase()) || + entity.DisplayName.toLowerCase().includes(searchTerm.toLowerCase())) && + filtered.some(f => f.type === 'entity' && f.entity.SchemaName === entity.SchemaName) ) })).filter(group => group.Entities.length > 0); - }, [groups, searchTerm]); + }, [groups, searchTerm, filtered]); // Debounced search to reduce performance impact useEffect(() => { diff --git a/Website/components/datamodelview/TimeSlicedSearch.tsx b/Website/components/datamodelview/TimeSlicedSearch.tsx index 634473e..aaa5247 100644 --- a/Website/components/datamodelview/TimeSlicedSearch.tsx +++ b/Website/components/datamodelview/TimeSlicedSearch.tsx @@ -5,8 +5,8 @@ import { createPortal } from 'react-dom'; import { useSidebar } from '@/contexts/SidebarContext'; import { useSettings } from '@/contexts/SettingsContext'; import { useIsMobile } from '@/hooks/use-mobile'; -import { Box, IconButton, InputAdornment, TextField } from '@mui/material'; -import { CloseRounded, SearchRounded } from '@mui/icons-material'; +import { Box, CircularProgress, Divider, IconButton, InputAdornment, InputBase, ListItemIcon, ListItemText, Menu, MenuItem, MenuList, Paper, Typography } from '@mui/material'; +import { ClearRounded, InfoRounded, KeyboardArrowDownRounded, KeyboardArrowUpRounded, NavigateBeforeRounded, NavigateNextRounded, SearchRounded } from '@mui/icons-material'; interface TimeSlicedSearchProps { onSearch: (value: string) => void; @@ -26,6 +26,8 @@ export const TimeSlicedSearch = ({ onNavigateNext, onNavigatePrevious, initialLocalValue, + currentIndex, + totalResults, placeholder = "Search attributes...", }: TimeSlicedSearchProps) => { const [localValue, setLocalValue] = useState(initialLocalValue); @@ -43,6 +45,39 @@ export const TimeSlicedSearch = ({ // Hide search on mobile when sidebar is open, or when settings are open const shouldHideSearch = (isMobile && isOpen) || isSettingsOpen; + useEffect(() => { + const handleGlobalKeyDown = (e: KeyboardEvent) => { + if (localValue.length === 0) return; + if (e.key === 'Escape') { + e.preventDefault(); + setLocalValue(''); + onSearch(''); // Only clear when explicitly using ESC + setIsTyping(false); + onLoadingChange(false); + } else if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + onNavigateNext?.(); + if ('vibrate' in navigator) { + navigator.vibrate(50); + } + } else if (e.key === 'Enter' && e.shiftKey) { + e.preventDefault(); + onNavigatePrevious?.(); + if ('vibrate' in navigator) { + navigator.vibrate(50); + } + } else if (e.key === 'ArrowDown' && e.ctrlKey) { + e.preventDefault(); + onNavigateNext?.(); + } else if (e.key === 'ArrowUp' && e.ctrlKey) { + e.preventDefault(); + onNavigatePrevious?.(); + } + }; + window.addEventListener('keydown', handleGlobalKeyDown); + return () => window.removeEventListener('keydown', handleGlobalKeyDown); + }, [localValue, onSearch, onLoadingChange, onNavigateNext, onNavigatePrevious]); + // Time-sliced debouncing using requestAnimationFrame const scheduleSearch = useCallback((value: string) => { if (searchTimeoutRef.current) { @@ -122,6 +157,7 @@ export const TimeSlicedSearch = ({ // Handle clear button const handleClear = useCallback(() => { + if (localValue.length === 0) return; // No-op if already empty setLocalValue(''); onSearch(''); // Clear search immediately setIsTyping(false); @@ -136,35 +172,6 @@ export const TimeSlicedSearch = ({ } }, [onSearch, onLoadingChange]); - // Handle keyboard navigation - const handleKeyDown = useCallback((e: React.KeyboardEvent) => { - if (e.key === 'Escape') { - e.preventDefault(); - setLocalValue(''); - onSearch(''); // Only clear when explicitly using ESC - setIsTyping(false); - onLoadingChange(false); - } else if (e.key === 'Enter' && !e.shiftKey) { - e.preventDefault(); - onNavigateNext?.(); - if ('vibrate' in navigator) { - navigator.vibrate(50); - } - } else if (e.key === 'Enter' && e.shiftKey) { - e.preventDefault(); - onNavigatePrevious?.(); - if ('vibrate' in navigator) { - navigator.vibrate(50); - } - } else if (e.key === 'ArrowDown' && e.ctrlKey) { - e.preventDefault(); - onNavigateNext?.(); - } else if (e.key === 'ArrowUp' && e.ctrlKey) { - e.preventDefault(); - onNavigatePrevious?.(); - } - }, [onNavigateNext, onNavigatePrevious, onSearch, onLoadingChange]); - // Cleanup useEffect(() => { return () => { @@ -208,53 +215,104 @@ export const TimeSlicedSearch = ({ }; }, []); + const [anchorEl, setAnchorEl] = React.useState(null); + const open = Boolean(anchorEl); + const handleClick = (event: React.MouseEvent) => { + if (open) { + setAnchorEl(null); + } else { + setAnchorEl(event.currentTarget); + } + }; + const handleClose = () => { + setAnchorEl(null); + }; + const searchInput = ( - - , - endAdornment: ( - - {isTyping && localValue.length >= 3 ? ( - - ) : localValue ? ( - - - - ) : null} - - ) - } - }} - /> + + + + + + + + + + + + {isTyping && localValue.length >= 3 ? ( + + ) : localValue ? ( + + {currentIndex} + {totalResults} + + ) : null} + + + + + + + + + + + + + + + + Next + Enter + + + + + + Previous + Shift + Enter + + + + + + Next + Ctrl + + + + + + + Previous + Ctrl + + + + + + + + Clear + Esc + + + ); diff --git a/Website/contexts/DatamodelDataContext.tsx b/Website/contexts/DatamodelDataContext.tsx index 62203f3..5072ddb 100644 --- a/Website/contexts/DatamodelDataContext.tsx +++ b/Website/contexts/DatamodelDataContext.tsx @@ -1,14 +1,17 @@ 'use client' import React, { createContext, useContext, useReducer, ReactNode } from "react"; -import { GroupType, SolutionWarningType } from "@/lib/Types"; +import { EntityType, GroupType, SolutionWarningType } from "@/lib/Types"; import { useSearchParams } from "next/navigation"; interface DatamodelDataState { groups: GroupType[]; warnings: SolutionWarningType[]; search: string; - filtered: any[]; + filtered: Array< + | { type: 'group'; group: GroupType } + | { type: 'entity'; group: GroupType; entity: EntityType } + >; } const initialState: DatamodelDataState = { diff --git a/Website/contexts/DatamodelViewContext.tsx b/Website/contexts/DatamodelViewContext.tsx index 3ab7765..97c540f 100644 --- a/Website/contexts/DatamodelViewContext.tsx +++ b/Website/contexts/DatamodelViewContext.tsx @@ -11,6 +11,7 @@ export interface DatamodelViewState { scrollToGroup: (groupName: string) => void; loading: boolean; loadingSection: string | null; + restoreSection: () => void; } const initialState: DatamodelViewState = { @@ -20,6 +21,7 @@ const initialState: DatamodelViewState = { scrollToGroup: () => { throw new Error("scrollToGroup not initialized yet!"); }, loading: true, loadingSection: null, + restoreSection: () => { throw new Error("restoreSection not initialized yet!"); }, } type DatamodelViewAction = @@ -29,6 +31,7 @@ type DatamodelViewAction = | { type: 'SET_SCROLL_TO_GROUP', payload: (groupName: string) => void } | { type: 'SET_LOADING', payload: boolean } | { type: 'SET_LOADING_SECTION', payload: string | null } + | { type: 'SET_RESTORE_SECTION', payload: () => void }; const datamodelViewReducer = (state: DatamodelViewState, action: DatamodelViewAction): DatamodelViewState => { @@ -45,6 +48,8 @@ const datamodelViewReducer = (state: DatamodelViewState, action: DatamodelViewAc return { ...state, loading: action.payload } case 'SET_LOADING_SECTION': return { ...state, loadingSection: action.payload } + case 'SET_RESTORE_SECTION': + return { ...state, restoreSection: action.payload } default: return state; }