Merge pull request #1223 from bitwarden/soft-delete
Soft delete feature
This commit is contained in:
commit
2486384f09
|
@ -498,7 +498,7 @@
|
||||||
"message": "Are you sure you want to delete this item?"
|
"message": "Are you sure you want to delete this item?"
|
||||||
},
|
},
|
||||||
"deletedItem": {
|
"deletedItem": {
|
||||||
"message": "Deleted item"
|
"message": "Sent item to trash"
|
||||||
},
|
},
|
||||||
"overwritePassword": {
|
"overwritePassword": {
|
||||||
"message": "Overwrite Password"
|
"message": "Overwrite Password"
|
||||||
|
@ -1258,6 +1258,31 @@
|
||||||
"message": "Lock",
|
"message": "Lock",
|
||||||
"description": "Verb form: to make secure or inaccesible by"
|
"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"
|
||||||
|
},
|
||||||
"vaultTimeoutLogOutConfirmation": {
|
"vaultTimeoutLogOutConfirmation": {
|
||||||
"message": "Logging out will remove all access to your vault and requires online authentication after the timeout period. Are you sure you want to use this setting?"
|
"message": "Logging out will remove all access to your vault and requires online authentication after the timeout period. Are you sure you want to use this setting?"
|
||||||
},
|
},
|
||||||
|
|
|
@ -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.searchPlaceholder = this.i18nService.t('searchType');
|
||||||
this.type = parseInt(params.type, null);
|
this.type = parseInt(params.type, null);
|
||||||
switch (this.type) {
|
switch (this.type) {
|
||||||
|
@ -198,6 +202,9 @@ export class CiphersComponent extends BaseCiphersComponent implements OnInit, On
|
||||||
}
|
}
|
||||||
|
|
||||||
addCipher() {
|
addCipher() {
|
||||||
|
if (this.deleted) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
super.addCipher();
|
super.addCipher();
|
||||||
this.router.navigate(['/add-cipher'], {
|
this.router.navigate(['/add-cipher'], {
|
||||||
queryParams: {
|
queryParams: {
|
||||||
|
|
|
@ -121,6 +121,23 @@
|
||||||
(onSelected)="selectCipher($event)" (onDoubleSelected)="launchCipher($event)"></app-ciphers-list>
|
(onSelected)="selectCipher($event)" (onDoubleSelected)="launchCipher($event)"></app-ciphers-list>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
<ng-container *ngIf="showSearching()">
|
<ng-container *ngIf="showSearching()">
|
||||||
<div class="no-items" *ngIf="!ciphers || !ciphers.length">
|
<div class="no-items" *ngIf="!ciphers || !ciphers.length">
|
||||||
|
|
|
@ -57,6 +57,7 @@ export class GroupingsComponent extends BaseGroupingsComponent implements OnInit
|
||||||
showLeftHeader = true;
|
showLeftHeader = true;
|
||||||
searchPending = false;
|
searchPending = false;
|
||||||
searchTypeSearch = false;
|
searchTypeSearch = false;
|
||||||
|
deletedCount = 0;
|
||||||
|
|
||||||
private loadedTimeout: number;
|
private loadedTimeout: number;
|
||||||
private selectedTimeout: number;
|
private selectedTimeout: number;
|
||||||
|
@ -167,6 +168,7 @@ export class GroupingsComponent extends BaseGroupingsComponent implements OnInit
|
||||||
if (!this.hasLoadedAllCiphers) {
|
if (!this.hasLoadedAllCiphers) {
|
||||||
this.hasLoadedAllCiphers = !this.searchService.isSearchable(this.searchText);
|
this.hasLoadedAllCiphers = !this.searchService.isSearchable(this.searchText);
|
||||||
}
|
}
|
||||||
|
this.deletedCount = this.allCiphers.filter((c) => c.isDeleted).length;
|
||||||
await this.search(null);
|
await this.search(null);
|
||||||
let favoriteCiphers: CipherView[] = null;
|
let favoriteCiphers: CipherView[] = null;
|
||||||
let noFolderCiphers: CipherView[] = null;
|
let noFolderCiphers: CipherView[] = null;
|
||||||
|
@ -175,6 +177,9 @@ export class GroupingsComponent extends BaseGroupingsComponent implements OnInit
|
||||||
const typeCounts = new Map<CipherType, number>();
|
const typeCounts = new Map<CipherType, number>();
|
||||||
|
|
||||||
this.ciphers.forEach((c) => {
|
this.ciphers.forEach((c) => {
|
||||||
|
if (c.isDeleted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (c.favorite) {
|
if (c.favorite) {
|
||||||
if (favoriteCiphers == null) {
|
if (favoriteCiphers == null) {
|
||||||
favoriteCiphers = [];
|
favoriteCiphers = [];
|
||||||
|
@ -224,9 +229,10 @@ export class GroupingsComponent extends BaseGroupingsComponent implements OnInit
|
||||||
if (this.searchTimeout != null) {
|
if (this.searchTimeout != null) {
|
||||||
clearTimeout(this.searchTimeout);
|
clearTimeout(this.searchTimeout);
|
||||||
}
|
}
|
||||||
|
const filterDeleted = (c: CipherView) => !c.isDeleted;
|
||||||
if (timeout == null) {
|
if (timeout == null) {
|
||||||
this.hasSearched = this.searchService.isSearchable(this.searchText);
|
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;
|
return;
|
||||||
}
|
}
|
||||||
this.searchPending = true;
|
this.searchPending = true;
|
||||||
|
@ -235,7 +241,7 @@ export class GroupingsComponent extends BaseGroupingsComponent implements OnInit
|
||||||
if (!this.hasLoadedAllCiphers && !this.hasSearched) {
|
if (!this.hasLoadedAllCiphers && !this.hasSearched) {
|
||||||
await this.loadCiphers();
|
await this.loadCiphers();
|
||||||
} else {
|
} 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;
|
this.searchPending = false;
|
||||||
}, timeout);
|
}, timeout);
|
||||||
|
@ -256,6 +262,11 @@ export class GroupingsComponent extends BaseGroupingsComponent implements OnInit
|
||||||
this.router.navigate(['/ciphers'], { queryParams: { collectionId: collection.id } });
|
this.router.navigate(['/ciphers'], { queryParams: { collectionId: collection.id } });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async selectTrash() {
|
||||||
|
super.selectTrash();
|
||||||
|
this.router.navigate(['/ciphers'], { queryParams: { deleted: true } });
|
||||||
|
}
|
||||||
|
|
||||||
async selectCipher(cipher: CipherView) {
|
async selectCipher(cipher: CipherView) {
|
||||||
this.selectedTimeout = window.setTimeout(() => {
|
this.selectedTimeout = window.setTimeout(() => {
|
||||||
if (!this.preventSelected) {
|
if (!this.preventSelected) {
|
||||||
|
@ -305,6 +316,7 @@ export class GroupingsComponent extends BaseGroupingsComponent implements OnInit
|
||||||
typeCounts: this.typeCounts,
|
typeCounts: this.typeCounts,
|
||||||
folders: this.folders,
|
folders: this.folders,
|
||||||
collections: this.collections,
|
collections: this.collections,
|
||||||
|
deletedCount: this.deletedCount,
|
||||||
};
|
};
|
||||||
await this.stateService.save(ScopeStateId, this.scopeState);
|
await this.stateService.save(ScopeStateId, this.scopeState);
|
||||||
}
|
}
|
||||||
|
@ -339,6 +351,9 @@ export class GroupingsComponent extends BaseGroupingsComponent implements OnInit
|
||||||
if (this.scopeState.collections != null) {
|
if (this.scopeState.collections != null) {
|
||||||
this.collections = this.scopeState.collections;
|
this.collections = this.scopeState.collections;
|
||||||
}
|
}
|
||||||
|
if (this.scopeState.deletedCiphers != null) {
|
||||||
|
this.deletedCount = this.scopeState.deletedCount;
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
<span class="title">{{'viewItem' | i18n}}</span>
|
<span class="title">{{'viewItem' | i18n}}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="right">
|
<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>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<content *ngIf="cipher">
|
<content *ngIf="cipher">
|
||||||
|
@ -259,9 +259,10 @@
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="box list" *ngIf="!cipher.organizationId">
|
<div class="box list">
|
||||||
<div class="box-content single-line">
|
<div class="box-content single-line">
|
||||||
<a class="box-content-row" href="#" appStopClick appBlurClick (click)="clone()">
|
<a class="box-content-row" href="#" appStopClick appBlurClick (click)="clone()"
|
||||||
|
*ngIf="!cipher.organizationId && !cipher.isDeleted">
|
||||||
<div class="row-main text-primary">
|
<div class="row-main text-primary">
|
||||||
<div class="icon text-primary" aria-hidden="true">
|
<div class="icon text-primary" aria-hidden="true">
|
||||||
<i class="fa fa-clone fa-lg fa-fw"></i>
|
<i class="fa fa-clone fa-lg fa-fw"></i>
|
||||||
|
@ -269,6 +270,22 @@
|
||||||
<span>{{'cloneItem' | i18n}}</span>
|
<span>{{'cloneItem' | i18n}}</span>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
|
<a class="box-content-row" href="#" appStopClick appBlurClick (click)="restore()" *ngIf="cipher.isDeleted">
|
||||||
|
<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>
|
||||||
|
<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>
|
</div>
|
||||||
<div class="box">
|
<div class="box">
|
||||||
|
|
|
@ -60,11 +60,17 @@ export class ViewComponent extends BaseViewComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
edit() {
|
edit() {
|
||||||
|
if (this.cipher.isDeleted) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
super.edit();
|
super.edit();
|
||||||
this.router.navigate(['/edit-cipher'], { queryParams: { cipherId: this.cipher.id } });
|
this.router.navigate(['/edit-cipher'], { queryParams: { cipherId: this.cipher.id } });
|
||||||
}
|
}
|
||||||
|
|
||||||
clone() {
|
clone() {
|
||||||
|
if (this.cipher.isDeleted) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
super.clone();
|
super.clone();
|
||||||
this.router.navigate(['/clone-cipher'], {
|
this.router.navigate(['/clone-cipher'], {
|
||||||
queryParams: {
|
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() {
|
close() {
|
||||||
this.location.back();
|
this.location.back();
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,7 +24,7 @@ describe('Browser Utils Service', () => {
|
||||||
it('should detect chrome', () => {
|
it('should detect chrome', () => {
|
||||||
Object.defineProperty(navigator, 'userAgent', {
|
Object.defineProperty(navigator, 'userAgent', {
|
||||||
configurable: true,
|
configurable: true,
|
||||||
value: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36'
|
value: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36',
|
||||||
});
|
});
|
||||||
|
|
||||||
const browserPlatformUtilsService = new BrowserPlatformUtilsService(null, null);
|
const browserPlatformUtilsService = new BrowserPlatformUtilsService(null, null);
|
||||||
|
@ -34,7 +34,7 @@ describe('Browser Utils Service', () => {
|
||||||
it('should detect firefox', () => {
|
it('should detect firefox', () => {
|
||||||
Object.defineProperty(navigator, 'userAgent', {
|
Object.defineProperty(navigator, 'userAgent', {
|
||||||
configurable: true,
|
configurable: true,
|
||||||
value: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:58.0) Gecko/20100101 Firefox/58.0'
|
value: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:58.0) Gecko/20100101 Firefox/58.0',
|
||||||
});
|
});
|
||||||
|
|
||||||
const browserPlatformUtilsService = new BrowserPlatformUtilsService(null, null);
|
const browserPlatformUtilsService = new BrowserPlatformUtilsService(null, null);
|
||||||
|
@ -44,12 +44,12 @@ describe('Browser Utils Service', () => {
|
||||||
it('should detect opera', () => {
|
it('should detect opera', () => {
|
||||||
Object.defineProperty(navigator, 'userAgent', {
|
Object.defineProperty(navigator, 'userAgent', {
|
||||||
configurable: true,
|
configurable: true,
|
||||||
value: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3175.3 Safari/537.36 OPR/49.0.2695.0 (Edition developer)'
|
value: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3175.3 Safari/537.36 OPR/49.0.2695.0 (Edition developer)',
|
||||||
});
|
});
|
||||||
|
|
||||||
Object.defineProperty(window, 'opr', {
|
Object.defineProperty(window, 'opr', {
|
||||||
configurable: true,
|
configurable: true,
|
||||||
value: {}
|
value: {},
|
||||||
});
|
});
|
||||||
|
|
||||||
const browserPlatformUtilsService = new BrowserPlatformUtilsService(null, null);
|
const browserPlatformUtilsService = new BrowserPlatformUtilsService(null, null);
|
||||||
|
@ -59,7 +59,7 @@ describe('Browser Utils Service', () => {
|
||||||
it('should detect edge', () => {
|
it('should detect edge', () => {
|
||||||
Object.defineProperty(navigator, 'userAgent', {
|
Object.defineProperty(navigator, 'userAgent', {
|
||||||
configurable: true,
|
configurable: true,
|
||||||
value: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; ServiceUI 9) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36 Edge/15.15063'
|
value: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; ServiceUI 9) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36 Edge/15.15063',
|
||||||
});
|
});
|
||||||
|
|
||||||
const browserPlatformUtilsService = new BrowserPlatformUtilsService(null, null);
|
const browserPlatformUtilsService = new BrowserPlatformUtilsService(null, null);
|
||||||
|
@ -69,7 +69,7 @@ describe('Browser Utils Service', () => {
|
||||||
it('should detect safari', () => {
|
it('should detect safari', () => {
|
||||||
Object.defineProperty(navigator, 'userAgent', {
|
Object.defineProperty(navigator, 'userAgent', {
|
||||||
configurable: true,
|
configurable: true,
|
||||||
value: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/602.4.8 (KHTML, like Gecko) Version/10.0.3 Safari/602.4.8'
|
value: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/602.4.8 (KHTML, like Gecko) Version/10.0.3 Safari/602.4.8',
|
||||||
});
|
});
|
||||||
|
|
||||||
Object.defineProperty(window, 'safariAppExtension', {
|
Object.defineProperty(window, 'safariAppExtension', {
|
||||||
|
@ -89,7 +89,7 @@ describe('Browser Utils Service', () => {
|
||||||
it('should detect vivaldi', () => {
|
it('should detect vivaldi', () => {
|
||||||
Object.defineProperty(navigator, 'userAgent', {
|
Object.defineProperty(navigator, 'userAgent', {
|
||||||
configurable: true,
|
configurable: true,
|
||||||
value: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.97 Safari/537.36 Vivaldi/1.94.1008.40'
|
value: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.97 Safari/537.36 Vivaldi/1.94.1008.40',
|
||||||
});
|
});
|
||||||
|
|
||||||
const browserPlatformUtilsService = new BrowserPlatformUtilsService(null, null);
|
const browserPlatformUtilsService = new BrowserPlatformUtilsService(null, null);
|
||||||
|
|
Loading…
Reference in New Issue