-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[PM-7231] Product Switcher within navigation sidebar (#8810)
* refactor: move logic for products into a service - This is in preparation for having having the navigation menu show products based off of the same logic. * add extra small font size to tailwind config * remove absolute positioning from toggle width component - it now sits beneath the product switcher * update product switcher to have UI details that are only shown in the navigation pane * add navigation oriented product switcher * integrate navigation product switcher into secrets manager * integrate navigation product switcher into provider console * integrate navigation product switcher into user layout * integrate navigation product switcher into organizations * add translation for "switch" * hide active styles from navigation product switcher * update storybook for product switcher stories * remove unneeded full width style * use protected readonly variable instead of getter * migrate stories to CSF3 * remove double subscription to `moreProducts$` * only use wrapping div in navigation switcher story - less vertical space is taken up * update to satisfies * refactor `navigationUI` to `otherProductOverrides` * move observables to protected readonly * apply margin-top via class on the host component * remove switch text from the navigation product switcher * Allow for the active navigation switcher to be shown * remove xxs font style * remove unneeded module * remove switch from stories * remove defensive nullish coalescing * remove merge leftovers * Defect PM-7899 - show organizations product at the top of the other products list * Defect PM-7951 use attr.icon to keep the icon as an attribute after prod mode is enabled * Defect PM-7948 update path based on the current org * force active styles for navigation items (#9128) * add horizontal margin to icon --------- Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com>
- Loading branch information
1 parent
ff19514
commit 07076eb
Showing
21 changed files
with
932 additions
and
168 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
35 changes: 35 additions & 0 deletions
35
...b/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.component.html
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
<div class="tw-mt-auto"> | ||
<!-- [attr.icon] is used to keep the icon attribute on the bit-nav-item after prod mode is enabled. Matches other navigation items and assists in automated testing. --> | ||
<bit-nav-item | ||
*ngFor="let product of accessibleProducts$ | async" | ||
[icon]="product.icon" | ||
[text]="product.name" | ||
[route]="product.appRoute" | ||
[attr.icon]="product.icon" | ||
[forceActiveStyles]="product.isActive" | ||
> | ||
</bit-nav-item> | ||
<ng-container *ngIf="moreProducts$ | async as moreProducts"> | ||
<section | ||
*ngIf="moreProducts.length > 0" | ||
class="tw-mt-2 tw-flex tw-w-full tw-flex-col tw-gap-2 tw-border-0 tw-border-t tw-border-solid tw-border-t-text-alt2" | ||
> | ||
<span class="tw-text-xs !tw-text-alt2 tw-p-2 tw-pb-0">{{ "moreFromBitwarden" | i18n }}</span> | ||
<a | ||
*ngFor="let more of moreProducts" | ||
[href]="more.marketingRoute" | ||
target="_blank" | ||
rel="noreferrer" | ||
class="tw-flex tw-py-2 tw-px-4 tw-font-semibold !tw-text-alt2 !tw-no-underline hover:tw-bg-primary-300/60 [&>:not(.bwi)]:hover:tw-underline" | ||
> | ||
<i class="bwi bwi-fw {{ more.icon }} tw-mt-1 tw-mx-1"></i> | ||
<div> | ||
{{ more.otherProductOverrides?.name ?? more.name }} | ||
<div *ngIf="more.otherProductOverrides?.supportingText" class="tw-text-xs tw-font-normal"> | ||
{{ more.otherProductOverrides.supportingText }} | ||
</div> | ||
</div> | ||
</a> | ||
</section> | ||
</ng-container> | ||
</div> |
194 changes: 194 additions & 0 deletions
194
...rc/app/layouts/product-switcher/navigation-switcher/navigation-switcher.component.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,194 @@ | ||
import { ComponentFixture, TestBed } from "@angular/core/testing"; | ||
import { By } from "@angular/platform-browser"; | ||
import { ActivatedRoute, RouterModule } from "@angular/router"; | ||
import { mock, MockProxy } from "jest-mock-extended"; | ||
import { BehaviorSubject } from "rxjs"; | ||
|
||
import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe"; | ||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; | ||
import { BitIconButtonComponent } from "@bitwarden/components/src/icon-button/icon-button.component"; | ||
import { NavItemComponent } from "@bitwarden/components/src/navigation/nav-item.component"; | ||
|
||
import { ProductSwitcherItem, ProductSwitcherService } from "../shared/product-switcher.service"; | ||
|
||
import { NavigationProductSwitcherComponent } from "./navigation-switcher.component"; | ||
|
||
describe("NavigationProductSwitcherComponent", () => { | ||
let fixture: ComponentFixture<NavigationProductSwitcherComponent>; | ||
let productSwitcherService: MockProxy<ProductSwitcherService>; | ||
|
||
const mockProducts$ = new BehaviorSubject<{ | ||
bento: ProductSwitcherItem[]; | ||
other: ProductSwitcherItem[]; | ||
}>({ | ||
bento: [], | ||
other: [], | ||
}); | ||
|
||
beforeEach(async () => { | ||
productSwitcherService = mock<ProductSwitcherService>(); | ||
productSwitcherService.products$ = mockProducts$; | ||
mockProducts$.next({ bento: [], other: [] }); | ||
|
||
await TestBed.configureTestingModule({ | ||
imports: [RouterModule], | ||
declarations: [ | ||
NavigationProductSwitcherComponent, | ||
NavItemComponent, | ||
BitIconButtonComponent, | ||
I18nPipe, | ||
], | ||
providers: [ | ||
{ provide: ProductSwitcherService, useValue: productSwitcherService }, | ||
{ | ||
provide: I18nService, | ||
useValue: mock<I18nService>(), | ||
}, | ||
{ | ||
provide: ActivatedRoute, | ||
useValue: mock<ActivatedRoute>(), | ||
}, | ||
], | ||
}).compileComponents(); | ||
}); | ||
|
||
beforeEach(() => { | ||
fixture = TestBed.createComponent(NavigationProductSwitcherComponent); | ||
fixture.detectChanges(); | ||
}); | ||
|
||
describe("other products", () => { | ||
it("links to `marketingRoute`", () => { | ||
mockProducts$.next({ | ||
bento: [], | ||
other: [ | ||
{ | ||
isActive: false, | ||
name: "Other Product", | ||
icon: "bwi-lock", | ||
marketingRoute: "https://www.example.com/", | ||
}, | ||
], | ||
}); | ||
|
||
fixture.detectChanges(); | ||
|
||
const link = fixture.nativeElement.querySelector("a"); | ||
|
||
expect(link.getAttribute("href")).toBe("https://www.example.com/"); | ||
}); | ||
|
||
it("uses `otherProductOverrides` when available", () => { | ||
mockProducts$.next({ | ||
bento: [], | ||
other: [ | ||
{ | ||
isActive: false, | ||
name: "Other Product", | ||
icon: "bwi-lock", | ||
marketingRoute: "https://www.example.com/", | ||
otherProductOverrides: { name: "Alternate name" }, | ||
}, | ||
], | ||
}); | ||
|
||
fixture.detectChanges(); | ||
|
||
expect(fixture.nativeElement.querySelector("a").textContent.trim()).toBe("Alternate name"); | ||
|
||
mockProducts$.next({ | ||
bento: [], | ||
other: [ | ||
{ | ||
isActive: false, | ||
name: "Other Product", | ||
icon: "bwi-lock", | ||
marketingRoute: "https://www.example.com/", | ||
otherProductOverrides: { name: "Alternate name", supportingText: "Supporting Text" }, | ||
}, | ||
], | ||
}); | ||
|
||
fixture.detectChanges(); | ||
|
||
expect(fixture.nativeElement.querySelector("a").textContent.trim().replace(/\s+/g, " ")).toBe( | ||
"Alternate name Supporting Text", | ||
); | ||
}); | ||
|
||
it("shows Organizations first in the other products list", () => { | ||
mockProducts$.next({ | ||
bento: [], | ||
other: [ | ||
{ name: "AA Product", icon: "bwi-lock", marketingRoute: "https://www.example.com/" }, | ||
{ name: "Test Product", icon: "bwi-lock", marketingRoute: "https://www.example.com/" }, | ||
{ name: "Organizations", icon: "bwi-lock", marketingRoute: "https://www.example.com/" }, | ||
], | ||
}); | ||
|
||
fixture.detectChanges(); | ||
|
||
const links = fixture.nativeElement.querySelectorAll("a"); | ||
|
||
expect(links.length).toBe(3); | ||
|
||
expect(links[0].textContent).toContain("Organizations"); | ||
expect(links[1].textContent).toContain("AA Product"); | ||
expect(links[2].textContent).toContain("Test Product"); | ||
}); | ||
|
||
it('shows the nav item as active when "isActive" is true', () => { | ||
mockProducts$.next({ | ||
bento: [ | ||
{ | ||
name: "Organizations", | ||
icon: "bwi-lock", | ||
marketingRoute: "https://www.example.com/", | ||
isActive: true, | ||
}, | ||
], | ||
other: [], | ||
}); | ||
|
||
fixture.detectChanges(); | ||
|
||
const navItem = fixture.debugElement.query(By.directive(NavItemComponent)); | ||
|
||
expect(navItem.componentInstance.forceActiveStyles).toBe(true); | ||
}); | ||
}); | ||
|
||
describe("available products", () => { | ||
it("shows all products", () => { | ||
mockProducts$.next({ | ||
bento: [ | ||
{ isActive: true, name: "Password Manager", icon: "bwi-lock", appRoute: "/vault" }, | ||
{ isActive: false, name: "Secret Manager", icon: "bwi-lock", appRoute: "/sm" }, | ||
], | ||
other: [], | ||
}); | ||
|
||
fixture.detectChanges(); | ||
|
||
const links = fixture.nativeElement.querySelectorAll("a"); | ||
|
||
expect(links.length).toBe(2); | ||
|
||
expect(links[0].textContent).toContain("Password Manager"); | ||
expect(links[1].textContent).toContain("Secret Manager"); | ||
}); | ||
}); | ||
|
||
it("links to `appRoute`", () => { | ||
mockProducts$.next({ | ||
bento: [{ isActive: false, name: "Password Manager", icon: "bwi-lock", appRoute: "/vault" }], | ||
other: [], | ||
}); | ||
|
||
fixture.detectChanges(); | ||
|
||
const link = fixture.nativeElement.querySelector("a"); | ||
|
||
expect(link.getAttribute("href")).toBe("/vault"); | ||
}); | ||
}); |
24 changes: 24 additions & 0 deletions
24
...web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.component.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
import { Component } from "@angular/core"; | ||
import { map, Observable } from "rxjs"; | ||
|
||
import { ProductSwitcherItem, ProductSwitcherService } from "../shared/product-switcher.service"; | ||
|
||
@Component({ | ||
selector: "navigation-product-switcher", | ||
templateUrl: "./navigation-switcher.component.html", | ||
}) | ||
export class NavigationProductSwitcherComponent { | ||
constructor(private productSwitcherService: ProductSwitcherService) {} | ||
|
||
protected readonly accessibleProducts$: Observable<ProductSwitcherItem[]> = | ||
this.productSwitcherService.products$.pipe(map((products) => products.bento ?? [])); | ||
|
||
protected readonly moreProducts$: Observable<ProductSwitcherItem[]> = | ||
this.productSwitcherService.products$.pipe( | ||
map((products) => products.other ?? []), | ||
// Ensure that organizations is displayed first in the other products list | ||
// This differs from the order in `ProductSwitcherContentComponent` but matches the intent | ||
// from product & design | ||
map((products) => products.sort((product) => (product.name === "Organizations" ? -1 : 1))), | ||
); | ||
} |
Oops, something went wrong.