Skip to content

feat: context-aware navigation and Drives Finder page - #706

Merged
2witstudios merged 3 commits into
masterfrom
feat/drives-finder-page
Feb 20, 2026
Merged

2witstudios merged 3 commits into
masterfrom
feat/drives-finder-page

Conversation

@2witstudios

@2witstudios 2witstudios commented Feb 20, 2026 •

Copy link
Copy Markdown
Owner

Summary

  • Revert DriveSwitcher split-button from PR Split DriveSwitcher into navigable name and dropdown toggle #704 back to single dropdown trigger, now context-aware: at dashboard level renders a "Drives" link to the new Finder page; inside a drive shows the dropdown switcher with an "All Drives" entry
  • Context-aware PrimaryNavigation: first item shows "Drive Home" → /dashboard/{driveId} when inside a drive, "Dashboard" → /dashboard otherwise
  • New /dashboard/drives Finder page with grid/list toggle, sortable columns (name, role, last accessed, created), Favorites / My Drives / Shared sections, right-click context menus (role-gated: Rename and Trash only for OWNER/ADMIN), empty state with Create Drive CTA
  • Fix dashboard context: added drives to FULL_PAGE_ROUTE_PATTERN so the sidebar Chat tab remains accessible on the Drives page

Test plan

  • /dashboard → DriveSwitcher shows "Drives" link → click navigates to /dashboard/drives
  • Drives page: grid/list toggle works, sections display correctly, sort by each column
  • Right-click drive → Open, Favorite/Unfavorite work for all roles; Rename and Trash only appear for OWNER/ADMIN
  • Click a drive → navigates to /dashboard/{driveId}, access tracking fires
  • Inside drive → DriveSwitcher shows dropdown with drive name, "All Drives" link works
  • Inside drive → PrimaryNav first item shows "Drive Home", links to drive root
  • TopBar home button still goes to /dashboard
  • Right sidebar Chat tab is visible on /dashboard/drives
  • pnpm --filter web build passes
  • useDashboardContext tests pass (20/20 including new /dashboard/drives test)

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • New Drives page with grid/list views, sorting, create-drive flow, and empty-state prompt.
    • Drive context menu with Open, Rename (role-restricted), Favorite/Unfavorite, and Delete flows.
    • Drive switcher updated to a dropdown with an "All Drives" option and streamlined selection UI.
    • New global "Drives" tab for quick access.
  • Bug Fixes / UX

    • Improved route handling so dashboard and drives routes behave correctly in navigation and context.
  • Tests

    • Added tests covering the new drives route and dashboard context behavior.

Revert the split DriveSwitcher from PR #704 back to a single dropdown
trigger and make it context-aware: at dashboard level it shows a "Drives"
link navigating to the new Finder page, inside a drive it shows the
dropdown switcher with an "All Drives" entry.

Make PrimaryNavigation context-aware so the first item shows "Drive Home"
linking to the drive root when inside a drive, and "Dashboard" otherwise.

Add /dashboard/drives as a Finder-style browser page with grid/list view
toggle, sortable columns, Favorites/My Drives/Shared sections, right-click
context menus (role-gated: Rename and Trash only for owners/admins), and
a Create Drive button.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@coderabbitai

coderabbitai Bot commented Feb 20, 2026 •

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

Adds a Drives feature: new /dashboard/drives page and DrivesBrowser UI, a DriveContextMenu component, tab metadata and hook updates to recognize drives as a full-page route, and navigation adjustments to expose and route to the Drives area.

Changes

