Merge pull request #4008 from tsiegleauq/speaker-indicator-in-list

Speaker indicators in Lists
This commit is contained in:
Sean 2018-11-12 09:07:03 -08:00 committed by GitHub
commit 759ae91aa0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 161 additions and 45 deletions

View File

@ -1,5 +1,5 @@
import { ProjectableBaseModel } from '../base/projectable-base-model'; 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 * 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 { public getTitle(): string {
return this.title; return this.title;
} }

View File

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

View File

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

View File

@ -9,15 +9,30 @@
<!-- title column --> <!-- title column -->
<ng-container matColumnDef="title"> <ng-container matColumnDef="title">
<mat-header-cell *matHeaderCellDef mat-sort-header>Topic</mat-header-cell> <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> </ng-container>
<!-- Duration column -->
<ng-container matColumnDef="duration"> <ng-container matColumnDef="duration">
<mat-header-cell *matHeaderCellDef mat-sort-header>Duration</mat-header-cell> <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> </ng-container>
<mat-header-row *matHeaderRowDef="['title', 'duration']"></mat-header-row> <!-- Speakers column -->
<mat-row (click)="selectAgendaItem(row)" *matRowDef="let row; columns: ['title', 'duration']"></mat-row> <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-table>
<mat-paginator class="on-transition-fade" [pageSizeOptions]="[25, 50, 75, 100, 125]"></mat-paginator> <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. * List view for the agenda.
*
* TODO: Not yet implemented
*/ */
@Component({ @Component({
selector: 'os-agenda-list', selector: 'os-agenda-list',
templateUrl: './agenda-list.component.html', templateUrl: './agenda-list.component.html',
styleUrls: ['./agenda-list.component.css'] styleUrls: ['./agenda-list.component.scss']
}) })
export class AgendaListComponent extends ListViewBaseComponent<ViewItem> implements OnInit { export class AgendaListComponent extends ListViewBaseComponent<ViewItem> implements OnInit {
/** /**
@ -64,6 +62,14 @@ export class AgendaListComponent extends ListViewBaseComponent<ViewItem> impleme
this.router.navigate([contentObject.getDetailStateURL()]); 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. * Handler for the plus button.
* Comes from the HeadBar Component * Comes from the HeadBar Component

View File

@ -1,9 +1,11 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router'; 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 { FormGroup, FormControl } from '@angular/forms';
import { BehaviorSubject } from 'rxjs'; 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 { DataStoreService } from 'app/core/services/data-store.service';
import { AgendaRepositoryService } from '../../services/agenda-repository.service'; import { AgendaRepositoryService } from '../../services/agenda-repository.service';
import { ViewItem } from '../../models/view-item'; import { ViewItem } from '../../models/view-item';

View File

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

View File

@ -1,17 +1,8 @@
import { BaseViewModel } from 'app/site/base/base-view-model'; 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 { User } from 'app/shared/models/users/user';
import { Selectable } from 'app/shared/components/selectable'; 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 * 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; 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 { public get state(): SpeakerState {
if (!this.begin_time && !this.end_time) { return this.speaker ? this.speaker.state : null;
return SpeakerState.WAITING;
} else if (this.begin_time && !this.end_time) {
return SpeakerState.CURRENT;
} else {
return SpeakerState.FINISHED;
}
} }
public get name(): string { public get name(): string {

View File

@ -25,7 +25,7 @@
<!-- identifier column --> <!-- identifier column -->
<ng-container matColumnDef="identifier"> <ng-container matColumnDef="identifier">
<mat-header-cell *matHeaderCellDef mat-sort-header>Identifier</mat-header-cell> <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"> <div class="innerTable">
{{ motion.identifier }} {{ motion.identifier }}
</div> </div>
@ -35,7 +35,7 @@
<!-- title column --> <!-- title column -->
<ng-container matColumnDef="title"> <ng-container matColumnDef="title">
<mat-header-cell *matHeaderCellDef mat-sort-header>Title</mat-header-cell> <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"> <div class="innerTable">
<span class="motion-list-title">{{ motion.title }}</span> <span class="motion-list-title">{{ motion.title }}</span>
<br> <br>
@ -50,7 +50,7 @@
<!-- state column --> <!-- state column -->
<ng-container matColumnDef="state"> <ng-container matColumnDef="state">
<mat-header-cell *matHeaderCellDef mat-sort-header>State</mat-header-cell> <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'> <!--div *ngIf='isDisplayIcon(motion.state) && motion.state' class='innerTable'>
<mat-icon>{{ getStateIcon(motion.state) }}</mat-icon> <mat-icon>{{ getStateIcon(motion.state) }}</mat-icon>
</div>--> </div>-->
@ -60,8 +60,22 @@
</mat-cell> </mat-cell>
</ng-container> </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-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-table>
<mat-paginator class="on-transition-fade" [pageSizeOptions]="[25, 50, 75, 100, 125]"></mat-paginator> <mat-paginator class="on-transition-fade" [pageSizeOptions]="[25, 50, 75, 100, 125]"></mat-paginator>

View File

@ -41,4 +41,10 @@
font-size: 150%; 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 * Use for minimal width
*/ */
public columnsToDisplayMinWidth = ['identifier', 'title', 'state']; public columnsToDisplayMinWidth = ['identifier', 'title', 'state', 'speakers'];
/** /**
* Use for maximal width * Use for maximal width
* *
* TODO: Needs vp.desktop check * TODO: Needs vp.desktop check
*/ */
public columnsToDisplayFullWidth = ['identifier', 'title', 'meta', 'state']; public columnsToDisplayFullWidth = ['identifier', 'title', 'state', 'speakers'];
/** /**
* Constructor implements title and translation Module. * 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 * 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 { BaseViewModel } from '../../base/base-view-model';
import { ViewMotionCommentSection } from './view-motion-comment-section'; import { ViewMotionCommentSection } from './view-motion-comment-section';
import { MotionComment } from '../../../shared/models/motions/motion-comment'; import { MotionComment } from '../../../shared/models/motions/motion-comment';
import { Item } from 'app/shared/models/agenda/item';
export enum LineNumberingMode { export enum LineNumberingMode {
None, None,
@ -35,6 +36,7 @@ export class ViewMotion extends BaseViewModel {
private _supporters: User[]; private _supporters: User[];
private _workflow: Workflow; private _workflow: Workflow;
private _state: WorkflowState; private _state: WorkflowState;
private _item: Item;
/** /**
* Indicates the LineNumberingMode Mode. * Indicates the LineNumberingMode Mode.
@ -164,13 +166,22 @@ export class ViewMotion extends BaseViewModel {
this._motion.submitters_id = users.map(user => user.id); 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( public constructor(
motion?: Motion, motion?: Motion,
category?: Category, category?: Category,
submitters?: User[], submitters?: User[],
supporters?: User[], supporters?: User[],
workflow?: Workflow, workflow?: Workflow,
state?: WorkflowState state?: WorkflowState,
item?: Item,
) { ) {
super(); super();
@ -180,6 +191,7 @@ export class ViewMotion extends BaseViewModel {
this._supporters = supporters; this._supporters = supporters;
this._workflow = workflow; this._workflow = workflow;
this._state = state; this._state = state;
this._item = item;
// TODO: Should be set using a a config variable // TODO: Should be set using a a config variable
this.lnMode = LineNumberingMode.Outside; this.lnMode = LineNumberingMode.Outside;
@ -216,13 +228,16 @@ export class ViewMotion extends BaseViewModel {
this.updateWorkflow(update as Workflow); this.updateWorkflow(update as Workflow);
} else if (update instanceof Category) { } else if (update instanceof Category) {
this.updateCategory(update as 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 // TODO: There is no way (yet) to add Submitters to a motion
// Thus, this feature could not be tested // 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 { public updateCategory(update: Category): void {
if (this.motion && update.id === this.motion.category_id) { 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 { public updateWorkflow(update: Workflow): void {
if (this.motion && update.id === this.motion.workflow_id) { 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 { public hasSupporters(): boolean {
return !!(this.supporters && this.supporters.length > 0); 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 { HttpService } from 'app/core/services/http.service';
import { ConfigService } from 'app/core/services/config.service'; import { ConfigService } from 'app/core/services/config.service';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { Item } from 'app/shared/models/agenda/item';
/** /**
* Repository Services for motions (and potentially categories) * Repository Services for motions (and potentially categories)
@ -53,7 +54,7 @@ export class MotionRepositoryService extends BaseRepository<ViewMotion, Motion>
private readonly lineNumbering: LinenumberingService, private readonly lineNumbering: LinenumberingService,
private readonly diff: DiffService 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 submitters = this.DS.getMany(User, motion.submitterIds);
const supporters = this.DS.getMany(User, motion.supporters_id); const supporters = this.DS.getMany(User, motion.supporters_id);
const workflow = this.DS.get(Workflow, motion.workflow_id); const workflow = this.DS.get(Workflow, motion.workflow_id);
const item = this.DS.get(Item, motion.agenda_item_id);
let state: WorkflowState = null; let state: WorkflowState = null;
if (workflow) { if (workflow) {
state = workflow.getStateById(motion.state_id); 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);
} }
/** /**