Implement cinema mode
Implements a viewer mode containing the most important information and Heavily refactors ListOfSpeaker and CurrentListOfSpeaker interaction heavy components on a single view: Current List of Speakers Currently Open Polls Current projector Permission in migration
This commit is contained in:
parent
acbddd3c53
commit
ec13ab56e8
@ -3,6 +3,7 @@ import { Injectable, Injector } from '@angular/core';
|
||||
import { AgendaAppConfig } from '../../site/agenda/agenda.config';
|
||||
import { AppConfig, ModelEntry, SearchableModelEntry } from '../definitions/app-config';
|
||||
import { BaseRepository } from 'app/core/repositories/base-repository';
|
||||
import { CinemaAppConfig } from 'app/site/cinema/cinema.config';
|
||||
import { HistoryAppConfig } from 'app/site/history/history.config';
|
||||
import { ProjectorAppConfig } from 'app/site/projector/projector.config';
|
||||
import { TopicsAppConfig } from 'app/site/topics/topics.config';
|
||||
@ -35,7 +36,8 @@ const appConfigs: AppConfig[] = [
|
||||
UsersAppConfig,
|
||||
HistoryAppConfig,
|
||||
ProjectorAppConfig,
|
||||
TopicsAppConfig
|
||||
TopicsAppConfig,
|
||||
CinemaAppConfig
|
||||
];
|
||||
|
||||
/**
|
||||
|
@ -40,6 +40,7 @@ export enum Permission {
|
||||
coreCanSeeProjector = 'core.can_see_projector',
|
||||
coreCanManageTags = 'core.can_manage_tags',
|
||||
coreCanSeeLiveStream = 'core.can_see_livestream',
|
||||
coreCanSeeAutopilot = 'core.can_see_autopilot',
|
||||
mediafilesCanManage = 'mediafiles.can_manage',
|
||||
mediafilesCanSee = 'mediafiles.can_see',
|
||||
motionsCanCreate = 'motions.can_create',
|
||||
|
@ -317,7 +317,7 @@ export class ProjectorService {
|
||||
* @param element The projector element
|
||||
* @returns the view model from the projector element
|
||||
*/
|
||||
public getViewModelFromProjectorElement<T extends BaseProjectableViewModel>(
|
||||
public getViewModelFromIdentifiableProjectorElement<T extends BaseProjectableViewModel>(
|
||||
element: IdentifiableProjectorElement
|
||||
): T {
|
||||
this.assertElementIsMappable(element);
|
||||
@ -328,12 +328,16 @@ export class ProjectorService {
|
||||
return viewModel;
|
||||
}
|
||||
|
||||
public getViewModelFromProjectorElement<T extends BaseProjectableViewModel>(element: ProjectorElement): T {
|
||||
const idElement = this.slideManager.getIdentifiableProjectorElement(element);
|
||||
return this.getViewModelFromIdentifiableProjectorElement(idElement);
|
||||
}
|
||||
|
||||
/**
|
||||
*/
|
||||
public getSlideTitle(element: ProjectorElement): ProjectorTitle {
|
||||
if (this.slideManager.canSlideBeMappedToModel(element.name)) {
|
||||
const idElement = this.slideManager.getIdentifiableProjectorElement(element);
|
||||
const viewModel = this.getViewModelFromProjectorElement(idElement);
|
||||
const viewModel = this.getViewModelFromProjectorElement(element);
|
||||
if (viewModel) {
|
||||
return viewModel.getProjectorTitle();
|
||||
}
|
||||
|
@ -138,6 +138,14 @@ export class ProjectorRepositoryService extends BaseRepository<ViewProjector, Pr
|
||||
await this.http.post<void>(`/rest/core/projector/${projector_id}/set_reference_projector/`);
|
||||
}
|
||||
|
||||
public getReferenceProjectorObservable(): Observable<ViewProjector> {
|
||||
return this.getViewModelListObservable().pipe(
|
||||
map(projectors => {
|
||||
return projectors.find(projector => projector.isReferenceProjector);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* return the id of the current reference projector
|
||||
* prefer the observable whenever possible
|
||||
@ -146,15 +154,4 @@ export class ProjectorRepositoryService extends BaseRepository<ViewProjector, Pr
|
||||
// TODO: After logging in, this is null this.getViewModelList() is null
|
||||
return this.getViewModelList().find(projector => projector.isReferenceProjector).id;
|
||||
}
|
||||
|
||||
public getReferenceProjectorIdObservable(): Observable<number> {
|
||||
return this.getViewModelListObservable().pipe(
|
||||
map(projectors => {
|
||||
const refProjector = projectors.find(projector => projector.isReferenceProjector);
|
||||
if (refProjector) {
|
||||
return refProjector.id;
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ import { Component, Input } from '@angular/core';
|
||||
|
||||
import { AssignmentPollMethod } from 'app/shared/models/assignments/assignment-poll';
|
||||
import { ViewAssignmentPoll } from 'app/site/assignments/models/view-assignment-poll';
|
||||
import { AssignmentPollService } from 'app/site/assignments/services/assignment-poll.service';
|
||||
import { AssignmentPollService } from 'app/site/assignments/modules/assignment-poll/services/assignment-poll.service';
|
||||
import { PollData, PollTableData, VotingResult } from 'app/site/polls/services/poll.service';
|
||||
|
||||
@Component({
|
||||
|
@ -0,0 +1,171 @@
|
||||
<mat-card class="os-card speaker-card">
|
||||
<!-- Title -->
|
||||
<h1 class="los-title" *ngIf="!customTitle">
|
||||
<span>
|
||||
{{ title }}
|
||||
</span>
|
||||
<mat-icon *ngIf="closed" matTooltip="{{ 'The list of speakers is closed.' | translate }}">
|
||||
lock
|
||||
</mat-icon>
|
||||
</h1>
|
||||
<span *ngIf="customTitle">
|
||||
<ng-content></ng-content>
|
||||
</span>
|
||||
|
||||
<!-- List of finished speakers -->
|
||||
<mat-expansion-panel *ngIf="finishedSpeakers?.length" class="finished-list">
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title>{{ 'Last speakers' | translate }}</mat-panel-title>
|
||||
</mat-expansion-panel-header>
|
||||
<mat-list>
|
||||
<mat-list-item *ngFor="let speaker of finishedSpeakers; let number = index">
|
||||
<div class="finished-speaker-grid">
|
||||
<div class="number">{{ number + 1 }}.</div>
|
||||
<div class="name">{{ speaker.getTitle() }}</div>
|
||||
<div class="time">
|
||||
{{ durationString(speaker) }} ({{ 'Start time' | translate }}: {{ startTimeToString(speaker) }})
|
||||
</div>
|
||||
<div class="controls">
|
||||
<button
|
||||
mat-icon-button
|
||||
matTooltip="{{ 'Remove' | translate }}"
|
||||
*osPerms="'agenda.can_manage_list_of_speakers'"
|
||||
(click)="onDeleteButton(speaker)"
|
||||
>
|
||||
<mat-icon>close</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</mat-list-item>
|
||||
</mat-list>
|
||||
</mat-expansion-panel>
|
||||
|
||||
<!-- horizontal separation line -->
|
||||
<mat-divider *ngIf="finishedSpeakers && finishedSpeakers.length"></mat-divider>
|
||||
<div *ngIf="finishedSpeakers && finishedSpeakers.length" class="spacer-bottom-40"></div>
|
||||
|
||||
<!-- Current Speaker -->
|
||||
<div class="current-speaker" *ngIf="activeSpeaker">
|
||||
<span class="prefix">
|
||||
<mat-icon>mic</mat-icon>
|
||||
</span>
|
||||
|
||||
<span class="name">{{ activeSpeaker.getListTitle() }}</span>
|
||||
|
||||
<span class="suffix">
|
||||
<!-- Stop speaker button -->
|
||||
<button
|
||||
mat-icon-button
|
||||
matTooltip="{{ 'End speech' | translate }}"
|
||||
*osPerms="'agenda.can_manage_list_of_speakers'"
|
||||
(click)="onStopButton()"
|
||||
>
|
||||
<mat-icon>stop</mat-icon>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Waiting speakers -->
|
||||
<div class="waiting-list" *ngIf="waitingSpeakers?.length">
|
||||
<os-sorting-list
|
||||
[live]="true"
|
||||
[input]="waitingSpeakers"
|
||||
[count]="true"
|
||||
[enable]="opCanManage && (isSortMode || !isMobile)"
|
||||
(sortEvent)="onSortingChanged($event)"
|
||||
>
|
||||
<!-- implicit speaker references into the component using ng-template slot -->
|
||||
<ng-template let-speaker>
|
||||
<span *osPerms="'agenda.can_manage_list_of_speakers'">
|
||||
<!-- Speaker count -->
|
||||
<span *ngIf="hasSpokenCount(speaker)" class="red-warning-text speaker-warning">
|
||||
{{ hasSpokenCount(speaker) + 1 }}. <span>{{ 'contribution' | translate }}</span>
|
||||
</span>
|
||||
|
||||
<!-- First contribution -->
|
||||
<span
|
||||
*ngIf="showFistContributionHint && isFirstContribution(speaker)"
|
||||
class="red-warning-text speaker-warning"
|
||||
>
|
||||
{{ 'First speech' | translate }}
|
||||
</span>
|
||||
|
||||
<!-- Speaker gender -->
|
||||
<span *ngIf="speaker.gender">({{ speaker.gender | translate }})</span>
|
||||
</span>
|
||||
|
||||
<!-- Start, start and delete buttons -->
|
||||
<span *osPerms="'agenda.can_manage_list_of_speakers'">
|
||||
<!-- start button -->
|
||||
<button
|
||||
mat-icon-button
|
||||
matTooltip="{{ 'Begin speech' | translate }}"
|
||||
(click)="onStartButton(speaker)"
|
||||
>
|
||||
<mat-icon>play_arrow</mat-icon>
|
||||
</button>
|
||||
|
||||
<!-- star button -->
|
||||
<button
|
||||
mat-icon-button
|
||||
matTooltip="{{ 'Mark speaker' | translate }}"
|
||||
(click)="onMarkButton(speaker)"
|
||||
>
|
||||
<mat-icon>{{ speaker.marked ? 'star' : 'star_border' }}</mat-icon>
|
||||
</button>
|
||||
|
||||
<!-- delete button -->
|
||||
<button mat-icon-button matTooltip="{{ 'Remove' | translate }}" (click)="onDeleteButton(speaker)">
|
||||
<mat-icon>close</mat-icon>
|
||||
</button>
|
||||
</span>
|
||||
|
||||
<!-- For thouse without LOS -->
|
||||
<span *osPerms="'agenda.can_manage_list_of_speakers'; complement: true">
|
||||
<mat-icon *ngIf="speaker.marked">
|
||||
star
|
||||
</mat-icon>
|
||||
</span>
|
||||
</ng-template>
|
||||
</os-sorting-list>
|
||||
</div>
|
||||
|
||||
<!-- Search for speakers -->
|
||||
<div *osPerms="'agenda.can_manage_list_of_speakers'">
|
||||
<form
|
||||
*ngIf="waitingSpeakers && filteredUsers?.value?.length"
|
||||
[formGroup]="addSpeakerForm"
|
||||
class="search-new-speaker-form"
|
||||
>
|
||||
<mat-form-field class="search-users-field">
|
||||
<os-search-value-selector
|
||||
class="search-users"
|
||||
formControlName="user_id"
|
||||
placeholder="{{ 'Select or search new speaker ...' | translate }}"
|
||||
[inputListValues]="filteredUsers"
|
||||
[showNotFoundButton]="true"
|
||||
(clickNotFound)="onCreateUser($event)"
|
||||
>
|
||||
<ng-container notFoundDescription>
|
||||
<mat-icon>add</mat-icon>
|
||||
{{ 'Create user' | translate }}
|
||||
</ng-container>
|
||||
</os-search-value-selector>
|
||||
</mat-form-field>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Add me and remove me if OP has correct permission -->
|
||||
<div *osPerms="'agenda.can_be_speaker'" class="add-self-buttons">
|
||||
<div *ngIf="waitingSpeakers && !closed">
|
||||
<button mat-stroked-button [disabled]="closed" (click)="addNewSpeaker()" *ngIf="!isOpInList && canAddSelf">
|
||||
<mat-icon>add</mat-icon>
|
||||
<span>{{ 'Add me' | translate }}</span>
|
||||
</button>
|
||||
<button mat-stroked-button (click)="onDeleteButton()" *ngIf="isOpInList">
|
||||
<mat-icon>remove</mat-icon>
|
||||
<span>{{ 'Remove me' | translate }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</mat-card>
|
@ -0,0 +1,125 @@
|
||||
@import '~assets/styles/variables.scss';
|
||||
|
||||
.los-title {
|
||||
margin-left: 25px;
|
||||
}
|
||||
|
||||
.finished-list {
|
||||
box-shadow: none !important;
|
||||
margin-bottom: 15px;
|
||||
|
||||
.mat-list-item {
|
||||
height: auto;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.finished-speaker-grid {
|
||||
display: grid;
|
||||
width: 100%;
|
||||
grid-gap: 5px;
|
||||
|
||||
grid-template-areas:
|
||||
'number name controls'
|
||||
'number time controls';
|
||||
grid-template-columns: 30px 1fr min-content;
|
||||
}
|
||||
|
||||
@include desktop {
|
||||
.finished-speaker-grid {
|
||||
grid-template-areas: 'number name time controls';
|
||||
grid-template-columns: 30px 1fr min-content min-content;
|
||||
}
|
||||
}
|
||||
|
||||
.number {
|
||||
grid-area: number;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.name {
|
||||
grid-area: name;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.time {
|
||||
grid-area: time;
|
||||
margin: 0;
|
||||
//allows pushing this grid area as small as possible and aligns the end to the same level
|
||||
white-space: nowrap;
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
.controls {
|
||||
grid-area: controls;
|
||||
margin: 0;
|
||||
opacity: 0.7;
|
||||
|
||||
.mat-icon-button {
|
||||
height: 20px;
|
||||
line-height: 1;
|
||||
|
||||
.mat-icon {
|
||||
line-height: 19px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.current-speaker {
|
||||
display: table;
|
||||
width: -webkit-fill-available;
|
||||
height: 50px;
|
||||
margin: 50px 25px 20px 25px;
|
||||
box-shadow: 0px 3px 10px 0px rgba(0, 0, 0, 0.25);
|
||||
|
||||
.prefix {
|
||||
display: table-cell;
|
||||
padding: 0 15px;
|
||||
vertical-align: middle;
|
||||
.mat-icon {
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
.name {
|
||||
display: table-cell;
|
||||
vertical-align: middle;
|
||||
font-weight: bold;
|
||||
padding-left: 10px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.suffix {
|
||||
display: table-cell;
|
||||
vertical-align: middle;
|
||||
white-space: nowrap;
|
||||
padding-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.waiting-list {
|
||||
padding: 10px 25px 0 25px;
|
||||
}
|
||||
|
||||
.search-new-speaker-form {
|
||||
padding: 15px 25px 10px 25px;
|
||||
width: auto;
|
||||
|
||||
.search-users-field {
|
||||
width: 100%;
|
||||
.search-users {
|
||||
display: grid;
|
||||
.mat-form-field {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.add-self-buttons {
|
||||
padding: 15px 0 20px 25px;
|
||||
}
|
||||
|
||||
.speaker-warning {
|
||||
margin-right: 5px;
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { E2EImportsModule } from 'e2e-imports.module';
|
||||
|
||||
import { ListOfSpeakersContentComponent } from './list-of-speakers-content.component';
|
||||
|
||||
describe('ListOfSpeakersContentComponent', () => {
|
||||
let component: ListOfSpeakersContentComponent;
|
||||
let fixture: ComponentFixture<ListOfSpeakersContentComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [E2EImportsModule]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ListOfSpeakersContentComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,344 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
EventEmitter,
|
||||
Input,
|
||||
OnInit,
|
||||
Output,
|
||||
ViewChild
|
||||
} from '@angular/core';
|
||||
import { FormControl, FormGroup } from '@angular/forms';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { Title } from '@angular/platform-browser';
|
||||
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
|
||||
import { OperatorService } from 'app/core/core-services/operator.service';
|
||||
import { ListOfSpeakersRepositoryService } from 'app/core/repositories/agenda/list-of-speakers-repository.service';
|
||||
import { UserRepositoryService } from 'app/core/repositories/users/user-repository.service';
|
||||
import { ConfigService } from 'app/core/ui-services/config.service';
|
||||
import { DurationService } from 'app/core/ui-services/duration.service';
|
||||
import { PromptService } from 'app/core/ui-services/prompt.service';
|
||||
import { ViewportService } from 'app/core/ui-services/viewport.service';
|
||||
import { ViewListOfSpeakers } from 'app/site/agenda/models/view-list-of-speakers';
|
||||
import { SpeakerState, ViewSpeaker } from 'app/site/agenda/models/view-speaker';
|
||||
import { BaseViewComponent } from 'app/site/base/base-view';
|
||||
import { ViewUser } from 'app/site/users/models/view-user';
|
||||
import { Selectable } from '../selectable';
|
||||
import { SortingListComponent } from '../sorting-list/sorting-list.component';
|
||||
|
||||
@Component({
|
||||
selector: 'os-list-of-speakers-content',
|
||||
templateUrl: './list-of-speakers-content.component.html',
|
||||
styleUrls: ['./list-of-speakers-content.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class ListOfSpeakersContentComponent extends BaseViewComponent implements OnInit {
|
||||
@ViewChild(SortingListComponent)
|
||||
public listElement: SortingListComponent;
|
||||
|
||||
private viewListOfSpeakers: ViewListOfSpeakers;
|
||||
public finishedSpeakers: ViewSpeaker[];
|
||||
public waitingSpeakers: ViewSpeaker[];
|
||||
public activeSpeaker: ViewSpeaker;
|
||||
|
||||
/**
|
||||
* Required for the user search selector
|
||||
*/
|
||||
public addSpeakerForm: FormGroup;
|
||||
|
||||
public users = new BehaviorSubject<ViewUser[]>([]);
|
||||
public filteredUsers = new BehaviorSubject<ViewUser[]>([]);
|
||||
|
||||
public isSortMode: boolean;
|
||||
|
||||
public isMobile: boolean;
|
||||
|
||||
public showFistContributionHint: boolean;
|
||||
|
||||
public get title(): string {
|
||||
return this.viewListOfSpeakers?.getTitle();
|
||||
}
|
||||
|
||||
public get closed(): boolean {
|
||||
return this.viewListOfSpeakers?.closed;
|
||||
}
|
||||
|
||||
public get opCanManage(): boolean {
|
||||
return this.operator.hasPerms(this.permission.agendaCanManageListOfSpeakers);
|
||||
}
|
||||
|
||||
public get isOpInList(): boolean {
|
||||
return this.waitingSpeakers.some(speaker => speaker.user_id === this.operator.user.id);
|
||||
}
|
||||
|
||||
public get canAddSelf(): boolean {
|
||||
return !this.config.instant('agenda_present_speakers_only') || this.operator.user.is_present;
|
||||
}
|
||||
|
||||
@Input()
|
||||
public set speakers(los: ViewListOfSpeakers) {
|
||||
this.setListOfSpeakers(los);
|
||||
}
|
||||
|
||||
@Input()
|
||||
public customTitle: boolean;
|
||||
|
||||
@Input()
|
||||
public set sortMode(isActive: boolean) {
|
||||
if (this.isSortMode) {
|
||||
this.listElement.restore();
|
||||
}
|
||||
this.isSortMode = isActive;
|
||||
}
|
||||
|
||||
@Output()
|
||||
private isListOfSpeakersEmptyEvent = new EventEmitter<boolean>();
|
||||
|
||||
@Output()
|
||||
private hasFinishesSpeakersEvent = new EventEmitter<boolean>();
|
||||
|
||||
public constructor(
|
||||
title: Title,
|
||||
protected translate: TranslateService,
|
||||
snackBar: MatSnackBar,
|
||||
private listOfSpeakersRepo: ListOfSpeakersRepositoryService,
|
||||
private operator: OperatorService,
|
||||
private promptService: PromptService,
|
||||
private durationService: DurationService,
|
||||
private userRepository: UserRepositoryService,
|
||||
private config: ConfigService,
|
||||
private viewport: ViewportService,
|
||||
private cd: ChangeDetectorRef
|
||||
) {
|
||||
super(title, translate, snackBar);
|
||||
this.addSpeakerForm = new FormGroup({ user_id: new FormControl() });
|
||||
}
|
||||
|
||||
public ngOnInit(): void {
|
||||
this.subscriptions.push(
|
||||
// Observe the user list
|
||||
this.userRepository.getViewModelListObservable().subscribe(users => {
|
||||
this.users.next(users);
|
||||
this.filterUsers();
|
||||
this.cd.markForCheck();
|
||||
}),
|
||||
// ovserve changes to the add-speaker 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);
|
||||
}
|
||||
}),
|
||||
// observe changes to the viewport
|
||||
this.viewport.isMobileSubject.subscribe(isMobile => {
|
||||
this.isMobile = isMobile;
|
||||
this.cd.markForCheck();
|
||||
}),
|
||||
// observe changes the agenda_present_speakers_only config
|
||||
this.config.get('agenda_present_speakers_only').subscribe(() => {
|
||||
this.filterUsers();
|
||||
}),
|
||||
// observe changes to the agenda_show_first_contribution config
|
||||
this.config.get<boolean>('agenda_show_first_contribution').subscribe(show => {
|
||||
this.showFistContributionHint = show;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private isListOfSpeakersEmpty(): void {
|
||||
if (this.waitingSpeakers && this.waitingSpeakers.length) {
|
||||
this.isListOfSpeakersEmptyEvent.emit(false);
|
||||
} else if (this.finishedSpeakers && this.finishedSpeakers.length) {
|
||||
this.isListOfSpeakersEmptyEvent.emit(false);
|
||||
}
|
||||
return this.isListOfSpeakersEmptyEvent.emit(!this.activeSpeaker);
|
||||
}
|
||||
|
||||
private hasFinishesSpeakers(): void {
|
||||
this.hasFinishesSpeakersEvent.emit(this.finishedSpeakers?.length > 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 addNewSpeaker(userId?: number): void {
|
||||
this.listOfSpeakersRepo
|
||||
.createSpeaker(this.viewListOfSpeakers, userId)
|
||||
.then(() => this.addSpeakerForm.reset(), this.raiseError);
|
||||
}
|
||||
|
||||
/**
|
||||
* Click on the X button - removes the speaker from the list of speakers
|
||||
*
|
||||
* @param speaker optional speaker to remove. If none is given,
|
||||
* the operator themself is removed
|
||||
*/
|
||||
public async onDeleteButton(speaker?: ViewSpeaker): Promise<void> {
|
||||
const title = this.translate.instant(
|
||||
'Are you sure you want to delete this speaker from this list of speakers?'
|
||||
);
|
||||
if (await this.promptService.open(title)) {
|
||||
try {
|
||||
await this.listOfSpeakersRepo.delete(this.viewListOfSpeakers, speaker ? speaker.id : null);
|
||||
this.filterUsers();
|
||||
} catch (e) {
|
||||
this.raiseError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Click on the mic button to mark a speaker as speaking
|
||||
*
|
||||
* @param speaker the speaker marked in the list
|
||||
*/
|
||||
public async onStartButton(speaker: ViewSpeaker): Promise<void> {
|
||||
try {
|
||||
await this.listOfSpeakersRepo.startSpeaker(this.viewListOfSpeakers, speaker);
|
||||
this.filterUsers();
|
||||
} catch (e) {
|
||||
this.raiseError(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Click on the mic-cross button to stop the current speaker
|
||||
*/
|
||||
public async onStopButton(): Promise<void> {
|
||||
try {
|
||||
await this.listOfSpeakersRepo.stopCurrentSpeaker(this.viewListOfSpeakers);
|
||||
this.filterUsers();
|
||||
} catch (e) {
|
||||
this.raiseError(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Click on the star button. Toggles the marked attribute.
|
||||
*
|
||||
* @param speaker The speaker clicked on.
|
||||
*/
|
||||
public onMarkButton(speaker: ViewSpeaker): void {
|
||||
this.listOfSpeakersRepo.markSpeaker(this.viewListOfSpeakers, speaker, !speaker.marked).catch(this.raiseError);
|
||||
}
|
||||
|
||||
/**
|
||||
* Receives an updated list from sorting-event.
|
||||
*
|
||||
* @param sortedSpeakerList The updated list.
|
||||
*/
|
||||
public onSortingChanged(sortedSpeakerList: Selectable[]): void {
|
||||
if (!this.isMobile) {
|
||||
this.onSaveSorting(sortedSpeakerList);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* send the current order of the sorting list to the server
|
||||
*
|
||||
* @param sortedSpeakerList The list to save.
|
||||
*/
|
||||
public async onSaveSorting(sortedSpeakerList: Selectable[] = this.listElement.sortedItems): Promise<void> {
|
||||
return await this.listOfSpeakersRepo
|
||||
.sortSpeakers(
|
||||
this.viewListOfSpeakers,
|
||||
sortedSpeakerList.map(el => el.id)
|
||||
)
|
||||
.catch(this.raiseError);
|
||||
}
|
||||
|
||||
private setListOfSpeakers(viewListOfSpeakers: ViewListOfSpeakers | undefined): void {
|
||||
this.viewListOfSpeakers = viewListOfSpeakers;
|
||||
const allSpeakers = viewListOfSpeakers?.speakers.sort((a, b) => a.weight - b.weight);
|
||||
this.waitingSpeakers = allSpeakers?.filter(speaker => speaker.state === SpeakerState.WAITING);
|
||||
this.waitingSpeakers?.sort((a: ViewSpeaker, b: ViewSpeaker) => a.weight - b.weight);
|
||||
this.filterUsers();
|
||||
this.finishedSpeakers = allSpeakers?.filter(speaker => speaker.state === SpeakerState.FINISHED);
|
||||
|
||||
// convert begin time to date and sort
|
||||
this.finishedSpeakers?.sort((a: ViewSpeaker, b: ViewSpeaker) => {
|
||||
const aTime = new Date(a.begin_time).getTime();
|
||||
const bTime = new Date(b.begin_time).getTime();
|
||||
return aTime - bTime;
|
||||
});
|
||||
|
||||
this.activeSpeaker = allSpeakers?.find(speaker => speaker.state === SpeakerState.CURRENT);
|
||||
this.hasFinishesSpeakers();
|
||||
this.isListOfSpeakersEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers an update of the filter for the list of available potential speakers
|
||||
* (triggered on an update of users or config)
|
||||
*/
|
||||
private filterUsers(): void {
|
||||
const presentUsersOnly = this.config.instant('agenda_present_speakers_only');
|
||||
const users = presentUsersOnly ? this.users.getValue().filter(u => u.is_present) : this.users.getValue();
|
||||
if (!this.waitingSpeakers || !this.waitingSpeakers.length) {
|
||||
this.filteredUsers.next(users);
|
||||
} else {
|
||||
this.filteredUsers.next(users.filter(u => !this.waitingSpeakers.some(speaker => speaker.user_id === u.id)));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Imports a new user by the given username.
|
||||
*
|
||||
* @param username The name of the new user.
|
||||
*/
|
||||
public async onCreateUser(username: string): Promise<void> {
|
||||
const newUser = await this.userRepository.createFromString(username);
|
||||
this.addNewSpeaker(newUser.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks how often a speaker has already finished speaking
|
||||
*
|
||||
* @param speaker
|
||||
* @returns 0 or the number of times a speaker occurs in finishedSpeakers
|
||||
*/
|
||||
public hasSpokenCount(speaker: ViewSpeaker): number {
|
||||
return this.finishedSpeakers.filter(finishedSpeaker => {
|
||||
if (finishedSpeaker && finishedSpeaker.user) {
|
||||
return finishedSpeaker.user.id === speaker.user.id;
|
||||
}
|
||||
}).length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the speaker did never appear on any list of speakers
|
||||
*
|
||||
* @param speaker
|
||||
*/
|
||||
public isFirstContribution(speaker: ViewSpeaker): boolean {
|
||||
return this.listOfSpeakersRepo.isFirstContribution(speaker);
|
||||
}
|
||||
|
||||
/**
|
||||
* get the duration of a speech
|
||||
*
|
||||
* @param speaker
|
||||
* @returns string representation of the duration in `[MM]M:SS minutes` format
|
||||
*/
|
||||
public durationString(speaker: ViewSpeaker): string {
|
||||
const duration = Math.floor(
|
||||
(new Date(speaker.end_time).valueOf() - new Date(speaker.begin_time).valueOf()) / 1000
|
||||
);
|
||||
return `${this.durationService.durationToString(duration, 'm')}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* returns a locale-specific version of the starting time for the given speaker item
|
||||
*
|
||||
* @param speaker
|
||||
* @returns a time string using the current language setting of the client
|
||||
*/
|
||||
public startTimeToString(speaker: ViewSpeaker): string {
|
||||
return new Date(speaker.begin_time).toLocaleString(this.translate.currentLang);
|
||||
}
|
||||
}
|
@ -2,7 +2,7 @@ import { inject, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { E2EImportsModule } from 'e2e-imports.module';
|
||||
|
||||
import { AssignmentPollService } from 'app/site/assignments/services/assignment-poll.service';
|
||||
import { AssignmentPollService } from 'app/site/assignments/modules/assignment-poll/services/assignment-poll.service';
|
||||
import { MotionPollService } from 'app/site/motions/services/motion-poll.service';
|
||||
import { PollPercentBasePipe } from './poll-percent-base.pipe';
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core';
|
||||
|
||||
import { AssignmentPollService } from 'app/site/assignments/services/assignment-poll.service';
|
||||
import { AssignmentPollService } from 'app/site/assignments/modules/assignment-poll/services/assignment-poll.service';
|
||||
import { MotionPollService } from 'app/site/motions/services/motion-poll.service';
|
||||
import { PollData } from 'app/site/polls/services/poll.service';
|
||||
|
||||
|
@ -129,6 +129,7 @@ import { UserMenuComponent } from './components/user-menu/user-menu.component';
|
||||
import { JitsiComponent } from './components/jitsi/jitsi.component';
|
||||
import { VjsPlayerComponent } from './components/vjs-player/vjs-player.component';
|
||||
import { LiveStreamComponent } from './components/live-stream/live-stream.component';
|
||||
import { ListOfSpeakersContentComponent } from './components/list-of-speakers-content/list-of-speakers-content.component';
|
||||
|
||||
/**
|
||||
* Share Module for all "dumb" components and pipes.
|
||||
@ -298,7 +299,8 @@ import { LiveStreamComponent } from './components/live-stream/live-stream.compon
|
||||
AssignmentPollDetailContentComponent,
|
||||
JitsiComponent,
|
||||
VjsPlayerComponent,
|
||||
LiveStreamComponent
|
||||
LiveStreamComponent,
|
||||
ListOfSpeakersContentComponent
|
||||
],
|
||||
declarations: [
|
||||
PermsDirective,
|
||||
@ -361,7 +363,8 @@ import { LiveStreamComponent } from './components/live-stream/live-stream.compon
|
||||
AssignmentPollDetailContentComponent,
|
||||
JitsiComponent,
|
||||
VjsPlayerComponent,
|
||||
LiveStreamComponent
|
||||
LiveStreamComponent,
|
||||
ListOfSpeakersContentComponent
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
|
@ -1,8 +1,8 @@
|
||||
<os-head-bar
|
||||
[nav]="false"
|
||||
[goBack]="true"
|
||||
[editMode]="isMobile && isSortMode"
|
||||
(cancelEditEvent)="onCancelSorting()"
|
||||
[editMode]="isMobile && manualSortMode"
|
||||
(cancelEditEvent)="setManualSortMode(false)"
|
||||
(saveEvent)="onMobileSaveSorting()"
|
||||
>
|
||||
<!-- Title -->
|
||||
@ -18,7 +18,7 @@
|
||||
mat-icon-button
|
||||
matTooltip="{{ 'Re-add last speaker' | translate }}"
|
||||
(click)="readdLastSpeaker()"
|
||||
[disabled]="!finishedSpeakers || !finishedSpeakers.length"
|
||||
[disabled]="!hasFinishedSpeakers"
|
||||
>
|
||||
<mat-icon>undo</mat-icon>
|
||||
</button>
|
||||
@ -26,167 +26,16 @@
|
||||
</div>
|
||||
</os-head-bar>
|
||||
|
||||
<mat-card class="os-card speaker-card" *ngIf="viewListOfSpeakers">
|
||||
<!-- Title -->
|
||||
<h1 class="los-title">
|
||||
{{ viewListOfSpeakers.getTitle() }}
|
||||
<mat-icon *ngIf="viewListOfSpeakers.closed">
|
||||
lock
|
||||
</mat-icon>
|
||||
</h1>
|
||||
|
||||
<!-- List of finished speakers -->
|
||||
<mat-expansion-panel *ngIf="finishedSpeakers && finishedSpeakers.length > 0" class="finished-list">
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title>{{ 'Last speakers' | translate }}</mat-panel-title>
|
||||
</mat-expansion-panel-header>
|
||||
<mat-list>
|
||||
<mat-list-item *ngFor="let speaker of finishedSpeakers; let number = index">
|
||||
<div class="finished-speaker-grid">
|
||||
<div class="number">{{ number + 1 }}.</div>
|
||||
<div class="name">{{ speaker.getTitle() }}</div>
|
||||
<div class="time">
|
||||
{{ durationString(speaker) }} ({{ 'Start time' | translate }}: {{ startTimeToString(speaker) }})
|
||||
</div>
|
||||
<div class="controls">
|
||||
<button
|
||||
mat-icon-button
|
||||
matTooltip="{{ 'Remove' | translate }}"
|
||||
*osPerms="'agenda.can_manage_list_of_speakers'"
|
||||
(click)="onDeleteButton(speaker)"
|
||||
>
|
||||
<mat-icon>close</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</mat-list-item>
|
||||
</mat-list>
|
||||
</mat-expansion-panel>
|
||||
|
||||
<!-- horizontal separation line -->
|
||||
<mat-divider *ngIf="finishedSpeakers && finishedSpeakers.length"></mat-divider>
|
||||
<div *ngIf="finishedSpeakers && finishedSpeakers.length" class="spacer-bottom-40"></div>
|
||||
|
||||
<!-- Current Speaker -->
|
||||
<div class="current-speaker" *ngIf="activeSpeaker">
|
||||
<span class="prefix">
|
||||
<mat-icon>mic</mat-icon>
|
||||
</span>
|
||||
|
||||
<span class="name">{{ activeSpeaker.getListTitle() }}</span>
|
||||
|
||||
<span class="suffix">
|
||||
<!-- Stop speaker button -->
|
||||
<button
|
||||
mat-icon-button
|
||||
matTooltip="{{ 'End speech' | translate }}"
|
||||
*osPerms="'agenda.can_manage_list_of_speakers'"
|
||||
(click)="onStopButton()"
|
||||
>
|
||||
<mat-icon>stop</mat-icon>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Waiting speakers -->
|
||||
<div class="waiting-list" *ngIf="speakers && speakers.length > 0">
|
||||
<os-sorting-list
|
||||
[live]="true"
|
||||
[input]="speakers"
|
||||
[count]="true"
|
||||
[enable]="opCanManage() && isSortMode"
|
||||
(sortEvent)="onSortingChanged($event)"
|
||||
>
|
||||
<!-- implicit speaker references into the component using ng-template slot -->
|
||||
<ng-template let-speaker>
|
||||
<span *osPerms="'agenda.can_manage_list_of_speakers'">
|
||||
|
||||
<!-- Speaker count -->
|
||||
<span *ngIf="hasSpokenCount(speaker)" class="red-warning-text speaker-warning">
|
||||
{{ hasSpokenCount(speaker) + 1 }}. <span>{{ 'contribution' | translate }}</span>
|
||||
</span>
|
||||
|
||||
<!-- First contribution -->
|
||||
<span *ngIf="showFistContributionHint && isFirstContribution(speaker)" class="red-warning-text speaker-warning">
|
||||
{{ 'First speech' | translate }}
|
||||
</span>
|
||||
|
||||
<!-- Speaker gender -->
|
||||
<span *ngIf="speaker.gender">({{ speaker.gender | translate }})</span>
|
||||
</span>
|
||||
|
||||
<!-- Start, start and delete buttons -->
|
||||
<span *osPerms="'agenda.can_manage_list_of_speakers'">
|
||||
<!-- start button -->
|
||||
<button
|
||||
mat-icon-button
|
||||
matTooltip="{{ 'Begin speech' | translate }}"
|
||||
(click)="onStartButton(speaker)"
|
||||
>
|
||||
<mat-icon>play_arrow</mat-icon>
|
||||
</button>
|
||||
|
||||
<!-- star button -->
|
||||
<button
|
||||
mat-icon-button
|
||||
matTooltip="{{ 'Mark speaker' | translate }}"
|
||||
(click)="onMarkButton(speaker)"
|
||||
>
|
||||
<mat-icon>{{ speaker.marked ? 'star' : 'star_border' }}</mat-icon>
|
||||
</button>
|
||||
|
||||
<!-- delete button -->
|
||||
<button mat-icon-button matTooltip="{{ 'Remove' | translate }}" (click)="onDeleteButton(speaker)">
|
||||
<mat-icon>close</mat-icon>
|
||||
</button>
|
||||
</span>
|
||||
</ng-template>
|
||||
</os-sorting-list>
|
||||
</div>
|
||||
|
||||
<!-- Search for speakers -->
|
||||
<div *osPerms="'agenda.can_manage_list_of_speakers'">
|
||||
<form *ngIf="filteredUsers && filteredUsers.value.length > 0" [formGroup]="addSpeakerForm">
|
||||
<mat-form-field class="search-users-field">
|
||||
<os-search-value-selector
|
||||
class="search-users"
|
||||
formControlName="user_id"
|
||||
placeholder="{{ 'Select or search new speaker ...' | translate }}"
|
||||
[inputListValues]="filteredUsers"
|
||||
[showNotFoundButton]="true"
|
||||
(clickNotFound)="onCreateUser($event)"
|
||||
>
|
||||
<ng-container notFoundDescription>
|
||||
<mat-icon>add</mat-icon>
|
||||
{{ 'Create user' | translate }}
|
||||
</ng-container>
|
||||
</os-search-value-selector>
|
||||
</mat-form-field>
|
||||
</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 && !closedList">
|
||||
<button
|
||||
mat-stroked-button
|
||||
[disabled]="viewListOfSpeakers.closed"
|
||||
(click)="addNewSpeaker()"
|
||||
*ngIf="!isOpInList() && canAddSelf"
|
||||
>
|
||||
<mat-icon>add</mat-icon>
|
||||
<span>{{ 'Add me' | translate }}</span>
|
||||
</button>
|
||||
<button mat-stroked-button (click)="onDeleteButton()" *ngIf="isOpInList()">
|
||||
<mat-icon>remove</mat-icon>
|
||||
<span>{{ 'Remove me' | translate }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</mat-card>
|
||||
<os-list-of-speakers-content
|
||||
#content
|
||||
[speakers]="viewListOfSpeakers"
|
||||
[sortMode]="manualSortMode"
|
||||
(isListOfSpeakersEmptyEvent)="isListOfSpeakersEmpty = $event"
|
||||
(hasFinishesSpeakersEvent)="hasFinishedSpeakers = $event"
|
||||
></os-list-of-speakers-content>
|
||||
|
||||
<mat-menu #speakerMenu="matMenu">
|
||||
<button *ngIf="isMobile" mat-menu-item (click)="isSortMode = true">
|
||||
<button *ngIf="isMobile" mat-menu-item (click)="setManualSortMode(true)">
|
||||
<mat-icon>sort</mat-icon>
|
||||
<span>{{ 'Sort' | translate }}</span>
|
||||
</button>
|
||||
@ -222,9 +71,9 @@
|
||||
<span>{{ 'Close list of speakers' | translate }}</span>
|
||||
</button>
|
||||
|
||||
<mat-divider *ngIf="!isListOfSpeakersEmpty"></mat-divider>
|
||||
<mat-divider></mat-divider>
|
||||
|
||||
<button mat-menu-item (click)="clearSpeakerList()" *ngIf="!isListOfSpeakersEmpty" class="red-warning-text">
|
||||
<button mat-menu-item (click)="clearSpeakerList()" [disabled]="isListOfSpeakersEmpty" class="red-warning-text">
|
||||
<mat-icon>delete</mat-icon>
|
||||
<span>{{ 'Remove all speakers' | translate }}</span>
|
||||
</button>
|
||||
|
@ -1,125 +0,0 @@
|
||||
@import '~assets/styles/variables.scss';
|
||||
|
||||
.los-title {
|
||||
margin-left: 25px;
|
||||
}
|
||||
|
||||
.finished-list {
|
||||
box-shadow: none !important;
|
||||
margin-bottom: 15px;
|
||||
|
||||
.mat-list-item {
|
||||
height: auto;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.finished-speaker-grid {
|
||||
display: grid;
|
||||
width: 100%;
|
||||
grid-gap: 5px;
|
||||
|
||||
grid-template-areas:
|
||||
'number name controls'
|
||||
'number time controls';
|
||||
grid-template-columns: 30px 1fr min-content;
|
||||
}
|
||||
|
||||
@include desktop {
|
||||
.finished-speaker-grid {
|
||||
grid-template-areas: 'number name time controls';
|
||||
grid-template-columns: 30px 1fr min-content min-content;
|
||||
}
|
||||
}
|
||||
|
||||
.number {
|
||||
grid-area: number;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.name {
|
||||
grid-area: name;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.time {
|
||||
grid-area: time;
|
||||
margin: 0;
|
||||
//allows pushing this grid area as small as possible and aligns the end to the same level
|
||||
white-space: nowrap;
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
.controls {
|
||||
grid-area: controls;
|
||||
margin: 0;
|
||||
opacity: 0.7;
|
||||
|
||||
.mat-icon-button {
|
||||
height: 20px;
|
||||
line-height: 1;
|
||||
|
||||
.mat-icon {
|
||||
line-height: 19px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.current-speaker {
|
||||
display: table;
|
||||
width: -webkit-fill-available;
|
||||
height: 50px;
|
||||
margin: 50px 25px 20px 25px;
|
||||
box-shadow: 0px 3px 10px 0px rgba(0, 0, 0, 0.25);
|
||||
|
||||
.prefix {
|
||||
display: table-cell;
|
||||
padding: 0 15px;
|
||||
vertical-align: middle;
|
||||
.mat-icon {
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
.name {
|
||||
display: table-cell;
|
||||
vertical-align: middle;
|
||||
font-weight: bold;
|
||||
padding-left: 10px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.suffix {
|
||||
display: table-cell;
|
||||
vertical-align: middle;
|
||||
white-space: nowrap;
|
||||
padding-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.waiting-list {
|
||||
padding: 10px 25px 0 25px;
|
||||
}
|
||||
|
||||
form {
|
||||
padding: 15px 25px 10px 25px;
|
||||
width: auto;
|
||||
|
||||
.search-users-field {
|
||||
width: 100%;
|
||||
.search-users {
|
||||
display: grid;
|
||||
.mat-form-field {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.add-self-buttons {
|
||||
padding: 15px 0 20px 25px;
|
||||
}
|
||||
|
||||
.speaker-warning {
|
||||
margin-right: 5px;
|
||||
}
|
@ -1,31 +1,21 @@
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit, ViewChild } from '@angular/core';
|
||||
import { FormControl, FormGroup } from '@angular/forms';
|
||||
import { Component, OnInit, ViewChild } from '@angular/core';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { Title } from '@angular/platform-browser';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { BehaviorSubject, Subscription } from 'rxjs';
|
||||
|
||||
import { CollectionStringMapperService } from 'app/core/core-services/collection-string-mapper.service';
|
||||
import { OperatorService, Permission } from 'app/core/core-services/operator.service';
|
||||
import { ListOfSpeakersRepositoryService } from 'app/core/repositories/agenda/list-of-speakers-repository.service';
|
||||
import { ProjectorRepositoryService } from 'app/core/repositories/projector/projector-repository.service';
|
||||
import { UserRepositoryService } from 'app/core/repositories/users/user-repository.service';
|
||||
import { ConfigService } from 'app/core/ui-services/config.service';
|
||||
import { DurationService } from 'app/core/ui-services/duration.service';
|
||||
import { PromptService } from 'app/core/ui-services/prompt.service';
|
||||
import { ViewportService } from 'app/core/ui-services/viewport.service';
|
||||
import { Selectable } from 'app/shared/components/selectable';
|
||||
import { SortingListComponent } from 'app/shared/components/sorting-list/sorting-list.component';
|
||||
import { ListOfSpeakersContentComponent } from 'app/shared/components/list-of-speakers-content/list-of-speakers-content.component';
|
||||
import { BaseViewComponent } from 'app/site/base/base-view';
|
||||
import { ProjectorElementBuildDeskriptor } from 'app/site/base/projectable';
|
||||
import { ViewProjector } from 'app/site/projector/models/view-projector';
|
||||
import { CurrentListOfSpeakersSlideService } from 'app/site/projector/services/current-list-of-speakers-slide.service';
|
||||
import { CurrentListOfSpeakersService } from 'app/site/projector/services/current-list-of-speakers.service';
|
||||
import { ViewUser } from 'app/site/users/models/view-user';
|
||||
import { ViewListOfSpeakers } from '../../models/view-list-of-speakers';
|
||||
import { SpeakerState, ViewSpeaker } from '../../models/view-speaker';
|
||||
|
||||
/**
|
||||
* The list of speakers for agenda items.
|
||||
@ -33,73 +23,26 @@ import { SpeakerState, ViewSpeaker } from '../../models/view-speaker';
|
||||
@Component({
|
||||
selector: 'os-list-of-speakers',
|
||||
templateUrl: './list-of-speakers.component.html',
|
||||
styleUrls: ['./list-of-speakers.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
styleUrls: ['./list-of-speakers.component.scss']
|
||||
})
|
||||
export class ListOfSpeakersComponent extends BaseViewComponent implements OnInit {
|
||||
@ViewChild(SortingListComponent)
|
||||
public listElement: SortingListComponent;
|
||||
|
||||
@ViewChild('content')
|
||||
private listOfSpeakersContentComponent: ListOfSpeakersContentComponent;
|
||||
/**
|
||||
* Determine if the user is viewing the current list if speakers
|
||||
*/
|
||||
public isCurrentListOfSpeakers = false;
|
||||
|
||||
/**
|
||||
* Holds whether the list is in sort mode or not
|
||||
*/
|
||||
public isSortMode = false;
|
||||
|
||||
/**
|
||||
* Holds the view item to the given topic
|
||||
*/
|
||||
public viewListOfSpeakers: ViewListOfSpeakers;
|
||||
|
||||
/**
|
||||
* Holds the speakers
|
||||
*/
|
||||
public speakers: ViewSpeaker[];
|
||||
|
||||
/**
|
||||
* Holds a list of projectors. Only in CurrentListOfSpeakers mode
|
||||
*/
|
||||
public projectors: ViewProjector[];
|
||||
|
||||
/**
|
||||
* Holds the subscription to the current projector (if any)
|
||||
*/
|
||||
private projectorSubscription: Subscription;
|
||||
|
||||
/**
|
||||
* Holds the active speaker
|
||||
*/
|
||||
public activeSpeaker: ViewSpeaker;
|
||||
|
||||
/**
|
||||
* Holds the speakers who were marked done
|
||||
*/
|
||||
public finishedSpeakers: ViewSpeaker[];
|
||||
|
||||
/**
|
||||
* Hold the users
|
||||
*/
|
||||
public users = new BehaviorSubject<ViewUser[]>([]);
|
||||
|
||||
/**
|
||||
* A filtered list of users, excluding those not available to be added to the list
|
||||
*/
|
||||
public filteredUsers = new BehaviorSubject<ViewUser[]>([]);
|
||||
|
||||
/**
|
||||
* Required for the user search selector
|
||||
*/
|
||||
public addSpeakerForm: FormGroup;
|
||||
|
||||
/**
|
||||
* Check, if list-view is seen on mobile-device.
|
||||
*/
|
||||
public isMobile = false;
|
||||
|
||||
/**
|
||||
* @returns true if the list of speakers list is currently closed
|
||||
*/
|
||||
@ -107,35 +50,19 @@ export class ListOfSpeakersComponent extends BaseViewComponent implements OnInit
|
||||
return this.viewListOfSpeakers && this.viewListOfSpeakers.closed;
|
||||
}
|
||||
|
||||
public get isListOfSpeakersEmpty(): boolean {
|
||||
if (this.speakers && this.speakers.length) {
|
||||
return false;
|
||||
} else if (this.finishedSpeakers && this.finishedSpeakers.length) {
|
||||
return false;
|
||||
}
|
||||
return !this.activeSpeaker;
|
||||
}
|
||||
public isMobile: boolean;
|
||||
|
||||
public manualSortMode = false;
|
||||
|
||||
/**
|
||||
* @returns true if the current user can be added to the list of speakers
|
||||
* filled by child component
|
||||
*/
|
||||
public get canAddSelf(): boolean {
|
||||
return !this.config.instant('agenda_present_speakers_only') || this.operator.user.is_present;
|
||||
}
|
||||
public isListOfSpeakersEmpty: boolean;
|
||||
|
||||
/**
|
||||
* Used to detect changes in the projector reference.
|
||||
* filled by child component
|
||||
*/
|
||||
private closReferenceProjectorId: number | null;
|
||||
|
||||
private closSubscription: Subscription | null;
|
||||
|
||||
public showFistContributionHint: boolean;
|
||||
|
||||
/**
|
||||
* List of speakers to save temporarily changes made by sorting-list.
|
||||
*/
|
||||
private speakerListAsSelectable: Selectable[] = [];
|
||||
public hasFinishedSpeakers: boolean;
|
||||
|
||||
/**
|
||||
* Constructor for speaker list component. Generates the forms.
|
||||
@ -143,7 +70,6 @@ export class ListOfSpeakersComponent extends BaseViewComponent implements OnInit
|
||||
* @param title
|
||||
* @param translate
|
||||
* @param snackBar
|
||||
* @param projectorRepo
|
||||
* @param route Angulars ActivatedRoute
|
||||
* @param DS the DataStore
|
||||
* @param listOfSpeakersRepo Repository for list of speakers
|
||||
@ -156,30 +82,17 @@ export class ListOfSpeakersComponent extends BaseViewComponent implements OnInit
|
||||
title: Title,
|
||||
protected translate: TranslateService, // protected required for ng-translate-extract
|
||||
snackBar: MatSnackBar,
|
||||
private projectorRepo: ProjectorRepositoryService,
|
||||
private route: ActivatedRoute,
|
||||
private listOfSpeakersRepo: ListOfSpeakersRepositoryService,
|
||||
private operator: OperatorService,
|
||||
private promptService: PromptService,
|
||||
private currentListOfSpeakersService: CurrentListOfSpeakersService,
|
||||
private durationService: DurationService,
|
||||
private userRepository: UserRepositoryService,
|
||||
private collectionStringMapper: CollectionStringMapperService,
|
||||
private currentListOfSpeakersSlideService: CurrentListOfSpeakersSlideService,
|
||||
private config: ConfigService,
|
||||
private viewport: ViewportService,
|
||||
private cd: ChangeDetectorRef
|
||||
private viewport: ViewportService
|
||||
) {
|
||||
super(title, translate, snackBar);
|
||||
this.addSpeakerForm = new FormGroup({ user_id: new FormControl() });
|
||||
}
|
||||
|
||||
/**
|
||||
* Init.
|
||||
*
|
||||
* Observe users,
|
||||
* React to form changes
|
||||
*/
|
||||
public ngOnInit(): void {
|
||||
// Check, if we are on the current list of speakers.
|
||||
this.isCurrentListOfSpeakers =
|
||||
@ -188,76 +101,17 @@ export class ListOfSpeakersComponent extends BaseViewComponent implements OnInit
|
||||
: true;
|
||||
|
||||
if (this.isCurrentListOfSpeakers) {
|
||||
this.projectors = this.projectorRepo.getViewModelList();
|
||||
this.updateClosProjector();
|
||||
this.subscriptions.push(
|
||||
this.projectorRepo.getViewModelListObservable().subscribe(newProjectors => {
|
||||
this.projectors = newProjectors;
|
||||
this.updateClosProjector();
|
||||
this.currentListOfSpeakersService.currentListOfSpeakersObservable.subscribe(clos => {
|
||||
this.setListOfSpeakers(clos);
|
||||
})
|
||||
);
|
||||
} else {
|
||||
const id = +this.route.snapshot.url[this.route.snapshot.url.length - 1].path;
|
||||
this.setListOfSpeakersId(id);
|
||||
this.setListOfSpeakersById(id);
|
||||
}
|
||||
|
||||
this.subscriptions.push(
|
||||
// Observe the user list
|
||||
this.userRepository.getViewModelListObservable().subscribe(users => {
|
||||
this.users.next(users);
|
||||
this.filterUsers();
|
||||
this.cd.markForCheck();
|
||||
}),
|
||||
// ovserve changes to the add-speaker 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);
|
||||
}
|
||||
}),
|
||||
// observe changes to the viewport
|
||||
this.viewport.isMobileSubject.subscribe(isMobile => this.checkSortMode(isMobile)),
|
||||
// observe changes the agenda_present_speakers_only config
|
||||
this.config.get('agenda_present_speakers_only').subscribe(() => {
|
||||
this.filterUsers();
|
||||
}),
|
||||
// observe changes to the agenda_show_first_contribution config
|
||||
this.config.get<boolean>('agenda_show_first_contribution').subscribe(show => {
|
||||
this.showFistContributionHint = show;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
public opCanManage(): boolean {
|
||||
return this.operator.hasPerms(Permission.agendaCanManageListOfSpeakers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the current list of speakers (CLOS) of a given projector.
|
||||
*/
|
||||
private updateClosProjector(): void {
|
||||
if (!this.projectors.length) {
|
||||
return;
|
||||
}
|
||||
const referenceProjector = this.projectors[0].referenceProjector;
|
||||
if (!referenceProjector || referenceProjector.id === this.closReferenceProjectorId) {
|
||||
return;
|
||||
}
|
||||
this.closReferenceProjectorId = referenceProjector.id;
|
||||
|
||||
if (this.projectorSubscription) {
|
||||
this.projectorSubscription.unsubscribe();
|
||||
this.viewListOfSpeakers = null;
|
||||
}
|
||||
|
||||
this.projectorSubscription = this.currentListOfSpeakersService
|
||||
.getListOfSpeakersObservable(referenceProjector)
|
||||
.subscribe(listOfSpeakers => {
|
||||
if (listOfSpeakers) {
|
||||
this.setListOfSpeakers(listOfSpeakers);
|
||||
}
|
||||
});
|
||||
this.subscriptions.push(this.projectorSubscription);
|
||||
this.subscriptions.push(this.viewport.isMobileSubject.subscribe(isMobile => (this.isMobile = isMobile)));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -272,16 +126,14 @@ export class ListOfSpeakersComponent extends BaseViewComponent implements OnInit
|
||||
*
|
||||
* @param id the list of speakers id
|
||||
*/
|
||||
private setListOfSpeakersId(id: number): void {
|
||||
if (this.closSubscription) {
|
||||
this.closSubscription.unsubscribe();
|
||||
}
|
||||
|
||||
this.closSubscription = this.listOfSpeakersRepo.getViewModelObservable(id).subscribe(listOfSpeakers => {
|
||||
if (listOfSpeakers) {
|
||||
this.setListOfSpeakers(listOfSpeakers);
|
||||
}
|
||||
});
|
||||
private setListOfSpeakersById(id: number): void {
|
||||
this.subscriptions.push(
|
||||
this.listOfSpeakersRepo.getViewModelObservable(id).subscribe(listOfSpeakers => {
|
||||
if (listOfSpeakers) {
|
||||
this.setListOfSpeakers(listOfSpeakers);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private setListOfSpeakers(listOfSpeakers: ViewListOfSpeakers): void {
|
||||
@ -290,21 +142,6 @@ export class ListOfSpeakersComponent extends BaseViewComponent implements OnInit
|
||||
: listOfSpeakers.getTitle() + ` - ${this.translate.instant('List of speakers')}`;
|
||||
super.setTitle(title);
|
||||
this.viewListOfSpeakers = listOfSpeakers;
|
||||
const allSpeakers = this.viewListOfSpeakers.speakers.sort((a, b) => a.weight - b.weight);
|
||||
this.speakers = allSpeakers.filter(speaker => speaker.state === SpeakerState.WAITING);
|
||||
// Since the speaker repository is not a normal repository, sorting cannot be handled there
|
||||
this.speakers.sort((a: ViewSpeaker, b: ViewSpeaker) => a.weight - b.weight);
|
||||
this.filterUsers();
|
||||
this.finishedSpeakers = allSpeakers.filter(speaker => speaker.state === SpeakerState.FINISHED);
|
||||
|
||||
// convert begin time to date and sort
|
||||
this.finishedSpeakers.sort((a: ViewSpeaker, b: ViewSpeaker) => {
|
||||
const aTime = new Date(a.begin_time).getTime();
|
||||
const bTime = new Date(b.begin_time).getTime();
|
||||
return aTime - bTime;
|
||||
});
|
||||
|
||||
this.activeSpeaker = allSpeakers.find(speaker => speaker.state === SpeakerState.CURRENT);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -318,80 +155,16 @@ export class ListOfSpeakersComponent extends BaseViewComponent implements OnInit
|
||||
return verboseName;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 addNewSpeaker(userId?: number): void {
|
||||
this.listOfSpeakersRepo
|
||||
.createSpeaker(this.viewListOfSpeakers, userId)
|
||||
.then(() => this.addSpeakerForm.reset(), this.raiseError);
|
||||
public setManualSortMode(active: boolean): void {
|
||||
this.manualSortMode = active;
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves sorting on mobile devices.
|
||||
*/
|
||||
public onMobileSaveSorting(): void {
|
||||
this.onSaveSorting(this.speakerListAsSelectable);
|
||||
this.isSortMode = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Receives an updated list from sorting-event.
|
||||
*
|
||||
* @param sortedSpeakerList The updated list.
|
||||
*/
|
||||
public onSortingChanged(sortedSpeakerList: Selectable[]): void {
|
||||
this.speakerListAsSelectable = sortedSpeakerList;
|
||||
if (!this.isMobile) {
|
||||
this.onSaveSorting(sortedSpeakerList);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore old order on cancel
|
||||
*/
|
||||
public onCancelSorting(): void {
|
||||
if (this.isSortMode) {
|
||||
this.isSortMode = false;
|
||||
this.listElement.restore();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Click on the mic button to mark a speaker as speaking
|
||||
*
|
||||
* @param speaker the speaker marked in the list
|
||||
*/
|
||||
public async onStartButton(speaker: ViewSpeaker): Promise<void> {
|
||||
try {
|
||||
await this.listOfSpeakersRepo.startSpeaker(this.viewListOfSpeakers, speaker);
|
||||
this.filterUsers();
|
||||
} catch (e) {
|
||||
this.raiseError(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Click on the mic-cross button
|
||||
*/
|
||||
public async onStopButton(): Promise<void> {
|
||||
try {
|
||||
await this.listOfSpeakersRepo.stopCurrentSpeaker(this.viewListOfSpeakers);
|
||||
this.filterUsers();
|
||||
} catch (e) {
|
||||
this.raiseError(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Click on the star button. Toggles the marked attribute.
|
||||
*
|
||||
* @param speaker The speaker clicked on.
|
||||
*/
|
||||
public onMarkButton(speaker: ViewSpeaker): void {
|
||||
this.listOfSpeakersRepo.markSpeaker(this.viewListOfSpeakers, speaker, !speaker.marked).catch(this.raiseError);
|
||||
public async onMobileSaveSorting(): Promise<void> {
|
||||
await this.listOfSpeakersContentComponent.onSaveSorting();
|
||||
this.manualSortMode = false;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -401,58 +174,6 @@ export class ListOfSpeakersComponent extends BaseViewComponent implements OnInit
|
||||
this.listOfSpeakersRepo.readdLastSpeaker(this.viewListOfSpeakers).catch(this.raiseError);
|
||||
}
|
||||
|
||||
/**
|
||||
* Click on the X button - removes the speaker from the list of speakers
|
||||
*
|
||||
* @param speaker optional speaker to remove. If none is given,
|
||||
* the operator themself is removed
|
||||
*/
|
||||
public async onDeleteButton(speaker?: ViewSpeaker): Promise<void> {
|
||||
const title = this.translate.instant(
|
||||
'Are you sure you want to delete this speaker from this list of speakers?'
|
||||
);
|
||||
if (await this.promptService.open(title)) {
|
||||
try {
|
||||
await this.listOfSpeakersRepo.delete(this.viewListOfSpeakers, speaker ? speaker.id : null);
|
||||
this.filterUsers();
|
||||
} catch (e) {
|
||||
this.raiseError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.operator.user.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks how often a speaker has already finished speaking
|
||||
*
|
||||
* @param speaker
|
||||
* @returns 0 or the number of times a speaker occurs in finishedSpeakers
|
||||
*/
|
||||
public hasSpokenCount(speaker: ViewSpeaker): number {
|
||||
return this.finishedSpeakers.filter(finishedSpeaker => {
|
||||
if (finishedSpeaker && finishedSpeaker.user) {
|
||||
return finishedSpeaker.user.id === speaker.user.id;
|
||||
}
|
||||
}).length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the speaker did never appear on any list of speakers
|
||||
*
|
||||
* @param speaker
|
||||
*/
|
||||
public isFirstContribution(speaker: ViewSpeaker): boolean {
|
||||
return this.listOfSpeakersRepo.isFirstContribution(speaker);
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the current list of speakers
|
||||
*/
|
||||
@ -483,77 +204,4 @@ export class ListOfSpeakersComponent extends BaseViewComponent implements OnInit
|
||||
this.listOfSpeakersRepo.deleteAllSpeakers(this.viewListOfSpeakers);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* returns a locale-specific version of the starting time for the given speaker item
|
||||
*
|
||||
* @param speaker
|
||||
* @returns a time string using the current language setting of the client
|
||||
*/
|
||||
public startTimeToString(speaker: ViewSpeaker): string {
|
||||
return new Date(speaker.begin_time).toLocaleString(this.translate.currentLang);
|
||||
}
|
||||
|
||||
/**
|
||||
* get the duration of a speech
|
||||
*
|
||||
* @param speaker
|
||||
* @returns string representation of the duration in `[MM]M:SS minutes` format
|
||||
*/
|
||||
public durationString(speaker: ViewSpeaker): string {
|
||||
const duration = Math.floor(
|
||||
(new Date(speaker.end_time).valueOf() - new Date(speaker.begin_time).valueOf()) / 1000
|
||||
);
|
||||
return `${this.durationService.durationToString(duration, 'm')}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Imports a new user by the given username.
|
||||
*
|
||||
* @param username The name of the new user.
|
||||
*/
|
||||
public async onCreateUser(username: string): Promise<void> {
|
||||
const newUser = await this.userRepository.createFromString(username);
|
||||
this.addNewSpeaker(newUser.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers an update of the filter for the list of available potential speakers
|
||||
* (triggered on an update of users or config)
|
||||
*/
|
||||
private filterUsers(): void {
|
||||
const presentUsersOnly = this.config.instant('agenda_present_speakers_only');
|
||||
const users = presentUsersOnly ? this.users.getValue().filter(u => u.is_present) : this.users.getValue();
|
||||
if (!this.speakers || !this.speakers.length) {
|
||||
this.filteredUsers.next(users);
|
||||
} else {
|
||||
this.filteredUsers.next(users.filter(u => !this.speakers.some(speaker => speaker.user_id === u.id)));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* send the current order of the sorting list to the server
|
||||
*
|
||||
* @param sortedSpeakerList The list to save.
|
||||
*/
|
||||
private onSaveSorting(sortedSpeakerList: Selectable[]): void {
|
||||
if (this.isSortMode) {
|
||||
this.listOfSpeakersRepo
|
||||
.sortSpeakers(
|
||||
this.viewListOfSpeakers,
|
||||
sortedSpeakerList.map(el => el.id)
|
||||
)
|
||||
.catch(this.raiseError);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check, that the sorting mode is immediately active, if not in mobile-view.
|
||||
*
|
||||
* @param isMobile If currently a mobile device is used.
|
||||
*/
|
||||
private checkSortMode(isMobile: boolean): void {
|
||||
this.isMobile = isMobile;
|
||||
this.isSortMode = !isMobile;
|
||||
}
|
||||
}
|
||||
|
@ -4,13 +4,15 @@ import { RouterModule, Routes } from '@angular/router';
|
||||
import { Permission } from 'app/core/core-services/operator.service';
|
||||
import { AssignmentDetailComponent } from './components/assignment-detail/assignment-detail.component';
|
||||
import { AssignmentListComponent } from './components/assignment-list/assignment-list.component';
|
||||
import { AssignmentPollDetailComponent } from './components/assignment-poll-detail/assignment-poll-detail.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{ path: '', component: AssignmentListComponent, pathMatch: 'full' },
|
||||
{ path: 'new', component: AssignmentDetailComponent, data: { basePerm: Permission.assignmentsCanManage } },
|
||||
{ path: ':id', component: AssignmentDetailComponent, data: { basePerm: Permission.assignmentsCanSee } },
|
||||
{ path: 'polls', children: [{ path: ':id', component: AssignmentPollDetailComponent }] }
|
||||
{
|
||||
path: 'polls',
|
||||
loadChildren: () => import('./modules/assignment-poll/assignment-poll.module').then(m => m.AssignmentPollModule)
|
||||
}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
@ -3,23 +3,12 @@ import { NgModule } from '@angular/core';
|
||||
|
||||
import { AssignmentDetailComponent } from './components/assignment-detail/assignment-detail.component';
|
||||
import { AssignmentListComponent } from './components/assignment-list/assignment-list.component';
|
||||
import { AssignmentPollDetailComponent } from './components/assignment-poll-detail/assignment-poll-detail.component';
|
||||
import { AssignmentPollDialogComponent } from './components/assignment-poll-dialog/assignment-poll-dialog.component';
|
||||
import { AssignmentPollVoteComponent } from './components/assignment-poll-vote/assignment-poll-vote.component';
|
||||
import { AssignmentPollComponent } from './components/assignment-poll/assignment-poll.component';
|
||||
import { AssignmentPollModule } from './modules/assignment-poll/assignment-poll.module';
|
||||
import { AssignmentsRoutingModule } from './assignments-routing.module';
|
||||
import { PollsModule } from '../polls/polls.module';
|
||||
import { SharedModule } from '../../shared/shared.module';
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule, AssignmentsRoutingModule, SharedModule, PollsModule],
|
||||
declarations: [
|
||||
AssignmentDetailComponent,
|
||||
AssignmentListComponent,
|
||||
AssignmentPollComponent,
|
||||
AssignmentPollDetailComponent,
|
||||
AssignmentPollVoteComponent,
|
||||
AssignmentPollDialogComponent
|
||||
]
|
||||
imports: [CommonModule, AssignmentsRoutingModule, AssignmentPollModule, SharedModule],
|
||||
declarations: [AssignmentDetailComponent, AssignmentListComponent]
|
||||
})
|
||||
export class AssignmentsModule {}
|
||||
|
@ -4,8 +4,8 @@ import { E2EImportsModule } from 'e2e-imports.module';
|
||||
|
||||
import { PollProgressComponent } from 'app/site/polls/components/poll-progress/poll-progress.component';
|
||||
import { AssignmentDetailComponent } from './assignment-detail.component';
|
||||
import { AssignmentPollVoteComponent } from '../assignment-poll-vote/assignment-poll-vote.component';
|
||||
import { AssignmentPollComponent } from '../assignment-poll/assignment-poll.component';
|
||||
import { AssignmentPollVoteComponent } from '../../modules/assignment-poll/components/assignment-poll-vote/assignment-poll-vote.component';
|
||||
import { AssignmentPollComponent } from '../../modules/assignment-poll/components/assignment-poll/assignment-poll.component';
|
||||
|
||||
describe('AssignmentDetailComponent', () => {
|
||||
let component: AssignmentDetailComponent;
|
||||
|
@ -22,8 +22,8 @@ import { LocalPermissionsService } from 'app/site/motions/services/local-permiss
|
||||
import { ViewTag } from 'app/site/tags/models/view-tag';
|
||||
import { ViewUser } from 'app/site/users/models/view-user';
|
||||
import { AssignmentPdfExportService } from '../../services/assignment-pdf-export.service';
|
||||
import { AssignmentPollDialogService } from '../../services/assignment-poll-dialog.service';
|
||||
import { AssignmentPollService } from '../../services/assignment-poll.service';
|
||||
import { AssignmentPollDialogService } from '../../modules/assignment-poll/services/assignment-poll-dialog.service';
|
||||
import { AssignmentPollService } from '../../modules/assignment-poll/services/assignment-poll.service';
|
||||
import { AssignmentPhases, ViewAssignment } from '../../models/view-assignment';
|
||||
import { ViewAssignmentPoll } from '../../models/view-assignment-poll';
|
||||
import { ViewAssignmentRelatedUser } from '../../models/view-assignment-related-user';
|
||||
|
@ -0,0 +1,12 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
|
||||
import { AssignmentPollDetailComponent } from './components/assignment-poll-detail/assignment-poll-detail.component';
|
||||
|
||||
const routes: Routes = [{ path: ':id', component: AssignmentPollDetailComponent }];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule]
|
||||
})
|
||||
export class AssignmentPollRoutingModule {}
|
@ -0,0 +1,22 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
|
||||
import { SharedModule } from 'app/shared/shared.module';
|
||||
import { PollsModule } from 'app/site/polls/polls.module';
|
||||
import { AssignmentPollDetailComponent } from './components/assignment-poll-detail/assignment-poll-detail.component';
|
||||
import { AssignmentPollDialogComponent } from './components/assignment-poll-dialog/assignment-poll-dialog.component';
|
||||
import { AssignmentPollRoutingModule } from './assignment-poll-routing.module';
|
||||
import { AssignmentPollVoteComponent } from './components/assignment-poll-vote/assignment-poll-vote.component';
|
||||
import { AssignmentPollComponent } from './components/assignment-poll/assignment-poll.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AssignmentPollComponent,
|
||||
AssignmentPollDetailComponent,
|
||||
AssignmentPollVoteComponent,
|
||||
AssignmentPollDialogComponent
|
||||
],
|
||||
exports: [AssignmentPollComponent, AssignmentPollVoteComponent],
|
||||
imports: [CommonModule, AssignmentPollRoutingModule, SharedModule, PollsModule]
|
||||
})
|
||||
export class AssignmentPollModule {}
|
@ -13,10 +13,10 @@ import { GroupRepositoryService } from 'app/core/repositories/users/group-reposi
|
||||
import { ConfigService } from 'app/core/ui-services/config.service';
|
||||
import { PromptService } from 'app/core/ui-services/prompt.service';
|
||||
import { VoteValue } from 'app/shared/models/poll/base-vote';
|
||||
import { ViewAssignmentPoll } from 'app/site/assignments/models/view-assignment-poll';
|
||||
import { BasePollDetailComponent } from 'app/site/polls/components/base-poll-detail.component';
|
||||
import { AssignmentPollDialogService } from '../../services/assignment-poll-dialog.service';
|
||||
import { AssignmentPollService } from '../../services/assignment-poll.service';
|
||||
import { ViewAssignmentPoll } from '../../models/view-assignment-poll';
|
||||
|
||||
@Component({
|
||||
selector: 'os-assignment-poll-detail',
|
@ -12,13 +12,13 @@ import { LOWEST_VOTE_VALUE, PollType } from 'app/shared/models/poll/base-poll';
|
||||
import { GeneralValueVerbose, VoteValue, VoteValueVerbose } from 'app/shared/models/poll/base-vote';
|
||||
import {
|
||||
AssignmentPollMethodVerbose,
|
||||
AssignmentPollPercentBaseVerbose
|
||||
AssignmentPollPercentBaseVerbose,
|
||||
ViewAssignmentPoll
|
||||
} from 'app/site/assignments/models/view-assignment-poll';
|
||||
import { BasePollDialogComponent } from 'app/site/polls/components/base-poll-dialog.component';
|
||||
import { PollFormComponent } from 'app/site/polls/components/poll-form/poll-form.component';
|
||||
import { ViewUser } from 'app/site/users/models/view-user';
|
||||
import { AssignmentPollService } from '../../services/assignment-poll.service';
|
||||
import { ViewAssignmentPoll } from '../../models/view-assignment-poll';
|
||||
|
||||
type OptionsObject = { user_id: number; user: ViewUser }[];
|
||||
|
@ -15,8 +15,8 @@ import { VotingService } from 'app/core/ui-services/voting.service';
|
||||
import { AssignmentPollMethod } from 'app/shared/models/assignments/assignment-poll';
|
||||
import { PollType } from 'app/shared/models/poll/base-poll';
|
||||
import { VoteValue } from 'app/shared/models/poll/base-vote';
|
||||
import { ViewAssignmentPoll } from 'app/site/assignments/models/view-assignment-poll';
|
||||
import { BasePollVoteComponent } from 'app/site/polls/components/base-poll-vote.component';
|
||||
import { ViewAssignmentPoll } from '../../models/view-assignment-poll';
|
||||
|
||||
// TODO: Duplicate
|
||||
interface VoteActions {
|
@ -10,11 +10,11 @@ import { AssignmentPollRepositoryService } from 'app/core/repositories/assignmen
|
||||
import { PromptService } from 'app/core/ui-services/prompt.service';
|
||||
import { VotingPrivacyWarningComponent } from 'app/shared/components/voting-privacy-warning/voting-privacy-warning.component';
|
||||
import { infoDialogSettings } from 'app/shared/utils/dialog-settings';
|
||||
import { ViewAssignmentPoll } from 'app/site/assignments/models/view-assignment-poll';
|
||||
import { BasePollComponent } from 'app/site/polls/components/base-poll.component';
|
||||
import { AssignmentPollDialogService } from '../../services/assignment-poll-dialog.service';
|
||||
import { AssignmentPollPdfService } from '../../services/assignment-poll-pdf.service';
|
||||
import { AssignmentPollService } from '../../services/assignment-poll.service';
|
||||
import { ViewAssignmentPoll } from '../../models/view-assignment-poll';
|
||||
|
||||
/**
|
||||
* Component for a single assignment poll. Used in assignment detail view
|
@ -3,9 +3,9 @@ import { MatDialog } from '@angular/material/dialog';
|
||||
|
||||
import { CollectionStringMapperService } from 'app/core/core-services/collection-string-mapper.service';
|
||||
import { BasePollDialogService } from 'app/core/ui-services/base-poll-dialog.service';
|
||||
import { AssignmentPollDialogComponent } from 'app/site/assignments/components/assignment-poll-dialog/assignment-poll-dialog.component';
|
||||
import { AssignmentPollDialogComponent } from '../components/assignment-poll-dialog/assignment-poll-dialog.component';
|
||||
import { AssignmentPollService } from './assignment-poll.service';
|
||||
import { ViewAssignmentPoll } from '../models/view-assignment-poll';
|
||||
import { ViewAssignmentPoll } from '../../../models/view-assignment-poll';
|
||||
|
||||
/**
|
||||
* Subclassed to provide the right `PollService` and `DialogComponent`
|
@ -8,7 +8,7 @@ import { AssignmentRepositoryService } from 'app/core/repositories/assignments/a
|
||||
import { UserRepositoryService } from 'app/core/repositories/users/user-repository.service';
|
||||
import { ConfigService } from 'app/core/ui-services/config.service';
|
||||
import { AssignmentPollMethod } from 'app/shared/models/assignments/assignment-poll';
|
||||
import { ViewAssignmentPoll } from '../models/view-assignment-poll';
|
||||
import { ViewAssignmentPoll } from 'app/site/assignments/models/view-assignment-poll';
|
||||
|
||||
/**
|
||||
* Creates a pdf for a motion poll. Takes as input any motionPoll
|
@ -13,6 +13,8 @@ import {
|
||||
import { MajorityMethod, PollType, VOTE_UNDOCUMENTED } from 'app/shared/models/poll/base-poll';
|
||||
import { ParsePollNumberPipe } from 'app/shared/pipes/parse-poll-number.pipe';
|
||||
import { PollKeyVerbosePipe } from 'app/shared/pipes/poll-key-verbose.pipe';
|
||||
import { ViewAssignmentOption } from 'app/site/assignments/models/view-assignment-option';
|
||||
import { ViewAssignmentPoll } from 'app/site/assignments/models/view-assignment-poll';
|
||||
import {
|
||||
PollData,
|
||||
PollDataOption,
|
||||
@ -20,8 +22,6 @@ import {
|
||||
PollTableData,
|
||||
VotingResult
|
||||
} from 'app/site/polls/services/poll.service';
|
||||
import { ViewAssignmentOption } from '../models/view-assignment-option';
|
||||
import { ViewAssignmentPoll } from '../models/view-assignment-poll';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
@ -8,7 +8,7 @@ import { ParsePollNumberPipe } from 'app/shared/pipes/parse-poll-number.pipe';
|
||||
import { PollKeyVerbosePipe } from 'app/shared/pipes/poll-key-verbose.pipe';
|
||||
import { PollPercentBasePipe } from 'app/shared/pipes/poll-percent-base.pipe';
|
||||
import { PollTableData, VotingResult } from 'app/site/polls/services/poll.service';
|
||||
import { AssignmentPollService } from './assignment-poll.service';
|
||||
import { AssignmentPollService } from '../modules/assignment-poll/services/assignment-poll.service';
|
||||
import { ViewAssignment } from '../models/view-assignment';
|
||||
import { ViewAssignmentPoll } from '../models/view-assignment-poll';
|
||||
|
||||
|
12
client/src/app/site/cinema/cinema-routing.module.ts
Normal file
12
client/src/app/site/cinema/cinema-routing.module.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
|
||||
import { CinemaComponent } from './components/cinema/cinema.component';
|
||||
|
||||
const routes: Routes = [{ path: '', component: CinemaComponent, pathMatch: 'full' }];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule]
|
||||
})
|
||||
export class CinemaRoutingModule {}
|
16
client/src/app/site/cinema/cinema.config.ts
Normal file
16
client/src/app/site/cinema/cinema.config.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { Permission } from 'app/core/core-services/operator.service';
|
||||
import { AppConfig } from 'app/core/definitions/app-config';
|
||||
|
||||
export const CinemaAppConfig: AppConfig = {
|
||||
name: 'cinema',
|
||||
models: [],
|
||||
mainMenuEntries: [
|
||||
{
|
||||
route: '/autopilot',
|
||||
displayName: 'Autopilot',
|
||||
icon: 'sync',
|
||||
weight: 150,
|
||||
permission: Permission.coreCanSeeAutopilot
|
||||
}
|
||||
]
|
||||
};
|
15
client/src/app/site/cinema/cinema.module.ts
Normal file
15
client/src/app/site/cinema/cinema.module.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
|
||||
import { SharedModule } from 'app/shared/shared.module';
|
||||
import { AssignmentPollModule } from '../assignments/modules/assignment-poll/assignment-poll.module';
|
||||
import { CinemaRoutingModule } from './cinema-routing.module';
|
||||
import { CinemaComponent } from './components/cinema/cinema.component';
|
||||
import { MotionPollModule } from '../motions/modules/motion-poll/motion-poll.module';
|
||||
import { PollCollectionComponent } from './components/poll-collection/poll-collection.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule, CinemaRoutingModule, MotionPollModule, AssignmentPollModule, SharedModule],
|
||||
declarations: [CinemaComponent, PollCollectionComponent]
|
||||
})
|
||||
export class CinemaModule {}
|
@ -0,0 +1,45 @@
|
||||
<os-head-bar>
|
||||
<div class="title-slot">
|
||||
<h2>{{ 'Autopilot' | translate }}</h2>
|
||||
</div>
|
||||
</os-head-bar>
|
||||
|
||||
<!-- Title Card -->
|
||||
<mat-card class="os-card">
|
||||
<h1 class="line-and-icon">
|
||||
{{ title }}
|
||||
<a
|
||||
mat-icon-button
|
||||
[disabled]="!viewModelUrl"
|
||||
[class.disabled]="!viewModelUrl"
|
||||
[routerLink]="viewModelUrl"
|
||||
[state]="{ back: 'true' }"
|
||||
>
|
||||
<mat-icon>open_in_new</mat-icon>
|
||||
</a>
|
||||
</h1>
|
||||
</mat-card>
|
||||
|
||||
<!-- List of speakers -->
|
||||
<os-list-of-speakers-content [customTitle]="true" [speakers]="listOfSpeakers">
|
||||
<p class="subtitle-text line-and-icon">
|
||||
<a [routerLink]="closUrl" [class.disabled]="!closUrl">
|
||||
{{ 'List of speakers' | translate }}
|
||||
</a>
|
||||
<mat-icon *ngIf="isLosClosed" matTooltip="{{ 'The list of speakers is closed.' | translate }}">
|
||||
lock
|
||||
</mat-icon>
|
||||
</p>
|
||||
</os-list-of-speakers-content>
|
||||
|
||||
<os-poll-collection [currentProjection]="projectedViewModel"></os-poll-collection>
|
||||
|
||||
<!-- Projector -->
|
||||
<mat-card class="os-card spacer-bottom-60">
|
||||
<p class="subtitle-text">{{ projectorTitle | translate }}</p>
|
||||
<a [routerLink]="projectorUrl" [target]="projectionTarget">
|
||||
<div class="projector">
|
||||
<os-projector *ngIf="projector" [projector]="projector"></os-projector>
|
||||
</div>
|
||||
</a>
|
||||
</mat-card>
|
@ -0,0 +1,3 @@
|
||||
.projector {
|
||||
border: 1px solid lightgray;
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { E2EImportsModule } from 'e2e-imports.module';
|
||||
|
||||
import { CinemaComponent } from './cinema.component';
|
||||
|
||||
describe('CinemaComponent', () => {
|
||||
let component: CinemaComponent;
|
||||
let fixture: ComponentFixture<CinemaComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [E2EImportsModule],
|
||||
declarations: [CinemaComponent]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(CinemaComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
113
client/src/app/site/cinema/components/cinema/cinema.component.ts
Normal file
113
client/src/app/site/cinema/components/cinema/cinema.component.ts
Normal file
@ -0,0 +1,113 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { Title } from '@angular/platform-browser';
|
||||
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
import { OperatorService } from 'app/core/core-services/operator.service';
|
||||
import { ProjectorService } from 'app/core/core-services/projector.service';
|
||||
import { ProjectorRepositoryService } from 'app/core/repositories/projector/projector-repository.service';
|
||||
import { DetailNavigable, isDetailNavigable } from 'app/shared/models/base/detail-navigable';
|
||||
import { ProjectorElement } from 'app/shared/models/core/projector';
|
||||
import { ViewListOfSpeakers } from 'app/site/agenda/models/view-list-of-speakers';
|
||||
import { BaseProjectableViewModel } from 'app/site/base/base-projectable-view-model';
|
||||
import { BaseViewComponent } from 'app/site/base/base-view';
|
||||
import { ViewProjector } from 'app/site/projector/models/view-projector';
|
||||
import { CurrentListOfSpeakersService } from 'app/site/projector/services/current-list-of-speakers.service';
|
||||
|
||||
@Component({
|
||||
selector: 'os-cinema',
|
||||
templateUrl: './cinema.component.html',
|
||||
styleUrls: ['./cinema.component.scss']
|
||||
})
|
||||
export class CinemaComponent extends BaseViewComponent implements OnInit {
|
||||
public listOfSpeakers: ViewListOfSpeakers;
|
||||
public projector: ViewProjector;
|
||||
private currentProjectorElement: ProjectorElement;
|
||||
public projectedViewModel: BaseProjectableViewModel;
|
||||
|
||||
public get title(): string {
|
||||
if (this.projectedViewModel) {
|
||||
return this.projectedViewModel.getListTitle();
|
||||
} else if (this.currentProjectorElement) {
|
||||
return this.projectorService.getSlideTitle(this.currentProjectorElement)?.title;
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
public get projectorTitle(): string {
|
||||
return this.projector?.getTitle() || '';
|
||||
}
|
||||
|
||||
public get closUrl(): string {
|
||||
if (this.listOfSpeakers && this.operator.hasPerms(this.permission.agendaCanManageListOfSpeakers)) {
|
||||
return this.listOfSpeakers?.listOfSpeakersUrl;
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
public get isLosClosed(): boolean {
|
||||
return this.listOfSpeakers?.closed;
|
||||
}
|
||||
|
||||
public get viewModelUrl(): string {
|
||||
if (this.projectedViewModel && isDetailNavigable(this.projectedViewModel)) {
|
||||
return (this.projectedViewModel as DetailNavigable).getDetailStateURL();
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
public get projectorUrl(): string {
|
||||
if (this.projector) {
|
||||
if (this.operator.hasPerms(this.permission.coreCanManageProjector)) {
|
||||
return `/projectors/detail/${this.projector.id}`;
|
||||
} else {
|
||||
return `/projector/${this.projector.id}`;
|
||||
}
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
public get projectionTarget(): '_blank' | '_self' {
|
||||
if (this.operator.hasPerms(this.permission.coreCanManageProjector)) {
|
||||
return '_self';
|
||||
} else {
|
||||
return '_blank';
|
||||
}
|
||||
}
|
||||
|
||||
public constructor(
|
||||
title: Title,
|
||||
translate: TranslateService,
|
||||
snackBar: MatSnackBar,
|
||||
private operator: OperatorService,
|
||||
private projectorService: ProjectorService,
|
||||
private projectorRepo: ProjectorRepositoryService,
|
||||
private closService: CurrentListOfSpeakersService
|
||||
) {
|
||||
super(title, translate, snackBar);
|
||||
}
|
||||
|
||||
public ngOnInit(): void {
|
||||
this.subscriptions.push(
|
||||
this.projectorRepo.getReferenceProjectorObservable().subscribe(refProjector => {
|
||||
this.projector = refProjector;
|
||||
this.currentProjectorElement = refProjector?.elements[0] || null;
|
||||
if (this.currentProjectorElement) {
|
||||
this.projectedViewModel = this.projectorService.getViewModelFromProjectorElement(
|
||||
this.currentProjectorElement
|
||||
);
|
||||
} else {
|
||||
this.projectedViewModel = null;
|
||||
}
|
||||
}),
|
||||
this.closService.currentListOfSpeakersObservable.subscribe(clos => {
|
||||
this.listOfSpeakers = clos;
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
<mat-card class="os-card" *ngFor="let poll of polls">
|
||||
<p class="subtitle-text">
|
||||
<a [routerLink]="getPollDetailLink(poll)" [state]="{ back: 'true' }">{{ getPollVoteTitle(poll) }}</a>
|
||||
</p>
|
||||
|
||||
<div *ngIf="poll.pollClassType === 'motion'">
|
||||
<os-motion-poll-vote [poll]="poll"></os-motion-poll-vote>
|
||||
</div>
|
||||
|
||||
<div *ngIf="poll.pollClassType === 'assignment'">
|
||||
<os-assignment-poll-vote [poll]="poll"></os-assignment-poll-vote>
|
||||
</div>
|
||||
</mat-card>
|
@ -0,0 +1,27 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { E2EImportsModule } from 'e2e-imports.module';
|
||||
|
||||
import { PollCollectionComponent } from './poll-collection.component';
|
||||
|
||||
describe('PollCollectionComponent', () => {
|
||||
let component: PollCollectionComponent;
|
||||
let fixture: ComponentFixture<PollCollectionComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [E2EImportsModule],
|
||||
declarations: [PollCollectionComponent]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(PollCollectionComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,72 @@
|
||||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { Title } from '@angular/platform-browser';
|
||||
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { map } from 'rxjs/operators';
|
||||
|
||||
import { BaseViewComponent } from 'app/site/base/base-view';
|
||||
import { BaseViewModel } from 'app/site/base/base-view-model';
|
||||
import { ViewBasePoll } from 'app/site/polls/models/view-base-poll';
|
||||
import { PollListObservableService } from 'app/site/polls/services/poll-list-observable.service';
|
||||
|
||||
@Component({
|
||||
selector: 'os-poll-collection',
|
||||
templateUrl: './poll-collection.component.html',
|
||||
styleUrls: ['./poll-collection.component.scss']
|
||||
})
|
||||
export class PollCollectionComponent extends BaseViewComponent implements OnInit {
|
||||
public polls: ViewBasePoll[];
|
||||
|
||||
@Input()
|
||||
private currentProjection: BaseViewModel<any>;
|
||||
|
||||
private get showExtendedTitle(): boolean {
|
||||
const areAllPollsSameModel = this.polls.every(
|
||||
poll => this.polls[0].getContentObject() === poll.getContentObject()
|
||||
);
|
||||
|
||||
if (this.currentProjection && areAllPollsSameModel) {
|
||||
return this.polls[0].getContentObject() !== this.currentProjection;
|
||||
} else {
|
||||
return !areAllPollsSameModel;
|
||||
}
|
||||
}
|
||||
|
||||
public constructor(
|
||||
title: Title,
|
||||
translate: TranslateService,
|
||||
snackBar: MatSnackBar,
|
||||
private pollService: PollListObservableService
|
||||
) {
|
||||
super(title, translate, snackBar);
|
||||
}
|
||||
|
||||
public ngOnInit(): void {
|
||||
this.subscriptions.push(
|
||||
this.pollService
|
||||
.getViewModelListObservable()
|
||||
.pipe(map(polls => polls.filter(poll => poll.canBeVotedFor())))
|
||||
.subscribe(polls => {
|
||||
this.polls = polls;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
public getPollVoteTitle(poll: ViewBasePoll): string {
|
||||
const contentObject = poll.getContentObject();
|
||||
const listTitle = contentObject.getListTitle();
|
||||
const model = contentObject.getVerboseName();
|
||||
const pollTitle = poll.getTitle();
|
||||
|
||||
if (this.showExtendedTitle) {
|
||||
return `(${model}) ${listTitle} - ${pollTitle}`;
|
||||
} else {
|
||||
return pollTitle;
|
||||
}
|
||||
}
|
||||
|
||||
public getPollDetailLink(poll: ViewBasePoll): string {
|
||||
return poll.parentLink;
|
||||
}
|
||||
}
|
@ -1,6 +1,3 @@
|
||||
<div *osPerms="'motions.can_manage_polls'; and: poll && poll.isStarted">
|
||||
<os-poll-progress [poll]="poll"></os-poll-progress>
|
||||
</div>
|
||||
<ng-container *ngIf="poll && !poll.user_has_voted; else userHasVotes">
|
||||
<div *ngIf="vmanager.canVote(poll) && !deliveringVote" class="vote-button-grid">
|
||||
<!-- Voting -->
|
||||
|
@ -10,7 +10,7 @@ import { MotionPollComponent } from './motion-poll/motion-poll.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule, SharedModule, MotionPollRoutingModule, PollsModule],
|
||||
exports: [MotionPollComponent],
|
||||
exports: [MotionPollComponent, MotionPollVoteComponent],
|
||||
declarations: [MotionPollComponent, MotionPollDetailComponent, MotionPollVoteComponent]
|
||||
})
|
||||
export class MotionPollModule {}
|
||||
|
@ -57,6 +57,9 @@
|
||||
|
||||
<!-- Results -->
|
||||
<ng-container *ngIf="poll && !poll.stateHasVotes && poll.type !== 'analog'; else votingResult">
|
||||
<div *osPerms="'motions.can_manage_polls'; and: poll && poll.isStarted">
|
||||
<os-poll-progress [poll]="poll"></os-poll-progress>
|
||||
</div>
|
||||
<os-motion-poll-vote [poll]="poll"></os-motion-poll-vote>
|
||||
</ng-container>
|
||||
|
||||
|
@ -19,9 +19,9 @@ import { ViewProjector } from '../models/view-projector';
|
||||
})
|
||||
export class CurrentListOfSpeakersService {
|
||||
/**
|
||||
* Id of the current lost of speakers projector. Filled through observer
|
||||
* Current clos reference projector
|
||||
*/
|
||||
private closId: number;
|
||||
private closRefProjector: ViewProjector;
|
||||
|
||||
/**
|
||||
* This map holds the current (number or null) los-id for the the projector.
|
||||
@ -56,9 +56,9 @@ export class CurrentListOfSpeakersService {
|
||||
}
|
||||
});
|
||||
|
||||
this.projectorRepo.getReferenceProjectorIdObservable().subscribe(closId => {
|
||||
if (closId) {
|
||||
this.closId = closId;
|
||||
this.projectorRepo.getReferenceProjectorObservable().subscribe(clos => {
|
||||
if (clos) {
|
||||
this.closRefProjector = clos;
|
||||
this.currentListOfSpeakerSubject.next(this.getCurrentListOfSpeakers());
|
||||
}
|
||||
});
|
||||
@ -68,8 +68,7 @@ export class CurrentListOfSpeakersService {
|
||||
* Use the subject to get it
|
||||
*/
|
||||
private getCurrentListOfSpeakers(): ViewListOfSpeakers | null {
|
||||
const refProjector = this.projectorRepo.getViewModel(this.closId);
|
||||
return this.getCurrentListOfSpeakersForProjector(refProjector);
|
||||
return this.getCurrentListOfSpeakersForProjector(this.closRefProjector);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -135,7 +134,9 @@ export class CurrentListOfSpeakersService {
|
||||
for (const nonStableElement of nonStableElements) {
|
||||
const identifiableNonStableElement = this.slideManager.getIdentifiableProjectorElement(nonStableElement);
|
||||
try {
|
||||
const viewModel = this.projectorService.getViewModelFromProjectorElement(identifiableNonStableElement);
|
||||
const viewModel = this.projectorService.getViewModelFromIdentifiableProjectorElement(
|
||||
identifiableNonStableElement
|
||||
);
|
||||
if (isBaseViewModelWithListOfSpeakers(viewModel)) {
|
||||
return viewModel.listOfSpeakers;
|
||||
}
|
||||
|
@ -75,6 +75,11 @@ const routes: Routes = [
|
||||
loadChildren: () => import('./polls/polls.module').then(m => m.PollsModule),
|
||||
// one of them is sufficient
|
||||
data: { basePerm: [Permission.motionsCanSee, Permission.assignmentsCanSee] }
|
||||
},
|
||||
{
|
||||
path: 'autopilot',
|
||||
loadChildren: () => import('./cinema/cinema.module').then(m => m.CinemaModule),
|
||||
data: { basePerm: Permission.coreCanSeeAutopilot }
|
||||
}
|
||||
],
|
||||
canActivateChild: [AuthGuard]
|
||||
|
@ -25,7 +25,7 @@
|
||||
|
||||
<!-- navigation -->
|
||||
<mat-nav-list class="main-nav">
|
||||
<span *ngFor="let entry of mainMenuService.entries">
|
||||
<span *ngFor="let entry of mainMenuEntries">
|
||||
<a
|
||||
[@navItemAnim]
|
||||
*osPerms="entry.permission"
|
||||
|
@ -14,7 +14,7 @@ import { OfflineService } from 'app/core/core-services/offline.service';
|
||||
import { OverlayService } from 'app/core/ui-services/overlay.service';
|
||||
import { UpdateService } from 'app/core/ui-services/update.service';
|
||||
import { BaseComponent } from '../base.component';
|
||||
import { MainMenuService } from '../core/core-services/main-menu.service';
|
||||
import { MainMenuEntry, MainMenuService } from '../core/core-services/main-menu.service';
|
||||
import { OpenSlidesStatusService } from '../core/core-services/openslides-status.service';
|
||||
import { OperatorService } from '../core/core-services/operator.service';
|
||||
import { TimeTravelService } from '../core/core-services/time-travel.service';
|
||||
@ -66,6 +66,10 @@ export class SiteComponent extends BaseComponent implements OnInit {
|
||||
*/
|
||||
private delayedUpdateAvailable = false;
|
||||
|
||||
public get mainMenuEntries(): MainMenuEntry[] {
|
||||
return this.mainMenuService.entries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* @param route
|
||||
@ -86,7 +90,7 @@ export class SiteComponent extends BaseComponent implements OnInit {
|
||||
public operator: OperatorService,
|
||||
public vp: ViewportService,
|
||||
public dialog: MatDialog,
|
||||
public mainMenuService: MainMenuService,
|
||||
private mainMenuService: MainMenuService,
|
||||
public OSStatus: OpenSlidesStatusService,
|
||||
public timeTravel: TimeTravelService,
|
||||
private matSnackBar: MatSnackBar,
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
import { PollState } from 'app/shared/models/poll/base-poll';
|
||||
import { AssignmentPollService } from 'app/site/assignments/services/assignment-poll.service';
|
||||
import { AssignmentPollService } from 'app/site/assignments/modules/assignment-poll/services/assignment-poll.service';
|
||||
import { BasePollSlideComponent } from 'app/slides/polls/base-poll-slide.component';
|
||||
import { AssignmentPollSlideData } from './assignment-poll-slide-data';
|
||||
|
||||
|
@ -40,7 +40,7 @@ $narrow-spacing: (
|
||||
@import './app/shared/components/banner/banner.component.scss-theme.scss';
|
||||
@import './app/site/motions/modules/motion-poll/motion-poll/motion-poll.component.scss-theme.scss';
|
||||
@import './app/site/motions/modules/motion-poll/motion-poll-detail/motion-poll-detail.component.scss-theme.scss';
|
||||
@import './app/site/assignments/components/assignment-poll-detail/assignment-poll-detail-component.scss-theme.scss';
|
||||
@import './app/site/assignments/modules/assignment-poll/components/assignment-poll-detail/assignment-poll-detail-component.scss-theme.scss';
|
||||
@import './app/shared/components/progress-snack-bar/progress-snack-bar.component.scss-theme.scss';
|
||||
@import './app/shared/components/jitsi/jitsi.component.scss-theme.scss';
|
||||
@import './app/shared/components/list-view-table/list-view-table.component.scss-theme.scss';
|
||||
|
@ -81,6 +81,13 @@
|
||||
color: mat-color($foreground, text);
|
||||
}
|
||||
|
||||
.subtitle-text {
|
||||
font-size: 125%;
|
||||
margin-top: 0;
|
||||
margin-bottom: 15px;
|
||||
color: mat-color(if($is-dark-theme, $accent, $primary));
|
||||
}
|
||||
|
||||
.tile-color {
|
||||
background-color: mat-color($background, selected-button);
|
||||
}
|
||||
|
@ -155,6 +155,15 @@ b,
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
// for aligning text lines with an icon and or link
|
||||
.line-and-icon {
|
||||
display: flex;
|
||||
.mat-icon-button,
|
||||
.mat-icon {
|
||||
margin: auto 0;
|
||||
}
|
||||
}
|
||||
|
||||
.red-warning-text {
|
||||
color: red;
|
||||
.mat-icon {
|
||||
|
@ -0,0 +1,57 @@
|
||||
# Generated by Finn Stutzenstein on 2020-08-25 10:22
|
||||
|
||||
from django.contrib.auth.models import Permission
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db import migrations
|
||||
|
||||
from openslides.users.models import Group
|
||||
|
||||
|
||||
def add_permission_to_delegates(apps, schema_editor):
|
||||
"""
|
||||
Adds the permissions `can_see_autopilot` to the delegate group.
|
||||
"""
|
||||
Projector = apps.get_model("core", "Projector")
|
||||
|
||||
try:
|
||||
delegate = Group.objects.get(name="Delegates")
|
||||
except Group.DoesNotExist:
|
||||
return
|
||||
|
||||
content_type = ContentType.objects.get_for_model(Projector)
|
||||
try:
|
||||
perm = Permission.objects.get(
|
||||
content_type=content_type, codename="can_see_autopilot"
|
||||
)
|
||||
except Permission.DoesNotExist:
|
||||
perm = Permission.objects.create(
|
||||
codename="can_see_autopilot",
|
||||
name="Can see the autopilot",
|
||||
content_type=content_type,
|
||||
)
|
||||
|
||||
delegate.permissions.add(perm)
|
||||
delegate.save()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("core", "0034_amendment_projection_defaults"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name="projector",
|
||||
options={
|
||||
"default_permissions": (),
|
||||
"permissions": (
|
||||
("can_see_projector", "Can see the projector"),
|
||||
("can_manage_projector", "Can manage the projector"),
|
||||
("can_see_frontpage", "Can see the front page"),
|
||||
("can_see_livestream", "Can see the live stream"),
|
||||
("can_see_autopilot", "Can see the autopilot"),
|
||||
),
|
||||
},
|
||||
),
|
||||
migrations.RunPython(add_permission_to_delegates),
|
||||
]
|
@ -119,6 +119,7 @@ class Projector(RESTModelMixin, models.Model):
|
||||
("can_manage_projector", "Can manage the projector"),
|
||||
("can_see_frontpage", "Can see the front page"),
|
||||
("can_see_livestream", "Can see the live stream"),
|
||||
("can_see_autopilot", "Can see the autopilot"),
|
||||
)
|
||||
|
||||
|
||||
|
@ -51,6 +51,7 @@ def create_builtin_groups_and_admin(**kwargs):
|
||||
"core.can_see_frontpage",
|
||||
"core.can_see_history",
|
||||
"core.can_see_projector",
|
||||
"core.can_see_autopilot",
|
||||
"mediafiles.can_manage",
|
||||
"mediafiles.can_see",
|
||||
"motions.can_create",
|
||||
@ -113,6 +114,7 @@ def create_builtin_groups_and_admin(**kwargs):
|
||||
permission_dict["assignments.can_nominate_self"],
|
||||
permission_dict["core.can_see_frontpage"],
|
||||
permission_dict["core.can_see_projector"],
|
||||
permission_dict["core.can_see_autopilot"],
|
||||
permission_dict["mediafiles.can_see"],
|
||||
permission_dict["motions.can_see"],
|
||||
permission_dict["motions.can_create"],
|
||||
|
Loading…
Reference in New Issue
Block a user