Cohort / File(s) Summary
Drives Page
apps/web/src/app/dashboard/DashboardLayoutClient.tsx, apps/web/src/app/dashboard/drives/page.tsx
Registered /dashboard/drives as a full-page route and added a client page DrivesPage that renders DrivesBrowser inside Suspense with DrivesSkeleton fallback.
Drives UI Components
apps/web/src/components/drives/DrivesBrowser.tsx, apps/web/src/components/drives/DriveContextMenu.tsx
Added DrivesBrowser (grid/list views, sorting, favorites sync, create flow, access tracking, navigation) and DriveContextMenu (Open, Favorite toggle, Rename/Delete with permission gating, dialogs, API calls, store updates, toasts).
Navigation / Header
apps/web/src/components/layout/left-sidebar/PrimaryNavigation.tsx, apps/web/src/components/layout/navbar/DriveSwitcher.tsx
PrimaryNavigation label/target now conditional on driveId ("Drive Home" vs "Dashboard"); DriveSwitcher links to /dashboard/drives when none selected and consolidates trigger into a dropdown including "All Drives".
Routing, Hooks & Tabs
apps/web/src/hooks/useDashboardContext.ts, apps/web/src/hooks/__tests__/useDashboardContext.test.ts, apps/web/src/lib/tabs/tab-title.ts, apps/web/src/lib/tabs/__tests__/tab-title.test.ts
Expanded FULL_PAGE_ROUTE_PATTERN to include drives, added test for /dashboard/drives, introduced dashboard-drives PathType and static tab metadata (title: "Drives", icon: "Folder") and corresponding tests.

Sequence Diagram(s)

mermaid
sequenceDiagram
actor User
participant DrivesBrowser
participant DriveAPI as API (drives)
participant DriveStore
participant Router
participant Toasts
User->>DrivesBrowser: Click drive / open context menu action
DrivesBrowser->>DriveAPI: PATCH/POST/DELETE (access, lastAccessed, rename, delete)
DriveAPI-->>DrivesBrowser: 200 OK (updated/removed drive)
DrivesBrowser->>DriveStore: update or remove drive entry
DrivesBrowser->>Router: navigate to /dashboard/{driveId} (on open)
DrivesBrowser->>Toasts: show success/error notifications
DrivesBrowser-->>User: UI updates (navigate, rename dialog, menu state)

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰
I hopped through folders, found a lane,
Starred the ones that felt like gain.
Menus open, names refined,
A tidy burrow, drives aligned.
Hooray — more hops, more files to find!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately captures the main features added: context-aware navigation (DriveSwitcher, PrimaryNavigation) and the new Drives Finder page.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/drives-finder-page

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/web/src/app/dashboard/DashboardLayoutClient.tsx (1)

9-18: ⚠️ Potential issue | 🟡 Minor

startsWith route matching can collide with drive IDs that happen to begin with a reserved word.

pathname?.startsWith('/dashboard/drives') will also match /dashboard/drivesabc123 (a valid CUID2 drive ID). Meanwhile, useDashboardContext.ts uses a regex with a stricter boundary check (/(drives)(\/|$)/), so the two checks disagree for such paths. This inconsistency is pre-existing for all entries in FULL_PAGE_ROUTES, not specific to drives.

Practically near-zero risk with CUID2 IDs, but you could tighten the check for correctness:

🔧 Suggested approach
-  const isFullPageRoute = FULL_PAGE_ROUTES.some(route =>
-    pathname?.startsWith(route)
-  ) || pathname?.match(/^\/dashboard\/[^/]+\/(activity|calendar|inbox|tasks|trash|settings|members)/);
+  const isFullPageRoute = FULL_PAGE_ROUTES.some(route =>
+    pathname === route || pathname?.startsWith(route + '/')
+  ) || pathname?.match(/^\/dashboard\/[^/]+\/(activity|calendar|inbox|tasks|trash|settings|members)/);

