mirror of
https://github.com/bitwarden/browser
synced 2025-12-15 07:43:35 +00: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:
@@ -85,6 +85,9 @@ export class CipherAttachmentsComponent implements OnInit, AfterViewInit {
|
||||
/** Emits after a file has been successfully uploaded */
|
||||
@Output() onUploadSuccess = new EventEmitter<void>();
|
||||
|
||||
/** Emits after a file has been successfully removed */
|
||||
@Output() onRemoveSuccess = new EventEmitter<void>();
|
||||
|
||||
cipher: CipherView;
|
||||
|
||||
attachmentForm: CipherAttachmentForm = this.formBuilder.group({
|
||||
@@ -216,5 +219,7 @@ export class CipherAttachmentsComponent implements OnInit, AfterViewInit {
|
||||
if (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 { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||
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 { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
@@ -39,6 +40,8 @@ describe("LoginDetailsSectionComponent", () => {
|
||||
let toastService: MockProxy<ToastService>;
|
||||
let totpCaptureService: MockProxy<TotpCaptureService>;
|
||||
let i18nService: MockProxy<I18nService>;
|
||||
let configService: MockProxy<ConfigService>;
|
||||
|
||||
const collect = jest.fn().mockResolvedValue(null);
|
||||
|
||||
beforeEach(async () => {
|
||||
@@ -49,6 +52,7 @@ describe("LoginDetailsSectionComponent", () => {
|
||||
toastService = mock<ToastService>();
|
||||
totpCaptureService = mock<TotpCaptureService>();
|
||||
i18nService = mock<I18nService>();
|
||||
configService = mock<ConfigService>();
|
||||
collect.mockClear();
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
@@ -60,6 +64,7 @@ describe("LoginDetailsSectionComponent", () => {
|
||||
{ provide: ToastService, useValue: toastService },
|
||||
{ provide: TotpCaptureService, useValue: totpCaptureService },
|
||||
{ provide: I18nService, useValue: i18nService },
|
||||
{ provide: ConfigService, useValue: configService },
|
||||
{ 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user