Merge pull request #3909 from tsiegleauq/shared-detail-bar
Shared detail bar
This commit is contained in:
commit
454488028f
36
client/package-lock.json
generated
36
client/package-lock.json
generated
@ -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",
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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)]);
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,6 @@
|
||||
describe('AutofocusDirective', () => {
|
||||
it('should create an instance', () => {
|
||||
// const directive = new AutofocusDirective();
|
||||
// expect(directive).toBeTruthy();
|
||||
});
|
||||
});
|
33
client/src/app/shared/directives/autofocus.directive.ts
Normal file
33
client/src/app/shared/directives/autofocus.directive.ts
Normal 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();
|
||||
});
|
||||
}
|
||||
}
|
@ -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,
|
||||
|
@ -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 -->
|
||||
|
@ -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>
|
||||
|
@ -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.
|
||||
*
|
||||
|
@ -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]();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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">
|
||||
|
@ -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">
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
*
|
||||
|
@ -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}}
|
||||
|
@ -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">
|
||||
|
@ -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> </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">
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
*
|
||||
|
@ -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>
|
||||
|
@ -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');
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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 {
|
||||
|
@ -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>
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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*/
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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).
|
||||
|
@ -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 {
|
||||
|
Loading…
Reference in New Issue
Block a user