diff --git a/client/src/app/shared/components/head-bar/head-bar.component.html b/client/src/app/shared/components/head-bar/head-bar.component.html index cd077f0da..575dd3f56 100644 --- a/client/src/app/shared/components/head-bar/head-bar.component.html +++ b/client/src/app/shared/components/head-bar/head-bar.component.html @@ -1,13 +1,13 @@ - - + +
- - @@ -16,22 +16,28 @@ close -
+
-
-
+ +
+
+ +
+
+ +
- @@ -46,6 +52,8 @@ - diff --git a/client/src/app/shared/components/head-bar/head-bar.component.scss b/client/src/app/shared/components/head-bar/head-bar.component.scss index dd75f5d1b..9eb650aea 100644 --- a/client/src/app/shared/components/head-bar/head-bar.component.scss +++ b/client/src/app/shared/components/head-bar/head-bar.component.scss @@ -18,6 +18,10 @@ margin-left: 10px; } } +.toolbar-centered { + margin: auto; + vertical-align: baseline; +} .toolbar-right { display: contents; @@ -29,3 +33,7 @@ display: flex; } } + +mat-toolbar.multi-select { + background-color: #757575; +} diff --git a/client/src/app/shared/components/head-bar/head-bar.component.ts b/client/src/app/shared/components/head-bar/head-bar.component.ts index a7309cd33..3cda0c940 100644 --- a/client/src/app/shared/components/head-bar/head-bar.component.ts +++ b/client/src/app/shared/components/head-bar/head-bar.component.ts @@ -20,6 +20,7 @@ import { MainMenuService } from '../../../core/services/main-menu.service'; * [mainButton]="opCanEdit()" * [mainButtonIcon]="edit" * [editMode]="editMotion" + * [multiSelectMode]="isMultiSelect" * (mainEvent)="setEditMode(!editMotion)" * (saveEvent)="saveMotion()"> * @@ -34,6 +35,13 @@ import { MainMenuService } from '../../../core/services/main-menu.service'; * more_vert * *
+ * + *
+ * + * {{ selectedRows.length }} selected + *
* * ``` */ @@ -61,6 +69,12 @@ export class HeadBarComponent { @Input() public editMode = false; + /** + * Determine multiSelect mode: changed interactions and head bar + */ + @Input() + public multiSelectMode = false; + /** * Determine if there should be the main action button */ diff --git a/client/src/app/site/agenda/components/agenda-list/agenda-list.component.html b/client/src/app/site/agenda/components/agenda-list/agenda-list.component.html index 3e9869fc5..9c79e730e 100644 --- a/client/src/app/site/agenda/components/agenda-list/agenda-list.component.html +++ b/client/src/app/site/agenda/components/agenda-list/agenda-list.component.html @@ -1,21 +1,50 @@ - +

Agenda

+ + + + +
+ + {{ selectedRows.length }} selected +
+ +
+ +
+
+ + + + + {{ isSelected(item) ? 'check_circle' : '' }} + + + Topic - {{ item.getListTitle() }} + {{ item.getListTitle() }} Duration - {{ item.duration }} + {{ item.duration }} @@ -32,7 +61,45 @@ - - + + + + +
+ +
+ +
+
+ + + + + + + + +
+
+
diff --git a/client/src/app/site/agenda/components/agenda-list/agenda-list.component.ts b/client/src/app/site/agenda/components/agenda-list/agenda-list.component.ts index 501b9e97a..dc382bd79 100644 --- a/client/src/app/site/agenda/components/agenda-list/agenda-list.component.ts +++ b/client/src/app/site/agenda/components/agenda-list/agenda-list.component.ts @@ -2,11 +2,12 @@ import { Component, OnInit } from '@angular/core'; import { Router, ActivatedRoute } from '@angular/router'; import { Title } from '@angular/platform-browser'; import { MatSnackBar } from '@angular/material'; - import { TranslateService } from '@ngx-translate/core'; + import { ViewItem } from '../../models/view-item'; import { ListViewBaseComponent } from 'app/site/base/list-view-base'; import { AgendaRepositoryService } from '../../services/agenda-repository.service'; +import { PromptService } from '../../../../core/services/prompt.service'; /** * List view for the agenda. @@ -24,7 +25,9 @@ export class AgendaListComponent extends ListViewBaseComponent impleme * @param matSnackBar Shows errors and messages * @param route Angulars ActivatedRoute * @param router Angulars router - * @param repo the agenda repository + * @param repo the agenda repository, + * promptService: + * */ public constructor( titleService: Title, @@ -32,9 +35,13 @@ export class AgendaListComponent extends ListViewBaseComponent impleme matSnackBar: MatSnackBar, private route: ActivatedRoute, private router: Router, - private repo: AgendaRepositoryService + private repo: AgendaRepositoryService, + private promptService: PromptService ) { super(titleService, translate, matSnackBar); + + // activate multiSelect mode for this listview + this.canMultiSelect = true; } /** @@ -46,18 +53,17 @@ export class AgendaListComponent extends ListViewBaseComponent impleme this.initTable(); this.repo.getViewModelListObservable().subscribe(newAgendaItem => { this.dataSource.data = newAgendaItem; + this.checkSelection(); }); } /** - * Handler for click events on agenda item rows - * Links to the content object if any - * + * Handler for click events on an agenda item row. Links to the content object * Gets content object from the repository rather than from the model * to avoid race conditions * @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); this.router.navigate([contentObject.getDetailStateURL()]); } @@ -77,4 +83,47 @@ export class AgendaListComponent extends ListViewBaseComponent impleme public onPlusButton(): void { 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 { + 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 { + 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 { + 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; + } } diff --git a/client/src/app/site/assignments/assignment-list/assignment-list.component.html b/client/src/app/site/assignments/assignment-list/assignment-list.component.html index a6e6810d3..125909d60 100644 --- a/client/src/app/site/assignments/assignment-list/assignment-list.component.html +++ b/client/src/app/site/assignments/assignment-list/assignment-list.component.html @@ -1,18 +1,40 @@ - +
+

Elections

- + + +
+ + {{ selectedRows.length }} selected +
+ +
+ +
+
+ + + + + {{ isSelected(assignment) ? 'check_circle' : '' }} + + Title @@ -35,15 +57,30 @@ - - + + + - +
+ + +
+ +
+ +
diff --git a/client/src/app/site/assignments/assignment-list/assignment-list.component.ts b/client/src/app/site/assignments/assignment-list/assignment-list.component.ts index 5db9e9dad..fc6bed6e2 100644 --- a/client/src/app/site/assignments/assignment-list/assignment-list.component.ts +++ b/client/src/app/site/assignments/assignment-list/assignment-list.component.ts @@ -5,6 +5,7 @@ import { ViewAssignment } from '../models/view-assignment'; import { ListViewBaseComponent } from '../../base/list-view-base'; import { AssignmentRepositoryService } from '../services/assignment-repository.service'; import { MatSnackBar } from '@angular/material'; +import { PromptService } from '../../../core/services/prompt.service'; /** * Listview for the assignments @@ -23,14 +24,18 @@ export class AssignmentListComponent extends ListViewBaseComponent { this.dataSource.data = newAssignments; + this.checkSelection(); }); } @@ -53,10 +59,10 @@ export class AssignmentListComponent extends ListViewBaseComponent { + 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; + } } diff --git a/client/src/app/site/base/list-view-base.ts b/client/src/app/site/base/list-view-base.ts index f392ca7b1..7fc7e2d6f 100644 --- a/client/src/app/site/base/list-view-base.ts +++ b/client/src/app/site/base/list-view-base.ts @@ -11,6 +11,22 @@ export abstract class ListViewBaseComponent extends Bas */ public dataSource: MatTableDataSource; + /** + * 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 */ @@ -37,6 +53,7 @@ export abstract class ListViewBaseComponent extends Bas */ public constructor(titleService: Title, translate: TranslateService, matSnackBar: MatSnackBar) { super(titleService, translate, matSnackBar); + this.selectedRows = []; } /** @@ -49,4 +66,88 @@ export abstract class ListViewBaseComponent extends Bas this.dataSource.paginator = this.paginator; 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; + } } diff --git a/client/src/app/site/mediafiles/components/mediafile-list/mediafile-list.component.html b/client/src/app/site/mediafiles/components/mediafile-list/mediafile-list.component.html index ca66f2455..a98624a83 100644 --- a/client/src/app/site/mediafiles/components/mediafile-list/mediafile-list.component.html +++ b/client/src/app/site/mediafiles/components/mediafile-list/mediafile-list.component.html @@ -1,4 +1,11 @@ - + +

Files

@@ -37,19 +44,48 @@ more_vert
+ + +
+ +
+ + +
+ + {{ selectedRows.length }} selected +
+ +
+ +
+ + + + + {{ isSelected(item) ? 'check_circle' : '' }} + + + Name - {{ file.title }} + {{ file.title }} Group - +
insert_drive_file {{ file.type }} data_usage {{ file.size }} @@ -86,7 +122,8 @@ - + @@ -130,9 +167,14 @@ - - + + + diff --git a/client/src/app/site/mediafiles/components/mediafile-list/mediafile-list.component.ts b/client/src/app/site/mediafiles/components/mediafile-list/mediafile-list.component.ts index 5b19bdfeb..84c110c53 100644 --- a/client/src/app/site/mediafiles/components/mediafile-list/mediafile-list.component.ts +++ b/client/src/app/site/mediafiles/components/mediafile-list/mediafile-list.component.ts @@ -84,6 +84,9 @@ export class MediafileListComponent extends ListViewBaseComponent public vp: ViewportService ) { super(titleService, translate, matSnackBar); + + // emables multiSelection for this listView + this.canMultiSelect = true; } /** @@ -170,18 +173,15 @@ export class MediafileListComponent extends ListViewBaseComponent } /** - * triggers a routine to delete all MediaFiles - * TODO: Remove after Multiselect - * - * @deprecated to be removed once multi selection is implemented + * Handler to delete several files at once. Requires data in selectedRows, which + * will be made available in multiSelect mode */ - public async onDeleteAllFiles(): Promise { - const content = this.translate.instant('This will delete all files.'); + public async deleteSelected(): Promise { + const content = this.translate.instant('This will delete all selected files.'); if (await this.promptService.open('Are you sure?', content)) { - const viewMediafiles = this.dataSource.data; - viewMediafiles.forEach(file => { - this.repo.delete(file); - }); + for (const mediafile of this.selectedRows) { + await this.repo.delete(mediafile); + } } } @@ -257,7 +257,11 @@ export class MediafileListComponent extends ListViewBaseComponent * @returns the column definition for the screen size */ 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 * * @param file the select file to download */ - public download(file: ViewMediafile): void { + public singleSelectAction(file: ViewMediafile): void { window.open(file.downloadUrl); } diff --git a/client/src/app/site/motions/components/motion-list/motion-list.component.html b/client/src/app/site/motions/components/motion-list/motion-list.component.html index 5c8abe65b..8a55fc0b1 100644 --- a/client/src/app/site/motions/components/motion-list/motion-list.component.html +++ b/client/src/app/site/motions/components/motion-list/motion-list.component.html @@ -1,4 +1,4 @@ - +

Motions

@@ -10,6 +10,21 @@ more_vert
+ + +
+ + {{ selectedRows.length }} selected +
+ +
+ +
+
@@ -22,10 +37,17 @@
+ + + + {{ isSelected(motion) ? 'check_circle' : '' }} + + + Identifier - +
{{ motion.identifier }}
@@ -35,7 +57,7 @@ Title - +
{{ motion.title }}
@@ -50,7 +72,7 @@ State - + @@ -64,7 +86,7 @@ Speakers - +
+ + - + - - - - + +
+
+
+ + + +
+ +
diff --git a/client/src/app/site/motions/components/motion-list/motion-list.component.ts b/client/src/app/site/motions/components/motion-list/motion-list.component.ts index bce0701c8..02e55a65f 100644 --- a/client/src/app/site/motions/components/motion-list/motion-list.component.ts +++ b/client/src/app/site/motions/components/motion-list/motion-list.component.ts @@ -1,7 +1,6 @@ import { Component, OnInit } from '@angular/core'; import { Router, ActivatedRoute } from '@angular/router'; import { Title } from '@angular/platform-browser'; - import { TranslateService } from '@ngx-translate/core'; import { MotionRepositoryService } from '../../services/motion-repository.service'; @@ -11,6 +10,8 @@ import { ListViewBaseComponent } from '../../../base/list-view-base'; import { MatSnackBar } from '@angular/material'; import { ConfigService } from '../../../../core/services/config.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. @@ -22,13 +23,14 @@ import { CsvExportService } from 'app/core/services/csv-export.service'; }) export class MotionListComponent extends ListViewBaseComponent 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']; /** - * 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 */ public columnsToDisplayFullWidth = ['identifier', 'title', 'state', 'speakers']; @@ -50,6 +52,7 @@ export class MotionListComponent extends ListViewBaseComponent imple * @param configService The configuration provider * @param repo Motion Repository * @param csvExport CSV Export Service + * @param promptService */ public constructor( titleService: Title, @@ -59,9 +62,13 @@ export class MotionListComponent extends ListViewBaseComponent imple private route: ActivatedRoute, private configService: ConfigService, private repo: MotionRepositoryService, - private csvExport: CsvExportService + private csvExport: CsvExportService, + private promptService: PromptService ) { super(titleService, translate, matSnackBar); + + // enable multiSelect for this listView + this.canMultiSelect = true; } /** @@ -73,6 +80,7 @@ export class MotionListComponent extends ListViewBaseComponent imple super.setTitle('Motions'); this.initTable(); this.repo.getViewModelListObservable().subscribe(newMotions => { + this.checkSelection(); // TODO: This is for testing purposes. Can be removed with #3963 this.dataSource.data = newMotions.sort((a, b) => { if (a.callListWeight !== b.callListWeight) { @@ -90,11 +98,10 @@ export class MotionListComponent extends ListViewBaseComponent 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 */ - public selectMotion(motion: ViewMotion): void { + public singleSelectAction(motion: ViewMotion): void { this.router.navigate(['./' + motion.id], { relativeTo: this.route }); } @@ -137,7 +144,8 @@ export class MotionListComponent extends ListViewBaseComponent imple * * @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`]); } @@ -166,4 +174,62 @@ export class MotionListComponent extends ListViewBaseComponent imple this.translate.instant('Motions') + '.csv' ); } + + /** + * Deletes the items selected. + * SelectedRows is only filled with data in multiSelect mode + */ + public async deleteSelected(): Promise { + 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): Promise { + // 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): Promise { + 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; + } } diff --git a/client/src/app/site/motions/services/motion-repository.service.ts b/client/src/app/site/motions/services/motion-repository.service.ts index 097f5ae1c..cdee7a979 100644 --- a/client/src/app/site/motions/services/motion-repository.service.ts +++ b/client/src/app/site/motions/services/motion-repository.service.ts @@ -123,7 +123,7 @@ export class MotionRepositoryService extends BaseRepository public async update(update: Partial, viewMotion: ViewMotion): Promise { const motion = viewMotion.motion; motion.patchValues(update); - await this.dataSend.partialUpdateModel(motion); + return await this.dataSend.partialUpdateModel(motion); } /** @@ -134,7 +134,7 @@ export class MotionRepositoryService extends BaseRepository * @param viewMotion */ public async delete(viewMotion: ViewMotion): Promise { - await this.dataSend.deleteModel(viewMotion.motion); + return await this.dataSend.deleteModel(viewMotion.motion); } /** diff --git a/client/src/app/site/tags/components/tag-list/tag-list.component.html b/client/src/app/site/tags/components/tag-list/tag-list.component.html index fcf15b07e..031a29810 100644 --- a/client/src/app/site/tags/components/tag-list/tag-list.component.html +++ b/client/src/app/site/tags/components/tag-list/tag-list.component.html @@ -1,5 +1,5 @@ + (mainEvent)="setEditMode(!editTag)" (saveEvent)="saveTag()" [multiSelectMode]="isMultiSelect">>

Tags

@@ -14,11 +14,12 @@
-
+ @@ -27,7 +28,7 @@ {{ tag.getTitle() }} - + diff --git a/client/src/app/site/tags/components/tag-list/tag-list.component.ts b/client/src/app/site/tags/components/tag-list/tag-list.component.ts index 4c85aa381..4465c7aab 100644 --- a/client/src/app/site/tags/components/tag-list/tag-list.component.ts +++ b/client/src/app/site/tags/components/tag-list/tag-list.component.ts @@ -116,10 +116,10 @@ export class TagListComponent extends ListViewBaseComponent implements } /** - * Select a row in the table + * Handler for a click on a row in the table * @param viewTag */ - public selectTag(viewTag: ViewTag): void { + public singleSelectAction(viewTag: ViewTag): void { this.selectedTag = viewTag; this.setEditMode(true, false); this.tagForm.setValue({ name: this.selectedTag.name }); diff --git a/client/src/app/site/users/components/user-list/user-list.component.html b/client/src/app/site/users/components/user-list/user-list.component.html index 35721ab7d..336da2017 100644 --- a/client/src/app/site/users/components/user-list/user-list.component.html +++ b/client/src/app/site/users/components/user-list/user-list.component.html @@ -1,4 +1,4 @@ - +

Participants

@@ -10,6 +10,21 @@ more_vert
+ + +
+ + {{ selectedRows.length }} selected +
+ +
+ +
+
@@ -22,6 +37,12 @@
+ + + + {{ isSelected(user) ? 'check_circle' : '' }} + + @@ -58,25 +79,91 @@ - - + + + - +
+ - + - + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+
diff --git a/client/src/app/site/users/components/user-list/user-list.component.ts b/client/src/app/site/users/components/user-list/user-list.component.ts index 74f62376d..ed94aec96 100644 --- a/client/src/app/site/users/components/user-list/user-list.component.ts +++ b/client/src/app/site/users/components/user-list/user-list.component.ts @@ -8,6 +8,8 @@ import { UserRepositoryService } from '../../services/user-repository.service'; import { ListViewBaseComponent } from '../../../base/list-view-base'; import { Router, ActivatedRoute } from '@angular/router'; 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. @@ -28,7 +30,8 @@ export class UserListComponent extends ListViewBaseComponent implement * @param repo the user repository * @param router the router service * @param route the local route - * @param csvExport CSV export Service + * @param csvExport CSV export Service, + * @param promptService */ public constructor( titleService: Title, @@ -37,9 +40,13 @@ export class UserListComponent extends ListViewBaseComponent implement private repo: UserRepositoryService, private router: Router, private route: ActivatedRoute, - protected csvExport: CsvExportService + protected csvExport: CsvExportService, + private promptService: PromptService ) { super(titleService, translate, matSnackBar); + + // enable multiSelect for this listView + this.canMultiSelect = true; } /** @@ -52,6 +59,7 @@ export class UserListComponent extends ListViewBaseComponent implement this.initTable(); this.repo.getViewModelListObservable().subscribe(newUsers => { this.dataSource.data = newUsers; + this.checkSelection(); }); } @@ -65,11 +73,10 @@ export class UserListComponent extends ListViewBaseComponent implement } /** - * Handles the click on a user row - * + * Handles the click on a user row if not in multiSelect modus * @param row selected row */ - public selectUser(row: ViewUser): void { + public singleSelectAction(row: ViewUser): void { this.router.navigate([`./${row.id}`], { relativeTo: this.route }); } @@ -103,4 +110,92 @@ export class UserListComponent extends ListViewBaseComponent implement this.translate.instant('Participants') + '.csv' ); } + + /** + * Bulk deletes users. Needs multiSelect mode to fill selectedRows + */ + public async deleteSelected(): Promise { + 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, unset?: boolean): Promise { + 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 { + // 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 { + 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 { + 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 { + 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 { + // 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; + } } diff --git a/client/src/app/site/users/services/user-repository.service.ts b/client/src/app/site/users/services/user-repository.service.ts index 2153d2be7..33acc7ec6 100644 --- a/client/src/app/site/users/services/user-repository.service.ts +++ b/client/src/app/site/users/services/user-repository.service.ts @@ -47,14 +47,14 @@ export class UserRepositoryService extends BaseRepository { updateUser.username = viewUser.username; } - await this.dataSend.updateModel(updateUser); + return await this.dataSend.updateModel(updateUser); } /** * Deletes a given user */ public async delete(viewUser: ViewUser): Promise { - await this.dataSend.deleteModel(viewUser.user); + return await this.dataSend.deleteModel(viewUser.user); } /** diff --git a/client/src/styles.scss b/client/src/styles.scss index 43d478db3..111630f42 100644 --- a/client/src/styles.scss +++ b/client/src/styles.scss @@ -2,7 +2,7 @@ @include mat-core(); /** 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 '../node_modules/roboto-fontface/css/roboto/roboto-fontface.css'; @import '../node_modules/roboto-fontface/css/roboto-condensed/roboto-condensed-fontface.css'; @@ -110,6 +110,10 @@ body { cursor: pointer; background-color: rgba(0, 0, 0, 0.025); } + mat-row.selected { + cursor: pointer; + background-color: rgba(0, 0, 0, 0.055); + } } .card-plus-distance { @@ -153,6 +157,18 @@ mat-panel-title mat-icon { 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 .file-drop-style { margin: auto;