Skip to content

feat(@helm/web): multi-product dashboard with tabs and URL routing (Session 8)#13

Merged
lhpaul merged 6 commits into
developfrom
feature/session-8-multi-product-dashboard
May 19, 2026
Merged

feat(@helm/web): multi-product dashboard with tabs and URL routing (Session 8)#13
lhpaul merged 6 commits into
developfrom
feature/session-8-multi-product-dashboard

Conversation

@lhpaul
Copy link
Copy Markdown
Owner

@lhpaul lhpaul commented May 19, 2026

Summary

  • API client (src/lib/api.ts): listProducts(), getProductBySlug(slug), listItemsForProduct(slug) targeting the new /api/products endpoints. Legacy methods kept for backward compat.
  • ProductTabs (src/components/ProductTabs.tsx): horizontal tabs from GET /api/products, polls every 5s, marks active tab via useParams slug, links to /products/:slug.
  • Routing (src/App.tsx): /products/:slug → Kanban, /products/:slug/items/:externalId → ItemDetail. ProductsRedirect resolves first product slug; shows empty state if registry is empty. LegacyItemRedirect maps old /items/:id to new path for backward compat.
  • Kanban refactor: reads slug from useParams, calls getProductBySlug + listItemsForProduct, renders ProductTabs above board. Invalid slug → error state with back link. useMemo moved before early returns (Rules of Hooks fix).
  • ItemDetail refactor: reads slug + externalId from useParams, back link resolves to /products/:slug.

Test plan

  • pnpm --filter @helm/web test — 32 tests pass
  • pnpm --filter @helm/web build — clean

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Multi-product navigation: product-specific boards, item pages, and automatic redirects from legacy item/bookmark URLs
    • Product tabs for switching between registered products with loading, empty, and error states
    • Item detail and kanban views scoped to the selected product with contextual "Back" navigation
  • Tests

    • Added tests covering routing, redirects, tabs, kanban rendering, item links, polling, and API behaviors

Review Change Stack

lhpaul and others added 4 commits May 19, 2026 09:58
listProducts() → GET /api/products
getProductBySlug(slug) → GET /api/products/:slug
listItemsForProduct(slug) → GET /api/products/:slug/items

Legacy methods (getProduct, listItems, getItem) kept for backward compat
until cleanup PR. 6 new tests covering happy paths, URL encoding, 404,
network error, and empty response.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Renders horizontal tabs from GET /api/products, marks active tab via
useParams slug, links to /products/:slug. Polls every 5s to surface
newly-registered products without reload. Loading: blank bar (no layout
shift). Error: inline ⚠ message. Empty registry: renders null.
6 tests covering: render, active marking, hrefs, error, empty, re-poll.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…roduct

App.tsx: /products/:slug → Kanban, /products/:slug/items/:externalId →
ItemDetail. ProductsRedirect resolves first product slug and navigates;
shows empty state if registry is empty. LegacyItemRedirect maps old
/items/:id to the new path for backward compat.

Kanban: reads slug from useParams, calls getProductBySlug + listItemsForProduct,
renders ProductTabs above the board. Product not found → inline error with
back link. ItemCard links updated to /products/:slug/items/:externalId.

ItemDetail: reads slug + externalId from useParams, back link resolves to
/products/:slug. Error and loading states unchanged.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
useMemo was called after early returns in Kanban — violates Rules of Hooks.
Moved before loading/error guards so it's always called unconditionally.

Tests added:
- Kanban.test.tsx: 5 cases — columns, item card, correct href, error+back
  link, empty items. Uses MemoryRouter with /products/:slug route.
- App.test.tsx: 2 cases — empty registry shows empty state, redirect to
  /products/:firstSlug when products exist.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 19, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: cf7252e7-7028-40e6-9fa4-103318bc8e39

📥 Commits

Reviewing files that changed from the base of the PR and between 55d3501 and 2589741.

📒 Files selected for processing (3)
  • apps/web/src/App.test.tsx
  • apps/web/src/lib/api.test.ts
  • apps/web/src/views/ItemDetail.tsx

