multiSelect on listViews

This commit is contained in:
Maximilian Krambach 2018-11-05 17:40:32 +01:00 committed by Sean Engelhardt
parent fed085c319
commit b5aebe5615
19 changed files with 786 additions and 121 deletions

View File

@ -1,13 +1,13 @@
<mat-toolbar color='primary' *ngIf="!vp.isMobile"></mat-toolbar> <mat-toolbar color="primary" [ngClass]="multiSelectMode ? 'multi-select' : '' " *ngIf="!vp.isMobile"></mat-toolbar>
<mat-toolbar color='primary' class="sticky-toolbar"> <mat-toolbar color="primary" [ngClass]="multiSelectMode ? 'multi-select' : '' " class="sticky-toolbar">
<div class="toolbar-left"> <div class="toolbar-left">
<!-- Nav menu --> <!-- Nav menu -->
<button mat-icon-button class="on-transition-fade" *ngIf="vp.isMobile && nav" (click)='clickHamburgerMenu()'> <button mat-icon-button class="on-transition-fade" *ngIf="vp.isMobile && nav && !multiSelectMode" (click)='clickHamburgerMenu()'>
<mat-icon>menu</mat-icon> <mat-icon>menu</mat-icon>
</button> </button>
<!-- Exit / Back button --> <!-- Exit / Back button -->
<button mat-icon-button class="on-transition-fade" *ngIf="!nav && !editMode" (click)="onBackButton()"> <button mat-icon-button class="on-transition-fade" *ngIf="!nav && !editMode && !multiSelectMode" (click)="onBackButton()">
<mat-icon>arrow_back</mat-icon> <mat-icon>arrow_back</mat-icon>
</button> </button>
@ -16,22 +16,28 @@
<mat-icon>close</mat-icon> <mat-icon>close</mat-icon>
</button> </button>
<div class="toolbar-left-text on-transition-fade"> <div class="toolbar-left-text on-transition-fade" *ngIf="!multiSelectMode">
<!-- Title slot --> <!-- Title slot -->
<ng-content select=".title-slot"></ng-content> <ng-content select=".title-slot"></ng-content>
</div> </div>
</div> </div>
<div class=spacer></div>
<div class="toolbar-right">
<!-- centered information slot-->
<div *ngIf="multiSelectMode" class=spacer></div>
<div class="toolbar-centered on-transition-fade" *ngIf="multiSelectMode">
<ng-content select=".central-info-slot"></ng-content>
</div>
<div class=spacer></div>
<div class="toolbar-right">
<!-- Extra controls slot --> <!-- Extra controls slot -->
<div class="extra-controls-wrapper on-transition-fade"> <div class="extra-controls-wrapper on-transition-fade">
<ng-content select=".extra-controls-slot"></ng-content> <ng-content select=".extra-controls-slot"></ng-content>
</div> </div>
<!-- Main action button - desktop --> <!-- Main action button - desktop -->
<button mat-mini-fab color="accent" class="on-transition-fade" *ngIf="mainButton && !editMode && !vp.isMobile" <button mat-mini-fab color="accent" class="on-transition-fade"
(click)="sendMainEvent()"> *ngIf="mainButton && !editMode && !vp.isMobile && !multiSelectMode" (click)="sendMainEvent()">
<mat-icon>{{ mainButtonIcon }}</mat-icon> <mat-icon>{{ mainButtonIcon }}</mat-icon>
</button> </button>
@ -46,6 +52,8 @@
</mat-toolbar> </mat-toolbar>
<!-- Main action button - mobile--> <!-- Main action button - mobile-->
<button mat-fab class="head-button on-transition-fade" *ngIf="mainButton && !editMode && vp.isMobile" (click)=sendMainEvent()>
<button mat-fab class="head-button on-transition-fade"
*ngIf="mainButton && !editMode && vp.isMobile && !multiSelectMode" (click)=sendMainEvent()>
<mat-icon>{{ mainButtonIcon }}</mat-icon> <mat-icon>{{ mainButtonIcon }}</mat-icon>
</button> </button>

View File

@ -18,6 +18,10 @@
margin-left: 10px; margin-left: 10px;
} }
} }
.toolbar-centered {
margin: auto;
vertical-align: baseline;
}
.toolbar-right { .toolbar-right {
display: contents; display: contents;
@ -29,3 +33,7 @@
display: flex; display: flex;
} }
} }
mat-toolbar.multi-select {
background-color: #757575;
}

View File

