21 KiB
Dashboard: My Flows, Favorites/Pin UI, and AI Builder — Implementation Plan
For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
Goal: Transform the dashboard into a structured personal workspace with a Favorites section (pinned flows), paginated "My Flows", shared pin store, and AI Builder access from the Library page.
Architecture: Zustand store (pinnedFlowsStore) becomes single source of truth for pin state across Sidebar, Dashboard, and Library. URL-synced pagination hook manages My Flows paging. Cached quota hook avoids redundant AI quota fetches. All three library view components gain optional pin button props.
Tech Stack: React 19, Zustand, React Router v7, Tailwind CSS, Lucide icons, Axios
Design doc: docs/plans/2026-02-20-final-dashboard-plan.md
Task 1: Add pin() to pinnedFlowsApi
Files:
- Modify:
frontend/src/api/pinnedFlows.ts
Step 1: Add pin method
In frontend/src/api/pinnedFlows.ts, add after the unpin method:
pin: async (treeId: string): Promise<void> => {
await apiClient.post(`/trees/${treeId}/pin`)
},
Step 2: Verify build
Run: cd /c/Dev/Projects/patherly/frontend && npx tsc --noEmit
Expected: No errors
Step 3: Commit
git add frontend/src/api/pinnedFlows.ts
git commit -m "feat: add pin() method to pinnedFlowsApi"
Task 2: Create pinnedFlowsStore
Files:
- Create:
frontend/src/store/pinnedFlowsStore.ts
Step 1: Create the store
Create frontend/src/store/pinnedFlowsStore.ts:
import { create } from 'zustand'
import { pinnedFlowsApi } from '@/api/pinnedFlows'
import type { PinnedFlow } from '@/api/pinnedFlows'
import { toast } from '@/lib/toast'
interface PinnedFlowsState {
items: PinnedFlow[]
isLoaded: boolean
isLoading: boolean
isMutatingByTreeId: Record<string, boolean>
error: string | null
// Actions
load: (force?: boolean) => Promise<void>
pin: (treeId: string) => Promise<void>
unpin: (treeId: string) => Promise<void>
toggle: (treeId: string) => void
// Derived helpers
isPinned: (treeId: string) => boolean
}
export const usePinnedFlowsStore = create<PinnedFlowsState>()((set, get) => ({
items: [],
isLoaded: false,
isLoading: false,
isMutatingByTreeId: {},
error: null,
load: async (force = false) => {
const state = get()
if (state.isLoaded && !force) return
if (state.isLoading) return
set({ isLoading: true, error: null })
try {
const data = await pinnedFlowsApi.list()
set({ items: data.items, isLoaded: true, isLoading: false })
} catch {
set({ error: 'Failed to load pinned flows', isLoading: false })
}
},
pin: async (treeId: string) => {
const state = get()
if (state.isMutatingByTreeId[treeId]) return
set({ isMutatingByTreeId: { ...state.isMutatingByTreeId, [treeId]: true } })
try {
await pinnedFlowsApi.pin(treeId)
// Reload to get full PinnedFlow object with tree_name, tree_type, etc.
const data = await pinnedFlowsApi.list()
set((s) => ({
items: data.items,
isMutatingByTreeId: { ...s.isMutatingByTreeId, [treeId]: false },
}))
} catch (err: unknown) {
const status = (err as { response?: { status?: number } })?.response?.status
if (status === 409) {
toast.error('Maximum of 15 favorites reached. Unpin a flow to add a new one.')
} else {
toast.error('Failed to pin flow')
}
set((s) => ({
isMutatingByTreeId: { ...s.isMutatingByTreeId, [treeId]: false },
}))
}
},
unpin: async (treeId: string) => {
const state = get()
if (state.isMutatingByTreeId[treeId]) return
// Optimistic remove
const prevItems = state.items
set({
items: state.items.filter((f) => f.tree_id !== treeId),
isMutatingByTreeId: { ...state.isMutatingByTreeId, [treeId]: true },
})
try {
await pinnedFlowsApi.unpin(treeId)
set((s) => ({
isMutatingByTreeId: { ...s.isMutatingByTreeId, [treeId]: false },
}))
} catch {
// Rollback
toast.error('Failed to unpin flow')
set((s) => ({
items: prevItems,
isMutatingByTreeId: { ...s.isMutatingByTreeId, [treeId]: false },
}))
}
},
toggle: (treeId: string) => {
const state = get()
if (state.isPinned(treeId)) {
state.unpin(treeId)
} else {
state.pin(treeId)
}
},
isPinned: (treeId: string) => {
return get().items.some((f) => f.tree_id === treeId)
},
}))
// Derived selectors (use outside component or in selectors)
export const selectPinnedTreeIds = (state: PinnedFlowsState): Set<string> =>
new Set(state.items.map((f) => f.tree_id))
export const selectPinLoadingTreeIds = (state: PinnedFlowsState): Set<string> =>
new Set(
Object.entries(state.isMutatingByTreeId)
.filter(([, v]) => v)
.map(([k]) => k)
)
Step 2: Verify build
Run: cd /c/Dev/Projects/patherly/frontend && npx tsc --noEmit
Expected: No errors
Step 3: Commit
git add frontend/src/store/pinnedFlowsStore.ts
git commit -m "feat: create pinnedFlowsStore — single source of truth for pin state"
Task 3: Add dashboardMyFlowsView to userPreferencesStore
Files:
- Modify:
frontend/src/store/userPreferencesStore.ts
Step 1: Add preference
In the UserPreferencesState interface, add:
dashboardMyFlowsView: 'grid' | 'list' | 'table'
setDashboardMyFlowsView: (view: 'grid' | 'list' | 'table') => void
In the store implementation, add alongside existing fields:
dashboardMyFlowsView: 'grid',
setDashboardMyFlowsView: (view) => set({ dashboardMyFlowsView: view }),
Step 2: Verify build
Run: cd /c/Dev/Projects/patherly/frontend && npx tsc --noEmit
Step 3: Commit
git add frontend/src/store/userPreferencesStore.ts
git commit -m "feat: add dashboardMyFlowsView preference (independent from Library)"
Task 4: Replace local pin state in Sidebar with store
Files:
- Modify:
frontend/src/components/layout/Sidebar.tsx
Step 1: Swap to store
Remove from imports:
pinnedFlowsApiimportPinnedFlowtype import
Add import:
import { usePinnedFlowsStore } from '@/store/pinnedFlowsStore'
Remove from component:
const [pinnedFlows, setPinnedFlows] = useState<PinnedFlow[]>([])- The
pinnedFlowsApi.list()call from theuseEffect - The entire
handleUnpinfunction
Replace with:
const pinnedItems = usePinnedFlowsStore((s) => s.items)
const loadPinned = usePinnedFlowsStore((s) => s.load)
const unpinFlow = usePinnedFlowsStore((s) => s.unpin)
In the useEffect that fetches data, remove the pinnedFlowsApi.list() from Promise.all. Add a separate call:
useEffect(() => { loadPinned() }, [loadPinned])
Update PinnedFlowsSection props:
<PinnedFlowsSection flows={pinnedItems} onUnpin={unpinFlow} />
Step 2: Verify build
Run: cd /c/Dev/Projects/patherly/frontend && npx tsc --noEmit
Step 3: Commit
git add frontend/src/components/layout/Sidebar.tsx
git commit -m "refactor: sidebar uses pinnedFlowsStore instead of local state"
Task 5: Create usePaginationParams hook
Files:
- Create:
frontend/src/hooks/usePaginationParams.ts
Step 1: Create the hook
import { useSearchParams } from 'react-router-dom'
import { useCallback, useMemo } from 'react'
type PageSize = number | 'all'
interface UsePaginationParamsOptions {
defaultPageSize?: number
allowedPageSizes?: PageSize[]
}
export function usePaginationParams(options: UsePaginationParamsOptions = {}) {
const { defaultPageSize = 10, allowedPageSizes = [10, 25, 50, 'all'] } = options
const [searchParams, setSearchParams] = useSearchParams()
const page = useMemo(() => {
const raw = searchParams.get('page')
const n = raw ? parseInt(raw, 10) : 1
return Number.isFinite(n) && n >= 1 ? n : 1
}, [searchParams])
const pageSize = useMemo((): PageSize => {
const raw = searchParams.get('size')
if (raw === 'all' && allowedPageSizes.includes('all')) return 'all'
const n = raw ? parseInt(raw, 10) : defaultPageSize
if (Number.isFinite(n) && allowedPageSizes.includes(n)) return n
return defaultPageSize
}, [searchParams, defaultPageSize, allowedPageSizes])
const setPage = useCallback(
(newPage: number) => {
setSearchParams((prev) => {
const next = new URLSearchParams(prev)
if (newPage <= 1) {
next.delete('page')
} else {
next.set('page', String(newPage))
}
return next
})
},
[setSearchParams]
)
const setPageSize = useCallback(
(newSize: PageSize) => {
setSearchParams((prev) => {
const next = new URLSearchParams(prev)
next.set('size', String(newSize))
next.delete('page') // reset to page 1
return next
})
},
[setSearchParams]
)
return { page, pageSize, setPage, setPageSize }
}
Step 2: Verify build
Run: cd /c/Dev/Projects/patherly/frontend && npx tsc --noEmit
Step 3: Commit
git add frontend/src/hooks/usePaginationParams.ts
git commit -m "feat: create usePaginationParams hook — URL-synced pagination"
Task 6: Create useCachedQuota hook
Files:
- Create:
frontend/src/hooks/useCachedQuota.ts
Step 1: Create the hook
import { useState, useEffect } from 'react'
import { aiBuilderApi } from '@/api'
const CACHE_TTL_MS = 5 * 60 * 1000 // 5 minutes
let cachedResult: { aiEnabled: boolean; timestamp: number } | null = null
export function useCachedQuota() {
const [aiEnabled, setAiEnabled] = useState(cachedResult?.aiEnabled ?? false)
const [isLoading, setIsLoading] = useState(!cachedResult)
useEffect(() => {
if (cachedResult && Date.now() - cachedResult.timestamp < CACHE_TTL_MS) {
setAiEnabled(cachedResult.aiEnabled)
setIsLoading(false)
return
}
setIsLoading(true)
aiBuilderApi
.getQuota()
.then((q) => {
cachedResult = { aiEnabled: q.ai_enabled, timestamp: Date.now() }
setAiEnabled(q.ai_enabled)
})
.catch(() => {
// Leave as false
})
.finally(() => setIsLoading(false))
}, [])
return { aiEnabled, isLoading }
}
Step 2: Verify build
Run: cd /c/Dev/Projects/patherly/frontend && npx tsc --noEmit
Step 3: Commit
git add frontend/src/hooks/useCachedQuota.ts
git commit -m "feat: create useCachedQuota hook — 5-min TTL AI quota cache"
Task 7: Add pin button props to TreeGridView
Files:
- Modify:
frontend/src/components/library/TreeGridView.tsx
Step 1: Read current file to understand structure
Read frontend/src/components/library/TreeGridView.tsx fully before editing.
Step 2: Add optional pin props to interface
Add to TreeGridViewProps:
pinnedTreeIds?: Set<string>
onTogglePin?: (treeId: string) => void
pinLoadingTreeIds?: Set<string>
Step 3: Add star button to each card
Import Star from lucide-react. In each card's top-right area, render (only when onTogglePin is provided):
{onTogglePin && (
<button
onClick={(e) => {
e.stopPropagation()
e.preventDefault()
onTogglePin(tree.id)
}}
disabled={pinLoadingTreeIds?.has(tree.id)}
aria-label={pinnedTreeIds?.has(tree.id) ? 'Remove from favorites' : 'Add to favorites'}
className={cn(
'absolute top-3 right-3 rounded-md p-1 transition-colors',
pinnedTreeIds?.has(tree.id)
? 'text-amber-400 hover:text-amber-300'
: 'text-muted-foreground/40 hover:text-amber-400',
pinLoadingTreeIds?.has(tree.id) && 'opacity-50 pointer-events-none'
)}
>
<Star size={16} fill={pinnedTreeIds?.has(tree.id) ? 'currentColor' : 'none'} />
</button>
)}
Step 4: Verify build
Run: cd /c/Dev/Projects/patherly/frontend && npx tsc --noEmit
Step 5: Commit
git add frontend/src/components/library/TreeGridView.tsx
git commit -m "feat: add optional pin/favorite button to TreeGridView"
Task 8: Add pin button props to TreeListView
Files:
- Modify:
frontend/src/components/library/TreeListView.tsx
Step 1: Read the file, then add same optional props and star button pattern as Task 7.
Place the star button at the end of each row (before the actions area). Use the exact same props interface addition and button pattern from Task 7.
Step 2: Verify build
Run: cd /c/Dev/Projects/patherly/frontend && npx tsc --noEmit
Step 3: Commit
git add frontend/src/components/library/TreeListView.tsx
git commit -m "feat: add optional pin/favorite button to TreeListView"
Task 9: Add pin button props to TreeTableView
Files:
- Modify:
frontend/src/components/library/TreeTableView.tsx
Step 1: Read the file, then add same optional props.
Add a narrow "Favorite" column as the leftmost column. Header: star icon. Cell: same button pattern from Task 7. Only render the column when onTogglePin is provided.
Step 2: Verify build
Run: cd /c/Dev/Projects/patherly/frontend && npx tsc --noEmit
Step 3: Commit
git add frontend/src/components/library/TreeTableView.tsx
git commit -m "feat: add optional pin/favorite column to TreeTableView"
Task 10: TreeLibraryPage — Create dropdown + AI Builder + pin wiring
Files:
- Modify:
frontend/src/pages/TreeLibraryPage.tsx
Step 1: Add imports
import { ChevronDown, Sparkles, FolderTree, ListOrdered, Wrench } from 'lucide-react'
import { usePinnedFlowsStore, selectPinnedTreeIds, selectPinLoadingTreeIds } from '@/store/pinnedFlowsStore'
import { useCachedQuota } from '@/hooks/useCachedQuota'
import { AIFlowBuilderModal } from '@/components/ai-builder/AIFlowBuilderModal'
Step 2: Add state and store selectors
Inside the component, add:
const [showCreateMenu, setShowCreateMenu] = useState(false)
const [showAIBuilder, setShowAIBuilder] = useState(false)
const { aiEnabled } = useCachedQuota()
const pinnedTreeIds = usePinnedFlowsStore(selectPinnedTreeIds)
const pinLoadingTreeIds = usePinnedFlowsStore(selectPinLoadingTreeIds)
const togglePin = usePinnedFlowsStore((s) => s.toggle)
const loadPinned = usePinnedFlowsStore((s) => s.load)
useEffect(() => { loadPinned() }, [loadPinned])
Step 3: Replace the <Link> create button
Replace the single <Link to={...}> create button with the dropdown menu pattern from MyTreesPage.tsx (lines 134-197). Copy that exact pattern — it already has Troubleshooting Tree, Procedural Flow, Maintenance Flow, divider, and Build with AI (conditional on aiEnabled).
Step 4: Pass pin props to view components
Add to each TreeGridView, TreeListView, TreeTableView render:
pinnedTreeIds={pinnedTreeIds}
onTogglePin={togglePin}
pinLoadingTreeIds={pinLoadingTreeIds}
Step 5: Add AI Builder modal
Before the closing </div> of the component, add:
{showAIBuilder && (
<AIFlowBuilderModal
isOpen={showAIBuilder}
onClose={() => setShowAIBuilder(false)}
/>
)}
Step 6: Verify build
Run: cd /c/Dev/Projects/patherly/frontend && npx tsc --noEmit
Step 7: Commit
git add frontend/src/pages/TreeLibraryPage.tsx
git commit -m "feat: Library page — create dropdown with AI Builder + pin controls on all views"
Task 11: QuickStartPage — Favorites section + paginated My Flows
Files:
- Modify:
frontend/src/pages/QuickStartPage.tsx
This is the largest task. The page gets a major refactor.
Step 1: Read the current file fully (already done in exploration)
Step 2: Rewrite QuickStartPage
The page structure becomes:
- Page header with "Create" dropdown (same pattern as Task 10)
- QuickStats (keep existing)
- Search (keep existing inline search)
- Recent Sessions (keep existing
SessionsPanel) - Favorites section (NEW — from
pinnedFlowsStore.items) - My Flows section (NEW — paginated,
author_idfilter, view toggle)
Key implementation details:
Favorites section:
- Source:
usePinnedFlowsStoreitems - Layout: wrapping grid, max 2 rows (~8 cards). "View all favorites" expands if > 8.
- Each card: flow name + type emoji + unpin star button
- Skeleton: 4 pulse placeholder cards while
isLoading - Empty: "Star a flow to pin it here for quick access."
My Flows section:
- Source:
treesApi.list({ author_id: user.id, sort_by: 'updated_at', limit: pageSize + 1, skip: (page - 1) * pageSize }) - Use
usePaginationParams({ defaultPageSize: 10, allowedPageSizes: [10, 25, 50, 'all'] }) hasNextPage = response.length > pageSize; displayItems = response.slice(0, pageSize)- For "All": fetch in chunks of 100 up to 500 max
- View toggle:
ViewTogglebound todashboardMyFlowsView - Render:
TreeGridView/TreeListView/TreeTableViewwith pin props - Pagination controls:
Prev/Next/ page label / size dropdown - Skeleton: 6 placeholder cards/rows
- Empty: "You haven't created any flows yet." + "Create your first flow" CTA
Remove:
- The
FiltersBar(All, Recently Used, My Flows, Team Flows) — replaced by the Favorites + My Flows structure - The
SectionGroup"All Flows" wrapper - The hard-cap of 20 items
Step 3: Verify build
Run: cd /c/Dev/Projects/patherly/frontend && npm run build
Expected: Build succeeds with no errors
Step 4: Commit
git add frontend/src/pages/QuickStartPage.tsx
git commit -m "feat: dashboard — Favorites grid + paginated My Flows + skeletons + empty states"
Task 12: PinnedFlowsSection — Dual collapse + smooth transitions
Files:
- Modify:
frontend/src/components/sidebar/PinnedFlowsSection.tsx
Step 1: Read the file (already done)
Step 2: Implement dual collapse
Add state:
const [showAll, setShowAll] = useState(false)
const TRUNCATE_COUNT = 5
Modify header collapse to reset truncation:
const handleToggleCollapse = () => {
if (collapsed) {
setShowAll(false) // Reset to truncated on re-expand
}
setCollapsed(!collapsed)
}
Replace the flow list rendering:
const visibleFlows = showAll ? flows : flows.slice(0, TRUNCATE_COUNT)
const hasMore = flows.length > TRUNCATE_COUNT
Add click handler on flow buttons that auto-collapses:
onClick={() => {
setShowAll(false) // Collapse back to 5
navigate(getTreeNavigatePath(flow.tree_id, flow.tree_type))
}}
Add "Show more" / "Show less" link after the flow list:
{hasMore && !collapsed && (
<button
onClick={() => setShowAll(!showAll)}
className="px-3 py-1 text-xs text-muted-foreground hover:text-foreground transition-colors"
>
{showAll ? 'Show less' : `Show more (${flows.length})`}
</button>
)}
Add CSS transition on the list container:
<div
className="space-y-0.5 overflow-hidden transition-[max-height] duration-250 ease-out"
style={{ maxHeight: collapsed ? 0 : showAll ? `${flows.length * 40 + 40}px` : `${TRUNCATE_COUNT * 40 + 40}px` }}
>
Step 3: Verify build
Run: cd /c/Dev/Projects/patherly/frontend && npx tsc --noEmit
Step 4: Commit
git add frontend/src/components/sidebar/PinnedFlowsSection.tsx
git commit -m "feat: sidebar pinned section — dual collapse + show more/less + smooth transitions"
Task 13: Final build validation
Step 1: Full build check
Run: cd /c/Dev/Projects/patherly/frontend && npm run build
Expected: Build succeeds with zero errors
Step 2: Fix any type errors or import issues
If build fails, fix issues and amend the relevant commit.
Step 3: Commit any final fixes
git add -A
git commit -m "fix: resolve build errors from dashboard implementation"
Summary of files changed
| # | File | Action |
|---|---|---|
| 1 | frontend/src/api/pinnedFlows.ts |
Add pin() method |
| 2 | frontend/src/store/pinnedFlowsStore.ts |
New — Zustand pin store |
| 3 | frontend/src/store/userPreferencesStore.ts |
Add dashboardMyFlowsView |
| 4 | frontend/src/components/layout/Sidebar.tsx |
Use store instead of local state |
| 5 | frontend/src/hooks/usePaginationParams.ts |
New — URL-synced pagination |
| 6 | frontend/src/hooks/useCachedQuota.ts |
New — AI quota cache |
| 7 | frontend/src/components/library/TreeGridView.tsx |
Optional pin button |
| 8 | frontend/src/components/library/TreeListView.tsx |
Optional pin button |
| 9 | frontend/src/components/library/TreeTableView.tsx |
Optional pin column |
| 10 | frontend/src/pages/TreeLibraryPage.tsx |
Create dropdown + AI Builder + pins |
| 11 | frontend/src/pages/QuickStartPage.tsx |
Major refactor: Favorites + My Flows |
| 12 | frontend/src/components/sidebar/PinnedFlowsSection.tsx |
Dual collapse |