mirror of
https://github.com/bitwarden/browser.git
synced 2024-11-23 11:56:00 +01:00
org component, org vault listing updates
This commit is contained in:
parent
db43f817f7
commit
32f62b7ceb
2
jslib
2
jslib
@ -1 +1 @@
|
||||
Subproject commit ff8c1dfea9a3a6e42e2191dd3816889ce43045e0
|
||||
Subproject commit 278b4402da94ab36b4def76f520599a23308f7f1
|
@ -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,
|
||||
|
@ -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';
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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') {
|
||||
|
62
src/app/settings/organizations.component.html
Normal file
62
src/app/settings/organizations.component.html
Normal 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>
|
61
src/app/settings/organizations.component.ts
Normal file
61
src/app/settings/organizations.component.ts
Normal 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 { }
|
||||
}
|
||||
}
|
@ -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>
|
||||
|
@ -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>();
|
||||
|
@ -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>
|
@ -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;
|
||||
}
|
||||
}
|
@ -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>
|
||||
|
@ -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();
|
||||
|
@ -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."
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user