From 11d3f5247ce287f4d766ab8d733ec0d82010e401 Mon Sep 17 00:00:00 2001 From: Mick Letofsky Date: Tue, 28 Oct 2025 19:00:56 +0100 Subject: [PATCH 01/25] Refactor canClone method to use CipherAuthorizationService (#16849) --- .../vault-items/vault-items.component.html | 2 +- .../vault-items/vault-items.component.ts | 49 ++--- .../restricted-item-types.service.spec.ts | 168 ++++++++++++++++++ 3 files changed, 185 insertions(+), 34 deletions(-) diff --git a/apps/web/src/app/vault/components/vault-items/vault-items.component.html b/apps/web/src/app/vault/components/vault-items/vault-items.component.html index 23400a7d782..d6b5fafe6ec 100644 --- a/apps/web/src/app/vault/components/vault-items/vault-items.component.html +++ b/apps/web/src/app/vault/components/vault-items/vault-items.component.html @@ -162,7 +162,7 @@ [showPremiumFeatures]="showPremiumFeatures" [useEvents]="useEvents" [viewingOrgVault]="viewingOrgVault" - [cloneable]="canClone(item)" + [cloneable]="canClone$(item) | async" [organizations]="allOrganizations" [collections]="allCollections" [checked]="selection.isSelected(item)" diff --git a/apps/web/src/app/vault/components/vault-items/vault-items.component.ts b/apps/web/src/app/vault/components/vault-items/vault-items.component.ts index 9ea4c209009..3ab643927f1 100644 --- a/apps/web/src/app/vault/components/vault-items/vault-items.component.ts +++ b/apps/web/src/app/vault/components/vault-items/vault-items.component.ts @@ -2,7 +2,7 @@ // @ts-strict-ignore import { SelectionModel } from "@angular/cdk/collections"; import { Component, EventEmitter, Input, Output } from "@angular/core"; -import { toSignal, takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { Observable, combineLatest, map, of, startWith, switchMap } from "rxjs"; import { CollectionView, Unassigned, CollectionAdminView } from "@bitwarden/admin-console/common"; @@ -111,8 +111,6 @@ export class VaultItemsComponent { // eslint-disable-next-line @angular-eslint/prefer-signals @Input() enforceOrgDataOwnershipPolicy: boolean; - private readonly restrictedPolicies = toSignal(this.restrictedItemTypesService.restricted$); - private _ciphers?: C[] = []; // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals // eslint-disable-next-line @angular-eslint/prefer-signals @@ -390,37 +388,22 @@ export class VaultItemsComponent { }); } - // TODO: PM-13944 Refactor to use cipherAuthorizationService.canClone$ instead - protected canClone(vaultItem: VaultItem) { - // This will check for restrictions from org policies before allowing cloning. - const isItemRestricted = this.restrictedPolicies().some( - (rt) => rt.cipherType === CipherViewLikeUtils.getType(vaultItem.cipher), + protected canClone$(vaultItem: VaultItem): Observable { + return this.restrictedItemTypesService.restricted$.pipe( + switchMap((restrictedTypes) => { + // This will check for restrictions from org policies before allowing cloning. + const isItemRestricted = restrictedTypes.some( + (rt) => rt.cipherType === CipherViewLikeUtils.getType(vaultItem.cipher), + ); + if (isItemRestricted) { + return of(false); + } + return this.cipherAuthorizationService.canCloneCipher$( + vaultItem.cipher, + this.showAdminActions, + ); + }), ); - if (isItemRestricted) { - return false; - } - - if (vaultItem.cipher.organizationId == null) { - return true; - } - - const org = this.allOrganizations.find((o) => o.id === vaultItem.cipher.organizationId); - - // Admins and custom users can always clone in the Org Vault - if (this.viewingOrgVault && (org.isAdmin || org.permissions.editAnyCollection)) { - return true; - } - - // Check if the cipher belongs to a collection with canManage permission - const orgCollections = this.allCollections.filter((c) => c.organizationId === org.id); - - for (const collection of orgCollections) { - if (vaultItem.cipher.collectionIds.includes(collection.id as any) && collection.manage) { - return true; - } - } - - return false; } protected canEditCipher(cipher: C) { diff --git a/libs/common/src/vault/services/restricted-item-types.service.spec.ts b/libs/common/src/vault/services/restricted-item-types.service.spec.ts index 3ae68d47c5c..c16a91d0884 100644 --- a/libs/common/src/vault/services/restricted-item-types.service.spec.ts +++ b/libs/common/src/vault/services/restricted-item-types.service.spec.ts @@ -12,6 +12,8 @@ import { Utils } from "@bitwarden/common/platform/misc/utils"; import { UserId } from "@bitwarden/common/types/guid"; import { CipherType } from "@bitwarden/common/vault/enums"; +import { CipherLike } from "../types/cipher-like"; + import { RestrictedItemTypesService, RestrictedCipherType } from "./restricted-item-types.service"; describe("RestrictedItemTypesService", () => { @@ -130,4 +132,170 @@ describe("RestrictedItemTypesService", () => { { cipherType: CipherType.Identity, allowViewOrgIds: ["org1"] }, ]); }); + + describe("isCipherRestricted", () => { + it("returns false when cipher type is not in restricted types", () => { + const cipher: CipherLike = { + type: CipherType.Login, + organizationId: "Pete the Cat", + } as CipherLike; + const restrictedTypes: RestrictedCipherType[] = [ + { cipherType: CipherType.Card, allowViewOrgIds: [] }, + ]; + + const result = service.isCipherRestricted(cipher, restrictedTypes); + + expect(result).toBe(false); + }); + + it("returns false when restricted types array is empty", () => { + const cipher: CipherLike = { type: CipherType.Card, organizationId: "org1" } as CipherLike; + const restrictedTypes: RestrictedCipherType[] = []; + + const result = service.isCipherRestricted(cipher, restrictedTypes); + + expect(result).toBe(false); + }); + + it("returns false when cipher type does not match any restricted types", () => { + const cipher: CipherLike = { + type: CipherType.SecureNote, + organizationId: "org1", + } as CipherLike; + const restrictedTypes: RestrictedCipherType[] = [ + { cipherType: CipherType.Card, allowViewOrgIds: [] }, + { cipherType: CipherType.Login, allowViewOrgIds: [] }, + { cipherType: CipherType.Identity, allowViewOrgIds: [] }, + ]; + + const result = service.isCipherRestricted(cipher, restrictedTypes); + + expect(result).toBe(false); + }); + + it("returns true for personal cipher when type is restricted", () => { + const cipher: CipherLike = { type: CipherType.Card, organizationId: null } as CipherLike; + const restrictedTypes: RestrictedCipherType[] = [ + { cipherType: CipherType.Card, allowViewOrgIds: ["org1"] }, + ]; + + const result = service.isCipherRestricted(cipher, restrictedTypes); + + expect(result).toBe(true); + }); + + it("returns true for personal cipher with undefined organizationId when type is restricted", () => { + const cipher: CipherLike = { + type: CipherType.Login, + organizationId: undefined, + } as CipherLike; + const restrictedTypes: RestrictedCipherType[] = [ + { cipherType: CipherType.Login, allowViewOrgIds: ["org1", "org2"] }, + ]; + + const result = service.isCipherRestricted(cipher, restrictedTypes); + + expect(result).toBe(true); + }); + + it("returns true for personal cipher regardless of allowViewOrgIds content", () => { + const cipher: CipherLike = { type: CipherType.Identity, organizationId: null } as CipherLike; + const restrictedTypes: RestrictedCipherType[] = [ + { cipherType: CipherType.Identity, allowViewOrgIds: [] }, + ]; + + const result = service.isCipherRestricted(cipher, restrictedTypes); + + expect(result).toBe(true); + }); + + it("returns false when organization is in allowViewOrgIds", () => { + const cipher: CipherLike = { type: CipherType.Card, organizationId: "org1" } as CipherLike; + const restrictedTypes: RestrictedCipherType[] = [ + { cipherType: CipherType.Card, allowViewOrgIds: ["org1"] }, + ]; + + const result = service.isCipherRestricted(cipher, restrictedTypes); + + expect(result).toBe(false); + }); + + it("returns false when organization is among multiple allowViewOrgIds", () => { + const cipher: CipherLike = { type: CipherType.Login, organizationId: "org2" } as CipherLike; + const restrictedTypes: RestrictedCipherType[] = [ + { cipherType: CipherType.Login, allowViewOrgIds: ["org1", "org2", "org3"] }, + ]; + + const result = service.isCipherRestricted(cipher, restrictedTypes); + + expect(result).toBe(false); + }); + + it("returns false when type is restricted globally but cipher org allows it", () => { + const cipher: CipherLike = { type: CipherType.Card, organizationId: "org2" } as CipherLike; + const restrictedTypes: RestrictedCipherType[] = [ + { cipherType: CipherType.Card, allowViewOrgIds: ["org2"] }, + ]; + + const result = service.isCipherRestricted(cipher, restrictedTypes); + + expect(result).toBe(false); + }); + + it("returns true when organization is not in allowViewOrgIds", () => { + const cipher: CipherLike = { type: CipherType.Card, organizationId: "org3" } as CipherLike; + const restrictedTypes: RestrictedCipherType[] = [ + { cipherType: CipherType.Card, allowViewOrgIds: ["org1", "org2"] }, + ]; + + const result = service.isCipherRestricted(cipher, restrictedTypes); + + expect(result).toBe(true); + }); + + it("returns true when allowViewOrgIds is empty for org cipher", () => { + const cipher: CipherLike = { type: CipherType.Login, organizationId: "org1" } as CipherLike; + const restrictedTypes: RestrictedCipherType[] = [ + { cipherType: CipherType.Login, allowViewOrgIds: [] }, + ]; + + const result = service.isCipherRestricted(cipher, restrictedTypes); + + expect(result).toBe(true); + }); + + it("returns true when cipher org differs from all allowViewOrgIds", () => { + const cipher: CipherLike = { + type: CipherType.Identity, + organizationId: "org5", + } as CipherLike; + const restrictedTypes: RestrictedCipherType[] = [ + { cipherType: CipherType.Identity, allowViewOrgIds: ["org1", "org2", "org3", "org4"] }, + ]; + + const result = service.isCipherRestricted(cipher, restrictedTypes); + + expect(result).toBe(true); + }); + }); + + describe("isCipherRestricted$", () => { + it("returns true when cipher is restricted by policy", async () => { + policyService.policiesByType$.mockReturnValue(of([policyOrg1])); + const cipher: CipherLike = { type: CipherType.Card, organizationId: null } as CipherLike; + + const result = await firstValueFrom(service.isCipherRestricted$(cipher)); + + expect(result).toBe(true); + }); + + it("returns false when cipher is not restricted", async () => { + policyService.policiesByType$.mockReturnValue(of([policyOrg1])); + const cipher: CipherLike = { type: CipherType.Login, organizationId: "org2" } as CipherLike; + + const result = await firstValueFrom(service.isCipherRestricted$(cipher)); + + expect(result).toBe(false); + }); + }); }); From 185c912c620d66892b69e4d1ccfbe04de3fcbc35 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 28 Oct 2025 14:46:17 -0400 Subject: [PATCH 02/25] [deps]: Update peter-evans/repository-dispatch action to v4 (#16848) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/test-browser-interactions.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-browser-interactions.yml b/.github/workflows/test-browser-interactions.yml index a5b92563f5a..fb31a93d51f 100644 --- a/.github/workflows/test-browser-interactions.yml +++ b/.github/workflows/test-browser-interactions.yml @@ -73,7 +73,7 @@ jobs: - name: Trigger test-all workflow in browser-interactions-testing if: steps.changed-files.outputs.monitored == 'true' - uses: peter-evans/repository-dispatch@ff45666b9427631e3450c54a1bcbee4d9ff4d7c0 # v3.0.0 + uses: peter-evans/repository-dispatch@5fc4efd1a4797ddb68ffd0714a238564e4cc0e6f # v4.0.0 with: token: ${{ steps.app-token.outputs.token }} repository: "bitwarden/browser-interactions-testing" From fe26826369f65cca5480fc15fe38688a252e4cf1 Mon Sep 17 00:00:00 2001 From: Daniel Riera Date: Tue, 28 Oct 2025 14:47:49 -0400 Subject: [PATCH 03/25] PM-27366 drop scss and convert to vanilla css (#17046) --- .../bootstrap-autofill-inline-menu-button.ts | 5 +---- .../pages/button/{button.scss => button.css} | 14 ++++++-------- 2 files changed, 7 insertions(+), 12 deletions(-) rename apps/browser/src/autofill/overlay/inline-menu/pages/button/{button.scss => button.css} (74%) diff --git a/apps/browser/src/autofill/overlay/inline-menu/pages/button/bootstrap-autofill-inline-menu-button.ts b/apps/browser/src/autofill/overlay/inline-menu/pages/button/bootstrap-autofill-inline-menu-button.ts index 36ef3897c56..dffacce0ffc 100644 --- a/apps/browser/src/autofill/overlay/inline-menu/pages/button/bootstrap-autofill-inline-menu-button.ts +++ b/apps/browser/src/autofill/overlay/inline-menu/pages/button/bootstrap-autofill-inline-menu-button.ts @@ -1,10 +1,7 @@ import { AutofillOverlayElement } from "../../../../enums/autofill-overlay.enum"; import { AutofillInlineMenuButton } from "./autofill-inline-menu-button"; - -// FIXME: Remove when updating file. Eslint update -// eslint-disable-next-line @typescript-eslint/no-require-imports -require("./button.scss"); +import "./button.css"; (function () { globalThis.customElements.define(AutofillOverlayElement.Button, AutofillInlineMenuButton); diff --git a/apps/browser/src/autofill/overlay/inline-menu/pages/button/button.scss b/apps/browser/src/autofill/overlay/inline-menu/pages/button/button.css similarity index 74% rename from apps/browser/src/autofill/overlay/inline-menu/pages/button/button.scss rename to apps/browser/src/autofill/overlay/inline-menu/pages/button/button.css index 64e54179893..a1fce6f14da 100644 --- a/apps/browser/src/autofill/overlay/inline-menu/pages/button/button.scss +++ b/apps/browser/src/autofill/overlay/inline-menu/pages/button/button.css @@ -1,5 +1,3 @@ -@import "../../../../shared/styles/variables"; - * { box-sizing: border-box; } @@ -27,10 +25,10 @@ autofill-inline-menu-button { border: none; background: transparent; cursor: pointer; - - .inline-menu-button-svg-icon { - display: block; - width: 100%; - height: auto; - } +} + +.inline-menu-button .inline-menu-button-svg-icon { + display: block; + width: 100%; + height: auto; } From af1809222f1cc7719b9119363bcd95c21a7ce324 Mon Sep 17 00:00:00 2001 From: Dave <3836813+enmande@users.noreply.github.com> Date: Tue, 28 Oct 2025 15:14:48 -0400 Subject: [PATCH 04/25] fix(input-password-component) [PM-24266]: Update modal title. (#16932) --- .../auth/src/angular/input-password/input-password.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/auth/src/angular/input-password/input-password.component.ts b/libs/auth/src/angular/input-password/input-password.component.ts index 019a9e3975e..62294f037a0 100644 --- a/libs/auth/src/angular/input-password/input-password.component.ts +++ b/libs/auth/src/angular/input-password/input-password.component.ts @@ -564,7 +564,7 @@ export class InputPasswordComponent implements OnInit { } } else if (passwordIsWeak) { const userAcceptedDialog = await this.dialogService.openSimpleDialog({ - title: { key: "weakMasterPasswordDesc" }, + title: { key: "weakMasterPassword" }, content: { key: "weakMasterPasswordDesc" }, type: "warning", }); From 69d5c533ef40b510c93a32fc61bef3166c6030ae Mon Sep 17 00:00:00 2001 From: Mick Letofsky Date: Tue, 28 Oct 2025 20:19:41 +0100 Subject: [PATCH 05/25] Implement Claude respond reusable workflow (#17079) --- .github/workflows/respond.yml | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 .github/workflows/respond.yml diff --git a/.github/workflows/respond.yml b/.github/workflows/respond.yml new file mode 100644 index 00000000000..d940ceee756 --- /dev/null +++ b/.github/workflows/respond.yml @@ -0,0 +1,28 @@ +name: Respond + +on: + issue_comment: + types: [created] + pull_request_review_comment: + types: [created] + issues: + types: [opened, assigned] + pull_request_review: + types: [submitted] + +permissions: {} + +jobs: + respond: + name: Respond + uses: bitwarden/gh-actions/.github/workflows/_respond.yml@main + secrets: + AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} + AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} + permissions: + actions: read + contents: write + id-token: write + issues: write + pull-requests: write From ff30df3dd6244e2ed5a1605c179e7c4d33bb1edc Mon Sep 17 00:00:00 2001 From: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com> Date: Tue, 28 Oct 2025 20:28:34 +0100 Subject: [PATCH 06/25] [PM-19300] Session timeout policy (#16583) * Session timeout policy * default "custom" is 8 hours, validation fixes * ownership update * default max allowed timeout is not selected * adjusting defaults, fixing backwards compatibility, skip type confirmation dialog when switching between the never and on system lock * unit test coverage * wording update, custom hours, minutes jumping on errors * wording update * wrong session timeout action dropdown label * show dialog as valid when opened first time, use @for loop, use controls instead of get * dialog static opener * easier to understand type value listener * unit tests * explicit maximum allowed timeout required error * eslint revert --- .github/CODEOWNERS | 1 + apps/web/src/locales/en/messages.json | 46 +- .../policies/policy-edit-definitions/index.ts | 1 - .../maximum-vault-timeout.component.html | 32 -- .../maximum-vault-timeout.component.ts | 79 ---- .../policies/policy-edit-register.ts | 4 +- ...-timeout-confirmation-never.component.html | 38 ++ ...meout-confirmation-never.component.spec.ts | 79 ++++ ...on-timeout-confirmation-never.component.ts | 18 + .../policies/session-timeout.component.html | 39 ++ .../session-timeout.component.spec.ts | 441 ++++++++++++++++++ .../policies/session-timeout.component.ts | 197 ++++++++ 12 files changed, 853 insertions(+), 122 deletions(-) delete mode 100644 bitwarden_license/bit-web/src/app/admin-console/policies/policy-edit-definitions/maximum-vault-timeout.component.html delete mode 100644 bitwarden_license/bit-web/src/app/admin-console/policies/policy-edit-definitions/maximum-vault-timeout.component.ts create mode 100644 bitwarden_license/bit-web/src/app/key-management/policies/session-timeout-confirmation-never.component.html create mode 100644 bitwarden_license/bit-web/src/app/key-management/policies/session-timeout-confirmation-never.component.spec.ts create mode 100644 bitwarden_license/bit-web/src/app/key-management/policies/session-timeout-confirmation-never.component.ts create mode 100644 bitwarden_license/bit-web/src/app/key-management/policies/session-timeout.component.html create mode 100644 bitwarden_license/bit-web/src/app/key-management/policies/session-timeout.component.spec.ts create mode 100644 bitwarden_license/bit-web/src/app/key-management/policies/session-timeout.component.ts diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 8affac3387b..676c4b4657b 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -174,6 +174,7 @@ apps/desktop/src/key-management @bitwarden/team-key-management-dev apps/web/src/app/key-management @bitwarden/team-key-management-dev apps/browser/src/key-management @bitwarden/team-key-management-dev apps/cli/src/key-management @bitwarden/team-key-management-dev +bitwarden_license/bit-web/src/app/key-management @bitwarden/team-key-management-dev libs/key-management @bitwarden/team-key-management-dev libs/key-management-ui @bitwarden/team-key-management-dev libs/common/src/key-management @bitwarden/team-key-management-dev diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 72ca4d73976..aa0353e754d 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -6495,17 +6495,32 @@ "tdeDisabledMasterPasswordRequired": { "message": "Your organization has updated your decryption options. Please set a master password to access your vault." }, - "maximumVaultTimeout": { - "message": "Vault timeout" + "sessionTimeoutPolicyTitle": { + "message": "Session timeout" }, - "maximumVaultTimeoutDesc": { - "message": "Set a maximum vault timeout for members." + "sessionTimeoutPolicyDescription": { + "message": "Set a maximum session timeout for all members except owners." }, - "maximumVaultTimeoutLabel": { - "message": "Maximum vault timeout" + "maximumAllowedTimeout": { + "message": "Maximum allowed timeout" }, - "invalidMaximumVaultTimeout": { - "message": "Invalid maximum vault timeout." + "maximumAllowedTimeoutRequired": { + "message": "Maximum allowed timeout is required." + }, + "sessionTimeoutPolicyInvalidTime": { + "message": "Time is invalid. Change at least one value." + }, + "sessionTimeoutAction": { + "message": "Session timeout action" + }, + "immediately": { + "message": "Immediately" + }, + "onSystemLock": { + "message": "On system lock" + }, + "onAppRestart": { + "message": "On app restart" }, "hours": { "message": "Hours" @@ -6513,6 +6528,21 @@ "minutes": { "message": "Minutes" }, + "sessionTimeoutConfirmationNeverTitle": { + "message": "Are you certain you want to allow a maximum timeout of \"Never\" for all members?" + }, + "sessionTimeoutConfirmationNeverDescription": { + "message": "This option will save your members' encryption keys on their devices. If you choose this option, ensure that their devices are adequately protected." + }, + "learnMoreAboutDeviceProtection": { + "message": "Learn more about device protection" + }, + "sessionTimeoutConfirmationOnSystemLockTitle": { + "message": "\"System lock\" will only apply to the browser and desktop app" + }, + "sessionTimeoutConfirmationOnSystemLockDescription": { + "message": "The mobile and web app will use \"on app restart\" as their maximum allowed timeout, since the option is not supported." + }, "vaultTimeoutPolicyInEffect": { "message": "Your organization policies have set your maximum allowed vault timeout to $HOURS$ hour(s) and $MINUTES$ minute(s).", "placeholders": { diff --git a/bitwarden_license/bit-web/src/app/admin-console/policies/policy-edit-definitions/index.ts b/bitwarden_license/bit-web/src/app/admin-console/policies/policy-edit-definitions/index.ts index 8c4be2eeea1..52325eae160 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/policies/policy-edit-definitions/index.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/policies/policy-edit-definitions/index.ts @@ -1,4 +1,3 @@ export { ActivateAutofillPolicy } from "./activate-autofill.component"; export { AutomaticAppLoginPolicy } from "./automatic-app-login.component"; export { DisablePersonalVaultExportPolicy } from "./disable-personal-vault-export.component"; -export { MaximumVaultTimeoutPolicy } from "./maximum-vault-timeout.component"; diff --git a/bitwarden_license/bit-web/src/app/admin-console/policies/policy-edit-definitions/maximum-vault-timeout.component.html b/bitwarden_license/bit-web/src/app/admin-console/policies/policy-edit-definitions/maximum-vault-timeout.component.html deleted file mode 100644 index deb72cfb3b5..00000000000 --- a/bitwarden_license/bit-web/src/app/admin-console/policies/policy-edit-definitions/maximum-vault-timeout.component.html +++ /dev/null @@ -1,32 +0,0 @@ - - {{ "requireSsoPolicyReq" | i18n }} - - - - - {{ "turnOn" | i18n }} - - -
-
- - {{ "maximumVaultTimeoutLabel" | i18n }} - - {{ "hours" | i18n }} - - - - {{ "minutes" | i18n }} - - - {{ "vaultTimeoutAction" | i18n }} - - - - -
-
diff --git a/bitwarden_license/bit-web/src/app/admin-console/policies/policy-edit-definitions/maximum-vault-timeout.component.ts b/bitwarden_license/bit-web/src/app/admin-console/policies/policy-edit-definitions/maximum-vault-timeout.component.ts deleted file mode 100644 index 277388e2883..00000000000 --- a/bitwarden_license/bit-web/src/app/admin-console/policies/policy-edit-definitions/maximum-vault-timeout.component.ts +++ /dev/null @@ -1,79 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { Component } from "@angular/core"; -import { FormBuilder, FormControl } from "@angular/forms"; - -import { PolicyType } from "@bitwarden/common/admin-console/enums"; -import { PolicyRequest } from "@bitwarden/common/admin-console/models/request/policy.request"; -import { VaultTimeoutAction } from "@bitwarden/common/key-management/vault-timeout"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { - BasePolicyEditDefinition, - BasePolicyEditComponent, -} from "@bitwarden/web-vault/app/admin-console/organizations/policies"; -import { SharedModule } from "@bitwarden/web-vault/app/shared"; - -export class MaximumVaultTimeoutPolicy extends BasePolicyEditDefinition { - name = "maximumVaultTimeout"; - description = "maximumVaultTimeoutDesc"; - type = PolicyType.MaximumVaultTimeout; - component = MaximumVaultTimeoutPolicyComponent; -} - -// 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: "maximum-vault-timeout.component.html", - imports: [SharedModule], -}) -export class MaximumVaultTimeoutPolicyComponent extends BasePolicyEditComponent { - vaultTimeoutActionOptions: { name: string; value: string }[]; - data = this.formBuilder.group({ - hours: new FormControl(null), - minutes: new FormControl(null), - action: new FormControl(null), - }); - - constructor( - private formBuilder: FormBuilder, - private i18nService: I18nService, - ) { - super(); - this.vaultTimeoutActionOptions = [ - { name: i18nService.t("userPreference"), value: null }, - { name: i18nService.t(VaultTimeoutAction.Lock), value: VaultTimeoutAction.Lock }, - { name: i18nService.t(VaultTimeoutAction.LogOut), value: VaultTimeoutAction.LogOut }, - ]; - } - - protected loadData() { - const minutes = this.policyResponse.data?.minutes; - const action = this.policyResponse.data?.action; - - this.data.patchValue({ - hours: minutes ? Math.floor(minutes / 60) : null, - minutes: minutes ? minutes % 60 : null, - action: action, - }); - } - - protected buildRequestData() { - if (this.data.value.hours == null && this.data.value.minutes == null) { - return null; - } - - return { - minutes: this.data.value.hours * 60 + this.data.value.minutes, - action: this.data.value.action, - }; - } - - async buildRequest(): Promise { - const request = await super.buildRequest(); - if (request.data?.minutes == null || request.data?.minutes <= 0) { - throw new Error(this.i18nService.t("invalidMaximumVaultTimeout")); - } - - return request; - } -} diff --git a/bitwarden_license/bit-web/src/app/admin-console/policies/policy-edit-register.ts b/bitwarden_license/bit-web/src/app/admin-console/policies/policy-edit-register.ts index 3438e706f10..015b4fc17be 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/policies/policy-edit-register.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/policies/policy-edit-register.ts @@ -4,12 +4,12 @@ import { } from "@bitwarden/web-vault/app/admin-console/organizations/policies"; import { FreeFamiliesSponsorshipPolicy } from "../../billing/policies/free-families-sponsorship.component"; +import { SessionTimeoutPolicy } from "../../key-management/policies/session-timeout.component"; import { ActivateAutofillPolicy, AutomaticAppLoginPolicy, DisablePersonalVaultExportPolicy, - MaximumVaultTimeoutPolicy, } from "./policy-edit-definitions"; /** @@ -18,7 +18,7 @@ import { * It will not appear in the web vault when running in OSS mode. */ const policyEditRegister: BasePolicyEditDefinition[] = [ - new MaximumVaultTimeoutPolicy(), + new SessionTimeoutPolicy(), new DisablePersonalVaultExportPolicy(), new FreeFamiliesSponsorshipPolicy(), new ActivateAutofillPolicy(), diff --git a/bitwarden_license/bit-web/src/app/key-management/policies/session-timeout-confirmation-never.component.html b/bitwarden_license/bit-web/src/app/key-management/policies/session-timeout-confirmation-never.component.html new file mode 100644 index 00000000000..2b718990c30 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/key-management/policies/session-timeout-confirmation-never.component.html @@ -0,0 +1,38 @@ + +
+ +

+ {{ "sessionTimeoutConfirmationNeverTitle" | i18n }} +

+
+ + +

{{ "sessionTimeoutConfirmationNeverDescription" | i18n }}

+ + {{ "learnMoreAboutDeviceProtection" | i18n }} + + +
+ +
+ + +
+
diff --git a/bitwarden_license/bit-web/src/app/key-management/policies/session-timeout-confirmation-never.component.spec.ts b/bitwarden_license/bit-web/src/app/key-management/policies/session-timeout-confirmation-never.component.spec.ts new file mode 100644 index 00000000000..332a0e323a7 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/key-management/policies/session-timeout-confirmation-never.component.spec.ts @@ -0,0 +1,79 @@ +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { NoopAnimationsModule } from "@angular/platform-browser/animations"; +import { mock } from "jest-mock-extended"; + +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { DialogRef, DialogService } from "@bitwarden/components"; + +import { SessionTimeoutConfirmationNeverComponent } from "./session-timeout-confirmation-never.component"; + +describe("SessionTimeoutConfirmationNeverComponent", () => { + let component: SessionTimeoutConfirmationNeverComponent; + let fixture: ComponentFixture; + let mockDialogRef: jest.Mocked; + + const mockI18nService = mock(); + const mockDialogService = mock(); + + beforeEach(async () => { + mockDialogRef = mock(); + mockI18nService.t.mockImplementation((key) => `${key}-used-i18n`); + + await TestBed.configureTestingModule({ + imports: [SessionTimeoutConfirmationNeverComponent, NoopAnimationsModule], + providers: [ + { provide: DialogRef, useValue: mockDialogRef }, + { provide: I18nService, useValue: mockI18nService }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(SessionTimeoutConfirmationNeverComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it("should create", () => { + expect(component).toBeTruthy(); + }); + + describe("open", () => { + it("should call dialogService.open with correct parameters", () => { + const mockResult = mock(); + mockDialogService.open.mockReturnValue(mockResult); + + const result = SessionTimeoutConfirmationNeverComponent.open(mockDialogService); + + expect(mockDialogService.open).toHaveBeenCalledWith( + SessionTimeoutConfirmationNeverComponent, + { + disableClose: true, + }, + ); + expect(result).toBe(mockResult); + }); + }); + + describe("button clicks", () => { + it("should close dialog with true when Yes button is clicked", () => { + const yesButton = fixture.nativeElement.querySelector( + 'button[buttonType="primary"]', + ) as HTMLButtonElement; + + yesButton.click(); + + expect(mockDialogRef.close).toHaveBeenCalledWith(true); + expect(yesButton.textContent?.trim()).toBe("yes-used-i18n"); + }); + + it("should close dialog with false when No button is clicked", () => { + const noButton = fixture.nativeElement.querySelector( + 'button[buttonType="secondary"]', + ) as HTMLButtonElement; + + noButton.click(); + + expect(mockDialogRef.close).toHaveBeenCalledWith(false); + expect(noButton.textContent?.trim()).toBe("no-used-i18n"); + }); + }); +}); diff --git a/bitwarden_license/bit-web/src/app/key-management/policies/session-timeout-confirmation-never.component.ts b/bitwarden_license/bit-web/src/app/key-management/policies/session-timeout-confirmation-never.component.ts new file mode 100644 index 00000000000..a909baf1c77 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/key-management/policies/session-timeout-confirmation-never.component.ts @@ -0,0 +1,18 @@ +import { Component } from "@angular/core"; + +import { DialogRef, DialogService } from "@bitwarden/components"; +import { SharedModule } from "@bitwarden/web-vault/app/shared"; + +@Component({ + imports: [SharedModule], + templateUrl: "./session-timeout-confirmation-never.component.html", +}) +export class SessionTimeoutConfirmationNeverComponent { + constructor(public dialogRef: DialogRef) {} + + static open(dialogService: DialogService) { + return dialogService.open(SessionTimeoutConfirmationNeverComponent, { + disableClose: true, + }); + } +} diff --git a/bitwarden_license/bit-web/src/app/key-management/policies/session-timeout.component.html b/bitwarden_license/bit-web/src/app/key-management/policies/session-timeout.component.html new file mode 100644 index 00000000000..22e9e07bea7 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/key-management/policies/session-timeout.component.html @@ -0,0 +1,39 @@ + + {{ "requireSsoPolicyReq" | i18n }} + + + + + {{ "turnOn" | i18n }} + + +
+
+ + {{ "maximumAllowedTimeout" | i18n }} + + @for (option of typeOptions; track option.value) { + + } + + + @if (data.value.type === "custom") { + + {{ "hours" | i18n }} + + + + {{ "minutes" | i18n }} + + + } + + {{ "sessionTimeoutAction" | i18n }} + + @for (option of actionOptions; track option.value) { + + } + + +
+
diff --git a/bitwarden_license/bit-web/src/app/key-management/policies/session-timeout.component.spec.ts b/bitwarden_license/bit-web/src/app/key-management/policies/session-timeout.component.spec.ts new file mode 100644 index 00000000000..694b0f1d1a2 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/key-management/policies/session-timeout.component.spec.ts @@ -0,0 +1,441 @@ +import { DialogCloseOptions } from "@angular/cdk/dialog"; +import { DebugElement } from "@angular/core"; +import { ComponentFixture, TestBed, fakeAsync, tick } from "@angular/core/testing"; +import { FormBuilder, ReactiveFormsModule } from "@angular/forms"; +import { By } from "@angular/platform-browser"; +import { mock } from "jest-mock-extended"; +import { Observable, of } from "rxjs"; + +import { PolicyResponse } from "@bitwarden/common/admin-console/models/response/policy.response"; +import { VaultTimeoutAction } from "@bitwarden/common/key-management/vault-timeout"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { DialogRef, DialogService } from "@bitwarden/components"; + +import { SessionTimeoutConfirmationNeverComponent } from "./session-timeout-confirmation-never.component"; +import { + SessionTimeoutAction, + SessionTimeoutPolicyComponent, + SessionTimeoutType, +} from "./session-timeout.component"; + +// Mock DialogRef, so we can mock "readonly closed" property. +class MockDialogRef extends DialogRef { + close(result: unknown | undefined, options: DialogCloseOptions | undefined): void {} + + closed: Observable = of(); + componentInstance: unknown | null; + disableClose: boolean | undefined; + isDrawer: boolean = false; +} + +describe("SessionTimeoutPolicyComponent", () => { + let component: SessionTimeoutPolicyComponent; + let fixture: ComponentFixture; + + const mockI18nService = mock(); + const mockDialogService = mock(); + const mockDialogRef = mock(); + + beforeEach(async () => { + jest.resetAllMocks(); + + mockDialogRef.closed = of(true); + mockDialogService.open.mockReturnValue(mockDialogRef); + mockDialogService.openSimpleDialog.mockResolvedValue(true); + + mockI18nService.t.mockImplementation((key) => `${key}-used-i18n`); + + const testBed = TestBed.configureTestingModule({ + imports: [SessionTimeoutPolicyComponent, ReactiveFormsModule], + providers: [FormBuilder, { provide: I18nService, useValue: mockI18nService }], + }); + + // Override DialogService provided from SharedModule (which includes DialogModule) + testBed.overrideProvider(DialogService, { useValue: mockDialogService }); + + await testBed.compileComponents(); + + fixture = TestBed.createComponent(SessionTimeoutPolicyComponent); + component = fixture.componentInstance; + }); + + function assertHoursAndMinutesInputsNotVisible() { + const hoursInput = fixture.nativeElement.querySelector('input[formControlName="hours"]'); + const minutesInput = fixture.nativeElement.querySelector('input[formControlName="minutes"]'); + + expect(hoursInput).toBeFalsy(); + expect(minutesInput).toBeFalsy(); + } + + function assertHoursAndMinutesInputs(expectedHours: string, expectedMinutes: string) { + const hoursInput = fixture.nativeElement.querySelector('input[formControlName="hours"]'); + const minutesInput = fixture.nativeElement.querySelector('input[formControlName="minutes"]'); + + expect(hoursInput).toBeTruthy(); + expect(minutesInput).toBeTruthy(); + expect(hoursInput.disabled).toBe(false); + expect(minutesInput.disabled).toBe(false); + expect(hoursInput.value).toBe(expectedHours); + expect(minutesInput.value).toBe(expectedMinutes); + } + + function setPolicyResponseType(type: SessionTimeoutType) { + component.policyResponse = new PolicyResponse({ + Data: { + type, + minutes: 480, + action: null, + }, + }); + } + + describe("initialization and data loading", () => { + function assertTypeAndActionSelectElementsVisible() { + // Type and action selects should always be present + const typeSelectDebug: DebugElement = fixture.debugElement.query( + By.css('bit-select[formControlName="type"]'), + ); + const actionSelectDebug: DebugElement = fixture.debugElement.query( + By.css('bit-select[formControlName="action"]'), + ); + + expect(typeSelectDebug).toBeTruthy(); + expect(actionSelectDebug).toBeTruthy(); + } + + it("should initialize with default state when policy have no value", () => { + component.policyResponse = undefined; + + fixture.detectChanges(); + + expect(component.data.controls.type.value).toBeNull(); + expect(component.data.controls.type.hasError("required")).toBe(true); + expect(component.data.controls.hours.value).toBe(8); + expect(component.data.controls.hours.disabled).toBe(true); + expect(component.data.controls.minutes.value).toBe(0); + expect(component.data.controls.minutes.disabled).toBe(true); + expect(component.data.controls.action.value).toBeNull(); + + assertTypeAndActionSelectElementsVisible(); + assertHoursAndMinutesInputsNotVisible(); + }); + + // This is for backward compatibility when type field did not exist + it("should load as custom type when type field does not exist but minutes does", () => { + component.policyResponse = new PolicyResponse({ + Data: { + minutes: 500, + action: VaultTimeoutAction.Lock, + }, + }); + + fixture.detectChanges(); + + expect(component.data.controls.type.value).toBe("custom"); + expect(component.data.controls.hours.value).toBe(8); + expect(component.data.controls.hours.disabled).toBe(false); + expect(component.data.controls.minutes.value).toBe(20); + expect(component.data.controls.minutes.disabled).toBe(false); + expect(component.data.controls.action.value).toBe(VaultTimeoutAction.Lock); + + assertTypeAndActionSelectElementsVisible(); + assertHoursAndMinutesInputs("8", "20"); + }); + + it.each([ + ["never", null], + ["never", VaultTimeoutAction.Lock], + ["never", VaultTimeoutAction.LogOut], + ["onAppRestart", null], + ["onAppRestart", VaultTimeoutAction.Lock], + ["onAppRestart", VaultTimeoutAction.LogOut], + ["onSystemLock", null], + ["onSystemLock", VaultTimeoutAction.Lock], + ["onSystemLock", VaultTimeoutAction.LogOut], + ["immediately", null], + ["immediately", VaultTimeoutAction.Lock], + ["immediately", VaultTimeoutAction.LogOut], + ["custom", null], + ["custom", VaultTimeoutAction.Lock], + ["custom", VaultTimeoutAction.LogOut], + ])("should load correctly when policy type is %s and action is %s", (type, action) => { + component.policyResponse = new PolicyResponse({ + Data: { + type, + minutes: 510, + action, + }, + }); + + fixture.detectChanges(); + + expect(component.data.controls.type.value).toBe(type); + expect(component.data.controls.action.value).toBe(action); + + assertTypeAndActionSelectElementsVisible(); + + if (type === "custom") { + expect(component.data.controls.hours.value).toBe(8); + expect(component.data.controls.minutes.value).toBe(30); + expect(component.data.controls.hours.disabled).toBe(false); + expect(component.data.controls.minutes.disabled).toBe(false); + + assertHoursAndMinutesInputs("8", "30"); + } else { + expect(component.data.controls.hours.disabled).toBe(true); + expect(component.data.controls.minutes.disabled).toBe(true); + + assertHoursAndMinutesInputsNotVisible(); + } + }); + + it("should have all type options and update form control when value changes", fakeAsync(() => { + expect(component.typeOptions.length).toBe(5); + expect(component.typeOptions[0].value).toBe("immediately"); + expect(component.typeOptions[1].value).toBe("custom"); + expect(component.typeOptions[2].value).toBe("onSystemLock"); + expect(component.typeOptions[3].value).toBe("onAppRestart"); + expect(component.typeOptions[4].value).toBe("never"); + })); + + it("should have all action options and update form control when value changes", () => { + expect(component.actionOptions.length).toBe(3); + expect(component.actionOptions[0].value).toBeNull(); + expect(component.actionOptions[1].value).toBe(VaultTimeoutAction.Lock); + expect(component.actionOptions[2].value).toBe(VaultTimeoutAction.LogOut); + }); + }); + + describe("form controls change detection", () => { + it.each(["never", "onAppRestart", "onSystemLock", "immediately"])( + "should disable hours and minutes inputs when type changes from custom to %s", + fakeAsync((newType: SessionTimeoutType) => { + setPolicyResponseType("custom"); + fixture.detectChanges(); + + expect(component.data.controls.hours.value).toBe(8); + expect(component.data.controls.minutes.value).toBe(0); + expect(component.data.controls.hours.disabled).toBe(false); + expect(component.data.controls.minutes.disabled).toBe(false); + + component.data.patchValue({ type: newType }); + tick(); + fixture.detectChanges(); + + expect(component.data.controls.hours.disabled).toBe(true); + expect(component.data.controls.minutes.disabled).toBe(true); + + assertHoursAndMinutesInputsNotVisible(); + }), + ); + + it.each(["never", "onAppRestart", "onSystemLock", "immediately"])( + "should enable hours and minutes inputs when type changes from %s to custom", + fakeAsync((oldType: SessionTimeoutType) => { + setPolicyResponseType(oldType); + fixture.detectChanges(); + + expect(component.data.controls.hours.disabled).toBe(true); + expect(component.data.controls.minutes.disabled).toBe(true); + + component.data.patchValue({ type: "custom", hours: 8, minutes: 1 }); + tick(); + fixture.detectChanges(); + + expect(component.data.controls.hours.value).toBe(8); + expect(component.data.controls.minutes.value).toBe(1); + expect(component.data.controls.hours.disabled).toBe(false); + expect(component.data.controls.minutes.disabled).toBe(false); + + assertHoursAndMinutesInputs("8", "1"); + }), + ); + + it.each(["custom", "onAppRestart", "immediately"])( + "should not show confirmation dialog when changing to %s type", + fakeAsync((newType: SessionTimeoutType) => { + setPolicyResponseType(null); + fixture.detectChanges(); + + component.data.patchValue({ type: newType }); + tick(); + fixture.detectChanges(); + + expect(mockDialogService.open).not.toHaveBeenCalled(); + expect(mockDialogService.openSimpleDialog).not.toHaveBeenCalled(); + }), + ); + + it("should show never confirmation dialog when changing to never type", fakeAsync(() => { + setPolicyResponseType(null); + fixture.detectChanges(); + + component.data.patchValue({ type: "never" }); + tick(); + fixture.detectChanges(); + + expect(mockDialogService.open).toHaveBeenCalledWith( + SessionTimeoutConfirmationNeverComponent, + { + disableClose: true, + }, + ); + expect(mockDialogService.openSimpleDialog).not.toHaveBeenCalled(); + })); + + it("should show simple confirmation dialog when changing to onSystemLock type", fakeAsync(() => { + setPolicyResponseType(null); + fixture.detectChanges(); + + component.data.patchValue({ type: "onSystemLock" }); + tick(); + fixture.detectChanges(); + + expect(mockDialogService.openSimpleDialog).toHaveBeenCalledWith({ + type: "info", + title: { key: "sessionTimeoutConfirmationOnSystemLockTitle" }, + content: { key: "sessionTimeoutConfirmationOnSystemLockDescription" }, + acceptButtonText: { key: "continue" }, + cancelButtonText: { key: "cancel" }, + }); + expect(mockDialogService.open).not.toHaveBeenCalled(); + expect(component.data.controls.type.value).toBe("onSystemLock"); + })); + + it("should revert to previous type when type changed to never and dialog not confirmed", fakeAsync(() => { + mockDialogRef.closed = of(false); + setPolicyResponseType("immediately"); + fixture.detectChanges(); + + component.data.patchValue({ type: "never" }); + tick(); + fixture.detectChanges(); + + expect(mockDialogService.open).toHaveBeenCalled(); + expect(mockDialogService.openSimpleDialog).not.toHaveBeenCalled(); + expect(component.data.controls.type.value).toBe("immediately"); + })); + + it("should revert to previous type when type changed to onSystemLock and dialog not confirmed", fakeAsync(() => { + mockDialogService.openSimpleDialog.mockResolvedValue(false); + setPolicyResponseType("immediately"); + fixture.detectChanges(); + + component.data.patchValue({ type: "onSystemLock" }); + tick(); + fixture.detectChanges(); + + expect(mockDialogService.openSimpleDialog).toHaveBeenCalled(); + expect(mockDialogService.open).not.toHaveBeenCalled(); + expect(component.data.controls.type.value).toBe("immediately"); + })); + + it("should revert to last confirmed type when canceling multiple times", fakeAsync(() => { + mockDialogRef.closed = of(false); + mockDialogService.openSimpleDialog.mockResolvedValue(false); + + setPolicyResponseType("custom"); + fixture.detectChanges(); + + // First attempt: custom -> never (cancel) + component.data.patchValue({ type: "never" }); + tick(); + fixture.detectChanges(); + + expect(component.data.controls.type.value).toBe("custom"); + + // Second attempt: custom -> onSystemLock (cancel) + component.data.patchValue({ type: "onSystemLock" }); + tick(); + fixture.detectChanges(); + + // Should revert to "custom", not "never" + expect(component.data.controls.type.value).toBe("custom"); + })); + }); + + describe("buildRequestData", () => { + beforeEach(() => { + setPolicyResponseType("custom"); + fixture.detectChanges(); + }); + + it("should throw max allowed timeout required error when type is invalid", () => { + component.data.patchValue({ type: null }); + + expect(() => component["buildRequestData"]()).toThrow( + "maximumAllowedTimeoutRequired-used-i18n", + ); + }); + + it.each([ + [null, null], + [null, 0], + [0, null], + [0, 0], + ])( + "should throw invalid time error when type is custom, hours is %o and minutes is %o ", + (hours, minutes) => { + component.data.patchValue({ + type: "custom", + hours: hours, + minutes: minutes, + }); + + expect(() => component["buildRequestData"]()).toThrow( + "sessionTimeoutPolicyInvalidTime-used-i18n", + ); + }, + ); + + it("should return correct data when type is custom with valid time", () => { + component.data.patchValue({ + type: "custom", + hours: 8, + minutes: 30, + action: VaultTimeoutAction.Lock, + }); + + const result = component["buildRequestData"](); + + expect(result).toEqual({ + type: "custom", + minutes: 510, + action: VaultTimeoutAction.Lock, + }); + }); + + it.each([ + ["never", null], + ["never", VaultTimeoutAction.Lock], + ["never", VaultTimeoutAction.LogOut], + ["immediately", null], + ["immediately", VaultTimeoutAction.Lock], + ["immediately", VaultTimeoutAction.LogOut], + ["onSystemLock", null], + ["onSystemLock", VaultTimeoutAction.Lock], + ["onSystemLock", VaultTimeoutAction.LogOut], + ["onAppRestart", null], + ["onAppRestart", VaultTimeoutAction.Lock], + ["onAppRestart", VaultTimeoutAction.LogOut], + ])( + "should return default 8 hours for backward compatibility when type is %s and action is %s", + (type, action) => { + component.data.patchValue({ + type: type as SessionTimeoutType, + hours: 5, + minutes: 25, + action: action as SessionTimeoutAction, + }); + + const result = component["buildRequestData"](); + + expect(result).toEqual({ + type, + minutes: 480, + action, + }); + }, + ); + }); +}); diff --git a/bitwarden_license/bit-web/src/app/key-management/policies/session-timeout.component.ts b/bitwarden_license/bit-web/src/app/key-management/policies/session-timeout.component.ts new file mode 100644 index 00000000000..3e40b9f0d80 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/key-management/policies/session-timeout.component.ts @@ -0,0 +1,197 @@ +import { Component, OnDestroy, OnInit } from "@angular/core"; +import { FormBuilder, FormControl, Validators } from "@angular/forms"; +import { + BehaviorSubject, + concatMap, + firstValueFrom, + Subject, + takeUntil, + withLatestFrom, +} from "rxjs"; + +import { PolicyType } from "@bitwarden/common/admin-console/enums"; +import { VaultTimeoutAction } from "@bitwarden/common/key-management/vault-timeout"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { DialogService } from "@bitwarden/components"; +import { + BasePolicyEditDefinition, + BasePolicyEditComponent, +} from "@bitwarden/web-vault/app/admin-console/organizations/policies"; +import { SharedModule } from "@bitwarden/web-vault/app/shared"; + +import { SessionTimeoutConfirmationNeverComponent } from "./session-timeout-confirmation-never.component"; + +export type SessionTimeoutAction = null | "lock" | "logOut"; +export type SessionTimeoutType = + | null + | "never" + | "onAppRestart" + | "onSystemLock" + | "immediately" + | "custom"; + +export class SessionTimeoutPolicy extends BasePolicyEditDefinition { + name = "sessionTimeoutPolicyTitle"; + description = "sessionTimeoutPolicyDescription"; + type = PolicyType.MaximumVaultTimeout; + component = SessionTimeoutPolicyComponent; +} + +const DEFAULT_HOURS = 8; +const DEFAULT_MINUTES = 0; + +@Component({ + templateUrl: "session-timeout.component.html", + imports: [SharedModule], +}) +export class SessionTimeoutPolicyComponent + extends BasePolicyEditComponent + implements OnInit, OnDestroy +{ + private destroy$ = new Subject(); + private lastConfirmedType$ = new BehaviorSubject(null); + + actionOptions: { name: string; value: SessionTimeoutAction }[]; + typeOptions: { name: string; value: SessionTimeoutType }[]; + data = this.formBuilder.group({ + type: new FormControl(null, [Validators.required]), + hours: new FormControl( + { + value: DEFAULT_HOURS, + disabled: true, + }, + [Validators.required], + ), + minutes: new FormControl( + { + value: DEFAULT_MINUTES, + disabled: true, + }, + [Validators.required], + ), + action: new FormControl(null), + }); + + constructor( + private formBuilder: FormBuilder, + private i18nService: I18nService, + private dialogService: DialogService, + ) { + super(); + this.actionOptions = [ + { name: i18nService.t("userPreference"), value: null }, + { name: i18nService.t("lock"), value: VaultTimeoutAction.Lock }, + { name: i18nService.t("logOut"), value: VaultTimeoutAction.LogOut }, + ]; + this.typeOptions = [ + { name: i18nService.t("immediately"), value: "immediately" }, + { name: i18nService.t("custom"), value: "custom" }, + { name: i18nService.t("onSystemLock"), value: "onSystemLock" }, + { name: i18nService.t("onAppRestart"), value: "onAppRestart" }, + { name: i18nService.t("never"), value: "never" }, + ]; + } + + ngOnInit() { + super.ngOnInit(); + + const typeControl = this.data.controls.type; + this.lastConfirmedType$.next(typeControl.value ?? null); + + typeControl.valueChanges + .pipe( + withLatestFrom(this.lastConfirmedType$), + concatMap(async ([newType, lastConfirmedType]) => { + const confirmed = await this.confirmTypeChange(newType); + if (confirmed) { + this.updateFormControls(newType); + this.lastConfirmedType$.next(newType); + } else { + typeControl.setValue(lastConfirmedType, { emitEvent: false }); + } + }), + takeUntil(this.destroy$), + ) + .subscribe(); + } + + ngOnDestroy() { + this.destroy$.next(); + this.destroy$.complete(); + } + + protected override loadData() { + const minutes: number | null = this.policyResponse?.data?.minutes ?? null; + const action: SessionTimeoutAction = + this.policyResponse?.data?.action ?? (null satisfies SessionTimeoutAction); + // For backward compatibility, the "type" field might not exist, hence we initialize it based on the presence of "minutes" + const type: SessionTimeoutType = + this.policyResponse?.data?.type ?? ((minutes ? "custom" : null) satisfies SessionTimeoutType); + + this.updateFormControls(type); + this.data.patchValue({ + type: type, + hours: minutes ? Math.floor(minutes / 60) : DEFAULT_HOURS, + minutes: minutes ? minutes % 60 : DEFAULT_MINUTES, + action: action, + }); + } + + protected override buildRequestData() { + this.data.markAllAsTouched(); + this.data.updateValueAndValidity(); + if (this.data.invalid) { + if (this.data.controls.type.hasError("required")) { + throw new Error(this.i18nService.t("maximumAllowedTimeoutRequired")); + } + throw new Error(this.i18nService.t("sessionTimeoutPolicyInvalidTime")); + } + + let minutes = this.data.value.hours! * 60 + this.data.value.minutes!; + + const type = this.data.value.type; + if (type === "custom") { + if (minutes <= 0) { + throw new Error(this.i18nService.t("sessionTimeoutPolicyInvalidTime")); + } + } else { + // For backwards compatibility, we set minutes to 8 hours, so older client's vault timeout will not be broken + minutes = DEFAULT_HOURS * 60 + DEFAULT_MINUTES; + } + + return { + type, + minutes, + action: this.data.value.action, + }; + } + + private async confirmTypeChange(newType: SessionTimeoutType): Promise { + if (newType === "never") { + const dialogRef = SessionTimeoutConfirmationNeverComponent.open(this.dialogService); + return !!(await firstValueFrom(dialogRef.closed)); + } else if (newType === "onSystemLock") { + return await this.dialogService.openSimpleDialog({ + type: "info", + title: { key: "sessionTimeoutConfirmationOnSystemLockTitle" }, + content: { key: "sessionTimeoutConfirmationOnSystemLockDescription" }, + acceptButtonText: { key: "continue" }, + cancelButtonText: { key: "cancel" }, + }); + } + + return true; + } + + private updateFormControls(type: SessionTimeoutType) { + const hoursControl = this.data.controls.hours; + const minutesControl = this.data.controls.minutes; + if (type === "custom") { + hoursControl.enable(); + minutesControl.enable(); + } else { + hoursControl.disable(); + minutesControl.disable(); + } + } +} From 460d66d62499f0a5d336d9a651f049d9611cf024 Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Wed, 29 Oct 2025 07:41:35 -0500 Subject: [PATCH 07/25] Remove FF: `pm-17772-admin-initiated-sponsorships` (#16873) * Remove FF * Fix test --- .../free-families-policy.service.spec.ts | 25 ------------------- .../services/free-families-policy.service.ts | 13 ++-------- libs/common/src/enums/feature-flag.enum.ts | 2 -- 3 files changed, 2 insertions(+), 38 deletions(-) diff --git a/apps/web/src/app/billing/services/free-families-policy.service.spec.ts b/apps/web/src/app/billing/services/free-families-policy.service.spec.ts index 10ccc448986..5b39a5a848a 100644 --- a/apps/web/src/app/billing/services/free-families-policy.service.spec.ts +++ b/apps/web/src/app/billing/services/free-families-policy.service.spec.ts @@ -38,7 +38,6 @@ describe("FreeFamiliesPolicyService", () => { describe("showSponsoredFamiliesDropdown$", () => { it("should return true when all conditions are met", async () => { // Configure mocks - configService.getFeatureFlag$.mockReturnValue(of(true)); policyService.policiesByType$.mockReturnValue(of([])); // Create a test organization that meets all criteria @@ -58,7 +57,6 @@ describe("FreeFamiliesPolicyService", () => { it("should return false when organization is not Enterprise", async () => { // Configure mocks - configService.getFeatureFlag$.mockReturnValue(of(true)); policyService.policiesByType$.mockReturnValue(of([])); // Create a test organization that is not Enterprise tier @@ -74,27 +72,8 @@ describe("FreeFamiliesPolicyService", () => { expect(result).toBe(false); }); - it("should return false when feature flag is disabled", async () => { - // Configure mocks to disable feature flag - configService.getFeatureFlag$.mockReturnValue(of(false)); - policyService.policiesByType$.mockReturnValue(of([])); - - // Create a test organization that meets other criteria - const organization = { - id: "org-id", - productTierType: ProductTierType.Enterprise, - useAdminSponsoredFamilies: true, - isAdmin: true, - } as Organization; - - // Test the method - const result = await firstValueFrom(service.showSponsoredFamiliesDropdown$(of(organization))); - expect(result).toBe(false); - }); - it("should return false when families feature is disabled by policy", async () => { // Configure mocks with a policy that disables the feature - configService.getFeatureFlag$.mockReturnValue(of(true)); policyService.policiesByType$.mockReturnValue( of([{ organizationId: "org-id", enabled: true } as Policy]), ); @@ -114,7 +93,6 @@ describe("FreeFamiliesPolicyService", () => { it("should return false when useAdminSponsoredFamilies is false", async () => { // Configure mocks - configService.getFeatureFlag$.mockReturnValue(of(true)); policyService.policiesByType$.mockReturnValue(of([])); // Create a test organization with useAdminSponsoredFamilies set to false @@ -132,7 +110,6 @@ describe("FreeFamiliesPolicyService", () => { it("should return true when user is an owner but not admin", async () => { // Configure mocks - configService.getFeatureFlag$.mockReturnValue(of(true)); policyService.policiesByType$.mockReturnValue(of([])); // Create a test organization where user is owner but not admin @@ -152,7 +129,6 @@ describe("FreeFamiliesPolicyService", () => { it("should return true when user can manage users but is not admin or owner", async () => { // Configure mocks - configService.getFeatureFlag$.mockReturnValue(of(true)); policyService.policiesByType$.mockReturnValue(of([])); // Create a test organization where user can manage users but is not admin or owner @@ -172,7 +148,6 @@ describe("FreeFamiliesPolicyService", () => { it("should return false when user has no admin permissions", async () => { // Configure mocks - configService.getFeatureFlag$.mockReturnValue(of(true)); policyService.policiesByType$.mockReturnValue(of([])); // Create a test organization where user has no admin permissions diff --git a/apps/web/src/app/billing/services/free-families-policy.service.ts b/apps/web/src/app/billing/services/free-families-policy.service.ts index 52041936e50..68e333d53ba 100644 --- a/apps/web/src/app/billing/services/free-families-policy.service.ts +++ b/apps/web/src/app/billing/services/free-families-policy.service.ts @@ -8,8 +8,6 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { ProductTierType } from "@bitwarden/common/billing/enums"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; interface EnterpriseOrgStatus { isFreeFamilyPolicyEnabled: boolean; @@ -23,7 +21,6 @@ export class FreeFamiliesPolicyService { private policyService: PolicyService, private organizationService: OrganizationService, private accountService: AccountService, - private configService: ConfigService, ) {} organizations$ = this.accountService.activeAccount$.pipe( @@ -58,20 +55,14 @@ export class FreeFamiliesPolicyService { userId, ); - return combineLatest([ - enterpriseOrganization$, - this.configService.getFeatureFlag$(FeatureFlag.PM17772_AdminInitiatedSponsorships), - organization, - policies$, - ]).pipe( - map(([isEnterprise, featureFlagEnabled, org, policies]) => { + return combineLatest([enterpriseOrganization$, organization, policies$]).pipe( + map(([isEnterprise, org, policies]) => { const familiesFeatureDisabled = policies.some( (policy) => policy.organizationId === org.id && policy.enabled, ); return ( isEnterprise && - featureFlagEnabled && !familiesFeatureDisabled && org.useAdminSponsoredFamilies && (org.isAdmin || org.isOwner || org.canManageUsers) diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index d9cd1dbfab3..085731b034e 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -23,7 +23,6 @@ export enum FeatureFlag { /* Billing */ TrialPaymentOptional = "PM-8163-trial-payment", - PM17772_AdminInitiatedSponsorships = "pm-17772-admin-initiated-sponsorships", PM21821_ProviderPortalTakeover = "pm-21821-provider-portal-takeover", PM22415_TaxIDWarnings = "pm-22415-tax-id-warnings", PM24032_NewNavigationPremiumUpgradeButton = "pm-24032-new-navigation-premium-upgrade-button", @@ -109,7 +108,6 @@ export const DefaultFeatureFlagValue = { /* Billing */ [FeatureFlag.TrialPaymentOptional]: FALSE, - [FeatureFlag.PM17772_AdminInitiatedSponsorships]: FALSE, [FeatureFlag.PM21821_ProviderPortalTakeover]: FALSE, [FeatureFlag.PM22415_TaxIDWarnings]: FALSE, [FeatureFlag.PM24032_NewNavigationPremiumUpgradeButton]: FALSE, From 5b815c4ae4b997151b7f2cf247d1110925dce14f Mon Sep 17 00:00:00 2001 From: Bryan Cunningham Date: Wed, 29 Oct 2025 09:49:16 -0400 Subject: [PATCH 08/25] [CL-879] use tooltip on icon button (#16576) * Add tooltip to icon button to display label * remove legacy cdr variable * create overlay on focus or hover * attach describdedby ids * fix type errors * remove aria-describedby when not necessary * fix failing tests * implement Claude feedback * fixing broken specs * remove host attr binding * Simplify directive aria logic * Move id to statis number * do not render empty tooltip * pass id to tooltip component * remove pointer-events none to allow tooltip on normal buttons * exclude some tooltip stories * change describedby input name * add story with tooltip on regular button * enhanced tooltip docs * set model directly * change model to input --- .../components/src/button/button.component.ts | 1 - .../src/icon-button/icon-button.component.ts | 19 ++++-- .../src/tooltip/tooltip.component.html | 18 +++--- .../src/tooltip/tooltip.component.ts | 1 + .../src/tooltip/tooltip.directive.ts | 64 +++++++++++++------ libs/components/src/tooltip/tooltip.mdx | 19 +++++- libs/components/src/tooltip/tooltip.spec.ts | 9 ++- .../components/src/tooltip/tooltip.stories.ts | 40 ++++++++++-- .../delete-attachment.component.spec.ts | 2 +- .../uri-option.component.spec.ts | 10 ++- .../download-attachment.component.spec.ts | 2 +- 11 files changed, 137 insertions(+), 48 deletions(-) diff --git a/libs/components/src/button/button.component.ts b/libs/components/src/button/button.component.ts index 350d493f832..6ef5309b018 100644 --- a/libs/components/src/button/button.component.ts +++ b/libs/components/src/button/button.component.ts @@ -92,7 +92,6 @@ export class ButtonComponent implements ButtonLikeAbstraction { "hover:!tw-text-muted", "aria-disabled:tw-cursor-not-allowed", "hover:tw-no-underline", - "aria-disabled:tw-pointer-events-none", ] : [], ) diff --git a/libs/components/src/icon-button/icon-button.component.ts b/libs/components/src/icon-button/icon-button.component.ts index f1edee7c089..9887c0bde8b 100644 --- a/libs/components/src/icon-button/icon-button.component.ts +++ b/libs/components/src/icon-button/icon-button.component.ts @@ -17,6 +17,7 @@ import { setA11yTitleAndAriaLabel } from "../a11y/set-a11y-title-and-aria-label" import { ButtonLikeAbstraction } from "../shared/button-like.abstraction"; import { FocusableElement } from "../shared/focusable-element"; import { SpinnerComponent } from "../spinner"; +import { TooltipDirective } from "../tooltip"; import { ariaDisableElement } from "../utils"; export type IconButtonType = "primary" | "danger" | "contrast" | "main" | "muted" | "nav-contrast"; @@ -100,7 +101,10 @@ const sizes: Record = { */ "[attr.bitIconButton]": "icon()", }, - hostDirectives: [AriaDisableDirective], + hostDirectives: [ + AriaDisableDirective, + { directive: TooltipDirective, inputs: ["tooltipPosition"] }, + ], }) export class BitIconButtonComponent implements ButtonLikeAbstraction, FocusableElement { readonly icon = model.required({ alias: "bitIconButton" }); @@ -109,6 +113,9 @@ export class BitIconButtonComponent implements ButtonLikeAbstraction, FocusableE readonly size = model("default"); + private elementRef = inject(ElementRef); + private tooltip = inject(TooltipDirective, { host: true, optional: true }); + /** * label input will be used to set the `aria-label` attributes on the button. * This is for accessibility purposes, as it provides a text alternative for the icon button. @@ -186,8 +193,6 @@ export class BitIconButtonComponent implements ButtonLikeAbstraction, FocusableE return this.elementRef.nativeElement; } - private elementRef = inject(ElementRef); - constructor() { const element = this.elementRef.nativeElement; @@ -198,9 +203,15 @@ export class BitIconButtonComponent implements ButtonLikeAbstraction, FocusableE effect(() => { setA11yTitleAndAriaLabel({ element: this.elementRef.nativeElement, - title: originalTitle ?? this.label(), + title: undefined, label: this.label(), }); + + const tooltipContent: string = originalTitle || this.label(); + + if (tooltipContent) { + this.tooltip?.tooltipContent.set(tooltipContent); + } }); } } diff --git a/libs/components/src/tooltip/tooltip.component.html b/libs/components/src/tooltip/tooltip.component.html index 4d354fc2765..ce9f1ceeffe 100644 --- a/libs/components/src/tooltip/tooltip.component.html +++ b/libs/components/src/tooltip/tooltip.component.html @@ -1,9 +1,11 @@ -
- +} diff --git a/libs/components/src/tooltip/tooltip.component.ts b/libs/components/src/tooltip/tooltip.component.ts index 34c67015004..79e2dfd7973 100644 --- a/libs/components/src/tooltip/tooltip.component.ts +++ b/libs/components/src/tooltip/tooltip.component.ts @@ -15,6 +15,7 @@ type TooltipData = { content: Signal; isVisible: Signal; tooltipPosition: Signal; + id: Signal; }; export const TOOLTIP_DATA = new InjectionToken("TOOLTIP_DATA"); diff --git a/libs/components/src/tooltip/tooltip.directive.ts b/libs/components/src/tooltip/tooltip.directive.ts index b2c1621d710..bcf9fc5e174 100644 --- a/libs/components/src/tooltip/tooltip.directive.ts +++ b/libs/components/src/tooltip/tooltip.directive.ts @@ -8,8 +8,9 @@ import { ElementRef, Injector, input, - effect, signal, + model, + computed, } from "@angular/core"; import { TooltipPositionIdentifier, tooltipPositions } from "./tooltip-positions"; @@ -26,30 +27,39 @@ import { TooltipComponent, TOOLTIP_DATA } from "./tooltip.component"; "(mouseleave)": "hideTooltip()", "(focus)": "showTooltip()", "(blur)": "hideTooltip()", + "[attr.aria-describedby]": "resolvedDescribedByIds()", }, }) export class TooltipDirective implements OnInit { + private static nextId = 0; /** * The value of this input is forwarded to the tooltip.component to render */ - readonly bitTooltip = input.required(); + readonly tooltipContent = model("", { alias: "bitTooltip" }); /** * The value of this input is forwarded to the tooltip.component to set its position explicitly. * @default "above-center" */ readonly tooltipPosition = input("above-center"); + /** + * Input so the consumer can choose to add the tooltip id to the aria-describedby attribute of the host element. + */ + readonly addTooltipToDescribedby = input(false); + private readonly isVisible = signal(false); private overlayRef: OverlayRef | undefined; - private elementRef = inject(ElementRef); + private elementRef = inject>(ElementRef); private overlay = inject(Overlay); private viewContainerRef = inject(ViewContainerRef); - private injector = inject(Injector); private positionStrategy = this.overlay .position() .flexibleConnectedTo(this.elementRef) .withFlexibleDimensions(false) .withPush(true); + private tooltipId = `bit-tooltip-${TooltipDirective.nextId++}`; + private currentDescribedByIds = + this.elementRef.nativeElement.getAttribute("aria-describedby") || null; private tooltipPortal = new ComponentPortal( TooltipComponent, @@ -59,23 +69,50 @@ export class TooltipDirective implements OnInit { { provide: TOOLTIP_DATA, useValue: { - content: this.bitTooltip, + content: this.tooltipContent, isVisible: this.isVisible, tooltipPosition: this.tooltipPosition, + id: signal(this.tooltipId), }, }, ], }), ); + private destroyTooltip = () => { + this.overlayRef?.dispose(); + this.overlayRef = undefined; + this.isVisible.set(false); + }; + private showTooltip = () => { + if (!this.overlayRef) { + this.overlayRef = this.overlay.create({ + ...this.defaultPopoverConfig, + positionStrategy: this.positionStrategy, + }); + + this.overlayRef.attach(this.tooltipPortal); + } this.isVisible.set(true); }; private hideTooltip = () => { - this.isVisible.set(false); + this.destroyTooltip(); }; + private readonly resolvedDescribedByIds = computed(() => { + if (this.addTooltipToDescribedby()) { + if (this.currentDescribedByIds) { + return `${this.currentDescribedByIds || ""} ${this.tooltipId}`; + } else { + return this.tooltipId; + } + } else { + return this.currentDescribedByIds; + } + }); + private computePositions(tooltipPosition: TooltipPositionIdentifier) { const chosenPosition = tooltipPositions.find((position) => position.id === tooltipPosition); @@ -91,20 +128,5 @@ export class TooltipDirective implements OnInit { ngOnInit() { this.positionStrategy.withPositions(this.computePositions(this.tooltipPosition())); - - this.overlayRef = this.overlay.create({ - ...this.defaultPopoverConfig, - positionStrategy: this.positionStrategy, - }); - - this.overlayRef.attach(this.tooltipPortal); - - effect( - () => { - this.positionStrategy.withPositions(this.computePositions(this.tooltipPosition())); - this.overlayRef?.updatePosition(); - }, - { injector: this.injector }, - ); } } diff --git a/libs/components/src/tooltip/tooltip.mdx b/libs/components/src/tooltip/tooltip.mdx index 4b6f10d97f8..13e159c98eb 100644 --- a/libs/components/src/tooltip/tooltip.mdx +++ b/libs/components/src/tooltip/tooltip.mdx @@ -11,7 +11,20 @@ import { TooltipDirective } from "@bitwarden/components"; <Description /> -NOTE: The `TooltipComponent` can't be used on its own. It must be applied via the `TooltipDirective` +### Tooltip usage + +The `TooltipComponent` can't be used on its own. It must be applied via the `TooltipDirective`. + +The `IconButtonComponent` will automatically apply a tooltip based on the component's `label` input. + +#### Adding the tooltip to the host element's `aria-describedby` list + +The `addTooltipToDescribedby="true"` model input can be used to add the tooltip id to the list of +the host element's `aria-describedby` element IDs. + +NOTE: This behavior is not always necessary and could be redundant if the host element's aria +attributes already convey the same message as the tooltip. Use only when the tooltip is extra, +non-essential contextual information. <Primary /> <Controls /> @@ -29,3 +42,7 @@ NOTE: The `TooltipComponent` can't be used on its own. It must be applied via th ### On disabled element <Canvas of={stories.OnDisabledButton} /> + +### On a Button + +<Canvas of={stories.OnNonIconButton} /> diff --git a/libs/components/src/tooltip/tooltip.spec.ts b/libs/components/src/tooltip/tooltip.spec.ts index b6a49acbc77..a88424de3bb 100644 --- a/libs/components/src/tooltip/tooltip.spec.ts +++ b/libs/components/src/tooltip/tooltip.spec.ts @@ -59,7 +59,14 @@ describe("TooltipDirective (visibility only)", () => { }; const overlayRefStub: OverlayRefStub = { - attach: jest.fn(() => ({})), + attach: jest.fn(() => ({ + changeDetectorRef: { detectChanges: jest.fn() }, + location: { + nativeElement: { + querySelector: jest.fn().mockReturnValue({ id: "tip-123" }), + }, + }, + })), updatePosition: jest.fn(), }; diff --git a/libs/components/src/tooltip/tooltip.stories.ts b/libs/components/src/tooltip/tooltip.stories.ts index 8ea3f52f913..73dad5801f3 100644 --- a/libs/components/src/tooltip/tooltip.stories.ts +++ b/libs/components/src/tooltip/tooltip.stories.ts @@ -72,7 +72,6 @@ type Story = StoryObj<TooltipDirective>; export const Default: Story = { args: { - bitTooltip: "This is a tooltip", tooltipPosition: "above-center", }, render: (args) => ({ @@ -81,6 +80,7 @@ export const Default: Story = { <div class="tw-p-4"> <button bitIconButton="bwi-ellipsis-v" + label="Your tooltip content here" ${formatArgsForCodeSnippet<TooltipDirective>(args)} > Button label here @@ -98,26 +98,29 @@ export const Default: Story = { export const AllPositions: Story = { render: () => ({ + parameters: { + chromatic: { disableSnapshot: true }, + }, template: ` <div class="tw-p-16 tw-grid tw-grid-cols-2 tw-gap-8 tw-place-items-center"> <button bitIconButton="bwi-angle-up" - bitTooltip="Top tooltip" + label="Top tooltip" tooltipPosition="above-center" ></button> <button bitIconButton="bwi-angle-right" - bitTooltip="Right tooltip" + label="Right tooltip" tooltipPosition="right-center" ></button> <button bitIconButton="bwi-angle-left" - bitTooltip="Left tooltip" + label="Left tooltip" tooltipPosition="left-center" ></button> <button bitIconButton="bwi-angle-down" - bitTooltip="Bottom tooltip" + label="Bottom tooltip" tooltipPosition="below-center" ></button> </div> @@ -127,11 +130,14 @@ export const AllPositions: Story = { export const LongContent: Story = { render: () => ({ + parameters: { + chromatic: { disableSnapshot: true }, + }, template: ` <div class="tw-p-16 tw-flex tw-items-center tw-justify-center"> <button bitIconButton="bwi-ellipsis-v" - bitTooltip="This is a very long tooltip that will wrap to multiple lines to demonstrate how the tooltip handles long content. This is not recommended for usability." + label="This is a very long tooltip that will wrap to multiple lines to demonstrate how the tooltip handles long content. This is not recommended for usability." ></button> </div> `, @@ -140,14 +146,34 @@ export const LongContent: Story = { export const OnDisabledButton: Story = { render: () => ({ + parameters: { + chromatic: { disableSnapshot: true }, + }, template: ` <div class="tw-p-16 tw-flex tw-items-center tw-justify-center"> <button bitIconButton="bwi-ellipsis-v" - bitTooltip="Tooltip on disabled button" + label="Tooltip on disabled button" [disabled]="true" ></button> </div> `, }), }; + +export const OnNonIconButton: Story = { + render: () => ({ + parameters: { + chromatic: { disableSnapshot: true }, + }, + template: ` + <div class="tw-p-16 tw-flex tw-items-center tw-justify-center"> + <button + bitButton + addTooltipToDescribedby="true" + bitTooltip="Some additional tooltip text to describe the button" + >Button label</button> + </div> + `, + }), +}; diff --git a/libs/vault/src/cipher-form/components/attachments/delete-attachment/delete-attachment.component.spec.ts b/libs/vault/src/cipher-form/components/attachments/delete-attachment/delete-attachment.component.spec.ts index 941b3740952..4e3899407d2 100644 --- a/libs/vault/src/cipher-form/components/attachments/delete-attachment/delete-attachment.component.spec.ts +++ b/libs/vault/src/cipher-form/components/attachments/delete-attachment/delete-attachment.component.spec.ts @@ -68,7 +68,7 @@ describe("DeleteAttachmentComponent", () => { it("renders delete button", () => { const deleteButton = fixture.debugElement.query(By.css("button")); - expect(deleteButton.attributes["title"]).toBe("deleteAttachmentName"); + expect(deleteButton.attributes["aria-label"]).toBe("deleteAttachmentName"); }); it("does not delete when the user cancels the dialog", async () => { diff --git a/libs/vault/src/cipher-form/components/autofill-options/uri-option.component.spec.ts b/libs/vault/src/cipher-form/components/autofill-options/uri-option.component.spec.ts index 0d7f3663967..2d06f5dcc29 100644 --- a/libs/vault/src/cipher-form/components/autofill-options/uri-option.component.spec.ts +++ b/libs/vault/src/cipher-form/components/autofill-options/uri-option.component.spec.ts @@ -149,13 +149,17 @@ describe("UriOptionComponent", () => { expect(getMatchDetectionSelect()).not.toBeNull(); }); - it("should update the match detection button title when the toggle is clicked", () => { + it("should update the match detection button aria-label when the toggle is clicked", () => { component.writeValue({ uri: "https://example.com", matchDetection: UriMatchStrategy.Exact }); fixture.detectChanges(); - expect(getToggleMatchDetectionBtn().title).toBe("showMatchDetection https://example.com"); + expect(getToggleMatchDetectionBtn().getAttribute("aria-label")).toBe( + "showMatchDetection https://example.com", + ); getToggleMatchDetectionBtn().click(); fixture.detectChanges(); - expect(getToggleMatchDetectionBtn().title).toBe("hideMatchDetection https://example.com"); + expect(getToggleMatchDetectionBtn().getAttribute("aria-label")).toBe( + "hideMatchDetection https://example.com", + ); }); }); 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 8ba7b29a526..ec5a9ce96fd 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 @@ -108,7 +108,7 @@ describe("DownloadAttachmentComponent", () => { it("renders delete button", () => { const deleteButton = fixture.debugElement.query(By.css("button")); - expect(deleteButton.attributes["title"]).toBe("downloadAttachmentName"); + expect(deleteButton.attributes["aria-label"]).toBe("downloadAttachmentName"); }); describe("download attachment", () => { From 4d00d0caa5c4a728952a67efeabc1abf763ec272 Mon Sep 17 00:00:00 2001 From: SmithThe4th <gsmith@bitwarden.com> Date: Wed, 29 Oct 2025 10:31:21 -0400 Subject: [PATCH 09/25] Fixed edit menu on admin console and removed favorite item on the admin console (#16982) --- .../organizations/collections/vault.component.ts | 5 ++++- .../vault-items/vault-cipher-row.component.html | 10 ++++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/apps/web/src/app/admin-console/organizations/collections/vault.component.ts b/apps/web/src/app/admin-console/organizations/collections/vault.component.ts index eb4e47e0ffd..f827dda9a9b 100644 --- a/apps/web/src/app/admin-console/organizations/collections/vault.component.ts +++ b/apps/web/src/app/admin-console/organizations/collections/vault.component.ts @@ -794,6 +794,9 @@ export class VaultComponent implements OnInit, OnDestroy { case "viewEvents": await this.viewEvents(event.item); break; + case "editCipher": + await this.editCipher(event.item); + break; } } finally { this.processingEvent$.next(false); @@ -856,7 +859,7 @@ export class VaultComponent implements OnInit, OnDestroy { * @param cipherView - When set, the cipher to be edited * @param cloneCipher - `true` when the cipher should be cloned. */ - async editCipher(cipher: CipherView | undefined, cloneCipher: boolean) { + async editCipher(cipher: CipherView | undefined, cloneCipher?: boolean) { if ( cipher && cipher.reprompt !== 0 && diff --git a/apps/web/src/app/vault/components/vault-items/vault-cipher-row.component.html b/apps/web/src/app/vault/components/vault-items/vault-cipher-row.component.html index 43ce8530d55..c09553dab9c 100644 --- a/apps/web/src/app/vault/components/vault-items/vault-cipher-row.component.html +++ b/apps/web/src/app/vault/components/vault-items/vault-cipher-row.component.html @@ -169,10 +169,12 @@ <bit-menu-divider *ngIf="showMenuDivider"></bit-menu-divider> - <button bitMenuItem type="button" (click)="toggleFavorite()"> - <i class="bwi bwi-fw bwi-star" aria-hidden="true"></i> - {{ (cipher.favorite ? "unfavorite" : "favorite") | i18n }} - </button> + @if (!viewingOrgVault) { + <button bitMenuItem type="button" (click)="toggleFavorite()"> + <i class="bwi bwi-fw bwi-star" aria-hidden="true"></i> + {{ (cipher.favorite ? "unfavorite" : "favorite") | i18n }} + </button> + } <button bitMenuItem type="button" (click)="editCipher()" *ngIf="canEditCipher"> <i class="bwi bwi-fw bwi-pencil-square" aria-hidden="true"></i> {{ "edit" | i18n }} From d31b9211693debcfd1823b9c229ef0b03e1c2e97 Mon Sep 17 00:00:00 2001 From: Daniel Riera <driera@livefront.com> Date: Wed, 29 Oct 2025 10:40:27 -0400 Subject: [PATCH 10/25] PM-27364 delete bar.scss and drop bar.html internals (#17023) * PM-27364 delete bar.scss and drop bar.html internals * no longer clear document --- .../src/autofill/notification/bar.html | 52 +-- .../src/autofill/notification/bar.scss | 304 ------------------ apps/browser/src/autofill/notification/bar.ts | 2 - 3 files changed, 1 insertion(+), 357 deletions(-) delete mode 100644 apps/browser/src/autofill/notification/bar.scss diff --git a/apps/browser/src/autofill/notification/bar.html b/apps/browser/src/autofill/notification/bar.html index 90df670d29c..c0b57de612e 100644 --- a/apps/browser/src/autofill/notification/bar.html +++ b/apps/browser/src/autofill/notification/bar.html @@ -5,55 +5,5 @@ <title>Bitwarden - - -
-
- - - -
-
-
- -
-
- - - - - - - + diff --git a/apps/browser/src/autofill/notification/bar.scss b/apps/browser/src/autofill/notification/bar.scss deleted file mode 100644 index c91c5f3ebac..00000000000 --- a/apps/browser/src/autofill/notification/bar.scss +++ /dev/null @@ -1,304 +0,0 @@ -@import "../shared/styles/variables"; - -body { - margin: 0; - padding: 0; - height: 100%; - font-size: 14px; - line-height: 16px; - font-family: $font-family-sans-serif; - - @include themify($themes) { - color: themed("textColor"); - background-color: themed("backgroundColor"); - } -} - -img { - margin: 0; - padding: 0; - border: 0; -} - -button, -select { - font-size: $font-size-base; - font-family: $font-family-sans-serif; -} - -.outer-wrapper { - display: block; - position: relative; - padding: 8px; - min-height: 42px; - border: 1px solid transparent; - border-bottom: 2px solid transparent; - border-radius: 4px; - box-sizing: border-box; - - @include themify($themes) { - border-color: themed("borderColor"); - border-bottom-color: themed("primaryColor"); - } - - &.success-event { - @include themify($themes) { - border-bottom-color: themed("successColor"); - } - } - - &.error-event { - @include themify($themes) { - border-bottom-color: themed("errorColor"); - } - } -} - -.inner-wrapper { - display: grid; - grid-template-columns: auto max-content; -} - -.outer-wrapper > *, -.inner-wrapper > * { - align-self: center; -} - -#logo { - width: 24px; - height: 24px; - display: block; -} - -.logo-wrapper { - position: absolute; - top: 8px; - left: 10px; - overflow: hidden; -} - -#close-button { - display: flex; - align-items: center; - justify-content: center; - width: 30px; - height: 30px; - margin-right: 10px; - padding: 0; - - &:hover { - @include themify($themes) { - border-color: rgba(themed("textColor"), 0.2); - background-color: rgba(themed("textColor"), 0.2); - } - } -} - -#close { - display: block; - width: 16px; - height: 16px; - - > path { - @include themify($themes) { - fill: themed("textColor"); - } - } -} - -.notification-close { - position: absolute; - top: 6px; - right: 6px; -} - -#content .inner-wrapper { - display: flex; - flex-wrap: wrap; - align-items: flex-start; - - .notification-body { - width: 100%; - padding: 4px 38px 24px 42px; - font-weight: 400; - } - - .notification-actions { - display: flex; - width: 100%; - align-items: stretch; - justify-content: flex-end; - - #never-save { - margin-right: auto; - padding: 0; - font-size: 16px; - font-weight: 500; - letter-spacing: 0.5px; - } - - #select-folder { - width: 125px; - margin-right: 6px; - font-size: 12px; - appearance: none; - background-repeat: no-repeat; - background-position: center right 4px; - background-size: 16px; - - @include themify($themes) { - color: themed("mutedTextColor"); - border-color: themed("mutedTextColor"); - } - - &:not([disabled]) { - display: block; - } - } - - .primary, - .secondary { - font-size: 12px; - } - - .secondary { - margin-right: 6px; - border-width: 1px; - } - - .primary { - margin-right: 2px; - } - - &.success-message, - &.error-message { - padding: 4px 36px 6px 42px; - } - } -} - -button { - padding: 4px 8px; - border-radius: $border-radius; - border: 1px solid transparent; - cursor: pointer; -} - -button.primary:not(.neutral) { - @include themify($themes) { - background-color: themed("primaryColor"); - color: themed("textContrast"); - border-color: themed("primaryColor"); - } - - &:hover { - @include themify($themes) { - background-color: darken(themed("primaryColor"), 1.5%); - color: darken(themed("textContrast"), 6%); - } - } -} - -button.secondary:not(.neutral) { - @include themify($themes) { - background-color: themed("backgroundColor"); - color: themed("mutedTextColor"); - border-color: themed("mutedTextColor"); - } - - &:hover { - @include themify($themes) { - background-color: themed("backgroundOffsetColor"); - color: darken(themed("mutedTextColor"), 6%); - } - } -} - -button.link, -button.neutral { - @include themify($themes) { - background-color: transparent; - color: themed("primaryColor"); - } - - &:hover { - text-decoration: underline; - - @include themify($themes) { - color: darken(themed("primaryColor"), 6%); - } - } -} - -select { - padding: 4px 6px; - border: 1px solid #000000; - border-radius: $border-radius; - - @include themify($themes) { - color: themed("textColor"); - background-color: themed("inputBackgroundColor"); - border-color: themed("inputBorderColor"); - } -} - -.success-message { - display: flex; - align-items: center; - justify-content: center; - - @include themify($themes) { - color: themed("successColor"); - } - - svg { - margin-right: 8px; - - path { - @include themify($themes) { - fill: themed("successColor"); - } - } - } -} - -.error-message { - @include themify($themes) { - color: themed("errorColor"); - } -} - -.success-event, -.error-event { - .notification-body { - display: none; - } -} - -@media screen and (max-width: 768px) { - #select-folder { - display: none; - } -} - -@media print { - body { - display: none; - } -} - -.theme_light { - #content .inner-wrapper { - #select-folder { - background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHhtbG5zOnhsaW5rPSdodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rJyB3aWR0aD0nMTYnIGhlaWdodD0nMTYnIGZpbGw9J25vbmUnPjxwYXRoIHN0cm9rZT0nIzIxMjUyOScgZD0nbTUgNiAzIDMgMy0zJy8+PC9zdmc+"); - } - } -} - -.theme_dark { - #content .inner-wrapper { - #select-folder { - background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHdpZHRoPScxNicgaGVpZ2h0PScxNicgZmlsbD0nbm9uZSc+PHBhdGggc3Ryb2tlPScjZmZmZmZmJyBkPSdtNSA2IDMgMyAzLTMnLz48L3N2Zz4="); - } - } -} diff --git a/apps/browser/src/autofill/notification/bar.ts b/apps/browser/src/autofill/notification/bar.ts index fcf91ca2e91..3673a9f7321 100644 --- a/apps/browser/src/autofill/notification/bar.ts +++ b/apps/browser/src/autofill/notification/bar.ts @@ -187,8 +187,6 @@ async function initNotificationBar(message: NotificationBarWindowMessage) { const notificationTestId = getNotificationTestId(notificationType); appendHeaderMessageToTitle(headerMessage); - document.body.innerHTML = ""; - if (isVaultLocked) { const notificationConfig = { ...notificationBarIframeInitData, From b1738cc6b207ed2f004757742385412293b5a188 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Wed, 29 Oct 2025 15:51:50 +0100 Subject: [PATCH 11/25] [PM-26340] Add linux biometrics v2 (#16660) * Extract windows biometrics v2 changes Co-authored-by: Bernd Schoolmann * Address some code review feedback * cargo fmt * rely on zeroizing allocator * Handle TDE edge cases * Update windows default * Make windows rust code async and fix restoring focus freezes * fix formatting * cleanup native logging * Add unit test coverage * Add missing logic to edge case for PIN disable. * Address code review feedback * fix test * code review changes * fix clippy warning * Swap to unimplemented on each method * Implement encrypted memory store * Make dpapi secure key container pub(super) * Add linux biometrics v2 * Run cargo fmt * Fix cargo lock * Undo AC changes * Undo change * Fix build * Cargo fmt --------- Co-authored-by: Thomas Avery Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> --- .../core/src/biometric_v2/linux.rs | 141 ++++++++++++++++++ .../core/src/biometric_v2/mod.rs | 2 +- .../core/src/secure_memory/mod.rs | 2 +- 3 files changed, 143 insertions(+), 2 deletions(-) create mode 100644 apps/desktop/desktop_native/core/src/biometric_v2/linux.rs diff --git a/apps/desktop/desktop_native/core/src/biometric_v2/linux.rs b/apps/desktop/desktop_native/core/src/biometric_v2/linux.rs new file mode 100644 index 00000000000..44cba4a9e5b --- /dev/null +++ b/apps/desktop/desktop_native/core/src/biometric_v2/linux.rs @@ -0,0 +1,141 @@ +//! This file implements Polkit based system unlock. +//! +//! # Security +//! This section describes the assumed security model and security guarantees achieved. In the required security +//! guarantee is that a locked vault - a running app - cannot be unlocked when the device (user-space) +//! is compromised in this state. +//! +//! When first unlocking the app, the app sends the user-key to this module, which holds it in secure memory, +//! protected by memfd_secret. This makes it inaccessible to other processes, even if they compromise root, a kernel compromise +//! has circumventable best-effort protections. While the app is running this key is held in memory, even if locked. +//! When unlocking, the app will prompt the user via `polkit` to get a yes/no decision on whether to release the key to the app. + +use anyhow::{anyhow, Result}; +use std::sync::Arc; +use tokio::sync::Mutex; +use tracing::{debug, warn}; +use zbus::Connection; +use zbus_polkit::policykit1::{AuthorityProxy, CheckAuthorizationFlags, Subject}; + +use crate::secure_memory::*; + +pub struct BiometricLockSystem { + // The userkeys that are held in memory MUST be protected from memory dumping attacks, to ensure + // locked vaults cannot be unlocked + secure_memory: Arc>, +} + +impl BiometricLockSystem { + pub fn new() -> Self { + Self { + secure_memory: Arc::new(Mutex::new( + crate::secure_memory::encrypted_memory_store::EncryptedMemoryStore::new(), + )), + } + } +} + +impl Default for BiometricLockSystem { + fn default() -> Self { + Self::new() + } +} + +impl super::BiometricTrait for BiometricLockSystem { + async fn authenticate(&self, _hwnd: Vec, _message: String) -> Result { + polkit_authenticate_bitwarden_policy().await + } + + async fn authenticate_available(&self) -> Result { + polkit_is_bitwarden_policy_available().await + } + + async fn enroll_persistent(&self, _user_id: &str, _key: &[u8]) -> Result<()> { + // Not implemented + Ok(()) + } + + async fn provide_key(&self, user_id: &str, key: &[u8]) { + self.secure_memory + .lock() + .await + .put(user_id.to_string(), key); + } + + async fn unlock(&self, user_id: &str, _hwnd: Vec) -> Result> { + if !polkit_authenticate_bitwarden_policy().await? { + return Err(anyhow!("Authentication failed")); + } + + self.secure_memory + .lock() + .await + .get(user_id) + .ok_or(anyhow!("No key found")) + } + + async fn unlock_available(&self, user_id: &str) -> Result { + Ok(self.secure_memory.lock().await.has(user_id)) + } + + async fn has_persistent(&self, _user_id: &str) -> Result { + Ok(false) + } + + async fn unenroll(&self, user_id: &str) -> Result<(), anyhow::Error> { + self.secure_memory.lock().await.remove(user_id); + Ok(()) + } +} + +/// Perform a polkit authorization against the bitwarden unlock policy. Note: This relies on no custom +/// rules in the system skipping the authorization check, in which case this counts as UV / authentication. +async fn polkit_authenticate_bitwarden_policy() -> Result { + debug!("[Polkit] Authenticating / performing UV"); + + let connection = Connection::system().await?; + let proxy = AuthorityProxy::new(&connection).await?; + let subject = Subject::new_for_owner(std::process::id(), None, None)?; + let details = std::collections::HashMap::new(); + let authorization_result = proxy + .check_authorization( + &subject, + "com.bitwarden.Bitwarden.unlock", + &details, + CheckAuthorizationFlags::AllowUserInteraction.into(), + "", + ) + .await; + + match authorization_result { + Ok(result) => Ok(result.is_authorized), + Err(e) => { + warn!("[Polkit] Error performing authentication: {:?}", e); + Ok(false) + } + } +} + +async fn polkit_is_bitwarden_policy_available() -> Result { + let connection = Connection::system().await?; + let proxy = AuthorityProxy::new(&connection).await?; + let actions = proxy.enumerate_actions("en").await?; + for action in actions { + if action.action_id == "com.bitwarden.Bitwarden.unlock" { + return Ok(true); + } + } + Ok(false) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + #[ignore] + async fn test_polkit_authenticate() { + let result = polkit_authenticate_bitwarden_policy().await; + assert!(result.is_ok()); + } +} diff --git a/apps/desktop/desktop_native/core/src/biometric_v2/mod.rs b/apps/desktop/desktop_native/core/src/biometric_v2/mod.rs index e37a101e2ae..669267b7829 100644 --- a/apps/desktop/desktop_native/core/src/biometric_v2/mod.rs +++ b/apps/desktop/desktop_native/core/src/biometric_v2/mod.rs @@ -1,7 +1,7 @@ use anyhow::Result; #[allow(clippy::module_inception)] -#[cfg_attr(target_os = "linux", path = "unimplemented.rs")] +#[cfg_attr(target_os = "linux", path = "linux.rs")] #[cfg_attr(target_os = "macos", path = "unimplemented.rs")] #[cfg_attr(target_os = "windows", path = "windows.rs")] mod biometric_v2; diff --git a/apps/desktop/desktop_native/core/src/secure_memory/mod.rs b/apps/desktop/desktop_native/core/src/secure_memory/mod.rs index 8695904758e..d4323ce40dd 100644 --- a/apps/desktop/desktop_native/core/src/secure_memory/mod.rs +++ b/apps/desktop/desktop_native/core/src/secure_memory/mod.rs @@ -1,7 +1,7 @@ #[cfg(target_os = "windows")] pub(crate) mod dpapi; -mod encrypted_memory_store; +pub(crate) mod encrypted_memory_store; mod secure_key; /// The secure memory store provides an ephemeral key-value store for sensitive data. From d567530e159c083cbf986dbd0f68f498a7199d2c Mon Sep 17 00:00:00 2001 From: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com> Date: Wed, 29 Oct 2025 16:02:59 +0100 Subject: [PATCH 12/25] resolve the button name (#17094) --- .../billing/individual/premium/premium-vnext.component.html | 2 +- .../upgrade/upgrade-account/upgrade-account.component.spec.ts | 2 +- .../upgrade/upgrade-account/upgrade-account.component.ts | 2 +- .../upgrade/upgrade-payment/upgrade-payment.component.ts | 2 +- apps/web/src/locales/en/messages.json | 3 +++ 5 files changed, 7 insertions(+), 4 deletions(-) diff --git a/apps/web/src/app/billing/individual/premium/premium-vnext.component.html b/apps/web/src/app/billing/individual/premium/premium-vnext.component.html index bf5d0f60861..ee2bef9baa3 100644 --- a/apps/web/src/app/billing/individual/premium/premium-vnext.component.html +++ b/apps/web/src/app/billing/individual/premium/premium-vnext.component.html @@ -38,7 +38,7 @@ diff --git a/apps/web/src/app/billing/individual/upgrade/upgrade-account/upgrade-account.component.spec.ts b/apps/web/src/app/billing/individual/upgrade/upgrade-account/upgrade-account.component.spec.ts index 27e69fcf0d4..a6038873e83 100644 --- a/apps/web/src/app/billing/individual/upgrade/upgrade-account/upgrade-account.component.spec.ts +++ b/apps/web/src/app/billing/individual/upgrade/upgrade-account/upgrade-account.component.spec.ts @@ -98,7 +98,7 @@ describe("UpgradeAccountComponent", () => { expect(sut["familiesCardDetails"].price.amount).toBe(40 / 12); expect(sut["familiesCardDetails"].price.cadence).toBe("monthly"); expect(sut["familiesCardDetails"].button.type).toBe("secondary"); - expect(sut["familiesCardDetails"].button.text).toBe("upgradeToFamilies"); + expect(sut["familiesCardDetails"].button.text).toBe("startFreeFamiliesTrial"); expect(sut["familiesCardDetails"].features).toEqual(["Feature A", "Feature B", "Feature C"]); }); diff --git a/apps/web/src/app/billing/individual/upgrade/upgrade-account/upgrade-account.component.ts b/apps/web/src/app/billing/individual/upgrade/upgrade-account/upgrade-account.component.ts index be09505d190..780b6bed433 100644 --- a/apps/web/src/app/billing/individual/upgrade/upgrade-account/upgrade-account.component.ts +++ b/apps/web/src/app/billing/individual/upgrade/upgrade-account/upgrade-account.component.ts @@ -119,7 +119,7 @@ export class UpgradeAccountComponent implements OnInit { }, button: { text: this.i18nService.t( - this.isFamiliesPlan(tier.id) ? "upgradeToFamilies" : "upgradeToPremium", + this.isFamiliesPlan(tier.id) ? "startFreeFamiliesTrial" : "upgradeToPremium", ), type: buttonType, }, diff --git a/apps/web/src/app/billing/individual/upgrade/upgrade-payment/upgrade-payment.component.ts b/apps/web/src/app/billing/individual/upgrade/upgrade-payment/upgrade-payment.component.ts index a0ba480fe1e..a80ff5d720a 100644 --- a/apps/web/src/app/billing/individual/upgrade/upgrade-payment/upgrade-payment.component.ts +++ b/apps/web/src/app/billing/individual/upgrade/upgrade-payment/upgrade-payment.component.ts @@ -161,7 +161,7 @@ export class UpgradePaymentComponent implements OnInit, AfterViewInit { }; this.upgradeToMessage = this.i18nService.t( - this.isFamiliesPlan ? "upgradeToFamilies" : "upgradeToPremium", + this.isFamiliesPlan ? "startFreeFamiliesTrial" : "upgradeToPremium", ); } else { this.complete.emit({ status: UpgradePaymentStatus.Closed, organizationId: null }); diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index aa0353e754d..c1026af4b6e 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -11975,5 +11975,8 @@ }, "cardNumberLabel": { "message": "Card number" + }, + "startFreeFamiliesTrial": { + "message": "Start free Families trial" } } From 687f3d144cc0c236b826f26b22719dece8c14caf Mon Sep 17 00:00:00 2001 From: Vijay Oommen Date: Wed, 29 Oct 2025 10:58:38 -0500 Subject: [PATCH 13/25] [PM-17577] Inactive two-step login report - check hostname and domain name (#16823) --- ...active-two-factor-report.component.spec.ts | 149 ++++++++++++++++++ .../inactive-two-factor-report.component.ts | 11 ++ 2 files changed, 160 insertions(+) diff --git a/apps/web/src/app/dirt/reports/pages/inactive-two-factor-report.component.spec.ts b/apps/web/src/app/dirt/reports/pages/inactive-two-factor-report.component.spec.ts index acc34232571..80893737ffd 100644 --- a/apps/web/src/app/dirt/reports/pages/inactive-two-factor-report.component.spec.ts +++ b/apps/web/src/app/dirt/reports/pages/inactive-two-factor-report.component.spec.ts @@ -121,4 +121,153 @@ describe("InactiveTwoFactorReportComponent", () => { it("should call fullSync method of syncService", () => { expect(syncServiceMock.fullSync).toHaveBeenCalledWith(false); }); + + describe("isInactive2faCipher", () => { + beforeEach(() => { + // Add both domain and host to services map + component.services.set("example.com", "https://example.com/2fa-doc"); + component.services.set("sub.example.com", "https://sub.example.com/2fa-doc"); + fixture.detectChanges(); + }); + it("should return true and documentation for cipher with matching domain", () => { + const cipher = createCipherView({ + login: { + uris: [{ uri: "https://example.com/login" }], + }, + }); + const [doc, isInactive] = (component as any).isInactive2faCipher(cipher); + expect(isInactive).toBe(true); + expect(doc).toBe("https://example.com/2fa-doc"); + }); + + it("should return true and documentation for cipher with matching host", () => { + const cipher = createCipherView({ + login: { + uris: [{ uri: "https://sub.example.com/login" }], + }, + }); + const [doc, isInactive] = (component as any).isInactive2faCipher(cipher); + expect(isInactive).toBe(true); + expect(doc).toBe("https://sub.example.com/2fa-doc"); + }); + + it("should return false for cipher with non-matching domain or host", () => { + const cipher = createCipherView({ + login: { + uris: [{ uri: "https://otherdomain.com/login" }], + }, + }); + const [doc, isInactive] = (component as any).isInactive2faCipher(cipher); + expect(isInactive).toBe(false); + expect(doc).toBe(""); + }); + + it("should return false if cipher type is not Login", () => { + const cipher = createCipherView({ + type: 2, + login: { + uris: [{ uri: "https://example.com/login" }], + }, + }); + const [doc, isInactive] = (component as any).isInactive2faCipher(cipher); + expect(isInactive).toBe(false); + expect(doc).toBe(""); + }); + + it("should return false if cipher has TOTP", () => { + const cipher = createCipherView({ + login: { + totp: "some-totp", + uris: [{ uri: "https://example.com/login" }], + }, + }); + const [doc, isInactive] = (component as any).isInactive2faCipher(cipher); + expect(isInactive).toBe(false); + expect(doc).toBe(""); + }); + + it("should return false if cipher is deleted", () => { + const cipher = createCipherView({ + isDeleted: true, + login: { + uris: [{ uri: "https://example.com/login" }], + }, + }); + const [doc, isInactive] = (component as any).isInactive2faCipher(cipher); + expect(isInactive).toBe(false); + expect(doc).toBe(""); + }); + + it("should return false if cipher does not have edit access and no organization", () => { + component.organization = null; + const cipher = createCipherView({ + edit: false, + login: { + uris: [{ uri: "https://example.com/login" }], + }, + }); + const [doc, isInactive] = (component as any).isInactive2faCipher(cipher); + expect(isInactive).toBe(false); + expect(doc).toBe(""); + }); + + it("should return false if cipher does not have viewPassword", () => { + const cipher = createCipherView({ + viewPassword: false, + login: { + uris: [{ uri: "https://example.com/login" }], + }, + }); + const [doc, isInactive] = (component as any).isInactive2faCipher(cipher); + expect(isInactive).toBe(false); + expect(doc).toBe(""); + }); + + it("should check all uris and return true if any matches domain or host", () => { + const cipher = createCipherView({ + login: { + uris: [ + { uri: "https://otherdomain.com/login" }, + { uri: "https://sub.example.com/dashboard" }, + ], + }, + }); + const [doc, isInactive] = (component as any).isInactive2faCipher(cipher); + expect(isInactive).toBe(true); + expect(doc).toBe("https://sub.example.com/2fa-doc"); + }); + + it("should return false if uris array is empty", () => { + const cipher = createCipherView({ + login: { + uris: [], + }, + }); + const [doc, isInactive] = (component as any).isInactive2faCipher(cipher); + expect(isInactive).toBe(false); + expect(doc).toBe(""); + }); + + function createCipherView({ + type = 1, + login = {}, + isDeleted = false, + edit = true, + viewPassword = true, + }: any): any { + return { + id: "test-id", + type, + login: { + totp: null, + hasUris: true, + uris: [], + ...login, + }, + isDeleted, + edit, + viewPassword, + }; + } + }); }); diff --git a/apps/web/src/app/dirt/reports/pages/inactive-two-factor-report.component.ts b/apps/web/src/app/dirt/reports/pages/inactive-two-factor-report.component.ts index 8b0fdda70e3..2a8ec12ac6a 100644 --- a/apps/web/src/app/dirt/reports/pages/inactive-two-factor-report.component.ts +++ b/apps/web/src/app/dirt/reports/pages/inactive-two-factor-report.component.ts @@ -109,7 +109,18 @@ export class InactiveTwoFactorReportComponent extends CipherReportComponent impl const u = login.uris[i]; if (u.uri != null && u.uri !== "") { const uri = u.uri.replace("www.", ""); + const host = Utils.getHost(uri); const domain = Utils.getDomain(uri); + // check host first + if (host != null && this.services.has(host)) { + if (this.services.get(host) != null) { + docFor2fa = this.services.get(host) || ""; + } + isInactive2faCipher = true; + break; + } + + // then check domain if (domain != null && this.services.has(domain)) { if (this.services.get(domain) != null) { docFor2fa = this.services.get(domain) || ""; From ee420258e6cd01384eeb7233736ccabb6d523d4b Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Wed, 29 Oct 2025 17:46:50 +0100 Subject: [PATCH 14/25] Remove deprecated encstring usage from dirt code (#17100) --- .../member-access-report.component.ts | 5 +++- .../member-access-report.service.spec.ts | 24 +++++++++++++++++-- .../services/member-access-report.service.ts | 22 ++++++++++++++--- 3 files changed, 45 insertions(+), 6 deletions(-) diff --git a/bitwarden_license/bit-web/src/app/dirt/reports/member-access-report/member-access-report.component.ts b/bitwarden_license/bit-web/src/app/dirt/reports/member-access-report/member-access-report.component.ts index ad15edd84df..445cee6683c 100644 --- a/bitwarden_license/bit-web/src/app/dirt/reports/member-access-report/member-access-report.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/reports/member-access-report/member-access-report.component.ts @@ -8,12 +8,15 @@ import { BehaviorSubject, debounceTime, firstValueFrom, lastValueFrom } from "rx import { UserNamePipe } from "@bitwarden/angular/pipes/user-name.pipe"; import { safeProvider } from "@bitwarden/angular/platform/utils/safe-provider"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions"; import { OrganizationMetadataServiceAbstraction } from "@bitwarden/common/billing/abstractions/organization-metadata.service.abstraction"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { OrganizationId } from "@bitwarden/common/types/guid"; import { DialogService, SearchModule, TableDataSource } from "@bitwarden/components"; +import { KeyService } from "@bitwarden/key-management"; import { ExportHelper } from "@bitwarden/vault-export-core"; import { CoreOrganizationModule } from "@bitwarden/web-vault/app/admin-console/organizations/core"; import { @@ -41,7 +44,7 @@ import { MemberAccessReportView } from "./view/member-access-report.view"; safeProvider({ provide: MemberAccessReportServiceAbstraction, useClass: MemberAccessReportService, - deps: [MemberAccessReportApiService, I18nService], + deps: [MemberAccessReportApiService, I18nService, EncryptService, KeyService, AccountService], }), ], }) diff --git a/bitwarden_license/bit-web/src/app/dirt/reports/member-access-report/services/member-access-report.service.spec.ts b/bitwarden_license/bit-web/src/app/dirt/reports/member-access-report/services/member-access-report.service.spec.ts index ad388cfed04..615e6d079b2 100644 --- a/bitwarden_license/bit-web/src/app/dirt/reports/member-access-report/services/member-access-report.service.spec.ts +++ b/bitwarden_license/bit-web/src/app/dirt/reports/member-access-report/services/member-access-report.service.spec.ts @@ -1,7 +1,13 @@ import { mock } from "jest-mock-extended"; +import { of } from "rxjs"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { OrganizationId } from "@bitwarden/common/types/guid"; +import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; +import { mockAccountServiceWith } from "@bitwarden/common/spec"; +import { OrganizationId, UserId } from "@bitwarden/common/types/guid"; +import { newGuid } from "@bitwarden/guid"; +import { KeyService } from "@bitwarden/key-management"; import { MemberAccessReportApiService } from "./member-access-report-api.service"; import { @@ -9,9 +15,14 @@ import { memberAccessWithoutAccessDetailsReportsMock, } from "./member-access-report.mock"; import { MemberAccessReportService } from "./member-access-report.service"; + describe("ImportService", () => { const mockOrganizationId = "mockOrgId" as OrganizationId; const reportApiService = mock(); + const mockEncryptService = mock(); + const userId = newGuid() as UserId; + const mockAccountService = mockAccountServiceWith(userId); + const mockKeyService = mock(); let memberAccessReportService: MemberAccessReportService; const i18nMock = mock({ t(key) { @@ -20,10 +31,19 @@ describe("ImportService", () => { }); beforeEach(() => { + mockKeyService.orgKeys$.mockReturnValue( + of({ mockOrgId: new SymmetricCryptoKey(new Uint8Array(64)) }), + ); reportApiService.getMemberAccessData.mockImplementation(() => Promise.resolve(memberAccessReportsMock), ); - memberAccessReportService = new MemberAccessReportService(reportApiService, i18nMock); + memberAccessReportService = new MemberAccessReportService( + reportApiService, + i18nMock, + mockEncryptService, + mockKeyService, + mockAccountService, + ); }); describe("generateMemberAccessReportView", () => { diff --git a/bitwarden_license/bit-web/src/app/dirt/reports/member-access-report/services/member-access-report.service.ts b/bitwarden_license/bit-web/src/app/dirt/reports/member-access-report/services/member-access-report.service.ts index caa27a75b82..f6d1139f619 100644 --- a/bitwarden_license/bit-web/src/app/dirt/reports/member-access-report/services/member-access-report.service.ts +++ b/bitwarden_license/bit-web/src/app/dirt/reports/member-access-report/services/member-access-report.service.ts @@ -1,11 +1,16 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { Injectable } from "@angular/core"; +import { firstValueFrom, map } from "rxjs"; import { CollectionAccessSelectionView } from "@bitwarden/admin-console/common"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { Guid, OrganizationId } from "@bitwarden/common/types/guid"; +import { KeyService } from "@bitwarden/key-management"; import { getPermissionList, convertToPermission, @@ -22,6 +27,9 @@ export class MemberAccessReportService { constructor( private reportApiService: MemberAccessReportApiService, private i18nService: I18nService, + private encryptService: EncryptService, + private keyService: KeyService, + private accountService: AccountService, ) {} /** * Transforms user data into a MemberAccessReportView. @@ -78,14 +86,22 @@ export class MemberAccessReportService { async generateUserReportExportItems( organizationId: OrganizationId, ): Promise { + const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); + const organizationSymmetricKey = await firstValueFrom( + this.keyService.orgKeys$(activeUserId).pipe(map((keys) => keys[organizationId])), + ); + const memberAccessReports = await this.reportApiService.getMemberAccessData(organizationId); const collectionNames = memberAccessReports.map((item) => item.collectionName.encryptedString); const collectionNameMap = new Map(collectionNames.map((col) => [col, ""])); for await (const key of collectionNameMap.keys()) { - const decrypted = new EncString(key); - await decrypted.decrypt(organizationId); - collectionNameMap.set(key, decrypted.decryptedValue); + const encryptedCollectionName = new EncString(key); + const collectionName = await this.encryptService.decryptString( + encryptedCollectionName, + organizationSymmetricKey, + ); + collectionNameMap.set(key, collectionName); } const exportItems = memberAccessReports.map((report) => { From 6896c77332cfc740aabdc39409ef04b1071237f5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 29 Oct 2025 13:21:07 -0400 Subject: [PATCH 15/25] [deps] UI Foundation: Update axe-playwright to v2.2.2 (#16629) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Bryan Cunningham --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index e131618ee4a..e39aeea5605 100644 --- a/package-lock.json +++ b/package-lock.json @@ -125,7 +125,7 @@ "@yao-pkg/pkg": "6.5.1", "angular-eslint": "19.6.0", "autoprefixer": "10.4.21", - "axe-playwright": "2.1.0", + "axe-playwright": "2.2.2", "babel-loader": "9.2.1", "base64-loader": "1.0.0", "browserslist": "4.23.2", @@ -16966,9 +16966,9 @@ } }, "node_modules/axe-playwright": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/axe-playwright/-/axe-playwright-2.1.0.tgz", - "integrity": "sha512-tY48SX56XaAp16oHPyD4DXpybz8Jxdz9P7exTjF/4AV70EGUavk+1fUPWirM0OYBR+YyDx6hUeDvuHVA6fB9YA==", + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/axe-playwright/-/axe-playwright-2.2.2.tgz", + "integrity": "sha512-h350/grzDCPgpuWV7eEOqr/f61Xn07Gi9f9B3Ew4rW6/nFtpdEJYW6jgRATorgAGXjEAYFTnaY3sEys39wDw4A==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 2c02ff68824..0190bc37bec 100644 --- a/package.json +++ b/package.json @@ -88,7 +88,7 @@ "@yao-pkg/pkg": "6.5.1", "angular-eslint": "19.6.0", "autoprefixer": "10.4.21", - "axe-playwright": "2.1.0", + "axe-playwright": "2.2.2", "babel-loader": "9.2.1", "base64-loader": "1.0.0", "browserslist": "4.23.2", From 4bdfefd001b553849c9f69bff52a3ea4dc637ac7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 29 Oct 2025 13:51:25 -0400 Subject: [PATCH 16/25] [deps] UI Foundation: Update chromatic to v13.3.1 (#16630) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Bryan Cunningham --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index e39aeea5605..d1858d4d508 100644 --- a/package-lock.json +++ b/package-lock.json @@ -129,7 +129,7 @@ "babel-loader": "9.2.1", "base64-loader": "1.0.0", "browserslist": "4.23.2", - "chromatic": "13.1.2", + "chromatic": "13.3.1", "concurrently": "9.2.0", "copy-webpack-plugin": "13.0.0", "cross-env": "10.1.0", @@ -18492,9 +18492,9 @@ } }, "node_modules/chromatic": { - "version": "13.1.2", - "resolved": "https://registry.npmjs.org/chromatic/-/chromatic-13.1.2.tgz", - "integrity": "sha512-jgVptQabJHOnzmmvLjbtfutREkWGhDDk2gVqMH6N+V7z56oIy4Sd2/U7ZxNvnVFPinZQMSjSdUce4b6JIP64Dg==", + "version": "13.3.1", + "resolved": "https://registry.npmjs.org/chromatic/-/chromatic-13.3.1.tgz", + "integrity": "sha512-qJ/el70Wo7jFgiXPpuukqxCEc7IKiH/e8MjTzIF9uKw+3XZ6GghOTTLC7lGfeZtosiQBMkRlYet77tC4KKHUng==", "dev": true, "license": "MIT", "bin": { diff --git a/package.json b/package.json index 0190bc37bec..32056a174b1 100644 --- a/package.json +++ b/package.json @@ -92,7 +92,7 @@ "babel-loader": "9.2.1", "base64-loader": "1.0.0", "browserslist": "4.23.2", - "chromatic": "13.1.2", + "chromatic": "13.3.1", "concurrently": "9.2.0", "copy-webpack-plugin": "13.0.0", "cross-env": "10.1.0", From d85b9986d0f97362d22cf30259dd4abaf3e91b6a Mon Sep 17 00:00:00 2001 From: Oscar Hinton Date: Wed, 29 Oct 2025 19:42:19 +0100 Subject: [PATCH 17/25] [CL-901] [CL-903] Unowned - Prefer signal & change detection (#16949) --- apps/browser/src/popup/app.component.ts | 2 ++ ...ktop-sync-verification-dialog.component.ts | 2 ++ apps/browser/src/popup/tabs-v2.component.ts | 2 ++ .../src/app/accounts/settings.component.ts | 2 ++ apps/desktop/src/app/app.component.ts | 14 ++++++++++ .../src/app/components/avatar.component.ts | 18 +++++++++++++ ...wser-sync-verification-dialog.component.ts | 2 ++ .../components/user-verification.component.ts | 2 ++ ...erify-native-messaging-dialog.component.ts | 2 ++ .../app/layout/account-switcher.component.ts | 2 ++ .../src/app/layout/header.component.ts | 2 ++ apps/desktop/src/app/layout/nav.component.ts | 2 ++ .../src/app/layout/search/search.component.ts | 2 ++ apps/web/src/app/app.component.ts | 2 ++ .../components/dynamic-avatar.component.ts | 12 +++++++++ .../environment-selector.component.ts | 2 ++ .../request-sm-access.component.ts | 2 ++ .../sm-landing.component.ts | 2 ++ .../app/settings/domain-rules.component.ts | 2 ++ .../src/app/settings/preferences.component.ts | 2 ++ .../bit-browser/src/popup/app.component.ts | 2 ++ .../bit-web/src/app/app.component.ts | 2 ++ .../activity/activity-card.component.ts | 6 +++++ .../guards/project-access.guard.spec.ts | 4 +++ .../integrations.component.spec.ts | 4 +++ .../integrations/integrations.component.ts | 2 ++ .../layout/layout.component.ts | 2 ++ .../layout/navigation.component.ts | 2 ++ .../overview/overview.component.ts | 2 ++ .../overview/section.component.ts | 4 +++ .../dialog/project-delete-dialog.component.ts | 2 ++ .../dialog/project-dialog.component.ts | 2 ++ .../project/project-people.component.ts | 2 ++ .../project/project-secrets.component.ts | 2 ++ .../project-service-accounts.component.ts | 2 ++ .../projects/project/project.component.ts | 2 ++ .../projects/projects/projects.component.ts | 2 ++ .../secrets/dialog/secret-delete.component.ts | 2 ++ .../secrets/dialog/secret-dialog.component.ts | 2 ++ .../dialog/secret-view-dialog.component.ts | 2 ++ .../secrets/secrets.component.ts | 2 ++ .../access/access-list.component.ts | 8 ++++++ .../access/access-tokens.component.ts | 2 ++ .../access-token-create-dialog.component.ts | 2 ++ .../dialogs/access-token-dialog.component.ts | 2 ++ .../dialogs/expiration-options.component.ts | 6 +++++ .../config/config.component.ts | 2 ++ ...service-account-delete-dialog.component.ts | 2 ++ .../service-account-dialog.component.ts | 2 ++ .../service-accounts-events.component.ts | 2 ++ .../service-account-access.guard.spec.ts | 4 +++ .../service-account-people.component.ts | 2 ++ .../service-account-projects.component.ts | 2 ++ .../service-account.component.ts | 2 ++ .../service-accounts-list.component.ts | 14 ++++++++++ .../service-accounts.component.ts | 2 ++ .../sm-import-error-dialog.component.ts | 2 ++ .../settings/porting/sm-export.component.ts | 2 ++ .../settings/porting/sm-import.component.ts | 2 ++ .../access-policy-selector.component.ts | 22 ++++++++++++++++ .../bulk-confirmation-dialog.component.ts | 2 ++ .../dialogs/bulk-status-dialog.component.ts | 2 ++ .../shared/new-menu.component.ts | 2 ++ .../shared/org-suspended.component.ts | 2 ++ .../shared/projects-list.component.ts | 16 ++++++++++++ .../shared/secrets-list.component.ts | 26 +++++++++++++++++++ .../dialog/secret-hard-delete.component.ts | 2 ++ .../trash/dialog/secret-restore.component.ts | 2 ++ .../secrets-manager/trash/trash.component.ts | 2 ++ eslint.config.mjs | 6 ++--- .../src/components/callout.component.ts | 14 ++++++++++ .../modal/dynamic-modal.component.ts | 4 +++ .../src/directives/api-action.directive.ts | 2 ++ .../src/directives/copy-text.directive.ts | 2 ++ .../src/directives/fallback-src.directive.ts | 2 ++ .../directives/if-feature.directive.spec.ts | 2 ++ .../src/directives/if-feature.directive.ts | 4 +++ .../directives/input-verbatim.directive.ts | 2 ++ .../src/directives/launch-click.directive.ts | 2 ++ .../src/directives/text-drag.directive.ts | 2 ++ .../directives/true-false-value.directive.ts | 4 +++ .../src/card/base-card/base-card.component.ts | 2 ++ .../src/card/card-content.component.ts | 2 ++ 83 files changed, 315 insertions(+), 3 deletions(-) diff --git a/apps/browser/src/popup/app.component.ts b/apps/browser/src/popup/app.component.ts index b85da665fa0..8f00569b720 100644 --- a/apps/browser/src/popup/app.component.ts +++ b/apps/browser/src/popup/app.component.ts @@ -67,6 +67,8 @@ import { initPopupClosedListener } from "../platform/services/popup-view-cache-b import { routerTransition } from "./app-routing.animations"; import { DesktopSyncVerificationDialogComponent } from "./components/desktop-sync-verification-dialog.component"; +// 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-root", styles: [], diff --git a/apps/browser/src/popup/components/desktop-sync-verification-dialog.component.ts b/apps/browser/src/popup/components/desktop-sync-verification-dialog.component.ts index 2ca24da6c75..510348927ce 100644 --- a/apps/browser/src/popup/components/desktop-sync-verification-dialog.component.ts +++ b/apps/browser/src/popup/components/desktop-sync-verification-dialog.component.ts @@ -15,6 +15,8 @@ export type DesktopSyncVerificationDialogParams = { fingerprint: string[]; }; +// 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: "desktop-sync-verification-dialog.component.html", imports: [JslibModule, ButtonModule, DialogModule], diff --git a/apps/browser/src/popup/tabs-v2.component.ts b/apps/browser/src/popup/tabs-v2.component.ts index f1e42799b35..1c409fee639 100644 --- a/apps/browser/src/popup/tabs-v2.component.ts +++ b/apps/browser/src/popup/tabs-v2.component.ts @@ -17,6 +17,8 @@ import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { NavButton } from "../platform/popup/layout/popup-tab-navigation.component"; +// 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-tabs-v2", templateUrl: "./tabs-v2.component.html", diff --git a/apps/desktop/src/app/accounts/settings.component.ts b/apps/desktop/src/app/accounts/settings.component.ts index 7666e9bef1b..abebdfa5fc3 100644 --- a/apps/desktop/src/app/accounts/settings.component.ts +++ b/apps/desktop/src/app/accounts/settings.component.ts @@ -67,6 +67,8 @@ import { DesktopSettingsService } from "../../platform/services/desktop-settings import { DesktopPremiumUpgradePromptService } from "../../services/desktop-premium-upgrade-prompt.service"; import { NativeMessagingManifestService } from "../services/native-messaging-manifest.service"; +// 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-settings", templateUrl: "settings.component.html", diff --git a/apps/desktop/src/app/app.component.ts b/apps/desktop/src/app/app.component.ts index 7f7eddcfe95..4b6dcab0dff 100644 --- a/apps/desktop/src/app/app.component.ts +++ b/apps/desktop/src/app/app.component.ts @@ -91,6 +91,8 @@ const BroadcasterSubscriptionId = "AppComponent"; const IdleTimeout = 60000 * 10; // 10 minutes const SyncInterval = 6 * 60 * 60 * 1000; // 6 hours +// 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-root", styles: [], @@ -115,14 +117,26 @@ const SyncInterval = 6 * 60 * 60 * 1000; // 6 hours standalone: false, }) export class AppComponent implements OnInit, OnDestroy { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @ViewChild("settings", { read: ViewContainerRef, static: true }) settingsRef: ViewContainerRef; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @ViewChild("premium", { read: ViewContainerRef, static: true }) premiumRef: ViewContainerRef; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @ViewChild("passwordHistory", { read: ViewContainerRef, static: true }) passwordHistoryRef: ViewContainerRef; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @ViewChild("exportVault", { read: ViewContainerRef, static: true }) exportVaultModalRef: ViewContainerRef; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @ViewChild("appGenerator", { read: ViewContainerRef, static: true }) generatorModalRef: ViewContainerRef; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @ViewChild("loginApproval", { read: ViewContainerRef, static: true }) loginApprovalModalRef: ViewContainerRef; diff --git a/apps/desktop/src/app/components/avatar.component.ts b/apps/desktop/src/app/components/avatar.component.ts index 1fba864686c..d17ebb5b942 100644 --- a/apps/desktop/src/app/components/avatar.component.ts +++ b/apps/desktop/src/app/components/avatar.component.ts @@ -5,20 +5,38 @@ import { DomSanitizer, SafeResourceUrl } from "@angular/platform-browser"; import { Utils } from "@bitwarden/common/platform/misc/utils"; +// 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-avatar", template: ``, standalone: false, }) export class AvatarComponent implements OnChanges, OnInit { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() size = 45; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() charCount = 2; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() fontSize = 20; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() dynamic = false; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() circle = false; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() color?: string; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() id?: string; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() text?: string; private svgCharCount = 2; diff --git a/apps/desktop/src/app/components/browser-sync-verification-dialog.component.ts b/apps/desktop/src/app/components/browser-sync-verification-dialog.component.ts index 713dc07e803..5d3c777f333 100644 --- a/apps/desktop/src/app/components/browser-sync-verification-dialog.component.ts +++ b/apps/desktop/src/app/components/browser-sync-verification-dialog.component.ts @@ -7,6 +7,8 @@ export type BrowserSyncVerificationDialogParams = { fingerprint: string[]; }; +// 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: "browser-sync-verification-dialog.component.html", imports: [JslibModule, ButtonModule, DialogModule], diff --git a/apps/desktop/src/app/components/user-verification.component.ts b/apps/desktop/src/app/components/user-verification.component.ts index 31d38b10183..e19916c3d6b 100644 --- a/apps/desktop/src/app/components/user-verification.component.ts +++ b/apps/desktop/src/app/components/user-verification.component.ts @@ -11,6 +11,8 @@ import { FormFieldModule } from "@bitwarden/components"; * @deprecated Jan 24, 2024: Use new libs/auth UserVerificationDialogComponent or UserVerificationFormInputComponent instead. * Each client specific component should eventually be converted over to use one of these new components. */ +// 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-user-verification", imports: [CommonModule, JslibModule, ReactiveFormsModule, FormFieldModule, FormsModule], diff --git a/apps/desktop/src/app/components/verify-native-messaging-dialog.component.ts b/apps/desktop/src/app/components/verify-native-messaging-dialog.component.ts index 72284d007b6..14c2b137d73 100644 --- a/apps/desktop/src/app/components/verify-native-messaging-dialog.component.ts +++ b/apps/desktop/src/app/components/verify-native-messaging-dialog.component.ts @@ -7,6 +7,8 @@ export type VerifyNativeMessagingDialogData = { applicationName: string; }; +// 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: "verify-native-messaging-dialog.component.html", imports: [JslibModule, ButtonModule, DialogModule], diff --git a/apps/desktop/src/app/layout/account-switcher.component.ts b/apps/desktop/src/app/layout/account-switcher.component.ts index a54674c3a1e..6a7e274ade4 100644 --- a/apps/desktop/src/app/layout/account-switcher.component.ts +++ b/apps/desktop/src/app/layout/account-switcher.component.ts @@ -31,6 +31,8 @@ type InactiveAccount = ActiveAccount & { authenticationStatus: AuthenticationStatus; }; +// 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-account-switcher", templateUrl: "account-switcher.component.html", diff --git a/apps/desktop/src/app/layout/header.component.ts b/apps/desktop/src/app/layout/header.component.ts index 9aef093423f..9630e3b1914 100644 --- a/apps/desktop/src/app/layout/header.component.ts +++ b/apps/desktop/src/app/layout/header.component.ts @@ -1,5 +1,7 @@ import { Component } from "@angular/core"; +// 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-header", templateUrl: "header.component.html", diff --git a/apps/desktop/src/app/layout/nav.component.ts b/apps/desktop/src/app/layout/nav.component.ts index bcc2b57fb17..72064a4de51 100644 --- a/apps/desktop/src/app/layout/nav.component.ts +++ b/apps/desktop/src/app/layout/nav.component.ts @@ -4,6 +4,8 @@ import { RouterLink, RouterLinkActive } from "@angular/router"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +// 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-nav", templateUrl: "nav.component.html", diff --git a/apps/desktop/src/app/layout/search/search.component.ts b/apps/desktop/src/app/layout/search/search.component.ts index 70196d74dda..c0b088a13d9 100644 --- a/apps/desktop/src/app/layout/search/search.component.ts +++ b/apps/desktop/src/app/layout/search/search.component.ts @@ -8,6 +8,8 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv import { SearchBarService, SearchBarState } from "./search-bar.service"; +// 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-search", templateUrl: "search.component.html", diff --git a/apps/web/src/app/app.component.ts b/apps/web/src/app/app.component.ts index 60911173308..13c4207992c 100644 --- a/apps/web/src/app/app.component.ts +++ b/apps/web/src/app/app.component.ts @@ -33,6 +33,8 @@ import { KeyService, BiometricStateService } from "@bitwarden/key-management"; const BroadcasterSubscriptionId = "AppComponent"; const IdleTimeout = 60000 * 10; // 10 minutes +// 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-root", templateUrl: "app.component.html", diff --git a/apps/web/src/app/components/dynamic-avatar.component.ts b/apps/web/src/app/components/dynamic-avatar.component.ts index 8cd73862151..ddaaa21758b 100644 --- a/apps/web/src/app/components/dynamic-avatar.component.ts +++ b/apps/web/src/app/components/dynamic-avatar.component.ts @@ -8,6 +8,8 @@ import { AvatarService } from "@bitwarden/common/auth/abstractions/avatar.servic import { SharedModule } from "../shared"; type SizeTypes = "xlarge" | "large" | "default" | "small" | "xsmall"; +// 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: "dynamic-avatar", imports: [SharedModule], @@ -25,10 +27,20 @@ type SizeTypes = "xlarge" | "large" | "default" | "small" | "xsmall"; `, }) export class DynamicAvatarComponent implements OnDestroy { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() border = false; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() id: string; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() text: string; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() title: string; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() size: SizeTypes = "default"; private destroy$ = new Subject(); diff --git a/apps/web/src/app/components/environment-selector/environment-selector.component.ts b/apps/web/src/app/components/environment-selector/environment-selector.component.ts index 37e5ae0c3d8..4f77cc96bf7 100644 --- a/apps/web/src/app/components/environment-selector/environment-selector.component.ts +++ b/apps/web/src/app/components/environment-selector/environment-selector.component.ts @@ -12,6 +12,8 @@ import { Utils } from "@bitwarden/common/platform/misc/utils"; import { SharedModule } from "../../shared"; +// 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: "environment-selector", templateUrl: "environment-selector.component.html", diff --git a/apps/web/src/app/secrets-manager/secrets-manager-landing/request-sm-access.component.ts b/apps/web/src/app/secrets-manager/secrets-manager-landing/request-sm-access.component.ts index 0e32321a0b3..afac3b059a8 100644 --- a/apps/web/src/app/secrets-manager/secrets-manager-landing/request-sm-access.component.ts +++ b/apps/web/src/app/secrets-manager/secrets-manager-landing/request-sm-access.component.ts @@ -19,6 +19,8 @@ import { RequestSMAccessRequest } from "../models/requests/request-sm-access.req import { SmLandingApiService } from "./sm-landing-api.service"; +// 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-request-sm-access", templateUrl: "request-sm-access.component.html", diff --git a/apps/web/src/app/secrets-manager/secrets-manager-landing/sm-landing.component.ts b/apps/web/src/app/secrets-manager/secrets-manager-landing/sm-landing.component.ts index 301e6f7dfad..c1cc2b63e28 100644 --- a/apps/web/src/app/secrets-manager/secrets-manager-landing/sm-landing.component.ts +++ b/apps/web/src/app/secrets-manager/secrets-manager-landing/sm-landing.component.ts @@ -12,6 +12,8 @@ import { NoItemsModule, SearchModule } from "@bitwarden/components"; import { HeaderModule } from "../../layouts/header/header.module"; import { SharedModule } from "../../shared/shared.module"; +// 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-sm-landing", imports: [SharedModule, SearchModule, NoItemsModule, HeaderModule], diff --git a/apps/web/src/app/settings/domain-rules.component.ts b/apps/web/src/app/settings/domain-rules.component.ts index 6c4cb13d5fa..0e9d2f422d9 100644 --- a/apps/web/src/app/settings/domain-rules.component.ts +++ b/apps/web/src/app/settings/domain-rules.component.ts @@ -12,6 +12,8 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { HeaderModule } from "../layouts/header/header.module"; import { SharedModule } from "../shared"; +// 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-domain-rules", templateUrl: "domain-rules.component.html", diff --git a/apps/web/src/app/settings/preferences.component.ts b/apps/web/src/app/settings/preferences.component.ts index 58a072ce76a..c1e8fce98ca 100644 --- a/apps/web/src/app/settings/preferences.component.ts +++ b/apps/web/src/app/settings/preferences.component.ts @@ -39,6 +39,8 @@ import { PermitCipherDetailsPopoverComponent } from "@bitwarden/vault"; import { HeaderModule } from "../layouts/header/header.module"; import { SharedModule } from "../shared"; +// 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-preferences", templateUrl: "preferences.component.html", diff --git a/bitwarden_license/bit-browser/src/popup/app.component.ts b/bitwarden_license/bit-browser/src/popup/app.component.ts index 339681d66da..f880b946cfa 100644 --- a/bitwarden_license/bit-browser/src/popup/app.component.ts +++ b/bitwarden_license/bit-browser/src/popup/app.component.ts @@ -2,6 +2,8 @@ import { Component, OnInit } from "@angular/core"; import { AppComponent as BaseAppComponent } from "@bitwarden/browser/popup/app.component"; +// 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-root", templateUrl: "../../../../apps/browser/src/popup/app.component.html", diff --git a/bitwarden_license/bit-web/src/app/app.component.ts b/bitwarden_license/bit-web/src/app/app.component.ts index abfb79b8f18..caa1e4b0cf6 100644 --- a/bitwarden_license/bit-web/src/app/app.component.ts +++ b/bitwarden_license/bit-web/src/app/app.component.ts @@ -2,6 +2,8 @@ import { Component } from "@angular/core"; import { AppComponent as BaseAppComponent } from "@bitwarden/web-vault/app/app.component"; +// 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-root", templateUrl: "../../../../apps/web/src/app/app.component.html", diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/activity-card.component.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/activity-card.component.ts index 84c763841b5..427e7262f50 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/activity-card.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/activity-card.component.ts @@ -39,11 +39,15 @@ export class ActivityCardComponent { /** * The text to display for the action link */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() actionText: string = ""; /** * Show action link */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() showActionLink: boolean = false; /** @@ -78,6 +82,8 @@ export class ActivityCardComponent { /** * Event emitted when action link is clicked */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() actionClick = new EventEmitter(); constructor(private router: Router) {} diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/guards/project-access.guard.spec.ts b/bitwarden_license/bit-web/src/app/secrets-manager/guards/project-access.guard.spec.ts index f442c85f46d..79c022e8fd2 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/guards/project-access.guard.spec.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/guards/project-access.guard.spec.ts @@ -20,12 +20,16 @@ import { ProjectService } from "../projects/project.service"; import { projectAccessGuard } from "./project-access.guard"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ template: "", standalone: false, }) export class GuardedRouteTestComponent {} +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ template: "", standalone: false, diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.component.spec.ts b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.component.spec.ts index 978cfeb1aa4..0e8c46c8864 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.component.spec.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.component.spec.ts @@ -21,6 +21,8 @@ import { IntegrationGridComponent } from "../../dirt/organization-integrations/i import { IntegrationsComponent } from "./integrations.component"; +// 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-header", template: "
", @@ -28,6 +30,8 @@ import { IntegrationsComponent } from "./integrations.component"; }) class MockHeaderComponent {} +// 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: "sm-new-menu", template: "
", diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.component.ts index b2279775191..37c7a93d27f 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.component.ts @@ -3,6 +3,8 @@ import { Component } from "@angular/core"; import { Integration } from "@bitwarden/bit-common/dirt/organization-integrations/models/integration"; import { IntegrationType } from "@bitwarden/common/enums"; +// 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: "sm-integrations", templateUrl: "./integrations.component.html", diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/layout/layout.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/layout/layout.component.ts index b50e586c337..00a4c6cc4d4 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/layout/layout.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/layout/layout.component.ts @@ -1,5 +1,7 @@ import { Component, OnInit } from "@angular/core"; +// 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: "sm-layout", templateUrl: "./layout.component.html", diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/layout/navigation.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/layout/navigation.component.ts index a714bc0d543..be9124ee3e1 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/layout/navigation.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/layout/navigation.component.ts @@ -31,6 +31,8 @@ import { ServiceAccountService } from "../service-accounts/service-account.servi import { SecretsManagerPortingApiService } from "../settings/services/sm-porting-api.service"; import { CountService } from "../shared/counts/count.service"; +// 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: "sm-navigation", templateUrl: "./navigation.component.html", diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/overview/overview.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/overview/overview.component.ts index e301c0462c3..12a5432c4b8 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/overview/overview.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/overview/overview.component.ts @@ -75,6 +75,8 @@ type OrganizationTasks = { createServiceAccount: boolean; }; +// 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: "sm-overview", templateUrl: "./overview.component.html", diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/overview/section.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/overview/section.component.ts index 6b71c81f09e..0691ed9dd73 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/overview/section.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/overview/section.component.ts @@ -1,11 +1,15 @@ import { Component, Input } from "@angular/core"; +// 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: "sm-section", templateUrl: "./section.component.html", standalone: false, }) export class SectionComponent { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() open = true; /** diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/projects/dialog/project-delete-dialog.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/projects/dialog/project-delete-dialog.component.ts index 8cdb1bb4d69..3ddf3233b38 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/projects/dialog/project-delete-dialog.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/projects/dialog/project-delete-dialog.component.ts @@ -25,6 +25,8 @@ export interface ProjectDeleteOperation { projects: ProjectListView[]; } +// 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: "./project-delete-dialog.component.html", standalone: false, diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/projects/dialog/project-dialog.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/projects/dialog/project-dialog.component.ts index 819f2107fcf..2f6b2229d75 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/projects/dialog/project-dialog.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/projects/dialog/project-dialog.component.ts @@ -25,6 +25,8 @@ export interface ProjectOperation { projectId?: string; } +// 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: "./project-dialog.component.html", standalone: false, diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-people.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-people.component.ts index ec7397a22a8..49b016e921c 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-people.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-people.component.ts @@ -24,6 +24,8 @@ import { import { ApItemEnum } from "../../shared/access-policies/access-policy-selector/models/enums/ap-item.enum"; import { AccessPolicyService } from "../../shared/access-policies/access-policy.service"; +// 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: "sm-project-people", templateUrl: "./project-people.component.html", diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-secrets.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-secrets.component.ts index 5c83f784431..7112a28010f 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-secrets.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-secrets.component.ts @@ -41,6 +41,8 @@ import { import { SecretService } from "../../secrets/secret.service"; import { SecretsListComponent } from "../../shared/secrets-list.component"; import { ProjectService } from "../project.service"; +// 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: "sm-project-secrets", templateUrl: "./project-secrets.component.html", diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-service-accounts.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-service-accounts.component.ts index fc3a489bce9..e2fd8556621 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-service-accounts.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-service-accounts.component.ts @@ -22,6 +22,8 @@ import { } from "../../shared/access-policies/access-policy-selector/models/ap-item-view.type"; import { AccessPolicyService } from "../../shared/access-policies/access-policy.service"; +// 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: "sm-project-service-accounts", templateUrl: "./project-service-accounts.component.html", diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project.component.ts index c79ebd733c0..7c1812e3f26 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project.component.ts @@ -34,6 +34,8 @@ import { } from "../dialog/project-dialog.component"; import { ProjectService } from "../project.service"; +// 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: "sm-project", templateUrl: "./project.component.html", diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/projects/projects/projects.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/projects/projects/projects.component.ts index 81a568f0c65..10e75cfb75a 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/projects/projects/projects.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/projects/projects/projects.component.ts @@ -40,6 +40,8 @@ import { } from "../dialog/project-dialog.component"; import { ProjectService } from "../project.service"; +// 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: "sm-projects", templateUrl: "./projects.component.html", diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/secrets/dialog/secret-delete.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/secrets/dialog/secret-delete.component.ts index 6340cc42f3b..344a20f02c2 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/secrets/dialog/secret-delete.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/secrets/dialog/secret-delete.component.ts @@ -18,6 +18,8 @@ export interface SecretDeleteOperation { secrets: SecretListView[]; } +// 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: "./secret-delete.component.html", standalone: false, diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/secrets/dialog/secret-dialog.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/secrets/dialog/secret-dialog.component.ts index 9172d44965d..6376b58423d 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/secrets/dialog/secret-dialog.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/secrets/dialog/secret-dialog.component.ts @@ -67,6 +67,8 @@ export interface SecretOperation { organizationEnabled: boolean; } +// 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: "./secret-dialog.component.html", standalone: false, diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/secrets/dialog/secret-view-dialog.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/secrets/dialog/secret-view-dialog.component.ts index b719014a382..ace8db4e6ba 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/secrets/dialog/secret-view-dialog.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/secrets/dialog/secret-view-dialog.component.ts @@ -10,6 +10,8 @@ export interface SecretViewDialogParams { secretId: string; } +// 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: "./secret-view-dialog.component.html", standalone: false, diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/secrets/secrets.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/secrets/secrets.component.ts index ca093f449c9..46cccb1d95d 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/secrets/secrets.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/secrets/secrets.component.ts @@ -34,6 +34,8 @@ import { } from "./dialog/secret-view-dialog.component"; import { SecretService } from "./secret.service"; +// 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: "sm-secrets", templateUrl: "./secrets.component.html", diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/access/access-list.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/access/access-list.component.ts index a714729d96f..7a8c0b37408 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/access/access-list.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/access/access-list.component.ts @@ -5,12 +5,16 @@ import { Component, EventEmitter, Input, Output } from "@angular/core"; import { AccessTokenView } from "../models/view/access-token.view"; +// 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: "sm-access-list", templateUrl: "./access-list.component.html", standalone: false, }) export class AccessListComponent { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() get tokens(): AccessTokenView[] { return this._tokens; @@ -21,7 +25,11 @@ export class AccessListComponent { } private _tokens: AccessTokenView[]; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() newAccessTokenEvent = new EventEmitter(); + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() revokeAccessTokensEvent = new EventEmitter(); protected selection = new SelectionModel(true, []); diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/access/access-tokens.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/access/access-tokens.component.ts index b9643ce8fd8..4e9069cd6cb 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/access/access-tokens.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/access/access-tokens.component.ts @@ -24,6 +24,8 @@ import { ServiceAccountService } from "../service-account.service"; import { AccessService } from "./access.service"; import { AccessTokenCreateDialogComponent } from "./dialogs/access-token-create-dialog.component"; +// 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: "sm-access-tokens", templateUrl: "./access-tokens.component.html", diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/access/dialogs/access-token-create-dialog.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/access/dialogs/access-token-create-dialog.component.ts index dfbe0a1511d..3aca93572ef 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/access/dialogs/access-token-create-dialog.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/access/dialogs/access-token-create-dialog.component.ts @@ -15,6 +15,8 @@ export interface AccessTokenOperation { serviceAccountView: ServiceAccountView; } +// 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: "./access-token-create-dialog.component.html", standalone: false, diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/access/dialogs/access-token-dialog.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/access/dialogs/access-token-dialog.component.ts index 0259b8d6e90..cf5118c5062 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/access/dialogs/access-token-dialog.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/access/dialogs/access-token-dialog.component.ts @@ -12,6 +12,8 @@ export interface AccessTokenDetails { accessToken: string; } +// 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: "./access-token-dialog.component.html", standalone: false, diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/access/dialogs/expiration-options.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/access/dialogs/expiration-options.component.ts index 891501874ff..a0db42d03b0 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/access/dialogs/expiration-options.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/access/dialogs/expiration-options.component.ts @@ -18,6 +18,8 @@ import { Subject, takeUntil } from "rxjs"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +// 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: "sm-expiration-options", templateUrl: "./expiration-options.component.html", @@ -40,8 +42,12 @@ export class ExpirationOptionsComponent { private destroy$ = new Subject(); + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() expirationDayOptions: number[]; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() set touched(val: boolean) { if (val) { this.form.markAllAsTouched(); diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/config/config.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/config/config.component.ts index f85cde90306..18ef397c6ae 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/config/config.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/config/config.component.ts @@ -24,6 +24,8 @@ class ServiceAccountConfig { projects: ProjectListView[]; } +// 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: "sm-service-account-config", templateUrl: "./config.component.html", diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/dialog/service-account-delete-dialog.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/dialog/service-account-delete-dialog.component.ts index 5edc57d8c74..638ee6862a3 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/dialog/service-account-delete-dialog.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/dialog/service-account-delete-dialog.component.ts @@ -25,6 +25,8 @@ export interface ServiceAccountDeleteOperation { serviceAccounts: ServiceAccountView[]; } +// 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: "./service-account-delete-dialog.component.html", standalone: false, diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/dialog/service-account-dialog.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/dialog/service-account-dialog.component.ts index 250e0870ecf..5c6072807a6 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/dialog/service-account-dialog.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/dialog/service-account-dialog.component.ts @@ -24,6 +24,8 @@ export interface ServiceAccountOperation { organizationEnabled: boolean; } +// 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: "./service-account-dialog.component.html", standalone: false, diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/event-logs/service-accounts-events.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/event-logs/service-accounts-events.component.ts index 2e364df1423..5968933064d 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/event-logs/service-accounts-events.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/event-logs/service-accounts-events.component.ts @@ -17,6 +17,8 @@ import { EventExportService } from "@bitwarden/web-vault/app/tools/event-export" import { ServiceAccountEventLogApiService } from "./service-account-event-log-api.service"; +// 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: "sm-service-accounts-events", templateUrl: "./service-accounts-events.component.html", diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/guards/service-account-access.guard.spec.ts b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/guards/service-account-access.guard.spec.ts index e0bcad8d6e9..e7b258ed1c2 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/guards/service-account-access.guard.spec.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/guards/service-account-access.guard.spec.ts @@ -20,12 +20,16 @@ import { ServiceAccountService } from "../service-account.service"; import { serviceAccountAccessGuard } from "./service-account-access.guard"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ template: "", standalone: false, }) export class GuardedRouteTestComponent {} +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ template: "", standalone: false, diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/people/service-account-people.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/people/service-account-people.component.ts index 4449757167d..42ab2ec613b 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/people/service-account-people.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/people/service-account-people.component.ts @@ -25,6 +25,8 @@ import { ApItemEnum } from "../../shared/access-policies/access-policy-selector/ import { ApPermissionEnum } from "../../shared/access-policies/access-policy-selector/models/enums/ap-permission.enum"; import { AccessPolicyService } from "../../shared/access-policies/access-policy.service"; +// 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: "sm-service-account-people", templateUrl: "./service-account-people.component.html", diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/projects/service-account-projects.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/projects/service-account-projects.component.ts index af334b22c63..6d4490bad3c 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/projects/service-account-projects.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/projects/service-account-projects.component.ts @@ -22,6 +22,8 @@ import { } from "../../shared/access-policies/access-policy-selector/models/ap-item-view.type"; import { AccessPolicyService } from "../../shared/access-policies/access-policy.service"; +// 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: "sm-service-account-projects", templateUrl: "./service-account-projects.component.html", diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/service-account.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/service-account.component.ts index 5eb074e3e99..285f03acb01 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/service-account.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/service-account.component.ts @@ -15,6 +15,8 @@ import { AccessService } from "./access/access.service"; import { AccessTokenCreateDialogComponent } from "./access/dialogs/access-token-create-dialog.component"; import { ServiceAccountService } from "./service-account.service"; +// 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: "sm-service-account", templateUrl: "./service-account.component.html", diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/service-accounts-list.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/service-accounts-list.component.ts index 21f11d6bfed..4febda9ea28 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/service-accounts-list.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/service-accounts-list.component.ts @@ -21,6 +21,8 @@ import { ServiceAccountView, } from "../models/view/service-account.view"; +// 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: "sm-service-accounts-list", templateUrl: "./service-accounts-list.component.html", @@ -29,6 +31,8 @@ import { export class ServiceAccountsListComponent implements OnDestroy, OnInit { protected dataSource = new TableDataSource(); + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() get serviceAccounts(): ServiceAccountSecretsDetailsView[] { return this._serviceAccounts; @@ -40,15 +44,25 @@ export class ServiceAccountsListComponent implements OnDestroy, OnInit { } private _serviceAccounts: ServiceAccountSecretsDetailsView[]; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() set search(search: string) { this.selection.clear(); this.dataSource.filter = search; } + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() newServiceAccountEvent = new EventEmitter(); + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() deleteServiceAccountsEvent = new EventEmitter(); + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() onServiceAccountCheckedEvent = new EventEmitter(); + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() editServiceAccountEvent = new EventEmitter(); private destroy$: Subject = new Subject(); diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/service-accounts.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/service-accounts.component.ts index 345fff03876..5d6b4fd49de 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/service-accounts.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/service-accounts.component.ts @@ -30,6 +30,8 @@ import { } from "./dialog/service-account-dialog.component"; import { ServiceAccountService } from "./service-account.service"; +// 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: "sm-service-accounts", templateUrl: "./service-accounts.component.html", diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/settings/dialog/sm-import-error-dialog.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/settings/dialog/sm-import-error-dialog.component.ts index 0bed0355a8c..85e054d998b 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/settings/dialog/sm-import-error-dialog.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/settings/dialog/sm-import-error-dialog.component.ts @@ -10,6 +10,8 @@ export interface SecretsManagerImportErrorDialogOperation { error: SecretsManagerImportError; } +// 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: "./sm-import-error-dialog.component.html", standalone: false, diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/settings/porting/sm-export.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/settings/porting/sm-export.component.ts index c2b726803c5..e2b66d9ffa6 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/settings/porting/sm-export.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/settings/porting/sm-export.component.ts @@ -26,6 +26,8 @@ type ExportFormat = { fileExtension: string; }; +// 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: "sm-export", templateUrl: "./sm-export.component.html", diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/settings/porting/sm-import.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/settings/porting/sm-import.component.ts index 65075d12bf6..c2ffe5536b8 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/settings/porting/sm-import.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/settings/porting/sm-import.component.ts @@ -18,6 +18,8 @@ import { import { SecretsManagerImportError } from "../models/error/sm-import-error"; import { SecretsManagerPortingApiService } from "../services/sm-porting-api.service"; +// 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: "sm-import", templateUrl: "./sm-import.component.html", diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/access-policy-selector.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/access-policy-selector.component.ts index fba3ff03ee0..2bb4d6cb37f 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/access-policy-selector.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/access-policy-selector.component.ts @@ -20,6 +20,8 @@ import { ApItemViewType } from "./models/ap-item-view.type"; import { ApItemEnumUtil, ApItemEnum } from "./models/enums/ap-item.enum"; import { ApPermissionEnum } from "./models/enums/ap-permission.enum"; +// 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: "sm-access-policy-selector", templateUrl: "access-policy-selector.component.html", @@ -108,23 +110,43 @@ export class AccessPolicySelectorComponent implements ControlValueAccessor, OnIn disabled: boolean; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() loading: boolean; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() addButtonMode: boolean; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() label: string; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() hint: string; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() columnTitle: string; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() emptyMessage: string; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() permissionList = [ { perm: ApPermissionEnum.CanRead, labelId: "canRead" }, { perm: ApPermissionEnum.CanReadWrite, labelId: "canReadWrite" }, ]; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() initialPermission = ApPermissionEnum.CanRead; // Pass in a static permission that wil be the only option for a given selector instance. // Will ignore permissionList and initialPermission. + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() staticPermission: ApPermissionEnum; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() get items(): ApItemViewType[] { return this.selectionList.allItems; diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/dialogs/bulk-confirmation-dialog.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/shared/dialogs/bulk-confirmation-dialog.component.ts index 9d2a3715e16..0f0991d52a9 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/shared/dialogs/bulk-confirmation-dialog.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/dialogs/bulk-confirmation-dialog.component.ts @@ -22,6 +22,8 @@ export enum BulkConfirmationResult { Cancel, } +// 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: "sm-bulk-confirmation-dialog", templateUrl: "./bulk-confirmation-dialog.component.html", diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/dialogs/bulk-status-dialog.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/shared/dialogs/bulk-status-dialog.component.ts index fc7890f1654..8e27b551e55 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/shared/dialogs/bulk-status-dialog.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/dialogs/bulk-status-dialog.component.ts @@ -18,6 +18,8 @@ export class BulkOperationStatus { errorMessage?: string; } +// 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: "./bulk-status-dialog.component.html", standalone: false, diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/new-menu.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/shared/new-menu.component.ts index 18823130d22..6c3d4228c06 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/shared/new-menu.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/new-menu.component.ts @@ -26,6 +26,8 @@ import { ServiceAccountOperation, } from "../service-accounts/dialog/service-account-dialog.component"; +// 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: "sm-new-menu", templateUrl: "./new-menu.component.html", diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/org-suspended.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/shared/org-suspended.component.ts index 6777df7ef7a..f2e0d48fe1d 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/shared/org-suspended.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/org-suspended.component.ts @@ -10,6 +10,8 @@ import { import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; +// 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: "./org-suspended.component.html", standalone: false, diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/projects-list.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/shared/projects-list.component.ts index 31114bcd1c4..5d3c806f386 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/shared/projects-list.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/projects-list.component.ts @@ -20,12 +20,16 @@ import { openEntityEventsDialog } from "@bitwarden/web-vault/app/admin-console/o import { ProjectListView } from "../models/view/project-list.view"; import { ProjectView } from "../models/view/project.view"; +// 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: "sm-projects-list", templateUrl: "./projects-list.component.html", standalone: false, }) export class ProjectsListComponent implements OnInit { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() get projects(): ProjectListView[] { return this._projects; @@ -40,17 +44,29 @@ export class ProjectsListComponent implements OnInit { protected isAdmin$: Observable; private destroy$: Subject = new Subject(); + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() showMenus?: boolean = true; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() set search(search: string) { this.selection.clear(); this.dataSource.filter = search; } + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() editProjectEvent = new EventEmitter(); + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() deleteProjectEvent = new EventEmitter(); + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() newProjectEvent = new EventEmitter(); + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() copiedProjectUUIdEvent = new EventEmitter(); selection = new SelectionModel(true, []); diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/secrets-list.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/shared/secrets-list.component.ts index 4ef7dbf22e7..05e38baff69 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/shared/secrets-list.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/secrets-list.component.ts @@ -21,6 +21,8 @@ import { SecretListView } from "../models/view/secret-list.view"; import { SecretView } from "../models/view/secret.view"; import { SecretService } from "../secrets/secret.service"; +// 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: "sm-secrets-list", templateUrl: "./secrets-list.component.html", @@ -29,6 +31,8 @@ import { SecretService } from "../secrets/secret.service"; export class SecretsListComponent implements OnDestroy, OnInit { protected dataSource = new TableDataSource(); + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() get secrets(): SecretListView[] { return this._secrets; @@ -40,22 +44,44 @@ export class SecretsListComponent implements OnDestroy, OnInit { } private _secrets: SecretListView[]; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() set search(search: string) { this.selection.clear(); this.dataSource.filter = search; } + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() trash: boolean; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() editSecretEvent = new EventEmitter(); + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() viewSecretEvent = new EventEmitter(); + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() copySecretNameEvent = new EventEmitter(); + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() copySecretValueEvent = new EventEmitter(); + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() copySecretUuidEvent = new EventEmitter(); + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() onSecretCheckedEvent = new EventEmitter(); + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() deleteSecretsEvent = new EventEmitter(); + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() newSecretEvent = new EventEmitter(); + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() restoreSecretsEvent = new EventEmitter(); private destroy$: Subject = new Subject(); diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/trash/dialog/secret-hard-delete.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/trash/dialog/secret-hard-delete.component.ts index 29f9a85250c..521550185f1 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/trash/dialog/secret-hard-delete.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/trash/dialog/secret-hard-delete.component.ts @@ -13,6 +13,8 @@ export interface SecretHardDeleteOperation { organizationId: string; } +// 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: "./secret-hard-delete.component.html", standalone: false, diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/trash/dialog/secret-restore.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/trash/dialog/secret-restore.component.ts index 712757445be..034b6f8de00 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/trash/dialog/secret-restore.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/trash/dialog/secret-restore.component.ts @@ -13,6 +13,8 @@ export interface SecretRestoreOperation { organizationId: string; } +// 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: "./secret-restore.component.html", standalone: false, diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/trash/trash.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/trash/trash.component.ts index 4392ae8b1bb..b4da7769127 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/trash/trash.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/trash/trash.component.ts @@ -21,6 +21,8 @@ import { SecretRestoreOperation, } from "./dialog/secret-restore.component"; +// 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: "sm-trash", templateUrl: "./trash.component.html", diff --git a/eslint.config.mjs b/eslint.config.mjs index d8b2094c37c..656972d2421 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -72,9 +72,9 @@ export default tseslint.config( "@angular-eslint/no-output-on-prefix": 0, "@angular-eslint/no-output-rename": 0, "@angular-eslint/no-outputs-metadata-property": 0, - "@angular-eslint/prefer-on-push-component-change-detection": "warn", - "@angular-eslint/prefer-output-emitter-ref": "warn", - "@angular-eslint/prefer-signals": "warn", + "@angular-eslint/prefer-on-push-component-change-detection": "error", + "@angular-eslint/prefer-output-emitter-ref": "error", + "@angular-eslint/prefer-signals": "error", "@angular-eslint/prefer-standalone": 0, "@angular-eslint/use-lifecycle-interface": "error", "@angular-eslint/use-pipe-transform-interface": 0, diff --git a/libs/angular/src/components/callout.component.ts b/libs/angular/src/components/callout.component.ts index 215de49f676..9630b761076 100644 --- a/libs/angular/src/components/callout.component.ts +++ b/libs/angular/src/components/callout.component.ts @@ -9,17 +9,31 @@ import { CalloutTypes } from "@bitwarden/components"; /** * @deprecated use the CL's `CalloutComponent` instead */ +// 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-callout", templateUrl: "callout.component.html", standalone: false, }) export class DeprecatedCalloutComponent implements OnInit { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() type: CalloutTypes = "info"; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() icon: string; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() title: string; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() enforcedPolicyOptions: MasterPasswordPolicyOptions; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() enforcedPolicyMessage: string; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() useAlertRole = false; calloutStyle: string; diff --git a/libs/angular/src/components/modal/dynamic-modal.component.ts b/libs/angular/src/components/modal/dynamic-modal.component.ts index 77491193916..ea40dd1a877 100644 --- a/libs/angular/src/components/modal/dynamic-modal.component.ts +++ b/libs/angular/src/components/modal/dynamic-modal.component.ts @@ -15,6 +15,8 @@ import { import { ModalRef } from "./modal.ref"; +// 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-modal", template: "", @@ -23,6 +25,8 @@ import { ModalRef } from "./modal.ref"; export class DynamicModalComponent implements AfterViewInit, OnDestroy { componentRef: ComponentRef; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @ViewChild("modalContent", { read: ViewContainerRef, static: true }) modalContentRef: ViewContainerRef; diff --git a/libs/angular/src/directives/api-action.directive.ts b/libs/angular/src/directives/api-action.directive.ts index 85ba8a7489c..6873e448589 100644 --- a/libs/angular/src/directives/api-action.directive.ts +++ b/libs/angular/src/directives/api-action.directive.ts @@ -18,6 +18,8 @@ import { ValidationService } from "@bitwarden/common/platform/abstractions/valid standalone: false, }) export class ApiActionDirective implements OnChanges { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() appApiAction: Promise; constructor( diff --git a/libs/angular/src/directives/copy-text.directive.ts b/libs/angular/src/directives/copy-text.directive.ts index 0f9018e19ad..aefb26ef07e 100644 --- a/libs/angular/src/directives/copy-text.directive.ts +++ b/libs/angular/src/directives/copy-text.directive.ts @@ -15,6 +15,8 @@ export class CopyTextDirective { private platformUtilsService: PlatformUtilsService, ) {} + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input("appCopyText") copyText: string; @HostListener("copy") onCopy() { diff --git a/libs/angular/src/directives/fallback-src.directive.ts b/libs/angular/src/directives/fallback-src.directive.ts index f1225245912..b63dc8671cf 100644 --- a/libs/angular/src/directives/fallback-src.directive.ts +++ b/libs/angular/src/directives/fallback-src.directive.ts @@ -7,6 +7,8 @@ import { Directive, ElementRef, HostListener, Input } from "@angular/core"; standalone: false, }) export class FallbackSrcDirective { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input("appFallbackSrc") appFallbackSrc: string; /** Only try setting the fallback once. This prevents an infinite loop if the fallback itself is missing. */ diff --git a/libs/angular/src/directives/if-feature.directive.spec.ts b/libs/angular/src/directives/if-feature.directive.spec.ts index d7c49994045..357209b0e64 100644 --- a/libs/angular/src/directives/if-feature.directive.spec.ts +++ b/libs/angular/src/directives/if-feature.directive.spec.ts @@ -13,6 +13,8 @@ const testBooleanFeature: FeatureFlag = "boolean-feature" as FeatureFlag; const testStringFeature: FeatureFlag = "string-feature" as FeatureFlag; const testStringFeatureValue = "test-value"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ template: `
diff --git a/libs/angular/src/directives/if-feature.directive.ts b/libs/angular/src/directives/if-feature.directive.ts index aa10c9e8081..28cf1d5c35f 100644 --- a/libs/angular/src/directives/if-feature.directive.ts +++ b/libs/angular/src/directives/if-feature.directive.ts @@ -20,12 +20,16 @@ export class IfFeatureDirective implements OnInit { /** * The feature flag to check. */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() appIfFeature: FeatureFlag; /** * Optional value to compare against the value of the feature flag in the config service. * @default true */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() appIfFeatureValue: AllowedFeatureFlagTypes = true; private hasView = false; diff --git a/libs/angular/src/directives/input-verbatim.directive.ts b/libs/angular/src/directives/input-verbatim.directive.ts index 7bd18b12659..1240523d2bf 100644 --- a/libs/angular/src/directives/input-verbatim.directive.ts +++ b/libs/angular/src/directives/input-verbatim.directive.ts @@ -7,6 +7,8 @@ import { Directive, ElementRef, Input, OnInit, Renderer2 } from "@angular/core"; standalone: false, }) export class InputVerbatimDirective implements OnInit { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() set appInputVerbatim(condition: boolean | string) { this.disableComplete = condition === "" || condition === true; } diff --git a/libs/angular/src/directives/launch-click.directive.ts b/libs/angular/src/directives/launch-click.directive.ts index b270dbba5e3..ce44648dc37 100644 --- a/libs/angular/src/directives/launch-click.directive.ts +++ b/libs/angular/src/directives/launch-click.directive.ts @@ -10,6 +10,8 @@ import { Utils } from "@bitwarden/common/platform/misc/utils"; export class LaunchClickDirective { constructor(private platformUtilsService: PlatformUtilsService) {} + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input("appLaunchClick") uriToLaunch = ""; @HostListener("click") onClick() { diff --git a/libs/angular/src/directives/text-drag.directive.ts b/libs/angular/src/directives/text-drag.directive.ts index 6202c552a87..aade2798dc7 100644 --- a/libs/angular/src/directives/text-drag.directive.ts +++ b/libs/angular/src/directives/text-drag.directive.ts @@ -8,6 +8,8 @@ import { Directive, HostListener, Input } from "@angular/core"; }, }) export class TextDragDirective { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input({ alias: "appTextDrag", required: true, diff --git a/libs/angular/src/directives/true-false-value.directive.ts b/libs/angular/src/directives/true-false-value.directive.ts index 5d25ac2a385..78c1b4647c6 100644 --- a/libs/angular/src/directives/true-false-value.directive.ts +++ b/libs/angular/src/directives/true-false-value.directive.ts @@ -14,7 +14,11 @@ import { ControlValueAccessor, NG_VALUE_ACCESSOR } from "@angular/forms"; standalone: false, }) export class TrueFalseValueDirective implements ControlValueAccessor { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() trueValue: boolean | string = true; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() falseValue: boolean | string = false; constructor( diff --git a/libs/components/src/card/base-card/base-card.component.ts b/libs/components/src/card/base-card/base-card.component.ts index 44f82a32c47..8c4dd80f2d1 100644 --- a/libs/components/src/card/base-card/base-card.component.ts +++ b/libs/components/src/card/base-card/base-card.component.ts @@ -6,6 +6,8 @@ import { BaseCardDirective } from "./base-card.directive"; * The base card component is a container that applies our standard card border and box-shadow. * In most cases using our `` component should suffice. */ +// 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: "bit-base-card", template: ``, diff --git a/libs/components/src/card/card-content.component.ts b/libs/components/src/card/card-content.component.ts index 60be20e78f0..650a2665475 100644 --- a/libs/components/src/card/card-content.component.ts +++ b/libs/components/src/card/card-content.component.ts @@ -1,5 +1,7 @@ import { Component } from "@angular/core"; +// 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: "bit-card-content", template: `
`, From 75846e8fb1cd2932b20a7dec8bc2dd69c574f35e Mon Sep 17 00:00:00 2001 From: Brandon Treston Date: Wed, 29 Oct 2025 15:04:37 -0400 Subject: [PATCH 18/25] add decryption logic (#17106) --- .../organization-members.service.spec.ts | 52 +++++++++++++++++ .../organization-members.service.ts | 56 +++++++++++++++---- 2 files changed, 98 insertions(+), 10 deletions(-) diff --git a/apps/web/src/app/admin-console/organizations/members/services/organization-members-service/organization-members.service.spec.ts b/apps/web/src/app/admin-console/organizations/members/services/organization-members-service/organization-members.service.spec.ts index 615d2ece463..aef4dd00312 100644 --- a/apps/web/src/app/admin-console/organizations/members/services/organization-members-service/organization-members.service.spec.ts +++ b/apps/web/src/app/admin-console/organizations/members/services/organization-members-service/organization-members.service.spec.ts @@ -1,13 +1,17 @@ import { TestBed } from "@angular/core/testing"; +import { of } from "rxjs"; import { + CollectionService, OrganizationUserApiService, OrganizationUserUserDetailsResponse, } from "@bitwarden/admin-console/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { ListResponse } from "@bitwarden/common/models/response/list.response"; import { OrganizationId } from "@bitwarden/common/types/guid"; +import { KeyService } from "@bitwarden/key-management"; import { GroupApiService } from "../../../core"; @@ -18,6 +22,9 @@ describe("OrganizationMembersService", () => { let organizationUserApiService: jest.Mocked; let groupService: jest.Mocked; let apiService: jest.Mocked; + let keyService: jest.Mocked; + let accountService: jest.Mocked; + let collectionService: jest.Mocked; const mockOrganizationId = "org-123" as OrganizationId; @@ -51,6 +58,7 @@ describe("OrganizationMembersService", () => { const createMockCollection = (id: string, name: string) => ({ id, name, + organizationId: mockOrganizationId, }); beforeEach(() => { @@ -66,12 +74,27 @@ describe("OrganizationMembersService", () => { getCollections: jest.fn(), } as any; + keyService = { + orgKeys$: jest.fn(), + } as any; + + accountService = { + activeAccount$: of({ id: "user-123" } as any), + } as any; + + collectionService = { + decryptMany$: jest.fn(), + } as any; + TestBed.configureTestingModule({ providers: [ OrganizationMembersService, { provide: OrganizationUserApiService, useValue: organizationUserApiService }, { provide: GroupApiService, useValue: groupService }, { provide: ApiService, useValue: apiService }, + { provide: KeyService, useValue: keyService }, + { provide: AccountService, useValue: accountService }, + { provide: CollectionService, useValue: collectionService }, ], }); @@ -88,11 +111,15 @@ describe("OrganizationMembersService", () => { data: [mockUser], } as any; const mockCollections = [createMockCollection("col-1", "Collection 1")]; + const mockOrgKey = { [mockOrganizationId]: {} as any }; + const mockDecryptedCollections = [{ id: "col-1", name: "Collection 1" }]; organizationUserApiService.getAllUsers.mockResolvedValue(mockUsersResponse); apiService.getCollections.mockResolvedValue({ data: mockCollections, } as any); + keyService.orgKeys$.mockReturnValue(of(mockOrgKey)); + collectionService.decryptMany$.mockReturnValue(of(mockDecryptedCollections as any)); const result = await service.loadUsers(organization); @@ -171,11 +198,19 @@ describe("OrganizationMembersService", () => { createMockCollection("col-2", "Alpha Collection"), createMockCollection("col-3", "Beta Collection"), ]; + const mockOrgKey = { [mockOrganizationId]: {} as any }; + const mockDecryptedCollections = [ + { id: "col-1", name: "Zebra Collection" }, + { id: "col-2", name: "Alpha Collection" }, + { id: "col-3", name: "Beta Collection" }, + ]; organizationUserApiService.getAllUsers.mockResolvedValue(mockUsersResponse); apiService.getCollections.mockResolvedValue({ data: mockCollections, } as any); + keyService.orgKeys$.mockReturnValue(of(mockOrgKey)); + collectionService.decryptMany$.mockReturnValue(of(mockDecryptedCollections as any)); const result = await service.loadUsers(organization); @@ -223,11 +258,19 @@ describe("OrganizationMembersService", () => { // col-2 is missing - should be filtered out createMockCollection("col-3", "Collection 3"), ]; + const mockOrgKey = { [mockOrganizationId]: {} as any }; + const mockDecryptedCollections = [ + { id: "col-1", name: "Collection 1" }, + // col-2 is missing - should be filtered out + { id: "col-3", name: "Collection 3" }, + ]; organizationUserApiService.getAllUsers.mockResolvedValue(mockUsersResponse); apiService.getCollections.mockResolvedValue({ data: mockCollections, } as any); + keyService.orgKeys$.mockReturnValue(of(mockOrgKey)); + collectionService.decryptMany$.mockReturnValue(of(mockDecryptedCollections as any)); const result = await service.loadUsers(organization); @@ -269,11 +312,14 @@ describe("OrganizationMembersService", () => { const mockUsersResponse: ListResponse = { data: null as any, } as any; + const mockOrgKey = { [mockOrganizationId]: {} as any }; organizationUserApiService.getAllUsers.mockResolvedValue(mockUsersResponse); apiService.getCollections.mockResolvedValue({ data: [], } as any); + keyService.orgKeys$.mockReturnValue(of(mockOrgKey)); + collectionService.decryptMany$.mockReturnValue(of([])); const result = await service.loadUsers(organization); @@ -285,11 +331,14 @@ describe("OrganizationMembersService", () => { const mockUsersResponse: ListResponse = { data: undefined as any, } as any; + const mockOrgKey = { [mockOrganizationId]: {} as any }; organizationUserApiService.getAllUsers.mockResolvedValue(mockUsersResponse); apiService.getCollections.mockResolvedValue({ data: [], } as any); + keyService.orgKeys$.mockReturnValue(of(mockOrgKey)); + collectionService.decryptMany$.mockReturnValue(of([])); const result = await service.loadUsers(organization); @@ -322,11 +371,14 @@ describe("OrganizationMembersService", () => { const mockUsersResponse: ListResponse = { data: [mockUser], } as any; + const mockOrgKey = { [mockOrganizationId]: {} as any }; organizationUserApiService.getAllUsers.mockResolvedValue(mockUsersResponse); apiService.getCollections.mockResolvedValue({ data: [], } as any); + keyService.orgKeys$.mockReturnValue(of(mockOrgKey)); + collectionService.decryptMany$.mockReturnValue(of([])); const result = await service.loadUsers(organization); diff --git a/apps/web/src/app/admin-console/organizations/members/services/organization-members-service/organization-members.service.ts b/apps/web/src/app/admin-console/organizations/members/services/organization-members-service/organization-members.service.ts index 613c7c1b9c0..0dc417cc2c6 100644 --- a/apps/web/src/app/admin-console/organizations/members/services/organization-members-service/organization-members.service.ts +++ b/apps/web/src/app/admin-console/organizations/members/services/organization-members-service/organization-members.service.ts @@ -1,8 +1,18 @@ import { Injectable } from "@angular/core"; +import { combineLatest, firstValueFrom, from, map, switchMap } from "rxjs"; -import { OrganizationUserApiService } from "@bitwarden/admin-console/common"; +import { + Collection, + CollectionData, + CollectionDetailsResponse, + CollectionService, + OrganizationUserApiService, +} from "@bitwarden/admin-console/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; +import { KeyService } from "@bitwarden/key-management"; import { GroupApiService } from "../../../core"; import { OrganizationUserView } from "../../../core/views/organization-user.view"; @@ -13,6 +23,9 @@ export class OrganizationMembersService { private organizationUserApiService: OrganizationUserApiService, private groupService: GroupApiService, private apiService: ApiService, + private keyService: KeyService, + private accountService: AccountService, + private collectionService: CollectionService, ) {} async loadUsers(organization: Organization): Promise { @@ -62,15 +75,38 @@ export class OrganizationMembersService { } private async getCollectionNameMap(organization: Organization): Promise> { - const response = this.apiService - .getCollections(organization.id) - .then((res) => - res.data.map((r: { id: string; name: string }) => ({ id: r.id, name: r.name })), - ); + const collections$ = from(this.apiService.getCollections(organization.id)).pipe( + map((response) => { + return response.data.map((r) => + Collection.fromCollectionData(new CollectionData(r as CollectionDetailsResponse)), + ); + }), + ); - const collections = await response; - const collectionMap = new Map(); - collections.forEach((c: { id: string; name: string }) => collectionMap.set(c.id, c.name)); - return collectionMap; + const orgKey$ = this.accountService.activeAccount$.pipe( + getUserId, + switchMap((userId) => this.keyService.orgKeys$(userId)), + map((orgKeys) => { + if (orgKeys == null) { + throw new Error("Organization keys not found for provided User."); + } + return orgKeys; + }), + ); + + return await firstValueFrom( + combineLatest([orgKey$, collections$]).pipe( + switchMap(([orgKey, collections]) => + this.collectionService.decryptMany$(collections, orgKey), + ), + map((decryptedCollections) => { + const collectionMap: Map = new Map(); + decryptedCollections.forEach((c) => { + collectionMap.set(c.id, c.name); + }); + return collectionMap; + }), + ), + ); } } From 66052b6dd366c6b0de088f5afa2fa66c0263cfd0 Mon Sep 17 00:00:00 2001 From: Vijay Oommen Date: Wed, 29 Oct 2025 14:06:18 -0500 Subject: [PATCH 19/25] PM-26676 change the org should update the access intelligence report (#17053) --- .../services/domain/risk-insights-orchestrator.service.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) 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 f52ab68985b..2435fe12038 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 @@ -165,6 +165,7 @@ export class RiskInsightsOrchestratorService { initializeForOrganization(organizationId: OrganizationId) { this.logService.debug("[RiskInsightsOrchestratorService] Initializing for org", organizationId); this._initializeOrganizationTriggerSubject.next(organizationId); + this.fetchReport(); } removeCriticalApplication$(criticalApplication: string): Observable { @@ -587,7 +588,7 @@ export class RiskInsightsOrchestratorService { private _setupEnrichedReportData() { // Setup the enriched report data pipeline const enrichmentSubscription = combineLatest([ - this.rawReportData$.pipe(filter((data) => !!data && !!data?.data)), + this.rawReportData$, this._ciphers$.pipe(filter((data) => !!data)), ]).pipe( switchMap(([rawReportData, ciphers]) => { @@ -627,7 +628,7 @@ export class RiskInsightsOrchestratorService { .pipe( withLatestFrom(this._userId$), filter(([orgId, userId]) => !!orgId && !!userId), - exhaustMap(([orgId, userId]) => + switchMap(([orgId, userId]) => this.organizationService.organizations$(userId!).pipe( getOrganizationById(orgId), map((org) => ({ organizationId: orgId!, organizationName: org?.name ?? "" })), @@ -725,7 +726,7 @@ export class RiskInsightsOrchestratorService { scan((prevState: ReportState, currState: ReportState) => ({ ...prevState, ...currState, - data: currState.data !== null ? currState.data : prevState.data, + data: currState.data, })), startWith({ loading: false, error: null, data: null }), shareReplay({ bufferSize: 1, refCount: true }), From e333c0a8bcc6735bb8705d4ee5e334de11446e6a Mon Sep 17 00:00:00 2001 From: John Harrington <84741727+harr1424@users.noreply.github.com> Date: Wed, 29 Oct 2025 12:49:31 -0700 Subject: [PATCH 20/25] Preserve export type across export source selections (#16922) --- .../vault-export.service.abstraction.ts | 27 +++++++++++ .../src/services/vault-export.service.ts | 29 +++++++++++- .../src/components/export.component.html | 2 +- .../src/components/export.component.ts | 45 ++++++++++++------- 4 files changed, 85 insertions(+), 18 deletions(-) diff --git a/libs/tools/export/vault-export/vault-export-core/src/services/vault-export.service.abstraction.ts b/libs/tools/export/vault-export/vault-export-core/src/services/vault-export.service.abstraction.ts index e25fec6eb82..0d58f168671 100644 --- a/libs/tools/export/vault-export/vault-export-core/src/services/vault-export.service.abstraction.ts +++ b/libs/tools/export/vault-export/vault-export-core/src/services/vault-export.service.abstraction.ts @@ -1,3 +1,5 @@ +import { Observable } from "rxjs"; + import { UserId, OrganizationId } from "@bitwarden/common/types/guid"; import { ExportedVault } from "../types"; @@ -5,6 +7,24 @@ import { ExportedVault } from "../types"; export const EXPORT_FORMATS = ["csv", "json", "encrypted_json", "zip"] as const; export type ExportFormat = (typeof EXPORT_FORMATS)[number]; +/** + * Options that determine which export formats are available + */ +export type FormatOptions = { + /** Whether the export is for the user's personal vault */ + isMyVault: boolean; +}; + +/** + * Metadata describing an available export format + */ +export type ExportFormatMetadata = { + /** Display name for the format (e.g., ".json", ".csv") */ + name: string; + /** The export format identifier */ + format: ExportFormat; +}; + export abstract class VaultExportServiceAbstraction { abstract getExport: ( userId: UserId, @@ -18,4 +38,11 @@ export abstract class VaultExportServiceAbstraction { password: string, onlyManagedCollections?: boolean, ) => Promise; + + /** + * Get available export formats based on vault context + * @param options Options determining which formats are available + * @returns Observable stream of available export formats + */ + abstract formats$(options: FormatOptions): Observable; } diff --git a/libs/tools/export/vault-export/vault-export-core/src/services/vault-export.service.ts b/libs/tools/export/vault-export/vault-export-core/src/services/vault-export.service.ts index b601478d06d..38d71136006 100644 --- a/libs/tools/export/vault-export/vault-export-core/src/services/vault-export.service.ts +++ b/libs/tools/export/vault-export/vault-export-core/src/services/vault-export.service.ts @@ -1,4 +1,4 @@ -import { firstValueFrom } from "rxjs"; +import { firstValueFrom, Observable, of } from "rxjs"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; @@ -9,7 +9,12 @@ import { ExportedVault } from "../types"; import { IndividualVaultExportServiceAbstraction } from "./individual-vault-export.service.abstraction"; import { OrganizationVaultExportServiceAbstraction } from "./org-vault-export.service.abstraction"; -import { ExportFormat, VaultExportServiceAbstraction } from "./vault-export.service.abstraction"; +import { + ExportFormat, + ExportFormatMetadata, + FormatOptions, + VaultExportServiceAbstraction, +} from "./vault-export.service.abstraction"; export class VaultExportService implements VaultExportServiceAbstraction { constructor( @@ -85,6 +90,26 @@ export class VaultExportService implements VaultExportServiceAbstraction { ); } + /** + * Get available export formats based on vault context + * @param options Options determining which formats are available + * @returns Observable stream of available export formats + */ + formats$(options: FormatOptions): Observable { + const baseFormats: ExportFormatMetadata[] = [ + { name: ".json", format: "json" }, + { name: ".csv", format: "csv" }, + { name: ".json (Encrypted)", format: "encrypted_json" }, + ]; + + // ZIP format with attachments is only available for individual vault exports + if (options.isMyVault) { + return of([...baseFormats, { name: ".zip (with attachments)", format: "zip" }]); + } + + return of(baseFormats); + } + /** Checks if the provided userId matches the currently authenticated user * @param userId The userId to check * @throws Error if the userId does not match the currently authenticated user diff --git a/libs/tools/export/vault-export/vault-export-ui/src/components/export.component.html b/libs/tools/export/vault-export/vault-export-ui/src/components/export.component.html index c638e5d7dde..f41375edd5a 100644 --- a/libs/tools/export/vault-export/vault-export-ui/src/components/export.component.html +++ b/libs/tools/export/vault-export/vault-export-ui/src/components/export.component.html @@ -35,7 +35,7 @@ {{ "fileFormat" | i18n }} - + diff --git a/libs/tools/export/vault-export/vault-export-ui/src/components/export.component.ts b/libs/tools/export/vault-export/vault-export-ui/src/components/export.component.ts index 19921b35162..610f30c1f67 100644 --- a/libs/tools/export/vault-export/vault-export-ui/src/components/export.component.ts +++ b/libs/tools/export/vault-export/vault-export-ui/src/components/export.component.ts @@ -67,7 +67,11 @@ import { } from "@bitwarden/components"; import { GeneratorServicesModule } from "@bitwarden/generator-components"; import { CredentialGeneratorService, GenerateRequest, Type } from "@bitwarden/generator-core"; -import { ExportedVault, VaultExportServiceAbstraction } from "@bitwarden/vault-export-core"; +import { + ExportedVault, + ExportFormatMetadata, + VaultExportServiceAbstraction, +} from "@bitwarden/vault-export-core"; import { EncryptedExportType } from "../enums/encrypted-export-type.enum"; @@ -231,11 +235,11 @@ export class ExportComponent implements OnInit, OnDestroy, AfterViewInit { fileEncryptionType: [EncryptedExportType.AccountEncrypted], }); - formatOptions = [ - { name: ".json", value: "json" }, - { name: ".csv", value: "csv" }, - { name: ".json (Encrypted)", value: "encrypted_json" }, - ]; + /** + * Observable stream of available export format options + * Dynamically updates based on vault selection (My Vault vs Organization) + */ + formatOptions$: Observable; private destroy$ = new Subject(); private onlyManagedCollections = true; @@ -338,17 +342,28 @@ export class ExportComponent implements OnInit, OnDestroy, AfterViewInit { } private observeFormSelections(): void { - this.exportForm.controls.vaultSelector.valueChanges - .pipe(takeUntil(this.destroy$)) - .subscribe((value) => { - this.organizationId = value !== "myVault" ? value : undefined; + // Set up dynamic format options based on vault selection + this.formatOptions$ = this.exportForm.controls.vaultSelector.valueChanges.pipe( + startWith(this.exportForm.controls.vaultSelector.value), + map((vaultSelection) => { + const isMyVault = vaultSelection === "myVault"; + // Update organizationId based on vault selection + this.organizationId = isMyVault ? undefined : vaultSelection; + return { isMyVault }; + }), + switchMap((options) => this.exportService.formats$(options)), + tap((formats) => { + // Preserve the current format selection if it's still available in the new format list + const currentFormat = this.exportForm.get("format").value; + const isFormatAvailable = formats.some((f) => f.format === currentFormat); - this.formatOptions = this.formatOptions.filter((option) => option.value !== "zip"); - this.exportForm.get("format").setValue("json"); - if (value === "myVault") { - this.formatOptions.push({ name: ".zip (with attachments)", value: "zip" }); + // Only reset to json if the current format is no longer available + if (!isFormatAvailable) { + this.exportForm.get("format").setValue("json"); } - }); + }), + shareReplay({ bufferSize: 1, refCount: true }), + ); } /** From 94f778006fb7d15d7e24aeff5a7702a7b994165a Mon Sep 17 00:00:00 2001 From: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> Date: Wed, 29 Oct 2025 14:49:48 -0500 Subject: [PATCH 21/25] Fix lint (#17113) --- .../policies/session-timeout-confirmation-never.component.ts | 2 ++ .../app/key-management/policies/session-timeout.component.ts | 2 ++ 2 files changed, 4 insertions(+) diff --git a/bitwarden_license/bit-web/src/app/key-management/policies/session-timeout-confirmation-never.component.ts b/bitwarden_license/bit-web/src/app/key-management/policies/session-timeout-confirmation-never.component.ts index a909baf1c77..884cbd10cac 100644 --- a/bitwarden_license/bit-web/src/app/key-management/policies/session-timeout-confirmation-never.component.ts +++ b/bitwarden_license/bit-web/src/app/key-management/policies/session-timeout-confirmation-never.component.ts @@ -3,6 +3,8 @@ import { Component } from "@angular/core"; import { DialogRef, DialogService } from "@bitwarden/components"; import { SharedModule } from "@bitwarden/web-vault/app/shared"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ imports: [SharedModule], templateUrl: "./session-timeout-confirmation-never.component.html", diff --git a/bitwarden_license/bit-web/src/app/key-management/policies/session-timeout.component.ts b/bitwarden_license/bit-web/src/app/key-management/policies/session-timeout.component.ts index 3e40b9f0d80..9c6129f64df 100644 --- a/bitwarden_license/bit-web/src/app/key-management/policies/session-timeout.component.ts +++ b/bitwarden_license/bit-web/src/app/key-management/policies/session-timeout.component.ts @@ -40,6 +40,8 @@ export class SessionTimeoutPolicy extends BasePolicyEditDefinition { const DEFAULT_HOURS = 8; const DEFAULT_MINUTES = 0; +// 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: "session-timeout.component.html", imports: [SharedModule], From c05ea23ce4a3e6361fc47a08e14817fd62f0def2 Mon Sep 17 00:00:00 2001 From: Jordan Aasen <166539328+jaasen-livefront@users.noreply.github.com> Date: Wed, 29 Oct 2025 12:55:23 -0700 Subject: [PATCH 22/25] [PM-25083][26650][26651][26652] - Autofill confirmation dialog (#16835) * add autofill confirmation dialog * fix key * better handle bad uris * add specs * adjustments to autofill confirmation to include exact match dialog. fix gradient * update logic. add tests --- apps/browser/src/_locales/en/messages.json | 48 ++++ ...utofill-confirmation-dialog.component.html | 68 +++++ ...fill-confirmation-dialog.component.spec.ts | 192 ++++++++++++++ .../autofill-confirmation-dialog.component.ts | 100 ++++++++ .../item-more-options.component.html | 14 +- .../item-more-options.component.spec.ts | 241 ++++++++++++++++++ .../item-more-options.component.ts | 95 +++++-- .../services/vault-popup-items.service.ts | 7 + libs/common/src/enums/feature-flag.enum.ts | 2 + 9 files changed, 749 insertions(+), 18 deletions(-) create mode 100644 apps/browser/src/vault/popup/components/vault-v2/autofill-confirmation-dialog/autofill-confirmation-dialog.component.html create mode 100644 apps/browser/src/vault/popup/components/vault-v2/autofill-confirmation-dialog/autofill-confirmation-dialog.component.spec.ts create mode 100644 apps/browser/src/vault/popup/components/vault-v2/autofill-confirmation-dialog/autofill-confirmation-dialog.component.ts create mode 100644 apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.spec.ts diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 29601bfa70c..4f230dd9883 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -588,6 +588,9 @@ "view": { "message": "View" }, + "viewAll": { + "message": "View all" + }, "viewLogin": { "message": "View login" }, @@ -1028,6 +1031,18 @@ "editedItem": { "message": "Item saved" }, + "savedWebsite": { + "message": "Saved website" + }, + "savedWebsites": { + "message": "Saved websites ( $COUNT$ )", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "deleteItemConfirmation": { "message": "Do you really want to send to the trash?" }, @@ -1676,9 +1691,30 @@ "turnOffAutofill": { "message": "Turn off autofill" }, + "confirmAutofill": { + "message": "Confirm autofill" + }, + "confirmAutofillDesc": { + "message": "This site doesn't match your saved login details. Before you fill in your login credentials, make sure it's a trusted site." + }, "showInlineMenuLabel": { "message": "Show autofill suggestions on form fields" }, + "howDoesBitwardenProtectFromPhishing": { + "message": "How does Bitwarden protect your data from phishing?" + }, + "currentWebsite": { + "message": "Current website" + }, + "autofillAndAddWebsite": { + "message": "Autofill and add this website" + }, + "autofillWithoutAdding": { + "message": "Autofill without adding" + }, + "doNotAutofill": { + "message": "Do not autofill" + }, "showInlineMenuIdentitiesLabel": { "message": "Display identities as suggestions" }, @@ -3240,6 +3276,9 @@ "decryptionError": { "message": "Decryption error" }, + "errorGettingAutoFillData": { + "message": "Error getting autofill data" + }, "couldNotDecryptVaultItemsBelow": { "message": "Bitwarden could not decrypt the vault item(s) listed below." }, @@ -4011,6 +4050,15 @@ "message": "Autofill on page load set to use default setting.", "description": "Toast message for informing the user that autofill on page load has been set to the default setting." }, + "cannotAutofill": { + "message": "Cannot autofill" + }, + "cannotAutofillExactMatch": { + "message": "Default matching is set to 'Exact Match'. The current website does not exactly match the saved login details for this item." + }, + "okay": { + "message": "Okay" + }, "toggleSideNavigation": { "message": "Toggle side navigation" }, 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-v2/autofill-confirmation-dialog/autofill-confirmation-dialog.component.html new file mode 100644 index 00000000000..77801edc8fe --- /dev/null +++ b/apps/browser/src/vault/popup/components/vault-v2/autofill-confirmation-dialog/autofill-confirmation-dialog.component.html @@ -0,0 +1,68 @@ + + {{ "confirmAutofill" | i18n }} +
+

+ {{ "confirmAutofillDesc" | i18n }} +

+ @if (savedUrls.length === 1) { +

+ {{ "savedWebsite" | i18n }} +

+ +
+ {{ savedUrls[0] }} +
+
+ } + @if (savedUrls.length > 1) { +
+

+ {{ "savedWebsites" | i18n: savedUrls.length }} +

+ +
+
+
+ +
+ {{ url }} +
+
+
+
+ } +

+ {{ "currentWebsite" | i18n }} +

+ +
+ {{ currentUrl }} +
+
+
+ + + +
+
+
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-v2/autofill-confirmation-dialog/autofill-confirmation-dialog.component.spec.ts new file mode 100644 index 00000000000..1fe3dfaf25a --- /dev/null +++ b/apps/browser/src/vault/popup/components/vault-v2/autofill-confirmation-dialog/autofill-confirmation-dialog.component.spec.ts @@ -0,0 +1,192 @@ +import { CUSTOM_ELEMENTS_SCHEMA } from "@angular/core"; +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { provideNoopAnimations } from "@angular/platform-browser/animations"; + +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { DIALOG_DATA, DialogRef, DialogService } from "@bitwarden/components"; + +import { + AutofillConfirmationDialogComponent, + AutofillConfirmationDialogResult, + AutofillConfirmationDialogParams, +} from "./autofill-confirmation-dialog.component"; + +describe("AutofillConfirmationDialogComponent", () => { + let fixture: ComponentFixture; + let component: AutofillConfirmationDialogComponent; + + const dialogRef = { + close: jest.fn(), + } as unknown as DialogRef; + + const params: AutofillConfirmationDialogParams = { + currentUrl: "https://example.com/path?q=1", + savedUrls: ["https://one.example.com/a", "https://two.example.com/b", "not-a-url.example"], + }; + + beforeEach(async () => { + jest.spyOn(Utils, "getHostname").mockImplementation((value: string | null | undefined) => { + if (typeof value !== "string" || !value) { + return ""; + } + try { + // handle non-URL host strings gracefully + if (!value.includes("://")) { + return value; + } + return new URL(value).hostname; + } catch { + return ""; + } + }); + + await TestBed.configureTestingModule({ + imports: [AutofillConfirmationDialogComponent], + providers: [ + provideNoopAnimations(), + { provide: DIALOG_DATA, useValue: params }, + { provide: DialogRef, useValue: dialogRef }, + { provide: I18nService, useValue: { t: (key: string) => key } }, + { provide: DialogService, useValue: {} }, + ], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(AutofillConfirmationDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + it("normalizes currentUrl and savedUrls via Utils.getHostname", () => { + expect(Utils.getHostname).toHaveBeenCalledTimes(1 + (params.savedUrls?.length ?? 0)); + // current + expect(component.currentUrl).toBe("example.com"); + // saved + expect(component.savedUrls).toEqual([ + "one.example.com", + "two.example.com", + "not-a-url.example", + ]); + }); + + it("renders normalized values into the template (shallow check)", () => { + const text = fixture.nativeElement.textContent as string; + expect(text).toContain("example.com"); + expect(text).toContain("one.example.com"); + expect(text).toContain("two.example.com"); + expect(text).toContain("not-a-url.example"); + }); + + it("emits Canceled on close()", () => { + const spy = jest.spyOn(dialogRef, "close"); + component["close"](); + expect(spy).toHaveBeenCalledWith(AutofillConfirmationDialogResult.Canceled); + }); + + it("emits AutofillAndUrlAdded on autofillAndAddUrl()", () => { + const spy = jest.spyOn(dialogRef, "close"); + component["autofillAndAddUrl"](); + expect(spy).toHaveBeenCalledWith(AutofillConfirmationDialogResult.AutofillAndUrlAdded); + }); + + it("emits AutofilledOnly on autofillOnly()", () => { + const spy = jest.spyOn(dialogRef, "close"); + component["autofillOnly"](); + expect(spy).toHaveBeenCalledWith(AutofillConfirmationDialogResult.AutofilledOnly); + }); + + it("applies collapsed list gradient class by default, then clears it after viewAllSavedUrls()", () => { + const initial = component["savedUrlsListClass"]; + expect(initial).toContain("gradient"); + + component["viewAllSavedUrls"](); + fixture.detectChanges(); + + const expanded = component["savedUrlsListClass"]; + expect(expanded).toBe(""); + }); + + it("handles empty savedUrls gracefully", async () => { + const newParams: AutofillConfirmationDialogParams = { + currentUrl: "https://bitwarden.com/help", + savedUrls: [], + }; + + const newFixture = TestBed.createComponent(AutofillConfirmationDialogComponent); + const newInstance = newFixture.componentInstance; + + (newInstance as any).params = newParams; + const fresh = new AutofillConfirmationDialogComponent( + newParams as any, + dialogRef, + ) as AutofillConfirmationDialogComponent; + + expect(fresh.savedUrls).toEqual([]); + expect(fresh.currentUrl).toBe("bitwarden.com"); + }); + + it("handles undefined savedUrls by defaulting to [] and empty strings from Utils.getHostname", () => { + const localParams: AutofillConfirmationDialogParams = { + currentUrl: "https://sub.domain.tld/x", + }; + + const local = new AutofillConfirmationDialogComponent(localParams as any, dialogRef); + + expect(local.savedUrls).toEqual([]); + expect(local.currentUrl).toBe("sub.domain.tld"); + }); + + it("filters out falsy/invalid values from Utils.getHostname in savedUrls", () => { + (Utils.getHostname as jest.Mock).mockImplementationOnce(() => "example.com"); + (Utils.getHostname as jest.Mock) + .mockImplementationOnce(() => "ok.example") + .mockImplementationOnce(() => "") + .mockImplementationOnce(() => undefined as unknown as string); + + const edgeParams: AutofillConfirmationDialogParams = { + currentUrl: "https://example.com", + savedUrls: ["https://ok.example", "://bad", "%%%"], + }; + + const edge = new AutofillConfirmationDialogComponent(edgeParams as any, dialogRef); + + expect(edge.currentUrl).toBe("example.com"); + expect(edge.savedUrls).toEqual(["ok.example"]); + }); + + it("renders one current-url callout and N saved-url callouts", () => { + const callouts = Array.from( + fixture.nativeElement.querySelectorAll("bit-callout"), + ) as HTMLElement[]; + expect(callouts.length).toBe(1 + params.savedUrls!.length); + }); + + it("renders normalized hostnames into the DOM text", () => { + const text = (fixture.nativeElement.textContent as string).replace(/\s+/g, " "); + expect(text).toContain("example.com"); + expect(text).toContain("one.example.com"); + expect(text).toContain("two.example.com"); + }); + + it("shows the 'view all' button when savedUrls > 1 and hides it after click", () => { + const findViewAll = () => + fixture.nativeElement.querySelector( + "button.tw-text-sm.tw-font-bold.tw-cursor-pointer", + ) as HTMLButtonElement | null; + + let btn = findViewAll(); + expect(btn).toBeTruthy(); + + btn!.click(); + fixture.detectChanges(); + + btn = findViewAll(); + expect(btn).toBeFalsy(); + expect(component.savedUrlsExpanded).toBe(true); + }); +}); 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-v2/autofill-confirmation-dialog/autofill-confirmation-dialog.component.ts new file mode 100644 index 00000000000..cc2fc546ae6 --- /dev/null +++ b/apps/browser/src/vault/popup/components/vault-v2/autofill-confirmation-dialog/autofill-confirmation-dialog.component.ts @@ -0,0 +1,100 @@ +import { CommonModule } from "@angular/common"; +import { Component, Inject } from "@angular/core"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { UnionOfValues } from "@bitwarden/common/vault/types/union-of-values"; +import { + DIALOG_DATA, + DialogConfig, + DialogRef, + ButtonModule, + DialogService, + DialogModule, + TypographyModule, + CalloutComponent, + LinkModule, +} from "@bitwarden/components"; + +export interface AutofillConfirmationDialogParams { + savedUrls?: string[]; + currentUrl: string; +} + +export const AutofillConfirmationDialogResult = Object.freeze({ + AutofillAndUrlAdded: "added", + AutofilledOnly: "autofilled", + Canceled: "canceled", +} as const); + +export type AutofillConfirmationDialogResultType = UnionOfValues< + typeof AutofillConfirmationDialogResult +>; + +@Component({ + templateUrl: "./autofill-confirmation-dialog.component.html", + imports: [ + ButtonModule, + CalloutComponent, + CommonModule, + DialogModule, + LinkModule, + TypographyModule, + JslibModule, + ], +}) +export class AutofillConfirmationDialogComponent { + AutofillConfirmationDialogResult = AutofillConfirmationDialogResult; + + currentUrl: string = ""; + savedUrls: string[] = []; + savedUrlsExpanded = false; + + constructor( + @Inject(DIALOG_DATA) protected params: AutofillConfirmationDialogParams, + private dialogRef: DialogRef, + ) { + this.currentUrl = Utils.getHostname(params.currentUrl); + this.savedUrls = + params.savedUrls?.map((url) => Utils.getHostname(url) ?? "").filter(Boolean) ?? []; + } + + protected get savedUrlsListClass(): string { + return this.savedUrlsExpanded + ? "" + : `tw-relative + tw-max-h-24 + tw-overflow-hidden + after:tw-pointer-events-none after:tw-content-[''] + after:tw-absolute after:tw-inset-x-0 after:tw-bottom-0 + after:tw-h-8 after:tw-bg-gradient-to-t + after:tw-from-background after:tw-to-transparent + `; + } + + protected viewAllSavedUrls() { + this.savedUrlsExpanded = true; + } + + protected close() { + this.dialogRef.close(AutofillConfirmationDialogResult.Canceled); + } + + protected autofillAndAddUrl() { + this.dialogRef.close(AutofillConfirmationDialogResult.AutofillAndUrlAdded); + } + + protected autofillOnly() { + this.dialogRef.close(AutofillConfirmationDialogResult.AutofilledOnly); + } + + static open( + dialogService: DialogService, + config: DialogConfig, + ) { + return dialogService.open( + AutofillConfirmationDialogComponent, + { ...config }, + ); + } +} 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-v2/item-more-options/item-more-options.component.html index 3a48f7eb449..b05d19498ac 100644 --- 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-v2/item-more-options/item-more-options.component.html @@ -13,9 +13,17 @@ - + + @if (!(showAutofillConfirmation$ | async)) { + + } 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-v2/item-more-options/item-more-options.component.spec.ts new file mode 100644 index 00000000000..15a9ba8f8e3 --- /dev/null +++ b/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.spec.ts @@ -0,0 +1,241 @@ +import { CUSTOM_ELEMENTS_SCHEMA } from "@angular/core"; +import { ComponentFixture, TestBed, waitForAsync } from "@angular/core/testing"; +import { NoopAnimationsModule } from "@angular/platform-browser/animations"; +import { Router } from "@angular/router"; +import { mock } from "jest-mock-extended"; +import { BehaviorSubject, of } from "rxjs"; + +import { CollectionService } from "@bitwarden/admin-console/common"; +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; +import { + UriMatchStrategy, + UriMatchStrategySetting, +} from "@bitwarden/common/models/domain/domain-service"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { CipherType } from "@bitwarden/common/vault/enums"; +import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service"; +import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service"; +import { DialogService, ToastService } from "@bitwarden/components"; +import { PasswordRepromptService } from "@bitwarden/vault"; + +import { VaultPopupAutofillService } from "../../../services/vault-popup-autofill.service"; +import { VaultPopupItemsService } from "../../../services/vault-popup-items.service"; +import { + AutofillConfirmationDialogComponent, + AutofillConfirmationDialogResult, +} from "../autofill-confirmation-dialog/autofill-confirmation-dialog.component"; + +import { ItemMoreOptionsComponent } from "./item-more-options.component"; + +describe("ItemMoreOptionsComponent", () => { + let fixture: ComponentFixture; + let component: ItemMoreOptionsComponent; + + const dialogService = { + openSimpleDialog: jest.fn().mockResolvedValue(true), + open: jest.fn(), + }; + const featureFlag$ = new BehaviorSubject(false); + const configService = { + getFeatureFlag$: jest.fn().mockImplementation(() => featureFlag$.asObservable()), + }; + const cipherService = { + getFullCipherView: jest.fn(), + encrypt: jest.fn(), + updateWithServer: jest.fn(), + softDeleteWithServer: jest.fn(), + }; + const autofillSvc = { + doAutofill: jest.fn(), + doAutofillAndSave: jest.fn(), + currentAutofillTab$: new BehaviorSubject<{ url?: string | null } | null>(null), + autofillAllowed$: new BehaviorSubject(true), + }; + + const uriMatchStrategy$ = new BehaviorSubject(UriMatchStrategy.Domain); + + const domainSettingsService = { + resolvedDefaultUriMatchStrategy$: uriMatchStrategy$.asObservable(), + }; + + const hasSearchText$ = new BehaviorSubject(false); + const vaultPopupItemsService = { + hasSearchText$: hasSearchText$.asObservable(), + }; + + const baseCipher = { + id: "cipher-1", + login: { + uris: [ + { uri: "https://one.example.com" }, + { uri: "" }, + { uri: undefined as unknown as string }, + { uri: "https://two.example.com/a" }, + ], + username: "user", + }, + favorite: false, + reprompt: 0, + type: CipherType.Login, + viewPassword: true, + edit: true, + } as any; + + beforeEach(waitForAsync(async () => { + jest.clearAllMocks(); + + cipherService.getFullCipherView.mockImplementation(async (c) => ({ ...baseCipher, ...c })); + + TestBed.configureTestingModule({ + imports: [ItemMoreOptionsComponent, NoopAnimationsModule], + providers: [ + { provide: ConfigService, useValue: configService }, + { provide: CipherService, useValue: cipherService }, + { provide: VaultPopupAutofillService, useValue: autofillSvc }, + + { provide: I18nService, useValue: { t: (k: string) => k } }, + { provide: AccountService, useValue: { activeAccount$: of({ id: "UserId" }) } }, + { provide: OrganizationService, useValue: { hasOrganizations: () => of(false) } }, + { + provide: CipherAuthorizationService, + useValue: { canDeleteCipher$: () => of(true), canCloneCipher$: () => of(true) }, + }, + { provide: CollectionService, useValue: { decryptedCollections$: () => of([]) } }, + { provide: RestrictedItemTypesService, useValue: { restricted$: of([]) } }, + { provide: CipherArchiveService, useValue: { userCanArchive$: () => of(true) } }, + { provide: ToastService, useValue: { showToast: () => {} } }, + { provide: Router, useValue: { navigate: () => Promise.resolve(true) } }, + { provide: PasswordRepromptService, useValue: mock() }, + { + provide: DomainSettingsService, + useValue: domainSettingsService, + }, + { + provide: VaultPopupItemsService, + useValue: vaultPopupItemsService, + }, + ], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + }); + TestBed.overrideProvider(DialogService, { useValue: dialogService }); + await TestBed.compileComponents(); + fixture = TestBed.createComponent(ItemMoreOptionsComponent); + component = fixture.componentInstance; + component.cipher = baseCipher; + })); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + function mockConfirmDialogResult(result: string) { + const openSpy = jest + .spyOn(AutofillConfirmationDialogComponent, "open") + .mockReturnValue({ closed: of(result) } as any); + return openSpy; + } + + it("calls doAutofill without showing the confirmation dialog when the feature flag is disabled or search text is not present", async () => { + autofillSvc.currentAutofillTab$.next({ url: "https://page.example.com" }); + + await component.doAutofill(); + + expect(cipherService.getFullCipherView).toHaveBeenCalled(); + expect(autofillSvc.doAutofill).toHaveBeenCalledTimes(1); + expect(autofillSvc.doAutofill).toHaveBeenCalledWith( + expect.objectContaining({ id: "cipher-1" }), + false, + ); + expect(autofillSvc.doAutofillAndSave).not.toHaveBeenCalled(); + expect(dialogService.openSimpleDialog).not.toHaveBeenCalled(); + }); + + it("opens the confirmation dialog with filtered saved URLs when the feature flag is enabled and search text is present", async () => { + featureFlag$.next(true); + hasSearchText$.next(true); + autofillSvc.currentAutofillTab$.next({ url: "https://page.example.com/path" }); + const openSpy = mockConfirmDialogResult(AutofillConfirmationDialogResult.Canceled); + + await component.doAutofill(); + + expect(openSpy).toHaveBeenCalledTimes(1); + const args = openSpy.mock.calls[0][1]; + expect(args.data.currentUrl).toBe("https://page.example.com/path"); + expect(args.data.savedUrls).toEqual(["https://one.example.com", "https://two.example.com/a"]); + }); + + it("does nothing when the user cancels the autofill confirmation dialog", async () => { + featureFlag$.next(true); + autofillSvc.currentAutofillTab$.next({ url: "https://page.example.com" }); + mockConfirmDialogResult(AutofillConfirmationDialogResult.Canceled); + + await component.doAutofill(); + + expect(autofillSvc.doAutofill).not.toHaveBeenCalled(); + expect(autofillSvc.doAutofillAndSave).not.toHaveBeenCalled(); + }); + + it("autofills the item without adding the URL when the user selects 'AutofilledOnly'", async () => { + featureFlag$.next(true); + autofillSvc.currentAutofillTab$.next({ url: "https://page.example.com" }); + mockConfirmDialogResult(AutofillConfirmationDialogResult.AutofilledOnly); + + await component.doAutofill(); + + expect(autofillSvc.doAutofill).toHaveBeenCalledTimes(1); + expect(autofillSvc.doAutofillAndSave).not.toHaveBeenCalled(); + }); + + it("autofills the item and adds the URL when the user selects 'AutofillAndUrlAdded'", async () => { + featureFlag$.next(true); + autofillSvc.currentAutofillTab$.next({ url: "https://page.example.com" }); + mockConfirmDialogResult(AutofillConfirmationDialogResult.AutofillAndUrlAdded); + + await component.doAutofill(); + + expect(autofillSvc.doAutofillAndSave).toHaveBeenCalledTimes(1); + expect(autofillSvc.doAutofillAndSave.mock.calls[0][1]).toBe(false); + expect(autofillSvc.doAutofill).not.toHaveBeenCalled(); + }); + + it("only shows the exact match dialog when the uri match strategy is Exact and no URIs match", async () => { + featureFlag$.next(true); + uriMatchStrategy$.next(UriMatchStrategy.Exact); + hasSearchText$.next(true); + autofillSvc.currentAutofillTab$.next({ url: "https://no-match.example.com" }); + + await component.doAutofill(); + + expect(dialogService.openSimpleDialog).toHaveBeenCalledTimes(1); + expect(dialogService.openSimpleDialog).toHaveBeenCalledWith( + expect.objectContaining({ + title: expect.objectContaining({ key: "cannotAutofill" }), + content: expect.objectContaining({ key: "cannotAutofillExactMatch" }), + type: "info", + }), + ); + expect(autofillSvc.doAutofill).not.toHaveBeenCalled(); + expect(autofillSvc.doAutofillAndSave).not.toHaveBeenCalled(); + }); + + it("hides the 'Fill and Save' button when showAutofillConfirmation$ is true", async () => { + // Enable both feature flag and search text → makes showAutofillConfirmation$ true + featureFlag$.next(true); + hasSearchText$.next(true); + + fixture.detectChanges(); + await fixture.whenStable(); + + const fillAndSaveButton = fixture.nativeElement.querySelector( + "button[bitMenuItem]:not([disabled])", + ); + + const buttonText = fillAndSaveButton?.textContent?.trim().toLowerCase() ?? ""; + expect(buttonText.includes("fillAndSave".toLowerCase())).toBe(false); + }); +}); 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-v2/item-more-options/item-more-options.component.ts index 94016d2670f..40b6476053b 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-v2/item-more-options/item-more-options.component.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { CommonModule } from "@angular/common"; import { booleanAttribute, Component, Input } from "@angular/core"; import { Router, RouterModule } from "@angular/router"; @@ -11,8 +9,12 @@ import { JslibModule } from "@bitwarden/angular/jslib.module"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; +import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { UriMatchStrategy } from "@bitwarden/common/models/domain/domain-service"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { CipherId } from "@bitwarden/common/types/guid"; +import { CipherId, UserId } from "@bitwarden/common/types/guid"; import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherRepromptType, CipherType } from "@bitwarden/common/vault/enums"; @@ -32,7 +34,12 @@ import { import { PasswordRepromptService } from "@bitwarden/vault"; import { VaultPopupAutofillService } from "../../../services/vault-popup-autofill.service"; +import { VaultPopupItemsService } from "../../../services/vault-popup-items.service"; import { AddEditQueryParams } from "../add-edit/add-edit-v2.component"; +import { + AutofillConfirmationDialogComponent, + AutofillConfirmationDialogResult, +} from "../autofill-confirmation-dialog/autofill-confirmation-dialog.component"; // FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush // eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @@ -42,7 +49,7 @@ import { AddEditQueryParams } from "../add-edit/add-edit-v2.component"; imports: [ItemModule, IconButtonModule, MenuModule, CommonModule, JslibModule, RouterModule], }) export class ItemMoreOptionsComponent { - private _cipher$ = new BehaviorSubject(undefined); + private _cipher$ = new BehaviorSubject({} as CipherViewLike); // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals // eslint-disable-next-line @angular-eslint/prefer-signals @@ -64,7 +71,7 @@ export class ItemMoreOptionsComponent { // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals // eslint-disable-next-line @angular-eslint/prefer-signals @Input({ transform: booleanAttribute }) - showViewOption: boolean; + showViewOption = false; /** * Flag to hide the autofill menu options. Used for items that are @@ -73,10 +80,17 @@ export class ItemMoreOptionsComponent { // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals // eslint-disable-next-line @angular-eslint/prefer-signals @Input({ transform: booleanAttribute }) - hideAutofillOptions: boolean; + hideAutofillOptions = false; protected autofillAllowed$ = this.vaultPopupAutofillService.autofillAllowed$; + protected showAutofillConfirmation$ = combineLatest([ + this.configService.getFeatureFlag$(FeatureFlag.AutofillConfirmation), + this.vaultPopupItemsService.hasSearchText$, + ]).pipe(map(([isFeatureFlagEnabled, hasSearchText]) => isFeatureFlagEnabled && hasSearchText)); + + protected uriMatchStrategy$ = this.domainSettingsService.resolvedDefaultUriMatchStrategy$; + /** * Observable that emits a boolean value indicating if the user is authorized to clone the cipher. * @protected @@ -146,6 +160,9 @@ export class ItemMoreOptionsComponent { private collectionService: CollectionService, private restrictedItemTypesService: RestrictedItemTypesService, private cipherArchiveService: CipherArchiveService, + private configService: ConfigService, + private vaultPopupItemsService: VaultPopupItemsService, + private domainSettingsService: DomainSettingsService, ) {} get canEdit() { @@ -177,14 +194,63 @@ export class ItemMoreOptionsComponent { return this.cipher.favorite ? "unfavorite" : "favorite"; } - async doAutofill() { - const cipher = await this.cipherService.getFullCipherView(this.cipher); - await this.vaultPopupAutofillService.doAutofill(cipher); - } - async doAutofillAndSave() { const cipher = await this.cipherService.getFullCipherView(this.cipher); - await this.vaultPopupAutofillService.doAutofillAndSave(cipher, false); + await this.vaultPopupAutofillService.doAutofillAndSave(cipher); + } + + async doAutofill() { + const cipher = await this.cipherService.getFullCipherView(this.cipher); + + const showAutofillConfirmation = await firstValueFrom(this.showAutofillConfirmation$); + + if (!showAutofillConfirmation) { + await this.vaultPopupAutofillService.doAutofill(cipher, false); + return; + } + + const uriMatchStrategy = await firstValueFrom(this.uriMatchStrategy$); + if (uriMatchStrategy === UriMatchStrategy.Exact) { + await this.dialogService.openSimpleDialog({ + title: { key: "cannotAutofill" }, + content: { key: "cannotAutofillExactMatch" }, + type: "info", + acceptButtonText: { key: "okay" }, + cancelButtonText: null, + }); + return; + } + + const currentTab = await firstValueFrom(this.vaultPopupAutofillService.currentAutofillTab$); + + if (!currentTab?.url) { + await this.dialogService.openSimpleDialog({ + title: { key: "error" }, + content: { key: "errorGettingAutoFillData" }, + type: "danger", + }); + return; + } + + const ref = AutofillConfirmationDialogComponent.open(this.dialogService, { + data: { + currentUrl: currentTab?.url || "", + savedUrls: cipher.login?.uris?.filter((u) => u.uri).map((u) => u.uri!) ?? [], + }, + }); + + const result = await firstValueFrom(ref.closed); + + switch (result) { + case AutofillConfirmationDialogResult.Canceled: + return; + case AutofillConfirmationDialogResult.AutofilledOnly: + await this.vaultPopupAutofillService.doAutofill(cipher); + return; + case AutofillConfirmationDialogResult.AutofillAndUrlAdded: + await this.vaultPopupAutofillService.doAutofillAndSave(cipher, false); + return; + } } async onView() { @@ -204,15 +270,14 @@ export class ItemMoreOptionsComponent { const cipher = await this.cipherService.getFullCipherView(this.cipher); cipher.favorite = !cipher.favorite; - const activeUserId = await firstValueFrom( + const activeUserId = (await firstValueFrom( this.accountService.activeAccount$.pipe(map((a) => a?.id)), - ); + )) as UserId; const encryptedCipher = await this.cipherService.encrypt(cipher, activeUserId); await this.cipherService.updateWithServer(encryptedCipher); this.toastService.showToast({ variant: "success", - title: null, message: this.i18nService.t( this.cipher.favorite ? "itemAddedToFavorites" : "itemRemovedFromFavorites", ), diff --git a/apps/browser/src/vault/popup/services/vault-popup-items.service.ts b/apps/browser/src/vault/popup/services/vault-popup-items.service.ts index a1820a975f1..afe9d61d5af 100644 --- a/apps/browser/src/vault/popup/services/vault-popup-items.service.ts +++ b/apps/browser/src/vault/popup/services/vault-popup-items.service.ts @@ -261,6 +261,13 @@ export class VaultPopupItemsService { this.remainingCiphers$.pipe(map(() => false)), ).pipe(startWith(true), distinctUntilChanged(), shareReplay({ refCount: false, bufferSize: 1 })); + /** Observable that indicates whether there is search text present. + */ + hasSearchText$: Observable = this._hasSearchText.pipe( + distinctUntilChanged(), + shareReplay({ bufferSize: 1, refCount: true }), + ); + /** * Observable that indicates whether a filter or search text is currently applied to the ciphers. */ diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index 085731b034e..bfb40aff106 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -55,6 +55,7 @@ export enum FeatureFlag { PM22134SdkCipherListView = "pm-22134-sdk-cipher-list-view", PM22136_SdkCipherEncryption = "pm-22136-sdk-cipher-encryption", CipherKeyEncryption = "cipher-key-encryption", + AutofillConfirmation = "pm-25083-autofill-confirm-from-search", /* Platform */ IpcChannelFramework = "ipc-channel-framework", @@ -102,6 +103,7 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.PM19941MigrateCipherDomainToSdk]: FALSE, [FeatureFlag.PM22134SdkCipherListView]: FALSE, [FeatureFlag.PM22136_SdkCipherEncryption]: FALSE, + [FeatureFlag.AutofillConfirmation]: FALSE, /* Auth */ [FeatureFlag.PM22110_DisableAlternateLoginMethods]: FALSE, From b8921cb079e3cdc69819033eab9a6b2be8965df4 Mon Sep 17 00:00:00 2001 From: Jordan Aasen <166539328+jaasen-livefront@users.noreply.github.com> Date: Wed, 29 Oct 2025 13:28:36 -0700 Subject: [PATCH 23/25] fix lint error (#17115) --- .../autofill-confirmation-dialog.component.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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-v2/autofill-confirmation-dialog/autofill-confirmation-dialog.component.ts index cc2fc546ae6..71c07ad8bfc 100644 --- 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-v2/autofill-confirmation-dialog/autofill-confirmation-dialog.component.ts @@ -1,5 +1,5 @@ import { CommonModule } from "@angular/common"; -import { Component, Inject } from "@angular/core"; +import { ChangeDetectionStrategy, Component, Inject } from "@angular/core"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { Utils } from "@bitwarden/common/platform/misc/utils"; @@ -33,6 +33,7 @@ export type AutofillConfirmationDialogResultType = UnionOfValues< @Component({ templateUrl: "./autofill-confirmation-dialog.component.html", + changeDetection: ChangeDetectionStrategy.OnPush, imports: [ ButtonModule, CalloutComponent, From 6b3c4f87c70543b3f2599f72acf8a10053006a15 Mon Sep 17 00:00:00 2001 From: Vicki League Date: Wed, 29 Oct 2025 16:52:28 -0400 Subject: [PATCH 24/25] [CL-807] Improve aria a11y of nav group (#17078) --- .../src/navigation/nav-group.component.html | 6 ++-- .../src/navigation/nav-group.component.ts | 32 +++++++++++++++++-- .../src/navigation/nav-item.component.html | 11 ++++--- .../src/navigation/nav-item.component.ts | 10 +++++- 4 files changed, 47 insertions(+), 12 deletions(-) diff --git a/libs/components/src/navigation/nav-group.component.html b/libs/components/src/navigation/nav-group.component.html index 195569292f6..bcf6ae2b5b7 100644 --- a/libs/components/src/navigation/nav-group.component.html +++ b/libs/components/src/navigation/nav-group.component.html @@ -8,7 +8,8 @@ [routerLinkActiveOptions]="routerLinkActiveOptions()" (mainContentClicked)="handleMainContentClicked()" [ariaLabel]="ariaLabel()" - [hideActiveStyles]="parentHideActiveStyles" + [hideActiveStyles]="parentHideActiveStyles()" + [ariaCurrentWhenActive]="ariaCurrent()" >