org component, org vault listing updates

This commit is contained in:
Kyle Spearrin 2018-07-04 09:55:52 -04:00
parent db43f817f7
commit 32f62b7ceb
16 changed files with 172 additions and 62 deletions

2
jslib

@ -1 +1 @@
Subproject commit ff8c1dfea9a3a6e42e2191dd3816889ce43045e0
Subproject commit 278b4402da94ab36b4def76f520599a23308f7f1

View File

@ -20,6 +20,7 @@ import { AccountComponent } from './settings/account.component';
import { CreateOrganizationComponent } from './settings/create-organization.component';
import { DomainRulesComponent } from './settings/domain-rules.component';
import { OptionsComponent } from './settings/options.component';
import { OrganizationsComponent } from './settings/organizations.component';
import { PremiumComponent } from './settings/premium.component';
import { SettingsComponent } from './settings/settings.component';
import { TwoFactorSetupComponent } from './settings/two-factor-setup.component';
@ -65,6 +66,7 @@ const routes: Routes = [
{ path: 'two-factor', component: TwoFactorSetupComponent, canActivate: [AuthGuardService] },
{ path: 'premium', component: PremiumComponent, canActivate: [AuthGuardService] },
{ path: 'billing', component: UserBillingComponent, canActivate: [AuthGuardService] },
{ path: 'organizations', component: OrganizationsComponent, canActivate: [AuthGuardService] },
{
path: 'create-organization',
component: CreateOrganizationComponent,

View File

@ -46,6 +46,7 @@ import { DeauthorizeSessionsComponent } from './settings/deauthorize-sessions.co
import { DeleteAccountComponent } from './settings/delete-account.component';
import { DomainRulesComponent } from './settings/domain-rules.component';
import { OptionsComponent } from './settings/options.component';
import { OrganizationsComponent } from './settings/organizations.component';
import { PaymentComponent } from './settings/payment.component';
import { PremiumComponent } from './settings/premium.component';
import { ProfileComponent } from './settings/profile.component';
@ -78,7 +79,6 @@ import { CiphersComponent } from './vault/ciphers.component';
import { CollectionsComponent } from './vault/collections.component';
import { FolderAddEditComponent } from './vault/folder-add-edit.component';
import { GroupingsComponent } from './vault/groupings.component';
import { OrganizationsComponent } from './vault/organizations.component';
import { ShareComponent } from './vault/share.component';
import { VaultComponent } from './vault/vault.component';

View File

@ -15,13 +15,13 @@
{{'vault' | i18n}}
</a>
</li>
<li class="nav-item">
<li class="nav-item" *ngIf="organization.isAdmin">
<a class="nav-link" routerLink="manage" routerLinkActive="active">
<i class="fa fa-sliders"></i>
{{'manage' | i18n}}
</a>
</li>
<li class="nav-item">
<li class="nav-item" *ngIf="organization.isAdmin">
<a class="nav-link" routerLink="tools" routerLinkActive="active">
<i class="fa fa-wrench"></i>
{{'tools' | i18n}}
@ -29,7 +29,7 @@
</li>
<li class="nav-item">
<a class="nav-link" routerLink="settings" routerLinkActive="active">
<i class="fa fa-cog"></i>
<i class="fa fa-cogs"></i>
{{'settings' | i18n}}
</a>
</li>

View File

@ -1,6 +1,7 @@
import {
Component,
EventEmitter,
Input,
Output,
} from '@angular/core';
@ -26,6 +27,7 @@ import { CipherView } from 'jslib/models/view/cipherView';
templateUrl: '../vault/ciphers.component.html',
})
export class CiphersComponent extends BaseCiphersComponent {
@Input() showAddNew = true;
@Output() onAttachmentsClicked = new EventEmitter<CipherView>();
@Output() onCollectionsClicked = new EventEmitter<CipherView>();
@ -58,7 +60,16 @@ export class CiphersComponent extends BaseCiphersComponent {
this.applyFilter(filter);
this.loaded = true;
} else {
await super.load((c) => c.organizationId === this.organization.id && (filter == null || filter(c)));
await super.load();
}
}
applyFilter(filter: (cipher: CipherView) => boolean = null) {
if (this.organization.isAdmin) {
super.applyFilter(filter);
} else {
const f = (c: CipherView) => c.organizationId === this.organization.id && (filter == null || filter(c));
super.applyFilter(f);
}
}

