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 { 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 autoupdateService
* @param notifyService * @param notifyService
* @param translate * @param translate
@ -35,5 +40,36 @@ export class AppComponent {
const browserLang = translate.getBrowserLang(); const browserLang = translate.getBrowserLang();
// try to use the browser language if it is available. If not, uses english. // try to use the browser language if it is available. If not, uses english.
translate.use(translate.getLangs().includes(browserLang) ? browserLang : 'en'); 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) {} public constructor(private http: HttpClient) {}
/** /**
* Save motion in the server * Sends a post request with the model to the server.
* * Usually for new Models
* @return Observable from
*/ */
public saveModel(model: BaseModel): Observable<BaseModel> { public createModel(model: BaseModel): Observable<BaseModel> {
if (!model.id) {
return this.http.post<BaseModel>('rest/' + model.collectionString + '/', model).pipe( return this.http.post<BaseModel>('rest/' + model.collectionString + '/', model).pipe(
tap( tap(
response => { response => {
// TODO: Message, Notify, Etc // TODO: Message, Notify, Etc
console.log('New Model added. Response : ', response); console.log('New Model added. Response :\n', response);
}, },
error => console.log('error. ', error) error => console.error('createModel has returned an Error:\n', 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)
) )
); );
} }
/**
* 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. * 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 { public hasPerms(...checkPerms: Permission[]): boolean {
return permissions.some(permisison => { return checkPerms.some(permission => {
return this.permissions.includes(permisison); return this.permissions.includes(permission);
}); });
} }

View File

@ -25,6 +25,20 @@ export class PermsDirective extends OpenSlidesComponent {
*/ */
private lastPermissionCheckResult = false; 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 * Constructs the directive once. Observes the operator for it's groups so the
* directive can perform changes dynamically * directive can perform changes dynamically
@ -61,6 +75,16 @@ export class PermsDirective extends OpenSlidesComponent {
this.updateView(); 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. * Shows or hides certain content in the view.
*/ */
@ -68,7 +92,7 @@ export class PermsDirective extends OpenSlidesComponent {
const hasPerms = this.checkPermissions(); const hasPerms = this.checkPermissions();
const permsChanged = hasPerms !== this.lastPermissionCheckResult; const permsChanged = hasPerms !== this.lastPermissionCheckResult;
if (hasPerms && permsChanged) { if ((hasPerms && permsChanged) || this.alternative) {
// clean up and add the template // clean up and add the template
this.viewContainer.clear(); this.viewContainer.clear();
this.viewContainer.createEmbeddedView(this.template); 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 { public patchValues(update: Partial<T>): void {
Object.assign(this, update); Object.assign(this, update);
} }

View File

@ -15,7 +15,8 @@ import {
MatSnackBarModule, MatSnackBarModule,
MatTableModule, MatTableModule,
MatPaginatorModule, MatPaginatorModule,
MatSortModule MatSortModule,
MatTooltipModule
} from '@angular/material'; } from '@angular/material';
import { MatAutocompleteModule } from '@angular/material/autocomplete'; import { MatAutocompleteModule } from '@angular/material/autocomplete';
import { MatChipsModule } from '@angular/material'; import { MatChipsModule } from '@angular/material';
@ -79,6 +80,7 @@ library.add(fas);
MatDialogModule, MatDialogModule,
MatSnackBarModule, MatSnackBarModule,
MatChipsModule, MatChipsModule,
MatTooltipModule,
FontAwesomeModule, FontAwesomeModule,
TranslateModule.forChild(), TranslateModule.forChild(),
RouterModule, RouterModule,
@ -106,6 +108,7 @@ library.add(fas);
MatDialogModule, MatDialogModule,
MatSnackBarModule, MatSnackBarModule,
MatChipsModule, MatChipsModule,
MatTooltipModule,
NgxMatSelectSearchModule, NgxMatSelectSearchModule,
FontAwesomeModule, FontAwesomeModule,
TranslateModule, TranslateModule,

View File

@ -23,8 +23,9 @@
<!-- Button on the right--> <!-- Button on the right-->
<div *ngIf="editMotion"> <div *ngIf="editMotion">
<button (click)='cancelEditMotionButton()' class='on-transition-fade' mat-icon-button> <button (click)='cancelEditMotionButton()' class='on-transition-fade' color="warn" mat-raised-button>
<fa-icon icon='times'></fa-icon> <span translate>Abort</span>
<fa-icon class="icon-text-distance" icon='times'></fa-icon>
</button> </button>
</div> </div>
<div *ngIf="!editMotion"> <div *ngIf="!editMotion">
@ -39,7 +40,7 @@
<button mat-menu-item translate>Export As...</button> <button mat-menu-item translate>Export As...</button>
<button mat-menu-item translate>Project</button> <button mat-menu-item translate>Project</button>
<mat-divider></mat-divider> <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-menu>
</mat-toolbar> </mat-toolbar>

View File

@ -2,14 +2,6 @@ span {
margin: 0; margin: 0;
} }
.save-button {
background-color: rgb(77, 243, 86);
}
.deleteMotionButton {
color: red;
}
.motion-title { .motion-title {
padding-left: 20px; padding-left: 20px;
line-height: 100%; 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 */ /** css hacks https://codepen.io/edge0703/pen/iHJuA */
.innerTable { .innerTable {
display: inline-block; display: inline-block;

View File

@ -40,7 +40,7 @@ export class CategoryRepositoryService extends BaseRepository<ViewCategory, Cate
if (this.osInDataStore(viewCategory)) { if (this.osInDataStore(viewCategory)) {
return this.update(update, viewCategory); return this.update(update, viewCategory);
} else { } 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 = new Category();
} }
updateCategory.patchValues(update); updateCategory.patchValues(update);
return this.dataSend.saveModel(updateCategory); return this.dataSend.updateModel(updateCategory, 'put');
} }
public delete(viewCategory: ViewCategory): Observable<any> { 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() * TODO: Remove the viewMotion and make it actually distignuishable from save()
*/ */
public create(motion: Motion): Observable<any> { 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> { public update(update: Partial<Motion>, viewMotion: ViewMotion): Observable<any> {
const motion = viewMotion.motion; const motion = viewMotion.motion;
motion.patchValues(update); 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 { Component, OnInit, ViewChild } from '@angular/core';
import { Router } from '@angular/router';
import { AuthService } from 'app/core/services/auth.service'; import { AuthService } from 'app/core/services/auth.service';
import { OperatorService } from 'app/core/services/operator.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 * 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) * Get the username from the operator (should be known already)
@ -37,6 +39,7 @@ export class SiteComponent extends BaseComponent implements OnInit {
* Constructor * Constructor
* *
* @param authService * @param authService
* @param router
* @param operator * @param operator
* @param vp * @param vp
* @param translate * @param translate
@ -44,6 +47,7 @@ export class SiteComponent extends BaseComponent implements OnInit {
*/ */
public constructor( public constructor(
private authService: AuthService, private authService: AuthService,
private router: Router,
public operator: OperatorService, public operator: OperatorService,
public vp: ViewportService, public vp: ViewportService,
public translate: TranslateService, public translate: TranslateService,
@ -109,7 +113,11 @@ export class SiteComponent extends BaseComponent implements OnInit {
} }
// TODO: Implement this // TODO: Implement this
public editProfile(): void {} public editProfile(): void {
if (this.operator.user) {
this.router.navigate([`./users/${this.operator.user.id}`]);
}
}
// TODO: Implement this // TODO: Implement this
public changePassword(): void {} 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)> (ellipsisMenuItem)=onEllipsisItem($event)>
</os-head-bar> </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> <mat-table class='os-listview-table on-transition-fade' [dataSource]="dataSource" matSort>
<!-- name column --> <!-- name column -->
<ng-container matColumnDef="name"> <ng-container matColumnDef="name">
@ -12,12 +21,30 @@
<!-- prefix column --> <!-- prefix column -->
<ng-container matColumnDef="group"> <ng-container matColumnDef="group">
<mat-header-cell *matHeaderCellDef mat-sort-header> Group </mat-header-cell> <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> </ng-container>
<!-- Presence column -->
<ng-container matColumnDef="presence"> <ng-container matColumnDef="presence">
<mat-header-cell *matHeaderCellDef mat-sort-header> Presence </mat-header-cell> <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> </ng-container>
<mat-header-row *matHeaderRowDef="['name', 'group', 'presence']"></mat-header-row> <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 { Title } from '@angular/platform-browser';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { ViewUser } from '../models/view-user'; import { ViewUser } from '../../models/view-user';
import { UserRepositoryService } from '../services/user-repository.service'; import { UserRepositoryService } from '../../services/user-repository.service';
import { ListViewBaseComponent } from '../../base/list-view-base'; import { ListViewBaseComponent } from '../../../base/list-view-base';
import { Router, ActivatedRoute } from '@angular/router';
/** /**
* Component for the user list view. * Component for the user list view.
@ -13,7 +14,7 @@ import { ListViewBaseComponent } from '../../base/list-view-base';
@Component({ @Component({
selector: 'os-user-list', selector: 'os-user-list',
templateUrl: './user-list.component.html', templateUrl: './user-list.component.html',
styleUrls: ['./user-list.component.css'] styleUrls: ['./user-list.component.scss']
}) })
export class UserListComponent extends ListViewBaseComponent<ViewUser> implements OnInit { export class UserListComponent extends ListViewBaseComponent<ViewUser> implements OnInit {
/** /**
@ -46,7 +47,9 @@ export class UserListComponent extends ListViewBaseComponent<ViewUser> implement
public constructor( public constructor(
private repo: UserRepositoryService, private repo: UserRepositoryService,
protected titleService: Title, protected titleService: Title,
protected translate: TranslateService protected translate: TranslateService,
private router: Router,
private route: ActivatedRoute
) { ) {
super(titleService, translate); super(titleService, translate);
} }
@ -86,13 +89,13 @@ export class UserListComponent extends ListViewBaseComponent<ViewUser> implement
* @param row selected row * @param row selected row
*/ */
public selectUser(row: ViewUser): void { 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 * Handles the click on the plus button
*/ */
public onPlusButton(): void { 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 _user: User;
private _groups: Group[]; private _groups: Group[];
public get id(): number {
return this._user ? this._user.id : null;
}
public get user(): User { public get user(): User {
return this._user; return this._user ? this._user : null;
} }
public get groups(): Group[] { public get groups(): Group[] {
return this._groups; 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 { public get fullName(): string {
return this.user ? this.user.full_name : null; return this.user ? this.user.full_name : null;
} }
/** public get email(): string {
* TODO: Make boolean, use function over view component. return this.user ? this.user.email : null;
*/
public get isActive(): string {
return this.user && this.user.is_active ? 'active' : 'inactive';
} }
public get structureLevel(): string { public get structureLevel(): string {
return this.user ? this.user.structure_level : null; 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[]) { public constructor(user?: User, groups?: Group[]) {
super(); super();
this._user = user; this._user = user;
this._groups = groups; this._groups = groups;
} }
/**
* required by BaseViewModel. Don't confuse with the users title.
*/
public getTitle(): string { public getTitle(): string {
return this.user ? this.user.toString() : null; return this.user ? this.user.toString() : null;
} }
@ -47,9 +104,7 @@ export class ViewUser extends BaseViewModel {
/** /**
* TODO: Implement * TODO: Implement
*/ */
public replaceGroup(newGroup: Group): void { public replaceGroup(newGroup: Group): void {}
console.log('replace group - not yet implemented, ', newGroup);
}
public updateValues(update: BaseModel): void { public updateValues(update: BaseModel): void {
if (update instanceof Group) { 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 { Group } from '../../../shared/models/users/group';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { DataStoreService } from '../../../core/services/data-store.service'; import { DataStoreService } from '../../../core/services/data-store.service';
import { DataSendService } from '../../../core/services/data-send.service';
/** /**
* Repository service for users * Repository service for users
@ -19,17 +20,29 @@ export class UserRepositoryService extends BaseRepository<ViewUser, User> {
/** /**
* Constructor calls the parent constructor * Constructor calls the parent constructor
*/ */
public constructor(DS: DataStoreService) { public constructor(DS: DataStoreService, private dataSend: DataSendService) {
super(DS, User, [Group]); 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> { public update(update: Partial<User>, viewUser: ViewUser): Observable<any> {
return null; 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 * TODO: used over not-yet-existing detail view
*/ */
public delete(user: ViewUser): Observable<User> { public delete(viewUser: ViewUser): Observable<any> {
return null; return this.dataSend.delete(viewUser.user);
} }
/** /**
* @ignore * creates and saves a new user
* *
* TODO: used over not-yet-existing detail view * 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> { public create(userData: Partial<User>): Observable<any> {
return null; 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 { public createViewModel(user: User): ViewUser {

View File

@ -1,8 +1,22 @@
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router'; 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({ @NgModule({
imports: [RouterModule.forChild(routes)], imports: [RouterModule.forChild(routes)],

View File

@ -3,10 +3,11 @@ import { CommonModule } from '@angular/common';
import { UsersRoutingModule } from './users-routing.module'; import { UsersRoutingModule } from './users-routing.module';
import { SharedModule } from '../../shared/shared.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({ @NgModule({
imports: [CommonModule, UsersRoutingModule, SharedModule], imports: [CommonModule, UsersRoutingModule, SharedModule],
declarations: [UserListComponent] declarations: [UserListComponent, UserDetailComponent]
}) })
export class UsersModule {} export class UsersModule {}

View File

@ -1,12 +1,29 @@
{ {
"Abort": "",
"About Me": "",
"Agenda": "Tagesordnung", "Agenda": "Tagesordnung",
"Assignments": "Wahlen", "Assignments": "Wahlen",
"Category": "", "Category": "",
"Change Password": "Passwort ändern", "Change Password": "Passwort ändern",
"Comment": "",
"Content": "", "Content": "",
"Copyright by": "Copyright by", "Copyright by": "Copyright by",
"Delete User": "",
"DeleteMotion": "", "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 Profile": "Profil bearbeiten",
"Edit category details:": "",
"English": "Englisch", "English": "Englisch",
"Export As": { "Export As": {
"0": { "0": {
@ -17,11 +34,18 @@
}, },
"FILTER": "", "FILTER": "",
"Files": "Dateien", "Files": "Dateien",
"First Name": "",
"French": "Französisch", "French": "Französisch",
"German": "Deutsch", "German": "Deutsch",
"Groups": "",
"Home": "Startseite", "Home": "Startseite",
"Identifier": "", "Identifier": "",
"Initial Password": "",
"Installed plugins": "", "Installed plugins": "",
"Is Active": "",
"Is Present": "",
"Is a committee": "",
"Last Name": "",
"Legal Notice": "Impressum", "Legal Notice": "Impressum",
"License": "", "License": "",
"Login": "", "Login": "",
@ -30,30 +54,42 @@
"Meta information": "", "Meta information": "",
"Motion": "", "Motion": "",
"Motions": "Anträge", "Motions": "Anträge",
"Name": "",
"None": "",
"OK": "", "OK": "",
"Offline mode: You can use OpenSlides but changes are not saved": { "Offline mode: You can use OpenSlides but changes are not saved": {
"0": "" "0": ""
}, },
"Only for internal notes": {
"0": ""
},
"Origin": "", "Origin": "",
"Participant Number": "",
"Participants": "Teilnehmer", "Participants": "Teilnehmer",
"Personal Note": "", "Personal Note": "",
"Personal note": "", "Personal note": "",
"Prefix": "",
"Present": "",
"Privacy Policy": "Datenschutz", "Privacy Policy": "Datenschutz",
"Project": "", "Project": "",
"Projector": "", "Projector": "",
"Reason": "", "Reason": "",
"Required": "",
"Reset State": "", "Reset State": "",
"Reset recommendation": "", "Reset recommendation": "",
"SORT": "", "SORT": "",
"Selected Values": "", "Selected Values": "",
"Settings": "Einstellungen", "Settings": "Einstellungen",
"State": "", "State": "",
"Structure Level": "",
"Submitters": "", "Submitters": "",
"Supporters": "", "Supporters": "",
"The assembly may decide:": "", "The assembly may decide:": "",
"The event manager hasn't set up a privacy policy yet": { "The event manager hasn't set up a privacy policy yet": {
"0": "" "0": ""
}, },
"Title": "",
"Username": "",
"Welcome to OpenSlides": "Willkommen bei OpenSlides", "Welcome to OpenSlides": "Willkommen bei OpenSlides",
"by": "" "by": ""
} }

View File

@ -1,12 +1,29 @@
{ {
"Abort": "",
"About Me": "",
"Agenda": "", "Agenda": "",
"Assignments": "", "Assignments": "",
"Category": "", "Category": "",
"Change Password": "", "Change Password": "",
"Comment": "",
"Content": "", "Content": "",
"Copyright by": "", "Copyright by": "",
"Delete User": "",
"DeleteMotion": "", "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 Profile": "",
"Edit category details:": "",
"English": "", "English": "",
"Export As": { "Export As": {
"0": { "0": {
@ -17,11 +34,18 @@
}, },
"FILTER": "", "FILTER": "",
"Files": "", "Files": "",
"First Name": "",
"French": "", "French": "",
"German": "", "German": "",
"Groups": "",
"Home": "", "Home": "",
"Identifier": "", "Identifier": "",
"Initial Password": "",
"Installed plugins": "", "Installed plugins": "",
"Is Active": "",
"Is Present": "",
"Is a committee": "",
"Last Name": "",
"Legal Notice": "", "Legal Notice": "",
"License": "", "License": "",
"Login": "", "Login": "",
@ -30,30 +54,42 @@
"Meta information": "", "Meta information": "",
"Motion": "", "Motion": "",
"Motions": "", "Motions": "",
"Name": "",
"None": "",
"OK": "", "OK": "",
"Offline mode: You can use OpenSlides but changes are not saved": { "Offline mode: You can use OpenSlides but changes are not saved": {
"0": "" "0": ""
}, },
"Only for internal notes": {
"0": ""
},
"Origin": "", "Origin": "",
"Participant Number": "",
"Participants": "", "Participants": "",
"Personal Note": "", "Personal Note": "",
"Personal note": "", "Personal note": "",
"Prefix": "",
"Present": "",
"Privacy Policy": "", "Privacy Policy": "",
"Project": "", "Project": "",
"Projector": "", "Projector": "",
"Reason": "", "Reason": "",
"Required": "",
"Reset State": "", "Reset State": "",
"Reset recommendation": "", "Reset recommendation": "",
"SORT": "", "SORT": "",
"Selected Values": "", "Selected Values": "",
"Settings": "", "Settings": "",
"State": "", "State": "",
"Structure Level": "",
"Submitters": "", "Submitters": "",
"Supporters": "", "Supporters": "",
"The assembly may decide:": "", "The assembly may decide:": "",
"The event manager hasn't set up a privacy policy yet": { "The event manager hasn't set up a privacy policy yet": {
"0": "" "0": ""
}, },
"Title": "",
"Username": "",
"Welcome to OpenSlides": "", "Welcome to OpenSlides": "",
"by": "" "by": ""
} }

View File

@ -1,12 +1,29 @@
{ {
"Abort": "",
"About Me": "",
"Agenda": "", "Agenda": "",
"Assignments": "", "Assignments": "",
"Category": "", "Category": "",
"Change Password": "", "Change Password": "",
"Comment": "",
"Content": "", "Content": "",
"Copyright by": "", "Copyright by": "",
"Delete User": "",
"DeleteMotion": "", "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 Profile": "",
"Edit category details:": "",
"English": "", "English": "",
"Export As": { "Export As": {
"0": { "0": {
@ -17,11 +34,18 @@
}, },
"FILTER": "", "FILTER": "",
"Files": "", "Files": "",
"First Name": "",
"French": "", "French": "",
"German": "", "German": "",
"Groups": "",
"Home": "", "Home": "",
"Identifier": "", "Identifier": "",
"Initial Password": "",
"Installed plugins": "", "Installed plugins": "",
"Is Active": "",
"Is Present": "",
"Is a committee": "",
"Last Name": "",
"Legal Notice": "", "Legal Notice": "",
"License": "", "License": "",
"Login": "", "Login": "",
@ -30,30 +54,42 @@
"Meta information": "", "Meta information": "",
"Motion": "", "Motion": "",
"Motions": "", "Motions": "",
"Name": "",
"None": "",
"OK": "", "OK": "",
"Offline mode: You can use OpenSlides but changes are not saved": { "Offline mode: You can use OpenSlides but changes are not saved": {
"0": "" "0": ""
}, },
"Only for internal notes": {
"0": ""
},
"Origin": "", "Origin": "",
"Participant Number": "",
"Participants": "", "Participants": "",
"Personal Note": "", "Personal Note": "",
"Personal note": "", "Personal note": "",
"Prefix": "",
"Present": "",
"Privacy Policy": "", "Privacy Policy": "",
"Project": "", "Project": "",
"Projector": "", "Projector": "",
"Reason": "", "Reason": "",
"Required": "",
"Reset State": "", "Reset State": "",
"Reset recommendation": "", "Reset recommendation": "",
"SORT": "", "SORT": "",
"Selected Values": "", "Selected Values": "",
"Settings": "", "Settings": "",
"State": "", "State": "",
"Structure Level": "",
"Submitters": "", "Submitters": "",
"Supporters": "", "Supporters": "",
"The assembly may decide:": "", "The assembly may decide:": "",
"The event manager hasn't set up a privacy policy yet": { "The event manager hasn't set up a privacy policy yet": {
"0": "" "0": ""
}, },
"Title": "",
"Username": "",
"Welcome to OpenSlides": "", "Welcome to OpenSlides": "",
"by": "" "by": ""
} }

View File

@ -26,6 +26,19 @@ body {
z-index: 100; 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 { .os-card {
max-width: 90%; max-width: 90%;
margin-top: 10px; margin-top: 10px;
@ -33,6 +46,16 @@ body {
margin-right: auto; 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 { .os-listview-table {
width: 100%; width: 100%;