mirror of
https://github.com/bitwarden/browser
synced 2026-02-10 21:50:15 +00:00
Merge remote-tracking branch 'origin/main' into playwright
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { KeyGenerationService } from "@bitwarden/common/key-management/crypto";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
@@ -12,7 +12,7 @@ import { KdfConfig, KdfConfigService, KdfType } from "@bitwarden/key-management"
|
||||
import { BitwardenCsvExportType, BitwardenPasswordProtectedFileFormat } from "../types";
|
||||
export class BaseVaultExportService {
|
||||
constructor(
|
||||
protected pinService: PinServiceAbstraction,
|
||||
protected keyGenerationService: KeyGenerationService,
|
||||
protected encryptService: EncryptService,
|
||||
private cryptoFunctionService: CryptoFunctionService,
|
||||
private kdfConfigService: KdfConfigService,
|
||||
@@ -26,7 +26,8 @@ export class BaseVaultExportService {
|
||||
const kdfConfig: KdfConfig = await this.kdfConfigService.getKdfConfig(userId);
|
||||
|
||||
const salt = Utils.fromBufferToB64(await this.cryptoFunctionService.randomBytes(16));
|
||||
const key = await this.pinService.makePinKey(password, salt, kdfConfig);
|
||||
|
||||
const key = await this.keyGenerationService.deriveVaultExportKey(password, salt, kdfConfig);
|
||||
|
||||
const encKeyValidation = await this.encryptService.encryptString(Utils.newGuid(), key);
|
||||
const encText = await this.encryptService.encryptString(clearText, key);
|
||||
|
||||
@@ -3,13 +3,13 @@ import * as JSZip from "jszip";
|
||||
import { BehaviorSubject, of } from "rxjs";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { KeyGenerationService } from "@bitwarden/common/key-management/crypto";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import {
|
||||
EncryptedString,
|
||||
EncString,
|
||||
} from "@bitwarden/common/key-management/crypto/models/enc-string";
|
||||
import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction";
|
||||
import { CipherWithIdExport } from "@bitwarden/common/models/export/cipher-with-ids.export";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { CipherId, emptyGuid, UserId } from "@bitwarden/common/types/guid";
|
||||
@@ -169,7 +169,7 @@ describe("VaultExportService", () => {
|
||||
let exportService: IndividualVaultExportService;
|
||||
let cryptoFunctionService: MockProxy<CryptoFunctionService>;
|
||||
let cipherService: MockProxy<CipherService>;
|
||||
let pinService: MockProxy<PinServiceAbstraction>;
|
||||
let keyGenerationService: MockProxy<KeyGenerationService>;
|
||||
let folderService: MockProxy<FolderService>;
|
||||
let keyService: MockProxy<KeyService>;
|
||||
let encryptService: MockProxy<EncryptService>;
|
||||
@@ -184,7 +184,7 @@ describe("VaultExportService", () => {
|
||||
beforeEach(() => {
|
||||
cryptoFunctionService = mock<CryptoFunctionService>();
|
||||
cipherService = mock<CipherService>();
|
||||
pinService = mock<PinServiceAbstraction>();
|
||||
keyGenerationService = mock<KeyGenerationService>();
|
||||
folderService = mock<FolderService>();
|
||||
keyService = mock<KeyService>();
|
||||
encryptService = mock<EncryptService>();
|
||||
@@ -220,7 +220,7 @@ describe("VaultExportService", () => {
|
||||
exportService = new IndividualVaultExportService(
|
||||
folderService,
|
||||
cipherService,
|
||||
pinService,
|
||||
keyGenerationService,
|
||||
keyService,
|
||||
encryptService,
|
||||
cryptoFunctionService,
|
||||
@@ -525,6 +525,20 @@ describe("VaultExportService", () => {
|
||||
const exportedData = actual as ExportedVaultAsString;
|
||||
expectEqualFolders(UserFolders, exportedData.data);
|
||||
});
|
||||
|
||||
it("does not export the key property in unencrypted exports", async () => {
|
||||
// Create a cipher with a key property
|
||||
const cipherWithKey = generateCipherView(false);
|
||||
(cipherWithKey as any).key = "shouldBeDeleted";
|
||||
cipherService.getAllDecrypted.mockResolvedValue([cipherWithKey]);
|
||||
|
||||
const actual = await exportService.getExport(userId, "json");
|
||||
expect(typeof actual.data).toBe("string");
|
||||
const exportedData = actual as ExportedVaultAsString;
|
||||
const parsed = JSON.parse(exportedData.data);
|
||||
expect(parsed.items.length).toBe(1);
|
||||
expect(parsed.items[0].key).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
export class FolderResponse {
|
||||
|
||||
@@ -5,9 +5,9 @@ import * as papa from "papaparse";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { KeyGenerationService } from "@bitwarden/common/key-management/crypto";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction";
|
||||
import { CipherWithIdExport, FolderWithIdExport } from "@bitwarden/common/models/export";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { CipherId, UserId } from "@bitwarden/common/types/guid";
|
||||
@@ -42,7 +42,7 @@ export class IndividualVaultExportService
|
||||
constructor(
|
||||
private folderService: FolderService,
|
||||
private cipherService: CipherService,
|
||||
pinService: PinServiceAbstraction,
|
||||
keyGenerationService: KeyGenerationService,
|
||||
private keyService: KeyService,
|
||||
encryptService: EncryptService,
|
||||
cryptoFunctionService: CryptoFunctionService,
|
||||
@@ -50,7 +50,7 @@ export class IndividualVaultExportService
|
||||
private apiService: ApiService,
|
||||
private restrictedItemTypesService: RestrictedItemTypesService,
|
||||
) {
|
||||
super(pinService, encryptService, cryptoFunctionService, kdfConfigService);
|
||||
super(keyGenerationService, encryptService, cryptoFunctionService, kdfConfigService);
|
||||
}
|
||||
|
||||
/** Creates an export of an individual vault (My Vault). Based on the provided format it will either be unencrypted, encrypted or password protected and in case zip is selected will include attachments
|
||||
@@ -317,6 +317,7 @@ export class IndividualVaultExportService
|
||||
const cipher = new CipherWithIdExport();
|
||||
cipher.build(c);
|
||||
cipher.collectionIds = null;
|
||||
delete cipher.key;
|
||||
jsonDoc.items.push(cipher);
|
||||
});
|
||||
|
||||
|
||||
@@ -3,16 +3,16 @@
|
||||
import * as papa from "papaparse";
|
||||
import { filter, firstValueFrom, map } from "rxjs";
|
||||
|
||||
import { CollectionService } from "@bitwarden/admin-console/common";
|
||||
import {
|
||||
CollectionService,
|
||||
CollectionData,
|
||||
Collection,
|
||||
CollectionDetailsResponse,
|
||||
CollectionView,
|
||||
} from "@bitwarden/admin-console/common";
|
||||
CollectionDetailsResponse,
|
||||
Collection,
|
||||
CollectionData,
|
||||
} from "@bitwarden/common/admin-console/models/collections";
|
||||
import { KeyGenerationService } from "@bitwarden/common/key-management/crypto";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction";
|
||||
import { CipherWithIdExport, CollectionWithIdExport } from "@bitwarden/common/models/export";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
@@ -46,7 +46,7 @@ export class OrganizationVaultExportService
|
||||
constructor(
|
||||
private cipherService: CipherService,
|
||||
private vaultExportApiService: VaultExportApiService,
|
||||
pinService: PinServiceAbstraction,
|
||||
keyGenerationService: KeyGenerationService,
|
||||
private keyService: KeyService,
|
||||
encryptService: EncryptService,
|
||||
cryptoFunctionService: CryptoFunctionService,
|
||||
@@ -54,7 +54,7 @@ export class OrganizationVaultExportService
|
||||
kdfConfigService: KdfConfigService,
|
||||
private restrictedItemTypesService: RestrictedItemTypesService,
|
||||
) {
|
||||
super(pinService, encryptService, cryptoFunctionService, kdfConfigService);
|
||||
super(keyGenerationService, encryptService, cryptoFunctionService, kdfConfigService);
|
||||
}
|
||||
|
||||
/** Creates a password protected export of an organizational vault.
|
||||
@@ -383,6 +383,7 @@ export class OrganizationVaultExportService
|
||||
decCiphers.forEach((c) => {
|
||||
const cipher = new CipherWithIdExport();
|
||||
cipher.build(c);
|
||||
delete cipher.key;
|
||||
jsonDoc.items.push(cipher);
|
||||
});
|
||||
return JSON.stringify(jsonDoc, null, " ");
|
||||
|
||||
@@ -5,42 +5,48 @@ import {
|
||||
} from "@bitwarden/common/models/export";
|
||||
|
||||
// Base
|
||||
export type BitwardenJsonExport = {
|
||||
encrypted: boolean;
|
||||
items: CipherWithIdExport[];
|
||||
};
|
||||
export type BitwardenJsonExport = BitwardenUnEncryptedJsonExport | BitwardenEncryptedJsonExport;
|
||||
|
||||
// Decrypted
|
||||
export type BitwardenUnEncryptedJsonExport = BitwardenJsonExport & {
|
||||
encrypted: false;
|
||||
};
|
||||
export type BitwardenUnEncryptedJsonExport =
|
||||
| BitwardenUnEncryptedIndividualJsonExport
|
||||
| BitwardenUnEncryptedOrgJsonExport;
|
||||
|
||||
export type BitwardenUnEncryptedIndividualJsonExport = BitwardenUnEncryptedJsonExport & {
|
||||
export type BitwardenUnEncryptedIndividualJsonExport = {
|
||||
encrypted: false;
|
||||
items: CipherWithIdExport[];
|
||||
folders: FolderWithIdExport[];
|
||||
};
|
||||
|
||||
export type BitwardenUnEncryptedOrgJsonExport = BitwardenUnEncryptedJsonExport & {
|
||||
export type BitwardenUnEncryptedOrgJsonExport = {
|
||||
encrypted: false;
|
||||
items: CipherWithIdExport[];
|
||||
collections: CollectionWithIdExport[];
|
||||
};
|
||||
|
||||
// Account-encrypted
|
||||
export type BitwardenEncryptedJsonExport = BitwardenJsonExport & {
|
||||
export type BitwardenEncryptedJsonExport =
|
||||
| BitwardenEncryptedIndividualJsonExport
|
||||
| BitwardenEncryptedOrgJsonExport;
|
||||
|
||||
export type BitwardenEncryptedIndividualJsonExport = {
|
||||
encrypted: true;
|
||||
encKeyValidation_DO_NOT_EDIT: string;
|
||||
};
|
||||
|
||||
export type BitwardenEncryptedIndividualJsonExport = BitwardenEncryptedJsonExport & {
|
||||
items: CipherWithIdExport[];
|
||||
folders: FolderWithIdExport[];
|
||||
};
|
||||
|
||||
export type BitwardenEncryptedOrgJsonExport = BitwardenEncryptedJsonExport & {
|
||||
export type BitwardenEncryptedOrgJsonExport = {
|
||||
encrypted: true;
|
||||
encKeyValidation_DO_NOT_EDIT: string;
|
||||
items: CipherWithIdExport[];
|
||||
collections: CollectionWithIdExport[];
|
||||
};
|
||||
|
||||
// Password-protected
|
||||
export type BitwardenPasswordProtectedFileFormat = {
|
||||
encrypted: boolean;
|
||||
passwordProtected: boolean;
|
||||
encrypted: true;
|
||||
passwordProtected: true;
|
||||
salt: string;
|
||||
kdfIterations: number;
|
||||
kdfMemory?: number;
|
||||
@@ -49,3 +55,50 @@ export type BitwardenPasswordProtectedFileFormat = {
|
||||
encKeyValidation_DO_NOT_EDIT: string;
|
||||
data: string;
|
||||
};
|
||||
|
||||
// Unencrypted type guards
|
||||
export function isUnencrypted(
|
||||
data: BitwardenJsonExport | null | undefined,
|
||||
): data is BitwardenUnEncryptedJsonExport {
|
||||
return data != null && (data as { encrypted?: unknown }).encrypted !== true;
|
||||
}
|
||||
|
||||
export function isIndividualUnEncrypted(
|
||||
data: BitwardenJsonExport | null | undefined,
|
||||
): data is BitwardenUnEncryptedIndividualJsonExport {
|
||||
return isUnencrypted(data) && (data as { folders?: unknown }).folders != null;
|
||||
}
|
||||
|
||||
export function isOrgUnEncrypted(
|
||||
data: BitwardenJsonExport | null | undefined,
|
||||
): data is BitwardenUnEncryptedOrgJsonExport {
|
||||
return isUnencrypted(data) && (data as { collections?: unknown }).collections != null;
|
||||
}
|
||||
|
||||
// Encrypted type guards
|
||||
export function isEncrypted(
|
||||
data: BitwardenJsonExport | null | undefined,
|
||||
): data is BitwardenEncryptedJsonExport {
|
||||
return data != null && (data as { encrypted?: unknown }).encrypted === true;
|
||||
}
|
||||
export function isPasswordProtected(
|
||||
data: BitwardenPasswordProtectedFileFormat | BitwardenJsonExport | null | undefined,
|
||||
): data is BitwardenPasswordProtectedFileFormat {
|
||||
return (
|
||||
data != null &&
|
||||
(data as { encrypted?: unknown }).encrypted === true &&
|
||||
(data as { passwordProtected?: unknown }).passwordProtected === true
|
||||
);
|
||||
}
|
||||
|
||||
export function isIndividualEncrypted(
|
||||
data: BitwardenJsonExport | null | undefined,
|
||||
): data is BitwardenEncryptedIndividualJsonExport {
|
||||
return isEncrypted(data) && (data as { folders?: unknown }).folders != null;
|
||||
}
|
||||
|
||||
export function isOrgEncrypted(
|
||||
data: BitwardenJsonExport | null | undefined,
|
||||
): data is BitwardenEncryptedOrgJsonExport {
|
||||
return isEncrypted(data) && (data as { collections?: unknown }).collections != null;
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@ import {
|
||||
BehaviorSubject,
|
||||
combineLatest,
|
||||
firstValueFrom,
|
||||
from,
|
||||
map,
|
||||
merge,
|
||||
Observable,
|
||||
@@ -43,8 +42,6 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { ClientType, EventType } from "@bitwarden/common/enums";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
@@ -100,7 +97,6 @@ import { ExportScopeCalloutComponent } from "./export-scope-callout.component";
|
||||
})
|
||||
export class ExportComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||
private _organizationId$ = new BehaviorSubject<OrganizationId | undefined>(undefined);
|
||||
private createDefaultLocationFlagEnabled$: Observable<boolean>;
|
||||
private _showExcludeMyItems = false;
|
||||
|
||||
/**
|
||||
@@ -259,13 +255,11 @@ export class ExportComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||
protected organizationService: OrganizationService,
|
||||
private accountService: AccountService,
|
||||
private collectionService: CollectionService,
|
||||
private configService: ConfigService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
@Optional() private router?: Router,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
this.observeFeatureFlags();
|
||||
this.observeFormState();
|
||||
this.observePolicyStatus();
|
||||
this.observeFormSelections();
|
||||
@@ -286,12 +280,6 @@ export class ExportComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||
this.setupPolicyBasedFormState();
|
||||
}
|
||||
|
||||
private observeFeatureFlags(): void {
|
||||
this.createDefaultLocationFlagEnabled$ = from(
|
||||
this.configService.getFeatureFlag(FeatureFlag.CreateDefaultLocation),
|
||||
).pipe(shareReplay({ bufferSize: 1, refCount: true }));
|
||||
}
|
||||
|
||||
private observeFormState(): void {
|
||||
this.exportForm.statusChanges.pipe(takeUntil(this.destroy$)).subscribe((c) => {
|
||||
this.formDisabled.emit(c === "DISABLED");
|
||||
@@ -380,32 +368,24 @@ export class ExportComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||
|
||||
/**
|
||||
* Determine value of showExcludeMyItems. Returns true when:
|
||||
* CreateDefaultLocation feature flag is on
|
||||
* AND organizationDataOwnershipPolicy is enabled for the selected organization
|
||||
* organizationDataOwnershipPolicy is enabled for the selected organization
|
||||
* AND a valid OrganizationId is present (not exporting from individual vault)
|
||||
*/
|
||||
private observeMyItemsExclusionCriteria(): void {
|
||||
combineLatest({
|
||||
createDefaultLocationFlagEnabled: this.createDefaultLocationFlagEnabled$,
|
||||
organizationDataOwnershipPolicyEnabledForOrg:
|
||||
this.organizationDataOwnershipPolicyEnabledForOrg$,
|
||||
organizationId: this._organizationId$,
|
||||
})
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe(
|
||||
({
|
||||
createDefaultLocationFlagEnabled,
|
||||
organizationDataOwnershipPolicyEnabledForOrg,
|
||||
organizationId,
|
||||
}) => {
|
||||
if (!createDefaultLocationFlagEnabled || !organizationId) {
|
||||
this._showExcludeMyItems = false;
|
||||
return;
|
||||
}
|
||||
.subscribe(({ organizationDataOwnershipPolicyEnabledForOrg, organizationId }) => {
|
||||
if (!organizationId) {
|
||||
this._showExcludeMyItems = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this._showExcludeMyItems = organizationDataOwnershipPolicyEnabledForOrg;
|
||||
},
|
||||
);
|
||||
this._showExcludeMyItems = organizationDataOwnershipPolicyEnabledForOrg;
|
||||
});
|
||||
}
|
||||
|
||||
// Setup validator adjustments based on format and encryption type changes
|
||||
@@ -620,7 +600,7 @@ export class ExportComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||
title: "confirmVaultExport",
|
||||
bodyText: confirmDescription,
|
||||
confirmButtonOptions: {
|
||||
text: "exportVault",
|
||||
text: "continue",
|
||||
type: "primary",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -8,15 +8,18 @@ import {
|
||||
Output,
|
||||
SimpleChanges,
|
||||
} from "@angular/core";
|
||||
import { FormBuilder } from "@angular/forms";
|
||||
import { FormBuilder, ReactiveFormsModule } from "@angular/forms";
|
||||
import { map, ReplaySubject, skip, Subject, takeUntil, withLatestFrom } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { Account } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { FormFieldModule } from "@bitwarden/components";
|
||||
import {
|
||||
CatchallGenerationOptions,
|
||||
CredentialGeneratorService,
|
||||
BuiltIn,
|
||||
} from "@bitwarden/generator-core";
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
|
||||
/** Options group for catchall emails */
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
@@ -24,7 +27,7 @@ import {
|
||||
@Component({
|
||||
selector: "tools-catchall-settings",
|
||||
templateUrl: "catchall-settings.component.html",
|
||||
standalone: false,
|
||||
imports: [ReactiveFormsModule, FormFieldModule, JslibModule, I18nPipe],
|
||||
})
|
||||
export class CatchallSettingsComponent implements OnInit, OnDestroy, OnChanges {
|
||||
/** Instantiates the component
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
<bit-dialog #dialog background="alt">
|
||||
<span bitDialogTitle>{{ "generatorHistory" | i18n }}</span>
|
||||
<ng-container bitDialogContent>
|
||||
<bit-empty-credential-history *ngIf="!(hasHistory$ | async)" style="display: contents" />
|
||||
<bit-credential-generator-history [account]="account$ | async" *ngIf="hasHistory$ | async" />
|
||||
@if (hasHistory$ | async) {
|
||||
<bit-credential-generator-history [account]="account$ | async" />
|
||||
} @else {
|
||||
<bit-empty-credential-history style="display: contents" />
|
||||
}
|
||||
</ng-container>
|
||||
<ng-container bitDialogFooter>
|
||||
<button
|
||||
|
||||
@@ -1,22 +1,24 @@
|
||||
<bit-item *ngFor="let credential of credentials$ | async">
|
||||
<bit-item-content>
|
||||
<bit-color-password class="tw-font-mono" [password]="credential.credential" />
|
||||
<div slot="secondary">
|
||||
{{ credential.generationDate | date: "medium" }}
|
||||
</div>
|
||||
</bit-item-content>
|
||||
<ng-container slot="end">
|
||||
<bit-item-action>
|
||||
<button
|
||||
type="button"
|
||||
bitIconButton="bwi-clone"
|
||||
[appCopyClick]="credential.credential"
|
||||
[valueLabel]="getGeneratedValueText(credential)"
|
||||
[label]="getCopyText(credential)"
|
||||
showToast
|
||||
>
|
||||
{{ getCopyText(credential) }}
|
||||
</button>
|
||||
</bit-item-action>
|
||||
</ng-container>
|
||||
</bit-item>
|
||||
@for (credential of credentials$ | async; track credential) {
|
||||
<bit-item>
|
||||
<bit-item-content>
|
||||
<bit-color-password class="tw-font-mono" [password]="credential.credential" />
|
||||
<div slot="secondary">
|
||||
{{ credential.generationDate | date: "medium" }}
|
||||
</div>
|
||||
</bit-item-content>
|
||||
<ng-container slot="end">
|
||||
<bit-item-action>
|
||||
<button
|
||||
type="button"
|
||||
bitIconButton="bwi-clone"
|
||||
[appCopyClick]="credential.credential"
|
||||
[valueLabel]="getGeneratedValueText(credential)"
|
||||
[label]="getCopyText(credential)"
|
||||
showToast
|
||||
>
|
||||
{{ getCopyText(credential) }}
|
||||
</button>
|
||||
</bit-item-action>
|
||||
</ng-container>
|
||||
</bit-item>
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@ import {
|
||||
import { AlgorithmsByType, CredentialGeneratorService } from "@bitwarden/generator-core";
|
||||
import { GeneratedCredential, GeneratorHistoryService } from "@bitwarden/generator-history";
|
||||
|
||||
import { GeneratorModule } from "./generator.module";
|
||||
import { translate } from "./util";
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
@@ -32,13 +31,12 @@ import { translate } from "./util";
|
||||
selector: "bit-credential-generator-history",
|
||||
templateUrl: "credential-generator-history.component.html",
|
||||
imports: [
|
||||
ColorPasswordModule,
|
||||
CommonModule,
|
||||
ColorPasswordModule,
|
||||
IconButtonModule,
|
||||
NoItemsModule,
|
||||
JslibModule,
|
||||
ItemModule,
|
||||
GeneratorModule,
|
||||
],
|
||||
})
|
||||
export class CredentialGeneratorHistoryComponent implements OnChanges, OnInit, OnDestroy {
|
||||
|
||||
@@ -6,9 +6,11 @@
|
||||
(selectedChange)="onRootChanged({ nav: $event })"
|
||||
attr.aria-label="{{ 'type' | i18n }}"
|
||||
>
|
||||
<bit-toggle *ngFor="let option of rootOptions$ | async" [value]="option.value">
|
||||
{{ option.label }}
|
||||
</bit-toggle>
|
||||
@for (option of rootOptions$ | async; track option) {
|
||||
<bit-toggle [value]="option.value">
|
||||
{{ option.label }}
|
||||
</bit-toggle>
|
||||
}
|
||||
</bit-toggle-group>
|
||||
|
||||
<nudge-generator-spotlight></nudge-generator-spotlight>
|
||||
@@ -40,69 +42,80 @@
|
||||
></button>
|
||||
</div>
|
||||
</bit-card>
|
||||
<tools-password-settings
|
||||
class="tw-mt-6"
|
||||
*ngIf="(showAlgorithm$ | async)?.id === Algorithm.password"
|
||||
[account]="account$ | async"
|
||||
(onUpdated)="generate('password settings')"
|
||||
/>
|
||||
<tools-passphrase-settings
|
||||
class="tw-mt-6"
|
||||
*ngIf="(showAlgorithm$ | async)?.id === Algorithm.passphrase"
|
||||
[account]="account$ | async"
|
||||
(onUpdated)="generate('passphrase settings')"
|
||||
/>
|
||||
<bit-section *ngIf="(category$ | async) !== 'password'">
|
||||
<bit-section-header>
|
||||
<h2 bitTypography="h6">{{ "options" | i18n }}</h2>
|
||||
</bit-section-header>
|
||||
<div class="tw-mb-4">
|
||||
<bit-card>
|
||||
<form [formGroup]="username" class="tw-container">
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "type" | i18n }}</bit-label>
|
||||
<bit-select
|
||||
[items]="usernameOptions$ | async"
|
||||
formControlName="nav"
|
||||
data-testid="username-type"
|
||||
>
|
||||
</bit-select>
|
||||
<bit-hint *ngIf="!!(credentialTypeHint$ | async)">{{
|
||||
credentialTypeHint$ | async
|
||||
}}</bit-hint>
|
||||
</bit-form-field>
|
||||
</form>
|
||||
<form *ngIf="showForwarder$ | async" [formGroup]="forwarder" class="tw-container">
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "service" | i18n }}</bit-label>
|
||||
<bit-select
|
||||
[items]="forwarderOptions$ | async"
|
||||
formControlName="nav"
|
||||
data-testid="email-forwarding-service"
|
||||
>
|
||||
</bit-select>
|
||||
</bit-form-field>
|
||||
</form>
|
||||
<tools-catchall-settings
|
||||
*ngIf="(showAlgorithm$ | async)?.id === Algorithm.catchall"
|
||||
[account]="account$ | async"
|
||||
(onUpdated)="generate('catchall settings')"
|
||||
/>
|
||||
<tools-forwarder-settings
|
||||
*ngIf="!!(forwarderId$ | async)"
|
||||
[account]="account$ | async"
|
||||
[forwarder]="forwarderId$ | async"
|
||||
/>
|
||||
<tools-subaddress-settings
|
||||
*ngIf="(showAlgorithm$ | async)?.id === Algorithm.plusAddress"
|
||||
[account]="account$ | async"
|
||||
(onUpdated)="generate('subaddress settings')"
|
||||
/>
|
||||
<tools-username-settings
|
||||
*ngIf="(showAlgorithm$ | async)?.id === Algorithm.username"
|
||||
[account]="account$ | async"
|
||||
(onUpdated)="generate('username settings')"
|
||||
/>
|
||||
</bit-card>
|
||||
</div>
|
||||
</bit-section>
|
||||
@let showAlgorithm = showAlgorithm$ | async;
|
||||
@let account = account$ | async;
|
||||
@switch (showAlgorithm?.id) {
|
||||
@case (Algorithm.password) {
|
||||
<tools-password-settings
|
||||
class="tw-mt-6"
|
||||
[account]="account"
|
||||
(onUpdated)="generate('password settings')"
|
||||
/>
|
||||
}
|
||||
@case (Algorithm.passphrase) {
|
||||
<tools-passphrase-settings
|
||||
class="tw-mt-6"
|
||||
[account]="account"
|
||||
(onUpdated)="generate('passphrase settings')"
|
||||
/>
|
||||
}
|
||||
}
|
||||
@if ((category$ | async) !== "password") {
|
||||
<bit-section>
|
||||
<bit-section-header>
|
||||
<h2 bitTypography="h6">{{ "options" | i18n }}</h2>
|
||||
</bit-section-header>
|
||||
<div class="tw-mb-4">
|
||||
<bit-card>
|
||||
<form [formGroup]="username" class="tw-container">
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "type" | i18n }}</bit-label>
|
||||
<bit-select
|
||||
[items]="usernameOptions$ | async"
|
||||
formControlName="nav"
|
||||
data-testid="username-type"
|
||||
>
|
||||
</bit-select>
|
||||
@if (credentialTypeHint$ | async) {
|
||||
<bit-hint>{{ credentialTypeHint$ | async }}</bit-hint>
|
||||
}
|
||||
</bit-form-field>
|
||||
</form>
|
||||
@if (showForwarder$ | async) {
|
||||
<form [formGroup]="forwarder" class="tw-container">
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "service" | i18n }}</bit-label>
|
||||
<bit-select
|
||||
[items]="forwarderOptions$ | async"
|
||||
formControlName="nav"
|
||||
data-testid="email-forwarding-service"
|
||||
>
|
||||
</bit-select>
|
||||
</bit-form-field>
|
||||
</form>
|
||||
}
|
||||
@if (showAlgorithm?.id === Algorithm.catchall) {
|
||||
<tools-catchall-settings
|
||||
[account]="account"
|
||||
(onUpdated)="generate('catchall settings')"
|
||||
/>
|
||||
}
|
||||
@if (forwarderId$ | async; as forwarderId) {
|
||||
<tools-forwarder-settings [account]="account" [forwarder]="forwarderId" />
|
||||
}
|
||||
@if (showAlgorithm?.id === Algorithm.plusAddress) {
|
||||
<tools-subaddress-settings
|
||||
[account]="account"
|
||||
(onUpdated)="generate('subaddress settings')"
|
||||
/>
|
||||
}
|
||||
@if (showAlgorithm?.id === Algorithm.username) {
|
||||
<tools-username-settings
|
||||
[account]="account"
|
||||
(onUpdated)="generate('username settings')"
|
||||
/>
|
||||
}
|
||||
</bit-card>
|
||||
</div>
|
||||
</bit-section>
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { LiveAnnouncer } from "@angular/cdk/a11y";
|
||||
import { AsyncPipe } from "@angular/common";
|
||||
import {
|
||||
Component,
|
||||
EventEmitter,
|
||||
@@ -10,7 +11,7 @@ import {
|
||||
Output,
|
||||
SimpleChanges,
|
||||
} from "@angular/core";
|
||||
import { FormBuilder } from "@angular/forms";
|
||||
import { FormBuilder, ReactiveFormsModule } from "@angular/forms";
|
||||
import {
|
||||
BehaviorSubject,
|
||||
catchError,
|
||||
@@ -27,6 +28,7 @@ import {
|
||||
withLatestFrom,
|
||||
} from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
@@ -37,7 +39,23 @@ import {
|
||||
ifEnabledSemanticLoggerProvider,
|
||||
} from "@bitwarden/common/tools/log";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { ToastService, Option } from "@bitwarden/components";
|
||||
import {
|
||||
ToastService,
|
||||
Option,
|
||||
BaseCardDirective,
|
||||
CardComponent,
|
||||
ColorPasswordComponent,
|
||||
AriaDisableDirective,
|
||||
TooltipDirective,
|
||||
BitIconButtonComponent,
|
||||
CopyClickDirective,
|
||||
SectionComponent,
|
||||
SectionHeaderComponent,
|
||||
ToggleGroupModule,
|
||||
TypographyModule,
|
||||
FormFieldModule,
|
||||
SelectModule,
|
||||
} from "@bitwarden/components";
|
||||
import {
|
||||
CredentialType,
|
||||
CredentialGeneratorService,
|
||||
@@ -55,7 +73,15 @@ import {
|
||||
Type,
|
||||
} from "@bitwarden/generator-core";
|
||||
import { GeneratorHistoryService } from "@bitwarden/generator-history";
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
|
||||
import { CatchallSettingsComponent } from "./catchall-settings.component";
|
||||
import { ForwarderSettingsComponent } from "./forwarder-settings.component";
|
||||
import { NudgeGeneratorSpotlightComponent } from "./nudge-generator-spotlight.component";
|
||||
import { PassphraseSettingsComponent } from "./passphrase-settings.component";
|
||||
import { PasswordSettingsComponent } from "./password-settings.component";
|
||||
import { SubaddressSettingsComponent } from "./subaddress-settings.component";
|
||||
import { UsernameSettingsComponent } from "./username-settings.component";
|
||||
import { translate } from "./util";
|
||||
|
||||
// constants used to identify navigation selections that are not
|
||||
@@ -69,7 +95,32 @@ const NONE_SELECTED = "none";
|
||||
@Component({
|
||||
selector: "tools-credential-generator",
|
||||
templateUrl: "credential-generator.component.html",
|
||||
standalone: false,
|
||||
imports: [
|
||||
ToggleGroupModule,
|
||||
NudgeGeneratorSpotlightComponent,
|
||||
BaseCardDirective,
|
||||
CardComponent,
|
||||
ColorPasswordComponent,
|
||||
AriaDisableDirective,
|
||||
TooltipDirective,
|
||||
BitIconButtonComponent,
|
||||
CopyClickDirective,
|
||||
PasswordSettingsComponent,
|
||||
PassphraseSettingsComponent,
|
||||
SectionComponent,
|
||||
SectionHeaderComponent,
|
||||
TypographyModule,
|
||||
ReactiveFormsModule,
|
||||
FormFieldModule,
|
||||
SelectModule,
|
||||
CatchallSettingsComponent,
|
||||
ForwarderSettingsComponent,
|
||||
SubaddressSettingsComponent,
|
||||
UsernameSettingsComponent,
|
||||
AsyncPipe,
|
||||
JslibModule,
|
||||
I18nPipe,
|
||||
],
|
||||
})
|
||||
export class CredentialGeneratorComponent implements OnInit, OnChanges, OnDestroy {
|
||||
private readonly destroyed = new Subject<void>();
|
||||
|
||||
@@ -1,28 +1,34 @@
|
||||
<form [formGroup]="settings" class="tw-container">
|
||||
<bit-form-field *ngIf="displayDomain">
|
||||
<bit-label>{{ "forwarderDomainName" | i18n }}</bit-label>
|
||||
<input
|
||||
bitInput
|
||||
formControlName="domain"
|
||||
type="text"
|
||||
placeholder="example.com"
|
||||
(change)="save('domain')"
|
||||
/>
|
||||
<bit-hint>{{ "forwarderDomainNameHint" | i18n }}</bit-hint>
|
||||
</bit-form-field>
|
||||
<bit-form-field *ngIf="displayToken">
|
||||
<bit-label>{{ "apiKey" | i18n }}</bit-label>
|
||||
<input bitInput formControlName="token" type="password" (change)="save('password')" />
|
||||
<button
|
||||
type="button"
|
||||
bitIconButton
|
||||
bitSuffix
|
||||
bitPasswordInputToggle
|
||||
(change)="save('token')"
|
||||
></button>
|
||||
</bit-form-field>
|
||||
<bit-form-field *ngIf="displayBaseUrl" disableMargin>
|
||||
<bit-label>{{ "selfHostBaseUrl" | i18n }}</bit-label>
|
||||
<input bitInput formControlName="baseUrl" type="text" (change)="save('baseUrl')" />
|
||||
</bit-form-field>
|
||||
@if (displayDomain) {
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "forwarderDomainName" | i18n }}</bit-label>
|
||||
<input
|
||||
bitInput
|
||||
formControlName="domain"
|
||||
type="text"
|
||||
placeholder="example.com"
|
||||
(change)="save('domain')"
|
||||
/>
|
||||
<bit-hint>{{ "forwarderDomainNameHint" | i18n }}</bit-hint>
|
||||
</bit-form-field>
|
||||
}
|
||||
@if (displayToken) {
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "apiKey" | i18n }}</bit-label>
|
||||
<input bitInput formControlName="token" type="password" (change)="save('password')" />
|
||||
<button
|
||||
type="button"
|
||||
bitIconButton
|
||||
bitSuffix
|
||||
bitPasswordInputToggle
|
||||
(change)="save('token')"
|
||||
></button>
|
||||
</bit-form-field>
|
||||
}
|
||||
@if (displayBaseUrl) {
|
||||
<bit-form-field disableMargin>
|
||||
<bit-label>{{ "selfHostBaseUrl" | i18n }}</bit-label>
|
||||
<input bitInput formControlName="baseUrl" type="text" (change)="save('baseUrl')" />
|
||||
</bit-form-field>
|
||||
}
|
||||
</form>
|
||||
|
||||
@@ -8,16 +8,24 @@ import {
|
||||
Output,
|
||||
SimpleChanges,
|
||||
} from "@angular/core";
|
||||
import { FormBuilder } from "@angular/forms";
|
||||
import { FormBuilder, ReactiveFormsModule } from "@angular/forms";
|
||||
import { map, ReplaySubject, skip, Subject, switchAll, takeUntil, withLatestFrom } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { Account } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { VendorId } from "@bitwarden/common/tools/extension";
|
||||
import {
|
||||
FormFieldModule,
|
||||
AriaDisableDirective,
|
||||
TooltipDirective,
|
||||
BitIconButtonComponent,
|
||||
} from "@bitwarden/components";
|
||||
import {
|
||||
CredentialGeneratorService,
|
||||
ForwarderOptions,
|
||||
GeneratorMetadata,
|
||||
} from "@bitwarden/generator-core";
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
|
||||
const Controls = Object.freeze({
|
||||
domain: "domain",
|
||||
@@ -31,7 +39,15 @@ const Controls = Object.freeze({
|
||||
@Component({
|
||||
selector: "tools-forwarder-settings",
|
||||
templateUrl: "forwarder-settings.component.html",
|
||||
standalone: false,
|
||||
imports: [
|
||||
ReactiveFormsModule,
|
||||
FormFieldModule,
|
||||
AriaDisableDirective,
|
||||
TooltipDirective,
|
||||
BitIconButtonComponent,
|
||||
JslibModule,
|
||||
I18nPipe,
|
||||
],
|
||||
})
|
||||
export class ForwarderSettingsComponent implements OnInit, OnChanges, OnDestroy {
|
||||
/** Instantiates the component
|
||||
|
||||
@@ -1,67 +1,13 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { NgModule } from "@angular/core";
|
||||
import { ReactiveFormsModule } from "@angular/forms";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import {
|
||||
CardComponent,
|
||||
ColorPasswordModule,
|
||||
CheckboxModule,
|
||||
FormFieldModule,
|
||||
IconButtonModule,
|
||||
InputModule,
|
||||
ItemModule,
|
||||
SectionComponent,
|
||||
SectionHeaderComponent,
|
||||
SelectModule,
|
||||
ToggleGroupModule,
|
||||
TypographyModule,
|
||||
} from "@bitwarden/components";
|
||||
|
||||
import { CatchallSettingsComponent } from "./catchall-settings.component";
|
||||
import { CredentialGeneratorComponent } from "./credential-generator.component";
|
||||
import { ForwarderSettingsComponent } from "./forwarder-settings.component";
|
||||
import { GeneratorServicesModule } from "./generator-services.module";
|
||||
import { NudgeGeneratorSpotlightComponent } from "./nudge-generator-spotlight.component";
|
||||
import { PassphraseSettingsComponent } from "./passphrase-settings.component";
|
||||
import { PasswordGeneratorComponent } from "./password-generator.component";
|
||||
import { PasswordSettingsComponent } from "./password-settings.component";
|
||||
import { SubaddressSettingsComponent } from "./subaddress-settings.component";
|
||||
import { UsernameGeneratorComponent } from "./username-generator.component";
|
||||
import { UsernameSettingsComponent } from "./username-settings.component";
|
||||
|
||||
/** Shared module containing generator component dependencies */
|
||||
/** @deprecated Use individual components instead. */
|
||||
@NgModule({
|
||||
imports: [
|
||||
CardComponent,
|
||||
ColorPasswordModule,
|
||||
CheckboxModule,
|
||||
CommonModule,
|
||||
FormFieldModule,
|
||||
GeneratorServicesModule,
|
||||
IconButtonModule,
|
||||
InputModule,
|
||||
ItemModule,
|
||||
JslibModule,
|
||||
ReactiveFormsModule,
|
||||
SectionComponent,
|
||||
SectionHeaderComponent,
|
||||
SelectModule,
|
||||
ToggleGroupModule,
|
||||
TypographyModule,
|
||||
NudgeGeneratorSpotlightComponent,
|
||||
],
|
||||
declarations: [
|
||||
CatchallSettingsComponent,
|
||||
CredentialGeneratorComponent,
|
||||
ForwarderSettingsComponent,
|
||||
SubaddressSettingsComponent,
|
||||
PasswordGeneratorComponent,
|
||||
PassphraseSettingsComponent,
|
||||
PasswordSettingsComponent,
|
||||
UsernameGeneratorComponent,
|
||||
UsernameSettingsComponent,
|
||||
],
|
||||
imports: [CredentialGeneratorComponent, PasswordGeneratorComponent, UsernameGeneratorComponent],
|
||||
exports: [CredentialGeneratorComponent, PasswordGeneratorComponent, UsernameGeneratorComponent],
|
||||
})
|
||||
export class GeneratorModule {
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
/**
|
||||
* This file contains the public interface for the generator components library.
|
||||
*
|
||||
* Be mindful of what you export here, as those components should be considered stable
|
||||
* and part of the public API contract.
|
||||
*/
|
||||
|
||||
export { CredentialGeneratorComponent } from "./credential-generator.component";
|
||||
export { CredentialGeneratorHistoryComponent } from "./credential-generator-history.component";
|
||||
export { CredentialGeneratorHistoryDialogComponent } from "./credential-generator-history-dialog.component";
|
||||
export { EmptyCredentialHistoryComponent } from "./empty-credential-history.component";
|
||||
export { GeneratorModule } from "./generator.module";
|
||||
export { GeneratorServicesModule, SYSTEM_SERVICE_PROVIDER } from "./generator-services.module";
|
||||
export { PasswordGeneratorComponent } from "./password-generator.component";
|
||||
export { UsernameGeneratorComponent } from "./username-generator.component";
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
<div class="tw-mb-4" *ngIf="showGeneratorSpotlight$ | async">
|
||||
<bit-spotlight
|
||||
[title]="'generatorNudgeTitle' | i18n"
|
||||
(onDismiss)="dismissGeneratorSpotlight(NudgeType.GeneratorNudgeStatus)"
|
||||
>
|
||||
<p class="tw-text-main tw-mb-0" bitTypography="body2">
|
||||
<span class="tw-sr-only">
|
||||
{{ "generatorNudgeBodyAria" | i18n }}
|
||||
</span>
|
||||
<span aria-hidden="true">
|
||||
{{ "generatorNudgeBodyOne" | i18n }} <i class="bwi bwi-generate"></i>
|
||||
{{ "generatorNudgeBodyTwo" | i18n }}
|
||||
</span>
|
||||
</p>
|
||||
</bit-spotlight>
|
||||
</div>
|
||||
@if (showGeneratorSpotlight$ | async) {
|
||||
<div class="tw-mb-4">
|
||||
<bit-spotlight
|
||||
[title]="'generatorNudgeTitle' | i18n"
|
||||
(onDismiss)="dismissGeneratorSpotlight(NudgeType.GeneratorNudgeStatus)"
|
||||
>
|
||||
<p class="tw-text-main tw-mb-0" bitTypography="body2">
|
||||
<span class="tw-sr-only">
|
||||
{{ "generatorNudgeBodyAria" | i18n }}
|
||||
</span>
|
||||
<span aria-hidden="true">
|
||||
{{ "generatorNudgeBodyOne" | i18n }} <i class="bwi bwi-generate"></i>
|
||||
{{ "generatorNudgeBodyTwo" | i18n }}
|
||||
</span>
|
||||
</p>
|
||||
</bit-spotlight>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
<bit-section [disableMargin]="disableMargin">
|
||||
<bit-section-header *ngIf="showHeader">
|
||||
<h6 bitTypography="h6">{{ "options" | i18n }}</h6>
|
||||
</bit-section-header>
|
||||
@if (showHeader) {
|
||||
<bit-section-header>
|
||||
<h6 bitTypography="h6">{{ "options" | i18n }}</h6>
|
||||
</bit-section-header>
|
||||
}
|
||||
<form [formGroup]="settings" class="tw-container">
|
||||
<div class="tw-mb-4">
|
||||
<bit-card>
|
||||
@@ -51,7 +53,9 @@
|
||||
/>
|
||||
<bit-label>{{ "includeNumber" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
<p *ngIf="policyInEffect" bitTypography="helper">{{ "generatorPolicyInEffect" | i18n }}</p>
|
||||
@if (policyInEffect) {
|
||||
<p bitTypography="helper">{{ "generatorPolicyInEffect" | i18n }}</p>
|
||||
}
|
||||
</bit-card>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { coerceBooleanProperty } from "@angular/cdk/coercion";
|
||||
import { AsyncPipe } from "@angular/common";
|
||||
import {
|
||||
OnInit,
|
||||
Input,
|
||||
@@ -9,9 +10,10 @@ import {
|
||||
SimpleChanges,
|
||||
OnChanges,
|
||||
} from "@angular/core";
|
||||
import { FormBuilder } from "@angular/forms";
|
||||
import { FormBuilder, ReactiveFormsModule } from "@angular/forms";
|
||||
import { skip, takeUntil, Subject, map, withLatestFrom, ReplaySubject, tap } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { Account } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
@@ -20,11 +22,21 @@ import {
|
||||
disabledSemanticLoggerProvider,
|
||||
ifEnabledSemanticLoggerProvider,
|
||||
} from "@bitwarden/common/tools/log";
|
||||
import {
|
||||
SectionComponent,
|
||||
SectionHeaderComponent,
|
||||
BaseCardDirective,
|
||||
CardComponent,
|
||||
TypographyModule,
|
||||
FormFieldModule,
|
||||
CheckboxModule,
|
||||
} from "@bitwarden/components";
|
||||
import {
|
||||
CredentialGeneratorService,
|
||||
PassphraseGenerationOptions,
|
||||
BuiltIn,
|
||||
} from "@bitwarden/generator-core";
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
|
||||
const Controls = Object.freeze({
|
||||
numWords: "numWords",
|
||||
@@ -39,7 +51,19 @@ const Controls = Object.freeze({
|
||||
@Component({
|
||||
selector: "tools-passphrase-settings",
|
||||
templateUrl: "passphrase-settings.component.html",
|
||||
standalone: false,
|
||||
imports: [
|
||||
SectionComponent,
|
||||
SectionHeaderComponent,
|
||||
TypographyModule,
|
||||
ReactiveFormsModule,
|
||||
BaseCardDirective,
|
||||
CardComponent,
|
||||
FormFieldModule,
|
||||
CheckboxModule,
|
||||
AsyncPipe,
|
||||
JslibModule,
|
||||
I18nPipe,
|
||||
],
|
||||
})
|
||||
export class PassphraseSettingsComponent implements OnInit, OnChanges, OnDestroy {
|
||||
/** Instantiates the component
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
<bit-toggle-group
|
||||
fullWidth
|
||||
class="tw-mb-4"
|
||||
[selected]="credentialType$ | async"
|
||||
(selectedChange)="onCredentialTypeChanged($event)"
|
||||
*ngIf="showCredentialTypes$ | async"
|
||||
attr.aria-label="{{ 'type' | i18n }}"
|
||||
>
|
||||
<bit-toggle *ngFor="let option of passwordOptions$ | async" [value]="option.value">
|
||||
{{ option.label }}
|
||||
</bit-toggle>
|
||||
</bit-toggle-group>
|
||||
@if (showCredentialTypes$ | async) {
|
||||
<bit-toggle-group
|
||||
fullWidth
|
||||
class="tw-mb-4"
|
||||
[selected]="credentialType$ | async"
|
||||
(selectedChange)="onCredentialTypeChanged($event)"
|
||||
attr.aria-label="{{ 'type' | i18n }}"
|
||||
>
|
||||
@for (option of passwordOptions$ | async; track option) {
|
||||
<bit-toggle [value]="option.value">
|
||||
{{ option.label }}
|
||||
</bit-toggle>
|
||||
}
|
||||
</bit-toggle-group>
|
||||
}
|
||||
<bit-card class="tw-flex tw-justify-between tw-mb-4">
|
||||
<div class="tw-grow tw-flex tw-items-center tw-min-w-0">
|
||||
<bit-color-password class="tw-font-mono" [password]="value$ | async"></bit-color-password>
|
||||
@@ -37,17 +40,19 @@
|
||||
></button>
|
||||
</div>
|
||||
</bit-card>
|
||||
<tools-password-settings
|
||||
class="tw-mt-6"
|
||||
*ngIf="(algorithm$ | async)?.id === Algorithm.password"
|
||||
[account]="account$ | async"
|
||||
[disableMargin]="disableMargin"
|
||||
(onUpdated)="generate('password settings')"
|
||||
/>
|
||||
<tools-passphrase-settings
|
||||
class="tw-mt-6"
|
||||
*ngIf="(algorithm$ | async)?.id === Algorithm.passphrase"
|
||||
[account]="account$ | async"
|
||||
(onUpdated)="generate('passphrase settings')"
|
||||
[disableMargin]="disableMargin"
|
||||
/>
|
||||
@if ((algorithm$ | async)?.id === Algorithm.password) {
|
||||
<tools-password-settings
|
||||
class="tw-mt-6"
|
||||
[account]="account$ | async"
|
||||
[disableMargin]="disableMargin"
|
||||
(onUpdated)="generate('password settings')"
|
||||
/>
|
||||
}
|
||||
@if ((algorithm$ | async)?.id === Algorithm.passphrase) {
|
||||
<tools-passphrase-settings
|
||||
class="tw-mt-6"
|
||||
[account]="account$ | async"
|
||||
(onUpdated)="generate('passphrase settings')"
|
||||
[disableMargin]="disableMargin"
|
||||
/>
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { LiveAnnouncer } from "@angular/cdk/a11y";
|
||||
import { coerceBooleanProperty } from "@angular/cdk/coercion";
|
||||
import { AsyncPipe } from "@angular/common";
|
||||
import {
|
||||
Component,
|
||||
EventEmitter,
|
||||
@@ -24,6 +25,7 @@ import {
|
||||
withLatestFrom,
|
||||
} from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
@@ -33,7 +35,18 @@ import {
|
||||
ifEnabledSemanticLoggerProvider,
|
||||
} from "@bitwarden/common/tools/log";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { ToastService, Option } from "@bitwarden/components";
|
||||
import {
|
||||
ToastService,
|
||||
Option,
|
||||
BaseCardDirective,
|
||||
CardComponent,
|
||||
ColorPasswordComponent,
|
||||
AriaDisableDirective,
|
||||
TooltipDirective,
|
||||
BitIconButtonComponent,
|
||||
CopyClickDirective,
|
||||
ToggleGroupModule,
|
||||
} from "@bitwarden/components";
|
||||
import {
|
||||
CredentialGeneratorService,
|
||||
GeneratedCredential,
|
||||
@@ -49,7 +62,10 @@ import {
|
||||
Profile,
|
||||
} from "@bitwarden/generator-core";
|
||||
import { GeneratorHistoryService } from "@bitwarden/generator-history";
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
|
||||
import { PassphraseSettingsComponent } from "./passphrase-settings.component";
|
||||
import { PasswordSettingsComponent } from "./password-settings.component";
|
||||
import { toAlgorithmInfo, translate } from "./util";
|
||||
|
||||
/** Options group for passwords */
|
||||
@@ -58,7 +74,21 @@ import { toAlgorithmInfo, translate } from "./util";
|
||||
@Component({
|
||||
selector: "tools-password-generator",
|
||||
templateUrl: "password-generator.component.html",
|
||||
standalone: false,
|
||||
imports: [
|
||||
ToggleGroupModule,
|
||||
BaseCardDirective,
|
||||
CardComponent,
|
||||
ColorPasswordComponent,
|
||||
AriaDisableDirective,
|
||||
TooltipDirective,
|
||||
BitIconButtonComponent,
|
||||
CopyClickDirective,
|
||||
PasswordSettingsComponent,
|
||||
PassphraseSettingsComponent,
|
||||
AsyncPipe,
|
||||
JslibModule,
|
||||
I18nPipe,
|
||||
],
|
||||
})
|
||||
export class PasswordGeneratorComponent implements OnInit, OnChanges, OnDestroy {
|
||||
constructor(
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
<bit-section [disableMargin]="disableMargin">
|
||||
<bit-section-header *ngIf="showHeader">
|
||||
<h2 bitTypography="h6">{{ "options" | i18n }}</h2>
|
||||
</bit-section-header>
|
||||
@if (showHeader) {
|
||||
<bit-section-header>
|
||||
<h2 bitTypography="h6">{{ "options" | i18n }}</h2>
|
||||
</bit-section-header>
|
||||
}
|
||||
<form [formGroup]="settings" class="tw-container">
|
||||
<div class="tw-mb-4">
|
||||
<bit-card>
|
||||
@@ -50,11 +52,7 @@
|
||||
<input bitCheckbox type="checkbox" formControlName="number" (change)="save('number')" />
|
||||
<bit-label>{{ "numbersLabel" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
<bit-form-control
|
||||
class="!tw-mb-0"
|
||||
attr.aria-description="{{ 'specialCharactersDescription' | i18n }}"
|
||||
title="{{ 'specialCharactersDescription' | i18n }}"
|
||||
>
|
||||
<bit-form-control class="!tw-mb-0" title="{{ 'specialCharactersDescription' | i18n }}">
|
||||
<input
|
||||
bitCheckbox
|
||||
type="checkbox"
|
||||
@@ -62,10 +60,15 @@
|
||||
(change)="save('special')"
|
||||
/>
|
||||
<!-- hard-coded the special characters string because `$` indicates an i18n interpolation,
|
||||
and is handled inconsistently across browsers. Angular template syntax is used to
|
||||
ensure special characters are entity-encoded.
|
||||
-->
|
||||
<bit-label>{{ "!@#$%^&*" }}</bit-label>
|
||||
and is handled inconsistently across browsers. Angular template syntax is used to
|
||||
ensure special characters are entity-encoded.
|
||||
-->
|
||||
<bit-label>
|
||||
<span aria-hidden="true">{{ "!@#$%^&*" }}</span>
|
||||
<span class="tw-sr-only">
|
||||
{{ "specialCharactersDescription" | i18n }}
|
||||
</span>
|
||||
</bit-label>
|
||||
</bit-form-control>
|
||||
</div>
|
||||
<div class="tw-flex">
|
||||
@@ -97,7 +100,9 @@
|
||||
/>
|
||||
<bit-label>{{ "avoidAmbiguous" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
<p *ngIf="policyInEffect" bitTypography="helper">{{ "generatorPolicyInEffect" | i18n }}</p>
|
||||
@if (policyInEffect) {
|
||||
<p bitTypography="helper">{{ "generatorPolicyInEffect" | i18n }}</p>
|
||||
}
|
||||
</bit-card>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { coerceBooleanProperty } from "@angular/cdk/coercion";
|
||||
import { AsyncPipe } from "@angular/common";
|
||||
import {
|
||||
OnInit,
|
||||
Input,
|
||||
@@ -9,16 +10,27 @@ import {
|
||||
SimpleChanges,
|
||||
OnChanges,
|
||||
} from "@angular/core";
|
||||
import { FormBuilder } from "@angular/forms";
|
||||
import { FormBuilder, ReactiveFormsModule } from "@angular/forms";
|
||||
import { takeUntil, Subject, map, filter, tap, skip, ReplaySubject, withLatestFrom } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { Account } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import {
|
||||
SectionComponent,
|
||||
SectionHeaderComponent,
|
||||
BaseCardDirective,
|
||||
CardComponent,
|
||||
FormFieldModule,
|
||||
TypographyModule,
|
||||
CheckboxModule,
|
||||
} from "@bitwarden/components";
|
||||
import {
|
||||
CredentialGeneratorService,
|
||||
PasswordGenerationOptions,
|
||||
BuiltIn,
|
||||
} from "@bitwarden/generator-core";
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
|
||||
import { hasRangeOfValues } from "./util";
|
||||
|
||||
@@ -39,7 +51,19 @@ const Controls = Object.freeze({
|
||||
@Component({
|
||||
selector: "tools-password-settings",
|
||||
templateUrl: "password-settings.component.html",
|
||||
standalone: false,
|
||||
imports: [
|
||||
SectionComponent,
|
||||
SectionHeaderComponent,
|
||||
TypographyModule,
|
||||
ReactiveFormsModule,
|
||||
BaseCardDirective,
|
||||
CardComponent,
|
||||
FormFieldModule,
|
||||
CheckboxModule,
|
||||
AsyncPipe,
|
||||
JslibModule,
|
||||
I18nPipe,
|
||||
],
|
||||
})
|
||||
export class PasswordSettingsComponent implements OnInit, OnChanges, OnDestroy {
|
||||
/** Instantiates the component
|
||||
|
||||
@@ -8,15 +8,18 @@ import {
|
||||
Output,
|
||||
SimpleChanges,
|
||||
} from "@angular/core";
|
||||
import { FormBuilder } from "@angular/forms";
|
||||
import { FormBuilder, ReactiveFormsModule } from "@angular/forms";
|
||||
import { map, ReplaySubject, skip, Subject, takeUntil, withLatestFrom } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { Account } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { FormFieldModule } from "@bitwarden/components";
|
||||
import {
|
||||
CredentialGeneratorService,
|
||||
BuiltIn,
|
||||
SubaddressGenerationOptions,
|
||||
} from "@bitwarden/generator-core";
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
|
||||
/** Options group for plus-addressed emails */
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
@@ -24,7 +27,7 @@ import {
|
||||
@Component({
|
||||
selector: "tools-subaddress-settings",
|
||||
templateUrl: "subaddress-settings.component.html",
|
||||
standalone: false,
|
||||
imports: [ReactiveFormsModule, FormFieldModule, JslibModule, I18nPipe],
|
||||
})
|
||||
export class SubaddressSettingsComponent implements OnInit, OnChanges, OnDestroy {
|
||||
/** Instantiates the component
|
||||
|
||||
@@ -42,42 +42,41 @@
|
||||
data-testid="username-type"
|
||||
>
|
||||
</bit-select>
|
||||
<bit-hint *ngIf="!!(credentialTypeHint$ | async)">{{
|
||||
credentialTypeHint$ | async
|
||||
}}</bit-hint>
|
||||
@if (credentialTypeHint$ | async) {
|
||||
<bit-hint>{{ credentialTypeHint$ | async }}</bit-hint>
|
||||
}
|
||||
</bit-form-field>
|
||||
</form>
|
||||
<form *ngIf="showForwarder$ | async" [formGroup]="forwarder" class="tw-container">
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "service" | i18n }}</bit-label>
|
||||
<bit-select
|
||||
[items]="forwarderOptions$ | async"
|
||||
formControlName="nav"
|
||||
data-testid="email-forwarding-service"
|
||||
>
|
||||
</bit-select>
|
||||
</bit-form-field>
|
||||
</form>
|
||||
<tools-catchall-settings
|
||||
*ngIf="(showAlgorithm$ | async)?.id === Algorithm.catchall"
|
||||
[account]="account$ | async"
|
||||
(onUpdated)="generate('catchall settings')"
|
||||
/>
|
||||
<tools-forwarder-settings
|
||||
*ngIf="!!(forwarderId$ | async)"
|
||||
[forwarder]="forwarderId$ | async"
|
||||
[account]="account$ | async"
|
||||
/>
|
||||
<tools-subaddress-settings
|
||||
*ngIf="(showAlgorithm$ | async)?.id === Algorithm.plusAddress"
|
||||
[account]="account$ | async"
|
||||
(onUpdated)="generate('subaddress settings')"
|
||||
/>
|
||||
<tools-username-settings
|
||||
*ngIf="(showAlgorithm$ | async)?.id === Algorithm.username"
|
||||
[account]="account$ | async"
|
||||
(onUpdated)="generate('username settings')"
|
||||
/>
|
||||
@if (showForwarder$ | async) {
|
||||
<form [formGroup]="forwarder" class="tw-container">
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "service" | i18n }}</bit-label>
|
||||
<bit-select
|
||||
[items]="forwarderOptions$ | async"
|
||||
formControlName="nav"
|
||||
data-testid="email-forwarding-service"
|
||||
>
|
||||
</bit-select>
|
||||
</bit-form-field>
|
||||
</form>
|
||||
}
|
||||
@let showAlgorithm = showAlgorithm$ | async;
|
||||
@let account = account$ | async;
|
||||
@if (showAlgorithm?.id === Algorithm.catchall) {
|
||||
<tools-catchall-settings [account]="account" (onUpdated)="generate('catchall settings')" />
|
||||
}
|
||||
@if (forwarderId$ | async; as forwarderId) {
|
||||
<tools-forwarder-settings [forwarder]="forwarderId" [account]="account" />
|
||||
}
|
||||
@if (showAlgorithm?.id === Algorithm.plusAddress) {
|
||||
<tools-subaddress-settings
|
||||
[account]="account"
|
||||
(onUpdated)="generate('subaddress settings')"
|
||||
/>
|
||||
}
|
||||
@if (showAlgorithm?.id === Algorithm.username) {
|
||||
<tools-username-settings [account]="account" (onUpdated)="generate('username settings')" />
|
||||
}
|
||||
</bit-card>
|
||||
</div>
|
||||
</bit-section>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { LiveAnnouncer } from "@angular/cdk/a11y";
|
||||
import { coerceBooleanProperty } from "@angular/cdk/coercion";
|
||||
import { NgClass, AsyncPipe } from "@angular/common";
|
||||
import {
|
||||
Component,
|
||||
EventEmitter,
|
||||
@@ -11,7 +12,7 @@ import {
|
||||
Output,
|
||||
SimpleChanges,
|
||||
} from "@angular/core";
|
||||
import { FormBuilder } from "@angular/forms";
|
||||
import { FormBuilder, ReactiveFormsModule } from "@angular/forms";
|
||||
import {
|
||||
BehaviorSubject,
|
||||
catchError,
|
||||
@@ -28,6 +29,7 @@ import {
|
||||
withLatestFrom,
|
||||
} from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
@@ -38,7 +40,22 @@ import {
|
||||
ifEnabledSemanticLoggerProvider,
|
||||
} from "@bitwarden/common/tools/log";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { ToastService, Option } from "@bitwarden/components";
|
||||
import {
|
||||
ToastService,
|
||||
Option,
|
||||
AriaDisableDirective,
|
||||
BaseCardDirective,
|
||||
CardComponent,
|
||||
ColorPasswordComponent,
|
||||
CopyClickDirective,
|
||||
BitIconButtonComponent,
|
||||
TooltipDirective,
|
||||
SectionComponent,
|
||||
SectionHeaderComponent,
|
||||
SelectComponent,
|
||||
TypographyModule,
|
||||
FormFieldModule,
|
||||
} from "@bitwarden/components";
|
||||
import {
|
||||
AlgorithmInfo,
|
||||
CredentialGeneratorService,
|
||||
@@ -55,7 +72,12 @@ import {
|
||||
Algorithm,
|
||||
} from "@bitwarden/generator-core";
|
||||
import { GeneratorHistoryService } from "@bitwarden/generator-history";
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
|
||||
import { CatchallSettingsComponent } from "./catchall-settings.component";
|
||||
import { ForwarderSettingsComponent } from "./forwarder-settings.component";
|
||||
import { SubaddressSettingsComponent } from "./subaddress-settings.component";
|
||||
import { UsernameSettingsComponent } from "./username-settings.component";
|
||||
import { toAlgorithmInfo, translate } from "./util";
|
||||
|
||||
// constants used to identify navigation selections that are not
|
||||
@@ -69,7 +91,29 @@ const NONE_SELECTED = "none";
|
||||
@Component({
|
||||
selector: "tools-username-generator",
|
||||
templateUrl: "username-generator.component.html",
|
||||
standalone: false,
|
||||
imports: [
|
||||
BaseCardDirective,
|
||||
CardComponent,
|
||||
ColorPasswordComponent,
|
||||
AriaDisableDirective,
|
||||
TooltipDirective,
|
||||
BitIconButtonComponent,
|
||||
CopyClickDirective,
|
||||
SectionComponent,
|
||||
SectionHeaderComponent,
|
||||
TypographyModule,
|
||||
NgClass,
|
||||
ReactiveFormsModule,
|
||||
SelectComponent,
|
||||
FormFieldModule,
|
||||
CatchallSettingsComponent,
|
||||
ForwarderSettingsComponent,
|
||||
SubaddressSettingsComponent,
|
||||
UsernameSettingsComponent,
|
||||
AsyncPipe,
|
||||
JslibModule,
|
||||
I18nPipe,
|
||||
],
|
||||
})
|
||||
export class UsernameGeneratorComponent implements OnInit, OnChanges, OnDestroy {
|
||||
/** Instantiates the username generator
|
||||
|
||||
@@ -8,15 +8,18 @@ import {
|
||||
Output,
|
||||
SimpleChanges,
|
||||
} from "@angular/core";
|
||||
import { FormBuilder } from "@angular/forms";
|
||||
import { FormBuilder, ReactiveFormsModule } from "@angular/forms";
|
||||
import { map, ReplaySubject, skip, Subject, takeUntil, withLatestFrom } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { Account } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { FormFieldModule, CheckboxModule } from "@bitwarden/components";
|
||||
import {
|
||||
CredentialGeneratorService,
|
||||
EffUsernameGenerationOptions,
|
||||
BuiltIn,
|
||||
} from "@bitwarden/generator-core";
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
|
||||
/** Options group for usernames */
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
@@ -24,7 +27,7 @@ import {
|
||||
@Component({
|
||||
selector: "tools-username-settings",
|
||||
templateUrl: "username-settings.component.html",
|
||||
standalone: false,
|
||||
imports: [ReactiveFormsModule, FormFieldModule, CheckboxModule, JslibModule, I18nPipe],
|
||||
})
|
||||
export class UsernameSettingsComponent implements OnInit, OnChanges, OnDestroy {
|
||||
/** Instantiates the component
|
||||
|
||||
@@ -7,8 +7,7 @@ import { ForwarderContext } from "../forwarder-context";
|
||||
export class CreateForwardingAddressRpc<
|
||||
Settings extends ApiSettings,
|
||||
Req extends IntegrationRequest = IntegrationRequest,
|
||||
> implements JsonRpc<Req, string>
|
||||
{
|
||||
> implements JsonRpc<Req, string> {
|
||||
constructor(
|
||||
readonly requestor: ForwarderConfiguration<Settings>,
|
||||
readonly context: ForwarderContext<Settings>,
|
||||
|
||||
@@ -9,8 +9,7 @@ import { ForwarderContext } from "../forwarder-context";
|
||||
export class GetAccountIdRpc<
|
||||
Settings extends ApiSettings,
|
||||
Req extends IntegrationRequest = IntegrationRequest,
|
||||
> implements JsonRpc<Req, string>
|
||||
{
|
||||
> implements JsonRpc<Req, string> {
|
||||
constructor(
|
||||
readonly requestor: ForwarderConfiguration<Settings>,
|
||||
readonly context: ForwarderContext<Settings>,
|
||||
|
||||
@@ -24,6 +24,7 @@ describe("availableAlgorithms_vNextPolicy", () => {
|
||||
overridePasswordType: override,
|
||||
},
|
||||
enabled: true,
|
||||
revisionDate: new Date().toISOString(),
|
||||
});
|
||||
|
||||
const result = availableAlgorithms([policy]);
|
||||
@@ -44,6 +45,7 @@ describe("availableAlgorithms_vNextPolicy", () => {
|
||||
overridePasswordType: override,
|
||||
},
|
||||
enabled: true,
|
||||
revisionDate: new Date().toISOString(),
|
||||
});
|
||||
|
||||
const result = availableAlgorithms([policy, policy]);
|
||||
@@ -64,6 +66,7 @@ describe("availableAlgorithms_vNextPolicy", () => {
|
||||
overridePasswordType: "password",
|
||||
},
|
||||
enabled: true,
|
||||
revisionDate: new Date().toISOString(),
|
||||
});
|
||||
const passphrase = new Policy({
|
||||
id: "" as PolicyId,
|
||||
@@ -73,6 +76,7 @@ describe("availableAlgorithms_vNextPolicy", () => {
|
||||
overridePasswordType: "passphrase",
|
||||
},
|
||||
enabled: true,
|
||||
revisionDate: new Date().toISOString(),
|
||||
});
|
||||
|
||||
const result = availableAlgorithms([password, passphrase]);
|
||||
@@ -93,6 +97,7 @@ describe("availableAlgorithms_vNextPolicy", () => {
|
||||
some: "policy",
|
||||
},
|
||||
enabled: true,
|
||||
revisionDate: new Date().toISOString(),
|
||||
});
|
||||
|
||||
const result = availableAlgorithms([policy]);
|
||||
@@ -111,6 +116,7 @@ describe("availableAlgorithms_vNextPolicy", () => {
|
||||
some: "policy",
|
||||
},
|
||||
enabled: false,
|
||||
revisionDate: new Date().toISOString(),
|
||||
});
|
||||
|
||||
const result = availableAlgorithms([policy]);
|
||||
@@ -129,6 +135,7 @@ describe("availableAlgorithms_vNextPolicy", () => {
|
||||
some: "policy",
|
||||
},
|
||||
enabled: true,
|
||||
revisionDate: new Date().toISOString(),
|
||||
});
|
||||
|
||||
const result = availableAlgorithms([policy]);
|
||||
|
||||
@@ -2,9 +2,10 @@ import { PolicyEvaluator } from "../abstractions";
|
||||
import { NoPolicy } from "../types";
|
||||
|
||||
/** A policy evaluator that does not apply any policy */
|
||||
export class DefaultPolicyEvaluator<PolicyTarget>
|
||||
implements PolicyEvaluator<NoPolicy, PolicyTarget>
|
||||
{
|
||||
export class DefaultPolicyEvaluator<PolicyTarget> implements PolicyEvaluator<
|
||||
NoPolicy,
|
||||
PolicyTarget
|
||||
> {
|
||||
/** {@link PolicyEvaluator.policy} */
|
||||
get policy() {
|
||||
return {};
|
||||
|
||||
@@ -13,9 +13,7 @@ import { atLeast, atLeastSum, maybe, readonlyTrueWhen, AtLeastOne, Zero } from "
|
||||
import { PasswordPolicyConstraints } from "./password-policy-constraints";
|
||||
|
||||
/** Creates state constraints by blending policy and password settings. */
|
||||
export class DynamicPasswordPolicyConstraints
|
||||
implements DynamicStateConstraints<PasswordGeneratorSettings>
|
||||
{
|
||||
export class DynamicPasswordPolicyConstraints implements DynamicStateConstraints<PasswordGeneratorSettings> {
|
||||
/** Instantiates the object.
|
||||
* @param policy the password policy to enforce. This cannot be
|
||||
* `null` or `undefined`.
|
||||
|
||||
@@ -17,6 +17,7 @@ function createPolicy(
|
||||
data,
|
||||
enabled,
|
||||
type,
|
||||
revisionDate: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ function createPolicy(
|
||||
data,
|
||||
enabled,
|
||||
type,
|
||||
revisionDate: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,11 @@ import { deepFreeze } from "@bitwarden/common/tools/util";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { BitwardenClient } from "@bitwarden/sdk-internal";
|
||||
|
||||
import { FakeAccountService, FakeStateProvider } from "../../../../../common/spec";
|
||||
import {
|
||||
FakeAccountService,
|
||||
FakeStateProvider,
|
||||
mockAccountInfoWith,
|
||||
} from "../../../../../common/spec";
|
||||
import { Algorithm, AlgorithmsByType, CredentialAlgorithm, Type, Types } from "../metadata";
|
||||
import catchall from "../metadata/email/catchall";
|
||||
import plusAddress from "../metadata/email/plus-address";
|
||||
@@ -40,9 +44,10 @@ import { GeneratorMetadataProvider } from "./generator-metadata-provider";
|
||||
const SomeUser = "some user" as UserId;
|
||||
const SomeAccount = {
|
||||
id: SomeUser,
|
||||
email: "someone@example.com",
|
||||
emailVerified: true,
|
||||
name: "Someone",
|
||||
...mockAccountInfoWith({
|
||||
email: "someone@example.com",
|
||||
name: "Someone",
|
||||
}),
|
||||
};
|
||||
const SomeAccount$ = new BehaviorSubject<Account>(SomeAccount);
|
||||
|
||||
|
||||
@@ -15,7 +15,12 @@ import { UserStateSubjectDependencyProvider } from "@bitwarden/common/tools/stat
|
||||
import { StateConstraints } from "@bitwarden/common/tools/types";
|
||||
import { OrganizationId, PolicyId, UserId } from "@bitwarden/common/types/guid";
|
||||
|
||||
import { FakeStateProvider, FakeAccountService, awaitAsync } from "../../../../../common/spec";
|
||||
import {
|
||||
FakeStateProvider,
|
||||
FakeAccountService,
|
||||
awaitAsync,
|
||||
mockAccountInfoWith,
|
||||
} from "../../../../../common/spec";
|
||||
import { CoreProfileMetadata, ProfileContext } from "../metadata/profile-metadata";
|
||||
import { GeneratorConstraints } from "../types";
|
||||
|
||||
@@ -31,21 +36,25 @@ const UnverifiedEmailUser = "UnverifiedEmailUser" as UserId;
|
||||
const accounts: Record<UserId, Account> = {
|
||||
[SomeUser]: {
|
||||
id: SomeUser,
|
||||
name: "some user",
|
||||
email: "some.user@example.com",
|
||||
emailVerified: true,
|
||||
...mockAccountInfoWith({
|
||||
name: "some user",
|
||||
email: "some.user@example.com",
|
||||
}),
|
||||
},
|
||||
[AnotherUser]: {
|
||||
id: AnotherUser,
|
||||
name: "some other user",
|
||||
email: "some.other.user@example.com",
|
||||
emailVerified: true,
|
||||
...mockAccountInfoWith({
|
||||
name: "some other user",
|
||||
email: "some.other.user@example.com",
|
||||
}),
|
||||
},
|
||||
[UnverifiedEmailUser]: {
|
||||
id: UnverifiedEmailUser,
|
||||
name: "a user with an unverfied email",
|
||||
email: "unverified@example.com",
|
||||
emailVerified: false,
|
||||
...mockAccountInfoWith({
|
||||
name: "a user with an unverfied email",
|
||||
email: "unverified@example.com",
|
||||
emailVerified: false,
|
||||
}),
|
||||
},
|
||||
};
|
||||
const accountService = new FakeAccountService(accounts);
|
||||
@@ -57,6 +66,7 @@ const somePolicy = new Policy({
|
||||
id: "" as PolicyId,
|
||||
organizationId: "" as OrganizationId,
|
||||
enabled: true,
|
||||
revisionDate: new Date().toISOString(),
|
||||
});
|
||||
|
||||
const stateProvider = new FakeStateProvider(accountService);
|
||||
|
||||
@@ -8,7 +8,7 @@ import { Vendor } from "@bitwarden/common/tools/extension/vendor/data";
|
||||
import { SemanticLogger, ifEnabledSemanticLoggerProvider } from "@bitwarden/common/tools/log";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
|
||||
import { awaitAsync } from "../../../../../common/spec";
|
||||
import { awaitAsync, mockAccountInfoWith } from "../../../../../common/spec";
|
||||
import {
|
||||
Algorithm,
|
||||
CredentialAlgorithm,
|
||||
@@ -56,9 +56,10 @@ describe("DefaultCredentialGeneratorService", () => {
|
||||
// Use a hard-coded value for mockAccount
|
||||
account = {
|
||||
id: "test-account-id" as UserId,
|
||||
emailVerified: true,
|
||||
email: "test@example.com",
|
||||
name: "Test User",
|
||||
...mockAccountInfoWith({
|
||||
email: "test@example.com",
|
||||
name: "Test User",
|
||||
}),
|
||||
};
|
||||
|
||||
system = {
|
||||
|
||||
@@ -13,9 +13,10 @@ import { observe$PerUserId, sharedStateByUserId } from "../util";
|
||||
import { CATCHALL_SETTINGS } from "./storage";
|
||||
|
||||
/** Strategy for creating usernames using a catchall email address */
|
||||
export class CatchallGeneratorStrategy
|
||||
implements GeneratorStrategy<CatchallGenerationOptions, NoPolicy>
|
||||
{
|
||||
export class CatchallGeneratorStrategy implements GeneratorStrategy<
|
||||
CatchallGenerationOptions,
|
||||
NoPolicy
|
||||
> {
|
||||
/** Instantiates the generation strategy
|
||||
* @param usernameService generates a catchall address for a domain
|
||||
*/
|
||||
|
||||
@@ -16,9 +16,10 @@ const UsernameDigits = Object.freeze({
|
||||
});
|
||||
|
||||
/** Strategy for creating usernames from the EFF wordlist */
|
||||
export class EffUsernameGeneratorStrategy
|
||||
implements GeneratorStrategy<EffUsernameGenerationOptions, NoPolicy>
|
||||
{
|
||||
export class EffUsernameGeneratorStrategy implements GeneratorStrategy<
|
||||
EffUsernameGenerationOptions,
|
||||
NoPolicy
|
||||
> {
|
||||
/** Instantiates the generation strategy
|
||||
* @param usernameService generates a username from EFF word list
|
||||
*/
|
||||
|
||||
@@ -10,8 +10,7 @@ import { Classifier } from "@bitwarden/common/tools/state/classifier";
|
||||
export class OptionsClassifier<
|
||||
Settings,
|
||||
Options extends IntegrationRequest & Settings = IntegrationRequest & Settings,
|
||||
> implements Classifier<Options, Record<string, never>, Settings>
|
||||
{
|
||||
> implements Classifier<Options, Record<string, never>, Settings> {
|
||||
/** Partitions `secret` into its disclosed properties and secret properties.
|
||||
* @param value The object to partition
|
||||
* @returns an object that classifies secrets.
|
||||
|
||||
@@ -14,9 +14,10 @@ import { observe$PerUserId, optionsToEffWordListRequest, sharedStateByUserId } f
|
||||
import { PASSPHRASE_SETTINGS } from "./storage";
|
||||
|
||||
/** Generates passphrases composed of random words */
|
||||
export class PassphraseGeneratorStrategy
|
||||
implements GeneratorStrategy<PassphraseGenerationOptions, PassphraseGeneratorPolicy>
|
||||
{
|
||||
export class PassphraseGeneratorStrategy implements GeneratorStrategy<
|
||||
PassphraseGenerationOptions,
|
||||
PassphraseGeneratorPolicy
|
||||
> {
|
||||
/** instantiates the password generator strategy.
|
||||
* @param legacy generates the passphrase
|
||||
* @param stateProvider provides durable state
|
||||
|
||||
@@ -12,9 +12,10 @@ import { observe$PerUserId, optionsToRandomAsciiRequest, sharedStateByUserId } f
|
||||
import { PASSWORD_SETTINGS } from "./storage";
|
||||
|
||||
/** Generates passwords composed of random characters */
|
||||
export class PasswordGeneratorStrategy
|
||||
implements GeneratorStrategy<PasswordGenerationOptions, PasswordGeneratorPolicy>
|
||||
{
|
||||
export class PasswordGeneratorStrategy implements GeneratorStrategy<
|
||||
PasswordGenerationOptions,
|
||||
PasswordGeneratorPolicy
|
||||
> {
|
||||
/** instantiates the password generator strategy.
|
||||
* @param legacy generates the password
|
||||
*/
|
||||
|
||||
@@ -17,9 +17,10 @@ import { SUBADDRESS_SETTINGS } from "./storage";
|
||||
* For example, if the email address is `jd+xyz@domain.io`,
|
||||
* the subaddress is `xyz`.
|
||||
*/
|
||||
export class SubaddressGeneratorStrategy
|
||||
implements GeneratorStrategy<SubaddressGenerationOptions, NoPolicy>
|
||||
{
|
||||
export class SubaddressGeneratorStrategy implements GeneratorStrategy<
|
||||
SubaddressGenerationOptions,
|
||||
NoPolicy
|
||||
> {
|
||||
/** Instantiates the generation strategy
|
||||
* @param usernameService generates an email subaddress from an email address
|
||||
*/
|
||||
|
||||
@@ -70,6 +70,7 @@ describe("DefaultGeneratorNavigationService", () => {
|
||||
enabled: true,
|
||||
type: PolicyType.PasswordGenerator,
|
||||
data: { overridePasswordType: "password" },
|
||||
revisionDate: new Date().toISOString(),
|
||||
}),
|
||||
]);
|
||||
},
|
||||
|
||||
@@ -8,9 +8,10 @@ import { GeneratorNavigationPolicy } from "./generator-navigation-policy";
|
||||
|
||||
/** Enforces policy for generator navigation options.
|
||||
*/
|
||||
export class GeneratorNavigationEvaluator
|
||||
implements PolicyEvaluator<GeneratorNavigationPolicy, GeneratorNavigation>
|
||||
{
|
||||
export class GeneratorNavigationEvaluator implements PolicyEvaluator<
|
||||
GeneratorNavigationPolicy,
|
||||
GeneratorNavigation
|
||||
> {
|
||||
/** Instantiates the evaluator.
|
||||
* @param policy The policy applied by the evaluator. When this conflicts with
|
||||
* the defaults, the policy takes precedence.
|
||||
|
||||
@@ -17,6 +17,7 @@ function createPolicy(
|
||||
data,
|
||||
enabled,
|
||||
type,
|
||||
revisionDate: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -6,9 +6,9 @@ import { FormsModule } from "@angular/forms";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { SendType } from "@bitwarden/common/tools/send/enums/send-type";
|
||||
import { SendView } from "@bitwarden/common/tools/send/models/view/send.view";
|
||||
import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction";
|
||||
import { SendType } from "@bitwarden/common/tools/send/types/send-type";
|
||||
import {
|
||||
DIALOG_DATA,
|
||||
DialogRef,
|
||||
@@ -44,8 +44,10 @@ export const SendItemDialogResult = Object.freeze({
|
||||
} as const);
|
||||
|
||||
/** A result of the Send add/edit dialog. */
|
||||
export type SendItemDialogResult = (typeof SendItemDialogResult)[keyof typeof SendItemDialogResult];
|
||||
|
||||
export type SendItemDialogResult = {
|
||||
result: (typeof SendItemDialogResult)[keyof typeof SendItemDialogResult];
|
||||
send?: SendView;
|
||||
};
|
||||
/**
|
||||
* Component for adding or editing a send item.
|
||||
*/
|
||||
@@ -93,7 +95,7 @@ export class SendAddEditDialogComponent {
|
||||
*/
|
||||
async onSendCreated(send: SendView) {
|
||||
// FIXME Add dialogService.open send-created dialog
|
||||
this.dialogRef.close(SendItemDialogResult.Saved);
|
||||
this.dialogRef.close({ result: SendItemDialogResult.Saved, send });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -101,14 +103,14 @@ export class SendAddEditDialogComponent {
|
||||
* Handles the event when the send is updated.
|
||||
*/
|
||||
async onSendUpdated(send: SendView) {
|
||||
this.dialogRef.close(SendItemDialogResult.Saved);
|
||||
this.dialogRef.close({ result: SendItemDialogResult.Saved });
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the event when the send is deleted.
|
||||
*/
|
||||
async onSendDeleted() {
|
||||
this.dialogRef.close(SendItemDialogResult.Deleted);
|
||||
this.dialogRef.close({ result: SendItemDialogResult.Deleted });
|
||||
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
@@ -174,4 +176,19 @@ export class SendAddEditDialogComponent {
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the send add/edit dialog in a drawer
|
||||
* @param dialogService Instance of the DialogService.
|
||||
* @param params The parameters for the drawer.
|
||||
* @returns The drawer result.
|
||||
*/
|
||||
static openDrawer(dialogService: DialogService, params: SendItemDialogParams) {
|
||||
return dialogService.openDrawer<SendItemDialogResult, SendItemDialogParams>(
|
||||
SendAddEditDialogComponent,
|
||||
{
|
||||
data: params,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
export * from "./send-form";
|
||||
export { NewSendDropdownComponent } from "./new-send-dropdown/new-send-dropdown.component";
|
||||
export { NewSendDropdownV2Component } from "./new-send-dropdown-v2/new-send-dropdown-v2.component";
|
||||
export * from "./add-edit/send-add-edit-dialog.component";
|
||||
export { SendListItemsContainerComponent } from "./send-list-items-container/send-list-items-container.component";
|
||||
export { SendItemsService } from "./services/send-items.service";
|
||||
export { SendSearchComponent } from "./send-search/send-search.component";
|
||||
export { SendListFiltersComponent } from "./send-list-filters/send-list-filters.component";
|
||||
export { SendListFiltersService } from "./services/send-list-filters.service";
|
||||
export { SendTableComponent } from "./send-table/send-table.component";
|
||||
export { SendListComponent, SendListState } from "./send-list/send-list.component";
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
<button bitButton [bitMenuTriggerFor]="itemOptions" [buttonType]="buttonType()" type="button">
|
||||
@if (!hideIcon()) {
|
||||
<i class="bwi bwi-plus tw-me-2" aria-hidden="true"></i>
|
||||
}
|
||||
{{ (hideIcon() ? "createSend" : "new") | i18n }}
|
||||
</button>
|
||||
<bit-menu #itemOptions>
|
||||
<button bitMenuItem type="button" (click)="onTextSendClick()">
|
||||
<i class="bwi bwi-file-text" slot="start" aria-hidden="true"></i>
|
||||
{{ "sendTypeText" | i18n }}
|
||||
</button>
|
||||
<button bitMenuItem type="button" (click)="onFileSendClick()">
|
||||
<div class="tw-flex tw-items-center tw-gap-2">
|
||||
<i class="bwi bwi-file" slot="start" aria-hidden="true"></i>
|
||||
{{ "sendTypeFile" | i18n }}
|
||||
<app-premium-badge></app-premium-badge>
|
||||
</div>
|
||||
</button>
|
||||
</bit-menu>
|
||||
@@ -0,0 +1,261 @@
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { BehaviorSubject, of } from "rxjs";
|
||||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { SendType } from "@bitwarden/common/tools/send/types/send-type";
|
||||
import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service";
|
||||
|
||||
import { NewSendDropdownV2Component } from "./new-send-dropdown-v2.component";
|
||||
|
||||
describe("NewSendDropdownV2Component", () => {
|
||||
let component: NewSendDropdownV2Component;
|
||||
let fixture: ComponentFixture<NewSendDropdownV2Component>;
|
||||
let billingService: MockProxy<BillingAccountProfileStateService>;
|
||||
let accountService: MockProxy<AccountService>;
|
||||
let premiumUpgradeService: MockProxy<PremiumUpgradePromptService>;
|
||||
|
||||
beforeEach(async () => {
|
||||
billingService = mock<BillingAccountProfileStateService>();
|
||||
accountService = mock<AccountService>();
|
||||
premiumUpgradeService = mock<PremiumUpgradePromptService>();
|
||||
|
||||
// Default: user has premium
|
||||
accountService.activeAccount$ = of({ id: "user-123" } as any);
|
||||
billingService.hasPremiumFromAnySource$.mockReturnValue(of(true));
|
||||
|
||||
const i18nService = mock<I18nService>();
|
||||
i18nService.t.mockImplementation((key: string) => key);
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [NewSendDropdownV2Component],
|
||||
providers: [
|
||||
{ provide: BillingAccountProfileStateService, useValue: billingService },
|
||||
{ provide: AccountService, useValue: accountService },
|
||||
{ provide: PremiumUpgradePromptService, useValue: premiumUpgradeService },
|
||||
{ provide: I18nService, useValue: i18nService },
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(NewSendDropdownV2Component);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it("should create", () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
describe("input signals", () => {
|
||||
it("has correct default input values", () => {
|
||||
expect(component.hideIcon()).toBe(false);
|
||||
expect(component.buttonType()).toBe("primary");
|
||||
});
|
||||
|
||||
it("accepts input signal values", () => {
|
||||
fixture.componentRef.setInput("hideIcon", true);
|
||||
fixture.componentRef.setInput("buttonType", "secondary");
|
||||
|
||||
expect(component.hideIcon()).toBe(true);
|
||||
expect(component.buttonType()).toBe("secondary");
|
||||
});
|
||||
});
|
||||
|
||||
describe("premium status detection", () => {
|
||||
it("hasNoPremium is false when user has premium", () => {
|
||||
billingService.hasPremiumFromAnySource$.mockReturnValue(of(true));
|
||||
accountService.activeAccount$ = of({ id: "user-123" } as any);
|
||||
|
||||
fixture = TestBed.createComponent(NewSendDropdownV2Component);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component["hasNoPremium"]()).toBe(false);
|
||||
});
|
||||
|
||||
it("hasNoPremium is true when user lacks premium", () => {
|
||||
billingService.hasPremiumFromAnySource$.mockReturnValue(of(false));
|
||||
accountService.activeAccount$ = of({ id: "user-123" } as any);
|
||||
|
||||
fixture = TestBed.createComponent(NewSendDropdownV2Component);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component["hasNoPremium"]()).toBe(true);
|
||||
});
|
||||
|
||||
it("hasNoPremium defaults to true when no active account", () => {
|
||||
accountService.activeAccount$ = of(null);
|
||||
|
||||
fixture = TestBed.createComponent(NewSendDropdownV2Component);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component["hasNoPremium"]()).toBe(true);
|
||||
});
|
||||
|
||||
it("hasNoPremium updates reactively when premium status changes", async () => {
|
||||
const premiumSubject = new BehaviorSubject(false);
|
||||
billingService.hasPremiumFromAnySource$.mockReturnValue(premiumSubject.asObservable());
|
||||
|
||||
fixture = TestBed.createComponent(NewSendDropdownV2Component);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component["hasNoPremium"]()).toBe(true);
|
||||
|
||||
premiumSubject.next(true);
|
||||
await fixture.whenStable();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component["hasNoPremium"]()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("text send functionality", () => {
|
||||
it("onTextSendClick emits SendType.Text", () => {
|
||||
const emitSpy = jest.fn();
|
||||
component.addSend.subscribe(emitSpy);
|
||||
|
||||
component["onTextSendClick"]();
|
||||
|
||||
expect(emitSpy).toHaveBeenCalledWith(SendType.Text);
|
||||
});
|
||||
|
||||
it("allows text send without premium", () => {
|
||||
billingService.hasPremiumFromAnySource$.mockReturnValue(of(false));
|
||||
|
||||
fixture = TestBed.createComponent(NewSendDropdownV2Component);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
|
||||
const emitSpy = jest.fn();
|
||||
component.addSend.subscribe(emitSpy);
|
||||
|
||||
component["onTextSendClick"]();
|
||||
|
||||
expect(emitSpy).toHaveBeenCalledWith(SendType.Text);
|
||||
expect(premiumUpgradeService.promptForPremium).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("file send premium gating", () => {
|
||||
it("onFileSendClick emits SendType.File when user has premium", async () => {
|
||||
const emitSpy = jest.fn();
|
||||
component.addSend.subscribe(emitSpy);
|
||||
|
||||
await component["onFileSendClick"]();
|
||||
|
||||
expect(emitSpy).toHaveBeenCalledWith(SendType.File);
|
||||
expect(premiumUpgradeService.promptForPremium).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("onFileSendClick shows premium prompt without premium", async () => {
|
||||
billingService.hasPremiumFromAnySource$.mockReturnValue(of(false));
|
||||
premiumUpgradeService.promptForPremium.mockResolvedValue();
|
||||
|
||||
fixture = TestBed.createComponent(NewSendDropdownV2Component);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
|
||||
const emitSpy = jest.fn();
|
||||
component.addSend.subscribe(emitSpy);
|
||||
|
||||
await component["onFileSendClick"]();
|
||||
|
||||
expect(premiumUpgradeService.promptForPremium).toHaveBeenCalled();
|
||||
expect(emitSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not emit file send type when premium prompt is shown", async () => {
|
||||
billingService.hasPremiumFromAnySource$.mockReturnValue(of(false));
|
||||
|
||||
fixture = TestBed.createComponent(NewSendDropdownV2Component);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
|
||||
const emitSpy = jest.fn();
|
||||
component.addSend.subscribe(emitSpy);
|
||||
|
||||
await component["onFileSendClick"]();
|
||||
|
||||
expect(emitSpy).not.toHaveBeenCalledWith(SendType.File);
|
||||
});
|
||||
|
||||
it("allows file send after user gains premium", async () => {
|
||||
const premiumSubject = new BehaviorSubject(false);
|
||||
billingService.hasPremiumFromAnySource$.mockReturnValue(premiumSubject.asObservable());
|
||||
|
||||
fixture = TestBed.createComponent(NewSendDropdownV2Component);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
|
||||
// Initially no premium
|
||||
let emitSpy = jest.fn();
|
||||
component.addSend.subscribe(emitSpy);
|
||||
await component["onFileSendClick"]();
|
||||
expect(premiumUpgradeService.promptForPremium).toHaveBeenCalled();
|
||||
|
||||
// Gain premium
|
||||
premiumSubject.next(true);
|
||||
await fixture.whenStable();
|
||||
fixture.detectChanges();
|
||||
|
||||
// Now should emit
|
||||
emitSpy = jest.fn();
|
||||
component.addSend.subscribe(emitSpy);
|
||||
await component["onFileSendClick"]();
|
||||
expect(emitSpy).toHaveBeenCalledWith(SendType.File);
|
||||
});
|
||||
});
|
||||
|
||||
describe("edge cases", () => {
|
||||
it("handles null account without errors", () => {
|
||||
accountService.activeAccount$ = of(null);
|
||||
|
||||
expect(() => {
|
||||
fixture = TestBed.createComponent(NewSendDropdownV2Component);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
}).not.toThrow();
|
||||
|
||||
expect(component["hasNoPremium"]()).toBe(true);
|
||||
});
|
||||
|
||||
it("handles rapid clicks without race conditions", async () => {
|
||||
const emitSpy = jest.fn();
|
||||
component.addSend.subscribe(emitSpy);
|
||||
|
||||
// Rapid text send clicks
|
||||
component["onTextSendClick"]();
|
||||
component["onTextSendClick"]();
|
||||
component["onTextSendClick"]();
|
||||
|
||||
expect(emitSpy).toHaveBeenCalledTimes(3);
|
||||
|
||||
// Rapid file send clicks (with premium)
|
||||
await Promise.all([
|
||||
component["onFileSendClick"](),
|
||||
component["onFileSendClick"](),
|
||||
component["onFileSendClick"](),
|
||||
]);
|
||||
|
||||
expect(emitSpy).toHaveBeenCalledTimes(6); // 3 text + 3 file
|
||||
});
|
||||
|
||||
it("cleans up subscriptions on destroy", () => {
|
||||
const subscription = component["hasNoPremium"];
|
||||
|
||||
fixture.destroy();
|
||||
|
||||
// Signal should still exist but component cleanup handled by Angular
|
||||
expect(() => subscription()).not.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,59 @@
|
||||
import { ChangeDetectionStrategy, Component, inject, input, output } from "@angular/core";
|
||||
import { toSignal } from "@angular/core/rxjs-interop";
|
||||
import { map, of, switchMap } from "rxjs";
|
||||
|
||||
import { PremiumBadgeComponent } from "@bitwarden/angular/billing/components/premium-badge";
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions";
|
||||
import { SendType } from "@bitwarden/common/tools/send/types/send-type";
|
||||
import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service";
|
||||
import { ButtonModule, ButtonType, MenuModule } from "@bitwarden/components";
|
||||
|
||||
// Desktop-specific version of NewSendDropdownComponent.
|
||||
// Unlike the shared library version, this component emits events instead of using Angular Router,
|
||||
// which aligns with Desktop's modal-based architecture.
|
||||
@Component({
|
||||
selector: "tools-new-send-dropdown-v2",
|
||||
templateUrl: "new-send-dropdown-v2.component.html",
|
||||
imports: [JslibModule, ButtonModule, MenuModule, PremiumBadgeComponent],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class NewSendDropdownV2Component {
|
||||
readonly hideIcon = input<boolean>(false);
|
||||
readonly buttonType = input<ButtonType>("primary");
|
||||
|
||||
readonly addSend = output<SendType>();
|
||||
|
||||
protected sendType = SendType;
|
||||
|
||||
private readonly billingAccountProfileStateService = inject(BillingAccountProfileStateService);
|
||||
private readonly accountService = inject(AccountService);
|
||||
private readonly premiumUpgradePromptService = inject(PremiumUpgradePromptService);
|
||||
|
||||
protected readonly hasNoPremium = toSignal(
|
||||
this.accountService.activeAccount$.pipe(
|
||||
switchMap((account) => {
|
||||
if (!account) {
|
||||
return of(true);
|
||||
}
|
||||
return this.billingAccountProfileStateService
|
||||
.hasPremiumFromAnySource$(account.id)
|
||||
.pipe(map((hasPremium) => !hasPremium));
|
||||
}),
|
||||
),
|
||||
{ initialValue: true },
|
||||
);
|
||||
|
||||
protected onTextSendClick(): void {
|
||||
this.addSend.emit(SendType.Text);
|
||||
}
|
||||
|
||||
protected async onFileSendClick(): Promise<void> {
|
||||
if (this.hasNoPremium()) {
|
||||
await this.premiumUpgradePromptService.promptForPremium();
|
||||
} else {
|
||||
this.addSend.emit(SendType.File);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
<button bitButton [bitMenuTriggerFor]="itemOptions" [buttonType]="buttonType" type="button">
|
||||
<i *ngIf="!hideIcon" class="bwi bwi-plus" aria-hidden="true"></i>
|
||||
<i *ngIf="!hideIcon" class="bwi bwi-plus tw-me-2" aria-hidden="true"></i>
|
||||
{{ (hideIcon ? "createSend" : "new") | i18n }}
|
||||
</button>
|
||||
<bit-menu #itemOptions>
|
||||
|
||||
@@ -7,7 +7,7 @@ import { PremiumBadgeComponent } from "@bitwarden/angular/billing/components/pre
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions";
|
||||
import { SendType } from "@bitwarden/common/tools/send/enums/send-type";
|
||||
import { SendType } from "@bitwarden/common/tools/send/types/send-type";
|
||||
import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service";
|
||||
import { ButtonModule, ButtonType, MenuModule } from "@bitwarden/components";
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { SendType } from "@bitwarden/common/tools/send/enums/send-type";
|
||||
import { Send } from "@bitwarden/common/tools/send/models/domain/send";
|
||||
import { SendType } from "@bitwarden/common/tools/send/types/send-type";
|
||||
import { SendId } from "@bitwarden/common/types/guid";
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { mock } from "jest-mock-extended";
|
||||
import { of } from "rxjs";
|
||||
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { SendView } from "@bitwarden/common/tools/send/models/view/send.view";
|
||||
import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction";
|
||||
import { SendType } from "@bitwarden/common/tools/send/types/send-type";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
import { CredentialGeneratorService } from "@bitwarden/generator-core";
|
||||
|
||||
import { SendFormContainer } from "../../send-form-container";
|
||||
|
||||
import { SendOptionsComponent } from "./send-options.component";
|
||||
|
||||
describe("SendOptionsComponent", () => {
|
||||
let component: SendOptionsComponent;
|
||||
let fixture: ComponentFixture<SendOptionsComponent>;
|
||||
const mockSendFormContainer = mock<SendFormContainer>();
|
||||
const mockAccountService = mock<AccountService>();
|
||||
|
||||
beforeAll(() => {
|
||||
mockAccountService.activeAccount$ = of({ id: "myTestAccount" } as Account);
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [SendOptionsComponent],
|
||||
declarations: [],
|
||||
providers: [
|
||||
{ provide: SendFormContainer, useValue: mockSendFormContainer },
|
||||
{ provide: DialogService, useValue: mock<DialogService>() },
|
||||
{ provide: SendApiService, useValue: mock<SendApiService>() },
|
||||
{ provide: PolicyService, useValue: mock<PolicyService>() },
|
||||
{ provide: I18nService, useValue: mock<I18nService>() },
|
||||
{ provide: ToastService, useValue: mock<ToastService>() },
|
||||
{ provide: CredentialGeneratorService, useValue: mock<CredentialGeneratorService>() },
|
||||
{ provide: AccountService, useValue: mockAccountService },
|
||||
{ provide: PlatformUtilsService, useValue: mock<PlatformUtilsService>() },
|
||||
],
|
||||
}).compileComponents();
|
||||
fixture = TestBed.createComponent(SendOptionsComponent);
|
||||
component = fixture.componentInstance;
|
||||
component.config = { areSendsAllowed: true, mode: "add", sendType: SendType.Text };
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("should create", () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should emit a null password when password textbox is empty", async () => {
|
||||
const newSend = {} as SendView;
|
||||
mockSendFormContainer.patchSend.mockImplementation((updateFn) => updateFn(newSend));
|
||||
component.sendOptionsForm.patchValue({ password: "testing" });
|
||||
expect(newSend.password).toBe("testing");
|
||||
component.sendOptionsForm.patchValue({ password: "" });
|
||||
expect(newSend.password).toBe(null);
|
||||
});
|
||||
});
|
||||
@@ -4,7 +4,7 @@ import { CommonModule } from "@angular/common";
|
||||
import { Component, Input, OnInit } from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { FormBuilder, ReactiveFormsModule } from "@angular/forms";
|
||||
import { BehaviorSubject, firstValueFrom, map, switchMap } from "rxjs";
|
||||
import { BehaviorSubject, firstValueFrom, map, switchMap, tap } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
@@ -12,6 +12,7 @@ import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { pin } from "@bitwarden/common/tools/rx";
|
||||
import { SendView } from "@bitwarden/common/tools/send/models/view/send.view";
|
||||
import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction";
|
||||
@@ -112,18 +113,27 @@ export class SendOptionsComponent implements OnInit {
|
||||
this.disableHideEmail = disableHideEmail;
|
||||
});
|
||||
|
||||
this.sendOptionsForm.valueChanges.pipe(takeUntilDestroyed()).subscribe((value) => {
|
||||
this.sendFormContainer.patchSend((send) => {
|
||||
Object.assign(send, {
|
||||
maxAccessCount: value.maxAccessCount,
|
||||
accessCount: value.accessCount,
|
||||
password: value.password,
|
||||
hideEmail: value.hideEmail,
|
||||
notes: value.notes,
|
||||
this.sendOptionsForm.valueChanges
|
||||
.pipe(
|
||||
tap((value) => {
|
||||
if (Utils.isNullOrWhitespace(value.password)) {
|
||||
value.password = null;
|
||||
}
|
||||
}),
|
||||
takeUntilDestroyed(),
|
||||
)
|
||||
.subscribe((value) => {
|
||||
this.sendFormContainer.patchSend((send) => {
|
||||
Object.assign(send, {
|
||||
maxAccessCount: value.maxAccessCount,
|
||||
accessCount: value.accessCount,
|
||||
password: value.password,
|
||||
hideEmail: value.hideEmail,
|
||||
notes: value.notes,
|
||||
});
|
||||
return send;
|
||||
});
|
||||
return send;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
generatePassword = async () => {
|
||||
|
||||
@@ -9,8 +9,8 @@ import { firstValueFrom } from "rxjs";
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { SendType } from "@bitwarden/common/tools/send/enums/send-type";
|
||||
import { SendView } from "@bitwarden/common/tools/send/models/view/send.view";
|
||||
import { SendType } from "@bitwarden/common/tools/send/types/send-type";
|
||||
import {
|
||||
SectionComponent,
|
||||
SectionHeaderComponent,
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
<bit-section [formGroup]="sendFileDetailsForm">
|
||||
<div *ngIf="config().mode === 'edit'">
|
||||
<div bitTypography="body2" class="tw-text-muted">{{ "file" | i18n }}</div>
|
||||
<div data-testid="file-name">{{ originalSendView().file.fileName }}</div>
|
||||
<div class="tw-text-wrap tw-break-all" data-testid="file-name">
|
||||
{{ originalSendView().file.fileName }}
|
||||
</div>
|
||||
<div data-testid="file-size" class="tw-text-muted">{{ originalSendView().file.sizeName }}</div>
|
||||
</div>
|
||||
<bit-form-field *ngIf="config().mode !== 'edit'">
|
||||
|
||||
@@ -4,9 +4,9 @@ import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { FormBuilder, Validators, ReactiveFormsModule, FormsModule } from "@angular/forms";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { SendType } from "@bitwarden/common/tools/send/enums/send-type";
|
||||
import { SendFileView } from "@bitwarden/common/tools/send/models/view/send-file.view";
|
||||
import { SendView } from "@bitwarden/common/tools/send/models/view/send.view";
|
||||
import { SendType } from "@bitwarden/common/tools/send/types/send-type";
|
||||
import {
|
||||
ButtonModule,
|
||||
FormFieldModule,
|
||||
|
||||
@@ -18,9 +18,8 @@ import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { FormBuilder, ReactiveFormsModule } from "@angular/forms";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { SendType } from "@bitwarden/common/tools/send/enums/send-type";
|
||||
import { SendView } from "@bitwarden/common/tools/send/models/view/send.view";
|
||||
import { SendType } from "@bitwarden/common/tools/send/types/send-type";
|
||||
import {
|
||||
AsyncActionsModule,
|
||||
BitSubmitDirective,
|
||||
@@ -227,10 +226,6 @@ export class SendFormComponent implements AfterViewInit, OnInit, OnChanges, Send
|
||||
return;
|
||||
}
|
||||
|
||||
if (Utils.isNullOrWhitespace(this.updatedSendView.password)) {
|
||||
this.updatedSendView.password = null;
|
||||
}
|
||||
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
|
||||
@@ -7,8 +7,8 @@ import { PolicyService } from "@bitwarden/common/admin-console/abstractions/poli
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { SendType } from "@bitwarden/common/tools/send/enums/send-type";
|
||||
import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction";
|
||||
import { SendType } from "@bitwarden/common/tools/send/types/send-type";
|
||||
import { SendId } from "@bitwarden/common/types/guid";
|
||||
|
||||
import {
|
||||
|
||||
@@ -8,6 +8,7 @@ import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { mockAccountInfoWith } from "@bitwarden/common/spec";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { ChipSelectComponent } from "@bitwarden/components";
|
||||
|
||||
@@ -31,9 +32,11 @@ describe("SendListFiltersComponent", () => {
|
||||
|
||||
accountService.activeAccount$ = of({
|
||||
id: userId,
|
||||
email: "test@email.com",
|
||||
emailVerified: true,
|
||||
name: "Test User",
|
||||
...mockAccountInfoWith({
|
||||
email: "test@email.com",
|
||||
name: "Test User",
|
||||
emailVerified: true,
|
||||
}),
|
||||
});
|
||||
billingAccountProfileStateService.hasPremiumFromAnySource$.mockReturnValue(of(true));
|
||||
|
||||
|
||||
@@ -10,9 +10,9 @@ import { EnvironmentService } from "@bitwarden/common/platform/abstractions/envi
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { SendType } from "@bitwarden/common/tools/send/enums/send-type";
|
||||
import { SendView } from "@bitwarden/common/tools/send/models/view/send.view";
|
||||
import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction";
|
||||
import { SendType } from "@bitwarden/common/tools/send/types/send-type";
|
||||
import {
|
||||
BadgeModule,
|
||||
ButtonModule,
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
@if (loading()) {
|
||||
<bit-spinner />
|
||||
} @else {
|
||||
@if (showSearchBar()) {
|
||||
<!-- Search Bar - hidden when no Sends exist -->
|
||||
<tools-send-search></tools-send-search>
|
||||
}
|
||||
<tools-send-table
|
||||
[dataSource]="dataSource"
|
||||
[disableSend]="disableSend()"
|
||||
(editSend)="onEditSend($event)"
|
||||
(copySend)="onCopySend($event)"
|
||||
(removePassword)="onRemovePassword($event)"
|
||||
(deleteSend)="onDeleteSend($event)"
|
||||
/>
|
||||
@if (noSearchResults()) {
|
||||
<!-- No Sends from Search results -->
|
||||
<bit-no-items [icon]="noItemIcon">
|
||||
<ng-container slot="title">{{ "sendsTitleNoSearchResults" | i18n }}</ng-container>
|
||||
<ng-container slot="description">{{ "sendsBodyNoSearchResults" | i18n }}</ng-container>
|
||||
</bit-no-items>
|
||||
} @else if (listState() === sendListState.NoResults || listState() === sendListState.Empty) {
|
||||
<!-- No Sends from Filter results ( File/Text ) -->
|
||||
<!-- No Sends exist at all -->
|
||||
<bit-no-items [icon]="noItemIcon">
|
||||
<ng-container slot="title">{{ "sendsTitleNoItems" | i18n }}</ng-container>
|
||||
<ng-container slot="description">{{ "sendsBodyNoItems" | i18n }}</ng-container>
|
||||
<ng-content select="[slot='empty-button']" slot="button" />
|
||||
</bit-no-items>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { of } from "rxjs";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
|
||||
import { SendItemsService } from "../services/send-items.service";
|
||||
|
||||
import { SendListComponent } from "./send-list.component";
|
||||
|
||||
describe("SendListComponent", () => {
|
||||
let component: SendListComponent;
|
||||
let fixture: ComponentFixture<SendListComponent>;
|
||||
let i18nService: MockProxy<I18nService>;
|
||||
let sendItemsService: MockProxy<SendItemsService>;
|
||||
|
||||
beforeEach(async () => {
|
||||
i18nService = mock<I18nService>();
|
||||
i18nService.t.mockImplementation((key) => key);
|
||||
|
||||
// Mock SendItemsService for SendSearchComponent child component
|
||||
sendItemsService = mock<SendItemsService>();
|
||||
sendItemsService.latestSearchText$ = of("");
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [SendListComponent],
|
||||
providers: [
|
||||
{ provide: I18nService, useValue: i18nService },
|
||||
{ provide: SendItemsService, useValue: sendItemsService },
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(SendListComponent);
|
||||
component = fixture.componentInstance;
|
||||
});
|
||||
|
||||
it("should create", () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should display empty state when listState is Empty", () => {
|
||||
fixture.componentRef.setInput("sends", []);
|
||||
fixture.componentRef.setInput("listState", "Empty");
|
||||
fixture.detectChanges();
|
||||
|
||||
const compiled = fixture.nativeElement;
|
||||
expect(compiled.textContent).toContain("sendsTitleNoItems");
|
||||
});
|
||||
|
||||
it("should display no results state when listState is NoResults", () => {
|
||||
fixture.componentRef.setInput("sends", []);
|
||||
fixture.componentRef.setInput("listState", "NoResults");
|
||||
fixture.detectChanges();
|
||||
|
||||
const compiled = fixture.nativeElement;
|
||||
// Component shows same empty state for both Empty and NoResults states
|
||||
expect(compiled.textContent).toContain("sendsTitleNoItems");
|
||||
});
|
||||
|
||||
it("should emit editSend event when send is edited", () => {
|
||||
const editSpy = jest.fn();
|
||||
component.editSend.subscribe(editSpy);
|
||||
|
||||
const mockSend = { id: "test-id", name: "Test Send" } as any;
|
||||
component["onEditSend"](mockSend);
|
||||
|
||||
expect(editSpy).toHaveBeenCalledWith(mockSend);
|
||||
});
|
||||
|
||||
it("should emit copySend event when send link is copied", () => {
|
||||
const copySpy = jest.fn();
|
||||
component.copySend.subscribe(copySpy);
|
||||
|
||||
const mockSend = { id: "test-id", name: "Test Send" } as any;
|
||||
component["onCopySend"](mockSend);
|
||||
|
||||
expect(copySpy).toHaveBeenCalledWith(mockSend);
|
||||
});
|
||||
|
||||
it("should emit deleteSend event when send is deleted", () => {
|
||||
const deleteSpy = jest.fn();
|
||||
component.deleteSend.subscribe(deleteSpy);
|
||||
|
||||
const mockSend = { id: "test-id", name: "Test Send" } as any;
|
||||
component["onDeleteSend"](mockSend);
|
||||
|
||||
expect(deleteSpy).toHaveBeenCalledWith(mockSend);
|
||||
});
|
||||
});
|
||||
94
libs/tools/send/send-ui/src/send-list/send-list.component.ts
Normal file
94
libs/tools/send/send-ui/src/send-list/send-list.component.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { ChangeDetectionStrategy, Component, computed, effect, input, output } from "@angular/core";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { NoResults, NoSendsIcon } from "@bitwarden/assets/svg";
|
||||
import { SendView } from "@bitwarden/common/tools/send/models/view/send.view";
|
||||
import {
|
||||
ButtonModule,
|
||||
NoItemsModule,
|
||||
SpinnerComponent,
|
||||
TableDataSource,
|
||||
} from "@bitwarden/components";
|
||||
|
||||
import { SendSearchComponent } from "../send-search/send-search.component";
|
||||
import { SendTableComponent } from "../send-table/send-table.component";
|
||||
|
||||
/** A state of the Send list UI. */
|
||||
export const SendListState = Object.freeze({
|
||||
/** No Sends exist at all (File or Text). */
|
||||
Empty: "Empty",
|
||||
/** Sends exist, but none match the current Side Nav Filter (File or Text). */
|
||||
NoResults: "NoResults",
|
||||
} as const);
|
||||
|
||||
/** A state of the Send list UI. */
|
||||
export type SendListState = (typeof SendListState)[keyof typeof SendListState];
|
||||
|
||||
/**
|
||||
* A container component for displaying the Send list with search, table, and empty states.
|
||||
* Handles the presentation layer while delegating data management to services.
|
||||
*/
|
||||
@Component({
|
||||
selector: "tools-send-list",
|
||||
templateUrl: "./send-list.component.html",
|
||||
imports: [
|
||||
CommonModule,
|
||||
JslibModule,
|
||||
ButtonModule,
|
||||
NoItemsModule,
|
||||
SpinnerComponent,
|
||||
SendSearchComponent,
|
||||
SendTableComponent,
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class SendListComponent {
|
||||
protected readonly noItemIcon = NoSendsIcon;
|
||||
protected readonly noResultsIcon = NoResults;
|
||||
protected readonly sendListState = SendListState;
|
||||
|
||||
readonly sends = input.required<SendView[]>();
|
||||
readonly loading = input<boolean>(false);
|
||||
readonly disableSend = input<boolean>(false);
|
||||
readonly listState = input<SendListState | null>(null);
|
||||
readonly searchText = input<string>("");
|
||||
|
||||
protected readonly showSearchBar = computed(
|
||||
() => this.sends().length > 0 || this.searchText().length > 0,
|
||||
);
|
||||
|
||||
protected readonly noSearchResults = computed(
|
||||
() => this.showSearchBar() && this.sends().length === 0,
|
||||
);
|
||||
|
||||
// Reusable data source instance - updated reactively when sends change
|
||||
protected readonly dataSource = new TableDataSource<SendView>();
|
||||
|
||||
constructor() {
|
||||
effect(() => {
|
||||
this.dataSource.data = this.sends();
|
||||
});
|
||||
}
|
||||
|
||||
readonly editSend = output<SendView>();
|
||||
readonly copySend = output<SendView>();
|
||||
readonly removePassword = output<SendView>();
|
||||
readonly deleteSend = output<SendView>();
|
||||
|
||||
protected onEditSend(send: SendView): void {
|
||||
this.editSend.emit(send);
|
||||
}
|
||||
|
||||
protected onCopySend(send: SendView): void {
|
||||
this.copySend.emit(send);
|
||||
}
|
||||
|
||||
protected onRemovePassword(send: SendView): void {
|
||||
this.removePassword.emit(send);
|
||||
}
|
||||
|
||||
protected onDeleteSend(send: SendView): void {
|
||||
this.deleteSend.emit(send);
|
||||
}
|
||||
}
|
||||
114
libs/tools/send/send-ui/src/send-table/send-table.component.html
Normal file
114
libs/tools/send/send-ui/src/send-table/send-table.component.html
Normal file
@@ -0,0 +1,114 @@
|
||||
<div class="tw-@container/send-table">
|
||||
<bit-table [dataSource]="dataSource()">
|
||||
<ng-container header>
|
||||
<tr>
|
||||
<th bitCell bitSortable="name" default>{{ "name" | i18n }}</th>
|
||||
<th bitCell bitSortable="deletionDate" class="@lg/send-table:tw-table-cell tw-hidden">
|
||||
{{ "deletionDate" | i18n }}
|
||||
</th>
|
||||
<th bitCell>{{ "options" | i18n }}</th>
|
||||
</tr>
|
||||
</ng-container>
|
||||
<ng-template body let-rows$>
|
||||
<tr bitRow *ngFor="let s of rows$ | async">
|
||||
<td bitCell (click)="onEditSend(s)" class="tw-cursor-pointer">
|
||||
<div class="tw-flex tw-gap-2 tw-items-center">
|
||||
<span aria-hidden="true">
|
||||
@if (s.type == sendType.File) {
|
||||
<i class="bwi bwi-fw bwi-lg bwi-file"></i>
|
||||
}
|
||||
@if (s.type == sendType.Text) {
|
||||
<i class="bwi bwi-fw bwi-lg bwi-file-text"></i>
|
||||
}
|
||||
</span>
|
||||
<button type="button" bitLink>
|
||||
{{ s.name }}
|
||||
</button>
|
||||
@if (s.disabled) {
|
||||
<i
|
||||
class="bwi bwi-exclamation-triangle"
|
||||
appStopProp
|
||||
title="{{ 'disabled' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="tw-sr-only">{{ "disabled" | i18n }}</span>
|
||||
}
|
||||
@if (s.authType !== authType.None) {
|
||||
@let titleKey =
|
||||
s.authType === authType.Email ? "emailProtected" : "passwordProtected";
|
||||
<i
|
||||
class="bwi bwi-lock"
|
||||
appStopProp
|
||||
title="{{ titleKey | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="tw-sr-only">{{ titleKey | i18n }}</span>
|
||||
}
|
||||
@if (s.maxAccessCountReached) {
|
||||
<i
|
||||
class="bwi bwi-exclamation-triangle"
|
||||
appStopProp
|
||||
title="{{ 'maxAccessCountReached' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="tw-sr-only">{{ "maxAccessCountReached" | i18n }}</span>
|
||||
}
|
||||
@if (s.expired) {
|
||||
<i
|
||||
class="bwi bwi-clock"
|
||||
appStopProp
|
||||
title="{{ 'expired' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="tw-sr-only">{{ "expired" | i18n }}</span>
|
||||
}
|
||||
@if (s.pendingDelete) {
|
||||
<i
|
||||
class="bwi bwi-trash"
|
||||
appStopProp
|
||||
title="{{ 'pendingDeletion' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="tw-sr-only">{{ "pendingDeletion" | i18n }}</span>
|
||||
}
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
bitCell
|
||||
(click)="onEditSend(s)"
|
||||
class="tw-text-muted tw-cursor-pointer @lg/send-table:tw-table-cell tw-hidden"
|
||||
>
|
||||
<small bitTypography="body2" appStopProp>
|
||||
{{ s.deletionDate | date: "medium" }}
|
||||
</small>
|
||||
</td>
|
||||
<td bitCell class="tw-w-0 tw-text-right">
|
||||
<button
|
||||
type="button"
|
||||
[bitMenuTriggerFor]="sendOptions"
|
||||
bitIconButton="bwi-ellipsis-v"
|
||||
label="{{ 'options' | i18n }}"
|
||||
></button>
|
||||
<bit-menu #sendOptions>
|
||||
<button type="button" bitMenuItem (click)="onCopy(s)">
|
||||
<i class="bwi bwi-fw bwi-clone" aria-hidden="true"></i>
|
||||
{{ "copySendLink" | i18n }}
|
||||
</button>
|
||||
@if (s.password && !disableSend()) {
|
||||
<button type="button" bitMenuItem (click)="onRemovePassword(s)">
|
||||
<i class="bwi bwi-fw bwi-close" aria-hidden="true"></i>
|
||||
{{ "removePassword" | i18n }}
|
||||
</button>
|
||||
}
|
||||
<button type="button" bitMenuItem (click)="onDelete(s)">
|
||||
<span class="tw-text-danger">
|
||||
<i class="bwi bwi-fw bwi-trash" aria-hidden="true"></i>
|
||||
{{ "delete" | i18n }}
|
||||
</span>
|
||||
</button>
|
||||
</bit-menu>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</bit-table>
|
||||
</div>
|
||||
@@ -0,0 +1,111 @@
|
||||
import { Meta, StoryObj, moduleMetadata } from "@storybook/angular";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { SendView } from "@bitwarden/common/tools/send/models/view/send.view";
|
||||
import { AuthType } from "@bitwarden/common/tools/send/types/auth-type";
|
||||
import { SendType } from "@bitwarden/common/tools/send/types/send-type";
|
||||
import { TableDataSource, I18nMockService } from "@bitwarden/components";
|
||||
|
||||
import { SendTableComponent } from "./send-table.component";
|
||||
|
||||
function createMockSend(id: number, overrides: Partial<SendView> = {}): SendView {
|
||||
const send = new SendView();
|
||||
|
||||
send.id = `send-${id}`;
|
||||
send.name = "My Send";
|
||||
send.type = SendType.Text;
|
||||
send.authType = AuthType.None;
|
||||
send.deletionDate = new Date("2030-01-01T12:00:00Z");
|
||||
send.password = null as any;
|
||||
|
||||
Object.assign(send, overrides);
|
||||
|
||||
return send;
|
||||
}
|
||||
|
||||
const dataSource = new TableDataSource<SendView>();
|
||||
dataSource.data = [
|
||||
createMockSend(0, {
|
||||
name: "Project Documentation",
|
||||
type: SendType.Text,
|
||||
}),
|
||||
createMockSend(1, {
|
||||
name: "Meeting Notes",
|
||||
type: SendType.File,
|
||||
}),
|
||||
createMockSend(2, {
|
||||
name: "Password Protected Send",
|
||||
type: SendType.Text,
|
||||
authType: AuthType.Password,
|
||||
password: "123",
|
||||
}),
|
||||
createMockSend(3, {
|
||||
name: "Email Protected Send",
|
||||
type: SendType.Text,
|
||||
authType: AuthType.Email,
|
||||
emails: ["ckent@dailyplanet.com"],
|
||||
}),
|
||||
createMockSend(4, {
|
||||
name: "Disabled Send",
|
||||
type: SendType.Text,
|
||||
disabled: true,
|
||||
}),
|
||||
createMockSend(5, {
|
||||
name: "Expired Send",
|
||||
type: SendType.File,
|
||||
expirationDate: new Date("2025-12-01T00:00:00Z"),
|
||||
}),
|
||||
createMockSend(6, {
|
||||
name: "Max Access Reached",
|
||||
type: SendType.Text,
|
||||
authType: AuthType.Password,
|
||||
maxAccessCount: 5,
|
||||
accessCount: 5,
|
||||
password: "123",
|
||||
}),
|
||||
];
|
||||
|
||||
export default {
|
||||
title: "Tools/Sends/Send Table",
|
||||
component: SendTableComponent,
|
||||
decorators: [
|
||||
moduleMetadata({
|
||||
providers: [
|
||||
{
|
||||
provide: I18nService,
|
||||
useFactory: () => {
|
||||
return new I18nMockService({
|
||||
name: "Name",
|
||||
deletionDate: "Deletion Date",
|
||||
options: "Options",
|
||||
disabled: "Disabled",
|
||||
passwordProtected: "Password protected",
|
||||
emailProtected: "Email protected",
|
||||
maxAccessCountReached: "Max access count reached",
|
||||
expired: "Expired",
|
||||
pendingDeletion: "Pending deletion",
|
||||
copySendLink: "Copy Send link",
|
||||
removePassword: "Remove password",
|
||||
delete: "Delete",
|
||||
loading: "Loading",
|
||||
});
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
args: {
|
||||
dataSource,
|
||||
disableSend: false,
|
||||
},
|
||||
argTypes: {
|
||||
editSend: { action: "editSend" },
|
||||
copySend: { action: "copySend" },
|
||||
removePassword: { action: "removePassword" },
|
||||
deleteSend: { action: "deleteSend" },
|
||||
},
|
||||
} as Meta<SendTableComponent>;
|
||||
|
||||
type Story = StoryObj<SendTableComponent>;
|
||||
|
||||
export const Default: Story = {};
|
||||
@@ -0,0 +1,94 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { ChangeDetectionStrategy, Component, input, output } from "@angular/core";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { SendView } from "@bitwarden/common/tools/send/models/view/send.view";
|
||||
import { AuthType } from "@bitwarden/common/tools/send/types/auth-type";
|
||||
import { SendType } from "@bitwarden/common/tools/send/types/send-type";
|
||||
import {
|
||||
BadgeModule,
|
||||
ButtonModule,
|
||||
IconButtonModule,
|
||||
LinkModule,
|
||||
MenuModule,
|
||||
TableDataSource,
|
||||
TableModule,
|
||||
TypographyModule,
|
||||
} from "@bitwarden/components";
|
||||
|
||||
/**
|
||||
* A table component for displaying Send items with sorting, status indicators, and action menus. Handles the presentation of sends in a tabular format with options
|
||||
* for editing, copying links, removing passwords, and deleting.
|
||||
*/
|
||||
@Component({
|
||||
selector: "tools-send-table",
|
||||
templateUrl: "./send-table.component.html",
|
||||
imports: [
|
||||
CommonModule,
|
||||
JslibModule,
|
||||
TableModule,
|
||||
ButtonModule,
|
||||
LinkModule,
|
||||
IconButtonModule,
|
||||
MenuModule,
|
||||
BadgeModule,
|
||||
TypographyModule,
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class SendTableComponent {
|
||||
protected readonly sendType = SendType;
|
||||
protected readonly authType = AuthType;
|
||||
|
||||
/**
|
||||
* The data source containing the Send items to display in the table.
|
||||
*/
|
||||
readonly dataSource = input<TableDataSource<SendView>>();
|
||||
|
||||
/**
|
||||
* Whether Send functionality is disabled by policy.
|
||||
* When true, the "Remove Password" option is hidden from the action menu.
|
||||
*/
|
||||
readonly disableSend = input(false);
|
||||
|
||||
/**
|
||||
* Emitted when a user clicks on a Send item to edit it.
|
||||
* The clicked SendView is passed as the event payload.
|
||||
*/
|
||||
readonly editSend = output<SendView>();
|
||||
|
||||
/**
|
||||
* Emitted when a user clicks the "Copy Send Link" action.
|
||||
* The SendView is passed as the event payload for generating and copying the link.
|
||||
*/
|
||||
readonly copySend = output<SendView>();
|
||||
|
||||
/**
|
||||
* Emitted when a user clicks the "Remove Password" action.
|
||||
* The SendView is passed as the event payload for password removal.
|
||||
* This action is only available if the Send has a password and Send is not disabled.
|
||||
*/
|
||||
readonly removePassword = output<SendView>();
|
||||
|
||||
/**
|
||||
* Emitted when a user clicks the "Delete" action.
|
||||
* The SendView is passed as the event payload for deletion.
|
||||
*/
|
||||
readonly deleteSend = output<SendView>();
|
||||
|
||||
protected onEditSend(send: SendView): void {
|
||||
this.editSend.emit(send);
|
||||
}
|
||||
|
||||
protected onCopy(send: SendView): void {
|
||||
this.copySend.emit(send);
|
||||
}
|
||||
|
||||
protected onRemovePassword(send: SendView): void {
|
||||
this.removePassword.emit(send);
|
||||
}
|
||||
|
||||
protected onDelete(send: SendView): void {
|
||||
this.deleteSend.emit(send);
|
||||
}
|
||||
}
|
||||
@@ -4,8 +4,8 @@ import { BehaviorSubject } from "rxjs";
|
||||
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { SendType } from "@bitwarden/common/tools/send/enums/send-type";
|
||||
import { SendView } from "@bitwarden/common/tools/send/models/view/send.view";
|
||||
import { SendType } from "@bitwarden/common/tools/send/types/send-type";
|
||||
|
||||
import { SendListFiltersService } from "./send-list-filters.service";
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@ import { FormBuilder } from "@angular/forms";
|
||||
import { map, Observable, startWith } from "rxjs";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { SendType } from "@bitwarden/common/tools/send/enums/send-type";
|
||||
import { SendView } from "@bitwarden/common/tools/send/models/view/send.view";
|
||||
import { SendType } from "@bitwarden/common/tools/send/types/send-type";
|
||||
import { ITreeNodeObject, TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
|
||||
import { ChipSelectOption } from "@bitwarden/components";
|
||||
|
||||
|
||||
Reference in New Issue
Block a user