Added new multiselect actions.
This commit is contained in:
parent
0a823877c2
commit
82b26347e2
@ -13,6 +13,7 @@ import { DataSendService } from './services/data-send.service';
|
||||
import { ViewportService } from './services/viewport.service';
|
||||
import { PromptDialogComponent } from '../shared/components/prompt-dialog/prompt-dialog.component';
|
||||
import { HttpService } from './services/http.service';
|
||||
import { ChoiceDialogComponent } from '../shared/components/choice-dialog/choice-dialog.component';
|
||||
|
||||
/** Global Core Module. Contains all global (singleton) services
|
||||
*
|
||||
@ -31,7 +32,7 @@ import { HttpService } from './services/http.service';
|
||||
ViewportService,
|
||||
WebsocketService
|
||||
],
|
||||
entryComponents: [PromptDialogComponent]
|
||||
entryComponents: [PromptDialogComponent, ChoiceDialogComponent]
|
||||
})
|
||||
export class CoreModule {
|
||||
/** make sure CoreModule is imported only by one NgModule, the AppModule */
|
||||
|
18
client/src/app/core/services/choice.service.spec.ts
Normal file
18
client/src/app/core/services/choice.service.spec.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ChoiceService } from './choice.service';
|
||||
import { E2EImportsModule } from 'e2e-imports.module';
|
||||
|
||||
describe('ChoiceService', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [E2EImportsModule],
|
||||
providers: [ChoiceService]
|
||||
});
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
const service: ChoiceService = TestBed.get(ChoiceService);
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
35
client/src/app/core/services/choice.service.ts
Normal file
35
client/src/app/core/services/choice.service.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { OpenSlidesComponent } from '../../openslides.component';
|
||||
import { MatDialog } from '@angular/material';
|
||||
import { ChoiceDialogComponent, ChoiceDialogOptions, ChoiceAnswer } from '../../shared/components/choice-dialog/choice-dialog.component';
|
||||
|
||||
/**
|
||||
* A service for prompting the user to select a choice.
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class ChoiceService extends OpenSlidesComponent {
|
||||
/**
|
||||
* Ctor.
|
||||
*
|
||||
* @param dialog For opening the ChoiceDialog
|
||||
*/
|
||||
public constructor(private dialog: MatDialog) {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the dialog. Returns the chosen value after the user accepts.
|
||||
* @param title The title to display in the dialog
|
||||
* @param choices The available choices
|
||||
* @returns an answer {@link ChoiceAnswer}
|
||||
*/
|
||||
public async open(title: string, choices: ChoiceDialogOptions, multiSelect: boolean = false): Promise<ChoiceAnswer> {
|
||||
const dialogRef = this.dialog.open(ChoiceDialogComponent, {
|
||||
minWidth: '250px',
|
||||
data: { title: title, choices: choices, multiSelect: multiSelect }
|
||||
});
|
||||
return dialogRef.afterClosed().toPromise();
|
||||
}
|
||||
}
|
@ -1,10 +1,12 @@
|
||||
import { TestBed, inject } from '@angular/core/testing';
|
||||
|
||||
import { ConfigService } from './config.service';
|
||||
import { E2EImportsModule } from 'e2e-imports.module';
|
||||
|
||||
describe('ConfigService', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [E2EImportsModule],
|
||||
providers: [ConfigService]
|
||||
});
|
||||
});
|
||||
|
@ -0,0 +1,24 @@
|
||||
<h2 mat-dialog-title>{{ data.title | translate }}</h2>
|
||||
<div class="scrollmenu">
|
||||
<mat-radio-group #radio name="choice" *ngIf="!data.multiSelect" class="choice-radio-group" [(ngModel)]="selectedChoice">
|
||||
<mat-radio-button class="choice-button" *ngFor="let choice of data.choices" [value]="choice.id">
|
||||
{{ getChoiceTitle(choice) | translate}}
|
||||
</mat-radio-button>
|
||||
</mat-radio-group>
|
||||
|
||||
<mat-list *ngIf="data.multiSelect">
|
||||
<mat-list-item *ngFor="let choice of data.choices">
|
||||
<mat-checkbox [checked]="isChosen(choice)" (change)="toggleChoice(choice)">
|
||||
{{ getChoiceTitle(choice) | translate }}
|
||||
</mat-checkbox>
|
||||
</mat-list-item>
|
||||
</mat-list>
|
||||
|
||||
<span *ngIf="!data.choices.length" translate>No choices available</span>
|
||||
</div>
|
||||
<mat-dialog-actions>
|
||||
<button *ngIf="!data.multiSelect || data.choices.length" mat-button (click)="closeDialog(true)">
|
||||
<span translate>Ok</span>
|
||||
</button>
|
||||
<button mat-button (click)="closeDialog(false)"><span translate>Cancel</span></button>
|
||||
</mat-dialog-actions>
|
@ -0,0 +1,15 @@
|
||||
mat-radio-group {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
|
||||
mat-radio-button {
|
||||
margin: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.scrollmenu {
|
||||
padding: 4px;
|
||||
overflow-y: auto;
|
||||
display: block;
|
||||
overflow-y: auto;
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
// import { ChoiceDialogComponent } from './choice-dialog.component';
|
||||
import { E2EImportsModule } from 'e2e-imports.module';
|
||||
|
||||
describe('ChoiceDialogComponent', () => {
|
||||
// let component: ChoiceDialogComponent;
|
||||
// let fixture: ComponentFixture<ChoiceDialogComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [E2EImportsModule]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
// TODO: You cannot create this component in the standard way. Needs different testing.
|
||||
beforeEach(() => {
|
||||
/*fixture = TestBed.createComponent(PromptDialogComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();*/
|
||||
});
|
||||
|
||||
/*it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});*/
|
||||
});
|
@ -0,0 +1,92 @@
|
||||
import { Component, Inject } from '@angular/core';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material';
|
||||
import { Displayable } from 'app/shared/models/base/displayable';
|
||||
import { Identifiable } from 'app/shared/models/base/identifiable';
|
||||
|
||||
/**
|
||||
* An option needs to be identifiable and should have a strnig to display. Either uses Displayble or
|
||||
* a label property.
|
||||
*/
|
||||
type ChoiceDialogOption = (Identifiable & Displayable) | (Identifiable & { label: string });
|
||||
|
||||
/**
|
||||
* All choices in the array should have the same type.
|
||||
*/
|
||||
export type ChoiceDialogOptions = (Identifiable & Displayable)[] | (Identifiable & { label: string })[];
|
||||
|
||||
interface ChoiceDialogData {
|
||||
title: string;
|
||||
choices: ChoiceDialogOptions;
|
||||
multiSelect: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
export type ChoiceAnswer = undefined | number | number[];
|
||||
|
||||
/**
|
||||
* A dialog with choice fields.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'os-choice-dialog',
|
||||
templateUrl: './choice-dialog.component.html',
|
||||
styleUrls: ['./choice-dialog.component.scss']
|
||||
})
|
||||
export class ChoiceDialogComponent {
|
||||
/**
|
||||
* One number selected, if this is a single select choice
|
||||
*/
|
||||
public selectedChoice: number;
|
||||
|
||||
/**
|
||||
* All selected ids, if this is a multiselect choice
|
||||
*/
|
||||
public selectedMultiChoices: number[] = [];
|
||||
|
||||
public constructor(
|
||||
public dialogRef: MatDialogRef<ChoiceDialogComponent>,
|
||||
@Inject(MAT_DIALOG_DATA) public data: ChoiceDialogData
|
||||
) {}
|
||||
|
||||
public getChoiceTitle(choice: ChoiceDialogOption): string {
|
||||
if ('label' in choice) {
|
||||
return choice.label;
|
||||
} else {
|
||||
return choice.getTitle();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the dialog with the selected choices
|
||||
*/
|
||||
public closeDialog(ok: boolean): void {
|
||||
if (ok) {
|
||||
this.dialogRef.close(this.data.multiSelect ? this.selectedMultiChoices : this.selectedChoice);
|
||||
} else {
|
||||
this.dialogRef.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* For multiSelect: Determines whether a choice has been activated
|
||||
* @param choice
|
||||
*/
|
||||
public isChosen(choice: Identifiable): boolean {
|
||||
return this.selectedMultiChoices.indexOf(choice.id) >= 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* For multiSelect: Activates/deactivates a multi-Choice option
|
||||
* @param choice
|
||||
*/
|
||||
public toggleChoice(choice: Identifiable) : void {
|
||||
const idx = this.selectedMultiChoices.indexOf(choice.id);
|
||||
if (idx < 0) {
|
||||
this.selectedMultiChoices.push(choice.id);
|
||||
} else {
|
||||
this.selectedMultiChoices.splice(idx, 1);
|
||||
}
|
||||
}
|
||||
}
|
@ -23,7 +23,7 @@
|
||||
</div>
|
||||
|
||||
<!-- centered information slot-->
|
||||
<div *ngIf="multiSelectMode" class=spacer></div>
|
||||
<div *ngIf="!multiSelectMode" class=spacer></div>
|
||||
<div class="toolbar-centered on-transition-fade" *ngIf="multiSelectMode">
|
||||
<ng-content select=".central-info-slot"></ng-content>
|
||||
</div>
|
||||
|
@ -66,6 +66,7 @@ import { PromptDialogComponent } from './components/prompt-dialog/prompt-dialog.
|
||||
import { SortingListComponent } from './components/sorting-list/sorting-list.component';
|
||||
import { SpeakerListComponent } from 'app/site/agenda/components/speaker-list/speaker-list.component';
|
||||
import { SortingTreeComponent } from './components/sorting-tree/sorting-tree.component';
|
||||
import { ChoiceDialogComponent } from './components/choice-dialog/choice-dialog.component';
|
||||
|
||||
/**
|
||||
* Share Module for all "dumb" components and pipes.
|
||||
@ -180,7 +181,8 @@ import { SortingTreeComponent } from './components/sorting-tree/sorting-tree.com
|
||||
PromptDialogComponent,
|
||||
SortingListComponent,
|
||||
SpeakerListComponent,
|
||||
SortingTreeComponent
|
||||
SortingTreeComponent,
|
||||
ChoiceDialogComponent
|
||||
],
|
||||
providers: [
|
||||
{ provide: DateAdapter, useClass: OpenSlidesDateAdapter },
|
||||
|
@ -71,16 +71,12 @@
|
||||
<div *ngIf="!isMultiSelect">
|
||||
<button mat-menu-item *osPerms="'agenda.can_manage'" (click)="toggleMultiSelect()">
|
||||
<mat-icon>library_add</mat-icon>
|
||||
<span translate>MultiSelect</span>
|
||||
<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>
|
||||
@ -100,6 +96,11 @@
|
||||
<mat-icon>visibility_off</mat-icon>
|
||||
<span translate>Set invisible</span>
|
||||
</button>
|
||||
<mat-divider></mat-divider>
|
||||
<button mat-menu-item class="red-warning-text" (click)="deleteSelected()">
|
||||
<mat-icon>delete</mat-icon>
|
||||
<span translate>Delete selected</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</mat-menu>
|
||||
|
@ -100,6 +100,7 @@ export class AgendaListComponent extends ListViewBaseComponent<ViewItem> impleme
|
||||
/**
|
||||
* 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> {
|
||||
@ -111,6 +112,7 @@ export class AgendaListComponent extends ListViewBaseComponent<ViewItem> impleme
|
||||
/**
|
||||
* 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> {
|
||||
|
@ -69,7 +69,7 @@
|
||||
<div *ngIf="!isMultiSelect">
|
||||
<button mat-menu-item *osPerms="'assignment.can_manage'" (click)="toggleMultiSelect()">
|
||||
<mat-icon>library_add</mat-icon>
|
||||
<span translate>MultiSelect</span>
|
||||
<span translate>Multiselect</span>
|
||||
</button>
|
||||
<button mat-menu-item (click)="downloadAssignmentButton()">
|
||||
<mat-icon>archive</mat-icon>
|
||||
@ -78,7 +78,12 @@
|
||||
</div>
|
||||
|
||||
<div *ngIf="isMultiSelect">
|
||||
<button mat-menu-item *osPerms="'assignment.can_manage'" (click)="deleteSelected()">
|
||||
<button mat-menu-item (click)="toggleMultiSelect()">
|
||||
<mat-icon>library_add</mat-icon>
|
||||
<span translate>Exit multiselect</span>
|
||||
</button>
|
||||
<mat-divider></mat-divider>
|
||||
<button mat-menu-item class="red-warning-text" *osPerms="'assignment.can_manage'" (click)="deleteSelected()">
|
||||
<mat-icon>delete</mat-icon>
|
||||
<span translate>Delete assignments</span>
|
||||
</button>
|
||||
|
@ -44,14 +44,6 @@
|
||||
<mat-icon>more_vert</mat-icon>
|
||||
</button>
|
||||
</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()">
|
||||
@ -167,14 +159,21 @@
|
||||
|
||||
<!-- Menu for Mediafiles -->
|
||||
<mat-menu #mediafilesMenu="matMenu">
|
||||
<button mat-menu-item *osPerms="'mediafiles.can_manage'" (click)="toggleMultiSelect()">
|
||||
<div *ngIf="!isMultiSelect">
|
||||
<button mat-menu-item *osPerms="'mediafiles.can_manage'" (click)="toggleMultiSelect()">
|
||||
<mat-icon>library_add</mat-icon>
|
||||
<span translate>Multiselect</span>
|
||||
</button>
|
||||
</div>
|
||||
<div *ngIf="isMultiSelect">
|
||||
<button mat-menu-item (click)="toggleMultiSelect()">
|
||||
<mat-icon>library_add</mat-icon>
|
||||
<span translate>MultiSelect</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>
|
||||
<span translate>Exit multiselect</span>
|
||||
</button>
|
||||
<mat-divider></mat-divider>
|
||||
<button mat-menu-item *osPerms="'mediafiles.can_manage'" (click)="deleteSelected()">
|
||||
<mat-icon>delete</mat-icon>
|
||||
<span translate>Delete selected</span>
|
||||
</button>
|
||||
</div>
|
||||
</mat-menu>
|
||||
|
@ -18,13 +18,6 @@
|
||||
</button>
|
||||
<span>{{ selectedRows.length }} </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>
|
||||
|
||||
<div class="custom-table-header on-transition-fade">
|
||||
@ -86,9 +79,9 @@
|
||||
<!-- state column -->
|
||||
<ng-container matColumnDef="state">
|
||||
<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='motion.category' class='small'>
|
||||
<mat-icon>device_hub</mat-icon> {{ motion.category }}
|
||||
<mat-icon>device_hub</mat-icon>{{ motion.category }}
|
||||
</div>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
@ -119,7 +112,7 @@
|
||||
<div *ngIf="!isMultiSelect">
|
||||
<button mat-menu-item *osPerms="'motions.can_manage'" (click)="toggleMultiSelect()">
|
||||
<mat-icon>library_add</mat-icon>
|
||||
<span translate>MultiSelect</span>
|
||||
<span translate>Multiselect</span>
|
||||
</button>
|
||||
<button mat-menu-item routerLink="category">
|
||||
<mat-icon>device_hub</mat-icon>
|
||||
@ -139,25 +132,62 @@
|
||||
</button>
|
||||
</div>
|
||||
<div *ngIf="isMultiSelect">
|
||||
<button mat-menu-item (click)="toggleMultiSelect()">
|
||||
<mat-icon>library_add</mat-icon>
|
||||
<span translate>Exit multiselect</span>
|
||||
</button>
|
||||
<div *osPerms="'motions.can_manage'">
|
||||
<button mat-menu-item (click)="deleteSelected()">
|
||||
<mat-icon>delete</mat-icon>
|
||||
<span translate>Delete selected</span>
|
||||
<mat-divider></mat-divider>
|
||||
<button mat-menu-item>
|
||||
<!-- TODO: Not implemented yet -->
|
||||
<mat-icon>sort</mat-icon>
|
||||
<span translate>Move to agenda item</span>
|
||||
</button>
|
||||
<button mat-menu-item (click)="openSetStatusMenu()">
|
||||
<mat-icon>sentiment_satisfied</mat-icon>
|
||||
<!-- TODO: icon -->
|
||||
<button mat-menu-item (click)="multiselectService.setStatus(selectedRows); toggleMultiSelect()">
|
||||
<mat-icon>label</mat-icon>
|
||||
<span translate>Set status</span>
|
||||
</button>
|
||||
<button mat-menu-item (click)="openSetCategoryMenu()">
|
||||
<mat-icon>sentiment_satisfied</mat-icon>
|
||||
<button mat-menu-item (click)="multiselectService.setRecommendation(selectedRows); toggleMultiSelect()">
|
||||
<mat-icon>report</mat-icon>
|
||||
<!-- TODO: better icon -->
|
||||
<span translate>Set recommendation</span>
|
||||
</button>
|
||||
<button mat-menu-item (click)="multiselectService.setCategory(selectedRows); toggleMultiSelect()">
|
||||
<mat-icon>device_hub</mat-icon>
|
||||
<!-- TODO: icon -->
|
||||
<span translate>Set categories</span>
|
||||
</button>
|
||||
<mat-divider></mat-divider>
|
||||
<button mat-menu-item (click)="multiselectService.addSubmitters(selectedRows); toggleMultiSelect()">
|
||||
<mat-icon>person_add</mat-icon>
|
||||
<!-- TODO: icon -->
|
||||
<span translate>Add submitters</span>
|
||||
</button>
|
||||
<button mat-menu-item (click)="multiselectService.removeSubmitters(selectedRows); toggleMultiSelect()">
|
||||
<mat-icon>person_outline</mat-icon>
|
||||
<!-- TODO: icon -->
|
||||
<span translate>remove submitters</span>
|
||||
</button>
|
||||
<button mat-menu-item (click)="multiselectService.addTags(selectedRows); toggleMultiSelect()">
|
||||
<mat-icon>bookmarks</mat-icon>
|
||||
<!-- TODO: icon -->
|
||||
<span translate>Add tags</span>
|
||||
</button>
|
||||
<button mat-menu-item (click)="multiselectService.removeTags(selectedRows); toggleMultiSelect()">
|
||||
<mat-icon>bookmark_border</mat-icon>
|
||||
<!-- TODO: icon -->
|
||||
<span translate>Remove tags</span>
|
||||
</button>
|
||||
</div>
|
||||
<button mat-menu-item (click)="csvExportMotionList()">
|
||||
<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 mat-menu-item class="red-warning-text" (click)="multiselectService.delete(selectedRows); toggleMultiSelect()">
|
||||
<mat-icon>delete</mat-icon>
|
||||
<span translate>Delete selected</span>
|
||||
</button>
|
||||
</div>
|
||||
</mat-menu>
|
||||
|
@ -3,15 +3,14 @@ import { Router, ActivatedRoute } from '@angular/router';
|
||||
import { Title } from '@angular/platform-browser';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
import { ConfigService } from '../../../../core/services/config.service';
|
||||
import { MotionCsvExportService } from '../../services/motion-csv-export.service';
|
||||
import { ListViewBaseComponent } from '../../../base/list-view-base';
|
||||
import { MatSnackBar } from '@angular/material';
|
||||
import { MotionRepositoryService } from '../../services/motion-repository.service';
|
||||
import { ViewMotion } from '../../models/view-motion';
|
||||
import { WorkflowState } from '../../../../shared/models/motions/workflow-state';
|
||||
import { ListViewBaseComponent } from '../../../base/list-view-base';
|
||||
import { MatSnackBar } from '@angular/material';
|
||||
import { ConfigService } from '../../../../core/services/config.service';
|
||||
import { Category } from '../../../../shared/models/motions/category';
|
||||
import { PromptService } from '../../../../core/services/prompt.service';
|
||||
import { MotionCsvExportService } from '../../services/motion-csv-export.service';
|
||||
import { MotionMultiselectService } from '../../services/motion-multiselect.service';
|
||||
|
||||
/**
|
||||
* Component that displays all the motions in a Table using DataSource.
|
||||
@ -51,8 +50,13 @@ export class MotionListComponent extends ListViewBaseComponent<ViewMotion> imple
|
||||
* @param route Current route
|
||||
* @param configService The configuration provider
|
||||
* @param repo Motion Repository
|
||||
* @param csvExport CSV Export Service
|
||||
* @param promptService
|
||||
* @param motionCsvExport
|
||||
* @param workflowRepo Workflow Repository
|
||||
* @param categoryRepo
|
||||
* @param userRepo
|
||||
* @param tagRepo
|
||||
* @param choiceService
|
||||
*/
|
||||
public constructor(
|
||||
titleService: Title,
|
||||
@ -62,8 +66,8 @@ export class MotionListComponent extends ListViewBaseComponent<ViewMotion> imple
|
||||
private route: ActivatedRoute,
|
||||
private configService: ConfigService,
|
||||
private repo: MotionRepositoryService,
|
||||
private promptService: PromptService,
|
||||
private motionCsvExport: MotionCsvExportService
|
||||
private motionCsvExport: MotionCsvExportService,
|
||||
public multiselectService: MotionMultiselectService
|
||||
) {
|
||||
super(titleService, translate, matSnackBar);
|
||||
|
||||
@ -90,11 +94,7 @@ export class MotionListComponent extends ListViewBaseComponent<ViewMotion> imple
|
||||
}
|
||||
});
|
||||
});
|
||||
this.configService.get('motions_statutes_enabled').subscribe(
|
||||
(enabled: boolean): void => {
|
||||
this.statutesEnabled = enabled;
|
||||
}
|
||||
);
|
||||
this.configService.get('motions_statutes_enabled').subscribe(enabled => (this.statutesEnabled = enabled));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -164,56 +164,8 @@ export class MotionListComponent extends ListViewBaseComponent<ViewMotion> imple
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the items selected.
|
||||
* SelectedRows is only filled with data in multiSelect mode
|
||||
* Returns current definitions for the listView table
|
||||
*/
|
||||
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);
|
||||
|
@ -153,6 +153,12 @@ export class ViewMotion extends BaseViewModel {
|
||||
return this.motion && this.motion.state_id ? this.motion.state_id : null;
|
||||
}
|
||||
|
||||
public get possibleStates(): WorkflowState[] {
|
||||
return this.workflow
|
||||
? this.workflow.states
|
||||
: null;
|
||||
}
|
||||
|
||||
public get recommendation_id(): number {
|
||||
return this.motion && this.motion.recommendation_id ? this.motion.recommendation_id : null;
|
||||
}
|
||||
@ -213,6 +219,10 @@ export class ViewMotion extends BaseViewModel {
|
||||
return this.motion && this.motion.amendment_paragraphs ? this.motion.amendment_paragraphs : [];
|
||||
}
|
||||
|
||||
public get tags_id(): number[] {
|
||||
return this._motion ? this._motion.tags_id : null;
|
||||
}
|
||||
|
||||
public constructor(
|
||||
motion?: Motion,
|
||||
category?: Category,
|
||||
|
63
client/src/app/site/motions/models/view-workflow.ts
Normal file
63
client/src/app/site/motions/models/view-workflow.ts
Normal file
@ -0,0 +1,63 @@
|
||||
import { Workflow } from '../../../shared/models/motions/workflow';
|
||||
import { WorkflowState } from '../../../shared/models/motions/workflow-state';
|
||||
import { BaseViewModel } from '../../base/base-view-model';
|
||||
|
||||
/**
|
||||
* class for the ViewWorkflow. Currently only a basic stub
|
||||
*
|
||||
* Stores a Category including all (implicit) references
|
||||
* Provides "safe" access to variables and functions in {@link Category}
|
||||
* @ignore
|
||||
*/
|
||||
export class ViewWorkflow extends BaseViewModel {
|
||||
private _workflow: Workflow;
|
||||
|
||||
public constructor(workflow?: Workflow, id?: number, name?: string) {
|
||||
super();
|
||||
if (!workflow) {
|
||||
workflow = new Workflow();
|
||||
workflow.id = id;
|
||||
workflow.name = name;
|
||||
}
|
||||
this._workflow = workflow;
|
||||
}
|
||||
|
||||
public get workflow(): Workflow {
|
||||
return this._workflow;
|
||||
}
|
||||
|
||||
public get id(): number {
|
||||
return this.workflow ? this.workflow.id : null;
|
||||
}
|
||||
|
||||
public get name(): string {
|
||||
return this.workflow ? this.workflow.name : null;
|
||||
}
|
||||
|
||||
public get states() : WorkflowState[] {
|
||||
return this.workflow ? this.workflow.states : null;
|
||||
}
|
||||
|
||||
public get first_state(): number {
|
||||
return this.workflow ? this.workflow.first_state : null;
|
||||
}
|
||||
|
||||
public getTitle(): string {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Duplicate this motion into a copy of itself
|
||||
*/
|
||||
public copy(): ViewWorkflow {
|
||||
return new ViewWorkflow(this._workflow);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the local objects if required
|
||||
* @param update
|
||||
*/
|
||||
public updateValues(update: Workflow): void {
|
||||
this._workflow = update;
|
||||
}
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
import { Category } from '../../../shared/models/motions/category';
|
||||
import { ViewCategory } from '../models/view-category';
|
||||
import { DataSendService } from '../../../core/services/data-send.service';
|
||||
|
@ -0,0 +1,20 @@
|
||||
import { TestBed, inject } from '@angular/core/testing';
|
||||
|
||||
import { E2EImportsModule } from 'e2e-imports.module';
|
||||
import { MotionMultiselectService } from './motion-multiselect.service';
|
||||
|
||||
describe('MotionMultiselectService', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [E2EImportsModule],
|
||||
providers: [MotionMultiselectService]
|
||||
});
|
||||
});
|
||||
|
||||
it('should be created', inject(
|
||||
[MotionMultiselectService],
|
||||
(service: MotionMultiselectService) => {
|
||||
expect(service).toBeTruthy();
|
||||
}
|
||||
));
|
||||
});
|
@ -0,0 +1,173 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
import { ViewMotion } from '../models/view-motion';
|
||||
import { ChoiceService } from 'app/core/services/choice.service';
|
||||
import { PromptService } from 'app/core/services/prompt.service';
|
||||
import { MotionRepositoryService } from './motion-repository.service';
|
||||
import { UserRepositoryService } from 'app/site/users/services/user-repository.service';
|
||||
import { WorkflowRepositoryService } from './workflow-repository.service';
|
||||
import { CategoryRepositoryService } from './category-repository.service';
|
||||
import { TagRepositoryService } from 'app/site/tags/services/tag-repository.service';
|
||||
|
||||
/**
|
||||
* Contains all multiselect actions for the motion list view.
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class MotionMultiselectService {
|
||||
/**
|
||||
* Does nothing.
|
||||
*
|
||||
* @param repo MotionRepositoryService
|
||||
* @param translate TranslateService
|
||||
* @param promptService
|
||||
* @param choiceService
|
||||
* @param userRepo
|
||||
* @param workflowRepo
|
||||
* @param categoryRepo
|
||||
* @param tagRepo
|
||||
*/
|
||||
public constructor(
|
||||
private repo: MotionRepositoryService,
|
||||
private translate: TranslateService,
|
||||
private promptService: PromptService,
|
||||
private choiceService: ChoiceService,
|
||||
private userRepo: UserRepositoryService,
|
||||
private workflowRepo: WorkflowRepositoryService,
|
||||
private categoryRepo: CategoryRepositoryService,
|
||||
private tagRepo: TagRepositoryService
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Deletes the given motions. Asks for confirmation.
|
||||
*
|
||||
* @param motions The motions to delete
|
||||
*/
|
||||
public async delete(motions: ViewMotion[]): 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 motions) {
|
||||
await this.repo.delete(motion);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a dialog and then sets the status for all motions.
|
||||
*
|
||||
* @param motions The motions to change
|
||||
*/
|
||||
public async setStatus(motions: ViewMotion[]): Promise<void> {
|
||||
const title = this.translate.instant('This will set the state of all selected motions to:');
|
||||
const choices = this.workflowRepo.getAllWorkflowStates().map(workflowState => ({
|
||||
id: workflowState.id,
|
||||
label: workflowState.name
|
||||
}));
|
||||
const selectedChoice = await this.choiceService.open(title, choices);
|
||||
if (selectedChoice) {
|
||||
for (const motion of motions) {
|
||||
await this.repo.setState(motion, selectedChoice as number);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a dialog and sets the recommendation to the users choice for all selected motions.
|
||||
*
|
||||
* @param motions The motions to change
|
||||
*/
|
||||
public async setRecommendation(motions: ViewMotion[]): Promise<void> {
|
||||
const title = this.translate.instant('This will set the recommendation for all selected motions to:');
|
||||
const choices = this.workflowRepo
|
||||
.getAllWorkflowStates()
|
||||
.filter(workflowState => !!workflowState.recommendation_label)
|
||||
.map(workflowState => ({
|
||||
id: workflowState.id,
|
||||
label: workflowState.recommendation_label
|
||||
}));
|
||||
const selectedChoice = await this.choiceService.open(title, choices);
|
||||
if (selectedChoice) {
|
||||
for (const motion of motions) {
|
||||
await this.repo.setRecommendation(motion, selectedChoice as number);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a dialog and sets the category for all given motions.
|
||||
*
|
||||
* @param motions The motions to change
|
||||
*/
|
||||
public async setCategory(motions: ViewMotion[]): Promise<void> {
|
||||
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());
|
||||
if (selectedChoice) {
|
||||
for (const motion of motions) {
|
||||
await this.repo.update({ category_id: selectedChoice as number }, motion);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a dialog and adds the selected submitters for all given motions.
|
||||
*
|
||||
* @param motions The motions to add the sumbitters to
|
||||
*/
|
||||
public async addSubmitters(motions: ViewMotion[]): Promise<void> {
|
||||
const title = this.translate.instant('This will add the following submitters of all selected motions:');
|
||||
const selectedChoice = await this.choiceService.open(title, this.userRepo.getViewModelList(), true);
|
||||
if (selectedChoice) {
|
||||
throw new Error("Not implemented on the server");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
throw new Error("Not implemented on the server");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a dialog and adds the selected tags for all given motions.
|
||||
*
|
||||
* @param motions The motions to add the tags to
|
||||
*/
|
||||
public async addTags(motions: ViewMotion[]): Promise<void> {
|
||||
const title = this.translate.instant('This will add the following tags to all selected motions:');
|
||||
const selectedChoice = await this.choiceService.open(title, this.tagRepo.getViewModelList(), true);
|
||||
if (selectedChoice) {
|
||||
for (const motion of motions) {
|
||||
let tagIds = [...motion.tags_id, ...(selectedChoice as number[])];
|
||||
tagIds = tagIds.filter((id, index, self) => self.indexOf(id) === index);
|
||||
await this.repo.update({ tags_id: tagIds }, motion);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
for (const motion of motions) {
|
||||
const tagIdsToRemove = selectedChoice as number[];
|
||||
const tagIds = motion.tags_id.filter(id => !tagIdsToRemove.includes(id));
|
||||
await this.repo.update({ tags_id: tagIds }, motion);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
import { TestBed, inject } from '@angular/core/testing';
|
||||
|
||||
import { WorkflowRepositoryService } from './workflow-repository.service';
|
||||
import { E2EImportsModule } from '../../../../e2e-imports.module';
|
||||
|
||||
describe('WorkflowRepositoryService', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [E2EImportsModule],
|
||||
providers: [WorkflowRepositoryService]
|
||||
});
|
||||
});
|
||||
|
||||
it('should be created', inject([WorkflowRepositoryService], (service: WorkflowRepositoryService) => {
|
||||
expect(service).toBeTruthy();
|
||||
}));
|
||||
});
|
@ -0,0 +1,80 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Workflow } from '../../../shared/models/motions/workflow';
|
||||
import { ViewWorkflow } from '../models/view-workflow';
|
||||
import { DataSendService } from '../../../core/services/data-send.service';
|
||||
import { DataStoreService } from '../../../core/services/data-store.service';
|
||||
import { BaseRepository } from '../../base/base-repository';
|
||||
import { Identifiable } from '../../../shared/models/base/identifiable';
|
||||
import { CollectionStringModelMapperService } from '../../../core/services/collectionStringModelMapper.service';
|
||||
import { WorkflowState } from 'app/shared/models/motions/workflow-state';
|
||||
|
||||
/**
|
||||
* Repository Services for Categories
|
||||
*
|
||||
* The repository is meant to process domain objects (those found under
|
||||
* shared/models), so components can display them and interact with them.
|
||||
*
|
||||
* Rather than manipulating models directly, the repository is meant to
|
||||
* inform the {@link DataSendService} about changes which will send
|
||||
* them to the Server.
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class WorkflowRepositoryService extends BaseRepository<ViewWorkflow, Workflow> {
|
||||
/**
|
||||
* Creates a WorkflowRepository
|
||||
* Converts existing and incoming workflow to ViewWorkflows
|
||||
* @param DS
|
||||
* @param dataSend
|
||||
*/
|
||||
public constructor(
|
||||
protected DS: DataStoreService,
|
||||
mapperService: CollectionStringModelMapperService,
|
||||
private dataSend: DataSendService
|
||||
) {
|
||||
super(DS, mapperService, Workflow);
|
||||
}
|
||||
|
||||
protected createViewModel(workflow: Workflow): ViewWorkflow {
|
||||
return new ViewWorkflow(workflow);
|
||||
}
|
||||
|
||||
public async create(newWorkflow: Workflow): Promise<Identifiable> {
|
||||
return await this.dataSend.createModel(newWorkflow);
|
||||
}
|
||||
|
||||
public async update(workflow: Partial<Workflow>, viewWorkflow: ViewWorkflow): Promise<void> {
|
||||
let updateWorkflow: Workflow;
|
||||
if (viewWorkflow) {
|
||||
updateWorkflow = viewWorkflow.workflow;
|
||||
} else {
|
||||
updateWorkflow = new Workflow();
|
||||
}
|
||||
updateWorkflow.patchValues(workflow);
|
||||
await this.dataSend.updateModel(updateWorkflow);
|
||||
}
|
||||
|
||||
public async delete(viewWorkflow: ViewWorkflow): Promise<void> {
|
||||
const workflow = viewWorkflow.workflow;
|
||||
await this.dataSend.deleteModel(workflow);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the workflow for the ID
|
||||
* @param workflow_id workflow ID
|
||||
*/
|
||||
public getWorkflowByID(workflow_id: number): Workflow {
|
||||
const wfList = this.DS.getAll(Workflow);
|
||||
return wfList.find(workflow => workflow.id === workflow_id);
|
||||
}
|
||||
|
||||
public getAllWorkflowStates(): WorkflowState[]{
|
||||
let states: WorkflowState[] = [];
|
||||
this.DS.getAll(Workflow).forEach(workflow => {
|
||||
states = states.concat(workflow.states);
|
||||
})
|
||||
return states;
|
||||
}
|
||||
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
import { Tag } from '../../../shared/models/core/tag';
|
||||
import { ViewTag } from '../models/view-tag';
|
||||
import { DataSendService } from '../../../core/services/data-send.service';
|
||||
|
@ -259,18 +259,10 @@ export class UserDetailComponent extends BaseViewComponent implements OnInit {
|
||||
|
||||
/**
|
||||
* Handler for the generate Password button.
|
||||
* Generates a password using 8 pseudo-random letters
|
||||
* from the `characters` const.
|
||||
*/
|
||||
public generatePassword(): void {
|
||||
let pw = '';
|
||||
const characters = 'abcdefghijkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789';
|
||||
const amount = 8;
|
||||
for (let i = 0; i < amount; i++) {
|
||||
pw += characters.charAt(Math.floor(Math.random() * characters.length));
|
||||
}
|
||||
this.personalInfoForm.patchValue({
|
||||
default_password: pw
|
||||
default_password: this.repo.getRandomPassword()
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -91,7 +91,7 @@
|
||||
<div *ngIf="!isMultiSelect">
|
||||
<button mat-menu-item *osPerms="'users.can_manage'" (click)="toggleMultiSelect()">
|
||||
<mat-icon>library_add</mat-icon>
|
||||
<span translate>MultiSelect</span>
|
||||
<span translate>Multiselect</span>
|
||||
</button>
|
||||
|
||||
<button mat-menu-item *osPerms="'users.can_manage'" routerLink="groups">
|
||||
@ -110,11 +110,19 @@
|
||||
</button>
|
||||
</div>
|
||||
<div *ngIf="isMultiSelect">
|
||||
<button mat-menu-item (click)="toggleMultiSelect()">
|
||||
<mat-icon>library_add</mat-icon>
|
||||
<span translate>Exit multiselect</span>
|
||||
</button>
|
||||
<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? -->
|
||||
<mat-divider></mat-divider>
|
||||
<button mat-menu-item (click)="setGroupSelected(true)">
|
||||
<mat-icon>people</mat-icon>
|
||||
<span translate>Add groups</span>
|
||||
</button>
|
||||
<button mat-menu-item (click)="setGroupSelected(false)">
|
||||
<mat-icon>people_outline</mat-icon>
|
||||
<span translate>Remove groups</span>
|
||||
</button>
|
||||
|
||||
<mat-divider></mat-divider>
|
||||
@ -123,44 +131,41 @@
|
||||
<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>
|
||||
<mat-icon>check_box</mat-icon>
|
||||
<span translate>Set as present</span>
|
||||
</button>
|
||||
|
||||
<button mat-menu-item (click)="setPresentSelected(false)">
|
||||
<mat-icon>remove_circle</mat-icon>
|
||||
<mat-icon>check_box_outline_blank</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>
|
||||
<mat-icon>account_balance</mat-icon>
|
||||
<span translate>Set as committee</span>
|
||||
</button>
|
||||
|
||||
<button mat-menu-item (click)="setCommitteeSelected(false)">
|
||||
<mat-icon>remove_circle</mat-icon>
|
||||
<mat-icon>account_balance</mat-icon>
|
||||
<span translate>Unset committee</span>
|
||||
</button>
|
||||
|
||||
<mat-divider></mat-divider>
|
||||
|
||||
<button mat-menu-item (click)="sendInvitationSelected()">
|
||||
<button mat-menu-item (click)="sendInvitationEmailSelected()">
|
||||
<mat-icon>mail</mat-icon>
|
||||
<span translate>Send invitations</span>
|
||||
<span translate>Send invitation emails</span>
|
||||
</button>
|
||||
|
||||
<button mat-menu-item (click)="deleteSelected()">
|
||||
<button mat-menu-item (click)="resetPasswordsSelected()">
|
||||
<mat-icon>vpn_key</mat-icon>
|
||||
<span translate>Generate new passwords</span>
|
||||
</button>
|
||||
<mat-divider></mat-divider>
|
||||
<button mat-menu-item class="red-warning-text" (click)="deleteSelected()">
|
||||
<mat-icon>delete</mat-icon>
|
||||
<span translate>Delete selected</span>
|
||||
</button>
|
||||
|
@ -1,15 +1,16 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { MatSnackBar } from '@angular/material';
|
||||
import { Router, ActivatedRoute } from '@angular/router';
|
||||
import { Title } from '@angular/platform-browser';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { CsvExportService } from '../../../../core/services/csv-export.service';
|
||||
|
||||
import { ViewUser } from '../../models/view-user';
|
||||
import { UserRepositoryService } from '../../services/user-repository.service';
|
||||
import { CsvExportService } from '../../../../core/services/csv-export.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 { GroupRepositoryService } from '../../services/group-repository.service';
|
||||
import { PromptService } from '../../../../core/services/prompt.service';
|
||||
import { UserRepositoryService } from '../../services/user-repository.service';
|
||||
import { ViewUser } from '../../models/view-user';
|
||||
import { ChoiceService } from '../../../../core/services/choice.service';
|
||||
|
||||
/**
|
||||
* Component for the user list view.
|
||||
@ -28,6 +29,7 @@ export class UserListComponent extends ListViewBaseComponent<ViewUser> implement
|
||||
* @param translate Service for translation handling
|
||||
* @param matSnackBar Helper to diplay errors
|
||||
* @param repo the user repository
|
||||
* @param groupRepo: The user group repository
|
||||
* @param router the router service
|
||||
* @param route the local route
|
||||
* @param csvExport CSV export Service,
|
||||
@ -38,10 +40,12 @@ export class UserListComponent extends ListViewBaseComponent<ViewUser> implement
|
||||
translate: TranslateService,
|
||||
matSnackBar: MatSnackBar,
|
||||
private repo: UserRepositoryService,
|
||||
private groupRepo: GroupRepositoryService,
|
||||
private choiceService: ChoiceService,
|
||||
private router: Router,
|
||||
private route: ActivatedRoute,
|
||||
protected csvExport: CsvExportService,
|
||||
private promptService: PromptService
|
||||
private promptService: PromptService,
|
||||
) {
|
||||
super(titleService, translate, matSnackBar);
|
||||
|
||||
@ -124,31 +128,31 @@ export class UserListComponent extends ListViewBaseComponent<ViewUser> implement
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* Opens a dialog and sets the group(s) for all selected users.
|
||||
* SelectedRows is only filled with data in multiSelect mode
|
||||
*/
|
||||
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);
|
||||
public async setGroupSelected(add: boolean): Promise<void> {
|
||||
let content: string;
|
||||
if (add){
|
||||
content = this.translate.instant('This will add the following groups to all selected users:');
|
||||
} else {
|
||||
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) {
|
||||
for (const user of this.selectedRows) {
|
||||
const newGroups = [...user.groups_id];
|
||||
(selectedChoice as number[]).forEach(newChoice => {
|
||||
const idx = newGroups.indexOf(newChoice);
|
||||
if (idx < 0 && add) {
|
||||
newGroups.push(newChoice);
|
||||
} else if (idx >= 0 && !add) {
|
||||
newGroups.slice(idx, 1);
|
||||
}
|
||||
});
|
||||
await this.repo.update({ groups_id: newGroups }, user);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -183,12 +187,20 @@ export class UserListComponent extends ListViewBaseComponent<ViewUser> implement
|
||||
|
||||
/**
|
||||
* Handler for bulk sending e-mail invitations. Uses selectedRows defined via
|
||||
* multiSelect mode. TODO: Not yet implemented (no service)
|
||||
* multiSelect mode.
|
||||
*/
|
||||
public async sendInvitationSelected(): Promise<void> {
|
||||
// this.selectedRows.forEach(vm => {
|
||||
// TODO if !vm.emailSent {vm.sendInvitation}
|
||||
// });
|
||||
public sendInvitationEmailSelected(): void {
|
||||
this.repo.sendInvitationEmail(this.selectedRows).then(this.raiseError, this.raiseError);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for bulk resetting passwords. Needs multiSelect mode.
|
||||
*/
|
||||
public async resetPasswordsSelected(): Promise<void> {
|
||||
for (const user of this.selectedRows) {
|
||||
const password = this.repo.getRandomPassword();
|
||||
this.repo.resetPassword(user, password);
|
||||
}
|
||||
}
|
||||
|
||||
public getColumnDefinition(): string[] {
|
||||
|
@ -39,6 +39,10 @@ export class ViewUser extends BaseViewModel {
|
||||
return this.user ? this.user.full_name : null;
|
||||
}
|
||||
|
||||
public get short_name(): string {
|
||||
return this.user ? this.user.short_name : null;
|
||||
}
|
||||
|
||||
public get email(): string {
|
||||
return this.user ? this.user.email : null;
|
||||
}
|
||||
|
@ -1,13 +1,13 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
import { ViewGroup } from '../models/view-group';
|
||||
import { BaseRepository } from '../../base/base-repository';
|
||||
import { Group } from '../../../shared/models/users/group';
|
||||
import { DataStoreService } from '../../../core/services/data-store.service';
|
||||
import { DataSendService } from '../../../core/services/data-send.service';
|
||||
import { ConstantsService } from '../../../core/services/constants.service';
|
||||
import { Identifiable } from '../../../shared/models/base/identifiable';
|
||||
import { CollectionStringModelMapperService } from '../../../core/services/collectionStringModelMapper.service';
|
||||
import { ConstantsService } from '../../../core/services/constants.service';
|
||||
import { DataSendService } from '../../../core/services/data-send.service';
|
||||
import { DataStoreService } from '../../../core/services/data-store.service';
|
||||
import { Group } from '../../../shared/models/users/group';
|
||||
import { Identifiable } from '../../../shared/models/base/identifiable';
|
||||
import { ViewGroup } from '../models/view-group';
|
||||
|
||||
/**
|
||||
* Set rules to define the shape of an app permission
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
import { BaseRepository } from '../../base/base-repository';
|
||||
import { ViewUser } from '../models/view-user';
|
||||
import { User } from '../../../shared/models/users/user';
|
||||
@ -8,6 +7,9 @@ import { DataStoreService } from '../../../core/services/data-store.service';
|
||||
import { DataSendService } from '../../../core/services/data-send.service';
|
||||
import { Identifiable } from '../../../shared/models/base/identifiable';
|
||||
import { CollectionStringModelMapperService } from '../../../core/services/collectionStringModelMapper.service';
|
||||
import { ConfigService } from 'app/core/services/config.service';
|
||||
import { HttpService } from 'app/core/services/http.service';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
/**
|
||||
* Repository service for users
|
||||
@ -24,7 +26,10 @@ export class UserRepositoryService extends BaseRepository<ViewUser, User> {
|
||||
public constructor(
|
||||
DS: DataStoreService,
|
||||
mapperService: CollectionStringModelMapperService,
|
||||
private dataSend: DataSendService
|
||||
private dataSend: DataSendService,
|
||||
private translate: TranslateService,
|
||||
private httpService: HttpService,
|
||||
private configService: ConfigService
|
||||
) {
|
||||
super(DS, mapperService, User, [Group]);
|
||||
}
|
||||
@ -84,4 +89,70 @@ export class UserRepositoryService extends BaseRepository<ViewUser, User> {
|
||||
const groups = this.DS.getMany(Group, user.groups_id);
|
||||
return new ViewUser(user, groups);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a random password
|
||||
*
|
||||
* @param length THe length of the password to generate
|
||||
* @returns a random password
|
||||
*/
|
||||
public getRandomPassword(length: number = 8): string {
|
||||
let pw = '';
|
||||
const characters = 'abcdefghijkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789';
|
||||
for (let i = 0; i < length; i++) {
|
||||
pw += characters.charAt(Math.floor(Math.random() * characters.length));
|
||||
}
|
||||
return pw;
|
||||
}
|
||||
|
||||
public async resetPassword(user: ViewUser, password: string): Promise<void> {
|
||||
await this.update({ default_password: password }, user);
|
||||
const path = `/rest/users/user/${user.id}/reset_password/`;
|
||||
await this.httpService.post(path, { password: password });
|
||||
}
|
||||
|
||||
public async sendInvitationEmail(users: ViewUser[]): Promise<string> {
|
||||
const user_ids = users.map(user => user.id);
|
||||
const subject = this.translate.instant(this.configService.instant('users_email_subject'));
|
||||
const message = this.translate.instant(this.configService.instant('users_email_body'));
|
||||
|
||||
const response = await this.httpService.post<{count: Number; no_email_ids: number[]}>('/rest/users/user/mass_invite_email/', {
|
||||
user_ids: user_ids,
|
||||
subject: subject,
|
||||
message: message,
|
||||
});
|
||||
const numEmails = response.count;
|
||||
const noEmailIds = response.no_email_ids;
|
||||
let msg;
|
||||
if (numEmails === 0) {
|
||||
msg = this.translate.instant('No emails were send.');
|
||||
} else if (numEmails === 1) {
|
||||
msg = this.translate.instant('One email was send sucessfully.');
|
||||
} else {
|
||||
msg = this.translate.instant('%num% emails were send sucessfully.').replace('%num%', numEmails);
|
||||
}
|
||||
|
||||
if (noEmailIds.length) {
|
||||
msg += ' ';
|
||||
|
||||
if (noEmailIds.length === 1) {
|
||||
msg += this.translate.instant('The user %user% has no email, so the invitation email could not be send.');
|
||||
} else {
|
||||
msg += this.translate.instant('The users %user% have no email, so the invitation emails could not be send.');
|
||||
}
|
||||
|
||||
// This one builds a username string like "user1, user2 and user3" with the full names.
|
||||
const usernames = noEmailIds.map(id => this.getViewModel(id)).filter(user => !!user).map(user => user.short_name);
|
||||
let userString;
|
||||
if (usernames.length > 1) {
|
||||
const lastUsername = usernames.pop();
|
||||
userString = usernames.join(', ') + ' ' + this.translate.instant('and') + ' ' + lastUsername;
|
||||
} else {
|
||||
userString = usernames.join(', ')
|
||||
}
|
||||
msg = msg.replace('%user%', userString);
|
||||
}
|
||||
|
||||
return msg;
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user