diff --git a/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.html b/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.html index 56acc421de4..c19329f7121 100644 --- a/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.html +++ b/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.html @@ -36,7 +36,10 @@ - + + @@ -62,7 +65,7 @@ type="button" buttonType="secondary" (click)="cancel()" - *ngIf="!showCipherView" + *ngIf="!showCipherView && !showRestore" > {{ "cancel" | i18n }} diff --git a/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts b/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts index 59638aad653..a530fd0cc85 100644 --- a/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts +++ b/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts @@ -49,6 +49,8 @@ import { AttachmentDialogResult, AttachmentsV2Component, } from "../../individual-vault/attachments-v2.component"; +import { RoutedVaultFilterService } from "../../individual-vault/vault-filter/services/routed-vault-filter.service"; +import { RoutedVaultFilterModel } from "../../individual-vault/vault-filter/shared/models/routed-vault-filter.model"; 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"; @@ -82,6 +84,11 @@ export interface VaultItemDialogParams { * If true, the dialog is being opened from the admin console. */ isAdminConsoleAction?: boolean; + + /** + * Function to restore a cipher from the trash. + */ + restore: (c: CipherView) => Promise; } export enum VaultItemDialogResult { @@ -99,6 +106,11 @@ export enum VaultItemDialogResult { * The dialog was closed to navigate the user the premium upgrade page. */ PremiumUpgrade = "premiumUpgrade", + + /** + * A cipher was restored + */ + Restored = "restored", } @Component({ @@ -121,6 +133,7 @@ export enum VaultItemDialogResult { { provide: PremiumUpgradePromptService, useClass: WebVaultPremiumUpgradePromptService }, { provide: ViewPasswordHistoryService, useClass: WebViewPasswordHistoryService }, { provide: CipherFormGenerationService, useClass: WebCipherFormGenerationService }, + RoutedVaultFilterService, ], }) export class VaultItemDialogComponent implements OnInit, OnDestroy { @@ -191,6 +204,20 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy { ), ); + /** + * Determines if the user may restore the item. + * A user may restore items if they have delete permissions and the item is in the trash. + */ + protected async canUserRestore() { + return ( + this.filter?.type === "trash" && + this.cipher?.isDeleted && + (await firstValueFrom(this.canDeleteCipher$)) + ); + } + + protected showRestore: boolean; + protected get loadingForm() { return this.loadForm && !this.formReady; } @@ -230,6 +257,8 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy { protected canDeleteCipher$: Observable; + protected filter: RoutedVaultFilterModel; + constructor( @Inject(DIALOG_DATA) protected params: VaultItemDialogParams, private dialogRef: DialogRef, @@ -246,6 +275,7 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy { private cipherAuthorizationService: CipherAuthorizationService, private apiService: ApiService, private eventCollectionService: EventCollectionService, + private routedVaultFilterService: RoutedVaultFilterService, ) { this.updateTitle(); } @@ -283,6 +313,9 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy { ); } + this.filter = await firstValueFrom(this.routedVaultFilterService.filter$); + + this.showRestore = await this.canUserRestore(); this.performingInitialLoad = false; } @@ -336,6 +369,11 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy { this._formReadySubject.next(); } + restore = async () => { + await this.params.restore(this.cipher); + this.dialogRef.close(VaultItemDialogResult.Restored); + }; + delete = async () => { if (!this.cipher) { return; diff --git a/apps/web/src/app/vault/individual-vault/vault.component.ts b/apps/web/src/app/vault/individual-vault/vault.component.ts index 8c1d08b269c..46c678fd987 100644 --- a/apps/web/src/app/vault/individual-vault/vault.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault.component.ts @@ -717,6 +717,7 @@ export class VaultComponent implements OnInit, OnDestroy { mode, formConfig, activeCollectionId, + restore: this.restore, }); const result = await lastValueFrom(this.vaultItemDialogRef.closed); diff --git a/apps/web/src/app/vault/org-vault/vault.component.ts b/apps/web/src/app/vault/org-vault/vault.component.ts index 14550968ba5..779266d830f 100644 --- a/apps/web/src/app/vault/org-vault/vault.component.ts +++ b/apps/web/src/app/vault/org-vault/vault.component.ts @@ -994,6 +994,7 @@ export class VaultComponent implements OnInit, OnDestroy { disableForm, activeCollectionId, isAdminConsoleAction: true, + restore: this.restore, }); const result = await lastValueFrom(this.vaultItemDialogRef.closed); @@ -1033,7 +1034,7 @@ export class VaultComponent implements OnInit, OnDestroy { }); } - async restore(c: CipherView): Promise { + restore = async (c: CipherView): Promise => { if (!c.isDeleted) { return; } @@ -1064,7 +1065,7 @@ export class VaultComponent implements OnInit, OnDestroy { } catch (e) { this.logService.error(e); } - } + }; async bulkRestore(ciphers: CipherView[]) { if (