Enhance table layouts

Enhance the result table layout for assignments
This commit is contained in:
Sean Engelhardt 2020-02-27 17:08:36 +01:00 committed by FinnStutzenstein
parent 9d7028ea5f
commit 53b9ce73f2
9 changed files with 159 additions and 182 deletions

View File

@ -215,6 +215,10 @@ export class ChartsComponent extends BaseViewComponent {
*/
@Input()
public pieChartOptions: ChartOptions = {
responsive: true,
legend: {
position: 'left'
},
aspectRatio: 1
};

View File

@ -194,6 +194,12 @@ export class ListViewTableComponent<V extends BaseViewModel, M extends BaseModel
@Input()
public showListOfSpeakers = true;
/**
* To optionally hide the menu slot
*/
@Input()
public showMenu = true;
/**
* Fix value for the height of the rows in the virtual-scroll-list.
*/
@ -347,7 +353,7 @@ export class ListViewTableComponent<V extends BaseViewModel, M extends BaseModel
hidden.push('selection');
}
if (!this.alwaysShowMenu && !this.isMobile) {
if ((!this.alwaysShowMenu && !this.isMobile) || !this.showMenu) {
hidden.push('menu');
}

View File

@ -38,6 +38,18 @@ export class AssignmentPoll extends BasePoll<
public global_abstain: boolean;
public description: string;
public get isMethodY(): boolean {
return this.pollmethod === AssignmentPollMethod.Votes;
}
public get isMethodYN(): boolean {
return this.pollmethod === AssignmentPollMethod.YN;
}
public get isMethodYNA(): boolean {
return this.pollmethod === AssignmentPollMethod.YNA;
}
public get pollmethodFields(): CalculablePollKey[] {
if (this.pollmethod === AssignmentPollMethod.YN) {
return ['yes', 'no'];

View File

@ -16,20 +16,29 @@
<!-- Detailview for poll -->
<ng-template #viewTemplate>
<ng-container *ngIf="isReady">
<div *ngIf="isReady">
<h1>{{ poll.title }}</h1>
<span *ngIf="poll.type !== 'analog'">{{ poll.typeVerbose | translate }}</span>
<div *ngIf="poll.stateHasVotes">
<div [class]="chartType === 'horizontalBar' ? 'result-wrapper-bar-chart' : 'result-wrapper-pie-chart'">
<div class="assignment-result-wrapper">
<!-- Result Table -->
<table class="assignment-result-table">
<tbody>
<tr>
<th translate>Candidates</th>
<th translate>Votes</th>
<th>
<span *ngIf="!poll.isMethodY" translate>
Yes
</span>
<span *ngIf="poll.isMethodY" translate>
Votes
</span>
</th>
<th translate *ngIf="!poll.isMethodY">No</th>
<th translate *ngIf="poll.isMethodYNA">Abstain</th>
</tr>
<tr *ngFor="let row of poll.tableData">
<tr *ngFor="let row of poll.tableData" [class]="row.class">
<td>
<span>
{{ row.votingOption | pollKeyVerbose | translate }}
@ -39,23 +48,14 @@
{{ row.votingOptionSubtitle }}
</span>
</td>
<td>
<div *ngFor="let vote of row.value">
<div class="single-result" *ngIf="voteFitsMethod(vote)">
<os-icon-container *ngIf="vote.icon" [icon]="vote.icon">
{{ vote.vote | pollKeyVerbose | translate }}
</os-icon-container>
<span *ngIf="!vote.icon">
{{ vote.vote | pollKeyVerbose | translate }}
<td *ngFor="let vote of row.value">
<div class="single-result" *ngIf="vote && voteFitsMethod(vote)">
<span>
{{ vote.amount | parsePollNumber }}
<span *ngIf="vote.showPercent">
{{ vote.amount | pollPercentBase: poll }}
</span>
<span>
{{ vote.amount | parsePollNumber }}
<span *ngIf="vote.showPercent">
{{ vote.amount | pollPercentBase: poll }}
</span>
</span>
</div>
</span>
</div>
</td>
</tr>
@ -71,12 +71,12 @@
[labels]="candidatesLabels"
[data]="chartDataSubject"
[hasPadding]="false"
[legendPosition]="isVotedPoll ? 'right' : 'top'"
legendPosition="right"
></os-charts>
</div>
<!-- Single Votes Table -->
<ng-container class="named-result-table" *ngIf="poll.type === 'named'">
<div class="named-result-table" *ngIf="poll.type === 'named'">
<h3>{{ 'Single votes' | translate }}</h3>
<os-list-view-table
*ngIf="votesDataObservable"
@ -85,47 +85,41 @@
[filterProps]="filterProps"
[allowProjector]="false"
[fullScreen]="false"
[vScrollFixed]="isVotedPoll ? -1 : 60"
[vScrollFixed]="-1"
listStorageKey="assignment-poll-vote"
[showListOfSpeakers]="false"
[showMenu]="false"
[cssClasses]="{ 'single-votes-table': true }"
>
<!-- Header -->
<div *pblNgridHeaderCellDef="'user'; col as col">
{{ col.label | translate }}
</div>
<div *pblNgridHeaderCellDef="'*'; col as col">
<div *pblNgridHeaderCellDef="'votes'; 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 !== AssignmentPollMethod.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 *ngIf="vote.user">
{{ vote.user.getShortName() }}
<div class="user-subtitle" *ngIf="vote.user.getLevelAndNumber()">
{{ vote.user.getLevelAndNumber() }}
</div>
</ng-container>
</ng-container>
<!-- Votes method -->
<ng-container *ngIf="poll.pollmethod === AssignmentPollMethod.Votes">
<div *pblNgridCellDef="'votes'; row as vote">
<div *ngFor="let candidate of vote.votes">{{ candidate }}</div>
</div>
</ng-container>
<div *ngIf="!vote.user">
{{ 'Anonymous' | translate }}
</div>
</div>
<div *pblNgridCellDef="'votes'; row as vote" >
<div class="single-vote-result" *ngFor="let candidate of vote.votes">{{ candidate }}</div>
</div>
</os-list-view-table>
<div *ngIf="!votesDataObservable">
{{ 'The individual votes were anonymized.' | translate }}
</div>
</ng-container>
</div>
</div>
<!-- Meta Infos -->
@ -142,7 +136,7 @@
{{ '100% base' | translate }}: {{ poll.percentBaseVerbose | translate }}
</small>
</div>
</ng-container>
</div>
</ng-template>
<!-- More Menu -->

View File

@ -1,73 +1,69 @@
@import '~assets/styles/variables.scss';
@import '~assets/styles/poll-colors.scss';
%assignment-result-wrapper {
.assignment-result-wrapper {
margin-top: 2em;
display: grid;
grid-gap: 10px;
}
.result-wrapper-bar-chart {
@extend %assignment-result-wrapper;
grid-template-areas:
'results'
'chart'
'names';
}
.assignment-result-table {
border-collapse: collapse;
.result-wrapper-pie-chart {
@extend %assignment-result-wrapper;
grid-template-areas:
'chart'
'results'
'names';
}
th {
text-align: left;
font-weight: initial;
}
@include desktop {
.result-wrapper-pie-chart {
grid-template-areas:
'results chart'
'names names';
grid-template-columns: 2fr 1fr;
tr {
height: 48px;
}
tr:last-child {
border-bottom: none;
}
tr.sums {
border-bottom: none;
td {
padding-top: 1em;
padding-bottom: 1em;
}
}
.user + .sums {
td {
padding-top: 4em;
}
}
}
.pie-chart {
margin-left: auto;
margin-right: auto;
width: 50%;
}
}
.assignment-result-table {
grid-area: results;
th {
text-align: left;
font-weight: initial;
}
tr {
height: 48px;
}
tr:last-child {
border-bottom: none;
}
.single-result {
display: flex;
}
}
.assignment-result-chart {
grid-area: chart;
}
.pie-chart {
max-width: 300px;
margin-left: auto;
margin-right: auto;
.single-vote-result + .single-vote-result {
margin-top: 1em;
}
.named-result-table {
grid-area: names;
.mat-form-field {
font-size: 14px;
width: 100%;
}
.single-votes-table {
display: block;
height: 500px;
}
.vote-field {
text-align: center;
width: 100%;
padding-right: 12px;
}
}
.assignment-poll-meta {
@ -76,22 +72,6 @@
padding-top: 20px;
}
.single-votes-table {
display: block;
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;
}
@ -104,6 +84,18 @@
color: $votes-abstain-color;
}
// theme
.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;
}
}
.openslides-theme .pbl-ngrid-no-data {
top: 10%;
}
@ -123,11 +115,3 @@
border-right: 1px solid #e0e0e0;
}
}
.vote-field {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
padding-right: 12px;
}

View File

@ -11,9 +11,7 @@ import { AssignmentPollRepositoryService } from 'app/core/repositories/assignmen
import { AssignmentVoteRepositoryService } from 'app/core/repositories/assignments/assignment-vote-repository.service';
import { GroupRepositoryService } from 'app/core/repositories/users/group-repository.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 { AssignmentPollMethod } from 'app/shared/models/assignments/assignment-poll';
import { BasePollDetailComponent } from 'app/site/polls/components/base-poll-detail.component';
import { VotingResult } from 'app/site/polls/models/view-base-poll';
import { PollService } from 'app/site/polls/services/poll.service';
@ -27,8 +25,6 @@ import { ViewAssignmentPoll } from '../../models/view-assignment-poll';
encapsulation: ViewEncapsulation.None
})
export class AssignmentPollDetailComponent extends BasePollDetailComponent<ViewAssignmentPoll> {
public AssignmentPollMethod = AssignmentPollMethod;
public columnDefinitionSingleVotes: PblColumnDefinition[];
public filterProps = ['user.getFullName'];
@ -41,10 +37,6 @@ export class AssignmentPollDetailComponent extends BasePollDetailComponent<ViewA
return this._chartType;
}
public get isVotedPoll(): boolean {
return this.poll.pollmethod === AssignmentPollMethod.Votes;
}
private _chartType: ChartType = 'horizontalBar';
public constructor(
@ -58,67 +50,48 @@ export class AssignmentPollDetailComponent extends BasePollDetailComponent<ViewA
pollDialog: AssignmentPollDialogService,
pollService: PollService,
votesRepo: AssignmentVoteRepositoryService,
private operator: OperatorService,
private viewport: ViewportService
private operator: OperatorService
) {
super(title, translate, matSnackbar, repo, route, groupRepo, prompt, pollDialog, pollService, votesRepo);
}
protected createVotesData(): void {
const votes = {};
let i = -1;
const definitions: PblColumnDefinition[] = [
{
prop: 'user',
label: 'Participant',
width: '180px',
pin: this.viewport.isMobile ? undefined : 'start'
width: '40%',
minWidth: 300
},
{
prop: 'votes',
label: 'Votes',
width: '60%',
minWidth: 300
}
];
if (this.isVotedPoll) {
definitions.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) {
if (!this.isVotedPoll) {
definitions.push(this.getVoteColumnDefinition('votes-' + option.user_id, option.user.getFullName()));
}
for (const vote of option.votes) {
// if poll was pseudoanonymized, use a negative index to not interfere with
// possible named votes (although this should never happen)
const userId = vote.user_id || --i;
const userId = vote.user_id;
if (!votes[userId]) {
votes[userId] = {
user: vote.user,
votes: this.isVotedPoll ? [] : {}
votes: []
};
}
// on votes method, we fill an array with all chosen candidates
// on YN(A) we map candidate ids to the vote
if (this.isVotedPoll) {
if (vote.weight > 0) {
if (vote.weight > 0) {
if (this.poll.isMethodY) {
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.push(this.voteValueToLabel(vote.value));
}
} else {
votes[userId].votes.push(`${option.user.getShortName()}: ${this.voteValueToLabel(vote.value)}`);
}
} else {
votes[userId].votes[option.user_id] = vote;
}
}
}
@ -129,17 +102,20 @@ export class AssignmentPollDetailComponent extends BasePollDetailComponent<ViewA
this.isReady = true;
}
private getVoteColumnDefinition(prop: string, label: string): PblColumnDefinition {
return {
prop: prop,
label: label,
minWidth: 80,
width: 'auto'
};
private voteValueToLabel(vote: 'Y' | 'N' | 'A'): string {
if (vote === 'Y') {
return this.translate.instant('Yes');
} else if (vote === 'N') {
return this.translate.instant('No');
} else if (vote === 'A') {
return this.translate.instant('Abstain');
} else {
throw new Error(`voteValueToLabel received illegal arguments: ${vote}`);
}
}
protected initChartData(): void {
if (this.isVotedPoll) {
if (this.poll.isMethodY) {
this._chartType = 'doughnut';
this.chartDataSubject.next(this.pollService.generateCircleChartData(this.poll));
} else {
@ -152,11 +128,11 @@ export class AssignmentPollDetailComponent extends BasePollDetailComponent<ViewA
}
public voteFitsMethod(result: VotingResult): boolean {
if (this.poll.pollmethod === AssignmentPollMethod.Votes) {
if (this.poll.isMethodY) {
if (result.vote === 'abstain' || result.vote === 'no') {
return false;
}
} else if (this.poll.pollmethod === AssignmentPollMethod.YN) {
} else if (this.poll.isMethodYN) {
if (result.vote === 'abstain') {
return false;
}

View File

@ -68,7 +68,7 @@ export class ViewAssignmentPoll extends ViewBasePoll<AssignmentPoll, AssignmentP
const tableData: PollTableData[] = this.options.map(candidate => ({
votingOption: candidate.user.short_name,
votingOptionSubtitle: candidate.user.getLevelAndNumber(),
class: 'user',
value: this.voteTableKeys.map(
key =>
({
@ -84,6 +84,7 @@ export class ViewAssignmentPoll extends ViewBasePoll<AssignmentPoll, AssignmentP
tableData.push(
...this.sumTableKeys.map(key => ({
votingOption: key.vote,
class: 'sums',
value: [
{
amount: this[key.vote],
@ -93,7 +94,6 @@ export class ViewAssignmentPoll extends ViewBasePoll<AssignmentPoll, AssignmentP
]
}))
);
return tableData;
}
}

View File

@ -4,7 +4,7 @@ import { E2EImportsModule } from 'e2e-imports.module';
import { PollFormComponent } from './poll-form.component';
fdescribe('PollFormComponent', () => {
describe('PollFormComponent', () => {
let component: PollFormComponent<any>;
let fixture: ComponentFixture<PollFormComponent<any>>;

View File

@ -17,6 +17,7 @@ export enum PollClassType {
export interface PollTableData {
votingOption: string;
votingOptionSubtitle?: string;
class?: string;
value: VotingResult[];
}
@ -162,10 +163,10 @@ export abstract class ViewBasePoll<
public abstract get percentBaseVerbose(): string;
public get showAbstainPercent(): boolean {
return this.onehundred_percent_base === PercentBase.YNA;
return this.poll.onehundred_percent_base === PercentBase.YNA;
}
public abstract readonly pollClassType: PollClassType;
public abstract readonly pollClassType: 'motion' | 'assignment';
public canBeVotedFor: () => boolean;