diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 55b77758b2..8189126f11 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -11,7 +11,6 @@ import { FormsModule } from '@angular/forms'; import { BrowserModule } from '@angular/platform-browser'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { OrganizationsModule } from './organizations/organizations.module'; import { ServicesModule } from './services/services.module'; import { AppComponent } from './app.component'; @@ -33,6 +32,10 @@ import { RegisterComponent } from './accounts/register.component'; import { TwoFactorOptionsComponent } from './accounts/two-factor-options.component'; import { TwoFactorComponent } from './accounts/two-factor.component'; +import { CiphersComponent as OrgCiphersComponent } from './organizations/ciphers.component'; +import { GroupingsComponent as OrgGroupingsComponent } from './organizations/groupings.component'; +import { VaultComponent as OrgVaultComponent } from './organizations/vault.component'; + import { AccountComponent } from './settings/account.component'; import { AdjustPaymentComponent } from './settings/adjust-payment.component'; import { AdjustStorageComponent } from './settings/adjust-storage.component'; @@ -100,7 +103,6 @@ import { SearchCiphersPipe } from 'jslib/angular/pipes/search-ciphers.pipe'; BrowserAnimationsModule, FormsModule, AppRoutingModule, - OrganizationsModule, ServicesModule, Angulartics2Module.forRoot([Angulartics2GoogleAnalytics], { pageTracking: { @@ -150,8 +152,11 @@ import { SearchCiphersPipe } from 'jslib/angular/pipes/search-ciphers.pipe'; ModalComponent, NavbarComponent, OptionsComponent, + OrgCiphersComponent, + OrgGroupingsComponent, OrganizationsComponent, OrganizationLayoutComponent, + OrgVaultComponent, PasswordGeneratorComponent, PasswordGeneratorHistoryComponent, PaymentComponent, diff --git a/src/app/organizations/ciphers.component.ts b/src/app/organizations/ciphers.component.ts new file mode 100644 index 0000000000..cbaa27ca83 --- /dev/null +++ b/src/app/organizations/ciphers.component.ts @@ -0,0 +1,101 @@ +import { + Component, + EventEmitter, + 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 { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service'; + +import { CiphersComponent as BaseCiphersComponent } from 'jslib/angular/components/ciphers.component'; + +import { CipherType } from 'jslib/enums/cipherType'; + +import { CipherData } from 'jslib/models/data/cipherData'; +import { Cipher } from 'jslib/models/domain/cipher'; +import { Organization } from 'jslib/models/domain/organization'; +import { CipherView } from 'jslib/models/view/cipherView'; + +@Component({ + selector: 'app-org-vault-ciphers', + templateUrl: '../vault/ciphers.component.html', +}) +export class CiphersComponent extends BaseCiphersComponent { + @Output() onAttachmentsClicked = new EventEmitter(); + @Output() onCollectionsClicked = new EventEmitter(); + + organization: Organization; + cipherType = CipherType; + + constructor(cipherService: CipherService, private analytics: Angulartics2, + private toasterService: ToasterService, private i18nService: I18nService, + private platformUtilsService: PlatformUtilsService, private apiService: ApiService) { + super(cipherService); + } + + async load(filter: (cipher: CipherView) => boolean = null) { + if (this.organization.isAdmin) { + const ciphers = await this.apiService.getCiphersOrganization(this.organization.id); + if (ciphers != null && ciphers.data != null && ciphers.data.length) { + const decCiphers: CipherView[] = []; + const promises: any[] = []; + ciphers.data.forEach((r) => { + const data = new CipherData(r); + const cipher = new Cipher(data); + promises.push(cipher.decrypt().then((c) => decCiphers.push(c))); + }); + await Promise.all(promises); + decCiphers.sort(this.cipherService.getLocaleSortingFunction()); + this.allCiphers = decCiphers; + } else { + this.allCiphers = []; + } + this.applyFilter(filter); + this.loaded = true; + } else { + await super.load((c) => c.organizationId === this.organization.id && (filter == null || filter(c))); + } + } + + checkCipher(c: CipherView) { + // do nothing + } + + attachments(c: CipherView) { + this.onAttachmentsClicked.emit(c); + } + + collections(c: CipherView) { + this.onCollectionsClicked.emit(c); + } + + async delete(c: CipherView): Promise { + const confirmed = await this.platformUtilsService.showDialog( + this.i18nService.t('deleteItemConfirmation'), this.i18nService.t('deleteItem'), + this.i18nService.t('yes'), this.i18nService.t('no'), 'warning'); + if (!confirmed) { + return false; + } + + await this.cipherService.deleteWithServer(c.id); + this.analytics.eventTrack.next({ action: 'Deleted Cipher' }); + this.toasterService.popAsync('success', null, this.i18nService.t('deletedItem')); + this.refresh(); + } + + copy(value: string, typeI18nKey: string, aType: string) { + if (value == null) { + return; + } + + this.analytics.eventTrack.next({ action: 'Copied ' + aType.toLowerCase() + ' from listing.' }); + this.platformUtilsService.copyToClipboard(value, { doc: window.document }); + this.toasterService.popAsync('info', null, + this.i18nService.t('valueCopied', this.i18nService.t(typeI18nKey))); + } +} diff --git a/src/app/organizations/groupings.component.ts b/src/app/organizations/groupings.component.ts new file mode 100644 index 0000000000..7d43ded425 --- /dev/null +++ b/src/app/organizations/groupings.component.ts @@ -0,0 +1,61 @@ +import { + Component, + EventEmitter, + Input, + Output, +} from '@angular/core'; + +import { ApiService } from 'jslib/abstractions/api.service'; +import { CollectionService } from 'jslib/abstractions/collection.service'; +import { FolderService } from 'jslib/abstractions/folder.service'; +import { UserService } from 'jslib/abstractions/user.service'; + +import { GroupingsComponent as BaseGroupingsComponent } from 'jslib/angular/components/groupings.component'; + +import { CollectionData } from 'jslib/models/data/collectionData'; +import { Collection } from 'jslib/models/domain/collection'; +import { Organization } from 'jslib/models/domain/organization'; +import { CollectionView } from 'jslib/models/view/collectionView'; + +@Component({ + selector: 'app-org-vault-groupings', + templateUrl: '../vault/groupings.component.html', +}) +export class GroupingsComponent extends BaseGroupingsComponent { + @Output() onSearchTextChanged = new EventEmitter(); + + organization: Organization; + searchText: string = ''; + searchPlaceholder: string = null; + + constructor(collectionService: CollectionService, folderService: FolderService, + private apiService: ApiService, private userService: UserService) { + super(collectionService, folderService); + } + + searchTextChanged() { + this.onSearchTextChanged.emit(this.searchText); + } + + async loadCollections() { + if (this.organization.isAdmin) { + const collections = await this.apiService.getCollections(this.organization.id); + if (collections != null && collections.data != null && collections.data.length) { + const decCollections: CollectionView[] = []; + const promises: any[] = []; + collections.data.forEach((r) => { + const data = new CollectionData(r); + const collection = new Collection(data); + promises.push(collection.decrypt().then((c) => decCollections.push(c))); + }); + await Promise.all(promises); + decCollections.sort(this.collectionService.getLocaleSortingFunction()); + this.collections = decCollections; + } else { + this.collections = []; + } + } else { + await super.loadCollections(this.organization.id); + } + } +} diff --git a/src/app/organizations/organizations.module.ts b/src/app/organizations/organizations.module.ts deleted file mode 100644 index a6c1adf02f..0000000000 --- a/src/app/organizations/organizations.module.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { NgModule } from '@angular/core'; - -import { VaultComponent } from './vault.component'; - -@NgModule({ - declarations: [ - VaultComponent, - ], - entryComponents: [], - providers: [], -}) -export class OrganizationsModule { } diff --git a/src/app/organizations/vault.component.html b/src/app/organizations/vault.component.html index ff047b25ca..8a4aa67025 100644 --- a/src/app/organizations/vault.component.html +++ b/src/app/organizations/vault.component.html @@ -1 +1,23 @@ -Org vault!! +
+
+
+ + +
+
+ + + +
+
+
+ + + diff --git a/src/app/organizations/vault.component.ts b/src/app/organizations/vault.component.ts index c5dd14d23a..9d4279a5c3 100644 --- a/src/app/organizations/vault.component.ts +++ b/src/app/organizations/vault.component.ts @@ -1,19 +1,127 @@ +import { Location } from '@angular/common'; import { Component, OnInit, + ViewChild, } from '@angular/core'; -import { ActivatedRoute } from '@angular/router'; +import { + ActivatedRoute, + Router, +} from '@angular/router'; + +import { I18nService } from 'jslib/abstractions/i18n.service'; +import { SyncService } from 'jslib/abstractions/sync.service'; +import { UserService } from 'jslib/abstractions/user.service'; + +import { Organization } from 'jslib/models/domain/organization'; +import { CipherView } from 'jslib/models/view/cipherView'; + +import { CipherType } from 'jslib/enums/cipherType'; + +import { CiphersComponent } from './ciphers.component'; +import { GroupingsComponent } from './groupings.component'; @Component({ selector: 'app-org-vault', templateUrl: 'vault.component.html', }) export class VaultComponent implements OnInit { - constructor(private route: ActivatedRoute) { } + @ViewChild(GroupingsComponent) groupingsComponent: GroupingsComponent; + @ViewChild(CiphersComponent) ciphersComponent: CiphersComponent; + + organization: Organization; + collectionId: string; + type: CipherType; + + constructor(private route: ActivatedRoute, private userService: UserService, + private location: Location, private router: Router, + private syncService: SyncService, private i18nService: I18nService) { } ngOnInit() { this.route.parent.params.subscribe(async (params) => { + this.organization = await this.userService.getOrganization(params.organizationId); + this.groupingsComponent.organization = this.organization; + this.ciphersComponent.organization = this.organization; + this.route.queryParams.subscribe(async (qParams) => { + if (!this.organization.isAdmin) { + await this.syncService.fullSync(false); + } + await this.groupingsComponent.load(); + + if (qParams == null) { + this.groupingsComponent.selectedAll = true; + await this.ciphersComponent.load(); + return; + } + + if (qParams.type) { + const t = parseInt(qParams.type, null); + this.groupingsComponent.selectedType = t; + await this.filterCipherType(t, true); + } else if (qParams.collectionId) { + this.groupingsComponent.selectedCollectionId = qParams.collectionId; + await this.filterCollection(qParams.collectionId, true); + } else { + this.groupingsComponent.selectedAll = true; + await this.ciphersComponent.load(); + } + }); }); } + + async clearGroupingFilters() { + this.groupingsComponent.searchPlaceholder = this.i18nService.t('searchVault'); + await this.ciphersComponent.applyFilter(); + this.clearFilters(); + this.go(); + } + + async filterCipherType(type: CipherType, load = false) { + this.groupingsComponent.searchPlaceholder = this.i18nService.t('searchType'); + const filter = (c: CipherView) => c.type === type; + if (load) { + await this.ciphersComponent.load(filter); + } else { + await this.ciphersComponent.applyFilter(filter); + } + this.clearFilters(); + this.type = type; + this.go(); + } + + async filterCollection(collectionId: string, load = false) { + this.groupingsComponent.searchPlaceholder = this.i18nService.t('searchCollection'); + const filter = (c: CipherView) => c.collectionIds.indexOf(collectionId) > -1; + if (load) { + await this.ciphersComponent.load(filter); + } else { + await this.ciphersComponent.applyFilter(filter); + } + this.clearFilters(); + this.collectionId = collectionId; + this.go(); + } + + filterSearchText(searchText: string) { + this.ciphersComponent.searchText = searchText; + } + + private clearFilters() { + this.collectionId = null; + this.type = null; + } + + private go(queryParams: any = null) { + if (queryParams == null) { + queryParams = { + type: this.type, + collectionId: this.collectionId, + }; + } + + const url = this.router.createUrlTree(['organizations', this.organization.id, 'vault'], + { queryParams: queryParams }).toString(); + this.location.go(url); + } } diff --git a/src/app/vault/groupings.component.html b/src/app/vault/groupings.component.html index dc421910a5..fc9fee31fe 100644 --- a/src/app/vault/groupings.component.html +++ b/src/app/vault/groupings.component.html @@ -11,7 +11,7 @@ {{'allItems' | i18n}} -
  • +
  • {{'favorites' | i18n}} @@ -44,22 +44,25 @@

    -

    - {{'folders' | i18n}} - - - -

    - -
    + + + +

    {{'collections' | i18n}}

    • @@ -67,7 +70,7 @@ {{c.name}}
    -
    +
    diff --git a/src/app/vault/vault.component.ts b/src/app/vault/vault.component.ts index 105a052053..6011fb80c8 100644 --- a/src/app/vault/vault.component.ts +++ b/src/app/vault/vault.component.ts @@ -47,7 +47,6 @@ export class VaultComponent implements OnInit { @ViewChild('bulkMoveTemplate', { read: ViewContainerRef }) bulkMoveModalRef: ViewContainerRef; @ViewChild('bulkShareTemplate', { read: ViewContainerRef }) bulkShareModalRef: ViewContainerRef; - cipherId: string = null; favorites: boolean = false; type: CipherType = null; folderId: string = null; @@ -62,9 +61,7 @@ export class VaultComponent implements OnInit { async ngOnInit() { this.route.queryParams.subscribe(async (params) => { await this.syncService.fullSync(false); - await Promise.all([ - this.groupingsComponent.load(), - ]); + await this.groupingsComponent.load(); if (params == null) { this.groupingsComponent.selectedAll = true; @@ -355,7 +352,6 @@ export class VaultComponent implements OnInit { private go(queryParams: any = null) { if (queryParams == null) { queryParams = { - cipherId: this.cipherId, favorites: this.favorites ? true : null, type: this.type, folderId: this.folderId, diff --git a/src/scss/styles.scss b/src/scss/styles.scss index e2710f8de8..76d0c90f0f 100644 --- a/src/scss/styles.scss +++ b/src/scss/styles.scss @@ -297,7 +297,7 @@ label:not(.form-check-label) { font-size: $font-size-lg; } -app-vault-groupings { +app-vault-groupings, app-org-vault-groupings { .card { #search { margin-bottom: 1rem;