added virtual scrolling for single votes tables
This commit is contained in:
parent
93dc78c7d6
commit
d4599a435b
@ -14,8 +14,9 @@
|
|||||||
<!-- vScrollAuto () -->
|
<!-- vScrollAuto () -->
|
||||||
<pbl-ngrid
|
<pbl-ngrid
|
||||||
[ngClass]="cssClasses"
|
[ngClass]="cssClasses"
|
||||||
[vScrollFixed]="vScrollFixed"
|
[attr.vScrollFixed]="vScrollFixed !== -1 ? vScrollFixed : false"
|
||||||
[showHeader]="!showFilterBar"
|
[attr.vScrollAuto]="vScrollFixed === -1"
|
||||||
|
[showHeader]="!showFilterBar || !fullScreen"
|
||||||
matCheckboxSelection="selection"
|
matCheckboxSelection="selection"
|
||||||
[dataSource]="dataSource"
|
[dataSource]="dataSource"
|
||||||
[columns]="columnSet"
|
[columns]="columnSet"
|
||||||
|
@ -47,7 +47,7 @@ export interface ColumnRestriction {
|
|||||||
* Creates a sort-filter-bar and table with virtual scrolling, where projector and multi select is already
|
* Creates a sort-filter-bar and table with virtual scrolling, where projector and multi select is already
|
||||||
* embedded
|
* embedded
|
||||||
*
|
*
|
||||||
* Takes a repository-service, a sort-service and a filter-service as an input to display data
|
* Takes a repository-service (or simple Observable), a sort-service and a filter-service as an input to display data
|
||||||
* Requires multi-select information
|
* Requires multi-select information
|
||||||
* Double binds selected rows
|
* Double binds selected rows
|
||||||
*
|
*
|
||||||
@ -65,6 +65,7 @@ export interface ColumnRestriction {
|
|||||||
* <os-list-view-table
|
* <os-list-view-table
|
||||||
* [listObservableProvider]="motionRepo"
|
* [listObservableProvider]="motionRepo"
|
||||||
* [filterService]="filterService"
|
* [filterService]="filterService"
|
||||||
|
* [filterProps]="filterProps"
|
||||||
* [sortService]="sortService"
|
* [sortService]="sortService"
|
||||||
* [columns]="motionColumnDefinition"
|
* [columns]="motionColumnDefinition"
|
||||||
* [restricted]="restrictedColumns"
|
* [restricted]="restrictedColumns"
|
||||||
@ -96,11 +97,17 @@ export class ListViewTableComponent<V extends BaseViewModel, M extends BaseModel
|
|||||||
private ngrid: PblNgridComponent;
|
private ngrid: PblNgridComponent;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The required repository
|
* The required repository (prioritized over listObservable)
|
||||||
*/
|
*/
|
||||||
@Input()
|
@Input()
|
||||||
public listObservableProvider: HasViewModelListObservable<V>;
|
public listObservableProvider: HasViewModelListObservable<V>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ...or the required observable
|
||||||
|
*/
|
||||||
|
@Input()
|
||||||
|
public listObservable: Observable<V[]>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The currently active sorting service for the list view
|
* The currently active sorting service for the list view
|
||||||
*/
|
*/
|
||||||
@ -193,6 +200,13 @@ export class ListViewTableComponent<V extends BaseViewModel, M extends BaseModel
|
|||||||
@Input()
|
@Input()
|
||||||
public vScrollFixed = 110;
|
public vScrollFixed = 110;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines whether the table should have a fixed 100vh height or not.
|
||||||
|
* If not, the height must be set by the component
|
||||||
|
*/
|
||||||
|
@Input()
|
||||||
|
public fullScreen = true;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Option to apply additional classes to the virtual-scrolling-list.
|
* Option to apply additional classes to the virtual-scrolling-list.
|
||||||
*/
|
*/
|
||||||
@ -211,8 +225,8 @@ export class ListViewTableComponent<V extends BaseViewModel, M extends BaseModel
|
|||||||
*/
|
*/
|
||||||
public get cssClasses(): CssClassDefinition {
|
public get cssClasses(): CssClassDefinition {
|
||||||
const defaultClasses = {
|
const defaultClasses = {
|
||||||
'virtual-scroll-with-head-bar ngrid-hide-head': this.showFilterBar,
|
'virtual-scroll-with-head-bar ngrid-hide-head': this.fullScreen && this.showFilterBar,
|
||||||
'virtual-scroll-full-page': !this.showFilterBar,
|
'virtual-scroll-full-page': this.fullScreen && !this.showFilterBar,
|
||||||
multiselect: this.multiSelect
|
multiselect: this.multiSelect
|
||||||
};
|
};
|
||||||
return Object.assign(this._cssClasses, defaultClasses);
|
return Object.assign(this._cssClasses, defaultClasses);
|
||||||
@ -468,8 +482,10 @@ export class ListViewTableComponent<V extends BaseViewModel, M extends BaseModel
|
|||||||
* to the used search and filter services
|
* to the used search and filter services
|
||||||
*/
|
*/
|
||||||
private getListObservable(): void {
|
private getListObservable(): void {
|
||||||
if (this.listObservableProvider) {
|
if (this.listObservableProvider || this.listObservable) {
|
||||||
const listObservable = this.listObservableProvider.getViewModelListObservable();
|
const listObservable = this.listObservableProvider
|
||||||
|
? this.listObservableProvider.getViewModelListObservable()
|
||||||
|
: this.listObservable;
|
||||||
if (this.filterService && this.sortService) {
|
if (this.filterService && this.sortService) {
|
||||||
// filtering and sorting
|
// filtering and sorting
|
||||||
this.filterService.initFilters(listObservable);
|
this.filterService.initFilters(listObservable);
|
||||||
@ -521,15 +537,24 @@ export class ListViewTableComponent<V extends BaseViewModel, M extends BaseModel
|
|||||||
// custom filter predicates
|
// custom filter predicates
|
||||||
if (this.filterProps && this.filterProps.length) {
|
if (this.filterProps && this.filterProps.length) {
|
||||||
for (const prop of this.filterProps) {
|
for (const prop of this.filterProps) {
|
||||||
if (item[prop]) {
|
// find nested props
|
||||||
|
const split = prop.split('.');
|
||||||
|
let currValue: any = item;
|
||||||
|
for (const subProp of split) {
|
||||||
|
if (currValue) {
|
||||||
|
currValue = currValue[subProp];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currValue) {
|
||||||
let propertyAsString = '';
|
let propertyAsString = '';
|
||||||
// If the property is a function, call it.
|
// If the property is a function, call it.
|
||||||
if (typeof item[prop] === 'function') {
|
if (typeof currValue === 'function') {
|
||||||
propertyAsString = '' + item[prop]();
|
propertyAsString = '' + currValue();
|
||||||
} else if (item[prop].constructor === Array) {
|
} else if (currValue.constructor === Array) {
|
||||||
propertyAsString = item[prop].join('');
|
propertyAsString = currValue.join('');
|
||||||
} else {
|
} else {
|
||||||
propertyAsString = '' + item[prop];
|
propertyAsString = '' + currValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (propertyAsString) {
|
if (propertyAsString) {
|
||||||
@ -647,7 +672,9 @@ export class ListViewTableComponent<V extends BaseViewModel, M extends BaseModel
|
|||||||
* This function changes the height of the row for virtual-scrolling in the relating `.scss`-file.
|
* This function changes the height of the row for virtual-scrolling in the relating `.scss`-file.
|
||||||
*/
|
*/
|
||||||
private changeRowHeight(): void {
|
private changeRowHeight(): void {
|
||||||
document.documentElement.style.setProperty('--pbl-height', this.vScrollFixed + 'px');
|
if (this.vScrollFixed > 0) {
|
||||||
|
document.documentElement.style.setProperty('--pbl-height', this.vScrollFixed + 'px');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -68,40 +68,58 @@
|
|||||||
[hasPadding]="false"
|
[hasPadding]="false"
|
||||||
[legendPosition]="isVotedPoll ? 'right' : 'top'"
|
[legendPosition]="isVotedPoll ? 'right' : 'top'"
|
||||||
></os-charts>
|
></os-charts>
|
||||||
|
|
||||||
<!-- Named Result -->
|
|
||||||
<div class="named-result-table" *ngIf="poll.type === 'named' && votesDataSource.data">
|
|
||||||
<h3>{{ 'Single votes' | translate }}</h3>
|
|
||||||
<mat-form-field>
|
|
||||||
<input matInput [(ngModel)]="votesDataSource.filter" placeholder="Filter" />
|
|
||||||
</mat-form-field>
|
|
||||||
<mat-table [dataSource]="votesDataSource">
|
|
||||||
<ng-container matColumnDef="users" sticky>
|
|
||||||
<mat-header-cell *matHeaderCellDef>{{ 'User' | translate }}</mat-header-cell>
|
|
||||||
<mat-cell *matCellDef="let row">
|
|
||||||
<div *ngIf="row.user">{{ row.user.getFullName() }}</div>
|
|
||||||
<div *ngIf="!row.user">{{ 'Unknown user' | translate }}</div>
|
|
||||||
</mat-cell>
|
|
||||||
</ng-container>
|
|
||||||
<ng-container
|
|
||||||
[matColumnDef]="'votes-' + option.user_id"
|
|
||||||
*ngFor="let option of poll.options"
|
|
||||||
sticky
|
|
||||||
>
|
|
||||||
<mat-header-cell *matHeaderCellDef>
|
|
||||||
<div *ngIf="option.user">{{ option.user.getFullName() }}</div>
|
|
||||||
<div *ngIf="!option.user">{{ 'Unknown user' | translate }}</div>
|
|
||||||
</mat-header-cell>
|
|
||||||
<mat-cell *matCellDef="let row">
|
|
||||||
{{ row.votes[option.user_id] }}
|
|
||||||
</mat-cell>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<mat-header-row *matHeaderRowDef="columnDefinitionPerName"></mat-header-row>
|
|
||||||
<mat-row *matRowDef="let row; columns: columnDefinitionPerName"></mat-row>
|
|
||||||
</mat-table>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Single Votes Table -->
|
||||||
|
<ng-container class="named-result-table" *ngIf="poll.type === 'named'">
|
||||||
|
<h3>{{ 'Single votes' | translate }}</h3>
|
||||||
|
<os-list-view-table
|
||||||
|
*ngIf="votesDataObservable"
|
||||||
|
[listObservable]="votesDataObservable"
|
||||||
|
[columns]="columnDefinitionSingleVotes"
|
||||||
|
[filterProps]="filterProps"
|
||||||
|
[allowProjector]="false"
|
||||||
|
[fullScreen]="false"
|
||||||
|
[vScrollFixed]="isVotedPoll ? -1 : 60"
|
||||||
|
listStorageKey="assignment-poll-vote"
|
||||||
|
[cssClasses]="{ 'single-votes-table': true }"
|
||||||
|
>
|
||||||
|
<!-- Header -->
|
||||||
|
<div *pblNgridHeaderCellDef="'user'; col as col">
|
||||||
|
{{ col.label | translate }}
|
||||||
|
</div>
|
||||||
|
<div *pblNgridHeaderCellDef="'*'; col as col">
|
||||||
|
{{ col.label | translate }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Content -->
|
||||||
|
<div *pblNgridCellDef="'user'; row as vote">
|
||||||
|
<b *ngIf="vote.user">{{ vote.user.getFullName() }}</b>
|
||||||
|
<b *ngIf="!vote.user">{{ 'Anonymous' | translate }}</b>
|
||||||
|
</div>
|
||||||
|
<!-- Y/N/(A) -->
|
||||||
|
<ng-container *ngIf="poll.pollmethod !== AssignmentPollMethods.Votes">
|
||||||
|
<ng-container *ngFor="let option of poll.options">
|
||||||
|
<div
|
||||||
|
*pblNgridCellDef="'votes-' + option.user_id; row as vote"
|
||||||
|
[ngClass]="voteOptionStyle[vote.votes[option.user_id].value].css"
|
||||||
|
class="vote-field"
|
||||||
|
>
|
||||||
|
<mat-icon> {{ voteOptionStyle[vote.votes[option.user_id].value].icon }}</mat-icon> {{ vote.votes[option.user_id].valueVerbose | translate }}
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
</ng-container>
|
||||||
|
<!-- Votes method -->
|
||||||
|
<ng-container *ngIf="poll.pollmethod === AssignmentPollMethods.Votes">
|
||||||
|
<div *pblNgridCellDef="'votes'; row as vote">
|
||||||
|
<div *ngFor="let candidate of vote.votes">{{ candidate }}</div>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
</os-list-view-table>
|
||||||
|
<div *ngIf="!votesDataObservable">
|
||||||
|
{{ 'The individual votes were anonymized.' | translate }}
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Meta Infos -->
|
<!-- Meta Infos -->
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
@import '~assets/styles/variables.scss';
|
@import '~assets/styles/variables.scss';
|
||||||
|
@import '~assets/styles/poll-colors.scss';
|
||||||
|
|
||||||
.result-wrapper {
|
.result-wrapper {
|
||||||
display: grid;
|
display: grid;
|
||||||
@ -38,3 +39,74 @@
|
|||||||
.poll-content {
|
.poll-content {
|
||||||
padding-top: 20px;
|
padding-top: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.chart-wrapper {
|
||||||
|
&.flex {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
.mat-table {
|
||||||
|
flex: 2;
|
||||||
|
.mat-column-votes {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.chart-inner-wrapper {
|
||||||
|
flex: 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.single-votes-table {
|
||||||
|
height: 500px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.openslides-theme .pbl-ngrid-row:hover {
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.openslides-theme os-list-view-table os-sort-filter-bar .custom-table-header {
|
||||||
|
&,
|
||||||
|
.action-buttons .input-container input {
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.voted-yes {
|
||||||
|
color: $votes-yes-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.voted-no {
|
||||||
|
color: $votes-no-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.voted-abstain {
|
||||||
|
color: $votes-abstain-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.openslides-theme .pbl-ngrid-no-data {
|
||||||
|
top: 10%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.openslides-theme .pbl-ngrid-header-cell:first-child {
|
||||||
|
& {
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: -1px;
|
||||||
|
height: 100%;
|
||||||
|
border-right: 1px solid #e0e0e0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.vote-field {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
padding-right: 12px;
|
||||||
|
}
|
||||||
|
@ -1,14 +1,16 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component, ViewEncapsulation } from '@angular/core';
|
||||||
import { MatSnackBar } from '@angular/material';
|
import { MatSnackBar } from '@angular/material';
|
||||||
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 { PblColumnDefinition } from '@pebula/ngrid';
|
||||||
|
|
||||||
import { OperatorService } from 'app/core/core-services/operator.service';
|
import { OperatorService } from 'app/core/core-services/operator.service';
|
||||||
import { AssignmentPollRepositoryService } from 'app/core/repositories/assignments/assignment-poll-repository.service';
|
import { AssignmentPollRepositoryService } from 'app/core/repositories/assignments/assignment-poll-repository.service';
|
||||||
import { GroupRepositoryService } from 'app/core/repositories/users/group-repository.service';
|
import { GroupRepositoryService } from 'app/core/repositories/users/group-repository.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 { ChartType } from 'app/shared/components/charts/charts.component';
|
import { ChartType } from 'app/shared/components/charts/charts.component';
|
||||||
import { AssignmentPollMethods } from 'app/shared/models/assignments/assignment-poll';
|
import { AssignmentPollMethods } from 'app/shared/models/assignments/assignment-poll';
|
||||||
import { BasePollDetailComponent } from 'app/site/polls/components/base-poll-detail.component';
|
import { BasePollDetailComponent } from 'app/site/polls/components/base-poll-detail.component';
|
||||||
@ -18,9 +20,16 @@ import { ViewAssignmentPoll } from '../../models/view-assignment-poll';
|
|||||||
@Component({
|
@Component({
|
||||||
selector: 'os-assignment-poll-detail',
|
selector: 'os-assignment-poll-detail',
|
||||||
templateUrl: './assignment-poll-detail.component.html',
|
templateUrl: './assignment-poll-detail.component.html',
|
||||||
styleUrls: ['./assignment-poll-detail.component.scss']
|
styleUrls: ['./assignment-poll-detail.component.scss'],
|
||||||
|
encapsulation: ViewEncapsulation.None
|
||||||
})
|
})
|
||||||
export class AssignmentPollDetailComponent extends BasePollDetailComponent<ViewAssignmentPoll> {
|
export class AssignmentPollDetailComponent extends BasePollDetailComponent<ViewAssignmentPoll> {
|
||||||
|
public AssignmentPollMethods = AssignmentPollMethods;
|
||||||
|
|
||||||
|
public columnDefinitionSingleVotes: PblColumnDefinition[];
|
||||||
|
|
||||||
|
public filterProps = ['user.getFullName'];
|
||||||
|
|
||||||
public isReady = false;
|
public isReady = false;
|
||||||
|
|
||||||
public candidatesLabels: string[] = [];
|
public candidatesLabels: string[] = [];
|
||||||
@ -40,9 +49,6 @@ export class AssignmentPollDetailComponent extends BasePollDetailComponent<ViewA
|
|||||||
}
|
}
|
||||||
return columns;
|
return columns;
|
||||||
}
|
}
|
||||||
|
|
||||||
public columnDefinitionPerName: string[];
|
|
||||||
|
|
||||||
private _chartType: ChartType = 'horizontalBar';
|
private _chartType: ChartType = 'horizontalBar';
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
@ -54,17 +60,45 @@ export class AssignmentPollDetailComponent extends BasePollDetailComponent<ViewA
|
|||||||
groupRepo: GroupRepositoryService,
|
groupRepo: GroupRepositoryService,
|
||||||
prompt: PromptService,
|
prompt: PromptService,
|
||||||
pollDialog: AssignmentPollDialogService,
|
pollDialog: AssignmentPollDialogService,
|
||||||
private operator: OperatorService
|
private operator: OperatorService,
|
||||||
|
private viewport: ViewportService
|
||||||
) {
|
) {
|
||||||
super(title, translate, matSnackbar, repo, route, groupRepo, prompt, pollDialog);
|
super(title, translate, matSnackbar, repo, route, groupRepo, prompt, pollDialog);
|
||||||
}
|
}
|
||||||
|
|
||||||
public onPollWithOptionsLoaded(): void {
|
public onPollWithOptionsLoaded(): void {
|
||||||
this.columnDefinitionPerName = ['users'].concat(this.poll.options.map(option => 'votes-' + option.user_id));
|
|
||||||
|
|
||||||
const votes = {};
|
const votes = {};
|
||||||
let i = -1;
|
let i = -1;
|
||||||
|
|
||||||
|
this.columnDefinitionSingleVotes = [
|
||||||
|
{
|
||||||
|
prop: 'user',
|
||||||
|
label: 'Participant',
|
||||||
|
width: '180px',
|
||||||
|
pin: this.viewport.isMobile ? undefined : 'start'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
if (this.isVotedPoll) {
|
||||||
|
this.columnDefinitionSingleVotes.push(this.getVoteColumnDefinition('votes', 'Votes'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* builds an object of the following form:
|
||||||
|
* {
|
||||||
|
* userId: {
|
||||||
|
* user: ViewUser,
|
||||||
|
* votes: { candidateId: voteValue } // for YN(A)
|
||||||
|
* | candidate_name[] // for Votes
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
*/
|
||||||
for (const option of this.poll.options) {
|
for (const option of this.poll.options) {
|
||||||
|
if (!this.isVotedPoll) {
|
||||||
|
this.columnDefinitionSingleVotes.push(
|
||||||
|
this.getVoteColumnDefinition('votes-' + option.user_id, option.user.getFullName())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
for (const vote of option.votes) {
|
for (const vote of option.votes) {
|
||||||
// if poll was pseudoanonymized, use a negative index to not interfere with
|
// if poll was pseudoanonymized, use a negative index to not interfere with
|
||||||
// possible named votes (although this should never happen)
|
// possible named votes (although this should never happen)
|
||||||
@ -72,11 +106,24 @@ export class AssignmentPollDetailComponent extends BasePollDetailComponent<ViewA
|
|||||||
if (!votes[userId]) {
|
if (!votes[userId]) {
|
||||||
votes[userId] = {
|
votes[userId] = {
|
||||||
user: vote.user,
|
user: vote.user,
|
||||||
votes: {}
|
votes: this.isVotedPoll ? [] : {}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
votes[userId].votes[option.user_id] =
|
// on votes method, we fill an array with all chosen candidates
|
||||||
this.poll.pollmethod === AssignmentPollMethods.Votes ? vote.weight : vote.valueVerbose;
|
// on YN(A) we map candidate ids to the vote
|
||||||
|
if (this.isVotedPoll) {
|
||||||
|
if (vote.weight > 0) {
|
||||||
|
if (vote.value === 'Y') {
|
||||||
|
votes[userId].votes.push(option.user.getFullName());
|
||||||
|
} else if (vote.value === 'N') {
|
||||||
|
votes[userId].votes.push(this.translate.instant('No'));
|
||||||
|
} else if (vote.value === 'A') {
|
||||||
|
votes[userId].votes.push(this.translate.instant('Abstain'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
votes[userId].votes[option.user_id] = vote;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -87,6 +134,15 @@ export class AssignmentPollDetailComponent extends BasePollDetailComponent<ViewA
|
|||||||
this.isReady = true;
|
this.isReady = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getVoteColumnDefinition(prop: string, label: string): PblColumnDefinition {
|
||||||
|
return {
|
||||||
|
prop: prop,
|
||||||
|
label: label,
|
||||||
|
minWidth: 80,
|
||||||
|
width: 'auto'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
protected initChartData(): void {
|
protected initChartData(): void {
|
||||||
if (this.isVotedPoll) {
|
if (this.isVotedPoll) {
|
||||||
this._chartType = 'doughnut';
|
this._chartType = 'doughnut';
|
||||||
|
@ -63,30 +63,38 @@
|
|||||||
<!-- Named table: only show if votes are present -->
|
<!-- Named table: only show if votes are present -->
|
||||||
<div class="named-result-table" *ngIf="poll.type === 'named'">
|
<div class="named-result-table" *ngIf="poll.type === 'named'">
|
||||||
<h3>{{ 'Single votes' | translate }}</h3>
|
<h3>{{ 'Single votes' | translate }}</h3>
|
||||||
<div *ngIf="votesDataSource.data">
|
<os-list-view-table
|
||||||
<mat-form-field>
|
[listObservable]="votesDataObservable"
|
||||||
<input matInput [(ngModel)]="votesDataSource.filter" placeholder="Filter" />
|
[columns]="columnDefinition"
|
||||||
</mat-form-field>
|
[filterProps]="filterProps"
|
||||||
<mat-table [dataSource]="votesDataSource">
|
[allowProjector]="false"
|
||||||
<ng-container matColumnDef="key" sticky>
|
[fullScreen]="false"
|
||||||
<mat-header-cell *matHeaderCellDef>{{ 'Participant' | translate }}</mat-header-cell>
|
[vScrollFixed]="60"
|
||||||
<mat-cell *matCellDef="let vote">
|
listStorageKey="motion-poll-vote"
|
||||||
<div *ngIf="vote.user">{{ vote.user.getFullName() }}</div>
|
[cssClasses]="{ 'single-votes-table': true }"
|
||||||
<div *ngIf="!vote.user">{{ 'Anonymous' | translate }}</div>
|
>
|
||||||
</mat-cell>
|
<!-- Header -->
|
||||||
</ng-container>
|
<div *pblNgridHeaderCellDef="'*'; col as col">
|
||||||
<ng-container matColumnDef="value" sticky>
|
{{ col.label | translate }}
|
||||||
<mat-header-cell *matHeaderCellDef>{{ 'Vote' | translate }}</mat-header-cell>
|
</div>
|
||||||
<mat-cell *matCellDef="let vote">{{ vote.valueVerbose }}</mat-cell>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<mat-header-row *matHeaderRowDef="columnDefinition"></mat-header-row>
|
<!-- Content -->
|
||||||
<mat-row *matRowDef="let vote; columns: columnDefinition"></mat-row>
|
<div *pblNgridCellDef="'user'; row as vote">
|
||||||
</mat-table>
|
<div *ngIf="vote.user">{{ vote.user.getFullName() }}</div>
|
||||||
</div>
|
<div *ngIf="!vote.user">{{ 'Anonymous' | translate }}</div>
|
||||||
<div *ngIf="!votesDataSource.data">
|
</div>
|
||||||
{{ 'The individual votes were made anonymous.' | translate }}
|
<div *pblNgridCellDef="'vote'; row as vote" class="vote-cell">
|
||||||
</div>
|
<div class="vote-cell-icon-container" [ngClass]="voteOptionStyle[vote.value].css">
|
||||||
|
<mat-icon>{{ voteOptionStyle[vote.value].icon }}</mat-icon>
|
||||||
|
</div>
|
||||||
|
<div>{{ vote.valueVerbose | translate }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- No Results -->
|
||||||
|
<div *pblNgridNoDataRef class="pbl-ngrid-no-data">
|
||||||
|
{{ 'The individual votes were made anonymous.' | translate }}
|
||||||
|
</div>
|
||||||
|
</os-list-view-table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
@import '~assets/styles/variables.scss';
|
@import '~assets/styles/variables.scss';
|
||||||
|
@import '~assets/styles/poll-colors.scss';
|
||||||
|
|
||||||
.poll-content {
|
.poll-content {
|
||||||
padding-top: 20px;
|
padding-top: 20px;
|
||||||
@ -56,4 +57,42 @@
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.vote-cell {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.vote-cell-icon-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-right: 7px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.single-votes-table {
|
||||||
|
height: 500px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.openslides-theme os-list-view-table os-sort-filter-bar .custom-table-header {
|
||||||
|
&,
|
||||||
|
.action-buttons .input-container input {
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.voted-yes {
|
||||||
|
color: $votes-yes-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.voted-no {
|
||||||
|
color: $votes-no-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.voted-abstain {
|
||||||
|
color: $votes-abstain-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.openslides-theme .pbl-ngrid-no-data {
|
||||||
|
top: 10%;
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
import { Component, OnInit } from '@angular/core';
|
import { Component, OnInit, ViewEncapsulation } from '@angular/core';
|
||||||
import { MatSnackBar } from '@angular/material';
|
import { MatSnackBar } from '@angular/material';
|
||||||
import { Title } from '@angular/platform-browser';
|
import { Title } from '@angular/platform-browser';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
import { PblColumnDefinition } from '@pebula/ngrid';
|
||||||
|
|
||||||
import { OperatorService } from 'app/core/core-services/operator.service';
|
import { OperatorService } from 'app/core/core-services/operator.service';
|
||||||
import { MotionPollRepositoryService } from 'app/core/repositories/motions/motion-poll-repository.service';
|
import { MotionPollRepositoryService } from 'app/core/repositories/motions/motion-poll-repository.service';
|
||||||
@ -18,11 +19,24 @@ import { BasePollDetailComponent } from 'app/site/polls/components/base-poll-det
|
|||||||
@Component({
|
@Component({
|
||||||
selector: 'os-motion-poll-detail',
|
selector: 'os-motion-poll-detail',
|
||||||
templateUrl: './motion-poll-detail.component.html',
|
templateUrl: './motion-poll-detail.component.html',
|
||||||
styleUrls: ['./motion-poll-detail.component.scss']
|
styleUrls: ['./motion-poll-detail.component.scss'],
|
||||||
|
encapsulation: ViewEncapsulation.None
|
||||||
})
|
})
|
||||||
export class MotionPollDetailComponent extends BasePollDetailComponent<ViewMotionPoll> implements OnInit {
|
export class MotionPollDetailComponent extends BasePollDetailComponent<ViewMotionPoll> implements OnInit {
|
||||||
public motion: ViewMotion;
|
public motion: ViewMotion;
|
||||||
public columnDefinition = ['key', 'value'];
|
public columnDefinition: PblColumnDefinition[] = [
|
||||||
|
{
|
||||||
|
prop: 'user',
|
||||||
|
width: 'auto',
|
||||||
|
label: 'Participant'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: 'vote',
|
||||||
|
width: 'auto',
|
||||||
|
label: 'Vote'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
public filterProps = ['user.getFullName', 'valueVerbose'];
|
||||||
|
|
||||||
public set chartType(type: ChartType) {
|
public set chartType(type: ChartType) {
|
||||||
this._chartType = type;
|
this._chartType = type;
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import { OnInit } from '@angular/core';
|
import { OnInit } from '@angular/core';
|
||||||
import { MatSnackBar, MatTableDataSource } from '@angular/material';
|
import { MatSnackBar } from '@angular/material';
|
||||||
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 { Label } from 'ng2-charts';
|
import { Label } from 'ng2-charts';
|
||||||
import { BehaviorSubject, Observable } from 'rxjs';
|
import { BehaviorSubject, from, Observable } from 'rxjs';
|
||||||
|
|
||||||
import { GroupRepositoryService } from 'app/core/repositories/users/group-repository.service';
|
import { GroupRepositoryService } from 'app/core/repositories/users/group-repository.service';
|
||||||
import { BasePollDialogService } from 'app/core/ui-services/base-poll-dialog.service';
|
import { BasePollDialogService } from 'app/core/ui-services/base-poll-dialog.service';
|
||||||
@ -34,6 +34,24 @@ export abstract class BasePollDetailComponent<V extends ViewBasePoll> extends Ba
|
|||||||
*/
|
*/
|
||||||
public groupObservable: Observable<ViewGroup[]> = null;
|
public groupObservable: Observable<ViewGroup[]> = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Details for the iconification of the votes
|
||||||
|
*/
|
||||||
|
public voteOptionStyle = {
|
||||||
|
Y: {
|
||||||
|
css: 'voted-yes',
|
||||||
|
icon: 'thumb_up'
|
||||||
|
},
|
||||||
|
N: {
|
||||||
|
css: 'voted-no',
|
||||||
|
icon: 'thumb_down'
|
||||||
|
},
|
||||||
|
A: {
|
||||||
|
css: 'voted-abstain',
|
||||||
|
icon: 'trip_origin'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The reference to the poll.
|
* The reference to the poll.
|
||||||
*/
|
*/
|
||||||
@ -59,8 +77,8 @@ export abstract class BasePollDetailComponent<V extends ViewBasePoll> extends Ba
|
|||||||
*/
|
*/
|
||||||
public chartDataSubject: BehaviorSubject<ChartData> = new BehaviorSubject(null);
|
public chartDataSubject: BehaviorSubject<ChartData> = new BehaviorSubject(null);
|
||||||
|
|
||||||
// The datasource for the votes-per-user table
|
// The observable for the votes-per-user table
|
||||||
public votesDataSource: MatTableDataSource<BaseVoteData> = new MatTableDataSource();
|
public votesDataObservable: Observable<BaseVoteData[]>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor
|
* Constructor
|
||||||
@ -88,7 +106,6 @@ export abstract class BasePollDetailComponent<V extends ViewBasePoll> extends Ba
|
|||||||
protected pollDialog: BasePollDialogService<V>
|
protected pollDialog: BasePollDialogService<V>
|
||||||
) {
|
) {
|
||||||
super(title, translate, matSnackbar);
|
super(title, translate, matSnackbar);
|
||||||
this.votesDataSource.filterPredicate = this.dataSourceFilterPredicate;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -141,26 +158,14 @@ export abstract class BasePollDetailComponent<V extends ViewBasePoll> extends Ba
|
|||||||
|
|
||||||
protected abstract hasPerms(): boolean;
|
protected abstract hasPerms(): boolean;
|
||||||
|
|
||||||
// custom filter for the data source: only search in usernames
|
|
||||||
protected dataSourceFilterPredicate(data: BaseVoteData, filter: string): boolean {
|
|
||||||
return (
|
|
||||||
data.user &&
|
|
||||||
data.user
|
|
||||||
.getFullName()
|
|
||||||
.trim()
|
|
||||||
.toLowerCase()
|
|
||||||
.indexOf(filter.trim().toLowerCase()) !== -1
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* sets the votes data only if the poll wasn't pseudoanonymized
|
* sets the votes data only if the poll wasn't pseudoanonymized
|
||||||
*/
|
*/
|
||||||
protected setVotesData(data: BaseVoteData[]): void {
|
protected setVotesData(data: BaseVoteData[]): void {
|
||||||
if (data.every(voteDate => !voteDate.user)) {
|
if (data.every(voteDate => !voteDate.user)) {
|
||||||
this.votesDataSource.data = null;
|
this.votesDataObservable = null;
|
||||||
} else {
|
} else {
|
||||||
this.votesDataSource.data = data;
|
this.votesDataObservable = from([data]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
<span> {{ 'Casted votes' }}: {{ poll.voted_id.length }} / {{ max }} </span>
|
<span> {{ 'Cast votes' }}: {{ poll.voted_id.length }} / {{ max }} </span>
|
||||||
|
|
||||||
<mat-progress-bar class="voting-progress-bar" [value]="valueInPercent"></mat-progress-bar>
|
<mat-progress-bar class="voting-progress-bar" [value]="valueInPercent"></mat-progress-bar>
|
||||||
|
@ -158,6 +158,10 @@
|
|||||||
background-color: rgba(0, 0, 0, 0.025);
|
background-color: rgba(0, 0, 0, 0.025);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pbl-ngrid-header-row, .pbl-ngrid-row {
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
.mat-progress-bar-buffer {
|
.mat-progress-bar-buffer {
|
||||||
background-color: mat-color($background, card) !important;
|
background-color: mat-color($background, card) !important;
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user