From e7416384dcb1b1b242a4672366bf5a6e81d1ee09 Mon Sep 17 00:00:00 2001 From: Will Martin Date: Tue, 30 Apr 2024 10:27:47 -0400 Subject: [PATCH 1/9] [CL-220] item components (#8870) --- .../popup/layout/popup-layout.stories.ts | 41 ++- .../src/a11y/a11y-cell.directive.ts | 33 ++ .../src/a11y/a11y-grid.directive.ts | 145 ++++++++ .../components/src/a11y/a11y-row.directive.ts | 31 ++ libs/components/src/badge/badge.directive.ts | 9 +- .../src/icon-button/icon-button.component.ts | 16 +- libs/components/src/index.ts | 1 + .../src/input/autofocus.directive.ts | 9 +- libs/components/src/item/index.ts | 1 + .../src/item/item-action.component.ts | 12 + .../src/item/item-content.component.html | 16 + .../src/item/item-content.component.ts | 15 + .../src/item/item-group.component.ts | 13 + libs/components/src/item/item.component.html | 21 ++ libs/components/src/item/item.component.ts | 29 ++ libs/components/src/item/item.mdx | 141 ++++++++ libs/components/src/item/item.module.ts | 12 + libs/components/src/item/item.stories.ts | 326 ++++++++++++++++++ .../components/src/search/search.component.ts | 6 +- .../src/shared/focusable-element.ts | 8 + libs/components/src/styles.scss | 2 +- 21 files changed, 858 insertions(+), 29 deletions(-) create mode 100644 libs/components/src/a11y/a11y-cell.directive.ts create mode 100644 libs/components/src/a11y/a11y-grid.directive.ts create mode 100644 libs/components/src/a11y/a11y-row.directive.ts create mode 100644 libs/components/src/item/index.ts create mode 100644 libs/components/src/item/item-action.component.ts create mode 100644 libs/components/src/item/item-content.component.html create mode 100644 libs/components/src/item/item-content.component.ts create mode 100644 libs/components/src/item/item-group.component.ts create mode 100644 libs/components/src/item/item.component.html create mode 100644 libs/components/src/item/item.component.ts create mode 100644 libs/components/src/item/item.mdx create mode 100644 libs/components/src/item/item.module.ts create mode 100644 libs/components/src/item/item.stories.ts create mode 100644 libs/components/src/shared/focusable-element.ts diff --git a/apps/browser/src/platform/popup/layout/popup-layout.stories.ts b/apps/browser/src/platform/popup/layout/popup-layout.stories.ts index 1b10e50c0c..77530d06e5 100644 --- a/apps/browser/src/platform/popup/layout/popup-layout.stories.ts +++ b/apps/browser/src/platform/popup/layout/popup-layout.stories.ts @@ -6,9 +6,11 @@ import { Meta, StoryObj, applicationConfig, moduleMetadata } from "@storybook/an import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { AvatarModule, + BadgeModule, ButtonModule, I18nMockService, IconButtonModule, + ItemModule, } from "@bitwarden/components"; import { PopupFooterComponent } from "./popup-footer.component"; @@ -30,23 +32,34 @@ class ExtensionContainerComponent {} @Component({ selector: "vault-placeholder", template: ` -
vault item
-
vault item
-
vault item
-
vault item
-
vault item
-
vault item
-
vault item
-
vault item
-
vault item
-
vault item
-
vault item
-
vault item
-
vault item last item
+ + + + + + + + + + + + + + + + + `, standalone: true, + imports: [CommonModule, ItemModule, BadgeModule, IconButtonModule], }) -class VaultComponent {} +class VaultComponent { + protected data = Array.from(Array(20).keys()); +} @Component({ selector: "generator-placeholder", diff --git a/libs/components/src/a11y/a11y-cell.directive.ts b/libs/components/src/a11y/a11y-cell.directive.ts new file mode 100644 index 0000000000..fdd75c076f --- /dev/null +++ b/libs/components/src/a11y/a11y-cell.directive.ts @@ -0,0 +1,33 @@ +import { ContentChild, Directive, ElementRef, HostBinding } from "@angular/core"; + +import { FocusableElement } from "../shared/focusable-element"; + +@Directive({ + selector: "bitA11yCell", + standalone: true, + providers: [{ provide: FocusableElement, useExisting: A11yCellDirective }], +}) +export class A11yCellDirective implements FocusableElement { + @HostBinding("attr.role") + role: "gridcell" | null; + + @ContentChild(FocusableElement) + private focusableChild: FocusableElement; + + getFocusTarget() { + let focusTarget: HTMLElement; + if (this.focusableChild) { + focusTarget = this.focusableChild.getFocusTarget(); + } else { + focusTarget = this.elementRef.nativeElement.querySelector("button, a"); + } + + if (!focusTarget) { + return this.elementRef.nativeElement; + } + + return focusTarget; + } + + constructor(private elementRef: ElementRef) {} +} diff --git a/libs/components/src/a11y/a11y-grid.directive.ts b/libs/components/src/a11y/a11y-grid.directive.ts new file mode 100644 index 0000000000..c632376f4f --- /dev/null +++ b/libs/components/src/a11y/a11y-grid.directive.ts @@ -0,0 +1,145 @@ +import { + AfterViewInit, + ContentChildren, + Directive, + HostBinding, + HostListener, + Input, + QueryList, +} from "@angular/core"; + +import type { A11yCellDirective } from "./a11y-cell.directive"; +import { A11yRowDirective } from "./a11y-row.directive"; + +@Directive({ + selector: "bitA11yGrid", + standalone: true, +}) +export class A11yGridDirective implements AfterViewInit { + @HostBinding("attr.role") + role = "grid"; + + @ContentChildren(A11yRowDirective) + rows: QueryList; + + /** The number of pages to navigate on `PageUp` and `PageDown` */ + @Input() pageSize = 5; + + private grid: A11yCellDirective[][]; + + /** The row that currently has focus */ + private activeRow = 0; + + /** The cell that currently has focus */ + private activeCol = 0; + + @HostListener("keydown", ["$event"]) + onKeyDown(event: KeyboardEvent) { + switch (event.code) { + case "ArrowUp": + this.updateCellFocusByDelta(-1, 0); + break; + case "ArrowRight": + this.updateCellFocusByDelta(0, 1); + break; + case "ArrowDown": + this.updateCellFocusByDelta(1, 0); + break; + case "ArrowLeft": + this.updateCellFocusByDelta(0, -1); + break; + case "Home": + this.updateCellFocusByDelta(-this.activeRow, -this.activeCol); + break; + case "End": + this.updateCellFocusByDelta(this.grid.length, this.grid[this.grid.length - 1].length); + break; + case "PageUp": + this.updateCellFocusByDelta(-this.pageSize, 0); + break; + case "PageDown": + this.updateCellFocusByDelta(this.pageSize, 0); + break; + default: + return; + } + + /** Prevent default scrolling behavior */ + event.preventDefault(); + } + + ngAfterViewInit(): void { + this.initializeGrid(); + } + + private initializeGrid(): void { + try { + this.grid = this.rows.map((listItem) => { + listItem.role = "row"; + return [...listItem.cells]; + }); + this.grid.flat().forEach((cell) => { + cell.role = "gridcell"; + cell.getFocusTarget().tabIndex = -1; + }); + + this.getActiveCellContent().tabIndex = 0; + } catch (error) { + // eslint-disable-next-line no-console + console.error("Unable to initialize grid"); + } + } + + /** Get the focusable content of the active cell */ + private getActiveCellContent(): HTMLElement { + return this.grid[this.activeRow][this.activeCol].getFocusTarget(); + } + + /** Move focus via a delta against the currently active gridcell */ + private updateCellFocusByDelta(rowDelta: number, colDelta: number) { + const prevActive = this.getActiveCellContent(); + + this.activeCol += colDelta; + this.activeRow += rowDelta; + + // Row upper bound + if (this.activeRow >= this.grid.length) { + this.activeRow = this.grid.length - 1; + } + + // Row lower bound + if (this.activeRow < 0) { + this.activeRow = 0; + } + + // Column upper bound + if (this.activeCol >= this.grid[this.activeRow].length) { + if (this.activeRow < this.grid.length - 1) { + // Wrap to next row on right arrow + this.activeCol = 0; + this.activeRow += 1; + } else { + this.activeCol = this.grid[this.activeRow].length - 1; + } + } + + // Column lower bound + if (this.activeCol < 0) { + if (this.activeRow > 0) { + // Wrap to prev row on left arrow + this.activeRow -= 1; + this.activeCol = this.grid[this.activeRow].length - 1; + } else { + this.activeCol = 0; + } + } + + const nextActive = this.getActiveCellContent(); + nextActive.tabIndex = 0; + nextActive.focus(); + + if (nextActive !== prevActive) { + prevActive.tabIndex = -1; + } + } +} diff --git a/libs/components/src/a11y/a11y-row.directive.ts b/libs/components/src/a11y/a11y-row.directive.ts new file mode 100644 index 0000000000..e062eb2b5a --- /dev/null +++ b/libs/components/src/a11y/a11y-row.directive.ts @@ -0,0 +1,31 @@ +import { + AfterViewInit, + ContentChildren, + Directive, + HostBinding, + QueryList, + ViewChildren, +} from "@angular/core"; + +import { A11yCellDirective } from "./a11y-cell.directive"; + +@Directive({ + selector: "bitA11yRow", + standalone: true, +}) +export class A11yRowDirective implements AfterViewInit { + @HostBinding("attr.role") + role: "row" | null; + + cells: A11yCellDirective[]; + + @ViewChildren(A11yCellDirective) + private viewCells: QueryList; + + @ContentChildren(A11yCellDirective) + private contentCells: QueryList; + + ngAfterViewInit(): void { + this.cells = [...this.viewCells, ...this.contentCells]; + } +} diff --git a/libs/components/src/badge/badge.directive.ts b/libs/components/src/badge/badge.directive.ts index b81b9f80e2..acce4a18aa 100644 --- a/libs/components/src/badge/badge.directive.ts +++ b/libs/components/src/badge/badge.directive.ts @@ -1,5 +1,7 @@ import { Directive, ElementRef, HostBinding, Input } from "@angular/core"; +import { FocusableElement } from "../shared/focusable-element"; + export type BadgeVariant = "primary" | "secondary" | "success" | "danger" | "warning" | "info"; const styles: Record = { @@ -22,8 +24,9 @@ const hoverStyles: Record = { @Directive({ selector: "span[bitBadge], a[bitBadge], button[bitBadge]", + providers: [{ provide: FocusableElement, useExisting: BadgeDirective }], }) -export class BadgeDirective { +export class BadgeDirective implements FocusableElement { @HostBinding("class") get classList() { return [ "tw-inline-block", @@ -62,6 +65,10 @@ export class BadgeDirective { */ @Input() truncate = true; + getFocusTarget() { + return this.el.nativeElement; + } + private hasHoverEffects = false; constructor(private el: ElementRef) { diff --git a/libs/components/src/icon-button/icon-button.component.ts b/libs/components/src/icon-button/icon-button.component.ts index 53e8032795..54f6dfda96 100644 --- a/libs/components/src/icon-button/icon-button.component.ts +++ b/libs/components/src/icon-button/icon-button.component.ts @@ -1,6 +1,7 @@ -import { Component, HostBinding, Input } from "@angular/core"; +import { Component, ElementRef, HostBinding, Input } from "@angular/core"; import { ButtonLikeAbstraction, ButtonType } from "../shared/button-like.abstraction"; +import { FocusableElement } from "../shared/focusable-element"; export type IconButtonType = ButtonType | "contrast" | "main" | "muted" | "light"; @@ -123,9 +124,12 @@ const sizes: Record = { @Component({ selector: "button[bitIconButton]:not(button[bitButton])", templateUrl: "icon-button.component.html", - providers: [{ provide: ButtonLikeAbstraction, useExisting: BitIconButtonComponent }], + providers: [ + { provide: ButtonLikeAbstraction, useExisting: BitIconButtonComponent }, + { provide: FocusableElement, useExisting: BitIconButtonComponent }, + ], }) -export class BitIconButtonComponent implements ButtonLikeAbstraction { +export class BitIconButtonComponent implements ButtonLikeAbstraction, FocusableElement { @Input("bitIconButton") icon: string; @Input() buttonType: IconButtonType; @@ -162,4 +166,10 @@ export class BitIconButtonComponent implements ButtonLikeAbstraction { setButtonType(value: "primary" | "secondary" | "danger" | "unstyled") { this.buttonType = value; } + + getFocusTarget() { + return this.elementRef.nativeElement; + } + + constructor(private elementRef: ElementRef) {} } diff --git a/libs/components/src/index.ts b/libs/components/src/index.ts index 36185911a6..1e4a3a86ff 100644 --- a/libs/components/src/index.ts +++ b/libs/components/src/index.ts @@ -16,6 +16,7 @@ export * from "./form-field"; export * from "./icon-button"; export * from "./icon"; export * from "./input"; +export * from "./item"; export * from "./layout"; export * from "./link"; export * from "./menu"; diff --git a/libs/components/src/input/autofocus.directive.ts b/libs/components/src/input/autofocus.directive.ts index f8161ee6e0..625e7fbc92 100644 --- a/libs/components/src/input/autofocus.directive.ts +++ b/libs/components/src/input/autofocus.directive.ts @@ -3,12 +3,7 @@ import { take } from "rxjs/operators"; import { Utils } from "@bitwarden/common/platform/misc/utils"; -/** - * Interface for implementing focusable components. Used by the AutofocusDirective. - */ -export abstract class FocusableElement { - focus: () => void; -} +import { FocusableElement } from "../shared/focusable-element"; /** * Directive to focus an element. @@ -46,7 +41,7 @@ export class AutofocusDirective { private focus() { if (this.focusableElement) { - this.focusableElement.focus(); + this.focusableElement.getFocusTarget().focus(); } else { this.el.nativeElement.focus(); } diff --git a/libs/components/src/item/index.ts b/libs/components/src/item/index.ts new file mode 100644 index 0000000000..56896cdc3c --- /dev/null +++ b/libs/components/src/item/index.ts @@ -0,0 +1 @@ +export * from "./item.module"; diff --git a/libs/components/src/item/item-action.component.ts b/libs/components/src/item/item-action.component.ts new file mode 100644 index 0000000000..8cabf5c5c2 --- /dev/null +++ b/libs/components/src/item/item-action.component.ts @@ -0,0 +1,12 @@ +import { Component } from "@angular/core"; + +import { A11yCellDirective } from "../a11y/a11y-cell.directive"; + +@Component({ + selector: "bit-item-action", + standalone: true, + imports: [], + template: ``, + providers: [{ provide: A11yCellDirective, useExisting: ItemActionComponent }], +}) +export class ItemActionComponent extends A11yCellDirective {} diff --git a/libs/components/src/item/item-content.component.html b/libs/components/src/item/item-content.component.html new file mode 100644 index 0000000000..d034a4a001 --- /dev/null +++ b/libs/components/src/item/item-content.component.html @@ -0,0 +1,16 @@ +
+ + +
+
+ +
+
+ +
+
+
+ +
+ +
diff --git a/libs/components/src/item/item-content.component.ts b/libs/components/src/item/item-content.component.ts new file mode 100644 index 0000000000..58a1198512 --- /dev/null +++ b/libs/components/src/item/item-content.component.ts @@ -0,0 +1,15 @@ +import { CommonModule } from "@angular/common"; +import { ChangeDetectionStrategy, Component } from "@angular/core"; + +@Component({ + selector: "bit-item-content, [bit-item-content]", + standalone: true, + imports: [CommonModule], + templateUrl: `item-content.component.html`, + host: { + class: + "fvw-target tw-outline-none tw-text-main hover:tw-text-main hover:tw-no-underline tw-text-base tw-py-2 tw-px-4 tw-bg-transparent tw-w-full tw-border-none tw-flex tw-gap-4 tw-items-center tw-justify-between", + }, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ItemContentComponent {} diff --git a/libs/components/src/item/item-group.component.ts b/libs/components/src/item/item-group.component.ts new file mode 100644 index 0000000000..2a9a8275cc --- /dev/null +++ b/libs/components/src/item/item-group.component.ts @@ -0,0 +1,13 @@ +import { ChangeDetectionStrategy, Component } from "@angular/core"; + +@Component({ + selector: "bit-item-group", + standalone: true, + imports: [], + template: ``, + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + class: "tw-block", + }, +}) +export class ItemGroupComponent {} diff --git a/libs/components/src/item/item.component.html b/libs/components/src/item/item.component.html new file mode 100644 index 0000000000..0c91c6848e --- /dev/null +++ b/libs/components/src/item/item.component.html @@ -0,0 +1,21 @@ + +
+ + + + +
+ +
+
diff --git a/libs/components/src/item/item.component.ts b/libs/components/src/item/item.component.ts new file mode 100644 index 0000000000..4b7b57fa9f --- /dev/null +++ b/libs/components/src/item/item.component.ts @@ -0,0 +1,29 @@ +import { CommonModule } from "@angular/common"; +import { ChangeDetectionStrategy, Component, HostListener, signal } from "@angular/core"; + +import { A11yRowDirective } from "../a11y/a11y-row.directive"; + +import { ItemActionComponent } from "./item-action.component"; + +@Component({ + selector: "bit-item", + standalone: true, + imports: [CommonModule, ItemActionComponent], + changeDetection: ChangeDetectionStrategy.OnPush, + templateUrl: "item.component.html", + providers: [{ provide: A11yRowDirective, useExisting: ItemComponent }], +}) +export class ItemComponent extends A11yRowDirective { + /** + * We have `:focus-within` and `:focus-visible` but no `:focus-visible-within` + */ + protected focusVisibleWithin = signal(false); + @HostListener("focusin", ["$event.target"]) + onFocusIn(target: HTMLElement) { + this.focusVisibleWithin.set(target.matches(".fvw-target:focus-visible")); + } + @HostListener("focusout") + onFocusOut() { + this.focusVisibleWithin.set(false); + } +} diff --git a/libs/components/src/item/item.mdx b/libs/components/src/item/item.mdx new file mode 100644 index 0000000000..8506de72bb --- /dev/null +++ b/libs/components/src/item/item.mdx @@ -0,0 +1,141 @@ +import { Meta, Story, Primary, Controls, Canvas } from "@storybook/addon-docs"; + +import * as stories from "./item.stories"; + + + +```ts +import { ItemModule } from "@bitwarden/components"; +``` + +# Item + +`` is a horizontal card that contains one or more interactive actions. + +It is a generic container that can be used for either standalone content, an alternative to tables, +or to list nav links. + + + + + +## Primary Content + +The primary content of an item is supplied by `bit-item-content`. + +### Content Types + +The content can be a button, anchor, or static container. + +```html + + Hi, I am a link. + + + + + + + + I'm just static :( + +``` + + + + + +### Content Slots + +`bit-item-content` contains the following slots to help position the content: + +| Slot | Description | +| ------------------ | --------------------------------------------------- | +| default | primary text or arbitrary content; fan favorite | +| `slot="secondary"` | supporting text; under the default slot | +| `slot="start"` | commonly an icon or avatar; before the default slot | +| `slot="end"` | commonly an icon; after the default slot | + +- Note: There is also an `end` slot within `bit-item` itself. Place + [interactive secondary actions](#secondary-actions) there, and place non-interactive content (such + as icons) in `bit-item-content` + +```html + + + +``` + + + + + +## Secondary Actions + +Secondary interactive actions can be placed in the item through the `"end"` slot, outside of +`bit-item-content`. + +Each action must be wrapped by ``. + +Actions are commonly icon buttons or badge buttons. + +```html + + + + + + + + + + + + + + + +``` + +## Item Groups + +Groups of items can be associated by wrapping them in the ``. + + + + + + + + + +### A11y + +Keyboard nav is currently disabled due to a bug when used within a virtual scroll viewport. + +Item groups utilize arrow-based keyboard navigation +([further reading here](https://www.w3.org/WAI/ARIA/apg/patterns/grid/examples/layout-grids/#kbd_label)). + +Use `aria-label` or `aria-labelledby` to give groups an accessible name. + +```html + + ... + ... + ... + +``` + +### Virtual Scrolling + + + + diff --git a/libs/components/src/item/item.module.ts b/libs/components/src/item/item.module.ts new file mode 100644 index 0000000000..226fed11d8 --- /dev/null +++ b/libs/components/src/item/item.module.ts @@ -0,0 +1,12 @@ +import { NgModule } from "@angular/core"; + +import { ItemActionComponent } from "./item-action.component"; +import { ItemContentComponent } from "./item-content.component"; +import { ItemGroupComponent } from "./item-group.component"; +import { ItemComponent } from "./item.component"; + +@NgModule({ + imports: [ItemComponent, ItemContentComponent, ItemActionComponent, ItemGroupComponent], + exports: [ItemComponent, ItemContentComponent, ItemActionComponent, ItemGroupComponent], +}) +export class ItemModule {} diff --git a/libs/components/src/item/item.stories.ts b/libs/components/src/item/item.stories.ts new file mode 100644 index 0000000000..b9d8d6cc2e --- /dev/null +++ b/libs/components/src/item/item.stories.ts @@ -0,0 +1,326 @@ +import { ScrollingModule } from "@angular/cdk/scrolling"; +import { CommonModule } from "@angular/common"; +import { Meta, StoryObj, componentWrapperDecorator, moduleMetadata } from "@storybook/angular"; + +import { A11yGridDirective } from "../a11y/a11y-grid.directive"; +import { AvatarModule } from "../avatar"; +import { BadgeModule } from "../badge"; +import { IconButtonModule } from "../icon-button"; +import { TypographyModule } from "../typography"; + +import { ItemActionComponent } from "./item-action.component"; +import { ItemContentComponent } from "./item-content.component"; +import { ItemGroupComponent } from "./item-group.component"; +import { ItemComponent } from "./item.component"; + +export default { + title: "Component Library/Item", + component: ItemComponent, + decorators: [ + moduleMetadata({ + imports: [ + CommonModule, + ItemGroupComponent, + AvatarModule, + IconButtonModule, + BadgeModule, + TypographyModule, + ItemActionComponent, + ItemContentComponent, + A11yGridDirective, + ScrollingModule, + ], + }), + componentWrapperDecorator((story) => `
${story}
`), + ], +} as Meta; + +type Story = StoryObj; + +export const Default: Story = { + render: (args) => ({ + props: args, + template: /*html*/ ` + + + + + + + + + + + + + + + + `, + }), +}; + +export const ContentSlots: Story = { + render: (args) => ({ + props: args, + template: /*html*/ ` + + + + `, + }), +}; + +export const ContentTypes: Story = { + render: (args) => ({ + props: args, + template: /*html*/ ` + + + Hi, I am a link. + + + + + + + + I'm just static :( + + + `, + }), +}; + +export const TextOverflow: Story = { + render: (args) => ({ + props: args, + template: /*html*/ ` +
TODO: Fix truncation
+ + + Helloooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo + + + `, + }), +}; + +export const MultipleActionList: Story = { + render: (args) => ({ + props: args, + template: /*html*/ ` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + `, + }), +}; + +export const SingleActionList: Story = { + render: (args) => ({ + props: args, + template: /*html*/ ` + + + + Foobar + + + + + + Foobar + + + + + + Foobar + + + + + + Foobar + + + + + + Foobar + + + + + + Foobar + + + + + `, + }), +}; + +export const VirtualScrolling: Story = { + render: (_args) => ({ + props: { + data: Array.from(Array(100000).keys()), + }, + template: /*html*/ ` + + + + + + + + + + + + + + + + + + + + `, + }), +}; diff --git a/libs/components/src/search/search.component.ts b/libs/components/src/search/search.component.ts index a0f3eb363f..27170d5d7b 100644 --- a/libs/components/src/search/search.component.ts +++ b/libs/components/src/search/search.component.ts @@ -1,7 +1,7 @@ import { Component, ElementRef, Input, ViewChild } from "@angular/core"; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from "@angular/forms"; -import { FocusableElement } from "../input/autofocus.directive"; +import { FocusableElement } from "../shared/focusable-element"; let nextId = 0; @@ -32,8 +32,8 @@ export class SearchComponent implements ControlValueAccessor, FocusableElement { @Input() disabled: boolean; @Input() placeholder: string; - focus() { - this.input.nativeElement.focus(); + getFocusTarget() { + return this.input.nativeElement; } onChange(searchText: string) { diff --git a/libs/components/src/shared/focusable-element.ts b/libs/components/src/shared/focusable-element.ts new file mode 100644 index 0000000000..1ea422aa6f --- /dev/null +++ b/libs/components/src/shared/focusable-element.ts @@ -0,0 +1,8 @@ +/** + * Interface for implementing focusable components. + * + * Used by the `AutofocusDirective` and `A11yGridDirective`. + */ +export abstract class FocusableElement { + getFocusTarget: () => HTMLElement; +} diff --git a/libs/components/src/styles.scss b/libs/components/src/styles.scss index ae97838e09..7ddcb1b64b 100644 --- a/libs/components/src/styles.scss +++ b/libs/components/src/styles.scss @@ -49,6 +49,6 @@ $card-icons-base: "../../src/billing/images/cards/"; @import "multi-select/scss/bw.theme.scss"; // Workaround for https://bitwarden.atlassian.net/browse/CL-110 -#storybook-docs pre.prismjs { +.sbdocs-preview pre.prismjs { color: white; } From 418d4642da81e09de6a4b445042a26592a017c98 Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Tue, 30 Apr 2024 10:55:00 -0400 Subject: [PATCH 2/9] Hide grace period note when in self-serve trial (#8768) --- ...organization-subscription-selfhost.component.html | 5 ++++- .../self-hosted-organization-subscription.view.ts | 12 ++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/apps/web/src/app/billing/organizations/organization-subscription-selfhost.component.html b/apps/web/src/app/billing/organizations/organization-subscription-selfhost.component.html index 6d6691f336..b4c1224db9 100644 --- a/apps/web/src/app/billing/organizations/organization-subscription-selfhost.component.html +++ b/apps/web/src/app/billing/organizations/organization-subscription-selfhost.component.html @@ -42,7 +42,10 @@ : subscription.expirationWithGracePeriod ) | date: "mediumDate" }} -
+
{{ "selfHostGracePeriodHelp" | i18n: (subscription.expirationWithGracePeriod | date: "mediumDate") diff --git a/libs/common/src/billing/models/view/self-hosted-organization-subscription.view.ts b/libs/common/src/billing/models/view/self-hosted-organization-subscription.view.ts index c1f5640207..7b49688294 100644 --- a/libs/common/src/billing/models/view/self-hosted-organization-subscription.view.ts +++ b/libs/common/src/billing/models/view/self-hosted-organization-subscription.view.ts @@ -58,4 +58,16 @@ export class SelfHostedOrganizationSubscriptionView implements View { get isExpiredAndOutsideGracePeriod() { return this.hasExpiration && this.expirationWithGracePeriod < new Date(); } + + /** + * In the case of a trial, where there is no grace period, the expirationWithGracePeriod and expirationWithoutGracePeriod will + * be exactly the same. This can be used to hide the grace period note. + */ + get isInTrial() { + return ( + this.expirationWithGracePeriod && + this.expirationWithoutGracePeriod && + this.expirationWithGracePeriod.getTime() === this.expirationWithoutGracePeriod.getTime() + ); + } } From 04decd1c09a69bde07c389c481264badba3355e9 Mon Sep 17 00:00:00 2001 From: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com> Date: Tue, 30 Apr 2024 16:35:39 +0100 Subject: [PATCH 3/9] [AC-2265] As a Provider Admin, I shouldn't be able to use my client organizations' billing pages (#8981) * initial commit * add the feature flag * Resolve pr comments --- .../icons/manage-billing.icon.ts | 25 +++++++++++++++++++ ...nization-subscription-cloud.component.html | 12 ++++++++- ...ganization-subscription-cloud.component.ts | 16 +++++++++++- apps/web/src/locales/en/messages.json | 3 +++ 4 files changed, 54 insertions(+), 2 deletions(-) create mode 100644 apps/web/src/app/billing/organizations/icons/manage-billing.icon.ts diff --git a/apps/web/src/app/billing/organizations/icons/manage-billing.icon.ts b/apps/web/src/app/billing/organizations/icons/manage-billing.icon.ts new file mode 100644 index 0000000000..6f583bf2e8 --- /dev/null +++ b/apps/web/src/app/billing/organizations/icons/manage-billing.icon.ts @@ -0,0 +1,25 @@ +import { svgIcon } from "@bitwarden/components"; + +export const ManageBilling = svgIcon` + + + + + + + + + + + + + + + + + + + + + + `; diff --git a/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.html b/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.html index 16641c0d52..38903bab19 100644 --- a/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.html +++ b/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.html @@ -1,6 +1,6 @@ - + {{ "loading" | i18n }} @@ -256,3 +256,13 @@ + +
+ + {{ + "manageBillingFromProviderPortalMessage" | i18n + }} +
+
diff --git a/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts b/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts index 477032deba..7acb108808 100644 --- a/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts +++ b/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts @@ -5,7 +5,7 @@ import { concatMap, firstValueFrom, lastValueFrom, Observable, Subject, takeUnti import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; -import { OrganizationApiKeyType } from "@bitwarden/common/admin-console/enums"; +import { OrganizationApiKeyType, ProviderType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { PlanType } from "@bitwarden/common/billing/enums"; import { OrganizationSubscriptionResponse } from "@bitwarden/common/billing/models/response/organization-subscription.response"; @@ -28,6 +28,7 @@ import { } from "../shared/offboarding-survey.component"; import { BillingSyncApiKeyComponent } from "./billing-sync-api-key.component"; +import { ManageBilling } from "./icons/manage-billing.icon"; import { SecretsManagerSubscriptionOptions } from "./sm-adjust-subscription.component"; @Component({ @@ -47,11 +48,17 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy loading: boolean; locale: string; showUpdatedSubscriptionStatusSection$: Observable; + manageBillingFromProviderPortal = ManageBilling; + IsProviderManaged = false; protected readonly teamsStarter = ProductType.TeamsStarter; private destroy$ = new Subject(); + protected enableConsolidatedBilling$ = this.configService.getFeatureFlag$( + FeatureFlag.EnableConsolidatedBilling, + ); + constructor( private apiService: ApiService, private platformUtilsService: PlatformUtilsService, @@ -99,6 +106,13 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy this.loading = true; this.locale = await firstValueFrom(this.i18nService.locale$); this.userOrg = await this.organizationService.get(this.organizationId); + const enableConsolidatedBilling = await firstValueFrom(this.enableConsolidatedBilling$); + this.IsProviderManaged = + this.userOrg.hasProvider && + this.userOrg.providerType == ProviderType.Msp && + enableConsolidatedBilling + ? true + : false; if (this.userOrg.canViewSubscription) { this.sub = await this.organizationApiService.getSubscription(this.organizationId); this.lineItems = this.sub?.subscription?.items; diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 63069a83de..3a590622b8 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -8049,5 +8049,8 @@ }, "collectionItemSelect": { "message": "Select collection item" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Manage billing from the Provider Portal" } } From be50a174def9763b88086c60552cc31320c34f78 Mon Sep 17 00:00:00 2001 From: Colton Hurst Date: Tue, 30 Apr 2024 12:31:09 -0400 Subject: [PATCH 4/9] SM-1196: Update export file name (#8865) --- .../event-logs/service-accounts-events.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/event-logs/service-accounts-events.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/event-logs/service-accounts-events.component.ts index 554e7fa37d..0547c4fcba 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/event-logs/service-accounts-events.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/event-logs/service-accounts-events.component.ts @@ -17,7 +17,7 @@ import { ServiceAccountEventLogApiService } from "./service-account-event-log-ap templateUrl: "./service-accounts-events.component.html", }) export class ServiceAccountEventsComponent extends BaseEventsComponent implements OnDestroy { - exportFileName = "service-account-events"; + exportFileName = "machine-account-events"; private destroy$ = new Subject(); private serviceAccountId: string; From 3acbffa0726d8b4bda5c410d2ad98cf226a8ab7a Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Tue, 30 Apr 2024 12:35:36 -0400 Subject: [PATCH 5/9] [PM-6144] Basic auth autofill in Manifest v3 (#8975) * Add Support for autofilling Basic Auth to MV3 * Remove `any` --- .../background/web-request.background.ts | 34 ++++++------------- .../browser/src/background/main.background.ts | 7 ++-- apps/browser/src/manifest.v3.json | 4 ++- 3 files changed, 16 insertions(+), 29 deletions(-) diff --git a/apps/browser/src/autofill/background/web-request.background.ts b/apps/browser/src/autofill/background/web-request.background.ts index 8cdfa0f027..2eb976529f 100644 --- a/apps/browser/src/autofill/background/web-request.background.ts +++ b/apps/browser/src/autofill/background/web-request.background.ts @@ -4,40 +4,29 @@ import { UriMatchStrategy } from "@bitwarden/common/models/domain/domain-service import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; -import { BrowserApi } from "../../platform/browser/browser-api"; - export default class WebRequestBackground { - private pendingAuthRequests: any[] = []; - private webRequest: any; + private pendingAuthRequests: Set = new Set([]); private isFirefox: boolean; constructor( platformUtilsService: PlatformUtilsService, private cipherService: CipherService, private authService: AuthService, + private readonly webRequest: typeof chrome.webRequest, ) { - if (BrowserApi.isManifestVersion(2)) { - this.webRequest = chrome.webRequest; - } this.isFirefox = platformUtilsService.isFirefox(); } - async init() { - if (!this.webRequest || !this.webRequest.onAuthRequired) { - return; - } - + startListening() { this.webRequest.onAuthRequired.addListener( - async (details: any, callback: any) => { - if (!details.url || this.pendingAuthRequests.indexOf(details.requestId) !== -1) { + async (details, callback) => { + if (!details.url || this.pendingAuthRequests.has(details.requestId)) { if (callback) { - callback(); + callback(null); } return; } - - this.pendingAuthRequests.push(details.requestId); - + this.pendingAuthRequests.add(details.requestId); if (this.isFirefox) { // eslint-disable-next-line return new Promise(async (resolve, reject) => { @@ -51,7 +40,7 @@ export default class WebRequestBackground { [this.isFirefox ? "blocking" : "asyncBlocking"], ); - this.webRequest.onCompleted.addListener((details: any) => this.completeAuthRequest(details), { + this.webRequest.onCompleted.addListener((details) => this.completeAuthRequest(details), { urls: ["http://*/*"], }); this.webRequest.onErrorOccurred.addListener( @@ -91,10 +80,7 @@ export default class WebRequestBackground { } } - private completeAuthRequest(details: any) { - const i = this.pendingAuthRequests.indexOf(details.requestId); - if (i > -1) { - this.pendingAuthRequests.splice(i, 1); - } + private completeAuthRequest(details: chrome.webRequest.WebResponseCacheDetails) { + this.pendingAuthRequests.delete(details.requestId); } } diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 01e325ad51..adcb4a21ba 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -1056,11 +1056,12 @@ export default class MainBackground { this.cipherService, ); - if (BrowserApi.isManifestVersion(2)) { + if (chrome.webRequest != null && chrome.webRequest.onAuthRequired != null) { this.webRequestBackground = new WebRequestBackground( this.platformUtilsService, this.cipherService, this.authService, + chrome.webRequest, ); } } @@ -1106,9 +1107,7 @@ export default class MainBackground { await this.tabsBackground.init(); this.contextMenusBackground?.init(); await this.idleBackground.init(); - if (BrowserApi.isManifestVersion(2)) { - await this.webRequestBackground.init(); - } + this.webRequestBackground?.startListening(); if (this.platformUtilsService.isFirefox() && !this.isPrivateMode) { // Set Private Mode windows to the default icon - they do not share state with the background page diff --git a/apps/browser/src/manifest.v3.json b/apps/browser/src/manifest.v3.json index c0c88706b8..b7aaba2e0e 100644 --- a/apps/browser/src/manifest.v3.json +++ b/apps/browser/src/manifest.v3.json @@ -60,7 +60,9 @@ "clipboardWrite", "idle", "scripting", - "offscreen" + "offscreen", + "webRequest", + "webRequestAuthProvider" ], "optional_permissions": ["nativeMessaging", "privacy"], "host_permissions": [""], From 200b0f75341dd38186f1ea05dab399ab12c4d771 Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Tue, 30 Apr 2024 12:46:01 -0400 Subject: [PATCH 6/9] Correct and test changeover point for userId source in storage migration (#8990) --- .../src/state-migrations/migration-helper.spec.ts | 5 +++++ .../src/state-migrations/migration-helper.ts | 6 +++--- .../migrations/60-known-accounts.spec.ts | 14 +++++--------- .../migrations/60-known-accounts.ts | 4 ++-- 4 files changed, 15 insertions(+), 14 deletions(-) diff --git a/libs/common/src/state-migrations/migration-helper.spec.ts b/libs/common/src/state-migrations/migration-helper.spec.ts index 162fac2fab..21c5c72a18 100644 --- a/libs/common/src/state-migrations/migration-helper.spec.ts +++ b/libs/common/src/state-migrations/migration-helper.spec.ts @@ -235,6 +235,11 @@ export function mockMigrationHelper( helper.setToUser(userId, keyDefinition, value), ); mockHelper.getAccounts.mockImplementation(() => helper.getAccounts()); + mockHelper.getKnownUserIds.mockImplementation(() => helper.getKnownUserIds()); + mockHelper.removeFromGlobal.mockImplementation((keyDefinition) => + helper.removeFromGlobal(keyDefinition), + ); + mockHelper.remove.mockImplementation((key) => helper.remove(key)); mockHelper.type = helper.type; diff --git a/libs/common/src/state-migrations/migration-helper.ts b/libs/common/src/state-migrations/migration-helper.ts index 5d1de8dd49..b377df8ef9 100644 --- a/libs/common/src/state-migrations/migration-helper.ts +++ b/libs/common/src/state-migrations/migration-helper.ts @@ -175,8 +175,8 @@ export class MigrationHelper { * Helper method to read known users ids. */ async getKnownUserIds(): Promise { - if (this.currentVersion < 61) { - return knownAccountUserIdsBuilderPre61(this.storageService); + if (this.currentVersion < 60) { + return knownAccountUserIdsBuilderPre60(this.storageService); } else { return knownAccountUserIdsBuilder(this.storageService); } @@ -245,7 +245,7 @@ function globalKeyBuilderPre9(): string { throw Error("No key builder should be used for versions prior to 9."); } -async function knownAccountUserIdsBuilderPre61( +async function knownAccountUserIdsBuilderPre60( storageService: AbstractStorageService, ): Promise { return (await storageService.get("authenticatedAccounts")) ?? []; diff --git a/libs/common/src/state-migrations/migrations/60-known-accounts.spec.ts b/libs/common/src/state-migrations/migrations/60-known-accounts.spec.ts index 28dedb3c39..01be4adb6a 100644 --- a/libs/common/src/state-migrations/migrations/60-known-accounts.spec.ts +++ b/libs/common/src/state-migrations/migrations/60-known-accounts.spec.ts @@ -51,17 +51,13 @@ const rollbackJson = () => { }, global_account_accounts: { user1: { - profile: { - email: "user1", - name: "User 1", - emailVerified: true, - }, + email: "user1", + name: "User 1", + emailVerified: true, }, user2: { - profile: { - email: "", - emailVerified: false, - }, + email: "", + emailVerified: false, }, }, global_account_activeAccountId: "user1", diff --git a/libs/common/src/state-migrations/migrations/60-known-accounts.ts b/libs/common/src/state-migrations/migrations/60-known-accounts.ts index 75117da5b4..3b02a5acc4 100644 --- a/libs/common/src/state-migrations/migrations/60-known-accounts.ts +++ b/libs/common/src/state-migrations/migrations/60-known-accounts.ts @@ -38,8 +38,8 @@ export class KnownAccountsMigrator extends Migrator<59, 60> { } async rollback(helper: MigrationHelper): Promise { // authenticated account are removed, but the accounts record also contains logged out accounts. Best we can do is to add them all back - const accounts = (await helper.getFromGlobal>(ACCOUNT_ACCOUNTS)) ?? {}; - await helper.set("authenticatedAccounts", Object.keys(accounts)); + const userIds = (await helper.getKnownUserIds()) ?? []; + await helper.set("authenticatedAccounts", userIds); await helper.removeFromGlobal(ACCOUNT_ACCOUNTS); // Active Account Id From b4631b0dd164ee34de9f5dff43a1bf559880ebd0 Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Tue, 30 Apr 2024 12:58:16 -0400 Subject: [PATCH 7/9] Ps/improve-log-service (#8989) * Match console method signatures in logService abstraction * Add a few usages of improved signature * Remove reality check test * Improve electron logging --- .../src/background/runtime.background.ts | 3 +- .../offscreen-document.spec.ts | 4 +- .../offscreen-document/offscreen-document.ts | 2 +- .../services/console-log.service.spec.ts | 36 ++++++------ .../platform/services/console-log.service.ts | 6 +- apps/desktop/src/platform/preload.ts | 3 +- .../services/electron-log.main.service.ts | 14 ++--- .../services/electron-log.renderer.service.ts | 14 +++-- .../services/logging-error-handler.ts | 2 +- libs/common/spec/intercept-console.ts | 23 +++----- .../src/platform/abstractions/log.service.ts | 10 ++-- .../services/console-log.service.spec.ts | 57 ++++++++++--------- .../platform/services/console-log.service.ts | 26 ++++----- 13 files changed, 104 insertions(+), 96 deletions(-) diff --git a/apps/browser/src/background/runtime.background.ts b/apps/browser/src/background/runtime.background.ts index 98b1df9c80..d8f3cf840f 100644 --- a/apps/browser/src/background/runtime.background.ts +++ b/apps/browser/src/background/runtime.background.ts @@ -76,7 +76,8 @@ export default class RuntimeBackground { void this.processMessageWithSender(msg, sender).catch((err) => this.logService.error( - `Error while processing message in RuntimeBackground '${msg?.command}'. Error: ${err?.message ?? "Unknown Error"}`, + `Error while processing message in RuntimeBackground '${msg?.command}'.`, + err, ), ); return false; diff --git a/apps/browser/src/platform/offscreen-document/offscreen-document.spec.ts b/apps/browser/src/platform/offscreen-document/offscreen-document.spec.ts index 1cbcc7a94c..933cd08c2e 100644 --- a/apps/browser/src/platform/offscreen-document/offscreen-document.spec.ts +++ b/apps/browser/src/platform/offscreen-document/offscreen-document.spec.ts @@ -28,6 +28,7 @@ describe("OffscreenDocument", () => { }); it("shows a console message if the handler throws an error", async () => { + const error = new Error("test error"); browserClipboardServiceCopySpy.mockRejectedValueOnce(new Error("test error")); sendExtensionRuntimeMessage({ command: "offscreenCopyToClipboard", text: "test" }); @@ -35,7 +36,8 @@ describe("OffscreenDocument", () => { expect(browserClipboardServiceCopySpy).toHaveBeenCalled(); expect(consoleErrorSpy).toHaveBeenCalledWith( - "Error resolving extension message response: Error: test error", + "Error resolving extension message response", + error, ); }); diff --git a/apps/browser/src/platform/offscreen-document/offscreen-document.ts b/apps/browser/src/platform/offscreen-document/offscreen-document.ts index 627036b80b..4994a6e9ba 100644 --- a/apps/browser/src/platform/offscreen-document/offscreen-document.ts +++ b/apps/browser/src/platform/offscreen-document/offscreen-document.ts @@ -71,7 +71,7 @@ class OffscreenDocument implements OffscreenDocumentInterface { Promise.resolve(messageResponse) .then((response) => sendResponse(response)) .catch((error) => - this.consoleLogService.error(`Error resolving extension message response: ${error}`), + this.consoleLogService.error("Error resolving extension message response", error), ); return true; }; diff --git a/apps/cli/src/platform/services/console-log.service.spec.ts b/apps/cli/src/platform/services/console-log.service.spec.ts index 10a0ad8cca..03598b16e6 100644 --- a/apps/cli/src/platform/services/console-log.service.spec.ts +++ b/apps/cli/src/platform/services/console-log.service.spec.ts @@ -2,13 +2,18 @@ import { interceptConsole, restoreConsole } from "@bitwarden/common/spec"; import { ConsoleLogService } from "./console-log.service"; -let caughtMessage: any = {}; - describe("CLI Console log service", () => { + const error = new Error("this is an error"); + const obj = { a: 1, b: 2 }; let logService: ConsoleLogService; + let consoleSpy: { + log: jest.Mock; + warn: jest.Mock; + error: jest.Mock; + }; + beforeEach(() => { - caughtMessage = {}; - interceptConsole(caughtMessage); + consoleSpy = interceptConsole(); logService = new ConsoleLogService(true); }); @@ -19,24 +24,21 @@ describe("CLI Console log service", () => { it("should redirect all console to error if BW_RESPONSE env is true", () => { process.env.BW_RESPONSE = "true"; - logService.debug("this is a debug message"); - expect(caughtMessage).toMatchObject({ - error: { 0: "this is a debug message" }, - }); + logService.debug("this is a debug message", error, obj); + expect(consoleSpy.error).toHaveBeenCalledWith("this is a debug message", error, obj); }); it("should not redirect console to error if BW_RESPONSE != true", () => { process.env.BW_RESPONSE = "false"; - logService.debug("debug"); - logService.info("info"); - logService.warning("warning"); - logService.error("error"); + logService.debug("debug", error, obj); + logService.info("info", error, obj); + logService.warning("warning", error, obj); + logService.error("error", error, obj); - expect(caughtMessage).toMatchObject({ - log: { 0: "info" }, - warn: { 0: "warning" }, - error: { 0: "error" }, - }); + expect(consoleSpy.log).toHaveBeenCalledWith("debug", error, obj); + expect(consoleSpy.log).toHaveBeenCalledWith("info", error, obj); + expect(consoleSpy.warn).toHaveBeenCalledWith("warning", error, obj); + expect(consoleSpy.error).toHaveBeenCalledWith("error", error, obj); }); }); diff --git a/apps/cli/src/platform/services/console-log.service.ts b/apps/cli/src/platform/services/console-log.service.ts index a35dae71fc..5bdc0b4015 100644 --- a/apps/cli/src/platform/services/console-log.service.ts +++ b/apps/cli/src/platform/services/console-log.service.ts @@ -6,17 +6,17 @@ export class ConsoleLogService extends BaseConsoleLogService { super(isDev, filter); } - write(level: LogLevelType, message: string) { + write(level: LogLevelType, message?: any, ...optionalParams: any[]) { if (this.filter != null && this.filter(level)) { return; } if (process.env.BW_RESPONSE === "true") { // eslint-disable-next-line - console.error(message); + console.error(message, ...optionalParams); return; } - super.write(level, message); + super.write(level, message, ...optionalParams); } } diff --git a/apps/desktop/src/platform/preload.ts b/apps/desktop/src/platform/preload.ts index 771d25ef0a..d81d647652 100644 --- a/apps/desktop/src/platform/preload.ts +++ b/apps/desktop/src/platform/preload.ts @@ -103,7 +103,8 @@ export default { isMacAppStore: isMacAppStore(), isWindowsStore: isWindowsStore(), reloadProcess: () => ipcRenderer.send("reload-process"), - log: (level: LogLevelType, message: string) => ipcRenderer.invoke("ipc.log", { level, message }), + log: (level: LogLevelType, message?: any, ...optionalParams: any[]) => + ipcRenderer.invoke("ipc.log", { level, message, optionalParams }), openContextMenu: ( menu: { diff --git a/apps/desktop/src/platform/services/electron-log.main.service.ts b/apps/desktop/src/platform/services/electron-log.main.service.ts index 832365785c..0725de3dc9 100644 --- a/apps/desktop/src/platform/services/electron-log.main.service.ts +++ b/apps/desktop/src/platform/services/electron-log.main.service.ts @@ -25,28 +25,28 @@ export class ElectronLogMainService extends BaseLogService { } log.initialize(); - ipcMain.handle("ipc.log", (_event, { level, message }) => { - this.write(level, message); + ipcMain.handle("ipc.log", (_event, { level, message, optionalParams }) => { + this.write(level, message, ...optionalParams); }); } - write(level: LogLevelType, message: string) { + write(level: LogLevelType, message?: any, ...optionalParams: any[]) { if (this.filter != null && this.filter(level)) { return; } switch (level) { case LogLevelType.Debug: - log.debug(message); + log.debug(message, ...optionalParams); break; case LogLevelType.Info: - log.info(message); + log.info(message, ...optionalParams); break; case LogLevelType.Warning: - log.warn(message); + log.warn(message, ...optionalParams); break; case LogLevelType.Error: - log.error(message); + log.error(message, ...optionalParams); break; default: break; diff --git a/apps/desktop/src/platform/services/electron-log.renderer.service.ts b/apps/desktop/src/platform/services/electron-log.renderer.service.ts index e0e0757e6a..cea939f160 100644 --- a/apps/desktop/src/platform/services/electron-log.renderer.service.ts +++ b/apps/desktop/src/platform/services/electron-log.renderer.service.ts @@ -6,27 +6,29 @@ export class ElectronLogRendererService extends BaseLogService { super(ipc.platform.isDev, filter); } - write(level: LogLevelType, message: string) { + write(level: LogLevelType, message?: any, ...optionalParams: any[]) { if (this.filter != null && this.filter(level)) { return; } /* eslint-disable no-console */ - ipc.platform.log(level, message).catch((e) => console.log("Error logging", e)); + ipc.platform + .log(level, message, ...optionalParams) + .catch((e) => console.log("Error logging", e)); /* eslint-disable no-console */ switch (level) { case LogLevelType.Debug: - console.debug(message); + console.debug(message, ...optionalParams); break; case LogLevelType.Info: - console.info(message); + console.info(message, ...optionalParams); break; case LogLevelType.Warning: - console.warn(message); + console.warn(message, ...optionalParams); break; case LogLevelType.Error: - console.error(message); + console.error(message, ...optionalParams); break; default: break; diff --git a/libs/angular/src/platform/services/logging-error-handler.ts b/libs/angular/src/platform/services/logging-error-handler.ts index 522412dd28..5644272d35 100644 --- a/libs/angular/src/platform/services/logging-error-handler.ts +++ b/libs/angular/src/platform/services/logging-error-handler.ts @@ -14,7 +14,7 @@ export class LoggingErrorHandler extends ErrorHandler { override handleError(error: any): void { try { const logService = this.injector.get(LogService, null); - logService.error(error); + logService.error("Unhandled error in angular", error); } catch { super.handleError(error); } diff --git a/libs/common/spec/intercept-console.ts b/libs/common/spec/intercept-console.ts index 01c4063e7a..565d475cae 100644 --- a/libs/common/spec/intercept-console.ts +++ b/libs/common/spec/intercept-console.ts @@ -2,22 +2,17 @@ const originalConsole = console; declare let console: any; -export function interceptConsole(interceptions: any): object { +export function interceptConsole(): { + log: jest.Mock; + warn: jest.Mock; + error: jest.Mock; +} { console = { - log: function () { - // eslint-disable-next-line - interceptions.log = arguments; - }, - warn: function () { - // eslint-disable-next-line - interceptions.warn = arguments; - }, - error: function () { - // eslint-disable-next-line - interceptions.error = arguments; - }, + log: jest.fn(), + warn: jest.fn(), + error: jest.fn(), }; - return interceptions; + return console; } export function restoreConsole() { diff --git a/libs/common/src/platform/abstractions/log.service.ts b/libs/common/src/platform/abstractions/log.service.ts index dffa3ca8d3..d77a4f6990 100644 --- a/libs/common/src/platform/abstractions/log.service.ts +++ b/libs/common/src/platform/abstractions/log.service.ts @@ -1,9 +1,9 @@ import { LogLevelType } from "../enums/log-level-type.enum"; export abstract class LogService { - abstract debug(message: string): void; - abstract info(message: string): void; - abstract warning(message: string): void; - abstract error(message: string): void; - abstract write(level: LogLevelType, message: string): void; + abstract debug(message?: any, ...optionalParams: any[]): void; + abstract info(message?: any, ...optionalParams: any[]): void; + abstract warning(message?: any, ...optionalParams: any[]): void; + abstract error(message?: any, ...optionalParams: any[]): void; + abstract write(level: LogLevelType, message?: any, ...optionalParams: any[]): void; } diff --git a/libs/common/src/platform/services/console-log.service.spec.ts b/libs/common/src/platform/services/console-log.service.spec.ts index 129969bbc4..508ca4eb32 100644 --- a/libs/common/src/platform/services/console-log.service.spec.ts +++ b/libs/common/src/platform/services/console-log.service.spec.ts @@ -2,13 +2,18 @@ import { interceptConsole, restoreConsole } from "../../../spec"; import { ConsoleLogService } from "./console-log.service"; -let caughtMessage: any; - describe("ConsoleLogService", () => { + const error = new Error("this is an error"); + const obj = { a: 1, b: 2 }; + let consoleSpy: { + log: jest.Mock; + warn: jest.Mock; + error: jest.Mock; + }; let logService: ConsoleLogService; + beforeEach(() => { - caughtMessage = {}; - interceptConsole(caughtMessage); + consoleSpy = interceptConsole(); logService = new ConsoleLogService(true); }); @@ -18,41 +23,41 @@ describe("ConsoleLogService", () => { it("filters messages below the set threshold", () => { logService = new ConsoleLogService(true, () => true); - logService.debug("debug"); - logService.info("info"); - logService.warning("warning"); - logService.error("error"); + logService.debug("debug", error, obj); + logService.info("info", error, obj); + logService.warning("warning", error, obj); + logService.error("error", error, obj); - expect(caughtMessage).toEqual({}); + expect(consoleSpy.log).not.toHaveBeenCalled(); + expect(consoleSpy.warn).not.toHaveBeenCalled(); + expect(consoleSpy.error).not.toHaveBeenCalled(); }); + it("only writes debug messages in dev mode", () => { logService = new ConsoleLogService(false); logService.debug("debug message"); - expect(caughtMessage.log).toBeUndefined(); + expect(consoleSpy.log).not.toHaveBeenCalled(); }); it("writes debug/info messages to console.log", () => { - logService.debug("this is a debug message"); - expect(caughtMessage).toMatchObject({ - log: { "0": "this is a debug message" }, - }); + logService.debug("this is a debug message", error, obj); + logService.info("this is an info message", error, obj); - logService.info("this is an info message"); - expect(caughtMessage).toMatchObject({ - log: { "0": "this is an info message" }, - }); + expect(consoleSpy.log).toHaveBeenCalledTimes(2); + expect(consoleSpy.log).toHaveBeenCalledWith("this is a debug message", error, obj); + expect(consoleSpy.log).toHaveBeenCalledWith("this is an info message", error, obj); }); + it("writes warning messages to console.warn", () => { - logService.warning("this is a warning message"); - expect(caughtMessage).toMatchObject({ - warn: { 0: "this is a warning message" }, - }); + logService.warning("this is a warning message", error, obj); + + expect(consoleSpy.warn).toHaveBeenCalledWith("this is a warning message", error, obj); }); + it("writes error messages to console.error", () => { - logService.error("this is an error message"); - expect(caughtMessage).toMatchObject({ - error: { 0: "this is an error message" }, - }); + logService.error("this is an error message", error, obj); + + expect(consoleSpy.error).toHaveBeenCalledWith("this is an error message", error, obj); }); }); diff --git a/libs/common/src/platform/services/console-log.service.ts b/libs/common/src/platform/services/console-log.service.ts index 3eb3ad1881..a1480a0c26 100644 --- a/libs/common/src/platform/services/console-log.service.ts +++ b/libs/common/src/platform/services/console-log.service.ts @@ -9,26 +9,26 @@ export class ConsoleLogService implements LogServiceAbstraction { protected filter: (level: LogLevelType) => boolean = null, ) {} - debug(message: string) { + debug(message?: any, ...optionalParams: any[]) { if (!this.isDev) { return; } - this.write(LogLevelType.Debug, message); + this.write(LogLevelType.Debug, message, ...optionalParams); } - info(message: string) { - this.write(LogLevelType.Info, message); + info(message?: any, ...optionalParams: any[]) { + this.write(LogLevelType.Info, message, ...optionalParams); } - warning(message: string) { - this.write(LogLevelType.Warning, message); + warning(message?: any, ...optionalParams: any[]) { + this.write(LogLevelType.Warning, message, ...optionalParams); } - error(message: string) { - this.write(LogLevelType.Error, message); + error(message?: any, ...optionalParams: any[]) { + this.write(LogLevelType.Error, message, ...optionalParams); } - write(level: LogLevelType, message: string) { + write(level: LogLevelType, message?: any, ...optionalParams: any[]) { if (this.filter != null && this.filter(level)) { return; } @@ -36,19 +36,19 @@ export class ConsoleLogService implements LogServiceAbstraction { switch (level) { case LogLevelType.Debug: // eslint-disable-next-line - console.log(message); + console.log(message, ...optionalParams); break; case LogLevelType.Info: // eslint-disable-next-line - console.log(message); + console.log(message, ...optionalParams); break; case LogLevelType.Warning: // eslint-disable-next-line - console.warn(message); + console.warn(message, ...optionalParams); break; case LogLevelType.Error: // eslint-disable-next-line - console.error(message); + console.error(message, ...optionalParams); break; default: break; From 7e9ab6a15b90a67bf375015e125b007317330718 Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Wed, 1 May 2024 07:59:30 -0400 Subject: [PATCH 8/9] [PM-7807][PM-7617] [PM-6185] Firefox private mode out of experimentation (#8921) * Remove getbgService for crypto service * Remove special authentication for state service * Use synced memory storage popup contexts use foreground, background contexts use background. Simple * Remove private mode warnings --- apps/browser/src/_locales/en/messages.json | 3 -- .../src/auth/popup/lock.component.html | 1 - .../src/auth/popup/login.component.html | 1 - .../browser/src/background/main.background.ts | 34 +++---------- .../src/platform/browser/browser-api.ts | 4 -- .../popup/browser-popup-utils.spec.ts | 22 -------- .../src/platform/popup/browser-popup-utils.ts | 7 --- .../services/default-browser-state.service.ts | 9 ---- apps/browser/src/popup/app.module.ts | 2 - .../private-mode-warning.component.html | 6 --- .../private-mode-warning.component.ts | 15 ------ apps/browser/src/popup/scss/pages.scss | 5 -- .../src/popup/services/services.module.ts | 50 +++++++++++++++++-- 13 files changed, 53 insertions(+), 106 deletions(-) delete mode 100644 apps/browser/src/popup/components/private-mode-warning.component.html delete mode 100644 apps/browser/src/popup/components/private-mode-warning.component.ts diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 1c0b178895..bd62b825e7 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "Lock the vault" }, - "privateModeWarning": { - "message": "Private mode support is experimental and some features are limited." - }, "customFields": { "message": "Custom fields" }, diff --git a/apps/browser/src/auth/popup/lock.component.html b/apps/browser/src/auth/popup/lock.component.html index 9892503a7b..5ea839470b 100644 --- a/apps/browser/src/auth/popup/lock.component.html +++ b/apps/browser/src/auth/popup/lock.component.html @@ -89,7 +89,6 @@

- {{ biometricError }}

{{ "awaitDesktop" | i18n }} diff --git a/apps/browser/src/auth/popup/login.component.html b/apps/browser/src/auth/popup/login.component.html index b24a25a0f1..7a4211a5cc 100644 --- a/apps/browser/src/auth/popup/login.component.html +++ b/apps/browser/src/auth/popup/login.component.html @@ -57,7 +57,6 @@

-