Files
resolutionflow/docs/plans/2026-02-20-final-dashboard-plan.md
Michael Chihlas 045fdd6200 docs: dashboard design doc and implementation plan
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 21:24:26 -05:00

16 KiB

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

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:

// 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)

// 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

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:
    // 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