Enhance os-head-bar

Headbar now works with multi slot transclusion
Supports more cases and detail bar

Also adds some UI UX improvements
This commit is contained in:
sean 2018-10-05 16:34:08 +02:00 committed by Sean Engelhardt
parent 794b978627
commit 4d26316e1e
44 changed files with 1037 additions and 753 deletions

View File

@ -560,7 +560,7 @@
}, },
"minimist": { "minimist": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
"integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
"dev": true "dev": true
}, },
@ -1357,35 +1357,6 @@
"is-negated-glob": "^1.0.0" "is-negated-glob": "^1.0.0"
} }
}, },
"@fortawesome/angular-fontawesome": {
"version": "0.1.0-10",
"resolved": "https://registry.npmjs.org/@fortawesome/angular-fontawesome/-/angular-fontawesome-0.1.0-10.tgz",
"integrity": "sha512-YW1cCbNo+D3mCrLEpRzb3xQiS/XpPDbsezf5W3hluIPO/vo3XIeid/B334sE+Y0p7h8TnaQMSPtUx0JxOhQyXw==",
"requires": {
"tslib": "^1.7.1"
}
},
"@fortawesome/fontawesome-common-types": {
"version": "0.2.4",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.2.4.tgz",
"integrity": "sha512-0qbIVm+MzkxMwKDx8V0C7w/6Nk+ZfBseOn2R1YK0f2DQP5pBcOQbu9NmaVaLzbJK6VJb1TuyTf0ZF97rc6iWJQ=="
},
"@fortawesome/fontawesome-svg-core": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-1.2.4.tgz",
"integrity": "sha512-oGtnwcdhJomoDxbJcy6S0JxK6ItDhJLNOujm+qILPqajJ2a0P/YRomzBbixFjAPquCoyPUlA9g9ejA22P7TKNA==",
"requires": {
"@fortawesome/fontawesome-common-types": "^0.2.4"
}
},
"@fortawesome/free-solid-svg-icons": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-5.3.1.tgz",
"integrity": "sha512-NkiLBFoiHtJ89cPJdM+W6cLvTVKkLh3j9t3MxkXyip0ncdD3lhCunSuzvFcrTHWeETEyoClGd8ZIWrr3HFZ3BA==",
"requires": {
"@fortawesome/fontawesome-common-types": "^0.2.4"
}
},
"@mrmlnc/readdir-enhanced": { "@mrmlnc/readdir-enhanced": {
"version": "2.2.1", "version": "2.2.1",
"resolved": "https://registry.npmjs.org/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz", "resolved": "https://registry.npmjs.org/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz",
@ -10497,6 +10468,11 @@
"inherits": "^2.0.1" "inherits": "^2.0.1"
} }
}, },
"roboto-fontface": {
"version": "0.10.0",
"resolved": "https://registry.npmjs.org/roboto-fontface/-/roboto-fontface-0.10.0.tgz",
"integrity": "sha512-OlwfYEgA2RdboZohpldlvJ1xngOins5d7ejqnIBWr9KaMxsnBqotpptRXTyfNRLnFpqzX6sTDt+X+a+6udnU8g=="
},
"run-async": { "run-async": {
"version": "2.3.0", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz", "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz",

View File

@ -1,4 +1,5 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { Subject } from 'rxjs';
/** /**
* This represents one entry in the main menu * This represents one entry in the main menu
@ -41,6 +42,12 @@ export class MainMenuService {
*/ */
private _entries: MainMenuEntry[] = []; private _entries: MainMenuEntry[] = [];
/**
* Observed by the site component.
* If a new value appears the sideNavContainer gets toggled
*/
public toggleMenuSubject = new Subject<void>();
/** /**
* Make the entries public. * Make the entries public.
*/ */
@ -58,4 +65,11 @@ export class MainMenuService {
this._entries.push(...entries); this._entries.push(...entries);
this._entries = this._entries.sort((a, b) => a.weight - b.weight); this._entries = this._entries.sort((a, b) => a.weight - b.weight);
} }
/**
* Emit signal to toggle the main Menu
*/
public toggleMenu(): void {
this.toggleMenuSubject.next();
}
} }

View File

@ -1,116 +1,70 @@
import { trigger, animate, transition, style, query, stagger, group } from '@angular/animations'; import { trigger, animate, transition, style, query, stagger, group } from '@angular/animations';
const fadeVanish = [
style({ transform: 'translateY(0%)', opacity: 1 }),
animate(
'200ms ease-in-out',
style({
transform: 'translateY(0%)',
opacity: 0
})
)
];
const fadeAppear = [
style({ transform: 'translateY(0%)', opacity: 0 }),
animate('200ms ease-in-out', style({ transform: 'translateY(0%)', opacity: 1 }))
];
const justEnterDom = [style({ opacity: 0 })];
const fadeMoveIn = [
style({ transform: 'translateY(30px)' }),
animate('250ms ease-in-out', style({ transform: 'translateY(0px)', opacity: 1 }))
];
export const pageTransition = trigger('pageTransition', [ export const pageTransition = trigger('pageTransition', [
transition('* => *', [ transition('* => *', [
/** this will avoid the dom-copy-effect */ /** this will avoid the dom-copy-effect */
query(':enter, :leave', style({ position: 'absolute', width: '100%' }), { optional: true }), query(':enter, :leave', style({ position: 'absolute', width: '100%' }), { optional: true }),
/** keep the dom clean - let all items "just" enter */ /** keep the dom clean - let all items "just" enter */
query(':enter mat-card', [style({ opacity: 0 })], { optional: true }), query(':enter mat-card', justEnterDom, { optional: true }),
query(':enter .on-transition-fade', [style({ opacity: 0 })], { optional: true }), query(':enter .on-transition-fade', justEnterDom, { optional: true }),
query(':enter mat-row', [style({ opacity: 0 })], { optional: true }), query(':enter mat-row', justEnterDom, { optional: true }),
query(':enter mat-expansion-panel', [style({ opacity: 0 })], { optional: true }), query(':enter mat-expansion-panel', justEnterDom, { optional: true }),
/** parallel vanishing */ /** parallel vanishing */
group([ group([
/** animate fade out for the selected components */ query(':leave .on-transition-fade', fadeVanish, { optional: true }),
query( query(':leave mat-card', fadeVanish, { optional: true }),
':leave .on-transition-fade', query(':leave mat-row', fadeVanish, { optional: true }),
[ query(':leave mat-expansion-panel', fadeVanish, { optional: true })
style({ opacity: 1 }),
animate(
'200ms ease-in-out',
style({
transform: 'translateY(0%)',
opacity: 0
})
)
],
{ optional: true }
),
/** how the material cards are leaving */
query(
':leave mat-card',
[
style({ transform: 'translateY(0%)', opacity: 1 }),
animate(
'200ms ease-in-out',
style({
transform: 'translateY(0%)',
opacity: 0
})
)
],
{ optional: true }
),
query(
':leave mat-row',
[
style({ transform: 'translateY(0%)', opacity: 1 }),
animate(
'200ms ease-in-out',
style({
transform: 'translateY(0%)',
opacity: 0
})
)
],
{ optional: true }
),
query(
':leave mat-expansion-panel',
[
style({ transform: 'translateY(0%)', opacity: 1 }),
animate(
'200ms ease-in-out',
style({
transform: 'translateY(0%)',
opacity: 0
})
)
],
{ optional: true }
)
]), ]),
/** parallel appearing */ /** parallel appearing */
group([ group([
/** animate fade in for the selected components */ /** animate fade in for the selected components */
query(':enter .on-transition-fade', [style({ opacity: 0 }), animate('0.2s', style({ opacity: 1 }))], { query(':enter .on-transition-fade', fadeAppear, { optional: true }),
optional: true
}), /** Staggered appearing = "one after another" */
/** how the mat cards enters the scene */ query(':enter mat-card', stagger(50, fadeMoveIn), { optional: true }),
query( query(':enter mat-row', stagger(30, fadeMoveIn), { optional: true })
':enter mat-card', // disabled for now. They somehow appear expanded which looks strange
/** stagger = "one after another" with a distance of 50ms" */ // query(':enter mat-expansion-panel', stagger(30, fadeMoveIn), { optional: true })
stagger(50, [
style({ transform: 'translateY(50px)' }),
animate('300ms ease-in-out', style({ transform: 'translateY(0px)', opacity: 1 }))
]),
{ optional: true }
),
query(
':enter mat-row',
/** stagger = "one after another" with a distance of 50ms" */
stagger(30, [
style({ transform: 'translateY(24px)' }),
animate('200ms ease-in-out', style({ transform: 'translateY(0px)', opacity: 1 }))
]),
{ optional: true }
),
query(
':enter mat-expansion-panel',
/** stagger = "one after another" with a distance of 50ms" */
stagger(100, [
style({ transform: 'translateY(50px)' }),
animate('300ms ease-in-out', style({ transform: 'translateY(0px)', opacity: 1 }))
]),
{ optional: true }
)
]) ])
]) ])
]); ]);
export const navItemAnim = trigger('navItemAnim', [ const slideIn = [style({ transform: 'translateX(-85%)' }), animate('600ms ease')];
transition(':enter', [style({ transform: 'translateX(-100%)' }), animate('500ms ease')]), const slideOut = [
transition(':leave', [style({ transform: 'translateX(100%)' }), animate('500ms ease')]) style({ transform: 'translateX(0)' }),
]); animate(
'600ms ease',
style({
transform: 'translateX(-85%)'
})
)
];
export const navItemAnim = trigger('navItemAnim', [transition(':enter', slideIn), transition(':leave', slideOut)]);

View File

@ -1,27 +1,58 @@
<mat-toolbar color='primary'> <mat-toolbar color='primary' [ngClass]="{'during-scroll': stickyToolbar}">
<button *ngIf="plusButton" class='head-button on-transition-fade' (click)=clickPlusButton()
mat-fab>
<mat-icon>add</mat-icon>
</button>
<span class='app-name on-transition-fade'> <mat-toolbar-row [ngClass]="{'hidden-bar': stickyToolbar}">
{{ appName | translate }} <!-- Nav menu -->
</span> <button mat-icon-button *ngIf="vp.isMobile && nav" (click)='clickHamburgerMenu()'>
<mat-icon>menu</mat-icon>
</button>
</mat-toolbar-row>
<span class='spacer'></span> <mat-toolbar-row [ngClass]="{'during-scroll': stickyToolbar}">
<div class="toolbar-left on-transition-fade">
<!-- Fab Button "Plus" -->
<button mat-fab class="head-button" *ngIf="plusButton && !editMode" (click)=clickPlusButton()>
<mat-icon>add</mat-icon>
</button>
<!-- Exit / Back button -->
<button mat-icon-button class="on-transition-fade" *ngIf="backButton && !editMode" (click)="onBackButton()">
<mat-icon>arrow_back</mat-icon>
</button>
<!-- Cancel edit button -->
<button mat-icon-button class="on-transition-fade" *ngIf="editMode" (click)="toggleEditMode()">
<mat-icon>close</mat-icon>
</button>
<div class="toolbar-left-text">
<!-- Title slot -->
<ng-content select=".title-slot"></ng-content>
</div>
</div>
<div class="toolbar-right on-transition-fade" [ngClass]="{'toolbar-right-scroll': stickyToolbar, 'toolbar-right-top': !stickyToolbar}">
<!-- Extra controls slot -->
<div class="extra-controls-wrapper on-transition-fade">
<ng-content select=".extra-controls-slot"></ng-content>
</div>
<!-- Save button -->
<button mat-button *ngIf="editMode" (click)="save()">
<strong translate class="upper">Save</strong>
</button>
<!-- Edit button-->
<button mat-icon-button *ngIf="!editMode && allowEdit" (click)="toggleEditMode()">
<mat-icon>{{ editIcon }}</mat-icon>
</button>
<!-- Menu button slot -->
<ng-content *ngIf="!editMode" select=".menu-slot"></ng-content>
</div>
</mat-toolbar-row>
<button *ngIf="menuList" class='on-transition-fade' [matMenuTriggerFor]="ellipsisMenu" mat-icon-button>
<mat-icon>more_vert</mat-icon>
</button>
</mat-toolbar> </mat-toolbar>
<mat-menu #ellipsisMenu="matMenu"> <!-- fake mat-toolbar to keep the distance when the real one gets a fixed position -->
<div class="fake-bar" [ngClass]="{'hidden-bar': !stickyToolbar}"></div>
<ng-container *ngFor="let item of menuList">
<button mat-menu-item *osPerms="item.perm" (click)=clickMenu(item)>
<mat-icon *ngIf="item.icon">{{ item.icon }}</mat-icon>
{{item.text | translate}}
</button>
</ng-container>
</mat-menu>

View File

@ -1,4 +1,51 @@
.head-button { .during-scroll {
bottom: -30px; position: fixed;
z-index: 100; z-index: 1;
}
.toolbar-left {
position: absolute;
display: inherit;
.head-button {
bottom: -30px;
}
.toolbar-left-text {
margin: auto 0 5px 20px;
}
}
.toolbar-right {
display: inherit;
}
.toolbar-right-scroll {
position: fixed;
right: 30px; // fixed and absolute somehow have different ideas of distance
}
.toolbar-right-top {
position: absolute;
right: 15px;
}
// to hide the first mat-toolbar-row while scrolling and the fake-bar while on top
.hidden-bar {
display: none;
position: inline;
}
// fake bar to simulate the size of the other one, show it when the position changes to fixed
.fake-bar {
width: 100%;
height: 120px; // height of two normal mat-toolbars
z-index: -1;
}
.extra-controls-wrapper {
display: contents;
::ng-deep .extra-controls-slot {
display: flex;
}
} }

