# 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` — 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` — for passing as prop to view components - `pinLoadingTreeIds: Set` — 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 onTogglePin?: (treeId: string) => void pinLoadingTreeIds?: Set ``` Props are optional so these components remain backward-compatible with any page that doesn't use pins. #### Pin button pattern (all three views): ```tsx ``` #### 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 `` 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. `` 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