mirror of
https://github.com/bitwarden/browser.git
synced 2024-11-11 10:10:25 +01:00
[PM- 9666] Implement edit item view individual vault (#10553)
* Add initial vault cipher form for cipher edit. * Add ability to add new cipher by type * Add ability to save and clone cipher, * Update canEditAllCiphers to take 1 argument. * Add attachments button to add/edit dialog. * Add semi-working attachment dialog. * Add working attachment functionality. * Remove debugging code. * Add tests for new attachments dialog component. * Add AddEditComponentV2 tests. * Remove AddEditComponentV2 delete functionality. * Remove unnecessary else statement. * Launch password generation in new dialog when extension refresh enabled. * Add tests for PasswordGeneratorComponent. * Adjust password and attachments dialog sizes. * run lint:fix * Remove unnecessary form from button. * Add missing provider in test. * Remove password generation events. * Add WebVaultGeneratorDialogComponent and WebCipherFormGenerationService * Move and rename CipherFormQueryParams * Use WebCipherFormGenerationService to launch password / user generation modals. * Add WebVaultGeneratorDialogComponent tests. * Remove unnecessary functionality and corresponding tests. * Fix failing tests. * Remove unused properties from AddEditComponentV2 * Pass CipherFormConfig to dialog. * Clean up unused attachment dialog functionality. * Update AddEdit cancel functionality to prevent navigating user. * Make attachment dialog open a static method. * Add addCipherV2 method and clean up tests. * Remove changes to QueryParams. * Add tests for WebCipherFormGenerationService * Remove unused onCipherSaved method. * Remove cipherSaved event. * Remove unused password generator component * Refactor to simplify editCipherId for extensionRefresh flag. * Add additional comments to AddEditComponentV2. * Simplify open vault generator dialog comment. * Remove unused organizationService * Remove unnecessary typecasting. * Remove extensionRefreshEnabled and related. * Remove slideIn animation * Remove unused AddEditComponentV2 properties. * Add back generic typing. * Condesnse properties into single form config. * Remove onDestroy and related code. * Run prettier * fix injection warning * Handle cipher save. * Redirect to vault on delete and make actions consistent. * Update comment.
This commit is contained in:
parent
a674f698a2
commit
931f86c948
@ -0,0 +1,34 @@
|
|||||||
|
<bit-dialog dialogSize="large">
|
||||||
|
<span bitDialogTitle>
|
||||||
|
{{ headerText }}
|
||||||
|
</span>
|
||||||
|
<ng-container bitDialogContent>
|
||||||
|
<vault-cipher-form
|
||||||
|
*ngIf="!loading"
|
||||||
|
formId="cipherForm"
|
||||||
|
[config]="config"
|
||||||
|
[submitBtn]="submitBtn"
|
||||||
|
(cipherSaved)="onCipherSaved($event)"
|
||||||
|
>
|
||||||
|
<bit-item slot="attachment-button">
|
||||||
|
<button bit-item-content type="button" (click)="openAttachmentsDialog()">
|
||||||
|
<p class="tw-m-0">
|
||||||
|
{{ "attachments" | i18n }}
|
||||||
|
<span *ngIf="!canAccessAttachments" bitBadge variant="success" class="tw-ml-2">
|
||||||
|
{{ "premium" | i18n }}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<i slot="end" class="bwi bwi-angle-right" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
</bit-item>
|
||||||
|
</vault-cipher-form>
|
||||||
|
</ng-container>
|
||||||
|
<ng-container bitDialogFooter>
|
||||||
|
<button bitButton type="submit" form="cipherForm" buttonType="primary" #submitBtn>
|
||||||
|
{{ "save" | i18n }}
|
||||||
|
</button>
|
||||||
|
<button bitButton type="button" buttonType="secondary" (click)="cancel()">
|
||||||
|
{{ "cancel" | i18n }}
|
||||||
|
</button>
|
||||||
|
</ng-container>
|
||||||
|
</bit-dialog>
|
@ -0,0 +1,124 @@
|
|||||||
|
import { DIALOG_DATA, DialogRef } from "@angular/cdk/dialog";
|
||||||
|
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||||
|
import { ActivatedRoute, Router } from "@angular/router";
|
||||||
|
import { mock, MockProxy } from "jest-mock-extended";
|
||||||
|
import { of } from "rxjs";
|
||||||
|
|
||||||
|
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||||
|
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||||
|
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||||
|
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||||
|
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||||
|
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||||
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
|
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||||
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
|
import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service";
|
||||||
|
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||||
|
import { DialogService } from "@bitwarden/components";
|
||||||
|
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
|
||||||
|
import { CipherFormConfig, DefaultCipherFormConfigService } from "@bitwarden/vault";
|
||||||
|
|
||||||
|
import { AddEditComponentV2 } from "./add-edit-v2.component";
|
||||||
|
|
||||||
|
describe("AddEditComponentV2", () => {
|
||||||
|
let component: AddEditComponentV2;
|
||||||
|
let fixture: ComponentFixture<AddEditComponentV2>;
|
||||||
|
let organizationService: MockProxy<OrganizationService>;
|
||||||
|
let policyService: MockProxy<PolicyService>;
|
||||||
|
let billingAccountProfileStateService: MockProxy<BillingAccountProfileStateService>;
|
||||||
|
let activatedRoute: MockProxy<ActivatedRoute>;
|
||||||
|
let dialogRef: MockProxy<DialogRef<any>>;
|
||||||
|
let dialogService: MockProxy<DialogService>;
|
||||||
|
let cipherService: MockProxy<CipherService>;
|
||||||
|
let messagingService: MockProxy<MessagingService>;
|
||||||
|
let folderService: MockProxy<FolderService>;
|
||||||
|
let collectionService: MockProxy<CollectionService>;
|
||||||
|
|
||||||
|
const mockParams = {
|
||||||
|
cloneMode: false,
|
||||||
|
cipherFormConfig: mock<CipherFormConfig>(),
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const mockOrganization: Organization = {
|
||||||
|
id: "org-id",
|
||||||
|
name: "Test Organization",
|
||||||
|
} as Organization;
|
||||||
|
|
||||||
|
organizationService = mock<OrganizationService>();
|
||||||
|
organizationService.organizations$ = of([mockOrganization]);
|
||||||
|
|
||||||
|
policyService = mock<PolicyService>();
|
||||||
|
policyService.policyAppliesToActiveUser$.mockImplementation((policyType: PolicyType) =>
|
||||||
|
of(true),
|
||||||
|
);
|
||||||
|
|
||||||
|
billingAccountProfileStateService = mock<BillingAccountProfileStateService>();
|
||||||
|
billingAccountProfileStateService.hasPremiumFromAnySource$ = of(true);
|
||||||
|
|
||||||
|
activatedRoute = mock<ActivatedRoute>();
|
||||||
|
activatedRoute.queryParams = of({});
|
||||||
|
|
||||||
|
dialogRef = mock<DialogRef<any>>();
|
||||||
|
dialogService = mock<DialogService>();
|
||||||
|
messagingService = mock<MessagingService>();
|
||||||
|
folderService = mock<FolderService>();
|
||||||
|
folderService.folderViews$ = of([]);
|
||||||
|
collectionService = mock<CollectionService>();
|
||||||
|
collectionService.decryptedCollections$ = of([]);
|
||||||
|
|
||||||
|
const mockDefaultCipherFormConfigService = {
|
||||||
|
buildConfig: jest.fn().mockResolvedValue({
|
||||||
|
allowPersonal: true,
|
||||||
|
allowOrganization: true,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [AddEditComponentV2],
|
||||||
|
providers: [
|
||||||
|
{ provide: DIALOG_DATA, useValue: mockParams },
|
||||||
|
{ provide: DialogRef, useValue: dialogRef },
|
||||||
|
{ provide: I18nService, useValue: { t: jest.fn().mockReturnValue("login") } },
|
||||||
|
{ provide: DialogService, useValue: dialogService },
|
||||||
|
{ provide: CipherService, useValue: cipherService },
|
||||||
|
{ provide: MessagingService, useValue: messagingService },
|
||||||
|
{ provide: OrganizationService, useValue: organizationService },
|
||||||
|
{ provide: Router, useValue: mock<Router>() },
|
||||||
|
{ provide: ActivatedRoute, useValue: activatedRoute },
|
||||||
|
{ provide: CollectionService, useValue: collectionService },
|
||||||
|
{ provide: FolderService, useValue: folderService },
|
||||||
|
{ provide: CryptoService, useValue: mock<CryptoService>() },
|
||||||
|
{ provide: BillingAccountProfileStateService, useValue: billingAccountProfileStateService },
|
||||||
|
{ provide: PolicyService, useValue: policyService },
|
||||||
|
{ provide: DefaultCipherFormConfigService, useValue: mockDefaultCipherFormConfigService },
|
||||||
|
{
|
||||||
|
provide: PasswordGenerationServiceAbstraction,
|
||||||
|
useValue: mock<PasswordGenerationServiceAbstraction>(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}).compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(AddEditComponentV2);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("ngOnInit", () => {
|
||||||
|
it("initializes the component with cipher", async () => {
|
||||||
|
await component.ngOnInit();
|
||||||
|
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("cancel", () => {
|
||||||
|
it("handles cancel action", async () => {
|
||||||
|
const spyClose = jest.spyOn(dialogRef, "close");
|
||||||
|
|
||||||
|
await component.cancel();
|
||||||
|
|
||||||
|
expect(spyClose).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
177
apps/web/src/app/vault/individual-vault/add-edit-v2.component.ts
Normal file
177
apps/web/src/app/vault/individual-vault/add-edit-v2.component.ts
Normal file
@ -0,0 +1,177 @@
|
|||||||
|
import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
|
||||||
|
import { CommonModule } from "@angular/common";
|
||||||
|
import { Component, Inject, OnInit } from "@angular/core";
|
||||||
|
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||||
|
|
||||||
|
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions";
|
||||||
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
|
import { CipherId } from "@bitwarden/common/types/guid";
|
||||||
|
import { CipherType } from "@bitwarden/common/vault/enums/cipher-type";
|
||||||
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||||
|
import { AsyncActionsModule, DialogModule, DialogService, ItemModule } from "@bitwarden/components";
|
||||||
|
import {
|
||||||
|
CipherAttachmentsComponent,
|
||||||
|
CipherFormConfig,
|
||||||
|
CipherFormGenerationService,
|
||||||
|
CipherFormMode,
|
||||||
|
CipherFormModule,
|
||||||
|
} from "@bitwarden/vault";
|
||||||
|
|
||||||
|
import { WebCipherFormGenerationService } from "../../../../../../libs/vault/src/cipher-form/services/web-cipher-form-generation.service";
|
||||||
|
import { CipherViewComponent } from "../../../../../../libs/vault/src/cipher-view/cipher-view.component";
|
||||||
|
import { SharedModule } from "../../shared/shared.module";
|
||||||
|
|
||||||
|
import { AttachmentsV2Component } from "./attachments-v2.component";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The result of the AddEditCipherDialogV2 component.
|
||||||
|
*/
|
||||||
|
export enum AddEditCipherDialogResult {
|
||||||
|
Edited = "edited",
|
||||||
|
Added = "added",
|
||||||
|
Canceled = "canceled",
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The close result of the AddEditCipherDialogV2 component.
|
||||||
|
*/
|
||||||
|
export interface AddEditCipherDialogCloseResult {
|
||||||
|
/**
|
||||||
|
* The action that was taken.
|
||||||
|
*/
|
||||||
|
action: AddEditCipherDialogResult;
|
||||||
|
/**
|
||||||
|
* The ID of the cipher that was edited or added.
|
||||||
|
*/
|
||||||
|
id?: CipherId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component for viewing a cipher, presented in a dialog.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: "app-vault-add-edit-v2",
|
||||||
|
templateUrl: "add-edit-v2.component.html",
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
CipherViewComponent,
|
||||||
|
CommonModule,
|
||||||
|
AsyncActionsModule,
|
||||||
|
DialogModule,
|
||||||
|
SharedModule,
|
||||||
|
CipherFormModule,
|
||||||
|
CipherAttachmentsComponent,
|
||||||
|
ItemModule,
|
||||||
|
],
|
||||||
|
providers: [{ provide: CipherFormGenerationService, useClass: WebCipherFormGenerationService }],
|
||||||
|
})
|
||||||
|
export class AddEditComponentV2 implements OnInit {
|
||||||
|
config: CipherFormConfig;
|
||||||
|
headerText: string;
|
||||||
|
canAccessAttachments: boolean = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor for the AddEditComponentV2 component.
|
||||||
|
* @param params The parameters for the component.
|
||||||
|
* @param dialogRef The reference to the dialog.
|
||||||
|
* @param i18nService The internationalization service.
|
||||||
|
* @param dialogService The dialog service.
|
||||||
|
* @param billingAccountProfileStateService The billing account profile state service.
|
||||||
|
*/
|
||||||
|
constructor(
|
||||||
|
@Inject(DIALOG_DATA) public params: CipherFormConfig,
|
||||||
|
private dialogRef: DialogRef<AddEditCipherDialogCloseResult>,
|
||||||
|
private i18nService: I18nService,
|
||||||
|
private dialogService: DialogService,
|
||||||
|
private billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||||
|
) {
|
||||||
|
this.billingAccountProfileStateService.hasPremiumFromAnySource$
|
||||||
|
.pipe(takeUntilDestroyed())
|
||||||
|
.subscribe((canAccessPremium) => {
|
||||||
|
this.canAccessAttachments = canAccessPremium;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lifecycle hook for component initialization.
|
||||||
|
*/
|
||||||
|
async ngOnInit() {
|
||||||
|
this.config = this.params;
|
||||||
|
this.headerText = this.setHeader(this.config?.mode, this.config.cipherType);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Getter to check if the component is loading.
|
||||||
|
*/
|
||||||
|
get loading() {
|
||||||
|
return this.config == null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method to handle cancel action. Called when a user clicks the cancel button.
|
||||||
|
*/
|
||||||
|
async cancel() {
|
||||||
|
this.dialogRef.close({ action: AddEditCipherDialogResult.Canceled });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the header text based on the mode and type of the cipher.
|
||||||
|
* @param mode The form mode.
|
||||||
|
* @param type The cipher type.
|
||||||
|
* @returns The header text.
|
||||||
|
*/
|
||||||
|
setHeader(mode: CipherFormMode, type: CipherType) {
|
||||||
|
const partOne = mode === "edit" || mode === "partial-edit" ? "editItemHeader" : "newItemHeader";
|
||||||
|
switch (type) {
|
||||||
|
case CipherType.Login:
|
||||||
|
return this.i18nService.t(partOne, this.i18nService.t("typeLogin").toLowerCase());
|
||||||
|
case CipherType.Card:
|
||||||
|
return this.i18nService.t(partOne, this.i18nService.t("typeCard").toLowerCase());
|
||||||
|
case CipherType.Identity:
|
||||||
|
return this.i18nService.t(partOne, this.i18nService.t("typeIdentity").toLowerCase());
|
||||||
|
case CipherType.SecureNote:
|
||||||
|
return this.i18nService.t(partOne, this.i18nService.t("note").toLowerCase());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens the attachments dialog.
|
||||||
|
*/
|
||||||
|
async openAttachmentsDialog() {
|
||||||
|
this.dialogService.open<AttachmentsV2Component, { cipherId: CipherId }>(
|
||||||
|
AttachmentsV2Component,
|
||||||
|
{
|
||||||
|
data: {
|
||||||
|
cipherId: this.config.originalCipher?.id as CipherId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the event when a cipher is saved.
|
||||||
|
* @param cipherView The cipher view that was saved.
|
||||||
|
*/
|
||||||
|
async onCipherSaved(cipherView: CipherView) {
|
||||||
|
this.dialogRef.close({
|
||||||
|
action:
|
||||||
|
this.config.mode === "add"
|
||||||
|
? AddEditCipherDialogResult.Added
|
||||||
|
: AddEditCipherDialogResult.Edited,
|
||||||
|
id: cipherView.id as CipherId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Strongly typed helper to open a cipher add/edit dialog
|
||||||
|
* @param dialogService Instance of the dialog service that will be used to open the dialog
|
||||||
|
* @param config Configuration for the dialog
|
||||||
|
* @returns A reference to the opened dialog
|
||||||
|
*/
|
||||||
|
export function openAddEditCipherDialog(
|
||||||
|
dialogService: DialogService,
|
||||||
|
config: DialogConfig<CipherFormConfig>,
|
||||||
|
): DialogRef<AddEditCipherDialogCloseResult> {
|
||||||
|
return dialogService.open(AddEditComponentV2, config);
|
||||||
|
}
|
@ -0,0 +1,19 @@
|
|||||||
|
<bit-dialog dialogSize="default">
|
||||||
|
<span bitDialogTitle>
|
||||||
|
{{ "attachments" | i18n }}
|
||||||
|
</span>
|
||||||
|
<ng-container bitDialogContent>
|
||||||
|
<app-cipher-attachments
|
||||||
|
*ngIf="cipherId"
|
||||||
|
[cipherId]="cipherId"
|
||||||
|
[submitBtn]="submitBtn"
|
||||||
|
(onUploadSuccess)="uploadSuccessful()"
|
||||||
|
(onRemoveSuccess)="removalSuccessful()"
|
||||||
|
></app-cipher-attachments>
|
||||||
|
</ng-container>
|
||||||
|
<ng-container bitDialogFooter>
|
||||||
|
<button bitButton type="submit" buttonType="primary" [attr.form]="attachmentFormId" #submitBtn>
|
||||||
|
{{ "upload" | i18n }}
|
||||||
|
</button>
|
||||||
|
</ng-container>
|
||||||
|
</bit-dialog>
|
@ -0,0 +1,65 @@
|
|||||||
|
import { DIALOG_DATA, DialogRef } from "@angular/cdk/dialog";
|
||||||
|
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||||
|
import { NoopAnimationsModule } from "@angular/platform-browser/animations";
|
||||||
|
import { mock } from "jest-mock-extended";
|
||||||
|
|
||||||
|
import { 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";
|
||||||
|
import { CipherId } from "@bitwarden/common/types/guid";
|
||||||
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
|
|
||||||
|
import {
|
||||||
|
AttachmentsV2Component,
|
||||||
|
AttachmentDialogResult,
|
||||||
|
AttachmentsDialogParams,
|
||||||
|
} from "./attachments-v2.component";
|
||||||
|
|
||||||
|
describe("AttachmentsV2Component", () => {
|
||||||
|
let component: AttachmentsV2Component;
|
||||||
|
let fixture: ComponentFixture<AttachmentsV2Component>;
|
||||||
|
|
||||||
|
const mockCipherId: CipherId = "cipher-id" as CipherId;
|
||||||
|
const mockParams: AttachmentsDialogParams = {
|
||||||
|
cipherId: mockCipherId,
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [AttachmentsV2Component, NoopAnimationsModule],
|
||||||
|
providers: [
|
||||||
|
{ provide: DIALOG_DATA, useValue: mockParams },
|
||||||
|
{ provide: DialogRef, useValue: mock<DialogRef>() },
|
||||||
|
{ provide: I18nService, useValue: mock<I18nService>() },
|
||||||
|
{ provide: CipherService, useValue: mock<CipherService>() },
|
||||||
|
{ provide: LogService, useValue: mock<LogService>() },
|
||||||
|
{ provide: AccountService, useValue: mock<AccountService>() },
|
||||||
|
],
|
||||||
|
}).compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(AttachmentsV2Component);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("initializes without errors and with the correct cipherId", () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
expect(component.cipherId).toBe(mockParams.cipherId);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("closes the dialog with 'uploaded' result on uploadSuccessful", () => {
|
||||||
|
const dialogRefCloseSpy = jest.spyOn(component["dialogRef"], "close");
|
||||||
|
|
||||||
|
component.uploadSuccessful();
|
||||||
|
|
||||||
|
expect(dialogRefCloseSpy).toHaveBeenCalledWith({ action: AttachmentDialogResult.Uploaded });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("closes the dialog with 'removed' result on removalSuccessful", () => {
|
||||||
|
const dialogRefCloseSpy = jest.spyOn(component["dialogRef"], "close");
|
||||||
|
|
||||||
|
component.removalSuccessful();
|
||||||
|
|
||||||
|
expect(dialogRefCloseSpy).toHaveBeenCalledWith({ action: AttachmentDialogResult.Removed });
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,87 @@
|
|||||||
|
import { DialogRef, DIALOG_DATA } from "@angular/cdk/dialog";
|
||||||
|
import { CommonModule } from "@angular/common";
|
||||||
|
import { Component, Inject } from "@angular/core";
|
||||||
|
|
||||||
|
import { CipherId } from "@bitwarden/common/types/guid";
|
||||||
|
import { DialogService } from "@bitwarden/components";
|
||||||
|
import { CipherAttachmentsComponent } from "@bitwarden/vault";
|
||||||
|
|
||||||
|
import { SharedModule } from "../../shared";
|
||||||
|
|
||||||
|
export interface AttachmentsDialogParams {
|
||||||
|
cipherId: CipherId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enum representing the possible results of the attachment dialog.
|
||||||
|
*/
|
||||||
|
export enum AttachmentDialogResult {
|
||||||
|
Uploaded = "uploaded",
|
||||||
|
Removed = "removed",
|
||||||
|
Closed = "closed",
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AttachmentDialogCloseResult {
|
||||||
|
action: AttachmentDialogResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component for the attachments dialog.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: "app-vault-attachments-v2",
|
||||||
|
templateUrl: "attachments-v2.component.html",
|
||||||
|
standalone: true,
|
||||||
|
imports: [CommonModule, SharedModule, CipherAttachmentsComponent],
|
||||||
|
})
|
||||||
|
export class AttachmentsV2Component {
|
||||||
|
cipherId: CipherId;
|
||||||
|
attachmentFormId = CipherAttachmentsComponent.attachmentFormID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor for AttachmentsV2Component.
|
||||||
|
* @param dialogRef - Reference to the dialog.
|
||||||
|
* @param params - Parameters passed to the dialog.
|
||||||
|
*/
|
||||||
|
constructor(
|
||||||
|
private dialogRef: DialogRef<AttachmentDialogCloseResult>,
|
||||||
|
@Inject(DIALOG_DATA) public params: AttachmentsDialogParams,
|
||||||
|
) {
|
||||||
|
this.cipherId = params.cipherId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens the attachments dialog.
|
||||||
|
* @param dialogService - The dialog service.
|
||||||
|
* @param params - The parameters for the dialog.
|
||||||
|
* @returns The dialog reference.
|
||||||
|
*/
|
||||||
|
static open(
|
||||||
|
dialogService: DialogService,
|
||||||
|
params: AttachmentsDialogParams,
|
||||||
|
): DialogRef<AttachmentDialogCloseResult> {
|
||||||
|
return dialogService.open(AttachmentsV2Component, {
|
||||||
|
data: params,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when an attachment is successfully uploaded.
|
||||||
|
* Closes the dialog with an 'uploaded' result.
|
||||||
|
*/
|
||||||
|
uploadSuccessful() {
|
||||||
|
this.dialogRef.close({
|
||||||
|
action: AttachmentDialogResult.Uploaded,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when an attachment is successfully removed.
|
||||||
|
* Closes the dialog with a 'removed' result.
|
||||||
|
*/
|
||||||
|
removalSuccessful() {
|
||||||
|
this.dialogRef.close({
|
||||||
|
action: AttachmentDialogResult.Removed,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -47,20 +47,25 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag
|
|||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
import { SyncService } from "@bitwarden/common/platform/sync";
|
import { SyncService } from "@bitwarden/common/platform/sync";
|
||||||
import { OrganizationId } from "@bitwarden/common/types/guid";
|
import { CipherId, OrganizationId, CollectionId } from "@bitwarden/common/types/guid";
|
||||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service";
|
import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service";
|
||||||
import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service";
|
import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service";
|
||||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||||
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
|
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
|
||||||
import { CollectionData } from "@bitwarden/common/vault/models/data/collection.data";
|
import { CollectionData } from "@bitwarden/common/vault/models/data/collection.data";
|
||||||
|
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
|
||||||
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
|
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
|
||||||
import { CollectionDetailsResponse } from "@bitwarden/common/vault/models/response/collection.response";
|
import { CollectionDetailsResponse } from "@bitwarden/common/vault/models/response/collection.response";
|
||||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||||
import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view";
|
import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view";
|
||||||
import { ServiceUtils } from "@bitwarden/common/vault/service-utils";
|
import { ServiceUtils } from "@bitwarden/common/vault/service-utils";
|
||||||
import { DialogService, Icons, ToastService } from "@bitwarden/components";
|
import { DialogService, Icons, ToastService } from "@bitwarden/components";
|
||||||
import { CollectionAssignmentResult, PasswordRepromptService } from "@bitwarden/vault";
|
import {
|
||||||
|
CollectionAssignmentResult,
|
||||||
|
DefaultCipherFormConfigService,
|
||||||
|
PasswordRepromptService,
|
||||||
|
} from "@bitwarden/vault";
|
||||||
|
|
||||||
import { SharedModule } from "../../shared/shared.module";
|
import { SharedModule } from "../../shared/shared.module";
|
||||||
import { AssignCollectionsWebComponent } from "../components/assign-collections";
|
import { AssignCollectionsWebComponent } from "../components/assign-collections";
|
||||||
@ -74,7 +79,17 @@ import { VaultItemEvent } from "../components/vault-items/vault-item-event";
|
|||||||
import { VaultItemsModule } from "../components/vault-items/vault-items.module";
|
import { VaultItemsModule } from "../components/vault-items/vault-items.module";
|
||||||
import { getNestedCollectionTree } from "../utils/collection-utils";
|
import { getNestedCollectionTree } from "../utils/collection-utils";
|
||||||
|
|
||||||
|
import {
|
||||||
|
AddEditCipherDialogCloseResult,
|
||||||
|
AddEditCipherDialogResult,
|
||||||
|
openAddEditCipherDialog,
|
||||||
|
} from "./add-edit-v2.component";
|
||||||
import { AddEditComponent } from "./add-edit.component";
|
import { AddEditComponent } from "./add-edit.component";
|
||||||
|
import {
|
||||||
|
AttachmentDialogCloseResult,
|
||||||
|
AttachmentDialogResult,
|
||||||
|
AttachmentsV2Component,
|
||||||
|
} from "./attachments-v2.component";
|
||||||
import { AttachmentsComponent } from "./attachments.component";
|
import { AttachmentsComponent } from "./attachments.component";
|
||||||
import {
|
import {
|
||||||
BulkDeleteDialogResult,
|
BulkDeleteDialogResult,
|
||||||
@ -131,7 +146,11 @@ const SearchTextDebounceInterval = 200;
|
|||||||
VaultItemsModule,
|
VaultItemsModule,
|
||||||
SharedModule,
|
SharedModule,
|
||||||
],
|
],
|
||||||
providers: [RoutedVaultFilterService, RoutedVaultFilterBridgeService],
|
providers: [
|
||||||
|
RoutedVaultFilterService,
|
||||||
|
RoutedVaultFilterBridgeService,
|
||||||
|
DefaultCipherFormConfigService,
|
||||||
|
],
|
||||||
})
|
})
|
||||||
export class VaultComponent implements OnInit, OnDestroy {
|
export class VaultComponent implements OnInit, OnDestroy {
|
||||||
@ViewChild("vaultFilter", { static: true }) filterComponent: VaultFilterComponent;
|
@ViewChild("vaultFilter", { static: true }) filterComponent: VaultFilterComponent;
|
||||||
@ -170,6 +189,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
private searchText$ = new Subject<string>();
|
private searchText$ = new Subject<string>();
|
||||||
private refresh$ = new BehaviorSubject<void>(null);
|
private refresh$ = new BehaviorSubject<void>(null);
|
||||||
private destroy$ = new Subject<void>();
|
private destroy$ = new Subject<void>();
|
||||||
|
private extensionRefreshEnabled: boolean;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private syncService: SyncService,
|
private syncService: SyncService,
|
||||||
@ -200,6 +220,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
private billingAccountProfileStateService: BillingAccountProfileStateService,
|
private billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||||
private toastService: ToastService,
|
private toastService: ToastService,
|
||||||
private accountService: AccountService,
|
private accountService: AccountService,
|
||||||
|
private cipherFormConfigService: DefaultCipherFormConfigService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
@ -416,6 +437,11 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
this.refreshing = false;
|
this.refreshing = false;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Check if the extension refresh feature flag is enabled
|
||||||
|
this.extensionRefreshEnabled = await this.configService.getFeatureFlag(
|
||||||
|
FeatureFlag.ExtensionRefresh,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy() {
|
ngOnDestroy() {
|
||||||
@ -511,6 +537,15 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
this.searchText$.next(searchText);
|
this.searchText$.next(searchText);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles opening the attachments dialog for a cipher.
|
||||||
|
* Runs several checks to ensure that the user has the correct permissions
|
||||||
|
* and then opens the attachments dialog.
|
||||||
|
* Uses the new AttachmentsV2Component if the extensionRefresh feature flag is enabled.
|
||||||
|
*
|
||||||
|
* @param cipher
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
async editCipherAttachments(cipher: CipherView) {
|
async editCipherAttachments(cipher: CipherView) {
|
||||||
if (cipher?.reprompt !== 0 && !(await this.passwordRepromptService.showPasswordPrompt())) {
|
if (cipher?.reprompt !== 0 && !(await this.passwordRepromptService.showPasswordPrompt())) {
|
||||||
this.go({ cipherId: null, itemId: null });
|
this.go({ cipherId: null, itemId: null });
|
||||||
@ -536,6 +571,24 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let madeAttachmentChanges = false;
|
let madeAttachmentChanges = false;
|
||||||
|
|
||||||
|
if (this.extensionRefreshEnabled) {
|
||||||
|
const dialogRef = AttachmentsV2Component.open(this.dialogService, {
|
||||||
|
cipherId: cipher.id as CipherId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result: AttachmentDialogCloseResult = await lastValueFrom(dialogRef.closed);
|
||||||
|
|
||||||
|
if (
|
||||||
|
result.action === AttachmentDialogResult.Uploaded ||
|
||||||
|
result.action === AttachmentDialogResult.Removed
|
||||||
|
) {
|
||||||
|
this.refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const [modal] = await this.modalService.openViewRef(
|
const [modal] = await this.modalService.openViewRef(
|
||||||
AttachmentsComponent,
|
AttachmentsComponent,
|
||||||
this.attachmentsModalRef,
|
this.attachmentsModalRef,
|
||||||
@ -598,7 +651,11 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async addCipher(cipherType?: CipherType) {
|
async addCipher(cipherType?: CipherType) {
|
||||||
const component = await this.editCipher(null);
|
if (this.extensionRefreshEnabled) {
|
||||||
|
return this.addCipherV2(cipherType);
|
||||||
|
}
|
||||||
|
|
||||||
|
const component = (await this.editCipher(null)) as AddEditComponent;
|
||||||
component.type = cipherType || this.activeFilter.cipherType;
|
component.type = cipherType || this.activeFilter.cipherType;
|
||||||
if (
|
if (
|
||||||
this.activeFilter.organizationId !== "MyVault" &&
|
this.activeFilter.organizationId !== "MyVault" &&
|
||||||
@ -622,18 +679,60 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
component.folderId = this.activeFilter.folderId;
|
component.folderId = this.activeFilter.folderId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens the add cipher dialog.
|
||||||
|
* @param cipherType The type of cipher to add.
|
||||||
|
* @returns The dialog reference.
|
||||||
|
*/
|
||||||
|
async addCipherV2(cipherType?: CipherType) {
|
||||||
|
const cipherFormConfig = await this.cipherFormConfigService.buildConfig(
|
||||||
|
"add",
|
||||||
|
null,
|
||||||
|
cipherType,
|
||||||
|
);
|
||||||
|
cipherFormConfig.initialValues = {
|
||||||
|
organizationId:
|
||||||
|
this.activeFilter.organizationId !== "MyVault" && this.activeFilter.organizationId != null
|
||||||
|
? (this.activeFilter.organizationId as OrganizationId)
|
||||||
|
: null,
|
||||||
|
collectionIds:
|
||||||
|
this.activeFilter.collectionId !== "AllCollections" &&
|
||||||
|
this.activeFilter.collectionId != null
|
||||||
|
? [this.activeFilter.collectionId as CollectionId]
|
||||||
|
: [],
|
||||||
|
folderId: this.activeFilter.folderId,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Open the dialog.
|
||||||
|
const dialogRef = openAddEditCipherDialog(this.dialogService, {
|
||||||
|
data: cipherFormConfig,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Wait for the dialog to close.
|
||||||
|
const result: AddEditCipherDialogCloseResult = await lastValueFrom(dialogRef.closed);
|
||||||
|
|
||||||
|
// Refresh the vault to show the new cipher.
|
||||||
|
if (result?.action === AddEditCipherDialogResult.Added) {
|
||||||
|
this.refresh();
|
||||||
|
this.go({ itemId: result.id, action: "view" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the dialog was closed by any other action navigate back to the vault.
|
||||||
|
this.go({ cipherId: null, itemId: null, action: null });
|
||||||
|
}
|
||||||
|
|
||||||
async navigateToCipher(cipher: CipherView) {
|
async navigateToCipher(cipher: CipherView) {
|
||||||
this.go({ itemId: cipher?.id });
|
this.go({ itemId: cipher?.id });
|
||||||
}
|
}
|
||||||
|
|
||||||
async editCipher(cipher: CipherView) {
|
async editCipher(cipher: CipherView, cloneMode?: boolean) {
|
||||||
return this.editCipherId(cipher?.id);
|
return this.editCipherId(cipher?.id, cloneMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
async editCipherId(id: string) {
|
async editCipherId(id: string, cloneMode?: boolean) {
|
||||||
const cipher = await this.cipherService.get(id);
|
const cipher = await this.cipherService.get(id);
|
||||||
// if cipher exists (cipher is null when new) and MP reprompt
|
|
||||||
// is on for this cipher, then show password reprompt
|
|
||||||
if (
|
if (
|
||||||
cipher &&
|
cipher &&
|
||||||
cipher.reprompt !== 0 &&
|
cipher.reprompt !== 0 &&
|
||||||
@ -644,6 +743,11 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.extensionRefreshEnabled) {
|
||||||
|
await this.editCipherIdV2(cipher, cloneMode);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const [modal, childComponent] = await this.modalService.openViewRef(
|
const [modal, childComponent] = await this.modalService.openViewRef(
|
||||||
AddEditComponent,
|
AddEditComponent,
|
||||||
this.cipherAddEditModalRef,
|
this.cipherAddEditModalRef,
|
||||||
@ -673,6 +777,46 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
return childComponent;
|
return childComponent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Edit a cipher using the new AddEditCipherDialogV2 component.
|
||||||
|
*
|
||||||
|
* @param cipher
|
||||||
|
* @param cloneMode
|
||||||
|
*/
|
||||||
|
private async editCipherIdV2(cipher: Cipher, cloneMode?: boolean) {
|
||||||
|
const cipherFormConfig = await this.cipherFormConfigService.buildConfig(
|
||||||
|
cloneMode ? "clone" : "edit",
|
||||||
|
cipher.id as CipherId,
|
||||||
|
cipher.type,
|
||||||
|
);
|
||||||
|
|
||||||
|
const dialogRef = openAddEditCipherDialog(this.dialogService, {
|
||||||
|
data: cipherFormConfig,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result: AddEditCipherDialogCloseResult = await firstValueFrom(dialogRef.closed);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refresh the vault if the dialog was closed by adding, editing, or deleting a cipher.
|
||||||
|
*/
|
||||||
|
if (result?.action === AddEditCipherDialogResult.Edited) {
|
||||||
|
this.refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* View the cipher if the dialog was closed by editing the cipher.
|
||||||
|
*/
|
||||||
|
if (result?.action === AddEditCipherDialogResult.Edited) {
|
||||||
|
this.go({ itemId: cipher.id, action: "view" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Navigate to the vault if the dialog was closed by any other action.
|
||||||
|
*/
|
||||||
|
this.go({ cipherId: null, itemId: null, action: null });
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Takes a CipherView and opens a dialog where it can be viewed (wraps viewCipherById).
|
* Takes a CipherView and opens a dialog where it can be viewed (wraps viewCipherById).
|
||||||
* @param cipher - CipherView
|
* @param cipher - CipherView
|
||||||
@ -718,8 +862,9 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
const result: ViewCipherDialogCloseResult = await lastValueFrom(dialogRef.closed);
|
const result: ViewCipherDialogCloseResult = await lastValueFrom(dialogRef.closed);
|
||||||
|
|
||||||
// If the dialog was closed by deleting the cipher, refresh the vault.
|
// If the dialog was closed by deleting the cipher, refresh the vault.
|
||||||
if (result?.action === ViewCipherDialogResult.deleted) {
|
if (result?.action === ViewCipherDialogResult.Deleted) {
|
||||||
this.refresh();
|
this.refresh();
|
||||||
|
this.go({ cipherId: null, itemId: null, action: null });
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the dialog was closed by any other action (close button, escape key, etc), navigate back to the vault.
|
// If the dialog was closed by any other action (close button, escape key, etc), navigate back to the vault.
|
||||||
@ -873,7 +1018,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const component = await this.editCipher(cipher);
|
const component = await this.editCipher(cipher, true);
|
||||||
component.cloneMode = true;
|
component.cloneMode = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -98,7 +98,7 @@ describe("ViewComponent", () => {
|
|||||||
organizationId: mockCipher.organizationId,
|
organizationId: mockCipher.organizationId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(dialogRefCloseSpy).toHaveBeenCalledWith({ action: ViewCipherDialogResult.edited });
|
expect(dialogRefCloseSpy).toHaveBeenCalledWith({ action: ViewCipherDialogResult.Edited });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -111,7 +111,7 @@ describe("ViewComponent", () => {
|
|||||||
await component.delete();
|
await component.delete();
|
||||||
|
|
||||||
expect(deleteSpy).toHaveBeenCalled();
|
expect(deleteSpy).toHaveBeenCalled();
|
||||||
expect(dialogRefCloseSpy).toHaveBeenCalledWith({ action: ViewCipherDialogResult.deleted });
|
expect(dialogRefCloseSpy).toHaveBeenCalledWith({ action: ViewCipherDialogResult.Deleted });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -27,8 +27,8 @@ export interface ViewCipherDialogParams {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export enum ViewCipherDialogResult {
|
export enum ViewCipherDialogResult {
|
||||||
edited = "edited",
|
Edited = "edited",
|
||||||
deleted = "deleted",
|
Deleted = "deleted",
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ViewCipherDialogCloseResult {
|
export interface ViewCipherDialogCloseResult {
|
||||||
@ -117,7 +117,7 @@ export class ViewComponent implements OnInit, OnDestroy {
|
|||||||
this.logService.error(e);
|
this.logService.error(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.dialogRef.close({ action: ViewCipherDialogResult.deleted });
|
this.dialogRef.close({ action: ViewCipherDialogResult.Deleted });
|
||||||
await this.router.navigate(["/vault"]);
|
await this.router.navigate(["/vault"]);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -137,7 +137,7 @@ export class ViewComponent implements OnInit, OnDestroy {
|
|||||||
* Method to handle cipher editing. Called when a user clicks the edit button.
|
* Method to handle cipher editing. Called when a user clicks the edit button.
|
||||||
*/
|
*/
|
||||||
async edit(): Promise<void> {
|
async edit(): Promise<void> {
|
||||||
this.dialogRef.close({ action: ViewCipherDialogResult.edited });
|
this.dialogRef.close({ action: ViewCipherDialogResult.Edited });
|
||||||
await this.router.navigate([], {
|
await this.router.navigate([], {
|
||||||
queryParams: {
|
queryParams: {
|
||||||
itemId: this.cipher.id,
|
itemId: this.cipher.id,
|
||||||
|
@ -886,8 +886,9 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
const result: ViewCipherDialogCloseResult = await lastValueFrom(dialogRef.closed);
|
const result: ViewCipherDialogCloseResult = await lastValueFrom(dialogRef.closed);
|
||||||
|
|
||||||
// If the dialog was closed by deleting the cipher, refresh the vault.
|
// If the dialog was closed by deleting the cipher, refresh the vault.
|
||||||
if (result.action === ViewCipherDialogResult.deleted) {
|
if (result.action === ViewCipherDialogResult.Deleted) {
|
||||||
this.refresh();
|
this.refresh();
|
||||||
|
this.go({ cipherId: null, itemId: null, action: null });
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the dialog was closed by any other action (close button, escape key, etc), navigate back to the vault.
|
// If the dialog was closed by any other action (close button, escape key, etc), navigate back to the vault.
|
||||||
|
@ -511,6 +511,24 @@
|
|||||||
"viewItem": {
|
"viewItem": {
|
||||||
"message": "View item"
|
"message": "View item"
|
||||||
},
|
},
|
||||||
|
"newItemHeader": {
|
||||||
|
"message": "New $TYPE$",
|
||||||
|
"placeholders": {
|
||||||
|
"type": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "login"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"editItemHeader": {
|
||||||
|
"message": "Edit $TYPE$",
|
||||||
|
"placeholders": {
|
||||||
|
"type": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "login"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"viewItemType": {
|
"viewItemType": {
|
||||||
"message": "View $ITEMTYPE$",
|
"message": "View $ITEMTYPE$",
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
@ -7399,6 +7417,9 @@
|
|||||||
"fileUpload": {
|
"fileUpload": {
|
||||||
"message": "File upload"
|
"message": "File upload"
|
||||||
},
|
},
|
||||||
|
"upload": {
|
||||||
|
"message": "Upload"
|
||||||
|
},
|
||||||
"acceptedFormats": {
|
"acceptedFormats": {
|
||||||
"message": "Accepted Formats:"
|
"message": "Accepted Formats:"
|
||||||
},
|
},
|
||||||
|
@ -85,6 +85,9 @@ export class CipherAttachmentsComponent implements OnInit, AfterViewInit {
|
|||||||
/** Emits after a file has been successfully uploaded */
|
/** Emits after a file has been successfully uploaded */
|
||||||
@Output() onUploadSuccess = new EventEmitter<void>();
|
@Output() onUploadSuccess = new EventEmitter<void>();
|
||||||
|
|
||||||
|
/** Emits after a file has been successfully removed */
|
||||||
|
@Output() onRemoveSuccess = new EventEmitter<void>();
|
||||||
|
|
||||||
cipher: CipherView;
|
cipher: CipherView;
|
||||||
|
|
||||||
attachmentForm: CipherAttachmentForm = this.formBuilder.group({
|
attachmentForm: CipherAttachmentForm = this.formBuilder.group({
|
||||||
@ -216,5 +219,7 @@ export class CipherAttachmentsComponent implements OnInit, AfterViewInit {
|
|||||||
if (index > -1) {
|
if (index > -1) {
|
||||||
this.cipher.attachments.splice(index, 1);
|
this.cipher.attachments.splice(index, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.onRemoveSuccess.emit();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@ import { mock, MockProxy } from "jest-mock-extended";
|
|||||||
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
|
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
|
||||||
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||||
import { EventType } from "@bitwarden/common/enums";
|
import { EventType } from "@bitwarden/common/enums";
|
||||||
|
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
|
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
|
||||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||||
@ -39,6 +40,8 @@ describe("LoginDetailsSectionComponent", () => {
|
|||||||
let toastService: MockProxy<ToastService>;
|
let toastService: MockProxy<ToastService>;
|
||||||
let totpCaptureService: MockProxy<TotpCaptureService>;
|
let totpCaptureService: MockProxy<TotpCaptureService>;
|
||||||
let i18nService: MockProxy<I18nService>;
|
let i18nService: MockProxy<I18nService>;
|
||||||
|
let configService: MockProxy<ConfigService>;
|
||||||
|
|
||||||
const collect = jest.fn().mockResolvedValue(null);
|
const collect = jest.fn().mockResolvedValue(null);
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
@ -49,6 +52,7 @@ describe("LoginDetailsSectionComponent", () => {
|
|||||||
toastService = mock<ToastService>();
|
toastService = mock<ToastService>();
|
||||||
totpCaptureService = mock<TotpCaptureService>();
|
totpCaptureService = mock<TotpCaptureService>();
|
||||||
i18nService = mock<I18nService>();
|
i18nService = mock<I18nService>();
|
||||||
|
configService = mock<ConfigService>();
|
||||||
collect.mockClear();
|
collect.mockClear();
|
||||||
|
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
@ -60,6 +64,7 @@ describe("LoginDetailsSectionComponent", () => {
|
|||||||
{ provide: ToastService, useValue: toastService },
|
{ provide: ToastService, useValue: toastService },
|
||||||
{ provide: TotpCaptureService, useValue: totpCaptureService },
|
{ provide: TotpCaptureService, useValue: totpCaptureService },
|
||||||
{ provide: I18nService, useValue: i18nService },
|
{ provide: I18nService, useValue: i18nService },
|
||||||
|
{ provide: ConfigService, useValue: configService },
|
||||||
{ provide: EventCollectionService, useValue: { collect } },
|
{ provide: EventCollectionService, useValue: { collect } },
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
@ -0,0 +1,22 @@
|
|||||||
|
<bit-dialog dialogSize="default">
|
||||||
|
<span bitDialogTitle>
|
||||||
|
{{ title }}
|
||||||
|
</span>
|
||||||
|
<ng-container bitDialogContent>
|
||||||
|
<vault-cipher-form-generator
|
||||||
|
[type]="params.type"
|
||||||
|
(valueGenerated)="onValueGenerated($event)"
|
||||||
|
></vault-cipher-form-generator>
|
||||||
|
</ng-container>
|
||||||
|
<ng-container bitDialogFooter>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
bitButton
|
||||||
|
buttonType="primary"
|
||||||
|
(click)="selectValue()"
|
||||||
|
data-testid="select-button"
|
||||||
|
>
|
||||||
|
{{ selectButtonText }}
|
||||||
|
</button>
|
||||||
|
</ng-container>
|
||||||
|
</bit-dialog>
|
@ -0,0 +1,125 @@
|
|||||||
|
import { DialogRef, DIALOG_DATA } from "@angular/cdk/dialog";
|
||||||
|
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||||
|
import { NoopAnimationsModule } from "@angular/platform-browser/animations";
|
||||||
|
import { mock, MockProxy } from "jest-mock-extended";
|
||||||
|
import { BehaviorSubject } from "rxjs";
|
||||||
|
|
||||||
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
|
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
|
||||||
|
|
||||||
|
import { UsernameGenerationServiceAbstraction } from "../../../../../../libs/tools/generator/extensions/legacy/src/username-generation.service.abstraction";
|
||||||
|
import { CipherFormGeneratorComponent } from "../cipher-generator/cipher-form-generator.component";
|
||||||
|
|
||||||
|
import {
|
||||||
|
WebVaultGeneratorDialogComponent,
|
||||||
|
WebVaultGeneratorDialogParams,
|
||||||
|
WebVaultGeneratorDialogAction,
|
||||||
|
} from "./web-generator-dialog.component";
|
||||||
|
|
||||||
|
describe("WebVaultGeneratorDialogComponent", () => {
|
||||||
|
let component: WebVaultGeneratorDialogComponent;
|
||||||
|
let fixture: ComponentFixture<WebVaultGeneratorDialogComponent>;
|
||||||
|
|
||||||
|
let dialogRef: MockProxy<DialogRef<any>>;
|
||||||
|
let mockI18nService: MockProxy<I18nService>;
|
||||||
|
let passwordOptionsSubject: BehaviorSubject<any>;
|
||||||
|
let usernameOptionsSubject: BehaviorSubject<any>;
|
||||||
|
let mockPasswordGenerationService: MockProxy<PasswordGenerationServiceAbstraction>;
|
||||||
|
let mockUsernameGenerationService: MockProxy<UsernameGenerationServiceAbstraction>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
dialogRef = mock<DialogRef<any>>();
|
||||||
|
mockI18nService = mock<I18nService>();
|
||||||
|
passwordOptionsSubject = new BehaviorSubject([{ type: "password" }]);
|
||||||
|
usernameOptionsSubject = new BehaviorSubject([{ type: "username" }]);
|
||||||
|
|
||||||
|
mockPasswordGenerationService = mock<PasswordGenerationServiceAbstraction>();
|
||||||
|
mockPasswordGenerationService.getOptions$.mockReturnValue(
|
||||||
|
passwordOptionsSubject.asObservable(),
|
||||||
|
);
|
||||||
|
|
||||||
|
mockUsernameGenerationService = mock<UsernameGenerationServiceAbstraction>();
|
||||||
|
mockUsernameGenerationService.getOptions$.mockReturnValue(
|
||||||
|
usernameOptionsSubject.asObservable(),
|
||||||
|
);
|
||||||
|
|
||||||
|
const mockDialogData: WebVaultGeneratorDialogParams = { type: "password" };
|
||||||
|
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [NoopAnimationsModule, WebVaultGeneratorDialogComponent],
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: DialogRef,
|
||||||
|
useValue: dialogRef,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: DIALOG_DATA,
|
||||||
|
useValue: mockDialogData,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: I18nService,
|
||||||
|
useValue: mockI18nService,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: PlatformUtilsService,
|
||||||
|
useValue: mock<PlatformUtilsService>(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: PasswordGenerationServiceAbstraction,
|
||||||
|
useValue: mockPasswordGenerationService,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: UsernameGenerationServiceAbstraction,
|
||||||
|
useValue: mockUsernameGenerationService,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: CipherFormGeneratorComponent,
|
||||||
|
useValue: {
|
||||||
|
passwordOptions$: passwordOptionsSubject.asObservable(),
|
||||||
|
usernameOptions$: usernameOptionsSubject.asObservable(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}).compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(WebVaultGeneratorDialogComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("initializes without errors", () => {
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("closes the dialog with 'canceled' result when close is called", () => {
|
||||||
|
const closeSpy = jest.spyOn(dialogRef, "close");
|
||||||
|
|
||||||
|
(component as any).close();
|
||||||
|
|
||||||
|
expect(closeSpy).toHaveBeenCalledWith({
|
||||||
|
action: WebVaultGeneratorDialogAction.Canceled,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("closes the dialog with 'selected' result when selectValue is called", () => {
|
||||||
|
const closeSpy = jest.spyOn(dialogRef, "close");
|
||||||
|
const generatedValue = "generated-value";
|
||||||
|
component.onValueGenerated(generatedValue);
|
||||||
|
|
||||||
|
(component as any).selectValue();
|
||||||
|
|
||||||
|
expect(closeSpy).toHaveBeenCalledWith({
|
||||||
|
action: WebVaultGeneratorDialogAction.Selected,
|
||||||
|
generatedValue: generatedValue,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("updates generatedValue when onValueGenerated is called", () => {
|
||||||
|
const generatedValue = "new-generated-value";
|
||||||
|
component.onValueGenerated(generatedValue);
|
||||||
|
|
||||||
|
expect((component as any).generatedValue).toBe(generatedValue);
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,89 @@
|
|||||||
|
import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
|
||||||
|
import { CommonModule } from "@angular/common";
|
||||||
|
import { Component, Inject } from "@angular/core";
|
||||||
|
|
||||||
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
|
import { ButtonModule, DialogService } from "@bitwarden/components";
|
||||||
|
import { CipherFormGeneratorComponent } from "@bitwarden/vault";
|
||||||
|
|
||||||
|
import { DialogModule } from "../../../../../../libs/components/src/dialog";
|
||||||
|
|
||||||
|
export interface WebVaultGeneratorDialogParams {
|
||||||
|
type: "password" | "username";
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WebVaultGeneratorDialogResult {
|
||||||
|
action: WebVaultGeneratorDialogAction;
|
||||||
|
generatedValue?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum WebVaultGeneratorDialogAction {
|
||||||
|
Selected = "selected",
|
||||||
|
Canceled = "canceled",
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: "web-vault-generator-dialog",
|
||||||
|
templateUrl: "./web-generator-dialog.component.html",
|
||||||
|
standalone: true,
|
||||||
|
imports: [CommonModule, CipherFormGeneratorComponent, ButtonModule, DialogModule],
|
||||||
|
})
|
||||||
|
export class WebVaultGeneratorDialogComponent {
|
||||||
|
protected title = this.i18nService.t(this.isPassword ? "passwordGenerator" : "usernameGenerator");
|
||||||
|
protected selectButtonText = this.i18nService.t(
|
||||||
|
this.isPassword ? "useThisPassword" : "useThisUsername",
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the dialog is generating a password/passphrase. If false, it is generating a username.
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
protected get isPassword() {
|
||||||
|
return this.params.type === "password";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The currently generated value.
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
protected generatedValue: string = "";
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Inject(DIALOG_DATA) protected params: WebVaultGeneratorDialogParams,
|
||||||
|
private dialogRef: DialogRef<WebVaultGeneratorDialogResult>,
|
||||||
|
private i18nService: I18nService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close the dialog without selecting a value.
|
||||||
|
*/
|
||||||
|
protected close = () => {
|
||||||
|
this.dialogRef.close({ action: WebVaultGeneratorDialogAction.Canceled });
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close the dialog and select the currently generated value.
|
||||||
|
*/
|
||||||
|
protected selectValue = () => {
|
||||||
|
this.dialogRef.close({
|
||||||
|
action: WebVaultGeneratorDialogAction.Selected,
|
||||||
|
generatedValue: this.generatedValue,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
onValueGenerated(value: string) {
|
||||||
|
this.generatedValue = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens the vault generator dialog.
|
||||||
|
*/
|
||||||
|
static open(dialogService: DialogService, config: DialogConfig<WebVaultGeneratorDialogParams>) {
|
||||||
|
return dialogService.open<WebVaultGeneratorDialogResult, WebVaultGeneratorDialogParams>(
|
||||||
|
WebVaultGeneratorDialogComponent,
|
||||||
|
{
|
||||||
|
...config,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,88 @@
|
|||||||
|
import { DialogRef } from "@angular/cdk/dialog";
|
||||||
|
import { TestBed } from "@angular/core/testing";
|
||||||
|
import { mock } from "jest-mock-extended";
|
||||||
|
import { of } from "rxjs";
|
||||||
|
|
||||||
|
import { DialogService } from "@bitwarden/components";
|
||||||
|
|
||||||
|
import { WebVaultGeneratorDialogComponent } from "../components/web-generator-dialog/web-generator-dialog.component";
|
||||||
|
|
||||||
|
import { WebCipherFormGenerationService } from "./web-cipher-form-generation.service";
|
||||||
|
|
||||||
|
describe("WebCipherFormGenerationService", () => {
|
||||||
|
let service: WebCipherFormGenerationService;
|
||||||
|
let dialogService: jest.Mocked<DialogService>;
|
||||||
|
let closed = of({});
|
||||||
|
const close = jest.fn();
|
||||||
|
const dialogRef = {
|
||||||
|
close,
|
||||||
|
get closed() {
|
||||||
|
return closed;
|
||||||
|
},
|
||||||
|
} as unknown as DialogRef<unknown, unknown>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
dialogService = mock<DialogService>();
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
providers: [
|
||||||
|
WebCipherFormGenerationService,
|
||||||
|
{ provide: DialogService, useValue: dialogService },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
service = TestBed.inject(WebCipherFormGenerationService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("creates without error", () => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("generatePassword", () => {
|
||||||
|
it("opens the password generator dialog and returns the generated value", async () => {
|
||||||
|
const generatedValue = "generated-password";
|
||||||
|
closed = of({ action: "generated", generatedValue });
|
||||||
|
dialogService.open.mockReturnValue(dialogRef);
|
||||||
|
|
||||||
|
const result = await service.generatePassword();
|
||||||
|
|
||||||
|
expect(dialogService.open).toHaveBeenCalledWith(WebVaultGeneratorDialogComponent, {
|
||||||
|
data: { type: "password" },
|
||||||
|
});
|
||||||
|
expect(result).toBe(generatedValue);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns null if the dialog is canceled", async () => {
|
||||||
|
closed = of({ action: "canceled" });
|
||||||
|
dialogService.open.mockReturnValue(dialogRef);
|
||||||
|
|
||||||
|
const result = await service.generatePassword();
|
||||||
|
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("generateUsername", () => {
|
||||||
|
it("opens the username generator dialog and returns the generated value", async () => {
|
||||||
|
const generatedValue = "generated-username";
|
||||||
|
closed = of({ action: "generated", generatedValue });
|
||||||
|
dialogService.open.mockReturnValue(dialogRef);
|
||||||
|
|
||||||
|
const result = await service.generateUsername();
|
||||||
|
|
||||||
|
expect(dialogService.open).toHaveBeenCalledWith(WebVaultGeneratorDialogComponent, {
|
||||||
|
data: { type: "username" },
|
||||||
|
});
|
||||||
|
expect(result).toBe(generatedValue);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns null if the dialog is canceled", async () => {
|
||||||
|
closed = of({ action: "canceled" });
|
||||||
|
dialogService.open.mockReturnValue(dialogRef);
|
||||||
|
|
||||||
|
const result = await service.generateUsername();
|
||||||
|
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,40 @@
|
|||||||
|
import { inject, Injectable } from "@angular/core";
|
||||||
|
import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
|
import { DialogService } from "@bitwarden/components";
|
||||||
|
import { CipherFormGenerationService } from "@bitwarden/vault";
|
||||||
|
|
||||||
|
import { WebVaultGeneratorDialogComponent } from "../components/web-generator-dialog/web-generator-dialog.component";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class WebCipherFormGenerationService implements CipherFormGenerationService {
|
||||||
|
private dialogService = inject(DialogService);
|
||||||
|
|
||||||
|
async generatePassword(): Promise<string> {
|
||||||
|
const dialogRef = WebVaultGeneratorDialogComponent.open(this.dialogService, {
|
||||||
|
data: { type: "password" },
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await firstValueFrom(dialogRef.closed);
|
||||||
|
|
||||||
|
if (result == null || result.action === "canceled") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.generatedValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
async generateUsername(): Promise<string> {
|
||||||
|
const dialogRef = WebVaultGeneratorDialogComponent.open(this.dialogService, {
|
||||||
|
data: { type: "username" },
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await firstValueFrom(dialogRef.closed);
|
||||||
|
|
||||||
|
if (result == null || result.action === "canceled") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.generatedValue;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user