View File

@ -1,29 +1,11 @@
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core'; import { Component, Input, Output, EventEmitter, OnInit, NgZone } from '@angular/core';
import { Permission } from '../../../core/services/operator.service'; import { Location } from '@angular/common';
import { Router, ActivatedRoute } from '@angular/router';
import { ScrollDispatcher, CdkScrollable } from '@angular/cdk/scrolling';
import { map } from 'rxjs/operators';
/** import { ViewportService } from '../../../core/services/viewport.service';
* One entry for the ellipsis menu. import { MainMenuService } from '../../../core/services/main-menu.service';
*/
export interface EllipsisMenuItem {
/**
* The text for the menu entry
*/
text: string;
/**
* An optional icon to display before the text.
*/
icon?: string;
/**
* The action to be performed on click.
*/
action: string;
/**
* An optional permission to see this entry.
*/
perm?: Permission;
}
/** /**
* Reusable head bar component for Apps. * Reusable head bar component for Apps.
@ -32,7 +14,6 @@ export interface EllipsisMenuItem {
* *
* Use `PlusButton=true` and `(plusButtonClicked)=myFunction()` if a plus button is needed * Use `PlusButton=true` and `(plusButtonClicked)=myFunction()` if a plus button is needed
* *
* Use `[menuLust]=myArray` and `(ellipsisMenuItem)=myFunction($event)` if a menu is needed
* *
* ## Examples: * ## Examples:
* *
@ -42,33 +23,10 @@ export interface EllipsisMenuItem {
* <os-head-bar * <os-head-bar
* appName="Files" * appName="Files"
* plusButton=true * plusButton=true
* [menuList]=myMenu
* (plusButtonClicked)=onPlusButton() * (plusButtonClicked)=onPlusButton()
* (ellipsisMenuItem)=onEllipsisItem($event)> * (ellipsisMenuItem)=onEllipsisItem($event)>
* </os-head-bar> * </os-head-bar>
* ``` * ```
*
* ### Declaration of a menu provided as `[menuList]=myMenu`:
*
* ```ts
* myMenu = [
* {
* text: 'Download All',
* icon: 'save_alt',
* action: 'downloadAllFiles'
* },
* ];
* ```
* The parent needs to react to `action` like the following.
* This will execute a function with the name provided in the
* `action` field.
* ```ts
* onEllipsisItem(item: EllipsisMenuItem) {
* if (typeof this[item.action] === 'function') {
* this[item.action]();
* }
* }
* ```
*/ */
@Component({ @Component({
selector: 'os-head-bar', selector: 'os-head-bar',
@ -77,24 +35,52 @@ export interface EllipsisMenuItem {
}) })
export class HeadBarComponent implements OnInit { export class HeadBarComponent implements OnInit {
/** /**
* Input declaration for the app name * determine weather the toolbar should be sticky or not
*/
public stickyToolbar = false;
/**
* Determine if the the navigation "hamburger" icon should be displayed in mobile mode
*/ */
@Input() @Input()
public appName: string; public nav = true;
/**
* Show or hide edit features
*/
@Input()
public allowEdit = false;
/**
* Custom edit icon if necessary
*/
@Input()
public editIcon = 'edit';
/**
* Determine edit mode
*/
@Input()
public editMode = false;
/** /**
* Determine if there should be a plus button. * Determine if there should be a plus button.
*/ */
@Input() @Input()
public plusButton: false; public plusButton = false;
/** /**
* If not empty shows a ellipsis menu on the right side * Determine if there should be a back button.
*
* The parent needs to provide a menu, i.e `[menuList]=myMenu`.
*/ */
@Input() @Input()
public menuList: EllipsisMenuItem[]; public backButton = false;
/**
* Set to true if the component should use location.back instead
* of navigating to the parent component
*/
@Input()
public goBack = false;
/** /**
* Emit a signal to the parent component if the plus button was clicked * Emit a signal to the parent component if the plus button was clicked
@ -103,28 +89,29 @@ export class HeadBarComponent implements OnInit {
public plusButtonClicked = new EventEmitter<boolean>(); public plusButtonClicked = new EventEmitter<boolean>();
/** /**
* Emit a signal to the parent of an item in the menuList was selected. * Sends a signal if a detail view should be edited or editing should be canceled
*/ */
@Output() @Output()
public ellipsisMenuItem = new EventEmitter<EllipsisMenuItem>(); public editEvent = new EventEmitter<boolean>();
/**
* Sends a signal if a detail view should be saved
*/
@Output()
public saveEvent = new EventEmitter<boolean>();
/** /**
* Empty constructor * Empty constructor
*/ */
public constructor() {} public constructor(
public vp: ViewportService,
/** private scrollDispatcher: ScrollDispatcher,
* empty onInit private ngZone: NgZone,
*/ private menu: MainMenuService,
public ngOnInit(): void {} private router: Router,
private route: ActivatedRoute,
/** private location: Location
* Emits a signal to the parent if an item in the menu was clicked. ) {}
* @param item
*/
public clickMenu(item: EllipsisMenuItem): void {
this.ellipsisMenuItem.emit(item);
}
/** /**
* Emits a signal to the parent if * Emits a signal to the parent if
@ -132,4 +119,71 @@ export class HeadBarComponent implements OnInit {
public clickPlusButton(): void { public clickPlusButton(): void {
this.plusButtonClicked.emit(true); this.plusButtonClicked.emit(true);
} }
/**
* Clicking the burger-menu-icon should toggle the menu
*/
public clickHamburgerMenu(): void {
this.menu.toggleMenu();
}
/**
* Toggle edit mode and send a signal to listeners
*/
public toggleEditMode(): void {
this.editEvent.next(!this.editMode);
}
/**
* Send a save signal and set edit mode
*/
public save(): void {
if (this.editMode) {
this.saveEvent.next(true);
}
}
/**
* Exits the view to return to the previous page or
* visit the parent view again.
*/
public onBackButton(): void {
if (this.goBack) {
this.location.back();
} else {
this.router.navigate(['../'], { relativeTo: this.route });
}
}
/**
* Init function. Subscribe to the scrollDispatcher and decide when to set the top bar to fixed
*
* Not working for now.
*/
public ngOnInit(): void {
this.scrollDispatcher
.scrolled()
.pipe(map((event: CdkScrollable) => this.getScrollPosition(event)))
.subscribe(scrollTop => {
this.ngZone.run(() => {
if (scrollTop > 60) {
this.stickyToolbar = true;
} else {
this.stickyToolbar = false;
}
});
});
}
/**
* returns the scroll position
* @param event
*/
public getScrollPosition(event: CdkScrollable): number {
if (event) {
return event.getElementRef().nativeElement.scrollTop;
} else {
return window.scrollY;
}
}
} }

View File

@ -0,0 +1,6 @@
describe('AutofocusDirective', () => {
it('should create an instance', () => {
// const directive = new AutofocusDirective();
// expect(directive).toBeTruthy();
});
});

View File

@ -0,0 +1,33 @@
import { Directive, ElementRef, OnInit } from '@angular/core';
/**
* enhanced version of `autofocus` for (but not exclusively) html input fields.
* Works even if the input field was added dynamically using `*ngIf`
*
* @example
* ```html
* <input matInput osAutofocus required>
* ```
*/
@Directive({
selector: '[osAutofocus]'
})
export class AutofocusDirective implements OnInit {
/**
* Constructor
*
* Gets the reference of the annotated element
* @param el ElementRef
*/
public constructor(private el: ElementRef) {}
/**
* Executed after page init, calls the focus function after an unnoticeable timeout
*/
public ngOnInit(): void {
// Otherwise Angular throws error: Expression has changed after it was checked.
setTimeout(() => {
this.el.nativeElement.focus();
});
}
}

View File

@ -40,6 +40,7 @@ import { TranslateModule } from '@ngx-translate/core';
// directives // directives
import { PermsDirective } from './directives/perms.directive'; import { PermsDirective } from './directives/perms.directive';
import { DomChangeDirective } from './directives/dom-change.directive'; import { DomChangeDirective } from './directives/dom-change.directive';
import { AutofocusDirective } from './directives/autofocus.directive';
// components // components
import { HeadBarComponent } from './components/head-bar/head-bar.component'; import { HeadBarComponent } from './components/head-bar/head-bar.component';
@ -127,6 +128,7 @@ import { PromptDialogComponent } from './components/prompt-dialog/prompt-dialog.
TranslateModule, TranslateModule,
PermsDirective, PermsDirective,
DomChangeDirective, DomChangeDirective,
AutofocusDirective,
FooterComponent, FooterComponent,
HeadBarComponent, HeadBarComponent,
SearchValueSelectorComponent, SearchValueSelectorComponent,
@ -137,6 +139,7 @@ import { PromptDialogComponent } from './components/prompt-dialog/prompt-dialog.
declarations: [ declarations: [
PermsDirective, PermsDirective,
DomChangeDirective, DomChangeDirective,
AutofocusDirective,
HeadBarComponent, HeadBarComponent,
FooterComponent, FooterComponent,
LegalNoticeContentComponent, LegalNoticeContentComponent,

View File

@ -1,4 +1,9 @@
<os-head-bar appName="Agenda" plusButton=true (plusButtonClicked)=onPlusButton()></os-head-bar> <os-head-bar plusButton=true (plusButtonClicked)=onPlusButton()>
<!-- Title -->
<div class="title-slot">
<h2 translate>Agenda</h2>
</div>
</os-head-bar>
<mat-table class='os-listview-table on-transition-fade' [dataSource]="dataSource" matSort> <mat-table class='os-listview-table on-transition-fade' [dataSource]="dataSource" matSort>
<!-- title column --> <!-- title column -->

View File

@ -1,5 +1,15 @@
<os-head-bar appName="Assignments" plusButton=true [menuList]=assignmentMenu (plusButtonClicked)=onPlusButton() <os-head-bar plusButton=true (plusButtonClicked)=onPlusButton()>
(ellipsisMenuItem)=onEllipsisItem($event)> <!-- Title -->
<div class="title-slot">
<h2 translate>Assignments</h2>
</div>
<!-- Menu -->
<div class="menu-slot">
<button type="button" mat-icon-button [matMenuTriggerFor]="assignmentMenu">
<mat-icon>more_vert</mat-icon>
</button>
</div>
</os-head-bar> </os-head-bar>
<mat-table class='os-listview-table on-transition-fade' [dataSource]="dataSource" matSort> <mat-table class='os-listview-table on-transition-fade' [dataSource]="dataSource" matSort>
@ -30,3 +40,10 @@
</mat-table> </mat-table>
<mat-paginator class="on-transition-fade" [pageSizeOptions]="[25, 50, 75, 100, 125]"></mat-paginator> <mat-paginator class="on-transition-fade" [pageSizeOptions]="[25, 50, 75, 100, 125]"></mat-paginator>
<mat-menu #assignmentMenu="matMenu">
<button mat-menu-item (click)="downloadAssignmentButton()">
<mat-icon>archive</mat-icon>
<span translate>Export ...</span>
</button>
</mat-menu>

View File

@ -15,18 +15,6 @@ import { AssignmentRepositoryService } from '../services/assignment-repository.s
styleUrls: ['./assignment-list.component.css'] styleUrls: ['./assignment-list.component.css']
}) })
export class AssignmentListComponent extends ListViewBaseComponent<ViewAssignment> implements OnInit { export class AssignmentListComponent extends ListViewBaseComponent<ViewAssignment> implements OnInit {
/**
* Define the content of the ellipsis menu.
* Give it to the HeadBar to display them.
*/
public assignmentMenu = [
{
text: 'Download All',
icon: 'save_alt',
action: 'downloadAssignmentButton'
}
];
/** /**
* Constructor. * Constructor.
* *

View File

@ -4,7 +4,6 @@ import { Title } from '@angular/platform-browser';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { MatTableDataSource, MatTable, MatSort, MatPaginator } from '@angular/material'; import { MatTableDataSource, MatTable, MatSort, MatPaginator } from '@angular/material';
import { BaseViewModel } from './base-view-model'; import { BaseViewModel } from './base-view-model';
import { EllipsisMenuItem } from '../../shared/components/head-bar/head-bar.component';
export abstract class ListViewBaseComponent<V extends BaseViewModel> extends BaseComponent { export abstract class ListViewBaseComponent<V extends BaseViewModel> extends BaseComponent {
/** /**
@ -49,16 +48,4 @@ export abstract class ListViewBaseComponent<V extends BaseViewModel> extends Bas
this.dataSource.paginator = this.paginator; this.dataSource.paginator = this.paginator;
this.dataSource.sort = this.sort; this.dataSource.sort = this.sort;
} }
/**
* handler function for clicking on items in the ellipsis menu.
* Ellipsis menu comes from the HeadBarComponent is is implemented by most ListViews
*
* @param event clicked entry from ellipsis menu
*/
public onEllipsisItem(item: EllipsisMenuItem): void {
if (typeof this[item.action] === 'function') {
this[item.action]();
}
}
} }

