Merge pull request #3891 from tsiegleauq/cu-user
User create update and details view Some open tasks left (features are quire heavy). See recent issues
This commit is contained in:
commit
b6ad0d759c
@ -15,7 +15,12 @@ import { ConstantsService } from './core/services/constants.service';
|
||||
})
|
||||
export class AppComponent {
|
||||
/**
|
||||
* Initialises the translation unit.
|
||||
* Master-component of all apps.
|
||||
*
|
||||
* Inits the translation service, the operator, the login data and the constants.
|
||||
*
|
||||
* Handles the altering of Array.toString()
|
||||
*
|
||||
* @param autoupdateService
|
||||
* @param notifyService
|
||||
* @param translate
|
||||
@ -35,5 +40,36 @@ export class AppComponent {
|
||||
const browserLang = translate.getBrowserLang();
|
||||
// try to use the browser language if it is available. If not, uses english.
|
||||
translate.use(translate.getLangs().includes(browserLang) ? browserLang : 'en');
|
||||
// change default JS functions
|
||||
this.overloadArrayToString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to alter the normal Array.toString - function
|
||||
*
|
||||
* Will add a whitespace after a comma and shorten the output to
|
||||
* three strings.
|
||||
*
|
||||
* TODO: There might be a better place for overloading functions than app.component
|
||||
* TODO: Overloading can be extended to more functions.
|
||||
*/
|
||||
private overloadArrayToString(): void {
|
||||
Array.prototype.toString = function(): string {
|
||||
let string = '';
|
||||
const iterations = Math.min(this.length, 3);
|
||||
|
||||
for (let i = 0; i <= iterations; i++) {
|
||||
if (i < iterations) {
|
||||
string += this[i];
|
||||
}
|
||||
|
||||
if (i < iterations - 1) {
|
||||
string += ', ';
|
||||
} else if (i === iterations && this.length > iterations) {
|
||||
string += ', ...';
|
||||
}
|
||||
}
|
||||
return string;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -21,31 +21,46 @@ export class DataSendService {
|
||||
public constructor(private http: HttpClient) {}
|
||||
|
||||
/**
|
||||
* Save motion in the server
|
||||
*
|
||||
* @return Observable from
|
||||
* Sends a post request with the model to the server.
|
||||
* Usually for new Models
|
||||
*/
|
||||
public saveModel(model: BaseModel): Observable<BaseModel> {
|
||||
if (!model.id) {
|
||||
return this.http.post<BaseModel>('rest/' + model.collectionString + '/', model).pipe(
|
||||
tap(
|
||||
response => {
|
||||
// TODO: Message, Notify, Etc
|
||||
console.log('New Model added. Response : ', response);
|
||||
},
|
||||
error => console.log('error. ', error)
|
||||
)
|
||||
);
|
||||
} else {
|
||||
return this.http.patch<BaseModel>('rest/' + model.collectionString + '/' + model.id, model).pipe(
|
||||
tap(
|
||||
response => {
|
||||
console.log('Update model. Response : ', response);
|
||||
},
|
||||
error => console.log('error. ', error)
|
||||
)
|
||||
);
|
||||
public createModel(model: BaseModel): Observable<BaseModel> {
|
||||
return this.http.post<BaseModel>('rest/' + model.collectionString + '/', model).pipe(
|
||||
tap(
|
||||
response => {
|
||||
// TODO: Message, Notify, Etc
|
||||
console.log('New Model added. Response :\n', response);
|
||||
},
|
||||
error => console.error('createModel has returned an Error:\n', error)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to change a model on the server.
|
||||
*
|
||||
* @param model the base model that is meant to be changed
|
||||
* @param method the required http method. might be put or patch
|
||||
*/
|
||||
public updateModel(model: BaseModel, method: 'put' | 'patch'): Observable<BaseModel> {
|
||||
const restPath = `rest/${model.collectionString}/${model.id}`;
|
||||
let httpMethod;
|
||||
|
||||
if (method === 'patch') {
|
||||
httpMethod = this.http.patch<BaseModel>(restPath, model);
|
||||
} else if (method === 'put') {
|
||||
httpMethod = this.http.put<BaseModel>(restPath, model);
|
||||
}
|
||||
|
||||
return httpMethod.pipe(
|
||||
tap(
|
||||
response => {
|
||||
// TODO: Message, Notify, Etc
|
||||
console.log('Update model. Response :\n', response);
|
||||
},
|
||||
error => console.error('updateModel has returned an Error:\n', error)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -121,11 +121,11 @@ export class OperatorService extends OpenSlidesComponent {
|
||||
|
||||
/**
|
||||
* Checks, if the operator has at least one of the given permissions.
|
||||
* @param permissions The permissions to check, if at least one matches.
|
||||
* @param checkPerms The permissions to check, if at least one matches.
|
||||
*/
|
||||
public hasPerms(...permissions: Permission[]): boolean {
|
||||
return permissions.some(permisison => {
|
||||
return this.permissions.includes(permisison);
|
||||
public hasPerms(...checkPerms: Permission[]): boolean {
|
||||
return checkPerms.some(permission => {
|
||||
return this.permissions.includes(permission);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -25,6 +25,20 @@ export class PermsDirective extends OpenSlidesComponent {
|
||||
*/
|
||||
private lastPermissionCheckResult = false;
|
||||
|
||||
/**
|
||||
* Alternative to the permissions. Used in special case where a combination
|
||||
* with *ngIf would be required.
|
||||
*
|
||||
* # Example:
|
||||
*
|
||||
* The div will render if the permission `user.can_manage` is set
|
||||
* or if `this.ownPage` is `true`
|
||||
* ```html
|
||||
* <div *osPerms="'users.can_manage';or:ownPage"> something </div>
|
||||
* ```
|
||||
*/
|
||||
private alternative: boolean;
|
||||
|
||||
/**
|
||||
* Constructs the directive once. Observes the operator for it's groups so the
|
||||
* directive can perform changes dynamically
|
||||
@ -61,6 +75,16 @@ export class PermsDirective extends OpenSlidesComponent {
|
||||
this.updateView();
|
||||
}
|
||||
|
||||
/**
|
||||
* Comes from the view.
|
||||
* `;or:` turns into osPermsOr during runtime.
|
||||
*/
|
||||
@Input('osPermsOr')
|
||||
public set osPermsAlt(value: boolean) {
|
||||
this.alternative = value;
|
||||
this.updateView();
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows or hides certain content in the view.
|
||||
*/
|
||||
@ -68,7 +92,7 @@ export class PermsDirective extends OpenSlidesComponent {
|
||||
const hasPerms = this.checkPermissions();
|
||||
const permsChanged = hasPerms !== this.lastPermissionCheckResult;
|
||||
|
||||
if (hasPerms && permsChanged) {
|
||||
if ((hasPerms && permsChanged) || this.alternative) {
|
||||
// clean up and add the template
|
||||
this.viewContainer.clear();
|
||||
this.viewContainer.createEmbeddedView(this.template);
|
||||
|
@ -60,6 +60,9 @@ export abstract class BaseModel<T = object> extends OpenSlidesComponent
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* update the values of the base model with new values
|
||||
*/
|
||||
public patchValues(update: Partial<T>): void {
|
||||
Object.assign(this, update);
|
||||
}
|
||||
|
@ -15,7 +15,8 @@ import {
|
||||
MatSnackBarModule,
|
||||
MatTableModule,
|
||||
MatPaginatorModule,
|
||||
MatSortModule
|
||||
MatSortModule,
|
||||
MatTooltipModule
|
||||
} from '@angular/material';
|
||||
import { MatAutocompleteModule } from '@angular/material/autocomplete';
|
||||
import { MatChipsModule } from '@angular/material';
|
||||
@ -79,6 +80,7 @@ library.add(fas);
|
||||
MatDialogModule,
|
||||
MatSnackBarModule,
|
||||
MatChipsModule,
|
||||
MatTooltipModule,
|
||||
FontAwesomeModule,
|
||||
TranslateModule.forChild(),
|
||||
RouterModule,
|
||||
@ -106,6 +108,7 @@ library.add(fas);
|
||||
MatDialogModule,
|
||||
MatSnackBarModule,
|
||||
MatChipsModule,
|
||||
MatTooltipModule,
|
||||
NgxMatSelectSearchModule,
|
||||
FontAwesomeModule,
|
||||
TranslateModule,
|
||||
|
@ -23,8 +23,9 @@
|
||||
|
||||
<!-- Button on the right-->
|
||||
<div *ngIf="editMotion">
|
||||
<button (click)='cancelEditMotionButton()' class='on-transition-fade' mat-icon-button>
|
||||
<fa-icon icon='times'></fa-icon>
|
||||
<button (click)='cancelEditMotionButton()' class='on-transition-fade' color="warn" mat-raised-button>
|
||||
<span translate>Abort</span>
|
||||
<fa-icon class="icon-text-distance" icon='times'></fa-icon>
|
||||
</button>
|
||||
</div>
|
||||
<div *ngIf="!editMotion">
|
||||
@ -39,7 +40,7 @@
|
||||
<button mat-menu-item translate>Export As...</button>
|
||||
<button mat-menu-item translate>Project</button>
|
||||
<mat-divider></mat-divider>
|
||||
<button mat-menu-item class='deleteMotionButton' (click)='deleteMotionButton()' translate>DeleteMotion</button>
|
||||
<button mat-menu-item class='red-warning-text' (click)='deleteMotionButton()' translate>DeleteMotion</button>
|
||||
</mat-menu>
|
||||
</mat-toolbar>
|
||||
|
||||
|
@ -2,14 +2,6 @@ span {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.save-button {
|
||||
background-color: rgb(77, 243, 86);
|
||||
}
|
||||
|
||||
.deleteMotionButton {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.motion-title {
|
||||
padding-left: 20px;
|
||||
line-height: 100%;
|
||||
|
@ -1,13 +1,3 @@
|
||||
.custom-table-header {
|
||||
// display: none;
|
||||
width: 100%;
|
||||
height: 60px;
|
||||
line-height: 60px;
|
||||
text-align: right;
|
||||
background: white;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
/** css hacks https://codepen.io/edge0703/pen/iHJuA */
|
||||
.innerTable {
|
||||
display: inline-block;
|
||||
|
@ -40,7 +40,7 @@ export class CategoryRepositoryService extends BaseRepository<ViewCategory, Cate
|
||||
if (this.osInDataStore(viewCategory)) {
|
||||
return this.update(update, viewCategory);
|
||||
} else {
|
||||
return this.dataSend.saveModel(viewCategory.category);
|
||||
return this.dataSend.createModel(viewCategory.category);
|
||||
}
|
||||
}
|
||||
|
||||
@ -52,7 +52,7 @@ export class CategoryRepositoryService extends BaseRepository<ViewCategory, Cate
|
||||
updateCategory = new Category();
|
||||
}
|
||||
updateCategory.patchValues(update);
|
||||
return this.dataSend.saveModel(updateCategory);
|
||||
return this.dataSend.updateModel(updateCategory, 'put');
|
||||
}
|
||||
|
||||
public delete(viewCategory: ViewCategory): Observable<any> {
|
||||
|
@ -66,7 +66,7 @@ export class MotionRepositoryService extends BaseRepository<ViewMotion, Motion>
|
||||
* TODO: Remove the viewMotion and make it actually distignuishable from save()
|
||||
*/
|
||||
public create(motion: Motion): Observable<any> {
|
||||
return this.dataSend.saveModel(motion);
|
||||
return this.dataSend.createModel(motion);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -81,7 +81,7 @@ export class MotionRepositoryService extends BaseRepository<ViewMotion, Motion>
|
||||
public update(update: Partial<Motion>, viewMotion: ViewMotion): Observable<any> {
|
||||
const motion = viewMotion.motion;
|
||||
motion.patchValues(update);
|
||||
return this.dataSend.saveModel(motion);
|
||||
return this.dataSend.updateModel(motion, 'patch');
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { Component, OnInit, ViewChild } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
import { AuthService } from 'app/core/services/auth.service';
|
||||
import { OperatorService } from 'app/core/services/operator.service';
|
||||
@ -21,7 +22,8 @@ export class SiteComponent extends BaseComponent implements OnInit {
|
||||
/**
|
||||
* HTML element of the side panel
|
||||
*/
|
||||
@ViewChild('sideNav') public sideNav: MatSidenav;
|
||||
@ViewChild('sideNav')
|
||||
public sideNav: MatSidenav;
|
||||
|
||||
/**
|
||||
* Get the username from the operator (should be known already)
|
||||
@ -37,6 +39,7 @@ export class SiteComponent extends BaseComponent implements OnInit {
|
||||
* Constructor
|
||||
*
|
||||
* @param authService
|
||||
* @param router
|
||||
* @param operator
|
||||
* @param vp
|
||||
* @param translate
|
||||
@ -44,6 +47,7 @@ export class SiteComponent extends BaseComponent implements OnInit {
|
||||
*/
|
||||
public constructor(
|
||||
private authService: AuthService,
|
||||
private router: Router,
|
||||
public operator: OperatorService,
|
||||
public vp: ViewportService,
|
||||
public translate: TranslateService,
|
||||
@ -109,7 +113,11 @@ export class SiteComponent extends BaseComponent implements OnInit {
|
||||
}
|
||||
|
||||
// TODO: Implement this
|
||||
public editProfile(): void {}
|
||||
public editProfile(): void {
|
||||
if (this.operator.user) {
|
||||
this.router.navigate([`./users/${this.operator.user.id}`]);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Implement this
|
||||
public changePassword(): void {}
|
||||
|
@ -0,0 +1,152 @@
|
||||
<mat-toolbar color='primary'>
|
||||
|
||||
<button *osPerms="'users.can_manage';or:ownPage" (click)='editUserButton()' [ngClass]="{'save-button': editUser}"
|
||||
class='generic-mini-button on-transition-fade' mat-mini-fab>
|
||||
<fa-icon *ngIf='!editUser' icon='pen'></fa-icon>
|
||||
<fa-icon *ngIf='editUser' icon='check'></fa-icon>
|
||||
</button>
|
||||
|
||||
<div class="on-transition-fade">
|
||||
<div *ngIf='editUser'>
|
||||
{{personalInfoForm.get('title').value}}
|
||||
{{personalInfoForm.get('first_name').value}}
|
||||
{{personalInfoForm.get('last_name').value}}
|
||||
</div>
|
||||
|
||||
<div *ngIf='!editUser'>
|
||||
{{user.fullName}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span class='spacer'></span>
|
||||
|
||||
<!-- Button on the right-->
|
||||
<div *ngIf="editUser">
|
||||
<button (click)='cancelEditMotionButton()' class='on-transition-fade' color="warn" mat-raised-button>
|
||||
<span translate>Abort</span>
|
||||
<fa-icon class="icon-text-distance" icon='times'></fa-icon>
|
||||
</button>
|
||||
</div>
|
||||
<div *ngIf="!editUser">
|
||||
<button class='on-transition-fade' mat-icon-button [matMenuTriggerFor]="userExtraMenu">
|
||||
<fa-icon icon='ellipsis-v'></fa-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<mat-menu #userExtraMenu="matMenu">
|
||||
<button mat-menu-item class="red-warning-text" (click)='deleteUserButton()' translate>Delete User</button>
|
||||
</mat-menu>
|
||||
|
||||
</mat-toolbar>
|
||||
|
||||
<mat-card class="os-card" *osPerms="'users.can_see_name'">
|
||||
<form [ngClass]="{'mat-form-field-enabled': editUser}" [formGroup]='personalInfoForm' (ngSubmit)='saveUser()' *ngIf="user">
|
||||
<!-- <h3 translate>Personal Data</h3> -->
|
||||
<div *ngIf='isAllowed("seeName")'>
|
||||
<!-- Title -->
|
||||
<mat-form-field class='form30 distance force-min-with' *ngIf='user.title || editUser && isAllowed("manage")'>
|
||||
<input type='text' matInput placeholder='{{"Title" | translate}}' formControlName='title' [value]='user.title'>
|
||||
</mat-form-field>
|
||||
|
||||
<!-- First name -->
|
||||
<mat-form-field class='form30 distance force-min-with' *ngIf='user.firstName || editUser && isAllowed("manage")'>
|
||||
<input type='text' matInput placeholder='{{"First Name" | translate}}' formControlName='first_name'
|
||||
[value]='user.firstName'>
|
||||
</mat-form-field>
|
||||
|
||||
<!-- Last name -->
|
||||
<mat-form-field class='form30 force-min-with' *ngIf='user.lastName || editUser && isAllowed("manage")'>
|
||||
<input type='text' matInput placeholder='{{"Last Name" | translate}}' formControlName='last_name'
|
||||
[value]='user.lastName'>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div *ngIf='isAllowed("seePersonal")'>
|
||||
<!-- E-Mail -->
|
||||
<mat-form-field class='form100' *ngIf="user.email || editUser">
|
||||
<input type='email' matInput placeholder='{{"EMail" | translate}}' name="email" formControlName='email'
|
||||
[value]='user.email'>
|
||||
<mat-error *ngIf="personalInfoForm.get('email').hasError('email')">
|
||||
Please enter a valid email address
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<!-- Strcuture Level -->
|
||||
<mat-form-field class='form70 distance' *ngIf='user.structureLevel || editUser && isAllowed("manage")'>
|
||||
<input type='text' matInput placeholder='{{"Structure Level" | translate}}' formControlName='structure_level'
|
||||
[value]='user.structureLevel'>
|
||||
</mat-form-field>
|
||||
|
||||
<!-- Partizipant Number -->
|
||||
<mat-form-field class='form20 force-min-with' *ngIf='user.participantNumber || editUser && isAllowed("manage")'>
|
||||
<input type='text' matInput placeholder='{{"Participant Number" | translate}}' formControlName='number'
|
||||
[value]='user.participantNumber'>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<!-- Groups -->
|
||||
<mat-form-field class='form100' *ngIf="user.groups && user.groups.length > 0 || editUser">
|
||||
<mat-select placeholder='{{"Groups" | translate}}' formControlName='groups_id' multiple>
|
||||
<mat-option *ngFor="let group of groups" [value]="group.id">{{group}}</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div *ngIf='isAllowed("manage")'>
|
||||
<!-- Initial Password -->
|
||||
<mat-form-field class='form100'>
|
||||
<input matInput placeholder='{{"Initial Password" | translate}}' formControlName='default_password'
|
||||
[value]='user.initialPassword'>
|
||||
<mat-hint align="end">Generate</mat-hint>
|
||||
<button type="button" mat-button matSuffix mat-icon-button [disabled]='!newUser' (click)='generatePassword()'>
|
||||
<fa-icon icon='magic'></fa-icon>
|
||||
</button>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div *ngIf='isAllowed("seePersonal")'>
|
||||
<!-- About me -->
|
||||
<!-- TODO: Needs Rich Text Editor -->
|
||||
<mat-form-field class='form100' *ngIf="user.about || editUser">
|
||||
<textarea formControlName='about_me' matInput placeholder='{{"About Me" | translate}}' [value]='user.about'></textarea>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div *ngIf='isAllowed("seePersonal")'>
|
||||
<!-- username -->
|
||||
<mat-form-field class='form100' *ngIf="user.username || editUser">
|
||||
<input type='text' matInput placeholder='{{"Username" | translate}}' formControlName='username' [value]='user.username'>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div *ngIf='isAllowed("seeExtra")'>
|
||||
<!-- Comment -->
|
||||
<mat-form-field class='form100' *ngIf="user.comment || editUser">
|
||||
<input matInput placeholder='{{"Comment"| translate}}' formControlName='comment' [value]='user.comment'>
|
||||
<mat-hint translate>Only for internal notes.</mat-hint>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div *ngIf='isAllowed("seeExtra")'>
|
||||
<!-- Present? -->
|
||||
<mat-checkbox formControlName='is_present' matTooltip='{{"Designates whether this user is in the room." | translate}} '
|
||||
[value]='user.isPresent'>
|
||||
<span translate>Is Present</span>
|
||||
</mat-checkbox>
|
||||
<!-- Active? -->
|
||||
<mat-checkbox *osPerms="'users.can_see_extra_data'" formControlName='is_active' matTooltip='{{"Designates whether this user should be treated as active. Unselect this instead of deleting the account." | translate}}'
|
||||
[value]='user.isActive'>
|
||||
<span translate>Is Active</span>
|
||||
</mat-checkbox>
|
||||
<!-- Commitee? -->
|
||||
<mat-checkbox formControlName='is_committee' matTooltip='{{"Designates whether this user should be treated as a committee." | translate}}'
|
||||
[value]='user.isCommittee'>
|
||||
<span translate>Is a committee</span>
|
||||
</mat-checkbox>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</mat-card>
|
@ -0,0 +1,67 @@
|
||||
// hide certain stuff whem editing is disabled
|
||||
.mat-form-field-disabled {
|
||||
::ng-deep {
|
||||
.mat-input-element {
|
||||
color: currentColor;
|
||||
}
|
||||
|
||||
.mat-select-value {
|
||||
color: currentColor;
|
||||
}
|
||||
|
||||
.mat-form-field-underline {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mat-hint {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mat-select-value {
|
||||
display: table-cell;
|
||||
}
|
||||
|
||||
button {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// angular material does not have this class. This is virtually set using ngClass
|
||||
.mat-form-field-enabled {
|
||||
.form100 {
|
||||
::ng-deep {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.form70 {
|
||||
::ng-deep {
|
||||
width: 70%;
|
||||
}
|
||||
}
|
||||
|
||||
.force-min-with {
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
.form30 {
|
||||
::ng-deep {
|
||||
width: 30%;
|
||||
}
|
||||
}
|
||||
|
||||
.form20 {
|
||||
::ng-deep {
|
||||
width: 25%;
|
||||
}
|
||||
}
|
||||
|
||||
.distance {
|
||||
padding-right: 5%;
|
||||
}
|
||||
}
|
||||
|
||||
mat-checkbox {
|
||||
margin-right: 10px;
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { UserDetailComponent } from './user-detail.component';
|
||||
|
||||
describe('UserDetailComponent', () => {
|
||||
let component: UserDetailComponent;
|
||||
let fixture: ComponentFixture<UserDetailComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [UserDetailComponent]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(UserDetailComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,336 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
|
||||
|
||||
import { ViewUser } from '../../models/view-user';
|
||||
import { UserRepositoryService } from '../../services/user-repository.service';
|
||||
import { Group } from '../../../../shared/models/users/group';
|
||||
import { DataStoreService } from '../../../../core/services/data-store.service';
|
||||
import { OperatorService } from '../../../../core/services/operator.service';
|
||||
|
||||
/**
|
||||
* Users detail component for both new and existing users
|
||||
*/
|
||||
@Component({
|
||||
selector: 'os-user-detail',
|
||||
templateUrl: './user-detail.component.html',
|
||||
styleUrls: ['./user-detail.component.scss']
|
||||
})
|
||||
export class UserDetailComponent implements OnInit {
|
||||
/**
|
||||
* Info form object
|
||||
*/
|
||||
public personalInfoForm: FormGroup;
|
||||
|
||||
/**
|
||||
* if this is the own page
|
||||
*/
|
||||
public ownPage = false;
|
||||
|
||||
/**
|
||||
* Editing a user
|
||||
*/
|
||||
public editUser = false;
|
||||
|
||||
/**
|
||||
* True if a new user is created
|
||||
*/
|
||||
public newUser = false;
|
||||
|
||||
/**
|
||||
* True if the operator has manage permissions
|
||||
*/
|
||||
public canManage = false;
|
||||
|
||||
/**
|
||||
* ViewUser model
|
||||
*/
|
||||
public user: ViewUser;
|
||||
|
||||
/**
|
||||
* Should contain all Groups, loaded or observed from DataStore
|
||||
*/
|
||||
public groups: Group[];
|
||||
|
||||
/**
|
||||
* Constructor for user
|
||||
*/
|
||||
public constructor(
|
||||
private formBuilder: FormBuilder,
|
||||
private route: ActivatedRoute,
|
||||
private router: Router,
|
||||
private repo: UserRepositoryService,
|
||||
private DS: DataStoreService,
|
||||
private op: OperatorService
|
||||
) {
|
||||
this.user = new ViewUser();
|
||||
if (route.snapshot.url[0].path === 'new') {
|
||||
this.newUser = true;
|
||||
this.setEditMode(true);
|
||||
} else {
|
||||
this.route.params.subscribe(params => {
|
||||
this.loadViewUser(params.id);
|
||||
|
||||
// will fail after reload - observable required
|
||||
this.ownPage = this.opOwnsPage(Number(params.id));
|
||||
|
||||
// observe operator to find out if we see our own page or not
|
||||
this.op.getObservable().subscribe(newOp => {
|
||||
if (newOp) {
|
||||
this.ownPage = this.opOwnsPage(Number(params.id));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
this.createForm();
|
||||
}
|
||||
|
||||
/**
|
||||
* sets the ownPage variable if the operator owns the page
|
||||
*/
|
||||
public opOwnsPage(userId: number): boolean {
|
||||
if (this.op.user && this.op.user.id === userId) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Should determine if the user (Operator) has the
|
||||
* correct permission to perform the given action.
|
||||
*
|
||||
* actions might be:
|
||||
* - seeName (title, 1st, last) (user.can_see_name or ownPage)
|
||||
* - seeExtra (checkboxes, comment) (user.can_see_extra_data)
|
||||
* - seePersonal (mail, username, about) (user.can_see_extra_data or ownPage)
|
||||
* - manage (everything) (user.can_manage)
|
||||
* - changePersonal (mail, username, about) (user.can_manage or ownPage)
|
||||
*
|
||||
* @param action the action the user tries to perform
|
||||
*/
|
||||
public isAllowed(action: string): boolean {
|
||||
switch (action) {
|
||||
case 'manage':
|
||||
return this.op.hasPerms('users.can_manage');
|
||||
case 'seeName':
|
||||
return this.op.hasPerms('users.can_see_name', 'users.can_manage') || this.ownPage;
|
||||
case 'seeExtra':
|
||||
return this.op.hasPerms('users.can_see_extra_data', 'users.can_manage');
|
||||
case 'seePersonal':
|
||||
return this.op.hasPerms('users.can_see_extra_data', 'users.can_manage') || this.ownPage;
|
||||
case 'changePersonal':
|
||||
return this.op.hasPerms('user.cans_manage') || this.ownPage;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a user from users repository
|
||||
* @param id the required ID
|
||||
*/
|
||||
public loadViewUser(id: number): void {
|
||||
this.repo.getViewModelObservable(id).subscribe(newViewUser => {
|
||||
// repo sometimes delivers undefined values
|
||||
// also ensures edition cannot be interrupted by autpupdate
|
||||
if (newViewUser && !this.editUser) {
|
||||
this.user = newViewUser;
|
||||
// personalInfoForm is undefined during 'new' and directly after reloading
|
||||
if (this.personalInfoForm) {
|
||||
this.patchFormValues();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* initialize the form with default values
|
||||
*/
|
||||
public createForm(): void {
|
||||
this.personalInfoForm = this.formBuilder.group({
|
||||
username: [''],
|
||||
title: [''],
|
||||
first_name: [''],
|
||||
last_name: [''],
|
||||
structure_level: [''],
|
||||
number: [''],
|
||||
about_me: [''],
|
||||
groups_id: [''],
|
||||
is_present: [true],
|
||||
is_committee: [false],
|
||||
email: ['', Validators.email],
|
||||
last_email_send: [''],
|
||||
comment: [''],
|
||||
is_active: [true],
|
||||
default_password: ['']
|
||||
});
|
||||
|
||||
// per default disable the whole form:
|
||||
|
||||
this.patchFormValues();
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads values that require external references
|
||||
* And allows async reading
|
||||
*/
|
||||
public patchFormValues(): void {
|
||||
this.personalInfoForm.patchValue({
|
||||
username: this.user.username,
|
||||
groups_id: this.user.groupIds,
|
||||
title: this.user.title,
|
||||
first_name: this.user.firstName,
|
||||
last_name: this.user.lastName
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes the form editable
|
||||
* @param editable
|
||||
*/
|
||||
public makeFormEditable(editable: boolean): void {
|
||||
if (this.personalInfoForm) {
|
||||
const formControlNames = Object.keys(this.personalInfoForm.controls);
|
||||
const allowedFormFields = [];
|
||||
|
||||
if (this.isAllowed('manage')) {
|
||||
// editable content with manage rights
|
||||
allowedFormFields.push(
|
||||
this.personalInfoForm.get('username'),
|
||||
this.personalInfoForm.get('title'),
|
||||
this.personalInfoForm.get('first_name'),
|
||||
this.personalInfoForm.get('last_name'),
|
||||
this.personalInfoForm.get('email'),
|
||||
this.personalInfoForm.get('structure_level'),
|
||||
this.personalInfoForm.get('number'),
|
||||
this.personalInfoForm.get('groups_id'),
|
||||
this.personalInfoForm.get('comment'),
|
||||
this.personalInfoForm.get('is_present'),
|
||||
this.personalInfoForm.get('is_active'),
|
||||
this.personalInfoForm.get('is_committee'),
|
||||
this.personalInfoForm.get('about_me')
|
||||
);
|
||||
} else if (this.isAllowed('changePersonal')) {
|
||||
// changeable personal data
|
||||
// FIXME: Own E-Mail and Password is hidden (server?)
|
||||
allowedFormFields.push(
|
||||
this.personalInfoForm.get('username'),
|
||||
this.personalInfoForm.get('email'),
|
||||
this.personalInfoForm.get('about_me')
|
||||
);
|
||||
}
|
||||
|
||||
// treatment for the initial password field
|
||||
if (!editable || this.newUser) {
|
||||
allowedFormFields.push(this.personalInfoForm.get('default_password'));
|
||||
}
|
||||
|
||||
if (editable) {
|
||||
allowedFormFields.forEach(formElement => {
|
||||
formElement.enable();
|
||||
});
|
||||
} else {
|
||||
formControlNames.forEach(formControlName => {
|
||||
this.personalInfoForm.get(formControlName).disable();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for the generate Password button.
|
||||
* Generates a password using 8 pseudo-random letters
|
||||
* from the `characters` const.
|
||||
*
|
||||
* Removed the letter 'O' from the alphabet cause it's easy to confuse
|
||||
* with the number '0'.
|
||||
*/
|
||||
public generatePassword(): void {
|
||||
let pw = '';
|
||||
const characters = 'ABCDEFGHIJKLMNPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
const amount = 8;
|
||||
for (let i = 0; i < amount; i++) {
|
||||
pw += characters.charAt(Math.floor(Math.random() * characters.length));
|
||||
}
|
||||
this.personalInfoForm.patchValue({
|
||||
default_password: pw
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Save / Submit a user
|
||||
*/
|
||||
public saveUser(): void {
|
||||
if (this.newUser) {
|
||||
this.repo.create(this.personalInfoForm.value).subscribe(
|
||||
response => {
|
||||
this.newUser = false;
|
||||
this.router.navigate([`./users/${response.id}`]);
|
||||
// this.setEditMode(false);
|
||||
// this.loadViewUser(response.id);
|
||||
},
|
||||
error => console.error('Creation of the user failed: ', error.error)
|
||||
);
|
||||
} else {
|
||||
this.repo.update(this.personalInfoForm.value, this.user).subscribe(
|
||||
response => {
|
||||
this.setEditMode(false);
|
||||
this.loadViewUser(response.id);
|
||||
},
|
||||
error => console.error('Update of the user failed: ', error.error)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* sets editUser variable and editable form
|
||||
* @param edit
|
||||
*/
|
||||
public setEditMode(edit: boolean): void {
|
||||
this.editUser = edit;
|
||||
this.makeFormEditable(edit);
|
||||
}
|
||||
|
||||
/**
|
||||
* click on the edit button
|
||||
*/
|
||||
public editUserButton(): void {
|
||||
if (this.editUser) {
|
||||
this.saveUser();
|
||||
} else {
|
||||
this.setEditMode(true);
|
||||
}
|
||||
}
|
||||
|
||||
public cancelEditMotionButton(): void {
|
||||
if (this.newUser) {
|
||||
this.router.navigate(['./users/']);
|
||||
} else {
|
||||
this.setEditMode(false);
|
||||
this.loadViewUser(this.user.id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* click on the delete user button
|
||||
*/
|
||||
public deleteUserButton(): void {
|
||||
this.repo.delete(this.user).subscribe(response => {
|
||||
this.router.navigate(['./users/']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Init function.
|
||||
*/
|
||||
public ngOnInit(): void {
|
||||
this.makeFormEditable(this.editUser);
|
||||
this.groups = this.DS.getAll(Group);
|
||||
this.DS.changeObservable.subscribe(model => {
|
||||
if (model instanceof Group) {
|
||||
this.groups.push(model as Group);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -2,6 +2,15 @@
|
||||
(ellipsisMenuItem)=onEllipsisItem($event)>
|
||||
</os-head-bar>
|
||||
|
||||
<div class='custom-table-header on-transition-fade'>
|
||||
<button mat-button>
|
||||
<span translate>SORT</span>
|
||||
</button>
|
||||
<button mat-button>
|
||||
<span translate>FILTER</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<mat-table class='os-listview-table on-transition-fade' [dataSource]="dataSource" matSort>
|
||||
<!-- name column -->
|
||||
<ng-container matColumnDef="name">
|
||||
@ -12,12 +21,30 @@
|
||||
<!-- prefix column -->
|
||||
<ng-container matColumnDef="group">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header> Group </mat-header-cell>
|
||||
<mat-cell *matCellDef="let user"> {{user.groups}} {{user.structureLevel}} </mat-cell>
|
||||
<mat-cell *matCellDef="let user">
|
||||
<div class='groupsCell'>
|
||||
<span *ngIf="user.groups.length > 0">
|
||||
<fa-icon icon="users"></fa-icon>
|
||||
{{user.groups}}
|
||||
</span>
|
||||
<br *ngIf="user.groups && user.structureLevel">
|
||||
<span *ngIf="user.structureLevel">
|
||||
<fa-icon icon="flag"></fa-icon>
|
||||
{{user.structureLevel}}
|
||||
</span>
|
||||
</div>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<!-- Presence column -->
|
||||
<ng-container matColumnDef="presence">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header> Presence </mat-header-cell>
|
||||
<mat-cell *matCellDef="let user"> {{user.isActive}} </mat-cell>
|
||||
<mat-cell *matCellDef="let user">
|
||||
<div *ngIf="user.isActive">
|
||||
<fa-icon icon="check-square"></fa-icon>
|
||||
<span translate>Present</span>
|
||||
</div>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<mat-header-row *matHeaderRowDef="['name', 'group', 'presence']"></mat-header-row>
|
@ -0,0 +1,32 @@
|
||||
.groupsCell {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
line-height: normal;
|
||||
|
||||
fa-icon {
|
||||
font-size: 80%;
|
||||
}
|
||||
}
|
||||
|
||||
.os-listview-table {
|
||||
.mat-column-name {
|
||||
flex: 1 0 200px;
|
||||
}
|
||||
|
||||
.mat-column-group {
|
||||
flex: 2 0 60px;
|
||||
}
|
||||
|
||||
.mat-column-presence {
|
||||
flex: 0 0 60px;
|
||||
|
||||
fa-icon {
|
||||
font-size: 100%;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
div {
|
||||
display: inherit;
|
||||
}
|
||||
}
|
||||
}
|
@ -2,9 +2,10 @@ import { Component, OnInit } from '@angular/core';
|
||||
import { Title } from '@angular/platform-browser';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
import { ViewUser } from '../models/view-user';
|
||||
import { UserRepositoryService } from '../services/user-repository.service';
|
||||
import { ListViewBaseComponent } from '../../base/list-view-base';
|
||||
import { ViewUser } from '../../models/view-user';
|
||||
import { UserRepositoryService } from '../../services/user-repository.service';
|
||||
import { ListViewBaseComponent } from '../../../base/list-view-base';
|
||||
import { Router, ActivatedRoute } from '@angular/router';
|
||||
|
||||
/**
|
||||
* Component for the user list view.
|
||||
@ -13,7 +14,7 @@ import { ListViewBaseComponent } from '../../base/list-view-base';
|
||||
@Component({
|
||||
selector: 'os-user-list',
|
||||
templateUrl: './user-list.component.html',
|
||||
styleUrls: ['./user-list.component.css']
|
||||
styleUrls: ['./user-list.component.scss']
|
||||
})
|
||||
export class UserListComponent extends ListViewBaseComponent<ViewUser> implements OnInit {
|
||||
/**
|
||||
@ -46,7 +47,9 @@ export class UserListComponent extends ListViewBaseComponent<ViewUser> implement
|
||||
public constructor(
|
||||
private repo: UserRepositoryService,
|
||||
protected titleService: Title,
|
||||
protected translate: TranslateService
|
||||
protected translate: TranslateService,
|
||||
private router: Router,
|
||||
private route: ActivatedRoute
|
||||
) {
|
||||
super(titleService, translate);
|
||||
}
|
||||
@ -86,13 +89,13 @@ export class UserListComponent extends ListViewBaseComponent<ViewUser> implement
|
||||
* @param row selected row
|
||||
*/
|
||||
public selectUser(row: ViewUser): void {
|
||||
console.log('clicked the row for user: ', row);
|
||||
this.router.navigate([`./${row.id}`], { relativeTo: this.route });
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the click on the plus button
|
||||
*/
|
||||
public onPlusButton(): void {
|
||||
console.log('new User');
|
||||
this.router.navigate(['./new'], { relativeTo: this.route });
|
||||
}
|
||||
}
|
@ -7,39 +7,96 @@ export class ViewUser extends BaseViewModel {
|
||||
private _user: User;
|
||||
private _groups: Group[];
|
||||
|
||||
public get id(): number {
|
||||
return this._user ? this._user.id : null;
|
||||
}
|
||||
|
||||
public get user(): User {
|
||||
return this._user;
|
||||
return this._user ? this._user : null;
|
||||
}
|
||||
|
||||
public get groups(): Group[] {
|
||||
return this._groups;
|
||||
}
|
||||
|
||||
public get id(): number {
|
||||
return this.user ? this.user.id : null;
|
||||
}
|
||||
|
||||
public get username(): string {
|
||||
return this.user ? this.user.username : null;
|
||||
}
|
||||
|
||||
public get title(): string {
|
||||
return this.user ? this.user.title : null;
|
||||
}
|
||||
|
||||
public get firstName(): string {
|
||||
return this.user ? this.user.first_name : null;
|
||||
}
|
||||
|
||||
public get lastName(): string {
|
||||
return this.user ? this.user.last_name : null;
|
||||
}
|
||||
|
||||
public get fullName(): string {
|
||||
return this.user ? this.user.full_name : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: Make boolean, use function over view component.
|
||||
*/
|
||||
public get isActive(): string {
|
||||
return this.user && this.user.is_active ? 'active' : 'inactive';
|
||||
public get email(): string {
|
||||
return this.user ? this.user.email : null;
|
||||
}
|
||||
|
||||
public get structureLevel(): string {
|
||||
return this.user ? this.user.structure_level : null;
|
||||
}
|
||||
|
||||
public get participantNumber(): string {
|
||||
return this.user ? this.user.number : null;
|
||||
}
|
||||
|
||||
public get groupIds(): number[] {
|
||||
return this.user ? this.user.groups_id : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Required by the input selector
|
||||
*/
|
||||
public set groupIds(ids: number[]) {
|
||||
if (this.user) {
|
||||
this.user.groups_id = ids;
|
||||
}
|
||||
}
|
||||
|
||||
public get initialPassword(): string {
|
||||
return this.user ? this.user.default_password : null;
|
||||
}
|
||||
|
||||
public get comment(): string {
|
||||
return this.user ? this.user.comment : null;
|
||||
}
|
||||
|
||||
public get isPresent(): boolean {
|
||||
return this.user ? this.user.is_present : null;
|
||||
}
|
||||
|
||||
public get isActive(): boolean {
|
||||
return this.user ? this.user.is_active : null;
|
||||
}
|
||||
|
||||
public get isCommittee(): boolean {
|
||||
return this.user ? this.user.is_committee : null;
|
||||
}
|
||||
|
||||
public get about(): string {
|
||||
return this.user ? this.user.about_me : null;
|
||||
}
|
||||
|
||||
public constructor(user?: User, groups?: Group[]) {
|
||||
super();
|
||||
this._user = user;
|
||||
this._groups = groups;
|
||||
}
|
||||
|
||||
/**
|
||||
* required by BaseViewModel. Don't confuse with the users title.
|
||||
*/
|
||||
public getTitle(): string {
|
||||
return this.user ? this.user.toString() : null;
|
||||
}
|
||||
@ -47,9 +104,7 @@ export class ViewUser extends BaseViewModel {
|
||||
/**
|
||||
* TODO: Implement
|
||||
*/
|
||||
public replaceGroup(newGroup: Group): void {
|
||||
console.log('replace group - not yet implemented, ', newGroup);
|
||||
}
|
||||
public replaceGroup(newGroup: Group): void {}
|
||||
|
||||
public updateValues(update: BaseModel): void {
|
||||
if (update instanceof Group) {
|
||||
|
@ -6,6 +6,7 @@ import { User } from '../../../shared/models/users/user';
|
||||
import { Group } from '../../../shared/models/users/group';
|
||||
import { Observable } from 'rxjs';
|
||||
import { DataStoreService } from '../../../core/services/data-store.service';
|
||||
import { DataSendService } from '../../../core/services/data-send.service';
|
||||
|
||||
/**
|
||||
* Repository service for users
|
||||
@ -19,17 +20,29 @@ export class UserRepositoryService extends BaseRepository<ViewUser, User> {
|
||||
/**
|
||||
* Constructor calls the parent constructor
|
||||
*/
|
||||
public constructor(DS: DataStoreService) {
|
||||
public constructor(DS: DataStoreService, private dataSend: DataSendService) {
|
||||
super(DS, User, [Group]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
* Updates a the selected user with the form values.
|
||||
*
|
||||
* TODO: used over not-yet-existing detail view
|
||||
* @param update the forms values
|
||||
* @param viewUser
|
||||
*/
|
||||
public update(user: Partial<User>, viewUser: ViewUser): Observable<User> {
|
||||
return null;
|
||||
public update(update: Partial<User>, viewUser: ViewUser): Observable<any> {
|
||||
const updateUser = new User();
|
||||
// copy the ViewUser to avoid manipulation of parameters
|
||||
updateUser.patchValues(viewUser.user);
|
||||
updateUser.patchValues(update);
|
||||
|
||||
// if the user deletes the username, reset
|
||||
// prevents the server of generating '<firstname> <lastname> +1' as username
|
||||
if (updateUser.username === '') {
|
||||
updateUser.username = viewUser.username;
|
||||
}
|
||||
|
||||
return this.dataSend.updateModel(updateUser, 'put');
|
||||
}
|
||||
|
||||
/**
|
||||
@ -37,17 +50,38 @@ export class UserRepositoryService extends BaseRepository<ViewUser, User> {
|
||||
*
|
||||
* TODO: used over not-yet-existing detail view
|
||||
*/
|
||||
public delete(user: ViewUser): Observable<User> {
|
||||
return null;
|
||||
public delete(viewUser: ViewUser): Observable<any> {
|
||||
return this.dataSend.delete(viewUser.user);
|
||||
}
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
* creates and saves a new user
|
||||
*
|
||||
* TODO: used over not-yet-existing detail view
|
||||
* @param userData blank form value. Usually not yet a real user
|
||||
*/
|
||||
public create(user: User): Observable<User> {
|
||||
return null;
|
||||
public create(userData: Partial<User>): Observable<any> {
|
||||
const newUser = new User();
|
||||
// collectionString of userData is still empty
|
||||
newUser.patchValues(userData);
|
||||
|
||||
// if the username is not presend, delete.
|
||||
// The server will generate a one
|
||||
if (!newUser.username) {
|
||||
delete newUser.username;
|
||||
}
|
||||
|
||||
// title must not be "null" during creation
|
||||
if (!newUser.title) {
|
||||
delete newUser.title;
|
||||
}
|
||||
|
||||
// null values will not be accepted for group_id
|
||||
if (!newUser.groups_id) {
|
||||
delete newUser.groups_id;
|
||||
}
|
||||
|
||||
return this.dataSend.createModel(newUser);
|
||||
}
|
||||
|
||||
public createViewModel(user: User): ViewUser {
|
||||
|
@ -1,8 +1,22 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { Routes, RouterModule } from '@angular/router';
|
||||
import { UserListComponent } from './user-list/user-list.component';
|
||||
import { UserListComponent } from './components/user-list/user-list.component';
|
||||
import { UserDetailComponent } from './components/user-detail/user-detail.component';
|
||||
|
||||
const routes: Routes = [{ path: '', component: UserListComponent }];
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: UserListComponent
|
||||
},
|
||||
{
|
||||
path: 'new',
|
||||
component: UserDetailComponent
|
||||
},
|
||||
{
|
||||
path: ':id',
|
||||
component: UserDetailComponent
|
||||
}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
|
@ -3,10 +3,11 @@ import { CommonModule } from '@angular/common';
|
||||
|
||||
import { UsersRoutingModule } from './users-routing.module';
|
||||
import { SharedModule } from '../../shared/shared.module';
|
||||
import { UserListComponent } from './user-list/user-list.component';
|
||||
import { UserListComponent } from './components/user-list/user-list.component';
|
||||
import { UserDetailComponent } from './components/user-detail/user-detail.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule, UsersRoutingModule, SharedModule],
|
||||
declarations: [UserListComponent]
|
||||
declarations: [UserListComponent, UserDetailComponent]
|
||||
})
|
||||
export class UsersModule {}
|
||||
|
@ -1,12 +1,29 @@
|
||||
{
|
||||
"Abort": "",
|
||||
"About Me": "",
|
||||
"Agenda": "Tagesordnung",
|
||||
"Assignments": "Wahlen",
|
||||
"Category": "",
|
||||
"Change Password": "Passwort ändern",
|
||||
"Comment": "",
|
||||
"Content": "",
|
||||
"Copyright by": "Copyright by",
|
||||
"Delete User": "",
|
||||
"DeleteMotion": "",
|
||||
"Designates whether this user is in the room": {
|
||||
"0": ""
|
||||
},
|
||||
"Designates whether this user should be treated as a committee": {
|
||||
"0": ""
|
||||
},
|
||||
"Designates whether this user should be treated as active": {
|
||||
" Unselect this instead of deleting the account": {
|
||||
"0": ""
|
||||
}
|
||||
},
|
||||
"EMail": "",
|
||||
"Edit Profile": "Profil bearbeiten",
|
||||
"Edit category details:": "",
|
||||
"English": "Englisch",
|
||||
"Export As": {
|
||||
"0": {
|
||||
@ -17,11 +34,18 @@
|
||||
},
|
||||
"FILTER": "",
|
||||
"Files": "Dateien",
|
||||
"First Name": "",
|
||||
"French": "Französisch",
|
||||
"German": "Deutsch",
|
||||
"Groups": "",
|
||||
"Home": "Startseite",
|
||||
"Identifier": "",
|
||||
"Initial Password": "",
|
||||
"Installed plugins": "",
|
||||
"Is Active": "",
|
||||
"Is Present": "",
|
||||
"Is a committee": "",
|
||||
"Last Name": "",
|
||||
"Legal Notice": "Impressum",
|
||||
"License": "",
|
||||
"Login": "",
|
||||
@ -30,30 +54,42 @@
|
||||
"Meta information": "",
|
||||
"Motion": "",
|
||||
"Motions": "Anträge",
|
||||
"Name": "",
|
||||
"None": "",
|
||||
"OK": "",
|
||||
"Offline mode: You can use OpenSlides but changes are not saved": {
|
||||
"0": ""
|
||||
},
|
||||
"Only for internal notes": {
|
||||
"0": ""
|
||||
},
|
||||
"Origin": "",
|
||||
"Participant Number": "",
|
||||
"Participants": "Teilnehmer",
|
||||
"Personal Note": "",
|
||||
"Personal note": "",
|
||||
"Prefix": "",
|
||||
"Present": "",
|
||||
"Privacy Policy": "Datenschutz",
|
||||
"Project": "",
|
||||
"Projector": "",
|
||||
"Reason": "",
|
||||
"Required": "",
|
||||
"Reset State": "",
|
||||
"Reset recommendation": "",
|
||||
"SORT": "",
|
||||
"Selected Values": "",
|
||||
"Settings": "Einstellungen",
|
||||
"State": "",
|
||||
"Structure Level": "",
|
||||
"Submitters": "",
|
||||
"Supporters": "",
|
||||
"The assembly may decide:": "",
|
||||
"The event manager hasn't set up a privacy policy yet": {
|
||||
"0": ""
|
||||
},
|
||||
"Title": "",
|
||||
"Username": "",
|
||||
"Welcome to OpenSlides": "Willkommen bei OpenSlides",
|
||||
"by": ""
|
||||
}
|
||||
|
@ -1,12 +1,29 @@
|
||||
{
|
||||
"Abort": "",
|
||||
"About Me": "",
|
||||
"Agenda": "",
|
||||
"Assignments": "",
|
||||
"Category": "",
|
||||
"Change Password": "",
|
||||
"Comment": "",
|
||||
"Content": "",
|
||||
"Copyright by": "",
|
||||
"Delete User": "",
|
||||
"DeleteMotion": "",
|
||||
"Designates whether this user is in the room": {
|
||||
"0": ""
|
||||
},
|
||||
"Designates whether this user should be treated as a committee": {
|
||||
"0": ""
|
||||
},
|
||||
"Designates whether this user should be treated as active": {
|
||||
" Unselect this instead of deleting the account": {
|
||||
"0": ""
|
||||
}
|
||||
},
|
||||
"EMail": "",
|
||||
"Edit Profile": "",
|
||||
"Edit category details:": "",
|
||||
"English": "",
|
||||
"Export As": {
|
||||
"0": {
|
||||
@ -17,11 +34,18 @@
|
||||
},
|
||||
"FILTER": "",
|
||||
"Files": "",
|
||||
"First Name": "",
|
||||
"French": "",
|
||||
"German": "",
|
||||
"Groups": "",
|
||||
"Home": "",
|
||||
"Identifier": "",
|
||||
"Initial Password": "",
|
||||
"Installed plugins": "",
|
||||
"Is Active": "",
|
||||
"Is Present": "",
|
||||
"Is a committee": "",
|
||||
"Last Name": "",
|
||||
"Legal Notice": "",
|
||||
"License": "",
|
||||
"Login": "",
|
||||
@ -30,30 +54,42 @@
|
||||
"Meta information": "",
|
||||
"Motion": "",
|
||||
"Motions": "",
|
||||
"Name": "",
|
||||
"None": "",
|
||||
"OK": "",
|
||||
"Offline mode: You can use OpenSlides but changes are not saved": {
|
||||
"0": ""
|
||||
},
|
||||
"Only for internal notes": {
|
||||
"0": ""
|
||||
},
|
||||
"Origin": "",
|
||||
"Participant Number": "",
|
||||
"Participants": "",
|
||||
"Personal Note": "",
|
||||
"Personal note": "",
|
||||
"Prefix": "",
|
||||
"Present": "",
|
||||
"Privacy Policy": "",
|
||||
"Project": "",
|
||||
"Projector": "",
|
||||
"Reason": "",
|
||||
"Required": "",
|
||||
"Reset State": "",
|
||||
"Reset recommendation": "",
|
||||
"SORT": "",
|
||||
"Selected Values": "",
|
||||
"Settings": "",
|
||||
"State": "",
|
||||
"Structure Level": "",
|
||||
"Submitters": "",
|
||||
"Supporters": "",
|
||||
"The assembly may decide:": "",
|
||||
"The event manager hasn't set up a privacy policy yet": {
|
||||
"0": ""
|
||||
},
|
||||
"Title": "",
|
||||
"Username": "",
|
||||
"Welcome to OpenSlides": "",
|
||||
"by": ""
|
||||
}
|
||||
|
@ -1,12 +1,29 @@
|
||||
{
|
||||
"Abort": "",
|
||||
"About Me": "",
|
||||
"Agenda": "",
|
||||
"Assignments": "",
|
||||
"Category": "",
|
||||
"Change Password": "",
|
||||
"Comment": "",
|
||||
"Content": "",
|
||||
"Copyright by": "",
|
||||
"Delete User": "",
|
||||
"DeleteMotion": "",
|
||||
"Designates whether this user is in the room": {
|
||||
"0": ""
|
||||
},
|
||||
"Designates whether this user should be treated as a committee": {
|
||||
"0": ""
|
||||
},
|
||||
"Designates whether this user should be treated as active": {
|
||||
" Unselect this instead of deleting the account": {
|
||||
"0": ""
|
||||
}
|
||||
},
|
||||
"EMail": "",
|
||||
"Edit Profile": "",
|
||||
"Edit category details:": "",
|
||||
"English": "",
|
||||
"Export As": {
|
||||
"0": {
|
||||
@ -17,11 +34,18 @@
|
||||
},
|
||||
"FILTER": "",
|
||||
"Files": "",
|
||||
"First Name": "",
|
||||
"French": "",
|
||||
"German": "",
|
||||
"Groups": "",
|
||||
"Home": "",
|
||||
"Identifier": "",
|
||||
"Initial Password": "",
|
||||
"Installed plugins": "",
|
||||
"Is Active": "",
|
||||
"Is Present": "",
|
||||
"Is a committee": "",
|
||||
"Last Name": "",
|
||||
"Legal Notice": "",
|
||||
"License": "",
|
||||
"Login": "",
|
||||
@ -30,30 +54,42 @@
|
||||
"Meta information": "",
|
||||
"Motion": "",
|
||||
"Motions": "",
|
||||
"Name": "",
|
||||
"None": "",
|
||||
"OK": "",
|
||||
"Offline mode: You can use OpenSlides but changes are not saved": {
|
||||
"0": ""
|
||||
},
|
||||
"Only for internal notes": {
|
||||
"0": ""
|
||||
},
|
||||
"Origin": "",
|
||||
"Participant Number": "",
|
||||
"Participants": "",
|
||||
"Personal Note": "",
|
||||
"Personal note": "",
|
||||
"Prefix": "",
|
||||
"Present": "",
|
||||
"Privacy Policy": "",
|
||||
"Project": "",
|
||||
"Projector": "",
|
||||
"Reason": "",
|
||||
"Required": "",
|
||||
"Reset State": "",
|
||||
"Reset recommendation": "",
|
||||
"SORT": "",
|
||||
"Selected Values": "",
|
||||
"Settings": "",
|
||||
"State": "",
|
||||
"Structure Level": "",
|
||||
"Submitters": "",
|
||||
"Supporters": "",
|
||||
"The assembly may decide:": "",
|
||||
"The event manager hasn't set up a privacy policy yet": {
|
||||
"0": ""
|
||||
},
|
||||
"Title": "",
|
||||
"Username": "",
|
||||
"Welcome to OpenSlides": "",
|
||||
"by": ""
|
||||
}
|
||||
|
@ -26,6 +26,19 @@ body {
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.save-button {
|
||||
// needs to be important or will be overwritten locally
|
||||
background-color: rgb(77, 243, 86) !important;
|
||||
}
|
||||
|
||||
.red-warning-text {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.icon-text-distance {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.os-card {
|
||||
max-width: 90%;
|
||||
margin-top: 10px;
|
||||
@ -33,6 +46,16 @@ body {
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
//custom table header for search button, filtering and more. Used in ListViews
|
||||
.custom-table-header {
|
||||
width: 100%;
|
||||
height: 60px;
|
||||
line-height: 60px;
|
||||
text-align: right;
|
||||
background: white;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.os-listview-table {
|
||||
width: 100%;
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user