From 20408347fb2279c113a4dbedc63953ab813b60d5 Mon Sep 17 00:00:00 2001 From: Addison Beck Date: Tue, 11 Aug 2020 11:30:30 -0400 Subject: [PATCH] Allow Bulk Delete In Org Vault (#577) * added the multi select checkbox to org ciphers * wired up select all/none * allowed for bulk delete of ciphers from the org vault * refactored bulk actions into a dedicated component * tweaked formatting settings and reformatted files * moved some shared code to jslib * some more formatting fixes * undid jslib connection changes * removed a function that was moved to jslib * reset jslib again? * set up delete many w/admin cipher methods * removed extra href tags * added organization id to bulk delete request model when coming from an org vault * fixed up some compiler warnings for formatting * code review fixups for bulk delete from org vault * added back a removed parameter from the vault component * seperated some imports with newlines * updated jslib * resolved some build errors * code review cleanup for bulk delete from an org vault * code review cleanup for bulk delete from an org vault * code review cleanup for bulk delete from an org vault * code review cleanup for bulk delete from an org vault * updated jslib to latest Co-authored-by: Addison Beck --- jslib | 2 +- src/app/accounts/login.component.ts | 12 +- src/app/app.module.ts | 3 + .../organizations/vault/ciphers.component.ts | 4 - .../organizations/vault/vault.component.html | 13 +- .../organizations/vault/vault.component.ts | 2 +- src/app/vault/bulk-actions.component.html | 38 +++++ src/app/vault/bulk-actions.component.ts | 154 ++++++++++++++++++ src/app/vault/bulk-delete.component.ts | 35 +++- src/app/vault/ciphers.component.html | 7 +- src/app/vault/ciphers.component.ts | 52 +++--- src/app/vault/vault.component.html | 40 +---- src/app/vault/vault.component.ts | 129 +-------------- 13 files changed, 282 insertions(+), 209 deletions(-) create mode 100644 src/app/vault/bulk-actions.component.html create mode 100644 src/app/vault/bulk-actions.component.ts diff --git a/jslib b/jslib index 7d49902eea4..420393700b3 160000 --- a/jslib +++ b/jslib @@ -1 +1 @@ -Subproject commit 7d49902eea45275d50c949beec32b3ab5b7db725 +Subproject commit 420393700b38ed6e8e812366faf9231858bdaa92 diff --git a/src/app/accounts/login.component.ts b/src/app/accounts/login.component.ts index 95c64296f2a..7a3a362b5b8 100644 --- a/src/app/accounts/login.component.ts +++ b/src/app/accounts/login.component.ts @@ -5,7 +5,10 @@ import { } from '@angular/router'; import { AuthService } from 'jslib/abstractions/auth.service'; +import { CryptoFunctionService } from 'jslib/abstractions/cryptoFunction.service'; +import { EnvironmentService } from 'jslib/abstractions/environment.service'; import { I18nService } from 'jslib/abstractions/i18n.service'; +import { PasswordGenerationService } from 'jslib/abstractions/passwordGeneration.service'; import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service'; import { StateService } from 'jslib/abstractions/state.service'; import { StorageService } from 'jslib/abstractions/storage.service'; @@ -20,8 +23,13 @@ export class LoginComponent extends BaseLoginComponent { constructor(authService: AuthService, router: Router, i18nService: I18nService, private route: ActivatedRoute, storageService: StorageService, stateService: StateService, - platformUtilsService: PlatformUtilsService) { - super(authService, router, platformUtilsService, i18nService, storageService, stateService); + platformUtilsService: PlatformUtilsService, environmentService: EnvironmentService, + passwordGenerationService: PasswordGenerationService, cryptoFunctionService: CryptoFunctionService) { + super(authService, router, + platformUtilsService, i18nService, + stateService, environmentService, + passwordGenerationService, cryptoFunctionService, + storageService); this.onSuccessfulLoginNavigate = this.goAfterLogIn; } diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 86e222381ad..b7627823911 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -145,6 +145,7 @@ import { WeakPasswordsReportComponent } from './tools/weak-passwords-report.comp import { AddEditComponent } from './vault/add-edit.component'; import { AttachmentsComponent } from './vault/attachments.component'; +import { BulkActionsComponent } from './vault/bulk-actions.component'; import { BulkDeleteComponent } from './vault/bulk-delete.component'; import { BulkMoveComponent } from './vault/bulk-move.component'; import { BulkRestoreComponent } from './vault/bulk-restore.component'; @@ -260,6 +261,7 @@ registerLocaleData(localeZhTw, 'zh-TW'); BlurClickDirective, BoxRowDirective, BreachReportComponent, + BulkActionsComponent, BulkDeleteComponent, BulkMoveComponent, BulkRestoreComponent, @@ -381,6 +383,7 @@ registerLocaleData(localeZhTw, 'zh-TW'); entryComponents: [ AddEditComponent, AttachmentsComponent, + BulkActionsComponent, BulkDeleteComponent, BulkMoveComponent, BulkRestoreComponent, diff --git a/src/app/organizations/vault/ciphers.component.ts b/src/app/organizations/vault/ciphers.component.ts index ea990cd75aa..c64962e3ba7 100644 --- a/src/app/organizations/vault/ciphers.component.ts +++ b/src/app/organizations/vault/ciphers.component.ts @@ -82,10 +82,6 @@ export class CiphersComponent extends BaseCiphersComponent { await this.resetPaging(); } - checkCipher(c: CipherView) { - // do nothing - } - events(c: CipherView) { this.onEventsClicked.emit(c); } diff --git a/src/app/organizations/vault/vault.component.html b/src/app/organizations/vault/vault.component.html index 3f836dee51e..521f7e36509 100644 --- a/src/app/organizations/vault/vault.component.html +++ b/src/app/organizations/vault/vault.component.html @@ -19,10 +19,15 @@ - +
+ + + +
+ + + + + + + + diff --git a/src/app/vault/bulk-actions.component.ts b/src/app/vault/bulk-actions.component.ts new file mode 100644 index 00000000000..20960720aaa --- /dev/null +++ b/src/app/vault/bulk-actions.component.ts @@ -0,0 +1,154 @@ +import { + Component, + ComponentFactoryResolver, + Input, + ViewChild, + ViewContainerRef, +} from '@angular/core'; +import { ToasterService } from 'angular2-toaster'; + +import { I18nService } from 'jslib/abstractions/i18n.service'; + +import { Organization } from 'jslib/models/domain/organization'; + +import { ModalComponent } from '../modal.component'; + +import { BulkDeleteComponent } from './bulk-delete.component'; +import { BulkMoveComponent } from './bulk-move.component'; +import { BulkRestoreComponent } from './bulk-restore.component'; +import { BulkShareComponent } from './bulk-share.component'; +import { CiphersComponent } from './ciphers.component'; + +@Component({ + selector: 'app-vault-bulk-actions', + templateUrl: 'bulk-actions.component.html', +}) +export class BulkActionsComponent { + @Input() ciphersComponent: CiphersComponent; + @Input() modal: ModalComponent; + @Input() deleted: boolean; + @Input() organization: Organization; + + @ViewChild('bulkDeleteTemplate', { read: ViewContainerRef }) bulkDeleteModalRef: ViewContainerRef; + @ViewChild('bulkRestoreTemplate', { read: ViewContainerRef }) bulkRestoreModalRef: ViewContainerRef; + @ViewChild('bulkMoveTemplate', { read: ViewContainerRef }) bulkMoveModalRef: ViewContainerRef; + @ViewChild('bulkShareTemplate', { read: ViewContainerRef }) bulkShareModalRef: ViewContainerRef; + + constructor(private toasterService: ToasterService, + private i18nService: I18nService, + private componentFactoryResolver: ComponentFactoryResolver) { } + + bulkDelete() { + const selectedIds = this.ciphersComponent.getSelectedIds(); + if (selectedIds.length === 0) { + this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'), + this.i18nService.t('nothingSelected')); + return; + } + + 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.permanent = this.deleted; + childComponent.cipherIds = selectedIds; + childComponent.organization = this.organization; + childComponent.onDeleted.subscribe(async () => { + this.modal.close(); + await this.ciphersComponent.refresh(); + }); + + this.modal.onClosed.subscribe(() => { + this.modal = null; + }); + } + + bulkRestore() { + const selectedIds = this.ciphersComponent.getSelectedIds(); + if (selectedIds.length === 0) { + this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'), + this.i18nService.t('nothingSelected')); + return; + } + + if (this.modal != null) { + this.modal.close(); + } + + const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent); + this.modal = this.bulkRestoreModalRef.createComponent(factory).instance; + const childComponent = this.modal.show(BulkRestoreComponent, this.bulkRestoreModalRef); + + childComponent.cipherIds = selectedIds; + childComponent.onRestored.subscribe(async () => { + this.modal.close(); + await this.ciphersComponent.refresh(); + }); + + this.modal.onClosed.subscribe(() => { + this.modal = null; + }); + } + + bulkShare() { + const selectedCiphers = this.ciphersComponent.getSelected(); + if (selectedCiphers.length === 0) { + this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'), + this.i18nService.t('nothingSelected')); + return; + } + + if (this.modal != null) { + this.modal.close(); + } + + const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent); + this.modal = this.bulkShareModalRef.createComponent(factory).instance; + const childComponent = this.modal.show(BulkShareComponent, this.bulkShareModalRef); + + childComponent.ciphers = selectedCiphers; + childComponent.onShared.subscribe(async () => { + this.modal.close(); + await this.ciphersComponent.refresh(); + }); + + this.modal.onClosed.subscribe(async () => { + this.modal = null; + }); + } + + bulkMove() { + const selectedIds = this.ciphersComponent.getSelectedIds(); + if (selectedIds.length === 0) { + this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'), + this.i18nService.t('nothingSelected')); + return; + } + + if (this.modal != null) { + this.modal.close(); + } + + const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent); + this.modal = this.bulkMoveModalRef.createComponent(factory).instance; + const childComponent = this.modal.show(BulkMoveComponent, this.bulkMoveModalRef); + + childComponent.cipherIds = selectedIds; + childComponent.onMoved.subscribe(async () => { + this.modal.close(); + await this.ciphersComponent.refresh(); + }); + + this.modal.onClosed.subscribe(() => { + this.modal = null; + }); + } + + selectAll(select: boolean) { + this.ciphersComponent.selectAll(select); + } +} diff --git a/src/app/vault/bulk-delete.component.ts b/src/app/vault/bulk-delete.component.ts index f0a609f48a3..fc6584f7c5c 100644 --- a/src/app/vault/bulk-delete.component.ts +++ b/src/app/vault/bulk-delete.component.ts @@ -4,13 +4,16 @@ import { Input, Output, } from '@angular/core'; - import { ToasterService } from 'angular2-toaster'; import { Angulartics2 } from 'angulartics2'; +import { ApiService } from 'jslib/abstractions/api.service'; import { CipherService } from 'jslib/abstractions/cipher.service'; import { I18nService } from 'jslib/abstractions/i18n.service'; +import { Organization } from 'jslib/models/domain/organization'; +import { CipherBulkDeleteRequest } from 'jslib/models/request/cipherBulkDeleteRequest'; + @Component({ selector: 'app-vault-bulk-delete', templateUrl: 'bulk-delete.component.html', @@ -18,20 +21,44 @@ import { I18nService } from 'jslib/abstractions/i18n.service'; export class BulkDeleteComponent { @Input() cipherIds: string[] = []; @Input() permanent: boolean = false; + @Input() organization: Organization; @Output() onDeleted = new EventEmitter(); formPromise: Promise; constructor(private analytics: Angulartics2, private cipherService: CipherService, - private toasterService: ToasterService, private i18nService: I18nService) { } + private toasterService: ToasterService, private i18nService: I18nService, + private apiService: ApiService) { } async submit() { - this.formPromise = this.permanent ? this.cipherService.deleteManyWithServer(this.cipherIds) : - this.cipherService.softDeleteManyWithServer(this.cipherIds); + if (!this.organization || !this.organization.isAdmin) { + await this.deleteCiphers(); + } else { + await this.deleteCiphersAdmin(); + } + await this.formPromise; + this.onDeleted.emit(); this.analytics.eventTrack.next({ action: 'Bulk Deleted Items' }); this.toasterService.popAsync('success', null, this.i18nService.t(this.permanent ? 'permanentlyDeletedItems' : 'deletedItems')); } + + private async deleteCiphers() { + if (this.permanent) { + this.formPromise = await this.cipherService.deleteManyWithServer(this.cipherIds); + } else { + this.formPromise = await this.cipherService.softDeleteManyWithServer(this.cipherIds); + } + } + + private async deleteCiphersAdmin() { + const deleteRequest = new CipherBulkDeleteRequest(this.cipherIds, this.organization.id); + if (this.permanent) { + this.formPromise = await this.apiService.deleteManyCiphersAdmin(deleteRequest); + } else { + this.formPromise = await this.apiService.putDeleteManyCiphersAdmin(deleteRequest); + } + } } diff --git a/src/app/vault/ciphers.component.html b/src/app/vault/ciphers.component.html index 64ab1dd8a16..8ec7f5ab64b 100644 --- a/src/app/vault/ciphers.component.html +++ b/src/app/vault/ciphers.component.html @@ -3,7 +3,7 @@ [infiniteScrollDistance]="1" [infiniteScrollDisabled]="!isPaging()" (scrolled)="loadMore()"> - + @@ -58,9 +58,8 @@ {{'clone' | i18n}} - + {{'share' | i18n}} diff --git a/src/app/vault/ciphers.component.ts b/src/app/vault/ciphers.component.ts index a13406690d2..5a06fc3413f 100644 --- a/src/app/vault/ciphers.component.ts +++ b/src/app/vault/ciphers.component.ts @@ -50,36 +50,11 @@ export class CiphersComponent extends BaseCiphersComponent implements OnDestroy this.selectAll(false); } - checkCipher(c: CipherView, select?: boolean) { - (c as any).checked = select == null ? !(c as any).checked : select; - } - launch(uri: string) { this.platformUtilsService.eventTrack('Launched Login URI'); this.platformUtilsService.launchUri(uri); } - 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.checkCipher(this.ciphers[i], select); - } - } - - getSelected(): CipherView[] { - if (this.ciphers == null) { - return []; - } - return this.ciphers.filter((c) => !!(c as any).checked); - } - - getSelectedIds(): string[] { - return this.getSelected().map((c) => c.id); - } - attachments(c: CipherView) { this.onAttachmentsClicked.emit(c); } @@ -159,6 +134,33 @@ export class CiphersComponent extends BaseCiphersComponent implements OnDestroy } } + 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.checkCipher(this.ciphers[i], select); + } + } + + checkCipher(c: CipherView, select?: boolean) { + (c as any).checked = select == null ? !(c as any).checked : select; + } + + getSelected(): CipherView[] { + if (this.ciphers == null) { + return []; + } + return this.ciphers.filter((c) => !!(c as any).checked); + } + + getSelectedIds(): string[] { + return this.getSelected().map((c) => c.id); + } + protected deleteCipher(id: string, permanent: boolean) { return permanent ? this.cipherService.deleteWithServer(id) : this.cipherService.softDeleteWithServer(id); } diff --git a/src/app/vault/vault.component.html b/src/app/vault/vault.component.html index 3b5ace702e8..838f17f921b 100644 --- a/src/app/vault/vault.component.html +++ b/src/app/vault/vault.component.html @@ -21,40 +21,8 @@
- + + @@ -122,8 +90,4 @@ - - - - diff --git a/src/app/vault/vault.component.ts b/src/app/vault/vault.component.ts index ee3705732d7..3d3024bc55a 100644 --- a/src/app/vault/vault.component.ts +++ b/src/app/vault/vault.component.ts @@ -13,8 +13,6 @@ import { Router, } from '@angular/router'; -import { ToasterService } from 'angular2-toaster'; - import { CipherType } from 'jslib/enums/cipherType'; import { CipherView } from 'jslib/models/view/cipherView'; @@ -25,10 +23,6 @@ import { OrganizationsComponent } from '../settings/organizations.component'; import { UpdateKeyComponent } from '../settings/update-key.component'; import { AddEditComponent } from './add-edit.component'; import { AttachmentsComponent } from './attachments.component'; -import { BulkDeleteComponent } from './bulk-delete.component'; -import { BulkMoveComponent } from './bulk-move.component'; -import { BulkRestoreComponent } from './bulk-restore.component'; -import { BulkShareComponent } from './bulk-share.component'; import { CiphersComponent } from './ciphers.component'; import { CollectionsComponent } from './collections.component'; import { FolderAddEditComponent } from './folder-add-edit.component'; @@ -60,10 +54,6 @@ export class VaultComponent implements OnInit, OnDestroy { @ViewChild('cipherAddEdit', { read: ViewContainerRef }) cipherAddEditModalRef: ViewContainerRef; @ViewChild('share', { read: ViewContainerRef }) shareModalRef: ViewContainerRef; @ViewChild('collections', { read: ViewContainerRef }) collectionsModalRef: ViewContainerRef; - @ViewChild('bulkDeleteTemplate', { read: ViewContainerRef }) bulkDeleteModalRef: ViewContainerRef; - @ViewChild('bulkRestoreTemplate', { read: ViewContainerRef }) bulkRestoreModalRef: ViewContainerRef; - @ViewChild('bulkMoveTemplate', { read: ViewContainerRef }) bulkMoveModalRef: ViewContainerRef; - @ViewChild('bulkShareTemplate', { read: ViewContainerRef }) bulkShareModalRef: ViewContainerRef; @ViewChild('updateKeyTemplate', { read: ViewContainerRef }) updateKeyModalRef: ViewContainerRef; favorites: boolean = false; @@ -76,15 +66,15 @@ export class VaultComponent implements OnInit, OnDestroy { showPremiumCallout = false; deleted: boolean = false; - private modal: ModalComponent = null; + modal: ModalComponent = null; constructor(private syncService: SyncService, private route: ActivatedRoute, private router: Router, private changeDetectorRef: ChangeDetectorRef, private i18nService: I18nService, private componentFactoryResolver: ComponentFactoryResolver, private tokenService: TokenService, private cryptoService: CryptoService, private messagingService: MessagingService, private userService: UserService, - private platformUtilsService: PlatformUtilsService, private toasterService: ToasterService, - private broadcasterService: BroadcasterService, private ngZone: NgZone) { } + private platformUtilsService: PlatformUtilsService, private broadcasterService: BroadcasterService, + private ngZone: NgZone) { } async ngOnInit() { this.showVerifyEmail = !(await this.tokenService.getEmailVerified()); @@ -391,119 +381,6 @@ export class VaultComponent implements OnInit, OnDestroy { component.cloneMode = true; } - bulkDelete() { - const selectedIds = this.ciphersComponent.getSelectedIds(); - if (selectedIds.length === 0) { - this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'), - this.i18nService.t('nothingSelected')); - return; - } - - 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.permanent = this.deleted; - childComponent.cipherIds = selectedIds; - childComponent.onDeleted.subscribe(async () => { - this.modal.close(); - await this.ciphersComponent.refresh(); - }); - - this.modal.onClosed.subscribe(() => { - this.modal = null; - }); - } - - bulkRestore() { - const selectedIds = this.ciphersComponent.getSelectedIds(); - if (selectedIds.length === 0) { - this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'), - this.i18nService.t('nothingSelected')); - return; - } - - if (this.modal != null) { - this.modal.close(); - } - - const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent); - this.modal = this.bulkRestoreModalRef.createComponent(factory).instance; - const childComponent = this.modal.show(BulkRestoreComponent, this.bulkRestoreModalRef); - - childComponent.cipherIds = selectedIds; - childComponent.onRestored.subscribe(async () => { - this.modal.close(); - await this.ciphersComponent.refresh(); - }); - - this.modal.onClosed.subscribe(() => { - this.modal = null; - }); - } - - bulkShare() { - const selectedCiphers = this.ciphersComponent.getSelected(); - if (selectedCiphers.length === 0) { - this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'), - this.i18nService.t('nothingSelected')); - return; - } - - if (this.modal != null) { - this.modal.close(); - } - - const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent); - this.modal = this.bulkShareModalRef.createComponent(factory).instance; - const childComponent = this.modal.show(BulkShareComponent, this.bulkShareModalRef); - - childComponent.ciphers = selectedCiphers; - childComponent.onShared.subscribe(async () => { - this.modal.close(); - await this.ciphersComponent.refresh(); - }); - - this.modal.onClosed.subscribe(async () => { - this.modal = null; - }); - } - - bulkMove() { - const selectedIds = this.ciphersComponent.getSelectedIds(); - if (selectedIds.length === 0) { - this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'), - this.i18nService.t('nothingSelected')); - return; - } - - if (this.modal != null) { - this.modal.close(); - } - - const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent); - this.modal = this.bulkMoveModalRef.createComponent(factory).instance; - const childComponent = this.modal.show(BulkMoveComponent, this.bulkMoveModalRef); - - childComponent.cipherIds = selectedIds; - childComponent.onMoved.subscribe(async () => { - this.modal.close(); - await this.ciphersComponent.refresh(); - }); - - this.modal.onClosed.subscribe(() => { - this.modal = null; - }); - } - - selectAll(select: boolean) { - this.ciphersComponent.selectAll(select); - } - updateKey() { if (this.modal != null) { this.modal.close();