@ -20,6 +20,7 @@ import { MainMenuService } from '../../../core/services/main-menu.service';
* [mainButton]="opCanEdit()" * [mainButton]="opCanEdit()"
* [mainButtonIcon]="edit" * [mainButtonIcon]="edit"
* [editMode]="editMotion" * [editMode]="editMotion"
* [multiSelectMode]="isMultiSelect"
* (mainEvent)="setEditMode(!editMotion)" * (mainEvent)="setEditMode(!editMotion)"
* (saveEvent)="saveMotion()"> * (saveEvent)="saveMotion()">
* *
@ -34,6 +35,13 @@ import { MainMenuService } from '../../../core/services/main-menu.service';
* <mat-icon>more_vert</mat-icon> * <mat-icon>more_vert</mat-icon>
* </button> * </button>
* </div> * </div>
* <!-- MultiSelect info -->
* <div class="central-info-slot">
* <button mat-icon-button (click)="toggleMultiSelect()">
* <mat-icon>arrow_back</mat-icon>
* </button>
* <span>{{ selectedRows.length }}&nbsp;</span><span translate>selected</span>
* </div>
* </os-head-bar> * </os-head-bar>
* ``` * ```
*/ */
@ -61,6 +69,12 @@ export class HeadBarComponent {
@Input() @Input()
public editMode = false; public editMode = false;
/**
* Determine multiSelect mode: changed interactions and head bar
*/
@Input()
public multiSelectMode = false;
/** /**
* Determine if there should be the main action button * Determine if there should be the main action button
*/ */

View File

@ -1,21 +1,50 @@
<os-head-bar [mainButton]="true" (mainEvent)=onPlusButton()> <os-head-bar [mainButton]="true" (mainEvent)=onPlusButton() [multiSelectMode]="isMultiSelect">
<!-- Title --> <!-- Title -->
<div class="title-slot"> <div class="title-slot">
<h2 translate>Agenda</h2> <h2 translate>Agenda</h2>
</div> </div>
<!-- Menu -->
<div class="menu-slot">
<button type="button" mat-icon-button [matMenuTriggerFor]="agendaMenu">
<mat-icon>more_vert</mat-icon>
</button>
</div>
<!-- Multiselect info -->
<div class="central-info-slot">
<button mat-icon-button (click)="toggleMultiSelect()">
<mat-icon>arrow_back</mat-icon>
</button>
<span>{{ selectedRows.length }}&nbsp;</span><span translate>selected</span>
</div>
<div class="extra-controls-slot on-transition-fade" *ngIf="isMultiSelect">
<button mat-icon-button (click)="deleteSelected()">
<mat-icon>delete</mat-icon>
</button>
</div>
</os-head-bar> </os-head-bar>
<mat-table class="os-listview-table on-transition-fade" [dataSource]="dataSource" matSort> <mat-table class="os-listview-table on-transition-fade" [dataSource]="dataSource" matSort>
<ng-container matColumnDef="selector">
<mat-header-cell *matHeaderCellDef mat-sort-header class="checkbox-cell"></mat-header-cell>
<mat-cell *matCellDef="let item" class="checkbox-cell">
<mat-icon>{{ isSelected(item) ? 'check_circle' : '' }}</mat-icon>
</mat-cell>
</ng-container>
<!-- title column --> <!-- title column -->
<ng-container matColumnDef="title"> <ng-container matColumnDef="title">
<mat-header-cell *matHeaderCellDef mat-sort-header>Topic</mat-header-cell> <mat-header-cell *matHeaderCellDef mat-sort-header>Topic</mat-header-cell>
<mat-cell *matCellDef="let item" (click)="selectAgendaItem(item)">{{ item.getListTitle() }}</mat-cell> <mat-cell *matCellDef="let item">{{ item.getListTitle() }}</mat-cell>
</ng-container> </ng-container>
<!-- Duration column --> <!-- Duration column -->
<ng-container matColumnDef="duration"> <ng-container matColumnDef="duration">
<mat-header-cell *matHeaderCellDef mat-sort-header>Duration</mat-header-cell> <mat-header-cell *matHeaderCellDef mat-sort-header>Duration</mat-header-cell>
<mat-cell *matCellDef="let item" (click)="selectAgendaItem(item)">{{ item.duration }}</mat-cell> <mat-cell *matCellDef="let item">{{ item.duration }}</mat-cell>
</ng-container> </ng-container>
<!-- Speakers column --> <!-- Speakers column -->
@ -32,7 +61,45 @@
</mat-cell> </mat-cell>
</ng-container> </ng-container>
<mat-header-row *matHeaderRowDef="['title', 'duration', 'speakers']"></mat-header-row> <mat-header-row *matHeaderRowDef="getColumnDefinition()"></mat-header-row>
<mat-row *matRowDef="let row; columns: ['title', 'duration', 'speakers']"></mat-row> <mat-row [ngClass]="selectedRows.indexOf(row) >= 0 ? 'selected': ''" (click)='selectItem(row, $event)'
*matRowDef="let row; columns: getColumnDefinition()"></mat-row>
</mat-table> </mat-table>
<mat-paginator class="on-transition-fade" [pageSizeOptions]="[25, 50, 75, 100, 125]"></mat-paginator> <mat-paginator class="on-transition-fade" [pageSizeOptions]="[25, 50, 75, 100, 125]"></mat-paginator>
<mat-menu #agendaMenu="matMenu">
<div *ngIf="!isMultiSelect">
<button mat-menu-item *osPerms="'agenda.can_manage'" (click)="toggleMultiSelect()">
<mat-icon>library_add</mat-icon>
<span translate>MultiSelect</span>
</button>
</div>
<div *ngIf="isMultiSelect" >
<div *osPerms="'agenda.can_manage'">
<button mat-menu-item (click)="deleteSelected()">
<mat-icon>delete</mat-icon>
<span translate>Delete selected</span>
</button>
<mat-divider></mat-divider>
<button mat-menu-item (click)="setClosedSelected(true)">
<mat-icon>done</mat-icon>
<span translate>Close selected</span>
</button>
<button mat-menu-item (click)="setClosedSelected(false)">
<mat-icon>redo</mat-icon>
<span translate>Open selected</span>
</button>
<mat-divider></mat-divider>
<button mat-menu-item (click)="setVisibilitySelected(true)">
<mat-icon>visibility</mat-icon>
<span translate>Set visible</span>
</button>
<button mat-menu-item (click)="setVisibilitySelected(false)">
<mat-icon>visibility_off</mat-icon>
<span translate>Set invisible</span>
</button>
</div>
</div>
</mat-menu>

View File

@ -2,11 +2,12 @@ import { Component, OnInit } from '@angular/core';
import { Router, ActivatedRoute } from '@angular/router'; import { Router, ActivatedRoute } from '@angular/router';
import { Title } from '@angular/platform-browser'; import { Title } from '@angular/platform-browser';
import { MatSnackBar } from '@angular/material'; import { MatSnackBar } from '@angular/material';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { ViewItem } from '../../models/view-item'; import { ViewItem } from '../../models/view-item';
import { ListViewBaseComponent } from 'app/site/base/list-view-base'; import { ListViewBaseComponent } from 'app/site/base/list-view-base';
import { AgendaRepositoryService } from '../../services/agenda-repository.service'; import { AgendaRepositoryService } from '../../services/agenda-repository.service';
import { PromptService } from '../../../../core/services/prompt.service';
/** /**
* List view for the agenda. * List view for the agenda.
@ -24,7 +25,9 @@ export class AgendaListComponent extends ListViewBaseComponent<ViewItem> impleme
* @param matSnackBar Shows errors and messages * @param matSnackBar Shows errors and messages
* @param route Angulars ActivatedRoute * @param route Angulars ActivatedRoute
* @param router Angulars router * @param router Angulars router
* @param repo the agenda repository * @param repo the agenda repository,
* promptService:
*
*/ */
public constructor( public constructor(
titleService: Title, titleService: Title,
@ -32,9 +35,13 @@ export class AgendaListComponent extends ListViewBaseComponent<ViewItem> impleme
matSnackBar: MatSnackBar, matSnackBar: MatSnackBar,
private route: ActivatedRoute, private route: ActivatedRoute,
private router: Router, private router: Router,
private repo: AgendaRepositoryService private repo: AgendaRepositoryService,
private promptService: PromptService
) { ) {
super(titleService, translate, matSnackBar); super(titleService, translate, matSnackBar);
// activate multiSelect mode for this listview
this.canMultiSelect = true;
} }
/** /**
@ -46,18 +53,17 @@ export class AgendaListComponent extends ListViewBaseComponent<ViewItem> impleme
this.initTable(); this.initTable();
this.repo.getViewModelListObservable().subscribe(newAgendaItem => { this.repo.getViewModelListObservable().subscribe(newAgendaItem => {
this.dataSource.data = newAgendaItem; this.dataSource.data = newAgendaItem;
this.checkSelection();
}); });
} }
/** /**
* Handler for click events on agenda item rows * Handler for click events on an agenda item row. Links to the content object
* Links to the content object if any
*
* Gets content object from the repository rather than from the model * Gets content object from the repository rather than from the model
* to avoid race conditions * to avoid race conditions
* @param item the item that was selected from the list view * @param item the item that was selected from the list view
*/ */
public selectAgendaItem(item: ViewItem): void { public singleSelectAction(item: ViewItem): void {
const contentObject = this.repo.getContentObject(item.item); const contentObject = this.repo.getContentObject(item.item);
this.router.navigate([contentObject.getDetailStateURL()]); this.router.navigate([contentObject.getDetailStateURL()]);
} }
@ -77,4 +83,47 @@ export class AgendaListComponent extends ListViewBaseComponent<ViewItem> impleme
public onPlusButton(): void { public onPlusButton(): void {
this.router.navigate(['topics/new'], { relativeTo: this.route }); this.router.navigate(['topics/new'], { relativeTo: this.route });
} }
/**
* Handler for deleting multiple entries. Needs items in selectedRows, which
* is only filled with any data in multiSelect mode
*/
public async deleteSelected(): Promise<void> {
const content = this.translate.instant('This will delete all selected agenda items.');
if (await this.promptService.open('Are you sure?', content)) {
for (const agenda of this.selectedRows) {
await this.repo.delete(agenda);
}
}
}
/**
* Sets multiple entries' open/closed state. Needs items in selectedRows, which
* is only filled with any data in multiSelect mode
* @param closed true if the item is to be considered done
*/
public async setClosedSelected(closed: boolean): Promise<void> {
for (const agenda of this.selectedRows) {
await this.repo.update({ closed: closed }, agenda);
}
}
/**
* Sets multiple entries' visibility. Needs items in selectedRows, which
* is only filled with any data in multiSelect mode.
* @param visible true if the item is to be shown
*/
public async setVisibilitySelected(visible: boolean): Promise<void> {
for (const agenda of this.selectedRows) {
await this.repo.update({ is_hidden: visible }, agenda);
}
}
public getColumnDefinition(): string[] {
const list = ['title', 'duration', 'speakers'];
if (this.isMultiSelect) {
return ['selector'].concat(list);
}
return list;
}
} }

View File

