{/* 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}
+
+
+
+
+
+
+
+
+
+
);
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;
}