From 540da69daf2d431719113e96078f03ea85caa0a4 Mon Sep 17 00:00:00 2001 From: Bryan Cunningham Date: Tue, 25 Nov 2025 11:04:37 -0500 Subject: [PATCH] [CL-761] Enable strict template typechecking (#17334) * enable strict template typechecking * add callout component to module * fixing popup action types * fixing cipher item copy types * fix archive cipher type * fixing trash list items types * fix remaining trash list item type errors * use CipherViewLike as correct type * change popup back directive to attribute selector * allow undefined in popupBackAction handler * Remove undefined from type * fix error with firefox commercial build --------- Co-authored-by: Vicki League --- apps/browser/src/popup/app.module.ts | 2 +- .../item-copy-actions.component.ts | 8 ++-- .../popup/settings/archive.component.html | 4 +- .../vault/popup/settings/archive.component.ts | 30 +++++++----- .../trash-list-items-container.component.html | 6 +-- .../trash-list-items-container.component.ts | 46 +++++++++++++++---- apps/browser/tsconfig.json | 3 ++ .../components/can-delete-cipher.directive.ts | 4 +- .../components/copy-cipher-field.directive.ts | 2 +- libs/vault/src/index.ts | 6 ++- .../src/services/copy-cipher-field.service.ts | 6 +++ 11 files changed, 84 insertions(+), 33 deletions(-) diff --git a/apps/browser/src/popup/app.module.ts b/apps/browser/src/popup/app.module.ts index ec35fc7c55..71846cc644 100644 --- a/apps/browser/src/popup/app.module.ts +++ b/apps/browser/src/popup/app.module.ts @@ -92,7 +92,7 @@ import "../platform/popup/locales"; TabsV2Component, RemovePasswordComponent, ], - exports: [], + exports: [CalloutModule], providers: [CurrencyPipe, DatePipe], bootstrap: [AppComponent], }) diff --git a/apps/browser/src/vault/popup/components/vault-v2/item-copy-action/item-copy-actions.component.ts b/apps/browser/src/vault/popup/components/vault-v2/item-copy-action/item-copy-actions.component.ts index 2e2ee5cd56..e24db60a55 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/item-copy-action/item-copy-actions.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/item-copy-action/item-copy-actions.component.ts @@ -10,7 +10,7 @@ import { } from "@bitwarden/common/vault/utils/cipher-view-like-utils"; import { IconButtonModule, ItemModule, MenuModule } from "@bitwarden/components"; import { CopyableCipherFields } from "@bitwarden/sdk-internal"; -import { CopyAction, CopyCipherFieldDirective } from "@bitwarden/vault"; +import { CopyFieldAction, CopyCipherFieldDirective } from "@bitwarden/vault"; import { VaultPopupCopyButtonsService } from "../../../services/vault-popup-copy-buttons.service"; @@ -18,7 +18,7 @@ type CipherItem = { /** Translation key for the respective value */ key: string; /** Property key on `CipherView` to retrieve the copy value */ - field: CopyAction; + field: CopyFieldAction; }; // FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush @@ -48,7 +48,7 @@ export class ItemCopyActionsComponent { * singleCopyableLogin uses appCopyField instead of appCopyClick. This allows for the TOTP * code to be copied correctly. See #14167 */ - get singleCopyableLogin() { + get singleCopyableLogin(): CipherItem | null { const loginItems: CipherItem[] = [ { key: "copyUsername", field: "username" }, { key: "copyPassword", field: "password" }, @@ -62,7 +62,7 @@ export class ItemCopyActionsComponent { ) { return { key: this.i18nService.t("copyUsername"), - field: "username", + field: "username" as const, }; } return this.findSingleCopyableItem(loginItems); diff --git a/apps/browser/src/vault/popup/settings/archive.component.html b/apps/browser/src/vault/popup/settings/archive.component.html index faaf0243fc..059d636c60 100644 --- a/apps/browser/src/vault/popup/settings/archive.component.html +++ b/apps/browser/src/vault/popup/settings/archive.component.html @@ -27,10 +27,10 @@ {{ cipher.name }} - @if (cipher.hasAttachments) { + @if (CipherViewLikeUtils.hasAttachments(cipher)) { } - {{ cipher.subTitle }} + {{ CipherViewLikeUtils.subtitle(cipher) }} @@ -45,7 +45,7 @@ type="button" bitMenuItem (click)="restore(cipher)" - *ngIf="!cipher.decryptionFailure" + *ngIf="!hasDecryptionFailure(cipher)" > {{ "restore" | i18n }} diff --git a/apps/browser/src/vault/popup/settings/trash-list-items-container/trash-list-items-container.component.ts b/apps/browser/src/vault/popup/settings/trash-list-items-container/trash-list-items-container.component.ts index 70ba6842a0..bad6011b2d 100644 --- a/apps/browser/src/vault/popup/settings/trash-list-items-container/trash-list-items-container.component.ts +++ b/apps/browser/src/vault/popup/settings/trash-list-items-container/trash-list-items-container.component.ts @@ -12,7 +12,6 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { CipherId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; -import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { DialogService, IconButtonModule, @@ -85,10 +84,40 @@ export class TrashListItemsContainerComponent { return collections[0]?.name; } - async restore(cipher: CipherView) { + /** + * Check if a cipher has attachments. CipherView has a hasAttachments getter, + * while CipherListView has an attachments count property. + */ + hasAttachments(cipher: PopupCipherViewLike): boolean { + if ("hasAttachments" in cipher) { + return cipher.hasAttachments; + } + return cipher.attachments > 0; + } + + /** + * Get the subtitle for a cipher. CipherView has a subTitle getter, + * while CipherListView has a subtitle property. + */ + getSubtitle(cipher: PopupCipherViewLike): string | undefined { + if ("subTitle" in cipher) { + return cipher.subTitle; + } + return cipher.subtitle; + } + + /** + * Check if a cipher has a decryption failure. CipherView has this property, + * while CipherListView does not. + */ + hasDecryptionFailure(cipher: PopupCipherViewLike): boolean { + return "decryptionFailure" in cipher && cipher.decryptionFailure; + } + + async restore(cipher: PopupCipherViewLike) { try { const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); - await this.cipherService.restoreWithServer(cipher.id, activeUserId); + await this.cipherService.restoreWithServer(cipher.id as string, activeUserId); await this.router.navigate(["/trash"]); this.toastService.showToast({ @@ -101,7 +130,7 @@ export class TrashListItemsContainerComponent { } } - async delete(cipher: CipherView) { + async delete(cipher: PopupCipherViewLike) { const repromptPassed = await this.passwordRepromptService.passwordRepromptCheck(cipher); if (!repromptPassed) { @@ -120,7 +149,7 @@ export class TrashListItemsContainerComponent { try { const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); - await this.cipherService.deleteWithServer(cipher.id, activeUserId); + await this.cipherService.deleteWithServer(cipher.id as string, activeUserId); await this.router.navigate(["/trash"]); this.toastService.showToast({ @@ -133,8 +162,9 @@ export class TrashListItemsContainerComponent { } } - async onViewCipher(cipher: CipherView) { - if (cipher.decryptionFailure) { + async onViewCipher(cipher: PopupCipherViewLike) { + // CipherListView doesn't have decryptionFailure, so we use optional chaining + if ("decryptionFailure" in cipher && cipher.decryptionFailure) { DecryptionFailureDialogComponent.open(this.dialogService, { cipherIds: [cipher.id as CipherId], }); @@ -147,7 +177,7 @@ export class TrashListItemsContainerComponent { } await this.router.navigate(["/view-cipher"], { - queryParams: { cipherId: cipher.id, type: cipher.type }, + queryParams: { cipherId: cipher.id as string, type: cipher.type }, }); } } diff --git a/apps/browser/tsconfig.json b/apps/browser/tsconfig.json index 0fd6cac423..6fb9dfbe46 100644 --- a/apps/browser/tsconfig.json +++ b/apps/browser/tsconfig.json @@ -1,5 +1,8 @@ { "extends": "../../tsconfig.base", + "angularCompilerOptions": { + "strictTemplates": true + }, "include": [ "src", "../../libs/common/src/autofill/constants", diff --git a/libs/vault/src/components/can-delete-cipher.directive.ts b/libs/vault/src/components/can-delete-cipher.directive.ts index 8ab59f9d64..f88e38049d 100644 --- a/libs/vault/src/components/can-delete-cipher.directive.ts +++ b/libs/vault/src/components/can-delete-cipher.directive.ts @@ -1,8 +1,8 @@ import { Directive, Input, OnDestroy, TemplateRef, ViewContainerRef } from "@angular/core"; import { Subject, takeUntil } from "rxjs"; -import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service"; +import { CipherViewLike } from "@bitwarden/common/vault/utils/cipher-view-like-utils"; /** * Only shows the element if the user can delete the cipher. @@ -15,7 +15,7 @@ export class CanDeleteCipherDirective implements OnDestroy { // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals // eslint-disable-next-line @angular-eslint/prefer-signals - @Input("appCanDeleteCipher") set cipher(cipher: CipherView) { + @Input("appCanDeleteCipher") set cipher(cipher: CipherViewLike) { this.viewContainer.clear(); this.cipherAuthorizationService diff --git a/libs/vault/src/components/copy-cipher-field.directive.ts b/libs/vault/src/components/copy-cipher-field.directive.ts index b4b1941273..52a4f59e7a 100644 --- a/libs/vault/src/components/copy-cipher-field.directive.ts +++ b/libs/vault/src/components/copy-cipher-field.directive.ts @@ -36,7 +36,7 @@ export class CopyCipherFieldDirective implements OnChanges { alias: "appCopyField", required: true, }) - action!: Exclude; + action!: CopyAction; // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals // eslint-disable-next-line @angular-eslint/prefer-signals diff --git a/libs/vault/src/index.ts b/libs/vault/src/index.ts index ccd830cd34..93a72ba14e 100644 --- a/libs/vault/src/index.ts +++ b/libs/vault/src/index.ts @@ -3,7 +3,11 @@ export { AtRiskPasswordCalloutData, } from "./services/at-risk-password-callout.service"; export { PasswordRepromptService } from "./services/password-reprompt.service"; -export { CopyCipherFieldService, CopyAction } from "./services/copy-cipher-field.service"; +export { + CopyCipherFieldService, + CopyAction, + CopyFieldAction, +} from "./services/copy-cipher-field.service"; export { CopyCipherFieldDirective } from "./components/copy-cipher-field.directive"; export { OrgIconDirective } from "./components/org-icon.directive"; export { CanDeleteCipherDirective } from "./components/can-delete-cipher.directive"; diff --git a/libs/vault/src/services/copy-cipher-field.service.ts b/libs/vault/src/services/copy-cipher-field.service.ts index 0a7d9e1f68..539c81b7be 100644 --- a/libs/vault/src/services/copy-cipher-field.service.ts +++ b/libs/vault/src/services/copy-cipher-field.service.ts @@ -35,6 +35,12 @@ export type CopyAction = | "publicKey" | "keyFingerprint"; +/** + * Copy actions that can be used with the appCopyField directive. + * Excludes "hiddenField" which requires special handling. + */ +export type CopyFieldAction = Exclude; + type CopyActionInfo = { /** * The i18n key for the type of field being copied. Will be used to display a toast message.