mirror of
https://github.com/block/goose.git
synced 2026-07-17 12:56:20 +02:00
fix search highlighting in sessions list view
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
import React, { useEffect, useState, useRef, useMemo } from 'react';
|
||||
import React, { useEffect, useState, useRef, useCallback } from 'react';
|
||||
import { MessageSquareText, Target, AlertCircle, Calendar, Folder } from 'lucide-react';
|
||||
import { VariableSizeList as List } from 'react-window';
|
||||
import { fetchSessions, type Session } from '../../sessions';
|
||||
import { Card } from '../ui/card';
|
||||
import { Button } from '../ui/button';
|
||||
@@ -22,13 +21,6 @@ interface SessionListViewProps {
|
||||
onSelectSession: (sessionId: string) => void;
|
||||
}
|
||||
|
||||
// Flattened item structure for virtualization
|
||||
interface FlattenedItem {
|
||||
type: 'header' | 'session';
|
||||
data: DateGroup | Session;
|
||||
index: number;
|
||||
}
|
||||
|
||||
const SessionListView: React.FC<SessionListViewProps> = ({ onSelectSession }) => {
|
||||
const [sessions, setSessions] = useState<Session[]>([]);
|
||||
const [filteredSessions, setFilteredSessions] = useState<Session[]>([]);
|
||||
@@ -43,37 +35,29 @@ const SessionListView: React.FC<SessionListViewProps> = ({ onSelectSession }) =>
|
||||
currentIndex: number;
|
||||
} | null>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const listRef = useRef<List>(null);
|
||||
|
||||
// Flatten date groups into a single array for virtualization
|
||||
const flattenedItems = useMemo((): FlattenedItem[] => {
|
||||
const items: FlattenedItem[] = [];
|
||||
let index = 0;
|
||||
|
||||
dateGroups.forEach((group) => {
|
||||
// Add header
|
||||
items.push({
|
||||
type: 'header',
|
||||
data: group,
|
||||
index: index++,
|
||||
});
|
||||
|
||||
// Add sessions
|
||||
group.sessions.forEach((session) => {
|
||||
items.push({
|
||||
type: 'session',
|
||||
data: session,
|
||||
index: index++,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return items;
|
||||
}, [dateGroups]);
|
||||
const loadSessions = useCallback(async () => {
|
||||
setIsLoading(true);
|
||||
setShowSkeleton(true);
|
||||
setShowContent(false);
|
||||
setError(null);
|
||||
try {
|
||||
const sessions = await fetchSessions();
|
||||
setSessions(sessions);
|
||||
setFilteredSessions(sessions);
|
||||
} catch (err) {
|
||||
console.error('Failed to load sessions:', err);
|
||||
setError('Failed to load sessions. Please try again later.');
|
||||
setSessions([]);
|
||||
setFilteredSessions([]);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
loadSessions();
|
||||
}, []);
|
||||
}, [loadSessions]);
|
||||
|
||||
// Timing logic to prevent flicker between skeleton and content on initial load
|
||||
useEffect(() => {
|
||||
@@ -100,13 +84,6 @@ const SessionListView: React.FC<SessionListViewProps> = ({ onSelectSession }) =>
|
||||
}
|
||||
}, [filteredSessions]);
|
||||
|
||||
// Reset list cache when items change
|
||||
useEffect(() => {
|
||||
if (listRef.current) {
|
||||
listRef.current.resetAfterIndex(0);
|
||||
}
|
||||
}, [flattenedItems]);
|
||||
|
||||
// Filter sessions when search term or case sensitivity changes
|
||||
const handleSearch = (term: string, caseSensitive: boolean) => {
|
||||
if (!term) {
|
||||
@@ -140,25 +117,6 @@ const SessionListView: React.FC<SessionListViewProps> = ({ onSelectSession }) =>
|
||||
setSearchResults(filtered.length > 0 ? { count: filtered.length, currentIndex: 1 } : null);
|
||||
};
|
||||
|
||||
const loadSessions = async () => {
|
||||
setIsLoading(true);
|
||||
setShowSkeleton(true);
|
||||
setShowContent(false);
|
||||
setError(null);
|
||||
try {
|
||||
const sessions = await fetchSessions();
|
||||
setSessions(sessions);
|
||||
setFilteredSessions(sessions);
|
||||
} catch (err) {
|
||||
console.error('Failed to load sessions:', err);
|
||||
setError('Failed to load sessions. Please try again later.');
|
||||
setSessions([]);
|
||||
setFilteredSessions([]);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle search result navigation
|
||||
const handleSearchNavigation = (direction: 'next' | 'prev') => {
|
||||
if (!searchResults || filteredSessions.length === 0) return;
|
||||
@@ -325,71 +283,68 @@ const SessionListView: React.FC<SessionListViewProps> = ({ onSelectSession }) =>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-h-0 relative px-8">
|
||||
<ScrollArea className="h-full">
|
||||
{/* Skeleton layer - always rendered but conditionally visible */}
|
||||
<div
|
||||
className={`absolute inset-0 transition-opacity duration-300 ${
|
||||
isLoading || showSkeleton ? 'opacity-100 z-10' : 'opacity-0 z-0 pointer-events-none'
|
||||
}`}
|
||||
>
|
||||
<ScrollArea className="h-full" data-search-scroll-area>
|
||||
<div ref={containerRef} className="h-full relative">
|
||||
<SearchView
|
||||
onSearch={handleSearch}
|
||||
onNavigate={handleSearchNavigation}
|
||||
searchResults={searchResults}
|
||||
className="relative"
|
||||
>
|
||||
<div className="space-y-8">
|
||||
{/* Today section */}
|
||||
<div className="space-y-4">
|
||||
<Skeleton className="h-6 w-16" />
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-4">
|
||||
<SessionSkeleton variant={0} />
|
||||
<SessionSkeleton variant={1} />
|
||||
<SessionSkeleton variant={2} />
|
||||
<SessionSkeleton variant={3} />
|
||||
<SessionSkeleton variant={0} />
|
||||
{/* Skeleton layer - always rendered but conditionally visible */}
|
||||
<div
|
||||
className={`absolute inset-0 transition-opacity duration-300 ${
|
||||
isLoading || showSkeleton
|
||||
? 'opacity-100 z-10'
|
||||
: 'opacity-0 z-0 pointer-events-none'
|
||||
}`}
|
||||
>
|
||||
<div className="space-y-8">
|
||||
{/* Today section */}
|
||||
<div className="space-y-4">
|
||||
<Skeleton className="h-6 w-16" />
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-4">
|
||||
<SessionSkeleton variant={0} />
|
||||
<SessionSkeleton variant={1} />
|
||||
<SessionSkeleton variant={2} />
|
||||
<SessionSkeleton variant={3} />
|
||||
<SessionSkeleton variant={0} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Yesterday section */}
|
||||
<div className="space-y-4">
|
||||
<Skeleton className="h-6 w-20" />
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-4">
|
||||
<SessionSkeleton variant={1} />
|
||||
<SessionSkeleton variant={2} />
|
||||
<SessionSkeleton variant={3} />
|
||||
<SessionSkeleton variant={0} />
|
||||
<SessionSkeleton variant={1} />
|
||||
<SessionSkeleton variant={2} />
|
||||
{/* Yesterday section */}
|
||||
<div className="space-y-4">
|
||||
<Skeleton className="h-6 w-20" />
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-4">
|
||||
<SessionSkeleton variant={1} />
|
||||
<SessionSkeleton variant={2} />
|
||||
<SessionSkeleton variant={3} />
|
||||
<SessionSkeleton variant={0} />
|
||||
<SessionSkeleton variant={1} />
|
||||
<SessionSkeleton variant={2} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Additional section */}
|
||||
<div className="space-y-4">
|
||||
<Skeleton className="h-6 w-24" />
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-4">
|
||||
<SessionSkeleton variant={3} />
|
||||
<SessionSkeleton variant={0} />
|
||||
<SessionSkeleton variant={1} />
|
||||
{/* Additional section */}
|
||||
<div className="space-y-4">
|
||||
<Skeleton className="h-6 w-24" />
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-4">
|
||||
<SessionSkeleton variant={3} />
|
||||
<SessionSkeleton variant={0} />
|
||||
<SessionSkeleton variant={1} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SearchView>
|
||||
</div>
|
||||
|
||||
{/* Content layer - always rendered but conditionally visible */}
|
||||
<div
|
||||
className={`relative transition-opacity duration-300 ${
|
||||
showContent ? 'opacity-100 z-10' : 'opacity-0 z-0'
|
||||
}`}
|
||||
>
|
||||
<SearchView
|
||||
onSearch={handleSearch}
|
||||
onNavigate={handleSearchNavigation}
|
||||
searchResults={searchResults}
|
||||
className="relative"
|
||||
>
|
||||
{renderActualContent()}
|
||||
{/* Content layer - always rendered but conditionally visible */}
|
||||
<div
|
||||
className={`relative transition-opacity duration-300 ${
|
||||
showContent ? 'opacity-100 z-10' : 'opacity-0 z-0'
|
||||
}`}
|
||||
>
|
||||
{renderActualContent()}
|
||||
</div>
|
||||
</SearchView>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
|
||||
@@ -29,7 +29,7 @@ export class SearchHighlighter {
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 1;
|
||||
z-index: 9999;
|
||||
`;
|
||||
|
||||
// Find scroll container (look for our custom data attribute first, then fallback to radix)
|
||||
@@ -151,8 +151,9 @@ export class SearchHighlighter {
|
||||
const scrollLeft = this.scrollContainer?.scrollLeft || 0;
|
||||
|
||||
// Calculate the highlight position relative to the scroll container
|
||||
const top = rect.top + scrollTop - containerRect.top;
|
||||
const left = rect.left + scrollLeft - containerRect.left;
|
||||
// rect.top/left are already relative to viewport, so we need to adjust for scroll container position
|
||||
const top = rect.top - containerRect.top + scrollTop;
|
||||
const left = rect.left - containerRect.left + scrollLeft;
|
||||
|
||||
highlightRect.style.cssText = `
|
||||
position: absolute;
|
||||
|
||||
Reference in New Issue
Block a user