From d7b962459acb33ac30f5e8311cf43e1933ce4edf Mon Sep 17 00:00:00 2001 From: chihlasm Date: Tue, 24 Feb 2026 03:52:32 -0500 Subject: [PATCH] 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 --- frontend/src/pages/MyTreesPage.tsx | 349 ++++++++------------------ frontend/src/pages/QuickStartPage.tsx | 188 +++++++++++--- 2 files changed, 255 insertions(+), 282 deletions(-) diff --git a/frontend/src/pages/MyTreesPage.tsx b/frontend/src/pages/MyTreesPage.tsx index 2ae04317..b02c7926 100644 --- a/frontend/src/pages/MyTreesPage.tsx +++ b/frontend/src/pages/MyTreesPage.tsx @@ -1,9 +1,9 @@ -import { useCallback, useEffect, useState } from 'react' +import { useEffect, useState } from 'react' 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 { treesApi } from '@/api/trees' import { sessionsApi } from '@/api/sessions' -import type { TreeListItem, TreeFilters } from '@/types' +import type { TreeListItem } from '@/types' import { TagBadges } from '@/components/common/TagBadges' import { ConfirmDialog } from '@/components/common/ConfirmDialog' import { ShareTreeModal } from '@/components/library/ShareTreeModal' @@ -22,8 +22,6 @@ interface TreeWithStats extends TreeListItem { parent_tree_name?: string | null } -type Tab = 'mine' | 'team' | 'public' | 'all' - export function MyTreesPage() { const navigate = useNavigate() const { user } = useAuthStore() @@ -38,26 +36,23 @@ export function MyTreesPage() { const [showCreateMenu, setShowCreateMenu] = useState(false) const [showAIBuilder, setShowAIBuilder] = useState(false) const [aiEnabled, setAiEnabled] = useState(false) - const [activeTab, setActiveTab] = useState('mine') - const [forkTarget, setForkTarget] = useState(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 setIsLoading(true) try { - const params: TreeFilters = { - 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 - + // Fetch trees and recent sessions in parallel (2 API calls total, not N+1) const [userTrees, recentSessions] = await Promise.all([ - treesApi.list(params), - activeTab === 'mine' ? sessionsApi.list({ size: 100 }) : Promise.resolve([]), + treesApi.list({ author_id: user.id }), + sessionsApi.list({ size: 100 }), ]) // Build a map of tree_id -> most recent session start time @@ -77,34 +72,12 @@ export function MyTreesPage() { setTrees(treesWithStats) } catch (err) { - toast.error('Failed to load flows') + toast.error('Failed to load your flows') console.error(err) } finally { 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) => { 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) => { if (!dateString) return 'Never' return new Date(dateString).toLocaleDateString('en-US', { @@ -167,105 +122,82 @@ export function MyTreesPage() { return (
-
+

My Flows

Your forked and custom flows

-
- - {/* Tab bar */} -
- {tabs.map((tab) => ( - + {showCreateMenu && ( + <> +
setShowCreateMenu(false)} /> +
+ setShowCreateMenu(false)} + className="flex items-center gap-3 rounded-md px-3 py-2.5 text-sm text-foreground hover:bg-accent" + > + +
+
Troubleshooting Tree
+
Branching decision flow
+
+ + setShowCreateMenu(false)} + className="flex items-center gap-3 rounded-md px-3 py-2.5 text-sm text-foreground hover:bg-accent" + > + +
+
Procedural Flow
+
Step-by-step procedure
+
+ + setShowCreateMenu(false)} + className="flex items-center gap-3 rounded-md px-3 py-2.5 text-sm text-foreground hover:bg-accent" + > + +
+
Maintenance Flow
+
Scheduled multi-target tasks
+
+ + {aiEnabled && ( + <> +
+ + + )} +
+ )} - > - {tab.label} - - ))} - - {/* Create button — only on My Flows tab */} - {activeTab === 'mine' && canCreateTrees && ( -
-
- - {showCreateMenu && ( - <> -
setShowCreateMenu(false)} /> -
- setShowCreateMenu(false)} - className="flex items-center gap-3 rounded-md px-3 py-2.5 text-sm text-foreground hover:bg-accent" - > - -
-
Troubleshooting Tree
-
Branching decision flow
-
- - setShowCreateMenu(false)} - className="flex items-center gap-3 rounded-md px-3 py-2.5 text-sm text-foreground hover:bg-accent" - > - -
-
Procedural Flow
-
Step-by-step procedure
-
- - setShowCreateMenu(false)} - className="flex items-center gap-3 rounded-md px-3 py-2.5 text-sm text-foreground hover:bg-accent" - > - -
-
Maintenance Flow
-
Scheduled multi-target tasks
-
- - {aiEnabled && ( - <> -
- - - )} -
- - )} -
)}
@@ -278,41 +210,33 @@ export function MyTreesPage() { ) : trees.length === 0 ? (
-

No flows found

+

No personal flows yet

- {activeTab === 'mine' - ? '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'} + Fork a flow from the library to customize it for your workflow

- {activeTab === 'mine' && ( -
+
+ + Browse Library + + {canCreateTrees && ( - Browse Library + + Create from Scratch - {canCreateTrees && ( - - - Create from Scratch - - )} -
- )} + )} +
) : (
@@ -330,14 +254,7 @@ export function MyTreesPage() { {tree.tree_type === 'maintenance' && ( )} -
-

{tree.name}

- {tree.author_id !== user?.id && tree.author_name && ( -

- by {tree.author_name} -

- )} -
+

{tree.name}

{tree.tree_type === 'procedural' && ( @@ -399,7 +316,7 @@ export function MyTreesPage() {
{/* Actions */} -
+
- )} - -
-
-
- )}
) } diff --git a/frontend/src/pages/QuickStartPage.tsx b/frontend/src/pages/QuickStartPage.tsx index 179f9e0a..69c48b26 100644 --- a/frontend/src/pages/QuickStartPage.tsx +++ b/frontend/src/pages/QuickStartPage.tsx @@ -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 { Search, Loader2, Star, ChevronLeft, ChevronRight } from 'lucide-react' +import { Search, Loader2, Star, ChevronLeft, ChevronRight, GitBranch } from 'lucide-react' import { treesApi } from '@/api/trees' import { sessionsApi } from '@/api/sessions' -import type { TreeListItem } from '@/types' +import type { TreeListItem, TreeFilters } from '@/types' import type { Session } from '@/types/session' import { getTreeNavigatePath } from '@/lib/routing' import { usePermissions } from '@/hooks/usePermissions' @@ -21,6 +21,7 @@ import { ViewToggle } from '@/components/library/ViewToggle' import { AIFlowBuilderModal } from '@/components/ai-builder/AIFlowBuilderModal' import { CreateFlowDropdown } from '@/components/common/CreateFlowDropdown' import { cn } from '@/lib/utils' +import { toast } from '@/lib/toast' function timeAgo(dateStr: string): string { const now = Date.now() @@ -66,6 +67,16 @@ export function QuickStartPage() { const [showAIBuilder, setShowAIBuilder] = useState(false) const { aiEnabled } = useCachedQuota() + // Tab state + type Tab = 'mine' | 'team' | 'public' | 'all' + const hasTeam = Boolean(user?.account_id) + const [activeTab, setActiveTab] = useState('mine') + + // Fork modal state + const [forkTarget, setForkTarget] = useState(null) + const [forkReason, setForkReason] = useState('') + const [isForking, setIsForking] = useState(false) + // Pin store const pinnedItems = usePinnedFlowsStore((s) => s.items) const pinnedIsLoading = usePinnedFlowsStore((s) => s.isLoading) @@ -102,34 +113,29 @@ export function QuickStartPage() { }) }, []) - // Load my flows when page/size or user changes - useEffect(() => { + // Load flows — tab-aware + const loadFlows = useCallback(async () => { if (!user?.id) return + setIsLoadingFlows(true) + setAllFlowsCeiling(false) - const loadFlows = async () => { - setIsLoadingFlows(true) - setAllFlowsCeiling(false) - + try { if (pageSize === 'all') { - // Fetch in chunks of 100, max 500 let allItems: TreeListItem[] = [] let skip = 0 const CHUNK = 100 const MAX = 500 while (true) { - const chunk = await treesApi.list({ - author_id: user.id, - sort_by: 'updated_at', - limit: CHUNK, - skip, - }) + const params: TreeFilters = { sort_by: 'updated_at', limit: CHUNK, skip } + if (activeTab === 'mine') params.author_id = user.id + if (activeTab === 'team') params.visibility = 'team' + if (activeTab === 'public') { params.visibility = 'public'; params.sort_by = 'usage_count' } + + const chunk = await treesApi.list(params) allItems = [...allItems, ...chunk] if (chunk.length < CHUNK || allItems.length >= MAX) { - if (allItems.length >= MAX) { - allItems = allItems.slice(0, MAX) - setAllFlowsCeiling(true) - } + if (allItems.length >= MAX) { allItems = allItems.slice(0, MAX); setAllFlowsCeiling(true) } break } skip += CHUNK @@ -138,20 +144,34 @@ export function QuickStartPage() { setHasNextPage(false) } else { const numSize = pageSize as number - const response = await treesApi.list({ - author_id: user.id, - sort_by: 'updated_at', + const params: TreeFilters = { + sort_by: activeTab === 'public' ? 'usage_count' : 'updated_at', limit: numSize + 1, 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) setMyFlows(response.slice(0, numSize)) } + } catch { + // silently fail + } finally { setIsLoadingFlows(false) } + }, [user?.id, page, pageSize, activeTab]) - loadFlows().catch(() => setIsLoadingFlows(false)) - }, [user?.id, page, pageSize]) + useEffect(() => { loadFlows() }, [loadFlows]) + + // 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 useEffect(() => { @@ -219,9 +239,35 @@ export function QuickStartPage() { const handleTagClick = () => {} // 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 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 (
{/* Page Header */} @@ -234,14 +280,6 @@ export function QuickStartPage() { Welcome back. Here's what's happening with your flows.

-
- {canCreateTrees && ( - setShowAIBuilder(true)} - /> - )} -
{/* Quick Stats */} @@ -354,11 +392,31 @@ export function QuickStartPage() { )}
- {/* My Flows Section */} + {/* My Flows Section — tabbed */}
-
-

My Flows

-
+
+ {tabs.map((tab) => ( + + ))} +
+ {activeTab === 'mine' && canCreateTrees && ( + setShowAIBuilder(true)} + /> + )}
@@ -371,8 +429,16 @@ export function QuickStartPage() {
) : myFlows.length === 0 ? (
-

You haven't created any flows yet.

- {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.'} +

+ {activeTab === 'mine' && canCreateTrees && ( setShowAIBuilder(true)} @@ -497,6 +563,48 @@ export function QuickStartPage() { )}
+ {/* Fork Modal */} + {forkTarget && ( +
+
+

Fork this flow?

+

+ Creates a copy of “{forkTarget.name}” under your account that you can edit freely. +

+ + 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()} + /> +
+ + +
+
+
+ )} + {/* AI Builder Modal */} {showAIBuilder && (