Merge pull request #3909 from tsiegleauq/shared-detail-bar

Shared detail bar
This commit is contained in:
Sean 2018-10-23 12:31:02 +02:00 committed by GitHub
commit 454488028f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
44 changed files with 1037 additions and 753 deletions

View File

@ -560,7 +560,7 @@
},
"minimist": {
"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=",
"dev": true
},
@ -1357,35 +1357,6 @@
"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": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz",
@ -10497,6 +10468,11 @@
"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": {
"version": "2.3.0",
"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 { Subject } from 'rxjs';
/**
* This represents one entry in the main menu
@ -41,6 +42,12 @@ export class MainMenuService {
*/
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.
*/
@ -58,4 +65,11 @@ export class MainMenuService {
this._entries.push(...entries);
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';
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', [
transition('* => *', [
/** this will avoid the dom-copy-effect */
query(':enter, :leave', style({ position: 'absolute', width: '100%' }), { optional: true }),
/** keep the dom clean - let all items "just" enter */
query(':enter mat-card', [style({ opacity: 0 })], { optional: true }),
query(':enter .on-transition-fade', [style({ opacity: 0 })], { optional: true }),
query(':enter mat-row', [style({ opacity: 0 })], { optional: true }),
query(':enter mat-expansion-panel', [style({ opacity: 0 })], { optional: true }),
query(':enter mat-card', justEnterDom, { optional: true }),
query(':enter .on-transition-fade', justEnterDom, { optional: true }),
query(':enter mat-row', justEnterDom, { optional: true }),
query(':enter mat-expansion-panel', justEnterDom, { optional: true }),
/** parallel vanishing */
group([
/** animate fade out for the selected components */
query(
':leave .on-transition-fade',
[
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 }
)
query(':leave .on-transition-fade', fadeVanish, { optional: true }),
query(':leave mat-card', fadeVanish, { optional: true }),
query(':leave mat-row', fadeVanish, { optional: true }),
query(':leave mat-expansion-panel', fadeVanish, { optional: true })
]),
/** parallel appearing */
group([
/** animate fade in for the selected components */
query(':enter .on-transition-fade', [style({ opacity: 0 }), animate('0.2s', style({ opacity: 1 }))], {
optional: true
}),
/** how the mat cards enters the scene */
query(
':enter mat-card',
/** stagger = "one after another" with a distance of 50ms" */
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 }
)
query(':enter .on-transition-fade', fadeAppear, { optional: true }),
/** Staggered appearing = "one after another" */
query(':enter mat-card', stagger(50, fadeMoveIn), { optional: true }),
query(':enter mat-row', stagger(30, fadeMoveIn), { optional: true })
// disabled for now. They somehow appear expanded which looks strange
// query(':enter mat-expansion-panel', stagger(30, fadeMoveIn), { optional: true })
])
])
]);
export const navItemAnim = trigger('navItemAnim', [
transition(':enter', [style({ transform: 'translateX(-100%)' }), animate('500ms ease')]),
transition(':leave', [style({ transform: 'translateX(100%)' }), animate('500ms ease')])
]);
const slideIn = [style({ transform: 'translateX(-85%)' }), animate('600ms ease')];
const slideOut = [
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'>
<button *ngIf="plusButton" class='head-button on-transition-fade' (click)=clickPlusButton()
mat-fab>
<mat-icon>add</mat-icon>
</button>
<mat-toolbar color='primary' [ngClass]="{'during-scroll': stickyToolbar}">
<span class='app-name on-transition-fade'>
{{ appName | translate }}
</span>
<mat-toolbar-row [ngClass]="{'hidden-bar': stickyToolbar}">
<!-- Nav menu -->
<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-menu #ellipsisMenu="matMenu">
<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>
<!-- fake mat-toolbar to keep the distance when the real one gets a fixed position -->
<div class="fake-bar" [ngClass]="{'hidden-bar': !stickyToolbar}"></div>

View File

@ -1,4 +1,51 @@
.head-button {
bottom: -30px;
z-index: 100;
.during-scroll {
position: fixed;
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 { Permission } from '../../../core/services/operator.service';
import { Component, Input, Output, EventEmitter, OnInit, NgZone } from '@angular/core';
import { Location } from '@angular/common';
import { Router, ActivatedRoute } from '@angular/router';
import { ScrollDispatcher, CdkScrollable } from '@angular/cdk/scrolling';
import { map } from 'rxjs/operators';
/**
* One entry for the ellipsis menu.
*/
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;
}
import { ViewportService } from '../../../core/services/viewport.service';
import { MainMenuService } from '../../../core/services/main-menu.service';
/**
* 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 `[menuLust]=myArray` and `(ellipsisMenuItem)=myFunction($event)` if a menu is needed
*
* ## Examples:
*
@ -42,33 +23,10 @@ export interface EllipsisMenuItem {
* <os-head-bar
* appName="Files"
* plusButton=true
* [menuList]=myMenu
* (plusButtonClicked)=onPlusButton()
* (ellipsisMenuItem)=onEllipsisItem($event)>
* </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({
selector: 'os-head-bar',
@ -77,24 +35,52 @@ export interface EllipsisMenuItem {
})
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()
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.
*/
@Input()
public plusButton: false;
public plusButton = false;
/**
* If not empty shows a ellipsis menu on the right side
*
* The parent needs to provide a menu, i.e `[menuList]=myMenu`.
* Determine if there should be a back button.
*/
@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
@ -103,28 +89,29 @@ export class HeadBarComponent implements OnInit {
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()
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
*/
public constructor() {}
/**
* empty onInit
*/
public ngOnInit(): void {}
/**
* 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);
}
public constructor(
public vp: ViewportService,
private scrollDispatcher: ScrollDispatcher,
private ngZone: NgZone,
private menu: MainMenuService,
private router: Router,
private route: ActivatedRoute,
private location: Location
) {}
/**
* Emits a signal to the parent if
@ -132,4 +119,71 @@ export class HeadBarComponent implements OnInit {
public clickPlusButton(): void {
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
import { PermsDirective } from './directives/perms.directive';
import { DomChangeDirective } from './directives/dom-change.directive';
import { AutofocusDirective } from './directives/autofocus.directive';
// components
import { HeadBarComponent } from './components/head-bar/head-bar.component';
@ -127,6 +128,7 @@ import { PromptDialogComponent } from './components/prompt-dialog/prompt-dialog.
TranslateModule,
PermsDirective,
DomChangeDirective,
AutofocusDirective,
FooterComponent,
HeadBarComponent,
SearchValueSelectorComponent,
@ -137,6 +139,7 @@ import { PromptDialogComponent } from './components/prompt-dialog/prompt-dialog.
declarations: [
PermsDirective,
DomChangeDirective,
AutofocusDirective,
HeadBarComponent,
FooterComponent,
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>
<!-- title column -->

View File

@ -1,5 +1,15 @@
<os-head-bar appName="Assignments" plusButton=true [menuList]=assignmentMenu (plusButtonClicked)=onPlusButton()
(ellipsisMenuItem)=onEllipsisItem($event)>
<os-head-bar plusButton=true (plusButtonClicked)=onPlusButton()>
<!-- 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>
<mat-table class='os-listview-table on-transition-fade' [dataSource]="dataSource" matSort>
@ -30,3 +40,10 @@
</mat-table>
<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']
})
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.
*

View File

@ -4,7 +4,6 @@ import { Title } from '@angular/platform-browser';
import { TranslateService } from '@ngx-translate/core';
import { MatTableDataSource, MatTable, MatSort, MatPaginator } from '@angular/material';
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 {
/**
@ -49,16 +48,4 @@ export abstract class ListViewBaseComponent<V extends BaseViewModel> extends Bas
this.dataSource.paginator = this.paginator;
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>

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>

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>
<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>
<ng-container *ngFor="let group of this.configs">

View File

@ -2,13 +2,13 @@
<mat-spinner *ngIf="inProcess"></mat-spinner>
<form [formGroup]="loginForm" class="login-form" (ngSubmit)="formLogin()">
<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>
<br>
<mat-form-field>
<input matInput required placeholder="Password" formControlName="password" [type]="!hide ? 'password' : 'text'"
[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-form-field>
@ -21,7 +21,7 @@
<br>
<!-- TODO: Next to each other...-->
<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"
(click)="guestLogin()" translate>Login as Guest</button>
<button mat-raised-button *ngIf="areGuestsEnabled()" color="primary" class='login-button' type="button" (click)="guestLogin()"
translate>Login as Guest</button>
</form>
</div>

View File

@ -1,6 +1,16 @@
<os-head-bar appName="Files" plusButton=true [menuList]=extraMenu (plusButtonClicked)=onPlusButton() (ellipsisMenuItem)=onEllipsisItem($event)>
</os-head-bar>
<os-head-bar plusButton=true (plusButtonClicked)=onPlusButton()>
<!-- 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>
<!-- name column -->
@ -31,3 +41,10 @@
</mat-table>
<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']
})
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
*

View File

@ -1,13 +1,27 @@
<os-head-bar appName="Categories" [plusButton]=true (plusButtonClicked)=onPlusButton()>
</os-head-bar>
<div class='custom-table-header on-transition-fade'>
<button mat-button>
<mat-icon>search</mat-icon>
<os-head-bar [nav]="false" [backButton]=true [allowEdit]="false">
<!-- Title -->
<div class="title-slot">
<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>
</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-expansion-panel [ngClass]="{new: category.id === undefined}" *ngFor="let category of this.dataSource" (opened)="panelOpening('true', category)" (closed)="panelOpening('false', category)"
multiple="false">
<mat-expansion-panel [ngClass]="{new: category.id === undefined}" *ngFor="let category of this.dataSource" (opened)="panelOpening('true', category)"
(closed)="panelOpening('false', category)" multiple="false">
<mat-expansion-panel-header>
<mat-panel-title *ngIf="!category.edit">
{{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>
<div class="head-spacer"></div>
<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>
<form [formGroup]="createForm" (keydown)="keyDownFunction($event)">
<p>
@ -15,11 +29,11 @@
</p>
<p>
<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>
<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>
</form>
</mat-card-content>
@ -29,8 +43,8 @@
</mat-card-actions>
</mat-card>
<mat-accordion class="os-card">
<mat-expansion-panel *ngFor="let section of this.commentSections" (opened)="openId = section.id"
(closed)="panelClosed(section)" [expanded]="openId === section.id" multiple="false">
<mat-expansion-panel *ngFor="let section of this.commentSections" (opened)="openId = section.id" (closed)="panelClosed(section)"
[expanded]="openId === section.id" multiple="false">
<mat-expansion-panel-header>
<mat-panel-title>
<div class="header-container">
@ -66,11 +80,11 @@
</p>
<p>
<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>
<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>
</form>
<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'
mat-mini-fab>
<mat-icon *ngIf="!editMotion">add</mat-icon>
<mat-icon *ngIf="editMotion">check</mat-icon>
</button>
<!-- Title -->
<div class="title-slot">
<h2 *ngIf="motion && !newMotion">
<span translate>Motion</span>
<!-- 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'>
<span *ngIf="newMotion">New </span>
<span translate>Motion</span>
<span *ngIf="motion && !editMotion"> {{motion.identifier}}</span>
<span *ngIf="editMotion && !newMotion"> {{metaInfoForm.get('identifier').value}}</span>
<span>:</span>
<span *ngIf="motion && !editMotion"> {{motion.title}}</span>
<span *ngIf="editMotion"> {{contentForm.get('title').value}}</span>
<br>
<div *ngIf="motion && !newMotion" class='motion-submitter'>
<span translate>by</span> {{motion.submitters}}
<!-- Back and forth buttons-->
<div *ngIf="!editMotion" class="extra-controls-slot on-transition-fade">
<div *ngIf="previousMotion">
<button mat-button (click)="navigateToMotion(previousMotion)">
<mat-icon>navigate_before</mat-icon>
<span>{{ previousMotion.identifier }}</span>
</button>
</div>
<div *ngIf="nextMotion">
<button mat-button (click)="navigateToMotion(nextMotion)">
<span>{{ nextMotion.identifier }}</span>
<mat-icon>navigate_next</mat-icon>
</button>
</div>
</div>
<span class='spacer'></span>
<!-- Button on the right-->
<div *ngIf="editMotion">
<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>
<!-- Menu -->
<div class="menu-slot">
<button type="button" mat-icon-button [matMenuTriggerFor]="motionExtraMenu">
<mat-icon>more_vert</mat-icon>
</button>
</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">
<!-- TODO: the functions for the buttons -->
<button mat-menu-item translate>Export As...</button>
<button mat-menu-item translate>Project</button>
<button mat-menu-item>
<mat-icon>picture_as_pdf</mat-icon>
<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>
<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-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>
@ -50,7 +68,8 @@
<mat-accordion multi='true' class='on-transition-fade'>
<!-- 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-panel-title>
<mat-icon>info</mat-icon>
@ -76,7 +95,7 @@
</mat-expansion-panel>
<!-- Content -->
<mat-expansion-panel #contentPanel [expanded]='true' class='content-panel'>
<mat-expansion-panel #contentPanel [expanded]='true'>
<mat-expansion-panel-header>
<mat-panel-title>
<mat-icon>format_align_left</mat-icon>
@ -123,7 +142,7 @@
<div class="desktop-right ">
<!-- Content -->
<mat-card class="content-panel">
<mat-card>
<ng-container *ngTemplateOutlet="contentTemplate"></ng-container>
</mat-card>
</div>
@ -186,7 +205,7 @@
</div>
<mat-form-field *ngIf="editMotion && !newMotion">
<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-option *ngFor="let state of motionCopy.nextStates" [value]="state.id">{{state}}</mat-option>
<mat-divider></mat-divider>
@ -199,7 +218,7 @@
<!-- Recommendation -->
<!-- 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'>
<h3>{{motion.recommender}}</h3>
{{motion.recommendation}}
@ -219,7 +238,7 @@
</div>
<!-- Category -->
<div *ngIf="motion && motion.categoryId || editMotion">
<div *ngIf="motion && motion.category_id || editMotion">
<div *ngIf='!editMotion'>
<h3 translate>Category</h3>
{{motion.category}}
@ -264,37 +283,27 @@
<!-- Title -->
<div *ngIf="motion && motion.title || editMotion">
<div *ngIf='!editMotion'>
<h2>{{motion.title}}</h2>
<h4>{{motion.title}}</h4>
</div>
<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>
</div>
<!-- Text -->
<!-- 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'>
<div *ngIf="!isRecoModeDiff()" class="motion-text"
[class.line-numbers-none]="isLineNumberingNone()"
[class.line-numbers-inline]="isLineNumberingInline()"
[class.line-numbers-outside]="isLineNumberingOutside()">
<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="!isRecoModeDiff()" class="motion-text" [class.line-numbers-none]="isLineNumberingNone()"
[class.line-numbers-inline]="isLineNumberingInline()" [class.line-numbers-outside]="isLineNumberingOutside()">
<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>
<os-motion-detail-diff *ngIf="isRecoModeDiff()"
[motion]="motion"
[changes]="allChangingObjects"
[scrollToChange]="scrollToChange"
(createChangeRecommendation)="createChangeRecommendation($event)"
></os-motion-detail-diff>
<os-motion-detail-diff *ngIf="isRecoModeDiff()" [motion]="motion" [changes]="allChangingObjects"
[scrollToChange]="scrollToChange" (createChangeRecommendation)="createChangeRecommendation($event)"></os-motion-detail-diff>
</ng-container>
<mat-form-field *ngIf="motion && editMotion" class="wide-form">
<textarea matInput placeholder='Motion Text' formControlName='text' [value]='motionCopy.text'></textarea>
@ -303,8 +312,8 @@
<!-- Reason -->
<div *ngIf="motion && motion.reason || editMotion">
<div *ngIf='!editMotion'>
<h4 translate>Reason</h4>
<h5 translate>Reason</h5>
<div class="motion-text" *ngIf='!editMotion'>
<div [innerHtml]='motion.reason'></div>
</div>
<mat-form-field *ngIf="editMotion" class="wide-form">

View File

@ -2,9 +2,32 @@ span {
margin: 0;
}
.extra-controls-slot {
div {
padding: 0px;
button {
.mat-button-wrapper {
display: inherit;
}
font-size: 100%;
}
span {
font-size: 80%;
}
}
}
.motion-title {
padding-left: 20px;
line-height: 100%;
padding: 40px;
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 {
@ -31,9 +54,20 @@ mat-panel-title {
}
.meta-info-block {
form {
div + div {
margin-top: 15px;
}
ul {
margin: 5px;
}
}
h3 {
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
font-size: 80%;
color: gray;
@ -43,10 +77,6 @@ mat-panel-title {
}
}
mat-form-field {
margin-top: 12px; //distance between heading and text
}
.mat-form-field-label {
font-size: 12pt;
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 {
padding-left: 55px;
}
}
.content-panel {
h2 {
display: block;
font-weight: bold;
font-size: 120%;
}
h3 {
display: block;
font-weight: initial;
font-size: 100%;
}
.motion-content {
h4 {
margin: 10px 10px 15px 0;
display: block;
font-weight: bold;
font-size: 110%;
}
h5 {
margin: 15px 10px 10px 0;
display: block;
font-weight: bold;
font-size: 100%;
}
.motion-text {
margin-left: 0px;
}
//the assembly may decide ...
.text-prefix-label {
display: block;
margin: 0 10px 7px 0px;
}
}
.desktop-view {
@ -109,7 +151,7 @@ mat-expansion-panel {
float: left;
.meta-info-desktop {
padding: 40px 20px 10px 20px;
padding-left: 20px;
}
.personal-note {
@ -156,7 +198,7 @@ mat-expansion-panel {
mat-card {
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 { DomSanitizer, SafeHtml } from '@angular/platform-browser';
import { ViewUnifiedChange } from '../../models/view-unified-change';
import { OperatorService } from '../../../../core/services/operator.service';
/**
* Component for the motion detail view
@ -86,6 +87,21 @@ export class MotionDetailComponent extends BaseComponent implements OnInit {
*/
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
*/
@ -122,6 +138,7 @@ export class MotionDetailComponent extends BaseComponent implements OnInit {
*/
public constructor(
public vp: ViewportService,
private op: OperatorService,
private router: Router,
private route: ActivatedRoute,
private formBuilder: FormBuilder,
@ -134,29 +151,8 @@ export class MotionDetailComponent extends BaseComponent implements OnInit {
) {
super();
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
this.submitterObserver = 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.
*/
public patchForm(formMotion: ViewMotion): void {
this.metaInfoForm.patchValue({
category_id: formMotion.categoryId,
supporters_id: formMotion.supporterIds,
submitters_id: formMotion.submitterIds,
state_id: formMotion.stateId,
recommendation_id: formMotion.recommendationId,
identifier: formMotion.identifier,
origin: formMotion.origin
const metaInfoPatch = {};
Object.keys(this.metaInfoForm.controls).forEach(ctrl => {
metaInfoPatch[ctrl] = formMotion[ctrl];
});
this.contentForm.patchValue({
title: formMotion.title,
text: formMotion.text,
reason: formMotion.reason
this.metaInfoForm.patchValue(metaInfoPatch);
const contentPatch = {};
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
* in the list view automatically
*
* TODO: state is not yet saved. Need a special "put" command
*
* TODO: Repo should handle
* TODO: state is not yet saved. Need a special "put" command. Repo should handle this.
*/
public saveMotion(): void {
const newMotionValues = { ...this.metaInfoForm.value, ...this.contentForm.value };
const fromForm = new Motion();
fromForm.deserialize(newMotionValues);
@ -289,36 +307,6 @@ export class MotionDetailComponent extends BaseComponent implements OnInit {
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
*
@ -411,6 +399,73 @@ export class MotionDetailComponent extends BaseComponent implements OnInit {
/**
* 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
(ellipsisMenuItem)=onEllipsisItem($event)>
<os-head-bar plusButton=true (plusButtonClicked)=onPlusButton()>
<!-- 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>
<div class='custom-table-header on-transition-fade'>
@ -52,3 +62,27 @@
</mat-table>
<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'];
/**
* 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.
*
@ -132,27 +108,6 @@ export class MotionListComponent extends ListViewBaseComponent<ViewMotion> imple
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
*

View File

@ -1,5 +1,24 @@
<os-head-bar appName="Statute paragraphs" [plusButton]=true (plusButtonClicked)=onPlusButton()
[menuList]="menuList" (ellipsisMenuItem)=onEllipsisItem($event)></os-head-bar>
<os-head-bar [nav]="false" [backButton]=true [allowEdit]="false">
<!-- 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>
<mat-card *ngIf="statuteParagraphToCreate">
<mat-card-title translate>Create new statute paragraph</mat-card-title>
@ -15,7 +34,8 @@
</p>
<p>
<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">
<span translate>Required</span>
</mat-hint>
@ -48,7 +68,8 @@
</p>
<p>
<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">
<span translate>Required</span>
</mat-hint>
@ -83,5 +104,14 @@
</mat-expansion-panel>
</mat-accordion>
<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-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 { ViewStatuteParagraph } from '../../models/view-statute-paragraph';
import { StatuteParagraphRepositoryService } from '../../services/statute-paragraph-repository.service';
import { EllipsisMenuItem } from '../../../../shared/components/head-bar/head-bar.component';
/**
* 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']
})
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;
/**
@ -154,10 +143,8 @@ export class StatuteParagraphListComponent extends BaseComponent implements OnIn
}
}
public onEllipsisItem(item: EllipsisMenuItem): void {
if (item.action === 'sortStatuteParagrpahs') {
this.sortStatuteParagrpahs();
}
public sortStatuteParagraphs(): void {
console.log('Not yet implemented. Depends on other Features');
}
/**

View File

@ -86,7 +86,7 @@ export class ViewMotion extends BaseViewModel {
return this._category;
}
public get categoryId(): number {
public get category_id(): number {
return this.motion && this.category ? this.motion.category_id : null;
}
@ -94,7 +94,7 @@ export class ViewMotion extends BaseViewModel {
return this._submitters;
}
public get submitterIds(): number[] {
public get submitters_id(): number[] {
return this.motion ? this.motion.submitters_id : null;
}
@ -102,7 +102,7 @@ export class ViewMotion extends BaseViewModel {
return this._supporters;
}
public get supporterIds(): number[] {
public get supporters_id(): number[] {
return this.motion ? this.motion.supporters_id : null;
}
@ -114,11 +114,11 @@ export class ViewMotion extends BaseViewModel {
return this._state;
}
public get stateId(): number {
public get state_id(): number {
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;
}
@ -134,7 +134,7 @@ export class ViewMotion extends BaseViewModel {
}
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 {

View File

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

View File

@ -10,9 +10,11 @@
background-size: contain;
background-repeat: no-repeat;
background-position: center;
cursor: pointer;
}
.side-panel {
border: 0;
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 */
.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);
min-height: 48px;
@ -58,6 +59,11 @@
.mat-expansion-indicator:after {
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*/

View File

@ -1,5 +1,5 @@
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 { OperatorService } from 'app/core/services/operator.service';
@ -34,6 +34,16 @@ export class SiteComponent extends BaseComponent implements OnInit {
*/
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
*
@ -71,10 +81,21 @@ export class SiteComponent extends BaseComponent implements OnInit {
public ngOnInit(): void {
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
// this.translate.get('Motions').subscribe((res: string) => {
// 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 {
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'>
<button *osPerms="'users.can_manage'" (click)='newGroupButton()' class='generic-mini-button on-transition-fade'
mat-mini-fab>
<mat-icon *ngIf="!newGroup">add</mat-icon>
<mat-icon *ngIf="newGroup">cancel</mat-icon>
</button>
<os-head-bar [nav]="false" [backButton]=true [allowEdit]="true" [editMode]="editGroup" editIcon="add" (editEvent)="setEditMode($event)"
(saveEvent)="saveGroup()">
<div class="on-transition-fade">
<span translate>Groups</span>
<!-- Title -->
<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>
<span class='spacer'></span>
</mat-toolbar>
<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)">
<!-- remove button button -->
<div class="extra-controls-slot on-transition-fade">
<button *ngIf="editGroup && !newGroup" type="button" mat-button (click)="deleteSelectedGroup()">
<mat-icon>delete</mat-icon>
<span translate>Delete</span>
</button>
</div>
<button type="button" mat-mini-fab color="primary" (click)="cancelEditing()">
<mat-icon>cancel</mat-icon>
</button>
</form>
</div>
</os-head-bar>
<div class="hint-text on-transition-fade">
<span translate>All your changes are saved immediately.</span>
@ -75,7 +54,7 @@
<mat-cell *matCellDef="let perm">
<div class="inner-table">
<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>
</div>
</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 { TranslateService } from '@ngx-translate/core';
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 { ViewGroup } from '../../models/view-group';
@ -43,6 +43,9 @@ export class GroupListComponent extends BaseComponent implements OnInit {
*/
public selectedGroup: ViewGroup;
@ViewChild('groupForm')
public groupForm: FormGroup;
/**
* Constructor
*
@ -55,25 +58,42 @@ export class GroupListComponent extends BaseComponent implements OnInit {
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 {
this.editGroup = false;
this.newGroup = !this.newGroup;
public selectGroup(group: ViewGroup): void {
this.selectedGroup = group;
this.setEditMode(true, false);
this.groupForm.setValue({ name: this.selectedGroup.name });
}
/**
* Saves a newly created group.
* @param form form data given by the group
*/
public submitNewGroup(form: FormGroup): void {
if (form.value) {
this.repo.create(form.value).subscribe(response => {
public submitNewGroup(): void {
if (this.groupForm.value && this.groupForm.valid) {
this.repo.create(this.groupForm.value).subscribe(response => {
if (response) {
form.reset();
// commenting the next line would allow to create multiple groups without reopening the form
this.newGroup = false;
this.groupForm.reset();
this.cancelEditing();
}
});
}
@ -83,9 +103,9 @@ export class GroupListComponent extends BaseComponent implements OnInit {
* Saves an edited group.
* @param form form data given by the group
*/
public submitEditedGroup(form: FormGroup): void {
if (form.value) {
const updateData = new Group({ name: form.value.name });
public submitEditedGroup(): void {
if (this.groupForm.value && this.groupForm.valid) {
const updateData = new Group({ name: this.groupForm.value.name });
this.repo.update(updateData, this.selectedGroup).subscribe(response => {
if (response) {
@ -106,16 +126,9 @@ export class GroupListComponent extends BaseComponent implements OnInit {
* Cancel the editing
*/
public cancelEditing(): void {
this.editGroup = false;
}
/**
* Select group in head bar
*/
public selectGroup(group: ViewGroup): void {
this.newGroup = false;
this.selectedGroup = group;
this.editGroup = true;
this.editGroup = false;
this.groupForm.reset();
}
/**
@ -182,6 +195,7 @@ export class GroupListComponent extends BaseComponent implements OnInit {
*/
public ngOnInit(): void {
super.setTitle('Groups');
this.groupForm = new FormGroup({ name: new FormControl('', Validators.required) });
this.repo.getViewModelListObservable().subscribe(newViewGroups => {
if (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}"
class='generic-mini-button on-transition-fade' mat-mini-fab>
<mat-icon *ngIf='!editUser'>add</mat-icon>
<mat-icon *ngIf='editUser'>check</mat-icon>
</button>
<div class="on-transition-fade">
<div *ngIf='editUser'>
<!-- Title -->
<div class="title-slot">
<h2 *ngIf='editUser'>
{{personalInfoForm.get('title').value}}
{{personalInfoForm.get('first_name').value}}
{{personalInfoForm.get('last_name').value}}
</div>
</h2>
<div *ngIf='!editUser'>
{{user.fullName}}
</div>
<h2 *ngIf='!editUser'>
{{user.full_name}}
</h2>
</div>
<span class='spacer'></span>
<!-- Button on the right-->
<div *ngIf="editUser">
<button (click)='cancelEditMotionButton()' class='on-transition-fade' color="warn" mat-raised-button>
<span translate>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">
<!-- Menu -->
<div class="menu-slot">
<button type="button" mat-icon-button [matMenuTriggerFor]="userExtraMenu">
<mat-icon>more_vert</mat-icon>
</button>
</div>
<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-toolbar>
</os-head-bar>
<mat-card class="os-card" *osPerms="'users.can_see_name'">
<form [ngClass]="{'mat-form-field-enabled': editUser}" [formGroup]='personalInfoForm' (ngSubmit)='saveUser()' *ngIf="user">
@ -45,19 +35,20 @@
<div *ngIf='isAllowed("seeName")'>
<!-- Title -->
<mat-form-field class='form30 distance force-min-with' *ngIf='user.title || editUser && isAllowed("manage")'>
<input type='text' matInput placeholder='{{"Title" | translate}}' formControlName='title' [value]='user.title'>
<input type='text' matInput osAutofocus placeholder='{{"Title" | translate}}' formControlName='title'
[value]='user.title'>
</mat-form-field>
<!-- First name -->
<mat-form-field class='form30 distance force-min-with' *ngIf='user.firstName || editUser && isAllowed("manage")'>
<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'
[value]='user.firstName'>
[value]='user.first_name'>
</mat-form-field>
<!-- 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'
[value]='user.lastName'>
[value]='user.last_name'>
</mat-form-field>
</div>
@ -74,15 +65,15 @@
<div>
<!-- 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'
[value]='user.structureLevel'>
[value]='user.structure_level'>
</mat-form-field>
<!-- 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'
[value]='user.participantNumber'>
[value]='user.participant_number'>
</mat-form-field>
</div>
@ -99,7 +90,7 @@
<!-- Initial Password -->
<mat-form-field class='form100'>
<input matInput placeholder='{{"Initial Password" | translate}}' formControlName='default_password'
[value]='user.initialPassword'>
[value]='user.default_password'>
<mat-hint align="end">Generate</mat-hint>
<button type="button" mat-button matSuffix mat-icon-button [disabled]='!newUser' (click)='generatePassword()'>
<mat-icon>sync_problem</mat-icon>
@ -110,8 +101,8 @@
<div *ngIf='isAllowed("seePersonal")'>
<!-- About me -->
<!-- TODO: Needs Rich Text Editor -->
<mat-form-field class='form100' *ngIf="user.about || editUser">
<textarea formControlName='about_me' matInput placeholder='{{"About Me" | translate}}' [value]='user.about'></textarea>
<mat-form-field class='form100' *ngIf="user.about_me || editUser">
<textarea formControlName='about_me' matInput placeholder='{{"About Me" | translate}}' [value]='user.about_me'></textarea>
</mat-form-field>
</div>
@ -133,17 +124,17 @@
<div *ngIf='isAllowed("seeExtra")'>
<!-- Present? -->
<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>
</mat-checkbox>
<!-- Active? -->
<mat-checkbox *osPerms="'users.can_see_extra_data'" formControlName='is_active' matTooltip='{{"Designates whether this user should be treated as active. Unselect this instead of deleting the account." | translate}}'
[value]='user.isActive'>
[value]='user.is_active'>
<span translate>Is Active</span>
</mat-checkbox>
<!-- Commitee? -->
<mat-checkbox formControlName='is_committee' matTooltip='{{"Designates whether this user should be treated as a committee." | translate}}'
[value]='user.isCommittee'>
[value]='user.is_committee'>
<span translate>Is a committee</span>
</mat-checkbox>
</div>

View File

@ -129,7 +129,7 @@ export class UserDetailComponent implements OnInit {
public loadViewUser(id: number): void {
this.repo.getViewModelObservable(id).subscribe(newViewUser => {
// 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) {
this.user = newViewUser;
// personalInfoForm is undefined during 'new' and directly after reloading
@ -162,9 +162,10 @@ export class UserDetailComponent implements OnInit {
default_password: ['']
});
// per default disable the whole form:
this.patchFormValues();
// patch the form only for existing users
if (!this.newUser) {
this.patchFormValues();
}
}
/**
@ -172,13 +173,11 @@ export class UserDetailComponent implements OnInit {
* And allows async reading
*/
public patchFormValues(): void {
this.personalInfoForm.patchValue({
username: this.user.username,
groups_id: this.user.groupIds,
title: this.user.title,
first_name: this.user.firstName,
last_name: this.user.lastName
const personalInfoPatch = {};
Object.keys(this.personalInfoForm.controls).forEach(ctrl => {
personalInfoPatch[ctrl] = this.user[ctrl];
});
this.personalInfoForm.patchValue(personalInfoPatch);
}
/**
@ -238,13 +237,10 @@ export class UserDetailComponent implements OnInit {
* Handler for the generate Password button.
* Generates a password using 8 pseudo-random letters
* from the `characters` const.
*
* Removed the letter 'O' from the alphabet cause it's easy to confuse
* with the number '0'.
*/
public generatePassword(): void {
let pw = '';
const characters = 'ABCDEFGHIJKLMNPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
const characters = 'abcdefghijkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789';
const amount = 8;
for (let i = 0; i < amount; i++) {
pw += characters.charAt(Math.floor(Math.random() * characters.length));
@ -263,8 +259,6 @@ export class UserDetailComponent implements OnInit {
response => {
this.newUser = false;
this.router.navigate([`./users/${response.id}`]);
// this.setEditMode(false);
// this.loadViewUser(response.id);
},
error => console.error('Creation of the user failed: ', error.error)
);
@ -286,25 +280,10 @@ export class UserDetailComponent implements OnInit {
public setEditMode(edit: boolean): void {
this.editUser = edit;
this.makeFormEditable(edit);
}
/**
* click on the edit button
*/
public editUserButton(): void {
if (this.editUser) {
this.saveUser();
} else {
this.setEditMode(true);
}
}
public cancelEditMotionButton(): void {
if (this.newUser) {
// case: abort creation of a new user
if (this.newUser && !edit) {
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
(ellipsisMenuItem)=onEllipsisItem($event)>
<os-head-bar plusButton=true (plusButtonClicked)=onPlusButton()>
<!-- 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>
<div class='custom-table-header on-transition-fade'>
@ -12,10 +22,11 @@
</div>
<mat-table class='os-listview-table on-transition-fade' [dataSource]="dataSource" matSort>
<!-- name column -->
<ng-container matColumnDef="name">
<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>
<!-- prefix column -->
@ -30,7 +41,7 @@
<br *ngIf="user.groups && user.structureLevel">
<span *ngIf="user.structureLevel">
<mat-icon>flag</mat-icon>
{{user.structureLevel}}
{{user.structure_level}}
</span>
</div>
</mat-cell>
@ -40,7 +51,7 @@
<ng-container matColumnDef="presence">
<mat-header-cell *matHeaderCellDef mat-sort-header> Presence </mat-header-cell>
<mat-cell *matCellDef="let user">
<div *ngIf="user.isActive">
<div *ngIf="user.is_active">
<mat-icon>check_box</mat-icon>
<span translate>Present</span>
</div>
@ -52,3 +63,20 @@
</mat-table>
<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']
})
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
* @param repo the user repository
@ -77,14 +55,6 @@ export class UserListComponent extends ListViewBaseComponent<ViewUser> implement
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
* @param row selected row

View File

@ -27,15 +27,15 @@ export class ViewUser extends BaseViewModel {
return this.user ? this.user.title : null;
}
public get firstName(): string {
public get first_name(): string {
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;
}
public get fullName(): string {
public get full_name(): string {
return this.user ? this.user.full_name : null;
}
@ -43,15 +43,15 @@ export class ViewUser extends BaseViewModel {
return this.user ? this.user.email : null;
}
public get structureLevel(): string {
public get structure_level(): string {
return this.user ? this.user.structure_level : null;
}
public get participantNumber(): string {
public get participant_number(): string {
return this.user ? this.user.number : null;
}
public get groupIds(): number[] {
public get groups_id(): number[] {
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;
}
@ -72,19 +72,19 @@ export class ViewUser extends BaseViewModel {
return this.user ? this.user.comment : null;
}
public get isPresent(): boolean {
public get is_present(): boolean {
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;
}
public get isCommittee(): boolean {
public get is_committee(): boolean {
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;
}

View File

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

View File

@ -35,7 +35,8 @@ $openslides-blue: (
// Generate paletes using: https://material.io/design/color/
// default values fir mat-palette: $default: 500, $lighter: 100, $darker: 700.
$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);
// Create the theme object (a Sass map containing all of the palettes).

View File

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