@ -1,18 +1,40 @@
<os-head-bar plusButton=true (plusButtonClicked)=onPlusButton()> <os-head-bar plusButton=true (plusButtonClicked)=onPlusButton() [multiSelectMode]="isMultiSelect">
<!-- Title --> <!-- Title -->
<div class="title-slot"> <div class="title-slot">
<h2 translate>Elections</h2> <h2 translate>Elections</h2>
</div> </div>
<!-- Menu --> <!-- Menu -->
<div class="menu-slot"> <div class="menu-slot">
<button type="button" mat-icon-button [matMenuTriggerFor]="assignmentMenu"> <button type="button" mat-icon-button [matMenuTriggerFor]="assignmentMenu">
<mat-icon>more_vert</mat-icon> <mat-icon>more_vert</mat-icon>
</button> </button>
</div> </div>
<!-- Multiselect info -->
<div class="central-info-slot">
<button mat-icon-button (click)="toggleMultiSelect()">
<mat-icon>arrow_back</mat-icon>
</button>
<span>{{ selectedRows.length }}&nbsp;</span><span translate>selected</span>
</div>
<div class="extra-controls-slot on-transition-fade" *ngIf="isMultiSelect">
<button mat-icon-button (click)="deleteSelected()">
<mat-icon>delete</mat-icon>
</button>
</div>
</os-head-bar> </os-head-bar>
<mat-table class="os-listview-table on-transition-fade" [dataSource]="dataSource" matSort> <mat-table class="os-listview-table on-transition-fade" [dataSource]="dataSource" matSort>
<ng-container matColumnDef="selector">
<mat-header-cell *matHeaderCellDef mat-sort-header class="checkbox-cell" ></mat-header-cell>
<mat-cell *matCellDef="let assignment" class="checkbox-cell" >
<mat-icon>{{ isSelected(assignment) ? 'check_circle' : '' }}</mat-icon>
</mat-cell>
</ng-container>
<!-- name column --> <!-- name column -->
<ng-container matColumnDef="title"> <ng-container matColumnDef="title">
<mat-header-cell *matHeaderCellDef mat-sort-header>Title</mat-header-cell> <mat-header-cell *matHeaderCellDef mat-sort-header>Title</mat-header-cell>
@ -35,15 +57,30 @@
</mat-cell> </mat-cell>
</ng-container> </ng-container>
<mat-header-row *matHeaderRowDef="['title', 'phase', 'candidates']"></mat-header-row> <mat-header-row *matHeaderRowDef="getColumnDefintion()"></mat-header-row>
<mat-row (click)="selectAssignment(row)" *matRowDef="let row; columns: ['title', 'phase', 'candidates']"></mat-row> <mat-row [ngClass]="selectedRows.indexOf(row) >= 0 ? 'selected': ''" (click)='selectItem(row, $event)'
*matRowDef="let row; columns: getColumnDefintion()">
</mat-row>
</mat-table> </mat-table>
<mat-paginator class="on-transition-fade" [pageSizeOptions]="[25, 50, 75, 100, 125]"></mat-paginator> <mat-paginator class="on-transition-fade" [pageSizeOptions]="[25, 50, 75, 100, 125]"></mat-paginator>
<mat-menu #assignmentMenu="matMenu"> <mat-menu #assignmentMenu="matMenu">
<button mat-menu-item (click)="downloadAssignmentButton()"> <div *ngIf="!isMultiSelect">
<mat-icon>archive</mat-icon> <button mat-menu-item *osPerms="'assignment.can_manage'" (click)="toggleMultiSelect()">
<span translate>Export ...</span> <mat-icon>library_add</mat-icon>
</button> <span translate>MultiSelect</span>
</button>
<button mat-menu-item (click)="downloadAssignmentButton()">
<mat-icon>archive</mat-icon>
<span translate>Export ...</span>
</button>
</div>
<div *ngIf="isMultiSelect">
<button mat-menu-item *osPerms="'assignment.can_manage'" (click)="deleteSelected()">
<mat-icon>delete</mat-icon>
<span translate>Delete assignments</span>
</button>
</div>
</mat-menu> </mat-menu>

View File

@ -5,6 +5,7 @@ import { ViewAssignment } from '../models/view-assignment';
import { ListViewBaseComponent } from '../../base/list-view-base'; import { ListViewBaseComponent } from '../../base/list-view-base';
import { AssignmentRepositoryService } from '../services/assignment-repository.service'; import { AssignmentRepositoryService } from '../services/assignment-repository.service';
import { MatSnackBar } from '@angular/material'; import { MatSnackBar } from '@angular/material';
import { PromptService } from '../../../core/services/prompt.service';
/** /**
* Listview for the assignments * Listview for the assignments
@ -23,14 +24,18 @@ export class AssignmentListComponent extends ListViewBaseComponent<ViewAssignmen
* @param translate * @param translate
* @param matSnackBar * @param matSnackBar
* @param repo the repository * @param repo the repository
* @param promptService
*/ */
public constructor( public constructor(
titleService: Title, titleService: Title,
translate: TranslateService, translate: TranslateService,
matSnackBar: MatSnackBar, matSnackBar: MatSnackBar,
private repo: AssignmentRepositoryService private repo: AssignmentRepositoryService,
private promptService: PromptService
) { ) {
super(titleService, translate, matSnackBar); super(titleService, translate, matSnackBar);
// activate multiSelect mode for this listview
this.canMultiSelect = true;
} }
/** /**
@ -42,6 +47,7 @@ export class AssignmentListComponent extends ListViewBaseComponent<ViewAssignmen
this.initTable(); this.initTable();
this.repo.getViewModelListObservable().subscribe(newAssignments => { this.repo.getViewModelListObservable().subscribe(newAssignments => {
this.dataSource.data = newAssignments; this.dataSource.data = newAssignments;
this.checkSelection();
}); });
} }
@ -53,10 +59,10 @@ export class AssignmentListComponent extends ListViewBaseComponent<ViewAssignmen
} }
/** /**
* Select an row in the table * Action to be performed after a click on a row in the table, if in single select mode
* @param assignment * @param assignment The entry of row clicked
*/ */
public selectAssignment(assignment: ViewAssignment): void { public singleSelectAction(assignment: ViewAssignment): void {
console.log('select assignment list: ', assignment); console.log('select assignment list: ', assignment);
} }
@ -67,4 +73,25 @@ export class AssignmentListComponent extends ListViewBaseComponent<ViewAssignmen
public downloadAssignmentButton(): void { public downloadAssignmentButton(): void {
console.log('Hello World'); console.log('Hello World');
} }
/**
* Handler for deleting multiple entries. Needs items in selectedRows, which
* is only filled with any data in multiSelect mode
*/
public async deleteSelected(): Promise<void> {
const content = this.translate.instant('This will delete all selected assignments.');
if (await this.promptService.open('Are you sure?', content)) {
for (const assignment of this.selectedRows) {
await this.repo.delete(assignment);
}
}
}
public getColumnDefintion(): string[] {
const list = ['title', 'phase', 'candidates'];
if (this.isMultiSelect) {
return ['selector'].concat(list);
}
return list;
}
} }

View File

@ -11,6 +11,22 @@ export abstract class ListViewBaseComponent<V extends BaseViewModel> extends Bas
*/ */
public dataSource: MatTableDataSource<V>; public dataSource: MatTableDataSource<V>;
/**
* Toggle for enabling the multiSelect mode. Defaults to false (inactive)
*/
protected canMultiSelect = false;
/**
* Current state of the multiSelect mode. TODO Could be merged with edit mode?
*/
private _multiSelectModus = false;
/**
* An array of currently selected items, upon which multiselect actions can be performed
* see {@link selectItem}.
*/
public selectedRows: V[];
/** /**
* The table itself * The table itself
*/ */
@ -37,6 +53,7 @@ export abstract class ListViewBaseComponent<V extends BaseViewModel> extends Bas
*/ */
public constructor(titleService: Title, translate: TranslateService, matSnackBar: MatSnackBar) { public constructor(titleService: Title, translate: TranslateService, matSnackBar: MatSnackBar) {
super(titleService, translate, matSnackBar); super(titleService, translate, matSnackBar);
this.selectedRows = [];
} }
/** /**
@ -49,4 +66,88 @@ export abstract class ListViewBaseComponent<V extends BaseViewModel> extends Bas
this.dataSource.paginator = this.paginator; this.dataSource.paginator = this.paginator;
this.dataSource.sort = this.sort; this.dataSource.sort = this.sort;
} }
/**
* Default click action on selecting an item. In multiselect modus,
* this just adds/removes from a selection, else it performs a {@link singleSelectAction}
* @param row The clicked row's {@link ViewModel}
* @param event The Mouse event
*/
public selectItem(row: V, event: MouseEvent): void {
event.stopPropagation();
if (!this._multiSelectModus) {
this.singleSelectAction(row);
} else {
const idx = this.selectedRows.indexOf(row);
if ( idx < 0){
this.selectedRows.push(row);
} else {
this.selectedRows.splice(idx, 1);
}
}
}
/**
* Method to perform an action on click on a row, if not in MultiSelect Modus.
* Should be overridden by implementations. Currently there is no default action.
* @param row a ViewModel
*/
public singleSelectAction(row: V) : void {
}
/**
* enables/disables the multiSelect Mode
*/
public toggleMultiSelect() : void {
if (!this.canMultiSelect || this.isMultiSelect) {
this._multiSelectModus = false;
this.clearSelection();
} else {
this._multiSelectModus = true;
}
}
/**
* Returns the current state of the multiSelect modus
*/
public get isMultiSelect(): boolean {
return this._multiSelectModus;
}
/**
* checks if a row is currently selected in the multiSelect modus.
* @param item The row's entry
*/
public isSelected(item: V): boolean {
if (!this._multiSelectModus) {
return false;
}
return this.selectedRows.indexOf(item) >= 0;
}
/**
* Handler to quickly unselect all items.
*/
public clearSelection(): void {
this.selectedRows = [];
}
/**
* Checks the array of selected items against the datastore data. This is
* meant to reselect items by their id even if some of their data changed,
* and to remove selected data that don't exist anymore.
* To be called after an update of data. Checks if updated selected items
* are still present in the dataSource, and (re-)selects them. This should
* be called as the observed datasource updates.
*/
protected checkSelection(): void {
const newSelection = [];
this.selectedRows.forEach(selectedrow => {
const newrow = this.dataSource.data.find(item => item.id === selectedrow.id);
if (newrow) {
newSelection.push(newrow);
}
});
this.selectedRows = newSelection;
}
} }

