Speaker indicators in Lists

This commit is contained in:
Sean Engelhardt 2018-11-12 15:24:23 +01:00
parent aa497ad543
commit d968761377
15 changed files with 161 additions and 45 deletions

View File

@ -1,5 +1,5 @@
import { ProjectableBaseModel } from '../base/projectable-base-model';
import { Speaker } from './speaker';
import { Speaker, SpeakerState } from './speaker';
/**
* The representation of the content object for agenda items. The unique combination
@ -44,6 +44,13 @@ export class Item extends ProjectableBaseModel {
}
}
/**
* Gets the amount of waiting speakers
*/
public get speakerAmount(): number {
return this.speakers.filter(speaker => speaker.state === SpeakerState.WAITING).length;
}
public getTitle(): string {
return this.title;
}

View File

@ -1,5 +1,14 @@
import { Deserializer } from '../base/deserializer';
/**
* Determine the state of the speaker
*/
export enum SpeakerState {
WAITING,
CURRENT,
FINISHED
}
/**
* Representation of a speaker in an agenda item.
*
@ -24,6 +33,22 @@ export class Speaker extends Deserializer {
super(input);
}
/**
* @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;
}
}
/**
* Getting the title of a speaker does not make much sense.
* Usually it would refer to the title of a user.

View File

@ -21,7 +21,8 @@ import {
MatNativeDateModule,
DateAdapter,
MatIconModule,
MatButtonToggleModule
MatButtonToggleModule,
MatBadgeModule
} from '@angular/material';
import { MatAutocompleteModule } from '@angular/material/autocomplete';
import { MatChipsModule } from '@angular/material';
@ -90,6 +91,7 @@ import { SpeakerListComponent } from 'app/site/agenda/components/speaker-list/sp
MatSnackBarModule,
MatChipsModule,
MatTooltipModule,
MatBadgeModule,
// TODO: there is an error with missing icons
// we either wait or include a fixed version manually (dirty)
// https://github.com/google/material-design-icons/issues/786
@ -125,6 +127,7 @@ import { SpeakerListComponent } from 'app/site/agenda/components/speaker-list/sp
MatSnackBarModule,
MatChipsModule,
MatTooltipModule,
MatBadgeModule,
MatIconModule,
MatRadioModule,
MatButtonToggleModule,

View File

@ -9,15 +9,30 @@
<!-- title column -->
<ng-container matColumnDef="title">
<mat-header-cell *matHeaderCellDef mat-sort-header>Topic</mat-header-cell>
<mat-cell *matCellDef="let item">{{ item.getListTitle() }}</mat-cell>
<mat-cell *matCellDef="let item" (click)="selectAgendaItem(item)">{{ item.getListTitle() }}</mat-cell>
</ng-container>
<!-- Duration column -->
<ng-container matColumnDef="duration">
<mat-header-cell *matHeaderCellDef mat-sort-header>Duration</mat-header-cell>
<mat-cell *matCellDef="let item">{{ item.duration }}</mat-cell>
<mat-cell *matCellDef="let item" (click)="selectAgendaItem(item)">{{ item.duration }}</mat-cell>
</ng-container>
<mat-header-row *matHeaderRowDef="['title', 'duration']"></mat-header-row>
<mat-row (click)="selectAgendaItem(row)" *matRowDef="let row; columns: ['title', 'duration']"></mat-row>
<!-- Speakers column -->
<ng-container matColumnDef="speakers">
<mat-header-cell *matHeaderCellDef mat-sort-header>Speakers</mat-header-cell>
<mat-cell *matCellDef="let item">
<button mat-icon-button (click)="onSpeakerIcon(item)">
<mat-icon
[matBadge]="item.speakerAmount > 0 ? item.speakerAmount : null"
matBadgeColor="accent">
mic
</mat-icon>
</button>
</mat-cell>
</ng-container>
<mat-header-row *matHeaderRowDef="['title', 'duration', 'speakers']"></mat-header-row>
<mat-row *matRowDef="let row; columns: ['title', 'duration', 'speakers']"></mat-row>
</mat-table>
<mat-paginator class="on-transition-fade" [pageSizeOptions]="[25, 50, 75, 100, 125]"></mat-paginator>

View File

@ -0,0 +1,19 @@
.os-listview-table {
/** Title */
.mat-column-title {
padding-left: 26px;
flex: 1 0 200px;
}
/** Duration */
.mat-column-duration {
flex: 0 0 100px;
}
/** Speakers indicator */
.mat-column-speakers {
flex: 0 0 100px;
justify-content: flex-end !important;
}
}

View File