View File

@ -1,3 +1,7 @@
<os-head-bar appName="Legal Notice"></os-head-bar> <os-head-bar [backButton]=true [goBack]=true>
<div class="title-slot">
<h2 translate>Legal Notice</h2>
</div>
</os-head-bar>
<os-legal-notice-content></os-legal-notice-content> <os-legal-notice-content></os-legal-notice-content>

View File

@ -1,3 +1,7 @@
<os-head-bar appName="Privacy Policy"></os-head-bar> <os-head-bar [backButton]=true [goBack]=true>
<div class="title-slot">
<h2 translate>Privacy Policy</h2>
</div>
</os-head-bar>
<os-privacy-policy-content></os-privacy-policy-content> <os-privacy-policy-content></os-privacy-policy-content>

View File

@ -1,4 +1,7 @@
<os-head-bar appName="Home"> <os-head-bar>
<div class="title-slot">
<h2 translate>Home</h2>
</div>
</os-head-bar> </os-head-bar>
<mat-card class="os-card"> <mat-card class="os-card">

View File

@ -1,4 +1,9 @@
<os-head-bar appName="Settings"></os-head-bar> <os-head-bar>
<!-- Title -->
<div class="title-slot">
<h2 translate>Settings</h2>
</div>
</os-head-bar>
<mat-accordion> <mat-accordion>
<ng-container *ngFor="let group of this.configs"> <ng-container *ngFor="let group of this.configs">

View File

@ -2,13 +2,13 @@
<mat-spinner *ngIf="inProcess"></mat-spinner> <mat-spinner *ngIf="inProcess"></mat-spinner>
<form [formGroup]="loginForm" class="login-form" (ngSubmit)="formLogin()"> <form [formGroup]="loginForm" class="login-form" (ngSubmit)="formLogin()">
<mat-form-field> <mat-form-field>
<input matInput required placeholder="User name" formControlName="username" [errorStateMatcher]="parentErrorStateMatcher"> <input matInput osAutofocus required placeholder="User name" formControlName="username" [errorStateMatcher]="parentErrorStateMatcher">
</mat-form-field> </mat-form-field>
<br> <br>
<mat-form-field> <mat-form-field>
<input matInput required placeholder="Password" formControlName="password" [type]="!hide ? 'password' : 'text'" <input matInput required placeholder="Password" formControlName="password" [type]="!hide ? 'password' : 'text'"
[errorStateMatcher]="parentErrorStateMatcher"> [errorStateMatcher]="parentErrorStateMatcher">
<mat-icon matSuffix (click)="hide = !hide">{{ hide ? "visibility_off" : "visibility_on" }}</mat-icon> <mat-icon matSuffix (click)="hide = !hide">{{ hide ? "visibility_off" : "visibility_on" }}</mat-icon>
<mat-error>{{loginErrorMsg}}</mat-error> <mat-error>{{loginErrorMsg}}</mat-error>
</mat-form-field> </mat-form-field>
@ -21,7 +21,7 @@
<br> <br>
<!-- TODO: Next to each other...--> <!-- TODO: Next to each other...-->
<button mat-raised-button color="primary" class='login-button' type="submit" translate>Login</button> <button mat-raised-button color="primary" class='login-button' type="submit" translate>Login</button>
<button mat-raised-button *ngIf="areGuestsEnabled()" color="primary" class='login-button' type="button" <button mat-raised-button *ngIf="areGuestsEnabled()" color="primary" class='login-button' type="button" (click)="guestLogin()"
(click)="guestLogin()" translate>Login as Guest</button> translate>Login as Guest</button>
</form> </form>
</div> </div>

View File

@ -1,6 +1,16 @@
<os-head-bar appName="Files" plusButton=true [menuList]=extraMenu (plusButtonClicked)=onPlusButton() (ellipsisMenuItem)=onEllipsisItem($event)> <os-head-bar plusButton=true (plusButtonClicked)=onPlusButton()>
</os-head-bar> <!-- Title -->
<div class="title-slot">
<h2 translate>Files</h2>
</div>
<!-- Menu -->
<div class="menu-slot">
<button type="button" mat-icon-button [matMenuTriggerFor]="mediafilesMenu">
<mat-icon>more_vert</mat-icon>
</button>
</div>
</os-head-bar>
<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 -->
@ -31,3 +41,10 @@
</mat-table> </mat-table>
<mat-paginator class="on-transition-fade" [pageSizeOptions]="[25, 50, 75, 100, 125]"></mat-paginator> <mat-paginator class="on-transition-fade" [pageSizeOptions]="[25, 50, 75, 100, 125]"></mat-paginator>
<mat-menu #mediafilesMenu="matMenu">
<button mat-menu-item (click)="deleteAllFiles()">
<mat-icon>delete</mat-icon>
<span translate>Delete All</span>
</button>
</mat-menu>

View File