Also applies to: 28-30

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/app/dashboard/DashboardLayoutClient.tsx` around lines 9 - 18,
FULL_PAGE_ROUTES vs pathname?.startsWith(...) mismatches because startsWith will
match IDs that merely begin with a route segment (e.g.,
'/dashboard/drivesabc123'); update the check that uses FULL_PAGE_ROUTES so it
enforces a segment boundary — for each route in FULL_PAGE_ROUTES, test either
exact equality (pathname === route) or prefix + slash (pathname.startsWith(route
+ '/')), or use a regex like new RegExp(`${route}(\\/|$)`) — adjust the code
that currently calls pathname?.startsWith('/dashboard/drives') to use this
stricter check so it matches the same boundary logic as useDashboardContext.ts.
🧹 Nitpick comments (2)
apps/web/src/components/drives/DrivesBrowser.tsx (2)

123-132: Fire-and-forget access tracking is fine, but consider logging failures.

The .catch(() => {}) on the access endpoint call silently drops all errors. If this tracking is used for "Recent" ordering, a persistent failure could silently degrade the user experience. Consider at minimum a console.warn for observability.

🔧 Suggested change
-    fetchWithAuth(`/api/drives/${drive.id}/access`, { method: "POST" }).catch(
-      () => {}
-    );
+    fetchWithAuth(`/api/drives/${drive.id}/access`, { method: "POST" }).catch(
+      (err) => console.warn("Failed to record drive access:", err)
+    );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/components/drives/DrivesBrowser.tsx` around lines 123 - 132, The
fire-and-forget fetch in handleDriveClick silently swallows errors; change the
catch on the fetchWithAuth('/api/drives/${drive.id}/access') call to at least
log failures (e.g., console.warn) including the drive.id and the error so
access-tracking failures are observable; update the .catch(() => {}) in
handleDriveClick to log a warning with context and the error rather than
ignoring it.

134-149: Duplicate loading skeleton exists here and in drives/page.tsx's Suspense fallback.

