mirror of
https://github.com/bitwarden/browser
synced 2026-02-22 12:24:01 +00:00
Merge branch 'main' into ps/extension-refresh
This commit is contained in:
90
libs/angular/src/directives/copy-click.directive.spec.ts
Normal file
90
libs/angular/src/directives/copy-click.directive.spec.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { Component, ElementRef, ViewChild } from "@angular/core";
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
|
||||
import { CopyClickDirective } from "./copy-click.directive";
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<button appCopyClick="no toast shown" #noToast></button>
|
||||
<button appCopyClick="info toast shown" showToast="info" #infoToast></button>
|
||||
<button appCopyClick="success toast shown" showToast #successToast></button>
|
||||
`,
|
||||
})
|
||||
class TestCopyClickComponent {
|
||||
@ViewChild("noToast") noToastButton: ElementRef<HTMLButtonElement>;
|
||||
@ViewChild("infoToast") infoToastButton: ElementRef<HTMLButtonElement>;
|
||||
@ViewChild("successToast") successToastButton: ElementRef<HTMLButtonElement>;
|
||||
}
|
||||
|
||||
describe("CopyClickDirective", () => {
|
||||
let fixture: ComponentFixture<TestCopyClickComponent>;
|
||||
const copyToClipboard = jest.fn();
|
||||
const showToast = jest.fn();
|
||||
|
||||
beforeEach(async () => {
|
||||
copyToClipboard.mockClear();
|
||||
showToast.mockClear();
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [CopyClickDirective, TestCopyClickComponent],
|
||||
providers: [
|
||||
{ provide: I18nService, useValue: { t: (key: string) => key } },
|
||||
{ provide: PlatformUtilsService, useValue: { copyToClipboard } },
|
||||
{ provide: ToastService, useValue: { showToast } },
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(TestCopyClickComponent);
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it("copies the the value for all variants of toasts ", () => {
|
||||
const noToastButton = fixture.componentInstance.noToastButton.nativeElement;
|
||||
|
||||
noToastButton.click();
|
||||
expect(copyToClipboard).toHaveBeenCalledWith("no toast shown");
|
||||
|
||||
const infoToastButton = fixture.componentInstance.infoToastButton.nativeElement;
|
||||
|
||||
infoToastButton.click();
|
||||
expect(copyToClipboard).toHaveBeenCalledWith("info toast shown");
|
||||
|
||||
const successToastButton = fixture.componentInstance.successToastButton.nativeElement;
|
||||
|
||||
successToastButton.click();
|
||||
expect(copyToClipboard).toHaveBeenCalledWith("success toast shown");
|
||||
});
|
||||
|
||||
it("does not show a toast when showToast is not present", () => {
|
||||
const noToastButton = fixture.componentInstance.noToastButton.nativeElement;
|
||||
|
||||
noToastButton.click();
|
||||
expect(showToast).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("shows a success toast when showToast is present", () => {
|
||||
const successToastButton = fixture.componentInstance.successToastButton.nativeElement;
|
||||
|
||||
successToastButton.click();
|
||||
expect(showToast).toHaveBeenCalledWith({
|
||||
message: "copySuccessful",
|
||||
title: null,
|
||||
variant: "success",
|
||||
});
|
||||
});
|
||||
|
||||
it("shows the toast variant when set with showToast", () => {
|
||||
const infoToastButton = fixture.componentInstance.infoToastButton.nativeElement;
|
||||
|
||||
infoToastButton.click();
|
||||
expect(showToast).toHaveBeenCalledWith({
|
||||
message: "copySuccessful",
|
||||
title: null,
|
||||
variant: "info",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,14 +1,17 @@
|
||||
import { coerceBooleanProperty } from "@angular/cdk/coercion";
|
||||
import { Directive, HostListener, Input } from "@angular/core";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
import { ToastVariant } from "@bitwarden/components/src/toast/toast.component";
|
||||
|
||||
@Directive({
|
||||
selector: "[appCopyClick]",
|
||||
})
|
||||
export class CopyClickDirective {
|
||||
private _showToast = false;
|
||||
private toastVariant: ToastVariant = "success";
|
||||
|
||||
constructor(
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private toastService: ToastService,
|
||||
@@ -16,14 +19,36 @@ export class CopyClickDirective {
|
||||
) {}
|
||||
|
||||
@Input("appCopyClick") valueToCopy = "";
|
||||
@Input({ transform: coerceBooleanProperty }) showToast?: boolean;
|
||||
|
||||
/**
|
||||
* When set without a value, a success toast will be shown when the value is copied
|
||||
* @example
|
||||
* ```html
|
||||
* <app-component [appCopyClick]="value to copy" showToast/></app-component>
|
||||
* ```
|
||||
* When set with a value, a toast with the specified variant will be shown when the value is copied
|
||||
*
|
||||
* @example
|
||||
* ```html
|
||||
* <app-component [appCopyClick]="value to copy" showToast="info"/></app-component>
|
||||
* ```
|
||||
*/
|
||||
@Input() set showToast(value: ToastVariant | "") {
|
||||
// When the `showToast` is set without a value, an empty string will be passed
|
||||
if (value === "") {
|
||||
this._showToast = true;
|
||||
} else {
|
||||
this._showToast = true;
|
||||
this.toastVariant = value;
|
||||
}
|
||||
}
|
||||
|
||||
@HostListener("click") onClick() {
|
||||
this.platformUtilsService.copyToClipboard(this.valueToCopy);
|
||||
|
||||
if (this.showToast) {
|
||||
if (this._showToast) {
|
||||
this.toastService.showToast({
|
||||
variant: "info",
|
||||
variant: this.toastVariant,
|
||||
title: null,
|
||||
message: this.i18nService.t("copySuccessful"),
|
||||
});
|
||||
|
||||
@@ -85,3 +85,17 @@ export const DisablePasswordManagerUris = {
|
||||
Vivaldi: "vivaldi://settings/autofill",
|
||||
Unknown: "https://bitwarden.com/help/disable-browser-autofill/",
|
||||
} as const;
|
||||
|
||||
export const ExtensionCommand = {
|
||||
AutofillCommand: "autofill_cmd",
|
||||
AutofillCard: "autofill_card",
|
||||
AutofillIdentity: "autofill_identity",
|
||||
AutofillLogin: "autofill_login",
|
||||
OpenAutofillOverlay: "open_autofill_overlay",
|
||||
GeneratePassword: "generate_password",
|
||||
OpenPopup: "open_popup",
|
||||
LockVault: "lock_vault",
|
||||
NoopCommand: "noop",
|
||||
} as const;
|
||||
|
||||
export type ExtensionCommandType = (typeof ExtensionCommand)[keyof typeof ExtensionCommand];
|
||||
|
||||
@@ -162,4 +162,6 @@ export abstract class CipherService implements UserKeyRotationDataProvider<Ciphe
|
||||
newUserKey: UserKey,
|
||||
userId: UserId,
|
||||
) => Promise<CipherWithIdRequest[]>;
|
||||
getNextCardCipher: () => Promise<CipherView>;
|
||||
getNextIdentityCipher: () => Promise<CipherView>;
|
||||
}
|
||||
|
||||
@@ -500,6 +500,13 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
});
|
||||
}
|
||||
|
||||
private async getAllDecryptedCiphersOfType(type: CipherType[]): Promise<CipherView[]> {
|
||||
const ciphers = await this.getAllDecrypted();
|
||||
return ciphers
|
||||
.filter((cipher) => cipher.deletedDate == null && type.includes(cipher.type))
|
||||
.sort((a, b) => this.sortCiphersByLastUsedThenName(a, b));
|
||||
}
|
||||
|
||||
async getAllFromApiForOrganization(organizationId: string): Promise<CipherView[]> {
|
||||
const response = await this.apiService.getCiphersOrganization(organizationId);
|
||||
return await this.decryptOrganizationCiphersResponse(response, organizationId);
|
||||
@@ -549,6 +556,36 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
return this.getCipherForUrl(url, false, false, false);
|
||||
}
|
||||
|
||||
async getNextCardCipher(): Promise<CipherView> {
|
||||
const cacheKey = "cardCiphers";
|
||||
|
||||
if (!this.sortedCiphersCache.isCached(cacheKey)) {
|
||||
const ciphers = await this.getAllDecryptedCiphersOfType([CipherType.Card]);
|
||||
if (!ciphers?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
this.sortedCiphersCache.addCiphers(cacheKey, ciphers);
|
||||
}
|
||||
|
||||
return this.sortedCiphersCache.getNext(cacheKey);
|
||||
}
|
||||
|
||||
async getNextIdentityCipher() {
|
||||
const cacheKey = "identityCiphers";
|
||||
|
||||
if (!this.sortedCiphersCache.isCached(cacheKey)) {
|
||||
const ciphers = await this.getAllDecryptedCiphersOfType([CipherType.Identity]);
|
||||
if (!ciphers?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
this.sortedCiphersCache.addCiphers(cacheKey, ciphers);
|
||||
}
|
||||
|
||||
return this.sortedCiphersCache.getNext(cacheKey);
|
||||
}
|
||||
|
||||
updateLastUsedIndexForUrl(url: string) {
|
||||
this.sortedCiphersCache.updateLastUsedIndex(url);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user