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:
Sean 2018-09-28 12:42:30 +02:00 committed by GitHub
commit b6ad0d759c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 1041 additions and 93 deletions

View File

@ -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;
};
}
}

View File

@ -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)
)
);
}
/**

View File

@ -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);
});
}

View File

@ -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);

View File

@ -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);
}

View File

@ -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,

View File

@ -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>

View File

@ -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%;

View File

@ -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;

View File

@ -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> {

View File

@ -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');
}
/**

View File

@ -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 {}

View File

@ -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>

View File

@ -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;
}

View File

@ -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();
});
});

View File

@ -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);
}
});
}
}

View File

@ -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>

View File

@ -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;
}
}
}

View File

@ -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 });
}
}

View File

@ -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) {

View File

@ -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 {

View File

@ -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)],

View File

@ -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 {}

View File

@ -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": ""
}

View File

@ -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": ""
}

View File

@ -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": ""
}

View File

@ -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%;