Rework Assignment UI

Reworks the Assignment UI to fit to the current MockUps
This commit is contained in:
Sean Engelhardt 2019-05-09 17:04:36 +02:00 committed by Emanuel Schütze
parent 0ed00ff603
commit 654c54e9ab
7 changed files with 238 additions and 167 deletions

View File

@ -51,56 +51,50 @@
</os-head-bar> </os-head-bar>
<div class="content-container"> <div class="content-container">
<ng-container *ngIf="vp.isMobile; then mobileView; else desktopView"></ng-container> <div *ngIf="editAssignment">
<ng-container [ngTemplateOutlet]="assignmentFormTemplate"></ng-container>
</div>
<div *ngIf="!editAssignment">
<ng-container [ngTemplateOutlet]="metaInfoTemplate"></ng-container>
<ng-container [ngTemplateOutlet]="contentTemplate"></ng-container>
</div>
</div> </div>
<ng-template #mobileView>
<div *ngIf="editAssignment">
<ng-container [ngTemplateOutlet]="assignmentFormTemplate"></ng-container>
</div>
<div *ngIf="!editAssignment">
<ng-container [ngTemplateOutlet]="metaInfoTemplate"></ng-container>
<ng-container [ngTemplateOutlet]="contentTemplate"></ng-container>
</div>
</ng-template>
<ng-template #desktopView>
<div *ngIf="editAssignment">
<ng-container [ngTemplateOutlet]="assignmentFormTemplate"></ng-container>
</div>
<div *ngIf="!editAssignment">
<ng-container [ngTemplateOutlet]="metaInfoTemplate"></ng-container>
<ng-container [ngTemplateOutlet]="contentTemplate"></ng-container>
</div>
</ng-template>
<ng-template #metaInfoTemplate> <ng-template #metaInfoTemplate>
<mat-card class="os-card" *ngIf="assignment"> <mat-card class="os-card " *ngIf="assignment">
<h1>{{ assignment.getTitle() }}</h1> <h1>{{ assignment.getTitle() }}</h1>
<div *ngIf="assignment"> <div *ngIf="assignment">
<div *ngIf="assignment.assignment.description" [innerHTML]="assignment.assignment.description"></div> <div *ngIf="assignment.assignment.description" [innerHTML]="assignment.assignment.description"></div>
</div> </div>
<div> <div class="meta-info-grid">
<span translate>Number of persons to be elected</span>:&nbsp; <div class="number-of-elected">
<span>{{ assignment.assignment.open_posts }}</span> <h4 translate>Number of persons to be elected</h4>
</div> <span>{{ assignment.assignment.open_posts }}</span>
<div> </div>
<span translate>Phase</span>:&nbsp; <div class="current-phase">
<mat-basic-chip *ngIf="hasPerms('manage')" [matMenuTriggerFor]="phaseMenu" class="bluegrey" disableRipple> <h4 translate>Phase</h4>
{{ assignment.phaseString | translate }} <mat-basic-chip
</mat-basic-chip> *ngIf="hasPerms('manage')"
<mat-basic-chip *ngIf="!hasPerms('manage')" class="bluegrey" disableRipple> [matMenuTriggerFor]="phaseMenu"
{{ assignment.phaseString | translate }} class="bluegrey"
</mat-basic-chip> disableRipple
<mat-menu #phaseMenu="matMenu"> >
<button *ngFor="let option of phaseOptions" mat-menu-item (click)="onSetPhaseButton(option.value)"> {{ assignment.phaseString | translate }}
{{ option.display_name | translate }} </mat-basic-chip>
</button> <mat-basic-chip *ngIf="!hasPerms('manage')" class="bluegrey" disableRipple>
</mat-menu> {{ assignment.phaseString | translate }}
</mat-basic-chip>
<mat-menu #phaseMenu="matMenu">
<button *ngFor="let option of phaseOptions" mat-menu-item (click)="onSetPhaseButton(option.value)">
{{ option.display_name | translate }}
</button>
</mat-menu>
</div>
</div> </div>
<div *ngIf="assignment.attachments.length"> <div *ngIf="assignment.attachments.length">
<span translate>Election documents</span>: <h4 translate>Election documents</h4>
<mat-list dense> <mat-list dense class="election-document-list">
<mat-list-item *ngFor="let file of assignment.attachments"> <mat-list-item *ngFor="let file of assignment.attachments">
<a [routerLink]="file.downloadUrl" target="_blank">{{ file.getTitle() }}</a> <a [routerLink]="file.downloadUrl" target="_blank">{{ file.getTitle() }}</a>
</mat-list-item> </mat-list-item>
@ -120,7 +114,20 @@
</ng-template> </ng-template>
<!-- poll template --> <!-- poll template -->
<ng-template #pollTemplate> <ng-template #pollTemplate
>
<div class="ballot-controls-grid">
<div class="ballot-title" *ngIf="assignment && assignment.polls && assignment.polls.length">
<h3 translate>Election result</h3>
</div>
<div class="ballot-button" *ngIf="assignment && hasPerms('createPoll')">
<button mat-button (click)="createPoll()">
<mat-icon color="primary">poll</mat-icon>
<span translate>New ballot</span>
</button>
</div>
</div>
<mat-tab-group <mat-tab-group
(selectedTabChange)="onTabChange()" (selectedTabChange)="onTabChange()"
*ngIf="assignment && assignment.polls && assignment.polls.length" *ngIf="assignment && assignment.polls && assignment.polls.length"
@ -136,72 +143,75 @@
</ng-template> </ng-template>
<ng-template #candidatesTemplate> <ng-template #candidatesTemplate>
<!-- candidates --> <!-- Candidates -->
<div <h3 translate>Candidates</h3>
class="candidates-list"
*ngIf="assignment && assignment.assignmentRelatedUsers && assignment.assignmentRelatedUsers.length > 0" <!-- Candidate List -->
> <div>
<os-sorting-list <div
[input]="assignment.assignmentRelatedUsers" class="candidates-list"
[live]="true" *ngIf="assignment && assignment.assignmentRelatedUsers && assignment.assignmentRelatedUsers.length > 0"
[count]="true"
[enable]="hasPerms('addOthers')"
(sortEvent)="onSortingChange($event)"
> >
<!-- implicit item references into the component using ng-template slot --> <os-sorting-list
<ng-template let-item> [input]="assignment.assignmentRelatedUsers"
<span *ngIf="hasPerms('addOthers')"> [live]="true"
<button [count]="true"
mat-icon-button [enable]="hasPerms('addOthers')"
matTooltip="{{ 'Remove candidate' | translate }}" (sortEvent)="onSortingChange($event)"
(click)="removeUser(item)" >
> <!-- implicit item references into the component using ng-template slot -->
<mat-icon>clear</mat-icon> <ng-template let-item>
<span *ngIf="hasPerms('addOthers')">
<button
mat-icon-button
matTooltip="{{ 'Remove candidate' | translate }}"
(click)="removeUser(item)"
>
<mat-icon>clear</mat-icon>
</button>
</span>
</ng-template>
</os-sorting-list>
</div>
<div class="add-candidates">
<!-- Search for candidates -->
<div class="search-field" *ngIf="hasPerms('addOthers')">
<form
*ngIf="hasPerms('addOthers') && filteredCandidates && filteredCandidates.value.length > 0"
[formGroup]="candidatesForm"
>
<os-search-value-selector
class="search-bar"
ngDefaultControl
[form]="candidatesForm"
[formControl]="candidatesForm.get('userId')"
[multiple]="false"
listname="{{ 'Select a new candidate' | translate }}"
[InputListValues]="filteredCandidates"
></os-search-value-selector>
</form>
</div>
<!-- Add me and remove me if OP has correct permission -->
<div *ngIf="assignment && hasPerms('addSelf') && assignment.candidates">
<div>
<button mat-button color="accent" (click)="addSelf()" *ngIf="!isSelfCandidate">
<mat-icon>add</mat-icon>
<span translate>Add me</span>
</button> </button>
</span> <button mat-button color="accent" (click)="removeSelf()" *ngIf="isSelfCandidate">
</ng-template> <mat-icon>remove</mat-icon>
</os-sorting-list> <span translate>Remove me</span>
</div> </button>
</div>
<!-- Search for candidates --> </div>
<div class="search-field" *ngIf="hasPerms('addOthers')">
<form
*ngIf="hasPerms('addOthers') && filteredCandidates && filteredCandidates.value.length > 0"
[formGroup]="candidatesForm"
>
<os-search-value-selector
class="search-bar"
ngDefaultControl
[form]="candidatesForm"
[formControl]="candidatesForm.get('userId')"
[multiple]="false"
listname="{{ 'Select a new candidate' | translate }}"
[InputListValues]="filteredCandidates"
></os-search-value-selector>
</form>
</div>
<!-- Add me and remove me if OP has correct permission -->
<div *ngIf="assignment && hasPerms('addSelf') && assignment.candidates" class="add-self-buttons">
<div>
<button mat-stroked-button (click)="addSelf()" *ngIf="!isSelfCandidate">
<mat-icon>add</mat-icon>
<span translate>Add me</span>
</button>
<button mat-stroked-button (click)="removeSelf()" *ngIf="isSelfCandidate">
<mat-icon>remove</mat-icon>
<span translate>Remove me</span>
</button>
</div> </div>
</div> </div>
<div *ngIf="assignment && hasPerms('createPoll')"> <mat-divider *ngIf="assignment && assignment.polls && assignment.polls.length" class="candidate-list-separator"></mat-divider>
<button mat-button (click)="createPoll()">
<mat-icon color="primary">poll</mat-icon>
<span translate>New ballot</span>
</button>
</div>
</ng-template> </ng-template>
<!-- Form -->
<ng-template #assignmentFormTemplate> <ng-template #assignmentFormTemplate>
<mat-card class="os-form-card"> <mat-card class="os-form-card">
<form <form