View File

@ -54,15 +54,15 @@ export class GroupingsComponent extends BaseGroupingsComponent {
} else {
this.collections = [];
}
const unassignedCollection = new CollectionView();
unassignedCollection.name = this.i18nService.t('unassigned');
unassignedCollection.id = 'unassigned';
unassignedCollection.organizationId = this.organization.id;
unassignedCollection.readOnly = true;
this.collections.push(unassignedCollection);
} else {
await super.loadCollections(this.organization.id);
}
const unassignedCollection = new CollectionView();
unassignedCollection.name = this.i18nService.t('unassigned');
unassignedCollection.id = 'unassigned';
unassignedCollection.organizationId = this.organization.id;
unassignedCollection.readOnly = true;
this.collections.push(unassignedCollection);
}
}

View File

@ -71,6 +71,7 @@ export class VaultComponent implements OnInit {
}
async clearGroupingFilters() {
this.ciphersComponent.showAddNew = true;
this.groupingsComponent.searchPlaceholder = this.i18nService.t('searchVault');
await this.ciphersComponent.applyFilter();
this.clearFilters();
@ -78,6 +79,7 @@ export class VaultComponent implements OnInit {
}
async filterCipherType(type: CipherType, load = false) {
this.ciphersComponent.showAddNew = true;
this.groupingsComponent.searchPlaceholder = this.i18nService.t('searchType');
const filter = (c: CipherView) => c.type === type;
if (load) {
@ -91,6 +93,7 @@ export class VaultComponent implements OnInit {
}
async filterCollection(collectionId: string, load = false) {
this.ciphersComponent.showAddNew = false;
this.groupingsComponent.searchPlaceholder = this.i18nService.t('searchCollection');
const filter = (c: CipherView) => {
if (collectionId === 'unassigned') {

View File

@ -0,0 +1,62 @@
<ng-container *ngIf="vault">
<p *ngIf="!loaded" class="text-muted">
<i class="fa fa-spinner fa-spin"></i>
</p>
<ng-container *ngIf="loaded">
<ul class="fa-ul card-ul carets" *ngIf="organizations && organizations.length">
<li *ngFor="let o of organizations">
<a [routerLink]="['/organizations', o.id]" class="text-body">
<i class="fa-li fa fa-caret-right"></i> {{o.name}}
</a>
</li>
</ul>
<p *ngIf="!organizations || !organizations.length">{{'noOrganizationsList' | i18n}}</p>
</ng-container>
<a href="#" routerLink="/settings/create-organization" class="btn btn-block btn-outline-primary">
<i class="fa fa-plus fa-fw"></i>
{{'newOrganization' | i18n}}
</a>
</ng-container>
<ng-container *ngIf="!vault">
<div class="page-header d-flex">
<h1>
{{'organizations' | i18n}}
<small [appApiAction]="actionPromise" #action>
<i class="fa fa-spinner fa-spin text-muted" *ngIf="action.loading"></i>
</small>
</h1>
<a href="#" routerLink="/settings/create-organization" class="btn btn-sm btn-outline-primary ml-auto">
<i class="fa fa-plus fa-fw"></i>
{{'newOrganization' | i18n}}
</a>
</div>
<i class="fa fa-spinner fa-spin text-muted" *ngIf="!loaded"></i>
<ng-container *ngIf="loaded">
<p *ngIf="!organizations || !organizations.length">{{'noOrganizationsList' | i18n}}</p>
<table class="table table-hover table-list" *ngIf="organizations && organizations.length">
<tbody>
<tr *ngFor="let o of organizations">
<td width="30">
<app-avatar [data]="o.name" width="25" height="25" [circle]="true" [fontSize]="14"></app-avatar>
</td>
<td>
<a href="#" [routerLink]="['/organizations', o.id]">{{o.name}}</a>
</td>
<td class="table-list-options">
<div class="dropdown" appListDropdown>
<button class="btn btn-outline-secondary dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<i class="fa fa-cog fa-lg"></i>
</button>
<div class="dropdown-menu dropdown-menu-right">
<a class="dropdown-item text-danger" href="#" appStopClick (click)="leave(o)">
<i class="fa fa-fw fa-sign-out"></i>
{{'leave' | i18n}}
</a>
</div>
</div>
</td>
</tr>
</tbody>
</table>
</ng-container>
</ng-container>

View File

@ -0,0 +1,61 @@
import {
Component,
Input,
OnInit,
} from '@angular/core';
import { ToasterService } from 'angular2-toaster';
import { Angulartics2 } from 'angulartics2';
import { ApiService } from 'jslib/abstractions/api.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
import { SyncService } from 'jslib/abstractions/sync.service';
import { UserService } from 'jslib/abstractions/user.service';
import { Organization } from 'jslib/models/domain/organization';
@Component({
selector: 'app-organizations',
templateUrl: 'organizations.component.html',
})
export class OrganizationsComponent implements OnInit {
@Input() vault = false;
organizations: Organization[];
loaded: boolean = false;
actionPromise: Promise<any>;
constructor(private userService: UserService, private platformUtilsService: PlatformUtilsService,
private i18nService: I18nService, private apiService: ApiService,
private analytics: Angulartics2, private toasterService: ToasterService,
private syncService: SyncService) { }
async ngOnInit() {
await this.load();
}
async load() {
this.organizations = await this.userService.getAllOrganizations();
this.loaded = true;
}
async leave(org: Organization) {
const confirmed = await this.platformUtilsService.showDialog(
this.i18nService.t('leaveOrganizationConfirmation'), org.name,
this.i18nService.t('yes'), this.i18nService.t('no'), 'warning');
if (!confirmed) {
return false;
}
try {
this.actionPromise = this.apiService.postLeaveOrganization(org.id).then(() => {
return this.syncService.fullSync(true);
});
await this.actionPromise;
this.analytics.eventTrack.next({ action: 'Left Organization' });
this.toasterService.popAsync('success', null, this.i18nService.t('leftOrganization'));
await this.load();
} catch { }
}
}

View File

@ -52,7 +52,7 @@
<i class="fa fa-spinner fa-spin text-muted" *ngIf="!loaded"></i>
<ng-container *ngIf="loaded">
<p>{{'noItemsInList' | i18n}}</p>
<button (click)="addCipher()" class="btn btn-outline-primary">
<button (click)="addCipher()" class="btn btn-outline-primary" *ngIf="showAddNew">
<i class="fa fa-plus fa-fw"></i>{{'addItem' | i18n}}</button>
</ng-container>
</div>

View File

@ -1,6 +1,7 @@
import {
Component,
EventEmitter,
Input,
Output,
} from '@angular/core';
@ -24,6 +25,7 @@ const MaxCheckedCount = 500;
templateUrl: 'ciphers.component.html',
})
export class CiphersComponent extends BaseCiphersComponent {
@Input() showAddNew = true;
@Output() onAttachmentsClicked = new EventEmitter<CipherView>();
@Output() onShareClicked = new EventEmitter<CipherView>();
@Output() onCollectionsClicked = new EventEmitter<CipherView>();

View File

@ -1,17 +0,0 @@
<p *ngIf="!loaded" class="text-muted">
<i class="fa fa-spinner fa-spin"></i>
</p>
<ng-container *ngIf="loaded">
<ul class="fa-ul card-ul carets" *ngIf="organizations && organizations.length">
<li *ngFor="let o of organizations">
<a [routerLink]="['/organizations', o.id]" class="text-body">
<i class="fa-li fa fa-caret-right"></i> {{o.name}}
</a>
</li>
</ul>
<p *ngIf="!organizations || !organizations.length">{{'noOrganizationsList' | i18n}}</p>
</ng-container>
<a href="#" routerLink="/settings/create-organization" class="btn btn-block btn-outline-primary">
<i class="fa fa-plus fa-fw"></i>
{{'newOrganization' | i18n}}
</a>

View File

@ -1,28 +0,0 @@
import {
Component,
OnInit,
} from '@angular/core';
import { UserService } from 'jslib/abstractions/user.service';
import { Organization } from 'jslib/models/domain/organization';
@Component({
selector: 'app-vault-organizations',
templateUrl: 'organizations.component.html',
})
export class OrganizationsComponent implements OnInit {
organizations: Organization[];
loaded: boolean = false;
constructor(private userService: UserService) { }
async ngOnInit() {
await this.load();
}
async load() {
this.organizations = await this.userService.getAllOrganizations();
this.loaded = true;
}
}

View File

@ -39,7 +39,7 @@
</a>
</div>
</div>
<button type="button" class="btn btn-primary btn-sm" (click)="addCipher()" appBlurClick>
<button type="button" class="btn btn-outline-primary btn-sm" (click)="addCipher()" appBlurClick>
<i class="fa fa-plus fa-fw"></i>{{'addItem' | i18n}}
</button>
</div>
@ -56,10 +56,10 @@
</div>
<div class="card mt-3">
<div class="card-header">
Organizations
{{'organizations' | i18n}}
</div>
<div class="card-body">
<app-vault-organizations></app-vault-organizations>
<app-organizations [vault]="true"></app-organizations>
</div>
</div>
</div>

View File

@ -91,6 +91,7 @@ export class VaultComponent implements OnInit {
}
async clearGroupingFilters() {
this.ciphersComponent.showAddNew = true;
this.groupingsComponent.searchPlaceholder = this.i18nService.t('searchVault');
await this.ciphersComponent.load();
this.clearFilters();
@ -98,6 +99,7 @@ export class VaultComponent implements OnInit {
}
async filterFavorites() {
this.ciphersComponent.showAddNew = true;
this.groupingsComponent.searchPlaceholder = this.i18nService.t('searchFavorites');
await this.ciphersComponent.load((c) => c.favorite);
this.clearFilters();
@ -106,6 +108,7 @@ export class VaultComponent implements OnInit {
}
async filterCipherType(type: CipherType) {
this.ciphersComponent.showAddNew = true;
this.groupingsComponent.searchPlaceholder = this.i18nService.t('searchType');
await this.ciphersComponent.load((c) => c.type === type);
this.clearFilters();
@ -114,6 +117,7 @@ export class VaultComponent implements OnInit {
}
async filterFolder(folderId: string) {
this.ciphersComponent.showAddNew = true;
folderId = folderId === 'none' ? null : folderId;
this.groupingsComponent.searchPlaceholder = this.i18nService.t('searchFolder');
await this.ciphersComponent.load((c) => c.folderId === folderId);
@ -123,6 +127,7 @@ export class VaultComponent implements OnInit {
}
async filterCollection(collectionId: string) {
this.ciphersComponent.showAddNew = false;
this.groupingsComponent.searchPlaceholder = this.i18nService.t('searchCollection');
await this.ciphersComponent.load((c) => c.collectionIds.indexOf(collectionId) > -1);
this.clearFilters();

View File

@ -1659,5 +1659,14 @@
},
"organizationReadyToGo": {
"message": "Your new organization is ready to go!"
},
"leave": {
"message": "Leave"
},
"leaveOrganizationConfirmation": {
"message": "Are you sure you want to leave this organization?"
},
"leftOrganization": {
"message": "You have left the organization."
}
}