diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 89b3b3ac5c6..4d69bf45311 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -1247,6 +1247,9 @@ "selectAll": { "message": "Select all" }, + "deselectAll": { + "message": "Deselect all" + }, "unselectAll": { "message": "Unselect all" }, diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications/applications.component.html b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications/applications.component.html index 765985d43b3..a3d29c521c5 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications/applications.component.html +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications/applications.component.html @@ -46,7 +46,6 @@ (); + this.dataSource.filteredData?.forEach((row) => { + if (this.selectedUrls().has(row.applicationName)) { + filteredUrls.add(row.applicationName); + } + }); + this.selectedUrls.set(filteredUrls); + if (this.dataSource?.filteredData?.length === 0) { this.emptyTableExplanation.set(this.i18nService.t("noApplicationsMatchTheseFilters")); } else { diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/app-table-row-scrollable-m11.component.html b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/app-table-row-scrollable-m11.component.html index 4f231efc04b..67cee2a4639 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/app-table-row-scrollable-m11.component.html +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/app-table-row-scrollable-m11.component.html @@ -1,7 +1,17 @@ - + - + + + {{ "application" | i18n }} @@ -20,17 +30,17 @@ {{ row.memberCount }} - @if (showRowMenuForCriticalApps) { - - - - - - - } diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/app-table-row-scrollable-m11.component.spec.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/app-table-row-scrollable-m11.component.spec.ts new file mode 100644 index 00000000000..42dcf4cfe28 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/app-table-row-scrollable-m11.component.spec.ts @@ -0,0 +1,181 @@ +import { DebugElement } from "@angular/core"; +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { By } from "@angular/platform-browser"; +import { mock } from "jest-mock-extended"; + +import { ApplicationHealthReportDetailEnriched } from "@bitwarden/bit-common/dirt/reports/risk-insights"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { TableDataSource } from "@bitwarden/components"; +import { I18nPipe } from "@bitwarden/ui-common"; + +import { AppTableRowScrollableM11Component } from "./app-table-row-scrollable-m11.component"; + +// Mock ResizeObserver +global.ResizeObserver = class ResizeObserver { + observe() {} + unobserve() {} + disconnect() {} +}; + +const mockTableData: ApplicationHealthReportDetailEnriched[] = [ + { + applicationName: "google.com", + passwordCount: 5, + atRiskPasswordCount: 2, + atRiskCipherIds: ["cipher-1" as any, "cipher-2" as any], + memberCount: 3, + atRiskMemberCount: 1, + memberDetails: [ + { + userGuid: "user-1", + userName: "John Doe", + email: "john@google.com", + cipherId: "cipher-1", + }, + ], + atRiskMemberDetails: [ + { + userGuid: "user-2", + userName: "Jane Smith", + email: "jane@google.com", + cipherId: "cipher-2", + }, + ], + cipherIds: ["cipher-1" as any, "cipher-2" as any], + isMarkedAsCritical: true, + }, + { + applicationName: "facebook.com", + passwordCount: 3, + atRiskPasswordCount: 1, + atRiskCipherIds: ["cipher-3" as any], + memberCount: 2, + atRiskMemberCount: 1, + memberDetails: [ + { + userGuid: "user-3", + userName: "Alice Johnson", + email: "alice@facebook.com", + cipherId: "cipher-3", + }, + ], + atRiskMemberDetails: [ + { + userGuid: "user-4", + userName: "Bob Wilson", + email: "bob@facebook.com", + cipherId: "cipher-4", + }, + ], + cipherIds: ["cipher-3" as any, "cipher-4" as any], + isMarkedAsCritical: false, + }, + { + applicationName: "twitter.com", + passwordCount: 4, + atRiskPasswordCount: 0, + atRiskCipherIds: [], + memberCount: 4, + atRiskMemberCount: 0, + memberDetails: [], + atRiskMemberDetails: [], + cipherIds: ["cipher-5" as any, "cipher-6" as any], + isMarkedAsCritical: false, + }, +]; + +describe("AppTableRowScrollableM11Component", () => { + let fixture: ComponentFixture; + + beforeEach(async () => { + const mockI18nService = mock(); + mockI18nService.t.mockImplementation((key: string) => key); + + await TestBed.configureTestingModule({ + imports: [AppTableRowScrollableM11Component], + providers: [ + { provide: I18nPipe, useValue: mock() }, + { provide: I18nService, useValue: mockI18nService }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(AppTableRowScrollableM11Component); + + await fixture.whenStable(); + }); + + describe("select all checkbox", () => { + let selectAllCheckboxEl: DebugElement; + + beforeEach(async () => { + selectAllCheckboxEl = fixture.debugElement.query(By.css('[data-testid="selectAll"]')); + }); + + it("should check all rows in table when checked", () => { + // arrange + const selectedUrls = new Set(); + const dataSource = new TableDataSource(); + dataSource.data = mockTableData; + + fixture.componentRef.setInput("selectedUrls", selectedUrls); + fixture.componentRef.setInput("dataSource", dataSource); + fixture.detectChanges(); + + // act + selectAllCheckboxEl.nativeElement.click(); + fixture.detectChanges(); + + // assert + expect(selectedUrls.has("google.com")).toBe(true); + expect(selectedUrls.has("facebook.com")).toBe(true); + expect(selectedUrls.has("twitter.com")).toBe(true); + expect(selectedUrls.size).toBe(3); + }); + + it("should uncheck all rows in table when unchecked", () => { + // arrange + const selectedUrls = new Set(["google.com", "facebook.com", "twitter.com"]); + const dataSource = new TableDataSource(); + dataSource.data = mockTableData; + + fixture.componentRef.setInput("selectedUrls", selectedUrls); + fixture.componentRef.setInput("dataSource", dataSource); + fixture.detectChanges(); + + // act + selectAllCheckboxEl.nativeElement.click(); + fixture.detectChanges(); + + // assert + expect(selectedUrls.size).toBe(0); + }); + + it("should become checked when all rows in table are checked", () => { + // arrange + const selectedUrls = new Set(["google.com", "facebook.com", "twitter.com"]); + const dataSource = new TableDataSource(); + dataSource.data = mockTableData; + + fixture.componentRef.setInput("selectedUrls", selectedUrls); + fixture.componentRef.setInput("dataSource", dataSource); + fixture.detectChanges(); + + // assert + expect(selectAllCheckboxEl.nativeElement.checked).toBe(true); + }); + + it("should become unchecked when any row in table is unchecked", () => { + // arrange + const selectedUrls = new Set(["google.com", "facebook.com"]); + const dataSource = new TableDataSource(); + dataSource.data = mockTableData; + + fixture.componentRef.setInput("selectedUrls", selectedUrls); + fixture.componentRef.setInput("dataSource", dataSource); + fixture.detectChanges(); + + // assert + expect(selectAllCheckboxEl.nativeElement.checked).toBe(false); + }); + }); +}); diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/app-table-row-scrollable-m11.component.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/app-table-row-scrollable-m11.component.ts index ef870bd5b38..a23d1855ba5 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/app-table-row-scrollable-m11.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/app-table-row-scrollable-m11.component.ts @@ -1,8 +1,8 @@ import { CommonModule } from "@angular/common"; -import { Component, Input } from "@angular/core"; +import { ChangeDetectionStrategy, Component, input } from "@angular/core"; import { JslibModule } from "@bitwarden/angular/jslib.module"; -import { MenuModule, TableDataSource, TableModule } from "@bitwarden/components"; +import { MenuModule, TableDataSource, TableModule, TooltipDirective } from "@bitwarden/components"; import { SharedModule } from "@bitwarden/web-vault/app/shared"; import { PipesModule } from "@bitwarden/web-vault/app/vault/individual-vault/pipes/pipes.module"; @@ -11,34 +11,52 @@ import { ApplicationTableDataSource } from "./app-table-row-scrollable.component //TODO: Rename this component to AppTableRowScrollableComponent once milestone 11 is fully rolled out //TODO: Move definition of ApplicationTableDataSource to this file from app-table-row-scrollable.component.ts -// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush -// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ + changeDetection: ChangeDetectionStrategy.OnPush, selector: "app-table-row-scrollable-m11", - imports: [CommonModule, JslibModule, TableModule, SharedModule, PipesModule, MenuModule], + imports: [ + CommonModule, + JslibModule, + TableModule, + SharedModule, + PipesModule, + MenuModule, + TooltipDirective, + ], templateUrl: "./app-table-row-scrollable-m11.component.html", }) export class AppTableRowScrollableM11Component { - // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals - // eslint-disable-next-line @angular-eslint/prefer-signals - @Input() - dataSource!: TableDataSource; - // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals - // eslint-disable-next-line @angular-eslint/prefer-signals - @Input() showRowMenuForCriticalApps: boolean = false; - // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals - // eslint-disable-next-line @angular-eslint/prefer-signals - @Input() selectedUrls: Set = new Set(); - // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals - // eslint-disable-next-line @angular-eslint/prefer-signals - @Input() openApplication: string = ""; - // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals - // eslint-disable-next-line @angular-eslint/prefer-signals - @Input() showAppAtRiskMembers!: (applicationName: string) => void; - // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals - // eslint-disable-next-line @angular-eslint/prefer-signals - @Input() unmarkAsCritical!: (applicationName: string) => void; - // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals - // eslint-disable-next-line @angular-eslint/prefer-signals - @Input() checkboxChange!: (applicationName: string, $event: Event) => void; + readonly dataSource = input>(); + readonly selectedUrls = input>(); + readonly openApplication = input(""); + readonly showAppAtRiskMembers = input<(applicationName: string) => void>(); + readonly checkboxChange = input<(applicationName: string, $event: Event) => void>(); + + allAppsSelected(): boolean { + const tableData = this.dataSource()?.filteredData; + const selectedUrls = this.selectedUrls(); + + if (!tableData || !selectedUrls) { + return false; + } + + return tableData.length > 0 && tableData.every((row) => selectedUrls.has(row.applicationName)); + } + + selectAllChanged(target: HTMLInputElement) { + const checked = target.checked; + + const tableData = this.dataSource()?.filteredData; + const selectedUrls = this.selectedUrls(); + + if (!tableData || !selectedUrls) { + return false; + } + + if (checked) { + tableData.forEach((row) => selectedUrls.add(row.applicationName)); + } else { + selectedUrls.clear(); + } + } }