1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-10 13:23:34 +00:00

[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 <vleague@bitwarden.com>
This commit is contained in:
Bryan Cunningham
2025-11-25 11:04:37 -05:00
committed by GitHub
parent 57946f6406
commit 540da69daf
11 changed files with 84 additions and 33 deletions

View File

@@ -92,7 +92,7 @@ import "../platform/popup/locales";
TabsV2Component,
RemovePasswordComponent,
],
exports: [],
exports: [CalloutModule],
providers: [CurrencyPipe, DatePipe],
bootstrap: [AppComponent],
})

View File

@@ -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);

View File

@@ -27,10 +27,10 @@
<app-vault-icon [cipher]="cipher"></app-vault-icon>
</div>
<span data-testid="item-name">{{ cipher.name }}</span>
@if (cipher.hasAttachments) {
@if (CipherViewLikeUtils.hasAttachments(cipher)) {
<i class="bwi bwi-paperclip bwi-sm" [appA11yTitle]="'attachments' | i18n"></i>
}
<span slot="secondary">{{ cipher.subTitle }}</span>
<span slot="secondary">{{ CipherViewLikeUtils.subtitle(cipher) }}</span>
</button>
<bit-item-action slot="end">
<button

View File

@@ -11,7 +11,10 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service"
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 { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import {
CipherViewLike,
CipherViewLikeUtils,
} from "@bitwarden/common/vault/utils/cipher-view-like-utils";
import {
DialogService,
IconButtonModule,
@@ -71,12 +74,14 @@ export class ArchiveComponent {
switchMap((userId) => this.cipherArchiveService.archivedCiphers$(userId)),
);
protected CipherViewLikeUtils = CipherViewLikeUtils;
protected loading$ = this.archivedCiphers$.pipe(
map(() => false),
startWith(true),
);
async view(cipher: CipherView) {
async view(cipher: CipherViewLike) {
if (!(await this.canInteract(cipher))) {
return;
}
@@ -86,7 +91,7 @@ export class ArchiveComponent {
});
}
async edit(cipher: CipherView) {
async edit(cipher: CipherViewLike) {
if (!(await this.canInteract(cipher))) {
return;
}
@@ -96,7 +101,7 @@ export class ArchiveComponent {
});
}
async delete(cipher: CipherView) {
async delete(cipher: CipherViewLike) {
if (!(await this.canInteract(cipher))) {
return;
}
@@ -113,7 +118,7 @@ export class ArchiveComponent {
const activeUserId = await firstValueFrom(this.userId$);
try {
await this.cipherService.softDeleteWithServer(cipher.id, activeUserId);
await this.cipherService.softDeleteWithServer(cipher.id as string, activeUserId);
} catch (e) {
this.logService.error(e);
return;
@@ -125,13 +130,16 @@ export class ArchiveComponent {
});
}
async unarchive(cipher: CipherView) {
async unarchive(cipher: CipherViewLike) {
if (!(await this.canInteract(cipher))) {
return;
}
const activeUserId = await firstValueFrom(this.userId$);
await this.cipherArchiveService.unarchiveWithServer(cipher.id as CipherId, activeUserId);
await this.cipherArchiveService.unarchiveWithServer(
cipher.id as unknown as CipherId,
activeUserId,
);
this.toastService.showToast({
variant: "success",
@@ -139,12 +147,12 @@ export class ArchiveComponent {
});
}
async clone(cipher: CipherView) {
async clone(cipher: CipherViewLike) {
if (!(await this.canInteract(cipher))) {
return;
}
if (cipher.login?.hasFido2Credentials) {
if (CipherViewLikeUtils.hasFido2Credentials(cipher)) {
const confirmed = await this.dialogService.openSimpleDialog({
title: { key: "passkeyNotCopied" },
content: { key: "passkeyNotCopiedAlert" },
@@ -171,8 +179,8 @@ export class ArchiveComponent {
* @param cipher
* @private
*/
private canInteract(cipher: CipherView) {
if (cipher.decryptionFailure) {
private canInteract(cipher: CipherViewLike) {
if (CipherViewLikeUtils.decryptionFailure(cipher)) {
DecryptionFailureDialogComponent.open(this.dialogService, {
cipherIds: [cipher.id as CipherId],
});

View File

@@ -25,11 +25,11 @@
[appA11yTitle]="orgIconTooltip(cipher)"
></i>
<i
*ngIf="cipher.hasAttachments"
*ngIf="hasAttachments(cipher)"
class="bwi bwi-paperclip bwi-sm"
[appA11yTitle]="'attachments' | i18n"
></i>
<span slot="secondary">{{ cipher.subTitle }}</span>
<span slot="secondary">{{ getSubtitle(cipher) }}</span>
</button>
<ng-container slot="end" *ngIf="cipher.permissions.restore">
<bit-item-action>
@@ -45,7 +45,7 @@
type="button"
bitMenuItem
(click)="restore(cipher)"
*ngIf="!cipher.decryptionFailure"
*ngIf="!hasDecryptionFailure(cipher)"
>
{{ "restore" | i18n }}
</button>

View File

@@ -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 },
});
}
}

View File

@@ -1,5 +1,8 @@
{
"extends": "../../tsconfig.base",
"angularCompilerOptions": {
"strictTemplates": true
},
"include": [
"src",
"../../libs/common/src/autofill/constants",

View File

@@ -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

View File

@@ -36,7 +36,7 @@ export class CopyCipherFieldDirective implements OnChanges {
alias: "appCopyField",
required: true,
})
action!: Exclude<CopyAction, "hiddenField">;
action!: CopyAction;
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
// eslint-disable-next-line @angular-eslint/prefer-signals

View File

@@ -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";

View File

@@ -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<CopyAction, "hiddenField">;
type CopyActionInfo = {
/**
* The i18n key for the type of field being copied. Will be used to display a toast message.