diff --git a/jslib b/jslib index 7a5a4e65..cfad521e 160000 --- a/jslib +++ b/jslib @@ -1 +1 @@ -Subproject commit 7a5a4e654a76b7a7e546d33b1e2ad4e3d6c9adc3 +Subproject commit cfad521ea8ef205abe774907d8f7a243b3adaf10 diff --git a/src/app/app.module.ts b/src/app/app.module.ts index f535650c..ec927de8 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -37,6 +37,7 @@ import { AddEditComponent } from './vault/add-edit.component'; import { AttachmentsComponent } from './vault/attachments.component'; import { BulkDeleteComponent } from './vault/bulk-delete.component'; import { BulkMoveComponent } from './vault/bulk-move.component'; +import { BulkShareComponent } from './vault/bulk-share.component'; import { CiphersComponent } from './vault/ciphers.component'; import { CollectionsComponent } from './vault/collections.component'; import { FolderAddEditComponent } from './vault/folder-add-edit.component'; @@ -85,6 +86,7 @@ import { Folder } from 'jslib/models/domain'; BoxRowDirective, BulkDeleteComponent, BulkMoveComponent, + BulkShareComponent, CiphersComponent, CollectionsComponent, ExportComponent, @@ -121,6 +123,7 @@ import { Folder } from 'jslib/models/domain'; AttachmentsComponent, BulkDeleteComponent, BulkMoveComponent, + BulkShareComponent, CollectionsComponent, FolderAddEditComponent, ModalComponent, diff --git a/src/app/vault/bulk-share.component.html b/src/app/vault/bulk-share.component.html new file mode 100644 index 00000000..6e9a4ca2 --- /dev/null +++ b/src/app/vault/bulk-share.component.html @@ -0,0 +1,54 @@ + diff --git a/src/app/vault/bulk-share.component.ts b/src/app/vault/bulk-share.component.ts new file mode 100644 index 00000000..09ff609c --- /dev/null +++ b/src/app/vault/bulk-share.component.ts @@ -0,0 +1,88 @@ +import { + Component, + EventEmitter, + Input, + OnInit, + Output, +} from '@angular/core'; + +import { ToasterService } from 'angular2-toaster'; +import { Angulartics2 } from 'angulartics2'; + +import { CipherService } from 'jslib/abstractions/cipher.service'; +import { CollectionService } from 'jslib/abstractions/collection.service'; +import { I18nService } from 'jslib/abstractions/i18n.service'; +import { UserService } from 'jslib/abstractions/user.service'; + +import { CipherView } from 'jslib/models/view'; +import { CollectionView } from 'jslib/models/view/collectionView'; + +import { Organization } from 'jslib/models/domain/organization'; + +@Component({ + selector: 'app-vault-bulk-share', + templateUrl: 'bulk-share.component.html', +}) +export class BulkShareComponent implements OnInit { + @Input() ciphers: CipherView[] = []; + @Input() organizationId: string; + @Output() onShared = new EventEmitter(); + + nonShareableCount = 0; + collections: CollectionView[] = []; + organizations: Organization[] = []; + formPromise: Promise; + + private writeableCollections: CollectionView[] = []; + private shareableCiphers: CipherView[] = []; + + constructor(private analytics: Angulartics2, private cipherService: CipherService, + private toasterService: ToasterService, private i18nService: I18nService, + private collectionService: CollectionService, private userService: UserService) { } + + async ngOnInit() { + this.shareableCiphers = this.ciphers.filter((c) => !c.hasAttachments && c.organizationId == null); + this.nonShareableCount = this.ciphers.length - this.shareableCiphers.length; + const allCollections = await this.collectionService.getAllDecrypted(); + this.writeableCollections = allCollections.filter((c) => !c.readOnly); + this.organizations = await this.userService.getAllOrganizations(); + if (this.organizationId == null && this.organizations.length > 0) { + this.organizationId = this.organizations[0].id; + } + this.filterCollections(); + } + + ngOnDestroy() { + this.selectAll(false); + } + + filterCollections() { + this.selectAll(false); + if (this.organizationId == null || this.writeableCollections.length === 0) { + this.collections = []; + } else { + this.collections = this.writeableCollections.filter((c) => c.organizationId === this.organizationId); + } + } + + async submit() { + const checkedCollectionIds = this.collections.filter((c) => (c as any).checked).map((c) => c.id); + this.formPromise = this.cipherService.shareManyWithServer(this.shareableCiphers, this.organizationId, + checkedCollectionIds); + await this.formPromise; + this.onShared.emit(); + this.analytics.eventTrack.next({ action: 'Bulk Shared Items' }); + this.toasterService.popAsync('success', null, this.i18nService.t('sharedItems')); + } + + check(c: CollectionView) { + (c as any).checked = !(c as any).checked; + } + + selectAll(select: false) { + const collections = select ? this.collections : this.writeableCollections; + for (const c of collections) { + (c as any).checked = select; + } + } +} diff --git a/src/app/vault/ciphers.component.ts b/src/app/vault/ciphers.component.ts index 1bc374de..acc237fe 100644 --- a/src/app/vault/ciphers.component.ts +++ b/src/app/vault/ciphers.component.ts @@ -50,11 +50,15 @@ export class CiphersComponent extends BaseCiphersComponent { } } - getSelected(): string[] { + getSelected(): CipherView[] { if (this.ciphers == null) { return []; } - return this.ciphers.filter((c) => !!(c as any).checked).map((c) => c.id); + return this.ciphers.filter((c) => !!(c as any).checked); + } + + getSelectedIds(): string[] { + return this.getSelected().map((c) => c.id); } attachments(c: CipherView) { diff --git a/src/app/vault/share.component.html b/src/app/vault/share.component.html index 48a8c5b2..0a6c74ba 100644 --- a/src/app/vault/share.component.html +++ b/src/app/vault/share.component.html @@ -21,10 +21,10 @@

{{'collections' | i18n}}

- - diff --git a/src/app/vault/share.component.ts b/src/app/vault/share.component.ts index 62a77710..5fcb459c 100644 --- a/src/app/vault/share.component.ts +++ b/src/app/vault/share.component.ts @@ -52,11 +52,11 @@ export class ShareComponent implements OnInit, OnDestroy { } ngOnDestroy() { - this.unselectAll(); + this.selectAll(false); } filterCollections() { - this.unselectAll(); + this.selectAll(false); if (this.organizationId == null || this.writeableCollections.length === 0) { this.collections = []; } else { @@ -77,17 +77,9 @@ export class ShareComponent implements OnInit, OnDestroy { } } - cipherView.organizationId = this.organizationId; - cipherView.collectionIds = []; - for (const collection of this.collections) { - if ((collection as any).checked) { - cipherView.collectionIds.push(collection.id); - } - } - + const checkedCollectionIds = this.collections.filter((c) => (c as any).checked).map((c) => c.id); this.formPromise = Promise.all(attachmentPromises).then(async () => { - const encCipher = await this.cipherService.encrypt(cipherView); - await this.cipherService.shareWithServer(encCipher); + await this.cipherService.shareWithServer(cipherView, this.organizationId, checkedCollectionIds); this.onSharedCipher.emit(); this.analytics.eventTrack.next({ action: 'Shared Cipher' }); this.toasterService.popAsync('success', null, this.i18nService.t('sharedItem')); @@ -99,15 +91,10 @@ export class ShareComponent implements OnInit, OnDestroy { (c as any).checked = !(c as any).checked; } - selectAll() { - for (const c of this.collections) { - (c as any).checked = true; - } - } - - unselectAll() { - for (const c of this.writeableCollections) { - (c as any).checked = false; + selectAll(select: false) { + const collections = select ? this.collections : this.writeableCollections; + for (const c of collections) { + (c as any).checked = select; } } } diff --git a/src/app/vault/vault.component.ts b/src/app/vault/vault.component.ts index 5a893f64..1d4365ae 100644 --- a/src/app/vault/vault.component.ts +++ b/src/app/vault/vault.component.ts @@ -31,6 +31,7 @@ import { ShareComponent } from './share.component'; import { I18nService } from 'jslib/abstractions/i18n.service'; import { SyncService } from 'jslib/abstractions/sync.service'; import { BulkMoveComponent } from './bulk-move.component'; +import { BulkShareComponent } from './bulk-share.component'; @Component({ selector: 'app-vault', @@ -47,6 +48,7 @@ export class VaultComponent implements OnInit { @ViewChild('collections', { read: ViewContainerRef }) collectionsModalRef: ViewContainerRef; @ViewChild('bulkDeleteTemplate', { read: ViewContainerRef }) bulkDeleteModalRef: ViewContainerRef; @ViewChild('bulkMoveTemplate', { read: ViewContainerRef }) bulkMoveModalRef: ViewContainerRef; + @ViewChild('bulkShareTemplate', { read: ViewContainerRef }) bulkShareModalRef: ViewContainerRef; cipherId: string = null; favorites: boolean = false; @@ -290,7 +292,7 @@ export class VaultComponent implements OnInit { this.modal = this.bulkDeleteModalRef.createComponent(factory).instance; const childComponent = this.modal.show(BulkDeleteComponent, this.bulkDeleteModalRef); - childComponent.cipherIds = this.ciphersComponent.getSelected(); + childComponent.cipherIds = this.ciphersComponent.getSelectedIds(); childComponent.onDeleted.subscribe(async () => { this.modal.close(); await this.ciphersComponent.refresh(); @@ -302,7 +304,23 @@ export class VaultComponent implements OnInit { } bulkShare() { - // + 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 = this.ciphersComponent.getSelected(); + childComponent.onShared.subscribe(async () => { + this.modal.close(); + await this.ciphersComponent.refresh(); + }); + + this.modal.onClosed.subscribe(async () => { + this.modal = null; + }); } bulkMove() { @@ -314,7 +332,7 @@ export class VaultComponent implements OnInit { this.modal = this.bulkMoveModalRef.createComponent(factory).instance; const childComponent = this.modal.show(BulkMoveComponent, this.bulkMoveModalRef); - childComponent.cipherIds = this.ciphersComponent.getSelected(); + childComponent.cipherIds = this.ciphersComponent.getSelectedIds(); childComponent.onMoved.subscribe(async () => { this.modal.close(); await this.ciphersComponent.refresh(); diff --git a/src/locales/en/messages.json b/src/locales/en/messages.json index 42d92884..9a8671b2 100644 --- a/src/locales/en/messages.json +++ b/src/locales/en/messages.json @@ -671,6 +671,9 @@ "shareDesc": { "message": "Choose an organization that you wish to share this item with. Sharing transfers ownership of the item to the organization. You will no longer be the direct owner of this item once it has been shared." }, + "shareManyDesc": { + "message": "Choose an organization that you wish to share these items with. Sharing transfers ownership of the items to the organization. You will no longer be the direct owner of these items once they have been shared." + }, "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." }, @@ -691,5 +694,22 @@ "example": "150" } } + }, + "shareSelectedItemsDesc": { + "message": "You have selected $COUNT$ item(s). $SHAREABLE_COUNT$ items are sharable, $NONSHAREABLE_COUNT$ are not. Items with attachments must be shared individually.", + "placeholders": { + "count": { + "content": "$1", + "example": "10" + }, + "shareable_count": { + "content": "$2", + "example": "8" + }, + "nonshareable_count": { + "content": "$3", + "example": "2" + } + } } }