@ -17,18 +17,6 @@ import { ListViewBaseComponent } from '../../base/list-view-base';
styleUrls: ['./mediafile-list.component.css'] styleUrls: ['./mediafile-list.component.css']
}) })
export class MediafileListComponent extends ListViewBaseComponent<ViewMediafile> implements OnInit { export class MediafileListComponent extends ListViewBaseComponent<ViewMediafile> implements OnInit {
/**
* Define the content of the ellipsis menu.
* Give it to the HeadBar to display them.
*/
public extraMenu = [
{
text: 'Download',
icon: 'save_alt',
action: 'downloadAllFiles'
}
];
/** /**
* Constructor * Constructor
* *

View File

@ -1,13 +1,27 @@
<os-head-bar appName="Categories" [plusButton]=true (plusButtonClicked)=onPlusButton()> <os-head-bar [nav]="false" [backButton]=true [allowEdit]="false">
</os-head-bar>
<div class='custom-table-header on-transition-fade'> <!-- Title -->
<button mat-button> <div class="title-slot">
<mat-icon>search</mat-icon> <h2 translate>Categories</h2>
</div>
<!-- Use the menu slot for an add button -->
<div class="menu-slot">
<button type="button" mat-icon-button (click)="onPlusButton()">
<mat-icon>add</mat-icon>
</button> </button>
</div> </div>
</os-head-bar>
<div class='custom-table-header on-transition-fade'>
<button mat-button>
<mat-icon>search</mat-icon>
</button>
</div>
<mat-accordion class="os-card"> <mat-accordion class="os-card">
<mat-expansion-panel [ngClass]="{new: category.id === undefined}" *ngFor="let category of this.dataSource" (opened)="panelOpening('true', category)" (closed)="panelOpening('false', category)" <mat-expansion-panel [ngClass]="{new: category.id === undefined}" *ngFor="let category of this.dataSource" (opened)="panelOpening('true', category)"
multiple="false"> (closed)="panelOpening('false', category)" multiple="false">
<mat-expansion-panel-header> <mat-expansion-panel-header>
<mat-panel-title *ngIf="!category.edit"> <mat-panel-title *ngIf="!category.edit">
{{category.name}} {{category.name}}

View File

@ -1,8 +1,22 @@
<os-head-bar appName="Motion comment sections" [plusButton]=true (plusButtonClicked)=onPlusButton()> <os-head-bar [nav]="false" [backButton]=true [allowEdit]="false">
<!-- Title -->
<div class="title-slot">
<h2 translate>Comments</h2>
</div>
<!-- Use the menu slot for an add button -->
<div class="menu-slot">
<button type="button" mat-icon-button (click)="onPlusButton()">
<mat-icon>add</mat-icon>
</button>
</div>
</os-head-bar> </os-head-bar>
<div class="head-spacer"></div> <div class="head-spacer"></div>
<mat-card *ngIf="commentSectionToCreate"> <mat-card *ngIf="commentSectionToCreate">
<mat-card-title translate>Create new comment section</mat-card-title> <mat-card-title translate>Create new comment field</mat-card-title>
<mat-card-content> <mat-card-content>
<form [formGroup]="createForm" (keydown)="keyDownFunction($event)"> <form [formGroup]="createForm" (keydown)="keyDownFunction($event)">
<p> <p>
@ -15,11 +29,11 @@
</p> </p>
<p> <p>
<os-search-value-selector ngDefaultControl [form]="createForm" [formControl]="this.createForm.get('read_groups_id')" <os-search-value-selector ngDefaultControl [form]="createForm" [formControl]="this.createForm.get('read_groups_id')"
[multiple]="true" listname="Groups with read permissions" [InputListValues]="this.groups"></os-search-value-selector> [multiple]="true" listname="Groups with read permissions" [InputListValues]="this.groups"></os-search-value-selector>
</p> </p>
<p> <p>
<os-search-value-selector ngDefaultControl [form]="createForm" [formControl]="this.createForm.get('write_groups_id')" <os-search-value-selector ngDefaultControl [form]="createForm" [formControl]="this.createForm.get('write_groups_id')"
[multiple]="true" listname="Groups with write permissions" [InputListValues]="this.groups"></os-search-value-selector> [multiple]="true" listname="Groups with write permissions" [InputListValues]="this.groups"></os-search-value-selector>
</p> </p>
</form> </form>
</mat-card-content> </mat-card-content>
@ -29,8 +43,8 @@
</mat-card-actions> </mat-card-actions>
</mat-card> </mat-card>
<mat-accordion class="os-card"> <mat-accordion class="os-card">
<mat-expansion-panel *ngFor="let section of this.commentSections" (opened)="openId = section.id" <mat-expansion-panel *ngFor="let section of this.commentSections" (opened)="openId = section.id" (closed)="panelClosed(section)"
(closed)="panelClosed(section)" [expanded]="openId === section.id" multiple="false"> [expanded]="openId === section.id" multiple="false">
<mat-expansion-panel-header> <mat-expansion-panel-header>
<mat-panel-title> <mat-panel-title>
<div class="header-container"> <div class="header-container">
@ -66,11 +80,11 @@
</p> </p>
<p> <p>
<os-search-value-selector ngDefaultControl [form]="updateForm" [formControl]="this.updateForm.get('read_groups_id')" <os-search-value-selector ngDefaultControl [form]="updateForm" [formControl]="this.updateForm.get('read_groups_id')"
[multiple]="true" listname="Groups with read permissions" [InputListValues]="this.groups"></os-search-value-selector> [multiple]="true" listname="Groups with read permissions" [InputListValues]="this.groups"></os-search-value-selector>
</p> </p>
<p> <p>
<os-search-value-selector ngDefaultControl [form]="updateForm" [formControl]="this.updateForm.get('write_groups_id')" <os-search-value-selector ngDefaultControl [form]="updateForm" [formControl]="this.updateForm.get('write_groups_id')"
[multiple]="true" listname="Groups with write permissions" [InputListValues]="this.groups"></os-search-value-selector> [multiple]="true" listname="Groups with write permissions" [InputListValues]="this.groups"></os-search-value-selector>
</p> </p>
</form> </form>
<ng-container *ngIf="editId !== section.id"> <ng-container *ngIf="editId !== section.id">

View File

@ -1,48 +1,66 @@
<mat-toolbar color='primary'> <os-head-bar [nav]="false" [backButton]=true [allowEdit]="opCanEdit()" [editMode]="editMotion" (editEvent)="setEditMode($event)"
(saveEvent)="saveMotion()">
<button (click)='editMotionButton()' [ngClass]="{'save-button': editMotion}" class='generic-mini-button on-transition-fade' <!-- Title -->
mat-mini-fab> <div class="title-slot">
<mat-icon *ngIf="!editMotion">add</mat-icon> <h2 *ngIf="motion && !newMotion">
<mat-icon *ngIf="editMotion">check</mat-icon> <span translate>Motion</span>
</button> <!-- Whitespace between "Motion" and identifier -->
<span>&nbsp;</span>
<span *ngIf="!editMotion">{{ motion.identifier }}</span>
<span *ngIf="editMotion">{{ metaInfoForm.get("identifier").value }}</span>
</h2>
<h2 *ngIf="newMotion" translate>
New motion
</h2>
</div>
<div class='motion-title on-transition-fade'> <!-- Back and forth buttons-->
<span *ngIf="newMotion">New </span> <div *ngIf="!editMotion" class="extra-controls-slot on-transition-fade">
<span translate>Motion</span> <div *ngIf="previousMotion">
<span *ngIf="motion && !editMotion"> {{motion.identifier}}</span> <button mat-button (click)="navigateToMotion(previousMotion)">
<span *ngIf="editMotion && !newMotion"> {{metaInfoForm.get('identifier').value}}</span> <mat-icon>navigate_before</mat-icon>
<span>:</span> <span>{{ previousMotion.identifier }}</span>
<span *ngIf="motion && !editMotion"> {{motion.title}}</span> </button>
<span *ngIf="editMotion"> {{contentForm.get('title').value}}</span> </div>
<br> <div *ngIf="nextMotion">
<div *ngIf="motion && !newMotion" class='motion-submitter'> <button mat-button (click)="navigateToMotion(nextMotion)">
<span translate>by</span> {{motion.submitters}} <span>{{ nextMotion.identifier }}</span>
<mat-icon>navigate_next</mat-icon>
</button>
</div> </div>
</div> </div>
<span class='spacer'></span>
<!-- Button on the right--> <!-- Menu -->
<div *ngIf="editMotion"> <div class="menu-slot">
<button (click)='cancelEditMotionButton()' class='on-transition-fade' color="warn" mat-raised-button> <button type="button" mat-icon-button [matMenuTriggerFor]="motionExtraMenu">
<span translate>Cancel</span> <mat-icon>more_vert</mat-icon>
<mat-icon class="icon-text-distance">cancel</mat-icon>
</button> </button>
</div> </div>
<div *ngIf="!editMotion">
<button class='on-transition-fade' mat-icon-button [matMenuTriggerFor]="motionExtraMenu">
<mat-icon icon>more_vert</mat-icon>
</button>
</div>
<mat-menu #motionExtraMenu="matMenu"> <mat-menu #motionExtraMenu="matMenu">
<!-- TODO: the functions for the buttons --> <button mat-menu-item>
<button mat-menu-item translate>Export As...</button> <mat-icon>picture_as_pdf</mat-icon>
<button mat-menu-item translate>Project</button> <span translate>PDF</span>
</button>
<button mat-menu-item>
<!-- possible icons: screen_share, cast, videocam -->
<mat-icon>videocam</mat-icon>
<span translate>Project</span>
</button>
<mat-divider></mat-divider> <mat-divider></mat-divider>
<button mat-menu-item class='red-warning-text' (click)='deleteMotionButton()' translate>DeleteMotion</button> <button mat-menu-item class='red-warning-text' (click)='deleteMotionButton()'>
<mat-icon>delete</mat-icon>
<span translate>Delete</span>
</button>
</mat-menu> </mat-menu>
</mat-toolbar> </os-head-bar>
<!-- Title -->
<div *ngIf="motion" class="motion-title on-transition-fade">
<h2 *ngIf="!editMotion">{{ motion.title }}</h2>
<h2 *ngIf="editMotion">{{ contentForm.get("title").value }}</h2>
</div>
<ng-container *ngIf="vp.isMobile ; then mobileView; else desktopView"></ng-container> <ng-container *ngIf="vp.isMobile ; then mobileView; else desktopView"></ng-container>
@ -50,7 +68,8 @@
<mat-accordion multi='true' class='on-transition-fade'> <mat-accordion multi='true' class='on-transition-fade'>
<!-- MetaInfo Panel--> <!-- MetaInfo Panel-->
<mat-expansion-panel #metaInfoPanel [expanded]="this.editReco && this.newReco" class='meta-info-block meta-info-panel'> <mat-expansion-panel #metaInfoPanel [expanded]="this.editMotion" class='meta-info-block meta-info-panel'>
<!-- <mat-expansion-panel #metaInfoPanel [expanded]="this.editReco && this.newReco" class='meta-info-block meta-info-panel'> -->
<mat-expansion-panel-header> <mat-expansion-panel-header>
<mat-panel-title> <mat-panel-title>
<mat-icon>info</mat-icon> <mat-icon>info</mat-icon>
@ -76,7 +95,7 @@
</mat-expansion-panel> </mat-expansion-panel>
<!-- Content --> <!-- Content -->
<mat-expansion-panel #contentPanel [expanded]='true' class='content-panel'> <mat-expansion-panel #contentPanel [expanded]='true'>
<mat-expansion-panel-header> <mat-expansion-panel-header>
<mat-panel-title> <mat-panel-title>
<mat-icon>format_align_left</mat-icon> <mat-icon>format_align_left</mat-icon>
@ -123,7 +142,7 @@
<div class="desktop-right "> <div class="desktop-right ">
<!-- Content --> <!-- Content -->
<mat-card class="content-panel"> <mat-card>
<ng-container *ngTemplateOutlet="contentTemplate"></ng-container> <ng-container *ngTemplateOutlet="contentTemplate"></ng-container>
</mat-card> </mat-card>
</div> </div>
@ -186,7 +205,7 @@
</div> </div>
<mat-form-field *ngIf="editMotion && !newMotion"> <mat-form-field *ngIf="editMotion && !newMotion">
<mat-select placeholder='State' formControlName='state_id'> <mat-select placeholder='State' formControlName='state_id'>
<mat-option [value]="motionCopy.stateId">{{motionCopy.state}}</mat-option> <mat-option [value]="motionCopy.state_id">{{motionCopy.state}}</mat-option>
<mat-divider></mat-divider> <mat-divider></mat-divider>
<mat-option *ngFor="let state of motionCopy.nextStates" [value]="state.id">{{state}}</mat-option> <mat-option *ngFor="let state of motionCopy.nextStates" [value]="state.id">{{state}}</mat-option>
<mat-divider></mat-divider> <mat-divider></mat-divider>
@ -199,7 +218,7 @@
<!-- Recommendation --> <!-- Recommendation -->
<!-- The suggestion of the work group weather or not a motion should be accepted --> <!-- The suggestion of the work group weather or not a motion should be accepted -->
<div *ngIf='motion && motion.recommender && (motion.recommendationId || editMotion)'> <div *ngIf='motion && motion.recommender && (motion.recommendation_id || editMotion)'>
<div *ngIf='!editMotion'> <div *ngIf='!editMotion'>
<h3>{{motion.recommender}}</h3> <h3>{{motion.recommender}}</h3>
{{motion.recommendation}} {{motion.recommendation}}
@ -219,7 +238,7 @@
</div> </div>
<!-- Category --> <!-- Category -->
<div *ngIf="motion && motion.categoryId || editMotion"> <div *ngIf="motion && motion.category_id || editMotion">
<div *ngIf='!editMotion'> <div *ngIf='!editMotion'>
<h3 translate>Category</h3> <h3 translate>Category</h3>
{{motion.category}} {{motion.category}}
@ -264,37 +283,27 @@
<!-- Title --> <!-- Title -->
<div *ngIf="motion && motion.title || editMotion"> <div *ngIf="motion && motion.title || editMotion">
<div *ngIf='!editMotion'> <div *ngIf='!editMotion'>
<h2>{{motion.title}}</h2> <h4>{{motion.title}}</h4>
</div> </div>
<mat-form-field *ngIf="editMotion" class="wide-form"> <mat-form-field *ngIf="editMotion" class="wide-form">
<input matInput placeholder='Title' formControlName='title' [value]='motionCopy.title'> <input matInput osAutofocus placeholder='Title' formControlName='title' [value]='motionCopy.title'>
</mat-form-field> </mat-form-field>
</div> </div>
<!-- Text --> <!-- Text -->
<!-- TODO: this is a config variable. Read it out --> <!-- TODO: this is a config variable. Read it out -->
<h3 translate>The assembly may decide:</h3> <span class="text-prefix-label" translate>The assembly may decide:</span>
<ng-container *ngIf='motion && !editMotion'> <ng-container *ngIf='motion && !editMotion'>
<div *ngIf="!isRecoModeDiff()" class="motion-text" <div *ngIf="!isRecoModeDiff()" class="motion-text" [class.line-numbers-none]="isLineNumberingNone()"
[class.line-numbers-none]="isLineNumberingNone()" [class.line-numbers-inline]="isLineNumberingInline()" [class.line-numbers-outside]="isLineNumberingOutside()">
[class.line-numbers-inline]="isLineNumberingInline()" <os-motion-detail-original-change-recommendations *ngIf="isLineNumberingOutside() && isRecoModeOriginal()"
[class.line-numbers-outside]="isLineNumberingOutside()"> [html]="getFormattedTextPlain()" [changeRecommendations]="changeRecommendations"
<os-motion-detail-original-change-recommendations (createChangeRecommendation)="createChangeRecommendation($event)" (gotoChangeRecommendation)="gotoChangeRecommendation($event)"></os-motion-detail-original-change-recommendations>
*ngIf="isLineNumberingOutside() && isRecoModeOriginal()"
[html]="getFormattedTextPlain()"
[changeRecommendations]="changeRecommendations"
(createChangeRecommendation)="createChangeRecommendation($event)"
(gotoChangeRecommendation)="gotoChangeRecommendation($event)"
></os-motion-detail-original-change-recommendations>
<div *ngIf="!isLineNumberingOutside() || !isRecoModeOriginal()" [innerHTML]="getFormattedText()"></div> <div *ngIf="!isLineNumberingOutside() || !isRecoModeOriginal()" [innerHTML]="getFormattedText()"></div>
</div> </div>
<os-motion-detail-diff *ngIf="isRecoModeDiff()" <os-motion-detail-diff *ngIf="isRecoModeDiff()" [motion]="motion" [changes]="allChangingObjects"
[motion]="motion" [scrollToChange]="scrollToChange" (createChangeRecommendation)="createChangeRecommendation($event)"></os-motion-detail-diff>
[changes]="allChangingObjects"
[scrollToChange]="scrollToChange"
(createChangeRecommendation)="createChangeRecommendation($event)"
></os-motion-detail-diff>
</ng-container> </ng-container>
<mat-form-field *ngIf="motion && editMotion" class="wide-form"> <mat-form-field *ngIf="motion && editMotion" class="wide-form">
<textarea matInput placeholder='Motion Text' formControlName='text' [value]='motionCopy.text'></textarea> <textarea matInput placeholder='Motion Text' formControlName='text' [value]='motionCopy.text'></textarea>
@ -303,8 +312,8 @@
<!-- Reason --> <!-- Reason -->
<div *ngIf="motion && motion.reason || editMotion"> <div *ngIf="motion && motion.reason || editMotion">
<div *ngIf='!editMotion'> <h5 translate>Reason</h5>
<h4 translate>Reason</h4> <div class="motion-text" *ngIf='!editMotion'>
<div [innerHtml]='motion.reason'></div> <div [innerHtml]='motion.reason'></div>
</div> </div>
<mat-form-field *ngIf="editMotion" class="wide-form"> <mat-form-field *ngIf="editMotion" class="wide-form">

View File

@ -2,9 +2,32 @@ span {
margin: 0; margin: 0;
} }
.extra-controls-slot {
div {
padding: 0px;
button {
.mat-button-wrapper {
display: inherit;
}
font-size: 100%;
}
span {
font-size: 80%;
}
}
}
.motion-title { .motion-title {
padding-left: 20px; padding: 40px;
line-height: 100%; padding-left: 25px;
line-height: 180%;
font-size: 120%;
color: #317796; // TODO: put in theme as $primary
h2 {
margin: 0;
font-weight: normal;
}
} }
.motion-content { .motion-content {
@ -31,9 +54,20 @@ mat-panel-title {
} }
.meta-info-block { .meta-info-block {
form {
div + div {
margin-top: 15px;
}
ul {
margin: 5px;
}
}
h3 { h3 {
display: block; display: block;
margin-top: 12px; //distance between heading and text // padding-top: 0;
margin-top: 0px; //distance between heading and text
margin-bottom: 3px; //distance between heading and text margin-bottom: 3px; //distance between heading and text
font-size: 80%; font-size: 80%;
color: gray; color: gray;
@ -43,10 +77,6 @@ mat-panel-title {
} }
} }
mat-form-field {
margin-top: 12px; //distance between heading and text
}
.mat-form-field-label { .mat-form-field-label {
font-size: 12pt; font-size: 12pt;
color: gray; color: gray;
@ -77,30 +107,42 @@ mat-panel-title {
} }
} }
mat-expansion-panel { .mat-accordion {
display: block;
margin-top: 0px;
}
.mat-expansion-panel {
padding-top: 0;
.expansion-panel-custom-body { .expansion-panel-custom-body {
padding-left: 55px; padding-left: 55px;
} }
} }
.content-panel { .motion-content {
h2 {
display: block;
font-weight: bold;
font-size: 120%;
}
h3 {
display: block;
font-weight: initial;
font-size: 100%;
}
h4 { h4 {
margin: 10px 10px 15px 0;
display: block;
font-weight: bold;
font-size: 110%;
}
h5 {
margin: 15px 10px 10px 0;
display: block; display: block;
font-weight: bold; font-weight: bold;
font-size: 100%; font-size: 100%;
} }
.motion-text {
margin-left: 0px;
}
//the assembly may decide ...
.text-prefix-label {
display: block;
margin: 0 10px 7px 0px;
}
} }
.desktop-view { .desktop-view {
@ -109,7 +151,7 @@ mat-expansion-panel {
float: left; float: left;
.meta-info-desktop { .meta-info-desktop {
padding: 40px 20px 10px 20px; padding-left: 20px;
} }
.personal-note { .personal-note {
@ -156,7 +198,7 @@ mat-expansion-panel {
mat-card { mat-card {
display: inline; display: inline;
margin: 20px; margin: 0px 40px 10px 10px;
} }
} }
} }

View File

@ -22,6 +22,7 @@ import { ChangeRecommendationRepositoryService } from '../../services/change-rec
import { ViewChangeReco } from '../../models/view-change-reco'; import { ViewChangeReco } from '../../models/view-change-reco';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
import { ViewUnifiedChange } from '../../models/view-unified-change'; import { ViewUnifiedChange } from '../../models/view-unified-change';
import { OperatorService } from '../../../../core/services/operator.service';
/** /**
* Component for the motion detail view * Component for the motion detail view
@ -86,6 +87,21 @@ export class MotionDetailComponent extends BaseComponent implements OnInit {
*/ */
public allChangingObjects: ViewUnifiedChange[]; public allChangingObjects: ViewUnifiedChange[];
/**
* Holds all motions. Required to navigate back and forth
*/
public allMotions: ViewMotion[];
/**
* preload the next motion for direct navigation
*/
public nextMotion: ViewMotion;
/**
* preload the previous motion for direct navigation
*/
public previousMotion: ViewMotion;
/** /**
* Subject for the Categories * Subject for the Categories
*/ */
@ -122,6 +138,7 @@ export class MotionDetailComponent extends BaseComponent implements OnInit {
*/ */
public constructor( public constructor(
public vp: ViewportService, public vp: ViewportService,
private op: OperatorService,
private router: Router, private router: Router,
private route: ActivatedRoute, private route: ActivatedRoute,
private formBuilder: FormBuilder, private formBuilder: FormBuilder,
@ -134,29 +151,8 @@ export class MotionDetailComponent extends BaseComponent implements OnInit {
) { ) {
super(); super();
this.createForm(); this.createForm();
this.getMotionByUrl();
if (route.snapshot.url[0] && route.snapshot.url[0].path === 'new') {
this.newMotion = true;
this.editMotion = true;
// Both are (temporarily) necessary until submitter and supporters are implemented
// TODO new Motion and ViewMotion
this.motion = new ViewMotion();
this.motionCopy = new ViewMotion();
} else {
// load existing motion
this.route.params.subscribe(params => {
this.repo.getViewModelObservable(params.id).subscribe(newViewMotion => {
this.motion = newViewMotion;
});
this.changeRecoRepo
.getChangeRecosOfMotionObservable(parseInt(params.id, 10))
.subscribe((recos: ViewChangeReco[]) => {
this.changeRecommendations = recos;
this.recalcUnifiedChanges();
});
});
}
// Initial Filling of the Subjects // Initial Filling of the Subjects
this.submitterObserver = new BehaviorSubject(DS.getAll(User)); this.submitterObserver = new BehaviorSubject(DS.getAll(User));
this.supporterObserver = new BehaviorSubject(DS.getAll(User)); this.supporterObserver = new BehaviorSubject(DS.getAll(User));
@ -192,24 +188,47 @@ export class MotionDetailComponent extends BaseComponent implements OnInit {
}); });
} }
/**
* determine the motion to display using the URL
*/
public getMotionByUrl(): void {
if (this.route.snapshot.url[0] && this.route.snapshot.url[0].path === 'new') {
// creates a new motion
this.newMotion = true;
this.editMotion = true;
this.motion = new ViewMotion();
this.motionCopy = new ViewMotion();
} else {
// load existing motion
this.route.params.subscribe(params => {
this.repo.getViewModelObservable(params.id).subscribe(newViewMotion => {
this.motion = newViewMotion;
});
this.changeRecoRepo
.getChangeRecosOfMotionObservable(parseInt(params.id, 10))
.subscribe((recos: ViewChangeReco[]) => {
this.changeRecommendations = recos;
this.recalcUnifiedChanges();
});
});
}
}
/** /**
* Async load the values of the motion in the Form. * Async load the values of the motion in the Form.
*/ */
public patchForm(formMotion: ViewMotion): void { public patchForm(formMotion: ViewMotion): void {
this.metaInfoForm.patchValue({ const metaInfoPatch = {};
category_id: formMotion.categoryId, Object.keys(this.metaInfoForm.controls).forEach(ctrl => {
supporters_id: formMotion.supporterIds, metaInfoPatch[ctrl] = formMotion[ctrl];
submitters_id: formMotion.submitterIds,
state_id: formMotion.stateId,
recommendation_id: formMotion.recommendationId,
identifier: formMotion.identifier,
origin: formMotion.origin
}); });
this.contentForm.patchValue({ this.metaInfoForm.patchValue(metaInfoPatch);
title: formMotion.title,
text: formMotion.text, const contentPatch = {};
reason: formMotion.reason Object.keys(this.contentForm.controls).forEach(ctrl => {
contentPatch[ctrl] = formMotion[ctrl];
}); });
this.contentForm.patchValue(contentPatch);
} }
/** /**
@ -241,12 +260,11 @@ export class MotionDetailComponent extends BaseComponent implements OnInit {
* The AutoUpdate-Service should see a change once it arrives and show it * The AutoUpdate-Service should see a change once it arrives and show it
* in the list view automatically * in the list view automatically
* *
* TODO: state is not yet saved. Need a special "put" command * TODO: state is not yet saved. Need a special "put" command. Repo should handle this.
*
* TODO: Repo should handle
*/ */
public saveMotion(): void { public saveMotion(): void {
const newMotionValues = { ...this.metaInfoForm.value, ...this.contentForm.value }; const newMotionValues = { ...this.metaInfoForm.value, ...this.contentForm.value };
const fromForm = new Motion(); const fromForm = new Motion();
fromForm.deserialize(newMotionValues); fromForm.deserialize(newMotionValues);
@ -289,36 +307,6 @@ export class MotionDetailComponent extends BaseComponent implements OnInit {
return this.sanitizer.bypassSecurityTrustHtml(this.getFormattedTextPlain()); return this.sanitizer.bypassSecurityTrustHtml(this.getFormattedTextPlain());
} }
/**
* Click on the edit button (pen-symbol)
*/
public editMotionButton(): void {
if (this.editMotion) {
this.saveMotion();
} else {
this.editMotion = true;
this.motionCopy = this.motion.copy();
this.patchForm(this.motionCopy);
if (this.vp.isMobile) {
this.metaInfoPanel.open();
this.contentPanel.open();
}
}
}
/**
* Cancel the editing process
*
* If a new motion was created, return to the list.
*/
public cancelEditMotionButton(): void {
if (this.newMotion) {
this.router.navigate(['./motions/']);
} else {
this.editMotion = false;
}
}
/** /**
* Trigger to delete the motion * Trigger to delete the motion
* *
@ -411,6 +399,73 @@ export class MotionDetailComponent extends BaseComponent implements OnInit {
/** /**
* Init. Does nothing here. * Init. Does nothing here.
* Comes from the head bar
* @param mode
*/ */
public ngOnInit(): void {} public setEditMode(mode: boolean): void {
this.editMotion = mode;
if (mode) {
this.motionCopy = this.motion.copy();
this.patchForm(this.motionCopy);
if (this.vp.isMobile) {
this.metaInfoPanel.open();
this.contentPanel.open();
}
}
if (!mode && this.newMotion) {
this.router.navigate(['./motions/']);
}
}
/**
* Navigates the user to the given ViewMotion
* @param motion target
*/
public navigateToMotion(motion: ViewMotion): void {
this.router.navigate(['../' + motion.id], { relativeTo: this.route });
// update the current motion
this.motion = motion;
this.setSurroundingMotions();
}
/**
* Sets the previous and next motion
*/
public setSurroundingMotions(): void {
const indexOfCurrent = this.allMotions.findIndex(motion => {
return motion === this.motion;
});
if (indexOfCurrent > -1) {
if (indexOfCurrent > 0) {
this.previousMotion = this.allMotions[indexOfCurrent - 1];
} else {
this.previousMotion = null;
}
if (indexOfCurrent < this.allMotions.length - 1) {
this.nextMotion = this.allMotions[indexOfCurrent + 1];
} else {
this.nextMotion = null;
}
}
}
/**
* Determine if the user has the correct requirements to alter the motion
*/
public opCanEdit(): boolean {
return this.op.hasPerms('motions.can_manage');
}
/**
* Init.
*/
public ngOnInit(): void {
this.repo.getViewModelListObservable().subscribe(newMotionList => {
if (newMotionList) {
this.allMotions = newMotionList;
this.setSurroundingMotions();
}
});
}
} }