View File

@ -1,4 +1,11 @@
<os-head-bar [mainButton]="true" (mainEvent)="onMainEvent()" [editMode]="editFile" (saveEvent)="onSaveEditedFile()"> <os-head-bar
[mainButton]="true"
[editMode]="editFile"
[multiSelectMode]="isMultiSelect"
(mainEvent)="onMainEvent()"
(saveEvent)="onSaveEditedFile()"
>
<!-- Title --> <!-- Title -->
<div class="title-slot"> <div class="title-slot">
<h2 *ngIf="!editFile" translate>Files</h2> <h2 *ngIf="!editFile" translate>Files</h2>
@ -37,19 +44,48 @@
<mat-icon>more_vert</mat-icon> <mat-icon>more_vert</mat-icon>
</button> </button>
</div> </div>
<!-- Multiselect menu -->
<div class="multiselect-menu-slot">
<button type="button" mat-icon-button [matMenuTriggerFor]="mediafilesMultiSelectMenu">
<mat-icon>more_vert</mat-icon>
</button>
</div>
<!-- Multiselect info -->
<div *ngIf="this.isMultiSelect" class="central-info-slot">
<button mat-icon-button (click)="toggleMultiSelect()">
<mat-icon >arrow_back</mat-icon>
</button>
<span>{{ selectedRows.length }}&nbsp;</span><span translate>selected</span>
</div>
<div class="extra-controls-slot on-transition-fade" *ngIf="isMultiSelect">
<button mat-icon-button (click)="deleteSelected()">
<mat-icon>delete</mat-icon>
</button>
</div>
</os-head-bar> </os-head-bar>
<mat-table class="os-listview-table on-transition-fade" [dataSource]="dataSource" matSort> <mat-table class="os-listview-table on-transition-fade" [dataSource]="dataSource" matSort>
<ng-container matColumnDef="selector">
<mat-header-cell *matHeaderCellDef mat-sort-header class="checkbox-cell"></mat-header-cell>
<mat-cell *matCellDef="let item" class="checkbox-cell">
<mat-icon>{{ isSelected(item) ? 'check_circle' : '' }}</mat-icon>
</mat-cell>
</ng-container>
<!-- Filename --> <!-- Filename -->
<ng-container matColumnDef="title"> <ng-container matColumnDef="title">
<mat-header-cell *matHeaderCellDef mat-sort-header>Name</mat-header-cell> <mat-header-cell *matHeaderCellDef mat-sort-header>Name</mat-header-cell>
<mat-cell *matCellDef="let file" (click)="download(file)">{{ file.title }}</mat-cell> <mat-cell *matCellDef="let file">{{ file.title }}</mat-cell>
</ng-container> </ng-container>
<!-- Info --> <!-- Info -->
<ng-container matColumnDef="info"> <ng-container matColumnDef="info">
<mat-header-cell *matHeaderCellDef mat-sort-header>Group</mat-header-cell> <mat-header-cell *matHeaderCellDef mat-sort-header>Group</mat-header-cell>
<mat-cell *matCellDef="let file" (click)="download(file)"> <mat-cell *matCellDef="let file">
<div class="file-info-cell"> <div class="file-info-cell">
<span> <mat-icon [inline]="true">insert_drive_file</mat-icon> {{ file.type }} </span> <span> <mat-icon [inline]="true">insert_drive_file</mat-icon> {{ file.type }} </span>
<span> <mat-icon [inline]="true">data_usage</mat-icon> {{ file.size }} </span> <span> <mat-icon [inline]="true">data_usage</mat-icon> {{ file.size }} </span>
@ -86,7 +122,8 @@
</ng-container> </ng-container>
<mat-header-row *matHeaderRowDef="getColumnDefinition()"></mat-header-row> <mat-header-row *matHeaderRowDef="getColumnDefinition()"></mat-header-row>
<mat-row *matRowDef="let row; columns: getColumnDefinition()"></mat-row> <mat-row *matRowDef="let row; columns: getColumnDefinition()"
[ngClass]="selectedRows.indexOf(row) >= 0 ? 'selected': ''" (click)='selectItem(row, $event)'></mat-row>
</mat-table> </mat-table>
<mat-paginator class="on-transition-fade" [pageSizeOptions]="[25, 50, 75, 100, 125]"></mat-paginator> <mat-paginator class="on-transition-fade" [pageSizeOptions]="[25, 50, 75, 100, 125]"></mat-paginator>
@ -130,9 +167,14 @@
<!-- Menu for Mediafiles --> <!-- Menu for Mediafiles -->
<mat-menu #mediafilesMenu="matMenu"> <mat-menu #mediafilesMenu="matMenu">
<!-- Delete all files - later replaced with multi-select function --> <button mat-menu-item *osPerms="'mediafiles.can_manage'" (click)="toggleMultiSelect()">
<button mat-menu-item class="red-warning-text" (click)="onDeleteAllFiles()"> <mat-icon>library_add</mat-icon>
<mat-icon>delete</mat-icon> <span translate>MultiSelect</span>
<span translate>Delete all files</span> </button>
</mat-menu>
<mat-menu #mediafilesMultiSelectMenu="matMenu">
<button mat-menu-item *osPerms="'mediafiles.can_manage'" (click)="deleteSelected()">
<mat-icon>delete</mat-icon>
<span translate>Delete selected</span>
</button> </button>
</mat-menu> </mat-menu>

View File

@ -84,6 +84,9 @@ export class MediafileListComponent extends ListViewBaseComponent<ViewMediafile>
public vp: ViewportService public vp: ViewportService
) { ) {
super(titleService, translate, matSnackBar); super(titleService, translate, matSnackBar);
// emables multiSelection for this listView
this.canMultiSelect = true;
} }
/** /**
@ -170,18 +173,15 @@ export class MediafileListComponent extends ListViewBaseComponent<ViewMediafile>
} }
/** /**
* triggers a routine to delete all MediaFiles * Handler to delete several files at once. Requires data in selectedRows, which
* TODO: Remove after Multiselect * will be made available in multiSelect mode
*
* @deprecated to be removed once multi selection is implemented
*/ */
public async onDeleteAllFiles(): Promise<void> { public async deleteSelected(): Promise<void> {
const content = this.translate.instant('This will delete all files.'); const content = this.translate.instant('This will delete all selected files.');
if (await this.promptService.open('Are you sure?', content)) { if (await this.promptService.open('Are you sure?', content)) {
const viewMediafiles = this.dataSource.data; for (const mediafile of this.selectedRows) {
viewMediafiles.forEach(file => { await this.repo.delete(mediafile);
this.repo.delete(file); }
});
} }
} }
@ -257,7 +257,11 @@ export class MediafileListComponent extends ListViewBaseComponent<ViewMediafile>
* @returns the column definition for the screen size * @returns the column definition for the screen size
*/ */
public getColumnDefinition(): string[] { public getColumnDefinition(): string[] {
return this.vp.isMobile ? this.displayedColumnsMobile : this.displayedColumnsDesktop; const columns = this.vp.isMobile ? this.displayedColumnsMobile : this.displayedColumnsDesktop;
if (this.isMultiSelect){
return ['selector'].concat(columns);
}
return columns;
} }
/** /**
@ -265,7 +269,7 @@ export class MediafileListComponent extends ListViewBaseComponent<ViewMediafile>
* *
* @param file the select file to download * @param file the select file to download
*/ */
public download(file: ViewMediafile): void { public singleSelectAction(file: ViewMediafile): void {
window.open(file.downloadUrl); window.open(file.downloadUrl);
} }