View File

@ -2,9 +2,85 @@
width: 100%; width: 100%;
} }
.candidates { .meta-info-grid {
width: 60%; display: grid;
grid-column-gap: 30px;
grid-template-columns: 1fr 2fr;
.number-of-elected {
grid-column: 1;
}
.current-phase {
grid-column: 2;
}
// will display the container under the one declared above on small screen
@media only screen and (max-width: 960px) {
grid-template-columns: 1fr;
.current-phase {
grid-column: 1;
}
}
} }
.election-document-list mat-list-item {
height: 20px;
}
.candidates-list {
padding-top: 10px;
}
.add-candidates {
.search-field {
padding-top: 15px;
width: auto;
.search-bar {
display: grid;
.mat-form-field {
width: 100%;
}
}
}
}
@media only screen and (max-width: 960px) {
.candidates-list {
// TODO: same as .waiting-list in list-of-speakers
padding: 10px 25px 0 25px;
}
.add-candidates {
.search-field {
padding: 15px 25px 0 25px;
}
}
}
.ballot-controls-grid {
display: grid;
grid-column-gap: 10px;
grid-template-columns: auto min-content;
.ballot-title {
grid-column: 1;
h3 {
margin: 0;
}
}
.ballot-button {
grid-column: 2;
}
}
.candidate-list-separator {
position: initial !important;
margin: 20px 0;
}
.candidate { .candidate {
display: flex; display: flex;
width: 100%; width: 100%;
@ -21,24 +97,3 @@
top: 0; top: 0;
left: 0; left: 0;
} }
// TODO: same as .waiting-list in list-of-speakers
.candidates-list {
padding: 10px 25px 0 25px;
}
// TODO: duplicate in list-of-speakers
.add-self-buttons {
padding: 15px 0 20px 25px;
}
.search-field {
margin: 20px 0;
.search-bar {
display: grid;
.mat-form-field {
width: 100%;
}
}
}

