mirror of
https://github.com/bitwarden/browser.git
synced 2025-01-03 18:28:13 +01:00
[PM-12389] Combined web vault item dialog (#11345)
* [PM-12389] Cleanup attachment dialog UI bugs * [PM-12389] Add formReady event to CipherForm * [PM-12389] Use ngOnChanges for CipherView component initialization * [PM-12389] Cleanup web specific services and components * [PM-12389] Introduce combined Vault Item Dialog component * [PM-12389] Use the new VaultItemDialog in the Individual Vault * [PM-12389] Deprecate the AddEditV2 and View dialogs in Web * [PM-12389] Fix failing test * [PM-12389] Fix broken imports after move * [PM-12389] Remove messages.json addition that is taken care of in another PR
This commit is contained in:
parent
4bd2e158c0
commit
0d877c4e77
apps/web/src/app/vault
components
vault-item-dialog
web-generator-dialog
individual-vault
services
libs/vault/src
@ -0,0 +1,79 @@
|
|||||||
|
<bit-dialog dialogSize="large" background="alt" [loading]="performingInitialLoad">
|
||||||
|
<span bitDialogTitle aria-live="polite">
|
||||||
|
{{ title }}
|
||||||
|
</span>
|
||||||
|
<div bitDialogContent #dialogContent>
|
||||||
|
<app-cipher-view
|
||||||
|
*ngIf="showCipherView"
|
||||||
|
[cipher]="cipher"
|
||||||
|
[collections]="collections"
|
||||||
|
></app-cipher-view>
|
||||||
|
<vault-cipher-form
|
||||||
|
*ngIf="loadForm"
|
||||||
|
formId="cipherForm"
|
||||||
|
[config]="formConfig"
|
||||||
|
[submitBtn]="submitBtn"
|
||||||
|
(formReady)="onFormReady()"
|
||||||
|
(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$ | async)"
|
||||||
|
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>
|
||||||
|
</div>
|
||||||
|
<ng-container bitDialogFooter>
|
||||||
|
<ng-container *ngIf="showCipherView">
|
||||||
|
<button
|
||||||
|
bitButton
|
||||||
|
[bitAction]="switchToEdit"
|
||||||
|
buttonType="primary"
|
||||||
|
type="button"
|
||||||
|
[disabled]="disableEdit"
|
||||||
|
>
|
||||||
|
{{ "edit" | i18n }}
|
||||||
|
</button>
|
||||||
|
</ng-container>
|
||||||
|
<button
|
||||||
|
bitButton
|
||||||
|
type="submit"
|
||||||
|
form="cipherForm"
|
||||||
|
buttonType="primary"
|
||||||
|
#submitBtn
|
||||||
|
[hidden]="showCipherView"
|
||||||
|
>
|
||||||
|
{{ "save" | i18n }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
bitButton
|
||||||
|
type="button"
|
||||||
|
buttonType="secondary"
|
||||||
|
(click)="cancel()"
|
||||||
|
*ngIf="!showCipherView"
|
||||||
|
>
|
||||||
|
{{ "cancel" | i18n }}
|
||||||
|
</button>
|
||||||
|
<div class="tw-ml-auto">
|
||||||
|
<button
|
||||||
|
bitIconButton="bwi-trash"
|
||||||
|
type="button"
|
||||||
|
buttonType="danger"
|
||||||
|
[appA11yTitle]="'delete' | i18n"
|
||||||
|
[bitAction]="delete"
|
||||||
|
[disabled]="!canDelete"
|
||||||
|
></button>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
</bit-dialog>
|
@ -0,0 +1,436 @@
|
|||||||
|
import { DIALOG_DATA, DialogRef } from "@angular/cdk/dialog";
|
||||||
|
import { CommonModule } from "@angular/common";
|
||||||
|
import { Component, ElementRef, Inject, OnDestroy, OnInit, ViewChild } from "@angular/core";
|
||||||
|
import { Router } from "@angular/router";
|
||||||
|
import { firstValueFrom, Subject } from "rxjs";
|
||||||
|
import { map } from "rxjs/operators";
|
||||||
|
|
||||||
|
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||||
|
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 { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
|
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||||
|
import { CipherId } from "@bitwarden/common/types/guid";
|
||||||
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
|
import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service";
|
||||||
|
import { ViewPasswordHistoryService } from "@bitwarden/common/vault/abstractions/view-password-history.service";
|
||||||
|
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||||
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||||
|
import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view";
|
||||||
|
import {
|
||||||
|
AsyncActionsModule,
|
||||||
|
ButtonModule,
|
||||||
|
DialogModule,
|
||||||
|
DialogService,
|
||||||
|
ItemModule,
|
||||||
|
ToastService,
|
||||||
|
} from "@bitwarden/components";
|
||||||
|
import {
|
||||||
|
CipherAttachmentsComponent,
|
||||||
|
CipherFormConfig,
|
||||||
|
CipherFormGenerationService,
|
||||||
|
CipherFormModule,
|
||||||
|
CipherViewComponent,
|
||||||
|
} from "@bitwarden/vault";
|
||||||
|
|
||||||
|
import { SharedModule } from "../../../shared/shared.module";
|
||||||
|
import {
|
||||||
|
AttachmentDialogCloseResult,
|
||||||
|
AttachmentDialogResult,
|
||||||
|
AttachmentsV2Component,
|
||||||
|
} from "../../individual-vault/attachments-v2.component";
|
||||||
|
import { WebCipherFormGenerationService } from "../../services/web-cipher-form-generation.service";
|
||||||
|
import { WebVaultPremiumUpgradePromptService } from "../../services/web-premium-upgrade-prompt.service";
|
||||||
|
import { WebViewPasswordHistoryService } from "../../services/web-view-password-history.service";
|
||||||
|
|
||||||
|
export type VaultItemDialogMode = "view" | "form";
|
||||||
|
|
||||||
|
export interface VaultItemDialogParams {
|
||||||
|
/**
|
||||||
|
* The mode of the dialog.
|
||||||
|
* - `view` is for viewing an existing cipher.
|
||||||
|
* - `form` is for editing or creating a new cipher.
|
||||||
|
*/
|
||||||
|
mode: VaultItemDialogMode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The configuration object for the dialog and form.
|
||||||
|
*/
|
||||||
|
formConfig: CipherFormConfig;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If true, the "edit" button will be disabled in the dialog.
|
||||||
|
*/
|
||||||
|
disableForm?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum VaultItemDialogResult {
|
||||||
|
/**
|
||||||
|
* A cipher was saved (created or updated).
|
||||||
|
*/
|
||||||
|
Saved = "saved",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A cipher was deleted.
|
||||||
|
*/
|
||||||
|
Deleted = "deleted",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The dialog was closed to navigate the user the premium upgrade page.
|
||||||
|
*/
|
||||||
|
PremiumUpgrade = "premiumUpgrade",
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: "app-vault-item-dialog",
|
||||||
|
templateUrl: "vault-item-dialog.component.html",
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
ButtonModule,
|
||||||
|
CipherViewComponent,
|
||||||
|
DialogModule,
|
||||||
|
CommonModule,
|
||||||
|
SharedModule,
|
||||||
|
CipherFormModule,
|
||||||
|
CipherAttachmentsComponent,
|
||||||
|
AsyncActionsModule,
|
||||||
|
ItemModule,
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
{ provide: PremiumUpgradePromptService, useClass: WebVaultPremiumUpgradePromptService },
|
||||||
|
{ provide: ViewPasswordHistoryService, useClass: WebViewPasswordHistoryService },
|
||||||
|
{ provide: CipherFormGenerationService, useClass: WebCipherFormGenerationService },
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class VaultItemDialogComponent implements OnInit, OnDestroy {
|
||||||
|
/**
|
||||||
|
* Reference to the dialog content element. Used to scroll to the top of the dialog when switching modes.
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
@ViewChild("dialogContent")
|
||||||
|
protected dialogContent: ElementRef<HTMLElement>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tracks if the cipher was ever modified while the dialog was open. Used to ensure the dialog emits the correct result
|
||||||
|
* in case of closing with the X button or ESC key.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private _cipherModified: boolean = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The original mode of the form when the dialog is first opened.
|
||||||
|
* Used to determine if the form should switch to edit mode after successfully creating a new cipher.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private _originalFormMode = this.params.formConfig.mode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subject to emit when the form is ready to be displayed.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private _formReadySubject = new Subject<void>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tracks if the dialog is performing the initial load. Used to display a spinner while loading.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
protected performingInitialLoad: boolean = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The title of the dialog. Updates based on the dialog mode and cipher type.
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
protected title: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current cipher being viewed. Undefined if creating a new cipher.
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
protected cipher?: CipherView;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The organization the current cipher belongs to. Undefined if creating a new cipher.
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
protected organization?: Organization;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The collections the current cipher is assigned to. Undefined if creating a new cipher.
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
protected collections?: CollectionView[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flag to indicate if the user has access to attachments via a premium subscription.
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
protected canAccessAttachments$ = this.billingAccountProfileStateService.hasPremiumFromAnySource$;
|
||||||
|
|
||||||
|
protected get loadingForm() {
|
||||||
|
return this.loadForm && !this.formReady;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected get disableEdit() {
|
||||||
|
return this.params.disableForm;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected get canDelete() {
|
||||||
|
return this.cipher?.edit ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected get showCipherView() {
|
||||||
|
return this.cipher != undefined && (this.params.mode === "view" || this.loadingForm);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flag to initialize/attach the form component.
|
||||||
|
*/
|
||||||
|
protected loadForm = this.params.mode === "form";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flag to indicate the form is ready to be displayed.
|
||||||
|
*/
|
||||||
|
protected formReady = false;
|
||||||
|
|
||||||
|
protected formConfig: CipherFormConfig = this.params.formConfig;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Inject(DIALOG_DATA) protected params: VaultItemDialogParams,
|
||||||
|
private dialogRef: DialogRef<VaultItemDialogResult>,
|
||||||
|
private dialogService: DialogService,
|
||||||
|
private i18nService: I18nService,
|
||||||
|
private toastService: ToastService,
|
||||||
|
private messagingService: MessagingService,
|
||||||
|
private logService: LogService,
|
||||||
|
private cipherService: CipherService,
|
||||||
|
private accountService: AccountService,
|
||||||
|
private router: Router,
|
||||||
|
private billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||||
|
) {
|
||||||
|
this.updateTitle();
|
||||||
|
}
|
||||||
|
|
||||||
|
async ngOnInit() {
|
||||||
|
this.cipher = await this.getDecryptedCipherView(this.formConfig);
|
||||||
|
|
||||||
|
if (this.cipher) {
|
||||||
|
this.collections = this.formConfig.collections.filter((c) =>
|
||||||
|
this.cipher.collectionIds?.includes(c.id),
|
||||||
|
);
|
||||||
|
this.organization = this.formConfig.organizations.find(
|
||||||
|
(o) => o.id === this.cipher.organizationId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.performingInitialLoad = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy() {
|
||||||
|
// If the cipher was modified, be sure we emit the saved result in case the dialog was closed with the X button or ESC key.
|
||||||
|
if (this._cipherModified) {
|
||||||
|
this.dialogRef.close(VaultItemDialogResult.Saved);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called by the CipherFormComponent when the cipher is saved successfully.
|
||||||
|
* @param cipherView - The newly saved cipher.
|
||||||
|
*/
|
||||||
|
protected async onCipherSaved(cipherView: CipherView) {
|
||||||
|
// We successfully saved the cipher, update the dialog state and switch to view mode.
|
||||||
|
this.cipher = cipherView;
|
||||||
|
this.collections = this.formConfig.collections.filter((c) =>
|
||||||
|
cipherView.collectionIds?.includes(c.id),
|
||||||
|
);
|
||||||
|
|
||||||
|
// If the cipher was newly created (via add/clone), switch the form to edit for subsequent edits.
|
||||||
|
if (this._originalFormMode === "add" || this._originalFormMode === "clone") {
|
||||||
|
this.formConfig.mode = "edit";
|
||||||
|
}
|
||||||
|
this.formConfig.originalCipher = await this.cipherService.get(cipherView.id);
|
||||||
|
this._cipherModified = true;
|
||||||
|
await this.changeMode("view");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called by the CipherFormComponent when the form is ready to be displayed.
|
||||||
|
*/
|
||||||
|
protected onFormReady() {
|
||||||
|
this.formReady = true;
|
||||||
|
this._formReadySubject.next();
|
||||||
|
}
|
||||||
|
|
||||||
|
delete = async () => {
|
||||||
|
if (!this.cipher) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const confirmed = await this.dialogService.openSimpleDialog({
|
||||||
|
title: { key: "deleteItem" },
|
||||||
|
content: {
|
||||||
|
key: this.cipher.isDeleted ? "permanentlyDeleteItemConfirmation" : "deleteItemConfirmation",
|
||||||
|
},
|
||||||
|
type: "warning",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!confirmed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.deleteCipher();
|
||||||
|
this.toastService.showToast({
|
||||||
|
variant: "success",
|
||||||
|
title: this.i18nService.t("success"),
|
||||||
|
message: this.i18nService.t(
|
||||||
|
this.cipher.isDeleted ? "permanentlyDeletedItem" : "deletedItem",
|
||||||
|
),
|
||||||
|
});
|
||||||
|
this.messagingService.send(
|
||||||
|
this.cipher.isDeleted ? "permanentlyDeletedCipher" : "deletedCipher",
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
this.logService.error(e);
|
||||||
|
}
|
||||||
|
this._cipherModified = false;
|
||||||
|
this.dialogRef.close(VaultItemDialogResult.Deleted);
|
||||||
|
};
|
||||||
|
|
||||||
|
openAttachmentsDialog = async () => {
|
||||||
|
const dialogRef = this.dialogService.open<AttachmentDialogCloseResult, { cipherId: CipherId }>(
|
||||||
|
AttachmentsV2Component,
|
||||||
|
{
|
||||||
|
data: {
|
||||||
|
cipherId: this.formConfig.originalCipher?.id as CipherId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await firstValueFrom(dialogRef.closed);
|
||||||
|
|
||||||
|
if (
|
||||||
|
result.action === AttachmentDialogResult.Removed ||
|
||||||
|
result.action === AttachmentDialogResult.Uploaded
|
||||||
|
) {
|
||||||
|
this._cipherModified = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
switchToEdit = async () => {
|
||||||
|
if (!this.cipher) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await this.changeMode("form");
|
||||||
|
};
|
||||||
|
|
||||||
|
cancel = async () => {
|
||||||
|
// We're in View mode, or we don't have a cipher, close the dialog.
|
||||||
|
if (this.params.mode === "view" || this.cipher == null) {
|
||||||
|
this.dialogRef.close(this._cipherModified ? VaultItemDialogResult.Saved : undefined);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We're in Form mode, and we have a cipher, switch back to View mode.
|
||||||
|
await this.changeMode("view");
|
||||||
|
};
|
||||||
|
|
||||||
|
private async getDecryptedCipherView(config: CipherFormConfig) {
|
||||||
|
if (config.originalCipher == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const activeUserId = await firstValueFrom(
|
||||||
|
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||||
|
);
|
||||||
|
return await config.originalCipher.decrypt(
|
||||||
|
await this.cipherService.getKeyForCipherKeyDecryption(config.originalCipher, activeUserId),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateTitle() {
|
||||||
|
let partOne: string;
|
||||||
|
|
||||||
|
if (this.params.mode === "view") {
|
||||||
|
partOne = "viewItemType";
|
||||||
|
} else if (this.formConfig.mode === "edit" || this.formConfig.mode === "partial-edit") {
|
||||||
|
partOne = "editItemHeader";
|
||||||
|
} else {
|
||||||
|
partOne = "newItemHeader";
|
||||||
|
}
|
||||||
|
|
||||||
|
const type = this.cipher?.type ?? this.formConfig.cipherType ?? CipherType.Login;
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case CipherType.Login:
|
||||||
|
this.title = this.i18nService.t(partOne, this.i18nService.t("typeLogin").toLowerCase());
|
||||||
|
break;
|
||||||
|
case CipherType.Card:
|
||||||
|
this.title = this.i18nService.t(partOne, this.i18nService.t("typeCard").toLowerCase());
|
||||||
|
break;
|
||||||
|
case CipherType.Identity:
|
||||||
|
this.title = this.i18nService.t(partOne, this.i18nService.t("typeIdentity").toLowerCase());
|
||||||
|
break;
|
||||||
|
case CipherType.SecureNote:
|
||||||
|
this.title = this.i18nService.t(partOne, this.i18nService.t("note").toLowerCase());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Changes the mode of the dialog. When switching to Form mode, the form is initialized first then displayed once ready.
|
||||||
|
* @param mode
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private async changeMode(mode: VaultItemDialogMode) {
|
||||||
|
this.formReady = false;
|
||||||
|
|
||||||
|
if (mode == "form") {
|
||||||
|
this.loadForm = true;
|
||||||
|
// Wait for the formReadySubject to emit before continuing.
|
||||||
|
// This helps prevent flashing an empty dialog while the form is initializing.
|
||||||
|
await firstValueFrom(this._formReadySubject);
|
||||||
|
} else {
|
||||||
|
this.loadForm = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.params.mode = mode;
|
||||||
|
this.updateTitle();
|
||||||
|
// Scroll to the top of the dialog content when switching modes.
|
||||||
|
this.dialogContent.nativeElement.parentElement.scrollTop = 0;
|
||||||
|
|
||||||
|
// Update the URL query params to reflect the new mode.
|
||||||
|
await this.router.navigate([], {
|
||||||
|
queryParams: {
|
||||||
|
action: mode === "form" ? "edit" : "view",
|
||||||
|
itemId: this.cipher?.id,
|
||||||
|
},
|
||||||
|
queryParamsHandling: "merge",
|
||||||
|
replaceUrl: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method to delete cipher.
|
||||||
|
*/
|
||||||
|
private async deleteCipher(): Promise<void> {
|
||||||
|
const asAdmin = this.organization?.canEditAllCiphers;
|
||||||
|
if (this.cipher.isDeleted) {
|
||||||
|
await this.cipherService.deleteWithServer(this.cipher.id, asAdmin);
|
||||||
|
} else {
|
||||||
|
await this.cipherService.softDeleteWithServer(this.cipher.id, asAdmin);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens the VaultItemDialog.
|
||||||
|
* @param dialogService
|
||||||
|
* @param params
|
||||||
|
*/
|
||||||
|
static open(dialogService: DialogService, params: VaultItemDialogParams) {
|
||||||
|
return dialogService.open<VaultItemDialogResult, VaultItemDialogParams>(
|
||||||
|
VaultItemDialogComponent,
|
||||||
|
{
|
||||||
|
data: params,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
import { DialogRef, DIALOG_DATA } from "@angular/cdk/dialog";
|
import { DIALOG_DATA, DialogRef } from "@angular/cdk/dialog";
|
||||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||||
import { NoopAnimationsModule } from "@angular/platform-browser/animations";
|
import { NoopAnimationsModule } from "@angular/platform-browser/animations";
|
||||||
import { mock, MockProxy } from "jest-mock-extended";
|
import { mock, MockProxy } from "jest-mock-extended";
|
||||||
@ -6,15 +6,16 @@ import { BehaviorSubject } from "rxjs";
|
|||||||
|
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
|
import {
|
||||||
|
PasswordGenerationServiceAbstraction,
|
||||||
import { UsernameGenerationServiceAbstraction } from "../../../../../../libs/tools/generator/extensions/legacy/src/username-generation.service.abstraction";
|
UsernameGenerationServiceAbstraction,
|
||||||
import { CipherFormGeneratorComponent } from "../cipher-generator/cipher-form-generator.component";
|
} from "@bitwarden/generator-legacy";
|
||||||
|
import { CipherFormGeneratorComponent } from "@bitwarden/vault";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
WebVaultGeneratorDialogAction,
|
||||||
WebVaultGeneratorDialogComponent,
|
WebVaultGeneratorDialogComponent,
|
||||||
WebVaultGeneratorDialogParams,
|
WebVaultGeneratorDialogParams,
|
||||||
WebVaultGeneratorDialogAction,
|
|
||||||
} from "./web-generator-dialog.component";
|
} from "./web-generator-dialog.component";
|
||||||
|
|
||||||
describe("WebVaultGeneratorDialogComponent", () => {
|
describe("WebVaultGeneratorDialogComponent", () => {
|
@ -3,11 +3,9 @@ import { CommonModule } from "@angular/common";
|
|||||||
import { Component, Inject } from "@angular/core";
|
import { Component, Inject } from "@angular/core";
|
||||||
|
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { ButtonModule, DialogService } from "@bitwarden/components";
|
import { ButtonModule, DialogModule, DialogService } from "@bitwarden/components";
|
||||||
import { CipherFormGeneratorComponent } from "@bitwarden/vault";
|
import { CipherFormGeneratorComponent } from "@bitwarden/vault";
|
||||||
|
|
||||||
import { DialogModule } from "../../../../../../libs/components/src/dialog";
|
|
||||||
|
|
||||||
export interface WebVaultGeneratorDialogParams {
|
export interface WebVaultGeneratorDialogParams {
|
||||||
type: "password" | "username";
|
type: "password" | "username";
|
||||||
}
|
}
|
@ -17,9 +17,8 @@ import {
|
|||||||
CipherFormModule,
|
CipherFormModule,
|
||||||
} from "@bitwarden/vault";
|
} 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 { SharedModule } from "../../shared/shared.module";
|
||||||
|
import { WebCipherFormGenerationService } from "../services/web-cipher-form-generation.service";
|
||||||
|
|
||||||
import { AttachmentsV2Component } from "./attachments-v2.component";
|
import { AttachmentsV2Component } from "./attachments-v2.component";
|
||||||
|
|
||||||
@ -48,13 +47,13 @@ export interface AddEditCipherDialogCloseResult {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Component for viewing a cipher, presented in a dialog.
|
* Component for viewing a cipher, presented in a dialog.
|
||||||
|
* @deprecated Use the VaultItemDialogComponent instead.
|
||||||
*/
|
*/
|
||||||
@Component({
|
@Component({
|
||||||
selector: "app-vault-add-edit-v2",
|
selector: "app-vault-add-edit-v2",
|
||||||
templateUrl: "add-edit-v2.component.html",
|
templateUrl: "add-edit-v2.component.html",
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [
|
imports: [
|
||||||
CipherViewComponent,
|
|
||||||
CommonModule,
|
CommonModule,
|
||||||
AsyncActionsModule,
|
AsyncActionsModule,
|
||||||
DialogModule,
|
DialogModule,
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
<bit-dialog dialogSize="default">
|
<bit-dialog dialogSize="default" background="alt">
|
||||||
<span bitDialogTitle>
|
<span bitDialogTitle>
|
||||||
{{ "attachments" | i18n }}
|
{{ "attachments" | i18n }}
|
||||||
</span>
|
</span>
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { DialogRef } from "@angular/cdk/dialog";
|
||||||
import {
|
import {
|
||||||
ChangeDetectorRef,
|
ChangeDetectorRef,
|
||||||
Component,
|
Component,
|
||||||
@ -63,6 +64,7 @@ import { CollectionView } from "@bitwarden/common/vault/models/view/collection.v
|
|||||||
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 {
|
import {
|
||||||
|
CipherFormConfig,
|
||||||
CollectionAssignmentResult,
|
CollectionAssignmentResult,
|
||||||
DefaultCipherFormConfigService,
|
DefaultCipherFormConfigService,
|
||||||
PasswordRepromptService,
|
PasswordRepromptService,
|
||||||
@ -75,16 +77,16 @@ import {
|
|||||||
CollectionDialogTabType,
|
CollectionDialogTabType,
|
||||||
openCollectionDialog,
|
openCollectionDialog,
|
||||||
} from "../components/collection-dialog";
|
} from "../components/collection-dialog";
|
||||||
|
import {
|
||||||
|
VaultItemDialogComponent,
|
||||||
|
VaultItemDialogMode,
|
||||||
|
VaultItemDialogResult,
|
||||||
|
} from "../components/vault-item-dialog/vault-item-dialog.component";
|
||||||
import { VaultItem } from "../components/vault-items/vault-item";
|
import { VaultItem } from "../components/vault-items/vault-item";
|
||||||
import { VaultItemEvent } from "../components/vault-items/vault-item-event";
|
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 {
|
import {
|
||||||
AttachmentDialogCloseResult,
|
AttachmentDialogCloseResult,
|
||||||
@ -116,11 +118,6 @@ import { FolderFilter, OrganizationFilter } from "./vault-filter/shared/models/v
|
|||||||
import { VaultFilterModule } from "./vault-filter/vault-filter.module";
|
import { VaultFilterModule } from "./vault-filter/vault-filter.module";
|
||||||
import { VaultHeaderComponent } from "./vault-header/vault-header.component";
|
import { VaultHeaderComponent } from "./vault-header/vault-header.component";
|
||||||
import { VaultOnboardingComponent } from "./vault-onboarding/vault-onboarding.component";
|
import { VaultOnboardingComponent } from "./vault-onboarding/vault-onboarding.component";
|
||||||
import {
|
|
||||||
openViewCipherDialog,
|
|
||||||
ViewCipherDialogCloseResult,
|
|
||||||
ViewCipherDialogResult,
|
|
||||||
} from "./view.component";
|
|
||||||
|
|
||||||
const BroadcasterSubscriptionId = "VaultComponent";
|
const BroadcasterSubscriptionId = "VaultComponent";
|
||||||
const SearchTextDebounceInterval = 200;
|
const SearchTextDebounceInterval = 200;
|
||||||
@ -179,6 +176,8 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
private destroy$ = new Subject<void>();
|
private destroy$ = new Subject<void>();
|
||||||
private extensionRefreshEnabled: boolean;
|
private extensionRefreshEnabled: boolean;
|
||||||
|
|
||||||
|
private vaultItemDialogRef?: DialogRef<VaultItemDialogResult> | undefined;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private syncService: SyncService,
|
private syncService: SyncService,
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
@ -352,12 +351,20 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
firstSetup$
|
firstSetup$
|
||||||
.pipe(
|
.pipe(
|
||||||
switchMap(() => this.route.queryParams),
|
switchMap(() => this.route.queryParams),
|
||||||
|
// Only process the queryParams if the dialog is not open (only when extension refresh is enabled)
|
||||||
|
filter(() => this.vaultItemDialogRef == undefined || !this.extensionRefreshEnabled),
|
||||||
switchMap(async (params) => {
|
switchMap(async (params) => {
|
||||||
const cipherId = getCipherIdFromParams(params);
|
const cipherId = getCipherIdFromParams(params);
|
||||||
|
|
||||||
if (cipherId) {
|
if (cipherId) {
|
||||||
if (await this.cipherService.get(cipherId)) {
|
if (await this.cipherService.get(cipherId)) {
|
||||||
if (params.action === "view") {
|
let action = params.action;
|
||||||
|
// Default to "view" if extension refresh is enabled
|
||||||
|
if (action == null && this.extensionRefreshEnabled) {
|
||||||
|
action = "view";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === "view") {
|
||||||
await this.viewCipherById(cipherId);
|
await this.viewCipherById(cipherId);
|
||||||
} else {
|
} else {
|
||||||
await this.editCipherId(cipherId);
|
await this.editCipherId(cipherId);
|
||||||
@ -526,7 +533,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
*/
|
*/
|
||||||
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 });
|
await this.go({ cipherId: null, itemId: null });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -590,6 +597,29 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open the combined view / edit dialog for a cipher.
|
||||||
|
* @param mode - Starting mode of the dialog.
|
||||||
|
* @param formConfig - Configuration for the form when editing/adding a cipher.
|
||||||
|
*/
|
||||||
|
async openVaultItemDialog(mode: VaultItemDialogMode, formConfig: CipherFormConfig) {
|
||||||
|
this.vaultItemDialogRef = VaultItemDialogComponent.open(this.dialogService, {
|
||||||
|
mode,
|
||||||
|
formConfig,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await lastValueFrom(this.vaultItemDialogRef.closed);
|
||||||
|
this.vaultItemDialogRef = undefined;
|
||||||
|
|
||||||
|
// If the dialog was closed by deleting the cipher, refresh the vault.
|
||||||
|
if (result === VaultItemDialogResult.Deleted || result === VaultItemDialogResult.Saved) {
|
||||||
|
this.refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear the query params when the dialog closes
|
||||||
|
await this.go({ cipherId: null, itemId: null, action: null });
|
||||||
|
}
|
||||||
|
|
||||||
async addCipher(cipherType?: CipherType) {
|
async addCipher(cipherType?: CipherType) {
|
||||||
if (this.extensionRefreshEnabled) {
|
if (this.extensionRefreshEnabled) {
|
||||||
return this.addCipherV2(cipherType);
|
return this.addCipherV2(cipherType);
|
||||||
@ -643,23 +673,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
folderId: this.activeFilter.folderId,
|
folderId: this.activeFilter.folderId,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Open the dialog.
|
await this.openVaultItemDialog("form", cipherFormConfig);
|
||||||
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 editCipher(cipher: CipherView, cloneMode?: boolean) {
|
async editCipher(cipher: CipherView, cloneMode?: boolean) {
|
||||||
@ -675,7 +689,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
!(await this.passwordRepromptService.showPasswordPrompt())
|
!(await this.passwordRepromptService.showPasswordPrompt())
|
||||||
) {
|
) {
|
||||||
// didn't pass password prompt, so don't open add / edit modal
|
// didn't pass password prompt, so don't open add / edit modal
|
||||||
this.go({ cipherId: null, itemId: null, action: null });
|
await this.go({ cipherId: null, itemId: null, action: null });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -707,14 +721,14 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
modal.onClosedPromise().then(() => {
|
modal.onClosedPromise().then(() => {
|
||||||
this.go({ cipherId: null, itemId: null, action: null });
|
void this.go({ cipherId: null, itemId: null, action: null });
|
||||||
});
|
});
|
||||||
|
|
||||||
return childComponent;
|
return childComponent;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Edit a cipher using the new AddEditCipherDialogV2 component.
|
* Edit a cipher using the new VaultItemDialog.
|
||||||
*
|
*
|
||||||
* @param cipher
|
* @param cipher
|
||||||
* @param cloneMode
|
* @param cloneMode
|
||||||
@ -726,31 +740,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
cipher.type,
|
cipher.type,
|
||||||
);
|
);
|
||||||
|
|
||||||
const dialogRef = openAddEditCipherDialog(this.dialogService, {
|
await this.openVaultItemDialog("form", cipherFormConfig);
|
||||||
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 });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -777,39 +767,17 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
!(await this.passwordRepromptService.showPasswordPrompt())
|
!(await this.passwordRepromptService.showPasswordPrompt())
|
||||||
) {
|
) {
|
||||||
// Didn't pass password prompt, so don't open add / edit modal.
|
// Didn't pass password prompt, so don't open add / edit modal.
|
||||||
this.go({ cipherId: null, itemId: null });
|
await this.go({ cipherId: null, itemId: null, action: null });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const activeUserId = await firstValueFrom(
|
const cipherFormConfig = await this.cipherFormConfigService.buildConfig(
|
||||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
cipher.edit ? "edit" : "partial-edit",
|
||||||
);
|
cipher.id as CipherId,
|
||||||
// Decrypt the cipher.
|
cipher.type,
|
||||||
const cipherView = await cipher.decrypt(
|
|
||||||
await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Open the dialog.
|
await this.openVaultItemDialog("view", cipherFormConfig);
|
||||||
const dialogRef = openViewCipherDialog(this.dialogService, {
|
|
||||||
data: { cipher: cipherView },
|
|
||||||
});
|
|
||||||
|
|
||||||
// Wait for the dialog to close.
|
|
||||||
const result: ViewCipherDialogCloseResult = await lastValueFrom(dialogRef.closed);
|
|
||||||
|
|
||||||
// If the dialog was closed by clicking the edit button, navigate to open the edit dialog.
|
|
||||||
if (result?.action === ViewCipherDialogResult.Edited) {
|
|
||||||
this.go({ itemId: cipherView.id, action: "edit" });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the dialog was closed by deleting the cipher, refresh the vault.
|
|
||||||
if (result?.action === ViewCipherDialogResult.Deleted) {
|
|
||||||
this.refresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear the query params when the view dialog closes
|
|
||||||
this.go({ cipherId: null, itemId: null, action: null });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async addCollection() {
|
async addCollection() {
|
||||||
@ -958,7 +926,10 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const component = await this.editCipher(cipher, true);
|
const component = await this.editCipher(cipher, true);
|
||||||
component.cloneMode = true;
|
|
||||||
|
if (component != null) {
|
||||||
|
component.cloneMode = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async restore(c: CipherView): Promise<boolean> {
|
async restore(c: CipherView): Promise<boolean> {
|
||||||
@ -1220,7 +1191,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
return organization.canEditAllCiphers;
|
return organization.canEditAllCiphers;
|
||||||
}
|
}
|
||||||
|
|
||||||
private go(queryParams: any = null) {
|
private async go(queryParams: any = null) {
|
||||||
if (queryParams == null) {
|
if (queryParams == null) {
|
||||||
queryParams = {
|
queryParams = {
|
||||||
favorites: this.activeFilter.isFavorites || null,
|
favorites: this.activeFilter.isFavorites || null,
|
||||||
@ -1231,7 +1202,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
void this.router.navigate([], {
|
await this.router.navigate([], {
|
||||||
relativeTo: this.route,
|
relativeTo: this.route,
|
||||||
queryParams: queryParams,
|
queryParams: queryParams,
|
||||||
queryParamsHandling: "merge",
|
queryParamsHandling: "merge",
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
|
import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
|
||||||
import { CommonModule } from "@angular/common";
|
import { CommonModule } from "@angular/common";
|
||||||
import { Component, Inject, OnInit, EventEmitter } from "@angular/core";
|
import { Component, EventEmitter, Inject, OnInit } from "@angular/core";
|
||||||
|
|
||||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||||
@ -52,6 +52,7 @@ export interface ViewCipherDialogCloseResult {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Component for viewing a cipher, presented in a dialog.
|
* Component for viewing a cipher, presented in a dialog.
|
||||||
|
* @deprecated Use the VaultItemDialogComponent instead.
|
||||||
*/
|
*/
|
||||||
@Component({
|
@Component({
|
||||||
selector: "app-vault-view",
|
selector: "app-vault-view",
|
||||||
|
@ -1,15 +1,12 @@
|
|||||||
import { DialogRef } from "@angular/cdk/dialog";
|
import { DialogRef } from "@angular/cdk/dialog";
|
||||||
import { TestBed } from "@angular/core/testing";
|
import { TestBed } from "@angular/core/testing";
|
||||||
import { Router } from "@angular/router";
|
import { Router } from "@angular/router";
|
||||||
import { of, lastValueFrom } from "rxjs";
|
import { lastValueFrom, of } from "rxjs";
|
||||||
|
|
||||||
import { OrganizationId } from "@bitwarden/common/types/guid";
|
import { OrganizationId } from "@bitwarden/common/types/guid";
|
||||||
import { DialogService } from "@bitwarden/components";
|
import { DialogService } from "@bitwarden/components";
|
||||||
|
|
||||||
import {
|
import { VaultItemDialogResult } from "../components/vault-item-dialog/vault-item-dialog.component";
|
||||||
ViewCipherDialogCloseResult,
|
|
||||||
ViewCipherDialogResult,
|
|
||||||
} from "../individual-vault/view.component";
|
|
||||||
|
|
||||||
import { WebVaultPremiumUpgradePromptService } from "./web-premium-upgrade-prompt.service";
|
import { WebVaultPremiumUpgradePromptService } from "./web-premium-upgrade-prompt.service";
|
||||||
|
|
||||||
@ -17,7 +14,7 @@ describe("WebVaultPremiumUpgradePromptService", () => {
|
|||||||
let service: WebVaultPremiumUpgradePromptService;
|
let service: WebVaultPremiumUpgradePromptService;
|
||||||
let dialogServiceMock: jest.Mocked<DialogService>;
|
let dialogServiceMock: jest.Mocked<DialogService>;
|
||||||
let routerMock: jest.Mocked<Router>;
|
let routerMock: jest.Mocked<Router>;
|
||||||
let dialogRefMock: jest.Mocked<DialogRef<ViewCipherDialogCloseResult>>;
|
let dialogRefMock: jest.Mocked<DialogRef<VaultItemDialogResult>>;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
dialogServiceMock = {
|
dialogServiceMock = {
|
||||||
@ -30,7 +27,7 @@ describe("WebVaultPremiumUpgradePromptService", () => {
|
|||||||
|
|
||||||
dialogRefMock = {
|
dialogRefMock = {
|
||||||
close: jest.fn(),
|
close: jest.fn(),
|
||||||
} as unknown as jest.Mocked<DialogRef<ViewCipherDialogCloseResult>>;
|
} as unknown as jest.Mocked<DialogRef<VaultItemDialogResult>>;
|
||||||
|
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
providers: [
|
providers: [
|
||||||
@ -62,9 +59,7 @@ describe("WebVaultPremiumUpgradePromptService", () => {
|
|||||||
"billing",
|
"billing",
|
||||||
"subscription",
|
"subscription",
|
||||||
]);
|
]);
|
||||||
expect(dialogRefMock.close).toHaveBeenCalledWith({
|
expect(dialogRefMock.close).toHaveBeenCalledWith(VaultItemDialogResult.PremiumUpgrade);
|
||||||
action: ViewCipherDialogResult.PremiumUpgrade,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("prompts for premium upgrade and navigates to premium subscription if organizationId is not provided", async () => {
|
it("prompts for premium upgrade and navigates to premium subscription if organizationId is not provided", async () => {
|
||||||
@ -79,9 +74,7 @@ describe("WebVaultPremiumUpgradePromptService", () => {
|
|||||||
type: "success",
|
type: "success",
|
||||||
});
|
});
|
||||||
expect(routerMock.navigate).toHaveBeenCalledWith(["settings/subscription/premium"]);
|
expect(routerMock.navigate).toHaveBeenCalledWith(["settings/subscription/premium"]);
|
||||||
expect(dialogRefMock.close).toHaveBeenCalledWith({
|
expect(dialogRefMock.close).toHaveBeenCalledWith(VaultItemDialogResult.PremiumUpgrade);
|
||||||
action: ViewCipherDialogResult.PremiumUpgrade,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does not navigate or close dialog if upgrade is no action is taken", async () => {
|
it("does not navigate or close dialog if upgrade is no action is taken", async () => {
|
||||||
|
@ -6,10 +6,7 @@ import { OrganizationId } from "@bitwarden/common/types/guid";
|
|||||||
import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service";
|
import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service";
|
||||||
import { DialogService } from "@bitwarden/components";
|
import { DialogService } from "@bitwarden/components";
|
||||||
|
|
||||||
import {
|
import { VaultItemDialogResult } from "../components/vault-item-dialog/vault-item-dialog.component";
|
||||||
ViewCipherDialogCloseResult,
|
|
||||||
ViewCipherDialogResult,
|
|
||||||
} from "../individual-vault/view.component";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This service is used to prompt the user to upgrade to premium.
|
* This service is used to prompt the user to upgrade to premium.
|
||||||
@ -19,7 +16,7 @@ export class WebVaultPremiumUpgradePromptService implements PremiumUpgradePrompt
|
|||||||
constructor(
|
constructor(
|
||||||
private dialogService: DialogService,
|
private dialogService: DialogService,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
private dialog: DialogRef<ViewCipherDialogCloseResult>,
|
private dialog: DialogRef<VaultItemDialogResult>,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -51,7 +48,7 @@ export class WebVaultPremiumUpgradePromptService implements PremiumUpgradePrompt
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (upgradeConfirmed) {
|
if (upgradeConfirmed) {
|
||||||
this.dialog.close({ action: ViewCipherDialogResult.PremiumUpgrade });
|
this.dialog.close(VaultItemDialogResult.PremiumUpgrade);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { NgIf } from "@angular/common";
|
import { NgIf } from "@angular/common";
|
||||||
import {
|
import {
|
||||||
AfterViewInit,
|
AfterViewInit,
|
||||||
|
ChangeDetectorRef,
|
||||||
Component,
|
Component,
|
||||||
DestroyRef,
|
DestroyRef,
|
||||||
EventEmitter,
|
EventEmitter,
|
||||||
@ -14,6 +15,7 @@ import {
|
|||||||
} from "@angular/core";
|
} from "@angular/core";
|
||||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||||
import { FormBuilder, FormGroup, ReactiveFormsModule } from "@angular/forms";
|
import { FormBuilder, FormGroup, ReactiveFormsModule } from "@angular/forms";
|
||||||
|
import { Subject } from "rxjs";
|
||||||
|
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { CipherType, SecureNoteType } from "@bitwarden/common/vault/enums";
|
import { CipherType, SecureNoteType } from "@bitwarden/common/vault/enums";
|
||||||
@ -101,6 +103,10 @@ export class CipherFormComponent implements AfterViewInit, OnInit, OnChanges, Ci
|
|||||||
*/
|
*/
|
||||||
@Output() cipherSaved = new EventEmitter<CipherView>();
|
@Output() cipherSaved = new EventEmitter<CipherView>();
|
||||||
|
|
||||||
|
private formReadySubject = new Subject<void>();
|
||||||
|
|
||||||
|
@Output() formReady = this.formReadySubject.asObservable();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The original cipher being edited or cloned. Null for add mode.
|
* The original cipher being edited or cloned. Null for add mode.
|
||||||
*/
|
*/
|
||||||
@ -173,9 +179,13 @@ export class CipherFormComponent implements AfterViewInit, OnInit, OnChanges, Ci
|
|||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
|
|
||||||
|
// Force change detection so that all child components are destroyed and re-created
|
||||||
|
this.changeDetectorRef.detectChanges();
|
||||||
|
|
||||||
this.updatedCipherView = new CipherView();
|
this.updatedCipherView = new CipherView();
|
||||||
this.originalCipherView = null;
|
this.originalCipherView = null;
|
||||||
this.cipherForm.reset();
|
this.cipherForm = this.formBuilder.group<CipherForm>({});
|
||||||
|
|
||||||
if (this.config == null) {
|
if (this.config == null) {
|
||||||
return;
|
return;
|
||||||
@ -207,6 +217,7 @@ export class CipherFormComponent implements AfterViewInit, OnInit, OnChanges, Ci
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
|
this.formReadySubject.next();
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@ -214,6 +225,7 @@ export class CipherFormComponent implements AfterViewInit, OnInit, OnChanges, Ci
|
|||||||
private addEditFormService: CipherFormService,
|
private addEditFormService: CipherFormService,
|
||||||
private toastService: ToastService,
|
private toastService: ToastService,
|
||||||
private i18nService: I18nService,
|
private i18nService: I18nService,
|
||||||
|
private changeDetectorRef: ChangeDetectorRef,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { CommonModule } from "@angular/common";
|
import { CommonModule } from "@angular/common";
|
||||||
import { Component, Input, OnDestroy, OnInit } from "@angular/core";
|
import { Component, Input, OnChanges, OnDestroy } from "@angular/core";
|
||||||
import { firstValueFrom, Observable, Subject, takeUntil } from "rxjs";
|
import { firstValueFrom, Observable, Subject, takeUntil } from "rxjs";
|
||||||
|
|
||||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||||
@ -44,7 +44,7 @@ import { ViewIdentitySectionsComponent } from "./view-identity-sections/view-ide
|
|||||||
AutofillOptionsViewComponent,
|
AutofillOptionsViewComponent,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class CipherViewComponent implements OnInit, OnDestroy {
|
export class CipherViewComponent implements OnChanges, OnDestroy {
|
||||||
@Input({ required: true }) cipher: CipherView;
|
@Input({ required: true }) cipher: CipherView;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -63,7 +63,11 @@ export class CipherViewComponent implements OnInit, OnDestroy {
|
|||||||
private folderService: FolderService,
|
private folderService: FolderService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnChanges() {
|
||||||
|
if (this.cipher == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await this.loadCipherData();
|
await this.loadCipherData();
|
||||||
|
|
||||||
this.cardIsExpired = isCardExpired(this.cipher.card);
|
this.cardIsExpired = isCardExpired(this.cipher.card);
|
||||||
|
Loading…
Reference in New Issue
Block a user