View File

@ -1,4 +1,4 @@
<os-head-bar [mainButton]="true" (mainEvent)=onPlusButton()> <os-head-bar [mainButton]="true" (mainEvent)=onPlusButton() [multiSelectMode]="isMultiSelect">
<!-- Title --> <!-- Title -->
<div class="title-slot"> <div class="title-slot">
<h2 translate>Motions</h2> <h2 translate>Motions</h2>
@ -10,6 +10,21 @@
<mat-icon>more_vert</mat-icon> <mat-icon>more_vert</mat-icon>
</button> </button>
</div> </div>
<!-- Multiselect info -->
<div class="central-info-slot">
<button mat-icon-button (click)="toggleMultiSelect()">
<mat-icon>arrow_back</mat-icon>
</button>
<span>{{ selectedRows.length }}&nbsp;</span><span translate>selected</span>
</div>
<div class="extra-controls-slot on-transition-fade" *ngIf="isMultiSelect">
<button mat-icon-button (click)="deleteSelected()">
<mat-icon>delete</mat-icon>
</button>
</div>
</os-head-bar> </os-head-bar>
<div class="custom-table-header on-transition-fade"> <div class="custom-table-header on-transition-fade">
@ -22,10 +37,17 @@
</div> </div>
<mat-table class="os-listview-table on-transition-fade" [dataSource]="dataSource" matSort> <mat-table class="os-listview-table on-transition-fade" [dataSource]="dataSource" matSort>
<ng-container matColumnDef="selector" >
<mat-header-cell *matHeaderCellDef mat-sort-header class="checkbox-cell"></mat-header-cell>
<mat-cell *matCellDef="let motion" class="checkbox-cell">
<mat-icon>{{ isSelected(motion) ? 'check_circle' : '' }}</mat-icon>
</mat-cell>
</ng-container>
<!-- identifier column --> <!-- identifier column -->
<ng-container matColumnDef="identifier"> <ng-container matColumnDef="identifier">
<mat-header-cell *matHeaderCellDef mat-sort-header>Identifier</mat-header-cell> <mat-header-cell *matHeaderCellDef mat-sort-header>Identifier</mat-header-cell>
<mat-cell *matCellDef="let motion" (click)="selectMotion(motion)"> <mat-cell *matCellDef="let motion">
<div class="innerTable"> <div class="innerTable">
{{ motion.identifier }} {{ motion.identifier }}
</div> </div>
@ -35,7 +57,7 @@
<!-- title column --> <!-- title column -->
<ng-container matColumnDef="title"> <ng-container matColumnDef="title">
<mat-header-cell *matHeaderCellDef mat-sort-header>Title</mat-header-cell> <mat-header-cell *matHeaderCellDef mat-sort-header>Title</mat-header-cell>
<mat-cell *matCellDef="let motion" (click)="selectMotion(motion)"> <mat-cell *matCellDef="let motion">
<div class="innerTable"> <div class="innerTable">
<span class="motion-list-title">{{ motion.title }}</span> <span class="motion-list-title">{{ motion.title }}</span>
<br> <br>
@ -50,7 +72,7 @@
<!-- state column --> <!-- state column -->
<ng-container matColumnDef="state"> <ng-container matColumnDef="state">
<mat-header-cell *matHeaderCellDef mat-sort-header>State</mat-header-cell> <mat-header-cell *matHeaderCellDef mat-sort-header>State</mat-header-cell>
<mat-cell *matCellDef="let motion" (click)="selectMotion(motion)"> <mat-cell *matCellDef="let motion">
<!--div *ngIf='isDisplayIcon(motion.state) && motion.state' class='innerTable'> <!--div *ngIf='isDisplayIcon(motion.state) && motion.state' class='innerTable'>
<mat-icon>{{ getStateIcon(motion.state) }}</mat-icon> <mat-icon>{{ getStateIcon(motion.state) }}</mat-icon>
</div>--> </div>-->
@ -64,7 +86,7 @@
<ng-container matColumnDef="speakers"> <ng-container matColumnDef="speakers">
<mat-header-cell *matHeaderCellDef mat-sort-header>Speakers</mat-header-cell> <mat-header-cell *matHeaderCellDef mat-sort-header>Speakers</mat-header-cell>
<mat-cell *matCellDef="let motion"> <mat-cell *matCellDef="let motion">
<button mat-icon-button (click)="onSpeakerIcon(motion)"> <button mat-icon-button (click)="onSpeakerIcon(motion, $event)">
<mat-icon <mat-icon
[matBadge]="motion.agendaSpeakerAmount > 0 ? motion.agendaSpeakerAmount : null" [matBadge]="motion.agendaSpeakerAmount > 0 ? motion.agendaSpeakerAmount : null"
matBadgeColor="accent"> matBadgeColor="accent">
@ -74,34 +96,55 @@
</mat-cell> </mat-cell>
</ng-container> </ng-container>
<mat-header-row *matHeaderRowDef="columnsToDisplayMinWidth"></mat-header-row> <mat-header-row *matHeaderRowDef="getColumnDefinition()"></mat-header-row>
<mat-row *matRowDef="let row; columns: columnsToDisplayMinWidth"></mat-row> <mat-row [ngClass]="selectedRows.indexOf(row) >= 0 ? 'selected': ''" (click)='selectItem(row, $event)'
*matRowDef="let row; columns: getColumnDefinition()">
</mat-row>
</mat-table> </mat-table>
<mat-paginator class="on-transition-fade" [pageSizeOptions]="[25, 50, 75, 100, 125]"></mat-paginator> <mat-paginator class="on-transition-fade" [pageSizeOptions]="[25, 50, 75, 100, 125]"></mat-paginator>
<mat-menu #motionListMenu="matMenu"> <mat-menu #motionListMenu="matMenu">
<button mat-menu-item routerLink="category"> <div *ngIf="!isMultiSelect">
<mat-icon>device_hub</mat-icon> <button mat-menu-item *osPerms="'motions.can_manage'" (click)="toggleMultiSelect()">
<span translate>Categories</span> <mat-icon>library_add</mat-icon>
</button> <span translate>MultiSelect</span>
</button>
<button mat-menu-item routerLink="category">
<mat-icon>device_hub</mat-icon>
<span translate>Categories</span>
</button>
<button mat-menu-item routerLink="comment-section"> <button mat-menu-item routerLink="comment-section">
<mat-icon>speaker_notes</mat-icon> <mat-icon>speaker_notes</mat-icon>
<span translate>Comment sections</span> <span translate>Comment sections</span>
</button> </button>
<button mat-menu-item routerLink="statute-paragraphs" *ngIf="statutesEnabled"> <button mat-menu-item routerLink="statute-paragraphs" *ngIf="statutesEnabled">
<mat-icon>account_balance</mat-icon> <mat-icon>account_balance</mat-icon>
<span translate>Statute paragraphs</span> <span translate>Statute paragraphs</span>
</button> </button>
</div>
<button mat-menu-item *osPerms="'motions.can_manage'" routerLink="call-list"> <div *ngIf="isMultiSelect">
<mat-icon>sort</mat-icon> <div *osPerms="'motions.can_manage'">
<span translate>Call list</span> <button mat-menu-item (click)="deleteSelected()">
</button> <mat-icon>delete</mat-icon>
<button mat-menu-item (click)="csvExportMotionList()"> <span translate>Delete selected</span>
<mat-icon>archive</mat-icon> </button>
<span translate>Export as CSV</span> <button mat-menu-item (click)="openSetStatusMenu()">
</button> <mat-icon>sentiment_satisfied</mat-icon>
<!-- TODO: icon -->
<span translate>Set status</span>
</button>
<button mat-menu-item (click)="openSetCategoryMenu()">
<mat-icon>sentiment_satisfied</mat-icon>
<!-- TODO: icon -->
<span translate>Set categories</span>
</button>
</div>
<button mat-menu-item (click)="csvExportMotionList()">
<mat-icon>archive</mat-icon>
<span translate>Export as CSV</span>
</button>
</div>
</mat-menu> </mat-menu>

View File

@ -1,7 +1,6 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { Router, ActivatedRoute } from '@angular/router'; import { Router, ActivatedRoute } from '@angular/router';
import { Title } from '@angular/platform-browser'; import { Title } from '@angular/platform-browser';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { MotionRepositoryService } from '../../services/motion-repository.service'; import { MotionRepositoryService } from '../../services/motion-repository.service';
@ -11,6 +10,8 @@ import { ListViewBaseComponent } from '../../../base/list-view-base';
import { MatSnackBar } from '@angular/material'; import { MatSnackBar } from '@angular/material';
import { ConfigService } from '../../../../core/services/config.service'; import { ConfigService } from '../../../../core/services/config.service';
import { CsvExportService } from 'app/core/services/csv-export.service'; import { CsvExportService } from 'app/core/services/csv-export.service';
import { Category } from '../../../../shared/models/motions/category';
import { PromptService } from '../../../../core/services/prompt.service';
/** /**
* Component that displays all the motions in a Table using DataSource. * Component that displays all the motions in a Table using DataSource.
@ -22,13 +23,14 @@ import { CsvExportService } from 'app/core/services/csv-export.service';
}) })
export class MotionListComponent extends ListViewBaseComponent<ViewMotion> implements OnInit { export class MotionListComponent extends ListViewBaseComponent<ViewMotion> implements OnInit {
/** /**
* Use for minimal width * Use for minimal width. Please note the 'selector' row for multiSelect mode,
* to be able to display an indicator for the state of selection
*/ */
public columnsToDisplayMinWidth = ['identifier', 'title', 'state', 'speakers']; public columnsToDisplayMinWidth = ['identifier', 'title', 'state', 'speakers'];
/** /**
* Use for maximal width * Use for maximal width. Please note the 'selector' row for multiSelect mode,
* * to be able to display an indicator for the state of selection
* TODO: Needs vp.desktop check * TODO: Needs vp.desktop check
*/ */
public columnsToDisplayFullWidth = ['identifier', 'title', 'state', 'speakers']; public columnsToDisplayFullWidth = ['identifier', 'title', 'state', 'speakers'];
@ -50,6 +52,7 @@ export class MotionListComponent extends ListViewBaseComponent<ViewMotion> imple
* @param configService The configuration provider * @param configService The configuration provider
* @param repo Motion Repository * @param repo Motion Repository
* @param csvExport CSV Export Service * @param csvExport CSV Export Service
* @param promptService
*/ */
public constructor( public constructor(
titleService: Title, titleService: Title,
@ -59,9 +62,13 @@ export class MotionListComponent extends ListViewBaseComponent<ViewMotion> imple
private route: ActivatedRoute, private route: ActivatedRoute,
private configService: ConfigService, private configService: ConfigService,
private repo: MotionRepositoryService, private repo: MotionRepositoryService,
private csvExport: CsvExportService private csvExport: CsvExportService,
private promptService: PromptService
) { ) {
super(titleService, translate, matSnackBar); super(titleService, translate, matSnackBar);
// enable multiSelect for this listView
this.canMultiSelect = true;
} }
/** /**
@ -73,6 +80,7 @@ export class MotionListComponent extends ListViewBaseComponent<ViewMotion> imple
super.setTitle('Motions'); super.setTitle('Motions');
this.initTable(); this.initTable();
this.repo.getViewModelListObservable().subscribe(newMotions => { this.repo.getViewModelListObservable().subscribe(newMotions => {
this.checkSelection();
// TODO: This is for testing purposes. Can be removed with #3963 // TODO: This is for testing purposes. Can be removed with #3963
this.dataSource.data = newMotions.sort((a, b) => { this.dataSource.data = newMotions.sort((a, b) => {
if (a.callListWeight !== b.callListWeight) { if (a.callListWeight !== b.callListWeight) {
@ -90,11 +98,10 @@ export class MotionListComponent extends ListViewBaseComponent<ViewMotion> imple
} }
/** /**
* Select a motion from list. Executed via click. * The action performed on a click in single select modus
*
* @param motion The row the user clicked at * @param motion The row the user clicked at
*/ */
public selectMotion(motion: ViewMotion): void { public singleSelectAction(motion: ViewMotion): void {
this.router.navigate(['./' + motion.id], { relativeTo: this.route }); this.router.navigate(['./' + motion.id], { relativeTo: this.route });
} }
@ -137,7 +144,8 @@ export class MotionListComponent extends ListViewBaseComponent<ViewMotion> imple
* *
* @param motion indicates the row that was clicked on * @param motion indicates the row that was clicked on
*/ */
public onSpeakerIcon(motion: ViewMotion): void { public onSpeakerIcon(motion: ViewMotion, event: MouseEvent): void {
event.stopPropagation();
this.router.navigate([`/agenda/${motion.agenda_item_id}/speakers`]); this.router.navigate([`/agenda/${motion.agenda_item_id}/speakers`]);
} }
@ -166,4 +174,62 @@ export class MotionListComponent extends ListViewBaseComponent<ViewMotion> imple
this.translate.instant('Motions') + '.csv' this.translate.instant('Motions') + '.csv'
); );
} }
/**
* Deletes the items selected.
* SelectedRows is only filled with data in multiSelect mode
*/
public async deleteSelected(): Promise<void> {
const content = this.translate.instant('This will delete all selected motions.');
if (await this.promptService.open('Are you sure?', content)) {
for (const motion of this.selectedRows) {
await this.repo.delete(motion);
}
}
}
/**
* Set the status in bulk.
* SelectedRows is only filled with data in multiSelect mode
* TODO: currently not yet functional, because no status (or state_id) is being selected
* in the ui
* @param status TODO: May still change type
*/
public async setStatusSelected(status: Partial<WorkflowState>): Promise<void> {
// TODO: check if id is there
for (const motion of this.selectedRows) {
await this.repo.update({ state_id: status.id }, motion);
}
}
/**
* Set the category for all selected items.
* SelectedRows is only filled with data in multiSelect mode
* TODO: currently not yet functional, because no category is being selected in the ui
* @param category TODO: May still change type
*/
public async setCategorySelected(category: Partial<Category>): Promise<void> {
for (const motion of this.selectedRows) {
await this.repo.update({ state_id: category.id }, motion);
}
}
/**
* TODO: Open an extra submenu. Design still undecided. Will be used for deciding
* the status of setStatusSelected
*/
public openSetStatusMenu(): void {}
/**
* TODO: Open an extra submenu. Design still undecided. Will be used for deciding
* the status of setCategorySelected
*/
public openSetCategoryMenu(): void {}
public getColumnDefinition(): string[] {
if (this.isMultiSelect) {
return ['selector'].concat(this.columnsToDisplayMinWidth);
}
return this.columnsToDisplayMinWidth;
}
} }

View File

@ -123,7 +123,7 @@ export class MotionRepositoryService extends BaseRepository<ViewMotion, Motion>
public async update(update: Partial<Motion>, viewMotion: ViewMotion): Promise<void> { public async update(update: Partial<Motion>, viewMotion: ViewMotion): Promise<void> {
const motion = viewMotion.motion; const motion = viewMotion.motion;
motion.patchValues(update); motion.patchValues(update);
await this.dataSend.partialUpdateModel(motion); return await this.dataSend.partialUpdateModel(motion);
} }
/** /**
@ -134,7 +134,7 @@ export class MotionRepositoryService extends BaseRepository<ViewMotion, Motion>
* @param viewMotion * @param viewMotion
*/ */
public async delete(viewMotion: ViewMotion): Promise<void> { public async delete(viewMotion: ViewMotion): Promise<void> {
await this.dataSend.deleteModel(viewMotion.motion); return await this.dataSend.deleteModel(viewMotion.motion);
} }
/** /**

View File

@ -1,5 +1,5 @@
<os-head-bar [mainButton]="true" [nav]="true" [editMode]="editTag" <os-head-bar [mainButton]="true" [nav]="true" [editMode]="editTag"
(mainEvent)="setEditMode(!editTag)" (saveEvent)="saveTag()"> (mainEvent)="setEditMode(!editTag)" (saveEvent)="saveTag()" [multiSelectMode]="isMultiSelect">>
<!-- Title --> <!-- Title -->
<div class="title-slot"> <div class="title-slot">
<h2 *ngIf="!editTag && !newTag" translate>Tags</h2> <h2 *ngIf="!editTag && !newTag" translate>Tags</h2>
@ -14,11 +14,12 @@
<!-- remove button --> <!-- remove button -->
<div class="extra-controls-slot on-transition-fade"> <div class="extra-controls-slot on-transition-fade">
<button *ngIf="editTag && !newTag" type="button" mat-button (click)="deleteSelectedTag()"> <button *ngIf="!isMultiSelect && editTag && !newTag" type="button" mat-button (click)="deleteSelectedTag()">
<mat-icon>delete</mat-icon> <mat-icon>delete</mat-icon>
<span translate>Delete</span> <span translate>Delete</span>
</button> </button>
</div> </div>
</os-head-bar> </os-head-bar>
<mat-table class="os-listview-table on-transition-fade" [dataSource]="dataSource" matSort> <mat-table class="os-listview-table on-transition-fade" [dataSource]="dataSource" matSort>
@ -27,7 +28,7 @@
<mat-cell *matCellDef="let tag">{{ tag.getTitle() }}</mat-cell> <mat-cell *matCellDef="let tag">{{ tag.getTitle() }}</mat-cell>
</ng-container> </ng-container>
<mat-header-row *matHeaderRowDef="['name']"></mat-header-row> <mat-header-row *matHeaderRowDef="['name']"></mat-header-row>
<mat-row (click)="selectTag(row)" *matRowDef="let row; columns: ['name']"></mat-row> <mat-row (click)='selectItem(row, $event)' *matRowDef="let row; columns: ['name']"></mat-row>
</mat-table> </mat-table>
<mat-paginator class="on-transition-fade" [pageSizeOptions]="[25, 50, 75, 100, 125]"></mat-paginator> <mat-paginator class="on-transition-fade" [pageSizeOptions]="[25, 50, 75, 100, 125]"></mat-paginator>

View File

@ -116,10 +116,10 @@ export class TagListComponent extends ListViewBaseComponent<ViewTag> implements
} }
/** /**
* Select a row in the table * Handler for a click on a row in the table
* @param viewTag * @param viewTag
*/ */
public selectTag(viewTag: ViewTag): void { public singleSelectAction(viewTag: ViewTag): void {
this.selectedTag = viewTag; this.selectedTag = viewTag;
this.setEditMode(true, false); this.setEditMode(true, false);
this.tagForm.setValue({ name: this.selectedTag.name }); this.tagForm.setValue({ name: this.selectedTag.name });

View File

@ -1,4 +1,4 @@
<os-head-bar mainButton=true (mainEvent)=onPlusButton()> <os-head-bar mainButton=true (mainEvent)=onPlusButton() [multiSelectMode]="isMultiSelect">
<!-- Title --> <!-- Title -->
<div class="title-slot"> <div class="title-slot">
<h2 translate>Participants</h2> <h2 translate>Participants</h2>
@ -10,6 +10,21 @@
<mat-icon>more_vert</mat-icon> <mat-icon>more_vert</mat-icon>
</button> </button>
</div> </div>
<!-- Multiselect info -->
<div class="central-info-slot">
<button mat-icon-button (click)="toggleMultiSelect()">
<mat-icon>arrow_back</mat-icon>
</button>
<span>{{ selectedRows.length }}&nbsp;</span><span translate>selected</span>
</div>
<div class="extra-controls-slot on-transition-fade" *ngIf="isMultiSelect">
<button mat-icon-button (click)="deleteSelected()">
<mat-icon>delete</mat-icon>
</button>
</div>
</os-head-bar> </os-head-bar>
<div class="custom-table-header on-transition-fade"> <div class="custom-table-header on-transition-fade">
@ -22,6 +37,12 @@
</div> </div>
<mat-table class="os-listview-table on-transition-fade" [dataSource]="dataSource" matSort> <mat-table class="os-listview-table on-transition-fade" [dataSource]="dataSource" matSort>
<ng-container matColumnDef="selector">
<mat-header-cell *matHeaderCellDef mat-sort-header class="checkbox-cell"></mat-header-cell>
<mat-cell *matCellDef="let user" class="checkbox-cell">
<mat-icon>{{ isSelected(user) ? 'check_circle' : '' }}</mat-icon>
</mat-cell>
</ng-container>
<!-- name column --> <!-- name column -->
<ng-container matColumnDef="name"> <ng-container matColumnDef="name">
@ -58,25 +79,91 @@
</mat-cell> </mat-cell>
</ng-container> </ng-container>
<mat-header-row *matHeaderRowDef="['name', 'group', 'presence']"></mat-header-row> <mat-header-row *matHeaderRowDef="getColumnDefinition()"></mat-header-row>
<mat-row (click)="selectUser(row)" *matRowDef="let row; columns: ['name', 'group', 'presence']"></mat-row> <mat-row [ngClass]="selectedRows.indexOf(row) >= 0 ? 'selected': ''" (click)='selectItem(row, $event)'
*matRowDef="let row; columns: getColumnDefinition()">
</mat-row>
</mat-table> </mat-table>
<mat-paginator class="on-transition-fade" [pageSizeOptions]="[25, 50, 75, 100, 125]"></mat-paginator> <mat-paginator class="on-transition-fade" [pageSizeOptions]="[25, 50, 75, 100, 125]"></mat-paginator>
<mat-menu #userMenu="matMenu"> <mat-menu #userMenu="matMenu">
<button mat-menu-item *osPerms="'users.can_manage'" routerLink="groups"> <div *ngIf="!isMultiSelect">
<mat-icon>people</mat-icon> <button mat-menu-item *osPerms="'users.can_manage'" (click)="toggleMultiSelect()">
<span translate>Groups</span> <mat-icon>library_add</mat-icon>
</button> <span translate>MultiSelect</span>
</button>
<button mat-menu-item> <button mat-menu-item *osPerms="'users.can_manage'" routerLink="groups">
<mat-icon>save_alt</mat-icon> <mat-icon>people</mat-icon>
<span translate>Import ...</span> <span translate>Groups</span>
</button> </button>
<button mat-menu-item (click)="csvExportUserList()"> <button mat-menu-item>
<mat-icon>archive</mat-icon> <mat-icon>save_alt</mat-icon>
<span translate>Export as CSV</span> <span translate>Import ...</span>
</button> </button>
<button mat-menu-item (click)="csvExportUserList()">
<mat-icon>archive</mat-icon>
<span translate>Export as CSV</span>
</button>
</div>
<div *ngIf="isMultiSelect">
<div *osPerms="'users.can_manage'">
<button mat-menu-item (click)="setGroupSelected(null)">
<mat-icon>archive</mat-icon>
<span translate>Set groups</span>
<!-- TODO bottomsheet/menu? -->
</button>
<mat-divider></mat-divider>
<button mat-menu-item (click)="setActiveSelected(true)">
<mat-icon>add_circle</mat-icon>
<span translate>Set active</span>
</button>
<button mat-menu-item (click)="setActiveSelected(false)">
<mat-icon>remove_circle</mat-icon>
<span translate>Set inactive</span>
</button>
<mat-divider></mat-divider>
<button mat-menu-item (click)="setPresentSelected(true)">
<mat-icon>add_circle</mat-icon>
<span translate>Set as present</span>
</button>
<button mat-menu-item (click)="setPresentSelected(false)">
<mat-icon>remove_circle</mat-icon>
<span translate>Set as not present</span>
</button>
<mat-divider></mat-divider>
<button mat-menu-item (click)="setCommitteeSelected(true)">
<mat-icon>add_circle</mat-icon>
<span translate>Set as committee</span>
</button>
<button mat-menu-item (click)="setCommitteeSelected(false)">
<mat-icon>remove_circle</mat-icon>
<span translate>Unset committee</span>
</button>
<mat-divider></mat-divider>
<button mat-menu-item (click)="sendInvitationSelected()">
<mat-icon>mail</mat-icon>
<span translate>Send invitations</span>
</button>
<button mat-menu-item (click)="deleteSelected()">
<mat-icon>delete</mat-icon>
<span translate>Delete selected</span>
</button>
</div>
</div>
</mat-menu> </mat-menu>

View File

@ -8,6 +8,8 @@ import { UserRepositoryService } from '../../services/user-repository.service';
import { ListViewBaseComponent } from '../../../base/list-view-base'; import { ListViewBaseComponent } from '../../../base/list-view-base';
import { Router, ActivatedRoute } from '@angular/router'; import { Router, ActivatedRoute } from '@angular/router';
import { MatSnackBar } from '@angular/material'; import { MatSnackBar } from '@angular/material';
import { Group } from '../../../../shared/models/users/group';
import { PromptService } from '../../../../core/services/prompt.service';
/** /**
* Component for the user list view. * Component for the user list view.
@ -28,7 +30,8 @@ export class UserListComponent extends ListViewBaseComponent<ViewUser> implement
* @param repo the user repository * @param repo the user repository
* @param router the router service * @param router the router service
* @param route the local route * @param route the local route
* @param csvExport CSV export Service * @param csvExport CSV export Service,
* @param promptService
*/ */
public constructor( public constructor(
titleService: Title, titleService: Title,
@ -37,9 +40,13 @@ export class UserListComponent extends ListViewBaseComponent<ViewUser> implement
private repo: UserRepositoryService, private repo: UserRepositoryService,
private router: Router, private router: Router,
private route: ActivatedRoute, private route: ActivatedRoute,
protected csvExport: CsvExportService protected csvExport: CsvExportService,
private promptService: PromptService
) { ) {
super(titleService, translate, matSnackBar); super(titleService, translate, matSnackBar);
// enable multiSelect for this listView
this.canMultiSelect = true;
} }
/** /**
@ -52,6 +59,7 @@ export class UserListComponent extends ListViewBaseComponent<ViewUser> implement
this.initTable(); this.initTable();
this.repo.getViewModelListObservable().subscribe(newUsers => { this.repo.getViewModelListObservable().subscribe(newUsers => {
this.dataSource.data = newUsers; this.dataSource.data = newUsers;
this.checkSelection();
}); });
} }
@ -65,11 +73,10 @@ export class UserListComponent extends ListViewBaseComponent<ViewUser> implement
} }
/** /**
* Handles the click on a user row * Handles the click on a user row if not in multiSelect modus
*
* @param row selected row * @param row selected row
*/ */
public selectUser(row: ViewUser): void { public singleSelectAction(row: ViewUser): void {
this.router.navigate([`./${row.id}`], { relativeTo: this.route }); this.router.navigate([`./${row.id}`], { relativeTo: this.route });
} }
@ -103,4 +110,92 @@ export class UserListComponent extends ListViewBaseComponent<ViewUser> implement
this.translate.instant('Participants') + '.csv' this.translate.instant('Participants') + '.csv'
); );
} }
/**
* Bulk deletes users. Needs multiSelect mode to fill selectedRows
*/
public async deleteSelected(): Promise<void> {
const content = this.translate.instant('This will delete all selected assignments.');
if (await this.promptService.open('Are you sure?', content)) {
for (const user of this.selectedRows) {
await this.repo.delete(user);
}
}
}
/**
* TODO: Not yet as expected
* Bulk sets the group for users. TODO: Group is still not decided in the ui
* @param group TODO: type may still change
* @param unset toggle for adding or removing from the group
*/
public async setGroupSelected(group: Partial<Group>, unset?: boolean): Promise<void> {
this.selectedRows.forEach(vm => {
const groups = vm.groupIds;
const idx = groups.indexOf(group.id);
if (unset && idx >= 0) {
groups.slice(idx, 1);
} else if (!unset && idx < 0) {
groups.push(group.id);
}
});
}
/**
* Handler for bulk resetting passwords. Needs multiSelect mode.
* TODO: Not yet implemented (no service yet)
*/
public async resetPasswordsSelected(): Promise<void> {
// for (const user of this.selectedRows) {
// await this.resetPassword(user);
// }
}
/**
* Handler for bulk setting/unsetting the 'active' attribute.
* Uses selectedRows defined via multiSelect mode.
*/
public async setActiveSelected(active: boolean): Promise<void> {
for (const user of this.selectedRows) {
await this.repo.update({ is_active: active }, user);
}
}
/**
* Handler for bulk setting/unsetting the 'is present' attribute.
* Uses selectedRows defined via multiSelect mode.
*/
public async setPresentSelected(present: boolean): Promise<void> {
for (const user of this.selectedRows) {
await this.repo.update({ is_present: present }, user);
}
}
/**
* Handler for bulk setting/unsetting the 'is committee' attribute.
* Uses selectedRows defined via multiSelect mode.
*/
public async setCommitteeSelected(is_committee: boolean): Promise<void> {
for (const user of this.selectedRows) {
await this.repo.update({ is_committee: is_committee }, user);
}
}
/**
* Handler for bulk sending e-mail invitations. Uses selectedRows defined via
* multiSelect mode. TODO: Not yet implemented (no service)
*/
public async sendInvitationSelected(): Promise<void> {
// this.selectedRows.forEach(vm => {
// TODO if !vm.emailSent {vm.sendInvitation}
// });
}
public getColumnDefinition(): string[] {
const columns = ['name', 'group', 'presence'];
if (this.isMultiSelect) {
return ['selector'].concat(columns);
}
return columns;
}
} }

View File

@ -47,14 +47,14 @@ export class UserRepositoryService extends BaseRepository<ViewUser, User> {
updateUser.username = viewUser.username; updateUser.username = viewUser.username;
} }
await this.dataSend.updateModel(updateUser); return await this.dataSend.updateModel(updateUser);
} }
/** /**
* Deletes a given user * Deletes a given user
*/ */
public async delete(viewUser: ViewUser): Promise<void> { public async delete(viewUser: ViewUser): Promise<void> {
await this.dataSend.deleteModel(viewUser.user); return await this.dataSend.deleteModel(viewUser.user);
} }
/** /**

View File

@ -2,7 +2,7 @@
@include mat-core(); @include mat-core();
/** Import brand theme and (new) component themes */ /** Import brand theme and (new) component themes */
@import './assets/styles/openslides-theme'; @import './assets/styles/openslides-theme.scss';
@import './app/site/site.component.scss-theme'; @import './app/site/site.component.scss-theme';
@import '../node_modules/roboto-fontface/css/roboto/roboto-fontface.css'; @import '../node_modules/roboto-fontface/css/roboto/roboto-fontface.css';
@import '../node_modules/roboto-fontface/css/roboto-condensed/roboto-condensed-fontface.css'; @import '../node_modules/roboto-fontface/css/roboto-condensed/roboto-condensed-fontface.css';
@ -110,6 +110,10 @@ body {
cursor: pointer; cursor: pointer;
background-color: rgba(0, 0, 0, 0.025); background-color: rgba(0, 0, 0, 0.025);
} }
mat-row.selected {
cursor: pointer;
background-color: rgba(0, 0, 0, 0.055);
}
} }
.card-plus-distance { .card-plus-distance {
@ -153,6 +157,18 @@ mat-panel-title mat-icon {
padding-right: 30px; padding-right: 30px;
} }
.hidden-cell {
flex: 0;
width: 0;
display: none;
}
.checkbox-cell {
flex: 1;
max-width: 30px;
}
// ngx-file-drop requires the custom style in the global css file // ngx-file-drop requires the custom style in the global css file
.file-drop-style { .file-drop-style {
margin: auto; margin: auto;