@ -10,13 +10,11 @@ import { AgendaRepositoryService } from '../../services/agenda-repository.servic
/**
* List view for the agenda.
*
* TODO: Not yet implemented
*/
@Component({
selector: 'os-agenda-list',
templateUrl: './agenda-list.component.html',
styleUrls: ['./agenda-list.component.css']
styleUrls: ['./agenda-list.component.scss']
})
export class AgendaListComponent extends ListViewBaseComponent<ViewItem> implements OnInit {
/**
@ -64,6 +62,14 @@ export class AgendaListComponent extends ListViewBaseComponent<ViewItem> impleme
this.router.navigate([contentObject.getDetailStateURL()]);
}
/**
* Handler for the speakers button
* @param item indicates the row that was clicked on
*/
public onSpeakerIcon(item: ViewItem): void {
this.router.navigate([`${item.id}/speakers`], { relativeTo: this.route });
}
/**
* Handler for the plus button.
* Comes from the HeadBar Component

View File

@ -1,9 +1,11 @@
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 { SpeakerState } from 'app/shared/models/agenda/speaker';
import { User } from 'app/shared/models/users/user';
import { ViewSpeaker } from '../../models/view-speaker';
import { DataStoreService } from 'app/core/services/data-store.service';
import { AgendaRepositoryService } from '../../services/agenda-repository.service';
import { ViewItem } from '../../models/view-item';

View File

@ -22,6 +22,10 @@ export class ViewItem extends BaseViewModel {
return this.item ? this.item.duration : null;
}
public get speakerAmount(): number {
return this.item ? this.item.speakerAmount : null;
}
public constructor(item: Item, contentObject: AgendaBaseModel) {
super();
this._item = item;

View File

@ -1,17 +1,8 @@
import { BaseViewModel } from 'app/site/base/base-view-model';
import { Speaker } from 'app/shared/models/agenda/speaker';
import { Speaker, SpeakerState } 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
*/
@ -47,20 +38,8 @@ export class ViewSpeaker extends BaseViewModel implements Selectable {
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;
}
return this.speaker ? this.speaker.state : null;
}
public get name(): string {

View File

@ -25,7 +25,7 @@
<!-- identifier column -->
<ng-container matColumnDef="identifier">
<mat-header-cell *matHeaderCellDef mat-sort-header>Identifier</mat-header-cell>
<mat-cell *matCellDef="let motion">
<mat-cell *matCellDef="let motion" (click)="selectMotion(motion)">
<div class="innerTable">
{{ motion.identifier }}
</div>
@ -35,7 +35,7 @@
<!-- title column -->
<ng-container matColumnDef="title">
<mat-header-cell *matHeaderCellDef mat-sort-header>Title</mat-header-cell>
<mat-cell *matCellDef="let motion">
<mat-cell *matCellDef="let motion" (click)="selectMotion(motion)">
<div class="innerTable">
<span class="motion-list-title">{{ motion.title }}</span>
<br>
@ -50,7 +50,7 @@
<!-- state column -->
<ng-container matColumnDef="state">
<mat-header-cell *matHeaderCellDef mat-sort-header>State</mat-header-cell>
<mat-cell *matCellDef="let motion">
<mat-cell *matCellDef="let motion" (click)="selectMotion(motion)">
<!--div *ngIf='isDisplayIcon(motion.state) && motion.state' class='innerTable'>
<mat-icon>{{ getStateIcon(motion.state) }}</mat-icon>
</div>-->
@ -60,8 +60,22 @@
</mat-cell>
</ng-container>
<!-- Speakers column -->
<ng-container matColumnDef="speakers">
<mat-header-cell *matHeaderCellDef mat-sort-header>Speakers</mat-header-cell>
<mat-cell *matCellDef="let motion">
<button mat-icon-button (click)="onSpeakerIcon(motion)">
<mat-icon
[matBadge]="motion.agendaSpeakerAmount > 0 ? motion.agendaSpeakerAmount : null"
matBadgeColor="accent">
mic
</mat-icon>
</button>
</mat-cell>
</ng-container>
<mat-header-row *matHeaderRowDef="columnsToDisplayMinWidth"></mat-header-row>
<mat-row (click)="selectMotion(row)" *matRowDef="let row; columns: columnsToDisplayMinWidth"></mat-row>
<mat-row *matRowDef="let row; columns: columnsToDisplayMinWidth"></mat-row>
</mat-table>
<mat-paginator class="on-transition-fade" [pageSizeOptions]="[25, 50, 75, 100, 125]"></mat-paginator>

View File

@ -41,4 +41,10 @@
font-size: 150%;
}
}
/** Speakers indicator */
.mat-column-speakers {
flex: 0 0 100px;
justify-content: flex-end !important;
}
}

View File

