fix: move tabbed flows view to Dashboard, restore MyTreesPage

Tabs (My Flows / My Team / Public / All) belong on the Dashboard page
(/), not on the separate Flow Editor page (/my-trees). MyTreesPage is
restored to its original state. QuickStartPage gets the tab bar replacing
the 'My Flows' section header, with Create button visible on My Flows tab
only, and window focus reload to fix stale data after returning from editor.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
chihlasm
2026-02-24 03:52:32 -05:00
parent 55d329e07b
commit d7b962459a
2 changed files with 255 additions and 282 deletions

View File

@@ -1,9 +1,9 @@
import { useCallback, useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { useNavigate, Link } from 'react-router-dom' import { useNavigate, Link } from 'react-router-dom'
import { Play, Pencil, Share2, Trash2, GitBranch, Clock, TrendingUp, FolderTree, Plus, ListOrdered, ChevronDown, Wrench, Sparkles } from 'lucide-react' import { Play, Pencil, Share2, Trash2, GitBranch, Clock, TrendingUp, FolderTree, Plus, ListOrdered, ChevronDown, Wrench, Sparkles } from 'lucide-react'
import { treesApi } from '@/api/trees' import { treesApi } from '@/api/trees'
import { sessionsApi } from '@/api/sessions' import { sessionsApi } from '@/api/sessions'
import type { TreeListItem, TreeFilters } from '@/types' import type { TreeListItem } from '@/types'
import { TagBadges } from '@/components/common/TagBadges' import { TagBadges } from '@/components/common/TagBadges'
import { ConfirmDialog } from '@/components/common/ConfirmDialog' import { ConfirmDialog } from '@/components/common/ConfirmDialog'
import { ShareTreeModal } from '@/components/library/ShareTreeModal' import { ShareTreeModal } from '@/components/library/ShareTreeModal'
@@ -22,8 +22,6 @@ interface TreeWithStats extends TreeListItem {
parent_tree_name?: string | null parent_tree_name?: string | null
} }
type Tab = 'mine' | 'team' | 'public' | 'all'
export function MyTreesPage() { export function MyTreesPage() {
const navigate = useNavigate() const navigate = useNavigate()
const { user } = useAuthStore() const { user } = useAuthStore()
@@ -38,26 +36,23 @@ export function MyTreesPage() {
const [showCreateMenu, setShowCreateMenu] = useState(false) const [showCreateMenu, setShowCreateMenu] = useState(false)
const [showAIBuilder, setShowAIBuilder] = useState(false) const [showAIBuilder, setShowAIBuilder] = useState(false)
const [aiEnabled, setAiEnabled] = useState(false) const [aiEnabled, setAiEnabled] = useState(false)
const [activeTab, setActiveTab] = useState<Tab>('mine')
const [forkTarget, setForkTarget] = useState<TreeWithStats | null>(null)
const [forkReason, setForkReason] = useState('')
const [isForking, setIsForking] = useState(false)
const loadTrees = useCallback(async () => { useEffect(() => {
loadMyTrees()
}, [user?.id])
useEffect(() => {
aiBuilderApi.getQuota().then((q) => setAiEnabled(q.ai_enabled)).catch(() => {})
}, [])
const loadMyTrees = async () => {
if (!user?.id) return if (!user?.id) return
setIsLoading(true) setIsLoading(true)
try { try {
const params: TreeFilters = { // Fetch trees and recent sessions in parallel (2 API calls total, not N+1)
sort_by: activeTab === 'public' ? 'usage_count' : 'updated_at',
}
if (activeTab === 'mine') params.author_id = user.id
if (activeTab === 'team') params.visibility = 'team'
if (activeTab === 'public') params.visibility = 'public'
// 'all' tab: no author/visibility filter
const [userTrees, recentSessions] = await Promise.all([ const [userTrees, recentSessions] = await Promise.all([
treesApi.list(params), treesApi.list({ author_id: user.id }),
activeTab === 'mine' ? sessionsApi.list({ size: 100 }) : Promise.resolve([]), sessionsApi.list({ size: 100 }),
]) ])
// Build a map of tree_id -> most recent session start time // Build a map of tree_id -> most recent session start time
@@ -77,34 +72,12 @@ export function MyTreesPage() {
setTrees(treesWithStats) setTrees(treesWithStats)
} catch (err) { } catch (err) {
toast.error('Failed to load flows') toast.error('Failed to load your flows')
console.error(err) console.error(err)
} finally { } finally {
setIsLoading(false) setIsLoading(false)
} }
}, [activeTab, user?.id]) }
useEffect(() => {
loadTrees()
}, [loadTrees])
useEffect(() => {
const onFocus = () => loadTrees()
window.addEventListener('focus', onFocus)
return () => window.removeEventListener('focus', onFocus)
}, [loadTrees])
useEffect(() => {
aiBuilderApi.getQuota().then((q) => setAiEnabled(q.ai_enabled)).catch(() => {})
}, [])
const hasTeam = Boolean(user?.account_id)
const tabs: { id: Tab; label: string }[] = [
{ id: 'mine', label: 'My Flows' },
...(hasTeam ? [{ id: 'team' as Tab, label: 'My Team' }] : []),
{ id: 'public', label: 'Public' },
{ id: 'all', label: 'All' },
]
const handleStartSession = (tree: TreeWithStats) => { const handleStartSession = (tree: TreeWithStats) => {
if (tree.tree_type === 'maintenance') { if (tree.tree_type === 'maintenance') {
@@ -138,24 +111,6 @@ export function MyTreesPage() {
} }
} }
const handleFork = async () => {
if (!forkTarget) return
setIsForking(true)
try {
const forked = await treesApi.fork(forkTarget.id, {
fork_reason: forkReason.trim() || undefined,
})
toast.success(`"${forked.name}" added to your flows`)
setForkTarget(null)
setForkReason('')
setActiveTab('mine')
} catch {
toast.error('Failed to fork flow')
} finally {
setIsForking(false)
}
}
const formatDate = (dateString?: string) => { const formatDate = (dateString?: string) => {
if (!dateString) return 'Never' if (!dateString) return 'Never'
return new Date(dateString).toLocaleDateString('en-US', { return new Date(dateString).toLocaleDateString('en-US', {
@@ -167,105 +122,82 @@ export function MyTreesPage() {
return ( return (
<div className="container mx-auto px-4 py-6 sm:px-6 sm:py-8"> <div className="container mx-auto px-4 py-6 sm:px-6 sm:py-8">
<div className="mb-4 flex items-center justify-between sm:mb-6"> <div className="mb-6 flex items-center justify-between sm:mb-8">
<div> <div>
<h1 className="text-2xl font-bold font-heading text-foreground sm:text-3xl">My Flows</h1> <h1 className="text-2xl font-bold font-heading text-foreground sm:text-3xl">My Flows</h1>
<p className="mt-2 text-muted-foreground"> <p className="mt-2 text-muted-foreground">
Your forked and custom flows Your forked and custom flows
</p> </p>
</div> </div>
</div> {canCreateTrees && (
<div className="relative">
{/* Tab bar */} <button
<div className="flex items-center gap-1 border-b border-border mb-4"> onClick={() => setShowCreateMenu(!showCreateMenu)}
{tabs.map((tab) => ( className="flex items-center gap-2 rounded-md bg-gradient-brand text-white shadow-lg shadow-primary/20 px-4 py-2 text-sm font-medium hover:opacity-90"
<button >
key={tab.id} <Plus className="h-4 w-4" />
type="button" Create New
onClick={() => setActiveTab(tab.id)} <ChevronDown className="h-3.5 w-3.5" />
className={cn( </button>
'px-4 py-2.5 text-sm font-medium transition-colors border-b-2 -mb-px', {showCreateMenu && (
activeTab === tab.id <>
? 'border-primary text-foreground' <div className="fixed inset-0 z-10" onClick={() => setShowCreateMenu(false)} />
: 'border-transparent text-muted-foreground hover:text-foreground' <div className="absolute right-0 z-20 mt-1 w-56 rounded-lg border border-border bg-card p-1 shadow-xl backdrop-blur-sm">
<Link
to="/trees/new"
onClick={() => setShowCreateMenu(false)}
className="flex items-center gap-3 rounded-md px-3 py-2.5 text-sm text-foreground hover:bg-accent"
>
<FolderTree className="h-4 w-4 text-muted-foreground" />
<div>
<div className="font-medium">Troubleshooting Tree</div>
<div className="text-xs text-muted-foreground">Branching decision flow</div>
</div>
</Link>
<Link
to="/flows/new"
onClick={() => setShowCreateMenu(false)}
className="flex items-center gap-3 rounded-md px-3 py-2.5 text-sm text-foreground hover:bg-accent"
>
<ListOrdered className="h-4 w-4 text-muted-foreground" />
<div>
<div className="font-medium">Procedural Flow</div>
<div className="text-xs text-muted-foreground">Step-by-step procedure</div>
</div>
</Link>
<Link
to="/flows/new?type=maintenance"
onClick={() => setShowCreateMenu(false)}
className="flex items-center gap-3 rounded-md px-3 py-2.5 text-sm text-foreground hover:bg-accent"
>
<Wrench className="h-4 w-4 text-amber-400" />
<div>
<div className="font-medium">Maintenance Flow</div>
<div className="text-xs text-muted-foreground">Scheduled multi-target tasks</div>
</div>
</Link>
{aiEnabled && (
<>
<div className="my-1 border-t border-border" />
<button
type="button"
onClick={() => {
setShowCreateMenu(false)
setShowAIBuilder(true)
}}
className="flex w-full items-center gap-3 rounded-md px-3 py-2.5 text-sm text-foreground hover:bg-accent"
>
<Sparkles className="h-4 w-4 text-primary" />
<div className="text-left">
<div className="font-medium">Build with AI</div>
<div className="text-xs text-muted-foreground">AI-assisted flow creation</div>
</div>
</button>
</>
)}
</div>
</>
)} )}
>
{tab.label}
</button>
))}
{/* Create button — only on My Flows tab */}
{activeTab === 'mine' && canCreateTrees && (
<div className="ml-auto pb-1.5">
<div className="relative">
<button
onClick={() => setShowCreateMenu(!showCreateMenu)}
className="flex items-center gap-2 rounded-md bg-gradient-brand text-white shadow-lg shadow-primary/20 px-4 py-2 text-sm font-medium hover:opacity-90"
>
<Plus className="h-4 w-4" />
Create New
<ChevronDown className="h-3.5 w-3.5" />
</button>
{showCreateMenu && (
<>
<div className="fixed inset-0 z-10" onClick={() => setShowCreateMenu(false)} />
<div className="absolute right-0 z-20 mt-1 w-56 rounded-lg border border-border bg-card p-1 shadow-xl backdrop-blur-sm">
<Link
to="/trees/new"
onClick={() => setShowCreateMenu(false)}
className="flex items-center gap-3 rounded-md px-3 py-2.5 text-sm text-foreground hover:bg-accent"
>
<FolderTree className="h-4 w-4 text-muted-foreground" />
<div>
<div className="font-medium">Troubleshooting Tree</div>
<div className="text-xs text-muted-foreground">Branching decision flow</div>
</div>
</Link>
<Link
to="/flows/new"
onClick={() => setShowCreateMenu(false)}
className="flex items-center gap-3 rounded-md px-3 py-2.5 text-sm text-foreground hover:bg-accent"
>
<ListOrdered className="h-4 w-4 text-muted-foreground" />
<div>
<div className="font-medium">Procedural Flow</div>
<div className="text-xs text-muted-foreground">Step-by-step procedure</div>
</div>
</Link>
<Link
to="/flows/new?type=maintenance"
onClick={() => setShowCreateMenu(false)}
className="flex items-center gap-3 rounded-md px-3 py-2.5 text-sm text-foreground hover:bg-accent"
>
<Wrench className="h-4 w-4 text-amber-400" />
<div>
<div className="font-medium">Maintenance Flow</div>
<div className="text-xs text-muted-foreground">Scheduled multi-target tasks</div>
</div>
</Link>
{aiEnabled && (
<>
<div className="my-1 border-t border-border" />
<button
type="button"
onClick={() => {
setShowCreateMenu(false)
setShowAIBuilder(true)
}}
className="flex w-full items-center gap-3 rounded-md px-3 py-2.5 text-sm text-foreground hover:bg-accent"
>
<Sparkles className="h-4 w-4 text-primary" />
<div className="text-left">
<div className="font-medium">Build with AI</div>
<div className="text-xs text-muted-foreground">AI-assisted flow creation</div>
</div>
</button>
</>
)}
</div>
</>
)}
</div>
</div> </div>
)} )}
</div> </div>
@@ -278,41 +210,33 @@ export function MyTreesPage() {
) : trees.length === 0 ? ( ) : trees.length === 0 ? (
<div className="rounded-lg border border-dashed border-border bg-accent px-4 py-12 text-center"> <div className="rounded-lg border border-dashed border-border bg-accent px-4 py-12 text-center">
<FolderTree className="mx-auto mb-4 h-12 w-12 text-muted-foreground" /> <FolderTree className="mx-auto mb-4 h-12 w-12 text-muted-foreground" />
<h2 className="mb-2 text-lg font-semibold text-foreground">No flows found</h2> <h2 className="mb-2 text-lg font-semibold text-foreground">No personal flows yet</h2>
<p className="mb-4 text-sm text-muted-foreground"> <p className="mb-4 text-sm text-muted-foreground">
{activeTab === 'mine' Fork a flow from the library to customize it for your workflow
? 'Fork a flow from the library to customize it for your workflow'
: activeTab === 'team'
? 'No team flows found'
: activeTab === 'public'
? 'No public flows found'
: 'No flows found'}
</p> </p>
{activeTab === 'mine' && ( <div className="flex items-center justify-center gap-3">
<div className="flex items-center justify-center gap-3"> <Link
to="/trees"
className={cn(
'inline-flex items-center gap-2 rounded-md bg-gradient-brand text-white shadow-lg shadow-primary/20 px-4 py-2 text-sm font-medium',
'hover:opacity-90'
)}
>
Browse Library
</Link>
{canCreateTrees && (
<Link <Link
to="/trees" to="/trees/new"
className={cn( className={cn(
'inline-flex items-center gap-2 rounded-md bg-gradient-brand text-white shadow-lg shadow-primary/20 px-4 py-2 text-sm font-medium', 'inline-flex items-center gap-2 rounded-md border border-border px-4 py-2 text-sm font-medium text-muted-foreground',
'hover:opacity-90' 'hover:bg-accent hover:text-foreground'
)} )}
> >
Browse Library <Plus className="h-4 w-4" />
Create from Scratch
</Link> </Link>
{canCreateTrees && ( )}
<Link </div>
to="/trees/new"
className={cn(
'inline-flex items-center gap-2 rounded-md border border-border px-4 py-2 text-sm font-medium text-muted-foreground',
'hover:bg-accent hover:text-foreground'
)}
>
<Plus className="h-4 w-4" />
Create from Scratch
</Link>
)}
</div>
)}
</div> </div>
) : ( ) : (
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3"> <div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
@@ -330,14 +254,7 @@ export function MyTreesPage() {
{tree.tree_type === 'maintenance' && ( {tree.tree_type === 'maintenance' && (
<Wrench className="h-4 w-4 shrink-0 text-amber-400" /> <Wrench className="h-4 w-4 shrink-0 text-amber-400" />
)} )}
<div> <h3 className="font-semibold text-foreground">{tree.name}</h3>
<h3 className="font-semibold text-foreground">{tree.name}</h3>
{tree.author_id !== user?.id && tree.author_name && (
<p className="text-[10px] font-label text-muted-foreground">
by {tree.author_name}
</p>
)}
</div>
</div> </div>
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
{tree.tree_type === 'procedural' && ( {tree.tree_type === 'procedural' && (
@@ -399,7 +316,7 @@ export function MyTreesPage() {
</div> </div>
{/* Actions */} {/* Actions */}
<div className="flex items-center gap-2 flex-wrap"> <div className="flex items-center gap-2">
<button <button
type="button" type="button"
onClick={() => handleStartSession(tree)} onClick={() => handleStartSession(tree)}
@@ -423,16 +340,6 @@ export function MyTreesPage() {
<Pencil className="h-4 w-4" /> <Pencil className="h-4 w-4" />
</Link> </Link>
)} )}
{tree.author_id !== user?.id && (
<button
type="button"
onClick={() => { setForkTarget(tree); setForkReason('') }}
className="flex items-center gap-1.5 rounded-lg border border-border px-3 py-1.5 text-xs text-muted-foreground hover:bg-accent hover:text-foreground"
>
<GitBranch className="h-3.5 w-3.5" />
Fork
</button>
)}
<button <button
type="button" type="button"
onClick={() => { onClick={() => {
@@ -499,48 +406,6 @@ export function MyTreesPage() {
isOpen={showAIBuilder} isOpen={showAIBuilder}
onClose={() => setShowAIBuilder(false)} onClose={() => setShowAIBuilder(false)}
/> />
{/* Fork Confirmation Modal */}
{forkTarget && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-background/80 backdrop-blur-sm">
<div className="w-full max-w-sm rounded-xl border border-border bg-card p-5 shadow-xl">
<h3 className="mb-1 text-sm font-semibold text-foreground">Fork this flow?</h3>
<p className="mb-4 text-xs text-muted-foreground">
Creates a copy of &ldquo;{forkTarget.name}&rdquo; under your account that you can edit freely.
</p>
<label className="mb-1 block text-xs text-muted-foreground">
Why are you forking? <span className="opacity-60">(optional)</span>
</label>
<input
type="text"
value={forkReason}
onChange={(e) => setForkReason(e.target.value)}
placeholder="e.g. Adding Cisco Meraki steps for our network"
maxLength={255}
className="mb-4 w-full rounded-lg border border-border bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20"
onKeyDown={(e) => e.key === 'Enter' && handleFork()}
/>
<div className="flex gap-2">
<button
type="button"
onClick={handleFork}
disabled={isForking}
className="flex flex-1 items-center justify-center gap-1.5 rounded-lg bg-gradient-brand py-2 text-sm font-medium text-white shadow-lg shadow-primary/20 hover:opacity-90 disabled:opacity-50"
>
<GitBranch className="h-3.5 w-3.5" />
{isForking ? 'Forking...' : 'Fork Flow'}
</button>
<button
type="button"
onClick={() => setForkTarget(null)}
className="rounded-lg border border-border px-4 py-2 text-sm text-muted-foreground hover:bg-accent"
>
Cancel
</button>
</div>
</div>
</div>
)}
</div> </div>
) )
} }

View File

@@ -1,9 +1,9 @@
import { useState, useEffect, useRef, useMemo } from 'react' import { useState, useEffect, useRef, useMemo, useCallback } from 'react'
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
import { Search, Loader2, Star, ChevronLeft, ChevronRight } from 'lucide-react' import { Search, Loader2, Star, ChevronLeft, ChevronRight, GitBranch } from 'lucide-react'
import { treesApi } from '@/api/trees' import { treesApi } from '@/api/trees'
import { sessionsApi } from '@/api/sessions' import { sessionsApi } from '@/api/sessions'
import type { TreeListItem } from '@/types' import type { TreeListItem, TreeFilters } from '@/types'
import type { Session } from '@/types/session' import type { Session } from '@/types/session'
import { getTreeNavigatePath } from '@/lib/routing' import { getTreeNavigatePath } from '@/lib/routing'
import { usePermissions } from '@/hooks/usePermissions' import { usePermissions } from '@/hooks/usePermissions'
@@ -21,6 +21,7 @@ import { ViewToggle } from '@/components/library/ViewToggle'
import { AIFlowBuilderModal } from '@/components/ai-builder/AIFlowBuilderModal' import { AIFlowBuilderModal } from '@/components/ai-builder/AIFlowBuilderModal'
import { CreateFlowDropdown } from '@/components/common/CreateFlowDropdown' import { CreateFlowDropdown } from '@/components/common/CreateFlowDropdown'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { toast } from '@/lib/toast'
function timeAgo(dateStr: string): string { function timeAgo(dateStr: string): string {
const now = Date.now() const now = Date.now()
@@ -66,6 +67,16 @@ export function QuickStartPage() {
const [showAIBuilder, setShowAIBuilder] = useState(false) const [showAIBuilder, setShowAIBuilder] = useState(false)
const { aiEnabled } = useCachedQuota() const { aiEnabled } = useCachedQuota()
// Tab state
type Tab = 'mine' | 'team' | 'public' | 'all'
const hasTeam = Boolean(user?.account_id)
const [activeTab, setActiveTab] = useState<Tab>('mine')
// Fork modal state
const [forkTarget, setForkTarget] = useState<TreeListItem | null>(null)
const [forkReason, setForkReason] = useState('')
const [isForking, setIsForking] = useState(false)
// Pin store // Pin store
const pinnedItems = usePinnedFlowsStore((s) => s.items) const pinnedItems = usePinnedFlowsStore((s) => s.items)
const pinnedIsLoading = usePinnedFlowsStore((s) => s.isLoading) const pinnedIsLoading = usePinnedFlowsStore((s) => s.isLoading)
@@ -102,34 +113,29 @@ export function QuickStartPage() {
}) })
}, []) }, [])
// Load my flows when page/size or user changes // Load flows — tab-aware
useEffect(() => { const loadFlows = useCallback(async () => {
if (!user?.id) return if (!user?.id) return
setIsLoadingFlows(true)
setAllFlowsCeiling(false)
const loadFlows = async () => { try {
setIsLoadingFlows(true)
setAllFlowsCeiling(false)
if (pageSize === 'all') { if (pageSize === 'all') {
// Fetch in chunks of 100, max 500
let allItems: TreeListItem[] = [] let allItems: TreeListItem[] = []
let skip = 0 let skip = 0
const CHUNK = 100 const CHUNK = 100
const MAX = 500 const MAX = 500
while (true) { while (true) {
const chunk = await treesApi.list({ const params: TreeFilters = { sort_by: 'updated_at', limit: CHUNK, skip }
author_id: user.id, if (activeTab === 'mine') params.author_id = user.id
sort_by: 'updated_at', if (activeTab === 'team') params.visibility = 'team'
limit: CHUNK, if (activeTab === 'public') { params.visibility = 'public'; params.sort_by = 'usage_count' }
skip,
}) const chunk = await treesApi.list(params)
allItems = [...allItems, ...chunk] allItems = [...allItems, ...chunk]
if (chunk.length < CHUNK || allItems.length >= MAX) { if (chunk.length < CHUNK || allItems.length >= MAX) {
if (allItems.length >= MAX) { if (allItems.length >= MAX) { allItems = allItems.slice(0, MAX); setAllFlowsCeiling(true) }
allItems = allItems.slice(0, MAX)
setAllFlowsCeiling(true)
}
break break
} }
skip += CHUNK skip += CHUNK
@@ -138,20 +144,34 @@ export function QuickStartPage() {
setHasNextPage(false) setHasNextPage(false)
} else { } else {
const numSize = pageSize as number const numSize = pageSize as number
const response = await treesApi.list({ const params: TreeFilters = {
author_id: user.id, sort_by: activeTab === 'public' ? 'usage_count' : 'updated_at',
sort_by: 'updated_at',
limit: numSize + 1, limit: numSize + 1,
skip: (page - 1) * numSize, skip: (page - 1) * numSize,
}) }
if (activeTab === 'mine') params.author_id = user.id
if (activeTab === 'team') params.visibility = 'team'
if (activeTab === 'public') params.visibility = 'public'
const response = await treesApi.list(params)
setHasNextPage(response.length > numSize) setHasNextPage(response.length > numSize)
setMyFlows(response.slice(0, numSize)) setMyFlows(response.slice(0, numSize))
} }
} catch {
// silently fail
} finally {
setIsLoadingFlows(false) setIsLoadingFlows(false)
} }
}, [user?.id, page, pageSize, activeTab])
loadFlows().catch(() => setIsLoadingFlows(false)) useEffect(() => { loadFlows() }, [loadFlows])
}, [user?.id, page, pageSize])
// Reload on window focus (fixes stale data after returning from editor)
useEffect(() => {
const onFocus = () => loadFlows()
window.addEventListener('focus', onFocus)
return () => window.removeEventListener('focus', onFocus)
}, [loadFlows])
// Debounced search // Debounced search
useEffect(() => { useEffect(() => {
@@ -219,9 +239,35 @@ export function QuickStartPage() {
const handleTagClick = () => {} // Not used on dashboard const handleTagClick = () => {} // Not used on dashboard
const handleFolderCreated = () => {} // Not used on dashboard const handleFolderCreated = () => {} // Not used on dashboard
const handleFork = async () => {
if (!forkTarget) return
setIsForking(true)
try {
const forked = await treesApi.fork(forkTarget.id, {
fork_reason: forkReason.trim() || undefined,
})
toast.success(`"${forked.name}" added to your flows`)
setForkTarget(null)
setForkReason('')
setActiveTab('mine')
} catch {
toast.error('Failed to fork flow')
} finally {
setIsForking(false)
}
}
// Page size options // Page size options
const pageSizeOptions: (number | 'all')[] = [10, 25, 50, 'all'] const pageSizeOptions: (number | 'all')[] = [10, 25, 50, 'all']
// Tabs
const tabs: { id: Tab; label: string }[] = [
{ id: 'mine', label: 'My Flows' },
...(hasTeam ? [{ id: 'team' as Tab, label: 'My Team' }] : []),
{ id: 'public', label: 'Public' },
{ id: 'all', label: 'All' },
]
return ( return (
<div className="p-6 space-y-6"> <div className="p-6 space-y-6">
{/* Page Header */} {/* Page Header */}
@@ -234,14 +280,6 @@ export function QuickStartPage() {
Welcome back. Here&apos;s what&apos;s happening with your flows. Welcome back. Here&apos;s what&apos;s happening with your flows.
</p> </p>
</div> </div>
<div className="flex items-center gap-2">
{canCreateTrees && (
<CreateFlowDropdown
aiEnabled={aiEnabled}
onOpenAIBuilder={() => setShowAIBuilder(true)}
/>
)}
</div>
</div> </div>
{/* Quick Stats */} {/* Quick Stats */}
@@ -354,11 +392,31 @@ export function QuickStartPage() {
)} )}
</div> </div>
{/* My Flows Section */} {/* My Flows Section — tabbed */}
<div> <div>
<div className="mb-3 flex items-center justify-between"> <div className="mb-3 flex items-center gap-1 border-b border-border">
<h2 className="font-heading text-lg font-semibold text-foreground">My Flows</h2> {tabs.map((tab) => (
<div className="flex items-center gap-2"> <button
key={tab.id}
type="button"
onClick={() => { setActiveTab(tab.id); setPage(1) }}
className={cn(
'px-4 py-2.5 text-sm font-medium transition-colors border-b-2 -mb-px',
activeTab === tab.id
? 'border-primary text-foreground'
: 'border-transparent text-muted-foreground hover:text-foreground'
)}
>
{tab.label}
</button>
))}
<div className="ml-auto flex items-center gap-2 pb-1.5">
{activeTab === 'mine' && canCreateTrees && (
<CreateFlowDropdown
aiEnabled={aiEnabled}
onOpenAIBuilder={() => setShowAIBuilder(true)}
/>
)}
<ViewToggle view={dashboardMyFlowsView} onChange={setDashboardMyFlowsView} /> <ViewToggle view={dashboardMyFlowsView} onChange={setDashboardMyFlowsView} />
</div> </div>
</div> </div>
@@ -371,8 +429,16 @@ export function QuickStartPage() {
</div> </div>
) : myFlows.length === 0 ? ( ) : myFlows.length === 0 ? (
<div className="py-12 text-center"> <div className="py-12 text-center">
<p className="text-muted-foreground mb-4">You haven&apos;t created any flows yet.</p> <p className="text-muted-foreground mb-4">
{canCreateTrees && ( {activeTab === 'mine'
? "You haven't created any flows yet."
: activeTab === 'team'
? 'No team flows found.'
: activeTab === 'public'
? 'No public flows found.'
: 'No flows found.'}
</p>
{activeTab === 'mine' && canCreateTrees && (
<CreateFlowDropdown <CreateFlowDropdown
aiEnabled={aiEnabled} aiEnabled={aiEnabled}
onOpenAIBuilder={() => setShowAIBuilder(true)} onOpenAIBuilder={() => setShowAIBuilder(true)}
@@ -497,6 +563,48 @@ export function QuickStartPage() {
)} )}
</div> </div>
{/* Fork Modal */}
{forkTarget && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-background/80 backdrop-blur-sm">
<div className="w-full max-w-sm rounded-xl border border-border bg-card p-5 shadow-xl">
<h3 className="mb-1 text-sm font-semibold text-foreground">Fork this flow?</h3>
<p className="mb-4 text-xs text-muted-foreground">
Creates a copy of &ldquo;{forkTarget.name}&rdquo; under your account that you can edit freely.
</p>
<label className="mb-1 block text-xs text-muted-foreground">
Why are you forking? <span className="opacity-60">(optional)</span>
</label>
<input
type="text"
value={forkReason}
onChange={(e) => setForkReason(e.target.value)}
placeholder="e.g. Adding Cisco Meraki steps for our network"
maxLength={255}
className="mb-4 w-full rounded-lg border border-border bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20"
onKeyDown={(e) => e.key === 'Enter' && handleFork()}
/>
<div className="flex gap-2">
<button
type="button"
onClick={handleFork}
disabled={isForking}
className="flex flex-1 items-center justify-center gap-1.5 rounded-lg bg-gradient-brand py-2 text-sm font-medium text-white shadow-lg shadow-primary/20 hover:opacity-90 disabled:opacity-50"
>
<GitBranch className="h-3.5 w-3.5" />
{isForking ? 'Forking...' : 'Fork Flow'}
</button>
<button
type="button"
onClick={() => setForkTarget(null)}
className="rounded-lg border border-border px-4 py-2 text-sm text-muted-foreground hover:bg-accent"
>
Cancel
</button>
</div>
</div>
</div>
)}
{/* AI Builder Modal */} {/* AI Builder Modal */}
{showAIBuilder && ( {showAIBuilder && (
<AIFlowBuilderModal <AIFlowBuilderModal