docs: dashboard design doc and implementation plan
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
745
docs/plans/2026-02-20-dashboard-implementation-plan.md
Normal file
745
docs/plans/2026-02-20-dashboard-implementation-plan.md
Normal file
@@ -0,0 +1,745 @@
|
||||
# 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:
|
||||
|
||||
```typescript
|
||||
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**
|
||||
|
||||
```bash
|
||||
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`:
|
||||
|
||||
```typescript
|
||||
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**
|
||||
|
||||
```bash
|
||||
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:
|
||||
```typescript
|
||||
dashboardMyFlowsView: 'grid' | 'list' | 'table'
|
||||
setDashboardMyFlowsView: (view: 'grid' | 'list' | 'table') => void
|
||||
```
|
||||
|
||||
In the store implementation, add alongside existing fields:
|
||||
```typescript
|
||||
dashboardMyFlowsView: 'grid',
|
||||
setDashboardMyFlowsView: (view) => set({ dashboardMyFlowsView: view }),
|
||||
```
|
||||
|
||||
**Step 2: Verify build**
|
||||
|
||||
Run: `cd /c/Dev/Projects/patherly/frontend && npx tsc --noEmit`
|
||||
|
||||
**Step 3: Commit**
|
||||
|
||||
```bash
|
||||
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:
|
||||
- `pinnedFlowsApi` import
|
||||
- `PinnedFlow` type import
|
||||
|
||||
Add import:
|
||||
```typescript
|
||||
import { usePinnedFlowsStore } from '@/store/pinnedFlowsStore'
|
||||
```
|
||||
|
||||
Remove from component:
|
||||
- `const [pinnedFlows, setPinnedFlows] = useState<PinnedFlow[]>([])`
|
||||
- The `pinnedFlowsApi.list()` call from the `useEffect`
|
||||
- The entire `handleUnpin` function
|
||||
|
||||
Replace with:
|
||||
```typescript
|
||||
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:
|
||||
```typescript
|
||||
useEffect(() => { loadPinned() }, [loadPinned])
|
||||
```
|
||||
|
||||
Update `PinnedFlowsSection` props:
|
||||
```tsx
|
||||
<PinnedFlowsSection flows={pinnedItems} onUnpin={unpinFlow} />
|
||||
```
|
||||
|
||||
**Step 2: Verify build**
|
||||
|
||||
Run: `cd /c/Dev/Projects/patherly/frontend && npx tsc --noEmit`
|
||||
|
||||
**Step 3: Commit**
|
||||
|
||||
```bash
|
||||
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**
|
||||
|
||||
```typescript
|
||||
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**
|
||||
|
||||
```bash
|
||||
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**
|
||||
|
||||
```typescript
|
||||
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**
|
||||
|
||||
```bash
|
||||
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`:
|
||||
```typescript
|
||||
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):
|
||||
|
||||
```tsx
|
||||
{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**
|
||||
|
||||
```bash
|
||||
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**
|
||||
|
||||
```bash
|
||||
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**
|
||||
|
||||
```bash
|
||||
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**
|
||||
|
||||
```typescript
|
||||
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:
|
||||
```typescript
|
||||
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:
|
||||
```tsx
|
||||
pinnedTreeIds={pinnedTreeIds}
|
||||
onTogglePin={togglePin}
|
||||
pinLoadingTreeIds={pinLoadingTreeIds}
|
||||
```
|
||||
|
||||
**Step 5: Add AI Builder modal**
|
||||
|
||||
Before the closing `</div>` of the component, add:
|
||||
```tsx
|
||||
{showAIBuilder && (
|
||||
<AIFlowBuilderModal
|
||||
isOpen={showAIBuilder}
|
||||
onClose={() => setShowAIBuilder(false)}
|
||||
/>
|
||||
)}
|
||||
```
|
||||
|
||||
**Step 6: Verify build**
|
||||
|
||||
Run: `cd /c/Dev/Projects/patherly/frontend && npx tsc --noEmit`
|
||||
|
||||
**Step 7: Commit**
|
||||
|
||||
```bash
|
||||
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:
|
||||
|
||||
1. **Page header** with "Create" dropdown (same pattern as Task 10)
|
||||
2. **QuickStats** (keep existing)
|
||||
3. **Search** (keep existing inline search)
|
||||
4. **Recent Sessions** (keep existing `SessionsPanel`)
|
||||
5. **Favorites section** (NEW — from `pinnedFlowsStore.items`)
|
||||
6. **My Flows section** (NEW — paginated, `author_id` filter, view toggle)
|
||||
|
||||
Key implementation details:
|
||||
|
||||
**Favorites section:**
|
||||
- Source: `usePinnedFlowsStore` items
|
||||
- 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: `ViewToggle` bound to `dashboardMyFlowsView`
|
||||
- Render: `TreeGridView` / `TreeListView` / `TreeTableView` with 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**
|
||||
|
||||
```bash
|
||||
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:
|
||||
```typescript
|
||||
const [showAll, setShowAll] = useState(false)
|
||||
const TRUNCATE_COUNT = 5
|
||||
```
|
||||
|
||||
Modify header collapse to reset truncation:
|
||||
```typescript
|
||||
const handleToggleCollapse = () => {
|
||||
if (collapsed) {
|
||||
setShowAll(false) // Reset to truncated on re-expand
|
||||
}
|
||||
setCollapsed(!collapsed)
|
||||
}
|
||||
```
|
||||
|
||||
Replace the flow list rendering:
|
||||
```typescript
|
||||
const visibleFlows = showAll ? flows : flows.slice(0, TRUNCATE_COUNT)
|
||||
const hasMore = flows.length > TRUNCATE_COUNT
|
||||
```
|
||||
|
||||
Add click handler on flow buttons that auto-collapses:
|
||||
```typescript
|
||||
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:
|
||||
```tsx
|
||||
{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:
|
||||
```tsx
|
||||
<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**
|
||||
|
||||
```bash
|
||||
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**
|
||||
|
||||
```bash
|
||||
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 |
|
||||
362
docs/plans/2026-02-20-final-dashboard-plan.md
Normal file
362
docs/plans/2026-02-20-final-dashboard-plan.md
Normal file
@@ -0,0 +1,362 @@
|
||||
# Dashboard: My Flows, Favorites/Pin UI, and AI Builder in Create Menu
|
||||
|
||||
## Final Implementation Plan (Reviewed & Merged)
|
||||
|
||||
### Decisions Locked
|
||||
|
||||
1. **"My Flows"** = trees where `author_id = currentUser.id` (includes forks, since forks set the forking user as author).
|
||||
2. **Pagination** = `Prev / Next` with page-size selector (10 / 25 / 50 / All). No numbered total pages — the `/trees` API returns no total count. Page and page size are synced to URL query params (`?page=3&size=25`).
|
||||
3. **Pin state** = shared Zustand store (`pinnedFlowsStore`), used by Sidebar, Dashboard, and Library. No local state for pins anywhere. **This store owns pin CRUD only — no other state belongs here.**
|
||||
4. **"Show: All"** = fetches in chunks of 100, capped at 500 items maximum. If ceiling is reached, show message: "Showing first 500 flows. Use search or filters to find specific flows."
|
||||
5. **Dashboard view preference** = separate key (`dashboardMyFlowsView`) from Library view preference. The two are independent.
|
||||
6. **Sidebar pinned section** = two independent collapse states: header collapse (hide/show entire section) and list truncation (5 vs. all). When header is collapsed and re-expanded, list resets to 5 items (truncated default).
|
||||
7. **Max pins** = 15, enforced by backend. Frontend handles 409 conflict with user-facing toast.
|
||||
8. **AI Builder quota** = cached with 5-minute TTL. Not re-fetched on every page load.
|
||||
9. **Favorites layout** = compact wrapping grid, max 2 rows (~8 cards visible). "View all" expand link if more than 8 pinned flows.
|
||||
10. **Loading states** = skeleton loaders for both Favorites and My Flows sections during initial fetch. Meaningful empty states with CTAs for new users.
|
||||
11. **Accessibility** = all pin/favorite buttons include `aria-label` (dynamic: "Add to favorites" / "Remove from favorites"), `e.stopPropagation()`, and `e.preventDefault()`.
|
||||
|
||||
### Known API Constraints (No Backend Changes)
|
||||
|
||||
- `GET /trees` returns `TreeListItem[]` with no total count metadata. Pagination uses `skip` + `limit` params (max `limit` is 100).
|
||||
- `POST /trees/{id}/pin`, `DELETE /trees/{id}/pin`, `GET /trees/pinned`, `PATCH /trees/pinned/reorder` all exist and work.
|
||||
- `pinnedFlowsApi` already has `list()` and `unpin()`. Needs `pin()` added.
|
||||
- Backend enforces `MAX_PINNED_FLOWS=15` and returns 409 on conflict.
|
||||
|
||||
### Files Modified
|
||||
|
||||
| File | Phase | Change |
|
||||
|------|-------|--------|
|
||||
| `frontend/src/api/pinnedFlows.ts` | 1 | Add `pin()` method |
|
||||
| `frontend/src/store/pinnedFlowsStore.ts` | 1 | **New file.** Zustand store — single source of truth for all pin state |
|
||||
| `frontend/src/store/userPreferencesStore.ts` | 1 | Add `dashboardMyFlowsView` preference + setter |
|
||||
| `frontend/src/hooks/usePaginationParams.ts` | 1 | **New file.** Custom hook — reads/writes `page` and `size` URL query params |
|
||||
| `frontend/src/hooks/useCachedQuota.ts` | 1 | **New file.** Custom hook — fetches AI quota with 5-min TTL cache |
|
||||
| `frontend/src/components/layout/Sidebar.tsx` | 1 | Replace local pinned state with `pinnedFlowsStore` selectors |
|
||||
| `frontend/src/components/library/TreeGridView.tsx` | 2 | Add optional pin props + star button with aria-label |
|
||||
| `frontend/src/components/library/TreeListView.tsx` | 2 | Add optional pin props + star button with aria-label |
|
||||
| `frontend/src/components/library/TreeTableView.tsx` | 2 | Add optional pin props + star/favorite column with aria-label |
|
||||
| `frontend/src/pages/TreeLibraryPage.tsx` | 3 | Replace Create link with dropdown menu + AI Builder (cached quota) + wire pin store |
|
||||
| `frontend/src/pages/QuickStartPage.tsx` | 4 | Major refactor: Favorites grid + paginated My Flows + skeletons + empty states |
|
||||
| `frontend/src/components/sidebar/PinnedFlowsSection.tsx` | 5 | Dual collapse states + reset-on-reexpand + auto-collapse on navigation |
|
||||
|
||||
---
|
||||
|
||||
### Phase 1: Infrastructure (Stores, Hooks, API)
|
||||
|
||||
**Goal:** Build the shared state and utility layers that every subsequent phase depends on.
|
||||
|
||||
#### 1a. Add `pin()` to API client
|
||||
|
||||
**File:** `frontend/src/api/pinnedFlows.ts`
|
||||
|
||||
```typescript
|
||||
pin: async (treeId: string) => apiClient.post(`/trees/${treeId}/pin`)
|
||||
```
|
||||
|
||||
#### 1b. Create `pinnedFlowsStore`
|
||||
|
||||
**File:** `frontend/src/store/pinnedFlowsStore.ts` (new)
|
||||
|
||||
State:
|
||||
- `items: PinnedFlow[]` — the pinned flows list
|
||||
- `isLoaded: boolean` — whether initial fetch has completed
|
||||
- `isLoading: boolean` — whether initial fetch is in progress
|
||||
- `isMutatingByTreeId: Record<string, boolean>` — per-tree mutation tracking
|
||||
- `error: string | null`
|
||||
|
||||
Actions:
|
||||
- `load(force?: boolean)` — fetch from API. Skip if `isLoaded` unless `force=true`.
|
||||
- `pin(treeId: string)` — optimistic add to `items`, call API, rollback + toast on failure. On 409: toast "Maximum of 15 favorites reached. Unpin a flow to add a new one." and do not add to items.
|
||||
- `unpin(treeId: string)` — optimistic remove from `items`, call API, rollback + toast on failure.
|
||||
- `toggle(treeId: string)` — calls `pin` or `unpin` based on current state.
|
||||
|
||||
Derived:
|
||||
- `isPinned(treeId: string): boolean`
|
||||
- `pinnedTreeIds: Set<string>` — for passing as prop to view components
|
||||
- `pinLoadingTreeIds: Set<string>` — derived from `isMutatingByTreeId` for disabling buttons
|
||||
|
||||
**Scope guardrail:** This store owns pin CRUD and derived pin state only. Dashboard layout preferences belong in `userPreferencesStore`. Recently viewed flows or other concerns belong in separate stores/hooks.
|
||||
|
||||
#### 1c. Replace local pin state in Sidebar
|
||||
|
||||
**File:** `frontend/src/components/layout/Sidebar.tsx`
|
||||
|
||||
- Remove local `useState` / `useEffect` for pinned flows (currently mount-only fetch)
|
||||
- Import and use `usePinnedFlowsStore` selectors and actions
|
||||
- Call `pinnedFlowsStore.load()` on mount (store handles deduplication)
|
||||
|
||||
#### 1d. Add dashboard view preference
|
||||
|
||||
**File:** `frontend/src/store/userPreferencesStore.ts`
|
||||
|
||||
- New field: `dashboardMyFlowsView: 'grid' | 'list' | 'table'` (default: `'grid'`)
|
||||
- New setter: `setDashboardMyFlowsView`
|
||||
- Persisted to localStorage alongside existing preferences
|
||||
- This is independent from the existing `treeLibraryView` preference
|
||||
|
||||
#### 1e. Create pagination params hook
|
||||
|
||||
**File:** `frontend/src/hooks/usePaginationParams.ts` (new)
|
||||
|
||||
A reusable hook that syncs pagination state to URL query params:
|
||||
|
||||
```typescript
|
||||
// Usage:
|
||||
const { page, pageSize, setPage, setPageSize } = usePaginationParams({
|
||||
defaultPageSize: 10,
|
||||
allowedPageSizes: [10, 25, 50, 'all'],
|
||||
})
|
||||
// URL: /dashboard?page=2&size=25
|
||||
// Handles: invalid values (falls back to defaults), page reset when size changes
|
||||
```
|
||||
|
||||
Behavior:
|
||||
- Reads `page` and `size` from URL search params on mount
|
||||
- Falls back to defaults if missing or invalid
|
||||
- `setPageSize` resets `page` to 1 (changing page size while on page 3 is confusing)
|
||||
- Validates `page` is a positive integer, `size` is one of the allowed values
|
||||
- Uses `useSearchParams` from React Router
|
||||
|
||||
#### 1f. Create cached quota hook
|
||||
|
||||
**File:** `frontend/src/hooks/useCachedQuota.ts` (new)
|
||||
|
||||
```typescript
|
||||
// Usage:
|
||||
const { aiEnabled, isLoading } = useCachedQuota()
|
||||
```
|
||||
|
||||
Behavior:
|
||||
- On first call, fetches `aiBuilderApi.getQuota()`
|
||||
- Caches result in module-level variable with timestamp
|
||||
- Subsequent calls within 5 minutes return cached value (no API call)
|
||||
- After 5 minutes, re-fetches on next call
|
||||
- Returns `{ aiEnabled: boolean, isLoading: boolean }`
|
||||
|
||||
---
|
||||
|
||||
### Phase 2: Pin/Unpin Controls in Library Views
|
||||
|
||||
**Goal:** Add favorite buttons to all three view components without breaking existing behavior.
|
||||
|
||||
**Files:** `TreeGridView.tsx`, `TreeListView.tsx`, `TreeTableView.tsx`
|
||||
|
||||
#### New optional props on all three:
|
||||
|
||||
```typescript
|
||||
pinnedTreeIds?: Set<string>
|
||||
onTogglePin?: (treeId: string) => void
|
||||
pinLoadingTreeIds?: Set<string>
|
||||
```
|
||||
|
||||
Props are optional so these components remain backward-compatible with any page that doesn't use pins.
|
||||
|
||||
#### Pin button pattern (all three views):
|
||||
|
||||
```tsx
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
onTogglePin?.(treeId);
|
||||
}}
|
||||
disabled={pinLoadingTreeIds?.has(treeId)}
|
||||
aria-label={pinnedTreeIds?.has(treeId) ? "Remove from favorites" : "Add to favorites"}
|
||||
className={/* reduced opacity when disabled */}
|
||||
>
|
||||
{pinnedTreeIds?.has(treeId) ? <StarFilledIcon /> : <StarOutlineIcon />}
|
||||
</button>
|
||||
```
|
||||
|
||||
#### View-specific placement:
|
||||
- **Grid:** Star icon in top-right corner of card
|
||||
- **List:** Star icon at the end of each row
|
||||
- **Table:** Dedicated narrow "Favorite" column (leftmost)
|
||||
|
||||
**Critical:** `e.stopPropagation()` + `e.preventDefault()` prevents the click from triggering card/row navigation. Button is disabled (reduced opacity, no pointer events) while that tree's mutation is in-flight.
|
||||
|
||||
---
|
||||
|
||||
### Phase 3: TreeLibraryPage — Create Menu + Pin Wiring
|
||||
|
||||
**Goal:** Add AI Builder access and connect Library page to shared pin store.
|
||||
|
||||
**File:** `frontend/src/pages/TreeLibraryPage.tsx`
|
||||
|
||||
#### 3a. Replace Create link with dropdown
|
||||
|
||||
Replace the single `<Link>` create button with a dropdown menu (same visual pattern as `MyTreesPage`'s `showCreateMenu`).
|
||||
|
||||
Menu items (fixed order):
|
||||
1. Troubleshooting Tree
|
||||
2. Procedural Flow
|
||||
3. Maintenance Flow
|
||||
4. `<divider>`
|
||||
5. Build with AI *(only shown when `aiEnabled` is true)*
|
||||
|
||||
#### 3b. AI Builder integration
|
||||
|
||||
- Use `useCachedQuota()` hook (from Phase 1f) — no fetch-on-mount, uses cached value
|
||||
- Import and render `AIFlowBuilderModal` (already built)
|
||||
- Modal state: `showAIBuilder` boolean, toggled by menu item click
|
||||
|
||||
#### 3c. Wire view components to pin store
|
||||
|
||||
- Import `usePinnedFlowsStore`
|
||||
- Call `store.load()` on mount
|
||||
- Pass `pinnedTreeIds`, `onTogglePin: store.toggle`, and `pinLoadingTreeIds` to active view component
|
||||
|
||||
---
|
||||
|
||||
### Phase 4: QuickStartPage Refactor — Favorites + My Flows
|
||||
|
||||
**Goal:** Transform the dashboard from a flat "All Flows" dump into a structured personal workspace.
|
||||
|
||||
**File:** `frontend/src/pages/QuickStartPage.tsx`
|
||||
|
||||
#### 4a. Favorites Section (above My Flows)
|
||||
|
||||
**Layout:** Compact wrapping grid. Max 2 visible rows (~8 cards). If more than 8 pinned flows, show "View all favorites" link that expands to show all.
|
||||
|
||||
**Data source:** `pinnedFlowsStore.items`, ordered by `display_order`.
|
||||
|
||||
**Each card:** Click to navigate + unpin button (star icon, same pattern as Phase 2).
|
||||
|
||||
**Section header:** "Favorites" with count badge (e.g., "Favorites (7)").
|
||||
|
||||
**Loading state:** Skeleton loader — 4 placeholder cards with pulse animation, matching the grid layout dimensions so content doesn't shift when data arrives.
|
||||
|
||||
**Empty state:** Subtle message: "Star a flow to pin it here for quick access." No CTA button — the action is contextual (you star flows from the Library or My Flows).
|
||||
|
||||
#### 4b. My Flows Section (replaces "All Flows")
|
||||
|
||||
**Section header:** "My Flows"
|
||||
|
||||
**Data source:** `treesApi.list({ author_id: currentUser.id, sort_by: 'updated_at', skip, limit })`
|
||||
|
||||
**Pagination (via `usePaginationParams` hook):**
|
||||
- Page size selector: dropdown with 10 (default), 25, 50, All
|
||||
- For numeric sizes:
|
||||
```typescript
|
||||
// Request one extra item to detect if there's a next page.
|
||||
// If response.length > pageSize, a next page exists.
|
||||
// We only display the first `pageSize` items.
|
||||
const response = await treesApi.list({
|
||||
author_id: currentUser.id,
|
||||
limit: pageSize + 1,
|
||||
skip: (page - 1) * pageSize,
|
||||
});
|
||||
const hasNextPage = response.length > pageSize;
|
||||
const displayItems = response.slice(0, pageSize);
|
||||
```
|
||||
- For "All": fetch in chunks of 100 (`skip=0, limit=100`, then `skip=100`, etc.) until response returns fewer than 100 items OR 500 items total reached. If ceiling hit, show: "Showing first 500 flows. Use search or filters to find specific flows."
|
||||
- Controls: `Prev` (disabled on page 1) / `Next` (disabled when `!hasNextPage`) / current page label / size dropdown
|
||||
- URL synced: `?page=2&size=25` — changing page size resets to page 1
|
||||
|
||||
**View toggle:** Reuse `ViewToggle` component, bound to `dashboardMyFlowsView` preference (independent from Library).
|
||||
|
||||
**Render:** Pass `TreeGridView` / `TreeListView` / `TreeTableView` with pin props from store.
|
||||
|
||||
**Loading state:** Skeleton loader — 6 placeholder cards/rows matching the active view type (grid skeleton for grid view, row skeletons for list/table view).
|
||||
|
||||
**Empty state:** "You haven't created any flows yet." with a CTA button: "Create your first flow" that triggers the Create dropdown menu (same options as TreeLibraryPage: Troubleshooting Tree, Procedural Flow, Maintenance Flow, divider, Build with AI).
|
||||
|
||||
#### 4c. Cleanup
|
||||
|
||||
- Remove the current hard-cap of 20 items
|
||||
- Remove the "All Flows" `SectionGroup` wrapper
|
||||
- Keep existing stats cards, recent sessions, and search panels unless explicitly removed later
|
||||
|
||||
---
|
||||
|
||||
### Phase 5: Sidebar PinnedFlowsSection — Dual Collapse
|
||||
|
||||
**Goal:** Make the sidebar pinned section polished without taking over the sidebar.
|
||||
|
||||
**File:** `frontend/src/components/sidebar/PinnedFlowsSection.tsx`
|
||||
|
||||
#### Two independent collapse states:
|
||||
|
||||
1. **Header collapse:** Click section header → hides/shows entire pinned flows area (existing behavior, keep it). When re-expanding after a collapse, **always reset list truncation to 5 items.**
|
||||
|
||||
2. **List truncation:** When section is expanded:
|
||||
- Show first 5 pinned flows by default
|
||||
- "Show more (X)" link at bottom expands to show all (X = total count)
|
||||
- "Show less" link collapses back to 5
|
||||
- Clicking a pinned flow link: navigate AND auto-collapse back to 5
|
||||
|
||||
#### Smooth transitions:
|
||||
|
||||
- CSS `max-height` transition on the list container: `transition: max-height 250ms ease-out`
|
||||
- Keep it subtle — no dramatic animations
|
||||
|
||||
---
|
||||
|
||||
### Test Cases
|
||||
|
||||
#### `pinnedFlowsStore` unit tests:
|
||||
- `load()` populates `items` from API
|
||||
- `load()` skips fetch when `isLoaded=true` (unless `force=true`)
|
||||
- `toggle()` pins an unpinned tree, unpins a pinned tree
|
||||
- Optimistic update: `items` updates immediately before API resolves
|
||||
- Rollback: `items` reverts if API call fails
|
||||
- 409 conflict: shows error toast, does not add to `items`
|
||||
- Sidebar + Dashboard selectors reflect same state after mutation
|
||||
- `pinnedTreeIds` derived set updates correctly
|
||||
|
||||
#### `usePaginationParams` hook tests:
|
||||
- Reads `page` and `size` from URL on mount
|
||||
- Falls back to defaults when URL params missing or invalid
|
||||
- `setPageSize` resets page to 1
|
||||
- Invalid page (negative, zero, non-number) falls back to 1
|
||||
|
||||
#### `useCachedQuota` hook tests:
|
||||
- First call fetches from API
|
||||
- Second call within 5 minutes returns cached value (no API call)
|
||||
- Call after 5 minutes re-fetches
|
||||
|
||||
#### `PinnedFlowsSection` component tests:
|
||||
- Shows max 5 items by default
|
||||
- "Show more" reveals all items
|
||||
- Clicking a flow collapses list back to 5
|
||||
- Header collapse hides entire section
|
||||
- Re-expanding after header collapse resets to 5 items
|
||||
|
||||
#### `QuickStartPage` integration tests:
|
||||
- My Flows uses `author_id` filter
|
||||
- Numeric page size: requests `limit=size+1`, displays `size` results
|
||||
- "All" fetches iteratively, stops at 500 ceiling
|
||||
- Favorites section updates immediately after pin/unpin
|
||||
- Empty state shows CTA when user has zero flows
|
||||
- Skeleton loaders appear during fetch
|
||||
- URL params update when page/size changes
|
||||
|
||||
#### `TreeLibraryPage` tests:
|
||||
- Create dropdown renders all flow type options
|
||||
- "Build with AI" only shown when `aiEnabled=true`
|
||||
- "Build with AI" opens `AIFlowBuilderModal`
|
||||
- Pin buttons work and sync with store
|
||||
|
||||
#### Regression:
|
||||
- `cd frontend && npm run test`
|
||||
- `cd frontend && npm run build`
|
||||
|
||||
### Manual Verification Checklist
|
||||
|
||||
- [ ] Dashboard: Favorites section shows pinned flows in compact grid, My Flows shows paginated authored flows
|
||||
- [ ] Pin a flow on Library page → appears in Dashboard Favorites AND Sidebar immediately (no navigation)
|
||||
- [ ] Unpin from Dashboard → removed from Sidebar immediately
|
||||
- [ ] Page size dropdown: 10/25/50/All all work, Prev/Next show/hide correctly
|
||||
- [ ] "Show All" stops at 500 items with message if ceiling hit
|
||||
- [ ] Change page → URL updates to `?page=X&size=Y`. Refresh → same page/size restored.
|
||||
- [ ] View toggle on Dashboard is independent from Library view toggle
|
||||
- [ ] Sidebar: max 5 shown, "Show more" expands, clicking a flow collapses and navigates
|
||||
- [ ] Collapse sidebar section → re-expand → list is back to 5 (not "show all")
|
||||
- [ ] Try to pin a 16th flow → toast about max limit, flow not pinned
|
||||
- [ ] AI Builder: Library page "Create New" → "Build with AI" opens modal (uses cached quota)
|
||||
- [ ] New user with zero flows: sees empty state with "Create your first flow" CTA
|
||||
- [ ] New user with zero favorites: sees "Star a flow to pin it here" message
|
||||
- [ ] During initial load: skeleton placeholders visible, no layout shift when data arrives
|
||||
- [ ] Pin button: screen reader announces "Add to favorites" / "Remove from favorites"
|
||||
- [ ] Build passes: `cd frontend && npm run build` with no errors
|
||||
Reference in New Issue
Block a user