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 { ViewportService } from './services/viewport.service';
|
||||||
import { PromptDialogComponent } from '../shared/components/prompt-dialog/prompt-dialog.component';
|
import { PromptDialogComponent } from '../shared/components/prompt-dialog/prompt-dialog.component';
|
||||||
import { HttpService } from './services/http.service';
|
import { HttpService } from './services/http.service';
|
||||||
|
import { ChoiceDialogComponent } from '../shared/components/choice-dialog/choice-dialog.component';
|
||||||
|
|
||||||
/** Global Core Module. Contains all global (singleton) services
|
/** Global Core Module. Contains all global (singleton) services
|
||||||
*
|
*
|
||||||
@ -31,7 +32,7 @@ import { HttpService } from './services/http.service';
|
|||||||
ViewportService,
|
ViewportService,
|
||||||
WebsocketService
|
WebsocketService
|
||||||
],
|
],
|
||||||
entryComponents: [PromptDialogComponent]
|
entryComponents: [PromptDialogComponent, ChoiceDialogComponent]
|
||||||
})
|
})
|
||||||
export class CoreModule {
|
export class CoreModule {
|
||||||
/** make sure CoreModule is imported only by one NgModule, the AppModule */
|
/** 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 { TestBed, inject } from '@angular/core/testing';
|
||||||
|
|
||||||
import { ConfigService } from './config.service';
|
import { ConfigService } from './config.service';
|
||||||
|
import { E2EImportsModule } from 'e2e-imports.module';
|
||||||
|
|
||||||
describe('ConfigService', () => {
|
describe('ConfigService', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
|
imports: [E2EImportsModule],
|
||||||
providers: [ConfigService]
|
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>
|
</div>
|
||||||
|
|
||||||
<!-- centered information slot-->
|
<!-- centered information slot-->
|
||||||
<div *ngIf="multiSelectMode" class=spacer></div>
|
<div *ngIf="!multiSelectMode" class=spacer></div>
|
||||||
<div class="toolbar-centered on-transition-fade" *ngIf="multiSelectMode">
|
<div class="toolbar-centered on-transition-fade" *ngIf="multiSelectMode">
|
||||||
<ng-content select=".central-info-slot"></ng-content>
|
<ng-content select=".central-info-slot"></ng-content>
|
||||||
</div>
|
</div>
|
||||||
|
@ -66,6 +66,7 @@ import { PromptDialogComponent } from './components/prompt-dialog/prompt-dialog.
|
|||||||
import { SortingListComponent } from './components/sorting-list/sorting-list.component';
|
import { SortingListComponent } from './components/sorting-list/sorting-list.component';
|
||||||
import { SpeakerListComponent } from 'app/site/agenda/components/speaker-list/speaker-list.component';
|
import { SpeakerListComponent } from 'app/site/agenda/components/speaker-list/speaker-list.component';
|
||||||
import { SortingTreeComponent } from './components/sorting-tree/sorting-tree.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.
|
* Share Module for all "dumb" components and pipes.
|
||||||
@ -180,7 +181,8 @@ import { SortingTreeComponent } from './components/sorting-tree/sorting-tree.com
|
|||||||
PromptDialogComponent,
|
PromptDialogComponent,
|
||||||
SortingListComponent,
|
SortingListComponent,
|
||||||
SpeakerListComponent,
|
SpeakerListComponent,
|
||||||
SortingTreeComponent
|
SortingTreeComponent,
|
||||||
|
ChoiceDialogComponent
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: DateAdapter, useClass: OpenSlidesDateAdapter },
|
{ provide: DateAdapter, useClass: OpenSlidesDateAdapter },
|
||||||
|
@ -71,16 +71,12 @@
|
|||||||
<div *ngIf="!isMultiSelect">
|
<div *ngIf="!isMultiSelect">
|
||||||
<button mat-menu-item *osPerms="'agenda.can_manage'" (click)="toggleMultiSelect()">
|
<button mat-menu-item *osPerms="'agenda.can_manage'" (click)="toggleMultiSelect()">
|
||||||
<mat-icon>library_add</mat-icon>
|
<mat-icon>library_add</mat-icon>
|
||||||
<span translate>MultiSelect</span>
|
<span translate>Multiselect</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div *ngIf="isMultiSelect" >
|
<div *ngIf="isMultiSelect" >
|
||||||
<div *osPerms="'agenda.can_manage'">
|
<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>
|
<mat-divider></mat-divider>
|
||||||
<button mat-menu-item (click)="setClosedSelected(true)">
|
<button mat-menu-item (click)="setClosedSelected(true)">
|
||||||
<mat-icon>done</mat-icon>
|
<mat-icon>done</mat-icon>
|
||||||
@ -100,6 +96,11 @@
|
|||||||
<mat-icon>visibility_off</mat-icon>
|
<mat-icon>visibility_off</mat-icon>
|
||||||
<span translate>Set invisible</span>
|
<span translate>Set invisible</span>
|
||||||
</button>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</mat-menu>
|
</mat-menu>
|
||||||
|
@ -100,6 +100,7 @@ export class AgendaListComponent extends ListViewBaseComponent<ViewItem> impleme
|
|||||||
/**
|
/**
|
||||||
* Sets multiple entries' open/closed state. Needs items in selectedRows, which
|
* Sets multiple entries' open/closed state. Needs items in selectedRows, which
|
||||||
* is only filled with any data in multiSelect mode
|
* is only filled with any data in multiSelect mode
|
||||||
|
*
|
||||||
* @param closed true if the item is to be considered done
|
* @param closed true if the item is to be considered done
|
||||||
*/
|
*/
|
||||||
public async setClosedSelected(closed: boolean): Promise<void> {
|
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
|
* Sets multiple entries' visibility. Needs items in selectedRows, which
|
||||||
* is only filled with any data in multiSelect mode.
|
* is only filled with any data in multiSelect mode.
|
||||||
|
*
|
||||||
* @param visible true if the item is to be shown
|
* @param visible true if the item is to be shown
|
||||||
*/
|
*/
|
||||||
public async setVisibilitySelected(visible: boolean): Promise<void> {
|
public async setVisibilitySelected(visible: boolean): Promise<void> {
|
||||||
|
@ -69,7 +69,7 @@
|
|||||||
<div *ngIf="!isMultiSelect">
|
<div *ngIf="!isMultiSelect">
|
||||||
<button mat-menu-item *osPerms="'assignment.can_manage'" (click)="toggleMultiSelect()">
|
<button mat-menu-item *osPerms="'assignment.can_manage'" (click)="toggleMultiSelect()">
|
||||||
<mat-icon>library_add</mat-icon>
|
<mat-icon>library_add</mat-icon>
|
||||||
<span translate>MultiSelect</span>
|
<span translate>Multiselect</span>
|
||||||
</button>
|
</button>
|
||||||
<button mat-menu-item (click)="downloadAssignmentButton()">
|
<button mat-menu-item (click)="downloadAssignmentButton()">
|
||||||
<mat-icon>archive</mat-icon>
|
<mat-icon>archive</mat-icon>
|
||||||
@ -78,7 +78,12 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div *ngIf="isMultiSelect">
|
<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>
|
<mat-icon>delete</mat-icon>
|
||||||
<span translate>Delete assignments</span>
|
<span translate>Delete assignments</span>
|
||||||
</button>
|
</button>
|
||||||
|
@ -44,14 +44,6 @@
|
|||||||
<mat-icon>more_vert</mat-icon>
|
<mat-icon>more_vert</mat-icon>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Multiselect menu -->
|
|
||||||
<div class="multiselect-menu-slot">
|
|
||||||
<button type="button" mat-icon-button [matMenuTriggerFor]="mediafilesMultiSelectMenu">
|
|
||||||
<mat-icon>more_vert</mat-icon>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Multiselect info -->
|
<!-- Multiselect info -->
|
||||||
<div *ngIf="this.isMultiSelect" class="central-info-slot">
|
<div *ngIf="this.isMultiSelect" class="central-info-slot">
|
||||||
<button mat-icon-button (click)="toggleMultiSelect()">
|
<button mat-icon-button (click)="toggleMultiSelect()">
|
||||||
@ -167,14 +159,21 @@
|
|||||||
|
|
||||||
<!-- Menu for Mediafiles -->
|
<!-- Menu for Mediafiles -->
|
||||||
<mat-menu #mediafilesMenu="matMenu">
|
<mat-menu #mediafilesMenu="matMenu">
|
||||||
|
<div *ngIf="!isMultiSelect">
|
||||||
<button mat-menu-item *osPerms="'mediafiles.can_manage'" (click)="toggleMultiSelect()">
|
<button mat-menu-item *osPerms="'mediafiles.can_manage'" (click)="toggleMultiSelect()">
|
||||||
<mat-icon>library_add</mat-icon>
|
<mat-icon>library_add</mat-icon>
|
||||||
<span translate>MultiSelect</span>
|
<span translate>Multiselect</span>
|
||||||
</button>
|
</button>
|
||||||
</mat-menu>
|
</div>
|
||||||
<mat-menu #mediafilesMultiSelectMenu="matMenu">
|
<div *ngIf="isMultiSelect">
|
||||||
|
<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 *osPerms="'mediafiles.can_manage'" (click)="deleteSelected()">
|
<button mat-menu-item *osPerms="'mediafiles.can_manage'" (click)="deleteSelected()">
|
||||||
<mat-icon>delete</mat-icon>
|
<mat-icon>delete</mat-icon>
|
||||||
<span translate>Delete selected</span>
|
<span translate>Delete selected</span>
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
</mat-menu>
|
</mat-menu>
|
||||||
|
@ -18,13 +18,6 @@
|
|||||||
</button>
|
</button>
|
||||||
<span>{{ selectedRows.length }} </span><span translate>selected</span>
|
<span>{{ selectedRows.length }} </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">
|
||||||
@ -86,9 +79,9 @@
|
|||||||
<!-- state column -->
|
<!-- state column -->
|
||||||
<ng-container matColumnDef="state">
|
<ng-container matColumnDef="state">
|
||||||
<mat-header-cell *matHeaderCellDef mat-sort-header>State</mat-header-cell>
|
<mat-header-cell *matHeaderCellDef mat-sort-header>State</mat-header-cell>
|
||||||
<mat-cell *matCellDef="let motion" (click)="selectMotion(motion)">
|
<mat-cell *matCellDef="let motion">
|
||||||
<div *ngIf='motion.category' class='small'>
|
<div *ngIf='motion.category' class='small'>
|
||||||
<mat-icon>device_hub</mat-icon> {{ motion.category }}
|
<mat-icon>device_hub</mat-icon>{{ motion.category }}
|
||||||
</div>
|
</div>
|
||||||
</mat-cell>
|
</mat-cell>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
@ -119,7 +112,7 @@
|
|||||||
<div *ngIf="!isMultiSelect">
|
<div *ngIf="!isMultiSelect">
|
||||||
<button mat-menu-item *osPerms="'motions.can_manage'" (click)="toggleMultiSelect()">
|
<button mat-menu-item *osPerms="'motions.can_manage'" (click)="toggleMultiSelect()">
|
||||||
<mat-icon>library_add</mat-icon>
|
<mat-icon>library_add</mat-icon>
|
||||||
<span translate>MultiSelect</span>
|
<span translate>Multiselect</span>
|
||||||
</button>
|
</button>
|
||||||
<button mat-menu-item routerLink="category">
|
<button mat-menu-item routerLink="category">
|
||||||
<mat-icon>device_hub</mat-icon>
|
<mat-icon>device_hub</mat-icon>
|
||||||
@ -139,25 +132,62 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div *ngIf="isMultiSelect">
|
<div *ngIf="isMultiSelect">
|
||||||
<div *osPerms="'motions.can_manage'">
|
<button mat-menu-item (click)="toggleMultiSelect()">
|
||||||
<button mat-menu-item (click)="deleteSelected()">
|
<mat-icon>library_add</mat-icon>
|
||||||
<mat-icon>delete</mat-icon>
|
<span translate>Exit multiselect</span>
|
||||||
<span translate>Delete selected</span>
|
|
||||||
</button>
|
</button>
|
||||||
<button mat-menu-item (click)="openSetStatusMenu()">
|
<div *osPerms="'motions.can_manage'">
|
||||||
<mat-icon>sentiment_satisfied</mat-icon>
|
<mat-divider></mat-divider>
|
||||||
<!-- TODO: icon -->
|
<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)="multiselectService.setStatus(selectedRows); toggleMultiSelect()">
|
||||||
|
<mat-icon>label</mat-icon>
|
||||||
<span translate>Set status</span>
|
<span translate>Set status</span>
|
||||||
</button>
|
</button>
|
||||||
<button mat-menu-item (click)="openSetCategoryMenu()">
|
<button mat-menu-item (click)="multiselectService.setRecommendation(selectedRows); toggleMultiSelect()">
|
||||||
<mat-icon>sentiment_satisfied</mat-icon>
|
<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 -->
|
<!-- TODO: icon -->
|
||||||
<span translate>Set categories</span>
|
<span translate>Set categories</span>
|
||||||
</button>
|
</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>
|
</div>
|
||||||
<button mat-menu-item (click)="csvExportMotionList()">
|
<mat-divider></mat-divider>
|
||||||
|
<button mat-menu-item (click)="csvExportMotionList(); toggleMultiSelect()">
|
||||||
<mat-icon>archive</mat-icon>
|
<mat-icon>archive</mat-icon>
|
||||||
<span translate>Export as CSV</span>
|
<span translate>Export as CSV</span>
|
||||||
</button>
|
</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>
|
</div>
|
||||||
</mat-menu>
|
</mat-menu>
|
||||||
|
@ -3,15 +3,14 @@ import { Router, ActivatedRoute } from '@angular/router';
|
|||||||
import { Title } from '@angular/platform-browser';
|
import { Title } from '@angular/platform-browser';
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
|
||||||
|
import { 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 { MotionRepositoryService } from '../../services/motion-repository.service';
|
||||||
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 { ListViewBaseComponent } from '../../../base/list-view-base';
|
import { MotionMultiselectService } from '../../services/motion-multiselect.service';
|
||||||
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';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component that displays all the motions in a Table using DataSource.
|
* 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 route Current route
|
||||||
* @param configService The configuration provider
|
* @param configService The configuration provider
|
||||||
* @param repo Motion Repository
|
* @param repo Motion Repository
|
||||||
* @param csvExport CSV Export Service
|
|
||||||
* @param promptService
|
* @param promptService
|
||||||
|
* @param motionCsvExport
|
||||||
|
* @param workflowRepo Workflow Repository
|
||||||
|
* @param categoryRepo
|
||||||
|
* @param userRepo
|
||||||
|
* @param tagRepo
|
||||||
|
* @param choiceService
|
||||||
*/
|
*/
|
||||||
public constructor(
|
public constructor(
|
||||||
titleService: Title,
|
titleService: Title,
|
||||||
@ -62,8 +66,8 @@ 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 promptService: PromptService,
|
private motionCsvExport: MotionCsvExportService,
|
||||||
private motionCsvExport: MotionCsvExportService
|
public multiselectService: MotionMultiselectService
|
||||||
) {
|
) {
|
||||||
super(titleService, translate, matSnackBar);
|
super(titleService, translate, matSnackBar);
|
||||||
|
|
||||||
@ -90,11 +94,7 @@ export class MotionListComponent extends ListViewBaseComponent<ViewMotion> imple
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
this.configService.get('motions_statutes_enabled').subscribe(
|
this.configService.get('motions_statutes_enabled').subscribe(enabled => (this.statutesEnabled = enabled));
|
||||||
(enabled: boolean): void => {
|
|
||||||
this.statutesEnabled = enabled;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -164,56 +164,8 @@ export class MotionListComponent extends ListViewBaseComponent<ViewMotion> imple
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes the items selected.
|
* Returns current definitions for the listView table
|
||||||
* SelectedRows is only filled with data in multiSelect mode
|
|
||||||
*/
|
*/
|
||||||
public async deleteSelected(): Promise<void> {
|
|
||||||
const content = this.translate.instant('This will delete all selected motions.');
|
|
||||||
if (await this.promptService.open('Are you sure?', content)) {
|
|
||||||
for (const motion of this.selectedRows) {
|
|
||||||
await this.repo.delete(motion);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the status in bulk.
|
|
||||||
* SelectedRows is only filled with data in multiSelect mode
|
|
||||||
* TODO: currently not yet functional, because no status (or state_id) is being selected
|
|
||||||
* in the ui
|
|
||||||
* @param status TODO: May still change type
|
|
||||||
*/
|
|
||||||
public async setStatusSelected(status: Partial<WorkflowState>): Promise<void> {
|
|
||||||
// TODO: check if id is there
|
|
||||||
for (const motion of this.selectedRows) {
|
|
||||||
await this.repo.update({ state_id: status.id }, motion);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the category for all selected items.
|
|
||||||
* SelectedRows is only filled with data in multiSelect mode
|
|
||||||
* TODO: currently not yet functional, because no category is being selected in the ui
|
|
||||||
* @param category TODO: May still change type
|
|
||||||
*/
|
|
||||||
public async setCategorySelected(category: Partial<Category>): Promise<void> {
|
|
||||||
for (const motion of this.selectedRows) {
|
|
||||||
await this.repo.update({ state_id: category.id }, motion);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* TODO: Open an extra submenu. Design still undecided. Will be used for deciding
|
|
||||||
* the status of setStatusSelected
|
|
||||||
*/
|
|
||||||
public openSetStatusMenu(): void {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* TODO: Open an extra submenu. Design still undecided. Will be used for deciding
|
|
||||||
* the status of setCategorySelected
|
|
||||||
*/
|
|
||||||
public openSetCategoryMenu(): void {}
|
|
||||||
|
|
||||||
public getColumnDefinition(): string[] {
|
public getColumnDefinition(): string[] {
|
||||||
if (this.isMultiSelect) {
|
if (this.isMultiSelect) {
|
||||||
return ['selector'].concat(this.columnsToDisplayMinWidth);
|
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;
|
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 {
|
public get recommendation_id(): number {
|
||||||
return this.motion && this.motion.recommendation_id ? this.motion.recommendation_id : null;
|
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 : [];
|
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(
|
public constructor(
|
||||||
motion?: Motion,
|
motion?: Motion,
|
||||||
category?: Category,
|
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 { Injectable } from '@angular/core';
|
||||||
|
|
||||||
import { Category } from '../../../shared/models/motions/category';
|
import { Category } from '../../../shared/models/motions/category';
|
||||||
import { ViewCategory } from '../models/view-category';
|
import { ViewCategory } from '../models/view-category';
|
||||||
import { DataSendService } from '../../../core/services/data-send.service';
|
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 { Injectable } from '@angular/core';
|
||||||
|
|
||||||
import { Tag } from '../../../shared/models/core/tag';
|
import { Tag } from '../../../shared/models/core/tag';
|
||||||
import { ViewTag } from '../models/view-tag';
|
import { ViewTag } from '../models/view-tag';
|
||||||
import { DataSendService } from '../../../core/services/data-send.service';
|
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.
|
* Handler for the generate Password button.
|
||||||
* Generates a password using 8 pseudo-random letters
|
|
||||||
* from the `characters` const.
|
|
||||||
*/
|
*/
|
||||||
public generatePassword(): void {
|
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({
|
this.personalInfoForm.patchValue({
|
||||||
default_password: pw
|
default_password: this.repo.getRandomPassword()
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -91,7 +91,7 @@
|
|||||||
<div *ngIf="!isMultiSelect">
|
<div *ngIf="!isMultiSelect">
|
||||||
<button mat-menu-item *osPerms="'users.can_manage'" (click)="toggleMultiSelect()">
|
<button mat-menu-item *osPerms="'users.can_manage'" (click)="toggleMultiSelect()">
|
||||||
<mat-icon>library_add</mat-icon>
|
<mat-icon>library_add</mat-icon>
|
||||||
<span translate>MultiSelect</span>
|
<span translate>Multiselect</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button mat-menu-item *osPerms="'users.can_manage'" routerLink="groups">
|
<button mat-menu-item *osPerms="'users.can_manage'" routerLink="groups">
|
||||||
@ -110,11 +110,19 @@
|
|||||||
</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>
|
||||||
<div *osPerms="'users.can_manage'">
|
<div *osPerms="'users.can_manage'">
|
||||||
<button mat-menu-item (click)="setGroupSelected(null)">
|
<mat-divider></mat-divider>
|
||||||
<mat-icon>archive</mat-icon>
|
<button mat-menu-item (click)="setGroupSelected(true)">
|
||||||
<span translate>Set groups</span>
|
<mat-icon>people</mat-icon>
|
||||||
<!-- TODO bottomsheet/menu? -->
|
<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>
|
</button>
|
||||||
|
|
||||||
<mat-divider></mat-divider>
|
<mat-divider></mat-divider>
|
||||||
@ -123,44 +131,41 @@
|
|||||||
<mat-icon>add_circle</mat-icon>
|
<mat-icon>add_circle</mat-icon>
|
||||||
<span translate>Set active</span>
|
<span translate>Set active</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button mat-menu-item (click)="setActiveSelected(false)">
|
<button mat-menu-item (click)="setActiveSelected(false)">
|
||||||
<mat-icon>remove_circle</mat-icon>
|
<mat-icon>remove_circle</mat-icon>
|
||||||
<span translate>Set inactive</span>
|
<span translate>Set inactive</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<mat-divider></mat-divider>
|
<mat-divider></mat-divider>
|
||||||
|
|
||||||
<button mat-menu-item (click)="setPresentSelected(true)">
|
<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>
|
<span translate>Set as present</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button mat-menu-item (click)="setPresentSelected(false)">
|
<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>
|
<span translate>Set as not present</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<mat-divider></mat-divider>
|
<mat-divider></mat-divider>
|
||||||
|
|
||||||
<button mat-menu-item (click)="setCommitteeSelected(true)">
|
<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>
|
<span translate>Set as committee</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button mat-menu-item (click)="setCommitteeSelected(false)">
|
<button mat-menu-item (click)="setCommitteeSelected(false)">
|
||||||
<mat-icon>remove_circle</mat-icon>
|
<mat-icon>account_balance</mat-icon>
|
||||||
<span translate>Unset committee</span>
|
<span translate>Unset committee</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<mat-divider></mat-divider>
|
<mat-divider></mat-divider>
|
||||||
|
|
||||||
<button mat-menu-item (click)="sendInvitationSelected()">
|
<button mat-menu-item (click)="sendInvitationEmailSelected()">
|
||||||
<mat-icon>mail</mat-icon>
|
<mat-icon>mail</mat-icon>
|
||||||
<span translate>Send invitations</span>
|
<span translate>Send invitation emails</span>
|
||||||
</button>
|
</button>
|
||||||
|
<button mat-menu-item (click)="resetPasswordsSelected()">
|
||||||
<button mat-menu-item (click)="deleteSelected()">
|
<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>
|
<mat-icon>delete</mat-icon>
|
||||||
<span translate>Delete selected</span>
|
<span translate>Delete selected</span>
|
||||||
</button>
|
</button>
|
||||||
|
@ -1,15 +1,16 @@
|
|||||||
import { Component, OnInit } from '@angular/core';
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { MatSnackBar } from '@angular/material';
|
||||||
|
import { Router, ActivatedRoute } from '@angular/router';
|
||||||
import { Title } from '@angular/platform-browser';
|
import { Title } from '@angular/platform-browser';
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
import { CsvExportService } from '../../../../core/services/csv-export.service';
|
|
||||||
|
|
||||||
import { ViewUser } from '../../models/view-user';
|
import { CsvExportService } from '../../../../core/services/csv-export.service';
|
||||||
import { UserRepositoryService } from '../../services/user-repository.service';
|
|
||||||
import { ListViewBaseComponent } from '../../../base/list-view-base';
|
import { ListViewBaseComponent } from '../../../base/list-view-base';
|
||||||
import { Router, ActivatedRoute } from '@angular/router';
|
import { GroupRepositoryService } from '../../services/group-repository.service';
|
||||||
import { MatSnackBar } from '@angular/material';
|
|
||||||
import { Group } from '../../../../shared/models/users/group';
|
|
||||||
import { PromptService } from '../../../../core/services/prompt.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.
|
* Component for the user list view.
|
||||||
@ -28,6 +29,7 @@ export class UserListComponent extends ListViewBaseComponent<ViewUser> implement
|
|||||||
* @param translate Service for translation handling
|
* @param translate Service for translation handling
|
||||||
* @param matSnackBar Helper to diplay errors
|
* @param matSnackBar Helper to diplay errors
|
||||||
* @param repo the user repository
|
* @param repo the user repository
|
||||||
|
* @param groupRepo: The user group repository
|
||||||
* @param router the router service
|
* @param router the router service
|
||||||
* @param route the local route
|
* @param route the local route
|
||||||
* @param csvExport CSV export Service,
|
* @param csvExport CSV export Service,
|
||||||
@ -38,10 +40,12 @@ export class UserListComponent extends ListViewBaseComponent<ViewUser> implement
|
|||||||
translate: TranslateService,
|
translate: TranslateService,
|
||||||
matSnackBar: MatSnackBar,
|
matSnackBar: MatSnackBar,
|
||||||
private repo: UserRepositoryService,
|
private repo: UserRepositoryService,
|
||||||
|
private groupRepo: GroupRepositoryService,
|
||||||
|
private choiceService: ChoiceService,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
protected csvExport: CsvExportService,
|
protected csvExport: CsvExportService,
|
||||||
private promptService: PromptService
|
private promptService: PromptService,
|
||||||
) {
|
) {
|
||||||
super(titleService, translate, matSnackBar);
|
super(titleService, translate, matSnackBar);
|
||||||
|
|
||||||
@ -124,31 +128,31 @@ export class UserListComponent extends ListViewBaseComponent<ViewUser> implement
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TODO: Not yet as expected
|
* Opens a dialog and sets the group(s) for all selected users.
|
||||||
* Bulk sets the group for users. TODO: Group is still not decided in the ui
|
* SelectedRows is only filled with data in multiSelect mode
|
||||||
* @param group TODO: type may still change
|
|
||||||
* @param unset toggle for adding or removing from the group
|
|
||||||
*/
|
*/
|
||||||
public async setGroupSelected(group: Partial<Group>, unset?: boolean): Promise<void> {
|
public async setGroupSelected(add: boolean): Promise<void> {
|
||||||
this.selectedRows.forEach(vm => {
|
let content: string;
|
||||||
const groups = vm.groupIds;
|
if (add){
|
||||||
const idx = groups.indexOf(group.id);
|
content = this.translate.instant('This will add the following groups to all selected users:');
|
||||||
if (unset && idx >= 0) {
|
} else {
|
||||||
groups.slice(idx, 1);
|
content = this.translate.instant('This will remove the following groups from all selected users:');
|
||||||
} else if (!unset && idx < 0) {
|
}
|
||||||
groups.push(group.id);
|
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
|
* 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> {
|
public sendInvitationEmailSelected(): void {
|
||||||
// this.selectedRows.forEach(vm => {
|
this.repo.sendInvitationEmail(this.selectedRows).then(this.raiseError, this.raiseError);
|
||||||
// TODO if !vm.emailSent {vm.sendInvitation}
|
}
|
||||||
// });
|
|
||||||
|
/**
|
||||||
|
* 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[] {
|
public getColumnDefinition(): string[] {
|
||||||
|
@ -39,6 +39,10 @@ export class ViewUser extends BaseViewModel {
|
|||||||
return this.user ? this.user.full_name : null;
|
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 {
|
public get email(): string {
|
||||||
return this.user ? this.user.email : null;
|
return this.user ? this.user.email : null;
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
import { ViewGroup } from '../models/view-group';
|
|
||||||
import { BaseRepository } from '../../base/base-repository';
|
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 { 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
|
* Set rules to define the shape of an app permission
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
import { BaseRepository } from '../../base/base-repository';
|
import { BaseRepository } from '../../base/base-repository';
|
||||||
import { ViewUser } from '../models/view-user';
|
import { ViewUser } from '../models/view-user';
|
||||||
import { User } from '../../../shared/models/users/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 { DataSendService } from '../../../core/services/data-send.service';
|
||||||
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 { 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
|
* Repository service for users
|
||||||
@ -24,7 +26,10 @@ export class UserRepositoryService extends BaseRepository<ViewUser, User> {
|
|||||||
public constructor(
|
public constructor(
|
||||||
DS: DataStoreService,
|
DS: DataStoreService,
|
||||||
mapperService: CollectionStringModelMapperService,
|
mapperService: CollectionStringModelMapperService,
|
||||||
private dataSend: DataSendService
|
private dataSend: DataSendService,
|
||||||
|
private translate: TranslateService,
|
||||||
|
private httpService: HttpService,
|
||||||
|
private configService: ConfigService
|
||||||
) {
|
) {
|
||||||
super(DS, mapperService, User, [Group]);
|
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);
|
const groups = this.DS.getMany(Group, user.groups_id);
|
||||||
return new ViewUser(user, groups);
|
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