mirror of
https://github.com/bitwarden/clients.git
synced 2026-01-11 20:07:18 +00:00
Feature/pm 28788 desktop header UI migration (#18221)
Some checks are pending
Auto Update Branch / Update Branch (push) Waiting to run
Chromatic / Check PR run (push) Waiting to run
Chromatic / Chromatic (push) Blocked by required conditions
Lint / Lint (push) Waiting to run
Lint / Run Rust lint on macos-14 (push) Waiting to run
Lint / Run Rust lint on ubuntu-24.04 (push) Waiting to run
Lint / Run Rust lint on windows-2022 (push) Waiting to run
Testing / Upload to Codecov (push) Blocked by required conditions
Scan / Check PR run (push) Waiting to run
Scan / Checkmarx (push) Blocked by required conditions
Scan / Sonar (push) Blocked by required conditions
Testing / Run tests (push) Waiting to run
Testing / Run Rust tests on macos-14 (push) Waiting to run
Testing / Run Rust tests on ubuntu-22.04 (push) Waiting to run
Testing / Run Rust tests on windows-2022 (push) Waiting to run
Testing / Rust Coverage (push) Waiting to run
Some checks are pending
Auto Update Branch / Update Branch (push) Waiting to run
Chromatic / Check PR run (push) Waiting to run
Chromatic / Chromatic (push) Blocked by required conditions
Lint / Lint (push) Waiting to run
Lint / Run Rust lint on macos-14 (push) Waiting to run
Lint / Run Rust lint on ubuntu-24.04 (push) Waiting to run
Lint / Run Rust lint on windows-2022 (push) Waiting to run
Testing / Upload to Codecov (push) Blocked by required conditions
Scan / Check PR run (push) Waiting to run
Scan / Checkmarx (push) Blocked by required conditions
Scan / Sonar (push) Blocked by required conditions
Testing / Run tests (push) Waiting to run
Testing / Run Rust tests on macos-14 (push) Waiting to run
Testing / Run Rust tests on ubuntu-22.04 (push) Waiting to run
Testing / Run Rust tests on windows-2022 (push) Waiting to run
Testing / Rust Coverage (push) Waiting to run
Add desktop header component
This commit is contained in:
parent
1022d21654
commit
95100b6f23
9 changed files with 229 additions and 34 deletions
|
|
@ -361,6 +361,7 @@ const routes: Routes = [
|
|||
{
|
||||
path: "new-sends",
|
||||
component: SendV2Component,
|
||||
data: { pageTitle: { key: "send" } } satisfies RouteDataProperties,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
<div class="tw-my-4 tw-px-4">
|
||||
<bit-header [title]="resolvedTitle()" [icon]="icon()">
|
||||
<ng-container slot="breadcrumbs">
|
||||
<ng-content select="[slot=breadcrumbs]" />
|
||||
</ng-container>
|
||||
|
||||
<ng-content />
|
||||
|
||||
<ng-container slot="title-suffix">
|
||||
<ng-content select="[slot=title-suffix]" />
|
||||
</ng-container>
|
||||
|
||||
<ng-container slot="secondary">
|
||||
<ng-content select="[slot=secondary]" />
|
||||
</ng-container>
|
||||
|
||||
<ng-container slot="tabs">
|
||||
<ng-content select="[slot=tabs]" />
|
||||
</ng-container>
|
||||
</bit-header>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,122 @@
|
|||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { ActivatedRoute } from "@angular/router";
|
||||
import { mock } from "jest-mock-extended";
|
||||
import { of } from "rxjs";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { HeaderComponent } from "@bitwarden/components";
|
||||
|
||||
import { DesktopHeaderComponent } from "./desktop-header.component";
|
||||
|
||||
describe("DesktopHeaderComponent", () => {
|
||||
let component: DesktopHeaderComponent;
|
||||
let fixture: ComponentFixture<DesktopHeaderComponent>;
|
||||
let mockI18nService: ReturnType<typeof mock<I18nService>>;
|
||||
let mockActivatedRoute: { data: any };
|
||||
|
||||
beforeEach(async () => {
|
||||
mockI18nService = mock<I18nService>();
|
||||
mockI18nService.t.mockImplementation((key: string) => `translated_${key}`);
|
||||
|
||||
mockActivatedRoute = {
|
||||
data: of({}),
|
||||
};
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [DesktopHeaderComponent, HeaderComponent],
|
||||
providers: [
|
||||
{
|
||||
provide: I18nService,
|
||||
useValue: mockI18nService,
|
||||
},
|
||||
{
|
||||
provide: ActivatedRoute,
|
||||
useValue: mockActivatedRoute,
|
||||
},
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(DesktopHeaderComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it("creates component", () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it("renders bit-header component", () => {
|
||||
const compiled = fixture.nativeElement;
|
||||
const headerElement = compiled.querySelector("bit-header");
|
||||
|
||||
expect(headerElement).toBeTruthy();
|
||||
});
|
||||
|
||||
describe("title resolution", () => {
|
||||
it("uses title input when provided", () => {
|
||||
fixture.componentRef.setInput("title", "Direct Title");
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component["resolvedTitle"]()).toBe("Direct Title");
|
||||
});
|
||||
|
||||
it("uses route data titleId when no direct title provided", () => {
|
||||
mockActivatedRoute.data = of({
|
||||
pageTitle: { key: "sends" },
|
||||
});
|
||||
|
||||
fixture = TestBed.createComponent(DesktopHeaderComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(mockI18nService.t).toHaveBeenCalledWith("sends");
|
||||
expect(component["resolvedTitle"]()).toBe("translated_sends");
|
||||
});
|
||||
|
||||
it("returns empty string when no title or route data provided", () => {
|
||||
mockActivatedRoute.data = of({});
|
||||
|
||||
fixture = TestBed.createComponent(DesktopHeaderComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component["resolvedTitle"]()).toBe("");
|
||||
});
|
||||
|
||||
it("prioritizes direct title over route data", () => {
|
||||
mockActivatedRoute.data = of({
|
||||
pageTitle: { key: "sends" },
|
||||
});
|
||||
|
||||
fixture = TestBed.createComponent(DesktopHeaderComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.componentRef.setInput("title", "Override Title");
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component["resolvedTitle"]()).toBe("Override Title");
|
||||
});
|
||||
});
|
||||
|
||||
describe("icon input", () => {
|
||||
it("accepts icon input", () => {
|
||||
fixture.componentRef.setInput("icon", "bwi-send");
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.icon()).toBe("bwi-send");
|
||||
});
|
||||
|
||||
it("defaults to undefined when no icon provided", () => {
|
||||
expect(component.icon()).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("content projection", () => {
|
||||
it("wraps bit-header component for slot pass-through", () => {
|
||||
const compiled = fixture.nativeElement;
|
||||
const bitHeader = compiled.querySelector("bit-header");
|
||||
|
||||
// Verify bit-header exists and can receive projected content
|
||||
expect(bitHeader).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
import { ChangeDetectionStrategy, Component, computed, inject, input } from "@angular/core";
|
||||
import { toSignal } from "@angular/core/rxjs-interop";
|
||||
import { ActivatedRoute } from "@angular/router";
|
||||
import { map } from "rxjs";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { HeaderComponent, BannerModule } from "@bitwarden/components";
|
||||
|
||||
@Component({
|
||||
selector: "app-header",
|
||||
templateUrl: "./desktop-header.component.html",
|
||||
imports: [BannerModule, HeaderComponent],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class DesktopHeaderComponent {
|
||||
private route = inject(ActivatedRoute);
|
||||
private i18nService = inject(I18nService);
|
||||
|
||||
/**
|
||||
* Title to display in header (takes precedence over route data)
|
||||
*/
|
||||
readonly title = input<string>();
|
||||
|
||||
/**
|
||||
* Icon to show before the title
|
||||
*/
|
||||
readonly icon = input<string>();
|
||||
|
||||
private readonly routeData = toSignal(
|
||||
this.route.data.pipe(
|
||||
map((params) => ({
|
||||
titleId: params["pageTitle"]?.["key"] as string | undefined,
|
||||
})),
|
||||
),
|
||||
{ initialValue: { titleId: undefined } },
|
||||
);
|
||||
|
||||
protected readonly resolvedTitle = computed(() => {
|
||||
const directTitle = this.title();
|
||||
if (directTitle) {
|
||||
return directTitle;
|
||||
}
|
||||
|
||||
const titleId = this.routeData().titleId;
|
||||
return titleId ? this.i18nService.t(titleId) : "";
|
||||
});
|
||||
}
|
||||
1
apps/desktop/src/app/layout/header/index.ts
Normal file
1
apps/desktop/src/app/layout/header/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { DesktopHeaderComponent } from "./desktop-header.component";
|
||||
|
|
@ -1,40 +1,31 @@
|
|||
<div id="sends" class="vault">
|
||||
<div class="send-items-panel tw-w-2/5">
|
||||
<div class="content">
|
||||
<!-- Header with Send title and New button -->
|
||||
<div
|
||||
class="tw-flex tw-items-center tw-justify-between tw-px-4 tw-py-3 tw-border-b tw-border-secondary-300"
|
||||
>
|
||||
<h1 class="tw-text-base tw-font-semibold tw-m-0">{{ "send" | i18n }}</h1>
|
||||
@if (!disableSend()) {
|
||||
<tools-new-send-dropdown-v2
|
||||
[buttonType]="'primary'"
|
||||
(addSend)="addSend($event)"
|
||||
></tools-new-send-dropdown-v2>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Header with Send title and New button -->
|
||||
<app-header>
|
||||
@if (!disableSend()) {
|
||||
<tools-new-send-dropdown-v2 buttonType="primary" (addSend)="addSend($event)" />
|
||||
}
|
||||
</app-header>
|
||||
<div class="tw-my-4 tw-px-4">
|
||||
<!-- Send List Component -->
|
||||
<div class="tw-my-4 tw-px-4">
|
||||
<tools-send-list
|
||||
[sends]="filteredSends()"
|
||||
[loading]="loading()"
|
||||
[disableSend]="disableSend()"
|
||||
[listState]="listState()"
|
||||
[searchText]="currentSearchText()"
|
||||
(editSend)="onEditSend($event)"
|
||||
(copySend)="onCopySend($event)"
|
||||
(deleteSend)="onDeleteSend($event)"
|
||||
(removePassword)="onRemovePassword($event)"
|
||||
>
|
||||
<tools-new-send-dropdown-v2
|
||||
slot="empty-button"
|
||||
[hideIcon]="true"
|
||||
buttonType="primary"
|
||||
(addSend)="addSend($event)"
|
||||
/>
|
||||
</tools-send-list>
|
||||
</div>
|
||||
<tools-send-list
|
||||
[sends]="filteredSends()"
|
||||
[loading]="loading()"
|
||||
[disableSend]="disableSend()"
|
||||
[listState]="listState()"
|
||||
[searchText]="currentSearchText()"
|
||||
(editSend)="onEditSend($event)"
|
||||
(copySend)="onCopySend($event)"
|
||||
(deleteSend)="onDeleteSend($event)"
|
||||
(removePassword)="onRemovePassword($event)"
|
||||
>
|
||||
<tools-new-send-dropdown-v2
|
||||
slot="empty-button"
|
||||
[hideIcon]="true"
|
||||
buttonType="primary"
|
||||
(addSend)="addSend($event)"
|
||||
/>
|
||||
</tools-send-list>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
import { ChangeDetectorRef } from "@angular/core";
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { provideNoopAnimations } from "@angular/platform-browser/animations";
|
||||
import { ActivatedRoute } from "@angular/router";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { of } from "rxjs";
|
||||
|
||||
|
|
@ -89,6 +90,12 @@ describe("SendV2Component", () => {
|
|||
},
|
||||
{ provide: MessagingService, useValue: mock<MessagingService>() },
|
||||
{ provide: ConfigService, useValue: mock<ConfigService>() },
|
||||
{
|
||||
provide: ActivatedRoute,
|
||||
useValue: {
|
||||
data: of({}),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ import {
|
|||
} from "@bitwarden/send-ui";
|
||||
|
||||
import { DesktopPremiumUpgradePromptService } from "../../../services/desktop-premium-upgrade-prompt.service";
|
||||
import { DesktopHeaderComponent } from "../../layout/header";
|
||||
import { AddEditComponent } from "../send/add-edit.component";
|
||||
|
||||
const Action = Object.freeze({
|
||||
|
|
@ -56,6 +57,7 @@ type Action = (typeof Action)[keyof typeof Action];
|
|||
AddEditComponent,
|
||||
SendListComponent,
|
||||
NewSendDropdownV2Component,
|
||||
DesktopHeaderComponent,
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
import { ChangeDetectionStrategy, Component, input } from "@angular/core";
|
||||
|
||||
import { TypographyDirective } from "../typography/typography.directive";
|
||||
|
||||
@Component({
|
||||
selector: "bit-header",
|
||||
templateUrl: "./header.component.html",
|
||||
imports: [TypographyDirective],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue