diff --git a/src/abstractions/search.service.ts b/src/abstractions/search.service.ts index 3e146e94bc..1cfc32b19c 100644 --- a/src/abstractions/search.service.ts +++ b/src/abstractions/search.service.ts @@ -4,7 +4,8 @@ export abstract class SearchService { clearIndex: () => void; isSearchable: (query: string) => boolean; indexCiphers: () => Promise; - searchCiphers: (query: string, filter?: (cipher: CipherView) => boolean, - ciphers?: CipherView[], deleted?: boolean) => Promise; + searchCiphers: (query: string, + filter?: ((cipher: CipherView) => boolean) | (Array<(cipher: CipherView) => boolean>), + ciphers?: CipherView[]) => Promise; searchCiphersBasic: (ciphers: CipherView[], query: string, deleted?: boolean) => CipherView[]; } diff --git a/src/angular/components/add-edit.component.ts b/src/angular/components/add-edit.component.ts index e0bbc59dc4..40d3261d63 100644 --- a/src/angular/components/add-edit.component.ts +++ b/src/angular/components/add-edit.component.ts @@ -50,6 +50,7 @@ export class AddEditComponent implements OnInit { @Input() organizationId: string = null; @Output() onSavedCipher = new EventEmitter(); @Output() onDeletedCipher = new EventEmitter(); + @Output() onRestoredCipher = new EventEmitter(); @Output() onCancelled = new EventEmitter(); @Output() onEditAttachments = new EventEmitter(); @Output() onShareCipher = new EventEmitter(); @@ -63,6 +64,7 @@ export class AddEditComponent implements OnInit { title: string; formPromise: Promise; deletePromise: Promise; + restorePromise: Promise; checkPasswordPromise: Promise; showPassword: boolean = false; showCardCode: boolean = false; @@ -221,6 +223,10 @@ export class AddEditComponent implements OnInit { } async submit(): Promise { + if (this.cipher.isDeleted) { + return this.restore(); + } + if (this.cipher.name == null || this.cipher.name === '') { this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), this.i18nService.t('nameRequired')); @@ -331,10 +337,35 @@ export class AddEditComponent implements OnInit { try { this.deletePromise = this.deleteCipher(); await this.deletePromise; - this.platformUtilsService.eventTrack('Deleted Cipher'); - this.platformUtilsService.showToast('success', null, this.i18nService.t('deletedItem')); + this.platformUtilsService.eventTrack((this.cipher.isDeleted ? 'Permanently ' : '') + 'Deleted Cipher'); + this.platformUtilsService.showToast('success', null, + this.i18nService.t(this.cipher.isDeleted ? 'permanentlyDeletedItem' : 'deletedItem')); this.onDeletedCipher.emit(this.cipher); - this.messagingService.send('deletedCipher'); + this.messagingService.send(this.cipher.isDeleted ? 'permanentlyDeletedCipher' : 'deletedCipher'); + } catch { } + + return true; + } + + async restore(): Promise { + if (!this.cipher.isDeleted) { + return false; + } + + const confirmed = await this.platformUtilsService.showDialog( + this.i18nService.t('restoreItemConfirmation'), this.i18nService.t('restoreItem'), + this.i18nService.t('yes'), this.i18nService.t('no'), 'warning'); + if (!confirmed) { + return false; + } + + try { + this.restorePromise = this.restoreCipher(); + await this.restorePromise; + this.platformUtilsService.eventTrack('Restored Cipher'); + this.platformUtilsService.showToast('success', null, this.i18nService.t('restoredItem')); + this.onRestoredCipher.emit(this.cipher); + this.messagingService.send('restoredCipher'); } catch { } return true; @@ -449,6 +480,11 @@ export class AddEditComponent implements OnInit { } protected deleteCipher() { - return this.cipherService.deleteWithServer(this.cipher.id); + return this.cipher.isDeleted ? this.cipherService.deleteWithServer(this.cipher.id) + : this.cipherService.softDeleteWithServer(this.cipher.id); + } + + protected restoreCipher() { + return this.cipherService.restoreWithServer(this.cipher.id); } } diff --git a/src/angular/components/ciphers.component.ts b/src/angular/components/ciphers.component.ts index 7244ca6e21..50beab2f54 100644 --- a/src/angular/components/ciphers.component.ts +++ b/src/angular/components/ciphers.component.ts @@ -64,7 +64,7 @@ export class CiphersComponent { async refresh() { try { this.refreshing = true; - await this.reload(this.filter); + await this.reload(this.filter, this.deleted); } finally { this.refreshing = false; } @@ -80,14 +80,15 @@ export class CiphersComponent { if (this.searchTimeout != null) { clearTimeout(this.searchTimeout); } + const deletedFilter: (cipher: CipherView) => boolean = (c) => c.isDeleted === this.deleted; if (timeout == null) { - this.ciphers = await this.searchService.searchCiphers(this.searchText, this.filter, null, this.deleted); + this.ciphers = await this.searchService.searchCiphers(this.searchText, [this.filter, deletedFilter], null); await this.resetPaging(); return; } this.searchPending = true; this.searchTimeout = setTimeout(async () => { - this.ciphers = await this.searchService.searchCiphers(this.searchText, this.filter, null, this.deleted); + this.ciphers = await this.searchService.searchCiphers(this.searchText, [this.filter, deletedFilter], null); await this.resetPaging(); this.searchPending = false; }, timeout); diff --git a/src/angular/components/view.component.ts b/src/angular/components/view.component.ts index a0d6076483..d325a5f895 100644 --- a/src/angular/components/view.component.ts +++ b/src/angular/components/view.component.ts @@ -34,6 +34,7 @@ export class ViewComponent implements OnDestroy, OnInit { @Input() cipherId: string; @Output() onEditCipher = new EventEmitter(); @Output() onCloneCipher = new EventEmitter(); + @Output() onRestoreCipher = new EventEmitter(); cipher: CipherView; showPassword: boolean; @@ -110,6 +111,13 @@ export class ViewComponent implements OnDestroy, OnInit { this.onCloneCipher.emit(this.cipher); } + restore() { + if (!this.cipher.isDeleted) { + return; + } + this.onRestoreCipher.emit(this.cipher); + } + togglePassword() { this.platformUtilsService.eventTrack('Toggled Password'); this.showPassword = !this.showPassword; diff --git a/src/services/search.service.ts b/src/services/search.service.ts index ff1f2bf994..4a994c411d 100644 --- a/src/services/search.service.ts +++ b/src/services/search.service.ts @@ -71,8 +71,9 @@ export class SearchService implements SearchServiceAbstraction { console.timeEnd('search indexing'); } - async searchCiphers(query: string, filter: (cipher: CipherView) => boolean = null, ciphers: CipherView[] = null, - deleted: boolean = false): + async searchCiphers(query: string, + filter: (((cipher: CipherView) => boolean) | (Array<(cipher: CipherView) => boolean>)) = null, + ciphers: CipherView[] = null): Promise { const results: CipherView[] = []; if (query != null) { @@ -86,15 +87,11 @@ export class SearchService implements SearchServiceAbstraction { ciphers = await this.cipherService.getAllDecrypted(); } - ciphers = ciphers.filter((c) => { - if (deleted !== c.isDeleted) { - return false; - } - if (filter != null) { - return filter(c); - } - return true; - }); + if (filter != null && Array.isArray(filter) && filter.length > 0) { + ciphers = ciphers.filter((c) => filter.every((f) => f == null || f(c))); + } else if (filter != null) { + ciphers = ciphers.filter(filter as (cipher: CipherView) => boolean); + } if (!this.isSearchable(query)) { return ciphers;