Skip to content

[PM-40316] Restructure the desktop side navigation - #22585

Open
gbubemismith wants to merge 15 commits into
mainfrom
vault/pm-40316-desktop-nav-restructure
Open

[PM-40316] Restructure the desktop side navigation#22585
gbubemismith wants to merge 15 commits into
mainfrom
vault/pm-40316-desktop-nav-restructure

Conversation

@gbubemismith

@gbubemismith gbubemismith commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

🎟️ Tracking

https://bitwarden.atlassian.net/browse/PM-40316

📔 Objective

The desktop counterpart to the web side-nav restructure in PM-40313. All changes sit behind the vfo1-foundation flag; the legacy navigation and vault filtering are untouched when it is off.

Navigation. The desktop-only vault filter is replaced by the shared VaultNavSectionComponent and VaultManageNavComponent, so Vaults, Tools and Manage match web. Both navigate through routerLink, which let the layout drop its vault, archive and trash handlers along with four service injections.

Routing. /vault/:vaultId is registered alongside the unscoped /vault, guarded by canAccessFeature(VFO1Foundation, ...) and vaultScopeGuard — the same shape as web's VaultRoutingModule. Without it the shared nav's links, and the vaultFilterLegacyRedirectGuard redirects to /vault/trash and /vault/archive, resolved to nothing on desktop.

📸 Screenshots

Screen.Recording.2026-08-21.at.16.21.29.mov

Side Nav with vaults

image

Light mode

image

shane-melton and others added 11 commits August 20, 2026 15:50
Register `/vault/:vaultId` alongside the unscoped `/vault` so the side nav's
vault, trash and archive links resolve. Guarded by the VFO1Foundation flag and
`vaultScopeGuard`, matching web's `VaultRoutingModule`.
Render the shared Vaults section and Manage links behind the VFO1Foundation
flag, replacing the desktop-only vault filter. `VaultNavSectionComponent` and
`VaultManageNavComponent` navigate through `routerLink`, so the layout no longer
carries handlers for vault, archive and trash selection.
Filter rows with `cipherInScope` under the VFO1Foundation flag rather than the
query-param filter, which the `:vaultId` path segment never reached. The table's
chips and the create buttons read the same scope, so trash and archive suppress
item creation and only in-scope vaults are offered.

The legacy flag-off path keeps `RoutedVaultFilterService` and does not subscribe
to `VaultNavService`.
@gbubemismith gbubemismith added the ai-review Request a Claude code review label Aug 21, 2026
@gbubemismith gbubemismith changed the title [PM-40316] feat: restructure the desktop side navigation [PM-40316] Restructure the desktop side navigation Aug 21, 2026
@gbubemismith gbubemismith added the t:feature Change Type - Feature Development label Aug 21, 2026
@github-actions

github-actions Bot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

🤖 Bitwarden Claude Code Review

Overall Assessment: REQUEST CHANGES

Re-reviewed the desktop side-nav restructure, the componentless /vault parent with its "" and :vaultId children, the VaultNavService registration, and the route-scoped filtering in the desktop vault component. Verified DI availability on desktop for the shared nav components (VaultNavService, CipherArchiveService, PremiumUpgradePromptService), the new i18n keys, guard ordering and redirect targets, and that route data.pageTitle still reaches the header through the componentless parent. The flag-off path in showAddCipherBtn$, canCreateCipher$, and ciphers$ is behaviour-preserving after the refactor. One previously raised finding is still open on the current head; no new inline comments were added.

Code Review Details
  • ⚠️ : Batch bar still derives trash state from the legacy ?type=trash filter, so on the new /vault/trash route bulk Delete soft-deletes instead of permanently deleting and Restore never appears (existing unresolved thread)
    • apps/desktop/src/vault/app/vault-v3/vault.component.ts:276
    • libs/vault/src/services/vault-batch-bar.service.ts:120 (inTrash), :575 (permanent = this.inTrash())

The "My folders" nav destination raised earlier is confirmed by the author as upcoming follow-up work and is not repeated. The archive "premium subscription ended" callout is likewise fed by the legacy filter and will not surface on /vault/archive, matching the gap web already documents as not-yet-wired — noted only, not raised as a separate finding.

</bit-nav-section>

<bit-nav-section [label]="'manage' | i18n">
<vault-manage-nav />

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.

⚠️ IMPORTANT: vault-manage-nav renders a "My folders" link, but desktop has no folders route.

Details and fix

VaultManageNavComponent's template hardcodes <bit-nav-item icon="bwi-folder" [text]="'myFolders' | i18n" route="folders">. The routerLink resolves against the layout route ({ path: "", component: DesktopLayoutComponent }), so it targets /folders, and apps/desktop/src/app/app-routing.module.ts registers no such path and no wildcard/fallback route — the click ends in NG04002: Cannot match any routes. (If it instead resolved under /vault, vaultScopeGuard would reject folders as neither a named scope nor a guid and bounce back to /vault, so the item is a dead end either way.)

Web wires this up in oss-routing.module.ts:

{
  path: "folders",
  component: MyFoldersComponent,
  canActivate: [canAccessFeature(FeatureFlag.VFO1Foundation, true, "/vault")],
  data: { titleId: "myFolders" } satisfies RouteDataProperties,
},

This PR added /vault/:vaultId so the nav's Trash and Archive links resolve; the same treatment is needed for the folders link, otherwise every user with vfo1-foundation on gets a broken item in the Manage section. If a desktop folders page is planned for a later PR in the stack, worth confirming the ordering so the flag is never on with a dead link.

The new spec asserts "myFolders" in the expected nav text (desktop-layout.component.spec.ts), so the item is expected to render — only its destination is missing.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Don't worry about this, there's an upcoming PR for this

@codecov