View File

@ -1,5 +1,15 @@
<os-head-bar appName="Motions" plusButton=true (plusButtonClicked)=onPlusButton() [menuList]=motionMenuList <os-head-bar plusButton=true (plusButtonClicked)=onPlusButton()>
(ellipsisMenuItem)=onEllipsisItem($event)> <!-- Title -->
<div class="title-slot">
<h2 translate>Motions</h2>
</div>
<!-- Menu -->
<div class="menu-slot">
<button type="button" mat-icon-button [matMenuTriggerFor]="motionListMenu">
<mat-icon>more_vert</mat-icon>
</button>
</div>
</os-head-bar> </os-head-bar>
<div class='custom-table-header on-transition-fade'> <div class='custom-table-header on-transition-fade'>
@ -52,3 +62,27 @@
</mat-table> </mat-table>
<mat-paginator class="on-transition-fade" [pageSizeOptions]="[25, 50, 75, 100, 125]"></mat-paginator> <mat-paginator class="on-transition-fade" [pageSizeOptions]="[25, 50, 75, 100, 125]"></mat-paginator>
<mat-menu #motionListMenu="matMenu">
<button mat-menu-item (click)="downloadMotions()">
<mat-icon>archive</mat-icon>
<span translate>Export ...</span>
</button>
<button mat-menu-item routerLink="category">
<mat-icon>device_hub</mat-icon>
<span translate>Categories</span>
</button>
<button mat-menu-item routerLink="comment-section">
<mat-icon>speaker_notes</mat-icon>
<span translate>Comments</span>
</button>
<button mat-menu-item routerLink="statute-paragraphs">
<mat-icon>account_balance</mat-icon>
<span translate>Statute paragrpahs</span>
</button>
</mat-menu>

View File

