Move completed plan docs to docs/plans/archive/. Add survey migration 046 and reference HTML/plan files. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
16 KiB
Dashboard: My Flows, Favorites/Pin UI, and AI Builder in Create Menu
Final Implementation Plan (Reviewed & Merged)
Decisions Locked
- "My Flows" = trees where
author_id = currentUser.id(includes forks, since forks set the forking user as author). - Pagination =
Prev / Nextwith page-size selector (10 / 25 / 50 / All). No numbered total pages — the/treesAPI returns no total count. Page and page size are synced to URL query params (?page=3&size=25). - 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. - "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."
- Dashboard view preference = separate key (
dashboardMyFlowsView) from Library view preference. The two are independent. - 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).
- Max pins = 15, enforced by backend. Frontend handles 409 conflict with user-facing toast.
- AI Builder quota = cached with 5-minute TTL. Not re-fetched on every page load.
- Favorites layout = compact wrapping grid, max 2 rows (~8 cards visible). "View all" expand link if more than 8 pinned flows.
- Loading states = skeleton loaders for both Favorites and My Flows sections during initial fetch. Meaningful empty states with CTAs for new users.
- Accessibility = all pin/favorite buttons include
aria-label(dynamic: "Add to favorites" / "Remove from favorites"),e.stopPropagation(), ande.preventDefault().
Known API Constraints (No Backend Changes)
GET /treesreturnsTreeListItem[]with no total count metadata. Pagination usesskip+limitparams (maxlimitis 100).POST /trees/{id}/pin,DELETE /trees/{id}/pin,GET /trees/pinned,PATCH /trees/pinned/reorderall exist and work.pinnedFlowsApialready haslist()andunpin(). Needspin()added.- Backend enforces
MAX_PINNED_FLOWS=15and 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
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 listisLoaded: boolean— whether initial fetch has completedisLoading: boolean— whether initial fetch is in progressisMutatingByTreeId: Record<string, boolean>— per-tree mutation trackingerror: string | null
Actions:
load(force?: boolean)— fetch from API. Skip ifisLoadedunlessforce=true.pin(treeId: string)— optimistic add toitems, 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 fromitems, call API, rollback + toast on failure.toggle(treeId: string)— callspinorunpinbased on current state.
Derived:
isPinned(treeId: string): booleanpinnedTreeIds: Set<string>— for passing as prop to view componentspinLoadingTreeIds: Set<string>— derived fromisMutatingByTreeIdfor 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/useEffectfor pinned flows (currently mount-only fetch) - Import and use
usePinnedFlowsStoreselectors 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
treeLibraryViewpreference
1e. Create pagination params hook
File: frontend/src/hooks/usePaginationParams.ts (new)
A reusable hook that syncs pagination state to URL query params:
// 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
pageandsizefrom URL search params on mount - Falls back to defaults if missing or invalid
setPageSizeresetspageto 1 (changing page size while on page 3 is confusing)- Validates
pageis a positive integer,sizeis one of the allowed values - Uses
useSearchParamsfrom React Router
1f. Create cached quota hook
File: frontend/src/hooks/useCachedQuota.ts (new)
// 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:
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):
<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):
- Troubleshooting Tree
- Procedural Flow
- Maintenance Flow
<divider>- Build with AI (only shown when
aiEnabledis 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:
showAIBuilderboolean, toggled by menu item click
3c. Wire view components to pin store
- Import
usePinnedFlowsStore - Call
store.load()on mount - Pass
pinnedTreeIds,onTogglePin: store.toggle, andpinLoadingTreeIdsto 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:
// 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, thenskip=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"
SectionGroupwrapper - 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:
-
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.
-
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-heighttransition on the list container:transition: max-height 250ms ease-out - Keep it subtle — no dramatic animations
Test Cases
pinnedFlowsStore unit tests:
load()populatesitemsfrom APIload()skips fetch whenisLoaded=true(unlessforce=true)toggle()pins an unpinned tree, unpins a pinned tree- Optimistic update:
itemsupdates immediately before API resolves - Rollback:
itemsreverts if API call fails - 409 conflict: shows error toast, does not add to
items - Sidebar + Dashboard selectors reflect same state after mutation
pinnedTreeIdsderived set updates correctly
usePaginationParams hook tests:
- Reads
pageandsizefrom URL on mount - Falls back to defaults when URL params missing or invalid
setPageSizeresets 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_idfilter - Numeric page size: requests
limit=size+1, displayssizeresults - "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 testcd 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 buildwith no errors