From b3cb99003744201894d285e808f338c38dbba035 Mon Sep 17 00:00:00 2001 From: Andreas Coroiu Date: Tue, 10 Feb 2026 15:11:12 +0100 Subject: [PATCH 01/28] [PM-29313] [Defect] TDE JIT Provisioning - Extension showing locked icon even if user already logged in (#18672) * fix: add better error handling to badge service * fix: lint --- .../src/platform/badge/badge.service.ts | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/apps/browser/src/platform/badge/badge.service.ts b/apps/browser/src/platform/badge/badge.service.ts index f6d799b2a80..0ecb8210dd0 100644 --- a/apps/browser/src/platform/badge/badge.service.ts +++ b/apps/browser/src/platform/badge/badge.service.ts @@ -1,5 +1,6 @@ import { BehaviorSubject, + catchError, combineLatest, combineLatestWith, concatMap, @@ -73,9 +74,25 @@ export class BadgeService { map((evt) => evt.tab), combineLatestWith(this.stateFunctions), switchMap(([tab, dynamicStateFunctions]) => { - const functions = [...Object.values(dynamicStateFunctions), defaultTabStateFunction]; + const functions = [ + ...Object.entries(dynamicStateFunctions), + ["default" as string, defaultTabStateFunction] as const, + ]; - return combineLatest(functions.map((f) => f(tab).pipe(startWith(undefined)))).pipe( + return combineLatest( + functions.map(([name, f]) => + f(tab).pipe( + startWith(undefined), + catchError((error: unknown) => { + this.logService.error( + `BadgeService: State function "${name}" threw an error`, + error, + ); + return of(undefined); + }), + ), + ), + ).pipe( map((states) => ({ tab, states: states.filter((s): s is BadgeStateSetting => s !== undefined), From 9853c141f96a3184b3c159c0438263814d5be300 Mon Sep 17 00:00:00 2001 From: Isaiah Inuwa Date: Tue, 10 Feb 2026 10:08:37 -0600 Subject: [PATCH 02/28] [BRE-1564] Fix Appx for MS App Store Release (#18812) * Change Appx publisher back to production certificate subject * Skip signing appx in PR CI * Use identifierName for publisher identifier --- apps/desktop/custom-appx-manifest.xml | 2 +- apps/desktop/electron-builder.beta.json | 2 +- apps/desktop/electron-builder.json | 2 +- apps/desktop/scripts/appx-cross-build.ps1 | 1 + apps/desktop/sign.js | 2 +- 5 files changed, 5 insertions(+), 4 deletions(-) diff --git a/apps/desktop/custom-appx-manifest.xml b/apps/desktop/custom-appx-manifest.xml index 2f7796c97cf..166b852588b 100644 --- a/apps/desktop/custom-appx-manifest.xml +++ b/apps/desktop/custom-appx-manifest.xml @@ -13,7 +13,7 @@ xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/re IgnorableNamespaces="uap rescap com uap10 build" xmlns:build="http://schemas.microsoft.com/developer/appx/2015/build"> - diff --git a/apps/desktop/electron-builder.beta.json b/apps/desktop/electron-builder.beta.json index 3e1ca673c3c..9c66b17aa1f 100644 --- a/apps/desktop/electron-builder.beta.json +++ b/apps/desktop/electron-builder.beta.json @@ -64,7 +64,7 @@ "customManifestPath": "./custom-appx-manifest.xml", "applicationId": "BitwardenBeta", "identityName": "8bitSolutionsLLC.BitwardenBeta", - "publisher": "CN=Bitwarden Inc., O=Bitwarden Inc., L=Santa Barbara, S=California, C=US, SERIALNUMBER=7654941, OID.2.5.4.15=Private Organization, OID.1.3.6.1.4.1.311.60.2.1.2=Delaware, OID.1.3.6.1.4.1.311.60.2.1.3=US", + "publisher": "CN=14D52771-DE3C-4886-B8BF-825BA7690418", "publisherDisplayName": "Bitwarden Inc", "languages": [ "en-US", diff --git a/apps/desktop/electron-builder.json b/apps/desktop/electron-builder.json index 481d12f02b4..151ce72182d 100644 --- a/apps/desktop/electron-builder.json +++ b/apps/desktop/electron-builder.json @@ -179,7 +179,7 @@ "customManifestPath": "./custom-appx-manifest.xml", "applicationId": "bitwardendesktop", "identityName": "8bitSolutionsLLC.bitwardendesktop", - "publisher": "CN=Bitwarden Inc., O=Bitwarden Inc., L=Santa Barbara, S=California, C=US, SERIALNUMBER=7654941, OID.2.5.4.15=Private Organization, OID.1.3.6.1.4.1.311.60.2.1.2=Delaware, OID.1.3.6.1.4.1.311.60.2.1.3=US", + "publisher": "CN=14D52771-DE3C-4886-B8BF-825BA7690418", "publisherDisplayName": "Bitwarden Inc", "languages": [ "en-US", diff --git a/apps/desktop/scripts/appx-cross-build.ps1 b/apps/desktop/scripts/appx-cross-build.ps1 index 62619d5ea37..ef2ab09104c 100755 --- a/apps/desktop/scripts/appx-cross-build.ps1 +++ b/apps/desktop/scripts/appx-cross-build.ps1 @@ -176,6 +176,7 @@ $translationMap = @{ 'applicationId' = $builderConfig.appx.applicationId 'displayName' = $productName 'executable' = "app\${productName}.exe" + 'identityName' = $builderConfig.appx.identityName 'publisher' = $builderConfig.appx.publisher 'publisherDisplayName' = $builderConfig.appx.publisherDisplayName 'version' = $version diff --git a/apps/desktop/sign.js b/apps/desktop/sign.js index f115e9b8097..a01388c703c 100644 --- a/apps/desktop/sign.js +++ b/apps/desktop/sign.js @@ -3,7 +3,7 @@ const child_process = require("child_process"); exports.default = async function (configuration) { const ext = configuration.path.split(".").at(-1); - if (parseInt(process.env.ELECTRON_BUILDER_SIGN) === 1 && ["exe", "appx"].includes(ext)) { + if (parseInt(process.env.ELECTRON_BUILDER_SIGN) === 1 && ["exe"].includes(ext)) { console.log(`[*] Signing file: ${configuration.path}`); child_process.execFileSync( "azuresigntool", From d1f2a91d582ba96a27d7717fd62bfd362c993c42 Mon Sep 17 00:00:00 2001 From: Vijay Oommen Date: Tue, 10 Feb 2026 10:14:27 -0600 Subject: [PATCH 03/28] [PM-31867] Ensure that row content has aria-label (#18872) --- .../all-applications/applications.component.html | 1 + .../app-table-row-scrollable-m11.component.html | 16 ++++++++++------ 2 files changed, 11 insertions(+), 6 deletions(-) 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 a3d29c521c5..efe07d50683 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 @@ -50,6 +50,7 @@ [openApplication]="drawerDetails.invokerId || ''" [checkboxChange]="onCheckboxChange" [showAppAtRiskMembers]="showAppAtRiskMembers" + class="tw-mb-10" > @if (emptyTableExplanation()) { 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 c23202b6832..29da8a7a818 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 @@ -25,6 +25,7 @@ @if (row.iconCipher) { @@ -57,12 +58,15 @@ (keydown.space)="showAppAtRiskMembers()(row.applicationName)" role="button" tabindex="0" - [attr.aria-label]="'viewItem' | i18n" + [attr.aria-label]=" + row.applicationName + ' ' + (row.isMarkedAsCritical ? ('criticalBadge' | i18n) : '') + " >
{{ row.applicationName }}
+ @if (row.isMarkedAsCritical) { {{ "criticalBadge" | i18n @@ -79,7 +83,7 @@ (keydown.space)="showAppAtRiskMembers()(row.applicationName)" role="button" tabindex="0" - [attr.aria-label]="'viewItem' | i18n" + [attr.aria-label]="('atRiskPasswords' | i18n) + ' ' + row.atRiskPasswordCount" > {{ row.atRiskPasswordCount }} @@ -94,7 +98,7 @@ (keydown.space)="showAppAtRiskMembers()(row.applicationName)" role="button" tabindex="0" - [attr.aria-label]="'viewItem' | i18n" + [attr.aria-label]="('totalPasswords' | i18n) + ' ' + row.passwordCount" > {{ row.passwordCount }} @@ -109,7 +113,7 @@ (keydown.space)="showAppAtRiskMembers()(row.applicationName)" role="button" tabindex="0" - [attr.aria-label]="'viewItem' | i18n" + [attr.aria-label]="('atRiskMembers' | i18n) + ' ' + row.atRiskMemberCount" > {{ row.atRiskMemberCount }} @@ -125,7 +129,7 @@ (keydown.space)="showAppAtRiskMembers()(row.applicationName)" role="button" tabindex="0" - [attr.aria-label]="'viewItem' | i18n" + [attr.aria-label]="('totalMembers' | i18n) + ' ' + row.memberCount" > {{ row.memberCount }} From aa1d477c582e8100e025ea40df2f47f989a60475 Mon Sep 17 00:00:00 2001 From: Nick Krantz <125900171+nick-livefront@users.noreply.github.com> Date: Tue, 10 Feb 2026 13:05:02 -0600 Subject: [PATCH 04/28] [PM-31740] Align Dialog Text (#18876) * center help links in transfer dialogs * fix icon warnings --- .../leave-confirmation-dialog.component.html | 12 ++++++------ .../leave-confirmation-dialog.component.ts | 3 ++- .../transfer-items-dialog.component.html | 4 ++-- .../transfer-items-dialog.component.ts | 3 ++- 4 files changed, 12 insertions(+), 10 deletions(-) diff --git a/libs/vault/src/components/vault-items-transfer/leave-confirmation-dialog.component.html b/libs/vault/src/components/vault-items-transfer/leave-confirmation-dialog.component.html index 6d1045e1a86..ac55c3cebd1 100644 --- a/libs/vault/src/components/vault-items-transfer/leave-confirmation-dialog.component.html +++ b/libs/vault/src/components/vault-items-transfer/leave-confirmation-dialog.component.html @@ -1,9 +1,9 @@ - + name="bwi-exclamation-triangle" + class="tw-text-warning tw-text-3xl" + > {{ "leaveConfirmationDialogTitle" | i18n }} @@ -25,9 +25,9 @@ {{ "goBack" | i18n }} - + {{ "howToManageMyVault" | i18n }} - + diff --git a/libs/vault/src/components/vault-items-transfer/leave-confirmation-dialog.component.ts b/libs/vault/src/components/vault-items-transfer/leave-confirmation-dialog.component.ts index af106376a79..44788a8234a 100644 --- a/libs/vault/src/components/vault-items-transfer/leave-confirmation-dialog.component.ts +++ b/libs/vault/src/components/vault-items-transfer/leave-confirmation-dialog.component.ts @@ -10,6 +10,7 @@ import { DialogService, ButtonModule, DialogModule, + IconModule, LinkModule, TypographyModule, CenterPositionStrategy, @@ -35,7 +36,7 @@ export type LeaveConfirmationDialogResultType = UnionOfValues(DIALOG_DATA); diff --git a/libs/vault/src/components/vault-items-transfer/transfer-items-dialog.component.html b/libs/vault/src/components/vault-items-transfer/transfer-items-dialog.component.html index 3cf626baaf7..5d1c3ba9aed 100644 --- a/libs/vault/src/components/vault-items-transfer/transfer-items-dialog.component.html +++ b/libs/vault/src/components/vault-items-transfer/transfer-items-dialog.component.html @@ -14,9 +14,9 @@ {{ "declineAndLeave" | i18n }} - + {{ "whyAmISeeingThis" | i18n }} - + diff --git a/libs/vault/src/components/vault-items-transfer/transfer-items-dialog.component.ts b/libs/vault/src/components/vault-items-transfer/transfer-items-dialog.component.ts index 619181f37fc..45f6305b5b3 100644 --- a/libs/vault/src/components/vault-items-transfer/transfer-items-dialog.component.ts +++ b/libs/vault/src/components/vault-items-transfer/transfer-items-dialog.component.ts @@ -10,6 +10,7 @@ import { DialogService, ButtonModule, DialogModule, + IconModule, LinkModule, TypographyModule, CenterPositionStrategy, @@ -35,7 +36,7 @@ export type TransferItemsDialogResultType = UnionOfValues(DIALOG_DATA); From 341de2c378a548aa15acf8d4cc46ddd54f1e3b82 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 10 Feb 2026 14:40:21 -0500 Subject: [PATCH 05/28] [deps]: Update Minor github-actions updates (#18714) * [deps]: Update Minor github-actions updates * Revert bump of create-github-app-token for test-browser-interactions.yml --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Daniel James Smith --- .github/workflows/build-browser.yml | 2 +- .github/workflows/build-desktop.yml | 8 ++++---- .github/workflows/build-web.yml | 4 ++-- .github/workflows/lint-crowdin-config.yml | 2 +- .github/workflows/lint.yml | 2 +- .github/workflows/publish-desktop.yml | 2 +- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/build-browser.yml b/.github/workflows/build-browser.yml index ef2c91f0a7d..6a334e31a18 100644 --- a/.github/workflows/build-browser.yml +++ b/.github/workflows/build-browser.yml @@ -565,7 +565,7 @@ jobs: uses: bitwarden/gh-actions/azure-logout@main - name: Upload Sources - uses: crowdin/github-action@60debf382ee245b21794321190ad0501db89d8c1 # v2.13.0 + uses: crowdin/github-action@b4b468cffefb50bdd99dd83e5d2eaeb63c880380 # v2.14.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }} diff --git a/.github/workflows/build-desktop.yml b/.github/workflows/build-desktop.yml index 6818064a808..c500e59d536 100644 --- a/.github/workflows/build-desktop.yml +++ b/.github/workflows/build-desktop.yml @@ -1007,7 +1007,7 @@ jobs: node-version: ${{ env._NODE_VERSION }} - name: Set up Python - uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: '3.14.2' @@ -1247,7 +1247,7 @@ jobs: node-version: ${{ env._NODE_VERSION }} - name: Set up Python - uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: '3.14.2' @@ -1522,7 +1522,7 @@ jobs: node-version: ${{ env._NODE_VERSION }} - name: Set up Python - uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: '3.14.2' @@ -1873,7 +1873,7 @@ jobs: uses: bitwarden/gh-actions/azure-logout@main - name: Upload Sources - uses: crowdin/github-action@60debf382ee245b21794321190ad0501db89d8c1 # v2.13.0 + uses: crowdin/github-action@b4b468cffefb50bdd99dd83e5d2eaeb63c880380 # v2.14.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }} diff --git a/.github/workflows/build-web.yml b/.github/workflows/build-web.yml index 71a2c62ec1a..688bd30bfe5 100644 --- a/.github/workflows/build-web.yml +++ b/.github/workflows/build-web.yml @@ -352,7 +352,7 @@ jobs: - name: Scan Docker image if: ${{ needs.setup.outputs.has_secrets == 'true' }} id: container-scan - uses: anchore/scan-action@62b74fb7bb810d2c45b1865f47a77655621862a5 # v7.2.3 + uses: anchore/scan-action@0d444ed77d83ee2ba7f5ced0d90d640a1281d762 # v7.3.0 with: image: ${{ steps.image-name.outputs.name }} fail-build: false @@ -408,7 +408,7 @@ jobs: uses: bitwarden/gh-actions/azure-logout@main - name: Upload Sources - uses: crowdin/github-action@60debf382ee245b21794321190ad0501db89d8c1 # v2.13.0 + uses: crowdin/github-action@b4b468cffefb50bdd99dd83e5d2eaeb63c880380 # v2.14.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }} diff --git a/.github/workflows/lint-crowdin-config.yml b/.github/workflows/lint-crowdin-config.yml index 61e2b3631e6..e1e620c864d 100644 --- a/.github/workflows/lint-crowdin-config.yml +++ b/.github/workflows/lint-crowdin-config.yml @@ -45,7 +45,7 @@ jobs: uses: bitwarden/gh-actions/azure-logout@main - name: Lint ${{ matrix.app.name }} config - uses: crowdin/github-action@60debf382ee245b21794321190ad0501db89d8c1 # v2.13.0 + uses: crowdin/github-action@b4b468cffefb50bdd99dd83e5d2eaeb63c880380 # v2.14.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} CROWDIN_PROJECT_ID: ${{ matrix.app.project_id }} diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 7862c14c186..efc8c25fc5e 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -142,7 +142,7 @@ jobs: run: cargo +nightly udeps --workspace --all-features --all-targets - name: Install cargo-deny - uses: taiki-e/install-action@542cebaaed782771e619bd5609d97659d109c492 # v2.66.7 + uses: taiki-e/install-action@887bc4e03483810873d617344dd5189cd82e7b8b # v2.67.11 with: tool: cargo-deny@0.18.6 diff --git a/.github/workflows/publish-desktop.yml b/.github/workflows/publish-desktop.yml index c5db7ea9295..45665f459e8 100644 --- a/.github/workflows/publish-desktop.yml +++ b/.github/workflows/publish-desktop.yml @@ -331,7 +331,7 @@ jobs: run: wget "https://github.com/bitwarden/clients/releases/download/${_RELEASE_TAG}/macos-build-number.json" - name: Setup Ruby and Install Fastlane - uses: ruby/setup-ruby@708024e6c902387ab41de36e1669e43b5ee7085e # v1.283.0 + uses: ruby/setup-ruby@90be1154f987f4dc0fe0dd0feedac9e473aa4ba8 # v1.286.0 with: ruby-version: '3.4.7' bundler-cache: false From 6f1a6187147b88bc3bfdf0570b65dd1032b0b0e6 Mon Sep 17 00:00:00 2001 From: Jackson Engstrom Date: Tue, 10 Feb 2026 11:46:03 -0800 Subject: [PATCH 06/28] [PM-31732] Fix issue with user flow from vault-item-dialog --- .../vault-item-dialog/vault-item-dialog.component.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts b/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts index 4a24bbcf3fd..d9eb03ea1ca 100644 --- a/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts +++ b/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts @@ -524,11 +524,12 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy { const dialogRef = this.dialogService.open< AttachmentDialogCloseResult, - { cipherId: CipherId; organizationId?: OrganizationId } + { cipherId: CipherId; organizationId?: OrganizationId; canEditCipher?: boolean } >(AttachmentsV2Component, { data: { cipherId: this.formConfig.originalCipher?.id as CipherId, organizationId: this.formConfig.originalCipher?.organizationId as OrganizationId, + canEditCipher: this.formConfig.originalCipher?.edit, }, }); From 3b535802dbb99c179ca8a19666d32ff290f7fd8e Mon Sep 17 00:00:00 2001 From: Jared Date: Tue, 10 Feb 2026 15:45:45 -0500 Subject: [PATCH 07/28] =?UTF-8?q?[PM-26020]=20Implement=20dynamic=20cipher?= =?UTF-8?q?=20creation=20permissions=20in=20vault=20header=20and=20new?= =?UTF-8?q?=E2=80=A6=20(#18579)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Implement dynamic cipher creation permissions in vault header and new cipher menu components * Enhance new cipher menu button behavior and accessibility. Implement dynamic button label based on creation permissions, allowing direct collection creation when applicable. Update button trigger logic to improve user experience. * Update apps/web/src/app/vault/individual-vault/vault-header/vault-header.component.ts Co-authored-by: SmithThe4th * Add canCreateCipher getter for improved readability --------- Co-authored-by: SmithThe4th --- .../vault-header/vault-header.component.html | 2 +- .../vault-header/vault-header.component.ts | 4 ++ .../new-cipher-menu.component.html | 7 +-- .../new-cipher-menu.component.ts | 53 +++++++++++++++++-- 4 files changed, 58 insertions(+), 8 deletions(-) diff --git a/apps/web/src/app/vault/individual-vault/vault-header/vault-header.component.html b/apps/web/src/app/vault/individual-vault/vault-header/vault-header.component.html index af4eb182eec..d2f5cc38013 100644 --- a/apps/web/src/app/vault/individual-vault/vault-header/vault-header.component.html +++ b/apps/web/src/app/vault/individual-vault/vault-header/vault-header.component.html @@ -76,7 +76,7 @@
- {{ "new" | i18n }} + {{ getButtonLabel() | i18n }} @for (item of cipherMenuItems$ | async; track item.type) { diff --git a/libs/vault/src/components/new-cipher-menu/new-cipher-menu.component.ts b/libs/vault/src/components/new-cipher-menu/new-cipher-menu.component.ts index 0a755a9cdb4..1a592809691 100644 --- a/libs/vault/src/components/new-cipher-menu/new-cipher-menu.component.ts +++ b/libs/vault/src/components/new-cipher-menu/new-cipher-menu.component.ts @@ -1,6 +1,7 @@ import { CommonModule } from "@angular/common"; import { Component, input, output } from "@angular/core"; -import { map, shareReplay } from "rxjs"; +import { toObservable } from "@angular/core/rxjs-interop"; +import { combineLatest, map, shareReplay } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { CipherType } from "@bitwarden/common/vault/enums"; @@ -38,10 +39,18 @@ export class NewCipherMenuComponent { /** * Returns an observable that emits the cipher menu items, filtered by the restricted types. */ - cipherMenuItems$ = this.restrictedItemTypesService.restricted$.pipe( - map((restrictedTypes) => { + cipherMenuItems$ = combineLatest([ + this.restrictedItemTypesService.restricted$, + toObservable(this.canCreateCipher), + toObservable(this.canCreateSshKey), + ]).pipe( + map(([restrictedTypes, canCreateCipher, canCreateSshKey]) => { + // If user cannot create ciphers at all, return empty array + if (!canCreateCipher) { + return []; + } return CIPHER_MENU_ITEMS.filter((item) => { - if (!this.canCreateSshKey() && item.type === CipherType.SshKey) { + if (!canCreateSshKey && item.type === CipherType.SshKey) { return false; } return !restrictedTypes.some((restrictedType) => restrictedType.cipherType === item.type); @@ -49,4 +58,40 @@ export class NewCipherMenuComponent { }), shareReplay({ bufferSize: 1, refCount: true }), ); + + /** + * Returns the appropriate button label based on what can be created. + * If only collections can be created (no ciphers or folders), show "New Collection". + * Otherwise, show "New". + */ + protected getButtonLabel(): string { + const canCreateCipher = this.canCreateCipher(); + const canCreateFolder = this.canCreateFolder(); + const canCreateCollection = this.canCreateCollection(); + + // If only collections can be created, be specific + if (!canCreateCipher && !canCreateFolder && canCreateCollection) { + return "newCollection"; + } + + return "new"; + } + + /** + * Returns true if only collections can be created (no other options). + * When this is true, the button should directly create a collection instead of showing a dropdown. + */ + protected isOnlyCollectionCreation(): boolean { + return !this.canCreateCipher() && !this.canCreateFolder() && this.canCreateCollection(); + } + + /** + * Handles the button click. If only collections can be created, directly emit the collection event. + * Otherwise, the menu trigger will handle opening the dropdown. + */ + protected handleButtonClick(): void { + if (this.isOnlyCollectionCreation()) { + this.collectionAdded.emit(); + } + } } From 1aef83b6e3b5665d10b752446484beefe9642163 Mon Sep 17 00:00:00 2001 From: Brandon Treston Date: Tue, 10 Feb 2026 16:20:39 -0500 Subject: [PATCH 08/28] [PM-28262] Bulk re-invite UI improvements (#18754) * implement dilogs and logic * clean up, add tests * add feature flag key * product requested changes * more product changes * edit error message --- .../bulk/bulk-progress-dialog.component.html | 22 ++ .../bulk/bulk-progress-dialog.component.ts | 46 +++++ ...ulk-reinvite-failure-dialog.component.html | 70 +++++++ .../bulk-reinvite-failure-dialog.component.ts | 62 ++++++ .../members/deprecated_members.component.ts | 10 +- .../members/members.component.html | 37 ++-- .../members/members.component.spec.ts | 2 +- .../members/members.component.ts | 24 ++- .../organizations/members/members.module.ts | 7 +- .../member-actions.service.spec.ts | 195 ++++++++++++++++-- .../member-actions/member-actions.service.ts | 96 ++++++--- .../member-dialog-manager.service.ts | 36 +++- apps/web/src/locales/en/messages.json | 56 +++++ .../manage/deprecated_members.component.ts | 5 +- .../providers/manage/members.component.ts | 5 +- libs/common/src/enums/feature-flag.enum.ts | 2 + 16 files changed, 596 insertions(+), 79 deletions(-) create mode 100644 apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-progress-dialog.component.html create mode 100644 apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-progress-dialog.component.ts create mode 100644 apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-reinvite-failure-dialog.component.html create mode 100644 apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-reinvite-failure-dialog.component.ts diff --git a/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-progress-dialog.component.html b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-progress-dialog.component.html new file mode 100644 index 00000000000..2fbcc8afd86 --- /dev/null +++ b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-progress-dialog.component.html @@ -0,0 +1,22 @@ + +
+
+
+ +
+
+
+

+ {{ "bulkReinviteProgressTitle" | i18n: progressCount() : allCount }} +

+ + {{ "bulkReinviteProgressSubtitle" | i18n }} + +
+
+
diff --git a/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-progress-dialog.component.ts b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-progress-dialog.component.ts new file mode 100644 index 00000000000..66582fb4434 --- /dev/null +++ b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-progress-dialog.component.ts @@ -0,0 +1,46 @@ +import { DialogConfig, DialogRef } from "@angular/cdk/dialog"; +import { + ChangeDetectionStrategy, + Component, + computed, + effect, + Inject, + Signal, +} from "@angular/core"; + +import { DIALOG_DATA, DialogService } from "@bitwarden/components"; + +export interface BulkProgressDialogParams { + progress: Signal; + allCount: number; +} + +@Component({ + templateUrl: "bulk-progress-dialog.component.html", + selector: "member-bulk-progress-dialog", + changeDetection: ChangeDetectionStrategy.OnPush, + standalone: false, +}) +export class BulkProgressDialogComponent { + protected allCount: string; + protected readonly progressCount: Signal; + protected readonly progressPercentage: Signal; + private readonly progressEffect = effect(() => { + if (this.progressPercentage() >= 100) { + this.dialogRef.close(); + } + }); + + constructor( + public dialogRef: DialogRef, + @Inject(DIALOG_DATA) data: BulkProgressDialogParams, + ) { + this.progressCount = computed(() => data.progress().toLocaleString()); + this.allCount = data.allCount.toLocaleString(); + this.progressPercentage = computed(() => (data.progress() / data.allCount) * 100); + } + + static open(dialogService: DialogService, config: DialogConfig) { + return dialogService.open(BulkProgressDialogComponent, config); + } +} diff --git a/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-reinvite-failure-dialog.component.html b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-reinvite-failure-dialog.component.html new file mode 100644 index 00000000000..0f216be6e5f --- /dev/null +++ b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-reinvite-failure-dialog.component.html @@ -0,0 +1,70 @@ + + @let failCount = dataSource().data.length; +
+ @if (failCount > 1) { + {{ "bulkReinviteFailuresTitle" | i18n: failCount }} + } @else { + {{ "bulkReinviteFailureTitle" | i18n }} + } +
+ +
+ {{ "bulkReinviteFailureDescription" | i18n: failCount : totalCount }} + + + {{ "contactSupportShort" | i18n | lowercase }} + + + +
+ + + + {{ "name" | i18n }} + + + + + @let rows = $any(rows$ | async); + @for (u of rows; track u.id) { + + +
+ +
+
+ +
+ @if (u.name) { +
+ {{ u.email }} +
+ } +
+
+ + + } +
+
+
+
+ + + + + + +
diff --git a/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-reinvite-failure-dialog.component.ts b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-reinvite-failure-dialog.component.ts new file mode 100644 index 00000000000..5cb11708fd0 --- /dev/null +++ b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-reinvite-failure-dialog.component.ts @@ -0,0 +1,62 @@ +import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog"; +import { ChangeDetectionStrategy, Component, Inject, signal, WritableSignal } from "@angular/core"; + +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; +import { DialogService } from "@bitwarden/components"; +import { MembersTableDataSource } from "@bitwarden/web-vault/app/admin-console/common/people-table-data-source"; + +import { OrganizationUserView } from "../../../core"; +import { + BulkActionResult, + MemberActionsService, +} from "../../services/member-actions/member-actions.service"; + +export interface BulkReinviteFailureDialogParams { + result: BulkActionResult; + users: OrganizationUserView[]; + organization: Organization; +} + +@Component({ + templateUrl: "bulk-reinvite-failure-dialog.component.html", + selector: "member-bulk-reinvite-failure-dialog", + changeDetection: ChangeDetectionStrategy.OnPush, + standalone: false, +}) +export class BulkReinviteFailureDialogComponent { + private organization: Organization; + protected totalCount: string; + protected readonly dataSource: WritableSignal; + + constructor( + public dialogRef: DialogRef, + private memberActionsService: MemberActionsService, + @Inject(DIALOG_DATA) data: BulkReinviteFailureDialogParams, + environmentService: EnvironmentService, + ) { + this.organization = data.organization; + this.totalCount = (data.users.length ?? 0).toLocaleString(); + this.dataSource = signal(new MembersTableDataSource(environmentService)); + this.dataSource().data = data.result.failed.map((failedUser) => { + const user = data.users.find((u) => u.id === failedUser.id); + if (user == null) { + throw new Error("Member not found"); + } + return user; + }); + } + + async resendInvitations() { + await this.memberActionsService.bulkReinvite(this.organization, this.dataSource().data); + this.dialogRef.close(); + } + + async cancel() { + this.dialogRef.close(); + } + + static open(dialogService: DialogService, config: DialogConfig) { + return dialogService.open(BulkReinviteFailureDialogComponent, config); + } +} diff --git a/apps/web/src/app/admin-console/organizations/members/deprecated_members.component.ts b/apps/web/src/app/admin-console/organizations/members/deprecated_members.component.ts index 197c5d3efb5..dae9bafbcfe 100644 --- a/apps/web/src/app/admin-console/organizations/members/deprecated_members.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/deprecated_members.component.ts @@ -444,10 +444,7 @@ export class MembersComponent extends BaseMembersComponent } try { - const result = await this.memberActionsService.bulkReinvite( - organization, - filteredUsers.map((user) => user.id as UserId), - ); + const result = await this.memberActionsService.bulkReinvite(organization, filteredUsers); if (!result.successful) { throw new Error(); @@ -472,7 +469,10 @@ export class MembersComponent extends BaseMembersComponent } else { this.toastService.showToast({ variant: "success", - message: this.i18nService.t("bulkReinviteSuccessToast", invitedCount.toString()), + message: + invitedCount === 1 + ? this.i18nService.t("reinviteSuccessToast") + : this.i18nService.t("bulkReinviteSentToast", invitedCount.toString()), }); } } else { diff --git a/apps/web/src/app/admin-console/organizations/members/members.component.html b/apps/web/src/app/admin-console/organizations/members/members.component.html index 0f074d4481d..75ef503366b 100644 --- a/apps/web/src/app/admin-console/organizations/members/members.component.html +++ b/apps/web/src/app/admin-console/organizations/members/members.component.html @@ -113,25 +113,24 @@ {{ "policies" | i18n }} @if (showUserManagementControls()) { - -
- - -
- +
+ + +
} diff --git a/apps/web/src/app/admin-console/organizations/members/members.component.spec.ts b/apps/web/src/app/admin-console/organizations/members/members.component.spec.ts index 246c3d8a1c0..1cd90989b12 100644 --- a/apps/web/src/app/admin-console/organizations/members/members.component.spec.ts +++ b/apps/web/src/app/admin-console/organizations/members/members.component.spec.ts @@ -519,7 +519,7 @@ describe("vNextMembersComponent", () => { await component.bulkReinvite(mockOrg); - expect(mockMemberActionsService.bulkReinvite).toHaveBeenCalledWith(mockOrg, [invitedUser.id]); + expect(mockMemberActionsService.bulkReinvite).toHaveBeenCalledWith(mockOrg, [invitedUser]); expect(mockMemberDialogManager.openBulkStatusDialog).toHaveBeenCalled(); }); diff --git a/apps/web/src/app/admin-console/organizations/members/members.component.ts b/apps/web/src/app/admin-console/organizations/members/members.component.ts index 36c207219a0..6139c5f07a5 100644 --- a/apps/web/src/app/admin-console/organizations/members/members.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/members.component.ts @@ -33,6 +33,8 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { OrganizationMetadataServiceAbstraction } from "@bitwarden/common/billing/abstractions/organization-metadata.service.abstraction"; import { OrganizationBillingMetadataResponse } from "@bitwarden/common/billing/models/response/organization-billing-metadata.response"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -101,6 +103,7 @@ export class vNextMembersComponent { private organizationMetadataService = inject(OrganizationMetadataServiceAbstraction); private environmentService = inject(EnvironmentService); private memberExportService = inject(MemberExportService); + private configService = inject(ConfigService); private userId$: Observable = this.accountService.activeAccount$.pipe(getUserId); @@ -145,6 +148,10 @@ export class vNextMembersComponent { () => this.organization()?.canManageUsers ?? false, ); + protected readonly bulkReinviteUIEnabled = toSignal( + this.configService.getFeatureFlag$(FeatureFlag.BulkReinviteUI), + ); + protected billingMetadata$: Observable; protected resetPasswordPolicyEnabled$: Observable; @@ -399,7 +406,7 @@ export class vNextMembersComponent { // In cloud environments, limit invited users and uncheck the excess let filteredUsers: OrganizationUserView[]; - if (this.dataSource().isIncreasedBulkLimitEnabled()) { + if (this.dataSource().isIncreasedBulkLimitEnabled() && !this.bulkReinviteUIEnabled()) { filteredUsers = this.dataSource().limitAndUncheckExcess( allInvitedUsers, CloudBulkReinviteLimit, @@ -417,10 +424,7 @@ export class vNextMembersComponent { return; } - const result = await this.memberActionsService.bulkReinvite( - organization, - filteredUsers.map((user) => user.id as UserId), - ); + const result = await this.memberActionsService.bulkReinvite(organization, filteredUsers); if (!result.successful) { this.validationService.showError(result.failed); @@ -431,7 +435,8 @@ export class vNextMembersComponent { const selectedCount = originalInvitedCount; const invitedCount = filteredUsers.length; - if (selectedCount > CloudBulkReinviteLimit) { + // Only show limited toast if feature flag is disabled and limit was applied + if (!this.bulkReinviteUIEnabled() && selectedCount > CloudBulkReinviteLimit) { const excludedCount = selectedCount - CloudBulkReinviteLimit; this.toastService.showToast({ variant: "success", @@ -445,7 +450,10 @@ export class vNextMembersComponent { } else { this.toastService.showToast({ variant: "success", - message: this.i18nService.t("bulkReinviteSuccessToast", invitedCount.toString()), + message: + invitedCount === 1 + ? this.i18nService.t("reinviteSuccessToast") + : this.i18nService.t("bulkReinviteSentToast", invitedCount.toString()), }); } } else { @@ -457,6 +465,8 @@ export class vNextMembersComponent { this.i18nService.t("bulkReinviteMessage"), ); } + + this.dataSource().uncheckAllUsers(); } async bulkConfirm(organization: Organization) { diff --git a/apps/web/src/app/admin-console/organizations/members/members.module.ts b/apps/web/src/app/admin-console/organizations/members/members.module.ts index 9fd477b1e29..54e2d1b6373 100644 --- a/apps/web/src/app/admin-console/organizations/members/members.module.ts +++ b/apps/web/src/app/admin-console/organizations/members/members.module.ts @@ -3,7 +3,7 @@ import { NgModule } from "@angular/core"; import { PasswordStrengthV2Component } from "@bitwarden/angular/tools/password-strength/password-strength-v2.component"; import { PasswordCalloutComponent } from "@bitwarden/auth/angular"; -import { ScrollLayoutDirective } from "@bitwarden/components"; +import { IconModule, ScrollLayoutDirective } from "@bitwarden/components"; import { BillingConstraintService } from "@bitwarden/web-vault/app/billing/members/billing-constraint/billing-constraint.service"; import { OrganizationFreeTrialWarningComponent } from "@bitwarden/web-vault/app/billing/organizations/warnings/components"; @@ -13,6 +13,8 @@ import { SharedOrganizationModule } from "../shared"; import { BulkConfirmDialogComponent } from "./components/bulk/bulk-confirm-dialog.component"; import { BulkDeleteDialogComponent } from "./components/bulk/bulk-delete-dialog.component"; import { BulkEnableSecretsManagerDialogComponent } from "./components/bulk/bulk-enable-sm-dialog.component"; +import { BulkProgressDialogComponent } from "./components/bulk/bulk-progress-dialog.component"; +import { BulkReinviteFailureDialogComponent } from "./components/bulk/bulk-reinvite-failure-dialog.component"; import { BulkRemoveDialogComponent } from "./components/bulk/bulk-remove-dialog.component"; import { BulkRestoreRevokeComponent } from "./components/bulk/bulk-restore-revoke.component"; import { BulkStatusComponent } from "./components/bulk/bulk-status.component"; @@ -39,6 +41,7 @@ import { PasswordStrengthV2Component, ScrollLayoutDirective, OrganizationFreeTrialWarningComponent, + IconModule, ], declarations: [ BulkConfirmDialogComponent, @@ -46,6 +49,8 @@ import { BulkRemoveDialogComponent, BulkRestoreRevokeComponent, BulkStatusComponent, + BulkProgressDialogComponent, + BulkReinviteFailureDialogComponent, MembersComponent, vNextMembersComponent, BulkDeleteDialogComponent, diff --git a/apps/web/src/app/admin-console/organizations/members/services/member-actions/member-actions.service.spec.ts b/apps/web/src/app/admin-console/organizations/members/services/member-actions/member-actions.service.spec.ts index 5924c2f7814..688c7ed77ce 100644 --- a/apps/web/src/app/admin-console/organizations/members/services/member-actions/member-actions.service.spec.ts +++ b/apps/web/src/app/admin-console/organizations/members/services/member-actions/member-actions.service.spec.ts @@ -18,6 +18,7 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga import { OrganizationMetadataServiceAbstraction } from "@bitwarden/common/billing/abstractions/organization-metadata.service.abstraction"; import { ListResponse } from "@bitwarden/common/models/response/list.response"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { OrganizationId, UserId } from "@bitwarden/common/types/guid"; import { DialogService } from "@bitwarden/components"; @@ -25,6 +26,7 @@ import { newGuid } from "@bitwarden/guid"; import { KeyService } from "@bitwarden/key-management"; import { OrganizationUserView } from "../../../core/views/organization-user.view"; +import { MemberDialogManagerService } from "../member-dialog-manager/member-dialog-manager.service"; import { REQUESTS_PER_BATCH, MemberActionsService } from "./member-actions.service"; @@ -34,6 +36,7 @@ describe("MemberActionsService", () => { let organizationUserService: MockProxy; let configService: MockProxy; let organizationMetadataService: MockProxy; + let memberDialogManager: MockProxy; const organizationId = newGuid() as OrganizationId; const userIdToManage = newGuid(); @@ -46,6 +49,7 @@ describe("MemberActionsService", () => { organizationUserService = mock(); configService = mock(); organizationMetadataService = mock(); + memberDialogManager = mock(); mockOrganization = { id: organizationId, @@ -82,6 +86,8 @@ describe("MemberActionsService", () => { useValue: mock(), }, { provide: UserNamePipe, useValue: mock() }, + { provide: MemberDialogManagerService, useValue: memberDialogManager }, + { provide: I18nService, useValue: mock() }, ], }); @@ -318,8 +324,13 @@ describe("MemberActionsService", () => { }); describe("bulkReinvite", () => { + beforeEach(() => { + configService.getFeatureFlag$.mockReturnValue(of(false)); + }); + it("should process users in a single batch when count equals REQUESTS_PER_BATCH", async () => { const userIdsBatch = Array.from({ length: REQUESTS_PER_BATCH }, () => newGuid() as UserId); + const users = userIdsBatch.map((id) => ({ id }) as OrganizationUserView); const mockResponse = new ListResponse( { data: userIdsBatch.map((id) => ({ @@ -333,10 +344,10 @@ describe("MemberActionsService", () => { organizationUserApiService.postManyOrganizationUserReinvite.mockResolvedValue(mockResponse); - const result = await service.bulkReinvite(mockOrganization, userIdsBatch); + const result = await service.bulkReinvite(mockOrganization, users); expect(result.successful).toBeDefined(); - expect(result.successful?.response).toHaveLength(REQUESTS_PER_BATCH); + expect(result.successful).toHaveLength(REQUESTS_PER_BATCH); expect(result.failed).toHaveLength(0); expect(organizationUserApiService.postManyOrganizationUserReinvite).toHaveBeenCalledTimes(1); expect(organizationUserApiService.postManyOrganizationUserReinvite).toHaveBeenCalledWith( @@ -348,6 +359,7 @@ describe("MemberActionsService", () => { it("should process users in multiple batches when count exceeds REQUESTS_PER_BATCH", async () => { const totalUsers = REQUESTS_PER_BATCH + 100; const userIdsBatch = Array.from({ length: totalUsers }, () => newGuid() as UserId); + const users = userIdsBatch.map((id) => ({ id }) as OrganizationUserView); const mockResponse1 = new ListResponse( { @@ -375,10 +387,10 @@ describe("MemberActionsService", () => { .mockResolvedValueOnce(mockResponse1) .mockResolvedValueOnce(mockResponse2); - const result = await service.bulkReinvite(mockOrganization, userIdsBatch); + const result = await service.bulkReinvite(mockOrganization, users); expect(result.successful).toBeDefined(); - expect(result.successful?.response).toHaveLength(totalUsers); + expect(result.successful).toHaveLength(totalUsers); expect(result.failed).toHaveLength(0); expect(organizationUserApiService.postManyOrganizationUserReinvite).toHaveBeenCalledTimes(2); expect(organizationUserApiService.postManyOrganizationUserReinvite).toHaveBeenNthCalledWith( @@ -396,6 +408,7 @@ describe("MemberActionsService", () => { it("should aggregate results across multiple successful batches", async () => { const totalUsers = REQUESTS_PER_BATCH + 50; const userIdsBatch = Array.from({ length: totalUsers }, () => newGuid() as UserId); + const users = userIdsBatch.map((id) => ({ id }) as OrganizationUserView); const mockResponse1 = new ListResponse( { @@ -423,18 +436,19 @@ describe("MemberActionsService", () => { .mockResolvedValueOnce(mockResponse1) .mockResolvedValueOnce(mockResponse2); - const result = await service.bulkReinvite(mockOrganization, userIdsBatch); + const result = await service.bulkReinvite(mockOrganization, users); expect(result.successful).toBeDefined(); - expect(result.successful?.response).toHaveLength(totalUsers); - expect(result.successful?.response.slice(0, REQUESTS_PER_BATCH)).toEqual(mockResponse1.data); - expect(result.successful?.response.slice(REQUESTS_PER_BATCH)).toEqual(mockResponse2.data); + expect(result.successful).toHaveLength(totalUsers); + expect(result.successful!.slice(0, REQUESTS_PER_BATCH)).toEqual(mockResponse1.data); + expect(result.successful!.slice(REQUESTS_PER_BATCH)).toEqual(mockResponse2.data); expect(result.failed).toHaveLength(0); }); it("should handle mixed individual errors across multiple batches", async () => { const totalUsers = REQUESTS_PER_BATCH + 4; const userIdsBatch = Array.from({ length: totalUsers }, () => newGuid() as UserId); + const users = userIdsBatch.map((id) => ({ id }) as OrganizationUserView); const mockResponse1 = new ListResponse( { @@ -464,7 +478,7 @@ describe("MemberActionsService", () => { .mockResolvedValueOnce(mockResponse1) .mockResolvedValueOnce(mockResponse2); - const result = await service.bulkReinvite(mockOrganization, userIdsBatch); + const result = await service.bulkReinvite(mockOrganization, users); // Count expected failures: every 10th index (0, 10, 20, ..., 490) in first batch + 2 explicit in second batch // Indices 0 to REQUESTS_PER_BATCH-1 where index % 10 === 0: that's floor((BATCH_SIZE-1)/10) + 1 values @@ -474,7 +488,7 @@ describe("MemberActionsService", () => { const expectedSuccesses = totalUsers - expectedTotalFailures; expect(result.successful).toBeDefined(); - expect(result.successful?.response).toHaveLength(expectedSuccesses); + expect(result.successful).toHaveLength(expectedSuccesses); expect(result.failed).toHaveLength(expectedTotalFailures); expect(result.failed.some((f) => f.error === "Rate limit exceeded")).toBe(true); expect(result.failed.some((f) => f.error === "Invalid email")).toBe(true); @@ -484,13 +498,14 @@ describe("MemberActionsService", () => { it("should aggregate all failures when all batches fail", async () => { const totalUsers = REQUESTS_PER_BATCH + 100; const userIdsBatch = Array.from({ length: totalUsers }, () => newGuid() as UserId); + const users = userIdsBatch.map((id) => ({ id }) as OrganizationUserView); const errorMessage = "All batches failed"; organizationUserApiService.postManyOrganizationUserReinvite.mockRejectedValue( new Error(errorMessage), ); - const result = await service.bulkReinvite(mockOrganization, userIdsBatch); + const result = await service.bulkReinvite(mockOrganization, users); expect(result.successful).toBeUndefined(); expect(result.failed).toHaveLength(totalUsers); @@ -501,6 +516,7 @@ describe("MemberActionsService", () => { it("should handle empty data in batch response", async () => { const totalUsers = REQUESTS_PER_BATCH + 50; const userIdsBatch = Array.from({ length: totalUsers }, () => newGuid() as UserId); + const users = userIdsBatch.map((id) => ({ id }) as OrganizationUserView); const mockResponse1 = new ListResponse( { @@ -525,16 +541,17 @@ describe("MemberActionsService", () => { .mockResolvedValueOnce(mockResponse1) .mockResolvedValueOnce(mockResponse2); - const result = await service.bulkReinvite(mockOrganization, userIdsBatch); + const result = await service.bulkReinvite(mockOrganization, users); expect(result.successful).toBeDefined(); - expect(result.successful?.response).toHaveLength(REQUESTS_PER_BATCH); + expect(result.successful).toHaveLength(REQUESTS_PER_BATCH); expect(result.failed).toHaveLength(0); }); it("should process batches sequentially in order", async () => { const totalUsers = REQUESTS_PER_BATCH * 2; const userIdsBatch = Array.from({ length: totalUsers }, () => newGuid() as UserId); + const users = userIdsBatch.map((id) => ({ id }) as OrganizationUserView); const callOrder: number[] = []; organizationUserApiService.postManyOrganizationUserReinvite.mockImplementation( @@ -555,11 +572,161 @@ describe("MemberActionsService", () => { }, ); - await service.bulkReinvite(mockOrganization, userIdsBatch); + await service.bulkReinvite(mockOrganization, users); expect(callOrder).toEqual([1, 2]); expect(organizationUserApiService.postManyOrganizationUserReinvite).toHaveBeenCalledTimes(2); }); + + describe("with BulkReinviteUI feature flag enabled", () => { + let mockDialogService: MockProxy; + let mockI18nService: MockProxy; + + beforeEach(() => { + configService.getFeatureFlag$.mockReturnValue(of(true)); + mockDialogService = TestBed.inject(DialogService) as MockProxy; + mockI18nService = TestBed.inject(I18nService) as MockProxy; + mockI18nService.t.mockImplementation((key: string) => key); + }); + + it("should open progress dialog when user count exceeds REQUESTS_PER_BATCH", async () => { + const totalUsers = REQUESTS_PER_BATCH + 100; + const userIdsBatch = Array.from({ length: totalUsers }, () => newGuid() as UserId); + const users = userIdsBatch.map((id) => ({ id }) as OrganizationUserView); + + mockDialogService.openSimpleDialog.mockResolvedValue(true); + + const mockDialogRef = { closed: of(undefined) }; + memberDialogManager.openBulkProgressDialog.mockReturnValue(mockDialogRef as any); + + const mockResponse1 = new ListResponse( + { + data: userIdsBatch.slice(0, REQUESTS_PER_BATCH).map((id) => ({ + id, + error: null, + })), + continuationToken: null, + }, + OrganizationUserBulkResponse, + ); + + const mockResponse2 = new ListResponse( + { + data: userIdsBatch.slice(REQUESTS_PER_BATCH).map((id) => ({ + id, + error: null, + })), + continuationToken: null, + }, + OrganizationUserBulkResponse, + ); + + organizationUserApiService.postManyOrganizationUserReinvite + .mockResolvedValueOnce(mockResponse1) + .mockResolvedValueOnce(mockResponse2); + + await service.bulkReinvite(mockOrganization, users); + + expect(memberDialogManager.openBulkReinviteFailureDialog).not.toHaveBeenCalled(); + expect(memberDialogManager.openBulkProgressDialog).toHaveBeenCalledWith( + expect.anything(), + totalUsers, + ); + }); + + it("should not open progress dialog when user count is or below REQUESTS_PER_BATCH", async () => { + const totalUsers = REQUESTS_PER_BATCH; + const userIdsBatch = Array.from({ length: totalUsers }, () => newGuid() as UserId); + const users = userIdsBatch.map((id) => ({ id }) as OrganizationUserView); + + const mockResponse = new ListResponse( + { + data: userIdsBatch.map((id) => ({ + id, + error: null, + })), + continuationToken: null, + }, + OrganizationUserBulkResponse, + ); + + organizationUserApiService.postManyOrganizationUserReinvite.mockResolvedValue(mockResponse); + + await service.bulkReinvite(mockOrganization, users); + + expect(memberDialogManager.openBulkReinviteFailureDialog).not.toHaveBeenCalled(); + expect(memberDialogManager.openBulkProgressDialog).not.toHaveBeenCalled(); + }); + + it("should open failure dialog when there are failures", async () => { + const totalUsers = 10; + const userIdsBatch = Array.from({ length: totalUsers }, () => newGuid() as UserId); + const users = userIdsBatch.map((id) => ({ id }) as OrganizationUserView); + + const mockResponse = new ListResponse( + { + data: userIdsBatch.map((id) => ({ + id, + error: "error", + })), + continuationToken: null, + }, + OrganizationUserBulkResponse, + ); + + organizationUserApiService.postManyOrganizationUserReinvite.mockResolvedValue(mockResponse); + + const result = await service.bulkReinvite(mockOrganization, users); + + expect(memberDialogManager.openBulkReinviteFailureDialog).toHaveBeenCalledWith( + mockOrganization, + users, + result, + ); + expect(result.failed.length).toBeGreaterThan(0); + }); + + it("should process batches when exceeding REQUESTS_PER_BATCH", async () => { + const totalUsers = REQUESTS_PER_BATCH + 100; + const userIdsBatch = Array.from({ length: totalUsers }, () => newGuid() as UserId); + const users = userIdsBatch.map((id) => ({ id }) as OrganizationUserView); + + const mockDialogRef = { closed: of(undefined) }; + memberDialogManager.openBulkProgressDialog.mockReturnValue(mockDialogRef as any); + + const mockResponse1 = new ListResponse( + { + data: userIdsBatch.slice(0, REQUESTS_PER_BATCH).map((id) => ({ + id, + error: null, + })), + continuationToken: null, + }, + OrganizationUserBulkResponse, + ); + + const mockResponse2 = new ListResponse( + { + data: userIdsBatch.slice(REQUESTS_PER_BATCH).map((id) => ({ + id, + error: null, + })), + continuationToken: null, + }, + OrganizationUserBulkResponse, + ); + + organizationUserApiService.postManyOrganizationUserReinvite + .mockResolvedValueOnce(mockResponse1) + .mockResolvedValueOnce(mockResponse2); + + await service.bulkReinvite(mockOrganization, users); + + expect(organizationUserApiService.postManyOrganizationUserReinvite).toHaveBeenCalledTimes( + 2, + ); + }); + }); }); describe("allowResetPassword", () => { diff --git a/apps/web/src/app/admin-console/organizations/members/services/member-actions/member-actions.service.ts b/apps/web/src/app/admin-console/organizations/members/services/member-actions/member-actions.service.ts index 3b0db124a6b..e5f8c0c6673 100644 --- a/apps/web/src/app/admin-console/organizations/members/services/member-actions/member-actions.service.ts +++ b/apps/web/src/app/admin-console/organizations/members/services/member-actions/member-actions.service.ts @@ -1,5 +1,5 @@ -import { inject, Injectable, signal } from "@angular/core"; -import { lastValueFrom, firstValueFrom, switchMap } from "rxjs"; +import { inject, Injectable, signal, WritableSignal } from "@angular/core"; +import { lastValueFrom, firstValueFrom, switchMap, take } from "rxjs"; import { OrganizationUserApiService, @@ -23,11 +23,11 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { Utils } from "@bitwarden/common/platform/misc/utils"; import { DialogService } from "@bitwarden/components"; import { KeyService } from "@bitwarden/key-management"; -import { UserId } from "@bitwarden/user-core"; import { ProviderUser } from "@bitwarden/web-vault/app/admin-console/common/people-table-data-source"; import { OrganizationUserView } from "../../../core/views/organization-user.view"; import { UserConfirmComponent } from "../../../manage/user-confirm.component"; +import { MemberDialogManagerService } from "../member-dialog-manager/member-dialog-manager.service"; export const REQUESTS_PER_BATCH = 500; @@ -36,9 +36,13 @@ export interface MemberActionResult { error?: string; } -export interface BulkActionResult { - successful?: ListResponse; - failed: { id: string; error: string }[]; +export class BulkActionResult { + constructor() { + this.failed = []; + } + + successful?: OrganizationUserBulkResponse[]; + failed: { id: string; error: string }[] = []; } @Injectable() @@ -53,17 +57,28 @@ export class MemberActionsService { private logService = inject(LogService); private orgManagementPrefs = inject(OrganizationManagementPreferencesService); private userNamePipe = inject(UserNamePipe); + private memberDialogManager = inject(MemberDialogManagerService); readonly isProcessing = signal(false); - private startProcessing(): void { + private startProcessing(length?: number): void { this.isProcessing.set(true); + if (length != null && length > REQUESTS_PER_BATCH) { + this.memberDialogManager + .openBulkProgressDialog(this.progressCount, length) + .closed.pipe(take(1)) + .subscribe(() => { + this.progressCount.set(0); + }); + } } private endProcessing(): void { this.isProcessing.set(false); } + private readonly progressCount: WritableSignal = signal(0); + async inviteUser( organization: Organization, email: string, @@ -186,19 +201,42 @@ export class MemberActionsService { } } - async bulkReinvite(organization: Organization, userIds: UserId[]): Promise { - this.startProcessing(); + async bulkReinvite( + organization: Organization, + users: OrganizationUserView[], + ): Promise { + let result = new BulkActionResult(); + const bulkReinviteUIEnabled = await firstValueFrom( + this.configService.getFeatureFlag$(FeatureFlag.BulkReinviteUI), + ); + + if (bulkReinviteUIEnabled) { + this.startProcessing(users.length); + } else { + this.startProcessing(); + } + try { - return this.processBatchedOperation(userIds, REQUESTS_PER_BATCH, (batch) => - this.organizationUserApiService.postManyOrganizationUserReinvite(organization.id, batch), - ); + result = await this.processBatchedOperation(users, REQUESTS_PER_BATCH, (userBatch) => { + const userIds = userBatch.map((u) => u.id); + return this.organizationUserApiService.postManyOrganizationUserReinvite( + organization.id, + userIds, + ); + }); + + if (bulkReinviteUIEnabled && result.failed.length > 0) { + this.memberDialogManager.openBulkReinviteFailureDialog(organization, users, result); + } } catch (error) { - return { - failed: userIds.map((id) => ({ id, error: (error as Error).message ?? String(error) })), - }; + result.failed = users.map((user) => ({ + id: user.id, + error: (error as Error).message ?? String(error), + })); } finally { this.endProcessing(); } + return result; } allowResetPassword( @@ -235,21 +273,23 @@ export class MemberActionsService { /** * Processes user IDs in sequential batches and aggregates results. - * @param userIds - Array of user IDs to process + * @param users - Array of users to process * @param batchSize - Number of IDs to process per batch - * @param processBatch - Async function that processes a single batch and returns the result + * @param processBatch - Async function that processes a single batch from the provided param `users` and returns the result. * @returns Aggregated bulk action result */ private async processBatchedOperation( - userIds: UserId[], + users: OrganizationUserView[], batchSize: number, - processBatch: (batch: string[]) => Promise>, + processBatch: ( + batch: OrganizationUserView[], + ) => Promise>, ): Promise { const allSuccessful: OrganizationUserBulkResponse[] = []; const allFailed: { id: string; error: string }[] = []; - for (let i = 0; i < userIds.length; i += batchSize) { - const batch = userIds.slice(i, i + batchSize); + for (let i = 0; i < users.length; i += batchSize) { + const batch = users.slice(i, i + batchSize); try { const result = await processBatch(batch); @@ -265,18 +305,18 @@ export class MemberActionsService { } } catch (error) { allFailed.push( - ...batch.map((id) => ({ id, error: (error as Error).message ?? String(error) })), + ...batch.map((user) => ({ + id: user.id, + error: (error as Error).message ?? String(error), + })), ); } + + this.progressCount.update((value) => value + batch.length); } - const successful = - allSuccessful.length > 0 - ? new ListResponse(allSuccessful, OrganizationUserBulkResponse) - : undefined; - return { - successful, + successful: allSuccessful.length > 0 ? allSuccessful : undefined, failed: allFailed, }; } diff --git a/apps/web/src/app/admin-console/organizations/members/services/member-dialog-manager/member-dialog-manager.service.ts b/apps/web/src/app/admin-console/organizations/members/services/member-dialog-manager/member-dialog-manager.service.ts index c6ef536af2b..18106031fd0 100644 --- a/apps/web/src/app/admin-console/organizations/members/services/member-dialog-manager/member-dialog-manager.service.ts +++ b/apps/web/src/app/admin-console/organizations/members/services/member-dialog-manager/member-dialog-manager.service.ts @@ -1,4 +1,4 @@ -import { Injectable } from "@angular/core"; +import { Injectable, WritableSignal } from "@angular/core"; import { firstValueFrom, lastValueFrom } from "rxjs"; import { UserNamePipe } from "@bitwarden/angular/pipes/user-name.pipe"; @@ -7,7 +7,7 @@ import { ProductTierType } from "@bitwarden/common/billing/enums"; import { OrganizationBillingMetadataResponse } from "@bitwarden/common/billing/models/response/organization-billing-metadata.response"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { OrganizationId } from "@bitwarden/common/types/guid"; -import { DialogService, ToastService } from "@bitwarden/components"; +import { CenterPositionStrategy, DialogService, ToastService } from "@bitwarden/components"; import { OrganizationUserView } from "../../../core/views/organization-user.view"; import { openEntityEventsDialog } from "../../../manage/entity-events.component"; @@ -18,6 +18,8 @@ import { import { BulkConfirmDialogComponent } from "../../components/bulk/bulk-confirm-dialog.component"; import { BulkDeleteDialogComponent } from "../../components/bulk/bulk-delete-dialog.component"; import { BulkEnableSecretsManagerDialogComponent } from "../../components/bulk/bulk-enable-sm-dialog.component"; +import { BulkProgressDialogComponent } from "../../components/bulk/bulk-progress-dialog.component"; +import { BulkReinviteFailureDialogComponent } from "../../components/bulk/bulk-reinvite-failure-dialog.component"; import { BulkRemoveDialogComponent } from "../../components/bulk/bulk-remove-dialog.component"; import { BulkRestoreRevokeComponent } from "../../components/bulk/bulk-restore-revoke.component"; import { BulkStatusComponent } from "../../components/bulk/bulk-status.component"; @@ -27,6 +29,7 @@ import { openUserAddEditDialog, } from "../../components/member-dialog"; import { DeleteManagedMemberWarningService } from "../delete-managed-member/delete-managed-member-warning.service"; +import { BulkActionResult } from "../member-actions/member-actions.service"; @Injectable() export class MemberDialogManagerService { @@ -319,4 +322,33 @@ export class MemberDialogManagerService { type: "warning", }); } + + openBulkProgressDialog(progress: WritableSignal, allCount: number) { + return this.dialogService.open(BulkProgressDialogComponent, { + disableClose: true, + positionStrategy: new CenterPositionStrategy(), + data: { + progress, + allCount, + }, + }); + } + + openBulkReinviteFailureDialog( + organization: Organization, + users: OrganizationUserView[], + result: BulkActionResult, + ) { + return this.dialogService.open( + BulkReinviteFailureDialogComponent, + { + positionStrategy: new CenterPositionStrategy(), + data: { + organization, + users, + result, + }, + }, + ); + } } diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index fc2f463d9e6..1f4e1c98e32 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -6637,6 +6637,18 @@ } } }, + "reinviteSuccessToast":{ + "message": "1 invitation sent" + }, + "bulkReinviteSentToast": { + "message": "$COUNT$ invitations sent", + "placeholders": { + "count": { + "content": "$1", + "example": "12" + } + } + }, "bulkReinviteLimitedSuccessToast": { "message": "$LIMIT$ of $SELECTEDCOUNT$ users re-invited. $EXCLUDEDCOUNT$ were not invited due to the $LIMIT$ invite limit.", "placeholders": { @@ -6654,6 +6666,50 @@ } } }, + "bulkReinviteProgressTitle": { + "message": "$COUNT$ of $TOTAL$ invitations sent...", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkReinviteProgressSubtitle": { + "message": "Keep this page open until all are sent." + }, + "bulkReinviteFailuresTitle": { + "message": "$COUNT$ invitations didn't send", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + } + } + }, + "bulkReinviteFailureTitle": { + "message": "1 invitation didn't send" + }, + "bulkReinviteFailureDescription":{ + "message": "An error occurred while sending invitations to $COUNT$ of $TOTAL$ members. Try sending again, and if the problem continues,", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkResendInvitations": { + "message": "Try sending again" + }, "bulkRemovedMessage": { "message": "Removed successfully" }, diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/deprecated_members.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/deprecated_members.component.ts index e581bf458d2..1b1ae25c027 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/deprecated_members.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/deprecated_members.component.ts @@ -215,7 +215,10 @@ export class MembersComponent extends BaseMembersComponent { } else { this.toastService.showToast({ variant: "success", - message: this.i18nService.t("bulkReinviteSuccessToast", invitedCount.toString()), + message: + invitedCount === 1 + ? this.i18nService.t("reinviteSuccessToast") + : this.i18nService.t("bulkReinviteSentToast", invitedCount.toString()), }); } } else { diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/members.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/members.component.ts index 3efeee17100..c63bda449c5 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/members.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/members.component.ts @@ -228,7 +228,10 @@ export class vNextMembersComponent { } else { this.toastService.showToast({ variant: "success", - message: this.i18nService.t("bulkReinviteSuccessToast", invitedCount.toString()), + message: + invitedCount === 1 + ? this.i18nService.t("reinviteSuccessToast") + : this.i18nService.t("bulkReinviteSentToast", invitedCount.toString()), }); } } else { diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index 9941e7671f4..7722138b88f 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -15,6 +15,7 @@ export enum FeatureFlag { BlockClaimedDomainAccountCreation = "pm-28297-block-uninvited-claimed-domain-registration", DefaultUserCollectionRestore = "pm-30883-my-items-restored-users", MembersComponentRefactor = "pm-29503-refactor-members-inheritance", + BulkReinviteUI = "pm-28416-bulk-reinvite-ux-improvements", /* Auth */ PM23801_PrefetchPasswordPrelogin = "pm-23801-prefetch-password-prelogin", @@ -109,6 +110,7 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.BlockClaimedDomainAccountCreation]: FALSE, [FeatureFlag.DefaultUserCollectionRestore]: FALSE, [FeatureFlag.MembersComponentRefactor]: FALSE, + [FeatureFlag.BulkReinviteUI]: FALSE, /* Autofill */ [FeatureFlag.UseUndeterminedCipherScenarioTriggeringLogic]: FALSE, From 4fe29c71ce03391c8944a3fa479e41083348795c Mon Sep 17 00:00:00 2001 From: Nick Krantz <125900171+nick-livefront@users.noreply.github.com> Date: Tue, 10 Feb 2026 15:31:55 -0600 Subject: [PATCH 09/28] allow archiving organization ciphers in the cli (#18793) --- apps/cli/src/vault/archive.command.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/apps/cli/src/vault/archive.command.ts b/apps/cli/src/vault/archive.command.ts index 5ced2282c6d..0f634f78fb3 100644 --- a/apps/cli/src/vault/archive.command.ts +++ b/apps/cli/src/vault/archive.command.ts @@ -99,9 +99,6 @@ export class ArchiveCommand { errorMessage: "Item is in the trash, the item must be restored before archiving.", }; } - case cipher.organizationId != null: { - return { canArchive: false, errorMessage: "Cannot archive items in an organization." }; - } default: return { canArchive: true }; } From cc03df495013ef4fe79192e358237d63266d0c6c Mon Sep 17 00:00:00 2001 From: Jordan Aasen <166539328+jaasen-livefront@users.noreply.github.com> Date: Tue, 10 Feb 2026 13:39:53 -0800 Subject: [PATCH 10/28] [PM-17735] - remove v2 suffix from browser vault components (#18108) * remove v2 suffix * fix test * fix path * fix test * Fix missed -v2 import path * fix test --------- Co-authored-by: Shane --- apps/browser/src/popup/app-routing.module.ts | 44 +++++++------- .../add-edit/add-edit.component.html} | 0 .../add-edit/add-edit.component.spec.ts} | 12 ++-- .../add-edit/add-edit.component.ts} | 6 +- .../assign-collections.component.html | 0 .../assign-collections.component.ts | 0 .../attachments/attachments.component.html} | 0 .../attachments.component.spec.ts} | 14 ++--- .../attachments/attachments.component.ts} | 6 +- .../open-attachments.component.html | 0 .../open-attachments.component.spec.ts | 0 .../open-attachments.component.ts | 0 ...utofill-confirmation-dialog.component.html | 0 ...fill-confirmation-dialog.component.spec.ts | 0 .../autofill-confirmation-dialog.component.ts | 0 .../autofill-vault-list-items.component.html | 0 .../autofill-vault-list-items.component.ts | 0 .../blocked-injection-banner.component.html | 0 .../blocked-injection-banner.component.ts | 0 .../components/{vault-v2 => vault}/index.ts | 0 .../intro-carousel.component.html | 0 .../intro-carousel.component.ts | 0 .../item-copy-actions.component.html | 0 .../item-copy-actions.component.spec.ts | 0 .../item-copy-actions.component.ts | 0 .../item-more-options.component.html | 0 .../item-more-options.component.spec.ts | 0 .../item-more-options.component.ts | 2 +- .../new-item-dropdown.component.html} | 0 .../new-item-dropdown.component.spec.ts} | 14 ++--- .../new-item-dropdown.component.ts} | 6 +- .../vault-generator-dialog.component.html | 0 .../vault-generator-dialog.component.spec.ts | 0 .../vault-generator-dialog.component.ts | 0 .../vault-header/vault-header.component.html} | 2 +- .../vault-header.component.spec.ts} | 16 ++--- .../vault-header/vault-header.component.ts} | 12 ++-- .../vault-list-filters.component.html | 0 .../vault-list-filters.component.ts | 0 .../vault-list-items-container.component.html | 0 .../vault-list-items-container.component.ts | 0 .../vault-password-history.component.html} | 0 .../vault-password-history.component.spec.ts} | 10 ++-- .../vault-password-history.component.ts} | 6 +- .../vault-search/vault-search.component.html} | 0 .../vault-search.component.spec.ts} | 12 ++-- .../vault-search/vault-search.component.ts} | 6 +- .../vault.component.html} | 2 +- .../vault.component.spec.ts} | 58 +++++++++---------- .../vault.component.ts} | 14 ++--- .../view/view.component.html} | 0 .../view/view.component.spec.ts} | 14 ++--- .../view/view.component.ts} | 17 +++--- .../guards/clear-vault-state.guard.spec.ts | 8 +-- .../popup/guards/clear-vault-state.guard.ts | 6 +- .../browser-cipher-form-generation.service.ts | 2 +- ...mponent.html => appearance.component.html} | 0 ...t.spec.ts => appearance.component.spec.ts} | 14 ++--- ...2.component.ts => appearance.component.ts} | 4 +- .../vault/popup/settings/archive.component.ts | 2 +- ....component.html => folders.component.html} | 0 ...nent.spec.ts => folders.component.spec.ts} | 14 ++--- ...s-v2.component.ts => folders.component.ts} | 4 +- ...> more-from-bitwarden-page.component.html} | 0 ... => more-from-bitwarden-page.component.ts} | 4 +- ...ent.html => vault-settings.component.html} | 0 ...ec.ts => vault-settings.component.spec.ts} | 18 +++--- ...mponent.ts => vault-settings.component.ts} | 4 +- 68 files changed, 176 insertions(+), 177 deletions(-) rename apps/browser/src/vault/popup/components/{vault-v2/add-edit/add-edit-v2.component.html => vault/add-edit/add-edit.component.html} (100%) rename apps/browser/src/vault/popup/components/{vault-v2/add-edit/add-edit-v2.component.spec.ts => vault/add-edit/add-edit.component.spec.ts} (98%) rename apps/browser/src/vault/popup/components/{vault-v2/add-edit/add-edit-v2.component.ts => vault/add-edit/add-edit.component.ts} (99%) rename apps/browser/src/vault/popup/components/{vault-v2 => vault}/assign-collections/assign-collections.component.html (100%) rename apps/browser/src/vault/popup/components/{vault-v2 => vault}/assign-collections/assign-collections.component.ts (100%) rename apps/browser/src/vault/popup/components/{vault-v2/attachments/attachments-v2.component.html => vault/attachments/attachments.component.html} (100%) rename apps/browser/src/vault/popup/components/{vault-v2/attachments/attachments-v2.component.spec.ts => vault/attachments/attachments.component.spec.ts} (92%) rename apps/browser/src/vault/popup/components/{vault-v2/attachments/attachments-v2.component.ts => vault/attachments/attachments.component.ts} (94%) rename apps/browser/src/vault/popup/components/{vault-v2 => vault}/attachments/open-attachments/open-attachments.component.html (100%) rename apps/browser/src/vault/popup/components/{vault-v2 => vault}/attachments/open-attachments/open-attachments.component.spec.ts (100%) rename apps/browser/src/vault/popup/components/{vault-v2 => vault}/attachments/open-attachments/open-attachments.component.ts (100%) rename apps/browser/src/vault/popup/components/{vault-v2 => vault}/autofill-confirmation-dialog/autofill-confirmation-dialog.component.html (100%) rename apps/browser/src/vault/popup/components/{vault-v2 => vault}/autofill-confirmation-dialog/autofill-confirmation-dialog.component.spec.ts (100%) rename apps/browser/src/vault/popup/components/{vault-v2 => vault}/autofill-confirmation-dialog/autofill-confirmation-dialog.component.ts (100%) rename apps/browser/src/vault/popup/components/{vault-v2 => vault}/autofill-vault-list-items/autofill-vault-list-items.component.html (100%) rename apps/browser/src/vault/popup/components/{vault-v2 => vault}/autofill-vault-list-items/autofill-vault-list-items.component.ts (100%) rename apps/browser/src/vault/popup/components/{vault-v2 => vault}/blocked-injection-banner/blocked-injection-banner.component.html (100%) rename apps/browser/src/vault/popup/components/{vault-v2 => vault}/blocked-injection-banner/blocked-injection-banner.component.ts (100%) rename apps/browser/src/vault/popup/components/{vault-v2 => vault}/index.ts (100%) rename apps/browser/src/vault/popup/components/{vault-v2 => vault}/intro-carousel/intro-carousel.component.html (100%) rename apps/browser/src/vault/popup/components/{vault-v2 => vault}/intro-carousel/intro-carousel.component.ts (100%) rename apps/browser/src/vault/popup/components/{vault-v2 => vault}/item-copy-action/item-copy-actions.component.html (100%) rename apps/browser/src/vault/popup/components/{vault-v2 => vault}/item-copy-action/item-copy-actions.component.spec.ts (100%) rename apps/browser/src/vault/popup/components/{vault-v2 => vault}/item-copy-action/item-copy-actions.component.ts (100%) rename apps/browser/src/vault/popup/components/{vault-v2 => vault}/item-more-options/item-more-options.component.html (100%) rename apps/browser/src/vault/popup/components/{vault-v2 => vault}/item-more-options/item-more-options.component.spec.ts (100%) rename apps/browser/src/vault/popup/components/{vault-v2 => vault}/item-more-options/item-more-options.component.ts (99%) rename apps/browser/src/vault/popup/components/{vault-v2/new-item-dropdown/new-item-dropdown-v2.component.html => vault/new-item-dropdown/new-item-dropdown.component.html} (100%) rename apps/browser/src/vault/popup/components/{vault-v2/new-item-dropdown/new-item-dropdown-v2.component.spec.ts => vault/new-item-dropdown/new-item-dropdown.component.spec.ts} (93%) rename apps/browser/src/vault/popup/components/{vault-v2/new-item-dropdown/new-item-dropdown-v2.component.ts => vault/new-item-dropdown/new-item-dropdown.component.ts} (94%) rename apps/browser/src/vault/popup/components/{vault-v2 => vault}/vault-generator-dialog/vault-generator-dialog.component.html (100%) rename apps/browser/src/vault/popup/components/{vault-v2 => vault}/vault-generator-dialog/vault-generator-dialog.component.spec.ts (100%) rename apps/browser/src/vault/popup/components/{vault-v2 => vault}/vault-generator-dialog/vault-generator-dialog.component.ts (100%) rename apps/browser/src/vault/popup/components/{vault-v2/vault-header/vault-header-v2.component.html => vault/vault-header/vault-header.component.html} (95%) rename apps/browser/src/vault/popup/components/{vault-v2/vault-header/vault-header-v2.component.spec.ts => vault/vault-header/vault-header.component.spec.ts} (92%) rename apps/browser/src/vault/popup/components/{vault-v2/vault-header/vault-header-v2.component.ts => vault/vault-header/vault-header.component.ts} (88%) rename apps/browser/src/vault/popup/components/{vault-v2 => vault}/vault-list-filters/vault-list-filters.component.html (100%) rename apps/browser/src/vault/popup/components/{vault-v2 => vault}/vault-list-filters/vault-list-filters.component.ts (100%) rename apps/browser/src/vault/popup/components/{vault-v2 => vault}/vault-list-items-container/vault-list-items-container.component.html (100%) rename apps/browser/src/vault/popup/components/{vault-v2 => vault}/vault-list-items-container/vault-list-items-container.component.ts (100%) rename apps/browser/src/vault/popup/components/{vault-v2/vault-password-history-v2/vault-password-history-v2.component.html => vault/vault-password-history/vault-password-history.component.html} (100%) rename apps/browser/src/vault/popup/components/{vault-v2/vault-password-history-v2/vault-password-history-v2.component.spec.ts => vault/vault-password-history/vault-password-history.component.spec.ts} (90%) rename apps/browser/src/vault/popup/components/{vault-v2/vault-password-history-v2/vault-password-history-v2.component.ts => vault/vault-password-history/vault-password-history.component.ts} (94%) rename apps/browser/src/vault/popup/components/{vault-v2/vault-search/vault-v2-search.component.html => vault/vault-search/vault-search.component.html} (100%) rename apps/browser/src/vault/popup/components/{vault-v2/vault-search/vault-v2-search.component.spec.ts => vault/vault-search/vault-search.component.spec.ts} (89%) rename apps/browser/src/vault/popup/components/{vault-v2/vault-search/vault-v2-search.component.ts => vault/vault-search/vault-search.component.ts} (95%) rename apps/browser/src/vault/popup/components/{vault-v2/vault-v2.component.html => vault/vault.component.html} (99%) rename apps/browser/src/vault/popup/components/{vault-v2/vault-v2.component.spec.ts => vault/vault.component.spec.ts} (94%) rename apps/browser/src/vault/popup/components/{vault-v2/vault-v2.component.ts => vault/vault.component.ts} (97%) rename apps/browser/src/vault/popup/components/{vault-v2/view-v2/view-v2.component.html => vault/view/view.component.html} (100%) rename apps/browser/src/vault/popup/components/{vault-v2/view-v2/view-v2.component.spec.ts => vault/view/view.component.spec.ts} (98%) rename apps/browser/src/vault/popup/components/{vault-v2/view-v2/view-v2.component.ts => vault/view/view.component.ts} (96%) rename apps/browser/src/vault/popup/settings/{appearance-v2.component.html => appearance.component.html} (100%) rename apps/browser/src/vault/popup/settings/{appearance-v2.component.spec.ts => appearance.component.spec.ts} (95%) rename apps/browser/src/vault/popup/settings/{appearance-v2.component.ts => appearance.component.ts} (98%) rename apps/browser/src/vault/popup/settings/{folders-v2.component.html => folders.component.html} (100%) rename apps/browser/src/vault/popup/settings/{folders-v2.component.spec.ts => folders.component.spec.ts} (93%) rename apps/browser/src/vault/popup/settings/{folders-v2.component.ts => folders.component.ts} (96%) rename apps/browser/src/vault/popup/settings/{more-from-bitwarden-page-v2.component.html => more-from-bitwarden-page.component.html} (100%) rename apps/browser/src/vault/popup/settings/{more-from-bitwarden-page-v2.component.ts => more-from-bitwarden-page.component.ts} (97%) rename apps/browser/src/vault/popup/settings/{vault-settings-v2.component.html => vault-settings.component.html} (100%) rename apps/browser/src/vault/popup/settings/{vault-settings-v2.component.spec.ts => vault-settings.component.spec.ts} (93%) rename apps/browser/src/vault/popup/settings/{vault-settings-v2.component.ts => vault-settings.component.ts} (97%) diff --git a/apps/browser/src/popup/app-routing.module.ts b/apps/browser/src/popup/app-routing.module.ts index 7fb466449f2..4e14d1171fd 100644 --- a/apps/browser/src/popup/app-routing.module.ts +++ b/apps/browser/src/popup/app-routing.module.ts @@ -78,13 +78,13 @@ import { ExportBrowserV2Component } from "../tools/popup/settings/export/export- import { ImportBrowserV2Component } from "../tools/popup/settings/import/import-browser-v2.component"; import { SettingsV2Component } from "../tools/popup/settings/settings-v2.component"; import { AtRiskPasswordsComponent } from "../vault/popup/components/at-risk-passwords/at-risk-passwords.component"; -import { AddEditV2Component } from "../vault/popup/components/vault-v2/add-edit/add-edit-v2.component"; -import { AssignCollections } from "../vault/popup/components/vault-v2/assign-collections/assign-collections.component"; -import { AttachmentsV2Component } from "../vault/popup/components/vault-v2/attachments/attachments-v2.component"; -import { IntroCarouselComponent } from "../vault/popup/components/vault-v2/intro-carousel/intro-carousel.component"; -import { PasswordHistoryV2Component } from "../vault/popup/components/vault-v2/vault-password-history-v2/vault-password-history-v2.component"; -import { VaultV2Component } from "../vault/popup/components/vault-v2/vault-v2.component"; -import { ViewV2Component } from "../vault/popup/components/vault-v2/view-v2/view-v2.component"; +import { AddEditComponent } from "../vault/popup/components/vault/add-edit/add-edit.component"; +import { AssignCollections } from "../vault/popup/components/vault/assign-collections/assign-collections.component"; +import { AttachmentsComponent } from "../vault/popup/components/vault/attachments/attachments.component"; +import { IntroCarouselComponent } from "../vault/popup/components/vault/intro-carousel/intro-carousel.component"; +import { PasswordHistoryComponent } from "../vault/popup/components/vault/vault-password-history/vault-password-history.component"; +import { VaultComponent } from "../vault/popup/components/vault/vault.component"; +import { ViewComponent } from "../vault/popup/components/vault/view/view.component"; import { atRiskPasswordAuthGuard, canAccessAtRiskPasswords, @@ -93,13 +93,13 @@ import { import { clearVaultStateGuard } from "../vault/popup/guards/clear-vault-state.guard"; import { IntroCarouselGuard } from "../vault/popup/guards/intro-carousel.guard"; import { AdminSettingsComponent } from "../vault/popup/settings/admin-settings.component"; -import { AppearanceV2Component } from "../vault/popup/settings/appearance-v2.component"; +import { AppearanceComponent } from "../vault/popup/settings/appearance.component"; import { ArchiveComponent } from "../vault/popup/settings/archive.component"; import { DownloadBitwardenComponent } from "../vault/popup/settings/download-bitwarden.component"; -import { FoldersV2Component } from "../vault/popup/settings/folders-v2.component"; -import { MoreFromBitwardenPageV2Component } from "../vault/popup/settings/more-from-bitwarden-page-v2.component"; +import { FoldersComponent } from "../vault/popup/settings/folders.component"; +import { MoreFromBitwardenPageComponent } from "../vault/popup/settings/more-from-bitwarden-page.component"; import { TrashComponent } from "../vault/popup/settings/trash.component"; -import { VaultSettingsV2Component } from "../vault/popup/settings/vault-settings-v2.component"; +import { VaultSettingsComponent } from "../vault/popup/settings/vault-settings.component"; import { RouteElevation } from "./app-routing.animations"; import { @@ -214,7 +214,7 @@ const routes: Routes = [ }, { path: "view-cipher", - component: ViewV2Component, + component: ViewComponent, canActivate: [authGuard], data: { // Above "trash" @@ -223,20 +223,20 @@ const routes: Routes = [ }, { path: "cipher-password-history", - component: PasswordHistoryV2Component, + component: PasswordHistoryComponent, canActivate: [authGuard], data: { elevation: 4 } satisfies RouteDataProperties, }, { path: "add-cipher", - component: AddEditV2Component, + component: AddEditComponent, canActivate: [authGuard, debounceNavigationGuard()], data: { elevation: 1, resetRouterCacheOnTabChange: true } satisfies RouteDataProperties, runGuardsAndResolvers: "always", }, { path: "edit-cipher", - component: AddEditV2Component, + component: AddEditComponent, canActivate: [authGuard, debounceNavigationGuard()], data: { // Above "trash" @@ -247,7 +247,7 @@ const routes: Routes = [ }, { path: "attachments", - component: AttachmentsV2Component, + component: AttachmentsComponent, canActivate: [authGuard, filePickerPopoutGuard()], data: { elevation: 4 } satisfies RouteDataProperties, }, @@ -301,13 +301,13 @@ const routes: Routes = [ }, { path: "vault-settings", - component: VaultSettingsV2Component, + component: VaultSettingsComponent, canActivate: [authGuard], data: { elevation: 1 } satisfies RouteDataProperties, }, { path: "folders", - component: FoldersV2Component, + component: FoldersComponent, canActivate: [authGuard], data: { elevation: 2 } satisfies RouteDataProperties, }, @@ -331,7 +331,7 @@ const routes: Routes = [ }, { path: "appearance", - component: AppearanceV2Component, + component: AppearanceComponent, canActivate: [authGuard], data: { elevation: 1 } satisfies RouteDataProperties, }, @@ -343,7 +343,7 @@ const routes: Routes = [ }, { path: "clone-cipher", - component: AddEditV2Component, + component: AddEditComponent, canActivate: [authGuard], data: { elevation: 1 } satisfies RouteDataProperties, }, @@ -635,7 +635,7 @@ const routes: Routes = [ }, { path: "more-from-bitwarden", - component: MoreFromBitwardenPageV2Component, + component: MoreFromBitwardenPageComponent, canActivate: [authGuard], data: { elevation: 2 } satisfies RouteDataProperties, }, @@ -696,7 +696,7 @@ const routes: Routes = [ }, { path: "vault", - component: VaultV2Component, + component: VaultComponent, canActivate: [authGuard], canDeactivate: [clearVaultStateGuard], data: { elevation: 0 } satisfies RouteDataProperties, diff --git a/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.html b/apps/browser/src/vault/popup/components/vault/add-edit/add-edit.component.html similarity index 100% rename from apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.html rename to apps/browser/src/vault/popup/components/vault/add-edit/add-edit.component.html diff --git a/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.spec.ts b/apps/browser/src/vault/popup/components/vault/add-edit/add-edit.component.spec.ts similarity index 98% rename from apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.spec.ts rename to apps/browser/src/vault/popup/components/vault/add-edit/add-edit.component.spec.ts index 8ea23e7e2b9..bc785996104 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.spec.ts +++ b/apps/browser/src/vault/popup/components/vault/add-edit/add-edit.component.spec.ts @@ -40,16 +40,16 @@ import BrowserPopupUtils from "../../../../../platform/browser/browser-popup-uti import { PopupRouterCacheService } from "../../../../../platform/popup/view-cache/popup-router-cache.service"; import { PopupCloseWarningService } from "../../../../../popup/services/popup-close-warning.service"; -import { AddEditV2Component } from "./add-edit-v2.component"; +import { AddEditComponent } from "./add-edit.component"; // 'qrcode-parser' is used by `BrowserTotpCaptureService` but is an es6 module that jest can't compile. // Mock the entire module here to prevent jest from throwing an error. I wasn't able to find a way to mock the // `BrowserTotpCaptureService` where jest would not load the file in the first place. jest.mock("qrcode-parser", () => {}); -describe("AddEditV2Component", () => { - let component: AddEditV2Component; - let fixture: ComponentFixture; +describe("AddEditComponent", () => { + let component: AddEditComponent; + let fixture: ComponentFixture; let addEditCipherInfo$: BehaviorSubject; let cipherServiceMock: MockProxy; @@ -85,7 +85,7 @@ describe("AddEditV2Component", () => { }); await TestBed.configureTestingModule({ - imports: [AddEditV2Component], + imports: [AddEditComponent], providers: [ provideNoopAnimations(), { provide: PlatformUtilsService, useValue: mock() }, @@ -143,7 +143,7 @@ describe("AddEditV2Component", () => { }) .compileComponents(); - fixture = TestBed.createComponent(AddEditV2Component); + fixture = TestBed.createComponent(AddEditComponent); component = fixture.componentInstance; fixture.detectChanges(); }); diff --git a/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.ts b/apps/browser/src/vault/popup/components/vault/add-edit/add-edit.component.ts similarity index 99% rename from apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.ts rename to apps/browser/src/vault/popup/components/vault/add-edit/add-edit.component.ts index 895a5fe0cce..d8f4ccc3d0c 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.ts +++ b/apps/browser/src/vault/popup/components/vault/add-edit/add-edit.component.ts @@ -157,8 +157,8 @@ export type AddEditQueryParams = Partial>; // FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush // eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ - selector: "app-add-edit-v2", - templateUrl: "add-edit-v2.component.html", + selector: "app-add-edit", + templateUrl: "add-edit.component.html", providers: [ { provide: CipherFormConfigService, useClass: DefaultCipherFormConfigService }, { provide: TotpCaptureService, useClass: BrowserTotpCaptureService }, @@ -182,7 +182,7 @@ export type AddEditQueryParams = Partial>; BadgeModule, ], }) -export class AddEditV2Component implements OnInit, OnDestroy { +export class AddEditComponent implements OnInit, OnDestroy { readonly cipherFormComponent = viewChild(CipherFormComponent); headerText: string; config: CipherFormConfig; diff --git a/apps/browser/src/vault/popup/components/vault-v2/assign-collections/assign-collections.component.html b/apps/browser/src/vault/popup/components/vault/assign-collections/assign-collections.component.html similarity index 100% rename from apps/browser/src/vault/popup/components/vault-v2/assign-collections/assign-collections.component.html rename to apps/browser/src/vault/popup/components/vault/assign-collections/assign-collections.component.html diff --git a/apps/browser/src/vault/popup/components/vault-v2/assign-collections/assign-collections.component.ts b/apps/browser/src/vault/popup/components/vault/assign-collections/assign-collections.component.ts similarity index 100% rename from apps/browser/src/vault/popup/components/vault-v2/assign-collections/assign-collections.component.ts rename to apps/browser/src/vault/popup/components/vault/assign-collections/assign-collections.component.ts diff --git a/apps/browser/src/vault/popup/components/vault-v2/attachments/attachments-v2.component.html b/apps/browser/src/vault/popup/components/vault/attachments/attachments.component.html similarity index 100% rename from apps/browser/src/vault/popup/components/vault-v2/attachments/attachments-v2.component.html rename to apps/browser/src/vault/popup/components/vault/attachments/attachments.component.html diff --git a/apps/browser/src/vault/popup/components/vault-v2/attachments/attachments-v2.component.spec.ts b/apps/browser/src/vault/popup/components/vault/attachments/attachments.component.spec.ts similarity index 92% rename from apps/browser/src/vault/popup/components/vault-v2/attachments/attachments-v2.component.spec.ts rename to apps/browser/src/vault/popup/components/vault/attachments/attachments.component.spec.ts index d8f1d34ef9a..377b4fa27cc 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/attachments/attachments-v2.component.spec.ts +++ b/apps/browser/src/vault/popup/components/vault/attachments/attachments.component.spec.ts @@ -23,7 +23,7 @@ import { PopupFooterComponent } from "../../../../../platform/popup/layout/popup import { PopupHeaderComponent } from "../../../../../platform/popup/layout/popup-header.component"; import { PopupRouterCacheService } from "../../../../../platform/popup/view-cache/popup-router-cache.service"; -import { AttachmentsV2Component } from "./attachments-v2.component"; +import { AttachmentsComponent } from "./attachments.component"; @Component({ selector: "popup-header", @@ -44,9 +44,9 @@ class MockPopupFooterComponent { readonly pageTitle = input(); } -describe("AttachmentsV2Component", () => { - let component: AttachmentsV2Component; - let fixture: ComponentFixture; +describe("AttachmentsComponent", () => { + let component: AttachmentsComponent; + let fixture: ComponentFixture; const queryParams = new BehaviorSubject<{ cipherId: string }>({ cipherId: "5555-444-3333" }); let cipherAttachment: CipherAttachmentsComponent; const navigate = jest.fn(); @@ -60,7 +60,7 @@ describe("AttachmentsV2Component", () => { navigate.mockClear(); await TestBed.configureTestingModule({ - imports: [AttachmentsV2Component], + imports: [AttachmentsComponent], providers: [ { provide: LogService, useValue: mock() }, { provide: ConfigService, useValue: mock() }, @@ -83,7 +83,7 @@ describe("AttachmentsV2Component", () => { { provide: OrganizationService, useValue: mock() }, ], }) - .overrideComponent(AttachmentsV2Component, { + .overrideComponent(AttachmentsComponent, { remove: { imports: [PopupHeaderComponent, PopupFooterComponent], }, @@ -95,7 +95,7 @@ describe("AttachmentsV2Component", () => { }); beforeEach(() => { - fixture = TestBed.createComponent(AttachmentsV2Component); + fixture = TestBed.createComponent(AttachmentsComponent); component = fixture.componentInstance; fixture.detectChanges(); diff --git a/apps/browser/src/vault/popup/components/vault-v2/attachments/attachments-v2.component.ts b/apps/browser/src/vault/popup/components/vault/attachments/attachments.component.ts similarity index 94% rename from apps/browser/src/vault/popup/components/vault-v2/attachments/attachments-v2.component.ts rename to apps/browser/src/vault/popup/components/vault/attachments/attachments.component.ts index 29282d293de..63196edab30 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/attachments/attachments-v2.component.ts +++ b/apps/browser/src/vault/popup/components/vault/attachments/attachments.component.ts @@ -20,8 +20,8 @@ import { PopupRouterCacheService } from "../../../../../platform/popup/view-cach // FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush // eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ - selector: "app-attachments-v2", - templateUrl: "./attachments-v2.component.html", + selector: "app-attachments", + templateUrl: "./attachments.component.html", imports: [ CommonModule, ButtonModule, @@ -33,7 +33,7 @@ import { PopupRouterCacheService } from "../../../../../platform/popup/view-cach PopOutComponent, ], }) -export class AttachmentsV2Component { +export class AttachmentsComponent { /** The `id` tied to the underlying HTMLFormElement */ attachmentFormId = CipherAttachmentsComponent.attachmentFormID; diff --git a/apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.html b/apps/browser/src/vault/popup/components/vault/attachments/open-attachments/open-attachments.component.html similarity index 100% rename from apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.html rename to apps/browser/src/vault/popup/components/vault/attachments/open-attachments/open-attachments.component.html diff --git a/apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.spec.ts b/apps/browser/src/vault/popup/components/vault/attachments/open-attachments/open-attachments.component.spec.ts similarity index 100% rename from apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.spec.ts rename to apps/browser/src/vault/popup/components/vault/attachments/open-attachments/open-attachments.component.spec.ts diff --git a/apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.ts b/apps/browser/src/vault/popup/components/vault/attachments/open-attachments/open-attachments.component.ts similarity index 100% rename from apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.ts rename to apps/browser/src/vault/popup/components/vault/attachments/open-attachments/open-attachments.component.ts diff --git a/apps/browser/src/vault/popup/components/vault-v2/autofill-confirmation-dialog/autofill-confirmation-dialog.component.html b/apps/browser/src/vault/popup/components/vault/autofill-confirmation-dialog/autofill-confirmation-dialog.component.html similarity index 100% rename from apps/browser/src/vault/popup/components/vault-v2/autofill-confirmation-dialog/autofill-confirmation-dialog.component.html rename to apps/browser/src/vault/popup/components/vault/autofill-confirmation-dialog/autofill-confirmation-dialog.component.html diff --git a/apps/browser/src/vault/popup/components/vault-v2/autofill-confirmation-dialog/autofill-confirmation-dialog.component.spec.ts b/apps/browser/src/vault/popup/components/vault/autofill-confirmation-dialog/autofill-confirmation-dialog.component.spec.ts similarity index 100% rename from apps/browser/src/vault/popup/components/vault-v2/autofill-confirmation-dialog/autofill-confirmation-dialog.component.spec.ts rename to apps/browser/src/vault/popup/components/vault/autofill-confirmation-dialog/autofill-confirmation-dialog.component.spec.ts diff --git a/apps/browser/src/vault/popup/components/vault-v2/autofill-confirmation-dialog/autofill-confirmation-dialog.component.ts b/apps/browser/src/vault/popup/components/vault/autofill-confirmation-dialog/autofill-confirmation-dialog.component.ts similarity index 100% rename from apps/browser/src/vault/popup/components/vault-v2/autofill-confirmation-dialog/autofill-confirmation-dialog.component.ts rename to apps/browser/src/vault/popup/components/vault/autofill-confirmation-dialog/autofill-confirmation-dialog.component.ts diff --git a/apps/browser/src/vault/popup/components/vault-v2/autofill-vault-list-items/autofill-vault-list-items.component.html b/apps/browser/src/vault/popup/components/vault/autofill-vault-list-items/autofill-vault-list-items.component.html similarity index 100% rename from apps/browser/src/vault/popup/components/vault-v2/autofill-vault-list-items/autofill-vault-list-items.component.html rename to apps/browser/src/vault/popup/components/vault/autofill-vault-list-items/autofill-vault-list-items.component.html diff --git a/apps/browser/src/vault/popup/components/vault-v2/autofill-vault-list-items/autofill-vault-list-items.component.ts b/apps/browser/src/vault/popup/components/vault/autofill-vault-list-items/autofill-vault-list-items.component.ts similarity index 100% rename from apps/browser/src/vault/popup/components/vault-v2/autofill-vault-list-items/autofill-vault-list-items.component.ts rename to apps/browser/src/vault/popup/components/vault/autofill-vault-list-items/autofill-vault-list-items.component.ts diff --git a/apps/browser/src/vault/popup/components/vault-v2/blocked-injection-banner/blocked-injection-banner.component.html b/apps/browser/src/vault/popup/components/vault/blocked-injection-banner/blocked-injection-banner.component.html similarity index 100% rename from apps/browser/src/vault/popup/components/vault-v2/blocked-injection-banner/blocked-injection-banner.component.html rename to apps/browser/src/vault/popup/components/vault/blocked-injection-banner/blocked-injection-banner.component.html diff --git a/apps/browser/src/vault/popup/components/vault-v2/blocked-injection-banner/blocked-injection-banner.component.ts b/apps/browser/src/vault/popup/components/vault/blocked-injection-banner/blocked-injection-banner.component.ts similarity index 100% rename from apps/browser/src/vault/popup/components/vault-v2/blocked-injection-banner/blocked-injection-banner.component.ts rename to apps/browser/src/vault/popup/components/vault/blocked-injection-banner/blocked-injection-banner.component.ts diff --git a/apps/browser/src/vault/popup/components/vault-v2/index.ts b/apps/browser/src/vault/popup/components/vault/index.ts similarity index 100% rename from apps/browser/src/vault/popup/components/vault-v2/index.ts rename to apps/browser/src/vault/popup/components/vault/index.ts diff --git a/apps/browser/src/vault/popup/components/vault-v2/intro-carousel/intro-carousel.component.html b/apps/browser/src/vault/popup/components/vault/intro-carousel/intro-carousel.component.html similarity index 100% rename from apps/browser/src/vault/popup/components/vault-v2/intro-carousel/intro-carousel.component.html rename to apps/browser/src/vault/popup/components/vault/intro-carousel/intro-carousel.component.html diff --git a/apps/browser/src/vault/popup/components/vault-v2/intro-carousel/intro-carousel.component.ts b/apps/browser/src/vault/popup/components/vault/intro-carousel/intro-carousel.component.ts similarity index 100% rename from apps/browser/src/vault/popup/components/vault-v2/intro-carousel/intro-carousel.component.ts rename to apps/browser/src/vault/popup/components/vault/intro-carousel/intro-carousel.component.ts diff --git a/apps/browser/src/vault/popup/components/vault-v2/item-copy-action/item-copy-actions.component.html b/apps/browser/src/vault/popup/components/vault/item-copy-action/item-copy-actions.component.html similarity index 100% rename from apps/browser/src/vault/popup/components/vault-v2/item-copy-action/item-copy-actions.component.html rename to apps/browser/src/vault/popup/components/vault/item-copy-action/item-copy-actions.component.html diff --git a/apps/browser/src/vault/popup/components/vault-v2/item-copy-action/item-copy-actions.component.spec.ts b/apps/browser/src/vault/popup/components/vault/item-copy-action/item-copy-actions.component.spec.ts similarity index 100% rename from apps/browser/src/vault/popup/components/vault-v2/item-copy-action/item-copy-actions.component.spec.ts rename to apps/browser/src/vault/popup/components/vault/item-copy-action/item-copy-actions.component.spec.ts diff --git a/apps/browser/src/vault/popup/components/vault-v2/item-copy-action/item-copy-actions.component.ts b/apps/browser/src/vault/popup/components/vault/item-copy-action/item-copy-actions.component.ts similarity index 100% rename from apps/browser/src/vault/popup/components/vault-v2/item-copy-action/item-copy-actions.component.ts rename to apps/browser/src/vault/popup/components/vault/item-copy-action/item-copy-actions.component.ts diff --git a/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.html b/apps/browser/src/vault/popup/components/vault/item-more-options/item-more-options.component.html similarity index 100% rename from apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.html rename to apps/browser/src/vault/popup/components/vault/item-more-options/item-more-options.component.html diff --git a/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.spec.ts b/apps/browser/src/vault/popup/components/vault/item-more-options/item-more-options.component.spec.ts similarity index 100% rename from apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.spec.ts rename to apps/browser/src/vault/popup/components/vault/item-more-options/item-more-options.component.spec.ts diff --git a/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.ts b/apps/browser/src/vault/popup/components/vault/item-more-options/item-more-options.component.ts similarity index 99% rename from apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.ts rename to apps/browser/src/vault/popup/components/vault/item-more-options/item-more-options.component.ts index d7de51ad20f..8ed2699254e 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.ts +++ b/apps/browser/src/vault/popup/components/vault/item-more-options/item-more-options.component.ts @@ -35,7 +35,7 @@ import { PasswordRepromptService } from "@bitwarden/vault"; import { BrowserPremiumUpgradePromptService } from "../../../services/browser-premium-upgrade-prompt.service"; import { VaultPopupAutofillService } from "../../../services/vault-popup-autofill.service"; -import { AddEditQueryParams } from "../add-edit/add-edit-v2.component"; +import { AddEditQueryParams } from "../add-edit/add-edit.component"; import { AutofillConfirmationDialogComponent, AutofillConfirmationDialogResult, diff --git a/apps/browser/src/vault/popup/components/vault-v2/new-item-dropdown/new-item-dropdown-v2.component.html b/apps/browser/src/vault/popup/components/vault/new-item-dropdown/new-item-dropdown.component.html similarity index 100% rename from apps/browser/src/vault/popup/components/vault-v2/new-item-dropdown/new-item-dropdown-v2.component.html rename to apps/browser/src/vault/popup/components/vault/new-item-dropdown/new-item-dropdown.component.html diff --git a/apps/browser/src/vault/popup/components/vault-v2/new-item-dropdown/new-item-dropdown-v2.component.spec.ts b/apps/browser/src/vault/popup/components/vault/new-item-dropdown/new-item-dropdown.component.spec.ts similarity index 93% rename from apps/browser/src/vault/popup/components/vault-v2/new-item-dropdown/new-item-dropdown-v2.component.spec.ts rename to apps/browser/src/vault/popup/components/vault/new-item-dropdown/new-item-dropdown.component.spec.ts index 48e87e2d192..a20724e3160 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/new-item-dropdown/new-item-dropdown-v2.component.spec.ts +++ b/apps/browser/src/vault/popup/components/vault/new-item-dropdown/new-item-dropdown.component.spec.ts @@ -21,13 +21,13 @@ import { ButtonModule, DialogService, MenuModule, NoItemsModule } from "@bitward import { BrowserApi } from "../../../../../platform/browser/browser-api"; import BrowserPopupUtils from "../../../../../platform/browser/browser-popup-utils"; -import { NewItemDropdownV2Component, NewItemInitialValues } from "./new-item-dropdown-v2.component"; +import { NewItemDropdownComponent, NewItemInitialValues } from "./new-item-dropdown.component"; -describe("NewItemDropdownV2Component", () => { - let component: NewItemDropdownV2Component; - let fixture: ComponentFixture; +describe("NewItemDropdownComponent", () => { + let component: NewItemDropdownComponent; + let fixture: ComponentFixture; let dialogServiceMock: jest.Mocked; - let browserApiMock: jest.Mocked; + const browserApiMock: jest.Mocked = mock(); let restrictedItemTypesServiceMock: jest.Mocked; const mockTab = { url: "https://example.com" }; @@ -62,7 +62,7 @@ describe("NewItemDropdownV2Component", () => { ButtonModule, MenuModule, NoItemsModule, - NewItemDropdownV2Component, + NewItemDropdownComponent, ], providers: [ { provide: I18nService, useValue: { t: (key: string) => key } }, @@ -80,7 +80,7 @@ describe("NewItemDropdownV2Component", () => { }); beforeEach(() => { - fixture = TestBed.createComponent(NewItemDropdownV2Component); + fixture = TestBed.createComponent(NewItemDropdownComponent); component = fixture.componentInstance; fixture.detectChanges(); }); diff --git a/apps/browser/src/vault/popup/components/vault-v2/new-item-dropdown/new-item-dropdown-v2.component.ts b/apps/browser/src/vault/popup/components/vault/new-item-dropdown/new-item-dropdown.component.ts similarity index 94% rename from apps/browser/src/vault/popup/components/vault-v2/new-item-dropdown/new-item-dropdown-v2.component.ts rename to apps/browser/src/vault/popup/components/vault/new-item-dropdown/new-item-dropdown.component.ts index 004980db181..aa43743960b 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/new-item-dropdown/new-item-dropdown-v2.component.ts +++ b/apps/browser/src/vault/popup/components/vault/new-item-dropdown/new-item-dropdown.component.ts @@ -15,7 +15,7 @@ import { AddEditFolderDialogComponent } from "@bitwarden/vault"; import { BrowserApi } from "../../../../../platform/browser/browser-api"; import BrowserPopupUtils from "../../../../../platform/browser/browser-popup-utils"; -import { AddEditQueryParams } from "../add-edit/add-edit-v2.component"; +import { AddEditQueryParams } from "../add-edit/add-edit.component"; export interface NewItemInitialValues { folderId?: string; @@ -27,10 +27,10 @@ export interface NewItemInitialValues { // eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-new-item-dropdown", - templateUrl: "new-item-dropdown-v2.component.html", + templateUrl: "new-item-dropdown.component.html", imports: [NoItemsModule, JslibModule, CommonModule, ButtonModule, RouterLink, MenuModule], }) -export class NewItemDropdownV2Component implements OnInit { +export class NewItemDropdownComponent implements OnInit { cipherType = CipherType; private tab?: chrome.tabs.Tab; /** diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-generator-dialog/vault-generator-dialog.component.html b/apps/browser/src/vault/popup/components/vault/vault-generator-dialog/vault-generator-dialog.component.html similarity index 100% rename from apps/browser/src/vault/popup/components/vault-v2/vault-generator-dialog/vault-generator-dialog.component.html rename to apps/browser/src/vault/popup/components/vault/vault-generator-dialog/vault-generator-dialog.component.html diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-generator-dialog/vault-generator-dialog.component.spec.ts b/apps/browser/src/vault/popup/components/vault/vault-generator-dialog/vault-generator-dialog.component.spec.ts similarity index 100% rename from apps/browser/src/vault/popup/components/vault-v2/vault-generator-dialog/vault-generator-dialog.component.spec.ts rename to apps/browser/src/vault/popup/components/vault/vault-generator-dialog/vault-generator-dialog.component.spec.ts diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-generator-dialog/vault-generator-dialog.component.ts b/apps/browser/src/vault/popup/components/vault/vault-generator-dialog/vault-generator-dialog.component.ts similarity index 100% rename from apps/browser/src/vault/popup/components/vault-v2/vault-generator-dialog/vault-generator-dialog.component.ts rename to apps/browser/src/vault/popup/components/vault/vault-generator-dialog/vault-generator-dialog.component.ts diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-header/vault-header-v2.component.html b/apps/browser/src/vault/popup/components/vault/vault-header/vault-header.component.html similarity index 95% rename from apps/browser/src/vault/popup/components/vault-v2/vault-header/vault-header-v2.component.html rename to apps/browser/src/vault/popup/components/vault/vault-header/vault-header.component.html index 1ab162b56fb..09b4cb2b461 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/vault-header/vault-header-v2.component.html +++ b/apps/browser/src/vault/popup/components/vault/vault-header/vault-header.component.html @@ -1,6 +1,6 @@
- +
- + diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.spec.ts b/apps/browser/src/vault/popup/components/vault/vault.component.spec.ts similarity index 94% rename from apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.spec.ts rename to apps/browser/src/vault/popup/components/vault/vault.component.spec.ts index a956b2fe68b..55cb18ba637 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.spec.ts +++ b/apps/browser/src/vault/popup/components/vault/vault.component.spec.ts @@ -50,10 +50,10 @@ import { AtRiskPasswordCalloutComponent } from "../at-risk-callout/at-risk-passw import { AutofillVaultListItemsComponent } from "./autofill-vault-list-items/autofill-vault-list-items.component"; import { BlockedInjectionBanner } from "./blocked-injection-banner/blocked-injection-banner.component"; -import { NewItemDropdownV2Component } from "./new-item-dropdown/new-item-dropdown-v2.component"; -import { VaultHeaderV2Component } from "./vault-header/vault-header-v2.component"; +import { NewItemDropdownComponent } from "./new-item-dropdown/new-item-dropdown.component"; +import { VaultHeaderComponent } from "./vault-header/vault-header.component"; import { VaultListItemsContainerComponent } from "./vault-list-items-container/vault-list-items-container.component"; -import { VaultV2Component } from "./vault-v2.component"; +import { VaultComponent, VaultComponent } from "./vault.component"; @Component({ selector: "popup-header", @@ -66,12 +66,12 @@ export class PopupHeaderStubComponent { } @Component({ - selector: "app-vault-header-v2", + selector: "app-vault-header", standalone: true, template: "", changeDetection: ChangeDetectionStrategy.OnPush, }) -export class VaultHeaderV2StubComponent {} +export class VaultHeaderStubComponent {} @Component({ selector: "app-current-account", @@ -158,8 +158,8 @@ const autoConfirmDialogSpy = jest jest.spyOn(BrowserApi, "isPopupOpen").mockResolvedValue(false); jest.spyOn(BrowserPopupUtils, "openCurrentPagePopout").mockResolvedValue(); -describe("VaultV2Component", () => { - let component: VaultV2Component; +describe("VaultComponent", () => { + let component: VaultComponent; interface FakeAccount { id: string; @@ -242,7 +242,7 @@ describe("VaultV2Component", () => { beforeEach(async () => { jest.clearAllMocks(); await TestBed.configureTestingModule({ - imports: [VaultV2Component, RouterTestingModule], + imports: [VaultComponent, RouterTestingModule], providers: [ provideNoopAnimations(), { provide: VaultPopupItemsService, useValue: itemsSvc }, @@ -298,13 +298,13 @@ describe("VaultV2Component", () => { schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); - TestBed.overrideComponent(VaultV2Component, { + TestBed.overrideComponent(VaultComponent, { remove: { imports: [ PopupHeaderComponent, - VaultHeaderV2Component, + VaultHeaderComponent, CurrentAccountComponent, - NewItemDropdownV2Component, + NewItemDropdownComponent, PopOutComponent, BlockedInjectionBanner, AtRiskPasswordCalloutComponent, @@ -318,7 +318,7 @@ describe("VaultV2Component", () => { add: { imports: [ PopupHeaderStubComponent, - VaultHeaderV2StubComponent, + VaultHeaderStubComponent, CurrentAccountStubComponent, NewItemDropdownStubComponent, PopOutStubComponent, @@ -331,7 +331,7 @@ describe("VaultV2Component", () => { }, }); - const fixture = TestBed.createComponent(VaultV2Component); + const fixture = TestBed.createComponent(VaultComponent); component = fixture.componentInstance; }); @@ -393,7 +393,7 @@ describe("VaultV2Component", () => { }); it("passes popup-page scroll region element to scroll position service", fakeAsync(() => { - const fixture = TestBed.createComponent(VaultV2Component); + const fixture = TestBed.createComponent(VaultComponent); const component = fixture.componentInstance; const readySubject$ = component["readySubject"] as unknown as BehaviorSubject; @@ -491,7 +491,7 @@ describe("VaultV2Component", () => { of(type === NudgeType.PremiumUpgrade), ); - const fixture = TestBed.createComponent(VaultV2Component); + const fixture = TestBed.createComponent(VaultComponent); const component = fixture.componentInstance; void component.ngOnInit(); @@ -524,7 +524,7 @@ describe("VaultV2Component", () => { return of(type === NudgeType.EmptyVaultNudge); }); - const fixture = TestBed.createComponent(VaultV2Component); + const fixture = TestBed.createComponent(VaultComponent); fixture.detectChanges(); tick(); @@ -541,7 +541,7 @@ describe("VaultV2Component", () => { return of(type === NudgeType.HasVaultItems); }); - const fixture = TestBed.createComponent(VaultV2Component); + const fixture = TestBed.createComponent(VaultComponent); fixture.detectChanges(); tick(); @@ -559,7 +559,7 @@ describe("VaultV2Component", () => { return of(type === NudgeType.PremiumUpgrade); }); - const fixture = TestBed.createComponent(VaultV2Component); + const fixture = TestBed.createComponent(VaultComponent); fixture.detectChanges(); tick(); @@ -575,7 +575,7 @@ describe("VaultV2Component", () => { return of(type === NudgeType.PremiumUpgrade); }); - const fixture = TestBed.createComponent(VaultV2Component); + const fixture = TestBed.createComponent(VaultComponent); fixture.detectChanges(); tick(); @@ -591,7 +591,7 @@ describe("VaultV2Component", () => { return of(type === NudgeType.PremiumUpgrade); }); - const fixture = TestBed.createComponent(VaultV2Component); + const fixture = TestBed.createComponent(VaultComponent); fixture.detectChanges(); tick(); @@ -602,7 +602,7 @@ describe("VaultV2Component", () => { it("does not render app-autofill-vault-list-items or favorites item container when hasSearchText$ is true", () => { itemsSvc.hasSearchText$.next(true); - const fixture = TestBed.createComponent(VaultV2Component); + const fixture = TestBed.createComponent(VaultComponent); component = fixture.componentInstance; const readySubject$ = component["readySubject"]; @@ -628,7 +628,7 @@ describe("VaultV2Component", () => { itemsSvc.hasSearchText$.next(false); loadingSvc.loading$.next(false); - const fixture = TestBed.createComponent(VaultV2Component); + const fixture = TestBed.createComponent(VaultComponent); component = fixture.componentInstance; const readySubject$ = component["readySubject"]; @@ -655,7 +655,7 @@ describe("VaultV2Component", () => { filtersSvc.numberOfAppliedFilters$.next(0); loadingSvc.loading$.next(false); - const fixture = TestBed.createComponent(VaultV2Component); + const fixture = TestBed.createComponent(VaultComponent); component = fixture.componentInstance; const readySubject$ = component["readySubject"]; @@ -679,7 +679,7 @@ describe("VaultV2Component", () => { itemsSvc.hasSearchText$.next(true); loadingSvc.loading$.next(false); - const fixture = TestBed.createComponent(VaultV2Component); + const fixture = TestBed.createComponent(VaultComponent); component = fixture.componentInstance; const readySubject$ = component["readySubject"]; @@ -704,7 +704,7 @@ describe("VaultV2Component", () => { filtersSvc.numberOfAppliedFilters$.next(1); loadingSvc.loading$.next(false); - const fixture = TestBed.createComponent(VaultV2Component); + const fixture = TestBed.createComponent(VaultComponent); component = fixture.componentInstance; const readySubject$ = component["readySubject"]; @@ -735,7 +735,7 @@ describe("VaultV2Component", () => { }), ); - const fixture = TestBed.createComponent(VaultV2Component); + const fixture = TestBed.createComponent(VaultComponent); const component = fixture.componentInstance; void component.ngOnInit(); @@ -754,7 +754,7 @@ describe("VaultV2Component", () => { }), ); - const fixture = TestBed.createComponent(VaultV2Component); + const fixture = TestBed.createComponent(VaultComponent); const component = fixture.componentInstance; void component.ngOnInit(); @@ -773,7 +773,7 @@ describe("VaultV2Component", () => { }), ); - const fixture = TestBed.createComponent(VaultV2Component); + const fixture = TestBed.createComponent(VaultComponent); const component = fixture.componentInstance; void component.ngOnInit(); @@ -792,7 +792,7 @@ describe("VaultV2Component", () => { }), ); - const fixture = TestBed.createComponent(VaultV2Component); + const fixture = TestBed.createComponent(VaultComponent); const component = fixture.componentInstance; void component.ngOnInit(); diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.ts b/apps/browser/src/vault/popup/components/vault/vault.component.ts similarity index 97% rename from apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.ts rename to apps/browser/src/vault/popup/components/vault/vault.component.ts index a5a74eb8ab8..281abc5f180 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.ts +++ b/apps/browser/src/vault/popup/components/vault/vault.component.ts @@ -71,10 +71,10 @@ import { VaultLoadingSkeletonComponent } from "../vault-loading-skeleton/vault-l import { BlockedInjectionBanner } from "./blocked-injection-banner/blocked-injection-banner.component"; import { - NewItemDropdownV2Component, + NewItemDropdownComponent, NewItemInitialValues, -} from "./new-item-dropdown/new-item-dropdown-v2.component"; -import { VaultHeaderV2Component } from "./vault-header/vault-header-v2.component"; +} from "./new-item-dropdown/new-item-dropdown.component"; +import { VaultHeaderComponent } from "./vault-header/vault-header.component"; import { AutofillVaultListItemsComponent, VaultListItemsContainerComponent } from "."; @@ -90,7 +90,7 @@ type VaultState = UnionOfValues; // eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-vault", - templateUrl: "vault-v2.component.html", + templateUrl: "vault.component.html", imports: [ BlockedInjectionBanner, PopupPageComponent, @@ -103,9 +103,9 @@ type VaultState = UnionOfValues; AutofillVaultListItemsComponent, VaultListItemsContainerComponent, ButtonModule, - NewItemDropdownV2Component, + NewItemDropdownComponent, ScrollingModule, - VaultHeaderV2Component, + VaultHeaderComponent, AtRiskPasswordCalloutComponent, SpotlightComponent, RouterModule, @@ -116,7 +116,7 @@ type VaultState = UnionOfValues; ], providers: [{ provide: VaultItemsTransferService, useClass: DefaultVaultItemsTransferService }], }) -export class VaultV2Component implements OnInit, OnDestroy { +export class VaultComponent implements OnInit, OnDestroy { NudgeType = NudgeType; cipherType = CipherType; private activeUserId$ = this.accountService.activeAccount$.pipe(getUserId); diff --git a/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.html b/apps/browser/src/vault/popup/components/vault/view/view.component.html similarity index 100% rename from apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.html rename to apps/browser/src/vault/popup/components/vault/view/view.component.html diff --git a/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.spec.ts b/apps/browser/src/vault/popup/components/vault/view/view.component.spec.ts similarity index 98% rename from apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.spec.ts rename to apps/browser/src/vault/popup/components/vault/view/view.component.spec.ts index 7e57cd69ba1..5c94af0205d 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.spec.ts +++ b/apps/browser/src/vault/popup/components/vault/view/view.component.spec.ts @@ -45,19 +45,19 @@ import { import { BrowserApi } from "../../../../../platform/browser/browser-api"; import BrowserPopupUtils from "../../../../../platform/browser/browser-popup-utils"; import { PopupRouterCacheService } from "../../../../../platform/popup/view-cache/popup-router-cache.service"; +import { VaultPopupAutofillService } from "../../../services/vault-popup-autofill.service"; import { VaultPopupScrollPositionService } from "../../../services/vault-popup-scroll-position.service"; -import { VaultPopupAutofillService } from "./../../../services/vault-popup-autofill.service"; -import { ViewV2Component } from "./view-v2.component"; +import { ViewComponent } from "./view.component"; // 'qrcode-parser' is used by `BrowserTotpCaptureService` but is an es6 module that jest can't compile. // Mock the entire module here to prevent jest from throwing an error. I wasn't able to find a way to mock the // `BrowserTotpCaptureService` where jest would not load the file in the first place. jest.mock("qrcode-parser", () => {}); -describe("ViewV2Component", () => { - let component: ViewV2Component; - let fixture: ComponentFixture; +describe("ViewComponent", () => { + let component: ViewComponent; + let fixture: ComponentFixture; const params$ = new Subject(); const mockNavigate = jest.fn(); const collect = jest.fn().mockResolvedValue(null); @@ -124,7 +124,7 @@ describe("ViewV2Component", () => { cipherArchiveService.unarchiveWithServer.mockResolvedValue({ id: "122-333-444" } as CipherData); await TestBed.configureTestingModule({ - imports: [ViewV2Component], + imports: [ViewComponent], providers: [ { provide: Router, useValue: { navigate: mockNavigate } }, { provide: CipherService, useValue: mockCipherService }, @@ -231,7 +231,7 @@ describe("ViewV2Component", () => { }) .compileComponents(); - fixture = TestBed.createComponent(ViewV2Component); + fixture = TestBed.createComponent(ViewComponent); component = fixture.componentInstance; fixture.detectChanges(); (component as any).showFooter$ = of(true); diff --git a/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.ts b/apps/browser/src/vault/popup/components/vault/view/view.component.ts similarity index 96% rename from apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.ts rename to apps/browser/src/vault/popup/components/vault/view/view.component.ts index f57b3e2d7f1..d63cd5920a1 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.ts +++ b/apps/browser/src/vault/popup/components/vault/view/view.component.ts @@ -56,17 +56,16 @@ import { sendExtensionMessage } from "../../../../../autofill/utils/index"; import { BrowserApi } from "../../../../../platform/browser/browser-api"; import BrowserPopupUtils from "../../../../../platform/browser/browser-popup-utils"; import { PopOutComponent } from "../../../../../platform/popup/components/pop-out.component"; +import { PopupFooterComponent } from "../../../../../platform/popup/layout/popup-footer.component"; +import { PopupHeaderComponent } from "../../../../../platform/popup/layout/popup-header.component"; +import { PopupPageComponent } from "../../../../../platform/popup/layout/popup-page.component"; import { PopupRouterCacheService } from "../../../../../platform/popup/view-cache/popup-router-cache.service"; import { BrowserPremiumUpgradePromptService } from "../../../services/browser-premium-upgrade-prompt.service"; import { BrowserViewPasswordHistoryService } from "../../../services/browser-view-password-history.service"; +import { VaultPopupAutofillService } from "../../../services/vault-popup-autofill.service"; import { VaultPopupScrollPositionService } from "../../../services/vault-popup-scroll-position.service"; import { closeViewVaultItemPopout, VaultPopoutType } from "../../../utils/vault-popout-window"; -import { ROUTES_AFTER_EDIT_DELETION } from "../add-edit/add-edit-v2.component"; - -import { PopupFooterComponent } from "./../../../../../platform/popup/layout/popup-footer.component"; -import { PopupHeaderComponent } from "./../../../../../platform/popup/layout/popup-header.component"; -import { PopupPageComponent } from "./../../../../../platform/popup/layout/popup-page.component"; -import { VaultPopupAutofillService } from "./../../../services/vault-popup-autofill.service"; +import { ROUTES_AFTER_EDIT_DELETION } from "../add-edit/add-edit.component"; /** * The types of actions that can be triggered when loading the view vault item popout via the @@ -83,8 +82,8 @@ type LoadAction = // FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush // eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ - selector: "app-view-v2", - templateUrl: "view-v2.component.html", + selector: "app-view", + templateUrl: "view.component.html", imports: [ CommonModule, SearchModule, @@ -107,7 +106,7 @@ type LoadAction = { provide: ChangeLoginPasswordService, useClass: DefaultChangeLoginPasswordService }, ], }) -export class ViewV2Component { +export class ViewComponent { private activeUserId: UserId; headerText: string; diff --git a/apps/browser/src/vault/popup/guards/clear-vault-state.guard.spec.ts b/apps/browser/src/vault/popup/guards/clear-vault-state.guard.spec.ts index 7ead8576b37..633a3b3295e 100644 --- a/apps/browser/src/vault/popup/guards/clear-vault-state.guard.spec.ts +++ b/apps/browser/src/vault/popup/guards/clear-vault-state.guard.spec.ts @@ -1,7 +1,7 @@ import { TestBed } from "@angular/core/testing"; import { RouterStateSnapshot } from "@angular/router"; -import { VaultV2Component } from "../components/vault-v2/vault-v2.component"; +import { VaultComponent } from "../components/vault/vault.component"; import { VaultPopupItemsService } from "../services/vault-popup-items.service"; import { VaultPopupListFiltersService } from "../services/vault-popup-list-filters.service"; @@ -42,7 +42,7 @@ describe("clearVaultStateGuard", () => { const nextState = { url } as RouterStateSnapshot; const result = TestBed.runInInjectionContext(() => - clearVaultStateGuard({} as VaultV2Component, null, null, nextState), + clearVaultStateGuard({} as VaultComponent, null, null, nextState), ); expect(result).toBe(true); @@ -56,7 +56,7 @@ describe("clearVaultStateGuard", () => { const nextState = { url } as RouterStateSnapshot; const result = TestBed.runInInjectionContext(() => - clearVaultStateGuard({} as VaultV2Component, null, null, nextState), + clearVaultStateGuard({} as VaultComponent, null, null, nextState), ); expect(result).toBe(true); @@ -67,7 +67,7 @@ describe("clearVaultStateGuard", () => { it("should not clear vault state when not changing states", () => { const result = TestBed.runInInjectionContext(() => - clearVaultStateGuard({} as VaultV2Component, null, null, null), + clearVaultStateGuard({} as VaultComponent, null, null, null), ); expect(result).toBe(true); diff --git a/apps/browser/src/vault/popup/guards/clear-vault-state.guard.ts b/apps/browser/src/vault/popup/guards/clear-vault-state.guard.ts index 2a87db6e903..5258c7cd741 100644 --- a/apps/browser/src/vault/popup/guards/clear-vault-state.guard.ts +++ b/apps/browser/src/vault/popup/guards/clear-vault-state.guard.ts @@ -1,7 +1,7 @@ import { inject } from "@angular/core"; import { CanDeactivateFn } from "@angular/router"; -import { VaultV2Component } from "../components/vault-v2/vault-v2.component"; +import { VaultComponent } from "../components/vault/vault.component"; import { VaultPopupItemsService } from "../services/vault-popup-items.service"; import { VaultPopupListFiltersService } from "../services/vault-popup-list-filters.service"; @@ -10,8 +10,8 @@ import { VaultPopupListFiltersService } from "../services/vault-popup-list-filte * This ensures the search and filter state is reset when navigating between different tabs, * except viewing or editing a cipher. */ -export const clearVaultStateGuard: CanDeactivateFn = ( - component: VaultV2Component, +export const clearVaultStateGuard: CanDeactivateFn = ( + component: VaultComponent, currentRoute, currentState, nextState, diff --git a/apps/browser/src/vault/popup/services/browser-cipher-form-generation.service.ts b/apps/browser/src/vault/popup/services/browser-cipher-form-generation.service.ts index ecba9aa1413..9e67953c251 100644 --- a/apps/browser/src/vault/popup/services/browser-cipher-form-generation.service.ts +++ b/apps/browser/src/vault/popup/services/browser-cipher-form-generation.service.ts @@ -7,7 +7,7 @@ import { firstValueFrom } from "rxjs"; import { DialogService } from "@bitwarden/components"; import { CipherFormGenerationService } from "@bitwarden/vault"; -import { VaultGeneratorDialogComponent } from "../components/vault-v2/vault-generator-dialog/vault-generator-dialog.component"; +import { VaultGeneratorDialogComponent } from "../components/vault/vault-generator-dialog/vault-generator-dialog.component"; @Injectable() export class BrowserCipherFormGenerationService implements CipherFormGenerationService { diff --git a/apps/browser/src/vault/popup/settings/appearance-v2.component.html b/apps/browser/src/vault/popup/settings/appearance.component.html similarity index 100% rename from apps/browser/src/vault/popup/settings/appearance-v2.component.html rename to apps/browser/src/vault/popup/settings/appearance.component.html diff --git a/apps/browser/src/vault/popup/settings/appearance-v2.component.spec.ts b/apps/browser/src/vault/popup/settings/appearance.component.spec.ts similarity index 95% rename from apps/browser/src/vault/popup/settings/appearance-v2.component.spec.ts rename to apps/browser/src/vault/popup/settings/appearance.component.spec.ts index 9e1beab5787..41e89ec30e8 100644 --- a/apps/browser/src/vault/popup/settings/appearance-v2.component.spec.ts +++ b/apps/browser/src/vault/popup/settings/appearance.component.spec.ts @@ -20,7 +20,7 @@ import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.co import { PopupSizeService } from "../../../platform/popup/layout/popup-size.service"; import { VaultPopupCopyButtonsService } from "../services/vault-popup-copy-buttons.service"; -import { AppearanceV2Component } from "./appearance-v2.component"; +import { AppearanceComponent } from "./appearance.component"; // FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush // eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @@ -49,9 +49,9 @@ class MockPopupPageComponent { @Input() loading: boolean; } -describe("AppearanceV2Component", () => { - let component: AppearanceV2Component; - let fixture: ComponentFixture; +describe("AppearanceComponent", () => { + let component: AppearanceComponent; + let fixture: ComponentFixture; const showFavicons$ = new BehaviorSubject(true); const enableBadgeCounter$ = new BehaviorSubject(true); @@ -80,7 +80,7 @@ describe("AppearanceV2Component", () => { setEnableRoutingAnimation.mockClear(); await TestBed.configureTestingModule({ - imports: [AppearanceV2Component], + imports: [AppearanceComponent], providers: [ { provide: ConfigService, useValue: mock() }, { provide: PlatformUtilsService, useValue: mock() }, @@ -120,7 +120,7 @@ describe("AppearanceV2Component", () => { }, ], }) - .overrideComponent(AppearanceV2Component, { + .overrideComponent(AppearanceComponent, { remove: { imports: [PopupHeaderComponent, PopupPageComponent], }, @@ -130,7 +130,7 @@ describe("AppearanceV2Component", () => { }) .compileComponents(); - fixture = TestBed.createComponent(AppearanceV2Component); + fixture = TestBed.createComponent(AppearanceComponent); component = fixture.componentInstance; fixture.detectChanges(); }); diff --git a/apps/browser/src/vault/popup/settings/appearance-v2.component.ts b/apps/browser/src/vault/popup/settings/appearance.component.ts similarity index 98% rename from apps/browser/src/vault/popup/settings/appearance-v2.component.ts rename to apps/browser/src/vault/popup/settings/appearance.component.ts index e02ccf25f3e..bff51335192 100644 --- a/apps/browser/src/vault/popup/settings/appearance-v2.component.ts +++ b/apps/browser/src/vault/popup/settings/appearance.component.ts @@ -36,7 +36,7 @@ import { VaultPopupCopyButtonsService } from "../services/vault-popup-copy-butto // FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush // eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ - templateUrl: "./appearance-v2.component.html", + templateUrl: "./appearance.component.html", imports: [ CommonModule, JslibModule, @@ -52,7 +52,7 @@ import { VaultPopupCopyButtonsService } from "../services/vault-popup-copy-butto PermitCipherDetailsPopoverComponent, ], }) -export class AppearanceV2Component implements OnInit { +export class AppearanceComponent implements OnInit { private compactModeService = inject(PopupCompactModeService); private copyButtonsService = inject(VaultPopupCopyButtonsService); private popupSizeService = inject(PopupSizeService); diff --git a/apps/browser/src/vault/popup/settings/archive.component.ts b/apps/browser/src/vault/popup/settings/archive.component.ts index a34609bd8f8..336d9be6d16 100644 --- a/apps/browser/src/vault/popup/settings/archive.component.ts +++ b/apps/browser/src/vault/popup/settings/archive.component.ts @@ -42,7 +42,7 @@ import { import { PopOutComponent } from "../../../platform/popup/components/pop-out.component"; import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component"; import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component"; -import { ROUTES_AFTER_EDIT_DELETION } from "../components/vault-v2/add-edit/add-edit-v2.component"; +import { ROUTES_AFTER_EDIT_DELETION } from "../components/vault/add-edit/add-edit.component"; // FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush // eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection diff --git a/apps/browser/src/vault/popup/settings/folders-v2.component.html b/apps/browser/src/vault/popup/settings/folders.component.html similarity index 100% rename from apps/browser/src/vault/popup/settings/folders-v2.component.html rename to apps/browser/src/vault/popup/settings/folders.component.html diff --git a/apps/browser/src/vault/popup/settings/folders-v2.component.spec.ts b/apps/browser/src/vault/popup/settings/folders.component.spec.ts similarity index 93% rename from apps/browser/src/vault/popup/settings/folders-v2.component.spec.ts rename to apps/browser/src/vault/popup/settings/folders.component.spec.ts index 3cb5503ed89..678e6d3f10e 100644 --- a/apps/browser/src/vault/popup/settings/folders-v2.component.spec.ts +++ b/apps/browser/src/vault/popup/settings/folders.component.spec.ts @@ -19,7 +19,7 @@ import { AddEditFolderDialogComponent } from "@bitwarden/vault"; import { PopupFooterComponent } from "../../../platform/popup/layout/popup-footer.component"; import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component"; -import { FoldersV2Component } from "./folders-v2.component"; +import { FoldersComponent } from "./folders.component"; // FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush // eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @@ -48,9 +48,9 @@ class MockPopupFooterComponent { @Input() pageTitle: string = ""; } -describe("FoldersV2Component", () => { - let component: FoldersV2Component; - let fixture: ComponentFixture; +describe("FoldersComponent", () => { + let component: FoldersComponent; + let fixture: ComponentFixture; const folderViews$ = new BehaviorSubject([]); const open = jest.spyOn(AddEditFolderDialogComponent, "open"); const mockDialogService = { open: jest.fn() }; @@ -59,7 +59,7 @@ describe("FoldersV2Component", () => { open.mockClear(); await TestBed.configureTestingModule({ - imports: [FoldersV2Component], + imports: [FoldersComponent], providers: [ { provide: PlatformUtilsService, useValue: mock() }, { provide: ConfigService, useValue: mock() }, @@ -69,7 +69,7 @@ describe("FoldersV2Component", () => { { provide: AccountService, useValue: mockAccountServiceWith("UserId" as UserId) }, ], }) - .overrideComponent(FoldersV2Component, { + .overrideComponent(FoldersComponent, { remove: { imports: [PopupHeaderComponent, PopupFooterComponent], }, @@ -80,7 +80,7 @@ describe("FoldersV2Component", () => { .overrideProvider(DialogService, { useValue: mockDialogService }) .compileComponents(); - fixture = TestBed.createComponent(FoldersV2Component); + fixture = TestBed.createComponent(FoldersComponent); component = fixture.componentInstance; fixture.detectChanges(); }); diff --git a/apps/browser/src/vault/popup/settings/folders-v2.component.ts b/apps/browser/src/vault/popup/settings/folders.component.ts similarity index 96% rename from apps/browser/src/vault/popup/settings/folders-v2.component.ts rename to apps/browser/src/vault/popup/settings/folders.component.ts index 20a816e7297..b70c17bd6a5 100644 --- a/apps/browser/src/vault/popup/settings/folders-v2.component.ts +++ b/apps/browser/src/vault/popup/settings/folders.component.ts @@ -25,7 +25,7 @@ import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.co // FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush // eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ - templateUrl: "./folders-v2.component.html", + templateUrl: "./folders.component.html", imports: [ CommonModule, JslibModule, @@ -39,7 +39,7 @@ import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.co AsyncActionsModule, ], }) -export class FoldersV2Component { +export class FoldersComponent { folders$: Observable; NoFoldersIcon = NoFolders; diff --git a/apps/browser/src/vault/popup/settings/more-from-bitwarden-page-v2.component.html b/apps/browser/src/vault/popup/settings/more-from-bitwarden-page.component.html similarity index 100% rename from apps/browser/src/vault/popup/settings/more-from-bitwarden-page-v2.component.html rename to apps/browser/src/vault/popup/settings/more-from-bitwarden-page.component.html diff --git a/apps/browser/src/vault/popup/settings/more-from-bitwarden-page-v2.component.ts b/apps/browser/src/vault/popup/settings/more-from-bitwarden-page.component.ts similarity index 97% rename from apps/browser/src/vault/popup/settings/more-from-bitwarden-page-v2.component.ts rename to apps/browser/src/vault/popup/settings/more-from-bitwarden-page.component.ts index 0b896547008..01537cbd62e 100644 --- a/apps/browser/src/vault/popup/settings/more-from-bitwarden-page-v2.component.ts +++ b/apps/browser/src/vault/popup/settings/more-from-bitwarden-page.component.ts @@ -19,7 +19,7 @@ import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.co // FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush // eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ - templateUrl: "more-from-bitwarden-page-v2.component.html", + templateUrl: "more-from-bitwarden-page.component.html", imports: [ CommonModule, JslibModule, @@ -30,7 +30,7 @@ import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.co ItemModule, ], }) -export class MoreFromBitwardenPageV2Component { +export class MoreFromBitwardenPageComponent { protected familySponsorshipAvailable$: Observable; protected isFreeFamilyPolicyEnabled$: Observable; protected hasSingleEnterpriseOrg$: Observable; diff --git a/apps/browser/src/vault/popup/settings/vault-settings-v2.component.html b/apps/browser/src/vault/popup/settings/vault-settings.component.html similarity index 100% rename from apps/browser/src/vault/popup/settings/vault-settings-v2.component.html rename to apps/browser/src/vault/popup/settings/vault-settings.component.html diff --git a/apps/browser/src/vault/popup/settings/vault-settings-v2.component.spec.ts b/apps/browser/src/vault/popup/settings/vault-settings.component.spec.ts similarity index 93% rename from apps/browser/src/vault/popup/settings/vault-settings-v2.component.spec.ts rename to apps/browser/src/vault/popup/settings/vault-settings.component.spec.ts index 554570de7f9..a948b811fd4 100644 --- a/apps/browser/src/vault/popup/settings/vault-settings-v2.component.spec.ts +++ b/apps/browser/src/vault/popup/settings/vault-settings.component.spec.ts @@ -19,7 +19,7 @@ import { PopOutComponent } from "../../../platform/popup/components/pop-out.comp import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component"; import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component"; -import { VaultSettingsV2Component } from "./vault-settings-v2.component"; +import { VaultSettingsComponent } from "./vault-settings.component"; @Component({ selector: "popup-header", @@ -47,9 +47,9 @@ class MockPopOutComponent { readonly show = input(true); } -describe("VaultSettingsV2Component", () => { - let component: VaultSettingsV2Component; - let fixture: ComponentFixture; +describe("VaultSettingsComponent", () => { + let component: VaultSettingsComponent; + let fixture: ComponentFixture; let router: Router; let mockCipherArchiveService: jest.Mocked; @@ -90,11 +90,11 @@ describe("VaultSettingsV2Component", () => { mockCipherArchiveService.hasArchiveFlagEnabled$ = mockHasArchiveFlagEnabled$.asObservable(); await TestBed.configureTestingModule({ - imports: [VaultSettingsV2Component], + imports: [VaultSettingsComponent], providers: [ provideRouter([ - { path: "archive", component: VaultSettingsV2Component }, - { path: "premium", component: VaultSettingsV2Component }, + { path: "archive", component: VaultSettingsComponent }, + { path: "premium", component: VaultSettingsComponent }, ]), { provide: SyncService, useValue: mock() }, { provide: ToastService, useValue: mock() }, @@ -117,7 +117,7 @@ describe("VaultSettingsV2Component", () => { }, ], }) - .overrideComponent(VaultSettingsV2Component, { + .overrideComponent(VaultSettingsComponent, { remove: { imports: [PopupHeaderComponent, PopupPageComponent, PopOutComponent], }, @@ -127,7 +127,7 @@ describe("VaultSettingsV2Component", () => { }) .compileComponents(); - fixture = TestBed.createComponent(VaultSettingsV2Component); + fixture = TestBed.createComponent(VaultSettingsComponent); component = fixture.componentInstance; router = TestBed.inject(Router); jest.spyOn(router, "navigate"); diff --git a/apps/browser/src/vault/popup/settings/vault-settings-v2.component.ts b/apps/browser/src/vault/popup/settings/vault-settings.component.ts similarity index 97% rename from apps/browser/src/vault/popup/settings/vault-settings-v2.component.ts rename to apps/browser/src/vault/popup/settings/vault-settings.component.ts index c35345bd8ab..f79cef56155 100644 --- a/apps/browser/src/vault/popup/settings/vault-settings-v2.component.ts +++ b/apps/browser/src/vault/popup/settings/vault-settings.component.ts @@ -23,7 +23,7 @@ import { BrowserPremiumUpgradePromptService } from "../services/browser-premium- // FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush // eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ - templateUrl: "vault-settings-v2.component.html", + templateUrl: "vault-settings.component.html", imports: [ CommonModule, JslibModule, @@ -39,7 +39,7 @@ import { BrowserPremiumUpgradePromptService } from "../services/browser-premium- { provide: PremiumUpgradePromptService, useClass: BrowserPremiumUpgradePromptService }, ], }) -export class VaultSettingsV2Component implements OnInit, OnDestroy { +export class VaultSettingsComponent implements OnInit, OnDestroy { private readonly premiumBadgeComponent = viewChild(PremiumBadgeComponent); lastSync = "--"; From 0f5163453e098e00cc2955922eba0dd86bdcf430 Mon Sep 17 00:00:00 2001 From: Brad <44413459+lastbestdev@users.noreply.github.com> Date: Tue, 10 Feb 2026 15:08:20 -0800 Subject: [PATCH 11/28] [PM-30540] Access Intelligence action button updates (mark critical, assign tasks) (#18730) Updates the buttons available in the Access Intelligence "Applications" tab. - The "Mark as critical" button appears when at least 1 row is selected in the table, and if all selected applications are already marked critical, changes to a "Mark as not critical" button. This functionality allows Admins to either bulk mark critical applications, or bulk unmark critical applications. - "Assign tasks" has been moved into this tab view, and now is only enabled when there are critical ciphers found without assigned password change tasks. A tooltip appears when hovering on the disabled state, informing the Admin that all tasks have already been assigned. --- apps/web/src/locales/en/messages.json | 39 ++++++ .../risk-insights-orchestrator.service.ts | 13 +- .../view/risk-insights-data.service.ts | 4 +- .../access-intelligence.module.ts | 2 +- .../password-change-metric.component.ts | 58 ++------- .../applications.component.html | 71 ++++++++--- .../applications.component.spec.ts | 8 ++ .../applications.component.ts | 114 ++++++++++++++++-- .../critical-applications.component.ts | 2 +- .../shared/security-tasks.service.spec.ts | 7 +- .../shared/security-tasks.service.ts | 55 ++++++++- 11 files changed, 282 insertions(+), 91 deletions(-) diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 1f4e1c98e32..763ce6634fe 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -268,6 +268,42 @@ } } }, + "numCriticalApplicationsMarkedSuccess": { + "message": "$COUNT$ applications marked critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "numApplicationsUnmarkedCriticalSuccess": { + "message": "$COUNT$ applications marked not critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsCritical": { + "message": "Mark $COUNT$ as critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsNotCritical": { + "message": "Mark $COUNT$ as not critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "applicationsMarkedAsCriticalFail": { "message": "Failed to mark applications as critical" }, @@ -10148,6 +10184,9 @@ "assignTasks": { "message": "Assign tasks" }, + "allTasksAssigned": { + "message": "All tasks have been assigned" + }, "assignSecurityTasksToMembers": { "message": "Send notifications to change passwords" }, diff --git a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/domain/risk-insights-orchestrator.service.ts b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/domain/risk-insights-orchestrator.service.ts index 88af8081a8b..886ae4c5008 100644 --- a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/domain/risk-insights-orchestrator.service.ts +++ b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/domain/risk-insights-orchestrator.service.ts @@ -228,7 +228,7 @@ export class RiskInsightsOrchestratorService { * @param criticalApplication Application name of the critical application to remove * @returns */ - removeCriticalApplication$(criticalApplication: string): Observable { + removeCriticalApplications$(applicationsToUnmark: Set): Observable { this.logService.info( "[RiskInsightsOrchestratorService] Removing critical applications from report", ); @@ -245,11 +245,10 @@ export class RiskInsightsOrchestratorService { throwError(() => Error("Tried to update critical applications without a report")); } - // Create a set for quick lookup of the new critical apps const existingApplicationData = report!.applicationData || []; - const updatedApplicationData = this._removeCriticalApplication( + const updatedApplicationData = this._removeCriticalApplications( existingApplicationData, - criticalApplication, + applicationsToUnmark, ); // Updated summary data after changing critical apps @@ -917,12 +916,12 @@ export class RiskInsightsOrchestratorService { } // Toggles the isCritical flag on applications via criticalApplicationName - private _removeCriticalApplication( + private _removeCriticalApplications( applicationData: OrganizationReportApplication[], - criticalApplication: string, + applicationsToUnmark: Set, ): OrganizationReportApplication[] { const updatedApplicationData = applicationData.map((application) => { - if (application.applicationName == criticalApplication) { + if (applicationsToUnmark.has(application.applicationName)) { return { ...application, isCritical: false } as OrganizationReportApplication; } return application; diff --git a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/view/risk-insights-data.service.ts b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/view/risk-insights-data.service.ts index d426a6b09c1..8cf799250f2 100644 --- a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/view/risk-insights-data.service.ts +++ b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/view/risk-insights-data.service.ts @@ -263,8 +263,8 @@ export class RiskInsightsDataService { return this.orchestrator.saveCriticalApplications$(selectedUrls); } - removeCriticalApplication(hostname: string) { - return this.orchestrator.removeCriticalApplication$(hostname); + removeCriticalApplications(selectedUrls: Set) { + return this.orchestrator.removeCriticalApplications$(selectedUrls); } saveApplicationReviewStatus(selectedCriticalApps: OrganizationReportApplication[]) { diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/access-intelligence.module.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/access-intelligence.module.ts index 5592e4cc546..555d6aa62e0 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/access-intelligence.module.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/access-intelligence.module.ts @@ -59,7 +59,7 @@ import { AccessIntelligenceSecurityTasksService } from "./shared/security-tasks. safeProvider({ provide: AccessIntelligenceSecurityTasksService, useClass: AccessIntelligenceSecurityTasksService, - deps: [DefaultAdminTaskService, SecurityTasksApiService], + deps: [DefaultAdminTaskService, SecurityTasksApiService, RiskInsightsDataService], }), safeProvider({ provide: PasswordHealthService, diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/activity-cards/password-change-metric.component.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/activity-cards/password-change-metric.component.ts index df47adb4635..0487ae726e3 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/activity-cards/password-change-metric.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/activity-cards/password-change-metric.component.ts @@ -20,7 +20,7 @@ import { } from "@bitwarden/bit-common/dirt/reports/risk-insights"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { CipherId, OrganizationId } from "@bitwarden/common/types/guid"; +import { OrganizationId } from "@bitwarden/common/types/guid"; import { SecurityTask, SecurityTaskStatus } from "@bitwarden/common/vault/tasks"; import { ButtonModule, @@ -57,10 +57,14 @@ export class PasswordChangeMetricComponent implements OnInit { // Signal states private readonly _tasks: Signal = signal([]); - private readonly _atRiskCipherIds: Signal = signal([]); private readonly _hasCriticalApplications: Signal = signal(false); - private readonly _reportGeneratedAt: Signal = signal( - undefined, + private readonly _unassignedCipherIds = toSignal( + this.securityTasksService.unassignedCriticalCipherIds$, + { initialValue: [] }, + ); + private readonly _atRiskCipherIds = toSignal( + this.riskInsightsDataService.criticalApplicationAtRiskCipherIds$, + { initialValue: [] }, ); // Computed properties @@ -74,41 +78,11 @@ export class PasswordChangeMetricComponent implements OnInit { return total > 0 ? Math.round((this.completedTasksCount() / total) * 100) : 0; }); - readonly unassignedCipherIds = computed(() => { - const atRiskIds = this._atRiskCipherIds(); - const tasks = this._tasks(); + readonly unassignedCipherIds = computed(() => this._unassignedCipherIds().length); - if (tasks.length === 0) { - return atRiskIds.length; - } - - const inProgressTasks = tasks.filter((task) => task.status === SecurityTaskStatus.Pending); - const inProgressTaskIds = new Set(inProgressTasks.map((task) => task.cipherId)); - - const reportGeneratedAt = this._reportGeneratedAt(); - const completedTasksAfterReportGeneration = reportGeneratedAt - ? tasks.filter( - (task) => - task.status === SecurityTaskStatus.Completed && - new Date(task.revisionDate) >= reportGeneratedAt, - ) - : []; - const completedTaskIds = new Set( - completedTasksAfterReportGeneration.map((task) => task.cipherId), - ); - - // find cipher ids from last report that do not have a corresponding in progress task (awaiting password reset) OR completed task - const unassignedIds = atRiskIds.filter( - (id) => !inProgressTaskIds.has(id) && !completedTaskIds.has(id), - ); - - return unassignedIds.length; - }); - - readonly atRiskPasswordCount = computed(() => { + readonly atRiskPasswordCount = computed(() => { const atRiskIds = this._atRiskCipherIds(); const atRiskIdsSet = new Set(atRiskIds); - return atRiskIdsSet.size; }); @@ -119,7 +93,7 @@ export class PasswordChangeMetricComponent implements OnInit { if (this.tasksCount() === 0) { return PasswordChangeView.NO_TASKS_ASSIGNED; } - if (this.unassignedCipherIds() > 0) { + if (this._unassignedCipherIds().length > 0) { return PasswordChangeView.NEW_TASKS_AVAILABLE; } return PasswordChangeView.PROGRESS; @@ -133,10 +107,6 @@ export class PasswordChangeMetricComponent implements OnInit { private toastService: ToastService, ) { this._tasks = toSignal(this.securityTasksService.tasks$, { initialValue: [] }); - this._atRiskCipherIds = toSignal( - this.riskInsightsDataService.criticalApplicationAtRiskCipherIds$, - { initialValue: [] }, - ); this._hasCriticalApplications = toSignal( this.riskInsightsDataService.criticalReportResults$.pipe( map((report) => { @@ -145,10 +115,6 @@ export class PasswordChangeMetricComponent implements OnInit { ), { initialValue: false }, ); - this._reportGeneratedAt = toSignal( - this.riskInsightsDataService.enrichedReportData$.pipe(map((report) => report?.creationDate)), - { initialValue: undefined }, - ); effect(() => { const isShowingProgress = this.currentView() === PasswordChangeView.PROGRESS; @@ -164,7 +130,7 @@ export class PasswordChangeMetricComponent implements OnInit { try { await this.securityTasksService.requestPasswordChangeForCriticalApplications( this.organizationId(), - this._atRiskCipherIds(), + this._unassignedCipherIds(), ); this.toastService.showToast({ message: this.i18nService.t("notifiedMembers"), 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 efe07d50683..27864fa2f87 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 @@ -21,27 +21,58 @@ > - +
+ @if (selectedUrls().size > 0) { + @if (allSelectedAppsAreCritical()) { + + } @else { + + } + } - + + + +
{ let mockLogService: MockProxy; let mockToastService: MockProxy; let mockDataService: MockProxy; + let mockSecurityTasksService: MockProxy; const reportStatus$ = new BehaviorSubject(ReportStatus.Complete); const enrichedReportData$ = new BehaviorSubject(null); @@ -47,6 +49,7 @@ describe("ApplicationsComponent", () => { appAtRiskMembers: null, atRiskAppDetails: null, }); + const unassignedCriticalCipherIds$ = new BehaviorSubject([]); beforeEach(async () => { mockI18nService = mock(); @@ -54,6 +57,7 @@ describe("ApplicationsComponent", () => { mockLogService = mock(); mockToastService = mock(); mockDataService = mock(); + mockSecurityTasksService = mock(); mockI18nService.t.mockImplementation((key: string) => key); @@ -65,6 +69,9 @@ describe("ApplicationsComponent", () => { get: () => criticalReportResults$, }); Object.defineProperty(mockDataService, "drawerDetails$", { get: () => drawerDetails$ }); + Object.defineProperty(mockSecurityTasksService, "unassignedCriticalCipherIds$", { + get: () => unassignedCriticalCipherIds$, + }); await TestBed.configureTestingModule({ imports: [ApplicationsComponent, ReactiveFormsModule], @@ -78,6 +85,7 @@ describe("ApplicationsComponent", () => { provide: ActivatedRoute, useValue: { snapshot: { paramMap: { get: (): string | null => null } } }, }, + { provide: AccessIntelligenceSecurityTasksService, useValue: mockSecurityTasksService }, ], }).compileComponents(); diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications/applications.component.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications/applications.component.ts index b5fae36bb2e..0020106ba7d 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications/applications.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications/applications.component.ts @@ -7,10 +7,10 @@ import { signal, computed, } from "@angular/core"; -import { takeUntilDestroyed, toObservable } from "@angular/core/rxjs-interop"; +import { takeUntilDestroyed, toObservable, toSignal } from "@angular/core/rxjs-interop"; import { FormControl, ReactiveFormsModule } from "@angular/forms"; import { ActivatedRoute } from "@angular/router"; -import { combineLatest, debounceTime, startWith } from "rxjs"; +import { combineLatest, debounceTime, EMPTY, map, startWith, switchMap } from "rxjs"; import { Security } from "@bitwarden/assets/svg"; import { RiskInsightsDataService } from "@bitwarden/bit-common/dirt/reports/risk-insights"; @@ -22,6 +22,7 @@ import { import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { OrganizationId } from "@bitwarden/common/types/guid"; import { ButtonModule, IconButtonModule, @@ -30,8 +31,10 @@ import { SearchModule, TableDataSource, ToastService, + TooltipDirective, TypographyModule, ChipSelectComponent, + IconComponent, } from "@bitwarden/components"; import { ExportHelper } from "@bitwarden/vault-export-core"; import { exportToCSV } from "@bitwarden/web-vault/app/dirt/reports/report-utils"; @@ -42,6 +45,7 @@ import { PipesModule } from "@bitwarden/web-vault/app/vault/individual-vault/pip import { AppTableRowScrollableM11Component } from "../shared/app-table-row-scrollable-m11.component"; import { ApplicationTableDataSource } from "../shared/app-table-row-scrollable.component"; import { ReportLoadingComponent } from "../shared/report-loading.component"; +import { AccessIntelligenceSecurityTasksService } from "../shared/security-tasks.service"; export const ApplicationFilterOption = { All: "all", @@ -70,6 +74,8 @@ export type ApplicationFilterOption = ButtonModule, ReactiveFormsModule, ChipSelectComponent, + IconComponent, + TooltipDirective, ], }) export class ApplicationsComponent implements OnInit { @@ -86,13 +92,14 @@ export class ApplicationsComponent implements OnInit { // Template driven properties protected readonly selectedUrls = signal(new Set()); - protected readonly markingAsCritical = signal(false); + protected readonly updatingCriticalApps = signal(false); protected readonly applicationSummary = signal(createNewSummaryData()); protected readonly criticalApplicationsCount = signal(0); protected readonly totalApplicationsCount = signal(0); protected readonly nonCriticalApplicationsCount = computed(() => { return this.totalApplicationsCount() - this.criticalApplicationsCount(); }); + protected readonly organizationId = signal(undefined); // filter related properties protected readonly selectedFilter = signal(ApplicationFilterOption.All); @@ -112,14 +119,46 @@ export class ApplicationsComponent implements OnInit { ]); protected readonly emptyTableExplanation = signal(""); + readonly allSelectedAppsAreCritical = computed(() => { + if (!this.dataSource.filteredData || this.selectedUrls().size == 0) { + return false; + } + + return this.dataSource.filteredData + .filter((row) => this.selectedUrls().has(row.applicationName)) + .every((row) => row.isMarkedAsCritical); + }); + + protected readonly unassignedCipherIds = toSignal( + this.securityTasksService.unassignedCriticalCipherIds$, + { initialValue: [] }, + ); + + readonly enableRequestPasswordChange = computed(() => this.unassignedCipherIds().length > 0); + constructor( protected i18nService: I18nService, protected activatedRoute: ActivatedRoute, protected toastService: ToastService, protected dataService: RiskInsightsDataService, + protected securityTasksService: AccessIntelligenceSecurityTasksService, ) {} async ngOnInit() { + this.activatedRoute.paramMap + .pipe( + takeUntilDestroyed(this.destroyRef), + map((params) => params.get("organizationId")), + switchMap(async (orgId) => { + if (orgId) { + this.organizationId.set(orgId as OrganizationId); + } else { + return EMPTY; + } + }), + ) + .subscribe(); + this.dataService.enrichedReportData$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe({ next: (report) => { if (report != null) { @@ -193,12 +232,8 @@ export class ApplicationsComponent implements OnInit { this.selectedFilter.set(value); } - isMarkedAsCriticalItem(applicationName: string) { - return this.selectedUrls().has(applicationName); - } - markAppsAsCritical = async () => { - this.markingAsCritical.set(true); + this.updatingCriticalApps.set(true); const count = this.selectedUrls().size; this.dataService @@ -209,10 +244,10 @@ export class ApplicationsComponent implements OnInit { this.toastService.showToast({ variant: "success", title: "", - message: this.i18nService.t("criticalApplicationsMarkedSuccess", count.toString()), + message: this.i18nService.t("numCriticalApplicationsMarkedSuccess", count), }); this.selectedUrls.set(new Set()); - this.markingAsCritical.set(false); + this.updatingCriticalApps.set(false); }, error: () => { this.toastService.showToast({ @@ -224,6 +259,65 @@ export class ApplicationsComponent implements OnInit { }); }; + unmarkAppsAsCritical = async () => { + this.updatingCriticalApps.set(true); + const appsToUnmark = this.selectedUrls(); + + this.dataService + .removeCriticalApplications(appsToUnmark) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe({ + next: () => { + this.toastService.showToast({ + message: this.i18nService.t( + "numApplicationsUnmarkedCriticalSuccess", + appsToUnmark.size, + ), + variant: "success", + }); + this.selectedUrls.set(new Set()); + this.updatingCriticalApps.set(false); + }, + error: () => { + this.toastService.showToast({ + message: this.i18nService.t("unexpectedError"), + variant: "error", + title: this.i18nService.t("error"), + }); + }, + }); + }; + + async requestPasswordChange() { + const orgId = this.organizationId(); + if (!orgId) { + this.toastService.showToast({ + message: this.i18nService.t("unexpectedError"), + variant: "error", + title: this.i18nService.t("error"), + }); + return; + } + + try { + await this.securityTasksService.requestPasswordChangeForCriticalApplications( + orgId, + this.unassignedCipherIds(), + ); + this.toastService.showToast({ + message: this.i18nService.t("notifiedMembers"), + variant: "success", + title: this.i18nService.t("success"), + }); + } catch { + this.toastService.showToast({ + message: this.i18nService.t("unexpectedError"), + variant: "error", + title: this.i18nService.t("error"), + }); + } + } + showAppAtRiskMembers = async (applicationName: string) => { await this.dataService.setDrawerForAppAtRiskMembers(applicationName); }; diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/critical-applications/critical-applications.component.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/critical-applications/critical-applications.component.ts index 3033bf139c3..7180d50fe05 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/critical-applications/critical-applications.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/critical-applications/critical-applications.component.ts @@ -131,7 +131,7 @@ export class CriticalApplicationsComponent implements OnInit { removeCriticalApplication = async (hostname: string) => { this.dataService - .removeCriticalApplication(hostname) + .removeCriticalApplications(new Set([hostname])) .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe({ next: () => { diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/security-tasks.service.spec.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/security-tasks.service.spec.ts index f6fb41cdbb0..4ee784337de 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/security-tasks.service.spec.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/security-tasks.service.spec.ts @@ -1,7 +1,10 @@ import { TestBed } from "@angular/core/testing"; import { mock } from "jest-mock-extended"; -import { SecurityTasksApiService } from "@bitwarden/bit-common/dirt/reports/risk-insights"; +import { + RiskInsightsDataService, + SecurityTasksApiService, +} from "@bitwarden/bit-common/dirt/reports/risk-insights"; import { CipherId, OrganizationId } from "@bitwarden/common/types/guid"; import { SecurityTaskType } from "@bitwarden/common/vault/tasks"; @@ -13,12 +16,14 @@ describe("AccessIntelligenceSecurityTasksService", () => { let service: AccessIntelligenceSecurityTasksService; const defaultAdminTaskServiceMock = mock(); const securityTasksApiServiceMock = mock(); + const riskInsightsDataServiceMock = mock(); beforeEach(() => { TestBed.configureTestingModule({}); service = new AccessIntelligenceSecurityTasksService( defaultAdminTaskServiceMock, securityTasksApiServiceMock, + riskInsightsDataServiceMock, ); }); diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/security-tasks.service.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/security-tasks.service.ts index 688ab039ca9..65a31896341 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/security-tasks.service.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/security-tasks.service.ts @@ -1,8 +1,10 @@ -import { BehaviorSubject } from "rxjs"; +import { BehaviorSubject, combineLatest, Observable } from "rxjs"; +import { map, shareReplay } from "rxjs/operators"; +import { RiskInsightsDataService } from "@bitwarden/bit-common/dirt/reports/risk-insights"; import { SecurityTasksApiService } from "@bitwarden/bit-common/dirt/reports/risk-insights"; import { CipherId, OrganizationId } from "@bitwarden/common/types/guid"; -import { SecurityTask, SecurityTaskType } from "@bitwarden/common/vault/tasks"; +import { SecurityTask, SecurityTaskStatus, SecurityTaskType } from "@bitwarden/common/vault/tasks"; import { CreateTasksRequest } from "../../../vault/services/abstractions/admin-task.abstraction"; import { DefaultAdminTaskService } from "../../../vault/services/default-admin-task.service"; @@ -14,10 +16,57 @@ export class AccessIntelligenceSecurityTasksService { private _tasksSubject$ = new BehaviorSubject([]); tasks$ = this._tasksSubject$.asObservable(); + /** + * Observable stream of unassigned critical cipher IDs. + * Returns cipher IDs from critical applications that don't have an associated task + * (either pending or completed after the report was generated). + */ + readonly unassignedCriticalCipherIds$: Observable; + constructor( private adminTaskService: DefaultAdminTaskService, private securityTasksApiService: SecurityTasksApiService, - ) {} + private riskInsightsDataService: RiskInsightsDataService, + ) { + this.unassignedCriticalCipherIds$ = combineLatest([ + this.tasks$, + this.riskInsightsDataService.criticalApplicationAtRiskCipherIds$, + this.riskInsightsDataService.enrichedReportData$, + ]).pipe( + map(([tasks, atRiskCipherIds, reportData]) => { + // If no tasks exist, return all at-risk cipher IDs + if (tasks.length === 0) { + return atRiskCipherIds; + } + + // Get in-progress tasks (awaiting password reset) + const inProgressTasks = tasks.filter((task) => task.status === SecurityTaskStatus.Pending); + const inProgressTaskIds = new Set(inProgressTasks.map((task) => task.cipherId)); + + // Get completed tasks after report generation + const reportGeneratedAt = reportData?.creationDate; + const completedTasksAfterReportGeneration = reportGeneratedAt + ? tasks.filter( + (task) => + task.status === SecurityTaskStatus.Completed && + new Date(task.revisionDate) >= reportGeneratedAt, + ) + : []; + const completedTaskIds = new Set( + completedTasksAfterReportGeneration.map((task) => task.cipherId), + ); + + // Filter out cipher IDs that have a corresponding in-progress or completed task + return atRiskCipherIds.filter( + (id) => !inProgressTaskIds.has(id) && !completedTaskIds.has(id), + ); + }), + shareReplay({ + bufferSize: 1, + refCount: true, + }), + ); + } /** * Gets security task metrics for the given organization From 8f6cf67f8d2e1ac1e219cd0f7d175232ae0e3c69 Mon Sep 17 00:00:00 2001 From: Jackson Engstrom Date: Tue, 10 Feb 2026 15:32:36 -0800 Subject: [PATCH 12/28] [PM-29116] UI Text cut off on default size extension for Download Bitwarden (#18789) * updates settings buttons to wrap instead of truncate * adds new download copy --- apps/browser/src/_locales/en/messages.json | 4 + .../popup/settings/settings-v2.component.html | 75 +++++++++---------- 2 files changed, 41 insertions(+), 38 deletions(-) diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index c6d9d325e00..940bb22c95c 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -6127,6 +6127,10 @@ "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, + + "downloadBitwardenApps": { + "message": "Download Bitwarden apps" + }, "emailProtected": { "message": "Email protected" }, diff --git a/apps/browser/src/tools/popup/settings/settings-v2.component.html b/apps/browser/src/tools/popup/settings/settings-v2.component.html index 19f2445b61d..5aa962d5cc3 100644 --- a/apps/browser/src/tools/popup/settings/settings-v2.component.html +++ b/apps/browser/src/tools/popup/settings/settings-v2.component.html @@ -1,19 +1,21 @@ - - {{ "unlockFeaturesWithPremium" | i18n }} - - - + @if (!(hasPremium$ | async)) { + + {{ "unlockFeaturesWithPremium" | i18n }} + + + + } @@ -23,14 +25,14 @@ - + {{ "accountSecurity" | i18n }} - +

{{ "autofill" | i18n }}

@@ -44,7 +46,7 @@
- + {{ "notifications" | i18n }} @@ -55,6 +57,7 @@ bit-item-content routerLink="/vault-settings" (click)="dismissBadge(NudgeType.EmptyVaultNudge)" + [truncate]="false" >
@@ -63,20 +66,18 @@ Currently can be only 1 item for notification. Will make this dynamic when more nudges are added --> - 1 + @if (showVaultBadge$ | async) { + 1 + }
- + {{ "appearance" | i18n }} @@ -85,7 +86,7 @@ @if (showAdminSettingsLink$ | async) { - +

{{ "admin" | i18n }}

@@ -101,30 +102,28 @@ } -
+ {{ "about" | i18n }} - +
-

{{ "downloadBitwardenOnAllDevices" | i18n }}

- 1 - +

{{ "downloadBitwardenApps" | i18n }}

+ @if (showDownloadBitwardenNudge$ | async) { + 1 + + }
- + {{ "moreFromBitwarden" | i18n }} From 7ccf1263a0da002841115d3854a51cd0d37ced0c Mon Sep 17 00:00:00 2001 From: Leslie Tilton <23057410+Banrion@users.noreply.github.com> Date: Tue, 10 Feb 2026 19:01:52 -0600 Subject: [PATCH 13/28] [PM-31939] Access Intelligence Documentation: Report Data Model Evolution (#18879) * Add report-data-model-evolution document * Change memberRefs to one record with flag for at risk or not * Update model evolution doc * Remove implementation section in favor of jira tracking * Remove todo comment * Add table of contents --- .../report-data-model-evolution.md | 807 ++++++++++++++++++ 1 file changed, 807 insertions(+) create mode 100644 bitwarden_license/bit-common/src/dirt/docs/access-intelligence/architecture/report-data-model-evolution.md diff --git a/bitwarden_license/bit-common/src/dirt/docs/access-intelligence/architecture/report-data-model-evolution.md b/bitwarden_license/bit-common/src/dirt/docs/access-intelligence/architecture/report-data-model-evolution.md new file mode 100644 index 00000000000..2e45d7fa9d8 --- /dev/null +++ b/bitwarden_license/bit-common/src/dirt/docs/access-intelligence/architecture/report-data-model-evolution.md @@ -0,0 +1,807 @@ +# Report Data Model Evolution + +> **Purpose**: Document the old report data model (what's stored today), the updated model +> from PR #17356 (merged, follows BW architecture), and the target model with the member +> registry optimization. This is a reference for understanding why the report was 450MB+ +> and how the member registry solves it. + +--- + +## Table of Contents + +1. [Current Storage Model](#1-current-storage-model-still-in-use--plain-interfaces-no-architecture) +2. [Proposed View Models — Following BW Architecture](#2-proposed-view-models--following-bw-architecture-what-should-be-implemented-next) +3. [Target Model — With Member Registry](#3-target-model--with-member-registry-what-were-building) +4. [Storage Structure Comparison](#4-storage-structure-comparison) +5. [Encryption Approaches (Current vs Future Options)](#5-encryption-approaches-current-vs-future-options) + +--- + +## 1. Current Storage Model (Still In Use) — Plain interfaces, no architecture + +**Status:** This is what's stored in the database today. These are simple TypeScript interfaces/types with no domain/data/view/api layers. No encryption support in the types themselves. Services do all the filtering and transformation. + +**Note:** While PR #17356 introduced architecture patterns, the actual storage structure still uses these plain types directly. The proposed view models (Section 2) describe the architecture we should migrate to next. + +### ApplicationHealthReportDetail (the old report row) + +**Source:** `models/report-models.ts:78-88` (current implementation, still in use) + +**Current structure (with arrays):** + +```typescript +// This is the main report model — one record per application (grouped by URI hostname) +// Used directly by services and UI components +export type ApplicationHealthReportDetail = { + applicationName: string; // hostname (e.g. "google.com") + passwordCount: number; // total ciphers for this app + atRiskPasswordCount: number; // ciphers with weak/reused/exposed passwords + cipherIds: CipherId[]; // IDs of all ciphers in this app - ARRAY + atRiskCipherIds: CipherId[]; // IDs of at-risk ciphers - ARRAY (subset of cipherIds) + memberCount: number; // count of unique members (redundant, = memberDetails.length) + atRiskMemberCount: number; // count of at-risk members (redundant, = atRiskMemberDetails.length) + memberDetails: MemberDetails[]; // ⚠️ FULL member objects repeated per app - ARRAY + atRiskMemberDetails: MemberDetails[]; // ⚠️ FULL member objects for at-risk only (subset of memberDetails) - ARRAY + // Members are deduplicated within a single app but NOT across apps. +}; +``` + +**Proposed structure (with Records for consistency):** + +```typescript +export type ApplicationHealthReportDetail = { + applicationName: string; + passwordCount: number; + atRiskPasswordCount: number; + cipherRefs: Record; // true = at-risk, false = not at-risk (combines cipherIds + atRiskCipherIds) + memberCount: number; // could be removed (= Object.keys(memberRefs).length) + atRiskMemberCount: number; // could be removed (= count of true values in memberRefs) + memberRefs: Record; // true = at-risk, false = not at-risk (combines memberDetails + atRiskMemberDetails) +}; +``` + +**Benefits of Record pattern for ciphers:** + +- ✅ Combines `cipherIds` and `atRiskCipherIds` into single structure +- ✅ No duplicate IDs (prevents data inconsistency) +- ✅ O(1) lookup to check if cipher is at-risk +- ✅ Consistent with `memberRefs` pattern +- ✅ Saves space (~50 bytes per duplicate cipher ID in large orgs) + +### MemberDetails (the old member model) + +**Source:** `models/report-models.ts:16-21` (current implementation, still in use) + +```typescript +// Repeated in EVERY ApplicationHealthReportDetail that a member has access to +// For a large org with 5,000 members accessing 200 apps → duplicated across apps +export type MemberDetails = { + userGuid: string; // Organization user ID (UUID) + userName: string | null; // Display name + email: string; // Email address + cipherId: string; // ⚠️ Meaningless after deduplication (first cipher processed) +}; +``` + +### RiskInsightsData (the storage container) + +**Source:** `models/report-models.ts:121-128` (current implementation, still in use) + +**Current structure (with arrays):** + +**Rename to:** RiskInsights + +```typescript +// The top-level container that is stored in the database +// Each field is encrypted separately as an EncString +export interface RiskInsightsData { + id: OrganizationReportId; // Report ID (generated by API) + creationDate: Date; // When report was generated + contentEncryptionKey: EncString; // Key used to encrypt report data + reportData: ApplicationHealthReportDetail[]; // ⚠️ Main payload - can be 700MB+ + summaryData: OrganizationReportSummary; // Pre-computed aggregates (~1KB) + applicationData: OrganizationReportApplication[]; // Per-app settings (~10KB) - ARRAY with O(n) lookup +} +``` + +**Proposed structure (with Records for O(1) lookup):** + +```typescript +export interface RiskInsights { + id: OrganizationReportId; + creationDate: Date; + contentEncryptionKey: EncString; + reportData: ApplicationHealthReportDetail[]; // Array is still needed here for iteration + summaryData: OrganizationReportSummary; + applicationData: Record; // Record for O(1) lookup +} +``` + +**Current encryption:** Each of `reportData`, `summaryData`, and `applicationData` is JSON.stringify'd and encrypted as a separate EncString. For large orgs, `reportData` is compressed before encryption to avoid WASM size limits. + +### OrganizationReportApplication (per-app user settings) + +**Source:** `models/report-models.ts:64-72` (current implementation, still in use) + +**Current (Array):** Stored as array with O(n) lookup (inefficient) + +**Rename to:** RiskInsightsApplication (If separate model is needed) + +```typescript +// User-defined settings per application (critical flag, review date) +// Stored in the report, carried over between report generations +export type OrganizationReportApplication = { + applicationName: string; // hostname (e.g. "google.com") + isCritical: boolean; // user-defined critical flag + reviewedDate: Date | null; // null = new/unreviewed application +}; +``` + +**Proposed (Record):** Should be stored as Record for O(1) lookup + +```typescript +// Key = applicationName (hostname) +type ApplicationDataRecord = Record; + +// Example: +applicationData: { + "google.com": { isCritical: true, reviewedDate: new Date("2026-01-15") }, + "github.com": { isCritical: false, reviewedDate: null }, // new/unreviewed + "slack.com": { isCritical: true, reviewedDate: new Date("2026-02-01") } +} +``` + +**Problem with current array structure:** + +```typescript +// Current inefficient O(n) lookup pattern found in code: +getCriticalApplications(): RiskInsightsReportView[] { + return this.report.filter((app) => { + const appMeta = this.applications.find((a) => a.hostname === app.applicationName); // O(n)! + return appMeta?.isCritical === true; + }); +} +``` + +**With Record (O(1) lookup):** + +```typescript +getCriticalApplications(): RiskInsightsReportView[] { + return this.report.filter((app) => { + return this.applicationData[app.applicationName]?.isCritical === true; // O(1)! + }); +} +``` + +### OrganizationReportSummary (pre-computed aggregates) + +**Source:** `models/report-models.ts:49-58` (current implementation, still in use) + +**Rename to:** RiskInsightsSummary + +```typescript +// Pre-computed aggregates for summary cards and filtering +// Recomputed when critical application markings change +export type OrganizationReportSummary = { + totalMemberCount: number; // All members in org + totalApplicationCount: number; // All applications in report + totalAtRiskMemberCount: number; // Members with at-risk access + totalAtRiskApplicationCount: number; // Applications with at-risk ciphers + totalCriticalApplicationCount: number; // Applications marked critical + totalCriticalMemberCount: number; // Members with access to critical apps + totalCriticalAtRiskMemberCount: number; // Members with at-risk access to critical apps + totalCriticalAtRiskApplicationCount: number; // Critical apps with at-risk ciphers +}; +``` + +**Note:** When a user marks/unmarks an application as critical, the summary is recomputed. This is why `atRiskMemberDetails[]` is stored separately per application - it allows efficient recalculation of critical app summaries without reprocessing all cipher health data. + +### Why This Was 450MB+ + +The core problem: **`MemberDetails` objects were fully duplicated per application**. + +Example for a large org: + +- 5,000 org members +- 200 applications in the report +- Each member might have access to 50+ applications +- Each `MemberDetails` object ~200 bytes + +**Worst case**: 5,000 members × 50 apps × 200 bytes = ~50MB just for member data +in `memberDetails[]` arrays. With `atRiskMemberDetails[]` duplicated alongside, +plus cipher health data, this easily reached 450MB+. + +This caused: + +1. **WASM encryption panics** — the encrypted blob exceeded SDK size limits +2. **Database storage limits** — even compressed, the JSON was too large for DB fields +3. **Memory pressure** — holding this in a `BehaviorSubject` blocked the UI +4. **Slow report generation** — building all these duplicated member arrays was O(n²) + +--- + +## 2. Proposed View Models — Following BW Architecture (What Should Be Implemented Next) + +**Status:** PR #17356 laid groundwork for architecture patterns, but storage still uses plain types from Section 1. This section describes the view models that SHOULD be implemented to follow Bitwarden's 4-layer pattern: `Api → Data → Domain → View` + +**Important:** These models are NOT currently in use. They represent the target architecture we should migrate to, with query methods replacing facade/orchestrator filtering logic. + +### What's Stored (Current Implementation) + +The current implementation stores the **exact types from Section 1** above: + +- `ApplicationHealthReportDetail` - report rows (700MB+ for large orgs) - using ARRAYS +- `OrganizationReportApplication` - per-app settings (~10KB) - using ARRAY +- `OrganizationReportSummary` - aggregates (~1KB) + +These are stored in `RiskInsightsData` and encrypted as separate EncStrings: + +```typescript +// What gets stored in the database today (using arrays): +RiskInsightsData { + reportData: ApplicationHealthReportDetail[] // ← JSON.stringify → EncString + // Contains duplicate member objects across apps + // Contains duplicate cipher IDs (cipherIds + atRiskCipherIds) + summaryData: OrganizationReportSummary // ← JSON.stringify → EncString + applicationData: OrganizationReportApplication[] // ← JSON.stringify → EncString (array with O(n) lookup) + contentEncryptionKey: EncString + id: OrganizationReportId + creationDate: Date +} +``` + +**Encryption approach:** Each field is JSON.stringify'd, optionally compressed (for `reportData` only, to avoid WASM limits), then encrypted with the `contentEncryptionKey`. + +**Problems with current structure:** + +- Member objects duplicated across applications (576MB for 10K org) +- Cipher and member IDs duplicated in separate arrays (~70MB wasted) +- ApplicationData requires O(n) find operations for every lookup + +### Proposed View Models (For Query Logic) + +The new architecture will introduce domain/view models with query methods. These are **NOT stored** - they're runtime transformations of the stored data. + +#### RiskInsightsView (proposed - replaces facade logic) + +```typescript +class RiskInsightsView { + report: ApplicationHealthReportDetail[]; // Decrypted from storage + applications: OrganizationReportApplication[]; // Decrypted from storage + summary: OrganizationReportSummary; // Decrypted from storage + memberRegistry: MemberRegistry; // ← NEW: Built at load time + createdDate: Date; + + // Query methods (replace current facade/orchestrator filtering): + getAtRiskMembers(): MemberRegistryEntry[]; + getCriticalApplications(): ApplicationHealthReportDetail[]; + getApplicationByHostname(hostname: string): ApplicationHealthReportDetail | undefined; + getNewApplications(): ApplicationHealthReportDetail[]; // reviewedDate === null + getSummary(): OrganizationReportSummary; +} +``` + +**Note:** The view model will have query methods, but the underlying storage structure (Section 1) remains the same until we implement the member registry optimization (Section 3). + +--- + +## 3. Target Model — With Member Registry (What We're Building) + +**Key optimization:** Replace duplicated `MemberDetails[]` arrays with lightweight member ID references that point into a shared `MemberRegistry`. This reduces a 10K org report from ~786MB to ~173MB (78% reduction). + +**Storage changes:** + +- Store members ONCE in a registry (not per application) +- Store only member IDs (userGuids) in application records +- Remove meaningless `cipherId` field from member data +- Combine `memberDetails` and `atRiskMemberDetails` into single array with flag (OR keep separate arrays with IDs only) + +### MemberRegistry (new — deduplicated member lookup) + +```typescript +// Single source of truth for member data in a report +// Stored once, referenced by index from every application that member appears in +class MemberRegistry { + // Map from org user ID → full member entry + private entries: Map; + + get(id: OrganizationUserId): MemberRegistryEntry | undefined; + getAll(): MemberRegistryEntry[]; + size(): number; +} + +interface MemberRegistryEntry { + id: OrganizationUserId; + userName: string; + email: string; + // Any other member metadata needed by the UI +} +``` + +### Member References (new — Record with at-risk flag) + +Instead of duplicating full member objects per application, each application stores member IDs as a `Record`, where: + +- **Key** = member ID (userGuid) +- **Value** = `true` if at-risk, `false` if not at-risk + +This provides: + +- **O(1) lookup** for checking membership and at-risk status +- **Automatic deduplication** (can't have duplicate keys) +- **Single source** for both member list and at-risk status +- **No duplicate IDs** (previously stored in both memberDetails and atRiskMemberDetails) + +```typescript +// Stored as a Record where value indicates at-risk status +type MemberRefs = Record; + +// Example: +memberRefs: { + "abc-123": true, // at-risk member + "def-456": false, // not at-risk + "ghi-789": true // at-risk member +} +``` + +### Updated RiskInsightsReportView (with registry references) + +```typescript +class RiskInsightsReportView { + applicationName: string; + passwordCount: number; + atRiskPasswordCount: number; + weakPasswordCount: number; + reusedPasswordCount: number; + exposedPasswordCount: number; + + // OLD: memberDetails: MemberDetails[] + atRiskMemberDetails: MemberDetails[] (duplicated arrays) + // NEW: Single Record with at-risk flag + memberRefs: Record; // { "abc": true, "def": false, ... } + + // OLD: cipherIds: CipherId[] + atRiskCipherIds: CipherId[] (duplicated arrays) + // NEW: Single Record with at-risk flag + cipherRefs: Record; // { "cipher-1": true, "cipher-2": false, ... } + + // The registry is held by the parent RiskInsightsView + // View model methods resolve refs → full entries on demand: + + getAllMembers(registry: MemberRegistry): MemberRegistryEntry[] { + return Object.keys(this.memberRefs) + .map((id) => registry.get(id as OrganizationUserId)) + .filter(Boolean); + } + + getAtRiskMembers(registry: MemberRegistry): MemberRegistryEntry[] { + return Object.entries(this.memberRefs) + .filter(([_, isAtRisk]) => isAtRisk) + .map(([id]) => registry.get(id as OrganizationUserId)) + .filter(Boolean); + } + + isAtRisk(): boolean { + return this.atRiskPasswordCount > 0; + } + + hasMember(memberId: OrganizationUserId): boolean { + return memberId in this.memberRefs; // O(1) lookup + } + + isMemberAtRisk(memberId: OrganizationUserId): boolean { + return this.memberRefs[memberId] === true; // O(1) lookup + } +} +``` + +### Updated RiskInsightsView (parent, holds registry) + +```typescript +class RiskInsightsView { + report: RiskInsightsReportView[]; + applications: Record; + summary: RiskInsightsSummaryView; + memberRegistry: MemberRegistry; // ← shared, deduplicated + createdDate: Date; + + // Smart query methods — these replace facade/orchestrator filtering logic: + + getAtRiskMembers(): MemberRegistryEntry[] { + // Deduplicate across all at-risk apps + const ids = new Set(); + for (const app of this.report) { + if (app.isAtRisk()) { + // memberRefs is a Record, iterate entries and filter for at-risk (value === true) + Object.entries(app.memberRefs).forEach(([id, isAtRisk]) => { + if (isAtRisk) ids.add(id as OrganizationUserId); + }); + } + } + return [...ids].map((id) => this.memberRegistry.get(id)).filter(Boolean); + } + + getCriticalApplications(): RiskInsightsReportView[] { + // OLD (O(n)): this.applications.find((a) => a.hostname === app.applicationName) + // NEW (O(1)): this.applicationData[app.applicationName] + return this.report.filter((app) => { + return this.applicationData[app.applicationName]?.isCritical === true; + }); + } + + getApplicationByHostname(hostname: string): RiskInsightsReportView | undefined { + return this.report.find((app) => app.applicationName === hostname); + } + + getNewApplications(): RiskInsightsReportView[] { + // OLD (O(n)): this.applications.find((a) => a.hostname === app.applicationName) + // NEW (O(1)): this.applicationData[app.applicationName] + return this.report.filter((app) => { + return this.applicationData[app.applicationName]?.reviewedDate === null; + }); + } + + getSummary(): RiskInsightsSummaryView { + return this.summary; + } +} +``` + +### Size Impact: Current vs Target + +#### Current (700MB+ for large orgs) + +**10K member org:** + +- `memberDetails`: 400 apps × 5,000 members × 180 bytes = **360MB** +- `atRiskMemberDetails`: 400 apps × 3,000 members × 180 bytes = **216MB** +- Cipher IDs + metadata: **~15MB** +- **Total unencrypted: ~591MB** +- **After encryption + Base64: ~786MB** + +#### Target (With Registry + Record Pattern) + +**10K member org:** + +- **MemberRegistry**: 10,000 members × 140 bytes (no cipherId) = **1.4MB** (stored once) +- **memberRefs**: 400 apps × 5,000 refs × 50 bytes (Record entry: `"id": false/true`) = **100MB** + - No separate atRiskMemberRefs needed - at-risk status is the boolean value +- **cipherRefs**: 400 apps × 100 ciphers × 50 bytes (Record entry: `"id": false/true`) = **2MB** + - No separate atRiskCipherIds array needed - at-risk status is the boolean value +- **applicationData** (as Record): 400 apps × 100 bytes = **0.04MB** (negligible) +- Metadata (counts, applicationName): **~10MB** +- **Total unencrypted: ~113MB** +- **After encryption + Base64: ~150MB** + +**Reduction: 786MB → 150MB = 81% smaller** 🎉 + +**Design Decision:** Use single `Record` for members, ciphers, AND Record for applicationData: + +- **memberRefs:** No duplicate member IDs, ~60MB saved (vs separate atRiskMemberDetails) +- **cipherRefs:** No duplicate cipher IDs, ~10MB saved (vs separate atRiskCipherIds array) +- **applicationData:** O(1) lookup, no functional size change but better performance +- **Trade-off:** Definitely worth it - saves ~70MB and prevents duplicate storage + +--- + +## 4. Storage Structure Comparison + +### Current Storage (What's in DB Today) + +```typescript +// Stored as RiskInsightsData in database +{ + id: OrganizationReportId, + creationDate: Date, + contentEncryptionKey: EncString, + + // ENCRYPTED FIELD 1: reportData (~700MB for large orgs) + reportData: [ + { + applicationName: "google.com", + cipherIds: ["cipher-id-1", "cipher-id-2", ...], // ~100 ciphers - ARRAY + atRiskCipherIds: ["cipher-id-1", ...], // ~50 at-risk - ARRAY (duplicates IDs from cipherIds) + memberDetails: [ // ~5,000 members - ARRAY + { userGuid: "abc", userName: "Alice", email: "alice@...", cipherId: "x" }, + { userGuid: "def", userName: "Bob", email: "bob@...", cipherId: "y" }, + // ... FULL member objects, deduplicated per app, duplicated across apps + ], + atRiskMemberDetails: [ // ~3,000 at-risk members - ARRAY (duplicates from memberDetails) + { userGuid: "abc", userName: "Alice", email: "alice@...", cipherId: "x" }, + // ... FULL member objects (subset of memberDetails) + ], + passwordCount: 100, + atRiskPasswordCount: 50, + memberCount: 5000, + atRiskMemberCount: 3000 + }, + // ... 400 applications + ], + + // ENCRYPTED FIELD 2: applicationData (~10KB) - ARRAY with O(n) lookup + applicationData: [ + { applicationName: "google.com", isCritical: true, reviewedDate: Date | null }, + // ... 400 applications + ], + + // ENCRYPTED FIELD 3: summaryData (~1KB) + summaryData: { + totalMemberCount: 10000, + totalApplicationCount: 400, + totalAtRiskMemberCount: 6000, + totalAtRiskApplicationCount: 300, + totalCriticalApplicationCount: 50, + totalCriticalMemberCount: 8000, + totalCriticalAtRiskMemberCount: 4500, + totalCriticalAtRiskApplicationCount: 40 + } +} +``` + +**Total size:** ~786MB encrypted for 10K member org +**Problem:** Member data duplicated across applications (360MB + 216MB = 576MB just for members) + +--- + +### Target Storage (With Member Registry) + +```typescript +// Stored as RiskInsightsData in database +{ + id: OrganizationReportId, + creationDate: Date, + contentEncryptionKey: EncString, + + // NEW: ENCRYPTED FIELD 0: memberRegistry (~1.4MB for 10K members) + memberRegistry: { + "abc": { userGuid: "abc", userName: "Alice", email: "alice@..." }, + "def": { userGuid: "def", userName: "Bob", email: "bob@..." }, + // ... 10,000 members stored ONCE + }, + + // ENCRYPTED FIELD 1: reportData (~116MB for 10K org - 80% reduction!) + reportData: [ + { + applicationName: "google.com", + cipherRefs: { // ~100 cipher IDs with at-risk flag - RECORD + "cipher-id-1": true, // at-risk + "cipher-id-2": false, // not at-risk + "cipher-id-3": true, // at-risk + // ... (no separate atRiskCipherIds array needed) + }, + memberRefs: { // ~5,000 member IDs with at-risk flag - RECORD + "abc": true, // at-risk member + "def": false, // not at-risk + "ghi": true, // at-risk member + // ... (no separate atRiskMemberRefs array needed) + }, + passwordCount: 100, + atRiskPasswordCount: 50, + memberCount: 5000, + atRiskMemberCount: 3000 + }, + // ... 400 applications + ], + + // ENCRYPTED FIELD 2: applicationData (~10KB) - RECORD with O(1) lookup + applicationData: { + "google.com": { isCritical: true, reviewedDate: new Date("2026-01-15") }, + "github.com": { isCritical: false, reviewedDate: null }, + "slack.com": { isCritical: true, reviewedDate: new Date("2026-02-01") } + // ... 400 applications as Record entries + }, + + // ENCRYPTED FIELD 3: summaryData (~1KB - unchanged) + summaryData: { + totalMemberCount: 10000, + totalApplicationCount: 400, + totalAtRiskMemberCount: 6000, + totalAtRiskApplicationCount: 300, + totalCriticalApplicationCount: 50, + totalCriticalMemberCount: 8000, + totalCriticalAtRiskMemberCount: 4500, + totalCriticalAtRiskApplicationCount: 40 + } +} +``` + +**Total size:** ~150MB encrypted for 10K member org (81% reduction) +**Benefits:** + +- Members stored once in registry, referenced by ID from applications +- Member and cipher IDs stored with at-risk flag (no duplicate arrays) +- ApplicationData as Record enables O(1) lookup instead of O(n) find operations + +--- + +### Design Decision: Single Record with Boolean Flag (for Members AND Ciphers) + +**Chosen approach:** Use single `Record` where the boolean indicates at-risk status for BOTH members and ciphers. + +```typescript +{ + applicationName: "google.com", + memberRefs: { + "abc": true, // at-risk member + "def": false, // not at-risk + "ghi": true // at-risk member + }, + cipherRefs: { + "cipher-1": true, // at-risk cipher + "cipher-2": false, // not at-risk + "cipher-3": true // at-risk cipher + } +} +``` + +**Pros:** + +- ✅ **Members:** No duplicate IDs (previously stored in both memberDetails AND atRiskMemberDetails) +- ✅ **Ciphers:** No duplicate IDs (previously stored in both cipherIds AND atRiskCipherIds) +- ✅ O(1) lookup for both membership/presence and at-risk status +- ✅ Automatic deduplication (can't have duplicate keys) +- ✅ Saves ~60MB for members + ~10MB for ciphers = **~70MB saved** compared to separate arrays +- ✅ Clear intent - one ID, one entry, one flag +- ✅ Consistent pattern across both members and ciphers + +**Cons:** + +- ⚠️ Slightly more complex iteration (need to check boolean value when filtering at-risk) +- ⚠️ Summary recalculation requires iterating entries instead of just counting keys + +**Trade-off Analysis:** + +- **Member size savings:** ~60MB (400 apps × 3K at-risk IDs × 50 bytes per duplicate entry) +- **Cipher size savings:** ~10MB (400 apps × 50 at-risk IDs × 50 bytes per duplicate entry) +- **Total savings:** ~70MB for 10K org +- **Performance:** Negligible - `Object.entries().filter()` is still O(n) like array iteration +- **Correctness:** Better - impossible to have ID in at-risk array but not in main array + +**Verdict:** Single Record with boolean flag is the clear winner for both members AND ciphers. + +--- + +## 5. Encryption Approaches (Current vs Future Options) + +### Current: Encrypt Whole Objects (No Compression) + +**How it works:** + +1. `JSON.stringify(reportData)` → encrypt → EncString (~700MB for large orgs) +2. `JSON.stringify(summaryData)` → encrypt → EncString (~1KB) +3. `JSON.stringify(applicationData)` → encrypt → EncString (~10KB) + +**Stored structure:** + +```typescript +{ + reportData: EncString, // ← Entire reportData[] array as one encrypted blob + summaryData: EncString, // ← Entire summary object as one encrypted blob + applicationData: EncString // ← Entire applicationData[] array as one encrypted blob + contentEncryptionKey: EncString, + id: OrganizationReportId, + creationDate: Date +} +``` + +**Problems:** + +- For large orgs (700MB+), may approach or exceed WASM encryption limits +- Must decrypt entire report to access any application +- Can't do field-level encryption with this approach + +--- + +### Option 1: Encrypt Per Top-Level Field (Current Approach) + +Encrypt `reportData`, `summaryData`, `applicationData` as separate EncStrings. + +**Pros:** + +- ✅ Allows decrypting summary without decrypting full report +- ✅ Simple encryption logic +- ✅ Separates metadata (summary, applicationData) from payload (reportData) + +**Cons:** + +- ❌ Can't access individual applications without decrypting entire report +- ❌ Can't do field-level encryption +- ❌ May hit WASM limits for very large orgs (700MB+ unencrypted) + +**Status:** This is what we have today. + +--- + +### Option 2: True Field-Level Encryption (Ideal) + +**Each field** within each object is encrypted separately, preserving JSON structure: + +```typescript +{ + memberRegistry: { + "abc": { + userGuid: EncString("abc"), + userName: EncString("Alice"), + email: EncString("alice@...") + }, + "def": { /* ... */ } + }, + reportData: [ + { + applicationName: EncString("google.com"), + cipherIds: [EncString("id1"), EncString("id2"), ...], + memberRefs: [EncString("abc"), EncString("def"), ...], + atRiskMemberRefs: [EncString("abc"), ...], + passwordCount: EncString("100"), + atRiskPasswordCount: EncString("50"), + memberCount: EncString("5000"), + atRiskMemberCount: EncString("3000") + }, + // ... each application + ], + summaryData: { + totalMemberCount: EncString("10000"), + totalApplicationCount: EncString("400"), + // ... each field encrypted + }, + applicationData: [ + { + applicationName: EncString("google.com"), + isCritical: EncString("true"), + reviewedDate: EncString("2026-02-10") + }, + // ... each application + ] +} +``` + +**Pros:** + +- ✅ Can decrypt individual fields on-demand +- ✅ Can access single application without decrypting all +- ✅ Each field is small enough for SDK (no size limits) +- ✅ Better for partial updates (re-encrypt only changed fields) +- ✅ Aligns with Bitwarden's data model architecture + +**Cons:** + +- ❌ More complex encryption/decryption logic +- ❌ Slightly larger overhead (each EncString has IV + metadata ~20 bytes) +- ❌ Requires updating all encryption/decryption code paths + +**Status:** **Can be implemented alongside member registry.** The member registry will reduce report size (making this easier), but field-level encryption is not blocked by it. + +**Size estimate with field-level encryption:** + +- Member registry (10K members): ~1.4MB unencrypted → ~2MB encrypted (each field encrypted) +- Report data: ~116MB unencrypted → ~145MB encrypted (overhead from EncString metadata) +- **Total: ~147MB** (vs ~154MB with whole-object encryption) + +Field-level encryption adds ~10MB overhead but enables partial decryption and avoids WASM limits. + +--- + +### Option 3: Compress Then Encrypt (Draft PR, Want to Avoid) + +Compress `reportData` before encrypting. `summaryData` and `applicationData` remain uncompressed (TBD if `applicationData` needs compression). + +**How it would work:** + +1. `JSON.stringify(reportData)` → compress with pako → encrypt → EncString +2. `JSON.stringify(summaryData)` → encrypt → EncString (no compression) +3. `JSON.stringify(applicationData)` → encrypt → EncString (compression TBD) + +**Pros:** + +- ✅ Stored object is as small as we can get it (compression reduces size by ~70%) +- ✅ Works for very large orgs without hitting WASM limits + +**Cons:** + +- ❌ Can't decrypt summary without decompressing everything if whole object is compressed +- ❌ Makes field-level encryption impossible (can't decrypt individual fields from compressed blob) +- ❌ More complex decryption logic (decompress → decrypt) +- ❌ Not the direction we want to go architecturally + +**Decision:** **Avoid if possible.** This was explored in a draft PR as a workaround, but we'd prefer to implement member registry (reduces size without compression) and move toward field-level encryption (Option 2). From 9b5f3a866dfd3f1aa0ed1b8d93de533ef252fa9f Mon Sep 17 00:00:00 2001 From: Zhaolin Liang Date: Wed, 11 Feb 2026 16:49:57 +0800 Subject: [PATCH 14/28] Fix lock vault from system tray not working (#18323) Co-authored-by: Bernd Schoolmann --- apps/desktop/src/app/app.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/desktop/src/app/app.component.ts b/apps/desktop/src/app/app.component.ts index 01eb8c728e5..fdd5012f5ee 100644 --- a/apps/desktop/src/app/app.component.ts +++ b/apps/desktop/src/app/app.component.ts @@ -273,7 +273,7 @@ export class AppComponent implements OnInit, OnDestroy { this.loading = false; break; case "lockVault": - await this.lockService.lock(message.userId); + await this.lockService.lock(message.userId ?? this.activeUserId); break; case "lockAllVaults": { await this.lockService.lockAll(); From d237994aae0e50d90bfddd5cd5b4c3b9ce898e6b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 11 Feb 2026 14:24:06 +0100 Subject: [PATCH 15/28] [deps] Platform: Update webpack to v5.104.1 [SECURITY] (#18797) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package-lock.json | 29 ++++++++++++++++++----------- package.json | 2 +- 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8f6a527627f..bf1b1798699 100644 --- a/package-lock.json +++ b/package-lock.json @@ -179,7 +179,7 @@ "url": "0.11.4", "util": "0.12.5", "wait-on": "9.0.3", - "webpack": "5.103.0", + "webpack": "5.104.1", "webpack-cli": "6.0.1", "webpack-dev-server": "5.2.2", "webpack-node-externals": "3.0.0" @@ -40910,9 +40910,9 @@ } }, "node_modules/terser-webpack-plugin": { - "version": "5.3.14", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.14.tgz", - "integrity": "sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==", + "version": "5.3.16", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.16.tgz", + "integrity": "sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q==", "dev": true, "license": "MIT", "dependencies": { @@ -43334,9 +43334,9 @@ } }, "node_modules/webpack": { - "version": "5.103.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.103.0.tgz", - "integrity": "sha512-HU1JOuV1OavsZ+mfigY0j8d1TgQgbZ6M+J75zDkpEAwYeXjWSqrGJtgnPblJjd/mAyTNQ7ygw0MiKOn6etz8yw==", + "version": "5.104.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.104.1.tgz", + "integrity": "sha512-Qphch25abbMNtekmEGJmeRUhLDbe+QfiWTiqpKYkpCOWY64v9eyl+KRRLmqOFA2AvKPpc9DC6+u2n76tQLBoaA==", "dev": true, "license": "MIT", "dependencies": { @@ -43348,10 +43348,10 @@ "@webassemblyjs/wasm-parser": "^1.14.1", "acorn": "^8.15.0", "acorn-import-phases": "^1.0.3", - "browserslist": "^4.26.3", + "browserslist": "^4.28.1", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.17.3", - "es-module-lexer": "^1.2.1", + "enhanced-resolve": "^5.17.4", + "es-module-lexer": "^2.0.0", "eslint-scope": "5.1.1", "events": "^3.2.0", "glob-to-regexp": "^0.4.1", @@ -43362,7 +43362,7 @@ "neo-async": "^2.6.2", "schema-utils": "^4.3.3", "tapable": "^2.3.0", - "terser-webpack-plugin": "^5.3.11", + "terser-webpack-plugin": "^5.3.16", "watchpack": "^2.4.4", "webpack-sources": "^3.3.3" }, @@ -44154,6 +44154,13 @@ "dev": true, "license": "MIT" }, + "node_modules/webpack/node_modules/es-module-lexer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", + "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", + "dev": true, + "license": "MIT" + }, "node_modules/webpack/node_modules/eslint-scope": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", diff --git a/package.json b/package.json index 751c67afcd1..f6cd3dd28c8 100644 --- a/package.json +++ b/package.json @@ -146,7 +146,7 @@ "url": "0.11.4", "util": "0.12.5", "wait-on": "9.0.3", - "webpack": "5.103.0", + "webpack": "5.104.1", "webpack-cli": "6.0.1", "webpack-dev-server": "5.2.2", "webpack-node-externals": "3.0.0" From e2710ee14aa6aec02cb00c0138dc38d017ac70ea Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Wed, 11 Feb 2026 15:34:24 +0100 Subject: [PATCH 16/28] [No ticket] Disable process reload on desktop during dev builds (#18905) * Disable process reload on desktop during dev builds * Fix linting --- apps/desktop/src/main/window.main.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/apps/desktop/src/main/window.main.ts b/apps/desktop/src/main/window.main.ts index b2008d57bcd..2872154aa44 100644 --- a/apps/desktop/src/main/window.main.ts +++ b/apps/desktop/src/main/window.main.ts @@ -55,6 +55,11 @@ export class WindowMain { // Perform a hard reload of the render process by crashing it. This is suboptimal but ensures that all memory gets // cleared, as the process itself will be completely garbage collected. ipcMain.on("reload-process", async () => { + if (isDev()) { + this.logService.info("Process reload requested, but skipping in development mode"); + return; + } + this.logService.info("Reloading render process"); // User might have changed theme, ensure the window is updated. this.win.setBackgroundColor(await this.getBackgroundColor()); From 952996099ad087e68cd66c65be8cab7560488e1b Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Wed, 11 Feb 2026 15:36:40 +0100 Subject: [PATCH 17/28] Remove duplicate import of VaultComponent (#18904) --- .../src/vault/popup/components/vault/vault.component.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/browser/src/vault/popup/components/vault/vault.component.spec.ts b/apps/browser/src/vault/popup/components/vault/vault.component.spec.ts index 55cb18ba637..70affd73ef3 100644 --- a/apps/browser/src/vault/popup/components/vault/vault.component.spec.ts +++ b/apps/browser/src/vault/popup/components/vault/vault.component.spec.ts @@ -53,7 +53,7 @@ import { BlockedInjectionBanner } from "./blocked-injection-banner/blocked-injec import { NewItemDropdownComponent } from "./new-item-dropdown/new-item-dropdown.component"; import { VaultHeaderComponent } from "./vault-header/vault-header.component"; import { VaultListItemsContainerComponent } from "./vault-list-items-container/vault-list-items-container.component"; -import { VaultComponent, VaultComponent } from "./vault.component"; +import { VaultComponent } from "./vault.component"; @Component({ selector: "popup-header", From 3c9569a90f491e702b4a01534d79ab168e76aaf0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20=C3=85berg?= Date: Wed, 11 Feb 2026 15:45:14 +0100 Subject: [PATCH 18/28] Downgrade open to 8.4.2 (#18459) --- apps/cli/package.json | 2 +- package-lock.json | 248 ++++++++---------------------------------- package.json | 2 +- 3 files changed, 47 insertions(+), 205 deletions(-) diff --git a/apps/cli/package.json b/apps/cli/package.json index 79653ec970f..40058bed16e 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -83,7 +83,7 @@ "multer": "2.0.2", "node-fetch": "2.7.0", "node-forge": "1.3.2", - "open": "11.0.0", + "open": "8.4.2", "papaparse": "5.5.3", "proper-lockfile": "4.1.2", "rxjs": "7.8.1", diff --git a/package-lock.json b/package-lock.json index bf1b1798699..680aad40adf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -55,7 +55,7 @@ "node-fetch": "2.7.0", "node-forge": "1.3.2", "oidc-client-ts": "2.4.1", - "open": "11.0.0", + "open": "8.4.2", "papaparse": "5.5.3", "proper-lockfile": "4.1.2", "qrcode-parser": "2.1.3", @@ -218,7 +218,7 @@ "multer": "2.0.2", "node-fetch": "2.7.0", "node-forge": "1.3.2", - "open": "11.0.0", + "open": "8.4.2", "papaparse": "5.5.3", "proper-lockfile": "4.1.2", "rxjs": "7.8.1", @@ -230,51 +230,6 @@ "bw": "build/bw.js" } }, - "apps/cli/node_modules/define-lazy-prop": { - "version": "2.0.0", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "apps/cli/node_modules/is-docker": { - "version": "2.2.1", - "license": "MIT", - "bin": { - "is-docker": "cli.js" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "apps/cli/node_modules/is-wsl": { - "version": "2.2.0", - "license": "MIT", - "dependencies": { - "is-docker": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "apps/cli/node_modules/open": { - "version": "8.4.2", - "license": "MIT", - "dependencies": { - "define-lazy-prop": "^2.0.0", - "is-docker": "^2.1.1", - "is-wsl": "^2.2.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "apps/desktop": { "name": "@bitwarden/desktop", "version": "2026.2.0", @@ -18920,63 +18875,6 @@ "node": ">=12.0.0" } }, - "node_modules/better-opn/node_modules/define-lazy-prop": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", - "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/better-opn/node_modules/is-docker": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", - "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", - "dev": true, - "license": "MIT", - "bin": { - "is-docker": "cli.js" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/better-opn/node_modules/is-wsl": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", - "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-docker": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/better-opn/node_modules/open": { - "version": "8.4.2", - "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", - "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-lazy-prop": "^2.0.0", - "is-docker": "^2.1.1", - "is-wsl": "^2.2.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/big-integer": { "version": "1.6.52", "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz", @@ -19384,6 +19282,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "dev": true, "license": "MIT", "dependencies": { "run-applescript": "^7.0.0" @@ -21648,6 +21547,7 @@ "version": "5.4.0", "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.4.0.tgz", "integrity": "sha512-XDuvSq38Hr1MdN47EDvYtx3U0MTqpCEn+F6ft8z2vYDzMrvQhVp0ui9oQdqW3MvK3vqUETglt1tVGgjLuJ5izg==", + "dev": true, "license": "MIT", "dependencies": { "bundle-name": "^4.1.0", @@ -21664,6 +21564,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.0.tgz", "integrity": "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==", + "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -21732,6 +21633,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -26848,6 +26750,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "dev": true, "license": "MIT", "bin": { "is-docker": "cli.js" @@ -26936,22 +26839,11 @@ "node": ">=0.10.0" } }, - "node_modules/is-in-ssh": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-in-ssh/-/is-in-ssh-1.0.0.tgz", - "integrity": "sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==", - "license": "MIT", - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/is-inside-container": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "dev": true, "license": "MIT", "dependencies": { "is-docker": "^3.0.0" @@ -27281,6 +27173,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", + "dev": true, "license": "MIT", "dependencies": { "is-inside-container": "^1.0.0" @@ -33968,16 +33861,6 @@ } } }, - "node_modules/nx/node_modules/define-lazy-prop": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", - "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/nx/node_modules/dotenv": { "version": "16.4.7", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", @@ -34001,35 +33884,6 @@ "node": ">= 4" } }, - "node_modules/nx/node_modules/is-docker": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", - "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", - "dev": true, - "license": "MIT", - "bin": { - "is-docker": "cli.js" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/nx/node_modules/is-wsl": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", - "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-docker": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/nx/node_modules/jsonc-parser": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", @@ -34037,24 +33891,6 @@ "dev": true, "license": "MIT" }, - "node_modules/nx/node_modules/open": { - "version": "8.4.2", - "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", - "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-lazy-prop": "^2.0.0", - "is-docker": "^2.1.1", - "is-wsl": "^2.2.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/nx/node_modules/ora": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/ora/-/ora-5.3.0.tgz", @@ -34629,41 +34465,58 @@ } }, "node_modules/open": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/open/-/open-11.0.0.tgz", - "integrity": "sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw==", + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", "license": "MIT", "dependencies": { - "default-browser": "^5.4.0", - "define-lazy-prop": "^3.0.0", - "is-in-ssh": "^1.0.0", - "is-inside-container": "^1.0.0", - "powershell-utils": "^0.1.0", - "wsl-utils": "^0.3.0" + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" }, "engines": { - "node": ">=20" + "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/open/node_modules/wsl-utils": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.3.0.tgz", - "integrity": "sha512-3sFIGLiaDP7rTO4xh3g+b3AzhYDIUGGywE/WsmqzJWDxus5aJXVnPTNC/6L+r2WzrwXqVOdD262OaO+cEyPMSQ==", + "node_modules/open/node_modules/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", "license": "MIT", - "dependencies": { - "is-wsl": "^3.1.0", - "powershell-utils": "^0.1.0" + "engines": { + "node": ">=8" + } + }, + "node_modules/open/node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" }, "engines": { - "node": ">=20" + "node": ">=8" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/open/node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/opencollective-postinstall": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/opencollective-postinstall/-/opencollective-postinstall-2.0.3.tgz", @@ -36653,18 +36506,6 @@ "node": "^12.20.0 || >=14" } }, - "node_modules/powershell-utils": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/powershell-utils/-/powershell-utils-0.1.0.tgz", - "integrity": "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==", - "license": "MIT", - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/prebuild-install": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", @@ -38066,6 +37907,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.0.0.tgz", "integrity": "sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==", + "dev": true, "license": "MIT", "engines": { "node": ">=18" diff --git a/package.json b/package.json index f6cd3dd28c8..c95d6af7437 100644 --- a/package.json +++ b/package.json @@ -193,7 +193,7 @@ "node-fetch": "2.7.0", "node-forge": "1.3.2", "oidc-client-ts": "2.4.1", - "open": "11.0.0", + "open": "8.4.2", "papaparse": "5.5.3", "proper-lockfile": "4.1.2", "qrcode-parser": "2.1.3", From 428a96902c77581b5338400527b4845694266877 Mon Sep 17 00:00:00 2001 From: Jason Ng Date: Wed, 11 Feb 2026 09:46:21 -0500 Subject: [PATCH 19/28] [PM-31679] remove archive from browser edit (#18854) * removing archive btns from browser edit form footer, remove archive items from showing in expired premium users vault --- .../vault/add-edit/add-edit.component.html | 18 --- .../vault/add-edit/add-edit.component.spec.ts | 105 ------------------ .../vault/add-edit/add-edit.component.ts | 46 -------- .../vault-popup-items.service.spec.ts | 2 +- .../services/vault-popup-items.service.ts | 17 ++- 5 files changed, 9 insertions(+), 179 deletions(-) diff --git a/apps/browser/src/vault/popup/components/vault/add-edit/add-edit.component.html b/apps/browser/src/vault/popup/components/vault/add-edit/add-edit.component.html index f8238a188e0..d4495cf4c92 100644 --- a/apps/browser/src/vault/popup/components/vault/add-edit/add-edit.component.html +++ b/apps/browser/src/vault/popup/components/vault/add-edit/add-edit.component.html @@ -41,24 +41,6 @@ - @if (isEditMode) { - @if ((archiveFlagEnabled$ | async) && isCipherArchived) { - - } - @if ((userCanArchive$ | async) && canCipherBeArchived) { - - } - } @if (canDeleteCipher$ | async) { + @if (!attachment.hasDecryptionError) { + + + + {{ attachment.fileName }} + + {{ attachment.sizeName }} + @if (attachment.key == null) { + } - - @if (cipher().edit) { + + + - + @if (attachment.key != null) { + + } @else { + + } - } - - + @if (cipher().edit) { + + + + } + + + } @else { + + + + {{ "errorCannotDecrypt" | i18n }} + + + + + @if (cipher().edit) { + + + + } + + + } } diff --git a/libs/vault/src/cipher-form/components/attachments/cipher-attachments.component.spec.ts b/libs/vault/src/cipher-form/components/attachments/cipher-attachments.component.spec.ts index 002ad019653..88ee1f9b599 100644 --- a/libs/vault/src/cipher-form/components/attachments/cipher-attachments.component.spec.ts +++ b/libs/vault/src/cipher-form/components/attachments/cipher-attachments.component.spec.ts @@ -173,7 +173,7 @@ describe("CipherAttachmentsComponent", () => { const fileSize = fixture.debugElement.query(By.css('[data-testid="file-size"]')); expect(fileName.nativeElement.textContent.trim()).toEqual(attachment.fileName); - expect(fileSize.nativeElement.textContent).toEqual(attachment.sizeName); + expect(fileSize.nativeElement.textContent.trim()).toEqual(attachment.sizeName); }); describe("bitSubmit", () => { diff --git a/libs/vault/src/cipher-view/attachments/attachments-v2-view.component.html b/libs/vault/src/cipher-view/attachments/attachments-v2-view.component.html index 0a46b83b086..c8110b9e863 100644 --- a/libs/vault/src/cipher-view/attachments/attachments-v2-view.component.html +++ b/libs/vault/src/cipher-view/attachments/attachments-v2-view.component.html @@ -5,8 +5,12 @@ - {{ attachment.fileName }} - {{ attachment.sizeName }} + + {{ getAttachmentFileName(attachment) }} + + @if (!attachment.hasDecryptionError) { + {{ attachment.sizeName }} + } diff --git a/libs/vault/src/cipher-view/attachments/attachments-v2-view.component.ts b/libs/vault/src/cipher-view/attachments/attachments-v2-view.component.ts index 4e324d8002e..3826d3a3ad0 100644 --- a/libs/vault/src/cipher-view/attachments/attachments-v2-view.component.ts +++ b/libs/vault/src/cipher-view/attachments/attachments-v2-view.component.ts @@ -8,9 +8,11 @@ import { NEVER, switchMap } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { StateProvider } from "@bitwarden/common/platform/state"; import { EmergencyAccessId, OrganizationId } from "@bitwarden/common/types/guid"; import { OrgKey } from "@bitwarden/common/types/key"; +import { AttachmentView } from "@bitwarden/common/vault/models/view/attachment.view"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { ItemModule, @@ -59,6 +61,7 @@ export class AttachmentsV2ViewComponent { private billingAccountProfileStateService: BillingAccountProfileStateService, private stateProvider: StateProvider, private accountService: AccountService, + private i18nService: I18nService, ) { this.subscribeToHasPremiumCheck(); this.subscribeToOrgKey(); @@ -89,4 +92,12 @@ export class AttachmentsV2ViewComponent { } }); } + + getAttachmentFileName(attachment: AttachmentView): string { + if (attachment.hasDecryptionError) { + return this.i18nService.t("errorCannotDecrypt"); + } + + return attachment.fileName ?? ""; + } } diff --git a/libs/vault/src/components/download-attachment/download-attachment.component.spec.ts b/libs/vault/src/components/download-attachment/download-attachment.component.spec.ts index a46ce28fca8..05d5e9bc276 100644 --- a/libs/vault/src/components/download-attachment/download-attachment.component.spec.ts +++ b/libs/vault/src/components/download-attachment/download-attachment.component.spec.ts @@ -36,12 +36,11 @@ describe("DownloadAttachmentComponent", () => { .mockResolvedValue({ url: "https://www.downloadattachement.com" }); const download = jest.fn(); - const attachment = { - id: "222-3333-4444", - url: "https://www.attachment.com", - fileName: "attachment-filename", - size: "1234", - } as AttachmentView; + const attachment = new AttachmentView(); + attachment.id = "222-3333-4444"; + attachment.url = "https://www.attachment.com"; + attachment.fileName = "attachment-filename"; + attachment.size = "1234"; const cipherView = { id: "5555-444-3333", @@ -123,7 +122,12 @@ describe("DownloadAttachmentComponent", () => { }); it("hides download button when the attachment has decryption failure", () => { - const decryptFailureAttachment = { ...attachment, fileName: DECRYPT_ERROR }; + const decryptFailureAttachment = new AttachmentView(); + decryptFailureAttachment.id = attachment.id; + decryptFailureAttachment.url = attachment.url; + decryptFailureAttachment.size = attachment.size; + decryptFailureAttachment.fileName = DECRYPT_ERROR; + fixture.componentRef.setInput("attachment", decryptFailureAttachment); fixture.detectChanges(); diff --git a/libs/vault/src/components/download-attachment/download-attachment.component.ts b/libs/vault/src/components/download-attachment/download-attachment.component.ts index 31ed609637c..bdca510c5aa 100644 --- a/libs/vault/src/components/download-attachment/download-attachment.component.ts +++ b/libs/vault/src/components/download-attachment/download-attachment.component.ts @@ -4,7 +4,6 @@ import { firstValueFrom } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { DECRYPT_ERROR } from "@bitwarden/common/key-management/crypto/models/enc-string"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -46,9 +45,7 @@ export class DownloadAttachmentComponent { private cipherService: CipherService, ) {} - protected readonly isDecryptionFailure = computed( - () => this.attachment().fileName === DECRYPT_ERROR, - ); + protected readonly isDecryptionFailure = computed(() => this.attachment().hasDecryptionError); /** Download the attachment */ download = async () => { From a9ccb421c441d79ca69e348c248144a6215299b3 Mon Sep 17 00:00:00 2001 From: Brad <44413459+lastbestdev@users.noreply.github.com> Date: Wed, 11 Feb 2026 09:00:14 -0800 Subject: [PATCH 26/28] [PM-30542] Conditionally render old Access Intelligence tabs, sub heading copy update (#18847) First step of removing code for old Access Intelligence tabs. The old tabs should not appear when the milestone 11 feature flag is on. Once flipped in Production, the remainder of this ticket can be completed (old code entirely removed) Also included in this change is a copy update for the sub heading of the page. --- apps/web/src/locales/en/messages.json | 4 +-- .../risk-insights.component.html | 30 ++++++++++--------- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index b86d88b42ad..59f5bc88419 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -47,8 +47,8 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, - "reviewAtRiskPasswords": { - "message": "Review at-risk passwords (weak, exposed, or reused) across applications. Select your most critical applications to prioritize security actions for your users to address at-risk passwords." + "reviewAccessIntelligence": { + "message": "Review security reports to find and fix credential risks before they escalate." }, "reviewAtRiskLoginsPrompt": { "message": "Review at-risk logins" diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/risk-insights.component.html b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/risk-insights.component.html index 1e58d334288..169c5d920ff 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/risk-insights.component.html +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/risk-insights.component.html @@ -46,7 +46,7 @@
@if (appsCount > 0) {
- {{ "reviewAtRiskPasswords" | i18n }} + {{ "reviewAccessIntelligence" | i18n }}
}
+ } @else { + + + + + + + {{ + "criticalApplicationsWithCount" + | i18n + : (dataService.criticalReportResults$ | async)?.reportData?.length ?? 0 + }} + + + } - - - - - - - {{ - "criticalApplicationsWithCount" - | i18n: (dataService.criticalReportResults$ | async)?.reportData?.length ?? 0 - }} - - -
From ea55aaaede0b917e6e582613503a3b5fbd278496 Mon Sep 17 00:00:00 2001 From: Leslie Xiong Date: Wed, 11 Feb 2026 12:21:33 -0500 Subject: [PATCH 27/28] [CL-1022] Update Berry Styles (#18799) * created 'berry' component * added 'bit-berry' to 'popup-tab-navigation' * simplified - removed null checks * changed 'effectiveSize' to 'computedSize' * fixed 'accentPrimary' color * updated to not render berry if 'count' is 0 or negative number * simplified checking count undefined * updated computed padding * switched from `[ngClass]` to `[class]` * updated 'popup-tab-navigation' berry to use 'danger' variant * fixed berry positioning in popup-tab-navigation * updated content logic * cleanup unused 'ngClass' * updated conditional rendering of berry * updated story 'Usage' * updates with adding berry 'type' * added type "status" to popup-tab-navigation * fixed type error * updated 'Count Behavior' description --- .../popup-tab-navigation.component.html | 4 +- .../layout/popup-tab-navigation.component.ts | 4 +- .../components/src/berry/berry.component.html | 3 + libs/components/src/berry/berry.component.ts | 80 +++++++++ libs/components/src/berry/berry.mdx | 48 +++++ libs/components/src/berry/berry.stories.ts | 167 ++++++++++++++++++ libs/components/src/berry/index.ts | 1 + libs/components/src/index.ts | 1 + libs/components/tailwind.config.base.js | 1 + 9 files changed, 305 insertions(+), 4 deletions(-) create mode 100644 libs/components/src/berry/berry.component.html create mode 100644 libs/components/src/berry/berry.component.ts create mode 100644 libs/components/src/berry/berry.mdx create mode 100644 libs/components/src/berry/berry.stories.ts create mode 100644 libs/components/src/berry/index.ts diff --git a/apps/browser/src/platform/popup/layout/popup-tab-navigation.component.html b/apps/browser/src/platform/popup/layout/popup-tab-navigation.component.html index e04d302ea2c..919d01f2d51 100644 --- a/apps/browser/src/platform/popup/layout/popup-tab-navigation.component.html +++ b/apps/browser/src/platform/popup/layout/popup-tab-navigation.component.html @@ -27,8 +27,8 @@ {{ button.label | i18n }} -
-
+
+
diff --git a/apps/browser/src/platform/popup/layout/popup-tab-navigation.component.ts b/apps/browser/src/platform/popup/layout/popup-tab-navigation.component.ts index 5a40b72daff..f873d25641b 100644 --- a/apps/browser/src/platform/popup/layout/popup-tab-navigation.component.ts +++ b/apps/browser/src/platform/popup/layout/popup-tab-navigation.component.ts @@ -5,7 +5,7 @@ import { RouterModule } from "@angular/router"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { BitSvg } from "@bitwarden/assets/svg"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { SvgModule, LinkModule } from "@bitwarden/components"; +import { SvgModule, LinkModule, BerryComponent } from "@bitwarden/components"; export type NavButton = { label: string; @@ -20,7 +20,7 @@ export type NavButton = { @Component({ selector: "popup-tab-navigation", templateUrl: "popup-tab-navigation.component.html", - imports: [CommonModule, LinkModule, RouterModule, JslibModule, SvgModule], + imports: [CommonModule, LinkModule, RouterModule, JslibModule, SvgModule, BerryComponent], host: { class: "tw-block tw-size-full tw-flex tw-flex-col", }, diff --git a/libs/components/src/berry/berry.component.html b/libs/components/src/berry/berry.component.html new file mode 100644 index 00000000000..2a05f534843 --- /dev/null +++ b/libs/components/src/berry/berry.component.html @@ -0,0 +1,3 @@ +@if (type() === "status" || content()) { + {{ content() }} +} diff --git a/libs/components/src/berry/berry.component.ts b/libs/components/src/berry/berry.component.ts new file mode 100644 index 00000000000..8e58b888f39 --- /dev/null +++ b/libs/components/src/berry/berry.component.ts @@ -0,0 +1,80 @@ +import { ChangeDetectionStrategy, Component, computed, input } from "@angular/core"; + +export type BerryVariant = + | "primary" + | "subtle" + | "success" + | "warning" + | "danger" + | "accentPrimary" + | "contrast"; + +/** + * The berry component is a compact visual indicator used to display short, + * supplemental status information about another element, + * like a navigation item, button, or icon button. + * They draw users’ attention to status changes or new notifications. + * + * > `NOTE:` The maximum displayed value is 999. If the value is over 999, a “+” character is appended to indicate more. + */ +@Component({ + selector: "bit-berry", + templateUrl: "berry.component.html", + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class BerryComponent { + protected readonly variant = input("primary"); + protected readonly value = input(); + protected readonly type = input<"status" | "count">("count"); + + protected readonly content = computed(() => { + const value = this.value(); + const type = this.type(); + + if (type === "status" || !value || value < 0) { + return undefined; + } + return value > 999 ? "999+" : `${value}`; + }); + + protected readonly textColor = computed(() => { + return this.variant() === "contrast" ? "tw-text-fg-dark" : "tw-text-fg-white"; + }); + + protected readonly padding = computed(() => { + return (this.value()?.toString().length ?? 0) > 2 ? "tw-px-1.5 tw-py-0.5" : ""; + }); + + protected readonly containerClasses = computed(() => { + const baseClasses = [ + "tw-inline-flex", + "tw-items-center", + "tw-justify-center", + "tw-align-middle", + "tw-text-xxs", + "tw-rounded-full", + ]; + + const typeClasses = { + status: ["tw-h-2", "tw-w-2"], + count: ["tw-h-4", "tw-min-w-4", this.padding()], + }; + + const variantClass = { + primary: "tw-bg-bg-brand", + subtle: "tw-bg-bg-contrast", + success: "tw-bg-bg-success", + warning: "tw-bg-bg-warning", + danger: "tw-bg-bg-danger", + accentPrimary: "tw-bg-fg-accent-primary-strong", + contrast: "tw-bg-bg-white", + }; + + return [ + ...baseClasses, + ...typeClasses[this.type()], + variantClass[this.variant()], + this.textColor(), + ].join(" "); + }); +} diff --git a/libs/components/src/berry/berry.mdx b/libs/components/src/berry/berry.mdx new file mode 100644 index 00000000000..b79ed35cac8 --- /dev/null +++ b/libs/components/src/berry/berry.mdx @@ -0,0 +1,48 @@ +import { Meta, Canvas, Primary, Controls, Title, Description } from "@storybook/addon-docs/blocks"; + +import * as stories from "./berry.stories"; + + + +```ts +import { BerryComponent } from "@bitwarden/components"; +``` + + +<Description /> + +<Primary /> +<Controls /> + +## Usage + +### Status + +- Use a status berry to indicate a new notification of a status change that is not related to a + specific count. + +<Canvas of={stories.statusType} /> + +### Count + +- Use a count berry with text to indicate item count information for multiple new notifications. + +<Canvas of={stories.countType} /> + +### All Variants + +<Canvas of={stories.AllVariants} /> + +## Count Behavior + +- Counts of **1-99**: Display in a compact circular shape +- Counts of **100-999**: Display in a pill shape with padding +- Counts **over 999**: Display as "999+" to prevent overflow + +## Accessibility + +- Use berries as **supplemental visual indicators** alongside descriptive text +- Ensure sufficient color contrast with surrounding elements +- For screen readers, provide appropriate labels on parent elements that describe the berry's + meaning +- Berries are decorative; important information should not rely solely on the berry color diff --git a/libs/components/src/berry/berry.stories.ts b/libs/components/src/berry/berry.stories.ts new file mode 100644 index 00000000000..0b71e7259d8 --- /dev/null +++ b/libs/components/src/berry/berry.stories.ts @@ -0,0 +1,167 @@ +import { Meta, moduleMetadata, StoryObj } from "@storybook/angular"; + +import { BerryComponent } from "./berry.component"; + +export default { + title: "Component Library/Berry", + component: BerryComponent, + decorators: [ + moduleMetadata({ + imports: [BerryComponent], + }), + ], + args: { + type: "count", + variant: "primary", + value: 5, + }, + argTypes: { + type: { + control: "select", + options: ["status", "count"], + description: "The type of the berry, which determines its size and content", + table: { + category: "Inputs", + type: { summary: '"status" | "count"' }, + defaultValue: { summary: '"count"' }, + }, + }, + variant: { + control: "select", + options: ["primary", "subtle", "success", "warning", "danger", "accentPrimary", "contrast"], + description: "The visual style variant of the berry", + table: { + category: "Inputs", + type: { summary: "BerryVariant" }, + defaultValue: { summary: "primary" }, + }, + }, + value: { + control: "number", + description: + "Optional value to display for berries with type 'count'. Maximum displayed is 999, values above show '999+'. If undefined, a small small berry is shown. If 0 or negative, the berry is hidden.", + table: { + category: "Inputs", + type: { summary: "number | undefined" }, + defaultValue: { summary: "undefined" }, + }, + }, + }, + parameters: { + design: { + type: "figma", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/branch/rKUVGKb7Kw3d6YGoQl6Ho7/Tailwind-Component-Library?node-id=38367-199458&p=f&m=dev", + }, + }, +} as Meta<BerryComponent>; + +type Story = StoryObj<BerryComponent>; + +export const Primary: Story = { + render: (args) => ({ + props: args, + template: `<bit-berry [type]="type" [variant]="variant" [value]="value"></bit-berry>`, + }), +}; + +export const statusType: Story = { + render: (args) => ({ + props: args, + template: ` + <div class="tw-flex tw-items-center tw-gap-4"> + <bit-berry [type]="'status'" variant="primary"></bit-berry> + <bit-berry [type]="'status'" variant="subtle"></bit-berry> + <bit-berry [type]="'status'" variant="success"></bit-berry> + <bit-berry [type]="'status'" variant="warning"></bit-berry> + <bit-berry [type]="'status'" variant="danger"></bit-berry> + <bit-berry [type]="'status'" variant="accentPrimary"></bit-berry> + <bit-berry [type]="'status'" variant="contrast"></bit-berry> + </div> + `, + }), +}; + +export const countType: Story = { + render: (args) => ({ + props: args, + template: ` + <div class="tw-flex tw-items-center tw-gap-4"> + <bit-berry [value]="5"></bit-berry> + <bit-berry [value]="50"></bit-berry> + <bit-berry [value]="500"></bit-berry> + <bit-berry [value]="5000"></bit-berry> + </div> + `, + }), +}; + +export const AllVariants: Story = { + render: () => ({ + template: ` + <div class="tw-flex tw-flex-col tw-gap-4"> + <div class="tw-flex tw-items-center tw-gap-4"> + <span class="tw-w-20">Primary:</span> + <bit-berry type="status" variant="primary"></bit-berry> + <bit-berry variant="primary" [value]="5"></bit-berry> + <bit-berry variant="primary" [value]="50"></bit-berry> + <bit-berry variant="primary" [value]="500"></bit-berry> + <bit-berry variant="primary" [value]="5000"></bit-berry> + </div> + + <div class="tw-flex tw-items-center tw-gap-4"> + <span class="tw-w-20">Subtle:</span> + <bit-berry type="status"variant="subtle"></bit-berry> + <bit-berry variant="subtle" [value]="5"></bit-berry> + <bit-berry variant="subtle" [value]="50"></bit-berry> + <bit-berry variant="subtle" [value]="500"></bit-berry> + <bit-berry variant="subtle" [value]="5000"></bit-berry> + </div> + + <div class="tw-flex tw-items-center tw-gap-4"> + <span class="tw-w-20">Success:</span> + <bit-berry type="status" variant="success"></bit-berry> + <bit-berry variant="success" [value]="5"></bit-berry> + <bit-berry variant="success" [value]="50"></bit-berry> + <bit-berry variant="success" [value]="500"></bit-berry> + <bit-berry variant="success" [value]="5000"></bit-berry> + </div> + + <div class="tw-flex tw-items-center tw-gap-4"> + <span class="tw-w-20">Warning:</span> + <bit-berry type="status" variant="warning"></bit-berry> + <bit-berry variant="warning" [value]="5"></bit-berry> + <bit-berry variant="warning" [value]="50"></bit-berry> + <bit-berry variant="warning" [value]="500"></bit-berry> + <bit-berry variant="warning" [value]="5000"></bit-berry> + </div> + + <div class="tw-flex tw-items-center tw-gap-4"> + <span class="tw-w-20">Danger:</span> + <bit-berry type="status" variant="danger"></bit-berry> + <bit-berry variant="danger" [value]="5"></bit-berry> + <bit-berry variant="danger" [value]="50"></bit-berry> + <bit-berry variant="danger" [value]="500"></bit-berry> + <bit-berry variant="danger" [value]="5000"></bit-berry> + </div> + + <div class="tw-flex tw-items-center tw-gap-4"> + <span class="tw-w-20">Accent primary:</span> + <bit-berry type="status" variant="accentPrimary"></bit-berry> + <bit-berry variant="accentPrimary" [value]="5"></bit-berry> + <bit-berry variant="accentPrimary" [value]="50"></bit-berry> + <bit-berry variant="accentPrimary" [value]="500"></bit-berry> + <bit-berry variant="accentPrimary" [value]="5000"></bit-berry> + </div> + + <div class="tw-flex tw-items-center tw-gap-4 tw-bg-bg-dark"> + <span class="tw-w-20 tw-text-fg-white">Contrast:</span> + <bit-berry type="status" variant="contrast"></bit-berry> + <bit-berry variant="contrast" [value]="5"></bit-berry> + <bit-berry variant="contrast" [value]="50"></bit-berry> + <bit-berry variant="contrast" [value]="500"></bit-berry> + <bit-berry variant="contrast" [value]="5000"></bit-berry> + </div> + </div> + `, + }), +}; diff --git a/libs/components/src/berry/index.ts b/libs/components/src/berry/index.ts new file mode 100644 index 00000000000..8f85908653e --- /dev/null +++ b/libs/components/src/berry/index.ts @@ -0,0 +1 @@ +export * from "./berry.component"; diff --git a/libs/components/src/index.ts b/libs/components/src/index.ts index d92e0770e49..d0bb8576095 100644 --- a/libs/components/src/index.ts +++ b/libs/components/src/index.ts @@ -7,6 +7,7 @@ export * from "./avatar"; export * from "./badge-list"; export * from "./badge"; export * from "./banner"; +export * from "./berry"; export * from "./breadcrumbs"; export * from "./button"; export * from "./callout"; diff --git a/libs/components/tailwind.config.base.js b/libs/components/tailwind.config.base.js index d8220c39ff8..5de00fac34f 100644 --- a/libs/components/tailwind.config.base.js +++ b/libs/components/tailwind.config.base.js @@ -317,6 +317,7 @@ module.exports = { base: ["1rem", "150%"], sm: ["0.875rem", "150%"], xs: [".75rem", "150%"], + xxs: [".5rem", "150%"], }, container: { "@5xl": "1100px", From bd3f8dd4c13717b771620a1f30b420c973518a57 Mon Sep 17 00:00:00 2001 From: Daniel Riera <driera@livefront.com> Date: Wed, 11 Feb 2026 12:24:02 -0500 Subject: [PATCH 28/28] [PM-29519]Remove @ts-strict-ignore in browser-fido2-user-interface.service.ts (#18691) --- .../fido2/services/browser-fido2-user-interface.service.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/apps/browser/src/autofill/fido2/services/browser-fido2-user-interface.service.ts b/apps/browser/src/autofill/fido2/services/browser-fido2-user-interface.service.ts index e0ab45e9f84..19c1dbc8790 100644 --- a/apps/browser/src/autofill/fido2/services/browser-fido2-user-interface.service.ts +++ b/apps/browser/src/autofill/fido2/services/browser-fido2-user-interface.service.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { BehaviorSubject, EmptyError, @@ -79,7 +77,7 @@ export type BrowserFido2Message = { sessionId: string } & ( } | { type: typeof BrowserFido2MessageTypes.PickCredentialResponse; - cipherId?: string; + cipherId: string; userVerified: boolean; } | {