diff --git a/client/package-lock.json b/client/package-lock.json index fb4b4ab03..f9db376a9 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -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", diff --git a/client/src/app/core/services/main-menu.service.ts b/client/src/app/core/services/main-menu.service.ts index cd61bcbe1..0e428683c 100644 --- a/client/src/app/core/services/main-menu.service.ts +++ b/client/src/app/core/services/main-menu.service.ts @@ -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(); + /** * 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(); + } } diff --git a/client/src/app/shared/animations.ts b/client/src/app/shared/animations.ts index fc40c68b8..c41f83bd6 100644 --- a/client/src/app/shared/animations.ts +++ b/client/src/app/shared/animations.ts @@ -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)]); diff --git a/client/src/app/shared/components/head-bar/head-bar.component.html b/client/src/app/shared/components/head-bar/head-bar.component.html index a90aea36f..8ca420546 100644 --- a/client/src/app/shared/components/head-bar/head-bar.component.html +++ b/client/src/app/shared/components/head-bar/head-bar.component.html @@ -1,27 +1,58 @@ - - + - - {{ appName | translate }} - + + + + - + +
+ + + + + + + + + +
+ + +
+
+
+ + +
+ +
+ + + + + + + + + +
+ +
-
- - - - - - - + +
diff --git a/client/src/app/shared/components/head-bar/head-bar.component.scss b/client/src/app/shared/components/head-bar/head-bar.component.scss index f1b01bb55..1ea5877e2 100644 --- a/client/src/app/shared/components/head-bar/head-bar.component.scss +++ b/client/src/app/shared/components/head-bar/head-bar.component.scss @@ -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; + } } diff --git a/client/src/app/shared/components/head-bar/head-bar.component.ts b/client/src/app/shared/components/head-bar/head-bar.component.ts index 8d8cef6d6..eaefb85b1 100644 --- a/client/src/app/shared/components/head-bar/head-bar.component.ts +++ b/client/src/app/shared/components/head-bar/head-bar.component.ts @@ -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 { * * * ``` - * - * ### 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(); /** - * 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(); + public editEvent = new EventEmitter(); + + /** + * Sends a signal if a detail view should be saved + */ + @Output() + public saveEvent = new EventEmitter(); /** * 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; + } + } } diff --git a/client/src/app/shared/directives/autofocus.directive.spec.ts b/client/src/app/shared/directives/autofocus.directive.spec.ts new file mode 100644 index 000000000..ea0d67366 --- /dev/null +++ b/client/src/app/shared/directives/autofocus.directive.spec.ts @@ -0,0 +1,6 @@ +describe('AutofocusDirective', () => { + it('should create an instance', () => { + // const directive = new AutofocusDirective(); + // expect(directive).toBeTruthy(); + }); +}); diff --git a/client/src/app/shared/directives/autofocus.directive.ts b/client/src/app/shared/directives/autofocus.directive.ts new file mode 100644 index 000000000..c399ff006 --- /dev/null +++ b/client/src/app/shared/directives/autofocus.directive.ts @@ -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 + * + * ``` + */ +@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(); + }); + } +} diff --git a/client/src/app/shared/shared.module.ts b/client/src/app/shared/shared.module.ts index 753ece21f..0d5feb8f8 100644 --- a/client/src/app/shared/shared.module.ts +++ b/client/src/app/shared/shared.module.ts @@ -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, diff --git a/client/src/app/site/agenda/agenda-list/agenda-list.component.html b/client/src/app/site/agenda/agenda-list/agenda-list.component.html index bdbbb0964..254dc017e 100644 --- a/client/src/app/site/agenda/agenda-list/agenda-list.component.html +++ b/client/src/app/site/agenda/agenda-list/agenda-list.component.html @@ -1,4 +1,9 @@ - + + +
+

Agenda

+
+
diff --git a/client/src/app/site/assignments/assignment-list/assignment-list.component.html b/client/src/app/site/assignments/assignment-list/assignment-list.component.html index fdf63fa7a..be9f0be07 100644 --- a/client/src/app/site/assignments/assignment-list/assignment-list.component.html +++ b/client/src/app/site/assignments/assignment-list/assignment-list.component.html @@ -1,5 +1,15 @@ - + + +
+

Assignments

+
+ + +
@@ -30,3 +40,10 @@ + + + + diff --git a/client/src/app/site/assignments/assignment-list/assignment-list.component.ts b/client/src/app/site/assignments/assignment-list/assignment-list.component.ts index 09aa13de6..9e782a449 100644 --- a/client/src/app/site/assignments/assignment-list/assignment-list.component.ts +++ b/client/src/app/site/assignments/assignment-list/assignment-list.component.ts @@ -15,18 +15,6 @@ import { AssignmentRepositoryService } from '../services/assignment-repository.s styleUrls: ['./assignment-list.component.css'] }) export class AssignmentListComponent extends ListViewBaseComponent 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. * diff --git a/client/src/app/site/base/list-view-base.ts b/client/src/app/site/base/list-view-base.ts index d684adc27..c20bf18d7 100644 --- a/client/src/app/site/base/list-view-base.ts +++ b/client/src/app/site/base/list-view-base.ts @@ -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 extends BaseComponent { /** @@ -49,16 +48,4 @@ export abstract class ListViewBaseComponent 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](); - } - } } diff --git a/client/src/app/site/common/components/legal-notice/legal-notice.component.html b/client/src/app/site/common/components/legal-notice/legal-notice.component.html index 6fa049b37..3990c71d5 100644 --- a/client/src/app/site/common/components/legal-notice/legal-notice.component.html +++ b/client/src/app/site/common/components/legal-notice/legal-notice.component.html @@ -1,3 +1,7 @@ - + +
+

Legal Notice

+
+
diff --git a/client/src/app/site/common/components/privacy-policy/privacy-policy.component.html b/client/src/app/site/common/components/privacy-policy/privacy-policy.component.html index 98737ff13..5e88a61c5 100644 --- a/client/src/app/site/common/components/privacy-policy/privacy-policy.component.html +++ b/client/src/app/site/common/components/privacy-policy/privacy-policy.component.html @@ -1,3 +1,7 @@ - + +
+

Privacy Policy

+
+
diff --git a/client/src/app/site/common/components/start/start.component.html b/client/src/app/site/common/components/start/start.component.html index d2d9fdf4c..6574489ec 100644 --- a/client/src/app/site/common/components/start/start.component.html +++ b/client/src/app/site/common/components/start/start.component.html @@ -1,4 +1,7 @@ - + +
+

Home

+
diff --git a/client/src/app/site/config/components/config-list/config-list.component.html b/client/src/app/site/config/components/config-list/config-list.component.html index 64e250fed..6f9745ff1 100644 --- a/client/src/app/site/config/components/config-list/config-list.component.html +++ b/client/src/app/site/config/components/config-list/config-list.component.html @@ -1,4 +1,9 @@ - + + +
+

Settings

+
+
diff --git a/client/src/app/site/login/components/login-mask/login-mask.component.html b/client/src/app/site/login/components/login-mask/login-mask.component.html index 2361d97dc..22813cc93 100644 --- a/client/src/app/site/login/components/login-mask/login-mask.component.html +++ b/client/src/app/site/login/components/login-mask/login-mask.component.html @@ -2,13 +2,13 @@ diff --git a/client/src/app/site/mediafiles/mediafile-list/mediafile-list.component.html b/client/src/app/site/mediafiles/mediafile-list/mediafile-list.component.html index f6551c7ff..c298ee037 100644 --- a/client/src/app/site/mediafiles/mediafile-list/mediafile-list.component.html +++ b/client/src/app/site/mediafiles/mediafile-list/mediafile-list.component.html @@ -1,6 +1,16 @@ - - + + +
+

Files

+
+ + +
@@ -31,3 +41,10 @@ + + + + diff --git a/client/src/app/site/mediafiles/mediafile-list/mediafile-list.component.ts b/client/src/app/site/mediafiles/mediafile-list/mediafile-list.component.ts index dfc6249e1..e84cad0c3 100644 --- a/client/src/app/site/mediafiles/mediafile-list/mediafile-list.component.ts +++ b/client/src/app/site/mediafiles/mediafile-list/mediafile-list.component.ts @@ -17,18 +17,6 @@ import { ListViewBaseComponent } from '../../base/list-view-base'; styleUrls: ['./mediafile-list.component.css'] }) export class MediafileListComponent extends ListViewBaseComponent 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 * diff --git a/client/src/app/site/motions/components/category-list/category-list.component.html b/client/src/app/site/motions/components/category-list/category-list.component.html index f34e1ba02..684020695 100644 --- a/client/src/app/site/motions/components/category-list/category-list.component.html +++ b/client/src/app/site/motions/components/category-list/category-list.component.html @@ -1,13 +1,27 @@ - - -
-
+ +
+ +
+ +
- + {{category.name}} diff --git a/client/src/app/site/motions/components/motion-comment-section-list/motion-comment-section-list.component.html b/client/src/app/site/motions/components/motion-comment-section-list/motion-comment-section-list.component.html index 49071fbd4..847d9c25a 100644 --- a/client/src/app/site/motions/components/motion-comment-section-list/motion-comment-section-list.component.html +++ b/client/src/app/site/motions/components/motion-comment-section-list/motion-comment-section-list.component.html @@ -1,8 +1,22 @@ - + + + +
+

Comments

+
+ + + +
+
- Create new comment section + Create new comment field

@@ -15,11 +29,11 @@

+ [multiple]="true" listname="Groups with read permissions" [InputListValues]="this.groups">

+ [multiple]="true" listname="Groups with write permissions" [InputListValues]="this.groups">

@@ -29,8 +43,8 @@
- +
@@ -66,11 +80,11 @@

+ [multiple]="true" listname="Groups with read permissions" [InputListValues]="this.groups">

+ [multiple]="true" listname="Groups with write permissions" [InputListValues]="this.groups">

diff --git a/client/src/app/site/motions/components/motion-detail/motion-detail.component.html b/client/src/app/site/motions/components/motion-detail/motion-detail.component.html index 98f17c7e9..eabda516a 100644 --- a/client/src/app/site/motions/components/motion-detail/motion-detail.component.html +++ b/client/src/app/site/motions/components/motion-detail/motion-detail.component.html @@ -1,48 +1,66 @@ - + - + +
+

+ Motion + +   + {{ motion.identifier }} + {{ metaInfoForm.get("identifier").value }} +

+

+ New motion +

+
-
- New - Motion - {{motion.identifier}} - {{metaInfoForm.get('identifier').value}} - : - {{motion.title}} - {{contentForm.get('title').value}} -
-
- by {{motion.submitters}} + +
+
+ +
+
+
- - -
-
-
- -
- - - - + + - + - + + + +
+

{{ motion.title }}

+

{{ contentForm.get("title").value }}

+
@@ -50,7 +68,8 @@ - + + info @@ -76,7 +95,7 @@ - + format_align_left @@ -123,7 +142,7 @@
- +
@@ -186,7 +205,7 @@
- {{motionCopy.state}} + {{motionCopy.state}} {{state}} @@ -199,7 +218,7 @@ -
+

{{motion.recommender}}

{{motion.recommendation}} @@ -219,7 +238,7 @@
-
+

Category

{{motion.category}} @@ -264,37 +283,27 @@
-

{{motion.title}}

+

{{motion.title}}

- +
-

The assembly may decide:

+ The assembly may decide: -
- +
+
- + @@ -303,8 +312,8 @@
-
-

Reason

+
Reason
+
diff --git a/client/src/app/site/motions/components/motion-detail/motion-detail.component.scss b/client/src/app/site/motions/components/motion-detail/motion-detail.component.scss index 554eaeb49..b15b62ece 100644 --- a/client/src/app/site/motions/components/motion-detail/motion-detail.component.scss +++ b/client/src/app/site/motions/components/motion-detail/motion-detail.component.scss @@ -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; } } } diff --git a/client/src/app/site/motions/components/motion-detail/motion-detail.component.ts b/client/src/app/site/motions/components/motion-detail/motion-detail.component.ts index 7a76be72f..c03d95de8 100644 --- a/client/src/app/site/motions/components/motion-detail/motion-detail.component.ts +++ b/client/src/app/site/motions/components/motion-detail/motion-detail.component.ts @@ -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(); + } + }); + } } diff --git a/client/src/app/site/motions/components/motion-list/motion-list.component.html b/client/src/app/site/motions/components/motion-list/motion-list.component.html index 640a74507..2e79f1024 100644 --- a/client/src/app/site/motions/components/motion-list/motion-list.component.html +++ b/client/src/app/site/motions/components/motion-list/motion-list.component.html @@ -1,5 +1,15 @@ - + + +
+

Motions

+
+ + +
@@ -52,3 +62,27 @@ + + + + + + + + + + + + diff --git a/client/src/app/site/motions/components/motion-list/motion-list.component.ts b/client/src/app/site/motions/components/motion-list/motion-list.component.ts index a8dea75fd..c6554400e 100644 --- a/client/src/app/site/motions/components/motion-list/motion-list.component.ts +++ b/client/src/app/site/motions/components/motion-list/motion-list.component.ts @@ -30,30 +30,6 @@ export class MotionListComponent extends ListViewBaseComponent 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 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 * diff --git a/client/src/app/site/motions/components/statute-paragraph-list/statute-paragraph-list.component.html b/client/src/app/site/motions/components/statute-paragraph-list/statute-paragraph-list.component.html index f8813ebb1..aa8f55ba6 100644 --- a/client/src/app/site/motions/components/statute-paragraph-list/statute-paragraph-list.component.html +++ b/client/src/app/site/motions/components/statute-paragraph-list/statute-paragraph-list.component.html @@ -1,5 +1,24 @@ - + + + +
+

Statute paragraphs

+
+ + + + +
+ +
Create new statute paragraph @@ -15,7 +34,8 @@

- + Required @@ -48,7 +68,8 @@

- + Required @@ -83,5 +104,14 @@ -

+ + +
+ + + + diff --git a/client/src/app/site/motions/components/statute-paragraph-list/statute-paragraph-list.component.ts b/client/src/app/site/motions/components/statute-paragraph-list/statute-paragraph-list.component.ts index e47c7861c..d70ec6801 100644 --- a/client/src/app/site/motions/components/statute-paragraph-list/statute-paragraph-list.component.ts +++ b/client/src/app/site/motions/components/statute-paragraph-list/statute-paragraph-list.component.ts @@ -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'); } /** diff --git a/client/src/app/site/motions/models/view-motion.ts b/client/src/app/site/motions/models/view-motion.ts index 9efe71253..7e8bdbc10 100644 --- a/client/src/app/site/motions/models/view-motion.ts +++ b/client/src/app/site/motions/models/view-motion.ts @@ -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 { diff --git a/client/src/app/site/site.component.html b/client/src/app/site/site.component.html index c1b4aa564..608efee35 100644 --- a/client/src/app/site/site.component.html +++ b/client/src/app/site/site.component.html @@ -1,9 +1,9 @@ - - + + - - + @@ -46,44 +46,31 @@ - - {{entry.icon}}{{ entry.displayName | translate}} + + {{ entry.icon }} + {{ entry.displayName | translate}} + - + videocam Projector -
-
- - - - - - - - - - -
- -
-
- -
-
- -
+ +
+
+
+ +
+
+ +
+
-
+ diff --git a/client/src/app/site/site.component.scss b/client/src/app/site/site.component.scss index e2f2ea3d2..c71b781e8 100644 --- a/client/src/app/site/site.component.scss +++ b/client/src/app/site/site.component.scss @@ -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); } diff --git a/client/src/app/site/site.component.scss-theme.scss b/client/src/app/site/site.component.scss-theme.scss index dfc4c5329..4f3914b20 100644 --- a/client/src/app/site/site.component.scss-theme.scss +++ b/client/src/app/site/site.component.scss-theme.scss @@ -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*/ diff --git a/client/src/app/site/site.component.ts b/client/src/app/site/site.component.ts index 8487f4e19..85d94ec5f 100644 --- a/client/src/app/site/site.component.ts +++ b/client/src/app/site/site.component.ts @@ -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(); + } + } + } } diff --git a/client/src/app/site/users/components/group-list/group-list.component.html b/client/src/app/site/users/components/group-list/group-list.component.html index c006e69b4..63582ec8a 100644 --- a/client/src/app/site/users/components/group-list/group-list.component.html +++ b/client/src/app/site/users/components/group-list/group-list.component.html @@ -1,48 +1,27 @@ - - + -
- Groups + +
+

Groups

+ +
+ + + A group name is required + +
- - - -
-
- - - - - -
-
- -
-
- - - - - - - +
- - -
+
All your changes are saved immediately. @@ -75,7 +54,7 @@
+ (change)='togglePerm(group, perm.value)'>
diff --git a/client/src/app/site/users/components/group-list/group-list.component.ts b/client/src/app/site/users/components/group-list/group-list.component.ts index d11867868..8020f6a79 100644 --- a/client/src/app/site/users/components/group-list/group-list.component.ts +++ b/client/src/app/site/users/components/group-list/group-list.component.ts @@ -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; diff --git a/client/src/app/site/users/components/user-detail/user-detail.component.html b/client/src/app/site/users/components/user-detail/user-detail.component.html index ecde987b2..26497c4fe 100644 --- a/client/src/app/site/users/components/user-detail/user-detail.component.html +++ b/client/src/app/site/users/components/user-detail/user-detail.component.html @@ -1,43 +1,33 @@ - + - - -
-
+ +
+

{{personalInfoForm.get('title').value}} {{personalInfoForm.get('first_name').value}} {{personalInfoForm.get('last_name').value}} -

+ -
- {{user.fullName}} -
+

+ {{user.full_name}} +

- - - -
- -
-
-
- + - - +
@@ -45,19 +35,20 @@
- + - + + [value]='user.first_name'> - + + [value]='user.last_name'>
@@ -74,15 +65,15 @@
- + + [value]='user.structure_level'> - + + [value]='user.participant_number'>
@@ -99,7 +90,7 @@ + [value]='user.default_password'> Generate +
@@ -12,10 +22,11 @@
+ Name - {{user.fullName}} + {{user.full_name}} @@ -30,7 +41,7 @@
flag - {{user.structureLevel}} + {{user.structure_level}}
@@ -40,7 +51,7 @@ Presence -
+
check_box Present
@@ -52,3 +63,20 @@ + + + + + + + + diff --git a/client/src/app/site/users/components/user-list/user-list.component.ts b/client/src/app/site/users/components/user-list/user-list.component.ts index 3a9d5cbf3..0c7eb484b 100644 --- a/client/src/app/site/users/components/user-list/user-list.component.ts +++ b/client/src/app/site/users/components/user-list/user-list.component.ts @@ -17,28 +17,6 @@ import { Router, ActivatedRoute } from '@angular/router'; styleUrls: ['./user-list.component.scss'] }) export class UserListComponent extends ListViewBaseComponent 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 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 diff --git a/client/src/app/site/users/models/view-user.ts b/client/src/app/site/users/models/view-user.ts index d16d07b1e..47939abf7 100644 --- a/client/src/app/site/users/models/view-user.ts +++ b/client/src/app/site/users/models/view-user.ts @@ -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; } diff --git a/client/src/app/site/users/services/user-repository.service.ts b/client/src/app/site/users/services/user-repository.service.ts index 2d14e5c9a..9bdb016d8 100644 --- a/client/src/app/site/users/services/user-repository.service.ts +++ b/client/src/app/site/users/services/user-repository.service.ts @@ -63,21 +63,14 @@ export class UserRepositoryService extends BaseRepository { // 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); } diff --git a/client/src/assets/styles/openslides-theme.scss b/client/src/assets/styles/openslides-theme.scss index f9dfdcf60..4e9500f9c 100644 --- a/client/src/assets/styles/openslides-theme.scss +++ b/client/src/assets/styles/openslides-theme.scss @@ -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). diff --git a/client/src/styles.scss b/client/src/styles.scss index 1824dcae3..cb0e1033d 100644 --- a/client/src/styles.scss +++ b/client/src/styles.scss @@ -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 {