@ -30,30 +30,6 @@ export class MotionListComponent extends ListViewBaseComponent<ViewMotion> imple
*/ */
public columnsToDisplayFullWidth = ['identifier', 'title', 'meta', 'state']; public columnsToDisplayFullWidth = ['identifier', 'title', 'meta', 'state'];
/**
* content of the ellipsis menu
*/
public motionMenuList = [
{
text: 'Download',
icon: 'save_alt',
action: 'downloadMotions'
},
{
text: 'Categories',
action: 'toCategories'
},
{
text: 'Motion comment sections',
action: 'toMotionCommentSections'
},
{
text: 'Statute paragrpahs',
action: 'toStatuteParagraphs',
perm: 'motions.can_manage'
}
];
/** /**
* Constructor implements title and translation Module. * Constructor implements title and translation Module.
* *
@ -132,27 +108,6 @@ export class MotionListComponent extends ListViewBaseComponent<ViewMotion> imple
this.router.navigate(['./new'], { relativeTo: this.route }); this.router.navigate(['./new'], { relativeTo: this.route });
} }
/**
* navigate to 'motion/category'
*/
public toCategories(): void {
this.router.navigate(['./category'], { relativeTo: this.route });
}
/**
* navigate to 'motion/comment-section'
*/
public toMotionCommentSections(): void {
this.router.navigate(['./comment-section'], { relativeTo: this.route });
}
/**
* navigate to 'motion/statute-paragraphs'
*/
public toStatuteParagraphs(): void {
this.router.navigate(['./statute-paragraphs'], { relativeTo: this.route });
}
/** /**
* Download all motions As PDF and DocX * Download all motions As PDF and DocX
* *

View File

@ -1,5 +1,24 @@
<os-head-bar appName="Statute paragraphs" [plusButton]=true (plusButtonClicked)=onPlusButton() <os-head-bar [nav]="false" [backButton]=true [allowEdit]="false">
[menuList]="menuList" (ellipsisMenuItem)=onEllipsisItem($event)></os-head-bar>
<!-- Title -->
<div class="title-slot">
<h2 translate>Statute paragraphs</h2>
</div>
<!-- Use the menu slot for an add button -->
<div class="menu-slot">
<button type="button" mat-icon-button (click)="onPlusButton()">
<mat-icon>add</mat-icon>
</button>
<button type="button" mat-icon-button [matMenuTriggerFor]="commentMenu">
<mat-icon>more_vert</mat-icon>
</button>
</div>
</os-head-bar>
<div class="head-spacer"></div> <div class="head-spacer"></div>
<mat-card *ngIf="statuteParagraphToCreate"> <mat-card *ngIf="statuteParagraphToCreate">
<mat-card-title translate>Create new statute paragraph</mat-card-title> <mat-card-title translate>Create new statute paragraph</mat-card-title>
@ -15,7 +34,8 @@
</p> </p>
<p> <p>
<mat-form-field> <mat-form-field>
<textarea formControlName="text" matInput placeholder="{{'Statute paragraph' | translate}}" required></textarea> <textarea formControlName="text" matInput placeholder="{{'Statute paragraph' | translate}}"
required></textarea>
<mat-hint *ngIf="!createForm.controls.text.valid"> <mat-hint *ngIf="!createForm.controls.text.valid">
<span translate>Required</span> <span translate>Required</span>
</mat-hint> </mat-hint>
@ -48,7 +68,8 @@
</p> </p>
<p> <p>
<mat-form-field> <mat-form-field>
<textarea formControlName="text" matInput placeholder="{{'Statute paragraph' | translate}}" required></textarea> <textarea formControlName="text" matInput placeholder="{{'Statute paragraph' | translate}}"
required></textarea>
<mat-hint *ngIf="!createForm.controls.text.valid"> <mat-hint *ngIf="!createForm.controls.text.valid">
<span translate>Required</span> <span translate>Required</span>
</mat-hint> </mat-hint>
@ -83,5 +104,14 @@
</mat-expansion-panel> </mat-expansion-panel>
</mat-accordion> </mat-accordion>
<mat-card *ngIf="statuteParagraphs.length === 0"> <mat-card *ngIf="statuteParagraphs.length === 0">
<mat-card-content><div class="noContent" translate>No statute paragraphs yet...</div></mat-card-content> <mat-card-content>
<div class="noContent" translate>No statute paragraphs yet...</div>
</mat-card-content>
</mat-card> </mat-card>
<mat-menu #commentMenu="matMenu">
<button mat-menu-item (click)="sortStatuteParagraphs()">
<mat-icon>sort</mat-icon>
<span translate>Sort ...</span>
</button>
</mat-menu>

View File

@ -9,7 +9,6 @@ import { PromptService } from '../../../../core/services/prompt.service';
import { StatuteParagraph } from '../../../../shared/models/motions/statute-paragraph'; import { StatuteParagraph } from '../../../../shared/models/motions/statute-paragraph';
import { ViewStatuteParagraph } from '../../models/view-statute-paragraph'; import { ViewStatuteParagraph } from '../../models/view-statute-paragraph';
import { StatuteParagraphRepositoryService } from '../../services/statute-paragraph-repository.service'; import { StatuteParagraphRepositoryService } from '../../services/statute-paragraph-repository.service';
import { EllipsisMenuItem } from '../../../../shared/components/head-bar/head-bar.component';
/** /**
* List view for the statute paragraphs. * List view for the statute paragraphs.
@ -20,16 +19,6 @@ import { EllipsisMenuItem } from '../../../../shared/components/head-bar/head-ba
styleUrls: ['./statute-paragraph-list.component.scss'] styleUrls: ['./statute-paragraph-list.component.scss']
}) })
export class StatuteParagraphListComponent extends BaseComponent implements OnInit { export class StatuteParagraphListComponent extends BaseComponent implements OnInit {
/**
* content of the ellipsis menu
*/
public menuList: EllipsisMenuItem[] = [
{
text: 'Sort statute paragraphs',
action: 'sortStatuteParagraphs'
}
];
public statuteParagraphToCreate: StatuteParagraph | null; public statuteParagraphToCreate: StatuteParagraph | null;
/** /**
@ -154,10 +143,8 @@ export class StatuteParagraphListComponent extends BaseComponent implements OnIn
} }
} }
public onEllipsisItem(item: EllipsisMenuItem): void { public sortStatuteParagraphs(): void {
if (item.action === 'sortStatuteParagrpahs') { console.log('Not yet implemented. Depends on other Features');
this.sortStatuteParagrpahs();
}
} }
/** /**

View File

@ -86,7 +86,7 @@ export class ViewMotion extends BaseViewModel {
return this._category; return this._category;
} }
public get categoryId(): number { public get category_id(): number {
return this.motion && this.category ? this.motion.category_id : null; return this.motion && this.category ? this.motion.category_id : null;
} }
@ -94,7 +94,7 @@ export class ViewMotion extends BaseViewModel {
return this._submitters; return this._submitters;
} }
public get submitterIds(): number[] { public get submitters_id(): number[] {
return this.motion ? this.motion.submitters_id : null; return this.motion ? this.motion.submitters_id : null;
} }
@ -102,7 +102,7 @@ export class ViewMotion extends BaseViewModel {
return this._supporters; return this._supporters;
} }
public get supporterIds(): number[] { public get supporters_id(): number[] {
return this.motion ? this.motion.supporters_id : null; return this.motion ? this.motion.supporters_id : null;
} }
@ -114,11 +114,11 @@ export class ViewMotion extends BaseViewModel {
return this._state; return this._state;
} }
public get stateId(): number { public get state_id(): number {
return this.motion && this.motion.state_id ? this.motion.state_id : null; return this.motion && this.motion.state_id ? this.motion.state_id : null;
} }
public get recommendationId(): number { public get recommendation_id(): number {
return this.motion && this.motion.recommendation_id ? this.motion.recommendation_id : null; return this.motion && this.motion.recommendation_id ? this.motion.recommendation_id : null;
} }
@ -134,7 +134,7 @@ export class ViewMotion extends BaseViewModel {
} }
public get recommendation(): WorkflowState { public get recommendation(): WorkflowState {
return this.recommendationId && this.workflow ? this.workflow.getStateById(this.recommendationId) : null; return this.recommendation_id && this.workflow ? this.workflow.getStateById(this.recommendation_id) : null;
} }
public get origin(): string { public get origin(): string {

View File

@ -1,9 +1,9 @@
<mat-sidenav-container autosize class='main-container'> <mat-sidenav-container #siteContainer class='main-container' (backdropClick)="toggleSideNav()">
<mat-sidenav #sideNav [mode]="vp.isMobile ? 'push' : 'side'" [opened]='!vp.isMobile' disableClose='!vp.isMobile' class="side-panel"> <mat-sidenav #sideNav [mode]="vp.isMobile ? 'push' : 'side'" [opened]='!vp.isMobile' disableClose='!vp.isMobile'
class="side-panel">
<mat-toolbar class='nav-toolbar'> <mat-toolbar class='nav-toolbar'>
<!-- logo --> <!-- logo -->
<mat-toolbar-row class='os-logo-container'> <mat-toolbar-row class='os-logo-container' routerLink='/' (click)="toggleSideNav()"></mat-toolbar-row>
</mat-toolbar-row>
</mat-toolbar> </mat-toolbar>
<!-- User Menu --> <!-- User Menu -->
@ -46,44 +46,31 @@
<!-- navigation --> <!-- navigation -->
<mat-nav-list class='main-nav'> <mat-nav-list class='main-nav'>
<span *ngFor="let entry of mainMenuService.entries"> <span *ngFor="let entry of mainMenuService.entries">
<a [@navItemAnim] *osPerms="entry.permission" mat-list-item (click)='toggleSideNav()' <a [@navItemAnim] *osPerms="entry.permission" mat-list-item (click)='toggleSideNav()' [routerLink]='entry.route'
[routerLink]='entry.route' routerLinkActive='active' [routerLinkActiveOptions]="{exact: true}"> routerLinkActive='active' [routerLinkActiveOptions]="{exact: entry.route === '/'}">
<mat-icon>{{entry.icon}}</mat-icon>{{ entry.displayName | translate}} <mat-icon>{{ entry.icon }}</mat-icon>
<span translate>{{ entry.displayName | translate}}</span>
</a> </a>
</span> </span>
<mat-divider></mat-divider> <mat-divider></mat-divider>
<a [@navItemAnim] *osPerms="'core.can_see_projector'" mat-list-item routerLink='/projector' routerLinkActive='active' (click)='toggleSideNav()'> <a [@navItemAnim] *osPerms="'core.can_see_projector'" mat-list-item routerLink='/projector'
routerLinkActive='active' (click)='toggleSideNav()'>
<mat-icon>videocam</mat-icon> <mat-icon>videocam</mat-icon>
<span translate>Projector</span> <span translate>Projector</span>
</a> </a>
</mat-nav-list> </mat-nav-list>
</mat-sidenav> </mat-sidenav>
<div class="content"> <mat-sidenav-content>
<header> <div (touchstart)="swipe($event, 'start')" (touchend)="swipe($event, 'end')" class="content">
<!-- the first toolbar row is (still) a global element <div class="relax">
the second one shall be handled by the apps --> <main [@pageTransition]="o.isActivated ? o.activatedRoute : ''">
<mat-toolbar color='primary'> <router-outlet #o="outlet"></router-outlet>
</main>
<!-- show/hide menu button --> <footer>
<button mat-icon-button *ngIf="vp.isMobile" (click)='sideNav.toggle()'> <os-footer></os-footer>
<mat-icon>menu</mat-icon> </footer>
</button> </div>
<!-- glob search and generic menu on the right -->
<span class='spacer'></span>
<button mat-icon-button (click)='sideNav.toggle()'>
<mat-icon>search</mat-icon>
</button>
</mat-toolbar>
</header>
<div class="relax">
<main [@pageTransition]="o.isActivated ? o.activatedRoute : ''">
<router-outlet #o="outlet"></router-outlet>
</main>
<footer>
<os-footer></os-footer>
</footer>
</div> </div>
</div> </mat-sidenav-content>
</mat-sidenav-container> </mat-sidenav-container>

View File

@ -10,9 +10,11 @@
background-size: contain; background-size: contain;
background-repeat: no-repeat; background-repeat: no-repeat;
background-position: center; background-position: center;
cursor: pointer;
} }
.side-panel { .side-panel {
border: 0;
box-shadow: 3px 0px 10px 0px rgba(0, 0, 0, 0.2); box-shadow: 3px 0px 10px 0px rgba(0, 0, 0, 0.2);
} }

View File

@ -38,7 +38,8 @@
/** make the .user-menu expansion panel look like the nav-toolbar above */ /** make the .user-menu expansion panel look like the nav-toolbar above */
.user-menu { .user-menu {
background-color: mat-color($primary, darker); background: mat-color($primary, darker);
// background-color: mat-color($primary, darker);
color: mat-color($background, raised-button); color: mat-color($background, raised-button);
min-height: 48px; min-height: 48px;
@ -58,6 +59,11 @@
.mat-expansion-indicator:after { .mat-expansion-indicator:after {
color: mat-color($background, raised-button); color: mat-color($background, raised-button);
} }
.mat-expansion-panel-header:hover {
// prevent the panel to become white after collapse
background: mat-color($primary, darker) !important;
}
} }
/** style and align the nav icons the icons*/ /** style and align the nav icons the icons*/

View File

@ -1,5 +1,5 @@
import { Component, OnInit, ViewChild } from '@angular/core'; import { Component, OnInit, ViewChild } from '@angular/core';
import { Router } from '@angular/router'; import { Router, NavigationEnd } 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';
@ -34,6 +34,16 @@ export class SiteComponent extends BaseComponent implements OnInit {
*/ */
public isLoggedIn: boolean; public isLoggedIn: boolean;
/**
* Holds the coordinates where a swipe gesture was used
*/
private swipeCoord?: [number, number];
/**
* Holds the time when the user was swiping
*/
private swipeTime?: number;
/** /**
* Constructor * Constructor
* *
@ -71,10 +81,21 @@ export class SiteComponent extends BaseComponent implements OnInit {
public ngOnInit(): void { public ngOnInit(): void {
this.vp.checkForChange(); this.vp.checkForChange();
// observe the mainMenuService to receive toggle-requests
this.mainMenuService.toggleMenuSubject.subscribe((value: void) => this.toggleSideNav());
// get a translation via code: use the translation service // get a translation via code: use the translation service
// this.translate.get('Motions').subscribe((res: string) => { // this.translate.get('Motions').subscribe((res: string) => {
// console.log('translation of motions in the target language: ' + res); // console.log('translation of motions in the target language: ' + res);
// }); // });
this.router.events.subscribe(event => {
// Scroll to top if accessing a page, not via browser history stack
if (event instanceof NavigationEnd) {
const contentContainer = document.querySelector('.mat-sidenav-content');
contentContainer.scrollTo(0, 0);
}
});
} }
/** /**
@ -123,4 +144,33 @@ export class SiteComponent extends BaseComponent implements OnInit {
public logout(): void { public logout(): void {
this.authService.logout(); this.authService.logout();
} }
/**
* Handle swipes and gestures
*/
public swipe(e: TouchEvent, when: string): void {
const coord: [number, number] = [e.changedTouches[0].pageX, e.changedTouches[0].pageY];
const time = new Date().getTime();
if (when === 'start') {
this.swipeCoord = coord;
this.swipeTime = time;
} else if (when === 'end') {
const direction = [coord[0] - this.swipeCoord[0], coord[1] - this.swipeCoord[1]];
const duration = time - this.swipeTime;
// definition of a "swipe right" gesture to move in the navigation.
// Required mobile view
// works anywhere on the screen, but could be limited
// to the left side of the screen easily if required)
if (
duration < 1000 &&
Math.abs(direction[0]) > 30 && // swipe length to be detected
Math.abs(direction[0]) > Math.abs(direction[1] * 3) && // 30° should be "horizontal enough"
direction[0] > 0 // swipe left to right
) {
this.toggleSideNav();
}
}
}
} }

View File

