refactor: collapse Flows nav, remove session recovery from library, fix race conditions
- Collapse "Guided Flows" + "Troubleshooting" sidebar items into single "Flow Library" entry - Remove incomplete session recovery and "Repeat Last" sections from TreeLibraryPage - Fix handleSearch race: now participates in shared loadTreesRequestId guard so stale search results can't overwrite newer filter results - Fix Sidebar refreshStats race: add statsRequestId ref to discard stale badge count responses Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -47,11 +47,15 @@ export function Sidebar() {
|
||||
const [flyoutIndex, setFlyoutIndex] = useState<string | null>(null)
|
||||
const flyoutTimeout = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||
const sidebarRef = useRef<HTMLElement>(null)
|
||||
const statsRequestId = useRef(0)
|
||||
|
||||
/* ── Stats fetching ───────────────────────────────── */
|
||||
|
||||
const refreshStats = useCallback(() => {
|
||||
sidebarApi.getStats().then(setStats).catch(() => {})
|
||||
const requestId = ++statsRequestId.current
|
||||
sidebarApi.getStats()
|
||||
.then(data => { if (requestId === statsRequestId.current) setStats(data) })
|
||||
.catch(() => {})
|
||||
}, [])
|
||||
|
||||
useEffect(() => { refreshStats() }, [location.pathname, refreshStats])
|
||||
@@ -84,8 +88,7 @@ export function Sidebar() {
|
||||
badge: stats?.tree_counts.total || undefined,
|
||||
matchPaths: ['/trees', '/flows', '/my-trees', '/step-library', '/review-queue'],
|
||||
children: [
|
||||
{ href: '/trees', label: 'Guided Flows', count: stats?.tree_counts.total || undefined },
|
||||
{ href: '/trees?type=troubleshooting', label: 'Troubleshooting', count: stats?.tree_counts.troubleshooting || undefined },
|
||||
{ href: '/trees', label: 'Flow Library', count: stats?.tree_counts.total || undefined },
|
||||
{ href: '/trees?type=procedural', label: 'Projects', count: stats?.tree_counts.procedural || undefined },
|
||||
{ href: '/step-library', label: 'Solutions Library' },
|
||||
{ href: '/review-queue', label: 'Review Queue' },
|
||||
@@ -123,12 +126,11 @@ export function Sidebar() {
|
||||
title: 'KNOWLEDGE',
|
||||
items: [
|
||||
{
|
||||
href: '/trees', icon: GitBranch, label: 'Guided Flows', shortLabel: 'Flows',
|
||||
href: '/trees', icon: GitBranch, label: 'Flow Library', shortLabel: 'Flows',
|
||||
badge: stats?.tree_counts.total || undefined,
|
||||
matchPaths: ['/trees', '/flows', '/my-trees'],
|
||||
children: [
|
||||
{ href: '/trees', label: 'All Flows' },
|
||||
{ href: '/trees?type=troubleshooting', label: 'Troubleshooting', count: stats?.tree_counts.troubleshooting || undefined },
|
||||
{ href: '/trees', label: 'Flow Library' },
|
||||
{ href: '/trees?type=procedural', label: 'Projects', count: stats?.tree_counts.procedural || undefined },
|
||||
],
|
||||
},
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import { useEffect, useState, useCallback, useRef } from 'react'
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom'
|
||||
import { X, RotateCcw, Play, FileUp } from 'lucide-react'
|
||||
import { X, FileUp } from 'lucide-react'
|
||||
import { PageMeta } from '@/components/common/PageMeta'
|
||||
import { Button } from '@/components/ui/Button'
|
||||
import { FlowIllustration } from '@/components/common/EmptyStateIllustrations'
|
||||
import { treesApi } from '@/api/trees'
|
||||
import { categoriesApi } from '@/api/categories'
|
||||
import { foldersApi } from '@/api/folders'
|
||||
import { sessionsApi } from '@/api/sessions'
|
||||
import type { TreeListItem, CategoryListItem, FolderListItem, Session, IntakeFormField } from '@/types'
|
||||
import type { TreeListItem, CategoryListItem, FolderListItem, IntakeFormField } from '@/types'
|
||||
import { FolderEditModal } from '@/components/library/FolderEditModal'
|
||||
import { ForkModal } from '@/components/library/ForkModal'
|
||||
import { ExportFlowModal } from '@/components/library/ExportFlowModal'
|
||||
@@ -21,8 +20,8 @@ import { TreeListView } from '@/components/library/TreeListView'
|
||||
import { TreeTableView } from '@/components/library/TreeTableView'
|
||||
import { ViewToggle } from '@/components/library/ViewToggle'
|
||||
import { SortDropdown } from '@/components/library/SortDropdown'
|
||||
import { cn, safeGetItem } from '@/lib/utils'
|
||||
import { getSessionResumePath, getTreeNavigatePath } from '@/lib/routing'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { getTreeNavigatePath } from '@/lib/routing'
|
||||
import { usePermissions } from '@/hooks/usePermissions'
|
||||
import { useUserPreferencesStore } from '@/store/userPreferencesStore'
|
||||
import { CreateFlowDropdown } from '@/components/common/CreateFlowDropdown'
|
||||
@@ -93,24 +92,6 @@ export function TreeLibraryPage() {
|
||||
|
||||
// AI builder state
|
||||
|
||||
|
||||
// Repeat Last Session
|
||||
const lastSessionData = (() => {
|
||||
const raw = safeGetItem('last-session')
|
||||
if (!raw) return null
|
||||
try { return JSON.parse(raw) as { tree_id: string; tree_name: string; client_name: string; ticket_number: string; tree_type?: string } }
|
||||
catch { return null }
|
||||
})()
|
||||
|
||||
// Incomplete sessions for auto-recovery
|
||||
const [incompleteSessions, setIncompleteSessions] = useState<Session[]>([])
|
||||
const [dismissedSessionIds, setDismissedSessionIds] = useState<Set<string>>(() => {
|
||||
try {
|
||||
const raw = sessionStorage.getItem('dismissed-sessions')
|
||||
return raw ? new Set(JSON.parse(raw) as string[]) : new Set()
|
||||
} catch { return new Set() }
|
||||
})
|
||||
|
||||
const loadFolders = useCallback(async () => {
|
||||
try {
|
||||
const foldersData = await foldersApi.list()
|
||||
@@ -120,30 +101,6 @@ export function TreeLibraryPage() {
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Load incomplete sessions on mount
|
||||
useEffect(() => {
|
||||
sessionsApi.list({ completed: false, size: 5 })
|
||||
.then(setIncompleteSessions)
|
||||
.catch((err) => console.error('Failed to load incomplete sessions:', err))
|
||||
}, [])
|
||||
|
||||
const dismissSession = (sessionId: string) => {
|
||||
const next = new Set(dismissedSessionIds)
|
||||
next.add(sessionId)
|
||||
setDismissedSessionIds(next)
|
||||
try { sessionStorage.setItem('dismissed-sessions', JSON.stringify([...next])) } catch { /* */ }
|
||||
}
|
||||
|
||||
const visibleIncompleteSessions = incompleteSessions.filter(s => !dismissedSessionIds.has(s.id))
|
||||
|
||||
const formatTimeAgo = (dateString: string) => {
|
||||
const diff = Math.floor((Date.now() - new Date(dateString).getTime()) / 1000)
|
||||
if (diff < 60) return 'just now'
|
||||
if (diff < 3600) return `${Math.floor(diff / 60)} min ago`
|
||||
if (diff < 86400) return `${Math.floor(diff / 3600)} hr ago`
|
||||
return `${Math.floor(diff / 86400)} days ago`
|
||||
}
|
||||
|
||||
// Load categories once on mount (they rarely change)
|
||||
useEffect(() => {
|
||||
categoriesApi.list()
|
||||
@@ -194,15 +151,18 @@ export function TreeLibraryPage() {
|
||||
loadTrees()
|
||||
return
|
||||
}
|
||||
const requestId = ++loadTreesRequestId.current
|
||||
setIsLoading(true)
|
||||
try {
|
||||
const results = await treesApi.search(searchQuery)
|
||||
if (requestId !== loadTreesRequestId.current) return
|
||||
setTrees(results)
|
||||
} catch (err) {
|
||||
if (requestId !== loadTreesRequestId.current) return
|
||||
toast.error('Failed to search flows')
|
||||
console.error(err)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
if (requestId === loadTreesRequestId.current) setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -430,59 +390,6 @@ export function TreeLibraryPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Incomplete Session Recovery */}
|
||||
{visibleIncompleteSessions.length > 0 && (
|
||||
<div className="mb-6 space-y-2">
|
||||
{visibleIncompleteSessions.map(s => (
|
||||
<div key={s.id} className="bg-card border border-border flex items-center justify-between rounded-xl p-4">
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="truncate font-medium text-foreground">
|
||||
{s.tree_snapshot?.name || 'Unknown tree'}
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{s.client_name && `${s.client_name} · `}
|
||||
{s.started_at ? `Started ${formatTimeAgo(s.started_at)}` : 'Not started'}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => navigate(getSessionResumePath(s.tree_id, s.tree_snapshot?.tree_type), { state: { sessionId: s.id } })}
|
||||
>
|
||||
<Play className="h-3.5 w-3.5" />
|
||||
Resume
|
||||
</Button>
|
||||
<button
|
||||
onClick={() => dismissSession(s.id)}
|
||||
className="rounded-md p-1.5 text-muted-foreground hover:bg-accent hover:text-foreground"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Repeat Last Session */}
|
||||
{lastSessionData && (
|
||||
<div className="mb-6">
|
||||
<button
|
||||
onClick={() => navigate(getSessionResumePath(lastSessionData.tree_id, lastSessionData.tree_type), {
|
||||
state: { prefillClientName: lastSessionData.client_name, prefillTicketNumber: lastSessionData.ticket_number },
|
||||
})}
|
||||
className={cn(
|
||||
'flex items-center gap-2 rounded-lg border border-border px-4 py-2.5 text-sm text-muted-foreground',
|
||||
'hover:border-border hover:bg-accent hover:text-foreground'
|
||||
)}
|
||||
>
|
||||
<RotateCcw className="h-4 w-4" />
|
||||
Repeat: {lastSessionData.tree_name}
|
||||
{lastSessionData.client_name && ` (${lastSessionData.client_name})`}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Loading State */}
|
||||
{isLoading ? (
|
||||
<div className="flex justify-center py-12">
|
||||
|
||||
Reference in New Issue
Block a user