[Soft Delete] - Trash bin in browser extension

This commit is contained in:
Chad Scharf 2020-04-13 10:26:11 -04:00
parent b8b3c01d68
commit 4116302965
6 changed files with 116 additions and 7 deletions

View File

@ -1257,5 +1257,30 @@
"lock": {
"message": "Lock",
"description": "Verb form: to make secure or inaccesible by"
},
"trash": {
"message": "Trash",
"description": "Noun: a special folder to hold deleted items"
},
"searchTrash": {
"message": "Search trash"
},
"permanentlyDeleteItem": {
"message": "Permanently Delete Item"
},
"permanentlyDeleteItemConfirmation": {
"message": "Are you sure you want to permanently delete this item?"
},
"permanentlyDeletedItem": {
"message": "Permanently Deleted item"
},
"restoreItem": {
"message": "Restore Item"
},
"restoreItemConfirmation": {
"message": "Are you sure you want to restore this item?"
},
"restoredItem": {
"message": "Restored Item"
}
}

View File

@ -79,7 +79,11 @@ export class CiphersComponent extends BaseCiphersComponent implements OnInit, On
}
}
if (params.type) {
if (params.deleted) {
this.groupingTitle = this.i18nService.t('trash');
this.searchPlaceholder = this.i18nService.t('searchTrash');
await this.load(null, true);
} else if (params.type) {
this.searchPlaceholder = this.i18nService.t('searchType');
this.type = parseInt(params.type, null);
switch (this.type) {
@ -198,6 +202,9 @@ export class CiphersComponent extends BaseCiphersComponent implements OnInit, On
}
addCipher() {
if (this.deleted) {
return false;
}
super.addCipher();
this.router.navigate(['/add-cipher'], {
queryParams: {

View File

@ -121,6 +121,23 @@
(onSelected)="selectCipher($event)" (onDoubleSelected)="launchCipher($event)"></app-ciphers-list>
</div>
</div>
<div class="box list" *ngIf="deletedCount">
<div class="box-header">
{{'trash' | i18n}}
<span class="flex-right">{{deletedCount}}</span>
</div>
<div class="box-content single-line">
<a href="#" class="box-content-row" appStopClick appBlurClick
(click)="selectTrash()">
<div class="row-main">
<div class="icon"><i class="fa fa-fw fa-lg fa-trash-o"></i></div>
<span class="text">{{'trash' | i18n}}</span>
</div>
<span class="row-sub-label">{{deletedCount}}</span>
<span><i class="fa fa-chevron-right fa-lg row-sub-icon"></i></span>
</a>
</div>
</div>
</ng-container>
<ng-container *ngIf="showSearching()">
<div class="no-items" *ngIf="!ciphers || !ciphers.length">

View File

@ -57,6 +57,7 @@ export class GroupingsComponent extends BaseGroupingsComponent implements OnInit
showLeftHeader = true;
searchPending = false;
searchTypeSearch = false;
deletedCount: number = 0;
private loadedTimeout: number;
private selectedTimeout: number;
@ -167,6 +168,7 @@ export class GroupingsComponent extends BaseGroupingsComponent implements OnInit
if (!this.hasLoadedAllCiphers) {
this.hasLoadedAllCiphers = !this.searchService.isSearchable(this.searchText);
}
this.deletedCount = this.allCiphers.filter((c) => c.isDeleted).length;
await this.search(null);
let favoriteCiphers: CipherView[] = null;
let noFolderCiphers: CipherView[] = null;
@ -175,6 +177,9 @@ export class GroupingsComponent extends BaseGroupingsComponent implements OnInit
const typeCounts = new Map<CipherType, number>();
this.ciphers.forEach((c) => {
if (c.isDeleted) {
return;
}
if (c.favorite) {
if (favoriteCiphers == null) {
favoriteCiphers = [];
@ -224,9 +229,10 @@ export class GroupingsComponent extends BaseGroupingsComponent implements OnInit
if (this.searchTimeout != null) {
clearTimeout(this.searchTimeout);
}
const filterDeleted = (c: CipherView) => !c.isDeleted;
if (timeout == null) {
this.hasSearched = this.searchService.isSearchable(this.searchText);
this.ciphers = await this.searchService.searchCiphers(this.searchText, null, this.allCiphers);
this.ciphers = await this.searchService.searchCiphers(this.searchText, filterDeleted, this.allCiphers);
return;
}
this.searchPending = true;
@ -235,7 +241,7 @@ export class GroupingsComponent extends BaseGroupingsComponent implements OnInit
if (!this.hasLoadedAllCiphers && !this.hasSearched) {
await this.loadCiphers();
} else {
this.ciphers = await this.searchService.searchCiphers(this.searchText, null, this.allCiphers);
this.ciphers = await this.searchService.searchCiphers(this.searchText, filterDeleted, this.allCiphers);
}
this.searchPending = false;
}, timeout);
@ -256,6 +262,11 @@ export class GroupingsComponent extends BaseGroupingsComponent implements OnInit
this.router.navigate(['/ciphers'], { queryParams: { collectionId: collection.id } });
}
async selectTrash() {
super.selectTrash();
this.router.navigate(['/ciphers'], { queryParams: { deleted: true } });
}
async selectCipher(cipher: CipherView) {
this.selectedTimeout = window.setTimeout(() => {
if (!this.preventSelected) {
@ -305,6 +316,7 @@ export class GroupingsComponent extends BaseGroupingsComponent implements OnInit
typeCounts: this.typeCounts,
folders: this.folders,
collections: this.collections,
deletedCount: this.deletedCount,
};
await this.stateService.save(ScopeStateId, this.scopeState);
}
@ -339,6 +351,9 @@ export class GroupingsComponent extends BaseGroupingsComponent implements OnInit
if (this.scopeState.collections != null) {
this.collections = this.scopeState.collections;
}
if (this.scopeState.deletedCiphers != null) {
this.deletedCount = this.scopeState.deletedCount;
}
return true;
}

View File

@ -6,7 +6,7 @@
<span class="title">{{'viewItem' | i18n}}</span>
</div>
<div class="right">
<button type="button" appBlurClick (click)="edit()">{{'edit' | i18n}}</button>
<button type="button" appBlurClick (click)="edit()" *ngIf="!cipher.isDeleted">{{'edit' | i18n}}</button>
</div>
</header>
<content *ngIf="cipher">
@ -259,8 +259,8 @@
</a>
</div>
</div>
<div class="box list" *ngIf="!cipher.organizationId">
<div class="box-content single-line">
<div class="box list">
<div class="box-content single-line" *ngIf="!cipher.organizationId && !cipher.isDeleted">
<a class="box-content-row" href="#" appStopClick appBlurClick (click)="clone()">
<div class="row-main text-primary">
<div class="icon text-primary" aria-hidden="true">
@ -270,6 +270,26 @@
</div>
</a>
</div>
<div class="box-content single-line" *ngIf="cipher.isDeleted">
<a class="box-content-row" href="#" appStopClick appBlurClick (click)="restore()">
<div class="row-main text-primary">
<div class="icon text-primary" aria-hidden="true">
<i class="fa fa-undo fa-lg fa-fw"></i>
</div>
<span>{{'restoreItem' | i18n}}</span>
</div>
</a>
</div>
<div class="box-content single-line">
<a class="box-content-row" href="#" appStopClick appBlurClick (click)="delete()">
<div class="row-main text-danger">
<div class="icon text-danger" aria-hidden="true">
<i class="fa fa-trash-o fa-lg fa-fw"></i>
</div>
<span>{{(cipher.isDeleted ? 'permanentlyDeleteItem' : 'deleteItem') | i18n}}</span>
</div>
</a>
</div>
</div>
<div class="box">
<div class="box-footer">

View File

@ -60,11 +60,17 @@ export class ViewComponent extends BaseViewComponent {
}
edit() {
if (this.cipher.isDeleted) {
return false;
}
super.edit();
this.router.navigate(['/edit-cipher'], { queryParams: { cipherId: this.cipher.id } });
}
clone() {
if (this.cipher.isDeleted) {
return false;
}
super.clone();
this.router.navigate(['/clone-cipher'], {
queryParams: {
@ -74,6 +80,25 @@ export class ViewComponent extends BaseViewComponent {
});
}
async restore() {
if (!this.cipher.isDeleted) {
return false;
}
if (await super.restore()) {
this.close();
return true;
}
return false;
}
async delete() {
if (await super.delete()) {
this.close();
return true;
}
return false;
}
close() {
this.location.back();
}