collection add/edit modal

This commit is contained in:
Kyle Spearrin 2018-07-10 10:06:57 -04:00
parent febc3093a9
commit edef454043
8 changed files with 307 additions and 42 deletions

2
jslib

@ -1 +1 @@
Subproject commit 36ab2ec78b0b235008312ab70c16882d28fd76b7
Subproject commit bded5eb625e3eb8f7577ad3da54d5c3b7e543eb0

View File

@ -32,6 +32,9 @@ import { RegisterComponent } from './accounts/register.component';
import { TwoFactorOptionsComponent } from './accounts/two-factor-options.component';
import { TwoFactorComponent } from './accounts/two-factor.component';
import {
CollectionAddEditComponent as OrgCollectionAddEditComponent,
} from './organizations/manage/collection-add-edit.component';
import { CollectionsComponent as OrgManageCollectionsComponent } from './organizations/manage/collections.component';
import { EntityUsersComponent as OrgEntityUsersComponent } from './organizations/manage/entity-users.component';
import { EventsComponent as OrgEventsComponent } from './organizations/manage/events.component';
@ -171,6 +174,7 @@ import { SearchPipe } from 'jslib/angular/pipes/search.pipe';
OrgAddEditComponent,
OrgAttachmentsComponent,
OrgCiphersComponent,
OrgCollectionAddEditComponent,
OrgCollectionsComponent,
OrgEntityUsersComponent,
OrgEventsComponent,
@ -229,6 +233,7 @@ import { SearchPipe } from 'jslib/angular/pipes/search.pipe';
ModalComponent,
OrgAddEditComponent,
OrgAttachmentsComponent,
OrgCollectionAddEditComponent,
OrgCollectionsComponent,
OrgEntityUsersComponent,
OrgGroupAddEditComponent,

View File

@ -0,0 +1,71 @@
<div class="modal fade">
<div class="modal-dialog">
<form class="modal-content" #form (ngSubmit)="submit()" [appApiAction]="formPromise">
<div class="modal-header">
<h2 class="modal-title">{{title}}</h2>
<button type="button" class="close" data-dismiss="modal" attr.aria-label="{{'close' | i18n}}">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body" *ngIf="loading">
<i class="fa fa-spinner fa-spin text-muted"></i>
</div>
<div class="modal-body" *ngIf="!loading">
<div class="form-group">
<label for="name">{{'name' | i18n}}</label>
<input id="name" class="form-control" type="text" name="Name" [(ngModel)]="name">
</div>
<h3 class="mt-4 d-flex mb-0">
{{'groupAccess' | i18n}}
<div class="ml-auto" *ngIf="groups && groups.length">
<button type="button" appBlurClick (click)="selectAll(true)" class="btn btn-link btn-sm py-0">
{{'selectAll' | i18n}}
</button>
<button type="button" appBlurClick (click)="selectAll(false)" class="btn btn-link btn-sm py-0">
{{'unselectAll' | i18n}}
</button>
</div>
</h3>
<div *ngIf="!groups || !groups.length">
{{'noGroupsInList' | i18n}}
</div>
<table class="table table-hover table-list mb-0" *ngIf="groups && groups.length">
<thead>
<tr>
<th>&nbsp;</th>
<th>{{'name' | i18n}}</th>
<th width="100" class="text-center">{{'readOnly' | i18n}}</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let g of groups; let i = index">
<td class="table-list-checkbox" (click)="check(g)">
<input type="checkbox" [(ngModel)]="g.checked" name="Groups[{{i}}].Checked">
</td>
<td (click)="check(g)">
<span appStopProp>{{g.name}}</span>
</td>
<td class="text-center">
<input type="checkbox" [(ngModel)]="g.readOnly" name="Groups[{{i}}].ReadOnly" [disabled]="!g.checked">
</td>
</tr>
</tbody>
</table>
</div>
<div class="modal-footer">
<button appBlurClick type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
<i class="fa fa-spinner fa-spin"></i>
<span>{{'save' | i18n}}</span>
</button>
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">{{'cancel' | i18n}}</button>
<div class="ml-auto">
<button #deleteBtn appBlurClick type="button" (click)="delete()" class="btn btn-outline-danger" title="{{'delete' | i18n}}"
*ngIf="editMode" [disabled]="deleteBtn.loading" [appApiAction]="deletePromise">
<i class="fa fa-trash-o fa-lg fa-fw" [hidden]="deleteBtn.loading"></i>
<i class="fa fa-spinner fa-spin fa-lg fa-fw" [hidden]="!deleteBtn.loading"></i>
</button>
</div>
</div>
</form>
</div>
</div>

