- {/* Add skeleton loading state */}
- {flatItems.length === 0 && datamodelView.loading && (
+
+ {/* Show skeleton loading state only when initially loading */}
+ {flatItems.length === 0 && datamodelView.loading && (!search || search.length < 3) && (
{[...Array(5)].map((_, i) => (
@@ -227,6 +249,16 @@ export const List = ({ }: IListProps) => {
))}
)}
+
+ {/* Show no results message when searching but no items found */}
+ {flatItems.length === 0 && search && search.length >= 3 && (
+
+
No tables found
+
+ No attributes match your search for "{search}"
+
+
+ )}
{/* Virtualized list */}
{
height: `${rowVirtualizer.getTotalSize()}px`,
width: '100%',
position: 'relative',
- visibility: flatItems.length === 0 && datamodelView.loading ? 'hidden' : 'visible'
+ visibility: flatItems.length === 0 ? 'hidden' : 'visible'
}}
>
{rowVirtualizer.getVirtualItems().map((virtualItem) => {
diff --git a/Website/components/datamodelview/Relationships.tsx b/Website/components/datamodelview/Relationships.tsx
index 38cfe0c..b8782a1 100644
--- a/Website/components/datamodelview/Relationships.tsx
+++ b/Website/components/datamodelview/Relationships.tsx
@@ -65,6 +65,17 @@ export const Relationships = ({ entity, onVisibleCountChange, search = "" }: IRe
)
}
+ // Also filter by parent search prop if provided
+ if (search && search.length >= 3) {
+ const query = search.toLowerCase()
+ filteredRelationships = filteredRelationships.filter(rel =>
+ rel.Name.toLowerCase().includes(query) ||
+ rel.TableSchema.toLowerCase().includes(query) ||
+ rel.LookupDisplayName.toLowerCase().includes(query) ||
+ rel.RelationshipSchema.toLowerCase().includes(query)
+ )
+ }
+
if (!sortColumn || !sortDirection) return filteredRelationships
return [...filteredRelationships].sort((a, b) => {
@@ -118,6 +129,7 @@ export const Relationships = ({ entity, onVisibleCountChange, search = "" }: IRe
]
const sortedRelationships = getSortedRelationships();
+ const highlightTerm = searchQuery || search; // Use internal search or parent search for highlighting
React.useEffect(() => {
if (onVisibleCountChange) {
@@ -126,41 +138,65 @@ export const Relationships = ({ entity, onVisibleCountChange, search = "" }: IRe
}, [onVisibleCountChange, sortedRelationships.length]);
return <>
-
-
-
-
setSearchQuery(e.target.value)}
- className="pl-6 h-8 text-xs md:pl-8 md:h-10 md:text-sm"
- />
+
+
+
+
+ setSearchQuery(e.target.value)}
+ onKeyDown={(e) => {
+ if (e.key === 'Escape') {
+ setSearchQuery("")
+ }
+ }}
+ className="pl-6 pr-8 h-8 text-xs md:pl-8 md:pr-10 md:h-10 md:text-sm"
+ />
+ {searchQuery && (
+
+ )}
+
+
+ {(searchQuery || typeFilter !== "all") && (
+
+ )}
-
- {(searchQuery || typeFilter !== "all") && (
-
+ {search && search.length >= 3 && searchQuery && (
+
+
+ Warning: Global search "{search}" is also active
+
)}
@@ -252,7 +288,7 @@ export const Relationships = ({ entity, onVisibleCountChange, search = "" }: IRe
}`}
>
- {highlightMatch(relationship.Name, search)}
+ {highlightMatch(relationship.Name, highlightTerm)}
{relationship.LookupDisplayName}
diff --git a/Website/components/datamodelview/TimeSlicedSearch.tsx b/Website/components/datamodelview/TimeSlicedSearch.tsx
index 88f2dbd..009a6d6 100644
--- a/Website/components/datamodelview/TimeSlicedSearch.tsx
+++ b/Website/components/datamodelview/TimeSlicedSearch.tsx
@@ -30,6 +30,7 @@ export const TimeSlicedSearch = ({
const [localValue, setLocalValue] = useState('');
const [isTyping, setIsTyping] = useState(false);
const [portalRoot, setPortalRoot] = useState
(null);
+ const [lastValidSearch, setLastValidSearch] = useState('');
const { isOpen } = useSidebar();
const isMobile = useIsMobile();
@@ -46,11 +47,28 @@ export const TimeSlicedSearch = ({
clearTimeout(searchTimeoutRef.current);
}
+ // If we're going from a valid search to an invalid one, clear the search
+ if (value.length < 3 && lastValidSearch.length >= 3) {
+ onSearch('');
+ setLastValidSearch('');
+ setIsTyping(false);
+ onLoadingChange(false);
+ return;
+ }
+
+ // Don't search if less than 3 characters
+ if (value.length < 3) {
+ setIsTyping(false);
+ onLoadingChange(false);
+ return;
+ }
+
searchTimeoutRef.current = window.setTimeout(() => {
// Use MessageChannel for immediate callback without blocking main thread
const channel = new MessageChannel();
channel.port2.onmessage = () => {
onSearch(value);
+ setLastValidSearch(value);
// Reset typing state in next frame
frameRef.current = requestAnimationFrame(() => {
@@ -59,7 +77,7 @@ export const TimeSlicedSearch = ({
};
channel.port1.postMessage(null);
}, 350);
- }, [onSearch]);
+ }, [onSearch, onLoadingChange, lastValidSearch]);
const handleChange = useCallback((e: React.ChangeEvent) => {
const value = e.target.value;
@@ -67,24 +85,36 @@ export const TimeSlicedSearch = ({
// Immediate visual update (highest priority)
setLocalValue(value);
- // Manage typing state
- if (!isTyping) {
- setIsTyping(true);
- onLoadingChange(true);
- }
+ // Only manage typing state and loading for searches >= 3 characters
+ if (value.length >= 3) {
+ // Manage typing state
+ if (!isTyping) {
+ setIsTyping(true);
+ onLoadingChange(true);
+ }
- // Reset typing timeout
- if (typingTimeoutRef.current) {
- clearTimeout(typingTimeoutRef.current);
+ // Reset typing timeout
+ if (typingTimeoutRef.current) {
+ clearTimeout(typingTimeoutRef.current);
+ }
+
+ // Auto-reset typing state if user stops typing
+ typingTimeoutRef.current = window.setTimeout(() => {
+ setIsTyping(false);
+ }, 2000);
+ } else {
+ // Clear typing state for short searches
+ setIsTyping(false);
+ onLoadingChange(false);
+
+ // Clear any pending timeouts
+ if (typingTimeoutRef.current) {
+ clearTimeout(typingTimeoutRef.current);
+ }
}
- // Schedule search
+ // Schedule search (will handle short searches internally)
scheduleSearch(value);
-
- // Auto-reset typing state if user stops typing
- typingTimeoutRef.current = window.setTimeout(() => {
- setIsTyping(false);
- }, 2000);
}, [isTyping, onLoadingChange, scheduleSearch]);
@@ -106,7 +136,13 @@ export const TimeSlicedSearch = ({
// Handle keyboard navigation
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
- if (e.key === 'Enter' && !e.shiftKey) {
+ 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) {
@@ -125,7 +161,7 @@ export const TimeSlicedSearch = ({
e.preventDefault();
onNavigatePrevious?.();
}
- }, [onNavigateNext, onNavigatePrevious]);
+ }, [onNavigateNext, onNavigatePrevious, onSearch, onLoadingChange]);
const hasResults = totalResults > 0;
const showNavigation = hasResults && localValue.length >= 3;
@@ -193,7 +229,7 @@ export const TimeSlicedSearch = ({
/>
{/* Clear button or loading indicator */}
- {isTyping ? (
+ {isTyping && localValue.length >= 3 ? (
) : localValue ? (