mirror of
https://github.com/bitwarden/browser.git
synced 2024-12-22 16:29:09 +01:00
[PM-8456] [PM-7683] Dynamic list items - 3 dot menu (#9422)
* [PM-7683] Add fullAddressForCopy helper to identity.view * [PM-7683] Introduce CopyCipherFieldService to the Vault library - A new CopyCipherFieldService that can be used to copy a cipher's field to the user clipboard - A new appCopyField directive to make it easy to copy a cipher's fields in templates - Tests for the CopyCipherFieldService * [PM-7683] Introduce item-copy-actions.component * [PM-7683] Fix username value in copy cipher directive * [PM-7683] Add title to View item link * [PM-8456] Introduce initial item-more-options.component * [PM-8456] Add logic to show/hide login menu options * [PM-8456] Implement favorite/unfavorite menu option * [PM-8456] Implement clone menu option * [PM-8456] Hide launch website instead of disabling it * [PM-8456] Ensure cipherList observable updates on cipher changes * [PM-7683] Move disabled logic into own method * [PM-8456] Cleanup spec file to use Angular testbed * [PM-8456] Fix more options tooltip
This commit is contained in:
parent
d1a9d6f613
commit
6eb94078f6
@ -389,6 +389,9 @@
|
|||||||
"favorite": {
|
"favorite": {
|
||||||
"message": "Favorite"
|
"message": "Favorite"
|
||||||
},
|
},
|
||||||
|
"unfavorite": {
|
||||||
|
"message": "Unfavorite"
|
||||||
|
},
|
||||||
"notes": {
|
"notes": {
|
||||||
"message": "Notes"
|
"message": "Notes"
|
||||||
},
|
},
|
||||||
@ -410,6 +413,9 @@
|
|||||||
"launch": {
|
"launch": {
|
||||||
"message": "Launch"
|
"message": "Launch"
|
||||||
},
|
},
|
||||||
|
"launchWebsite": {
|
||||||
|
"message": "Launch website"
|
||||||
|
},
|
||||||
"website": {
|
"website": {
|
||||||
"message": "Website"
|
"message": "Website"
|
||||||
},
|
},
|
||||||
@ -822,7 +828,7 @@
|
|||||||
},
|
},
|
||||||
"exportPasswordDescription": {
|
"exportPasswordDescription": {
|
||||||
"message": "This password will be used to export and import this file"
|
"message": "This password will be used to export and import this file"
|
||||||
},
|
},
|
||||||
"accountRestrictedOptionDescription": {
|
"accountRestrictedOptionDescription": {
|
||||||
"message": "Use your account encryption key, derived from your account's username and Master Password, to encrypt the export and restrict import to only the current Bitwarden account."
|
"message": "Use your account encryption key, derived from your account's username and Master Password, to encrypt the export and restrict import to only the current Bitwarden account."
|
||||||
},
|
},
|
||||||
@ -1660,6 +1666,9 @@
|
|||||||
"autoFillAndSave": {
|
"autoFillAndSave": {
|
||||||
"message": "Auto-fill and save"
|
"message": "Auto-fill and save"
|
||||||
},
|
},
|
||||||
|
"fillAndSave": {
|
||||||
|
"message": "Fill and save"
|
||||||
|
},
|
||||||
"autoFillSuccessAndSavedUri": {
|
"autoFillSuccessAndSavedUri": {
|
||||||
"message": "Item auto-filled and URI saved"
|
"message": "Item auto-filled and URI saved"
|
||||||
},
|
},
|
||||||
@ -3351,6 +3360,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"assignCollections": {
|
||||||
|
"message": "Assign collections"
|
||||||
|
},
|
||||||
"copyEmail": {
|
"copyEmail": {
|
||||||
"message": "Copy email"
|
"message": "Copy email"
|
||||||
},
|
},
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
[title]="'autofillSuggestions' | i18n"
|
[title]="'autofillSuggestions' | i18n"
|
||||||
[showRefresh]="showRefresh"
|
[showRefresh]="showRefresh"
|
||||||
(onRefresh)="refreshCurrentTab()"
|
(onRefresh)="refreshCurrentTab()"
|
||||||
showAutoFill
|
showAutofillButton
|
||||||
></app-vault-list-items-container>
|
></app-vault-list-items-container>
|
||||||
<ng-container *ngIf="showEmptyAutofillTip$ | async">
|
<ng-container *ngIf="showEmptyAutofillTip$ | async">
|
||||||
<bit-section>
|
<bit-section>
|
||||||
|
@ -0,0 +1,36 @@
|
|||||||
|
<bit-item-action>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
bitIconButton="bwi-ellipsis-v"
|
||||||
|
size="small"
|
||||||
|
[attr.aria-label]="'moreOptionsLabel' | i18n: cipher.name"
|
||||||
|
[title]="'moreOptionsTitle' | i18n: cipher.name"
|
||||||
|
[bitMenuTriggerFor]="moreOptions"
|
||||||
|
></button>
|
||||||
|
<bit-menu #moreOptions>
|
||||||
|
<ng-container *ngIf="isLogin && !hideLoginOptions">
|
||||||
|
<ng-container *ngIf="autofillAllowed$ | async">
|
||||||
|
<button type="button" bitMenuItem>
|
||||||
|
{{ "autofill" | i18n }}
|
||||||
|
</button>
|
||||||
|
<button type="button" bitMenuItem *ngIf="canEdit">
|
||||||
|
{{ "fillAndSave" | i18n }}
|
||||||
|
</button>
|
||||||
|
</ng-container>
|
||||||
|
<button type="button" bitMenuItem *ngIf="this.canLaunch" (click)="launchCipher()">
|
||||||
|
{{ "launchWebsite" | i18n }}
|
||||||
|
</button>
|
||||||
|
</ng-container>
|
||||||
|
<button type="button" bitMenuItem (click)="toggleFavorite()">
|
||||||
|
{{ favoriteText | i18n }}
|
||||||
|
</button>
|
||||||
|
<ng-container *ngIf="canEdit">
|
||||||
|
<a routerLink="" bitMenuItem (click)="clone()">
|
||||||
|
{{ "clone" | i18n }}
|
||||||
|
</a>
|
||||||
|
<button type="button" bitMenuItem>
|
||||||
|
{{ "assignCollections" | i18n }}
|
||||||
|
</button>
|
||||||
|
</ng-container>
|
||||||
|
</bit-menu>
|
||||||
|
</bit-item-action>
|
@ -0,0 +1,122 @@
|
|||||||
|
import { CommonModule } from "@angular/common";
|
||||||
|
import { booleanAttribute, Component, Input } from "@angular/core";
|
||||||
|
import { Router, RouterModule } from "@angular/router";
|
||||||
|
|
||||||
|
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||||
|
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 { DialogService, IconButtonModule, ItemModule, MenuModule } from "@bitwarden/components";
|
||||||
|
import { PasswordRepromptService } from "@bitwarden/vault";
|
||||||
|
|
||||||
|
import { BrowserApi } from "../../../../../platform/browser/browser-api";
|
||||||
|
import BrowserPopupUtils from "../../../../../platform/popup/browser-popup-utils";
|
||||||
|
import { VaultPopupItemsService } from "../../../services/vault-popup-items.service";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
standalone: true,
|
||||||
|
selector: "app-item-more-options",
|
||||||
|
templateUrl: "./item-more-options.component.html",
|
||||||
|
imports: [ItemModule, IconButtonModule, MenuModule, CommonModule, JslibModule, RouterModule],
|
||||||
|
})
|
||||||
|
export class ItemMoreOptionsComponent {
|
||||||
|
@Input({
|
||||||
|
required: true,
|
||||||
|
})
|
||||||
|
cipher: CipherView;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flag to hide the login specific menu options. Used for login items that are
|
||||||
|
* already in the autofill list suggestion.
|
||||||
|
*/
|
||||||
|
@Input({ transform: booleanAttribute })
|
||||||
|
hideLoginOptions: boolean;
|
||||||
|
|
||||||
|
protected autofillAllowed$ = this.vaultPopupItemsService.autofillAllowed$;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private cipherService: CipherService,
|
||||||
|
private vaultPopupItemsService: VaultPopupItemsService,
|
||||||
|
private passwordRepromptService: PasswordRepromptService,
|
||||||
|
private dialogService: DialogService,
|
||||||
|
private router: Router,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
get canEdit() {
|
||||||
|
return this.cipher.edit;
|
||||||
|
}
|
||||||
|
|
||||||
|
get isLogin() {
|
||||||
|
return this.cipher.type === CipherType.Login;
|
||||||
|
}
|
||||||
|
|
||||||
|
get favoriteText() {
|
||||||
|
return this.cipher.favorite ? "unfavorite" : "favorite";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if the login cipher can be launched in a new browser tab.
|
||||||
|
*/
|
||||||
|
get canLaunch() {
|
||||||
|
return this.isLogin && this.cipher.login.canLaunch;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Launches the login cipher in a new browser tab.
|
||||||
|
*/
|
||||||
|
async launchCipher() {
|
||||||
|
if (!this.canLaunch) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.cipherService.updateLastLaunchedDate(this.cipher.id);
|
||||||
|
|
||||||
|
await BrowserApi.createNewTab(this.cipher.login.launchUri);
|
||||||
|
|
||||||
|
if (BrowserPopupUtils.inPopup(window)) {
|
||||||
|
BrowserApi.closePopup(window);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggles the favorite status of the cipher and updates it on the server.
|
||||||
|
*/
|
||||||
|
async toggleFavorite() {
|
||||||
|
this.cipher.favorite = !this.cipher.favorite;
|
||||||
|
const encryptedCipher = await this.cipherService.encrypt(this.cipher);
|
||||||
|
await this.cipherService.updateWithServer(encryptedCipher);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Navigate to the clone cipher page with the current cipher as the source.
|
||||||
|
* A password reprompt is attempted if the cipher requires it.
|
||||||
|
* A confirmation dialog is shown if the cipher has FIDO2 credentials.
|
||||||
|
*/
|
||||||
|
async clone() {
|
||||||
|
if (
|
||||||
|
this.cipher.reprompt === CipherRepromptType.Password &&
|
||||||
|
!(await this.passwordRepromptService.showPasswordPrompt())
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.cipher.login?.hasFido2Credentials) {
|
||||||
|
const confirmed = await this.dialogService.openSimpleDialog({
|
||||||
|
title: { key: "passkeyNotCopied" },
|
||||||
|
content: { key: "passkeyNotCopiedAlert" },
|
||||||
|
type: "info",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!confirmed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.router.navigate(["/clone-cipher"], {
|
||||||
|
queryParams: {
|
||||||
|
cloneMode: true,
|
||||||
|
cipherId: this.cipher.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -24,18 +24,14 @@
|
|||||||
<span slot="secondary">{{ cipher.subTitle }}</span>
|
<span slot="secondary">{{ cipher.subTitle }}</span>
|
||||||
</a>
|
</a>
|
||||||
<ng-container slot="end">
|
<ng-container slot="end">
|
||||||
<bit-item-action *ngIf="showAutoFill">
|
<bit-item-action *ngIf="showAutofillButton">
|
||||||
<button type="button" bitBadge variant="primary">{{ "autoFill" | i18n }}</button>
|
<button type="button" bitBadge variant="primary">{{ "autoFill" | i18n }}</button>
|
||||||
</bit-item-action>
|
</bit-item-action>
|
||||||
<app-item-copy-actions [cipher]="cipher"></app-item-copy-actions>
|
<app-item-copy-actions [cipher]="cipher"></app-item-copy-actions>
|
||||||
<bit-item-action>
|
<app-item-more-options
|
||||||
<button
|
[cipher]="cipher"
|
||||||
type="button"
|
[hideLoginOptions]="showAutofillButton"
|
||||||
bitIconButton="bwi-ellipsis-v"
|
></app-item-more-options>
|
||||||
size="small"
|
|
||||||
[attr.aria-label]="'moreOptions' | i18n: cipher.name"
|
|
||||||
></button>
|
|
||||||
</bit-item-action>
|
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</bit-item>
|
</bit-item>
|
||||||
</bit-item-group>
|
</bit-item-group>
|
||||||
|
@ -15,6 +15,7 @@ import {
|
|||||||
|
|
||||||
import { PopupSectionHeaderComponent } from "../../../../../platform/popup/popup-section-header/popup-section-header.component";
|
import { PopupSectionHeaderComponent } from "../../../../../platform/popup/popup-section-header/popup-section-header.component";
|
||||||
import { ItemCopyActionsComponent } from "../item-copy-action/item-copy-actions.component";
|
import { ItemCopyActionsComponent } from "../item-copy-action/item-copy-actions.component";
|
||||||
|
import { ItemMoreOptionsComponent } from "../item-more-options/item-more-options.component";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
imports: [
|
imports: [
|
||||||
@ -29,6 +30,7 @@ import { ItemCopyActionsComponent } from "../item-copy-action/item-copy-actions.
|
|||||||
PopupSectionHeaderComponent,
|
PopupSectionHeaderComponent,
|
||||||
RouterLink,
|
RouterLink,
|
||||||
ItemCopyActionsComponent,
|
ItemCopyActionsComponent,
|
||||||
|
ItemMoreOptionsComponent,
|
||||||
],
|
],
|
||||||
selector: "app-vault-list-items-container",
|
selector: "app-vault-list-items-container",
|
||||||
templateUrl: "vault-list-items-container.component.html",
|
templateUrl: "vault-list-items-container.component.html",
|
||||||
@ -63,5 +65,5 @@ export class VaultListItemsContainerComponent {
|
|||||||
* Option to show the autofill button for each item.
|
* Option to show the autofill button for each item.
|
||||||
*/
|
*/
|
||||||
@Input({ transform: booleanAttribute })
|
@Input({ transform: booleanAttribute })
|
||||||
showAutoFill: boolean;
|
showAutofillButton: boolean;
|
||||||
}
|
}
|
||||||
|
@ -22,10 +22,12 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ng-container *ngIf="!(showEmptyState$ | async)">
|
<ng-container *ngIf="!(showEmptyState$ | async)">
|
||||||
<app-vault-v2-search (searchTextChanged)="handleSearchTextChange($event)">
|
<div class="tw-fixed">
|
||||||
</app-vault-v2-search>
|
<app-vault-v2-search (searchTextChanged)="handleSearchTextChange($event)">
|
||||||
|
</app-vault-v2-search>
|
||||||
|
|
||||||
<app-vault-list-filters></app-vault-list-filters>
|
<app-vault-list-filters></app-vault-list-filters>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
*ngIf="(showNoResultsState$ | async) && !(showDeactivatedOrg$ | async)"
|
*ngIf="(showNoResultsState$ | async) && !(showDeactivatedOrg$ | async)"
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { TestBed } from "@angular/core/testing";
|
||||||
import { mock } from "jest-mock-extended";
|
import { mock } from "jest-mock-extended";
|
||||||
import { BehaviorSubject } from "rxjs";
|
import { BehaviorSubject } from "rxjs";
|
||||||
|
|
||||||
@ -16,6 +17,7 @@ import { VaultPopupItemsService } from "./vault-popup-items.service";
|
|||||||
import { VaultPopupListFiltersService } from "./vault-popup-list-filters.service";
|
import { VaultPopupListFiltersService } from "./vault-popup-list-filters.service";
|
||||||
|
|
||||||
describe("VaultPopupItemsService", () => {
|
describe("VaultPopupItemsService", () => {
|
||||||
|
let testBed: TestBed;
|
||||||
let service: VaultPopupItemsService;
|
let service: VaultPopupItemsService;
|
||||||
let allCiphers: Record<CipherId, CipherView>;
|
let allCiphers: Record<CipherId, CipherView>;
|
||||||
let autoFillCiphers: CipherView[];
|
let autoFillCiphers: CipherView[];
|
||||||
@ -39,11 +41,12 @@ describe("VaultPopupItemsService", () => {
|
|||||||
cipherList[2].favorite = true;
|
cipherList[2].favorite = true;
|
||||||
cipherList[3].favorite = true;
|
cipherList[3].favorite = true;
|
||||||
|
|
||||||
cipherServiceMock.cipherViews$ = new BehaviorSubject(allCiphers).asObservable();
|
cipherServiceMock.getAllDecrypted.mockResolvedValue(cipherList);
|
||||||
|
cipherServiceMock.ciphers$ = new BehaviorSubject(null).asObservable();
|
||||||
searchService.searchCiphers.mockImplementation(async () => cipherList);
|
searchService.searchCiphers.mockImplementation(async () => cipherList);
|
||||||
cipherServiceMock.filterCiphersForUrl.mockImplementation(async () => autoFillCiphers);
|
cipherServiceMock.filterCiphersForUrl.mockImplementation(async () => autoFillCiphers);
|
||||||
vaultSettingsServiceMock.showCardsCurrentTab$ = new BehaviorSubject(false).asObservable();
|
vaultSettingsServiceMock.showCardsCurrentTab$ = new BehaviorSubject(false);
|
||||||
vaultSettingsServiceMock.showIdentitiesCurrentTab$ = new BehaviorSubject(false).asObservable();
|
vaultSettingsServiceMock.showIdentitiesCurrentTab$ = new BehaviorSubject(false);
|
||||||
|
|
||||||
vaultPopupListFiltersServiceMock.filters$ = new BehaviorSubject({
|
vaultPopupListFiltersServiceMock.filters$ = new BehaviorSubject({
|
||||||
organization: null,
|
organization: null,
|
||||||
@ -55,28 +58,26 @@ describe("VaultPopupItemsService", () => {
|
|||||||
vaultPopupListFiltersServiceMock.filterFunction$ = new BehaviorSubject(
|
vaultPopupListFiltersServiceMock.filterFunction$ = new BehaviorSubject(
|
||||||
(ciphers: CipherView[]) => ciphers,
|
(ciphers: CipherView[]) => ciphers,
|
||||||
);
|
);
|
||||||
|
|
||||||
jest.spyOn(BrowserPopupUtils, "inPopout").mockReturnValue(false);
|
jest.spyOn(BrowserPopupUtils, "inPopout").mockReturnValue(false);
|
||||||
jest
|
jest
|
||||||
.spyOn(BrowserApi, "getTabFromCurrentWindow")
|
.spyOn(BrowserApi, "getTabFromCurrentWindow")
|
||||||
.mockResolvedValue({ url: "https://example.com" } as chrome.tabs.Tab);
|
.mockResolvedValue({ url: "https://example.com" } as chrome.tabs.Tab);
|
||||||
service = new VaultPopupItemsService(
|
|
||||||
cipherServiceMock,
|
testBed = TestBed.configureTestingModule({
|
||||||
vaultSettingsServiceMock,
|
providers: [
|
||||||
vaultPopupListFiltersServiceMock,
|
{ provide: CipherService, useValue: cipherServiceMock },
|
||||||
organizationServiceMock,
|
{ provide: VaultSettingsService, useValue: vaultSettingsServiceMock },
|
||||||
searchService,
|
{ provide: SearchService, useValue: searchService },
|
||||||
);
|
{ provide: OrganizationService, useValue: organizationServiceMock },
|
||||||
|
{ provide: VaultPopupListFiltersService, useValue: vaultPopupListFiltersServiceMock },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
service = testBed.inject(VaultPopupItemsService);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should be created", () => {
|
it("should be created", () => {
|
||||||
service = new VaultPopupItemsService(
|
service = testBed.inject(VaultPopupItemsService);
|
||||||
cipherServiceMock,
|
|
||||||
vaultSettingsServiceMock,
|
|
||||||
vaultPopupListFiltersServiceMock,
|
|
||||||
organizationServiceMock,
|
|
||||||
searchService,
|
|
||||||
);
|
|
||||||
expect(service).toBeTruthy();
|
expect(service).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -100,18 +101,10 @@ describe("VaultPopupItemsService", () => {
|
|||||||
it("should filter ciphers for the current tab and types", (done) => {
|
it("should filter ciphers for the current tab and types", (done) => {
|
||||||
const currentTab = { url: "https://example.com" } as chrome.tabs.Tab;
|
const currentTab = { url: "https://example.com" } as chrome.tabs.Tab;
|
||||||
|
|
||||||
vaultSettingsServiceMock.showCardsCurrentTab$ = new BehaviorSubject(true).asObservable();
|
(vaultSettingsServiceMock.showCardsCurrentTab$ as BehaviorSubject<boolean>).next(true);
|
||||||
vaultSettingsServiceMock.showIdentitiesCurrentTab$ = new BehaviorSubject(true).asObservable();
|
(vaultSettingsServiceMock.showIdentitiesCurrentTab$ as BehaviorSubject<boolean>).next(true);
|
||||||
jest.spyOn(BrowserApi, "getTabFromCurrentWindow").mockResolvedValue(currentTab);
|
jest.spyOn(BrowserApi, "getTabFromCurrentWindow").mockResolvedValue(currentTab);
|
||||||
|
|
||||||
service = new VaultPopupItemsService(
|
|
||||||
cipherServiceMock,
|
|
||||||
vaultSettingsServiceMock,
|
|
||||||
vaultPopupListFiltersServiceMock,
|
|
||||||
organizationServiceMock,
|
|
||||||
searchService,
|
|
||||||
);
|
|
||||||
|
|
||||||
service.autoFillCiphers$.subscribe((ciphers) => {
|
service.autoFillCiphers$.subscribe((ciphers) => {
|
||||||
expect(cipherServiceMock.filterCiphersForUrl.mock.calls.length).toBe(1);
|
expect(cipherServiceMock.filterCiphersForUrl.mock.calls.length).toBe(1);
|
||||||
expect(cipherServiceMock.filterCiphersForUrl).toHaveBeenCalledWith(
|
expect(cipherServiceMock.filterCiphersForUrl).toHaveBeenCalledWith(
|
||||||
@ -136,14 +129,6 @@ describe("VaultPopupItemsService", () => {
|
|||||||
Object.values(allCiphers),
|
Object.values(allCiphers),
|
||||||
);
|
);
|
||||||
|
|
||||||
service = new VaultPopupItemsService(
|
|
||||||
cipherServiceMock,
|
|
||||||
vaultSettingsServiceMock,
|
|
||||||
vaultPopupListFiltersServiceMock,
|
|
||||||
organizationServiceMock,
|
|
||||||
searchService,
|
|
||||||
);
|
|
||||||
|
|
||||||
service.autoFillCiphers$.subscribe((ciphers) => {
|
service.autoFillCiphers$.subscribe((ciphers) => {
|
||||||
expect(ciphers.length).toBe(10);
|
expect(ciphers.length).toBe(10);
|
||||||
|
|
||||||
@ -248,14 +233,7 @@ describe("VaultPopupItemsService", () => {
|
|||||||
|
|
||||||
describe("emptyVault$", () => {
|
describe("emptyVault$", () => {
|
||||||
it("should return true if there are no ciphers", (done) => {
|
it("should return true if there are no ciphers", (done) => {
|
||||||
cipherServiceMock.cipherViews$ = new BehaviorSubject({}).asObservable();
|
cipherServiceMock.getAllDecrypted.mockResolvedValue([]);
|
||||||
service = new VaultPopupItemsService(
|
|
||||||
cipherServiceMock,
|
|
||||||
vaultSettingsServiceMock,
|
|
||||||
vaultPopupListFiltersServiceMock,
|
|
||||||
organizationServiceMock,
|
|
||||||
searchService,
|
|
||||||
);
|
|
||||||
service.emptyVault$.subscribe((empty) => {
|
service.emptyVault$.subscribe((empty) => {
|
||||||
expect(empty).toBe(true);
|
expect(empty).toBe(true);
|
||||||
done();
|
done();
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Injectable } from "@angular/core";
|
import { inject, Injectable, NgZone } from "@angular/core";
|
||||||
import {
|
import {
|
||||||
BehaviorSubject,
|
BehaviorSubject,
|
||||||
combineLatest,
|
combineLatest,
|
||||||
@ -15,12 +15,14 @@ import {
|
|||||||
|
|
||||||
import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
||||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||||
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service";
|
import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service";
|
||||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||||
|
|
||||||
import { BrowserApi } from "../../../platform/browser/browser-api";
|
import { BrowserApi } from "../../../platform/browser/browser-api";
|
||||||
|
import { runInsideAngular } from "../../../platform/browser/run-inside-angular.operator";
|
||||||
import BrowserPopupUtils from "../../../platform/popup/browser-popup-utils";
|
import BrowserPopupUtils from "../../../platform/popup/browser-popup-utils";
|
||||||
|
|
||||||
import { MY_VAULT_ID, VaultPopupListFiltersService } from "./vault-popup-list-filters.service";
|
import { MY_VAULT_ID, VaultPopupListFiltersService } from "./vault-popup-list-filters.service";
|
||||||
@ -72,9 +74,11 @@ export class VaultPopupItemsService {
|
|||||||
* Observable that contains the list of all decrypted ciphers.
|
* Observable that contains the list of all decrypted ciphers.
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
private _cipherList$: Observable<CipherView[]> = this.cipherService.cipherViews$.pipe(
|
private _cipherList$: Observable<CipherView[]> = this.cipherService.ciphers$.pipe(
|
||||||
|
runInsideAngular(inject(NgZone)), // Workaround to ensure cipher$ state provider emissions are run inside Angular
|
||||||
|
switchMap(() => Utils.asyncToObservable(() => this.cipherService.getAllDecrypted())),
|
||||||
map((ciphers) => Object.values(ciphers)),
|
map((ciphers) => Object.values(ciphers)),
|
||||||
shareReplay({ refCount: false, bufferSize: 1 }),
|
shareReplay({ refCount: true, bufferSize: 1 }),
|
||||||
);
|
);
|
||||||
|
|
||||||
private _filteredCipherList$: Observable<CipherView[]> = combineLatest([
|
private _filteredCipherList$: Observable<CipherView[]> = combineLatest([
|
||||||
|
@ -13,6 +13,7 @@ import { AddEditCipherInfo } from "../types/add-edit-cipher-info";
|
|||||||
|
|
||||||
export abstract class CipherService {
|
export abstract class CipherService {
|
||||||
cipherViews$: Observable<Record<CipherId, CipherView>>;
|
cipherViews$: Observable<Record<CipherId, CipherView>>;
|
||||||
|
ciphers$: Observable<Record<CipherId, CipherData>>;
|
||||||
/**
|
/**
|
||||||
* An observable monitoring the add/edit cipher info saved to memory.
|
* An observable monitoring the add/edit cipher info saved to memory.
|
||||||
*/
|
*/
|
||||||
|
Loading…
Reference in New Issue
Block a user