View File

@ -0,0 +1,127 @@
import {
Component,
EventEmitter,
Input,
OnInit,
Output,
} from '@angular/core';
import { ToasterService } from 'angular2-toaster';
import { Angulartics2 } from 'angulartics2';
import { ApiService } from 'jslib/abstractions/api.service';
import { CryptoService } from 'jslib/abstractions/crypto.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
import { CipherString } from 'jslib/models/domain/cipherString';
import { SymmetricCryptoKey } from 'jslib/models/domain/symmetricCryptoKey';
import { CollectionRequest } from 'jslib/models/request/collectionRequest';
import { SelectionReadOnlyRequest } from 'jslib/models/request/selectionReadOnlyRequest';
import { GroupResponse } from 'jslib/models/response/groupResponse';
@Component({
selector: 'app-collection-add-edit',
templateUrl: 'collection-add-edit.component.html',
})
export class CollectionAddEditComponent implements OnInit {
@Input() collectionId: string;
@Input() organizationId: string;
@Output() onSavedCollection = new EventEmitter();
@Output() onDeletedCollection = new EventEmitter();
loading = true;
editMode: boolean = false;
title: string;
name: string;
groups: GroupResponse[] = [];
formPromise: Promise<any>;
deletePromise: Promise<any>;
private orgKey: SymmetricCryptoKey;
constructor(private apiService: ApiService, private i18nService: I18nService,
private analytics: Angulartics2, private toasterService: ToasterService,
private platformUtilsService: PlatformUtilsService, private cryptoService: CryptoService) { }
async ngOnInit() {
this.editMode = this.loading = this.collectionId != null;
const groupsResponse = await this.apiService.getGroups(this.organizationId);
this.groups = groupsResponse.data.map((r) => r);
this.orgKey = await this.cryptoService.getOrgKey(this.organizationId);
if (this.editMode) {
this.editMode = true;
this.title = this.i18nService.t('editCollection');
try {
const collection = await this.apiService.getCollectionDetails(this.organizationId, this.collectionId);
this.name = await this.cryptoService.decryptToUtf8(new CipherString(collection.name), this.orgKey);
if (collection.groups != null && this.groups != null) {
collection.groups.forEach((s) => {
const group = this.groups.filter((g) => g.id === s.id);
if (group != null && group.length > 0) {
(group[0] as any).checked = true;
(group[0] as any).readOnly = s.readOnly;
}
});
}
} catch { }
} else {
this.title = this.i18nService.t('addCollection');
}
this.loading = false;
}
check(g: GroupResponse, select?: boolean) {
(g as any).checked = select == null ? !(g as any).checked : select;
if (!(g as any).checked) {
(g as any).readOnly = false;
}
}
selectAll(select: boolean) {
this.groups.forEach((g) => this.check(g, select));
}
async submit() {
const request = new CollectionRequest();
request.name = (await this.cryptoService.encrypt(this.name, this.orgKey)).encryptedString;
request.groups = this.groups.filter((g) => (g as any).checked)
.map((g) => new SelectionReadOnlyRequest(g.id, !!(g as any).readOnly));
try {
if (this.editMode) {
this.formPromise = this.apiService.putCollection(this.organizationId, this.collectionId, request);
} else {
this.formPromise = this.apiService.postCollection(this.organizationId, request);
}
await this.formPromise;
this.analytics.eventTrack.next({ action: this.editMode ? 'Edited Collection' : 'Created Collection' });
this.toasterService.popAsync('success', null,
this.i18nService.t(this.editMode ? 'editedCollectionId' : 'createdCollectionId', this.name));
this.onSavedCollection.emit();
} catch { }
}
async delete() {
if (!this.editMode) {
return;
}
const confirmed = await this.platformUtilsService.showDialog(
this.i18nService.t('deleteCollectionConfirmation'), this.name,
this.i18nService.t('yes'), this.i18nService.t('no'), 'warning');
if (!confirmed) {
return false;
}
try {
this.deletePromise = this.apiService.deleteCollection(this.organizationId, this.collectionId);
await this.deletePromise;
this.analytics.eventTrack.next({ action: 'Deleted Collection' });
this.toasterService.popAsync('success', null, this.i18nService.t('deletedCollectionId', this.name));
this.onDeletedCollection.emit();
} catch { }
}
}

