diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 053d1da6d8..a62eea6336 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -35,6 +35,7 @@ import { ToolsComponent } from './tools/tools.component'; import { AddEditComponent } from './vault/add-edit.component'; import { AttachmentsComponent } from './vault/attachments.component'; +import { BulkDeleteComponent } from './vault/bulk-delete.component'; import { CiphersComponent } from './vault/ciphers.component'; import { CollectionsComponent } from './vault/collections.component'; import { FolderAddEditComponent } from './vault/folder-add-edit.component'; @@ -81,6 +82,7 @@ import { Folder } from 'jslib/models/domain'; AutofocusDirective, BlurClickDirective, BoxRowDirective, + BulkDeleteComponent, CiphersComponent, CollectionsComponent, ExportComponent, @@ -115,6 +117,7 @@ import { Folder } from 'jslib/models/domain'; entryComponents: [ AddEditComponent, AttachmentsComponent, + BulkDeleteComponent, CollectionsComponent, FolderAddEditComponent, ModalComponent, diff --git a/src/app/vault/bulk-delete.component.html b/src/app/vault/bulk-delete.component.html new file mode 100644 index 0000000000..0b26cec73c --- /dev/null +++ b/src/app/vault/bulk-delete.component.html @@ -0,0 +1,25 @@ + diff --git a/src/app/vault/bulk-delete.component.ts b/src/app/vault/bulk-delete.component.ts new file mode 100644 index 0000000000..ae15150143 --- /dev/null +++ b/src/app/vault/bulk-delete.component.ts @@ -0,0 +1,34 @@ +import { + Component, + EventEmitter, + Input, + Output, +} from '@angular/core'; + +import { ToasterService } from 'angular2-toaster'; +import { Angulartics2 } from 'angulartics2'; + +import { CipherService } from 'jslib/abstractions/cipher.service'; +import { I18nService } from 'jslib/abstractions/i18n.service'; + +@Component({ + selector: 'app-vault-bulk-delete', + templateUrl: 'bulk-delete.component.html', +}) +export class BulkDeleteComponent { + @Input() cipherIds: string[] = []; + @Output() onDeleted = new EventEmitter(); + + formPromise: Promise; + + constructor(private analytics: Angulartics2, private cipherService: CipherService, + private toasterService: ToasterService, private i18nService: I18nService) { } + + async submit() { + this.formPromise = this.cipherService.deleteManyWithServer(this.cipherIds); + await this.formPromise; + this.onDeleted.emit(); + this.analytics.eventTrack.next({ action: 'Bulk Deleted Items' }); + this.toasterService.popAsync('success', null, this.i18nService.t('deletedItems')); + } +} diff --git a/src/app/vault/ciphers.component.ts b/src/app/vault/ciphers.component.ts index 89ccc21f57..1bc374de93 100644 --- a/src/app/vault/ciphers.component.ts +++ b/src/app/vault/ciphers.component.ts @@ -17,6 +17,8 @@ import { CipherType } from 'jslib/enums/cipherType'; import { CipherView } from 'jslib/models/view/cipherView'; +const MaxCheckedCount = 500; + @Component({ selector: 'app-vault-ciphers', templateUrl: 'ciphers.component.html', @@ -25,6 +27,7 @@ export class CiphersComponent extends BaseCiphersComponent { @Output() onAttachmentsClicked = new EventEmitter(); @Output() onShareClicked = new EventEmitter(); @Output() onCollectionsClicked = new EventEmitter(); + cipherType = CipherType; constructor(cipherService: CipherService, private analytics: Angulartics2, @@ -37,6 +40,23 @@ export class CiphersComponent extends BaseCiphersComponent { (c as any).checked = !(c as any).checked; } + selectAll(select: boolean) { + if (select) { + this.selectAll(false); + } + const selectCount = select && this.ciphers.length > MaxCheckedCount ? MaxCheckedCount : this.ciphers.length; + for (let i = 0; i < selectCount; i++) { + (this.ciphers[i] as any).checked = select; + } + } + + getSelected(): string[] { + if (this.ciphers == null) { + return []; + } + return this.ciphers.filter((c) => !!(c as any).checked).map((c) => c.id); + } + attachments(c: CipherView) { this.onAttachmentsClicked.emit(c); } diff --git a/src/app/vault/vault.component.html b/src/app/vault/vault.component.html index f1cca02744..499ab5a277 100644 --- a/src/app/vault/vault.component.html +++ b/src/app/vault/vault.component.html @@ -11,35 +11,35 @@

{{'myVault' | i18n}}

@@ -71,3 +71,6 @@ + + + diff --git a/src/app/vault/vault.component.ts b/src/app/vault/vault.component.ts index 0865143e7e..f7fa729091 100644 --- a/src/app/vault/vault.component.ts +++ b/src/app/vault/vault.component.ts @@ -20,6 +20,7 @@ import { ModalComponent } from '../modal.component'; import { AddEditComponent } from './add-edit.component'; import { AttachmentsComponent } from './attachments.component'; +import { BulkDeleteComponent } from './bulk-delete.component'; import { CiphersComponent } from './ciphers.component'; import { CollectionsComponent } from './collections.component'; import { FolderAddEditComponent } from './folder-add-edit.component'; @@ -43,6 +44,7 @@ export class VaultComponent implements OnInit { @ViewChild('cipherAddEdit', { read: ViewContainerRef }) cipherAddEditModalRef: ViewContainerRef; @ViewChild('share', { read: ViewContainerRef }) shareModalRef: ViewContainerRef; @ViewChild('collections', { read: ViewContainerRef }) collectionsModalRef: ViewContainerRef; + @ViewChild('bulkDeleteTemplate', { read: ViewContainerRef }) bulkDeleteModalRef: ViewContainerRef; cipherId: string = null; favorites: boolean = false; @@ -277,6 +279,39 @@ export class VaultComponent implements OnInit { return childComponent; } + bulkDelete() { + if (this.modal != null) { + this.modal.close(); + } + + const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent); + this.modal = this.bulkDeleteModalRef.createComponent(factory).instance; + const childComponent = this.modal.show( + BulkDeleteComponent, this.bulkDeleteModalRef); + + childComponent.cipherIds = this.ciphersComponent.getSelected(); + childComponent.onDeleted.subscribe(async () => { + this.modal.close(); + await this.ciphersComponent.refresh(); + }); + + this.modal.onClosed.subscribe(() => { + this.modal = null; + }); + } + + bulkShare() { + // + } + + bulkMove() { + // + } + + selectAll(select: boolean) { + this.ciphersComponent.selectAll(select); + } + private clearFilters() { this.folderId = null; this.collectionId = null; diff --git a/src/locales/en/messages.json b/src/locales/en/messages.json index 8ef34bcb8e..c08293f28e 100644 --- a/src/locales/en/messages.json +++ b/src/locales/en/messages.json @@ -419,6 +419,9 @@ "sharedItem": { "message": "Shared item" }, + "sharedItems": { + "message": "Shared items" + }, "deleteItem": { "message": "Delete Item" }, @@ -434,6 +437,12 @@ "deletedItem": { "message": "Deleted item" }, + "deletedItems": { + "message": "Deleted items" + }, + "movedItems": { + "message": "Moved items" + }, "overwritePasswordConfirmation": { "message": "Are you sure you want to overwrite the current password?" }, @@ -664,5 +673,14 @@ }, "collectionsDesc": { "message": "Edit the collections that this item is being shared with. Only organization users with access to these collections will be able to see this item." + }, + "deleteSelectedItemsDesc": { + "message": "You have selected $COUNT$ item(s) to delete. Are you sure you want to delete all of these items?", + "placeholders": { + "count": { + "content": "$1", + "example": "150" + } + } } }