codecov Bot commented Aug 21, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 2.56410% with 38 lines in your changes missing coverage. Please review.
✅ Project coverage is 54.61%. Comparing base (34e13a0) to head (9985594).
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
.../desktop/src/vault/app/vault-v3/vault.component.ts 0.00% 35 Missing ⚠️
apps/desktop/src/app/app-routing.module.ts 0.00% 3 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #22585      +/-   ##
==========================================
- Coverage   54.62%   54.61%   -0.01%     
==========================================
  Files        4309     4309              
  Lines      137618   137632      +14     
  Branches    21797    21803       +6     
==========================================
- Hits        75174    75171       -3     
- Misses      56960    56977      +17     
  Partials     5484     5484              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@gbubemismith
gbubemismith marked this pull request as ready for review August 21, 2026 20:41
@gbubemismith
gbubemismith requested a review from a team as a code owner August 21, 2026 20:41
Comment on lines +276 to +287
/** The vault the `:vaultId` segment scopes this page to; always All items on the legacy nav. */
private readonly vaultScope$ = this.vfo1Foundation$.pipe(
switchMap((vfo1Foundation) =>
vfo1Foundation
? combineLatest([
this.route.paramMap.pipe(map((params) => params.get("vaultId"))),
this.vaultNavService.viewModel$,
]).pipe(map(([vaultId, nav]) => resolveVaultScope(vaultId, nav) ?? ALL_ITEMS_SCOPE))
: of(ALL_ITEMS_SCOPE),
),
shareReplay({ refCount: true, bufferSize: 1 }),
);

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.

⚠️ IMPORTANT: In the new /vault/trash scope the batch bar still believes it is outside trash, so bulk Delete soft-deletes and Restore never appears.

Details and fix

VaultBatchBarService derives its trash state from the legacy query param, not from the scope:

readonly inTrash = toSignal(
  this.routedVaultFilterService.filter$.pipe(map((f) => f.type === "trash")),
  { initialValue: false },
);

Under vfo1-foundation trash is the /vault/trash route and vaultFilterLegacyRedirectGuard strips ?type=trash, so filter.type is never "trash" and inTrash() stays false. bit-vault-batch-action renders for both nav variants (vault.component.html gates it only on vaultBatchBarFeatureFlag()), and app-vault-list-table pushes its selection into the service via handleSelectionChange. With vfo1-foundation + PM37785_VaultBatchBar + PM37785_DesktopVaultBatchBar on, selecting rows in Trash gives:

  • Delete labelled delete and bulkDelete running with permanent = this.inTrash()false, i.e. a soft delete of already-trashed items that appears to do nothing
  • No Restore action — canRestore ANDs on inTrash
  • Archive / Add to folder / Add to shared folder offered on trashed items

Before this PR the trash scope was unreachable on desktop (as the description notes, the nav's /vault/trash link resolved to nothing), so the new route is what exposes this.

One option is to feed the scope in rather than have the service re-derive it: add a trash flag to VaultBatchBarConfig and set it from the existing setConfig({ isOrgVault: false, allCollections, hasCiphers }) call using vaultScope$, falling back to the routed filter when the flag is off.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

#22260 Is in the best position to fix this.

Base automatically changed from vault/pm-42183/side-nav-vault-scopes to main August 21, 2026 21:00
@shane-melton
shane-melton requested a review from a team as a code owner August 21, 2026 21:00
Register `/vault/:vaultId` alongside the unscoped `/vault` so the side nav's
vault, trash and archive links resolve. Guarded by the VFO1Foundation flag and
`vaultScopeGuard`, matching web's `VaultRoutingModule`.
Render the shared Vaults section and Manage links behind the VFO1Foundation
flag, replacing the desktop-only vault filter. `VaultNavSectionComponent` and
`VaultManageNavComponent` navigate through `routerLink`, so the layout no longer
carries handlers for vault, archive and trash selection.
Filter rows with `cipherInScope` under the VFO1Foundation flag rather than the
query-param filter, which the `:vaultId` path segment never reached. The table's
chips and the create buttons read the same scope, so trash and archive suppress
item creation and only in-scope vaults are offered.

The legacy flag-off path keeps `RoutedVaultFilterService` and does not subscribe
to `VaultNavService`.
…itwarden/clients into vault/pm-40316-desktop-nav-restructure

# Conflicts:
#	apps/web/src/app/vault/individual-vault/vault-routing.module.ts
#	libs/vault/src/routing/vault-scope.guard.spec.ts
#	libs/vault/src/routing/vault-scope.guard.ts

@shane-melton shane-melton left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Changes look good! Just a few non-blocking comments/suggestions.


@if (!vfo1Foundation()) {
@if (vfo1Foundation()) {
<vault-nav-section />

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

👍 Glad we get to reuse these! 🎉

},
}),
{
// The side nav's vault scopes: `my-vault` and an organization id. "All items" is the unscoped

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

⛏️ (non-blocking) I dropped these comments in my original PR as they were explained elsewhere better and sound very claude-like ("whole story here"...). Open to keeping if you find them beneficial.

this.allOrganizations = vfo1Foundation
? allOrganizations.filter((organization) => organizationInScope(organization, scope))
: allOrganizations;
this.ciphers = ciphers;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

❓ Would it make sense to do the same cipherInScope filtering here instead of the rowFilter change above? Just trying to think what would be easiest for eventual removal of the VFO1 feature flag.

@shane-melton shane-melton Aug 21, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

🎨 (non-blocking) The page header is still always "Vault" and doesn't update based on the scope. We can set the [title] on app-header in the template based on the scope. Though, I'm okay with leaving this alone since it'll be reworked in the breadcrumb epic.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai-review Request a Claude code review t:feature Change Type - Feature Development

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants