Create agenda topics
Also: Enhanced DragNDrop Component List Of Speakers
This commit is contained in:
parent
080b6f52ad
commit
55d279ca4e
@ -28,6 +28,14 @@ export class HttpService {
|
|||||||
*/
|
*/
|
||||||
public constructor(private http: HttpClient, private translate: TranslateService) {}
|
public constructor(private http: HttpClient, private translate: TranslateService) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send the a http request the the given URL.
|
||||||
|
* Optionally accepts a request body.
|
||||||
|
*
|
||||||
|
* @param url the target url, usually starting with /rest
|
||||||
|
* @param method the required HTTP method (i.e get, post, put)
|
||||||
|
* @param data optional, if sending a data body is required
|
||||||
|
*/
|
||||||
private async send<T>(url: string, method: HTTPMethod, data?: any): Promise<T> {
|
private async send<T>(url: string, method: HTTPMethod, data?: any): Promise<T> {
|
||||||
if (!url.endsWith('/')) {
|
if (!url.endsWith('/')) {
|
||||||
url += '/';
|
url += '/';
|
||||||
@ -76,7 +84,7 @@ export class HttpService {
|
|||||||
} else if (e.status === 500) {
|
} else if (e.status === 500) {
|
||||||
error += this.translate.instant('A server error occured. Please contact your system administrator.');
|
error += this.translate.instant('A server error occured. Please contact your system administrator.');
|
||||||
} else if (e.status > 500) {
|
} else if (e.status > 500) {
|
||||||
error += this.translate.instant('The server cound not be reached') + ` (${e.status})`
|
error += this.translate.instant('The server cound not be reached') + ` (${e.status})`;
|
||||||
} else {
|
} else {
|
||||||
error += e.message;
|
error += e.message;
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,17 @@
|
|||||||
<div cdkDropList class="list" (cdkDropListDropped)="drop($event)">
|
<div cdkDropList class="list" (cdkDropListDropped)="drop($event)">
|
||||||
<div class="box line" *ngFor="let item of array" cdkDrag>
|
<div class="box line" *ngFor="let item of array; let i = index" cdkDrag>
|
||||||
<div class="section-one" cdkDragHandle>
|
<div class="section-one" cdkDragHandle>
|
||||||
<mat-icon>drag_handle</mat-icon>
|
<!-- TODO: drag_indicator after icon update -->
|
||||||
|
<mat-icon>unfold_more</mat-icon>
|
||||||
</div>
|
</div>
|
||||||
<div class="section-two">
|
<div class="section-two">
|
||||||
{{item}}
|
<!-- {number}. {item.toString()} -->
|
||||||
|
<span *ngIf="count">{{ i+1 }}. </span>
|
||||||
|
<span>{{ item }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="section-three">
|
||||||
|
<!-- Extra controls slot using implicit template references -->
|
||||||
|
<ng-template [ngTemplateOutlet]="templateRef" [ngTemplateOutletContext]="{ $implicit: item }"></ng-template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -2,23 +2,17 @@
|
|||||||
width: 75%;
|
width: 75%;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
border: solid 1px #ccc;
|
border: solid 1px #ccc;
|
||||||
min-height: 60px;
|
|
||||||
display: block;
|
display: block;
|
||||||
background: white;
|
background: white; // TODO theme
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.box {
|
.box {
|
||||||
padding: 20px 10px;
|
width: 100%;
|
||||||
border-bottom: solid 1px #ccc;
|
border-bottom: solid 1px #ccc;
|
||||||
color: rgba(0, 0, 0, 0.87);
|
color: rgba(0, 0, 0, 0.87);
|
||||||
display: flex;
|
background: white; // TODO theme
|
||||||
flex-direction: row;
|
|
||||||
align-items: left;
|
|
||||||
justify-content: space-between;
|
|
||||||
box-sizing: border-box;
|
|
||||||
background: white;
|
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -34,7 +28,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.cdk-drag-animating {
|
.cdk-drag-animating {
|
||||||
transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
|
transition: transform 125ms ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.box:last-child {
|
.box:last-child {
|
||||||
@ -42,23 +36,30 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.line {
|
.line {
|
||||||
display: grid;
|
display: table;
|
||||||
grid-template-rows: auto;
|
min-height: 60px;
|
||||||
grid-template-columns: 15% 85%;
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
> div {
|
|
||||||
grid-row-start: 1;
|
|
||||||
grid-row-end: span 1;
|
|
||||||
grid-column-end: span 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-one {
|
.section-one {
|
||||||
grid-column-start: 1;
|
display: table-cell;
|
||||||
|
padding: 0 10px;
|
||||||
|
line-height: 0px;
|
||||||
|
vertical-align: middle;
|
||||||
|
width: 50px;
|
||||||
|
color: slategrey;
|
||||||
cursor: move;
|
cursor: move;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-two {
|
.section-two {
|
||||||
grid-column-start: 2;
|
display: table-cell;
|
||||||
|
vertical-align: middle;
|
||||||
|
width: 80%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-three {
|
||||||
|
display: table-cell;
|
||||||
|
padding-right: 10px;
|
||||||
|
vertical-align: middle;
|
||||||
|
width: auto;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Component, OnInit, Input } from '@angular/core';
|
import { Component, OnInit, Input, Output, EventEmitter, ContentChild, TemplateRef } from '@angular/core';
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';
|
import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';
|
||||||
import { Selectable } from '../selectable';
|
import { Selectable } from '../selectable';
|
||||||
@ -15,7 +15,10 @@ import { EmptySelectable } from '../empty-selectable';
|
|||||||
*
|
*
|
||||||
* ```html
|
* ```html
|
||||||
* <os-sorting-list
|
* <os-sorting-list
|
||||||
* [input]="listOfSelectables">
|
* [input]="listOfSelectables"
|
||||||
|
* [live]="true"
|
||||||
|
* [count]="true"
|
||||||
|
* (sortEvent)="onSortingChange($event)">
|
||||||
* </os-sorting-list>
|
* </os-sorting-list>
|
||||||
* ```
|
* ```
|
||||||
*
|
*
|
||||||
@ -27,28 +30,74 @@ import { EmptySelectable } from '../empty-selectable';
|
|||||||
})
|
})
|
||||||
export class SortingListComponent implements OnInit {
|
export class SortingListComponent implements OnInit {
|
||||||
/**
|
/**
|
||||||
* The Input List Values
|
* Sorted and returned
|
||||||
*/
|
*/
|
||||||
@Input()
|
|
||||||
public input: Array<Selectable>;
|
|
||||||
|
|
||||||
public array: Array<Selectable>;
|
public array: Array<Selectable>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Empty constructor
|
* Declare the templateRef to coexist between parent in child
|
||||||
*/
|
*/
|
||||||
public constructor(public translate: TranslateService) {}
|
@ContentChild(TemplateRef)
|
||||||
|
public templateRef: TemplateRef<Selectable>;
|
||||||
|
|
||||||
public ngOnInit(): void {
|
/**
|
||||||
|
* Set to true if events are directly fired after sorting.
|
||||||
|
* usually combined with sortEvent.
|
||||||
|
* Prevents the `@input` from resetting the sorting
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```html
|
||||||
|
* <os-sorting-list ... [live]="true" (sortEvent)="onSortingChange($event)">
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
@Input()
|
||||||
|
public live = false;
|
||||||
|
|
||||||
|
/** Determine whether to put an index number in front of the list */
|
||||||
|
@Input()
|
||||||
|
public count = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Input List Values
|
||||||
|
*
|
||||||
|
* If live updates are enabled, new values are always converted into the sorting array.
|
||||||
|
*
|
||||||
|
* If live updates are disabled, new values are processed when the auto update adds
|
||||||
|
* or removes relevant objects
|
||||||
|
*/
|
||||||
|
@Input()
|
||||||
|
public set input(newValues: Array<Selectable>) {
|
||||||
|
if (newValues) {
|
||||||
|
if (this.array.length !== newValues.length || this.live) {
|
||||||
this.array = [];
|
this.array = [];
|
||||||
if (this.input) {
|
this.array = newValues.map(val => val);
|
||||||
this.input.forEach(inputElement => {
|
} else if (this.array.length === 0) {
|
||||||
this.array.push(inputElement);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this.array.push(new EmptySelectable(this.translate));
|
this.array.push(new EmptySelectable(this.translate));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inform the parent view about sorting.
|
||||||
|
* Alternative approach to submit a new order of elements
|
||||||
|
*/
|
||||||
|
@Output()
|
||||||
|
public sortEvent = new EventEmitter<Array<Selectable>>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor for the sorting list.
|
||||||
|
*
|
||||||
|
* Creates an empty array.
|
||||||
|
* @param translate the translation service
|
||||||
|
*/
|
||||||
|
public constructor(public translate: TranslateService) {
|
||||||
|
this.array = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Required by components using the selector as directive
|
||||||
|
*/
|
||||||
|
public ngOnInit(): void {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* drop event
|
* drop event
|
||||||
@ -56,5 +105,6 @@ export class SortingListComponent implements OnInit {
|
|||||||
*/
|
*/
|
||||||
public drop(event: CdkDragDrop<Selectable[]>): void {
|
public drop(event: CdkDragDrop<Selectable[]>): void {
|
||||||
moveItemInArray(this.array, event.previousIndex, event.currentIndex);
|
moveItemInArray(this.array, event.previousIndex, event.currentIndex);
|
||||||
|
this.sortEvent.emit(this.array);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
import { Deserializer } from '../base/deserializer';
|
import { BaseModel } from '../base/base-model';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Representation of a speaker in an agenda item
|
* Representation of a speaker in an agenda item.
|
||||||
*
|
*
|
||||||
|
* Needs to be a baseModel since it has an own view class.
|
||||||
* Part of the 'speakers' list.
|
* Part of the 'speakers' list.
|
||||||
* @ignore
|
* @ignore
|
||||||
*/
|
*/
|
||||||
export class Speaker extends Deserializer {
|
export class Speaker extends BaseModel {
|
||||||
public id: number;
|
public id: number;
|
||||||
public user_id: number;
|
public user_id: number;
|
||||||
public begin_time: string; // TODO this is a time object
|
public begin_time: string; // TODO this is a time object
|
||||||
@ -22,4 +23,12 @@ export class Speaker extends Deserializer {
|
|||||||
public constructor(input?: any) {
|
public constructor(input?: any) {
|
||||||
super(input);
|
super(input);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Getting the title of a speaker does not make much sense.
|
||||||
|
* Usually it would refer to the title of a user.
|
||||||
|
*/
|
||||||
|
public getTitle(): string {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -25,6 +25,6 @@ export class Topic extends AgendaBaseModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public getDetailStateURL(): string {
|
public getDetailStateURL(): string {
|
||||||
return 'TODO';
|
return `/agenda/topics/${this.id}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -52,6 +52,7 @@ import { SearchValueSelectorComponent } from './components/search-value-selector
|
|||||||
import { OpenSlidesDateAdapter } from './date-adapter';
|
import { OpenSlidesDateAdapter } from './date-adapter';
|
||||||
import { PromptDialogComponent } from './components/prompt-dialog/prompt-dialog.component';
|
import { PromptDialogComponent } from './components/prompt-dialog/prompt-dialog.component';
|
||||||
import { SortingListComponent } from './components/sorting-list/sorting-list.component';
|
import { SortingListComponent } from './components/sorting-list/sorting-list.component';
|
||||||
|
import { SpeakerListComponent } from 'app/site/agenda/components/speaker-list/speaker-list.component';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Share Module for all "dumb" components and pipes.
|
* Share Module for all "dumb" components and pipes.
|
||||||
@ -151,7 +152,8 @@ import { SortingListComponent } from './components/sorting-list/sorting-list.com
|
|||||||
PrivacyPolicyContentComponent,
|
PrivacyPolicyContentComponent,
|
||||||
SearchValueSelectorComponent,
|
SearchValueSelectorComponent,
|
||||||
PromptDialogComponent,
|
PromptDialogComponent,
|
||||||
SortingListComponent
|
SortingListComponent,
|
||||||
|
SpeakerListComponent
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: DateAdapter, useClass: OpenSlidesDateAdapter },
|
{ provide: DateAdapter, useClass: OpenSlidesDateAdapter },
|
||||||
|
@ -1,8 +1,15 @@
|
|||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { Routes, RouterModule } from '@angular/router';
|
import { Routes, RouterModule } from '@angular/router';
|
||||||
import { AgendaListComponent } from './agenda-list/agenda-list.component';
|
import { AgendaListComponent } from './components/agenda-list/agenda-list.component';
|
||||||
|
import { TopicDetailComponent } from './components/topic-detail/topic-detail.component';
|
||||||
|
import { SpeakerListComponent } from './components/speaker-list/speaker-list.component';
|
||||||
|
|
||||||
const routes: Routes = [{ path: '', component: AgendaListComponent }];
|
const routes: Routes = [
|
||||||
|
{ path: '', component: AgendaListComponent },
|
||||||
|
{ path: 'topics/new', component: TopicDetailComponent },
|
||||||
|
{ path: 'topics/:id', component: TopicDetailComponent },
|
||||||
|
{ path: ':id/speakers', component: SpeakerListComponent }
|
||||||
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [RouterModule.forChild(routes)],
|
imports: [RouterModule.forChild(routes)],
|
||||||
|
@ -3,10 +3,14 @@ import { CommonModule } from '@angular/common';
|
|||||||
|
|
||||||
import { AgendaRoutingModule } from './agenda-routing.module';
|
import { AgendaRoutingModule } from './agenda-routing.module';
|
||||||
import { SharedModule } from '../../shared/shared.module';
|
import { SharedModule } from '../../shared/shared.module';
|
||||||
import { AgendaListComponent } from './agenda-list/agenda-list.component';
|
import { AgendaListComponent } from './components/agenda-list/agenda-list.component';
|
||||||
|
import { TopicDetailComponent } from './components/topic-detail/topic-detail.component';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AppModule for the agenda and it's children.
|
||||||
|
*/
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [CommonModule, AgendaRoutingModule, SharedModule],
|
imports: [CommonModule, AgendaRoutingModule, SharedModule],
|
||||||
declarations: [AgendaListComponent]
|
declarations: [AgendaListComponent, TopicDetailComponent]
|
||||||
})
|
})
|
||||||
export class AgendaModule {}
|
export class AgendaModule {}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
import { AgendaListComponent } from './agenda-list.component';
|
import { AgendaListComponent } from './agenda-list.component';
|
||||||
import { E2EImportsModule } from '../../../../e2e-imports.module';
|
import { E2EImportsModule } from '../../../../../e2e-imports.module';
|
||||||
|
|
||||||
describe('AgendaListComponent', () => {
|
describe('AgendaListComponent', () => {
|
||||||
let component: AgendaListComponent;
|
let component: AgendaListComponent;
|
@ -1,12 +1,13 @@
|
|||||||
import { Component, OnInit } from '@angular/core';
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { Router, ActivatedRoute } from '@angular/router';
|
||||||
import { Title } from '@angular/platform-browser';
|
import { Title } from '@angular/platform-browser';
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
|
||||||
import { ViewItem } from '../models/view-item';
|
|
||||||
import { ListViewBaseComponent } from '../../base/list-view-base';
|
|
||||||
import { AgendaRepositoryService } from '../services/agenda-repository.service';
|
|
||||||
import { Router } from '@angular/router';
|
|
||||||
import { MatSnackBar } from '@angular/material';
|
import { MatSnackBar } from '@angular/material';
|
||||||
|
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
import { ViewItem } from '../../models/view-item';
|
||||||
|
import { ListViewBaseComponent } from 'app/site/base/list-view-base';
|
||||||
|
import { AgendaRepositoryService } from '../../services/agenda-repository.service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List view for the agenda.
|
* List view for the agenda.
|
||||||
*
|
*
|
||||||
@ -20,16 +21,18 @@ import { MatSnackBar } from '@angular/material';
|
|||||||
export class AgendaListComponent extends ListViewBaseComponent<ViewItem> implements OnInit {
|
export class AgendaListComponent extends ListViewBaseComponent<ViewItem> implements OnInit {
|
||||||
/**
|
/**
|
||||||
* The usual constructor for components
|
* The usual constructor for components
|
||||||
* @param titleService
|
* @param titleService Setting the browser tab title
|
||||||
* @param translate
|
* @param translate translations
|
||||||
* @param matSnackBar
|
* @param matSnackBar Shows errors and messages
|
||||||
* @param router
|
* @param route Angulars ActivatedRoute
|
||||||
* @param repo
|
* @param router Angulars router
|
||||||
|
* @param repo the agenda repository
|
||||||
*/
|
*/
|
||||||
public constructor(
|
public constructor(
|
||||||
titleService: Title,
|
titleService: Title,
|
||||||
translate: TranslateService,
|
translate: TranslateService,
|
||||||
matSnackBar: MatSnackBar,
|
matSnackBar: MatSnackBar,
|
||||||
|
private route: ActivatedRoute,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
private repo: AgendaRepositoryService
|
private repo: AgendaRepositoryService
|
||||||
) {
|
) {
|
||||||
@ -51,13 +54,14 @@ export class AgendaListComponent extends ListViewBaseComponent<ViewItem> impleme
|
|||||||
/**
|
/**
|
||||||
* Handler for click events on agenda item rows
|
* Handler for click events on agenda item rows
|
||||||
* Links to the content object if any
|
* Links to the content object if any
|
||||||
|
*
|
||||||
|
* Gets content object from the repository rather than from the model
|
||||||
|
* to avoid race conditions
|
||||||
|
* @param item the item that was selected from the list view
|
||||||
*/
|
*/
|
||||||
public selectAgendaItem(item: ViewItem): void {
|
public selectAgendaItem(item: ViewItem): void {
|
||||||
if (item.contentObject) {
|
const contentObject = this.repo.getContentObject(item.item);
|
||||||
this.router.navigate([item.contentObject.getDetailStateURL()]);
|
this.router.navigate([contentObject.getDetailStateURL()]);
|
||||||
} else {
|
|
||||||
console.error(`The selected item ${item} has no content object`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -65,6 +69,6 @@ export class AgendaListComponent extends ListViewBaseComponent<ViewItem> impleme
|
|||||||
* Comes from the HeadBar Component
|
* Comes from the HeadBar Component
|
||||||
*/
|
*/
|
||||||
public onPlusButton(): void {
|
public onPlusButton(): void {
|
||||||
console.log('create new motion');
|
this.router.navigate(['topics/new'], { relativeTo: this.route });
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -0,0 +1,116 @@
|
|||||||
|
<os-head-bar [nav]="false" [goBack]="true">
|
||||||
|
<!-- Title -->
|
||||||
|
<div class="title-slot">
|
||||||
|
<h2>
|
||||||
|
<span *ngIf="viewItem">{{ viewItem.getTitle() }}:</span> <span translate> List of speakers </span>
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
</os-head-bar>
|
||||||
|
|
||||||
|
<mat-card class="speaker-card">
|
||||||
|
<!-- List of finished speakers -->
|
||||||
|
<mat-expansion-panel *ngIf="finishedSpeakers && finishedSpeakers.length > 0" class="finished-list">
|
||||||
|
<mat-expansion-panel-header>
|
||||||
|
<mat-panel-title translate> Last speakers </mat-panel-title>
|
||||||
|
</mat-expansion-panel-header>
|
||||||
|
<mat-list>
|
||||||
|
<!-- {Number}. {full_name} {time} minutes (Start time: {begin_time}) [close button] -->
|
||||||
|
<mat-list-item *ngFor="let speaker of finishedSpeakers; let number = index">
|
||||||
|
<div class="finished-prefix">
|
||||||
|
<span>{{ number + 1 }}</span> <span>. {{ speaker }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="finished-suffix">
|
||||||
|
<!-- TODO: No Date or time class yet -->
|
||||||
|
<!-- <span> calc time here </span> -->
|
||||||
|
<!-- <span translate>minutes</span> -->
|
||||||
|
<span> (</span> <span translate>Start time</span> <span>: {{ speaker.begin_time }})</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
mat-stroked-button
|
||||||
|
matTooltip="{{ 'Remove' | translate }}"
|
||||||
|
*osPerms="'agenda.can_manage_list_of_speakers'"
|
||||||
|
(click)="onDeleteButton(speaker)"
|
||||||
|
>
|
||||||
|
<mat-icon>close</mat-icon>
|
||||||
|
</button>
|
||||||
|
</mat-list-item>
|
||||||
|
</mat-list>
|
||||||
|
</mat-expansion-panel>
|
||||||
|
|
||||||
|
<!-- Current Speaker -->
|
||||||
|
<div class="current-speaker" *ngIf="activeSpeaker">
|
||||||
|
<mat-icon class="speaking-icon">play_arrow</mat-icon>
|
||||||
|
<span class="speaking-name">{{ activeSpeaker }}</span>
|
||||||
|
|
||||||
|
<button
|
||||||
|
mat-stroked-button
|
||||||
|
matTooltip="{{ 'End speech' | translate }}"
|
||||||
|
*osPerms="'agenda.can_manage_list_of_speakers'"
|
||||||
|
(click)="onStopButton()"
|
||||||
|
>
|
||||||
|
<mat-icon>mic_off</mat-icon>
|
||||||
|
<span translate>Stop</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Waiting speakers -->
|
||||||
|
<div *osPerms="'agenda.can_manage_list_of_speakers'">
|
||||||
|
<div class="waiting-list" *ngIf="speakers && speakers.length > 0">
|
||||||
|
<os-sorting-list [input]="speakers" [live]="true" [count]="true" (sortEvent)="onSortingChange($event)">
|
||||||
|
<!-- implicit item references into the component using ng-template slot -->
|
||||||
|
<ng-template let-item>
|
||||||
|
<div class="speak-action-buttons">
|
||||||
|
<mat-button-toggle-group>
|
||||||
|
<mat-button-toggle
|
||||||
|
matTooltip="{{ 'Begin speech' | translate }}"
|
||||||
|
(click)="onStartButton(item)"
|
||||||
|
>
|
||||||
|
<mat-icon>mic</mat-icon>
|
||||||
|
<span translate>Start</span>
|
||||||
|
</mat-button-toggle>
|
||||||
|
<mat-button-toggle
|
||||||
|
matTooltip="{{ 'Mark speaker' | translate }}"
|
||||||
|
(click)="onMarkButton(item)"
|
||||||
|
>
|
||||||
|
<mat-icon>{{ item.marked ? 'star' : 'star_border' }}</mat-icon>
|
||||||
|
</mat-button-toggle>
|
||||||
|
<mat-button-toggle matTooltip="{{ 'Remove' | translate }}" (click)="onDeleteButton(item)">
|
||||||
|
<mat-icon>close</mat-icon>
|
||||||
|
</mat-button-toggle>
|
||||||
|
</mat-button-toggle-group>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
</os-sorting-list>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Search for speakers -->
|
||||||
|
<div *osPerms="'agenda.can_manage_list_of_speakers'">
|
||||||
|
<form *ngIf="users && users.value.length > 0" [formGroup]="addSpeakerForm">
|
||||||
|
<os-search-value-selector
|
||||||
|
class="search-users"
|
||||||
|
ngDefaultControl
|
||||||
|
[form]="addSpeakerForm"
|
||||||
|
[formControl]="addSpeakerForm.get('user_id')"
|
||||||
|
[multiple]="false"
|
||||||
|
listname="{{ 'Select or search new speaker ...' | translate }}"
|
||||||
|
[InputListValues]="users"
|
||||||
|
></os-search-value-selector>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Add me and remove me if OP has correct permission -->
|
||||||
|
<div *osPerms="'agenda.can_be_speaker'" class="add-self-buttons">
|
||||||
|
<div *ngIf="speakers">
|
||||||
|
<button mat-raised-button (click)="addNewSpeaker()" *ngIf="!isOpInList()">
|
||||||
|
<mat-icon>add</mat-icon>
|
||||||
|
<span translate>Add me</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button mat-raised-button (click)="onDeleteButton()" *ngIf="isOpInList()">
|
||||||
|
<mat-icon>remove</mat-icon>
|
||||||
|
<span translate>Remove me</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</mat-card>
|
@ -0,0 +1,63 @@
|
|||||||
|
.speaker-card {
|
||||||
|
margin: 0 20px 0 20px;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
.finished-list {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
.finished-suffix {
|
||||||
|
color: slategray;
|
||||||
|
font-size: 80%;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-list-item {
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.current-speaker {
|
||||||
|
padding: 10px 25px 15px 25px;
|
||||||
|
display: table;
|
||||||
|
.speaking-icon {
|
||||||
|
display: table-cell;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.speaking-name {
|
||||||
|
display: table-cell;
|
||||||
|
vertical-align: middle;
|
||||||
|
font-weight: bold;
|
||||||
|
padding-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
display: table-cell;
|
||||||
|
vertical-align: middle;
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.waiting-list {
|
||||||
|
padding: 10px 25px 0 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
form {
|
||||||
|
padding: 15px 25px 10px 25px;
|
||||||
|
width: auto;
|
||||||
|
|
||||||
|
.search-users {
|
||||||
|
display: grid;
|
||||||
|
.mat-form-field {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-self-buttons {
|
||||||
|
padding: 0 0 20px 25px;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,25 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { SpeakerListComponent } from './speaker-list.component';
|
||||||
|
import { E2EImportsModule } from '../../../../../e2e-imports.module';
|
||||||
|
|
||||||
|
describe('SpeakerListComponent', () => {
|
||||||
|
let component: SpeakerListComponent;
|
||||||
|
let fixture: ComponentFixture<SpeakerListComponent>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [E2EImportsModule]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(SpeakerListComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,171 @@
|
|||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
import { ViewSpeaker, SpeakerState } from '../../models/view-speaker';
|
||||||
|
import { User } from 'app/shared/models/users/user';
|
||||||
|
import { FormGroup, FormControl } from '@angular/forms';
|
||||||
|
import { BehaviorSubject } from 'rxjs';
|
||||||
|
import { DataStoreService } from 'app/core/services/data-store.service';
|
||||||
|
import { AgendaRepositoryService } from '../../services/agenda-repository.service';
|
||||||
|
import { ViewItem } from '../../models/view-item';
|
||||||
|
import { OperatorService } from 'app/core/services/operator.service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The list of speakers for agenda items.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'os-speaker-list',
|
||||||
|
templateUrl: './speaker-list.component.html',
|
||||||
|
styleUrls: ['./speaker-list.component.scss']
|
||||||
|
})
|
||||||
|
export class SpeakerListComponent implements OnInit {
|
||||||
|
/**
|
||||||
|
* Holds the view item to the given topic
|
||||||
|
*/
|
||||||
|
public viewItem: ViewItem;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Holds the speakers
|
||||||
|
*/
|
||||||
|
public speakers: ViewSpeaker[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Holds the active speaker
|
||||||
|
*/
|
||||||
|
public activeSpeaker: ViewSpeaker;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Holds the speakers who were marked done
|
||||||
|
*/
|
||||||
|
public finishedSpeakers: ViewSpeaker[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hold the users
|
||||||
|
*/
|
||||||
|
public users: BehaviorSubject<User[]>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Required for the user search selector
|
||||||
|
*/
|
||||||
|
public addSpeakerForm: FormGroup;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor for speaker list component
|
||||||
|
* @param route Angulars ActivatedRoute
|
||||||
|
* @param DS the DataStore
|
||||||
|
* @param itemRepo Repository fpr agenda items
|
||||||
|
* @param op the current operator
|
||||||
|
*/
|
||||||
|
public constructor(
|
||||||
|
private route: ActivatedRoute,
|
||||||
|
private DS: DataStoreService,
|
||||||
|
private itemRepo: AgendaRepositoryService,
|
||||||
|
private op: OperatorService
|
||||||
|
) {
|
||||||
|
this.addSpeakerForm = new FormGroup({ user_id: new FormControl([]) });
|
||||||
|
this.getAgendaItemByUrl();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Init.
|
||||||
|
*
|
||||||
|
* Observe users,
|
||||||
|
* React to form changes
|
||||||
|
*/
|
||||||
|
public ngOnInit(): void {
|
||||||
|
// load and observe users
|
||||||
|
this.users = new BehaviorSubject(this.DS.getAll(User));
|
||||||
|
this.DS.changeObservable.subscribe(model => {
|
||||||
|
if (model instanceof User) {
|
||||||
|
this.users.next(this.DS.getAll(User));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// detect changes in the form
|
||||||
|
this.addSpeakerForm.valueChanges.subscribe(formResult => {
|
||||||
|
// resetting a form triggers a form.next(null) - check if user_id
|
||||||
|
if (formResult && formResult.user_id) {
|
||||||
|
this.addNewSpeaker(formResult.user_id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract the ID from the url
|
||||||
|
* Determine whether the speaker list belongs to a motion or a topic
|
||||||
|
*/
|
||||||
|
public getAgendaItemByUrl(): void {
|
||||||
|
const id = +this.route.snapshot.url[0];
|
||||||
|
|
||||||
|
this.itemRepo.getViewModelObservable(id).subscribe(newAgendaItem => {
|
||||||
|
if (newAgendaItem) {
|
||||||
|
this.viewItem = newAgendaItem;
|
||||||
|
|
||||||
|
const allSpeakers = this.itemRepo.createViewSpeakers(newAgendaItem.item);
|
||||||
|
|
||||||
|
this.speakers = allSpeakers.filter(speaker => speaker.state === SpeakerState.WAITING);
|
||||||
|
this.finishedSpeakers = allSpeakers.filter(speaker => speaker.state === SpeakerState.FINISHED);
|
||||||
|
this.activeSpeaker = allSpeakers.find(speaker => speaker.state === SpeakerState.CURRENT);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a speaker out of an id
|
||||||
|
* @param userId the user id to add to the list. No parameter adds the operators user as speaker.
|
||||||
|
*/
|
||||||
|
public async addNewSpeaker(userId?: number): Promise<void> {
|
||||||
|
await this.itemRepo.addSpeaker(userId, this.viewItem.item);
|
||||||
|
this.addSpeakerForm.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* React to manual in the sorting order.
|
||||||
|
* Informs the repo about changes in the order
|
||||||
|
* @param listInNewOrder Contains the newly ordered list of ViewSpeakers
|
||||||
|
*/
|
||||||
|
public onSortingChange(listInNewOrder: ViewSpeaker[]): void {
|
||||||
|
// extract the ids from the ViewSpeaker array
|
||||||
|
const userIds = listInNewOrder.map(speaker => speaker.id);
|
||||||
|
this.itemRepo.sortSpeakers(userIds, this.viewItem.item);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Click on the mic button to mark a speaker as speaking
|
||||||
|
* @param item the speaker marked in the list
|
||||||
|
*/
|
||||||
|
public onStartButton(item: ViewSpeaker): void {
|
||||||
|
this.itemRepo.startSpeaker(item.id, this.viewItem.item);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Click on the mic-cross button
|
||||||
|
*/
|
||||||
|
public onStopButton(): void {
|
||||||
|
this.itemRepo.stopSpeaker(this.viewItem.item);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Click on the star button
|
||||||
|
* @param item
|
||||||
|
*/
|
||||||
|
public onMarkButton(item: ViewSpeaker): void {
|
||||||
|
this.itemRepo.markSpeaker(item.user.id, !item.marked, this.viewItem.item);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Click on the X button
|
||||||
|
* @param item
|
||||||
|
*/
|
||||||
|
public onDeleteButton(item?: ViewSpeaker): void {
|
||||||
|
this.itemRepo.deleteSpeaker(this.viewItem.item, item ? item.id : null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the operator is in the list of (waiting) speakers
|
||||||
|
*
|
||||||
|
* @returns whether or not the current operator is in the list
|
||||||
|
*/
|
||||||
|
public isOpInList(): boolean {
|
||||||
|
return this.speakers.some(speaker => speaker.user.id === this.op.user.id);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,83 @@
|
|||||||
|
<os-head-bar
|
||||||
|
[nav]="false"
|
||||||
|
[goBack]="true"
|
||||||
|
[editMode]="editTopic"
|
||||||
|
(mainEvent)="setEditMode(!editTopic)"
|
||||||
|
(saveEvent)="saveTopic()"
|
||||||
|
>
|
||||||
|
<!-- Title -->
|
||||||
|
<div class="title-slot">
|
||||||
|
<h2>
|
||||||
|
<span *ngIf="newTopic && !editTopic" translate>New</span>
|
||||||
|
<span *ngIf="editTopic && !newTopic" translate>Edit</span>
|
||||||
|
<!-- Whitespace between "New" and "Topic" -->
|
||||||
|
<span> </span> <span translate>Topic</span>
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Menu -->
|
||||||
|
<div class="menu-slot">
|
||||||
|
<button type="button" mat-icon-button [matMenuTriggerFor]="topicExtraMenu">
|
||||||
|
<mat-icon>more_vert</mat-icon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</os-head-bar>
|
||||||
|
|
||||||
|
<div *ngIf="topic" class="topic-container on-transition-fade">
|
||||||
|
<div class="topic-title">
|
||||||
|
<h2 *ngIf="!editTopic">{{ topic.title }}</h2>
|
||||||
|
<h2 *ngIf="editTopic">{{ topicForm.get('title').value }}</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<mat-card *ngIf="topic || editTopic" class="topic-text">
|
||||||
|
<div>
|
||||||
|
<span *ngIf="!editTopic">
|
||||||
|
{{ topic.text }}
|
||||||
|
<div *ngIf="topic.text === ''" class="missing" translate>No description provided.</div>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div *ngIf="topic.attachments && topic.attachments.length > 0">
|
||||||
|
<h3>
|
||||||
|
<span translate>Attachments</span> <span>:</span>
|
||||||
|
<!-- TODO: Mediafiles and attachments are not fully implemented -->
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form *ngIf="editTopic" [formGroup]="topicForm" (ngSubmit)="saveTopic()" (keydown)="keyDownFunction($event)">
|
||||||
|
<div>
|
||||||
|
<mat-form-field>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
matInput
|
||||||
|
osAutofocus
|
||||||
|
required
|
||||||
|
formControlName="title"
|
||||||
|
placeholder="{{ 'Title' | translate}}"
|
||||||
|
/>
|
||||||
|
<mat-error *ngIf="topicForm.invalid" translate>A name is required</mat-error>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<mat-form-field>
|
||||||
|
<textarea matInput formControlName="text" placeholder="{{ 'Description ' | translate}}"></textarea>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- TODO: Select Mediafiles as attachments here -->
|
||||||
|
</form>
|
||||||
|
</mat-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<mat-menu #topicExtraMenu="matMenu">
|
||||||
|
<button mat-menu-item *ngIf="topic" [routerLink]="getSpeakerLink()">
|
||||||
|
<mat-icon>mic</mat-icon>
|
||||||
|
<span translate>List of speakers</span>
|
||||||
|
</button>
|
||||||
|
<mat-divider></mat-divider>
|
||||||
|
<button mat-menu-item class="red-warning-text" (click)="onDeleteButton()">
|
||||||
|
<mat-icon>delete</mat-icon>
|
||||||
|
<span translate>Delete</span>
|
||||||
|
</button>
|
||||||
|
</mat-menu>
|
@ -0,0 +1,26 @@
|
|||||||
|
.topic-container {
|
||||||
|
max-width: 1200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topic-title {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.topic-text {
|
||||||
|
margin: 0 20px 0 20px;
|
||||||
|
padding: 25px;
|
||||||
|
|
||||||
|
.missing {
|
||||||
|
color: slategray; // TODO: Colors in theme
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,26 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { TopicDetailComponent } from './topic-detail.component';
|
||||||
|
import { E2EImportsModule } from '../../../../../e2e-imports.module';
|
||||||
|
|
||||||
|
describe('TopicComponent', () => {
|
||||||
|
let component: TopicDetailComponent;
|
||||||
|
let fixture: ComponentFixture<TopicDetailComponent>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [E2EImportsModule],
|
||||||
|
declarations: [TopicDetailComponent]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(TopicDetailComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,185 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { Location } from '@angular/common';
|
||||||
|
import { FormGroup, Validators, FormBuilder } from '@angular/forms';
|
||||||
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
|
||||||
|
import { PromptService } from 'app/core/services/prompt.service';
|
||||||
|
import { TopicRepositoryService } from '../../services/topic-repository.service';
|
||||||
|
import { ViewTopic } from '../../models/view-topic';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detail page for topics.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'os-topic-detail',
|
||||||
|
templateUrl: './topic-detail.component.html',
|
||||||
|
styleUrls: ['./topic-detail.component.scss']
|
||||||
|
})
|
||||||
|
export class TopicDetailComponent {
|
||||||
|
/**
|
||||||
|
* Determine if the topic is in edit mode
|
||||||
|
*/
|
||||||
|
public editTopic: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine is created
|
||||||
|
*/
|
||||||
|
public newTopic: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Holds the current view topic
|
||||||
|
*/
|
||||||
|
public topic: ViewTopic;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Topic form
|
||||||
|
*/
|
||||||
|
public topicForm: FormGroup;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor for the topic detail page.
|
||||||
|
*
|
||||||
|
* @param route Angulars ActivatedRoute
|
||||||
|
* @param router Angulars Router
|
||||||
|
* @param location Enables to navigate back
|
||||||
|
* @param formBuilder Angulars FormBuilder
|
||||||
|
* @param translate Handles translations
|
||||||
|
* @param repo The topic repository
|
||||||
|
* @param promptService Allows warning before deletion attempts
|
||||||
|
*/
|
||||||
|
public constructor(
|
||||||
|
private route: ActivatedRoute,
|
||||||
|
private router: Router,
|
||||||
|
private location: Location,
|
||||||
|
private formBuilder: FormBuilder,
|
||||||
|
private translate: TranslateService,
|
||||||
|
private repo: TopicRepositoryService,
|
||||||
|
private promptService: PromptService
|
||||||
|
) {
|
||||||
|
this.getTopicByUrl();
|
||||||
|
this.createForm();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the edit mode to the given Status
|
||||||
|
* @param mode
|
||||||
|
*/
|
||||||
|
public setEditMode(mode: boolean): void {
|
||||||
|
this.editTopic = mode;
|
||||||
|
if (mode) {
|
||||||
|
this.patchForm();
|
||||||
|
}
|
||||||
|
if (!mode && this.newTopic) {
|
||||||
|
this.router.navigate(['./agenda/']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save a new topic as agenda item
|
||||||
|
*/
|
||||||
|
public async saveTopic(): Promise<void> {
|
||||||
|
if (this.newTopic && this.topicForm.valid) {
|
||||||
|
const response = await this.repo.create(this.topicForm.value);
|
||||||
|
this.router.navigate([`/agenda/topics/${response.id}`]);
|
||||||
|
// after creating a new topic, go "back" to agenda list view
|
||||||
|
this.location.replaceState('/agenda/');
|
||||||
|
} else {
|
||||||
|
await this.repo.update(this.topicForm.value, this.topic);
|
||||||
|
this.setEditMode(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup the form to create or alter the topic
|
||||||
|
*/
|
||||||
|
public createForm(): void {
|
||||||
|
this.topicForm = this.formBuilder.group({
|
||||||
|
title: ['', Validators.required],
|
||||||
|
text: ['']
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Overwrite form Values with values from the topic
|
||||||
|
*/
|
||||||
|
public patchForm(): void {
|
||||||
|
const topicPatch = {};
|
||||||
|
Object.keys(this.topicForm.controls).forEach(ctrl => {
|
||||||
|
topicPatch[ctrl] = this.topic[ctrl];
|
||||||
|
});
|
||||||
|
this.topicForm.patchValue(topicPatch);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether a new topic should be created or an existing one should
|
||||||
|
* be loaded using the ID from the URL
|
||||||
|
*/
|
||||||
|
public getTopicByUrl(): void {
|
||||||
|
if (this.route.snapshot.url[1] && this.route.snapshot.url[1].path === 'new') {
|
||||||
|
// creates a new topic
|
||||||
|
this.newTopic = true;
|
||||||
|
this.editTopic = true;
|
||||||
|
this.topic = new ViewTopic();
|
||||||
|
} else {
|
||||||
|
// load existing topic
|
||||||
|
this.route.params.subscribe(params => {
|
||||||
|
this.loadTopic(params.id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads a top from the repository
|
||||||
|
* @param id the id of the required topic
|
||||||
|
*/
|
||||||
|
public loadTopic(id: number): void {
|
||||||
|
this.repo.getViewModelObservable(id).subscribe(newViewTopic => {
|
||||||
|
// repo sometimes delivers undefined values
|
||||||
|
// also ensures edition cannot be interrupted by autoupdate
|
||||||
|
if (newViewTopic && !this.editTopic) {
|
||||||
|
this.topic = newViewTopic;
|
||||||
|
// personalInfoForm is undefined during 'new' and directly after reloading
|
||||||
|
if (this.topicForm) {
|
||||||
|
this.patchForm();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create the absolute path to the corresponding list of speakers
|
||||||
|
*
|
||||||
|
* @returns the link to the list of speakers as string
|
||||||
|
*/
|
||||||
|
public getSpeakerLink(): string {
|
||||||
|
if (!this.newTopic && this.topic) {
|
||||||
|
const item = this.repo.getAgendaItem(this.topic.topic);
|
||||||
|
if (item) {
|
||||||
|
return `/agenda/${item.id}/speakers`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler for the delete button. Uses the PromptService
|
||||||
|
*/
|
||||||
|
public async onDeleteButton(): Promise<any> {
|
||||||
|
const content = this.translate.instant('Delete') + ` ${this.topic.title}?`;
|
||||||
|
if (await this.promptService.open('Are you sure?', content)) {
|
||||||
|
await this.repo.delete(this.topic);
|
||||||
|
this.router.navigate(['/agenda']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hitting escape while in topicForm should cancel editing
|
||||||
|
* @param event has the code
|
||||||
|
*/
|
||||||
|
public keyDownFunction(event: KeyboardEvent): void {
|
||||||
|
if (event.keyCode === 27) {
|
||||||
|
this.setEditMode(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
89
client/src/app/site/agenda/models/view-speaker.ts
Normal file
89
client/src/app/site/agenda/models/view-speaker.ts
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
import { BaseViewModel } from 'app/site/base/base-view-model';
|
||||||
|
import { Speaker } from 'app/shared/models/agenda/speaker';
|
||||||
|
import { User } from 'app/shared/models/users/user';
|
||||||
|
import { Selectable } from 'app/shared/components/selectable';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine the state of the speaker
|
||||||
|
*/
|
||||||
|
export enum SpeakerState {
|
||||||
|
WAITING,
|
||||||
|
CURRENT,
|
||||||
|
FINISHED
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides "safe" access to a speaker with all it's components
|
||||||
|
*/
|
||||||
|
export class ViewSpeaker extends BaseViewModel implements Selectable {
|
||||||
|
private _speaker: Speaker;
|
||||||
|
private _user: User;
|
||||||
|
|
||||||
|
public get speaker(): Speaker {
|
||||||
|
return this._speaker;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get user(): User {
|
||||||
|
return this._user;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get id(): number {
|
||||||
|
return this.speaker ? this.speaker.id : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get weight(): number {
|
||||||
|
return this.speaker ? this.speaker.weight : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get marked(): boolean {
|
||||||
|
return this.speaker ? this.speaker.marked : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get begin_time(): string {
|
||||||
|
return this.speaker ? this.speaker.begin_time : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get end_time(): string {
|
||||||
|
return this.speaker ? this.speaker.end_time : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns:
|
||||||
|
* - waiting if there is no begin nor end time
|
||||||
|
* - current if there is a begin time and not end time
|
||||||
|
* - finished if there are both begin and end time
|
||||||
|
*/
|
||||||
|
public get state(): SpeakerState {
|
||||||
|
if (!this.begin_time && !this.end_time) {
|
||||||
|
return SpeakerState.WAITING;
|
||||||
|
} else if (this.begin_time && !this.end_time) {
|
||||||
|
return SpeakerState.CURRENT;
|
||||||
|
} else {
|
||||||
|
return SpeakerState.FINISHED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public get name(): string {
|
||||||
|
return this.user.full_name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public constructor(speaker?: Speaker, user?: User) {
|
||||||
|
super();
|
||||||
|
this._speaker = speaker;
|
||||||
|
this._user = user;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getTitle(): string {
|
||||||
|
return this.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Speaker is not a base model,
|
||||||
|
* @param update the incoming update
|
||||||
|
*/
|
||||||
|
public updateValues(update: Speaker): void {
|
||||||
|
if (this.id === update.id) {
|
||||||
|
this._speaker = update;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
58
client/src/app/site/agenda/models/view-topic.ts
Normal file
58
client/src/app/site/agenda/models/view-topic.ts
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import { BaseViewModel } from '../../base/base-view-model';
|
||||||
|
import { Topic } from 'app/shared/models/topics/topic';
|
||||||
|
import { Mediafile } from 'app/shared/models/mediafiles/mediafile';
|
||||||
|
import { Item } from 'app/shared/models/agenda/item';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides "safe" access to topic with all it's components
|
||||||
|
*/
|
||||||
|
export class ViewTopic extends BaseViewModel {
|
||||||
|
private _topic: Topic;
|
||||||
|
private _attachments: Mediafile[];
|
||||||
|
private _agenda_item: Item;
|
||||||
|
|
||||||
|
public get topic(): Topic {
|
||||||
|
return this._topic;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get attachments(): Mediafile[] {
|
||||||
|
return this._attachments;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get agenda_item(): Item {
|
||||||
|
return this._agenda_item;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get id(): number {
|
||||||
|
return this.topic ? this.topic.id : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get agenda_item_id(): number {
|
||||||
|
return this.topic ? this.topic.agenda_item_id : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get title(): string {
|
||||||
|
return this.topic ? this.topic.title : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get text(): string {
|
||||||
|
return this.topic ? this.topic.text : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public constructor(topic?: Topic, attachments?: Mediafile[], item?: Item) {
|
||||||
|
super();
|
||||||
|
this._topic = topic;
|
||||||
|
this._attachments = attachments;
|
||||||
|
this._agenda_item = item;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getTitle(): string {
|
||||||
|
return this.title;
|
||||||
|
}
|
||||||
|
|
||||||
|
public updateValues(update: Topic): void {
|
||||||
|
if (this.id === update.id) {
|
||||||
|
this._topic = update;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -8,6 +8,10 @@ import { AgendaBaseModel } from '../../../shared/models/base/agenda-base-model';
|
|||||||
import { BaseModel } from '../../../shared/models/base/base-model';
|
import { BaseModel } from '../../../shared/models/base/base-model';
|
||||||
import { Identifiable } from '../../../shared/models/base/identifiable';
|
import { Identifiable } from '../../../shared/models/base/identifiable';
|
||||||
import { CollectionStringModelMapperService } from '../../../core/services/collectionStringModelMapper.service';
|
import { CollectionStringModelMapperService } from '../../../core/services/collectionStringModelMapper.service';
|
||||||
|
import { ViewSpeaker } from '../models/view-speaker';
|
||||||
|
import { Speaker } from 'app/shared/models/agenda/speaker';
|
||||||
|
import { User } from 'app/shared/models/users/user';
|
||||||
|
import { HttpService } from 'app/core/services/http.service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Repository service for users
|
* Repository service for users
|
||||||
@ -18,15 +22,27 @@ import { CollectionStringModelMapperService } from '../../../core/services/colle
|
|||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
})
|
})
|
||||||
export class AgendaRepositoryService extends BaseRepository<ViewItem, Item> {
|
export class AgendaRepositoryService extends BaseRepository<ViewItem, Item> {
|
||||||
public constructor(DS: DataStoreService, mapperService: CollectionStringModelMapperService) {
|
/**
|
||||||
|
* Contructor for agenda repository.
|
||||||
|
* @param DS The DataStore
|
||||||
|
* @param httpService OpenSlides own HttpService
|
||||||
|
* @param mapperService OpenSlides mapping service for collection strings
|
||||||
|
*/
|
||||||
|
public constructor(
|
||||||
|
protected DS: DataStoreService,
|
||||||
|
private httpService: HttpService,
|
||||||
|
mapperService: CollectionStringModelMapperService
|
||||||
|
) {
|
||||||
super(DS, mapperService, Item);
|
super(DS, mapperService, Item);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the corresponding content object to a given {@link Item} as an {@link AgendaBaseModel}
|
* Returns the corresponding content object to a given {@link Item} as an {@link AgendaBaseModel}
|
||||||
* @param agendaItem
|
* Used dynamically because of heavy race conditions
|
||||||
|
* @param agendaItem the target agenda Item
|
||||||
|
* @returns the content object of the given item. Might be null if it was not found.
|
||||||
*/
|
*/
|
||||||
private getContentObject(agendaItem: Item): AgendaBaseModel {
|
public getContentObject(agendaItem: Item): AgendaBaseModel {
|
||||||
const contentObject = this.DS.get<BaseModel>(
|
const contentObject = this.DS.get<BaseModel>(
|
||||||
agendaItem.content_object.collection,
|
agendaItem.content_object.collection,
|
||||||
agendaItem.content_object.id
|
agendaItem.content_object.id
|
||||||
@ -45,6 +61,88 @@ export class AgendaRepositoryService extends BaseRepository<ViewItem, Item> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate viewSpeaker objects from a given agenda Item
|
||||||
|
* @param item agenda Item holding speakers
|
||||||
|
* @returns the list of view speakers corresponding to the given item
|
||||||
|
*/
|
||||||
|
public createViewSpeakers(item: Item): ViewSpeaker[] {
|
||||||
|
let viewSpeakers = [];
|
||||||
|
const speakers = item.speakers;
|
||||||
|
if (speakers && speakers.length > 0) {
|
||||||
|
speakers.forEach((speaker: Speaker) => {
|
||||||
|
const user = this.DS.get(User, speaker.user_id);
|
||||||
|
viewSpeakers.push(new ViewSpeaker(speaker, user));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// sort speakers by their weight
|
||||||
|
viewSpeakers = viewSpeakers.sort((a, b) => a.weight - b.weight);
|
||||||
|
return viewSpeakers;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a new speaker to an agenda item.
|
||||||
|
* Sends the users ID to the server
|
||||||
|
*
|
||||||
|
* Might need another repo
|
||||||
|
* @param id {@link User} id of the new speaker
|
||||||
|
* @param agenda the target agenda item
|
||||||
|
*/
|
||||||
|
public async addSpeaker(id: number, agenda: Item): Promise<void> {
|
||||||
|
const restUrl = `rest/agenda/item/${agenda.id}/manage_speaker/`;
|
||||||
|
await this.httpService.post<Identifiable>(restUrl, { user: id });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the given speaker ID to Speak
|
||||||
|
* @param id the speakers id
|
||||||
|
* @param agenda the target agenda item
|
||||||
|
*/
|
||||||
|
public async startSpeaker(id: number, agenda: Item): Promise<void> {
|
||||||
|
const restUrl = `rest/agenda/item/${agenda.id}/speak/`;
|
||||||
|
await this.httpService.put(restUrl, { speaker: id });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stops the current speaker
|
||||||
|
* @param agenda the target agenda item
|
||||||
|
*/
|
||||||
|
public async stopSpeaker(agenda: Item): Promise<void> {
|
||||||
|
const restUrl = `rest/agenda/item/${agenda.id}/speak/`;
|
||||||
|
await this.httpService.delete(restUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marks the current speaker
|
||||||
|
* @param id {@link User} id of the new speaker
|
||||||
|
* @param mark determine if the user was marked or not
|
||||||
|
* @param agenda the target agenda item
|
||||||
|
*/
|
||||||
|
public async markSpeaker(id: number, mark: boolean, agenda: Item): Promise<void> {
|
||||||
|
const restUrl = `rest/agenda/item/${agenda.id}/manage_speaker/`;
|
||||||
|
await this.httpService.patch(restUrl, { user: id, marked: mark });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes the given speaker for the agenda
|
||||||
|
* @param id the speakers id
|
||||||
|
* @param agenda the target agenda item
|
||||||
|
*/
|
||||||
|
public async deleteSpeaker(agenda: Item, id?: number): Promise<void> {
|
||||||
|
const restUrl = `rest/agenda/item/${agenda.id}/manage_speaker/`;
|
||||||
|
await this.httpService.delete(restUrl, { speaker: id });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Posts an (manually) sorted speaker list to the server
|
||||||
|
* @param ids array of speaker id numbers
|
||||||
|
* @param Item the target agenda item
|
||||||
|
*/
|
||||||
|
public async sortSpeakers(ids: number[], agenda: Item): Promise<void> {
|
||||||
|
const restUrl = `rest/agenda/item/${agenda.id}/sort_speakers/`;
|
||||||
|
await this.httpService.post(restUrl, { speakers: ids });
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @ignore
|
* @ignore
|
||||||
*
|
*
|
||||||
@ -72,9 +170,13 @@ export class AgendaRepositoryService extends BaseRepository<ViewItem, Item> {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the viewItem out of a given item
|
||||||
|
* @param item the item that should be converted to view item
|
||||||
|
* @returns a new view item
|
||||||
|
*/
|
||||||
public createViewModel(item: Item): ViewItem {
|
public createViewModel(item: Item): ViewItem {
|
||||||
const contentObject = this.getContentObject(item);
|
const contentObject = this.getContentObject(item);
|
||||||
|
|
||||||
return new ViewItem(item, contentObject);
|
return new ViewItem(item, contentObject);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,16 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { TopicRepositoryService } from './topic-repository.service';
|
||||||
|
import { E2EImportsModule } from '../../../../e2e-imports.module';
|
||||||
|
|
||||||
|
describe('TopicRepositoryService', () => {
|
||||||
|
beforeEach(() =>
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [E2EImportsModule]
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
const service: TopicRepositoryService = TestBed.get(TopicRepositoryService);
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,89 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
|
import { Topic } from 'app/shared/models/topics/topic';
|
||||||
|
import { BaseRepository } from 'app/site/base/base-repository';
|
||||||
|
import { Mediafile } from 'app/shared/models/mediafiles/mediafile';
|
||||||
|
import { Item } from 'app/shared/models/agenda/item';
|
||||||
|
import { DataStoreService } from 'app/core/services/data-store.service';
|
||||||
|
import { DataSendService } from 'app/core/services/data-send.service';
|
||||||
|
import { ViewTopic } from '../models/view-topic';
|
||||||
|
import { Identifiable } from 'app/shared/models/base/identifiable';
|
||||||
|
import { CollectionStringModelMapperService } from 'app/core/services/collectionStringModelMapper.service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Repository for topics
|
||||||
|
*/
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class TopicRepositoryService extends BaseRepository<ViewTopic, Topic> {
|
||||||
|
/**
|
||||||
|
* Constructor calls the parent constructor
|
||||||
|
*
|
||||||
|
* @param DS Access the DataStore
|
||||||
|
* @param mapperService OpenSlides mapping service for collections
|
||||||
|
* @param dataSend Access the DataSendService
|
||||||
|
*/
|
||||||
|
public constructor(
|
||||||
|
DS: DataStoreService,
|
||||||
|
mapperService: CollectionStringModelMapperService,
|
||||||
|
private dataSend: DataSendService
|
||||||
|
) {
|
||||||
|
super(DS, mapperService, Topic, [Mediafile, Item]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new viewModel out of the given model
|
||||||
|
* @param topic The topic that shall be converted into a view topic
|
||||||
|
* @returns a new view topic
|
||||||
|
*/
|
||||||
|
public createViewModel(topic: Topic): ViewTopic {
|
||||||
|
const attachments = this.DS.getMany(Mediafile, topic.attachments_id);
|
||||||
|
const item = this.getAgendaItem(topic);
|
||||||
|
return new ViewTopic(topic, attachments, item);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the corresponding agendaItem to the topic.
|
||||||
|
* Used to deal with race conditions
|
||||||
|
*
|
||||||
|
* @param topic the topic for the agenda item
|
||||||
|
* @returns an agenda item that fits for the topic
|
||||||
|
*/
|
||||||
|
public getAgendaItem(topic: Topic): Item {
|
||||||
|
return this.DS.get(Item, topic.agenda_item_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save a new topic
|
||||||
|
* @param topicData Partial topic data to be created
|
||||||
|
* @returns an Identifiable (usually id) as promise
|
||||||
|
*/
|
||||||
|
public async create(topicData: Partial<Topic>): Promise<Identifiable> {
|
||||||
|
const newTopic = new Topic();
|
||||||
|
newTopic.patchValues(topicData);
|
||||||
|
return await this.dataSend.createModel(newTopic);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change an existing topic
|
||||||
|
*
|
||||||
|
* @param updateData form value containing the data meant to update the topic
|
||||||
|
* @param viewTopic the topic that should receive the update
|
||||||
|
*/
|
||||||
|
public async update(updateData: Partial<Topic>, viewTopic: ViewTopic): Promise<void> {
|
||||||
|
const updateTopic = new Topic();
|
||||||
|
updateTopic.patchValues(viewTopic.topic);
|
||||||
|
updateTopic.patchValues(updateData);
|
||||||
|
|
||||||
|
return await this.dataSend.updateModel(updateTopic);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a topic
|
||||||
|
* @param viewTopic the topic that should be removed
|
||||||
|
*/
|
||||||
|
public async delete(viewTopic: ViewTopic): Promise<void> {
|
||||||
|
return await this.dataSend.deleteModel(viewTopic.topic);
|
||||||
|
}
|
||||||
|
}
|
@ -39,20 +39,31 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<mat-menu #motionExtraMenu="matMenu">
|
<mat-menu #motionExtraMenu="matMenu">
|
||||||
|
<div *ngIf="motion">
|
||||||
<button mat-menu-item>
|
<button mat-menu-item>
|
||||||
<mat-icon>picture_as_pdf</mat-icon>
|
<mat-icon>picture_as_pdf</mat-icon>
|
||||||
<span translate>PDF</span>
|
<span translate>PDF</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<button mat-menu-item [routerLink]="getSpeakerLink()">
|
||||||
|
<mat-icon>mic</mat-icon>
|
||||||
|
<span translate>List of speakers</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
<button mat-menu-item>
|
<button mat-menu-item>
|
||||||
<!-- possible icons: screen_share, cast, videocam -->
|
<!-- possible icons: screen_share, cast, videocam -->
|
||||||
<mat-icon>videocam</mat-icon>
|
<mat-icon>videocam</mat-icon>
|
||||||
<span translate>Project</span>
|
<span translate>Project</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<mat-divider></mat-divider>
|
<mat-divider></mat-divider>
|
||||||
|
|
||||||
<button mat-menu-item class='red-warning-text' (click)='deleteMotionButton()'>
|
<button mat-menu-item class='red-warning-text' (click)='deleteMotionButton()'>
|
||||||
<mat-icon>delete</mat-icon>
|
<mat-icon>delete</mat-icon>
|
||||||
<span translate>Delete</span>
|
<span translate>Delete</span>
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
</mat-menu>
|
</mat-menu>
|
||||||
</os-head-bar>
|
</os-head-bar>
|
||||||
|
|
||||||
|
@ -318,21 +318,21 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Trigger to delete the motion
|
* Trigger to delete the motion.
|
||||||
*
|
* Sends a delete request over the repository and
|
||||||
* TODO: Repo should handle
|
* shows a "are you sure" dialog
|
||||||
*/
|
*/
|
||||||
public deleteMotionButton(): void {
|
public async deleteMotionButton(): Promise<void> {
|
||||||
this.repo.delete(this.motion).then(() => {
|
await this.repo.delete(this.motion).then();
|
||||||
this.router.navigate(['./motions/']);
|
this.router.navigate(['./motions/']);
|
||||||
}, this.raiseError);
|
|
||||||
// TODO: this needs to be in the autoupdate code.
|
// This should happen during auto update
|
||||||
/*const motList = this.categoryRepo.getMotionsOfCategory(this.motion.category);
|
// const motList = this.categoryRepo.getMotionsOfCategory(this.motion.category);
|
||||||
const index = motList.indexOf(this.motion.motion, 0);
|
// const index = motList.indexOf(this.motion.motion, 0);
|
||||||
if (index > -1) {
|
// if (index > -1) {
|
||||||
motList.splice(index, 1);
|
// motList.splice(index, 1);
|
||||||
}
|
// }
|
||||||
this.categoryRepo.updateCategoryNumbering(this.motion.category, motList);*/
|
// this.categoryRepo.updateCategoryNumbering(this.motion.category, motList);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -415,7 +415,6 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Init. Does nothing here.
|
|
||||||
* Comes from the head bar
|
* Comes from the head bar
|
||||||
* @param mode
|
* @param mode
|
||||||
*/
|
*/
|
||||||
@ -492,6 +491,14 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create the absolute path to the corresponding list of speakers
|
||||||
|
* @returns the link to the corresponding list of speakers as string
|
||||||
|
*/
|
||||||
|
public getSpeakerLink(): string {
|
||||||
|
return `/agenda/${this.motion.agenda_item_id}/speakers`;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine if the user has the correct requirements to alter the motion
|
* Determine if the user has the correct requirements to alter the motion
|
||||||
*/
|
*/
|
||||||
|
@ -88,6 +88,10 @@ export class ViewMotion extends BaseViewModel {
|
|||||||
return this._category;
|
return this._category;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public get agenda_item_id(): number {
|
||||||
|
return this.motion ? this.motion.agenda_item_id : null;
|
||||||
|
}
|
||||||
|
|
||||||
public get category_id(): number {
|
public get category_id(): number {
|
||||||
return this.motion && this.category ? this.motion.category_id : null;
|
return this.motion && this.category ? this.motion.category_id : null;
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,7 @@ import { MotionDetailComponent } from './components/motion-detail/motion-detail.
|
|||||||
import { CategoryListComponent } from './components/category-list/category-list.component';
|
import { CategoryListComponent } from './components/category-list/category-list.component';
|
||||||
import { MotionCommentSectionListComponent } from './components/motion-comment-section-list/motion-comment-section-list.component';
|
import { MotionCommentSectionListComponent } from './components/motion-comment-section-list/motion-comment-section-list.component';
|
||||||
import { StatuteParagraphListComponent } from './components/statute-paragraph-list/statute-paragraph-list.component';
|
import { StatuteParagraphListComponent } from './components/statute-paragraph-list/statute-paragraph-list.component';
|
||||||
|
import { SpeakerListComponent } from '../agenda/components/speaker-list/speaker-list.component';
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{ path: '', component: MotionListComponent },
|
{ path: '', component: MotionListComponent },
|
||||||
@ -12,7 +13,8 @@ const routes: Routes = [
|
|||||||
{ path: 'comment-section', component: MotionCommentSectionListComponent },
|
{ path: 'comment-section', component: MotionCommentSectionListComponent },
|
||||||
{ path: 'statute-paragraphs', component: StatuteParagraphListComponent },
|
{ path: 'statute-paragraphs', component: StatuteParagraphListComponent },
|
||||||
{ path: 'new', component: MotionDetailComponent },
|
{ path: 'new', component: MotionDetailComponent },
|
||||||
{ path: ':id', component: MotionDetailComponent }
|
{ path: ':id', component: MotionDetailComponent },
|
||||||
|
{ path: ':id/speakers', component: SpeakerListComponent }
|
||||||
];
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
@ -32,10 +32,45 @@ $openslides-blue: (
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
$openslides-green: (
|
||||||
|
50: #e9f2e6,
|
||||||
|
100: #c8e0bf,
|
||||||
|
200: #a3cb95,
|
||||||
|
300: #7eb66b,
|
||||||
|
400: #62a64b,
|
||||||
|
500: #46962b,
|
||||||
|
600: #3f8e26,
|
||||||
|
700: #0a321e,
|
||||||
|
800: #092d1a,
|
||||||
|
900: #072616,
|
||||||
|
A100: #acff9d,
|
||||||
|
A200: #80ff6a,
|
||||||
|
A400: #55ff37,
|
||||||
|
A700: #3fff1e,
|
||||||
|
contrast: (
|
||||||
|
50: #000000,
|
||||||
|
100: #000000,
|
||||||
|
200: #000000,
|
||||||
|
300: #000000,
|
||||||
|
400: #000000,
|
||||||
|
500: #ffffff,
|
||||||
|
600: #ffffff,
|
||||||
|
700: #ffffff,
|
||||||
|
800: #ffffff,
|
||||||
|
900: #ffffff,
|
||||||
|
A100: #000000,
|
||||||
|
A200: #000000,
|
||||||
|
A400: #000000,
|
||||||
|
A700: #000000
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
// Generate paletes using: https://material.io/design/color/
|
// Generate paletes using: https://material.io/design/color/
|
||||||
// default values fir mat-palette: $default: 500, $lighter: 100, $darker: 700.
|
// default values fir mat-palette: $default: 500, $lighter: 100, $darker: 700.
|
||||||
$openslides-primary: mat-palette($openslides-blue);
|
$openslides-primary: mat-palette($openslides-blue);
|
||||||
$openslides-accent: mat-palette($mat-blue);
|
$openslides-accent: mat-palette($mat-light-blue);
|
||||||
|
// $openslides-primary: mat-palette($openslides-green);
|
||||||
|
// $openslides-accent: mat-palette($mat-amber);
|
||||||
|
|
||||||
$openslides-warn: mat-palette($mat-red);
|
$openslides-warn: mat-palette($mat-red);
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user