Merge pull request #4040 from MaximilianKrambach/os3/multiSelect2
Multiselect for advanced options
This commit is contained in:
commit
d4d4ac838f
@ -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();
|
||||
});
|
||||
});
|
44
client/src/app/core/services/choice.service.ts
Normal file
44
client/src/app/core/services/choice.service.ts
Normal file
@ -0,0 +1,44 @@
|
||||
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',
|
||||
maxHeight: '90vh',
|
||||
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,32 @@
|
||||
<div class="scrollmenu-outer">
|
||||
<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>
|
||||
</div>
|
@ -0,0 +1,17 @@
|
||||
mat-radio-group {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
|
||||
mat-radio-button {
|
||||
margin: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.scrollmenu {
|
||||
padding: 5px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.scrollmenu-outer {
|
||||
max-height: inherit;
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
import { async, 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,112 @@
|
||||
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 })[];
|
||||
|
||||
/**
|
||||
* All data needed for this dialog
|
||||
*/
|
||||
interface ChoiceDialogData {
|
||||
/**
|
||||
* A title to display
|
||||
*/
|
||||
title: string;
|
||||
|
||||
/**
|
||||
* The choices to display
|
||||
*/
|
||||
choices: ChoiceDialogOptions;
|
||||
|
||||
/**
|
||||
* Select, if this should be a multiselect choice
|
||||
*/
|
||||
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
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Get the title from a choice. Maybe saved in a label property or using getTitle().
|
||||
*
|
||||
* @param choice The choice
|
||||
* @return the title
|
||||
*/
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,13 +1,23 @@
|
||||
<mat-toolbar color="primary" [ngClass]="multiSelectMode ? 'multi-select' : '' " *ngIf="!vp.isMobile"></mat-toolbar>
|
||||
<mat-toolbar color="primary" [ngClass]="multiSelectMode ? 'multi-select' : '' " class="sticky-toolbar">
|
||||
<mat-toolbar color="primary" [ngClass]="multiSelectMode ? 'multi-select' : ''" *ngIf="!vp.isMobile"></mat-toolbar>
|
||||
<mat-toolbar color="primary" [ngClass]="multiSelectMode ? 'multi-select' : ''" class="sticky-toolbar">
|
||||
<div class="toolbar-left">
|
||||
<!-- Nav menu -->
|
||||
<button mat-icon-button class="on-transition-fade" *ngIf="vp.isMobile && nav && !multiSelectMode" (click)='clickHamburgerMenu()'>
|
||||
<button
|
||||
mat-icon-button
|
||||
class="on-transition-fade"
|
||||
*ngIf="vp.isMobile && nav && !multiSelectMode"
|
||||
(click)="clickHamburgerMenu()"
|
||||
>
|
||||
<mat-icon>menu</mat-icon>
|
||||
</button>
|
||||
|
||||
<!-- Exit / Back button -->
|
||||
<button mat-icon-button class="on-transition-fade" *ngIf="!nav && !editMode && !multiSelectMode" (click)="onBackButton()">
|
||||
<button
|
||||
mat-icon-button
|
||||
class="on-transition-fade"
|
||||
*ngIf="!nav && !editMode && !multiSelectMode"
|
||||
(click)="onBackButton()"
|
||||
>
|
||||
<mat-icon>arrow_back</mat-icon>
|
||||
</button>
|
||||
|
||||
@ -22,12 +32,12 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- centered information slot-->
|
||||
<div *ngIf="multiSelectMode" class=spacer></div>
|
||||
<!-- centered information slot -->
|
||||
<div *ngIf="!multiSelectMode" class="spacer"></div>
|
||||
<div class="toolbar-centered on-transition-fade" *ngIf="multiSelectMode">
|
||||
<ng-content select=".central-info-slot"></ng-content>
|
||||
</div>
|
||||
<div class=spacer></div>
|
||||
<div class="spacer"></div>
|
||||
|
||||
<div class="toolbar-right">
|
||||
<!-- Extra controls slot -->
|
||||
@ -36,24 +46,31 @@
|
||||
</div>
|
||||
|
||||
<!-- Main action button - desktop -->
|
||||
<button mat-mini-fab color="accent" class="on-transition-fade"
|
||||
*ngIf="mainButton && !editMode && !vp.isMobile && !multiSelectMode" (click)="sendMainEvent()">
|
||||
<button
|
||||
mat-mini-fab
|
||||
color="accent"
|
||||
class="on-transition-fade"
|
||||
*ngIf="mainButton && !editMode && !vp.isMobile && !multiSelectMode"
|
||||
(click)="sendMainEvent()"
|
||||
>
|
||||
<mat-icon>{{ mainButtonIcon }}</mat-icon>
|
||||
</button>
|
||||
|
||||
<!-- Save button -->
|
||||
<button mat-button *ngIf="editMode" (click)="save()">
|
||||
<strong translate class="upper">Save</strong>
|
||||
</button>
|
||||
<button mat-button *ngIf="editMode" (click)="save()"><strong translate class="upper">Save</strong></button>
|
||||
|
||||
<!-- Menu button slot -->
|
||||
<ng-content class="on-transition-fade" *ngIf="!editMode" select=".menu-slot"></ng-content>
|
||||
</div>
|
||||
</mat-toolbar>
|
||||
|
||||
<!-- Main action button - mobile-->
|
||||
<!-- Main action button - mobile -->
|
||||
|
||||
<button mat-fab class="head-button on-transition-fade"
|
||||
*ngIf="mainButton && !editMode && vp.isMobile && !multiSelectMode" (click)=sendMainEvent()>
|
||||
<button
|
||||
mat-fab
|
||||
class="head-button on-transition-fade"
|
||||
*ngIf="mainButton && !editMode && vp.isMobile && !multiSelectMode"
|
||||
(click)="sendMainEvent()"
|
||||
>
|
||||
<mat-icon>{{ mainButtonIcon }}</mat-icon>
|
||||
</button>
|
||||
|
@ -23,7 +23,6 @@ export class Motion extends AgendaBaseModel {
|
||||
public motion_block_id: number;
|
||||
public origin: string;
|
||||
public submitters: MotionSubmitter[];
|
||||
public submitters_id: number[];
|
||||
public supporters_id: number[];
|
||||
public comments: MotionComment[];
|
||||
public workflow_id: number;
|
||||
|
@ -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 },
|
||||
|
@ -1,32 +1,22 @@
|
||||
<os-head-bar [mainButton]="true" (mainEvent)=onPlusButton() [multiSelectMode]="isMultiSelect">
|
||||
<os-head-bar [mainButton]="true" (mainEvent)="onPlusButton()" [multiSelectMode]="isMultiSelect">
|
||||
<!-- Title -->
|
||||
<div class="title-slot">
|
||||
<h2 translate>Agenda</h2>
|
||||
</div>
|
||||
<div class="title-slot"><h2 translate>Agenda</h2></div>
|
||||
<!-- Menu -->
|
||||
<div class="menu-slot">
|
||||
<button type="button" mat-icon-button [matMenuTriggerFor]="agendaMenu">
|
||||
<mat-icon>more_vert</mat-icon>
|
||||
</button>
|
||||
<button type="button" mat-icon-button [matMenuTriggerFor]="agendaMenu"><mat-icon>more_vert</mat-icon></button>
|
||||
</div>
|
||||
|
||||
<!-- Multiselect info -->
|
||||
<div class="central-info-slot">
|
||||
<button mat-icon-button (click)="toggleMultiSelect()">
|
||||
<mat-icon>arrow_back</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button (click)="toggleMultiSelect()"><mat-icon>arrow_back</mat-icon></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>
|
||||
<button mat-icon-button (click)="deleteSelected()"><mat-icon>delete</mat-icon></button>
|
||||
</div>
|
||||
|
||||
</os-head-bar>
|
||||
|
||||
|
||||
<mat-table class="os-listview-table on-transition-fade" [dataSource]="dataSource" matSort>
|
||||
<ng-container matColumnDef="selector">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header class="checkbox-cell"></mat-header-cell>
|
||||
@ -52,9 +42,7 @@
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header>Speakers</mat-header-cell>
|
||||
<mat-cell *matCellDef="let item">
|
||||
<button mat-icon-button (click)="onSpeakerIcon(item)">
|
||||
<mat-icon
|
||||
[matBadge]="item.speakerAmount > 0 ? item.speakerAmount : null"
|
||||
matBadgeColor="accent">
|
||||
<mat-icon [matBadge]="item.speakerAmount > 0 ? item.speakerAmount : null" matBadgeColor="accent">
|
||||
mic
|
||||
</mat-icon>
|
||||
</button>
|
||||
@ -62,8 +50,11 @@
|
||||
</ng-container>
|
||||
|
||||
<mat-header-row *matHeaderRowDef="getColumnDefinition()"></mat-header-row>
|
||||
<mat-row [ngClass]="selectedRows.indexOf(row) >= 0 ? 'selected': ''" (click)='selectItem(row, $event)'
|
||||
*matRowDef="let row; columns: getColumnDefinition()"></mat-row>
|
||||
<mat-row
|
||||
[ngClass]="selectedRows.indexOf(row) >= 0 ? 'selected' : ''"
|
||||
(click)="selectItem(row, $event)"
|
||||
*matRowDef="let row; columns: getColumnDefinition()"
|
||||
></mat-row>
|
||||
</mat-table>
|
||||
<mat-paginator class="on-transition-fade" [pageSizeOptions]="[25, 50, 75, 100, 125]"></mat-paginator>
|
||||
|
||||
@ -71,16 +62,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 *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 +87,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> {
|
||||
|
@ -1,9 +1,6 @@
|
||||
<os-head-bar plusButton=true (plusButtonClicked)=onPlusButton() [multiSelectMode]="isMultiSelect">
|
||||
<os-head-bar plusButton="true" (plusButtonClicked)="onPlusButton()" [multiSelectMode]="isMultiSelect">
|
||||
<!-- Title -->
|
||||
<div class="title-slot">
|
||||
|
||||
<h2 translate>Elections</h2>
|
||||
</div>
|
||||
<div class="title-slot"><h2 translate>Elections</h2></div>
|
||||
<!-- Menu -->
|
||||
<div class="menu-slot">
|
||||
<button type="button" mat-icon-button [matMenuTriggerFor]="assignmentMenu">
|
||||
@ -13,25 +10,19 @@
|
||||
|
||||
<!-- Multiselect info -->
|
||||
<div class="central-info-slot">
|
||||
<button mat-icon-button (click)="toggleMultiSelect()">
|
||||
<mat-icon>arrow_back</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button (click)="toggleMultiSelect()"><mat-icon>arrow_back</mat-icon></button>
|
||||
<span>{{ selectedRows.length }} </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>
|
||||
<button mat-icon-button (click)="deleteSelected()"><mat-icon>delete</mat-icon></button>
|
||||
</div>
|
||||
|
||||
</os-head-bar>
|
||||
|
||||
|
||||
<mat-table class="os-listview-table on-transition-fade" [dataSource]="dataSource" matSort>
|
||||
<ng-container matColumnDef="selector">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header class="checkbox-cell" ></mat-header-cell>
|
||||
<mat-cell *matCellDef="let assignment" class="checkbox-cell" >
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header class="checkbox-cell"></mat-header-cell>
|
||||
<mat-cell *matCellDef="let assignment" class="checkbox-cell">
|
||||
<mat-icon>{{ isSelected(assignment) ? 'check_circle' : '' }}</mat-icon>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
@ -58,8 +49,11 @@
|
||||
</ng-container>
|
||||
|
||||
<mat-header-row *matHeaderRowDef="getColumnDefintion()"></mat-header-row>
|
||||
<mat-row [ngClass]="selectedRows.indexOf(row) >= 0 ? 'selected': ''" (click)='selectItem(row, $event)'
|
||||
*matRowDef="let row; columns: getColumnDefintion()">
|
||||
<mat-row
|
||||
[ngClass]="selectedRows.indexOf(row) >= 0 ? 'selected' : ''"
|
||||
(click)="selectItem(row, $event)"
|
||||
*matRowDef="let row; columns: getColumnDefintion()"
|
||||
>
|
||||
</mat-row>
|
||||
</mat-table>
|
||||
|
||||
@ -69,7 +63,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 +72,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>
|
||||
|
@ -25,7 +25,7 @@ describe('ResetPasswordConfirmComponent', () => {
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ResetPasswordConfirmComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
// fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
|
@ -5,7 +5,6 @@
|
||||
(mainEvent)="onMainEvent()"
|
||||
(saveEvent)="onSaveEditedFile()"
|
||||
>
|
||||
|
||||
<!-- Title -->
|
||||
<div class="title-slot">
|
||||
<h2 *ngIf="!editFile" translate>Files</h2>
|
||||
@ -44,31 +43,18 @@
|
||||
<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()">
|
||||
<mat-icon >arrow_back</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button (click)="toggleMultiSelect()"><mat-icon>arrow_back</mat-icon></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>
|
||||
<button mat-icon-button (click)="deleteSelected()"><mat-icon>delete</mat-icon></button>
|
||||
</div>
|
||||
</os-head-bar>
|
||||
|
||||
<mat-table class="os-listview-table on-transition-fade" [dataSource]="dataSource" matSort>
|
||||
|
||||
<ng-container matColumnDef="selector">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header class="checkbox-cell"></mat-header-cell>
|
||||
<mat-cell *matCellDef="let item" class="checkbox-cell">
|
||||
@ -122,8 +108,11 @@
|
||||
</ng-container>
|
||||
|
||||
<mat-header-row *matHeaderRowDef="getColumnDefinition()"></mat-header-row>
|
||||
<mat-row *matRowDef="let row; columns: getColumnDefinition()"
|
||||
[ngClass]="selectedRows.indexOf(row) >= 0 ? 'selected': ''" (click)='selectItem(row, $event)'></mat-row>
|
||||
<mat-row
|
||||
*matRowDef="let row; columns: getColumnDefinition()"
|
||||
[ngClass]="selectedRows.indexOf(row) >= 0 ? 'selected' : ''"
|
||||
(click)="selectItem(row, $event)"
|
||||
></mat-row>
|
||||
</mat-table>
|
||||
|
||||
<mat-paginator class="on-transition-fade" [pageSizeOptions]="[25, 50, 75, 100, 125]"></mat-paginator>
|
||||
@ -167,14 +156,21 @@
|
||||
|
||||
<!-- Menu for Mediafiles -->
|
||||
<mat-menu #mediafilesMenu="matMenu">
|
||||
<div *ngIf="!isMultiSelect">
|
||||
<button mat-menu-item *osPerms="'mediafiles.can_manage'" (click)="toggleMultiSelect()">
|
||||
<mat-icon>library_add</mat-icon>
|
||||
<span translate>MultiSelect</span>
|
||||
<span translate>Multiselect</span>
|
||||
</button>
|
||||
</mat-menu>
|
||||
<mat-menu #mediafilesMultiSelectMenu="matMenu">
|
||||
</div>
|
||||
<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()">
|
||||
<mat-icon>delete</mat-icon>
|
||||
<span translate>Delete selected</span>
|
||||
</button>
|
||||
</div>
|
||||
</mat-menu>
|
||||
|
@ -9,8 +9,8 @@ import { TranslateService } from '@ngx-translate/core';
|
||||
import { MotionRepositoryService } from '../../services/motion-repository.service';
|
||||
import { ViewMotion } from '../../models/view-motion';
|
||||
import { LinenumberingService } from '../../services/linenumbering.service';
|
||||
import { Motion } from '../../../../shared/models/motions/motion';
|
||||
import { BaseViewComponent } from '../../../base/base-view';
|
||||
import { CreateMotion } from '../../models/create-motion';
|
||||
|
||||
/**
|
||||
* Describes the single paragraphs from the base motion.
|
||||
@ -167,10 +167,10 @@ export class AmendmentCreateWizardComponent extends BaseViewComponent {
|
||||
amendment_paragraphs: amendedParagraphs
|
||||
};
|
||||
|
||||
const fromForm = new Motion();
|
||||
fromForm.deserialize(newMotionValues);
|
||||
const motion = new CreateMotion();
|
||||
motion.deserialize(newMotionValues);
|
||||
|
||||
const response = await this.repo.create(fromForm);
|
||||
const response = await this.repo.create(motion);
|
||||
this.router.navigate(['./motions/' + response.id]);
|
||||
}
|
||||
}
|
||||
|
@ -29,6 +29,8 @@ import { ConfigService } from '../../../../core/services/config.service';
|
||||
import { Workflow } from 'app/shared/models/motions/workflow';
|
||||
import { take, takeWhile, multicast, skipWhile } from 'rxjs/operators';
|
||||
import { LocalPermissionsService } from '../../services/local-permissions.service';
|
||||
import { ViewCreateMotion } from '../../models/view-create-motion';
|
||||
import { CreateMotion } from '../../models/create-motion';
|
||||
|
||||
/**
|
||||
* Component for the motion detail view
|
||||
@ -260,28 +262,10 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit {
|
||||
}
|
||||
});
|
||||
// load config variables
|
||||
this.configService.get('motions_statutes_enabled').subscribe(
|
||||
(enabled: boolean): void => {
|
||||
this.statutesEnabled = enabled;
|
||||
}
|
||||
);
|
||||
this.configService.get('motions_min_supporters').subscribe(
|
||||
(supporters: number): void => {
|
||||
this.minSupporters = supporters;
|
||||
}
|
||||
);
|
||||
|
||||
this.configService.get('motions_preamble').subscribe(
|
||||
(preamble: string): void => {
|
||||
this.preamble = preamble;
|
||||
}
|
||||
);
|
||||
|
||||
this.configService.get('motions_amendments_enabled').subscribe(
|
||||
(enabled: boolean): void => {
|
||||
this.amendmentsEnabled = enabled;
|
||||
}
|
||||
);
|
||||
this.configService.get('motions_statutes_enabled').subscribe(enabled => (this.statutesEnabled = enabled));
|
||||
this.configService.get('motions_min_supporters').subscribe(supporters => (this.minSupporters = supporters));
|
||||
this.configService.get('motions_preamble').subscribe(preamble => (this.preamble = preamble));
|
||||
this.configService.get('motions_amendments_enabled').subscribe(enabled => (this.amendmentsEnabled = enabled));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -327,8 +311,8 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit {
|
||||
// creates a new motion
|
||||
this.newMotion = true;
|
||||
this.editMotion = true;
|
||||
this.motion = new ViewMotion();
|
||||
this.motionCopy = new ViewMotion();
|
||||
this.motion = new ViewCreateMotion();
|
||||
this.motionCopy = new ViewCreateMotion();
|
||||
} else {
|
||||
// load existing motion
|
||||
this.route.params.subscribe(params => {
|
||||
@ -393,7 +377,7 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit {
|
||||
state_id: [''],
|
||||
recommendation_id: [''],
|
||||
submitters_id: [],
|
||||
supporters_id: [],
|
||||
supporters_id: [[]],
|
||||
workflow_id: [],
|
||||
origin: ['']
|
||||
});
|
||||
@ -419,45 +403,65 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit {
|
||||
}
|
||||
|
||||
/**
|
||||
* Save a motion. Calls the "patchValues" function in the MotionObject
|
||||
*
|
||||
* http:post the motion to the server.
|
||||
* The AutoUpdate-Service should see a change once it arrives and show it
|
||||
* in the list view automatically
|
||||
* Before updating or creating, the motions needs to be prepared for paragraph based amendments.
|
||||
* A motion of type T is created, prepared and deserialized from the given motionValues
|
||||
*
|
||||
* @param motionValues valus for the new motion
|
||||
* @param ctor The motion constructor, so different motion types can be created.
|
||||
*/
|
||||
public async saveMotion(): Promise<void> {
|
||||
const newMotionValues = { ...this.metaInfoForm.value, ...this.contentForm.value };
|
||||
|
||||
const fromForm = new Motion();
|
||||
private prepareMotionForSave<T extends Motion>(motionValues: any, ctor: new (...args: any[]) => T): T {
|
||||
const motion = new ctor();
|
||||
if (this.motion.isParagraphBasedAmendment()) {
|
||||
fromForm.amendment_paragraphs = this.motion.amendment_paragraphs.map(
|
||||
(para: string): string => {
|
||||
if (para === null) {
|
||||
motion.amendment_paragraphs = this.motion.amendment_paragraphs.map(
|
||||
(paragraph: string): string => {
|
||||
if (paragraph === null) {
|
||||
return null;
|
||||
} else {
|
||||
return newMotionValues.text;
|
||||
return motionValues.text;
|
||||
}
|
||||
}
|
||||
);
|
||||
newMotionValues.text = '';
|
||||
motionValues.text = '';
|
||||
}
|
||||
fromForm.deserialize(newMotionValues);
|
||||
motion.deserialize(motionValues);
|
||||
return motion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a motion. Calls the "patchValues" function in the MotionObject
|
||||
*/
|
||||
public async createMotion(): Promise<void> {
|
||||
const newMotionValues = { ...this.metaInfoForm.value, ...this.contentForm.value };
|
||||
const motion = this.prepareMotionForSave(newMotionValues, CreateMotion);
|
||||
|
||||
try {
|
||||
if (this.newMotion) {
|
||||
const response = await this.repo.create(fromForm);
|
||||
const response = await this.repo.create(motion);
|
||||
this.router.navigate(['./motions/' + response.id]);
|
||||
} else {
|
||||
await this.repo.update(fromForm, this.motionCopy);
|
||||
// if the motion was successfully updated, change the edit mode.
|
||||
this.editMotion = false;
|
||||
}
|
||||
} catch (e) {
|
||||
this.raiseError(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save a motion. Calls the "patchValues" function in the MotionObject
|
||||
*/
|
||||
public async updateMotion(): Promise<void> {
|
||||
const newMotionValues = { ...this.metaInfoForm.value, ...this.contentForm.value };
|
||||
const motion = this.prepareMotionForSave(newMotionValues, Motion);
|
||||
this.repo.update(motion, this.motionCopy).then(() => (this.editMotion = false), this.raiseError);
|
||||
}
|
||||
|
||||
/**
|
||||
* In the ui are no distinct buttons for update or create. This is decided here.
|
||||
*/
|
||||
public saveMotion(): void {
|
||||
if (this.newMotion) {
|
||||
this.createMotion();
|
||||
} else {
|
||||
this.updateMotion();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* get the formated motion text from the repository.
|
||||
*/
|
||||
|
@ -1,8 +1,6 @@
|
||||
<os-head-bar [mainButton]="true" (mainEvent)=onPlusButton() [multiSelectMode]="isMultiSelect">
|
||||
<os-head-bar [mainButton]="true" (mainEvent)="onPlusButton()" [multiSelectMode]="isMultiSelect">
|
||||
<!-- Title -->
|
||||
<div class="title-slot">
|
||||
<h2 translate>Motions</h2>
|
||||
</div>
|
||||
<div class="title-slot"><h2 translate>Motions</h2></div>
|
||||
|
||||
<!-- Menu -->
|
||||
<div class="menu-slot">
|
||||
@ -13,31 +11,17 @@
|
||||
|
||||
<!-- Multiselect info -->
|
||||
<div class="central-info-slot">
|
||||
<button mat-icon-button (click)="toggleMultiSelect()">
|
||||
<mat-icon>arrow_back</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button (click)="toggleMultiSelect()"><mat-icon>arrow_back</mat-icon></button>
|
||||
<span>{{ selectedRows.length }} </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">
|
||||
<button mat-button>
|
||||
<span translate>SORT</span>
|
||||
</button>
|
||||
<button mat-button>
|
||||
<span translate>FILTER</span>
|
||||
</button>
|
||||
<button mat-button><span translate>SORT</span></button> <button mat-button><span translate>FILTER</span></button>
|
||||
</div>
|
||||
|
||||
<mat-table class="os-listview-table on-transition-fade" [dataSource]="dataSource" matSort>
|
||||
<ng-container matColumnDef="selector" >
|
||||
<ng-container matColumnDef="selector">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header class="checkbox-cell"></mat-header-cell>
|
||||
<mat-cell *matCellDef="let motion" class="checkbox-cell">
|
||||
<mat-icon>{{ isSelected(motion) ? 'check_circle' : '' }}</mat-icon>
|
||||
@ -48,9 +32,7 @@
|
||||
<ng-container matColumnDef="identifier">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header>Identifier</mat-header-cell>
|
||||
<mat-cell *matCellDef="let motion">
|
||||
<div class="innerTable">
|
||||
{{ motion.identifier }}
|
||||
</div>
|
||||
<div class="innerTable">{{ motion.identifier }}</div>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
@ -59,25 +41,28 @@
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header>Title</mat-header-cell>
|
||||
<mat-cell *matCellDef="let motion">
|
||||
<div class="innerTable">
|
||||
<span class="motion-list-title">{{ motion.title }}</span>
|
||||
<br>
|
||||
<span class="motion-list-from">
|
||||
<span translate>by</span>
|
||||
{{ motion.submitters }}
|
||||
<span class="motion-list-title">{{ motion.title }}</span> <br />
|
||||
<span class="motion-list-from" *ngIf="motion.submitters.length">
|
||||
<span translate>by</span> {{ motion.submitters }}
|
||||
</span>
|
||||
<br>
|
||||
<br *ngIf="motion.submitters.length" />
|
||||
<!-- state -->
|
||||
<mat-basic-chip [ngClass]="{
|
||||
'green': motion.state.css_class === 'success',
|
||||
'red': motion.state.css_class === 'danger',
|
||||
'grey': motion.state.css_class === 'default',
|
||||
'lightblue': motion.state.css_class === 'primary' }">
|
||||
<mat-basic-chip
|
||||
[ngClass]="{
|
||||
green: motion.state.css_class === 'success',
|
||||
red: motion.state.css_class === 'danger',
|
||||
grey: motion.state.css_class === 'default',
|
||||
lightblue: motion.state.css_class === 'primary'
|
||||
}"
|
||||
>
|
||||
{{ motion.state.name | translate }}
|
||||
</mat-basic-chip>
|
||||
|
||||
<!-- recommendation -->
|
||||
<span *ngIf="motion.recommendation" >
|
||||
<mat-basic-chip class="bluegrey">{{ motion.recommendation.recommendation_label | translate }}</mat-basic-chip>
|
||||
<span *ngIf="motion.recommendation">
|
||||
<mat-basic-chip class="bluegrey">{{
|
||||
motion.recommendation.recommendation_label | translate
|
||||
}}</mat-basic-chip>
|
||||
</span>
|
||||
</div>
|
||||
</mat-cell>
|
||||
@ -86,10 +71,8 @@
|
||||
<!-- 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)">
|
||||
<div *ngIf='motion.category' class='small'>
|
||||
<mat-icon>device_hub</mat-icon> {{ motion.category }}
|
||||
</div>
|
||||
<mat-cell *matCellDef="let motion">
|
||||
<div *ngIf="motion.category" class="small"><mat-icon>device_hub</mat-icon>{{ motion.category }}</div>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
@ -100,7 +83,8 @@
|
||||
<button mat-icon-button (click)="onSpeakerIcon(motion, $event)">
|
||||
<mat-icon
|
||||
[matBadge]="motion.agendaSpeakerAmount > 0 ? motion.agendaSpeakerAmount : null"
|
||||
matBadgeColor="accent">
|
||||
matBadgeColor="accent"
|
||||
>
|
||||
mic
|
||||
</mat-icon>
|
||||
</button>
|
||||
@ -108,8 +92,12 @@
|
||||
</ng-container>
|
||||
|
||||
<mat-header-row *matHeaderRowDef="getColumnDefinition()"></mat-header-row>
|
||||
<mat-row [ngClass]="selectedRows.indexOf(row) >= 0 ? 'selected': ''" (click)='selectItem(row, $event)'
|
||||
*matRowDef="let row; columns: getColumnDefinition()" class="lg">
|
||||
<mat-row
|
||||
[ngClass]="selectedRows.indexOf(row) >= 0 ? 'selected' : ''"
|
||||
(click)="selectItem(row, $event)"
|
||||
*matRowDef="let row; columns: getColumnDefinition()"
|
||||
class="lg"
|
||||
>
|
||||
</mat-row>
|
||||
</mat-table>
|
||||
|
||||
@ -119,7 +107,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 +127,66 @@
|
||||
</button>
|
||||
</div>
|
||||
<div *ngIf="isMultiSelect">
|
||||
<div *osPerms="'motions.can_manage'">
|
||||
<button mat-menu-item (click)="deleteSelected()">
|
||||
<mat-icon>delete</mat-icon>
|
||||
<span translate>Delete selected</span>
|
||||
<button mat-menu-item (click)="toggleMultiSelect()">
|
||||
<mat-icon>library_add</mat-icon>
|
||||
<span translate>Exit multiselect</span>
|
||||
</button>
|
||||
<button mat-menu-item (click)="openSetStatusMenu()">
|
||||
<mat-icon>sentiment_satisfied</mat-icon>
|
||||
<!-- TODO: icon -->
|
||||
<div *osPerms="'motions.can_manage'">
|
||||
<mat-divider></mat-divider>
|
||||
<button mat-menu-item (click)="multiselectWrapper(multiselectService.moveToItem(selectedRows))">
|
||||
<!-- TODO: Not implemented yet -->
|
||||
<mat-icon>sort</mat-icon>
|
||||
<span translate>Move to agenda item</span>
|
||||
</button>
|
||||
<button mat-menu-item (click)="multiselectWrapper(multiselectService.setStatus(selectedRows))">
|
||||
<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)="multiselectWrapper(multiselectService.setRecommendation(selectedRows))">
|
||||
<mat-icon>report</mat-icon>
|
||||
<!-- TODO: better icon -->
|
||||
<span translate>Set recommendation</span>
|
||||
</button>
|
||||
<button mat-menu-item (click)="multiselectWrapper(multiselectService.setCategory(selectedRows))">
|
||||
<mat-icon>device_hub</mat-icon>
|
||||
<!-- TODO: icon -->
|
||||
<span translate>Set categories</span>
|
||||
</button>
|
||||
<mat-divider></mat-divider>
|
||||
<button mat-menu-item (click)="multiselectWrapper(multiselectService.addSubmitters(selectedRows))">
|
||||
<mat-icon>person_add</mat-icon>
|
||||
<!-- TODO: icon -->
|
||||
<span translate>Add submitters</span>
|
||||
</button>
|
||||
<button mat-menu-item (click)="multiselectWrapper(multiselectService.removeSubmitters(selectedRows))">
|
||||
<mat-icon>person_outline</mat-icon>
|
||||
<!-- TODO: icon -->
|
||||
<span translate>remove submitters</span>
|
||||
</button>
|
||||
<button mat-menu-item (click)="multiselectWrapper(multiselectService.addTags(selectedRows))">
|
||||
<mat-icon>bookmarks</mat-icon>
|
||||
<!-- TODO: icon -->
|
||||
<span translate>Add tags</span>
|
||||
</button>
|
||||
<button mat-menu-item (click)="multiselectWrapper(multiselectService.removeTags(selectedRows))">
|
||||
<mat-icon>bookmark_border</mat-icon>
|
||||
<!-- TODO: icon -->
|
||||
<span translate>Remove tags</span>
|
||||
</button>
|
||||
</div>
|
||||
<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,60 +164,21 @@ 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);
|
||||
}
|
||||
return this.columnsToDisplayMinWidth;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps multiselect actions to close the multiselect mode or throw an error if one happens.
|
||||
*
|
||||
* @param multiselectPromise The promise returned by multiselect actions.
|
||||
*/
|
||||
public multiselectWrapper(multiselectPromise: Promise<void>): void {
|
||||
multiselectPromise.then(() => this.toggleMultiSelect(), this.raiseError);
|
||||
}
|
||||
}
|
||||
|
13
client/src/app/site/motions/models/create-motion.ts
Normal file
13
client/src/app/site/motions/models/create-motion.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { Motion } from 'app/shared/models/motions/motion';
|
||||
|
||||
/**
|
||||
* Representation of Motion during creation. The submitters_id is added to send this information
|
||||
* as an array of user ids to the server.
|
||||
*/
|
||||
export class CreateMotion extends Motion {
|
||||
public submitters_id: number[];
|
||||
|
||||
public constructor(input?: any) {
|
||||
super(input);
|
||||
}
|
||||
}
|
62
client/src/app/site/motions/models/view-create-motion.ts
Normal file
62
client/src/app/site/motions/models/view-create-motion.ts
Normal file
@ -0,0 +1,62 @@
|
||||
import { Category } from '../../../shared/models/motions/category';
|
||||
import { User } from '../../../shared/models/users/user';
|
||||
import { Workflow } from '../../../shared/models/motions/workflow';
|
||||
import { WorkflowState } from '../../../shared/models/motions/workflow-state';
|
||||
import { Item } from 'app/shared/models/agenda/item';
|
||||
import { MotionBlock } from 'app/shared/models/motions/motion-block';
|
||||
import { ViewMotion } from './view-motion';
|
||||
import { CreateMotion } from './create-motion';
|
||||
|
||||
/**
|
||||
* Create motion class for the View. Its different to ViewMotion in fact that the submitter handling is different
|
||||
* on motion creation.
|
||||
*
|
||||
* @ignore
|
||||
*/
|
||||
export class ViewCreateMotion extends ViewMotion {
|
||||
protected _motion: CreateMotion;
|
||||
|
||||
public get motion(): CreateMotion {
|
||||
return this._motion;
|
||||
}
|
||||
|
||||
public get submitters(): User[] {
|
||||
return this._submitters;
|
||||
}
|
||||
|
||||
public get submitters_id(): number[] {
|
||||
return this.motion ? this.motion.submitters_id : null;
|
||||
}
|
||||
|
||||
public set submitters(users: User[]) {
|
||||
this._submitters = users;
|
||||
this._motion.submitters_id = users.map(user => user.id);
|
||||
}
|
||||
|
||||
public constructor(
|
||||
motion?: CreateMotion,
|
||||
category?: Category,
|
||||
submitters?: User[],
|
||||
supporters?: User[],
|
||||
workflow?: Workflow,
|
||||
state?: WorkflowState,
|
||||
item?: Item,
|
||||
block?: MotionBlock
|
||||
) {
|
||||
super(motion, category, submitters, supporters, workflow, state, item, block);
|
||||
}
|
||||
|
||||
/**
|
||||
* Duplicate this motion into a copy of itself
|
||||
*/
|
||||
public copy(): ViewCreateMotion {
|
||||
return new ViewCreateMotion(
|
||||
this._motion,
|
||||
this._category,
|
||||
this._submitters,
|
||||
this._supporters,
|
||||
this._workflow,
|
||||
this._state
|
||||
);
|
||||
}
|
||||
}
|
@ -31,14 +31,14 @@ export enum ChangeRecoMode {
|
||||
* @ignore
|
||||
*/
|
||||
export class ViewMotion extends BaseViewModel {
|
||||
private _motion: Motion;
|
||||
private _category: Category;
|
||||
private _submitters: User[];
|
||||
private _supporters: User[];
|
||||
private _workflow: Workflow;
|
||||
private _state: WorkflowState;
|
||||
private _item: Item;
|
||||
private _block: MotionBlock;
|
||||
protected _motion: Motion;
|
||||
protected _category: Category;
|
||||
protected _submitters: User[];
|
||||
protected _supporters: User[];
|
||||
protected _workflow: Workflow;
|
||||
protected _state: WorkflowState;
|
||||
protected _item: Item;
|
||||
protected _block: MotionBlock;
|
||||
|
||||
/**
|
||||
* Indicates the LineNumberingMode Mode.
|
||||
@ -126,7 +126,7 @@ export class ViewMotion extends BaseViewModel {
|
||||
}
|
||||
|
||||
public get submitters_id(): number[] {
|
||||
return this.motion ? this.motion.submitters_id : null;
|
||||
return this.motion ? this.motion.submitterIds : null;
|
||||
}
|
||||
|
||||
public get supporters(): User[] {
|
||||
@ -184,11 +184,6 @@ export class ViewMotion extends BaseViewModel {
|
||||
this._motion.supporters_id = users.map(user => user.id);
|
||||
}
|
||||
|
||||
public set submitters(users: User[]) {
|
||||
this._submitters = users;
|
||||
this._motion.submitters_id = users.map(user => user.id);
|
||||
}
|
||||
|
||||
public get item(): Item {
|
||||
return this._item;
|
||||
}
|
||||
@ -213,6 +208,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,
|
||||
|
58
client/src/app/site/motions/models/view-workflow.ts
Normal file
58
client/src/app/site/motions/models/view-workflow.ts
Normal file
@ -0,0 +1,58 @@
|
||||
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) {
|
||||
super();
|
||||
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,17 @@
|
||||
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,224 @@
|
||||
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';
|
||||
import { HttpService } from 'app/core/services/http.service';
|
||||
import { AgendaRepositoryService } from 'app/site/agenda/services/agenda-repository.service';
|
||||
import { Displayable } from 'app/shared/models/base/displayable';
|
||||
import { Identifiable } from 'app/shared/models/base/identifiable';
|
||||
|
||||
/**
|
||||
* 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,
|
||||
private agendaRepo: AgendaRepositoryService,
|
||||
private httpService: HttpService
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves the related agenda items from the motions as childs under a selected (parent) agenda item.
|
||||
*/
|
||||
public async moveToItem(motions: ViewMotion[]): Promise<void> {
|
||||
const title = this.translate.instant('This will move all selected motions as childs to:');
|
||||
const choices: (Displayable & Identifiable)[] = this.agendaRepo.getViewModelList();
|
||||
const selectedChoice = await this.choiceService.open(title, choices);
|
||||
if (selectedChoice) {
|
||||
const requestData = {
|
||||
items: motions.map(motion => motion.agenda_item_id),
|
||||
parent_id: selectedChoice as number
|
||||
};
|
||||
await this.httpService.post('/rest/agenda/item/assign', requestData);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
}));
|
||||
choices.push({ id: 0, label: 'Delete recommendation' });
|
||||
const selectedChoice = await this.choiceService.open(title, choices);
|
||||
if (typeof selectedChoice === 'number') {
|
||||
const requestData = motions.map(motion => ({
|
||||
id: motion.id,
|
||||
recommendation: selectedChoice
|
||||
}));
|
||||
await this.httpService.post('/rest/motions/motion/manage_multiple_recommendation', {
|
||||
motions: requestData
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
const requestData = motions.map(motion => {
|
||||
let submitterIds = [...motion.submitters_id, ...(selectedChoice as number[])];
|
||||
submitterIds = submitterIds.filter((id, index, self) => self.indexOf(id) === index); // remove duplicates
|
||||
return {
|
||||
id: motion.id,
|
||||
submitters: submitterIds
|
||||
};
|
||||
});
|
||||
await this.httpService.post('/rest/motions/motion/manage_multiple_submitters', { motions: requestData });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a dialog and removes the selected submitters for all given motions.
|
||||
*
|
||||
* @param motions The motions to remove the submitters from
|
||||
*/
|
||||
public async removeSubmitters(motions: ViewMotion[]): Promise<void> {
|
||||
const title = this.translate.instant('This will remove the following submitters from all selected motions:');
|
||||
const selectedChoice = await this.choiceService.open(title, this.userRepo.getViewModelList(), true);
|
||||
if (selectedChoice) {
|
||||
const requestData = motions.map(motion => {
|
||||
const submitterIdsToRemove = selectedChoice as number[];
|
||||
const submitterIds = motion.submitters_id.filter(id => !submitterIdsToRemove.includes(id));
|
||||
return {
|
||||
id: motion.id,
|
||||
submitters: submitterIds
|
||||
};
|
||||
});
|
||||
await this.httpService.post('/rest/motions/motion/manage_multiple_submitters', { motions: requestData });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
const requestData = motions.map(motion => {
|
||||
let tagIds = [...motion.tags_id, ...(selectedChoice as number[])];
|
||||
tagIds = tagIds.filter((id, index, self) => self.indexOf(id) === index); // remove duplicates
|
||||
return {
|
||||
id: motion.id,
|
||||
tags: tagIds
|
||||
};
|
||||
});
|
||||
await this.httpService.post('/rest/motions/motion/manage_multiple_tags', { motions: requestData });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a dialog and removes the selected tags for all given motions.
|
||||
*
|
||||
* @param motions The motions to remove the tags from
|
||||
*/
|
||||
public async removeTags(motions: ViewMotion[]): Promise<void> {
|
||||
const title = this.translate.instant('This will remove the following tags from all selected motions:');
|
||||
const selectedChoice = await this.choiceService.open(title, this.tagRepo.getViewModelList(), true);
|
||||
if (selectedChoice) {
|
||||
const requestData = motions.map(motion => {
|
||||
const tagIdsToRemove = selectedChoice as number[];
|
||||
const tagIds = motion.tags_id.filter(id => !tagIdsToRemove.includes(id));
|
||||
return {
|
||||
id: motion.id,
|
||||
tags: tagIds
|
||||
};
|
||||
});
|
||||
await this.httpService.post('/rest/motions/motion/manage_multiple_tags', { motions: requestData });
|
||||
}
|
||||
}
|
||||
}
|
@ -25,6 +25,7 @@ import { Item } from 'app/shared/models/agenda/item';
|
||||
import { OSTreeSortEvent } from 'app/shared/components/sorting-tree/sorting-tree.component';
|
||||
import { TreeService } from 'app/core/services/tree.service';
|
||||
import { ViewMotionAmendedParagraph } from '../models/view-motion-amended-paragraph';
|
||||
import { CreateMotion } from '../models/create-motion';
|
||||
|
||||
/**
|
||||
* Repository Services for motions (and potentially categories)
|
||||
@ -109,12 +110,8 @@ export class MotionRepositoryService extends BaseRepository<ViewMotion, Motion>
|
||||
*
|
||||
* @param update the form data containing the updated values
|
||||
* @param viewMotion The View Motion. If not present, a new motion will be created
|
||||
* TODO: Remove the viewMotion and make it actually distignuishable from save()
|
||||
*/
|
||||
public async create(motion: Motion): Promise<Identifiable> {
|
||||
if (!motion.supporters_id) {
|
||||
delete motion.supporters_id;
|
||||
}
|
||||
public async create(motion: CreateMotion): Promise<Identifiable> {
|
||||
return await this.dataSend.createModel(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,73 @@
|
||||
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(
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Collects all existing states from all workflows
|
||||
*/
|
||||
public getAllWorkflowStates(): WorkflowState[] {
|
||||
let states: WorkflowState[] = [];
|
||||
this.getViewModelList().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()
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1,39 +1,25 @@
|
||||
<os-head-bar mainButton=true (mainEvent)=onPlusButton() [multiSelectMode]="isMultiSelect">
|
||||
<os-head-bar mainButton="true" (mainEvent)="onPlusButton()" [multiSelectMode]="isMultiSelect">
|
||||
<!-- Title -->
|
||||
<div class="title-slot">
|
||||
<h2 translate>Participants</h2>
|
||||
</div>
|
||||
<div class="title-slot"><h2 translate>Participants</h2></div>
|
||||
|
||||
<!-- Menu -->
|
||||
<div class="menu-slot">
|
||||
<button type="button" mat-icon-button [matMenuTriggerFor]="userMenu">
|
||||
<mat-icon>more_vert</mat-icon>
|
||||
</button>
|
||||
<button type="button" mat-icon-button [matMenuTriggerFor]="userMenu"><mat-icon>more_vert</mat-icon></button>
|
||||
</div>
|
||||
|
||||
<!-- Multiselect info -->
|
||||
<div class="central-info-slot">
|
||||
<button mat-icon-button (click)="toggleMultiSelect()">
|
||||
<mat-icon>arrow_back</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button (click)="toggleMultiSelect()"><mat-icon>arrow_back</mat-icon></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>
|
||||
<button mat-icon-button (click)="deleteSelected()"><mat-icon>delete</mat-icon></button>
|
||||
</div>
|
||||
|
||||
</os-head-bar>
|
||||
|
||||
<div class="custom-table-header on-transition-fade">
|
||||
<button mat-button>
|
||||
<span translate>SORT</span>
|
||||
</button>
|
||||
<button mat-button>
|
||||
<span translate>FILTER</span>
|
||||
</button>
|
||||
<button mat-button><span translate>SORT</span></button> <button mat-button><span translate>FILTER</span></button>
|
||||
</div>
|
||||
|
||||
<mat-table class="os-listview-table on-transition-fade" [dataSource]="dataSource" matSort>
|
||||
@ -54,12 +40,12 @@
|
||||
<ng-container matColumnDef="group">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header>Group</mat-header-cell>
|
||||
<mat-cell *matCellDef="let user">
|
||||
<div class='groupsCell'>
|
||||
<div class="groupsCell">
|
||||
<span *ngIf="user.groups.length > 0">
|
||||
<mat-icon>people</mat-icon>
|
||||
{{ user.groups }}
|
||||
</span>
|
||||
<br *ngIf="user.groups && user.structureLevel">
|
||||
<br *ngIf="user.groups && user.structureLevel" />
|
||||
<span *ngIf="user.structureLevel">
|
||||
<mat-icon>flag</mat-icon>
|
||||
{{ user.structure_level }}
|
||||
@ -80,8 +66,11 @@
|
||||
</ng-container>
|
||||
|
||||
<mat-header-row *matHeaderRowDef="getColumnDefinition()"></mat-header-row>
|
||||
<mat-row [ngClass]="selectedRows.indexOf(row) >= 0 ? 'selected': ''" (click)='selectItem(row, $event)'
|
||||
*matRowDef="let row; columns: getColumnDefinition()">
|
||||
<mat-row
|
||||
[ngClass]="selectedRows.indexOf(row) >= 0 ? 'selected' : ''"
|
||||
(click)="selectItem(row, $event)"
|
||||
*matRowDef="let row; columns: getColumnDefinition()"
|
||||
>
|
||||
</mat-row>
|
||||
</mat-table>
|
||||
|
||||
@ -91,7 +80,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 +99,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 +120,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,6 +40,8 @@ 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,
|
||||
@ -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,93 @@ 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the default password and sets the real password.
|
||||
*
|
||||
* @param user The user to update
|
||||
* @param password The password to set
|
||||
*/
|
||||
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 });
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends invitation emails to all given users. Returns a prepared string to show the user.
|
||||
* This string should always be shown, becuase even in success cases, some users may not get
|
||||
* an email and the user should be notified about this.
|
||||
*
|
||||
* @param users All affected users
|
||||
*/
|
||||
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