📝 Walkthrough

Walkthrough

Multi-product routing is introduced: client API gained product-scoped methods, ProductTabs provides product navigation, App adds redirect helpers and new routes, and Kanban/ItemDetail were refactored to use slug-scoped data. Tests were added across components and API client.

Changes

Multi-product routing migration

Layer / File(s) Summary
API client expansion for multi-product support
apps/web/src/lib/api.ts, apps/web/src/lib/api.test.ts
Three new API methods (listProducts, getProductBySlug, listItemsForProduct) are added with Vitest coverage for success and error paths, forming the foundation for slug-based routing throughout the app.
ProductTabs navigation component
apps/web/src/components/ProductTabs.tsx, apps/web/src/components/ProductTabs.test.tsx
A new reusable React component polls the product registry, renders a horizontal tab navigation with active-state styling based on the current route slug, and handles loading/error/empty states. Full test suite covers tab rendering, active-state detection, link validation, error display, and polling refresh intervals.
App routing refactor with redirect components
apps/web/src/App.tsx, apps/web/src/App.test.tsx
Two redirect components (ProductsRedirect, LegacyItemRedirect) enable backward compatibility by redirecting legacy / and /items/:id bookmarks to the new /products/:slug structure. App routes are extended to support /products/:slug, /products/:slug/items/:externalId, and conditional redirects. Routing tests validate empty-product and populated-product states.
Kanban view refactored for product-scoped routing
apps/web/src/views/Kanban.tsx, apps/web/src/views/Kanban.test.tsx
Kanban reads slug from route params and switches to slug-scoped API calls. ItemCard and KanbanColumn components are updated to carry slug through item navigation links. ProductTabs is integrated for product switching in both normal and error states. Tests verify stage/item rendering, correct item link generation with slug, error handling, and empty-items safety.
ItemDetail view refactored for slug-based navigation
apps/web/src/views/ItemDetail.tsx
ItemDetail now reads { slug, externalId } from route params and computes a context-aware backLink (/products/:slug or /products) for all back-navigation instead of using a hardcoded /, and it renders a slug-mismatch full-page error when appropriate.

Sequence Diagrams

sequenceDiagram
  participant Router as React Router
  participant ProductTabs
  participant API as api.listProducts
  participant Browser as Browser DOM
  Router->>ProductTabs: slug param
  ProductTabs->>API: fetch products (polling every 5s)
  alt loading
    ProductTabs->>Browser: render placeholder
  else error
    ProductTabs->>Browser: render error message
  else empty
    ProductTabs->>Browser: return null
  else success
    API-->>ProductTabs: Product[] result
    ProductTabs->>Browser: render nav tabs, mark active by slug
    Browser->>Router: on tab click navigate to /products/:slug
  end
