From e486e2d73c35424af7ea279f591432cf8ede9bf5 Mon Sep 17 00:00:00 2001 From: Isaac Ivins Date: Tue, 25 Nov 2025 09:35:29 -0500 Subject: [PATCH] apply patch from non forked repo --- apps/desktop/src/app/app-routing.module.ts | 25 ++++- .../app/layout/desktop-layout.component.html | 4 + .../layout/desktop-layout.component.spec.ts | 61 +++++++++++ .../app/layout/desktop-layout.component.ts | 14 +++ .../src/app/layout/desktop-layout.module.ts | 14 +++ .../layout/desktop-side-nav.component.html | 3 + .../layout/desktop-side-nav.component.spec.ts | 74 +++++++++++++ .../app/layout/desktop-side-nav.component.ts | 15 +++ .../src/app/layout/user-layout.component.html | 10 ++ .../app/layout/user-layout.component.spec.ts | 102 ++++++++++++++++++ .../src/app/layout/user-layout.component.ts | 19 ++++ .../app/tools/send-v2/sends.component.spec.ts | 22 ++++ .../src/app/tools/send-v2/sends.component.ts | 10 ++ .../app/vault-v3/vault.component.spec.ts | 22 ++++ .../src/vault/app/vault-v3/vault.component.ts | 10 ++ libs/common/src/enums/feature-flag.enum.ts | 6 ++ 16 files changed, 410 insertions(+), 1 deletion(-) 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-layout.module.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/layout/user-layout.component.html create mode 100644 apps/desktop/src/app/layout/user-layout.component.spec.ts create mode 100644 apps/desktop/src/app/layout/user-layout.component.ts create mode 100644 apps/desktop/src/app/tools/send-v2/sends.component.spec.ts create mode 100644 apps/desktop/src/app/tools/send-v2/sends.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..fd06640737c 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 { UserLayoutComponent } from "./layout/user-layout.component"; import { SendComponent } from "./tools/send/send.component"; +import { SendsComponent } from "./tools/send-v2/sends.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: UserLayoutComponent, + canActivate: [authGuard], + children: [ + { + path: "new-vault", + component: VaultComponent, + }, + { + path: "new-sends", + component: SendsComponent, + }, + ], + }, ]; @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..71a760735ca --- /dev/null +++ b/apps/desktop/src/app/layout/desktop-layout.component.html @@ -0,0 +1,4 @@ + + + + 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..695f3db37ab --- /dev/null +++ b/apps/desktop/src/app/layout/desktop-layout.component.ts @@ -0,0 +1,14 @@ +import { CommonModule } from "@angular/common"; +import { ChangeDetectionStrategy, Component } from "@angular/core"; +import { RouterModule } from "@angular/router"; + +import { LayoutComponent, NavigationModule } from "@bitwarden/components"; + +@Component({ + selector: "app-desktop-layout", + standalone: true, + imports: [CommonModule, RouterModule, LayoutComponent, NavigationModule], + templateUrl: "./desktop-layout.component.html", + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class DesktopLayoutComponent {} diff --git a/apps/desktop/src/app/layout/desktop-layout.module.ts b/apps/desktop/src/app/layout/desktop-layout.module.ts new file mode 100644 index 00000000000..efef167c95e --- /dev/null +++ b/apps/desktop/src/app/layout/desktop-layout.module.ts @@ -0,0 +1,14 @@ +import { NgModule } from "@angular/core"; + +import { NavigationModule } from "@bitwarden/components"; + +import { DesktopLayoutComponent } from "./desktop-layout.component"; +import { DesktopSideNavComponent } from "./desktop-side-nav.component"; + +@NgModule({ + imports: [DesktopLayoutComponent, DesktopSideNavComponent], + exports: [NavigationModule, DesktopLayoutComponent, DesktopSideNavComponent], + declarations: [], + providers: [], +}) +export class DesktopLayoutModule {} 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..c74e9bc989a --- /dev/null +++ b/apps/desktop/src/app/layout/desktop-side-nav.component.ts @@ -0,0 +1,15 @@ +import { CommonModule } from "@angular/common"; +import { ChangeDetectionStrategy, Component, input } from "@angular/core"; + +import { NavigationModule, SideNavVariant } from "@bitwarden/components"; + +@Component({ + selector: "app-desktop-side-nav", + standalone: true, + 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/layout/user-layout.component.html b/apps/desktop/src/app/layout/user-layout.component.html new file mode 100644 index 00000000000..ff3ba579425 --- /dev/null +++ b/apps/desktop/src/app/layout/user-layout.component.html @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/apps/desktop/src/app/layout/user-layout.component.spec.ts b/apps/desktop/src/app/layout/user-layout.component.spec.ts new file mode 100644 index 00000000000..00aa8486529 --- /dev/null +++ b/apps/desktop/src/app/layout/user-layout.component.spec.ts @@ -0,0 +1,102 @@ +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 { DesktopLayoutModule } from "./desktop-layout.module"; +import { UserLayoutComponent } from "./user-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("UserLayoutComponent", () => { + let component: UserLayoutComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [UserLayoutComponent, RouterModule.forRoot([]), DesktopLayoutModule], + providers: [ + { + provide: I18nService, + useValue: mock(), + }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(UserLayoutComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it("creates component", () => { + expect(component).toBeTruthy(); + }); + + it("renders desktop layout", () => { + const compiled = fixture.nativeElement; + const layoutElement = compiled.querySelector("app-desktop-layout"); + + expect(layoutElement).toBeTruthy(); + }); + + it("renders desktop side nav", () => { + const compiled = fixture.nativeElement; + const sideNavElement = compiled.querySelector("app-desktop-side-nav"); + + expect(sideNavElement).toBeTruthy(); + }); + + it("renders logo with correct properties", () => { + const compiled = fixture.nativeElement; + const logoElement = compiled.querySelector("bit-nav-logo"); + + expect(logoElement).toBeTruthy(); + expect(logoElement.getAttribute("route")).toBe("."); + }); + + it("renders vault navigation item", () => { + const compiled = fixture.nativeElement; + const navItems = compiled.querySelectorAll("bit-nav-item"); + const vaultItem = Array.from(navItems).find( + (item) => (item as Element).getAttribute("icon") === "bwi-vault", + ) as Element | undefined; + + expect(vaultItem).toBeTruthy(); + expect(vaultItem?.getAttribute("route")).toBe("new-vault"); + }); + + it("renders send navigation item", () => { + const compiled = fixture.nativeElement; + const navItems = compiled.querySelectorAll("bit-nav-item"); + const sendItem = Array.from(navItems).find( + (item) => (item as Element).getAttribute("icon") === "bwi-send", + ) as Element | undefined; + + expect(sendItem).toBeTruthy(); + expect(sendItem?.getAttribute("route")).toBe("new-sends"); + }); + + it("renders router outlet", () => { + const compiled = fixture.nativeElement; + const routerOutlet = compiled.querySelector("router-outlet"); + + expect(routerOutlet).toBeTruthy(); + }); + + it("has logo property set", () => { + expect(component["logo"]).toBeDefined(); + }); +}); diff --git a/apps/desktop/src/app/layout/user-layout.component.ts b/apps/desktop/src/app/layout/user-layout.component.ts new file mode 100644 index 00000000000..81031b19d9a --- /dev/null +++ b/apps/desktop/src/app/layout/user-layout.component.ts @@ -0,0 +1,19 @@ +import { CommonModule } from "@angular/common"; +import { ChangeDetectionStrategy, Component } from "@angular/core"; +import { RouterModule } from "@angular/router"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { PasswordManagerLogo } from "@bitwarden/assets/svg"; + +import { DesktopLayoutModule } from "./desktop-layout.module"; + +@Component({ + selector: "app-user-layout", + standalone: true, + templateUrl: "user-layout.component.html", + imports: [CommonModule, RouterModule, JslibModule, DesktopLayoutModule], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class UserLayoutComponent { + protected readonly logo = PasswordManagerLogo; +} diff --git a/apps/desktop/src/app/tools/send-v2/sends.component.spec.ts b/apps/desktop/src/app/tools/send-v2/sends.component.spec.ts new file mode 100644 index 00000000000..2156bc80b1d --- /dev/null +++ b/apps/desktop/src/app/tools/send-v2/sends.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from "@angular/core/testing"; + +import { SendsComponent } from "./sends.component"; + +describe("SendsComponent", () => { + let component: SendsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [SendsComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(SendsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it("creates component", () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/apps/desktop/src/app/tools/send-v2/sends.component.ts b/apps/desktop/src/app/tools/send-v2/sends.component.ts new file mode 100644 index 00000000000..c8e9ac4c1d9 --- /dev/null +++ b/apps/desktop/src/app/tools/send-v2/sends.component.ts @@ -0,0 +1,10 @@ +import { Component, ChangeDetectionStrategy } from "@angular/core"; + +@Component({ + selector: "app-sends-v2", + standalone: true, + imports: [], + template: "

Sends V2 Component

", + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class SendsComponent {} 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..0a4a38742fc --- /dev/null +++ b/apps/desktop/src/vault/app/vault-v3/vault.component.ts @@ -0,0 +1,10 @@ +import { ChangeDetectionStrategy, Component } from "@angular/core"; + +@Component({ + selector: "app-vault-v3", + standalone: true, + 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 17d5f4e9df5..068a8f1e410 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", } export type AllowedFeatureFlagTypes = boolean | number | string; @@ -150,6 +153,9 @@ export const DefaultFeatureFlagValue = { /* Innovation */ [FeatureFlag.PM19148_InnovationArchive]: FALSE, + + /* Desktop */ + [FeatureFlag.DesktopUiMigrationMilestone1]: FALSE, } satisfies Record; export type DefaultFeatureFlagValueType = typeof DefaultFeatureFlagValue;