View File

@ -22,7 +22,6 @@ import { UserRepositoryService } from 'app/core/repositories/users/user-reposito
import { ViewAssignment, AssignmentPhases } from '../../models/view-assignment'; import { ViewAssignment, AssignmentPhases } from '../../models/view-assignment';
import { ViewAssignmentRelatedUser } from '../../models/view-assignment-related-user'; import { ViewAssignmentRelatedUser } from '../../models/view-assignment-related-user';
import { ViewItem } from 'app/site/agenda/models/view-item'; import { ViewItem } from 'app/site/agenda/models/view-item';
import { ViewportService } from 'app/core/ui-services/viewport.service';
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 { ViewMediafile } from 'app/site/mediafiles/models/view-mediafile'; import { ViewMediafile } from 'app/site/mediafiles/models/view-mediafile';
@ -159,7 +158,6 @@ export class AssignmentDetailComponent extends BaseViewComponent implements OnIn
* @param title * @param title
* @param translate * @param translate
* @param matSnackBar * @param matSnackBar
* @param vp
* @param operator * @param operator
* @param perms * @param perms
* @param router * @param router
@ -176,7 +174,6 @@ export class AssignmentDetailComponent extends BaseViewComponent implements OnIn
title: Title, title: Title,
protected translate: TranslateService, // protected required for ng-translate-extract protected translate: TranslateService, // protected required for ng-translate-extract
matSnackBar: MatSnackBar, matSnackBar: MatSnackBar,
public vp: ViewportService,
private operator: OperatorService, private operator: OperatorService,
public perms: LocalPermissionsService, public perms: LocalPermissionsService,
private router: Router, private router: Router,