Loading
sequenceDiagram
  participant Router as React Router
  participant Kanban
  participant API as api slug-scoped
  participant ProductTabs
  participant Board as Kanban Board
  Router->>Kanban: :slug param
  Kanban->>API: getProductBySlug(slug)
  Kanban->>API: listItemsForProduct(slug)
  API-->>Kanban: Product, ItemState[]
  Kanban->>ProductTabs: render tabs
  Kanban->>Board: render stages + items with slug
  Board->>Router: item link to /products/:slug/items/:externalId
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • lhpaul/helm#11: Introduces the backend /api/products endpoints and product-registry behavior that the new frontend API methods and routing directly depend upon.
  • lhpaul/helm#10: Prior polling/API flow and Kanban/ItemDetail foundations that this PR refactors to be product-slug scoped.
  • lhpaul/helm#6: Backend item endpoints (/api/items/:externalId) that the ItemDetail view still uses for item fetches.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding multi-product dashboard support with tabs and URL routing. It is specific, clear, and directly reflects the core feature additions across the codebase.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/session-8-multi-product-dashboard

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/web/src/App.test.tsx`:
- Around line 57-67: The test currently only checks rendered content but should
assert the redirected URL; after rendering App() and waiting for the UI, add an
assertion that the browser pathname equals the expected product path (e.g.
expect(window.location.pathname).toBe(`/products/${PRODUCTS[0].slug}`)); keep
the existing mocks (mockListProducts, mockGetProductBySlug,
mockListItemsForProduct) and the render(<App />) + vi.waitFor block and add this
pathname check alongside the heading assertion to verify the actual redirect.

In `@apps/web/src/App.tsx`:
- Line 39: The redirect constructs raw path segments from product identifiers
which can break routes for reserved characters; update the two Navigate usages
in App.tsx (the one that uses products[0]!.product.slug and the other at the
mentioned location that builds a path from a product id/slug) to encode path
segments with encodeURIComponent (e.g., apply encodeURIComponent to the slug or
id before interpolating into the `/products/...` path) so navigation uses safe,
encoded URLs.
- Line 16: The product registry fetch currently treats failures as empty results
because App.tsx only checks usePolling(fetchProducts) for data/loading; update
the flow so fetch errors are surfaced and handled instead of falling through to
the “no products” redirect. Modify usePolling (or the call site) to return an
error value (e.g., { data, loading, error }) when fetchProducts fails, then in
App.tsx where products/loading are used (the call to usePolling, and the
subsequent branches around lines referenced) add an explicit check for error and
route to an error UI/stop the redirect logic (or show a retry) rather than
treating error as an empty products array; reference the
usePolling(fetchProducts) call, the fetchProducts function, and the
redirect/empty-state branch in App.tsx when making these changes.

In `@apps/web/src/components/ProductTabs.tsx`:
- Line 38: The product tab URL is built using the raw slug
(product.product.slug) which can break for slugs containing reserved URL
characters; update the link construction in ProductTabs (where
`to={`/products/${product.product.slug}`}` is used) to encode the slug with
encodeURIComponent before interpolation so the resulting path is a valid URL
(e.g., use encodeURIComponent(product.product.slug) when creating the `to` value
and any other places in this component that build product URLs).

In `@apps/web/src/lib/api.test.ts`:
- Around line 103-108: The test named "URL-encodes the slug" doesn't actually
exercise encoding because it passes a plain slug; update it to use a slug with
characters that require encoding (e.g., a slug with spaces or ampersands), call
api.getProductBySlug(slug) as before, and assert that mockFetch was called with
a URL containing encodeURIComponent(slug) (or the explicit percent-encoded
string) instead of the raw slug; reference the existing test harness
variables/mockFetch and the api.getProductBySlug function when making this
change.

In `@apps/web/src/views/ItemDetail.tsx`:
- Around line 53-58: The component currently fetches the item via
fetcher/api.getItem using externalId but never validates that the returned item
belongs to the route's slug; add a check that item.productSlug === slug before
rendering details and handle mismatches by either navigating to the correct
product-scoped URL (e.g., replace/redirect to
`/products/${item.productSlug}/items/${externalId}`) or setting an
error/NotFound state; implement this check after the usePolling result (watch
item and slug) — update backLink only when slug is confirmed or use the item's
productSlug for backLink when redirecting.

In `@apps/web/src/views/Kanban.tsx`:
- Line 29: The link builds a route using raw slug and item.externalId
(to={`/products/${slug}/items/${item.externalId}`}) which can break for reserved
characters; update the Kanban.tsx link construction to URL-encode those dynamic
segments by wrapping slug and item.externalId with encodeURIComponent before
interpolating into the to prop (i.e., use encodeURIComponent(slug) and
encodeURIComponent(item.externalId)) so the generated path is safe for routing.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: e5c1c062-c63e-415a-b404-bef835956b33

📥 Commits

Reviewing files that changed from the base of the PR and between 976e382 and 98f37bc.

📒 Files selected for processing (9)
  • apps/web/src/App.test.tsx
  • apps/web/src/App.tsx
  • apps/web/src/components/ProductTabs.test.tsx
  • apps/web/src/components/ProductTabs.tsx
  • apps/web/src/lib/api.test.ts
  • apps/web/src/lib/api.ts
  • apps/web/src/views/ItemDetail.tsx
  • apps/web/src/views/Kanban.test.tsx
  • apps/web/src/views/Kanban.tsx

Comment thread apps/web/src/App.test.tsx
Comment thread apps/web/src/App.tsx Outdated
Comment thread apps/web/src/App.tsx Outdated
Comment thread apps/web/src/components/ProductTabs.tsx Outdated
Comment thread apps/web/src/lib/api.test.ts Outdated
Comment thread apps/web/src/views/ItemDetail.tsx Outdated
Comment thread apps/web/src/views/Kanban.tsx Outdated
1. App.test.tsx: assert window.location.pathname === '/products/helm' to
   verify the redirect contract directly (not just rendered content)
2. App.tsx ProductsRedirect: expose error from usePolling so registry
   failures show "Failed to load products" instead of "no products" empty
   state. LegacyItemRedirect: redirect to / on error.
3+4. App.tsx + ProductTabs.tsx: encodeURIComponent on slug in Navigate
   and tab Link hrefs
5. api.test.ts: getProductBySlug URL-encoding test uses 'helm/playground
   with space' to actually exercise encodeURIComponent
6. ItemDetail.tsx: validate item.productSlug === slug — mismatch shows
   "Item does not belong to this product." error instead of rendering
   wrong item silently
7. Kanban.tsx: encodeURIComponent on slug + externalId in ItemCard href

Addresses CodeRabbit review comments on PR #13.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/web/src/App.test.tsx`:
- Around line 28-30: The helper function ok<T>(data: T) returns the payload
directly, exposing mutable references; modify ok to defensive-copy mutable
inputs before returning by detecting arrays and plain objects and returning a
shallow copy (e.g., spread for objects and slice/[…array] for arrays) while
returning primitives as-is; update the function named ok to perform this shallow
cloning so tests don't share mutable state across calls.

