mirror of
https://github.com/bitwarden/browser
synced 2025-12-15 07:43:35 +00:00
[PM-22134] Migrate list views to CipherListView from the SDK (#15174)
* add `CipherViewLike` and utilities to handle `CipherView` and `CipherViewLike` * migrate libs needed for web vault to support `CipherViewLike` * migrate web vault components to support * add for CipherView. will have to be later * fetch full CipherView for copying a password * have only the cipher service utilize SDK migration flag - This keeps feature flag logic away from the component - Also cuts down on what is needed for other platforms * strongly type CipherView for AC vault - Probably temporary before migration of the AC vault to `CipherListView` SDK * fix build icon tests by being more gracious with the uri structure * migrate desktop components to CipherListViews$ * consume card from sdk * add browser implementation for `CipherListView` * update copy message for single copiable items * refactor `getCipherViewLikeLogin` to `getLogin` * refactor `getCipherViewLikeCard` to `getCard` * add `hasFido2Credentials` helper * add decryption failure to cipher like utils * add todo with ticket * fix decryption failure typing * fix copy card messages * fix addition of organizations and collections for `PopupCipherViewLike` - accessors were being lost * refactor to getters to fix re-rendering bug * fix decryption failure helper * fix sorting functions for `CipherViewLike` * formatting * add `CipherViewLikeUtils` tests * refactor "copiable" to "copyable" to match SDK * use `hasOldAttachments` from cipherlistview * fix typing * update SDK version * add feature flag for cipher list view work * use `CipherViewLikeUtils` for copyable values rather than referring to the cipher directly * update restricted item type to support CipherViewLike * add cipher support to `CipherViewLikeUtils` * update `isCipherListView` check * refactor CipherLike to a separate type * refactor `getFullCipherView` into the cipher service * add optional chaining for `uriChecksum` * set empty array for decrypted CipherListView * migrate nudge service to use `cipherListViews` * update web vault to not depend on `cipherViews$` * update popup list filters to use `CipherListView` * fix storybook * fix tests * accept undefined as a MY VAULT filter value for cipher list views * use `LoginUriView` for uri logic (#15530) * filter out null ciphers from the `_allDecryptedCiphers$` (#15539) * use `launchUri` to avoid any unexpected behavior in URIs - this appends `http://` when missing
This commit is contained in:
@@ -1829,6 +1829,9 @@
|
||||
"securityCode": {
|
||||
"message": "Security code"
|
||||
},
|
||||
"cardNumber": {
|
||||
"message": "card number"
|
||||
},
|
||||
"ex": {
|
||||
"message": "ex."
|
||||
},
|
||||
@@ -4563,17 +4566,17 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"copyFieldValue": {
|
||||
"message": "Copy $FIELD$, $VALUE$",
|
||||
"copyFieldCipherName": {
|
||||
"message": "Copy $FIELD$, $CIPHERNAME$",
|
||||
"description": "Title for a button that copies a field value to the clipboard.",
|
||||
"placeholders": {
|
||||
"field": {
|
||||
"content": "$1",
|
||||
"example": "Username"
|
||||
},
|
||||
"value": {
|
||||
"ciphername": {
|
||||
"content": "$2",
|
||||
"example": "Foo"
|
||||
"example": "Login Item"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -6,12 +6,13 @@ import { combineLatest, map, Observable, startWith } from "rxjs";
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { CipherViewLikeUtils } from "@bitwarden/common/vault/utils/cipher-view-like-utils";
|
||||
import { IconButtonModule, TypographyModule } from "@bitwarden/components";
|
||||
|
||||
import BrowserPopupUtils from "../../../../../platform/browser/browser-popup-utils";
|
||||
import { VaultPopupAutofillService } from "../../../services/vault-popup-autofill.service";
|
||||
import { VaultPopupItemsService } from "../../../services/vault-popup-items.service";
|
||||
import { PopupCipherView } from "../../../views/popup-cipher.view";
|
||||
import { PopupCipherViewLike } from "../../../views/popup-cipher.view";
|
||||
import { VaultListItemsContainerComponent } from "../vault-list-items-container/vault-list-items-container.component";
|
||||
|
||||
@Component({
|
||||
@@ -30,7 +31,7 @@ export class AutofillVaultListItemsComponent {
|
||||
* The list of ciphers that can be used to autofill the current page.
|
||||
* @protected
|
||||
*/
|
||||
protected autofillCiphers$: Observable<PopupCipherView[]> =
|
||||
protected autofillCiphers$: Observable<PopupCipherViewLike[]> =
|
||||
this.vaultPopupItemsService.autoFillCiphers$;
|
||||
|
||||
/**
|
||||
@@ -62,7 +63,9 @@ export class AutofillVaultListItemsComponent {
|
||||
]).pipe(
|
||||
map(
|
||||
([hasFilter, ciphers, canAutoFill]) =>
|
||||
!hasFilter && canAutoFill && ciphers.filter((c) => c.type == CipherType.Login).length === 0,
|
||||
!hasFilter &&
|
||||
canAutoFill &&
|
||||
ciphers.filter((c) => CipherViewLikeUtils.getType(c) == CipherType.Login).length === 0,
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<ng-container *ngIf="cipher.type === CipherType.Login">
|
||||
<ng-container *ngIf="CipherViewLikeUtils.getType(cipher) === CipherType.Login">
|
||||
<ng-container *ngIf="showQuickCopyActions$ | async; else loginCopyMenu">
|
||||
<bit-item-action>
|
||||
<button
|
||||
@@ -36,15 +36,15 @@
|
||||
<ng-template #loginCopyMenu>
|
||||
<bit-item-action>
|
||||
<button
|
||||
*ngIf="singleCopiableLogin"
|
||||
*ngIf="singleCopyableLogin"
|
||||
type="button"
|
||||
bitIconButton="bwi-clone"
|
||||
size="small"
|
||||
[appA11yTitle]="singleCopiableLogin.key"
|
||||
[appCopyField]="$any(singleCopiableLogin.field)"
|
||||
[appA11yTitle]="'copyFieldCipherName' | i18n: singleCopyableLogin.key : cipher.name"
|
||||
[appCopyField]="singleCopyableLogin.field"
|
||||
[cipher]="cipher"
|
||||
></button>
|
||||
<ng-container *ngIf="!singleCopiableLogin">
|
||||
<ng-container *ngIf="!singleCopyableLogin">
|
||||
<button
|
||||
type="button"
|
||||
bitIconButton="bwi-clone"
|
||||
@@ -77,7 +77,7 @@
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="cipher.type === CipherType.Card">
|
||||
<ng-container *ngIf="CipherViewLikeUtils.getType(cipher) === CipherType.Card">
|
||||
<ng-container *ngIf="showQuickCopyActions$ | async; else cardCopyMenu">
|
||||
<bit-item-action>
|
||||
<button
|
||||
@@ -103,16 +103,16 @@
|
||||
<ng-template #cardCopyMenu>
|
||||
<bit-item-action>
|
||||
<button
|
||||
*ngIf="singleCopiableCard"
|
||||
*ngIf="singleCopyableCard"
|
||||
type="button"
|
||||
bitIconButton="bwi-clone"
|
||||
size="small"
|
||||
[appA11yTitle]="'copyFieldValue' | i18n: singleCopiableCard.key : singleCopiableCard.value"
|
||||
[appCopyClick]="singleCopiableCard.value"
|
||||
[valueLabel]="singleCopiableCard.key"
|
||||
[appA11yTitle]="'copyFieldCipherName' | i18n: singleCopyableCard.key : cipher.name"
|
||||
[appCopyField]="singleCopyableCard.field"
|
||||
[cipher]="cipher"
|
||||
showToast
|
||||
></button>
|
||||
<ng-container *ngIf="!singleCopiableCard">
|
||||
<ng-container *ngIf="!singleCopyableCard">
|
||||
<button
|
||||
type="button"
|
||||
bitIconButton="bwi-clone"
|
||||
@@ -136,20 +136,18 @@
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
|
||||
<bit-item-action *ngIf="cipher.type === CipherType.Identity">
|
||||
<bit-item-action *ngIf="CipherViewLikeUtils.getType(cipher) === CipherType.Identity">
|
||||
<button
|
||||
*ngIf="singleCopiableIdentity"
|
||||
*ngIf="singleCopyableIdentity"
|
||||
type="button"
|
||||
bitIconButton="bwi-clone"
|
||||
size="small"
|
||||
[appA11yTitle]="
|
||||
'copyFieldValue' | i18n: singleCopiableIdentity.key : singleCopiableIdentity.value
|
||||
"
|
||||
[appCopyClick]="singleCopiableIdentity.value"
|
||||
[valueLabel]="singleCopiableIdentity.key"
|
||||
[appA11yTitle]="'copyFieldCipherName' | i18n: singleCopyableIdentity.key : cipher.name"
|
||||
[appCopyField]="singleCopyableIdentity.field"
|
||||
[cipher]="cipher"
|
||||
showToast
|
||||
></button>
|
||||
<ng-container *ngIf="!singleCopiableIdentity">
|
||||
<ng-container *ngIf="!singleCopyableIdentity">
|
||||
<button
|
||||
type="button"
|
||||
bitIconButton="bwi-clone"
|
||||
@@ -177,7 +175,7 @@
|
||||
</ng-container>
|
||||
</bit-item-action>
|
||||
|
||||
<bit-item-action *ngIf="cipher.type === CipherType.SecureNote">
|
||||
<bit-item-action *ngIf="CipherViewLikeUtils.getType(cipher) === CipherType.SecureNote">
|
||||
<button
|
||||
type="button"
|
||||
bitIconButton="bwi-clone"
|
||||
@@ -190,7 +188,7 @@
|
||||
></button>
|
||||
</bit-item-action>
|
||||
|
||||
<bit-item-action *ngIf="cipher.type === CipherType.SshKey">
|
||||
<bit-item-action *ngIf="CipherViewLikeUtils.getType(cipher) === CipherType.SshKey">
|
||||
<button
|
||||
type="button"
|
||||
bitIconButton="bwi-clone"
|
||||
|
||||
@@ -1,21 +1,24 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, Input, inject } from "@angular/core";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import {
|
||||
CipherViewLike,
|
||||
CipherViewLikeUtils,
|
||||
} from "@bitwarden/common/vault/utils/cipher-view-like-utils";
|
||||
import { IconButtonModule, ItemModule, MenuModule } from "@bitwarden/components";
|
||||
import { CopyCipherFieldDirective } from "@bitwarden/vault";
|
||||
import { CopyableCipherFields } from "@bitwarden/sdk-internal";
|
||||
import { CopyAction, CopyCipherFieldDirective } from "@bitwarden/vault";
|
||||
|
||||
import { VaultPopupCopyButtonsService } from "../../../services/vault-popup-copy-buttons.service";
|
||||
|
||||
type CipherItem = {
|
||||
value: string;
|
||||
/** Translation key for the respective value */
|
||||
key: string;
|
||||
field?: string;
|
||||
/** Property key on `CipherView` to retrieve the copy value */
|
||||
field: CopyAction;
|
||||
};
|
||||
|
||||
@Component({
|
||||
@@ -32,91 +35,155 @@ type CipherItem = {
|
||||
})
|
||||
export class ItemCopyActionsComponent {
|
||||
protected showQuickCopyActions$ = inject(VaultPopupCopyButtonsService).showQuickCopyActions$;
|
||||
@Input({ required: true }) cipher!: CipherViewLike;
|
||||
|
||||
@Input() cipher: CipherView;
|
||||
|
||||
protected CipherViewLikeUtils = CipherViewLikeUtils;
|
||||
protected CipherType = CipherType;
|
||||
|
||||
get hasLoginValues() {
|
||||
return (
|
||||
!!this.cipher.login.hasTotp || !!this.cipher.login.password || !!this.cipher.login.username
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
* singleCopiableLogin uses appCopyField instead of appCopyClick. This allows for the TOTP
|
||||
* singleCopyableLogin uses appCopyField instead of appCopyClick. This allows for the TOTP
|
||||
* code to be copied correctly. See #14167
|
||||
*/
|
||||
get singleCopiableLogin() {
|
||||
get singleCopyableLogin() {
|
||||
const loginItems: CipherItem[] = [
|
||||
{ value: this.cipher.login.username, key: "copyUsername", field: "username" },
|
||||
{ value: this.cipher.login.password, key: "copyPassword", field: "password" },
|
||||
{ value: this.cipher.login.totp, key: "copyVerificationCode", field: "totp" },
|
||||
{ key: "copyUsername", field: "username" },
|
||||
{ key: "copyPassword", field: "password" },
|
||||
{ key: "copyVerificationCode", field: "totp" },
|
||||
];
|
||||
// If both the password and username are visible but the password is hidden, return the username
|
||||
if (!this.cipher.viewPassword && this.cipher.login.username && this.cipher.login.password) {
|
||||
if (
|
||||
!this.cipher.viewPassword &&
|
||||
CipherViewLikeUtils.hasCopyableValue(this.cipher, "username") &&
|
||||
CipherViewLikeUtils.hasCopyableValue(this.cipher, "password")
|
||||
) {
|
||||
return {
|
||||
value: this.cipher.login.username,
|
||||
key: this.i18nService.t("copyUsername"),
|
||||
field: "username",
|
||||
};
|
||||
}
|
||||
return this.findSingleCopiableItem(loginItems);
|
||||
return this.findSingleCopyableItem(loginItems);
|
||||
}
|
||||
|
||||
get singleCopiableCard() {
|
||||
get singleCopyableCard() {
|
||||
const cardItems: CipherItem[] = [
|
||||
{ value: this.cipher.card.code, key: "code" },
|
||||
{ value: this.cipher.card.number, key: "number" },
|
||||
{ key: "securityCode", field: "securityCode" },
|
||||
{ key: "cardNumber", field: "cardNumber" },
|
||||
];
|
||||
return this.findSingleCopiableItem(cardItems);
|
||||
return this.findSingleCopyableItem(cardItems);
|
||||
}
|
||||
|
||||
get singleCopiableIdentity() {
|
||||
get singleCopyableIdentity() {
|
||||
const identityItems: CipherItem[] = [
|
||||
{ value: this.cipher.identity.fullAddressForCopy, key: "address" },
|
||||
{ value: this.cipher.identity.email, key: "email" },
|
||||
{ value: this.cipher.identity.username, key: "username" },
|
||||
{ value: this.cipher.identity.phone, key: "phone" },
|
||||
{ key: "address", field: "address" },
|
||||
{ key: "email", field: "email" },
|
||||
{ key: "username", field: "username" },
|
||||
{ key: "phone", field: "phone" },
|
||||
];
|
||||
return this.findSingleCopiableItem(identityItems);
|
||||
return this.findSingleCopyableItem(identityItems);
|
||||
}
|
||||
|
||||
/*
|
||||
* Given a list of CipherItems, if there is only one item with a value,
|
||||
* return it with the translated key. Otherwise return null
|
||||
*/
|
||||
findSingleCopiableItem(items: CipherItem[]): CipherItem | null {
|
||||
const itemsWithValue = items.filter(({ value }) => !!value);
|
||||
findSingleCopyableItem(items: CipherItem[]): CipherItem | null {
|
||||
const itemsWithValue = items.filter(({ field }) =>
|
||||
CipherViewLikeUtils.hasCopyableValue(this.cipher, field),
|
||||
);
|
||||
return itemsWithValue.length === 1
|
||||
? { ...itemsWithValue[0], key: this.i18nService.t(itemsWithValue[0].key) }
|
||||
: null;
|
||||
}
|
||||
|
||||
get hasLoginValues() {
|
||||
return this.getNumberOfLoginValues() > 0;
|
||||
}
|
||||
|
||||
get hasCardValues() {
|
||||
return !!this.cipher.card.code || !!this.cipher.card.number;
|
||||
return this.getNumberOfCardValues() > 0;
|
||||
}
|
||||
|
||||
get hasIdentityValues() {
|
||||
return (
|
||||
!!this.cipher.identity.fullAddressForCopy ||
|
||||
!!this.cipher.identity.email ||
|
||||
!!this.cipher.identity.username ||
|
||||
!!this.cipher.identity.phone
|
||||
);
|
||||
return this.getNumberOfIdentityValues() > 0;
|
||||
}
|
||||
|
||||
get hasSecureNoteValue() {
|
||||
return !!this.cipher.notes;
|
||||
return this.getNumberOfSecureNoteValues() > 0;
|
||||
}
|
||||
|
||||
get hasSshKeyValues() {
|
||||
return (
|
||||
!!this.cipher.sshKey.privateKey ||
|
||||
!!this.cipher.sshKey.publicKey ||
|
||||
!!this.cipher.sshKey.keyFingerprint
|
||||
);
|
||||
return this.getNumberOfSshKeyValues() > 0;
|
||||
}
|
||||
|
||||
constructor(private i18nService: I18nService) {}
|
||||
|
||||
/** Sets the number of populated login values for the cipher */
|
||||
private getNumberOfLoginValues() {
|
||||
if (CipherViewLikeUtils.isCipherListView(this.cipher)) {
|
||||
const copyableLoginFields: CopyableCipherFields[] = [
|
||||
"LoginUsername",
|
||||
"LoginPassword",
|
||||
"LoginTotp",
|
||||
];
|
||||
return this.cipher.copyableFields.filter((field) => copyableLoginFields.includes(field))
|
||||
.length;
|
||||
}
|
||||
|
||||
return [this.cipher.login.username, this.cipher.login.password, this.cipher.login.totp].filter(
|
||||
Boolean,
|
||||
).length;
|
||||
}
|
||||
|
||||
/** Sets the number of populated card values for the cipher */
|
||||
private getNumberOfCardValues() {
|
||||
if (CipherViewLikeUtils.isCipherListView(this.cipher)) {
|
||||
const copyableCardFields: CopyableCipherFields[] = ["CardSecurityCode", "CardNumber"];
|
||||
return this.cipher.copyableFields.filter((field) => copyableCardFields.includes(field))
|
||||
.length;
|
||||
}
|
||||
|
||||
return [this.cipher.card.code, this.cipher.card.number].filter(Boolean).length;
|
||||
}
|
||||
|
||||
/** Sets the number of populated identity values for the cipher */
|
||||
private getNumberOfIdentityValues() {
|
||||
if (CipherViewLikeUtils.isCipherListView(this.cipher)) {
|
||||
const copyableIdentityFields: CopyableCipherFields[] = [
|
||||
"IdentityAddress",
|
||||
"IdentityEmail",
|
||||
"IdentityUsername",
|
||||
"IdentityPhone",
|
||||
];
|
||||
return this.cipher.copyableFields.filter((field) => copyableIdentityFields.includes(field))
|
||||
.length;
|
||||
}
|
||||
|
||||
return [
|
||||
this.cipher.identity.fullAddressForCopy,
|
||||
this.cipher.identity.email,
|
||||
this.cipher.identity.username,
|
||||
this.cipher.identity.phone,
|
||||
].filter(Boolean).length;
|
||||
}
|
||||
/** Sets the number of populated secure note values for the cipher */
|
||||
private getNumberOfSecureNoteValues(): number {
|
||||
if (CipherViewLikeUtils.isCipherListView(this.cipher)) {
|
||||
return this.cipher.copyableFields.includes("SecureNotes") ? 1 : 0;
|
||||
}
|
||||
|
||||
return this.cipher.notes ? 1 : 0;
|
||||
}
|
||||
|
||||
/** Sets the number of populated SSH key values for the cipher */
|
||||
private getNumberOfSshKeyValues() {
|
||||
if (CipherViewLikeUtils.isCipherListView(this.cipher)) {
|
||||
return this.cipher.copyableFields.includes("SshKey") ? 1 : 0;
|
||||
}
|
||||
|
||||
return [
|
||||
this.cipher.sshKey.privateKey,
|
||||
this.cipher.sshKey.publicKey,
|
||||
this.cipher.sshKey.keyFingerprint,
|
||||
].filter(Boolean).length;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
size="small"
|
||||
[attr.aria-label]="'moreOptionsLabel' | i18n: cipher.name"
|
||||
[title]="'moreOptionsTitle' | i18n: cipher.name"
|
||||
[disabled]="cipher.decryptionFailure"
|
||||
[disabled]="decryptionFailure"
|
||||
[bitMenuTriggerFor]="moreOptions"
|
||||
></button>
|
||||
<bit-menu #moreOptions>
|
||||
|
||||
@@ -14,8 +14,11 @@ import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CipherRepromptType, CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
|
||||
import {
|
||||
CipherViewLike,
|
||||
CipherViewLikeUtils,
|
||||
} from "@bitwarden/common/vault/utils/cipher-view-like-utils";
|
||||
import {
|
||||
DialogService,
|
||||
IconButtonModule,
|
||||
@@ -34,12 +37,12 @@ import { AddEditQueryParams } from "../add-edit/add-edit-v2.component";
|
||||
imports: [ItemModule, IconButtonModule, MenuModule, CommonModule, JslibModule, RouterModule],
|
||||
})
|
||||
export class ItemMoreOptionsComponent {
|
||||
private _cipher$ = new BehaviorSubject<CipherView>(undefined);
|
||||
private _cipher$ = new BehaviorSubject<CipherViewLike>(undefined);
|
||||
|
||||
@Input({
|
||||
required: true,
|
||||
})
|
||||
set cipher(c: CipherView) {
|
||||
set cipher(c: CipherViewLike) {
|
||||
this._cipher$.next(c);
|
||||
}
|
||||
|
||||
@@ -109,17 +112,22 @@ export class ItemMoreOptionsComponent {
|
||||
get canViewPassword() {
|
||||
return this.cipher.viewPassword;
|
||||
}
|
||||
|
||||
get decryptionFailure() {
|
||||
return CipherViewLikeUtils.decryptionFailure(this.cipher);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the cipher can be autofilled.
|
||||
*/
|
||||
get canAutofill() {
|
||||
return ([CipherType.Login, CipherType.Card, CipherType.Identity] as CipherType[]).includes(
|
||||
this.cipher.type,
|
||||
CipherViewLikeUtils.getType(this.cipher),
|
||||
);
|
||||
}
|
||||
|
||||
get isLogin() {
|
||||
return this.cipher.type === CipherType.Login;
|
||||
return CipherViewLikeUtils.getType(this.cipher) === CipherType.Login;
|
||||
}
|
||||
|
||||
get favoriteText() {
|
||||
@@ -127,11 +135,13 @@ export class ItemMoreOptionsComponent {
|
||||
}
|
||||
|
||||
async doAutofill() {
|
||||
await this.vaultPopupAutofillService.doAutofill(this.cipher);
|
||||
const cipher = await this.cipherService.getFullCipherView(this.cipher);
|
||||
await this.vaultPopupAutofillService.doAutofill(cipher);
|
||||
}
|
||||
|
||||
async doAutofillAndSave() {
|
||||
await this.vaultPopupAutofillService.doAutofillAndSave(this.cipher, false);
|
||||
const cipher = await this.cipherService.getFullCipherView(this.cipher);
|
||||
await this.vaultPopupAutofillService.doAutofillAndSave(cipher, false);
|
||||
}
|
||||
|
||||
async onView() {
|
||||
@@ -140,7 +150,7 @@ export class ItemMoreOptionsComponent {
|
||||
return;
|
||||
}
|
||||
await this.router.navigate(["/view-cipher"], {
|
||||
queryParams: { cipherId: this.cipher.id, type: this.cipher.type },
|
||||
queryParams: { cipherId: this.cipher.id, type: CipherViewLikeUtils.getType(this.cipher) },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -148,11 +158,14 @@ export class ItemMoreOptionsComponent {
|
||||
* Toggles the favorite status of the cipher and updates it on the server.
|
||||
*/
|
||||
async toggleFavorite() {
|
||||
this.cipher.favorite = !this.cipher.favorite;
|
||||
const cipher = await this.cipherService.getFullCipherView(this.cipher);
|
||||
|
||||
cipher.favorite = !cipher.favorite;
|
||||
const activeUserId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||
);
|
||||
const encryptedCipher = await this.cipherService.encrypt(this.cipher, activeUserId);
|
||||
|
||||
const encryptedCipher = await this.cipherService.encrypt(cipher, activeUserId);
|
||||
await this.cipherService.updateWithServer(encryptedCipher);
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
@@ -176,7 +189,7 @@ export class ItemMoreOptionsComponent {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.cipher.login?.hasFido2Credentials) {
|
||||
if (CipherViewLikeUtils.hasFido2Credentials(this.cipher)) {
|
||||
const confirmed = await this.dialogService.openSimpleDialog({
|
||||
title: { key: "passkeyNotCopied" },
|
||||
content: { key: "passkeyNotCopiedAlert" },
|
||||
@@ -192,7 +205,7 @@ export class ItemMoreOptionsComponent {
|
||||
queryParams: {
|
||||
clone: true.toString(),
|
||||
cipherId: this.cipher.id,
|
||||
type: this.cipher.type.toString(),
|
||||
type: CipherViewLikeUtils.getType(this.cipher).toString(),
|
||||
} as AddEditQueryParams,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -97,7 +97,8 @@
|
||||
(click)="primaryActionOnSelect(cipher)"
|
||||
(dblclick)="launchCipher(cipher)"
|
||||
[appA11yTitle]="
|
||||
cipherItemTitleKey()(cipher) | i18n: cipher.name : cipher.login.username
|
||||
cipherItemTitleKey()(cipher)
|
||||
| i18n: cipher.name : CipherViewLikeUtils.getLogin(cipher)?.username
|
||||
"
|
||||
class="{{ itemHeightClass }}"
|
||||
>
|
||||
@@ -114,11 +115,11 @@
|
||||
[appA11yTitle]="orgIconTooltip(cipher)"
|
||||
></i>
|
||||
<i
|
||||
*ngIf="cipher.hasAttachments"
|
||||
*ngIf="CipherViewLikeUtils.hasAttachments(cipher)"
|
||||
class="bwi bwi-paperclip bwi-sm"
|
||||
[appA11yTitle]="'attachments' | i18n"
|
||||
></i>
|
||||
<span slot="secondary">{{ cipher.subTitle }}</span>
|
||||
<span slot="secondary">{{ CipherViewLikeUtils.subtitle(cipher) }}</span>
|
||||
</button>
|
||||
|
||||
<ng-container slot="end">
|
||||
@@ -134,7 +135,7 @@
|
||||
{{ "fill" | i18n }}
|
||||
</button>
|
||||
</bit-item-action>
|
||||
<bit-item-action *ngIf="!showAutofillButton() && cipher.canLaunch">
|
||||
<bit-item-action *ngIf="!showAutofillButton() && CipherViewLikeUtils.canLaunch(cipher)">
|
||||
<button
|
||||
type="button"
|
||||
bitIconButton="bwi-external-link"
|
||||
|
||||
@@ -26,7 +26,10 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
|
||||
import { CipherId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import {
|
||||
CipherViewLike,
|
||||
CipherViewLikeUtils,
|
||||
} from "@bitwarden/common/vault/utils/cipher-view-like-utils";
|
||||
import {
|
||||
BadgeModule,
|
||||
ButtonModule,
|
||||
@@ -54,7 +57,7 @@ import {
|
||||
VaultPopupSectionService,
|
||||
PopupSectionOpen,
|
||||
} from "../../../services/vault-popup-section.service";
|
||||
import { PopupCipherView } from "../../../views/popup-cipher.view";
|
||||
import { PopupCipherViewLike } from "../../../views/popup-cipher.view";
|
||||
import { ItemCopyActionsComponent } from "../item-copy-action/item-copy-actions.component";
|
||||
import { ItemMoreOptionsComponent } from "../item-more-options/item-more-options.component";
|
||||
|
||||
@@ -84,6 +87,7 @@ import { ItemMoreOptionsComponent } from "../item-more-options/item-more-options
|
||||
export class VaultListItemsContainerComponent implements AfterViewInit {
|
||||
private compactModeService = inject(CompactModeService);
|
||||
private vaultPopupSectionService = inject(VaultPopupSectionService);
|
||||
protected CipherViewLikeUtils = CipherViewLikeUtils;
|
||||
|
||||
@ViewChild(CdkVirtualScrollViewport, { static: false }) viewPort!: CdkVirtualScrollViewport;
|
||||
@ViewChild(DisclosureComponent) disclosure!: DisclosureComponent;
|
||||
@@ -125,7 +129,7 @@ export class VaultListItemsContainerComponent implements AfterViewInit {
|
||||
*/
|
||||
private viewCipherTimeout?: number;
|
||||
|
||||
ciphers = input<PopupCipherView[]>([]);
|
||||
ciphers = input<PopupCipherViewLike[]>([]);
|
||||
|
||||
/**
|
||||
* If true, we will group ciphers by type (Login, Card, Identity)
|
||||
@@ -139,7 +143,7 @@ export class VaultListItemsContainerComponent implements AfterViewInit {
|
||||
cipherGroups = computed<
|
||||
{
|
||||
subHeaderKey?: string;
|
||||
ciphers: PopupCipherView[];
|
||||
ciphers: PopupCipherViewLike[];
|
||||
}[]
|
||||
>(() => {
|
||||
// Not grouping by type, return a single group with all ciphers
|
||||
@@ -147,11 +151,11 @@ export class VaultListItemsContainerComponent implements AfterViewInit {
|
||||
return [{ ciphers: this.ciphers() }];
|
||||
}
|
||||
|
||||
const groups: Record<string, PopupCipherView[]> = {};
|
||||
const groups: Record<string, PopupCipherViewLike[]> = {};
|
||||
|
||||
this.ciphers().forEach((cipher) => {
|
||||
let groupKey = "all";
|
||||
switch (cipher.type) {
|
||||
switch (CipherViewLikeUtils.getType(cipher)) {
|
||||
case CipherType.Card:
|
||||
groupKey = "cards";
|
||||
break;
|
||||
@@ -212,8 +216,9 @@ export class VaultListItemsContainerComponent implements AfterViewInit {
|
||||
* Resolved i18n key to use for suggested cipher items
|
||||
*/
|
||||
cipherItemTitleKey = computed(() => {
|
||||
return (cipher: CipherView) => {
|
||||
const hasUsername = cipher.login?.username != null;
|
||||
return (cipher: CipherViewLike) => {
|
||||
const login = CipherViewLikeUtils.getLogin(cipher);
|
||||
const hasUsername = login?.username != null;
|
||||
const key =
|
||||
this.primaryActionAutofill() && !this.currentURIIsBlocked()
|
||||
? "autofillTitle"
|
||||
@@ -259,12 +264,12 @@ export class VaultListItemsContainerComponent implements AfterViewInit {
|
||||
* The tooltip text for the organization icon for ciphers that belong to an organization.
|
||||
* @param cipher
|
||||
*/
|
||||
orgIconTooltip(cipher: PopupCipherView) {
|
||||
if (cipher.collectionIds.length > 1 || !cipher.collections) {
|
||||
return this.i18nService.t("nCollections", cipher.collectionIds.length);
|
||||
orgIconTooltip({ collectionIds, collections }: PopupCipherViewLike) {
|
||||
if (collectionIds.length > 1 || !collections) {
|
||||
return this.i18nService.t("nCollections", collectionIds.length);
|
||||
}
|
||||
|
||||
return cipher.collections[0]?.name;
|
||||
return collections[0]?.name;
|
||||
}
|
||||
|
||||
protected autofillShortcutTooltip = signal<string | undefined>(undefined);
|
||||
@@ -292,7 +297,7 @@ export class VaultListItemsContainerComponent implements AfterViewInit {
|
||||
}
|
||||
}
|
||||
|
||||
primaryActionOnSelect(cipher: CipherView) {
|
||||
primaryActionOnSelect(cipher: PopupCipherViewLike) {
|
||||
return this.primaryActionAutofill() && !this.currentURIIsBlocked()
|
||||
? this.doAutofill(cipher)
|
||||
: this.onViewCipher(cipher);
|
||||
@@ -301,8 +306,9 @@ export class VaultListItemsContainerComponent implements AfterViewInit {
|
||||
/**
|
||||
* Launches the login cipher in a new browser tab.
|
||||
*/
|
||||
async launchCipher(cipher: CipherView) {
|
||||
if (!cipher.canLaunch) {
|
||||
async launchCipher(cipher: CipherViewLike) {
|
||||
const launchURI = CipherViewLikeUtils.getLaunchUri(cipher);
|
||||
if (!CipherViewLikeUtils.canLaunch(cipher) || !launchURI) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -313,20 +319,30 @@ export class VaultListItemsContainerComponent implements AfterViewInit {
|
||||
}
|
||||
|
||||
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
await this.cipherService.updateLastLaunchedDate(cipher.id, activeUserId);
|
||||
await this.cipherService.updateLastLaunchedDate(cipher.id!, activeUserId);
|
||||
|
||||
await BrowserApi.createNewTab(cipher.login.launchUri);
|
||||
await BrowserApi.createNewTab(launchURI);
|
||||
|
||||
if (BrowserPopupUtils.inPopup(window)) {
|
||||
BrowserApi.closePopup(window);
|
||||
}
|
||||
}
|
||||
|
||||
async doAutofill(cipher: PopupCipherView) {
|
||||
await this.vaultPopupAutofillService.doAutofill(cipher);
|
||||
async doAutofill(cipher: PopupCipherViewLike) {
|
||||
if (!CipherViewLikeUtils.isCipherListView(cipher)) {
|
||||
await this.vaultPopupAutofillService.doAutofill(cipher);
|
||||
return;
|
||||
}
|
||||
|
||||
// When only the `CipherListView` is available, fetch the full cipher details
|
||||
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
const _cipher = await this.cipherService.get(cipher.id!, activeUserId);
|
||||
const cipherView = await this.cipherService.decrypt(_cipher, activeUserId);
|
||||
|
||||
await this.vaultPopupAutofillService.doAutofill(cipherView);
|
||||
}
|
||||
|
||||
async onViewCipher(cipher: PopupCipherView) {
|
||||
async onViewCipher(cipher: PopupCipherViewLike) {
|
||||
// We already have a view action in progress, don't start another
|
||||
if (this.viewCipherTimeout != null) {
|
||||
return;
|
||||
@@ -336,7 +352,7 @@ export class VaultListItemsContainerComponent implements AfterViewInit {
|
||||
this.viewCipherTimeout = window.setTimeout(
|
||||
async () => {
|
||||
try {
|
||||
if (cipher.decryptionFailure) {
|
||||
if (CipherViewLikeUtils.decryptionFailure(cipher)) {
|
||||
DecryptionFailureDialogComponent.open(this.dialogService, {
|
||||
cipherIds: [cipher.id as CipherId],
|
||||
});
|
||||
@@ -355,7 +371,7 @@ export class VaultListItemsContainerComponent implements AfterViewInit {
|
||||
this.viewCipherTimeout = undefined;
|
||||
}
|
||||
},
|
||||
cipher.canLaunch ? 200 : 0,
|
||||
CipherViewLikeUtils.canLaunch(cipher) ? 200 : 0,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { WritableSignal, signal } from "@angular/core";
|
||||
import { TestBed } from "@angular/core/testing";
|
||||
import { mock } from "jest-mock-extended";
|
||||
import { BehaviorSubject, firstValueFrom, timeout } from "rxjs";
|
||||
import { BehaviorSubject, firstValueFrom, of, take, timeout } from "rxjs";
|
||||
|
||||
import { CollectionService, CollectionView } from "@bitwarden/admin-console/common";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
@@ -23,10 +23,12 @@ import {
|
||||
RestrictedCipherType,
|
||||
RestrictedItemTypesService,
|
||||
} from "@bitwarden/common/vault/services/restricted-item-types.service";
|
||||
import { CipherViewLikeUtils } from "@bitwarden/common/vault/utils/cipher-view-like-utils";
|
||||
|
||||
import { InlineMenuFieldQualificationService } from "../../../autofill/services/inline-menu-field-qualification.service";
|
||||
import { BrowserApi } from "../../../platform/browser/browser-api";
|
||||
import { PopupViewCacheService } from "../../../platform/popup/view-cache/popup-view-cache.service";
|
||||
import { PopupCipherViewLike } from "../views/popup-cipher.view";
|
||||
|
||||
import { VaultPopupAutofillService } from "./vault-popup-autofill.service";
|
||||
import { VaultPopupItemsService } from "./vault-popup-items.service";
|
||||
@@ -80,7 +82,9 @@ describe("VaultPopupItemsService", () => {
|
||||
cipherList[2].favorite = true;
|
||||
cipherList[3].favorite = true;
|
||||
|
||||
cipherServiceMock.getAllDecrypted.mockResolvedValue(cipherList);
|
||||
const cipherList$ = new BehaviorSubject<CipherView[]>(cipherList);
|
||||
|
||||
cipherServiceMock.cipherListViews$.mockReturnValue(cipherList$.asObservable());
|
||||
|
||||
ciphersSubject = new BehaviorSubject<Record<CipherId, CipherData>>({});
|
||||
localDataSubject = new BehaviorSubject<Record<CipherId, LocalData>>({});
|
||||
@@ -111,7 +115,7 @@ describe("VaultPopupItemsService", () => {
|
||||
});
|
||||
// Return all ciphers, `filterFunction$` will be tested in `VaultPopupListFiltersService`
|
||||
vaultPopupListFiltersServiceMock.filterFunction$ = new BehaviorSubject(
|
||||
(ciphers: CipherView[]) => ciphers,
|
||||
(ciphers: PopupCipherViewLike[]) => ciphers,
|
||||
);
|
||||
|
||||
vaultAutofillServiceMock.currentAutofillTab$ = new BehaviorSubject({
|
||||
@@ -279,7 +283,9 @@ describe("VaultPopupItemsService", () => {
|
||||
const current = ciphers[i];
|
||||
const next = ciphers[i + 1];
|
||||
|
||||
expect(expectedTypeOrder[current.type]).toBeLessThanOrEqual(expectedTypeOrder[next.type]);
|
||||
expect(expectedTypeOrder[CipherViewLikeUtils.getType(current)]).toBeLessThanOrEqual(
|
||||
expectedTypeOrder[CipherViewLikeUtils.getType(next)],
|
||||
);
|
||||
}
|
||||
expect(cipherServiceMock.sortCiphersByLastUsedThenName).toHaveBeenCalled();
|
||||
done();
|
||||
@@ -365,28 +371,34 @@ describe("VaultPopupItemsService", () => {
|
||||
|
||||
describe("emptyVault$", () => {
|
||||
it("should return true if there are no ciphers", (done) => {
|
||||
cipherServiceMock.getAllDecrypted.mockResolvedValue([]);
|
||||
service.emptyVault$.subscribe((empty) => {
|
||||
cipherServiceMock.cipherListViews$.mockReturnValue(of([]));
|
||||
service.emptyVault$.pipe(take(1)).subscribe((empty) => {
|
||||
expect(empty).toBe(true);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should return false if there are ciphers", (done) => {
|
||||
service.emptyVault$.subscribe((empty) => {
|
||||
cipherServiceMock.cipherListViews$.mockReturnValue(
|
||||
of([{ id: "1", type: CipherType.Login, name: "Login 1" }] as CipherView[]),
|
||||
);
|
||||
|
||||
service.emptyVault$.pipe(take(1)).subscribe((empty) => {
|
||||
expect(empty).toBe(false);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should return true when all ciphers are deleted", (done) => {
|
||||
cipherServiceMock.getAllDecrypted.mockResolvedValue([
|
||||
{ id: "1", type: CipherType.Login, name: "Login 1", isDeleted: true },
|
||||
{ id: "2", type: CipherType.Login, name: "Login 2", isDeleted: true },
|
||||
{ id: "3", type: CipherType.Login, name: "Login 3", isDeleted: true },
|
||||
] as CipherView[]);
|
||||
cipherServiceMock.cipherListViews$.mockReturnValue(
|
||||
of([
|
||||
{ id: "1", type: CipherType.Login, name: "Login 1", isDeleted: true },
|
||||
{ id: "2", type: CipherType.Login, name: "Login 2", isDeleted: true },
|
||||
{ id: "3", type: CipherType.Login, name: "Login 3", isDeleted: true },
|
||||
] as CipherView[]),
|
||||
);
|
||||
|
||||
service.emptyVault$.subscribe((empty) => {
|
||||
service.emptyVault$.pipe(take(1)).subscribe((empty) => {
|
||||
expect(empty).toBe(true);
|
||||
done();
|
||||
});
|
||||
@@ -416,8 +428,7 @@ describe("VaultPopupItemsService", () => {
|
||||
deletedCipher.deletedDate = new Date();
|
||||
const ciphers = [new CipherView(), new CipherView(), new CipherView(), deletedCipher];
|
||||
|
||||
cipherServiceMock.getAllDecrypted.mockResolvedValue(ciphers);
|
||||
|
||||
cipherServiceMock.cipherListViews$.mockReturnValue(of(ciphers));
|
||||
ciphersSubject.next({});
|
||||
|
||||
const deletedCiphers = await firstValueFrom(service.deletedCiphers$);
|
||||
|
||||
@@ -23,20 +23,22 @@ 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 { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { SyncService } from "@bitwarden/common/platform/sync";
|
||||
import { CollectionId, OrganizationId, UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { SearchService } from "@bitwarden/common/vault/abstractions/search.service";
|
||||
import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service";
|
||||
import {
|
||||
CipherViewLike,
|
||||
CipherViewLikeUtils,
|
||||
} from "@bitwarden/common/vault/utils/cipher-view-like-utils";
|
||||
|
||||
import { runInsideAngular } from "../../../platform/browser/run-inside-angular.operator";
|
||||
import { PopupViewCacheService } from "../../../platform/popup/view-cache/popup-view-cache.service";
|
||||
import { waitUntil } from "../../util";
|
||||
import { PopupCipherView } from "../views/popup-cipher.view";
|
||||
import { PopupCipherViewLike } from "../views/popup-cipher.view";
|
||||
|
||||
import { VaultPopupAutofillService } from "./vault-popup-autofill.service";
|
||||
import { MY_VAULT_ID, VaultPopupListFiltersService } from "./vault-popup-list-filters.service";
|
||||
@@ -96,49 +98,52 @@ export class VaultPopupItemsService {
|
||||
* Observable that contains the list of all decrypted ciphers.
|
||||
* @private
|
||||
*/
|
||||
private _allDecryptedCiphers$: Observable<CipherView[]> = this.accountService.activeAccount$.pipe(
|
||||
map((a) => a?.id),
|
||||
filter((userId): userId is UserId => userId != null),
|
||||
switchMap((userId) =>
|
||||
merge(this.cipherService.ciphers$(userId), this.cipherService.localData$(userId)).pipe(
|
||||
runInsideAngular(this.ngZone),
|
||||
tap(() => this._ciphersLoading$.next()),
|
||||
waitUntilSync(this.syncService),
|
||||
switchMap(() =>
|
||||
combineLatest([
|
||||
Utils.asyncToObservable(() => this.cipherService.getAllDecrypted(userId)),
|
||||
this.cipherService.failedToDecryptCiphers$(userId),
|
||||
this.restrictedItemTypesService.restricted$.pipe(startWith([])),
|
||||
]),
|
||||
private _allDecryptedCiphers$: Observable<CipherViewLike[]> =
|
||||
this.accountService.activeAccount$.pipe(
|
||||
map((a) => a?.id),
|
||||
filter((userId): userId is UserId => userId != null),
|
||||
switchMap((userId) =>
|
||||
merge(this.cipherService.ciphers$(userId), this.cipherService.localData$(userId)).pipe(
|
||||
runInsideAngular(this.ngZone),
|
||||
tap(() => this._ciphersLoading$.next()),
|
||||
waitUntilSync(this.syncService),
|
||||
switchMap(() =>
|
||||
combineLatest([
|
||||
this.cipherService
|
||||
.cipherListViews$(userId)
|
||||
.pipe(filter((ciphers) => ciphers != null)),
|
||||
this.cipherService.failedToDecryptCiphers$(userId),
|
||||
this.restrictedItemTypesService.restricted$.pipe(startWith([])),
|
||||
]),
|
||||
),
|
||||
map(([ciphers, failedToDecryptCiphers, restrictions]) => {
|
||||
const allCiphers = [...(failedToDecryptCiphers || []), ...ciphers];
|
||||
|
||||
return allCiphers.filter(
|
||||
(cipher) => !this.restrictedItemTypesService.isCipherRestricted(cipher, restrictions),
|
||||
);
|
||||
}),
|
||||
),
|
||||
map(([ciphers, failedToDecryptCiphers, restrictions]) => {
|
||||
const allCiphers = [...(failedToDecryptCiphers || []), ...ciphers];
|
||||
|
||||
return allCiphers.filter(
|
||||
(cipher) => !this.restrictedItemTypesService.isCipherRestricted(cipher, restrictions),
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
shareReplay({ refCount: true, bufferSize: 1 }),
|
||||
);
|
||||
shareReplay({ refCount: true, bufferSize: 1 }),
|
||||
);
|
||||
|
||||
private _activeCipherList$: Observable<PopupCipherView[]> = this._allDecryptedCiphers$.pipe(
|
||||
private _activeCipherList$: Observable<PopupCipherViewLike[]> = this._allDecryptedCiphers$.pipe(
|
||||
switchMap((ciphers) =>
|
||||
combineLatest([this.organizations$, this.collectionService.decryptedCollections$]).pipe(
|
||||
map(([organizations, collections]) => {
|
||||
const orgMap = Object.fromEntries(organizations.map((org) => [org.id, org]));
|
||||
const collectionMap = Object.fromEntries(collections.map((col) => [col.id, col]));
|
||||
return ciphers
|
||||
.filter((c) => !c.isDeleted)
|
||||
.map(
|
||||
(cipher) =>
|
||||
new PopupCipherView(
|
||||
cipher,
|
||||
cipher.collectionIds?.map((colId) => collectionMap[colId as CollectionId]),
|
||||
orgMap[cipher.organizationId as OrganizationId],
|
||||
),
|
||||
);
|
||||
.filter((c) => !CipherViewLikeUtils.isDeleted(c))
|
||||
.map((cipher) => {
|
||||
(cipher as PopupCipherViewLike).collections = cipher.collectionIds?.map(
|
||||
(colId) => collectionMap[colId as CollectionId],
|
||||
);
|
||||
(cipher as PopupCipherViewLike).organization =
|
||||
orgMap[cipher.organizationId as OrganizationId];
|
||||
return cipher;
|
||||
});
|
||||
}),
|
||||
),
|
||||
),
|
||||
@@ -157,21 +162,23 @@ export class VaultPopupItemsService {
|
||||
}),
|
||||
);
|
||||
|
||||
private _filteredCipherList$: Observable<PopupCipherView[]> = combineLatest([
|
||||
private _filteredCipherList$: Observable<PopupCipherViewLike[]> = combineLatest([
|
||||
this._activeCipherList$,
|
||||
this.searchText$,
|
||||
this.vaultPopupListFiltersService.filterFunction$,
|
||||
getUserId(this.accountService.activeAccount$),
|
||||
]).pipe(
|
||||
map(([ciphers, searchText, filterFunction, userId]): [CipherView[], string, UserId] => [
|
||||
filterFunction(ciphers),
|
||||
searchText,
|
||||
userId,
|
||||
]),
|
||||
map(
|
||||
([ciphers, searchText, filterFunction, userId]): [PopupCipherViewLike[], string, UserId] => [
|
||||
filterFunction(ciphers),
|
||||
searchText,
|
||||
userId,
|
||||
],
|
||||
),
|
||||
switchMap(
|
||||
([ciphers, searchText, userId]) =>
|
||||
this.searchService.searchCiphers(userId, searchText, undefined, ciphers) as Promise<
|
||||
PopupCipherView[]
|
||||
PopupCipherViewLike[]
|
||||
>,
|
||||
),
|
||||
shareReplay({ refCount: true, bufferSize: 1 }),
|
||||
@@ -183,7 +190,7 @@ export class VaultPopupItemsService {
|
||||
*
|
||||
* See {@link refreshCurrentTab} to trigger re-evaluation of the current tab.
|
||||
*/
|
||||
autoFillCiphers$: Observable<PopupCipherView[]> = combineLatest([
|
||||
autoFillCiphers$: Observable<PopupCipherViewLike[]> = combineLatest([
|
||||
this._filteredCipherList$,
|
||||
this._otherAutoFillTypes$,
|
||||
this.vaultPopupAutofillService.currentAutofillTab$,
|
||||
@@ -202,7 +209,7 @@ export class VaultPopupItemsService {
|
||||
* List of favorite ciphers that are not currently suggested for autofill.
|
||||
* Ciphers are sorted by name.
|
||||
*/
|
||||
favoriteCiphers$: Observable<PopupCipherView[]> = this.autoFillCiphers$.pipe(
|
||||
favoriteCiphers$: Observable<PopupCipherViewLike[]> = this.autoFillCiphers$.pipe(
|
||||
withLatestFrom(this._filteredCipherList$),
|
||||
map(([autoFillCiphers, ciphers]) =>
|
||||
ciphers.filter((cipher) => cipher.favorite && !autoFillCiphers.includes(cipher)),
|
||||
@@ -214,7 +221,7 @@ export class VaultPopupItemsService {
|
||||
* List of all remaining ciphers that are not currently suggested for autofill or marked as favorite.
|
||||
* Ciphers are sorted by name.
|
||||
*/
|
||||
remainingCiphers$: Observable<PopupCipherView[]> = this.favoriteCiphers$.pipe(
|
||||
remainingCiphers$: Observable<PopupCipherViewLike[]> = this.favoriteCiphers$.pipe(
|
||||
concatMap(
|
||||
(
|
||||
favoriteCiphers, // concatMap->of is used to make withLatestFrom lazy to avoid race conditions with autoFillCiphers$
|
||||
@@ -282,21 +289,23 @@ export class VaultPopupItemsService {
|
||||
/**
|
||||
* Observable that contains the list of ciphers that have been deleted.
|
||||
*/
|
||||
deletedCiphers$: Observable<PopupCipherView[]> = this._allDecryptedCiphers$.pipe(
|
||||
deletedCiphers$: Observable<PopupCipherViewLike[]> = this._allDecryptedCiphers$.pipe(
|
||||
switchMap((ciphers) =>
|
||||
combineLatest([this.organizations$, this.collectionService.decryptedCollections$]).pipe(
|
||||
map(([organizations, collections]) => {
|
||||
const orgMap = Object.fromEntries(organizations.map((org) => [org.id, org]));
|
||||
const collectionMap = Object.fromEntries(collections.map((col) => [col.id, col]));
|
||||
return ciphers
|
||||
.filter((c) => c.isDeleted)
|
||||
.filter((c) => CipherViewLikeUtils.isDeleted(c))
|
||||
.map(
|
||||
(cipher) =>
|
||||
new PopupCipherView(
|
||||
cipher,
|
||||
cipher.collectionIds?.map((colId) => collectionMap[colId as CollectionId]),
|
||||
orgMap[cipher.organizationId as OrganizationId],
|
||||
),
|
||||
({
|
||||
...cipher,
|
||||
collections: cipher.collectionIds?.map(
|
||||
(colId) => collectionMap[colId as CollectionId],
|
||||
),
|
||||
organization: orgMap[cipher.organizationId as OrganizationId],
|
||||
}) as PopupCipherViewLike,
|
||||
);
|
||||
}),
|
||||
),
|
||||
@@ -327,7 +336,7 @@ export class VaultPopupItemsService {
|
||||
* Sorts by type, then by last used date, and finally by name.
|
||||
* @private
|
||||
*/
|
||||
private sortCiphersForAutofill(a: CipherView, b: CipherView): number {
|
||||
private sortCiphersForAutofill(a: CipherViewLike, b: CipherViewLike): number {
|
||||
const typeOrder = {
|
||||
[CipherType.Login]: 1,
|
||||
[CipherType.Card]: 2,
|
||||
@@ -336,10 +345,13 @@ export class VaultPopupItemsService {
|
||||
[CipherType.SshKey]: 5,
|
||||
} as Record<CipherType, number>;
|
||||
|
||||
const aType = CipherViewLikeUtils.getType(a);
|
||||
const bType = CipherViewLikeUtils.getType(b);
|
||||
|
||||
// Compare types first
|
||||
if (typeOrder[a.type] < typeOrder[b.type]) {
|
||||
if (typeOrder[aType] < typeOrder[bType]) {
|
||||
return -1;
|
||||
} else if (typeOrder[a.type] > typeOrder[b.type]) {
|
||||
} else if (typeOrder[aType] > typeOrder[bType]) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,8 @@ import {
|
||||
RestrictedItemTypesService,
|
||||
} from "@bitwarden/common/vault/services/restricted-item-types.service";
|
||||
|
||||
import { PopupCipherViewLike } from "../views/popup-cipher.view";
|
||||
|
||||
import {
|
||||
CachedFilterState,
|
||||
MY_VAULT_ID,
|
||||
@@ -47,7 +49,7 @@ describe("VaultPopupListFiltersService", () => {
|
||||
const memberOrganizations$ = (userId: UserId) => _memberOrganizations$;
|
||||
const organizations$ = new BehaviorSubject<Organization[]>([]);
|
||||
let folderViews$ = new BehaviorSubject([]);
|
||||
const cipherViews$ = new BehaviorSubject({});
|
||||
const cipherListViews$ = new BehaviorSubject({});
|
||||
let decryptedCollections$ = new BehaviorSubject<CollectionView[]>([]);
|
||||
const policyAppliesToUser$ = new BehaviorSubject<boolean>(false);
|
||||
let viewCacheService: {
|
||||
@@ -65,7 +67,7 @@ describe("VaultPopupListFiltersService", () => {
|
||||
} as unknown as FolderService;
|
||||
|
||||
const cipherService = {
|
||||
cipherViews$: () => cipherViews$,
|
||||
cipherListViews$: () => cipherListViews$,
|
||||
} as unknown as CipherService;
|
||||
|
||||
const organizationService = {
|
||||
@@ -508,7 +510,7 @@ describe("VaultPopupListFiltersService", () => {
|
||||
{ id: "2345", name: "Folder 2" },
|
||||
]);
|
||||
|
||||
cipherViews$.next({
|
||||
cipherListViews$.next({
|
||||
"1": { folderId: "1234", organizationId: "1234" },
|
||||
"2": { folderId: "2345", organizationId: "56789" },
|
||||
});
|
||||
@@ -566,6 +568,28 @@ describe("VaultPopupListFiltersService", () => {
|
||||
service.filterForm.patchValue({ organization });
|
||||
});
|
||||
|
||||
it("keeps ciphers with null and undefined for organizationId when MyVault is selected", (done) => {
|
||||
const organization = { id: MY_VAULT_ID } as Organization;
|
||||
|
||||
const undefinedOrgIdCipher = {
|
||||
type: CipherType.SecureNote,
|
||||
collectionIds: [],
|
||||
organizationId: undefined,
|
||||
} as unknown as PopupCipherViewLike;
|
||||
|
||||
service.filterFunction$.subscribe((filterFunction) => {
|
||||
expect(filterFunction([...ciphers, undefinedOrgIdCipher])).toEqual([
|
||||
ciphers[0],
|
||||
ciphers[2],
|
||||
ciphers[3],
|
||||
undefinedOrgIdCipher,
|
||||
]);
|
||||
done();
|
||||
});
|
||||
|
||||
service.filterForm.patchValue({ organization });
|
||||
});
|
||||
|
||||
it("filters out ciphers that do not belong to the selected organization", (done) => {
|
||||
const organization = { id: "8978" } as Organization;
|
||||
|
||||
@@ -717,7 +741,10 @@ function createSeededVaultPopupListFiltersService(
|
||||
collections: CollectionView[],
|
||||
folderViews: FolderView[],
|
||||
cachedState: CachedFilterState = {},
|
||||
): { service: VaultPopupListFiltersService; cachedSignal: WritableSignal<CachedFilterState> } {
|
||||
): {
|
||||
service: VaultPopupListFiltersService;
|
||||
cachedSignal: WritableSignal<CachedFilterState>;
|
||||
} {
|
||||
const seededMemberOrganizations$ = new BehaviorSubject<Organization[]>(organizations);
|
||||
const seededCollections$ = new BehaviorSubject<CollectionView[]>(collections);
|
||||
const seededFolderViews$ = new BehaviorSubject<FolderView[]>(folderViews);
|
||||
@@ -744,7 +771,7 @@ function createSeededVaultPopupListFiltersService(
|
||||
} as any;
|
||||
|
||||
const cipherServiceMock = {
|
||||
cipherViews$: () => new BehaviorSubject({}),
|
||||
cipherListViews$: () => new BehaviorSubject({}),
|
||||
} as any;
|
||||
|
||||
const i18nServiceMock = {
|
||||
|
||||
@@ -40,13 +40,15 @@ import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.servi
|
||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { ITreeNodeObject, TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
||||
import { ServiceUtils } from "@bitwarden/common/vault/service-utils";
|
||||
import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service";
|
||||
import { CIPHER_MENU_ITEMS } from "@bitwarden/common/vault/types/cipher-menu-items";
|
||||
import { CipherViewLikeUtils } from "@bitwarden/common/vault/utils/cipher-view-like-utils";
|
||||
import { ChipSelectOption } from "@bitwarden/components";
|
||||
|
||||
import { PopupCipherViewLike } from "../views/popup-cipher.view";
|
||||
|
||||
const FILTER_VISIBILITY_KEY = new KeyDefinition<boolean>(VAULT_SETTINGS_DISK, "filterVisibility", {
|
||||
deserializer: (obj) => obj,
|
||||
});
|
||||
@@ -111,7 +113,7 @@ export class VaultPopupListFiltersService {
|
||||
/**
|
||||
* Static list of ciphers views used in synchronous context
|
||||
*/
|
||||
private cipherViews: CipherView[] = [];
|
||||
private cipherViews: PopupCipherViewLike[] = [];
|
||||
|
||||
private activeUserId$ = this.accountService.activeAccount$.pipe(
|
||||
map((a) => a?.id),
|
||||
@@ -216,21 +218,22 @@ export class VaultPopupListFiltersService {
|
||||
filterVisibilityState$ = this.filterVisibilityState.state$;
|
||||
|
||||
/**
|
||||
* Observable whose value is a function that filters an array of `CipherView` objects based on the current filters
|
||||
* Observable whose value is a function that filters an array of `PopupCipherViewLike` objects based on the current filters
|
||||
*/
|
||||
filterFunction$: Observable<(ciphers: CipherView[]) => CipherView[]> = combineLatest([
|
||||
this.filters$,
|
||||
]).pipe(
|
||||
map(
|
||||
([filters]) =>
|
||||
(ciphers: CipherView[]) =>
|
||||
filterFunction$: Observable<(ciphers: PopupCipherViewLike[]) => PopupCipherViewLike[]> =
|
||||
this.filters$.pipe(
|
||||
map(
|
||||
(filters) => (ciphers: PopupCipherViewLike[]) =>
|
||||
ciphers.filter((cipher) => {
|
||||
// Vault popup lists never shows deleted ciphers
|
||||
if (cipher.isDeleted) {
|
||||
if (CipherViewLikeUtils.isDeleted(cipher)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (filters.cipherType !== null && cipher.type !== filters.cipherType) {
|
||||
if (
|
||||
filters.cipherType !== null &&
|
||||
CipherViewLikeUtils.getType(cipher) !== filters.cipherType
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -245,7 +248,7 @@ export class VaultPopupListFiltersService {
|
||||
const isMyVault = filters.organization?.id === MY_VAULT_ID;
|
||||
|
||||
if (isMyVault) {
|
||||
if (cipher.organizationId !== null) {
|
||||
if (cipher.organizationId != null) {
|
||||
return false;
|
||||
}
|
||||
} else if (filters.organization) {
|
||||
@@ -256,8 +259,8 @@ export class VaultPopupListFiltersService {
|
||||
|
||||
return true;
|
||||
}),
|
||||
),
|
||||
);
|
||||
),
|
||||
);
|
||||
|
||||
/**
|
||||
* All available cipher types (filtered by policy restrictions)
|
||||
@@ -356,7 +359,7 @@ export class VaultPopupListFiltersService {
|
||||
folders$: Observable<ChipSelectOption<FolderView>[]> = this.activeUserId$.pipe(
|
||||
switchMap((userId) => {
|
||||
// Observable of cipher views
|
||||
const cipherViews$ = this.cipherService.cipherViews$(userId).pipe(
|
||||
const cipherViews$ = this.cipherService.cipherListViews$(userId).pipe(
|
||||
map((ciphers) => {
|
||||
this.cipherViews = ciphers ? Object.values(ciphers) : [];
|
||||
return this.cipherViews;
|
||||
@@ -374,30 +377,36 @@ export class VaultPopupListFiltersService {
|
||||
this.folderService.folderViews$(userId),
|
||||
cipherViews$,
|
||||
]).pipe(
|
||||
map(([filters, folders, cipherViews]): [PopupListFilter, FolderView[], CipherView[]] => {
|
||||
if (folders.length === 1 && folders[0].id === null) {
|
||||
// Do not display folder selections when only the "no folder" option is available.
|
||||
return [filters as PopupListFilter, [], cipherViews];
|
||||
}
|
||||
map(
|
||||
([filters, folders, cipherViews]): [
|
||||
PopupListFilter,
|
||||
FolderView[],
|
||||
PopupCipherViewLike[],
|
||||
] => {
|
||||
if (folders.length === 1 && folders[0].id === null) {
|
||||
// Do not display folder selections when only the "no folder" option is available.
|
||||
return [filters as PopupListFilter, [], cipherViews];
|
||||
}
|
||||
|
||||
// Sort folders by alphabetic name
|
||||
folders.sort(Utils.getSortFunction(this.i18nService, "name"));
|
||||
let arrangedFolders = folders;
|
||||
// Sort folders by alphabetic name
|
||||
folders.sort(Utils.getSortFunction(this.i18nService, "name"));
|
||||
let arrangedFolders = folders;
|
||||
|
||||
const noFolder = folders.find((f) => f.id === null);
|
||||
const noFolder = folders.find((f) => f.id === null);
|
||||
|
||||
if (noFolder) {
|
||||
// Update `name` of the "no folder" option to "Items with no folder"
|
||||
const updatedNoFolder = {
|
||||
...noFolder,
|
||||
name: this.i18nService.t("itemsWithNoFolder"),
|
||||
};
|
||||
if (noFolder) {
|
||||
// Update `name` of the "no folder" option to "Items with no folder"
|
||||
const updatedNoFolder = {
|
||||
...noFolder,
|
||||
name: this.i18nService.t("itemsWithNoFolder"),
|
||||
};
|
||||
|
||||
// Move the "no folder" option to the end of the list
|
||||
arrangedFolders = [...folders.filter((f) => f.id !== null), updatedNoFolder];
|
||||
}
|
||||
return [filters as PopupListFilter, arrangedFolders, cipherViews];
|
||||
}),
|
||||
// Move the "no folder" option to the end of the list
|
||||
arrangedFolders = [...folders.filter((f) => f.id !== null), updatedNoFolder];
|
||||
}
|
||||
return [filters as PopupListFilter, arrangedFolders, cipherViews];
|
||||
},
|
||||
),
|
||||
map(([filters, folders, cipherViews]) => {
|
||||
const organizationId = filters.organization?.id ?? null;
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ import {
|
||||
PasswordRepromptService,
|
||||
} from "@bitwarden/vault";
|
||||
|
||||
import { PopupCipherView } from "../../views/popup-cipher.view";
|
||||
import { PopupCipherViewLike } from "../../views/popup-cipher.view";
|
||||
|
||||
@Component({
|
||||
selector: "app-trash-list-items-container",
|
||||
@@ -54,7 +54,7 @@ export class TrashListItemsContainerComponent {
|
||||
* The list of trashed items to display.
|
||||
*/
|
||||
@Input()
|
||||
ciphers: PopupCipherView[] = [];
|
||||
ciphers: PopupCipherViewLike[] = [];
|
||||
|
||||
@Input()
|
||||
headerText: string;
|
||||
@@ -73,12 +73,12 @@ export class TrashListItemsContainerComponent {
|
||||
/**
|
||||
* The tooltip text for the organization icon for ciphers that belong to an organization.
|
||||
*/
|
||||
orgIconTooltip(cipher: PopupCipherView) {
|
||||
if (cipher.collectionIds.length > 1) {
|
||||
return this.i18nService.t("nCollections", cipher.collectionIds.length);
|
||||
orgIconTooltip({ collections, collectionIds }: PopupCipherViewLike) {
|
||||
if (collectionIds.length > 1) {
|
||||
return this.i18nService.t("nCollections", collectionIds.length);
|
||||
}
|
||||
|
||||
return cipher.collections[0]?.name;
|
||||
return collections[0]?.name;
|
||||
}
|
||||
|
||||
async restore(cipher: CipherView) {
|
||||
|
||||
@@ -1,25 +1,17 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { CollectionView } from "@bitwarden/admin-console/common";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { CipherListView } from "@bitwarden/sdk-internal";
|
||||
|
||||
/**
|
||||
* Extended cipher view for the popup. Includes the associated collections and organization
|
||||
* if applicable.
|
||||
*/
|
||||
export class PopupCipherView extends CipherView {
|
||||
interface CommonPopupCipherView {
|
||||
collections?: CollectionView[];
|
||||
organization?: Organization;
|
||||
|
||||
constructor(
|
||||
cipher: CipherView,
|
||||
collections: CollectionView[] = null,
|
||||
organization: Organization = null,
|
||||
) {
|
||||
super();
|
||||
Object.assign(this, cipher);
|
||||
this.collections = collections;
|
||||
this.organization = organization;
|
||||
}
|
||||
}
|
||||
|
||||
/** Extended view for the popup based off of `CipherView` */
|
||||
interface PopupCipherView extends CipherView, CommonPopupCipherView {}
|
||||
|
||||
/** Extended view for the popup based off of `CipherListView` from the SDK. */
|
||||
interface PopupCipherListView extends CipherListView, CommonPopupCipherView {}
|
||||
|
||||
export type PopupCipherViewLike = PopupCipherListView | PopupCipherView;
|
||||
|
||||
@@ -572,6 +572,20 @@
|
||||
"copyVerificationCodeTotp": {
|
||||
"message": "Copy verification code (TOTP)"
|
||||
},
|
||||
"copyFieldCipherName": {
|
||||
"message": "Copy $FIELD$, $CIPHERNAME$",
|
||||
"description": "Title for a button that copies a field value to the clipboard.",
|
||||
"placeholders": {
|
||||
"field": {
|
||||
"content": "$1",
|
||||
"example": "Username"
|
||||
},
|
||||
"ciphername": {
|
||||
"content": "$2",
|
||||
"example": "Login Item"
|
||||
}
|
||||
}
|
||||
},
|
||||
"length": {
|
||||
"message": "Length"
|
||||
},
|
||||
@@ -1425,6 +1439,9 @@
|
||||
"message": "Copy security code",
|
||||
"description": "Copy credit card security code (CVV)"
|
||||
},
|
||||
"cardNumber": {
|
||||
"message": "card number"
|
||||
},
|
||||
"premiumMembership": {
|
||||
"message": "Premium membership"
|
||||
},
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
></i>
|
||||
<span class="sr-only">{{ "shared" | i18n }}</span>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="c.hasAttachments">
|
||||
<ng-container *ngIf="CipherViewLikeUtils.hasAttachments(c)">
|
||||
<i
|
||||
class="bwi bwi-paperclip text-muted"
|
||||
title="{{ 'attachments' | i18n }}"
|
||||
@@ -44,7 +44,9 @@
|
||||
</ng-container>
|
||||
</span>
|
||||
</span>
|
||||
<span *ngIf="c.subTitle" class="detail">{{ c.subTitle }}</span>
|
||||
<span *ngIf="CipherViewLikeUtils.subtitle(c)" class="detail">{{
|
||||
CipherViewLikeUtils.subtitle(c)
|
||||
}}</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -9,8 +9,11 @@ import { VaultItemsComponent as BaseVaultItemsComponent } from "@bitwarden/angul
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { SearchService } from "@bitwarden/common/vault/abstractions/search.service";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service";
|
||||
import {
|
||||
CipherViewLike,
|
||||
CipherViewLikeUtils,
|
||||
} from "@bitwarden/common/vault/utils/cipher-view-like-utils";
|
||||
import { MenuModule } from "@bitwarden/components";
|
||||
|
||||
import { SearchBarService } from "../../../app/layout/search/search-bar.service";
|
||||
@@ -20,7 +23,8 @@ import { SearchBarService } from "../../../app/layout/search/search-bar.service"
|
||||
templateUrl: "vault-items-v2.component.html",
|
||||
imports: [MenuModule, CommonModule, JslibModule, ScrollingModule],
|
||||
})
|
||||
export class VaultItemsV2Component extends BaseVaultItemsComponent {
|
||||
export class VaultItemsV2Component<C extends CipherViewLike> extends BaseVaultItemsComponent<C> {
|
||||
protected CipherViewLikeUtils = CipherViewLikeUtils;
|
||||
constructor(
|
||||
searchService: SearchService,
|
||||
private readonly searchBarService: SearchBarService,
|
||||
@@ -37,7 +41,7 @@ export class VaultItemsV2Component extends BaseVaultItemsComponent {
|
||||
});
|
||||
}
|
||||
|
||||
trackByFn(index: number, c: CipherView): string {
|
||||
return c.id;
|
||||
trackByFn(index: number, c: C): string {
|
||||
return c.id!;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,6 +40,10 @@ import { ViewPasswordHistoryService } from "@bitwarden/common/vault/abstractions
|
||||
import { CipherType, toCipherType } from "@bitwarden/common/vault/enums";
|
||||
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import {
|
||||
CipherViewLike,
|
||||
CipherViewLikeUtils,
|
||||
} from "@bitwarden/common/vault/utils/cipher-view-like-utils";
|
||||
import {
|
||||
BadgeModule,
|
||||
ButtonModule,
|
||||
@@ -124,9 +128,11 @@ const BroadcasterSubscriptionId = "VaultComponent";
|
||||
},
|
||||
],
|
||||
})
|
||||
export class VaultV2Component implements OnInit, OnDestroy, CopyClickListener {
|
||||
export class VaultV2Component<C extends CipherViewLike>
|
||||
implements OnInit, OnDestroy, CopyClickListener
|
||||
{
|
||||
@ViewChild(VaultItemsV2Component, { static: true })
|
||||
vaultItemsComponent: VaultItemsV2Component | null = null;
|
||||
vaultItemsComponent: VaultItemsV2Component<C> | null = null;
|
||||
@ViewChild(VaultFilterComponent, { static: true })
|
||||
vaultFilterComponent: VaultFilterComponent | null = null;
|
||||
@ViewChild("folderAddEdit", { read: ViewContainerRef, static: true })
|
||||
@@ -407,14 +413,14 @@ export class VaultV2Component implements OnInit, OnDestroy, CopyClickListener {
|
||||
this.messagingService.send("minimizeOnCopy");
|
||||
}
|
||||
|
||||
async viewCipher(cipher: CipherView) {
|
||||
if (cipher.decryptionFailure) {
|
||||
async viewCipher(c: CipherViewLike) {
|
||||
if (CipherViewLikeUtils.decryptionFailure(c)) {
|
||||
DecryptionFailureDialogComponent.open(this.dialogService, {
|
||||
cipherIds: [cipher.id as CipherId],
|
||||
cipherIds: [c.id as CipherId],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const cipher = await this.cipherService.getFullCipherView(c);
|
||||
if (await this.shouldReprompt(cipher, "view")) {
|
||||
return;
|
||||
}
|
||||
@@ -472,7 +478,8 @@ export class VaultV2Component implements OnInit, OnDestroy, CopyClickListener {
|
||||
}
|
||||
}
|
||||
|
||||
viewCipherMenu(cipher: CipherView) {
|
||||
async viewCipherMenu(c: CipherViewLike) {
|
||||
const cipher = await this.cipherService.getFullCipherView(c);
|
||||
const menu: RendererMenuItem[] = [
|
||||
{
|
||||
label: this.i18nService.t("view"),
|
||||
|
||||
@@ -536,7 +536,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
const filterFunction = createFilterFunction(filter);
|
||||
|
||||
if (await this.searchService.isSearchable(this.userId, searchText)) {
|
||||
return await this.searchService.searchCiphers(
|
||||
return await this.searchService.searchCiphers<CipherView>(
|
||||
this.userId,
|
||||
searchText,
|
||||
[filterFunction],
|
||||
@@ -772,7 +772,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
this.destroy$.complete();
|
||||
}
|
||||
|
||||
async onVaultItemsEvent(event: VaultItemEvent) {
|
||||
async onVaultItemsEvent(event: VaultItemEvent<CipherView>) {
|
||||
this.processingEvent = true;
|
||||
|
||||
try {
|
||||
|
||||
@@ -28,6 +28,7 @@ import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
|
||||
import { UnionOfValues } from "@bitwarden/common/vault/types/union-of-values";
|
||||
import { CipherViewLike } from "@bitwarden/common/vault/utils/cipher-view-like-utils";
|
||||
import {
|
||||
DIALOG_DATA,
|
||||
DialogRef,
|
||||
@@ -91,7 +92,7 @@ export interface VaultItemDialogParams {
|
||||
/**
|
||||
* Function to restore a cipher from the trash.
|
||||
*/
|
||||
restore?: (c: CipherView) => Promise<boolean>;
|
||||
restore?: (c: CipherViewLike) => Promise<boolean>;
|
||||
}
|
||||
|
||||
export const VaultItemDialogResult = {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
type="checkbox"
|
||||
bitCheckbox
|
||||
appStopProp
|
||||
[disabled]="disabled || cipher.decryptionFailure"
|
||||
[disabled]="disabled || decryptionFailure"
|
||||
[checked]="checked"
|
||||
(change)="$event ? this.checkedToggled.next() : null"
|
||||
[attr.aria-label]="'vaultItemSelect' | i18n"
|
||||
@@ -30,7 +30,7 @@
|
||||
>
|
||||
{{ cipher.name }}
|
||||
</button>
|
||||
<ng-container *ngIf="cipher.hasAttachments">
|
||||
<ng-container *ngIf="hasAttachments">
|
||||
<i
|
||||
class="bwi bwi-paperclip tw-ml-2 tw-leading-normal"
|
||||
appStopProp
|
||||
@@ -50,7 +50,7 @@
|
||||
</ng-container>
|
||||
</div>
|
||||
<br />
|
||||
<span class="tw-text-sm tw-text-muted" appStopProp>{{ cipher.subTitle }}</span>
|
||||
<span class="tw-text-sm tw-text-muted" appStopProp>{{ subtitle }}</span>
|
||||
</td>
|
||||
<td bitCell [ngClass]="RowHeightClass" *ngIf="showOwner" class="tw-hidden lg:tw-table-cell">
|
||||
<app-org-badge
|
||||
@@ -76,7 +76,7 @@
|
||||
</td>
|
||||
<td bitCell [ngClass]="RowHeightClass" class="tw-text-right">
|
||||
<button
|
||||
*ngIf="cipher.decryptionFailure"
|
||||
*ngIf="decryptionFailure"
|
||||
[disabled]="disabled || !canManageCollection"
|
||||
[bitMenuTriggerFor]="corruptedCipherOptions"
|
||||
size="small"
|
||||
@@ -89,12 +89,12 @@
|
||||
<button bitMenuItem *ngIf="canDeleteCipher" (click)="deleteCipher()" type="button">
|
||||
<span class="tw-text-danger">
|
||||
<i class="bwi bwi-fw bwi-trash" aria-hidden="true"></i>
|
||||
{{ (cipher.isDeleted ? "permanentlyDelete" : "delete") | i18n }}
|
||||
{{ (isDeleted ? "permanentlyDelete" : "delete") | i18n }}
|
||||
</span>
|
||||
</button>
|
||||
</bit-menu>
|
||||
<button
|
||||
*ngIf="!cipher.decryptionFailure"
|
||||
*ngIf="!decryptionFailure"
|
||||
[disabled]="disabled || disableMenu"
|
||||
[bitMenuTriggerFor]="cipherOptions"
|
||||
size="small"
|
||||
@@ -105,11 +105,11 @@
|
||||
></button>
|
||||
<bit-menu #cipherOptions>
|
||||
<ng-container *ngIf="isNotDeletedLoginCipher">
|
||||
<button bitMenuItem type="button" (click)="copy('username')" *ngIf="cipher.login.username">
|
||||
<button bitMenuItem type="button" (click)="copy('username')" *ngIf="hasUsernameToCopy">
|
||||
<i class="bwi bwi-fw bwi-clone" aria-hidden="true"></i>
|
||||
{{ "copyUsername" | i18n }}
|
||||
</button>
|
||||
<button bitMenuItem type="button" (click)="copy('password')" *ngIf="cipher.login.password">
|
||||
<button bitMenuItem type="button" (click)="copy('password')" *ngIf="hasPasswordToCopy">
|
||||
<i class="bwi bwi-fw bwi-clone" aria-hidden="true"></i>
|
||||
{{ "copyPassword" | i18n }}
|
||||
</button>
|
||||
@@ -119,9 +119,9 @@
|
||||
</button>
|
||||
<a
|
||||
bitMenuItem
|
||||
*ngIf="cipher.login.canLaunch"
|
||||
*ngIf="canLaunch"
|
||||
type="button"
|
||||
[href]="cipher.login.launchUri"
|
||||
[href]="launchUri"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
@@ -151,19 +151,14 @@
|
||||
<i class="bwi bwi-fw bwi-file-text" aria-hidden="true"></i>
|
||||
{{ "eventLogs" | i18n }}
|
||||
</button>
|
||||
<button
|
||||
bitMenuItem
|
||||
(click)="restore()"
|
||||
type="button"
|
||||
*ngIf="cipher.isDeleted && canRestoreCipher"
|
||||
>
|
||||
<button bitMenuItem (click)="restore()" type="button" *ngIf="isDeleted && canRestoreCipher">
|
||||
<i class="bwi bwi-fw bwi-undo" aria-hidden="true"></i>
|
||||
{{ "restore" | i18n }}
|
||||
</button>
|
||||
<button bitMenuItem *ngIf="canDeleteCipher" (click)="deleteCipher()" type="button">
|
||||
<span class="tw-text-danger">
|
||||
<i class="bwi bwi-fw bwi-trash" aria-hidden="true"></i>
|
||||
{{ (cipher.isDeleted ? "permanentlyDelete" : "delete") | i18n }}
|
||||
{{ (isDeleted ? "permanentlyDelete" : "delete") | i18n }}
|
||||
</span>
|
||||
</button>
|
||||
</bit-menu>
|
||||
|
||||
@@ -6,7 +6,10 @@ import { CollectionView } from "@bitwarden/admin-console/common";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import {
|
||||
CipherViewLike,
|
||||
CipherViewLikeUtils,
|
||||
} from "@bitwarden/common/vault/utils/cipher-view-like-utils";
|
||||
|
||||
import {
|
||||
convertToPermission,
|
||||
@@ -20,11 +23,11 @@ import { RowHeightClass } from "./vault-items.component";
|
||||
templateUrl: "vault-cipher-row.component.html",
|
||||
standalone: false,
|
||||
})
|
||||
export class VaultCipherRowComponent implements OnInit {
|
||||
export class VaultCipherRowComponent<C extends CipherViewLike> implements OnInit {
|
||||
protected RowHeightClass = RowHeightClass;
|
||||
|
||||
@Input() disabled: boolean;
|
||||
@Input() cipher: CipherView;
|
||||
@Input() cipher: C;
|
||||
@Input() showOwner: boolean;
|
||||
@Input() showCollections: boolean;
|
||||
@Input() showGroups: boolean;
|
||||
@@ -46,7 +49,7 @@ export class VaultCipherRowComponent implements OnInit {
|
||||
*/
|
||||
@Input() canRestoreCipher: boolean;
|
||||
|
||||
@Output() onEvent = new EventEmitter<VaultItemEvent>();
|
||||
@Output() onEvent = new EventEmitter<VaultItemEvent<C>>();
|
||||
|
||||
@Input() checked: boolean;
|
||||
@Output() checkedToggled = new EventEmitter<void>();
|
||||
@@ -74,33 +77,63 @@ export class VaultCipherRowComponent implements OnInit {
|
||||
}
|
||||
|
||||
protected get clickAction() {
|
||||
if (this.cipher.decryptionFailure) {
|
||||
if (this.decryptionFailure) {
|
||||
return "showFailedToDecrypt";
|
||||
}
|
||||
|
||||
return "view";
|
||||
}
|
||||
|
||||
protected get showTotpCopyButton() {
|
||||
return (
|
||||
(this.cipher.login?.hasTotp ?? false) &&
|
||||
(this.cipher.organizationUseTotp || this.showPremiumFeatures)
|
||||
);
|
||||
const login = CipherViewLikeUtils.getLogin(this.cipher);
|
||||
|
||||
const hasTotp = login?.totp ?? false;
|
||||
|
||||
return hasTotp && (this.cipher.organizationUseTotp || this.showPremiumFeatures);
|
||||
}
|
||||
|
||||
protected get showFixOldAttachments() {
|
||||
return this.cipher.hasOldAttachments && this.cipher.organizationId == null;
|
||||
}
|
||||
|
||||
protected get hasAttachments() {
|
||||
return CipherViewLikeUtils.hasAttachments(this.cipher);
|
||||
}
|
||||
|
||||
protected get showAttachments() {
|
||||
return this.canEditCipher || this.cipher.attachments?.length > 0;
|
||||
return this.canEditCipher || this.hasAttachments;
|
||||
}
|
||||
|
||||
protected get canLaunch() {
|
||||
return CipherViewLikeUtils.canLaunch(this.cipher);
|
||||
}
|
||||
|
||||
protected get launchUri() {
|
||||
return CipherViewLikeUtils.getLaunchUri(this.cipher);
|
||||
}
|
||||
|
||||
protected get subtitle() {
|
||||
return CipherViewLikeUtils.subtitle(this.cipher);
|
||||
}
|
||||
|
||||
protected get isDeleted() {
|
||||
return CipherViewLikeUtils.isDeleted(this.cipher);
|
||||
}
|
||||
|
||||
protected get decryptionFailure() {
|
||||
return CipherViewLikeUtils.decryptionFailure(this.cipher);
|
||||
}
|
||||
|
||||
protected get showAssignToCollections() {
|
||||
return this.organizations?.length && this.canAssignCollections && !this.cipher.isDeleted;
|
||||
return (
|
||||
this.organizations?.length &&
|
||||
this.canAssignCollections &&
|
||||
!CipherViewLikeUtils.isDeleted(this.cipher)
|
||||
);
|
||||
}
|
||||
|
||||
protected get showClone() {
|
||||
return this.cloneable && !this.cipher.isDeleted;
|
||||
return this.cloneable && !CipherViewLikeUtils.isDeleted(this.cipher);
|
||||
}
|
||||
|
||||
protected get showEventLogs() {
|
||||
@@ -108,7 +141,18 @@ export class VaultCipherRowComponent implements OnInit {
|
||||
}
|
||||
|
||||
protected get isNotDeletedLoginCipher() {
|
||||
return this.cipher.type === this.CipherType.Login && !this.cipher.isDeleted;
|
||||
return (
|
||||
CipherViewLikeUtils.getType(this.cipher) === this.CipherType.Login &&
|
||||
!CipherViewLikeUtils.isDeleted(this.cipher)
|
||||
);
|
||||
}
|
||||
|
||||
protected get hasPasswordToCopy() {
|
||||
return CipherViewLikeUtils.hasCopyableValue(this.cipher, "password");
|
||||
}
|
||||
|
||||
protected get hasUsernameToCopy() {
|
||||
return CipherViewLikeUtils.hasCopyableValue(this.cipher, "username");
|
||||
}
|
||||
|
||||
protected get permissionText() {
|
||||
@@ -154,7 +198,7 @@ export class VaultCipherRowComponent implements OnInit {
|
||||
}
|
||||
|
||||
protected get showLaunchUri(): boolean {
|
||||
return this.isNotDeletedLoginCipher && this.cipher.login.canLaunch;
|
||||
return this.isNotDeletedLoginCipher && this.canLaunch;
|
||||
}
|
||||
|
||||
protected get disableMenu() {
|
||||
@@ -166,7 +210,7 @@ export class VaultCipherRowComponent implements OnInit {
|
||||
this.showAttachments ||
|
||||
this.showClone ||
|
||||
this.canEditCipher ||
|
||||
(this.cipher.isDeleted && this.canRestoreCipher)
|
||||
(CipherViewLikeUtils.isDeleted(this.cipher) && this.canRestoreCipher)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Component, EventEmitter, Input, Output } from "@angular/core";
|
||||
import { CollectionAdminView, Unassigned, CollectionView } from "@bitwarden/admin-console/common";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { CipherViewLike } from "@bitwarden/common/vault/utils/cipher-view-like-utils";
|
||||
|
||||
import { GroupView } from "../../../admin-console/organizations/core";
|
||||
|
||||
@@ -20,7 +21,7 @@ import { RowHeightClass } from "./vault-items.component";
|
||||
templateUrl: "vault-collection-row.component.html",
|
||||
standalone: false,
|
||||
})
|
||||
export class VaultCollectionRowComponent {
|
||||
export class VaultCollectionRowComponent<C extends CipherViewLike> {
|
||||
protected RowHeightClass = RowHeightClass;
|
||||
protected Unassigned = "unassigned";
|
||||
|
||||
@@ -36,7 +37,7 @@ export class VaultCollectionRowComponent {
|
||||
@Input() groups: GroupView[];
|
||||
@Input() showPermissionsColumn: boolean;
|
||||
|
||||
@Output() onEvent = new EventEmitter<VaultItemEvent>();
|
||||
@Output() onEvent = new EventEmitter<VaultItemEvent<C>>();
|
||||
|
||||
@Input() checked: boolean;
|
||||
@Output() checkedToggled = new EventEmitter<void>();
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import { CollectionView } from "@bitwarden/admin-console/common";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { CipherViewLike } from "@bitwarden/common/vault/utils/cipher-view-like-utils";
|
||||
|
||||
import { VaultItem } from "./vault-item";
|
||||
|
||||
export type VaultItemEvent =
|
||||
| { type: "viewAttachments"; item: CipherView }
|
||||
export type VaultItemEvent<C extends CipherViewLike> =
|
||||
| { type: "viewAttachments"; item: C }
|
||||
| { type: "bulkEditCollectionAccess"; items: CollectionView[] }
|
||||
| { type: "viewCollectionAccess"; item: CollectionView; readonly: boolean }
|
||||
| { type: "viewEvents"; item: CipherView }
|
||||
| { type: "viewEvents"; item: C }
|
||||
| { type: "editCollection"; item: CollectionView; readonly: boolean }
|
||||
| { type: "clone"; item: CipherView }
|
||||
| { type: "restore"; items: CipherView[] }
|
||||
| { type: "delete"; items: VaultItem[] }
|
||||
| { type: "copyField"; item: CipherView; field: "username" | "password" | "totp" }
|
||||
| { type: "moveToFolder"; items: CipherView[] }
|
||||
| { type: "assignToCollections"; items: CipherView[] };
|
||||
| { type: "clone"; item: C }
|
||||
| { type: "restore"; items: C[] }
|
||||
| { type: "delete"; items: VaultItem<C>[] }
|
||||
| { type: "copyField"; item: C; field: "username" | "password" | "totp" }
|
||||
| { type: "moveToFolder"; items: C[] }
|
||||
| { type: "assignToCollections"; items: C[] };
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { CollectionView } from "@bitwarden/admin-console/common";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { CipherViewLike } from "@bitwarden/common/vault/utils/cipher-view-like-utils";
|
||||
|
||||
export interface VaultItem {
|
||||
export interface VaultItem<C extends CipherViewLike> {
|
||||
collection?: CollectionView;
|
||||
cipher?: CipherView;
|
||||
cipher?: C;
|
||||
}
|
||||
|
||||
@@ -6,8 +6,11 @@ import { Observable, combineLatest, map, of, startWith, switchMap } from "rxjs";
|
||||
|
||||
import { CollectionView, Unassigned, CollectionAdminView } from "@bitwarden/admin-console/common";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
|
||||
import {
|
||||
CipherViewLike,
|
||||
CipherViewLikeUtils,
|
||||
} from "@bitwarden/common/vault/utils/cipher-view-like-utils";
|
||||
import { SortDirection, TableDataSource } from "@bitwarden/components";
|
||||
|
||||
import { GroupView } from "../../../admin-console/organizations/core";
|
||||
@@ -32,7 +35,7 @@ type ItemPermission = CollectionPermission | "NoAccess";
|
||||
templateUrl: "vault-items.component.html",
|
||||
standalone: false,
|
||||
})
|
||||
export class VaultItemsComponent {
|
||||
export class VaultItemsComponent<C extends CipherViewLike> {
|
||||
protected RowHeight = RowHeight;
|
||||
|
||||
@Input() disabled: boolean;
|
||||
@@ -56,11 +59,11 @@ export class VaultItemsComponent {
|
||||
@Input() addAccessToggle: boolean;
|
||||
@Input() activeCollection: CollectionView | undefined;
|
||||
|
||||
private _ciphers?: CipherView[] = [];
|
||||
@Input() get ciphers(): CipherView[] {
|
||||
private _ciphers?: C[] = [];
|
||||
@Input() get ciphers(): C[] {
|
||||
return this._ciphers;
|
||||
}
|
||||
set ciphers(value: CipherView[] | undefined) {
|
||||
set ciphers(value: C[] | undefined) {
|
||||
this._ciphers = value ?? [];
|
||||
this.refreshItems();
|
||||
}
|
||||
@@ -74,11 +77,11 @@ export class VaultItemsComponent {
|
||||
this.refreshItems();
|
||||
}
|
||||
|
||||
@Output() onEvent = new EventEmitter<VaultItemEvent>();
|
||||
@Output() onEvent = new EventEmitter<VaultItemEvent<C>>();
|
||||
|
||||
protected editableItems: VaultItem[] = [];
|
||||
protected dataSource = new TableDataSource<VaultItem>();
|
||||
protected selection = new SelectionModel<VaultItem>(true, [], true);
|
||||
protected editableItems: VaultItem<C>[] = [];
|
||||
protected dataSource = new TableDataSource<VaultItem<C>>();
|
||||
protected selection = new SelectionModel<VaultItem<C>>(true, [], true);
|
||||
protected canDeleteSelected$: Observable<boolean>;
|
||||
protected canRestoreSelected$: Observable<boolean>;
|
||||
protected disableMenu$: Observable<boolean>;
|
||||
@@ -233,7 +236,7 @@ export class VaultItemsComponent {
|
||||
: this.selection.select(...this.editableItems.slice(0, MaxSelectionCount));
|
||||
}
|
||||
|
||||
protected event(event: VaultItemEvent) {
|
||||
protected event(event: VaultItemEvent<C>) {
|
||||
this.onEvent.emit(event);
|
||||
}
|
||||
|
||||
@@ -263,7 +266,7 @@ export class VaultItemsComponent {
|
||||
}
|
||||
|
||||
// TODO: PM-13944 Refactor to use cipherAuthorizationService.canClone$ instead
|
||||
protected canClone(vaultItem: VaultItem) {
|
||||
protected canClone(vaultItem: VaultItem<C>) {
|
||||
if (vaultItem.cipher.organizationId == null) {
|
||||
return true;
|
||||
}
|
||||
@@ -287,7 +290,7 @@ export class VaultItemsComponent {
|
||||
return false;
|
||||
}
|
||||
|
||||
protected canEditCipher(cipher: CipherView) {
|
||||
protected canEditCipher(cipher: C) {
|
||||
if (cipher.organizationId == null) {
|
||||
return true;
|
||||
}
|
||||
@@ -296,17 +299,17 @@ export class VaultItemsComponent {
|
||||
return (organization.canEditAllCiphers && this.viewingOrgVault) || cipher.edit;
|
||||
}
|
||||
|
||||
protected canAssignCollections(cipher: CipherView) {
|
||||
protected canAssignCollections(cipher: C) {
|
||||
const organization = this.allOrganizations.find((o) => o.id === cipher.organizationId);
|
||||
const editableCollections = this.allCollections.filter((c) => !c.readOnly);
|
||||
|
||||
return (
|
||||
(organization?.canEditAllCiphers && this.viewingOrgVault) ||
|
||||
(cipher.canAssignToCollections && editableCollections.length > 0)
|
||||
(CipherViewLikeUtils.canAssignToCollections(cipher) && editableCollections.length > 0)
|
||||
);
|
||||
}
|
||||
|
||||
protected canManageCollection(cipher: CipherView) {
|
||||
protected canManageCollection(cipher: C) {
|
||||
// If the cipher is not part of an organization (personal item), user can manage it
|
||||
if (cipher.organizationId == null) {
|
||||
return true;
|
||||
@@ -338,9 +341,11 @@ export class VaultItemsComponent {
|
||||
}
|
||||
|
||||
private refreshItems() {
|
||||
const collections: VaultItem[] = this.collections.map((collection) => ({ collection }));
|
||||
const ciphers: VaultItem[] = this.ciphers.map((cipher) => ({ cipher }));
|
||||
const items: VaultItem[] = [].concat(collections).concat(ciphers);
|
||||
const collections: VaultItem<C>[] = this.collections.map((collection) => ({ collection }));
|
||||
const ciphers: VaultItem<C>[] = this.ciphers.map((cipher) => ({
|
||||
cipher,
|
||||
}));
|
||||
const items: VaultItem<C>[] = [].concat(collections).concat(ciphers);
|
||||
|
||||
// All ciphers are selectable, collections only if they can be edited or deleted
|
||||
this.editableItems = items.filter(
|
||||
@@ -419,7 +424,7 @@ export class VaultItemsComponent {
|
||||
/**
|
||||
* Sorts VaultItems, grouping collections before ciphers, and sorting each group alphabetically by name.
|
||||
*/
|
||||
protected sortByName = (a: VaultItem, b: VaultItem, direction: SortDirection) => {
|
||||
protected sortByName = (a: VaultItem<C>, b: VaultItem<C>, direction: SortDirection) => {
|
||||
// Collections before ciphers
|
||||
const collectionCompare = this.prioritizeCollections(a, b, direction);
|
||||
if (collectionCompare !== 0) {
|
||||
@@ -432,7 +437,7 @@ export class VaultItemsComponent {
|
||||
/**
|
||||
* Sorts VaultItems based on group names
|
||||
*/
|
||||
protected sortByGroups = (a: VaultItem, b: VaultItem, direction: SortDirection) => {
|
||||
protected sortByGroups = (a: VaultItem<C>, b: VaultItem<C>, direction: SortDirection) => {
|
||||
if (
|
||||
!(a.collection instanceof CollectionAdminView) &&
|
||||
!(b.collection instanceof CollectionAdminView)
|
||||
@@ -473,8 +478,8 @@ export class VaultItemsComponent {
|
||||
* Sorts VaultItems based on their permissions, with higher permissions taking precedence.
|
||||
* If permissions are equal, it falls back to sorting by name.
|
||||
*/
|
||||
protected sortByPermissions = (a: VaultItem, b: VaultItem, direction: SortDirection) => {
|
||||
const getPermissionPriority = (item: VaultItem): number => {
|
||||
protected sortByPermissions = (a: VaultItem<C>, b: VaultItem<C>, direction: SortDirection) => {
|
||||
const getPermissionPriority = (item: VaultItem<C>): number => {
|
||||
const permission = item.collection
|
||||
? this.getCollectionPermission(item.collection)
|
||||
: this.getCipherPermission(item.cipher);
|
||||
@@ -508,8 +513,8 @@ export class VaultItemsComponent {
|
||||
return this.compareNames(a, b);
|
||||
};
|
||||
|
||||
private compareNames(a: VaultItem, b: VaultItem): number {
|
||||
const getName = (item: VaultItem) => item.collection?.name || item.cipher?.name;
|
||||
private compareNames(a: VaultItem<C>, b: VaultItem<C>): number {
|
||||
const getName = (item: VaultItem<C>) => item.collection?.name || item.cipher?.name;
|
||||
return getName(a)?.localeCompare(getName(b)) ?? -1;
|
||||
}
|
||||
|
||||
@@ -517,7 +522,11 @@ export class VaultItemsComponent {
|
||||
* Sorts VaultItems by prioritizing collections over ciphers.
|
||||
* Collections are always placed before ciphers, regardless of the sorting direction.
|
||||
*/
|
||||
private prioritizeCollections(a: VaultItem, b: VaultItem, direction: SortDirection): number {
|
||||
private prioritizeCollections(
|
||||
a: VaultItem<C>,
|
||||
b: VaultItem<C>,
|
||||
direction: SortDirection,
|
||||
): number {
|
||||
if (a.collection && !b.collection) {
|
||||
return direction === "asc" ? -1 : 1;
|
||||
}
|
||||
@@ -561,7 +570,7 @@ export class VaultItemsComponent {
|
||||
return "NoAccess";
|
||||
}
|
||||
|
||||
private getCipherPermission(cipher: CipherView): ItemPermission {
|
||||
private getCipherPermission(cipher: C): ItemPermission {
|
||||
if (!cipher.organizationId || cipher.collectionIds.length === 0) {
|
||||
return CollectionPermission.Manage;
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view
|
||||
import { LoginView } from "@bitwarden/common/vault/models/view/login.view";
|
||||
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
|
||||
import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service";
|
||||
import { CipherViewLike } from "@bitwarden/common/vault/utils/cipher-view-like-utils";
|
||||
import { LayoutComponent } from "@bitwarden/components";
|
||||
|
||||
import { GroupView } from "../../../admin-console/organizations/core";
|
||||
@@ -158,7 +159,7 @@ export default {
|
||||
argTypes: { onEvent: { action: "onEvent" } },
|
||||
} as Meta;
|
||||
|
||||
type Story = StoryObj<VaultItemsComponent>;
|
||||
type Story = StoryObj<VaultItemsComponent<CipherViewLike>>;
|
||||
|
||||
export const Individual: Story = {
|
||||
args: {
|
||||
|
||||
@@ -85,7 +85,7 @@ describe("vault filter service", () => {
|
||||
policyService.policyAppliesToUser$
|
||||
.calledWith(PolicyType.SingleOrg, mockUserId)
|
||||
.mockReturnValue(singleOrgPolicy);
|
||||
cipherService.cipherViews$.mockReturnValue(cipherViews);
|
||||
cipherService.cipherListViews$.mockReturnValue(cipherViews);
|
||||
|
||||
vaultFilterService = new VaultFilterService(
|
||||
organizationService,
|
||||
|
||||
@@ -38,6 +38,7 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
||||
import { ServiceUtils } from "@bitwarden/common/vault/service-utils";
|
||||
import { COLLAPSED_GROUPINGS } from "@bitwarden/common/vault/services/key-state/collapsed-groupings.state";
|
||||
import { CipherListView } from "@bitwarden/sdk-internal";
|
||||
|
||||
import {
|
||||
CipherTypeFilter,
|
||||
@@ -85,7 +86,7 @@ export class VaultFilterService implements VaultFilterServiceAbstraction {
|
||||
switchMap((userId) =>
|
||||
combineLatest([
|
||||
this.folderService.folderViews$(userId),
|
||||
this.cipherService.cipherViews$(userId),
|
||||
this.cipherService.cipherListViews$(userId),
|
||||
this._organizationFilter,
|
||||
]),
|
||||
),
|
||||
@@ -280,7 +281,7 @@ export class VaultFilterService implements VaultFilterServiceAbstraction {
|
||||
|
||||
protected async filterFolders(
|
||||
storedFolders: FolderView[],
|
||||
ciphers: CipherView[],
|
||||
ciphers: CipherView[] | CipherListView[],
|
||||
org?: Organization,
|
||||
): Promise<FolderView[]> {
|
||||
// If no org or "My Vault" is selected, show all folders
|
||||
|
||||
@@ -221,7 +221,7 @@ function createCipher(options: Partial<CipherView> = {}) {
|
||||
|
||||
cipher.favorite = options.favorite ?? false;
|
||||
cipher.deletedDate = options.deletedDate;
|
||||
cipher.type = options.type;
|
||||
cipher.type = options.type ?? CipherType.Login;
|
||||
cipher.folderId = options.folderId;
|
||||
cipher.collectionIds = options.collectionIds;
|
||||
cipher.organizationId = options.organizationId;
|
||||
|
||||
@@ -1,40 +1,46 @@
|
||||
import { Unassigned } from "@bitwarden/admin-console/common";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import {
|
||||
CipherViewLike,
|
||||
CipherViewLikeUtils,
|
||||
} from "@bitwarden/common/vault/utils/cipher-view-like-utils";
|
||||
|
||||
import { All, RoutedVaultFilterModel } from "./routed-vault-filter.model";
|
||||
|
||||
export type FilterFunction = (cipher: CipherView) => boolean;
|
||||
export type FilterFunction = (cipher: CipherViewLike) => boolean;
|
||||
|
||||
export function createFilterFunction(filter: RoutedVaultFilterModel): FilterFunction {
|
||||
return (cipher) => {
|
||||
const type = CipherViewLikeUtils.getType(cipher);
|
||||
const isDeleted = CipherViewLikeUtils.isDeleted(cipher);
|
||||
|
||||
if (filter.type === "favorites" && !cipher.favorite) {
|
||||
return false;
|
||||
}
|
||||
if (filter.type === "card" && cipher.type !== CipherType.Card) {
|
||||
if (filter.type === "card" && type !== CipherType.Card) {
|
||||
return false;
|
||||
}
|
||||
if (filter.type === "identity" && cipher.type !== CipherType.Identity) {
|
||||
if (filter.type === "identity" && type !== CipherType.Identity) {
|
||||
return false;
|
||||
}
|
||||
if (filter.type === "login" && cipher.type !== CipherType.Login) {
|
||||
if (filter.type === "login" && type !== CipherType.Login) {
|
||||
return false;
|
||||
}
|
||||
if (filter.type === "note" && cipher.type !== CipherType.SecureNote) {
|
||||
if (filter.type === "note" && type !== CipherType.SecureNote) {
|
||||
return false;
|
||||
}
|
||||
if (filter.type === "sshKey" && cipher.type !== CipherType.SshKey) {
|
||||
if (filter.type === "sshKey" && type !== CipherType.SshKey) {
|
||||
return false;
|
||||
}
|
||||
if (filter.type === "trash" && !cipher.isDeleted) {
|
||||
if (filter.type === "trash" && !isDeleted) {
|
||||
return false;
|
||||
}
|
||||
// Hide trash unless explicitly selected
|
||||
if (filter.type !== "trash" && cipher.isDeleted) {
|
||||
if (filter.type !== "trash" && isDeleted) {
|
||||
return false;
|
||||
}
|
||||
// No folder
|
||||
if (filter.folderId === Unassigned && cipher.folderId !== null) {
|
||||
if (filter.folderId === Unassigned && cipher.folderId != null) {
|
||||
return false;
|
||||
}
|
||||
// Folder
|
||||
|
||||
@@ -24,7 +24,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums/cipher-type";
|
||||
import { VaultMessages } from "@bitwarden/common/vault/enums/vault-messages.enum";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { CipherViewLike } from "@bitwarden/common/vault/utils/cipher-view-like-utils";
|
||||
import { LinkModule } from "@bitwarden/components";
|
||||
|
||||
import { OnboardingModule } from "../../../shared/components/onboarding/onboarding.module";
|
||||
@@ -44,7 +44,7 @@ import { VaultOnboardingService, VaultOnboardingTasks } from "./services/vault-o
|
||||
templateUrl: "vault-onboarding.component.html",
|
||||
})
|
||||
export class VaultOnboardingComponent implements OnInit, OnChanges, OnDestroy {
|
||||
@Input() ciphers: CipherView[];
|
||||
@Input() ciphers: CipherViewLike[];
|
||||
@Input() orgs: Organization[];
|
||||
@Output() onAddCipher = new EventEmitter<CipherType>();
|
||||
|
||||
|
||||
@@ -67,8 +67,13 @@ import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { ServiceUtils } from "@bitwarden/common/vault/service-utils";
|
||||
import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service";
|
||||
import {
|
||||
CipherViewLike,
|
||||
CipherViewLikeUtils,
|
||||
} from "@bitwarden/common/vault/utils/cipher-view-like-utils";
|
||||
import { filterOutNullish } from "@bitwarden/common/vault/utils/observable-utilities";
|
||||
import { DialogRef, DialogService, Icons, ToastService } from "@bitwarden/components";
|
||||
import { CipherListView } from "@bitwarden/sdk-internal";
|
||||
import {
|
||||
AddEditFolderDialogComponent,
|
||||
AddEditFolderDialogResult,
|
||||
@@ -149,7 +154,7 @@ const SearchTextDebounceInterval = 200;
|
||||
DefaultCipherFormConfigService,
|
||||
],
|
||||
})
|
||||
export class VaultComponent implements OnInit, OnDestroy {
|
||||
export class VaultComponent<C extends CipherViewLike> implements OnInit, OnDestroy {
|
||||
@ViewChild("vaultFilter", { static: true }) filterComponent: VaultFilterComponent;
|
||||
|
||||
trashCleanupWarning: string = null;
|
||||
@@ -165,7 +170,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
protected canAccessPremium: boolean;
|
||||
protected allCollections: CollectionView[];
|
||||
protected allOrganizations: Organization[] = [];
|
||||
protected ciphers: CipherView[];
|
||||
protected ciphers: C[];
|
||||
protected collections: CollectionView[];
|
||||
protected isEmpty: boolean;
|
||||
protected selectedCollection: TreeNode<CollectionView> | undefined;
|
||||
@@ -350,11 +355,15 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
|
||||
this.currentSearchText$ = this.route.queryParams.pipe(map((queryParams) => queryParams.search));
|
||||
|
||||
const _ciphers = this.cipherService
|
||||
.cipherListViews$(activeUserId)
|
||||
.pipe(filter((c) => c !== null));
|
||||
|
||||
/**
|
||||
* This observable filters the ciphers based on the active user ID and the restricted item types.
|
||||
*/
|
||||
const allowedCiphers$ = combineLatest([
|
||||
this.cipherService.cipherViews$(activeUserId).pipe(filter((c) => c !== null)),
|
||||
_ciphers,
|
||||
this.restrictedItemTypesService.restricted$,
|
||||
]).pipe(
|
||||
map(([ciphers, restrictedTypes]) =>
|
||||
@@ -374,15 +383,15 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
const allCiphers = [...failedCiphers, ...ciphers];
|
||||
|
||||
if (await this.searchService.isSearchable(activeUserId, searchText)) {
|
||||
return await this.searchService.searchCiphers(
|
||||
return await this.searchService.searchCiphers<C>(
|
||||
activeUserId,
|
||||
searchText,
|
||||
[filterFunction],
|
||||
allCiphers,
|
||||
allCiphers as C[],
|
||||
);
|
||||
}
|
||||
|
||||
return allCiphers.filter(filterFunction);
|
||||
return ciphers.filter(filterFunction) as C[];
|
||||
}),
|
||||
shareReplay({ refCount: true, bufferSize: 1 }),
|
||||
);
|
||||
@@ -566,7 +575,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
this.vaultFilterService.clearOrganizationFilter();
|
||||
}
|
||||
|
||||
async onVaultItemsEvent(event: VaultItemEvent) {
|
||||
async onVaultItemsEvent(event: VaultItemEvent<C>) {
|
||||
this.processingEvent = true;
|
||||
try {
|
||||
switch (event.type) {
|
||||
@@ -654,7 +663,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
* @param cipher
|
||||
* @returns
|
||||
*/
|
||||
async editCipherAttachments(cipher: CipherView) {
|
||||
async editCipherAttachments(cipher: C) {
|
||||
if (cipher?.reprompt !== 0 && !(await this.passwordRepromptService.showPasswordPrompt())) {
|
||||
await this.go({ cipherId: null, itemId: null });
|
||||
return;
|
||||
@@ -761,7 +770,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
await this.openVaultItemDialog("form", cipherFormConfig);
|
||||
}
|
||||
|
||||
async editCipher(cipher: CipherView, cloneMode?: boolean) {
|
||||
async editCipher(cipher: CipherView | CipherListView, cloneMode?: boolean) {
|
||||
return this.editCipherId(cipher?.id, cloneMode);
|
||||
}
|
||||
|
||||
@@ -929,7 +938,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
async bulkAssignToCollections(ciphers: CipherView[]) {
|
||||
async bulkAssignToCollections(ciphers: C[]) {
|
||||
if (!(await this.repromptCipher(ciphers))) {
|
||||
return;
|
||||
}
|
||||
@@ -955,9 +964,28 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
);
|
||||
}
|
||||
|
||||
let ciphersToAssign: CipherView[];
|
||||
|
||||
// Convert `CipherListView` to `CipherView` if necessary
|
||||
if (ciphers.some(CipherViewLikeUtils.isCipherListView)) {
|
||||
const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
ciphersToAssign = await firstValueFrom(
|
||||
this.cipherService
|
||||
.cipherViews$(userId)
|
||||
.pipe(
|
||||
map(
|
||||
(cipherViews) =>
|
||||
cipherViews.filter((c) => ciphers.some((cc) => cc.id === c.id)) as CipherView[],
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
ciphersToAssign = ciphers as CipherView[];
|
||||
}
|
||||
|
||||
const dialog = AssignCollectionsWebComponent.open(this.dialogService, {
|
||||
data: {
|
||||
ciphers,
|
||||
ciphers: ciphersToAssign,
|
||||
organizationId: orgId as OrganizationId,
|
||||
availableCollections,
|
||||
activeCollection: this.activeFilter?.selectedCollectionNode?.node,
|
||||
@@ -970,8 +998,8 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
async cloneCipher(cipher: CipherView) {
|
||||
if (cipher.login?.hasFido2Credentials) {
|
||||
async cloneCipher(cipher: CipherView | CipherListView) {
|
||||
if (CipherViewLikeUtils.hasFido2Credentials(cipher)) {
|
||||
const confirmed = await this.dialogService.openSimpleDialog({
|
||||
title: { key: "passkeyNotCopied" },
|
||||
content: { key: "passkeyNotCopiedAlert" },
|
||||
@@ -986,8 +1014,8 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
await this.editCipher(cipher, true);
|
||||
}
|
||||
|
||||
restore = async (c: CipherView): Promise<boolean> => {
|
||||
if (!c.isDeleted) {
|
||||
restore = async (c: C): Promise<boolean> => {
|
||||
if (!CipherViewLikeUtils.isDeleted(c)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1014,7 +1042,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
};
|
||||
|
||||
async bulkRestore(ciphers: CipherView[]) {
|
||||
async bulkRestore(ciphers: C[]) {
|
||||
if (ciphers.some((c) => !c.edit)) {
|
||||
this.showMissingPermissionsError();
|
||||
return;
|
||||
@@ -1044,8 +1072,8 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
private async handleDeleteEvent(items: VaultItem[]) {
|
||||
const ciphers = items.filter((i) => i.collection === undefined).map((i) => i.cipher);
|
||||
private async handleDeleteEvent(items: VaultItem<C>[]) {
|
||||
const ciphers: C[] = items.filter((i) => i.collection === undefined).map((i) => i.cipher);
|
||||
const collections = items.filter((i) => i.cipher === undefined).map((i) => i.collection);
|
||||
if (ciphers.length === 1 && collections.length === 0) {
|
||||
await this.deleteCipher(ciphers[0]);
|
||||
@@ -1062,7 +1090,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
async deleteCipher(c: CipherView): Promise<boolean> {
|
||||
async deleteCipher(c: C): Promise<boolean> {
|
||||
if (!(await this.repromptCipher([c]))) {
|
||||
return;
|
||||
}
|
||||
@@ -1072,7 +1100,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
return;
|
||||
}
|
||||
|
||||
const permanent = c.isDeleted;
|
||||
const permanent = CipherViewLikeUtils.isDeleted(c);
|
||||
|
||||
const confirmed = await this.dialogService.openSimpleDialog({
|
||||
title: { key: permanent ? "permanentlyDeleteItem" : "deleteItem" },
|
||||
@@ -1099,11 +1127,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
async bulkDelete(
|
||||
ciphers: CipherView[],
|
||||
collections: CollectionView[],
|
||||
organizations: Organization[],
|
||||
) {
|
||||
async bulkDelete(ciphers: C[], collections: CollectionView[], organizations: Organization[]) {
|
||||
if (!(await this.repromptCipher(ciphers))) {
|
||||
return;
|
||||
}
|
||||
@@ -1142,7 +1166,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
async bulkMove(ciphers: CipherView[]) {
|
||||
async bulkMove(ciphers: C[]) {
|
||||
if (!(await this.repromptCipher(ciphers))) {
|
||||
return;
|
||||
}
|
||||
@@ -1167,22 +1191,32 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
async copy(cipher: CipherView, field: "username" | "password" | "totp") {
|
||||
async copy(cipher: C, field: "username" | "password" | "totp") {
|
||||
let aType;
|
||||
let value;
|
||||
let typeI18nKey;
|
||||
|
||||
const login = CipherViewLikeUtils.getLogin(cipher);
|
||||
|
||||
if (!login) {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: null,
|
||||
message: this.i18nService.t("unexpectedError"),
|
||||
});
|
||||
}
|
||||
|
||||
if (field === "username") {
|
||||
aType = "Username";
|
||||
value = cipher.login.username;
|
||||
value = login.username;
|
||||
typeI18nKey = "username";
|
||||
} else if (field === "password") {
|
||||
aType = "Password";
|
||||
value = cipher.login.password;
|
||||
value = await this.getPasswordFromCipherViewLike(cipher);
|
||||
typeI18nKey = "password";
|
||||
} else if (field === "totp") {
|
||||
aType = "TOTP";
|
||||
const totpResponse = await firstValueFrom(this.totpService.getCode$(cipher.login.totp));
|
||||
const totpResponse = await firstValueFrom(this.totpService.getCode$(login.totp));
|
||||
value = totpResponse.code;
|
||||
typeI18nKey = "verificationCodeTotp";
|
||||
} else {
|
||||
@@ -1228,7 +1262,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
: this.cipherService.softDeleteWithServer(id, userId);
|
||||
}
|
||||
|
||||
protected async repromptCipher(ciphers: CipherView[]) {
|
||||
protected async repromptCipher(ciphers: C[]) {
|
||||
const notProtected = !ciphers.find((cipher) => cipher.reprompt !== CipherRepromptType.None);
|
||||
|
||||
return notProtected || (await this.passwordRepromptService.showPasswordPrompt());
|
||||
@@ -1264,6 +1298,21 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
message: this.i18nService.t("missingPermissions"),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the password for a `CipherViewLike` object.
|
||||
* `CipherListView` does not contain the password, the full `CipherView` needs to be fetched.
|
||||
*/
|
||||
private async getPasswordFromCipherViewLike(cipher: C): Promise<string | undefined> {
|
||||
if (!CipherViewLikeUtils.isCipherListView(cipher)) {
|
||||
return Promise.resolve(cipher.login?.password);
|
||||
}
|
||||
|
||||
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
const _cipher = await this.cipherService.get(cipher.id, activeUserId);
|
||||
const cipherView = await this.cipherService.decrypt(_cipher, activeUserId);
|
||||
return cipherView.login?.password;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -864,6 +864,23 @@
|
||||
"copyName": {
|
||||
"message": "Copy name"
|
||||
},
|
||||
"cardNumber": {
|
||||
"message": "card number"
|
||||
},
|
||||
"copyFieldCipherName": {
|
||||
"message": "Copy $FIELD$, $CIPHERNAME$",
|
||||
"description": "Title for a button that copies a field value to the clipboard.",
|
||||
"placeholders": {
|
||||
"field": {
|
||||
"content": "$1",
|
||||
"example": "Username"
|
||||
},
|
||||
"ciphername": {
|
||||
"content": "$2",
|
||||
"example": "Login Item"
|
||||
}
|
||||
}
|
||||
},
|
||||
"me": {
|
||||
"message": "Me"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user