View File

@ -7,8 +7,13 @@ import {
} from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { ToasterService } from 'angular2-toaster';
import { Angulartics2 } from 'angulartics2';
import { ApiService } from 'jslib/abstractions/api.service';
import { CollectionService } from 'jslib/abstractions/collection.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
import { CollectionData } from 'jslib/models/data/collectionData';
import { Collection } from 'jslib/models/domain/collection';
@ -16,6 +21,7 @@ import { CollectionDetailsResponse } from 'jslib/models/response/collectionRespo
import { CollectionView } from 'jslib/models/view/collectionView';
import { ModalComponent } from '../../modal.component';
import { CollectionAddEditComponent } from './collection-add-edit.component';
import { EntityUsersComponent } from './entity-users.component';
@Component({
@ -34,7 +40,9 @@ export class CollectionsComponent implements OnInit {
private modal: ModalComponent = null;
constructor(private apiService: ApiService, private route: ActivatedRoute,
private collectionService: CollectionService, private componentFactoryResolver: ComponentFactoryResolver) { }
private collectionService: CollectionService, private componentFactoryResolver: ComponentFactoryResolver,
private analytics: Angulartics2, private toasterService: ToasterService,
private i18nService: I18nService, private platformUtilsService: PlatformUtilsService) { }
async ngOnInit() {
this.route.parent.parent.params.subscribe(async (params) => {
@ -52,7 +60,29 @@ export class CollectionsComponent implements OnInit {
}
edit(collection: CollectionView) {
//
if (this.modal != null) {
this.modal.close();
}
const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
this.modal = this.addEditModalRef.createComponent(factory).instance;
const childComponent = this.modal.show<CollectionAddEditComponent>(
CollectionAddEditComponent, this.addEditModalRef);
childComponent.organizationId = this.organizationId;
childComponent.collectionId = collection != null ? collection.id : null;
childComponent.onSavedCollection.subscribe(() => {
this.modal.close();
this.load();
});
childComponent.onDeletedCollection.subscribe(() => {
this.modal.close();
this.removeCollection(collection);
});
this.modal.onClosed.subscribe(() => {
this.modal = null;
});
}
add() {
@ -60,7 +90,19 @@ export class CollectionsComponent implements OnInit {
}
async delete(collection: CollectionView) {
//
const confirmed = await this.platformUtilsService.showDialog(
this.i18nService.t('deleteCollectionConfirmation'), collection.name,
this.i18nService.t('yes'), this.i18nService.t('no'), 'warning');
if (!confirmed) {
return false;
}
try {
await this.apiService.deleteCollection(this.organizationId, collection.id);
this.analytics.eventTrack.next({ action: 'Deleted Collection' });
this.toasterService.popAsync('success', null, this.i18nService.t('deletedCollectionId', collection.name));
this.removeCollection(collection);
} catch { }
}
users(collection: CollectionView) {
@ -82,4 +124,11 @@ export class CollectionsComponent implements OnInit {
this.modal = null;
});
}
private removeCollection(collection: CollectionView) {
const index = this.collections.indexOf(collection);
if (index > -1) {
this.collections.splice(index, 1);
}
}
}

View File

@ -20,7 +20,19 @@
<input id="externalId" class="form-control" type="text" name="ExternalId" [(ngModel)]="externalId">
<small class="form-text text-muted">{{'externalIdGroupDesc' | i18n}}</small>
</div>
<h3 class="mt-4">{{'accessControl' | i18n}}</h3>
<h3 class="mt-4 d-flex">
<div class="mb-1">
{{'accessControl' | i18n}}
</div>
<div class="ml-auto" *ngIf="access === 'selected' && collections && collections.length">
<button type="button" appBlurClick (click)="selectAll(true)" class="btn btn-link btn-sm py-0">
{{'selectAll' | i18n}}
</button>
<button type="button" appBlurClick (click)="selectAll(false)" class="btn btn-link btn-sm py-0">
{{'unselectAll' | i18n}}
</button>
</div>
</h3>
<div class="form-group">
<div class="form-check">
<input class="form-check-input" type="radio" name="access" id="accessAll" value="all" [(ngModel)]="access">
@ -39,37 +51,28 @@
<div *ngIf="!collections || !collections.length">
{{'noCollectionsInList' | i18n}}
</div>
<ng-container *ngIf="collections && collections.length">
<hr>
<button type="button" appBlurClick (click)="selectAll(true)" class="btn btn-link btn-sm py-0">
{{'selectAll' | i18n}}
</button>
<button type="button" appBlurClick (click)="selectAll(false)" class="btn btn-link btn-sm py-0">
{{'unselectAll' | i18n}}
</button>
<table class="table table-hover table-list mb-0">
<thead>
<tr>
<th>&nbsp;</th>
<th>{{'collections' | i18n}}</th>
<th width="100" class="text-center">{{'readOnly' | i18n}}</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let c of collections; let i = index">
<td class="table-list-checkbox" (click)="check(c)">
<input type="checkbox" [(ngModel)]="c.checked" name="Collection[{{i}}].Checked">
</td>
<td (click)="check(c)">
<span appStopProp>{{c.name}}</span>
</td>
<td class="text-center">
<input type="checkbox" [(ngModel)]="c.readOnly" name="Collection[{{i}}].ReadOnly" [disabled]="!c.checked">
</td>
</tr>
</tbody>
</table>
</ng-container>
<table class="table table-hover table-list mb-0" *ngIf="collections && collections.length">
<thead>
<tr>
<th>&nbsp;</th>
<th>{{'name' | i18n}}</th>
<th width="100" class="text-center">{{'readOnly' | i18n}}</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let c of collections; let i = index">
<td class="table-list-checkbox" (click)="check(c)">
<input type="checkbox" [(ngModel)]="c.checked" name="Collection[{{i}}].Checked">
</td>
<td (click)="check(c)">
<span appStopProp>{{c.name}}</span>
</td>
<td class="text-center">
<input type="checkbox" [(ngModel)]="c.readOnly" name="Collection[{{i}}].ReadOnly" [disabled]="!c.checked">
</td>
</tr>
</tbody>
</table>
</ng-container>
</div>
<div class="modal-footer">

View File

@ -85,7 +85,7 @@ export class GroupsComponent implements OnInit {
});
childComponent.onDeletedGroup.subscribe(() => {
this.modal.close();
this.load();
this.removeGroup(group);
});
this.modal.onClosed.subscribe(() => {
@ -109,10 +109,7 @@ export class GroupsComponent implements OnInit {
await this.apiService.deleteGroup(this.organizationId, group.id);
this.analytics.eventTrack.next({ action: 'Deleted Group' });
this.toasterService.popAsync('success', null, this.i18nService.t('deletedGroupId', group.name));
const index = this.groups.indexOf(group);
if (index > -1) {
this.groups.splice(index, 1);
}
this.removeGroup(group);
} catch { }
}
@ -135,4 +132,11 @@ export class GroupsComponent implements OnInit {
this.modal = null;
});
}
private removeGroup(group: GroupResponse) {
const index = this.groups.indexOf(group);
if (index > -1) {
this.groups.splice(index, 1);
}
}
}

View File

@ -595,7 +595,7 @@
"message": "There are no collections to list."
},
"noGroupsInList": {
"message": "There are no collections to list."
"message": "There are no groups to list."
},
"noUsersInList": {
"message": "There are no users to list."
@ -1751,6 +1751,9 @@
"editCollection": {
"message": "Edit Collection"
},
"deleteCollectionConfirmation": {
"message": "Are you sure you want to delete this collection?"
},
"inviteUser": {
"message": "Invite User"
},
@ -2014,5 +2017,8 @@
},
"userAccess": {
"message": "User Access"
},
"groupAccess": {
"message": "Group Access"
}
}