View File

@ -1,5 +1,5 @@
<mat-card class="os-card" *ngIf="poll"> <div class="poll-wrapper" *ngIf="poll">
<div class="flex-spaced poll-menu"> <div class="poll-menu">
<!-- Buttons --> <!-- Buttons -->
<button <button
mat-icon-button mat-icon-button
@ -7,7 +7,7 @@
[matMenuTriggerFor]="pollItemMenu" [matMenuTriggerFor]="pollItemMenu"
(click)="$event.stopPropagation()" (click)="$event.stopPropagation()"
> >
<mat-icon>more_vert</mat-icon> <mat-icon>more_horiz</mat-icon>
</button> </button>
<mat-menu #pollItemMenu="matMenu"> <mat-menu #pollItemMenu="matMenu">
<div *osPerms="'assignments.can_manage'"> <div *osPerms="'assignments.can_manage'">
@ -39,23 +39,20 @@
</div> </div>
</mat-menu> </mat-menu>
</div> </div>
<div class="on-transition-fade" *ngIf="poll.options">
<div class="on-transition-fade poll-main-content" *ngIf="poll.options">
<div *ngIf="pollData"> <div *ngIf="pollData">
<div class="poll-grid"> <div class="poll-grid">
<div></div> <div></div>
<div><span class="table-view-list-title" translate>Candidates</span></div> <div><span class="table-view-list-title" translate>Candidates</span></div>
<div><span class="table-view-list-title" translate>Votes</span></div> <div><span class="table-view-list-title" translate>Votes</span></div>
<div *ngIf="pollService.majorityMethods && majorityChoice"> <div *ngIf="pollService.majorityMethods && majorityChoice && canManage">
<div> <div>
<span class="table-view-list-title" translate>Quorum</span> <span class="table-view-list-title" translate>Quorum</span>
</div> </div>
<div> <div>
<!-- manager majority chip (menu trigger) --> <!-- manager majority chip (menu trigger) -->
<mat-basic-chip *ngIf="canManage" [matMenuTriggerFor]="majorityMenu" class="grey" disableRipple> <mat-basic-chip [matMenuTriggerFor]="majorityMenu" class="grey" disableRipple>
{{ majorityChoice.display_name | translate }}
</mat-basic-chip>
<!-- non-manager (menuless) majority chip -->
<mat-basic-chip *ngIf="!canManage" class="grey" disableRipple>
{{ majorityChoice.display_name | translate }} {{ majorityChoice.display_name | translate }}
</mat-basic-chip> </mat-basic-chip>
<!-- menu for selecting quorum choices --> <!-- menu for selecting quorum choices -->
@ -93,7 +90,7 @@
<mat-icon <mat-icon
*ngIf="!option.is_elected && canManage && !assignment.isFinished" *ngIf="!option.is_elected && canManage && !assignment.isFinished"
class="top-aligned primary" class="top-aligned primary"
matTooltip="{{ 'Not elected' | translate }}" matTooltip="{{ 'Mark as elected' | translate }}"
> >
check_box_outline_blank</mat-icon check_box_outline_blank</mat-icon
> >
@ -131,7 +128,7 @@
</div> </div>
</div> </div>
</div> </div>
<div> <div *ngIf="canManage">
<div <div
*ngIf=" *ngIf="
poll.has_votes && poll.has_votes &&
@ -177,26 +174,20 @@
</div> </div>
</div> </div>
<div class="spacer-bottom-10"> <!-- Election Method -->
<div *ngIf="canManage" class="spacer-bottom-10">
<h4 translate>Election method</h4> <h4 translate>Election method</h4>
<span>{{ pollMethodName | translate }}</span> <span>{{ pollMethodName | translate }}</span>
</div> </div>
<div>
<h4> <!-- Poll paper hint -->
<span translate>Hint for ballot paper</span> <div *ngIf="canManage" class="hint-form" [formGroup]="descriptionForm">
</h4> <mat-form-field class="wide">
<div [formGroup]="descriptionForm"> <mat-label translate>Hint for ballot paper</mat-label>
<mat-form-field class="wide"> <input matInput formControlName="description"/>
<input matInput formControlName="description" [disabled]="!canManage" /> </mat-form-field>
</mat-form-field> <button mat-icon-button [disabled]="!dirtyDescription" (click)="onEditDescriptionButton()">
<button <mat-icon inline>check</mat-icon>
mat-icon-button </button>
[disabled]="!dirtyDescription"
*ngIf="canManage"
(click)="onEditDescriptionButton()"
>
<mat-icon inline>check</mat-icon>
</button>
</div>
</div> </div>
</mat-card> </div>