@ -1,48 +1,27 @@
<mat-toolbar color='primary'> <os-head-bar [nav]="false" [backButton]=true [allowEdit]="true" [editMode]="editGroup" editIcon="add" (editEvent)="setEditMode($event)"
<button *osPerms="'users.can_manage'" (click)='newGroupButton()' class='generic-mini-button on-transition-fade' (saveEvent)="saveGroup()">
mat-mini-fab>
<mat-icon *ngIf="!newGroup">add</mat-icon>
<mat-icon *ngIf="newGroup">cancel</mat-icon>
</button>
<div class="on-transition-fade"> <!-- Title -->
<span translate>Groups</span> <div class="title-slot">
<h2 *ngIf="!editGroup && !newGroup" translate>Groups</h2>
<form *ngIf="editGroup" [formGroup]="groupForm" (ngSubmit)="saveGroup()" (keydown)="keyDownFunction($event)">
<mat-form-field>
<input type="text" matInput osAutofocus required formControlName="name" placeholder="{{ 'New group name' | translate}}">
<mat-error *ngIf="groupForm.invalid" translate>A group name is required</mat-error>
</mat-form-field>
</form>
</div> </div>
<span class='spacer'></span> <!-- remove button button -->
</mat-toolbar> <div class="extra-controls-slot on-transition-fade">
<button *ngIf="editGroup && !newGroup" type="button" mat-button (click)="deleteSelectedGroup()">
<div class="on-transition-fade new-group-form" *ngIf="newGroup">
<form #newGroupForm="ngForm" (ngSubmit)="submitNewGroup(newGroupForm.form)" (keydown)="keyDownFunction($event)">
<mat-form-field>
<input type="text" matInput name="name" ngModel #nameField="ngModel" placeholder="{{ 'New group name' | translate}}">
</mat-form-field>
<button type="submit" mat-mini-fab color="primary">
<mat-icon>save</mat-icon>
</button>
</form>
</div>
<div class="on-transition-fade new-group-form" *ngIf="editGroup">
<form #editGroupForm="ngForm" (ngSubmit)="submitEditedGroup(editGroupForm.form)">
<mat-form-field>
<input type="text" matInput name="name" [(ngModel)]="selectedGroup.name" #nameField="ngModel" placeholder="{{ 'Edit group name' | translate}}">
</mat-form-field>
<button type="submit" mat-mini-fab color="primary">
<mat-icon>save</mat-icon>>
</button>
<button type="button" mat-mini-fab color="warn" (click)="deleteSelectedGroup()" [disabled]="isProtected(selectedGroup)">
<mat-icon>delete</mat-icon> <mat-icon>delete</mat-icon>
<span translate>Delete</span>
</button> </button>
</div>
<button type="button" mat-mini-fab color="primary" (click)="cancelEditing()"> </os-head-bar>
<mat-icon>cancel</mat-icon>
</button>
</form>
</div>
<div class="hint-text on-transition-fade"> <div class="hint-text on-transition-fade">
<span translate>All your changes are saved immediately.</span> <span translate>All your changes are saved immediately.</span>
@ -75,7 +54,7 @@
<mat-cell *matCellDef="let perm"> <mat-cell *matCellDef="let perm">
<div class="inner-table"> <div class="inner-table">
<mat-checkbox *ngIf="group.id !== 2" [checked]="group.hasPermission(perm.value)" <mat-checkbox *ngIf="group.id !== 2" [checked]="group.hasPermission(perm.value)"
(change)='togglePerm(group, perm.value)'></mat-checkbox> (change)='togglePerm(group, perm.value)'></mat-checkbox>
<mat-checkbox *ngIf="group.id === 2" [checked]="true" [disabled]="true"></mat-checkbox> <mat-checkbox *ngIf="group.id === 2" [checked]="true" [disabled]="true"></mat-checkbox>
</div> </div>
</mat-cell> </mat-cell>

View File

