From d05356dbeb254a4dc24c17aa8fb341426c73d563 Mon Sep 17 00:00:00 2001 From: Isaac Ivins Date: Mon, 1 Dec 2025 13:04:07 -0500 Subject: [PATCH] [PM-27792] Scaffold layout desktop migration (#17658) Introduces foundational scaffolding for the Bitwarden Desktop application UI migration --- apps/desktop/src/app/app-routing.module.ts | 25 ++++++- .../app/layout/desktop-layout.component.html | 10 +++ .../layout/desktop-layout.component.spec.ts | 61 +++++++++++++++ .../app/layout/desktop-layout.component.ts | 18 +++++ .../layout/desktop-side-nav.component.html | 3 + .../layout/desktop-side-nav.component.spec.ts | 74 +++++++++++++++++++ .../app/layout/desktop-side-nav.component.ts | 14 ++++ .../tools/send-v2/send-v2.component.spec.ts | 22 ++++++ .../app/tools/send-v2/send-v2.component.ts | 9 +++ apps/desktop/src/locales/en/messages.json | 7 +- .../app/vault-v3/vault.component.spec.ts | 22 ++++++ .../src/vault/app/vault-v3/vault.component.ts | 9 +++ libs/common/src/enums/feature-flag.enum.ts | 6 ++ 13 files changed, 278 insertions(+), 2 deletions(-) create mode 100644 apps/desktop/src/app/layout/desktop-layout.component.html create mode 100644 apps/desktop/src/app/layout/desktop-layout.component.spec.ts create mode 100644 apps/desktop/src/app/layout/desktop-layout.component.ts create mode 100644 apps/desktop/src/app/layout/desktop-side-nav.component.html create mode 100644 apps/desktop/src/app/layout/desktop-side-nav.component.spec.ts create mode 100644 apps/desktop/src/app/layout/desktop-side-nav.component.ts create mode 100644 apps/desktop/src/app/tools/send-v2/send-v2.component.spec.ts create mode 100644 apps/desktop/src/app/tools/send-v2/send-v2.component.ts create mode 100644 apps/desktop/src/vault/app/vault-v3/vault.component.spec.ts create mode 100644 apps/desktop/src/vault/app/vault-v3/vault.component.ts diff --git a/apps/desktop/src/app/app-routing.module.ts b/apps/desktop/src/app/app-routing.module.ts index b6e86ba19ff..8fab7df1cd8 100644 --- a/apps/desktop/src/app/app-routing.module.ts +++ b/apps/desktop/src/app/app-routing.module.ts @@ -14,6 +14,7 @@ import { } from "@bitwarden/angular/auth/guards"; import { ChangePasswordComponent } from "@bitwarden/angular/auth/password-management/change-password"; import { SetInitialPasswordComponent } from "@bitwarden/angular/auth/password-management/set-initial-password/set-initial-password.component"; +import { canAccessFeature } from "@bitwarden/angular/platform/guard/feature-flag.guard"; import { DevicesIcon, RegistrationUserAddIcon, @@ -39,15 +40,19 @@ import { TwoFactorAuthGuard, NewDeviceVerificationComponent, } from "@bitwarden/auth/angular"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { AnonLayoutWrapperComponent, AnonLayoutWrapperData } from "@bitwarden/components"; import { LockComponent, ConfirmKeyConnectorDomainComponent } from "@bitwarden/key-management-ui"; import { maxAccountsGuardFn } from "../auth/guards/max-accounts.guard"; import { RemovePasswordComponent } from "../key-management/key-connector/remove-password.component"; import { VaultV2Component } from "../vault/app/vault/vault-v2.component"; +import { VaultComponent } from "../vault/app/vault-v3/vault.component"; import { Fido2PlaceholderComponent } from "./components/fido2placeholder.component"; +import { DesktopLayoutComponent } from "./layout/desktop-layout.component"; import { SendComponent } from "./tools/send/send.component"; +import { SendV2Component } from "./tools/send-v2/send-v2.component"; /** * Data properties acceptable for use in route objects in the desktop @@ -99,7 +104,10 @@ const routes: Routes = [ { path: "vault", component: VaultV2Component, - canActivate: [authGuard], + canActivate: [ + authGuard, + canAccessFeature(FeatureFlag.DesktopUiMigrationMilestone1, false, "new-vault", false), + ], }, { path: "send", @@ -325,6 +333,21 @@ const routes: Routes = [ }, ], }, + { + path: "", + component: DesktopLayoutComponent, + canActivate: [authGuard], + children: [ + { + path: "new-vault", + component: VaultComponent, + }, + { + path: "new-sends", + component: SendV2Component, + }, + ], + }, ]; @NgModule({ diff --git a/apps/desktop/src/app/layout/desktop-layout.component.html b/apps/desktop/src/app/layout/desktop-layout.component.html new file mode 100644 index 00000000000..94b9201ae21 --- /dev/null +++ b/apps/desktop/src/app/layout/desktop-layout.component.html @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/apps/desktop/src/app/layout/desktop-layout.component.spec.ts b/apps/desktop/src/app/layout/desktop-layout.component.spec.ts new file mode 100644 index 00000000000..cc2f7e58dfb --- /dev/null +++ b/apps/desktop/src/app/layout/desktop-layout.component.spec.ts @@ -0,0 +1,61 @@ +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { RouterModule } from "@angular/router"; +import { mock } from "jest-mock-extended"; + +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { NavigationModule } from "@bitwarden/components"; + +import { DesktopLayoutComponent } from "./desktop-layout.component"; + +Object.defineProperty(window, "matchMedia", { + writable: true, + value: jest.fn().mockImplementation((query) => ({ + matches: true, + media: query, + onchange: null, + addListener: jest.fn(), + removeListener: jest.fn(), + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + dispatchEvent: jest.fn(), + })), +}); + +describe("DesktopLayoutComponent", () => { + let component: DesktopLayoutComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [DesktopLayoutComponent, RouterModule.forRoot([]), NavigationModule], + providers: [ + { + provide: I18nService, + useValue: mock(), + }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(DesktopLayoutComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it("creates component", () => { + expect(component).toBeTruthy(); + }); + + it("renders bit-layout component", () => { + const compiled = fixture.nativeElement; + const layoutElement = compiled.querySelector("bit-layout"); + + expect(layoutElement).toBeTruthy(); + }); + + it("supports content projection for side-nav", () => { + const compiled = fixture.nativeElement; + const ngContent = compiled.querySelectorAll("ng-content"); + + expect(ngContent).toBeTruthy(); + }); +}); diff --git a/apps/desktop/src/app/layout/desktop-layout.component.ts b/apps/desktop/src/app/layout/desktop-layout.component.ts new file mode 100644 index 00000000000..5059a6e4d0b --- /dev/null +++ b/apps/desktop/src/app/layout/desktop-layout.component.ts @@ -0,0 +1,18 @@ +import { ChangeDetectionStrategy, Component } from "@angular/core"; +import { RouterModule } from "@angular/router"; + +import { PasswordManagerLogo } from "@bitwarden/assets/svg"; +import { LayoutComponent, NavigationModule } from "@bitwarden/components"; +import { I18nPipe } from "@bitwarden/ui-common"; + +import { DesktopSideNavComponent } from "./desktop-side-nav.component"; + +@Component({ + selector: "app-layout", + imports: [RouterModule, I18nPipe, LayoutComponent, NavigationModule, DesktopSideNavComponent], + templateUrl: "./desktop-layout.component.html", + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class DesktopLayoutComponent { + protected readonly logo = PasswordManagerLogo; +} diff --git a/apps/desktop/src/app/layout/desktop-side-nav.component.html b/apps/desktop/src/app/layout/desktop-side-nav.component.html new file mode 100644 index 00000000000..ede3f9131b7 --- /dev/null +++ b/apps/desktop/src/app/layout/desktop-side-nav.component.html @@ -0,0 +1,3 @@ + + + diff --git a/apps/desktop/src/app/layout/desktop-side-nav.component.spec.ts b/apps/desktop/src/app/layout/desktop-side-nav.component.spec.ts new file mode 100644 index 00000000000..59e743f430a --- /dev/null +++ b/apps/desktop/src/app/layout/desktop-side-nav.component.spec.ts @@ -0,0 +1,74 @@ +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { mock } from "jest-mock-extended"; + +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { NavigationModule } from "@bitwarden/components"; + +import { DesktopSideNavComponent } from "./desktop-side-nav.component"; + +Object.defineProperty(window, "matchMedia", { + writable: true, + value: jest.fn().mockImplementation((query) => ({ + matches: true, + media: query, + onchange: null, + addListener: jest.fn(), + removeListener: jest.fn(), + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + dispatchEvent: jest.fn(), + })), +}); + +describe("DesktopSideNavComponent", () => { + let component: DesktopSideNavComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [DesktopSideNavComponent, NavigationModule], + providers: [ + { + provide: I18nService, + useValue: mock(), + }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(DesktopSideNavComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it("creates component", () => { + expect(component).toBeTruthy(); + }); + + it("renders bit-side-nav component", () => { + const compiled = fixture.nativeElement; + const sideNavElement = compiled.querySelector("bit-side-nav"); + + expect(sideNavElement).toBeTruthy(); + }); + + it("uses primary variant by default", () => { + expect(component.variant()).toBe("primary"); + }); + + it("accepts variant input", () => { + fixture.componentRef.setInput("variant", "secondary"); + fixture.detectChanges(); + + expect(component.variant()).toBe("secondary"); + }); + + it("passes variant to bit-side-nav", () => { + fixture.componentRef.setInput("variant", "secondary"); + fixture.detectChanges(); + + const compiled = fixture.nativeElement; + const sideNavElement = compiled.querySelector("bit-side-nav"); + + expect(sideNavElement.getAttribute("ng-reflect-variant")).toBe("secondary"); + }); +}); diff --git a/apps/desktop/src/app/layout/desktop-side-nav.component.ts b/apps/desktop/src/app/layout/desktop-side-nav.component.ts new file mode 100644 index 00000000000..b0d9fd16fcc --- /dev/null +++ b/apps/desktop/src/app/layout/desktop-side-nav.component.ts @@ -0,0 +1,14 @@ +import { CommonModule } from "@angular/common"; +import { ChangeDetectionStrategy, Component, input } from "@angular/core"; + +import { NavigationModule, SideNavVariant } from "@bitwarden/components"; + +@Component({ + selector: "app-side-nav", + templateUrl: "desktop-side-nav.component.html", + imports: [CommonModule, NavigationModule], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class DesktopSideNavComponent { + readonly variant = input("primary"); +} diff --git a/apps/desktop/src/app/tools/send-v2/send-v2.component.spec.ts b/apps/desktop/src/app/tools/send-v2/send-v2.component.spec.ts new file mode 100644 index 00000000000..8055bc07667 --- /dev/null +++ b/apps/desktop/src/app/tools/send-v2/send-v2.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from "@angular/core/testing"; + +import { SendV2Component } from "./send-v2.component"; + +describe("SendV2Component", () => { + let component: SendV2Component; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [SendV2Component], + }).compileComponents(); + + fixture = TestBed.createComponent(SendV2Component); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it("creates component", () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/apps/desktop/src/app/tools/send-v2/send-v2.component.ts b/apps/desktop/src/app/tools/send-v2/send-v2.component.ts new file mode 100644 index 00000000000..4840cd4cce8 --- /dev/null +++ b/apps/desktop/src/app/tools/send-v2/send-v2.component.ts @@ -0,0 +1,9 @@ +import { Component, ChangeDetectionStrategy } from "@angular/core"; + +@Component({ + selector: "app-send-v2", + imports: [], + template: "

Sends V2 Component

", + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class SendV2Component {} diff --git a/apps/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json index 6bef882d970..f6f078611c9 100644 --- a/apps/desktop/src/locales/en/messages.json +++ b/apps/desktop/src/locales/en/messages.json @@ -2228,6 +2228,10 @@ "contactInfo": { "message": "Contact information" }, + "send": { + "message": "Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "allSends": { "message": "All Sends", "description": "'Sends' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2991,7 +2995,8 @@ "message": "Are you sure you want to use the \"Never\" option? Setting your lock options to \"Never\" stores your vault's encryption key on your device. If you use this option you should ensure that you keep your device properly protected." }, "vault": { - "message": "Vault" + "message": "Vault", + "description": "'Vault' is a noun and refers to the Bitwarden Vault feature." }, "loginWithMasterPassword": { "message": "Log in with master password" diff --git a/apps/desktop/src/vault/app/vault-v3/vault.component.spec.ts b/apps/desktop/src/vault/app/vault-v3/vault.component.spec.ts new file mode 100644 index 00000000000..89ba05055f8 --- /dev/null +++ b/apps/desktop/src/vault/app/vault-v3/vault.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from "@angular/core/testing"; + +import { VaultComponent } from "./vault.component"; + +describe("VaultComponent", () => { + let component: VaultComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [VaultComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(VaultComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it("creates component", () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/apps/desktop/src/vault/app/vault-v3/vault.component.ts b/apps/desktop/src/vault/app/vault-v3/vault.component.ts new file mode 100644 index 00000000000..b29b66225c7 --- /dev/null +++ b/apps/desktop/src/vault/app/vault-v3/vault.component.ts @@ -0,0 +1,9 @@ +import { ChangeDetectionStrategy, Component } from "@angular/core"; + +@Component({ + selector: "app-vault-v3", + imports: [], + template: "

Vault V3 Component

", + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class VaultComponent {} diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index be2ea75203c..6010110f069 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -72,6 +72,9 @@ export enum FeatureFlag { /* Innovation */ PM19148_InnovationArchive = "pm-19148-innovation-archive", + /* Desktop */ + DesktopUiMigrationMilestone1 = "desktop-ui-migration-milestone-1", + /* UIF */ RouterFocusManagement = "router-focus-management", } @@ -152,6 +155,9 @@ export const DefaultFeatureFlagValue = { /* Innovation */ [FeatureFlag.PM19148_InnovationArchive]: FALSE, + /* Desktop */ + [FeatureFlag.DesktopUiMigrationMilestone1]: FALSE, + /* UIF */ [FeatureFlag.RouterFocusManagement]: FALSE, } satisfies Record;