@ -22,14 +22,14 @@ export class MotionListComponent extends ListViewBaseComponent<ViewMotion> imple
/**
* Use for minimal width
*/
public columnsToDisplayMinWidth = ['identifier', 'title', 'state'];
public columnsToDisplayMinWidth = ['identifier', 'title', 'state', 'speakers'];
/**
* Use for maximal width
*
* TODO: Needs vp.desktop check
*/
public columnsToDisplayFullWidth = ['identifier', 'title', 'meta', 'state'];
public columnsToDisplayFullWidth = ['identifier', 'title', 'state', 'speakers'];
/**
* Constructor implements title and translation Module.
@ -110,6 +110,14 @@ export class MotionListComponent extends ListViewBaseComponent<ViewMotion> imple
}
}
/**
* Handler for the speakers button
* @param motion indicates the row that was clicked on
*/
public onSpeakerIcon(motion: ViewMotion): void {
this.router.navigate([`/agenda/${motion.agenda_item_id}/speakers`]);
}
/**
* Handler for the plus button
*/

View File

@ -7,6 +7,7 @@ import { BaseModel } from '../../../shared/models/base/base-model';
import { BaseViewModel } from '../../base/base-view-model';
import { ViewMotionCommentSection } from './view-motion-comment-section';
import { MotionComment } from '../../../shared/models/motions/motion-comment';
import { Item } from 'app/shared/models/agenda/item';
export enum LineNumberingMode {
None,
@ -35,6 +36,7 @@ export class ViewMotion extends BaseViewModel {
private _supporters: User[];
private _workflow: Workflow;
private _state: WorkflowState;
private _item: Item;
/**
* Indicates the LineNumberingMode Mode.
@ -164,13 +166,22 @@ export class ViewMotion extends BaseViewModel {
this._motion.submitters_id = users.map(user => user.id);
}
public get item(): Item {
return this._item;
}
public get agendaSpeakerAmount(): number {
return this.item ? this.item.speakerAmount : null
}
public constructor(
motion?: Motion,
category?: Category,
submitters?: User[],
supporters?: User[],
workflow?: Workflow,
state?: WorkflowState
state?: WorkflowState,
item?: Item,
) {
super();
@ -180,6 +191,7 @@ export class ViewMotion extends BaseViewModel {
this._supporters = supporters;
this._workflow = workflow;
this._state = state;
this._item = item;
// TODO: Should be set using a a config variable
this.lnMode = LineNumberingMode.Outside;
@ -216,13 +228,16 @@ export class ViewMotion extends BaseViewModel {
this.updateWorkflow(update as Workflow);
} else if (update instanceof Category) {
this.updateCategory(update as Category);
} else if (update instanceof Item) {
this.updateItem(update as Item);
}
// TODO: There is no way (yet) to add Submitters to a motion
// Thus, this feature could not be tested
}
/**
* Updates the Category
* Update routine for the category
* @param update potentially the changed category. Needs manual verification
*/
public updateCategory(update: Category): void {
if (this.motion && update.id === this.motion.category_id) {
@ -231,7 +246,8 @@ export class ViewMotion extends BaseViewModel {
}
/**
* updates the Workflow
* Update routine for the workflow
* @param update potentially the changed workflow (state). Needs manual verification
*/
public updateWorkflow(update: Workflow): void {
if (this.motion && update.id === this.motion.workflow_id) {
@ -239,6 +255,16 @@ export class ViewMotion extends BaseViewModel {
}
}
/**
* Update routine for the agenda Item
* @param update potentially the changed agenda Item. Needs manual verification
*/
public updateItem(update: Item): void {
if (this.motion && update.id === this.motion.agenda_item_id) {
this._item = update as Item;
}
}
public hasSupporters(): boolean {
return !!(this.supporters && this.supporters.length > 0);
}

View File

@ -19,6 +19,7 @@ import { CollectionStringModelMapperService } from '../../../core/services/colle
import { HttpService } from 'app/core/services/http.service';
import { ConfigService } from 'app/core/services/config.service';
import { Observable } from 'rxjs';
import { Item } from 'app/shared/models/agenda/item';
/**
* Repository Services for motions (and potentially categories)
@ -53,7 +54,7 @@ export class MotionRepositoryService extends BaseRepository<ViewMotion, Motion>
private readonly lineNumbering: LinenumberingService,
private readonly diff: DiffService
) {
super(DS, mapperService, Motion, [Category, User, Workflow]);
super(DS, mapperService, Motion, [Category, User, Workflow, Item]);
}
/**
@ -69,11 +70,12 @@ export class MotionRepositoryService extends BaseRepository<ViewMotion, Motion>
const submitters = this.DS.getMany(User, motion.submitterIds);
const supporters = this.DS.getMany(User, motion.supporters_id);
const workflow = this.DS.get(Workflow, motion.workflow_id);
const item = this.DS.get(Item, motion.agenda_item_id);
let state: WorkflowState = null;
if (workflow) {
state = workflow.getStateById(motion.state_id);
}
return new ViewMotion(motion, category, submitters, supporters, workflow, state);
return new ViewMotion(motion, category, submitters, supporters, workflow, state, item);
}
/**