DrivesBrowser is a client component that doesn't use the use() hook, so the <Suspense> boundary in page.tsx will only flash during the JS chunk load (code-splitting), not during data fetching. The real loading state is handled here in lines 134-149. The page-level DrivesSkeleton is therefore only useful for the brief dynamic import window. This is fine as-is, but be aware the two skeletons have slightly different markup (this one lacks the header button placeholders). If you want visual consistency, unify or extract a shared skeleton.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/components/drives/DrivesBrowser.tsx` around lines 134 - 149,
DrivesBrowser currently renders a loading skeleton that duplicates slightly
different markup from the Suspense fallback in drives/page.tsx; extract a shared
skeleton component (e.g., DrivesSkeleton) and replace the inline JSX in
DrivesBrowser (and the Suspense fallback) with that reusable component so both
places render identical markup (include the header button placeholders the page
variant had), export it from a shared file and import it into DrivesBrowser and
page.tsx to keep the loading UI consistent.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/web/src/components/drives/DrivesBrowser.tsx`:
- Around line 159-164: The ArrowUpDown icon is symmetric so rotating it doesn't
show sort direction; update the render in DrivesBrowser to use directional icons
instead: import ArrowUp and ArrowDown (or ChevronUp/ChevronDown) and, where you
currently render ArrowUpDown inside the sortKey === key check, conditionally
render ArrowUp when sortDirection === "asc" and ArrowDown when sortDirection ===
"desc" (remove the rotate class). Ensure the JSX uses the same className sizing
(e.g., "ml-2 h-4 w-4") and update any references to ArrowUpDown accordingly so
the visual cue matches sortDirection.

---

Outside diff comments:
In `@apps/web/src/app/dashboard/DashboardLayoutClient.tsx`:
- Around line 9-18: FULL_PAGE_ROUTES vs pathname?.startsWith(...) mismatches
because startsWith will match IDs that merely begin with a route segment (e.g.,
'/dashboard/drivesabc123'); update the check that uses FULL_PAGE_ROUTES so it
enforces a segment boundary — for each route in FULL_PAGE_ROUTES, test either
exact equality (pathname === route) or prefix + slash (pathname.startsWith(route
+ '/')), or use a regex like new RegExp(`${route}(\\/|$)`) — adjust the code
that currently calls pathname?.startsWith('/dashboard/drives') to use this
stricter check so it matches the same boundary logic as useDashboardContext.ts.

---

Nitpick comments:
In `@apps/web/src/components/drives/DrivesBrowser.tsx`:
- Around line 123-132: The fire-and-forget fetch in handleDriveClick silently
swallows errors; change the catch on the
fetchWithAuth('/api/drives/${drive.id}/access') call to at least log failures
(e.g., console.warn) including the drive.id and the error so access-tracking
failures are observable; update the .catch(() => {}) in handleDriveClick to log
a warning with context and the error rather than ignoring it.
- Around line 134-149: DrivesBrowser currently renders a loading skeleton that
duplicates slightly different markup from the Suspense fallback in
drives/page.tsx; extract a shared skeleton component (e.g., DrivesSkeleton) and
replace the inline JSX in DrivesBrowser (and the Suspense fallback) with that
reusable component so both places render identical markup (include the header
button placeholders the page variant had), export it from a shared file and
import it into DrivesBrowser and page.tsx to keep the loading UI consistent.

Comment thread apps/web/src/components/drives/DrivesBrowser.tsx Outdated
…page

- Replace symmetric ArrowUpDown icon with directional ArrowUp/ArrowDown
  for clear sort direction indication
- Tighten FULL_PAGE_ROUTES matching to enforce segment boundaries
  (exact match or prefix + slash) preventing false collisions with
  drive IDs that start with reserved words
- Add console.warn for fire-and-forget access tracking failures
- Extract shared DrivesSkeleton component to unify loading states
  between DrivesBrowser and drives/page.tsx
- Register /dashboard/drives in tab route classifier so it shows
  "Drives" title with Folder icon instead of being misclassified
  as a drive tab

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

@2witstudios 2witstudios left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Addressing all review feedback

All CodeRabbit review comments have been addressed in commit f9255f8:

1. ArrowUpDown icon rotation (inline comment, lines 159-164)

Fixed: Replaced symmetric ArrowUpDown with directional ArrowUp/ArrowDown icons from lucide-react. Users now see a clear visual cue for sort direction.

  • File: apps/web/src/components/drives/DrivesBrowser.tsx

2. startsWith route matching boundary (outside-diff comment, lines 9-18)

Fixed: Tightened FULL_PAGE_ROUTES matching from pathname?.startsWith(route) to pathname === route || pathname?.startsWith(route + '/'). This prevents false collisions with CUID2 drive IDs that happen to start with a reserved word (e.g., /dashboard/drivesabc123).

  • File: apps/web/src/app/dashboard/DashboardLayoutClient.tsx

3. Silent error swallowing in access tracking (nitpick, lines 123-132)

Fixed: Changed .catch(() => {}) to .catch((err) => console.warn("Failed to record drive access:", err)) for observability.

  • File: apps/web/src/components/drives/DrivesBrowser.tsx

4. Duplicate loading skeleton (nitpick, lines 134-149)

Fixed: Extracted shared DrivesSkeleton component from DrivesBrowser.tsx (exported) and updated drives/page.tsx to import and reuse it. Both places now render identical markup including header button placeholders.

  • Files: apps/web/src/components/drives/DrivesBrowser.tsx, apps/web/src/app/dashboard/drives/page.tsx

5. Tab route classification for /dashboard/drives (additional fix)

Fixed: Added 'drives' to GLOBAL_DASHBOARD_ROUTES in tab-title.ts and registered 'dashboard-drives' PathType with "Drives" title and Folder icon. Previously, /dashboard/drives was misclassified as a drive tab with driveId: 'drives'. Added tests covering the parse and meta lookup.

  • Files: apps/web/src/lib/tabs/tab-title.ts, apps/web/src/lib/tabs/__tests__/tab-title.test.ts

Verification

  • All 73 tab-title tests pass (including 2 new)
  • All 20 useDashboardContext tests pass
  • pnpm --filter web build passes cleanly
  • Lint & TypeScript check passed on CI
  • Unit Tests CI failure is pre-existing (DB FK violations and missing root role in test infrastructure - same pattern seen on master)

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
apps/web/src/components/drives/DrivesBrowser.tsx (1)

80-82: forceRefresh: true bypasses the cache on every mount.

fetchDrives(false, true) forces a network request each time DrivesBrowser mounts, ignoring lastFetched. Rapid back-and-forth navigation between this page and a drive will hammer the API endpoint with redundant fetches.

Consider removing the force flag and instead only refreshing when the cached data is stale (i.e. use fetchDrives() or fetchDrives(false) and let the store's lastFetched TTL govern revalidation), or constraining the refresh to when the component re-mounts after a meaningful idle period.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/components/drives/DrivesBrowser.tsx` around lines 80 - 82, The
component DrivesBrowser currently calls fetchDrives(false, true) on mount which
forces a network fetch every time; change this to use the store's cache/TTL by
calling fetchDrives() or fetchDrives(false) so the store's lastFetched logic
handles revalidation, or implement a stale-check around fetchDrives (read
lastFetched from the drives store and only call fetchDrives(false, true) when
lastFetched is older than your TTL) — locate the useEffect inside DrivesBrowser
and modify the fetchDrives invocation accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/web/src/components/drives/DrivesBrowser.tsx`:
- Around line 301-314: The icon-only view toggle buttons in DrivesBrowser.tsx
(the Button components that call setViewMode("list") and setViewMode("grid") and
check viewMode) lack accessible labels; update each Button to include an
appropriate accessible name (e.g., aria-label="List view" and aria-label="Grid
view" or a title/sr-only text node) so screen readers can distinguish them,
ensuring the labels match the action performed by the onClick handlers and the
visual icons (List and Grip).

---

Nitpick comments:
In `@apps/web/src/components/drives/DrivesBrowser.tsx`:
- Around line 80-82: The component DrivesBrowser currently calls
fetchDrives(false, true) on mount which forces a network fetch every time;
change this to use the store's cache/TTL by calling fetchDrives() or
fetchDrives(false) so the store's lastFetched logic handles revalidation, or
implement a stale-check around fetchDrives (read lastFetched from the drives
store and only call fetchDrives(false, true) when lastFetched is older than your
TTL) — locate the useEffect inside DrivesBrowser and modify the fetchDrives
invocation accordingly.

Comment thread apps/web/src/components/drives/DrivesBrowser.tsx
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
apps/web/src/components/drives/DrivesBrowser.tsx (1)

73-78: Consider using individual selectors for useFavorites to reduce re-renders.

Currently useFavorites() subscribes to the entire store — any mutation to any field (e.g. isLoading, favorites array) triggers a re-render of DrivesBrowser. Since this component only needs isFavorite, fetchFavorites, isSynced, and driveIds, you could use individual selectors (e.g., useFavorites(s => s.driveIds)) to narrow the subscription. This is optional since the current approach works correctly.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/components/drives/DrivesBrowser.tsx` around lines 73 - 78, The
component currently destructures multiple values from useFavorites() which
subscribes to the whole favorites store; replace that single call with
individual selector calls to avoid unnecessary re-renders—call useFavorites(s =>
s.isFavorite), useFavorites(s => s.fetchFavorites), useFavorites(s =>
s.isSynced), and useFavorites(s => s.driveIds) (or similar per-value selectors)
inside DrivesBrowser so each of isFavorite, fetchFavorites, isSynced, and
favoriteDriveIds only subscribes to the specific slice it needs.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@apps/web/src/components/drives/DrivesBrowser.tsx`:
- Around line 73-78: The component currently destructures multiple values from
useFavorites() which subscribes to the whole favorites store; replace that
single call with individual selector calls to avoid unnecessary re-renders—call
useFavorites(s => s.isFavorite), useFavorites(s => s.fetchFavorites),
useFavorites(s => s.isSynced), and useFavorites(s => s.driveIds) (or similar
per-value selectors) inside DrivesBrowser so each of isFavorite, fetchFavorites,
isSynced, and favoriteDriveIds only subscribes to the specific slice it needs.

@2witstudios
2witstudios merged commit d58eaf5 into master Feb 20, 2026
3 checks passed
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