In `@apps/web/src/lib/api.test.ts`:
- Around line 120-126: Update the test for listItemsForProduct to use a product
slug containing reserved/unsafe characters (e.g., spaces or symbols) so we
verify URL encoding; call api.listItemsForProduct with that raw slug, then
assert that the captured calledUrl (from mockFetch.mock.calls[0][0]) contains
the encoded form (use the expected encodeURIComponent result string) and still
assert result.ok, ensuring the test will catch regressions in the URL
construction inside listItemsForProduct.

In `@apps/web/src/views/ItemDetail.tsx`:
- Around line 58-59: The backLink is built from the decoded slug (const backLink
= slug ? `/products/${slug}` : '/products') which can produce invalid URLs for
slugs with reserved characters; update the ItemDetail component to encode the
slug when constructing backLink (use encodeURIComponent(slug)) so the generated
path is safe, and ensure the same encoded-backLink is used for the back
navigation points where backLink is referenced.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: c1ff32f5-68df-45a7-baff-a15d2a74af0b

📥 Commits

Reviewing files that changed from the base of the PR and between 98f37bc and 55d3501.

📒 Files selected for processing (6)
  • apps/web/src/App.test.tsx
  • apps/web/src/App.tsx
  • apps/web/src/components/ProductTabs.tsx
  • apps/web/src/lib/api.test.ts
  • apps/web/src/views/ItemDetail.tsx
  • apps/web/src/views/Kanban.tsx

Comment thread apps/web/src/App.test.tsx
Comment thread apps/web/src/lib/api.test.ts Outdated
Comment thread apps/web/src/views/ItemDetail.tsx Outdated
1. ItemDetail.tsx: encode slug in backLink (missed in previous round —
   inconsistent with App.tsx and Kanban.tsx encoding)
2. App.test.tsx: shallow-copy data in ok() helper so mutable references
   don't leak between tests
3. api.test.ts: listItemsForProduct URL encoding test uses slug with
   reserved chars ('helm/playground with space') to actually exercise
   encodeURIComponent

Addresses CodeRabbit review comments on PR #13.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@lhpaul lhpaul merged commit 6103743 into develop May 19, 2026
1 check passed
@lhpaul lhpaul deleted the feature/session-8-multi-product-dashboard branch May 19, 2026 17:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant