Merge pull request #4075 from MaximilianKrambach/multiSelect-tweaks

Multiselect dialogs with multiple choice options
This commit is contained in:
Emanuel Schütze 2019-01-07 08:39:01 +01:00 committed by GitHub
commit 256f4e75a3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 275 additions and 206 deletions

View File

@ -27,17 +27,32 @@ export class ChoiceService extends OpenSlidesComponent {
* Opens the dialog. Returns the chosen value after the user accepts. * Opens the dialog. Returns the chosen value after the user accepts.
* @param title The title to display in the dialog * @param title The title to display in the dialog
* @param choices The available choices * @param choices The available choices
* @param multiSelect turn on the option to select multiple entries.
* The answer.items will then be an array.
* @param actions optional strings for buttons replacing the regular confirmation.
* The answer.action will reflect the button selected
* @param clearChoice A string for an extra, visually slightly separated
* choice for 'explicitly set an empty selection'. The answer's action may
* have this string's value
* @returns an answer {@link ChoiceAnswer} * @returns an answer {@link ChoiceAnswer}
*/ */
public async open( public async open(
title: string, title: string,
choices: ChoiceDialogOptions, choices: ChoiceDialogOptions,
multiSelect: boolean = false multiSelect: boolean = false,
actions?: string[],
clearChoice?: string
): Promise<ChoiceAnswer> { ): Promise<ChoiceAnswer> {
const dialogRef = this.dialog.open(ChoiceDialogComponent, { const dialogRef = this.dialog.open(ChoiceDialogComponent, {
minWidth: '250px', minWidth: '250px',
maxHeight: '90vh', maxHeight: '90vh',
data: { title: title, choices: choices, multiSelect: multiSelect } data: {
title: title,
choices: choices,
multiSelect: multiSelect,
actionButtons: actions,
clearChoice: clearChoice
}
}); });
return dialogRef.afterClosed().toPromise(); return dialogRef.afterClosed().toPromise();
} }

View File

@ -4,29 +4,41 @@
<mat-radio-group <mat-radio-group
#radio #radio
name="choice" name="choice"
*ngIf="!data.multiSelect" *ngIf="!data.multiSelect && data.choices"
class="choice-radio-group" class="choice-radio-group"
[(ngModel)]="selectedChoice" [(ngModel)]="selectedChoice"
> >
<mat-radio-button class="choice-button" *ngFor="let choice of data.choices" [value]="choice.id"> <mat-radio-button class="choice-button" *ngFor="let choice of data.choices" [value]="choice.id">
{{ getChoiceTitle(choice) | translate }} {{ getChoiceTitle(choice) | translate }}
</mat-radio-button> </mat-radio-button>
<mat-divider *ngIf="data.clearChoice"></mat-divider>
<mat-radio-button *ngIf="data.clearChoice" [value]="null">
{{ data.clearChoice | translate }}
</mat-radio-button>
</mat-radio-group> </mat-radio-group>
<mat-list *ngIf="data.multiSelect"> <mat-list *ngIf="data.multiSelect && data.choices">
<mat-list-item *ngFor="let choice of data.choices"> <mat-list-item *ngFor="let choice of data.choices">
<mat-checkbox [checked]="isChosen(choice)" (change)="toggleChoice(choice)"> <mat-checkbox [checked]="isChosen(choice)" (change)="toggleChoice(choice)">
{{ getChoiceTitle(choice) | translate }} {{ getChoiceTitle(choice) | translate }}
</mat-checkbox> </mat-checkbox>
</mat-list-item> </mat-list-item>
</mat-list> </mat-list>
<span *ngIf="!data.choices.length" translate>No choices available</span>
</div> </div>
<mat-dialog-actions> <mat-dialog-actions>
<div *ngIf="data.actionButtons">
<button *ngFor="let button of data.actionButtons" mat-button (click)="closeDialog(true, button)">
<span translate>{{ button }}</span>
</button>
</div>
<div *ngIf="!data.actionButtons">
<button *ngIf="!data.multiSelect || data.choices.length" mat-button (click)="closeDialog(true)"> <button *ngIf="!data.multiSelect || data.choices.length" mat-button (click)="closeDialog(true)">
<span translate>Ok</span> <span translate>Ok</span>
</button> </button>
</div>
<button mat-button (click)="closeDialog(false)"><span translate>Cancel</span></button> <button mat-button (click)="closeDialog(false)"><span translate>Cancel</span></button>
</mat-dialog-actions> </mat-dialog-actions>
</div> </div>

View File

@ -15,3 +15,7 @@ mat-radio-group {
.scrollmenu-outer { .scrollmenu-outer {
max-height: inherit; max-height: inherit;
} }
mat-divider {
margin-top: 10px;
margin-bottom: 10px;
}

View File

@ -29,19 +29,33 @@ interface ChoiceDialogData {
choices: ChoiceDialogOptions; choices: ChoiceDialogOptions;
/** /**
* Select, if this should be a multiselect choice * Select if this should be a multiselect choice
*/ */
multiSelect: boolean; multiSelect: boolean;
/**
* Additional action buttons which will add their value to the
* {@link closeDialog} feedback if chosen
*/
actionButtons?: string[];
/**
* An optional string for 'explicitly select none of the options'. Only
* displayed in the single-select variation
*/
clearChoice?: string;
} }
/** /**
* undefined is returned, if the dialog is closed. If a choice is submitted, * undefined is returned, if the dialog is closed. If a choice is submitted,
* it might be a number oder an array of numbers for multiselect. * it will be an array of numbers and optionally an action string for multichoice
* dialogs
*/ */
export type ChoiceAnswer = undefined | number | number[]; export type ChoiceAnswer = undefined | { action?: string; items: number | number[]};
/** /**
* A dialog with choice fields. * A dialog with choice fields.
*
*/ */
@Component({ @Component({
selector: 'os-choice-dialog', selector: 'os-choice-dialog',
@ -81,9 +95,15 @@ export class ChoiceDialogComponent {
/** /**
* Closes the dialog with the selected choices * Closes the dialog with the selected choices
*/ */
public closeDialog(ok: boolean): void { public closeDialog(ok: boolean, action?: string): void {
if (!this.data.multiSelect && this.selectedChoice === null) {
action = this.data.clearChoice;
}
if (ok) { if (ok) {
this.dialogRef.close(this.data.multiSelect ? this.selectedMultiChoices : this.selectedChoice); this.dialogRef.close({
action: action ? action : null,
items: this.data.multiSelect ? this.selectedMultiChoices : this.selectedChoice
});
} else { } else {
this.dialogRef.close(); this.dialogRef.close();
} }

View File

@ -12,9 +12,6 @@
<span>{{ selectedRows.length }}&nbsp;</span><span translate>selected</span> <span>{{ selectedRows.length }}&nbsp;</span><span translate>selected</span>
</div> </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>
@ -110,12 +107,6 @@
</div> </div>
<div *ngIf="isMultiSelect"> <div *ngIf="isMultiSelect">
<!-- Exit multi select -->
<button mat-menu-item (click)="toggleMultiSelect()">
<mat-icon>library_add</mat-icon>
<span translate>Exit multiselect</span>
</button>
<!-- Select all --> <!-- Select all -->
<button mat-menu-item (click)="selectAll()"> <button mat-menu-item (click)="selectAll()">
<mat-icon>done_all</mat-icon> <mat-icon>done_all</mat-icon>

View File

@ -13,10 +13,6 @@
<button mat-icon-button (click)="toggleMultiSelect()"><mat-icon>arrow_back</mat-icon></button> <button mat-icon-button (click)="toggleMultiSelect()"><mat-icon>arrow_back</mat-icon></button>
<span>{{ selectedRows.length }}&nbsp;</span><span translate>selected</span> <span>{{ selectedRows.length }}&nbsp;</span><span translate>selected</span>
</div> </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>
@ -40,10 +36,6 @@
<mat-chip color="primary" selected>{{ assignment.phase }}</mat-chip> <mat-chip color="primary" selected>{{ assignment.phase }}</mat-chip>
</mat-chip-list> </mat-chip-list>
</mat-cell> </mat-cell>
<button mat-menu-item (click)="toggleMultiSelect()">
<mat-icon>library_add</mat-icon>
<span translate>Exit multiselect</span>
</button>
<button mat-menu-item (click)="selectAll()"> <button mat-menu-item (click)="selectAll()">
<mat-icon>done_all</mat-icon> <mat-icon>done_all</mat-icon>
<span translate>Select all</span> <span translate>Select all</span>
@ -87,10 +79,6 @@
</div> </div>
<div *ngIf="isMultiSelect"> <div *ngIf="isMultiSelect">
<button mat-menu-item (click)="toggleMultiSelect()">
<mat-icon>library_add</mat-icon>
<span translate>Exit multiselect</span>
</button>
<button mat-menu-item (click)="selectAll()"> <button mat-menu-item (click)="selectAll()">
<mat-icon>done_all</mat-icon> <mat-icon>done_all</mat-icon>
<span translate>Select all</span> <span translate>Select all</span>

View File

@ -46,9 +46,6 @@
<span>{{ selectedRows.length }}&nbsp;</span><span translate>selected</span> <span>{{ selectedRows.length }}&nbsp;</span><span translate>selected</span>
</div> </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>
@ -160,10 +157,6 @@
</div> </div>
<div *ngIf="isMultiSelect"> <div *ngIf="isMultiSelect">
<mat-divider></mat-divider> <mat-divider></mat-divider>
<button mat-menu-item (click)="toggleMultiSelect()">
<mat-icon>library_add</mat-icon>
<span translate>Exit multiselect</span>
</button>
<button mat-menu-item (click)="selectAll()"> <button mat-menu-item (click)="selectAll()">
<mat-icon>done_all</mat-icon> <mat-icon>done_all</mat-icon>
<span translate>Select all</span> <span translate>Select all</span>

View File

@ -152,12 +152,12 @@
<mat-icon>speaker_notes</mat-icon> <mat-icon>speaker_notes</mat-icon>
<span translate>Comment fields</span> <span translate>Comment fields</span>
</button> </button>
<button mat-menu-item (click)="csvExportMotionList()">
<mat-icon>archive</mat-icon>
<span translate>Export as CSV</span>
</button>
</div> </div>
<div *ngIf="isMultiSelect"> <div *ngIf="isMultiSelect">
<button mat-menu-item (click)="toggleMultiSelect()">
<mat-icon>library_add</mat-icon>
<span translate>Exit multiselect</span>
</button>
<button mat-menu-item (click)="selectAll()"> <button mat-menu-item (click)="selectAll()">
<mat-icon>done_all</mat-icon> <mat-icon>done_all</mat-icon>
<span translate>Select all</span> <span translate>Select all</span>
@ -168,53 +168,47 @@
</button> </button>
<div *osPerms="'motions.can_manage'"> <div *osPerms="'motions.can_manage'">
<mat-divider></mat-divider> <mat-divider></mat-divider>
<button mat-menu-item (click)="multiselectWrapper(multiselectService.setStateOfMultiple(selectedRows))">
<mat-icon>label</mat-icon>
<span translate>Set status</span>
</button>
<button *ngIf="recomendationEnabled" mat-menu-item
(click)="multiselectWrapper(multiselectService.setRecommendation(selectedRows))">
<mat-icon>report</mat-icon>
<!-- TODO: better icon -->
<span translate>Set recommendation</span>
</button>
<button mat-menu-item *ngIf="categories.length"
(click)="multiselectWrapper(multiselectService.setCategory(selectedRows))">
<mat-icon>device_hub</mat-icon>
<!-- TODO: icon -->
<span translate>Set category</span>
</button>
<button mat-menu-item *ngIf="motionBlocks.length"
(click)="multiselectWrapper(multiselectService.setMotionBlock(selectedRows))">
<mat-icon>widgets</mat-icon>
<!-- TODO: icon -->
<span translate>Set motion block</span>
</button>
<button mat-menu-item (click)="multiselectWrapper(multiselectService.changeSubmitters(selectedRows))">
<mat-icon>person_add</mat-icon>
<!-- TODO: icon -->
<span translate>Add/remove submitters</span>
</button>
<button mat-menu-item *ngIf="tags.length"
(click)="multiselectWrapper(multiselectService.changeTags(selectedRows))">
<mat-icon>bookmarks</mat-icon>
<!-- TODO: icon -->
<span translate>Add/remove tags</span>
</button>
<button mat-menu-item (click)="multiselectWrapper(multiselectService.moveToItem(selectedRows))"> <button mat-menu-item (click)="multiselectWrapper(multiselectService.moveToItem(selectedRows))">
<!-- TODO: Not implemented yet --> <!-- TODO: Not implemented yet -->
<mat-icon>sort</mat-icon> <mat-icon>sort</mat-icon>
<span translate>Move to agenda item</span> <span translate>Move to agenda item</span>
</button> </button>
<button mat-menu-item (click)="multiselectWrapper(multiselectService.setStateOfMultiple(selectedRows))">
<mat-icon>label</mat-icon>
<span translate>Set status</span>
</button>
<button mat-menu-item (click)="multiselectWrapper(multiselectService.setRecommendation(selectedRows))">
<mat-icon>report</mat-icon>
<!-- TODO: better icon -->
<span translate>Set recommendation</span>
</button>
<button mat-menu-item (click)="multiselectWrapper(multiselectService.setCategory(selectedRows))">
<mat-icon>device_hub</mat-icon>
<!-- TODO: icon -->
<span translate>Set category</span>
</button>
<mat-divider></mat-divider>
<button mat-menu-item (click)="multiselectWrapper(multiselectService.addSubmitters(selectedRows))">
<mat-icon>person_add</mat-icon>
<!-- TODO: icon -->
<span translate>Add submitters</span>
</button>
<button mat-menu-item (click)="multiselectWrapper(multiselectService.removeSubmitters(selectedRows))">
<mat-icon>person_outline</mat-icon>
<!-- TODO: icon -->
<span translate>remove submitters</span>
</button>
<button mat-menu-item (click)="multiselectWrapper(multiselectService.addTags(selectedRows))">
<mat-icon>bookmarks</mat-icon>
<!-- TODO: icon -->
<span translate>Add tags</span>
</button>
<button mat-menu-item (click)="multiselectWrapper(multiselectService.removeTags(selectedRows))">
<mat-icon>bookmark_border</mat-icon>
<!-- TODO: icon -->
<span translate>Remove tags</span>
</button>
</div> </div>
<mat-divider></mat-divider> <mat-divider></mat-divider>
<button mat-menu-item (click)="csvExportMotionList(); toggleMultiSelect()">
<mat-icon>archive</mat-icon>
<span translate>Export as CSV</span>
</button>
<mat-divider></mat-divider>
<button <button
mat-menu-item mat-menu-item
class="red-warning-text" class="red-warning-text"

View File

@ -11,6 +11,14 @@ import { MotionRepositoryService } from '../../services/motion-repository.servic
import { ViewMotion } from '../../models/view-motion'; import { ViewMotion } from '../../models/view-motion';
import { WorkflowState } from '../../../../shared/models/motions/workflow-state'; import { WorkflowState } from '../../../../shared/models/motions/workflow-state';
import { MotionMultiselectService } from '../../services/motion-multiselect.service'; import { MotionMultiselectService } from '../../services/motion-multiselect.service';
import { TagRepositoryService } from 'app/site/tags/services/tag-repository.service';
import { MotionBlockRepositoryService } from '../../services/motion-block-repository.service';
import { CategoryRepositoryService } from '../../services/category-repository.service';
import { ViewTag } from 'app/site/tags/models/view-tag';
import { ViewWorkflow } from '../../models/view-workflow';
import { ViewCategory } from '../../models/view-category';
import { ViewMotionBlock } from '../../models/view-motion-block';
import { WorkflowRepositoryService } from '../../services/workflow-repository.service';
/** /**
* Component that displays all the motions in a Table using DataSource. * Component that displays all the motions in a Table using DataSource.
@ -39,6 +47,13 @@ export class MotionListComponent extends ListViewBaseComponent<ViewMotion> imple
* @TODO replace by direct access to config variable, once it's available from the templates * @TODO replace by direct access to config variable, once it's available from the templates
*/ */
public statutesEnabled: boolean; public statutesEnabled: boolean;
public recomendationEnabled: boolean;
public tags: ViewTag[] = [];
public workflows: ViewWorkflow[] = [];
public categories: ViewCategory[] = [];
public motionBlocks: ViewMotionBlock[] = [];
/** /**
* Constructor implements title and translation Module. * Constructor implements title and translation Module.
@ -50,13 +65,11 @@ export class MotionListComponent extends ListViewBaseComponent<ViewMotion> imple
* @param route Current route * @param route Current route
* @param configService The configuration provider * @param configService The configuration provider
* @param repo Motion Repository * @param repo Motion Repository
* @param promptService * @param tagRepo Tag Repository
* @param motionCsvExport * @param motionBlockRepo
* @param workflowRepo Workflow Repository
* @param categoryRepo * @param categoryRepo
* @param userRepo * @param motionCsvExport
* @param tagRepo * @param multiselectService Service for the multiSelect actions
* @param choiceService
*/ */
public constructor( public constructor(
titleService: Title, titleService: Title,
@ -66,6 +79,10 @@ 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 tagRepo: TagRepositoryService,
private motionBlockRepo: MotionBlockRepositoryService,
private categoryRepo: CategoryRepositoryService,
private workflowRepo: WorkflowRepositoryService,
private motionCsvExport: MotionCsvExportService, private motionCsvExport: MotionCsvExportService,
public multiselectService: MotionMultiselectService public multiselectService: MotionMultiselectService
) { ) {
@ -84,7 +101,6 @@ 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) {
@ -93,8 +109,14 @@ export class MotionListComponent extends ListViewBaseComponent<ViewMotion> imple
return a.id - b.id; return a.id - b.id;
} }
}); });
this.checkSelection();
}); });
this.configService.get('motions_statutes_enabled').subscribe(enabled => (this.statutesEnabled = enabled)); this.configService.get('motions_statutes_enabled').subscribe(enabled => (this.statutesEnabled = enabled));
this.configService.get('motions_recommendations_by').subscribe(id => (this.recomendationEnabled = !!id));
this.motionBlockRepo.getViewModelListObservable().subscribe(mBs => this.motionBlocks = mBs);
this.categoryRepo.getViewModelListObservable().subscribe(cats => this.categories = cats);
this.tagRepo.getViewModelListObservable().subscribe(tags => this.tags = tags);
this.workflowRepo.getViewModelListObservable().subscribe(wfs => this.workflows = wfs);
} }
/** /**
@ -178,7 +200,11 @@ export class MotionListComponent extends ListViewBaseComponent<ViewMotion> imple
* *
* @param multiselectPromise The promise returned by multiselect actions. * @param multiselectPromise The promise returned by multiselect actions.
*/ */
public multiselectWrapper(multiselectPromise: Promise<void>): void { public async multiselectWrapper(multiselectPromise: Promise<void>): Promise<void> {
multiselectPromise.then(() => this.toggleMultiSelect(), this.raiseError); try {
await multiselectPromise;
} catch (e) {
this.raiseError(e);
}
} }
} }

View File

@ -14,6 +14,7 @@ import { HttpService } from 'app/core/services/http.service';
import { AgendaRepositoryService } from 'app/site/agenda/services/agenda-repository.service'; import { AgendaRepositoryService } from 'app/site/agenda/services/agenda-repository.service';
import { Displayable } from 'app/shared/models/base/displayable'; import { Displayable } from 'app/shared/models/base/displayable';
import { Identifiable } from 'app/shared/models/base/identifiable'; import { Identifiable } from 'app/shared/models/base/identifiable';
import { MotionBlockRepositoryService } from './motion-block-repository.service';
/** /**
* Contains all multiselect actions for the motion list view. * Contains all multiselect actions for the motion list view.
@ -33,6 +34,9 @@ export class MotionMultiselectService {
* @param workflowRepo * @param workflowRepo
* @param categoryRepo * @param categoryRepo
* @param tagRepo * @param tagRepo
* @param agendaRepo
* @param motionBlockRepo
* @param httpService
*/ */
public constructor( public constructor(
private repo: MotionRepositoryService, private repo: MotionRepositoryService,
@ -44,6 +48,7 @@ export class MotionMultiselectService {
private categoryRepo: CategoryRepositoryService, private categoryRepo: CategoryRepositoryService,
private tagRepo: TagRepositoryService, private tagRepo: TagRepositoryService,
private agendaRepo: AgendaRepositoryService, private agendaRepo: AgendaRepositoryService,
private motionBlockRepo: MotionBlockRepositoryService,
private httpService: HttpService private httpService: HttpService
) {} ) {}
@ -71,7 +76,7 @@ export class MotionMultiselectService {
if (selectedChoice) { if (selectedChoice) {
const requestData = { const requestData = {
items: motions.map(motion => motion.agenda_item_id), items: motions.map(motion => motion.agenda_item_id),
parent_id: selectedChoice as number parent_id: selectedChoice.items as number
}; };
await this.httpService.post('/rest/agenda/item/assign', requestData); await this.httpService.post('/rest/agenda/item/assign', requestData);
} }
@ -84,14 +89,15 @@ export class MotionMultiselectService {
*/ */
public async setStateOfMultiple(motions: ViewMotion[]): Promise<void> { public async setStateOfMultiple(motions: ViewMotion[]): Promise<void> {
const title = this.translate.instant('This will set the state of all selected motions to:'); const title = this.translate.instant('This will set the state of all selected motions to:');
const choices = this.workflowRepo.getAllWorkflowStates().map(workflowState => ({ const choices = this.workflowRepo.getWorkflowStatesForMotions(motions)
.map(workflowState => ({
id: workflowState.id, id: workflowState.id,
label: workflowState.name label: workflowState.name
})); }));
const selectedChoice = await this.choiceService.open(title, choices); const selectedChoice = await this.choiceService.open(title, choices);
if (selectedChoice) { if (selectedChoice) {
for (const motion of motions) { for (const motion of motions) {
await this.repo.setState(motion, selectedChoice as number); await this.repo.setState(motion, selectedChoice.items as number);
} }
} }
} }
@ -104,18 +110,19 @@ export class MotionMultiselectService {
public async setRecommendation(motions: ViewMotion[]): Promise<void> { public async setRecommendation(motions: ViewMotion[]): Promise<void> {
const title = this.translate.instant('This will set the recommendation for all selected motions to:'); const title = this.translate.instant('This will set the recommendation for all selected motions to:');
const choices = this.workflowRepo const choices = this.workflowRepo
.getAllWorkflowStates() .getWorkflowStatesForMotions(motions)
.filter(workflowState => !!workflowState.recommendation_label) .filter(workflowState => !!workflowState.recommendation_label)
.map(workflowState => ({ .map(workflowState => ({
id: workflowState.id, id: workflowState.id,
label: workflowState.recommendation_label label: workflowState.recommendation_label
})); }));
choices.push({ id: 0, label: 'Delete recommendation' }); const clearChoice = 'Delete recommendation';
const selectedChoice = await this.choiceService.open(title, choices); const selectedChoice = await this.choiceService.open(title, choices, false,
if (typeof selectedChoice === 'number') { null, clearChoice);
if (selectedChoice) {
const requestData = motions.map(motion => ({ const requestData = motions.map(motion => ({
id: motion.id, id: motion.id,
recommendation: selectedChoice recommendation: selectedChoice.action ? 0 : selectedChoice.items as number
})); }));
await this.httpService.post('/rest/motions/motion/manage_multiple_recommendation', { await this.httpService.post('/rest/motions/motion/manage_multiple_recommendation', {
motions: requestData motions: requestData
@ -130,25 +137,31 @@ export class MotionMultiselectService {
*/ */
public async setCategory(motions: ViewMotion[]): Promise<void> { public async setCategory(motions: ViewMotion[]): Promise<void> {
const title = this.translate.instant('This will set the category of all selected motions to:'); const title = this.translate.instant('This will set the category of all selected motions to:');
const selectedChoice = await this.choiceService.open(title, this.categoryRepo.getViewModelList()); const clearChoice = 'No category';
const selectedChoice = await this.choiceService.open(title, this.categoryRepo.getViewModelList(),
false, null, clearChoice);
if (selectedChoice) { if (selectedChoice) {
for (const motion of motions) { for (const motion of motions) {
await this.repo.update({ category_id: selectedChoice as number }, motion); await this.repo.update(
{category_id: selectedChoice.action ? 0 : selectedChoice.items as number },
motion);
} }
} }
} }
/** /**
* Opens a dialog and adds the selected submitters for all given motions. * Opens a dialog and adds or removes the selected submitters for all given motions.
* *
* @param motions The motions to add the sumbitters to * @param motions The motions to add/remove the sumbitters to
*/ */
public async addSubmitters(motions: ViewMotion[]): Promise<void> { public async changeSubmitters(motions: ViewMotion[]): Promise<void> {
const title = this.translate.instant('This will add the following submitters of all selected motions:'); const title = this.translate.instant('This will add or remove the following submitters for all selected motions:');
const selectedChoice = await this.choiceService.open(title, this.userRepo.getViewModelList(), true); const choices = ['Add', 'Remove'];
if (selectedChoice) { const selectedChoice = await this.choiceService.open(title,
this.userRepo.getViewModelList(), true, choices);
if (selectedChoice && selectedChoice.action === choices[0]) {
const requestData = motions.map(motion => { const requestData = motions.map(motion => {
let submitterIds = [...motion.submitters_id, ...(selectedChoice as number[])]; let submitterIds = [...motion.submitters_id, ...(selectedChoice.items as number[])];
submitterIds = submitterIds.filter((id, index, self) => self.indexOf(id) === index); // remove duplicates submitterIds = submitterIds.filter((id, index, self) => self.indexOf(id) === index); // remove duplicates
return { return {
id: motion.id, id: motion.id,
@ -156,20 +169,9 @@ export class MotionMultiselectService {
}; };
}); });
await this.httpService.post('/rest/motions/motion/manage_multiple_submitters', { motions: requestData }); await this.httpService.post('/rest/motions/motion/manage_multiple_submitters', { motions: requestData });
} } else if (selectedChoice && selectedChoice.action === choices[1]) {
}
/**
* Opens a dialog and removes the selected submitters for all given motions.
*
* @param motions The motions to remove the submitters from
*/
public async removeSubmitters(motions: ViewMotion[]): Promise<void> {
const title = this.translate.instant('This will remove the following submitters from all selected motions:');
const selectedChoice = await this.choiceService.open(title, this.userRepo.getViewModelList(), true);
if (selectedChoice) {
const requestData = motions.map(motion => { const requestData = motions.map(motion => {
const submitterIdsToRemove = selectedChoice as number[]; const submitterIdsToRemove = selectedChoice.items as number[];
const submitterIds = motion.submitters_id.filter(id => !submitterIdsToRemove.includes(id)); const submitterIds = motion.submitters_id.filter(id => !submitterIdsToRemove.includes(id));
return { return {
id: motion.id, id: motion.id,
@ -181,16 +183,18 @@ export class MotionMultiselectService {
} }
/** /**
* Opens a dialog and adds the selected tags for all given motions. * Opens a dialog and adds/removes the selected tags for all given motions.
* *
* @param motions The motions to add the tags to * @param motions The motions to add the tags to
*/ */
public async addTags(motions: ViewMotion[]): Promise<void> { public async changeTags(motions: ViewMotion[]): Promise<void> {
const title = this.translate.instant('This will add the following tags to all selected motions:'); const title = this.translate.instant('This will add or remove the following tags for all selected motions:');
const selectedChoice = await this.choiceService.open(title, this.tagRepo.getViewModelList(), true); const choices = ['Add', 'Remove', 'Clear tags'];
if (selectedChoice) { const selectedChoice = await this.choiceService.open(title, this.tagRepo.getViewModelList(), true,
choices);
if (selectedChoice && selectedChoice.action === choices[0]) {
const requestData = motions.map(motion => { const requestData = motions.map(motion => {
let tagIds = [...motion.tags_id, ...(selectedChoice as number[])]; let tagIds = [...motion.tags_id, ...(selectedChoice.items as number[])];
tagIds = tagIds.filter((id, index, self) => self.indexOf(id) === index); // remove duplicates tagIds = tagIds.filter((id, index, self) => self.indexOf(id) === index); // remove duplicates
return { return {
id: motion.id, id: motion.id,
@ -198,20 +202,9 @@ export class MotionMultiselectService {
}; };
}); });
await this.httpService.post('/rest/motions/motion/manage_multiple_tags', { motions: requestData }); await this.httpService.post('/rest/motions/motion/manage_multiple_tags', { motions: requestData });
} } else if (selectedChoice && selectedChoice.action === choices[1]) {
}
/**
* Opens a dialog and removes the selected tags for all given motions.
*
* @param motions The motions to remove the tags from
*/
public async removeTags(motions: ViewMotion[]): Promise<void> {
const title = this.translate.instant('This will remove the following tags from all selected motions:');
const selectedChoice = await this.choiceService.open(title, this.tagRepo.getViewModelList(), true);
if (selectedChoice) {
const requestData = motions.map(motion => { const requestData = motions.map(motion => {
const tagIdsToRemove = selectedChoice as number[]; const tagIdsToRemove = selectedChoice.items as number[];
const tagIds = motion.tags_id.filter(id => !tagIdsToRemove.includes(id)); const tagIds = motion.tags_id.filter(id => !tagIdsToRemove.includes(id));
return { return {
id: motion.id, id: motion.id,
@ -219,6 +212,33 @@ export class MotionMultiselectService {
}; };
}); });
await this.httpService.post('/rest/motions/motion/manage_multiple_tags', { motions: requestData }); await this.httpService.post('/rest/motions/motion/manage_multiple_tags', { motions: requestData });
} else if (selectedChoice && selectedChoice.action === choices[2]) {
const requestData = motions.map(motion => {
return {
id: motion.id,
tags: []
};
});
await this.httpService.post('/rest/motions/motion/manage_multiple_tags', { motions: requestData });
}
}
/**
* Opens a dialog and changes the motionBlock for all given motions.
*
* @param motions The motions for which to change the motionBlock
*/
public async setMotionBlock(motions: ViewMotion[]): Promise<void> {
const title = this.translate.instant('This will change the motion Block for all selected motions:');
const clearChoice = 'Clear motion block';
const selectedChoice = await this.choiceService.open(title, this.motionBlockRepo.getViewModelList(),
false, null, clearChoice);
if (selectedChoice) {
for (const motion of motions) {
const blockId = selectedChoice.action ? null : selectedChoice.items as number;
await this.repo.update({motion_block_id: blockId}, motion);
}
} }
} }
} }

View File

@ -7,6 +7,7 @@ import { BaseRepository } from '../../base/base-repository';
import { Identifiable } from '../../../shared/models/base/identifiable'; import { Identifiable } from '../../../shared/models/base/identifiable';
import { CollectionStringModelMapperService } from '../../../core/services/collectionStringModelMapper.service'; import { CollectionStringModelMapperService } from '../../../core/services/collectionStringModelMapper.service';
import { WorkflowState } from 'app/shared/models/motions/workflow-state'; import { WorkflowState } from 'app/shared/models/motions/workflow-state';
import { ViewMotion } from '../models/view-motion';
/** /**
* Repository Services for Categories * Repository Services for Categories
@ -70,4 +71,18 @@ export class WorkflowRepositoryService extends BaseRepository<ViewWorkflow, Work
}); });
return states; return states;
} }
/**
* Returns all workflowStates that cover the list of viewMotions given
* @param motions
*/
public getWorkflowStatesForMotions(motions: ViewMotion[]): WorkflowState[] {
let states: WorkflowState[] = [];
const workflowIds = motions.map(motion => motion.workflow_id).filter((value, index, self) => self.indexOf(value) === index);
workflowIds.forEach(id => {
const workflow = this.getViewModel(id);
states = states.concat(workflow.states);
});
return states;
}
} }

View File

@ -12,10 +12,6 @@
<button mat-icon-button (click)="toggleMultiSelect()"><mat-icon>arrow_back</mat-icon></button> <button mat-icon-button (click)="toggleMultiSelect()"><mat-icon>arrow_back</mat-icon></button>
<span>{{ selectedRows.length }}&nbsp;</span><span translate>selected</span> <span>{{ selectedRows.length }}&nbsp;</span><span translate>selected</span>
</div> </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">
@ -100,10 +96,6 @@
</button> </button>
</div> </div>
<div *ngIf="isMultiSelect"> <div *ngIf="isMultiSelect">
<button mat-menu-item (click)="toggleMultiSelect()">
<mat-icon>library_add</mat-icon>
<span translate>Exit multiselect</span>
</button>
<button mat-menu-item (click)="selectAll()"> <button mat-menu-item (click)="selectAll()">
<mat-icon>done_all</mat-icon> <mat-icon>done_all</mat-icon>
<span translate>Select all</span> <span translate>Select all</span>
@ -114,42 +106,23 @@
</button> </button>
<div *osPerms="'users.can_manage'"> <div *osPerms="'users.can_manage'">
<mat-divider></mat-divider> <mat-divider></mat-divider>
<button mat-menu-item (click)="setGroupSelected(true)"> <button mat-menu-item (click)="setGroupSelected()">
<mat-icon>people</mat-icon> <mat-icon>people</mat-icon>
<span translate>Add groups</span> <span translate>Add/remove groups</span>
</button>
<button mat-menu-item (click)="setGroupSelected(false)">
<mat-icon>people_outline</mat-icon>
<span translate>Remove groups</span>
</button> </button>
<mat-divider></mat-divider> <button mat-menu-item (click)="setActiveSelected()">
<button mat-menu-item (click)="setActiveSelected(true)">
<mat-icon>add_circle</mat-icon> <mat-icon>add_circle</mat-icon>
<span translate>Is active</span> <span translate>Set/unset active</span>
</button> </button>
<button mat-menu-item (click)="setActiveSelected(false)">
<mat-icon>remove_circle</mat-icon> <button mat-menu-item (click)="setPresentSelected()">
<span translate>Is inactive</span>
</button>
<mat-divider></mat-divider>
<button mat-menu-item (click)="setPresentSelected(true)">
<mat-icon>check_box</mat-icon> <mat-icon>check_box</mat-icon>
<span translate>Is present</span> <span translate>Set/unset presence</span>
</button> </button>
<button mat-menu-item (click)="setPresentSelected(false)"> <button mat-menu-item (click)="setCommitteeSelected()">
<mat-icon>check_box_outline_blank</mat-icon>
<span translate>Is not present</span>
</button>
<mat-divider></mat-divider>
<button mat-menu-item (click)="setCommitteeSelected(true)">
<mat-icon>account_balance</mat-icon> <mat-icon>account_balance</mat-icon>
<span translate>Is committee</span> <span translate>Set/unset committee</span>
</button>
<button mat-menu-item (click)="setCommitteeSelected(false)">
<mat-icon>account_balance</mat-icon>
<span translate>Is no committee</span>
</button> </button>
<mat-divider></mat-divider> <mat-divider></mat-divider>

View File

@ -131,23 +131,20 @@ export class UserListComponent extends ListViewBaseComponent<ViewUser> implement
* Opens a dialog and sets the group(s) for all selected users. * Opens a dialog and sets the group(s) for all selected users.
* SelectedRows is only filled with data in multiSelect mode * SelectedRows is only filled with data in multiSelect mode
*/ */
public async setGroupSelected(add: boolean): Promise<void> { public async setGroupSelected(): Promise<void> {
let content: string; const content = this.translate.instant('This will add or remove the following groups for all selected users:');
if (add) { const choices = ['Add group(s)', 'Remove group(s)'];
content = this.translate.instant('This will add the following groups to all selected users:'); const selectedChoice = await this.choiceService.open(content,
} else { this.groupRepo.getViewModelList(), true, choices);
content = this.translate.instant('This will remove the following groups from all selected users:');
}
const selectedChoice = await this.choiceService.open(content, this.groupRepo.getViewModelList(), true);
if (selectedChoice) { if (selectedChoice) {
for (const user of this.selectedRows) { for (const user of this.selectedRows) {
const newGroups = [...user.groups_id]; const newGroups = [...user.groups_id];
(selectedChoice as number[]).forEach(newChoice => { (selectedChoice.items as number[]).forEach(newChoice => {
const idx = newGroups.indexOf(newChoice); const idx = newGroups.indexOf(newChoice);
if (idx < 0 && add) { if (idx < 0 && selectedChoice.action === choices[0]) {
newGroups.push(newChoice); newGroups.push(newChoice);
} else if (idx >= 0 && !add) { } else if (idx >= 0 && selectedChoice.action === choices[1]) {
newGroups.slice(idx, 1); newGroups.splice(idx, 1);
} }
}); });
await this.repo.update({ groups_id: newGroups }, user); await this.repo.update({ groups_id: newGroups }, user);
@ -159,29 +156,48 @@ export class UserListComponent extends ListViewBaseComponent<ViewUser> implement
* Handler for bulk setting/unsetting the 'active' attribute. * Handler for bulk setting/unsetting the 'active' attribute.
* Uses selectedRows defined via multiSelect mode. * Uses selectedRows defined via multiSelect mode.
*/ */
public async setActiveSelected(active: boolean): Promise<void> { public async setActiveSelected(): Promise<void> {
const content = this.translate.instant('Set the active status for the selected users');
const options = ['Active', 'Not active'];
const selectedChoice = await this.choiceService.open(content, null, false, options);
if (selectedChoice) {
const active = selectedChoice.action === options[0];
for (const user of this.selectedRows) { for (const user of this.selectedRows) {
await this.repo.update({ is_active: active }, user); await this.repo.update({ is_active: active }, user);
} }
} }
}
/** /**
* Handler for bulk setting/unsetting the 'is present' attribute. * Handler for bulk setting/unsetting the 'is present' attribute.
* Uses selectedRows defined via multiSelect mode. * Uses selectedRows defined via multiSelect mode.
*/ */
public async setPresentSelected(present: boolean): Promise<void> { public async setPresentSelected(): Promise<void> {
const content = this.translate.instant('Set the presence status for the selected users');
const options = ['Present', 'Not present'];
const selectedChoice = await this.choiceService.open(content, null, false, options);
if (selectedChoice) {
const present = selectedChoice.action === options[0];
for (const user of this.selectedRows) { for (const user of this.selectedRows) {
await this.repo.update({ is_present: present }, user); await this.repo.update({ is_present: present }, user);
} }
} }
}
/** /**
* Handler for bulk setting/unsetting the 'is committee' attribute. * Handler for bulk setting/unsetting the 'is committee' attribute.
* Uses selectedRows defined via multiSelect mode. * Uses selectedRows defined via multiSelect mode.
*/ */
public async setCommitteeSelected(is_committee: boolean): Promise<void> { public async setCommitteeSelected(): Promise<void> {
const content = this.translate.instant(
'Sets/unsets the committee status for the selected users');
const options = ['Is committee', 'Is not committee'];
const selectedChoice = await this.choiceService.open(content, null, false, options);
if (selectedChoice) {
const committee = selectedChoice.action === options[0];
for (const user of this.selectedRows) { for (const user of this.selectedRows) {
await this.repo.update({ is_committee: is_committee }, user); await this.repo.update({ is_committee: committee }, user);
}
} }
} }
@ -194,7 +210,7 @@ export class UserListComponent extends ListViewBaseComponent<ViewUser> implement
} }
/** /**
* Handler for bulk resetting passwords. Needs multiSelect mode. * Handler for bulk setting new passwords. Needs multiSelect mode.
*/ */
public async resetPasswordsSelected(): Promise<void> { public async resetPasswordsSelected(): Promise<void> {
for (const user of this.selectedRows) { for (const user of this.selectedRows) {

View File

@ -111,7 +111,8 @@ export class UserRepositoryService extends BaseRepository<ViewUser, User> {
} }
/** /**
* Updates the password and sets the password without checking for the old one * Updates the password and sets the password without checking for the old one.
* Also resets the 'default password' to the newly created one.
* *
* @param user The user to update * @param user The user to update
* @param password The password to set * @param password The password to set
@ -119,6 +120,7 @@ export class UserRepositoryService extends BaseRepository<ViewUser, User> {
public async resetPassword(user: ViewUser, password: string): Promise<void> { public async resetPassword(user: ViewUser, password: string): Promise<void> {
const path = `/rest/users/user/${user.id}/reset_password/`; const path = `/rest/users/user/${user.id}/reset_password/`;
await this.httpService.post(path, { password: password }); await this.httpService.post(path, { password: password });
await this.update({default_password: password}, user);
} }
/** /**