@ -1,8 +1,8 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit, ViewChild } 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 { MatTableDataSource } from '@angular/material'; import { MatTableDataSource } from '@angular/material';
import { FormGroup } from '@angular/forms'; import { FormGroup, FormControl, Validators } from '@angular/forms';
import { GroupRepositoryService } from '../../services/group-repository.service'; import { GroupRepositoryService } from '../../services/group-repository.service';
import { ViewGroup } from '../../models/view-group'; import { ViewGroup } from '../../models/view-group';
@ -43,6 +43,9 @@ export class GroupListComponent extends BaseComponent implements OnInit {
*/ */
public selectedGroup: ViewGroup; public selectedGroup: ViewGroup;
@ViewChild('groupForm')
public groupForm: FormGroup;
/** /**
* Constructor * Constructor
* *
@ -55,25 +58,42 @@ export class GroupListComponent extends BaseComponent implements OnInit {
super(titleService, translate); super(titleService, translate);
} }
public setEditMode(mode: boolean, newGroup: boolean = true): void {
this.editGroup = mode;
this.newGroup = newGroup;
if (!mode) {
this.cancelEditing();
}
}
public saveGroup(): void {
if (this.editGroup && this.newGroup) {
this.submitNewGroup();
} else if (this.editGroup && !this.newGroup) {
this.submitEditedGroup();
}
}
/** /**
* Trigger for the new Group button * Select group in head bar
*/ */
public newGroupButton(): void { public selectGroup(group: ViewGroup): void {
this.editGroup = false; this.selectedGroup = group;
this.newGroup = !this.newGroup; this.setEditMode(true, false);
this.groupForm.setValue({ name: this.selectedGroup.name });
} }
/** /**
* Saves a newly created group. * Saves a newly created group.
* @param form form data given by the group * @param form form data given by the group
*/ */
public submitNewGroup(form: FormGroup): void { public submitNewGroup(): void {
if (form.value) { if (this.groupForm.value && this.groupForm.valid) {
this.repo.create(form.value).subscribe(response => { this.repo.create(this.groupForm.value).subscribe(response => {
if (response) { if (response) {
form.reset(); this.groupForm.reset();
// commenting the next line would allow to create multiple groups without reopening the form this.cancelEditing();
this.newGroup = false;
} }
}); });
} }
@ -83,9 +103,9 @@ export class GroupListComponent extends BaseComponent implements OnInit {
* Saves an edited group. * Saves an edited group.
* @param form form data given by the group * @param form form data given by the group
*/ */
public submitEditedGroup(form: FormGroup): void { public submitEditedGroup(): void {
if (form.value) { if (this.groupForm.value && this.groupForm.valid) {
const updateData = new Group({ name: form.value.name }); const updateData = new Group({ name: this.groupForm.value.name });
this.repo.update(updateData, this.selectedGroup).subscribe(response => { this.repo.update(updateData, this.selectedGroup).subscribe(response => {
if (response) { if (response) {
@ -106,16 +126,9 @@ export class GroupListComponent extends BaseComponent implements OnInit {
* Cancel the editing * Cancel the editing
*/ */
public cancelEditing(): void { public cancelEditing(): void {
this.editGroup = false;
}
/**
* Select group in head bar
*/
public selectGroup(group: ViewGroup): void {
this.newGroup = false; this.newGroup = false;
this.selectedGroup = group; this.editGroup = false;
this.editGroup = true; this.groupForm.reset();
} }
/** /**
@ -182,6 +195,7 @@ export class GroupListComponent extends BaseComponent implements OnInit {
*/ */
public ngOnInit(): void { public ngOnInit(): void {
super.setTitle('Groups'); super.setTitle('Groups');
this.groupForm = new FormGroup({ name: new FormControl('', Validators.required) });
this.repo.getViewModelListObservable().subscribe(newViewGroups => { this.repo.getViewModelListObservable().subscribe(newViewGroups => {
if (newViewGroups) { if (newViewGroups) {
this.groups = newViewGroups; this.groups = newViewGroups;

View File

@ -1,43 +1,33 @@
<mat-toolbar color='primary'> <os-head-bar [nav]="false" [backButton]=true [allowEdit]="isAllowed('manage')" [editMode]="editUser" (editEvent)="setEditMode($event)"
(saveEvent)="saveUser()">
<button *osPerms="'users.can_manage';or:ownPage" (click)='editUserButton()' [ngClass]="{'save-button': editUser}" <!-- Title -->
class='generic-mini-button on-transition-fade' mat-mini-fab> <div class="title-slot">
<mat-icon *ngIf='!editUser'>add</mat-icon> <h2 *ngIf='editUser'>
<mat-icon *ngIf='editUser'>check</mat-icon>
</button>
<div class="on-transition-fade">
<div *ngIf='editUser'>
{{personalInfoForm.get('title').value}} {{personalInfoForm.get('title').value}}
{{personalInfoForm.get('first_name').value}} {{personalInfoForm.get('first_name').value}}
{{personalInfoForm.get('last_name').value}} {{personalInfoForm.get('last_name').value}}
</div> </h2>
<div *ngIf='!editUser'> <h2 *ngIf='!editUser'>
{{user.fullName}} {{user.full_name}}
</div> </h2>
</div> </div>
<span class='spacer'></span> <!-- Menu -->
<div class="menu-slot">
<!-- Button on the right--> <button type="button" mat-icon-button [matMenuTriggerFor]="userExtraMenu">
<div *ngIf="editUser">
<button (click)='cancelEditMotionButton()' class='on-transition-fade' color="warn" mat-raised-button>
<span translate>Cancel</span>
<mat-icon class="icon-text-distance">cancel</mat-icon>
</button>
</div>
<div *ngIf="!editUser">
<button class='on-transition-fade' mat-icon-button [matMenuTriggerFor]="userExtraMenu">
<mat-icon>more_vert</mat-icon> <mat-icon>more_vert</mat-icon>
</button> </button>
</div> </div>
<mat-menu #userExtraMenu="matMenu"> <mat-menu #userExtraMenu="matMenu">
<button mat-menu-item class="red-warning-text" (click)='deleteUserButton()' translate>Delete User</button> <button mat-menu-item class="red-warning-text" (click)='deleteUserButton()'>
<mat-icon>delete</mat-icon>
<span translate>Delete</span>
</button>
</mat-menu> </mat-menu>
</os-head-bar>
</mat-toolbar>
<mat-card class="os-card" *osPerms="'users.can_see_name'"> <mat-card class="os-card" *osPerms="'users.can_see_name'">
<form [ngClass]="{'mat-form-field-enabled': editUser}" [formGroup]='personalInfoForm' (ngSubmit)='saveUser()' *ngIf="user"> <form [ngClass]="{'mat-form-field-enabled': editUser}" [formGroup]='personalInfoForm' (ngSubmit)='saveUser()' *ngIf="user">
@ -45,19 +35,20 @@
<div *ngIf='isAllowed("seeName")'> <div *ngIf='isAllowed("seeName")'>
<!-- Title --> <!-- Title -->
<mat-form-field class='form30 distance force-min-with' *ngIf='user.title || editUser && isAllowed("manage")'> <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'> <input type='text' matInput osAutofocus placeholder='{{"Title" | translate}}' formControlName='title'
[value]='user.title'>
</mat-form-field> </mat-form-field>
<!-- First name --> <!-- First name -->
<mat-form-field class='form30 distance force-min-with' *ngIf='user.firstName || editUser && isAllowed("manage")'> <mat-form-field class='form30 distance force-min-with' *ngIf='user.first_name || editUser && isAllowed("manage")'>
<input type='text' matInput placeholder='{{"First Name" | translate}}' formControlName='first_name' <input type='text' matInput placeholder='{{"First Name" | translate}}' formControlName='first_name'
[value]='user.firstName'> [value]='user.first_name'>
</mat-form-field> </mat-form-field>
<!-- Last name --> <!-- Last name -->
<mat-form-field class='form30 force-min-with' *ngIf='user.lastName || editUser && isAllowed("manage")'> <mat-form-field class='form30 force-min-with' *ngIf='user.last_name || editUser && isAllowed("manage")'>
<input type='text' matInput placeholder='{{"Last Name" | translate}}' formControlName='last_name' <input type='text' matInput placeholder='{{"Last Name" | translate}}' formControlName='last_name'
[value]='user.lastName'> [value]='user.last_name'>
</mat-form-field> </mat-form-field>
</div> </div>
@ -74,15 +65,15 @@
<div> <div>
<!-- Strcuture Level --> <!-- Strcuture Level -->
<mat-form-field class='form70 distance' *ngIf='user.structureLevel || editUser && isAllowed("manage")'> <mat-form-field class='form70 distance' *ngIf='user.structure_level || editUser && isAllowed("manage")'>
<input type='text' matInput placeholder='{{"Structure Level" | translate}}' formControlName='structure_level' <input type='text' matInput placeholder='{{"Structure Level" | translate}}' formControlName='structure_level'
[value]='user.structureLevel'> [value]='user.structure_level'>
</mat-form-field> </mat-form-field>
<!-- Partizipant Number --> <!-- Partizipant Number -->
<mat-form-field class='form20 force-min-with' *ngIf='user.participantNumber || editUser && isAllowed("manage")'> <mat-form-field class='form20 force-min-with' *ngIf='user.participant_number || editUser && isAllowed("manage")'>
<input type='text' matInput placeholder='{{"Participant Number" | translate}}' formControlName='number' <input type='text' matInput placeholder='{{"Participant Number" | translate}}' formControlName='number'
[value]='user.participantNumber'> [value]='user.participant_number'>
</mat-form-field> </mat-form-field>
</div> </div>
@ -99,7 +90,7 @@
<!-- Initial Password --> <!-- Initial Password -->
<mat-form-field class='form100'> <mat-form-field class='form100'>
<input matInput placeholder='{{"Initial Password" | translate}}' formControlName='default_password' <input matInput placeholder='{{"Initial Password" | translate}}' formControlName='default_password'
[value]='user.initialPassword'> [value]='user.default_password'>
<mat-hint align="end">Generate</mat-hint> <mat-hint align="end">Generate</mat-hint>
<button type="button" mat-button matSuffix mat-icon-button [disabled]='!newUser' (click)='generatePassword()'> <button type="button" mat-button matSuffix mat-icon-button [disabled]='!newUser' (click)='generatePassword()'>
<mat-icon>sync_problem</mat-icon> <mat-icon>sync_problem</mat-icon>
@ -110,8 +101,8 @@
<div *ngIf='isAllowed("seePersonal")'> <div *ngIf='isAllowed("seePersonal")'>
<!-- About me --> <!-- About me -->
<!-- TODO: Needs Rich Text Editor --> <!-- TODO: Needs Rich Text Editor -->
<mat-form-field class='form100' *ngIf="user.about || editUser"> <mat-form-field class='form100' *ngIf="user.about_me || editUser">
<textarea formControlName='about_me' matInput placeholder='{{"About Me" | translate}}' [value]='user.about'></textarea> <textarea formControlName='about_me' matInput placeholder='{{"About Me" | translate}}' [value]='user.about_me'></textarea>
</mat-form-field> </mat-form-field>
</div> </div>
@ -133,17 +124,17 @@
<div *ngIf='isAllowed("seeExtra")'> <div *ngIf='isAllowed("seeExtra")'>
<!-- Present? --> <!-- Present? -->
<mat-checkbox formControlName='is_present' matTooltip='{{"Designates whether this user is in the room." | translate}} ' <mat-checkbox formControlName='is_present' matTooltip='{{"Designates whether this user is in the room." | translate}} '
[value]='user.isPresent'> [value]='user.is_present'>
<span translate>Is Present</span> <span translate>Is Present</span>
</mat-checkbox> </mat-checkbox>
<!-- Active? --> <!-- 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}}' <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'> [value]='user.is_active'>
<span translate>Is Active</span> <span translate>Is Active</span>
</mat-checkbox> </mat-checkbox>
<!-- Commitee? --> <!-- Commitee? -->
<mat-checkbox formControlName='is_committee' matTooltip='{{"Designates whether this user should be treated as a committee." | translate}}' <mat-checkbox formControlName='is_committee' matTooltip='{{"Designates whether this user should be treated as a committee." | translate}}'
[value]='user.isCommittee'> [value]='user.is_committee'>
<span translate>Is a committee</span> <span translate>Is a committee</span>
</mat-checkbox> </mat-checkbox>
</div> </div>

View File

@ -129,7 +129,7 @@ export class UserDetailComponent implements OnInit {
public loadViewUser(id: number): void { public loadViewUser(id: number): void {
this.repo.getViewModelObservable(id).subscribe(newViewUser => { this.repo.getViewModelObservable(id).subscribe(newViewUser => {
// repo sometimes delivers undefined values // repo sometimes delivers undefined values
// also ensures edition cannot be interrupted by autpupdate // also ensures edition cannot be interrupted by autoupdate
if (newViewUser && !this.editUser) { if (newViewUser && !this.editUser) {
this.user = newViewUser; this.user = newViewUser;
// personalInfoForm is undefined during 'new' and directly after reloading // personalInfoForm is undefined during 'new' and directly after reloading
@ -162,9 +162,10 @@ export class UserDetailComponent implements OnInit {
default_password: [''] default_password: ['']
}); });
// per default disable the whole form: // patch the form only for existing users
if (!this.newUser) {
this.patchFormValues(); this.patchFormValues();
}
} }
/** /**
@ -172,13 +173,11 @@ export class UserDetailComponent implements OnInit {
* And allows async reading * And allows async reading
*/ */
public patchFormValues(): void { public patchFormValues(): void {
this.personalInfoForm.patchValue({ const personalInfoPatch = {};
username: this.user.username, Object.keys(this.personalInfoForm.controls).forEach(ctrl => {
groups_id: this.user.groupIds, personalInfoPatch[ctrl] = this.user[ctrl];
title: this.user.title,
first_name: this.user.firstName,
last_name: this.user.lastName
}); });
this.personalInfoForm.patchValue(personalInfoPatch);
} }
/** /**
@ -238,13 +237,10 @@ export class UserDetailComponent implements OnInit {
* Handler for the generate Password button. * Handler for the generate Password button.
* Generates a password using 8 pseudo-random letters * Generates a password using 8 pseudo-random letters
* from the `characters` const. * from the `characters` const.
*
* Removed the letter 'O' from the alphabet cause it's easy to confuse
* with the number '0'.
*/ */
public generatePassword(): void { public generatePassword(): void {
let pw = ''; let pw = '';
const characters = 'ABCDEFGHIJKLMNPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; const characters = 'abcdefghijkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789';
const amount = 8; const amount = 8;
for (let i = 0; i < amount; i++) { for (let i = 0; i < amount; i++) {
pw += characters.charAt(Math.floor(Math.random() * characters.length)); pw += characters.charAt(Math.floor(Math.random() * characters.length));
@ -263,8 +259,6 @@ export class UserDetailComponent implements OnInit {
response => { response => {
this.newUser = false; this.newUser = false;
this.router.navigate([`./users/${response.id}`]); this.router.navigate([`./users/${response.id}`]);
// this.setEditMode(false);
// this.loadViewUser(response.id);
}, },
error => console.error('Creation of the user failed: ', error.error) error => console.error('Creation of the user failed: ', error.error)
); );
@ -286,25 +280,10 @@ export class UserDetailComponent implements OnInit {
public setEditMode(edit: boolean): void { public setEditMode(edit: boolean): void {
this.editUser = edit; this.editUser = edit;
this.makeFormEditable(edit); this.makeFormEditable(edit);
}
/** // case: abort creation of a new user
* click on the edit button if (this.newUser && !edit) {
*/
public editUserButton(): void {
if (this.editUser) {
this.saveUser();
} else {
this.setEditMode(true);
}
}
public cancelEditMotionButton(): void {
if (this.newUser) {
this.router.navigate(['./users/']); this.router.navigate(['./users/']);
} else {
this.setEditMode(false);
this.loadViewUser(this.user.id);
} }
} }

View File

@ -1,5 +1,15 @@
<os-head-bar appName="Users" plusButton=true (plusButtonClicked)=onPlusButton() [menuList]=userMenuList <os-head-bar plusButton=true (plusButtonClicked)=onPlusButton()>
(ellipsisMenuItem)=onEllipsisItem($event)> <!-- Title -->
<div class="title-slot">
<h2 translate>Users</h2>
</div>
<!-- Menu -->
<div class="menu-slot">
<button type="button" mat-icon-button [matMenuTriggerFor]="userMenu">
<mat-icon>more_vert</mat-icon>
</button>
</div>
</os-head-bar> </os-head-bar>
<div class='custom-table-header on-transition-fade'> <div class='custom-table-header on-transition-fade'>
@ -12,10 +22,11 @@
</div> </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">
<mat-header-cell *matHeaderCellDef mat-sort-header> Name </mat-header-cell> <mat-header-cell *matHeaderCellDef mat-sort-header> Name </mat-header-cell>
<mat-cell *matCellDef="let user"> {{user.fullName}} </mat-cell> <mat-cell *matCellDef="let user"> {{user.full_name}} </mat-cell>
</ng-container> </ng-container>
<!-- prefix column --> <!-- prefix column -->
@ -30,7 +41,7 @@
<br *ngIf="user.groups && user.structureLevel"> <br *ngIf="user.groups && user.structureLevel">
<span *ngIf="user.structureLevel"> <span *ngIf="user.structureLevel">
<mat-icon>flag</mat-icon> <mat-icon>flag</mat-icon>
{{user.structureLevel}} {{user.structure_level}}
</span> </span>
</div> </div>
</mat-cell> </mat-cell>
@ -40,7 +51,7 @@
<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"> <mat-cell *matCellDef="let user">
<div *ngIf="user.isActive"> <div *ngIf="user.is_active">
<mat-icon>check_box</mat-icon> <mat-icon>check_box</mat-icon>
<span translate>Present</span> <span translate>Present</span>
</div> </div>
@ -52,3 +63,20 @@
</mat-table> </mat-table>
<mat-paginator class="on-transition-fade" [pageSizeOptions]="[25, 50, 75, 100, 125]"></mat-paginator> <mat-paginator class="on-transition-fade" [pageSizeOptions]="[25, 50, 75, 100, 125]"></mat-paginator>
<mat-menu #userMenu="matMenu">
<button mat-menu-item *osPerms="'users.can_manage'" routerLink="groups">
<mat-icon>people</mat-icon>
<span translate>Groups</span>
</button>
<button mat-menu-item>
<mat-icon>save_alt</mat-icon>
<span translate>Import ...</span>
</button>
<button mat-menu-item>
<mat-icon>archive</mat-icon>
<span translate>Export ...</span>
</button>
</mat-menu>

View File

@ -17,28 +17,6 @@ import { Router, ActivatedRoute } from '@angular/router';
styleUrls: ['./user-list.component.scss'] styleUrls: ['./user-list.component.scss']
}) })
export class UserListComponent extends ListViewBaseComponent<ViewUser> implements OnInit { export class UserListComponent extends ListViewBaseComponent<ViewUser> implements OnInit {
/**
* content of the ellipsis menu
*/
public userMenuList = [
{
text: 'Groups',
icon: 'people',
action: 'toGroups',
perm: 'users.can_manage'
},
{
text: 'Import',
icon: 'save_alt',
action: 'toGroups'
},
{
text: 'Export',
icon: 'archive',
action: 'toGroups'
}
];
/** /**
* The usual constructor for components * The usual constructor for components
* @param repo the user repository * @param repo the user repository
@ -77,14 +55,6 @@ export class UserListComponent extends ListViewBaseComponent<ViewUser> implement
console.log('click on Import'); console.log('click on Import');
} }
/**
* Navigate to groups page
* TODO: implement
*/
public toGroups(): void {
this.router.navigate(['./groups'], { relativeTo: this.route });
}
/** /**
* Handles the click on a user row * Handles the click on a user row
* @param row selected row * @param row selected row

View File

@ -27,15 +27,15 @@ export class ViewUser extends BaseViewModel {
return this.user ? this.user.title : null; return this.user ? this.user.title : null;
} }
public get firstName(): string { public get first_name(): string {
return this.user ? this.user.first_name : null; return this.user ? this.user.first_name : null;
} }
public get lastName(): string { public get last_name(): string {
return this.user ? this.user.last_name : null; return this.user ? this.user.last_name : null;
} }
public get fullName(): string { public get full_name(): string {
return this.user ? this.user.full_name : null; return this.user ? this.user.full_name : null;
} }
@ -43,15 +43,15 @@ export class ViewUser extends BaseViewModel {
return this.user ? this.user.email : null; return this.user ? this.user.email : null;
} }
public get structureLevel(): string { public get structure_level(): string {
return this.user ? this.user.structure_level : null; return this.user ? this.user.structure_level : null;
} }
public get participantNumber(): string { public get participant_number(): string {
return this.user ? this.user.number : null; return this.user ? this.user.number : null;
} }
public get groupIds(): number[] { public get groups_id(): number[] {
return this.user ? this.user.groups_id : null; return this.user ? this.user.groups_id : null;
} }
@ -64,7 +64,7 @@ export class ViewUser extends BaseViewModel {
} }
} }
public get initialPassword(): string { public get default_password(): string {
return this.user ? this.user.default_password : null; return this.user ? this.user.default_password : null;
} }
@ -72,19 +72,19 @@ export class ViewUser extends BaseViewModel {
return this.user ? this.user.comment : null; return this.user ? this.user.comment : null;
} }
public get isPresent(): boolean { public get is_present(): boolean {
return this.user ? this.user.is_present : null; return this.user ? this.user.is_present : null;
} }
public get isActive(): boolean { public get is_active(): boolean {
return this.user ? this.user.is_active : null; return this.user ? this.user.is_active : null;
} }
public get isCommittee(): boolean { public get is_committee(): boolean {
return this.user ? this.user.is_committee : null; return this.user ? this.user.is_committee : null;
} }
public get about(): string { public get about_me(): string {
return this.user ? this.user.about_me : null; return this.user ? this.user.about_me : null;
} }

View File

@ -63,21 +63,14 @@ export class UserRepositoryService extends BaseRepository<ViewUser, User> {
// collectionString of userData is still empty // collectionString of userData is still empty
newUser.patchValues(userData); newUser.patchValues(userData);
// if the username is not present, delete. // during creation, the server demands that basically nothing must be null.
// The server will generate a one // during the update process, null values are interpreted as delete.
if (!newUser.username) { // therefore, remove "null" values.
delete newUser.username; Object.keys(newUser).forEach(key => {
} if (!newUser[key]) {
delete newUser[key];
// 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); return this.dataSend.createModel(newUser);
} }

View File

@ -35,7 +35,8 @@ $openslides-blue: (
// Generate paletes using: https://material.io/design/color/ // Generate paletes using: https://material.io/design/color/
// default values fir mat-palette: $default: 500, $lighter: 100, $darker: 700. // default values fir mat-palette: $default: 500, $lighter: 100, $darker: 700.
$openslides-primary: mat-palette($openslides-blue); $openslides-primary: mat-palette($openslides-blue);
$openslides-accent: mat-palette($mat-pink, A200, A100, A400); $openslides-accent: mat-palette($mat-blue);
$openslides-warn: mat-palette($mat-red); $openslides-warn: mat-palette($mat-red);
// Create the theme object (a Sass map containing all of the palettes). // Create the theme object (a Sass map containing all of the palettes).

View File

@ -49,8 +49,16 @@ body {
color: rgb(77, 243, 86); color: rgb(77, 243, 86);
} }
// transform text to uppercase. Use on span, p, h, (...)
.upper {
text-transform: uppercase;
}
.red-warning-text { .red-warning-text {
color: red; color: red;
mat-icon {
color: red !important;
}
} }
.icon-text-distance { .icon-text-distance {