Merge pull request #5515 from tsiegleauq/lazy-user-mode
Implement cinema mode (autopilot)
This commit is contained in:
commit
cf4573cb54
@ -3,6 +3,7 @@ import { Injectable, Injector } from '@angular/core';
|
|||||||
import { AgendaAppConfig } from '../../site/agenda/agenda.config';
|
import { AgendaAppConfig } from '../../site/agenda/agenda.config';
|
||||||
import { AppConfig, ModelEntry, SearchableModelEntry } from '../definitions/app-config';
|
import { AppConfig, ModelEntry, SearchableModelEntry } from '../definitions/app-config';
|
||||||
import { BaseRepository } from 'app/core/repositories/base-repository';
|
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 { HistoryAppConfig } from 'app/site/history/history.config';
|
||||||
import { ProjectorAppConfig } from 'app/site/projector/projector.config';
|
import { ProjectorAppConfig } from 'app/site/projector/projector.config';
|
||||||
import { TopicsAppConfig } from 'app/site/topics/topics.config';
|
import { TopicsAppConfig } from 'app/site/topics/topics.config';
|
||||||
@ -35,7 +36,8 @@ const appConfigs: AppConfig[] = [
|
|||||||
UsersAppConfig,
|
UsersAppConfig,
|
||||||
HistoryAppConfig,
|
HistoryAppConfig,
|
||||||
ProjectorAppConfig,
|
ProjectorAppConfig,
|
||||||
TopicsAppConfig
|
TopicsAppConfig,
|
||||||
|
CinemaAppConfig
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -40,6 +40,7 @@ export enum Permission {
|
|||||||
coreCanSeeProjector = 'core.can_see_projector',
|
coreCanSeeProjector = 'core.can_see_projector',
|
||||||
coreCanManageTags = 'core.can_manage_tags',
|
coreCanManageTags = 'core.can_manage_tags',
|
||||||
coreCanSeeLiveStream = 'core.can_see_livestream',
|
coreCanSeeLiveStream = 'core.can_see_livestream',
|
||||||
|
coreCanSeeAutopilot = 'core.can_see_autopilot',
|
||||||
mediafilesCanManage = 'mediafiles.can_manage',
|
mediafilesCanManage = 'mediafiles.can_manage',
|
||||||
mediafilesCanSee = 'mediafiles.can_see',
|
mediafilesCanSee = 'mediafiles.can_see',
|
||||||
motionsCanCreate = 'motions.can_create',
|
motionsCanCreate = 'motions.can_create',
|
||||||
|
@ -317,7 +317,7 @@ export class ProjectorService {
|
|||||||
* @param element The projector element
|
* @param element The projector element
|
||||||
* @returns the view model from the projector element
|
* @returns the view model from the projector element
|
||||||
*/
|
*/
|
||||||
public getViewModelFromProjectorElement<T extends BaseProjectableViewModel>(
|
public getViewModelFromIdentifiableProjectorElement<T extends BaseProjectableViewModel>(
|
||||||
element: IdentifiableProjectorElement
|
element: IdentifiableProjectorElement
|
||||||
): T {
|
): T {
|
||||||
this.assertElementIsMappable(element);
|
this.assertElementIsMappable(element);
|
||||||
@ -328,12 +328,16 @@ export class ProjectorService {
|
|||||||
return viewModel;
|
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 {
|
public getSlideTitle(element: ProjectorElement): ProjectorTitle {
|
||||||
if (this.slideManager.canSlideBeMappedToModel(element.name)) {
|
if (this.slideManager.canSlideBeMappedToModel(element.name)) {
|
||||||
const idElement = this.slideManager.getIdentifiableProjectorElement(element);
|
const viewModel = this.getViewModelFromProjectorElement(element);
|
||||||
const viewModel = this.getViewModelFromProjectorElement(idElement);
|
|
||||||
if (viewModel) {
|
if (viewModel) {
|
||||||
return viewModel.getProjectorTitle();
|
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/`);
|
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
|
* return the id of the current reference projector
|
||||||
* prefer the observable whenever possible
|
* 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
|
// TODO: After logging in, this is null this.getViewModelList() is null
|
||||||
return this.getViewModelList().find(projector => projector.isReferenceProjector).id;
|
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 { AssignmentPollMethod } from 'app/shared/models/assignments/assignment-poll';
|
||||||
import { ViewAssignmentPoll } from 'app/site/assignments/models/view-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';
|
import { PollData, PollTableData, VotingResult } from 'app/site/polls/services/poll.service';
|
||||||
|
|
||||||
@Component({
|
@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 { 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 { MotionPollService } from 'app/site/motions/services/motion-poll.service';
|
||||||
import { PollPercentBasePipe } from './poll-percent-base.pipe';
|
import { PollPercentBasePipe } from './poll-percent-base.pipe';
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { Pipe, PipeTransform } from '@angular/core';
|
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 { MotionPollService } from 'app/site/motions/services/motion-poll.service';
|
||||||
import { PollData } from 'app/site/polls/services/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 { JitsiComponent } from './components/jitsi/jitsi.component';
|
||||||
import { VjsPlayerComponent } from './components/vjs-player/vjs-player.component';
|
import { VjsPlayerComponent } from './components/vjs-player/vjs-player.component';
|
||||||
import { LiveStreamComponent } from './components/live-stream/live-stream.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.
|
* Share Module for all "dumb" components and pipes.
|
||||||
@ -298,7 +299,8 @@ import { LiveStreamComponent } from './components/live-stream/live-stream.compon
|
|||||||
AssignmentPollDetailContentComponent,
|
AssignmentPollDetailContentComponent,
|
||||||
JitsiComponent,
|
JitsiComponent,
|
||||||
VjsPlayerComponent,
|
VjsPlayerComponent,
|
||||||
LiveStreamComponent
|
LiveStreamComponent,
|
||||||
|
ListOfSpeakersContentComponent
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
PermsDirective,
|
PermsDirective,
|
||||||
@ -361,7 +363,8 @@ import { LiveStreamComponent } from './components/live-stream/live-stream.compon
|
|||||||
AssignmentPollDetailContentComponent,
|
AssignmentPollDetailContentComponent,
|
||||||
JitsiComponent,
|
JitsiComponent,
|
||||||
VjsPlayerComponent,
|
VjsPlayerComponent,
|
||||||
LiveStreamComponent
|
LiveStreamComponent,
|
||||||
|
ListOfSpeakersContentComponent
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
{
|
{
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
<os-head-bar
|
<os-head-bar
|
||||||
[nav]="false"
|
[nav]="false"
|
||||||
[goBack]="true"
|
[goBack]="true"
|
||||||
[editMode]="isMobile && isSortMode"
|
[editMode]="isMobile && manualSortMode"
|
||||||
(cancelEditEvent)="onCancelSorting()"
|
(cancelEditEvent)="setManualSortMode(false)"
|
||||||
(saveEvent)="onMobileSaveSorting()"
|
(saveEvent)="onMobileSaveSorting()"
|
||||||
>
|
>
|
||||||
<!-- Title -->
|
<!-- Title -->
|
||||||
@ -18,7 +18,7 @@
|
|||||||
mat-icon-button
|
mat-icon-button
|
||||||
matTooltip="{{ 'Re-add last speaker' | translate }}"
|
matTooltip="{{ 'Re-add last speaker' | translate }}"
|
||||||
(click)="readdLastSpeaker()"
|
(click)="readdLastSpeaker()"
|
||||||
[disabled]="!finishedSpeakers || !finishedSpeakers.length"
|
[disabled]="!hasFinishedSpeakers"
|
||||||
>
|
>
|
||||||
<mat-icon>undo</mat-icon>
|
<mat-icon>undo</mat-icon>
|
||||||
</button>
|
</button>
|
||||||
@ -26,167 +26,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</os-head-bar>
|
</os-head-bar>
|
||||||
|
|
||||||
<mat-card class="os-card speaker-card" *ngIf="viewListOfSpeakers">
|
<os-list-of-speakers-content
|
||||||
<!-- Title -->
|
#content
|
||||||
<h1 class="los-title">
|
[speakers]="viewListOfSpeakers"
|
||||||
{{ viewListOfSpeakers.getTitle() }}
|
[sortMode]="manualSortMode"
|
||||||
<mat-icon *ngIf="viewListOfSpeakers.closed">
|
(isListOfSpeakersEmptyEvent)="isListOfSpeakersEmpty = $event"
|
||||||
lock
|
(hasFinishesSpeakersEvent)="hasFinishedSpeakers = $event"
|
||||||
</mat-icon>
|
></os-list-of-speakers-content>
|
||||||
</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>
|
|
||||||
|
|
||||||
<mat-menu #speakerMenu="matMenu">
|
<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>
|
<mat-icon>sort</mat-icon>
|
||||||
<span>{{ 'Sort' | translate }}</span>
|
<span>{{ 'Sort' | translate }}</span>
|
||||||
</button>
|
</button>
|
||||||
@ -222,9 +71,9 @@
|
|||||||
<span>{{ 'Close list of speakers' | translate }}</span>
|
<span>{{ 'Close list of speakers' | translate }}</span>
|
||||||
</button>
|
</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>
|
<mat-icon>delete</mat-icon>
|
||||||
<span>{{ 'Remove all speakers' | translate }}</span>
|
<span>{{ 'Remove all speakers' | translate }}</span>
|
||||||
</button>
|
</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 { Component, OnInit, ViewChild } from '@angular/core';
|
||||||
import { FormControl, FormGroup } from '@angular/forms';
|
|
||||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||||
import { Title } from '@angular/platform-browser';
|
import { Title } from '@angular/platform-browser';
|
||||||
import { ActivatedRoute } from '@angular/router';
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
import { BehaviorSubject, Subscription } from 'rxjs';
|
|
||||||
|
|
||||||
import { CollectionStringMapperService } from 'app/core/core-services/collection-string-mapper.service';
|
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 { 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 { PromptService } from 'app/core/ui-services/prompt.service';
|
||||||
import { ViewportService } from 'app/core/ui-services/viewport.service';
|
import { ViewportService } from 'app/core/ui-services/viewport.service';
|
||||||
import { Selectable } from 'app/shared/components/selectable';
|
import { ListOfSpeakersContentComponent } from 'app/shared/components/list-of-speakers-content/list-of-speakers-content.component';
|
||||||
import { SortingListComponent } from 'app/shared/components/sorting-list/sorting-list.component';
|
|
||||||
import { BaseViewComponent } from 'app/site/base/base-view';
|
import { BaseViewComponent } from 'app/site/base/base-view';
|
||||||
import { ProjectorElementBuildDeskriptor } from 'app/site/base/projectable';
|
import { ProjectorElementBuildDeskriptor } from 'app/site/base/projectable';
|
||||||
import { ViewProjector } from 'app/site/projector/models/view-projector';
|
import { ViewProjector } from 'app/site/projector/models/view-projector';
|
||||||
import { CurrentListOfSpeakersSlideService } from 'app/site/projector/services/current-list-of-speakers-slide.service';
|
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 { 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 { ViewListOfSpeakers } from '../../models/view-list-of-speakers';
|
||||||
import { SpeakerState, ViewSpeaker } from '../../models/view-speaker';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The list of speakers for agenda items.
|
* The list of speakers for agenda items.
|
||||||
@ -33,73 +23,26 @@ import { SpeakerState, ViewSpeaker } from '../../models/view-speaker';
|
|||||||
@Component({
|
@Component({
|
||||||
selector: 'os-list-of-speakers',
|
selector: 'os-list-of-speakers',
|
||||||
templateUrl: './list-of-speakers.component.html',
|
templateUrl: './list-of-speakers.component.html',
|
||||||
styleUrls: ['./list-of-speakers.component.scss'],
|
styleUrls: ['./list-of-speakers.component.scss']
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush
|
|
||||||
})
|
})
|
||||||
export class ListOfSpeakersComponent extends BaseViewComponent implements OnInit {
|
export class ListOfSpeakersComponent extends BaseViewComponent implements OnInit {
|
||||||
@ViewChild(SortingListComponent)
|
@ViewChild('content')
|
||||||
public listElement: SortingListComponent;
|
private listOfSpeakersContentComponent: ListOfSpeakersContentComponent;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine if the user is viewing the current list if speakers
|
* Determine if the user is viewing the current list if speakers
|
||||||
*/
|
*/
|
||||||
public isCurrentListOfSpeakers = false;
|
public isCurrentListOfSpeakers = false;
|
||||||
|
|
||||||
/**
|
|
||||||
* Holds whether the list is in sort mode or not
|
|
||||||
*/
|
|
||||||
public isSortMode = false;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Holds the view item to the given topic
|
* Holds the view item to the given topic
|
||||||
*/
|
*/
|
||||||
public viewListOfSpeakers: ViewListOfSpeakers;
|
public viewListOfSpeakers: ViewListOfSpeakers;
|
||||||
|
|
||||||
/**
|
|
||||||
* Holds the speakers
|
|
||||||
*/
|
|
||||||
public speakers: ViewSpeaker[];
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Holds a list of projectors. Only in CurrentListOfSpeakers mode
|
* Holds a list of projectors. Only in CurrentListOfSpeakers mode
|
||||||
*/
|
*/
|
||||||
public projectors: ViewProjector[];
|
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
|
* @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;
|
return this.viewListOfSpeakers && this.viewListOfSpeakers.closed;
|
||||||
}
|
}
|
||||||
|
|
||||||
public get isListOfSpeakersEmpty(): boolean {
|
public isMobile: boolean;
|
||||||
if (this.speakers && this.speakers.length) {
|
|
||||||
return false;
|
public manualSortMode = false;
|
||||||
} else if (this.finishedSpeakers && this.finishedSpeakers.length) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return !this.activeSpeaker;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns true if the current user can be added to the list of speakers
|
* filled by child component
|
||||||
*/
|
*/
|
||||||
public get canAddSelf(): boolean {
|
public isListOfSpeakersEmpty: boolean;
|
||||||
return !this.config.instant('agenda_present_speakers_only') || this.operator.user.is_present;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Used to detect changes in the projector reference.
|
* filled by child component
|
||||||
*/
|
*/
|
||||||
private closReferenceProjectorId: number | null;
|
public hasFinishedSpeakers: boolean;
|
||||||
|
|
||||||
private closSubscription: Subscription | null;
|
|
||||||
|
|
||||||
public showFistContributionHint: boolean;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* List of speakers to save temporarily changes made by sorting-list.
|
|
||||||
*/
|
|
||||||
private speakerListAsSelectable: Selectable[] = [];
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor for speaker list component. Generates the forms.
|
* Constructor for speaker list component. Generates the forms.
|
||||||
@ -143,7 +70,6 @@ export class ListOfSpeakersComponent extends BaseViewComponent implements OnInit
|
|||||||
* @param title
|
* @param title
|
||||||
* @param translate
|
* @param translate
|
||||||
* @param snackBar
|
* @param snackBar
|
||||||
* @param projectorRepo
|
|
||||||
* @param route Angulars ActivatedRoute
|
* @param route Angulars ActivatedRoute
|
||||||
* @param DS the DataStore
|
* @param DS the DataStore
|
||||||
* @param listOfSpeakersRepo Repository for list of speakers
|
* @param listOfSpeakersRepo Repository for list of speakers
|
||||||
@ -156,30 +82,17 @@ export class ListOfSpeakersComponent extends BaseViewComponent implements OnInit
|
|||||||
title: Title,
|
title: Title,
|
||||||
protected translate: TranslateService, // protected required for ng-translate-extract
|
protected translate: TranslateService, // protected required for ng-translate-extract
|
||||||
snackBar: MatSnackBar,
|
snackBar: MatSnackBar,
|
||||||
private projectorRepo: ProjectorRepositoryService,
|
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
private listOfSpeakersRepo: ListOfSpeakersRepositoryService,
|
private listOfSpeakersRepo: ListOfSpeakersRepositoryService,
|
||||||
private operator: OperatorService,
|
|
||||||
private promptService: PromptService,
|
private promptService: PromptService,
|
||||||
private currentListOfSpeakersService: CurrentListOfSpeakersService,
|
private currentListOfSpeakersService: CurrentListOfSpeakersService,
|
||||||
private durationService: DurationService,
|
|
||||||
private userRepository: UserRepositoryService,
|
|
||||||
private collectionStringMapper: CollectionStringMapperService,
|
private collectionStringMapper: CollectionStringMapperService,
|
||||||
private currentListOfSpeakersSlideService: CurrentListOfSpeakersSlideService,
|
private currentListOfSpeakersSlideService: CurrentListOfSpeakersSlideService,
|
||||||
private config: ConfigService,
|
private viewport: ViewportService
|
||||||
private viewport: ViewportService,
|
|
||||||
private cd: ChangeDetectorRef
|
|
||||||
) {
|
) {
|
||||||
super(title, translate, snackBar);
|
super(title, translate, snackBar);
|
||||||
this.addSpeakerForm = new FormGroup({ user_id: new FormControl() });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Init.
|
|
||||||
*
|
|
||||||
* Observe users,
|
|
||||||
* React to form changes
|
|
||||||
*/
|
|
||||||
public ngOnInit(): void {
|
public ngOnInit(): void {
|
||||||
// Check, if we are on the current list of speakers.
|
// Check, if we are on the current list of speakers.
|
||||||
this.isCurrentListOfSpeakers =
|
this.isCurrentListOfSpeakers =
|
||||||
@ -188,76 +101,17 @@ export class ListOfSpeakersComponent extends BaseViewComponent implements OnInit
|
|||||||
: true;
|
: true;
|
||||||
|
|
||||||
if (this.isCurrentListOfSpeakers) {
|
if (this.isCurrentListOfSpeakers) {
|
||||||
this.projectors = this.projectorRepo.getViewModelList();
|
|
||||||
this.updateClosProjector();
|
|
||||||
this.subscriptions.push(
|
this.subscriptions.push(
|
||||||
this.projectorRepo.getViewModelListObservable().subscribe(newProjectors => {
|
this.currentListOfSpeakersService.currentListOfSpeakersObservable.subscribe(clos => {
|
||||||
this.projectors = newProjectors;
|
this.setListOfSpeakers(clos);
|
||||||
this.updateClosProjector();
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
const id = +this.route.snapshot.url[this.route.snapshot.url.length - 1].path;
|
const id = +this.route.snapshot.url[this.route.snapshot.url.length - 1].path;
|
||||||
this.setListOfSpeakersId(id);
|
this.setListOfSpeakersById(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.subscriptions.push(
|
this.subscriptions.push(this.viewport.isMobileSubject.subscribe(isMobile => (this.isMobile = isMobile)));
|
||||||
// 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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -272,16 +126,14 @@ export class ListOfSpeakersComponent extends BaseViewComponent implements OnInit
|
|||||||
*
|
*
|
||||||
* @param id the list of speakers id
|
* @param id the list of speakers id
|
||||||
*/
|
*/
|
||||||
private setListOfSpeakersId(id: number): void {
|
private setListOfSpeakersById(id: number): void {
|
||||||
if (this.closSubscription) {
|
this.subscriptions.push(
|
||||||
this.closSubscription.unsubscribe();
|
this.listOfSpeakersRepo.getViewModelObservable(id).subscribe(listOfSpeakers => {
|
||||||
}
|
if (listOfSpeakers) {
|
||||||
|
this.setListOfSpeakers(listOfSpeakers);
|
||||||
this.closSubscription = this.listOfSpeakersRepo.getViewModelObservable(id).subscribe(listOfSpeakers => {
|
}
|
||||||
if (listOfSpeakers) {
|
})
|
||||||
this.setListOfSpeakers(listOfSpeakers);
|
);
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private setListOfSpeakers(listOfSpeakers: ViewListOfSpeakers): void {
|
private setListOfSpeakers(listOfSpeakers: ViewListOfSpeakers): void {
|
||||||
@ -290,21 +142,6 @@ export class ListOfSpeakersComponent extends BaseViewComponent implements OnInit
|
|||||||
: listOfSpeakers.getTitle() + ` - ${this.translate.instant('List of speakers')}`;
|
: listOfSpeakers.getTitle() + ` - ${this.translate.instant('List of speakers')}`;
|
||||||
super.setTitle(title);
|
super.setTitle(title);
|
||||||
this.viewListOfSpeakers = listOfSpeakers;
|
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;
|
return verboseName;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public setManualSortMode(active: boolean): void {
|
||||||
* Create a speaker out of an id
|
this.manualSortMode = active;
|
||||||
*
|
|
||||||
* @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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Saves sorting on mobile devices.
|
* Saves sorting on mobile devices.
|
||||||
*/
|
*/
|
||||||
public onMobileSaveSorting(): void {
|
public async onMobileSaveSorting(): Promise<void> {
|
||||||
this.onSaveSorting(this.speakerListAsSelectable);
|
await this.listOfSpeakersContentComponent.onSaveSorting();
|
||||||
this.isSortMode = false;
|
this.manualSortMode = 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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -401,58 +174,6 @@ export class ListOfSpeakersComponent extends BaseViewComponent implements OnInit
|
|||||||
this.listOfSpeakersRepo.readdLastSpeaker(this.viewListOfSpeakers).catch(this.raiseError);
|
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
|
* Closes the current list of speakers
|
||||||
*/
|
*/
|
||||||
@ -483,77 +204,4 @@ export class ListOfSpeakersComponent extends BaseViewComponent implements OnInit
|
|||||||
this.listOfSpeakersRepo.deleteAllSpeakers(this.viewListOfSpeakers);
|
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 { Permission } from 'app/core/core-services/operator.service';
|
||||||
import { AssignmentDetailComponent } from './components/assignment-detail/assignment-detail.component';
|
import { AssignmentDetailComponent } from './components/assignment-detail/assignment-detail.component';
|
||||||
import { AssignmentListComponent } from './components/assignment-list/assignment-list.component';
|
import { AssignmentListComponent } from './components/assignment-list/assignment-list.component';
|
||||||
import { AssignmentPollDetailComponent } from './components/assignment-poll-detail/assignment-poll-detail.component';
|
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{ path: '', component: AssignmentListComponent, pathMatch: 'full' },
|
{ path: '', component: AssignmentListComponent, pathMatch: 'full' },
|
||||||
{ path: 'new', component: AssignmentDetailComponent, data: { basePerm: Permission.assignmentsCanManage } },
|
{ path: 'new', component: AssignmentDetailComponent, data: { basePerm: Permission.assignmentsCanManage } },
|
||||||
{ path: ':id', component: AssignmentDetailComponent, data: { basePerm: Permission.assignmentsCanSee } },
|
{ 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({
|
@NgModule({
|
||||||
|
@ -3,23 +3,12 @@ import { NgModule } from '@angular/core';
|
|||||||
|
|
||||||
import { AssignmentDetailComponent } from './components/assignment-detail/assignment-detail.component';
|
import { AssignmentDetailComponent } from './components/assignment-detail/assignment-detail.component';
|
||||||
import { AssignmentListComponent } from './components/assignment-list/assignment-list.component';
|
import { AssignmentListComponent } from './components/assignment-list/assignment-list.component';
|
||||||
import { AssignmentPollDetailComponent } from './components/assignment-poll-detail/assignment-poll-detail.component';
|
import { AssignmentPollModule } from './modules/assignment-poll/assignment-poll.module';
|
||||||
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 { AssignmentsRoutingModule } from './assignments-routing.module';
|
import { AssignmentsRoutingModule } from './assignments-routing.module';
|
||||||
import { PollsModule } from '../polls/polls.module';
|
|
||||||
import { SharedModule } from '../../shared/shared.module';
|
import { SharedModule } from '../../shared/shared.module';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [CommonModule, AssignmentsRoutingModule, SharedModule, PollsModule],
|
imports: [CommonModule, AssignmentsRoutingModule, AssignmentPollModule, SharedModule],
|
||||||
declarations: [
|
declarations: [AssignmentDetailComponent, AssignmentListComponent]
|
||||||
AssignmentDetailComponent,
|
|
||||||
AssignmentListComponent,
|
|
||||||
AssignmentPollComponent,
|
|
||||||
AssignmentPollDetailComponent,
|
|
||||||
AssignmentPollVoteComponent,
|
|
||||||
AssignmentPollDialogComponent
|
|
||||||
]
|
|
||||||
})
|
})
|
||||||
export class AssignmentsModule {}
|
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 { PollProgressComponent } from 'app/site/polls/components/poll-progress/poll-progress.component';
|
||||||
import { AssignmentDetailComponent } from './assignment-detail.component';
|
import { AssignmentDetailComponent } from './assignment-detail.component';
|
||||||
import { AssignmentPollVoteComponent } from '../assignment-poll-vote/assignment-poll-vote.component';
|
import { AssignmentPollVoteComponent } from '../../modules/assignment-poll/components/assignment-poll-vote/assignment-poll-vote.component';
|
||||||
import { AssignmentPollComponent } from '../assignment-poll/assignment-poll.component';
|
import { AssignmentPollComponent } from '../../modules/assignment-poll/components/assignment-poll/assignment-poll.component';
|
||||||
|
|
||||||
describe('AssignmentDetailComponent', () => {
|
describe('AssignmentDetailComponent', () => {
|
||||||
let component: 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 { ViewTag } from 'app/site/tags/models/view-tag';
|
||||||
import { ViewUser } from 'app/site/users/models/view-user';
|
import { ViewUser } from 'app/site/users/models/view-user';
|
||||||
import { AssignmentPdfExportService } from '../../services/assignment-pdf-export.service';
|
import { AssignmentPdfExportService } from '../../services/assignment-pdf-export.service';
|
||||||
import { AssignmentPollDialogService } from '../../services/assignment-poll-dialog.service';
|
import { AssignmentPollDialogService } from '../../modules/assignment-poll/services/assignment-poll-dialog.service';
|
||||||
import { AssignmentPollService } from '../../services/assignment-poll.service';
|
import { AssignmentPollService } from '../../modules/assignment-poll/services/assignment-poll.service';
|
||||||
import { AssignmentPhases, ViewAssignment } from '../../models/view-assignment';
|
import { AssignmentPhases, ViewAssignment } from '../../models/view-assignment';
|
||||||
import { ViewAssignmentPoll } from '../../models/view-assignment-poll';
|
import { ViewAssignmentPoll } from '../../models/view-assignment-poll';
|
||||||
import { ViewAssignmentRelatedUser } from '../../models/view-assignment-related-user';
|
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 { ConfigService } from 'app/core/ui-services/config.service';
|
||||||
import { PromptService } from 'app/core/ui-services/prompt.service';
|
import { PromptService } from 'app/core/ui-services/prompt.service';
|
||||||
import { VoteValue } from 'app/shared/models/poll/base-vote';
|
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 { BasePollDetailComponent } from 'app/site/polls/components/base-poll-detail.component';
|
||||||
import { AssignmentPollDialogService } from '../../services/assignment-poll-dialog.service';
|
import { AssignmentPollDialogService } from '../../services/assignment-poll-dialog.service';
|
||||||
import { AssignmentPollService } from '../../services/assignment-poll.service';
|
import { AssignmentPollService } from '../../services/assignment-poll.service';
|
||||||
import { ViewAssignmentPoll } from '../../models/view-assignment-poll';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'os-assignment-poll-detail',
|
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 { GeneralValueVerbose, VoteValue, VoteValueVerbose } from 'app/shared/models/poll/base-vote';
|
||||||
import {
|
import {
|
||||||
AssignmentPollMethodVerbose,
|
AssignmentPollMethodVerbose,
|
||||||
AssignmentPollPercentBaseVerbose
|
AssignmentPollPercentBaseVerbose,
|
||||||
|
ViewAssignmentPoll
|
||||||
} from 'app/site/assignments/models/view-assignment-poll';
|
} from 'app/site/assignments/models/view-assignment-poll';
|
||||||
import { BasePollDialogComponent } from 'app/site/polls/components/base-poll-dialog.component';
|
import { BasePollDialogComponent } from 'app/site/polls/components/base-poll-dialog.component';
|
||||||
import { PollFormComponent } from 'app/site/polls/components/poll-form/poll-form.component';
|
import { PollFormComponent } from 'app/site/polls/components/poll-form/poll-form.component';
|
||||||
import { ViewUser } from 'app/site/users/models/view-user';
|
import { ViewUser } from 'app/site/users/models/view-user';
|
||||||
import { AssignmentPollService } from '../../services/assignment-poll.service';
|
import { AssignmentPollService } from '../../services/assignment-poll.service';
|
||||||
import { ViewAssignmentPoll } from '../../models/view-assignment-poll';
|
|
||||||
|
|
||||||
type OptionsObject = { user_id: number; user: ViewUser }[];
|
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 { AssignmentPollMethod } from 'app/shared/models/assignments/assignment-poll';
|
||||||
import { PollType } from 'app/shared/models/poll/base-poll';
|
import { PollType } from 'app/shared/models/poll/base-poll';
|
||||||
import { VoteValue } from 'app/shared/models/poll/base-vote';
|
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 { BasePollVoteComponent } from 'app/site/polls/components/base-poll-vote.component';
|
||||||
import { ViewAssignmentPoll } from '../../models/view-assignment-poll';
|
|
||||||
|
|
||||||
// TODO: Duplicate
|
// TODO: Duplicate
|
||||||
interface VoteActions {
|
interface VoteActions {
|
@ -10,11 +10,11 @@ import { AssignmentPollRepositoryService } from 'app/core/repositories/assignmen
|
|||||||
import { PromptService } from 'app/core/ui-services/prompt.service';
|
import { PromptService } from 'app/core/ui-services/prompt.service';
|
||||||
import { VotingPrivacyWarningComponent } from 'app/shared/components/voting-privacy-warning/voting-privacy-warning.component';
|
import { VotingPrivacyWarningComponent } from 'app/shared/components/voting-privacy-warning/voting-privacy-warning.component';
|
||||||
import { infoDialogSettings } from 'app/shared/utils/dialog-settings';
|
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 { BasePollComponent } from 'app/site/polls/components/base-poll.component';
|
||||||
import { AssignmentPollDialogService } from '../../services/assignment-poll-dialog.service';
|
import { AssignmentPollDialogService } from '../../services/assignment-poll-dialog.service';
|
||||||
import { AssignmentPollPdfService } from '../../services/assignment-poll-pdf.service';
|
import { AssignmentPollPdfService } from '../../services/assignment-poll-pdf.service';
|
||||||
import { AssignmentPollService } from '../../services/assignment-poll.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
|
* 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 { CollectionStringMapperService } from 'app/core/core-services/collection-string-mapper.service';
|
||||||
import { BasePollDialogService } from 'app/core/ui-services/base-poll-dialog.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 { 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`
|
* 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 { UserRepositoryService } from 'app/core/repositories/users/user-repository.service';
|
||||||
import { ConfigService } from 'app/core/ui-services/config.service';
|
import { ConfigService } from 'app/core/ui-services/config.service';
|
||||||
import { AssignmentPollMethod } from 'app/shared/models/assignments/assignment-poll';
|
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
|
* 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 { MajorityMethod, PollType, VOTE_UNDOCUMENTED } from 'app/shared/models/poll/base-poll';
|
||||||
import { ParsePollNumberPipe } from 'app/shared/pipes/parse-poll-number.pipe';
|
import { ParsePollNumberPipe } from 'app/shared/pipes/parse-poll-number.pipe';
|
||||||
import { PollKeyVerbosePipe } from 'app/shared/pipes/poll-key-verbose.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 {
|
import {
|
||||||
PollData,
|
PollData,
|
||||||
PollDataOption,
|
PollDataOption,
|
||||||
@ -20,8 +22,6 @@ import {
|
|||||||
PollTableData,
|
PollTableData,
|
||||||
VotingResult
|
VotingResult
|
||||||
} from 'app/site/polls/services/poll.service';
|
} from 'app/site/polls/services/poll.service';
|
||||||
import { ViewAssignmentOption } from '../models/view-assignment-option';
|
|
||||||
import { ViewAssignmentPoll } from '../models/view-assignment-poll';
|
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
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 { PollKeyVerbosePipe } from 'app/shared/pipes/poll-key-verbose.pipe';
|
||||||
import { PollPercentBasePipe } from 'app/shared/pipes/poll-percent-base.pipe';
|
import { PollPercentBasePipe } from 'app/shared/pipes/poll-percent-base.pipe';
|
||||||
import { PollTableData, VotingResult } from 'app/site/polls/services/poll.service';
|
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 { ViewAssignment } from '../models/view-assignment';
|
||||||
import { ViewAssignmentPoll } from '../models/view-assignment-poll';
|
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">
|
<ng-container *ngIf="poll && !poll.user_has_voted; else userHasVotes">
|
||||||
<div *ngIf="vmanager.canVote(poll) && !deliveringVote" class="vote-button-grid">
|
<div *ngIf="vmanager.canVote(poll) && !deliveringVote" class="vote-button-grid">
|
||||||
<!-- Voting -->
|
<!-- Voting -->
|
||||||
|
@ -10,7 +10,7 @@ import { MotionPollComponent } from './motion-poll/motion-poll.component';
|
|||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [CommonModule, SharedModule, MotionPollRoutingModule, PollsModule],
|
imports: [CommonModule, SharedModule, MotionPollRoutingModule, PollsModule],
|
||||||
exports: [MotionPollComponent],
|
exports: [MotionPollComponent, MotionPollVoteComponent],
|
||||||
declarations: [MotionPollComponent, MotionPollDetailComponent, MotionPollVoteComponent]
|
declarations: [MotionPollComponent, MotionPollDetailComponent, MotionPollVoteComponent]
|
||||||
})
|
})
|
||||||
export class MotionPollModule {}
|
export class MotionPollModule {}
|
||||||
|
@ -57,6 +57,9 @@
|
|||||||
|
|
||||||
<!-- Results -->
|
<!-- Results -->
|
||||||
<ng-container *ngIf="poll && !poll.stateHasVotes && poll.type !== 'analog'; else votingResult">
|
<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>
|
<os-motion-poll-vote [poll]="poll"></os-motion-poll-vote>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
|
@ -19,9 +19,9 @@ import { ViewProjector } from '../models/view-projector';
|
|||||||
})
|
})
|
||||||
export class CurrentListOfSpeakersService {
|
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.
|
* 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 => {
|
this.projectorRepo.getReferenceProjectorObservable().subscribe(clos => {
|
||||||
if (closId) {
|
if (clos) {
|
||||||
this.closId = closId;
|
this.closRefProjector = clos;
|
||||||
this.currentListOfSpeakerSubject.next(this.getCurrentListOfSpeakers());
|
this.currentListOfSpeakerSubject.next(this.getCurrentListOfSpeakers());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -68,8 +68,7 @@ export class CurrentListOfSpeakersService {
|
|||||||
* Use the subject to get it
|
* Use the subject to get it
|
||||||
*/
|
*/
|
||||||
private getCurrentListOfSpeakers(): ViewListOfSpeakers | null {
|
private getCurrentListOfSpeakers(): ViewListOfSpeakers | null {
|
||||||
const refProjector = this.projectorRepo.getViewModel(this.closId);
|
return this.getCurrentListOfSpeakersForProjector(this.closRefProjector);
|
||||||
return this.getCurrentListOfSpeakersForProjector(refProjector);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -135,7 +134,9 @@ export class CurrentListOfSpeakersService {
|
|||||||
for (const nonStableElement of nonStableElements) {
|
for (const nonStableElement of nonStableElements) {
|
||||||
const identifiableNonStableElement = this.slideManager.getIdentifiableProjectorElement(nonStableElement);
|
const identifiableNonStableElement = this.slideManager.getIdentifiableProjectorElement(nonStableElement);
|
||||||
try {
|
try {
|
||||||
const viewModel = this.projectorService.getViewModelFromProjectorElement(identifiableNonStableElement);
|
const viewModel = this.projectorService.getViewModelFromIdentifiableProjectorElement(
|
||||||
|
identifiableNonStableElement
|
||||||
|
);
|
||||||
if (isBaseViewModelWithListOfSpeakers(viewModel)) {
|
if (isBaseViewModelWithListOfSpeakers(viewModel)) {
|
||||||
return viewModel.listOfSpeakers;
|
return viewModel.listOfSpeakers;
|
||||||
}
|
}
|
||||||
|
@ -75,6 +75,11 @@ const routes: Routes = [
|
|||||||
loadChildren: () => import('./polls/polls.module').then(m => m.PollsModule),
|
loadChildren: () => import('./polls/polls.module').then(m => m.PollsModule),
|
||||||
// one of them is sufficient
|
// one of them is sufficient
|
||||||
data: { basePerm: [Permission.motionsCanSee, Permission.assignmentsCanSee] }
|
data: { basePerm: [Permission.motionsCanSee, Permission.assignmentsCanSee] }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'autopilot',
|
||||||
|
loadChildren: () => import('./cinema/cinema.module').then(m => m.CinemaModule),
|
||||||
|
data: { basePerm: Permission.coreCanSeeAutopilot }
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
canActivateChild: [AuthGuard]
|
canActivateChild: [AuthGuard]
|
||||||
|
@ -25,7 +25,7 @@
|
|||||||
|
|
||||||
<!-- navigation -->
|
<!-- navigation -->
|
||||||
<mat-nav-list class="main-nav">
|
<mat-nav-list class="main-nav">
|
||||||
<span *ngFor="let entry of mainMenuService.entries">
|
<span *ngFor="let entry of mainMenuEntries">
|
||||||
<a
|
<a
|
||||||
[@navItemAnim]
|
[@navItemAnim]
|
||||||
*osPerms="entry.permission"
|
*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 { OverlayService } from 'app/core/ui-services/overlay.service';
|
||||||
import { UpdateService } from 'app/core/ui-services/update.service';
|
import { UpdateService } from 'app/core/ui-services/update.service';
|
||||||
import { BaseComponent } from '../base.component';
|
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 { OpenSlidesStatusService } from '../core/core-services/openslides-status.service';
|
||||||
import { OperatorService } from '../core/core-services/operator.service';
|
import { OperatorService } from '../core/core-services/operator.service';
|
||||||
import { TimeTravelService } from '../core/core-services/time-travel.service';
|
import { TimeTravelService } from '../core/core-services/time-travel.service';
|
||||||
@ -66,6 +66,10 @@ export class SiteComponent extends BaseComponent implements OnInit {
|
|||||||
*/
|
*/
|
||||||
private delayedUpdateAvailable = false;
|
private delayedUpdateAvailable = false;
|
||||||
|
|
||||||
|
public get mainMenuEntries(): MainMenuEntry[] {
|
||||||
|
return this.mainMenuService.entries;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor
|
* Constructor
|
||||||
* @param route
|
* @param route
|
||||||
@ -86,7 +90,7 @@ export class SiteComponent extends BaseComponent implements OnInit {
|
|||||||
public operator: OperatorService,
|
public operator: OperatorService,
|
||||||
public vp: ViewportService,
|
public vp: ViewportService,
|
||||||
public dialog: MatDialog,
|
public dialog: MatDialog,
|
||||||
public mainMenuService: MainMenuService,
|
private mainMenuService: MainMenuService,
|
||||||
public OSStatus: OpenSlidesStatusService,
|
public OSStatus: OpenSlidesStatusService,
|
||||||
public timeTravel: TimeTravelService,
|
public timeTravel: TimeTravelService,
|
||||||
private matSnackBar: MatSnackBar,
|
private matSnackBar: MatSnackBar,
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
|
|
||||||
import { PollState } from 'app/shared/models/poll/base-poll';
|
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 { BasePollSlideComponent } from 'app/slides/polls/base-poll-slide.component';
|
||||||
import { AssignmentPollSlideData } from './assignment-poll-slide-data';
|
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/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/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/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/progress-snack-bar/progress-snack-bar.component.scss-theme.scss';
|
||||||
@import './app/shared/components/jitsi/jitsi.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';
|
@import './app/shared/components/list-view-table/list-view-table.component.scss-theme.scss';
|
||||||
|
@ -81,6 +81,13 @@
|
|||||||
color: mat-color($foreground, text);
|
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 {
|
.tile-color {
|
||||||
background-color: mat-color($background, selected-button);
|
background-color: mat-color($background, selected-button);
|
||||||
}
|
}
|
||||||
|
@ -155,6 +155,15 @@ b,
|
|||||||
text-transform: uppercase;
|
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 {
|
.red-warning-text {
|
||||||
color: red;
|
color: red;
|
||||||
.mat-icon {
|
.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_manage_projector", "Can manage the projector"),
|
||||||
("can_see_frontpage", "Can see the front page"),
|
("can_see_frontpage", "Can see the front page"),
|
||||||
("can_see_livestream", "Can see the live stream"),
|
("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_frontpage",
|
||||||
"core.can_see_history",
|
"core.can_see_history",
|
||||||
"core.can_see_projector",
|
"core.can_see_projector",
|
||||||
|
"core.can_see_autopilot",
|
||||||
"mediafiles.can_manage",
|
"mediafiles.can_manage",
|
||||||
"mediafiles.can_see",
|
"mediafiles.can_see",
|
||||||
"motions.can_create",
|
"motions.can_create",
|
||||||
@ -113,6 +114,7 @@ def create_builtin_groups_and_admin(**kwargs):
|
|||||||
permission_dict["assignments.can_nominate_self"],
|
permission_dict["assignments.can_nominate_self"],
|
||||||
permission_dict["core.can_see_frontpage"],
|
permission_dict["core.can_see_frontpage"],
|
||||||
permission_dict["core.can_see_projector"],
|
permission_dict["core.can_see_projector"],
|
||||||
|
permission_dict["core.can_see_autopilot"],
|
||||||
permission_dict["mediafiles.can_see"],
|
permission_dict["mediafiles.can_see"],
|
||||||
permission_dict["motions.can_see"],
|
permission_dict["motions.can_see"],
|
||||||
permission_dict["motions.can_create"],
|
permission_dict["motions.can_create"],
|
||||||
|
Loading…
Reference in New Issue
Block a user