View File

@ -25,6 +25,15 @@
} }
} }
.poll-wrapper {
position: relative;
padding: 0 15px;
}
.poll-main-content {
padding-top: 10px;
}
.poll-result { .poll-result {
.poll-progress-bar { .poll-progress-bar {
height: 5px; height: 5px;
@ -69,12 +78,13 @@
word-wrap: break-word; word-wrap: break-word;
} }
} }
.poll-border {
border: 1px solid lightgray;
}
.poll-menu { .poll-menu {
justify-content: flex-end; position: absolute;
top: 0;
right: 0;
} }
.poll-quorum { .poll-quorum {
text-align: right; text-align: right;
margin-right: 10px; margin-right: 10px;
@ -83,6 +93,7 @@
font-size: 100%; font-size: 100%;
} }
} }
.top-aligned { .top-aligned {
position: absolute; position: absolute;
top: 0; top: 0;
@ -92,3 +103,7 @@
.wide { .wide {
width: 90%; width: 90%;
} }
.hint-form {
margin-top: 20px;
}

View File

@ -201,8 +201,8 @@ export class AssignmentPollComponent extends BaseViewComponent implements OnInit
// TODO deep copy of this.poll (JSON parse is ugly workaround) or sending just copy of the options // TODO deep copy of this.poll (JSON parse is ugly workaround) or sending just copy of the options
data: this.poll.copy(), data: this.poll.copy(),
maxHeight: '90vh', maxHeight: '90vh',
minWidth: '450px', width: '600px',
maxWidth: '80vw', maxWidth: '90vw',
disableClose: true disableClose: true
}); });
dialogRef.afterClosed().subscribe(result => { dialogRef.afterClosed().subscribe(result => {

View File

@ -57,7 +57,8 @@
} }
} }
.openslides-dark-theme, .openslides-developer-dark-theme { .openslides-dark-theme,
.openslides-developer-dark-theme {
.logo-container { .logo-container {
img.dark { img.dark {
display: inherit; display: inherit;
@ -88,7 +89,8 @@
} }
os-site { os-site {
.active mat-icon, .active span { .active mat-icon,
.active span {
color: mat-color(map-get($openslides-developer-theme, accent)); color: mat-color(map-get($openslides-developer-theme, accent));
} }
} }
@ -249,6 +251,7 @@ mat-card {
.os-card { .os-card {
max-width: 770px; max-width: 770px;
margin-top: 20px !important; margin-top: 20px !important;
margin-bottom: 20px !important;
margin-left: auto !important; margin-left: auto !important;
margin-right: auto !important; margin-right: auto !important;
font-size: 16px; font-size: 16px;