Merge pull request #4189 from tsiegleauq/os3-motion-export-dialog

Add Motion Export dialog
This commit is contained in:
Emanuel Schütze 2019-01-28 10:34:43 +01:00 committed by GitHub
commit 56ff765708
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 441 additions and 95 deletions

View File

@ -9,7 +9,7 @@ import { ConfigService } from './config.service';
* Defines a csv column with a property of the model and an optional label. If this is not given, the
* translated and capitalized property name is used.
*/
interface CsvColumnDefinitionProperty<T> {
export interface CsvColumnDefinitionProperty<T> {
label?: string;
property: keyof T;
}

View File

@ -0,0 +1,68 @@
<h1 mat-dialog-title>{{ 'Export motions' | translate }}</h1>
<form [formGroup]="exportForm">
<!-- Content -->
<div mat-dialog-content>
<div>
<p class="toggle-group-head" translate>Format</p>
<mat-button-toggle-group formControlName="format">
<mat-button-toggle value="pdf">PDF</mat-button-toggle>
<mat-button-toggle value="csv">CSV</mat-button-toggle>
</mat-button-toggle-group>
</div>
<div>
<p class="toggle-group-head" translate>Line numbering</p>
<mat-button-toggle-group formControlName="lnMode">
<mat-button-toggle [value]="lnMode.None"> <span translate>None</span> </mat-button-toggle>
<mat-button-toggle [value]="lnMode.Inside"> <span translate>Inline</span> </mat-button-toggle>
<mat-button-toggle [value]="lnMode.Outside"> <span translate>Outside</span> </mat-button-toggle>
</mat-button-toggle-group>
</div>
<div>
<p class="toggle-group-head" translate>Change recommendations</p>
<mat-button-toggle-group formControlName="crMode">
<mat-button-toggle [value]="crMode.Original">
<span translate>Original version</span>
</mat-button-toggle>
<mat-button-toggle [value]="crMode.Changed"> <span translate>Changed version</span> </mat-button-toggle>
<mat-button-toggle [value]="crMode.Diff" #diffVersionButton>
<span translate>Diff version</span>
</mat-button-toggle>
<mat-button-toggle [value]="crMode.Final"> <span translate>Final version</span> </mat-button-toggle>
</mat-button-toggle-group>
</div>
<div>
<p class="toggle-group-head" translate>Content</p>
<mat-button-toggle-group multiple formControlName="content">
<mat-button-toggle value="text"> <span translate>Text</span> </mat-button-toggle>
<mat-button-toggle value="reason"> <span translate>Reason</span> </mat-button-toggle>
</mat-button-toggle-group>
</div>
<div>
<p class="toggle-group-head" translate>Meta information</p>
<mat-button-toggle-group multiple formControlName="metaInfo">
<mat-button-toggle value="submitters"> <span translate>Submitters</span> </mat-button-toggle>
<mat-button-toggle value="state" #stateButton> <span translate>State</span> </mat-button-toggle>
<mat-button-toggle value="recommendation" #recommendationButton> <span translate>Recommendation</span> </mat-button-toggle>
<mat-button-toggle value="category"> <span translate>Category</span> </mat-button-toggle>
<mat-button-toggle value="origin"> <span translate>Origin</span> </mat-button-toggle>
<mat-button-toggle value="block"> <span translate>Motion block</span> </mat-button-toggle>
<mat-button-toggle value="votingResult" #votingResultButton> <span translate>Voting Result</span> </mat-button-toggle>
</mat-button-toggle-group>
</div>
<br />
</div>
<!-- Action buttons -->
<div mat-dialog-actions>
<button mat-button type="button" color="primary" [mat-dialog-close]="exportForm.value">
<span translate>Export</span>
</button>
<button mat-button type="button" (click)="onCloseClick()"><span translate>Cancel</span></button>
</div>
</form>

View File

@ -0,0 +1,3 @@
.toggle-group-head {
margin-bottom: 0;
}

View File

@ -0,0 +1,28 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { MotionExportDialogComponent } from './motion-export-dialog.component';
import { E2EImportsModule } from 'e2e-imports.module';
import { MatDialogRef } from '@angular/material';
describe('MotionExportDialogComponent', () => {
let component: MotionExportDialogComponent;
let fixture: ComponentFixture<MotionExportDialogComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [E2EImportsModule],
declarations: [MotionExportDialogComponent],
providers: [{ provide: MatDialogRef, useValue: {} }]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(MotionExportDialogComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,186 @@
import { Component, OnInit, ViewChild } from '@angular/core';
import { FormGroup, FormBuilder } from '@angular/forms';
import { MatDialogRef, MatButtonToggle } from '@angular/material';
import { ConfigService } from 'app/core/services/config.service';
import { LineNumberingMode, ChangeRecoMode } from '../../models/view-motion';
/**
* Dialog component to determine exporting.
*/
@Component({
selector: 'os-motion-export-dialog',
templateUrl: './motion-export-dialog.component.html',
styleUrls: ['./motion-export-dialog.component.scss']
})
export class MotionExportDialogComponent implements OnInit {
/**
* For using the enum constants from the template.
*/
public lnMode = LineNumberingMode;
/**
* For using the enum constants from the template.
*/
public crMode = ChangeRecoMode;
/**
* The form that contains the export information.
*/
public exportForm: FormGroup;
/**
* determine the default format to export
*/
private defaultExportFormat = 'pdf';
/**
* Determine the default content to export.
*/
private defaultContentToExport = ['text', 'reason'];
/**
* Determine the default meta info to export.
*/
private defaultInfoToExport = [
'submitters',
'state',
'recommendation',
'category',
'origin',
'block',
'votingResult'
];
/**
* Hold the default lnMode. Will be set by the constructor.
*/
private defaultLnMode: LineNumberingMode;
/**
* Hold the default crMode. Will be set by the constructor.
*/
private defaultCrMode: ChangeRecoMode;
/**
* To deactivate the export-as-diff button
*/
@ViewChild('diffVersionButton')
public diffVersionButton: MatButtonToggle;
/**
* To deactivate the export-as-diff button
*/
@ViewChild('votingResultButton')
public votingResultButton: MatButtonToggle;
/**
* To deactivate the state button
*/
@ViewChild('stateButton')
public stateButton: MatButtonToggle;
/**
* To deactivate the state button
*/
@ViewChild('recommendationButton')
public recommendationButton: MatButtonToggle;
/**
* Constructor
* Sets the default values for the lineNumberingMode and changeRecoMode and creates the form.
* This uses "instant" over observables to prevent on-fly-changes by auto update while
* the dialog is open.
*
* @param formBuilder Creates the export form
* @param dialogRef Make the dialog available
*/
public constructor(
public formBuilder: FormBuilder,
public dialogRef: MatDialogRef<MotionExportDialogComponent>,
public configService: ConfigService
) {
this.defaultLnMode = this.configService.instant('motions_default_line_numbering');
this.defaultCrMode = this.configService.instant('motions_recommendation_text_mode');
this.createForm();
}
/**
* Init.
* Observes the form for changes to react dynamically
*/
public ngOnInit(): void {
this.exportForm.get('format').valueChanges.subscribe((value: string) => {
if (value === 'csv') {
// disable and deselect "lnMode"
this.exportForm.get('lnMode').setValue(this.lnMode.None);
this.exportForm.get('lnMode').disable();
// disable and deselect "Change Reco Mode"
// TODO: The current implementation of the motion csv export does not consider anything else than
// the "normal" motion.text, therefore this is disabled for now
this.exportForm.get('crMode').setValue(this.crMode.Original);
this.exportForm.get('crMode').disable();
// remove the selection of "Diff Version" and set it to default or original
// TODO: Use this over the disable block logic above when the export service supports more than
// just the normal motion text
// if (this.exportForm.get('crMode').value === this.crMode.Diff) {
// if (this.defaultCrMode === this.crMode.Diff) {
// this.exportForm.get('crMode').setValue(this.crMode.Original);
// } else {
// this.exportForm.get('crMode').setValue(this.defaultCrMode);
// }
// }
// remove the selection of "votingResult", "state" and "recommendation"
let metaInfoVal: string[] = this.exportForm.get('metaInfo').value;
metaInfoVal = metaInfoVal.filter(info => {
return info !== 'votingResult' && info !== 'state' && info !== 'recommendation';
});
this.exportForm.get('metaInfo').setValue(metaInfoVal);
// disable "Diff Version", "Voting Result", "State" and "Recommendation"
this.votingResultButton.disabled = true;
this.stateButton.disabled = true;
this.recommendationButton.disabled = true;
// TODO: CSV Issues
// this.diffVersionButton.disabled = true;
} else if (value === 'pdf') {
this.exportForm.get('lnMode').enable();
this.exportForm.get('lnMode').setValue(this.defaultLnMode);
// TODO: Temporarily necessary until CSV has been fixed
this.exportForm.get('crMode').enable();
this.exportForm.get('crMode').setValue(this.defaultCrMode);
// enable "Diff Version", "Voting Result", "State" and "Recommendation"
this.votingResultButton.disabled = false;
this.stateButton.disabled = false;
this.recommendationButton.disabled = false;
// TODO: Temporarily disabled. Will be required after CSV fixes
// this.diffVersionButton.disabled = false;
}
});
}
/**
* Creates the form with default values
*/
public createForm(): void {
this.exportForm = this.formBuilder.group({
format: [this.defaultExportFormat],
lnMode: [this.defaultLnMode],
crMode: [this.defaultCrMode],
content: [this.defaultContentToExport],
metaInfo: [this.defaultInfoToExport]
});
}
/**
* Just close the dialog
*/
public onCloseClick(): void {
this.dialogRef.close();
}
}

View File

@ -174,13 +174,9 @@
<mat-icon>local_offer</mat-icon>
<span translate>Tags</span>
</button>
<button mat-menu-item (click)="csvExportMotionList()">
<button mat-menu-item (click)="openExportDialog()">
<mat-icon>archive</mat-icon>
<span translate>Export as CSV</span>
</button>
<button mat-menu-item (click)="onExportAsPdf()">
<mat-icon>picture_as_pdf</mat-icon>
<span translate>Export all as PDF</span>
<span translate>Export</span><span>&nbsp;...</span>
</button>
<button mat-menu-item routerLink="import">
<mat-icon>save_alt</mat-icon>

View File

@ -1,13 +1,14 @@
import { Component, OnInit } from '@angular/core';
import { Router, ActivatedRoute } from '@angular/router';
import { Title } from '@angular/platform-browser';
import { MatSnackBar, MatDialog } from '@angular/material';
import { TranslateService } from '@ngx-translate/core';
import { CategoryRepositoryService } from '../../services/category-repository.service';
import { ConfigService } from '../../../../core/services/config.service';
import { ListViewBaseComponent } from '../../../base/list-view-base';
import { LocalPermissionsService } from '../../services/local-permissions.service';
import { MatSnackBar } from '@angular/material';
import { MotionBlockRepositoryService } from '../../services/motion-block-repository.service';
import { MotionCsvExportService } from '../../services/motion-csv-export.service';
import { MotionFilterListService } from '../../services/motion-filter-list.service';
@ -23,6 +24,7 @@ import { ViewWorkflow } from '../../models/view-workflow';
import { WorkflowState } from '../../../../shared/models/motions/workflow-state';
import { WorkflowRepositoryService } from '../../services/workflow-repository.service';
import { MotionPdfExportService } from '../../services/motion-pdf-export.service';
import { MotionExportDialogComponent } from '../motion-export-dialog/motion-export-dialog.component';
/**
* Component that displays all the motions in a Table using DataSource.
@ -96,6 +98,7 @@ export class MotionListComponent extends ListViewBaseComponent<ViewMotion> imple
private motionRepo: MotionRepositoryService,
private motionCsvExport: MotionCsvExportService,
private pdfExport: MotionPdfExportService,
private dialog: MatDialog,
public multiselectService: MotionMultiselectService,
public sortService: MotionSortListService,
public filterService: MotionFilterListService,
@ -189,17 +192,29 @@ export class MotionListComponent extends ListViewBaseComponent<ViewMotion> imple
}
/**
* Export all motions as CSV
* Opens the export dialog
*/
public csvExportMotionList(): void {
this.motionCsvExport.exportMotionList(this.dataSource.data);
}
public openExportDialog(): void {
const exportDialogRef = this.dialog.open(MotionExportDialogComponent, {
width: '750px',
data: this.dataSource
});
/**
* Exports motions as PDF.
*/
public onExportAsPdf(): void {
this.pdfExport.exportMotionCatalog(this.dataSource.data);
exportDialogRef.afterClosed().subscribe((result: any) => {
if (result && result.format) {
if (result.format === 'pdf') {
this.pdfExport.exportMotionCatalog(
this.dataSource.data,
result.lnMode,
result.crMode,
result.content,
result.metaInfo
);
} else if (result.format === 'csv') {
this.motionCsvExport.exportMotionList(this.dataSource.data, result.content, result.metaInfo);
}
}
});
}
/**

View File

@ -22,6 +22,7 @@ import { MotionImportListComponent } from './components/motion-import-list/motio
import { ManageSubmittersComponent } from './components/manage-submitters/manage-submitters.component';
import { MotionPollComponent } from './components/motion-poll/motion-poll.component';
import { MotionPollDialogComponent } from './components/motion-poll/motion-poll-dialog.component';
import { MotionExportDialogComponent } from './components/motion-export-dialog/motion-export-dialog.component';
@NgModule({
imports: [CommonModule, MotionsRoutingModule, SharedModule],
@ -44,7 +45,8 @@ import { MotionPollDialogComponent } from './components/motion-poll/motion-poll-
MotionImportListComponent,
ManageSubmittersComponent,
MotionPollComponent,
MotionPollDialogComponent
MotionPollDialogComponent,
MotionExportDialogComponent
],
entryComponents: [
MotionChangeRecommendationComponent,
@ -54,7 +56,8 @@ import { MotionPollDialogComponent } from './components/motion-poll/motion-poll-
MetaTextBlockComponent,
PersonalNoteComponent,
ManageSubmittersComponent,
MotionPollDialogComponent
MotionPollDialogComponent,
MotionExportDialogComponent
]
})
export class MotionsModule {}

View File

@ -2,7 +2,7 @@ import { Injectable } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { CsvExportService } from 'app/core/services/csv-export.service';
import { CsvExportService, CsvColumnDefinitionProperty } from 'app/core/services/csv-export.service';
import { ViewMotion } from '../models/view-motion';
import { FileExportService } from 'app/core/services/file-export.service';
@ -29,21 +29,16 @@ export class MotionCsvExportService {
* Export all motions as CSV
*
* @param motions Motions to export
* @param contentToExport content properties to export
* @param infoToExport meta info to export
*/
public exportMotionList(motions: ViewMotion[]): void {
this.csvExport.export(
motions,
[
{ property: 'identifier' },
{ property: 'title' },
{ property: 'text' },
{ property: 'reason' },
{ property: 'submitters' },
{ property: 'category' },
{ property: 'origin' }
],
this.translate.instant('Motions') + '.csv'
);
public exportMotionList(motions: ViewMotion[], contentToExport: string[], infoToExport: string[]): void {
const propertyList = ['identifier', 'title'].concat(contentToExport, infoToExport);
const exportProperties: CsvColumnDefinitionProperty<ViewMotion>[] = propertyList.map(option => {
return { property: option } as CsvColumnDefinitionProperty<ViewMotion>;
});
this.csvExport.export(motions, exportProperties, this.translate.instant('Motions') + '.csv');
}
/**

View File

@ -2,7 +2,7 @@ import { Injectable } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { ViewMotion } from '../models/view-motion';
import { ViewMotion, LineNumberingMode, ChangeRecoMode } from '../models/view-motion';
import { MotionPdfService } from './motion-pdf.service';
import { ConfigService } from 'app/core/services/config.service';
import { Category } from 'app/shared/models/motions/category';
@ -47,12 +47,24 @@ export class MotionPdfCatalogService {
* @param motions the list of view motions to convert
* @returns pdfmake doc definition as object
*/
public motionListToDocDef(motions: ViewMotion[]): object {
public motionListToDocDef(
motions: ViewMotion[],
lnMode?: LineNumberingMode,
crMode?: ChangeRecoMode,
contentToExport?: string[],
infoToExport?: string[]
): object {
let doc = [];
const motionDocList = [];
for (let motionIndex = 0; motionIndex < motions.length; ++motionIndex) {
const motionDocDef: any = this.motionPdfService.motionToDocDef(motions[motionIndex]);
const motionDocDef: any = this.motionPdfService.motionToDocDef(
motions[motionIndex],
lnMode,
crMode,
contentToExport,
infoToExport
);
// add id field to the first page of a motion to make it findable over TOC
motionDocDef[0].id = `${motions[motionIndex].id}`;

View File

@ -50,10 +50,20 @@ export class MotionPdfExportService {
/**
* Exports multiple motions to a collection of PDFs
*
* @param motions
* @param motions the motions to export
* @param lnMode lineNumbering Mode
* @param crMode Change Recommendation Mode
* @param contentToExport Determine to determine with text and/or reason
* @param infoToExport Determine the meta info to export
*/
public exportMotionCatalog(motions: ViewMotion[]): void {
const doc = this.pdfCatalogService.motionListToDocDef(motions);
public exportMotionCatalog(
motions: ViewMotion[],
lnMode?: LineNumberingMode,
crMode?: ChangeRecoMode,
contentToExport?: string[],
infoToExport?: string[]
): void {
const doc = this.pdfCatalogService.motionListToDocDef(motions, lnMode, crMode, contentToExport, infoToExport);
const filename = this.translate.instant(this.configService.instant<string>('motions_export_title'));
const metadata = {
title: filename

View File

@ -47,9 +47,19 @@ export class MotionPdfService {
* @param motion the motion to convert to pdf
* @param lnMode determine the used line mode
* @param crMode determine the used change Recommendation mode
* @param contentToExport determine which content is to export. If left out, everything will be exported
* @param infoToExport determine which metaInfo to export. If left out, everything will be exported.
* @returns doc def for the motion
*/
public motionToDocDef(motion: ViewMotion, lnMode?: LineNumberingMode, crMode?: ChangeRecoMode): object {
public motionToDocDef(
motion: ViewMotion,
lnMode?: LineNumberingMode,
crMode?: ChangeRecoMode,
contentToExport?: string[],
infoToExport?: string[]
): object {
let motionPdfContent = [];
// determine the default lnMode if not explicitly given
if (!lnMode) {
lnMode = this.configService.instant('motions_default_line_numbering');
@ -62,12 +72,26 @@ export class MotionPdfService {
const title = this.createTitle(motion);
const subtitle = this.createSubtitle(motion);
const metaInfo = this.createMetaInfoTable(motion, crMode);
const preamble = this.createPreamble(motion);
const text = this.createText(motion, lnMode, crMode);
const reason = this.createReason(motion, lnMode);
const motionPdfContent = [title, subtitle, metaInfo, preamble, text, reason];
motionPdfContent = [title, subtitle];
if ((infoToExport && infoToExport.length > 0) || !infoToExport) {
const metaInfo = this.createMetaInfoTable(motion, crMode, infoToExport);
motionPdfContent.push(metaInfo);
}
if (!contentToExport || contentToExport.includes('text')) {
const preamble = this.createPreamble(motion);
motionPdfContent.push(preamble);
const text = this.createText(motion, lnMode, crMode);
motionPdfContent.push(text);
}
if (!contentToExport || contentToExport.includes('reason')) {
const reason = this.createReason(motion, lnMode);
motionPdfContent.push(reason);
}
return motionPdfContent;
}
@ -119,39 +143,43 @@ export class MotionPdfService {
* @param motion the target motion
* @returns doc def for the meta infos
*/
private createMetaInfoTable(motion: ViewMotion, crMode: ChangeRecoMode): object {
private createMetaInfoTable(motion: ViewMotion, crMode: ChangeRecoMode, infoToExport?: string[]): object {
const metaTableBody = [];
// submitters
const submitters = motion.submitters
.map(submitter => {
return submitter.full_name;
})
.join(', ');
if (!infoToExport || infoToExport.includes('submitters')) {
const submitters = motion.submitters
.map(submitter => {
return submitter.full_name;
})
.join(', ');
metaTableBody.push([
{
text: `${this.translate.instant('Submitters')}:`,
style: 'boldText'
},
{
text: submitters
}
]);
metaTableBody.push([
{
text: `${this.translate.instant('Submitters')}:`,
style: 'boldText'
},
{
text: submitters
}
]);
}
// state
metaTableBody.push([
{
text: `${this.translate.instant('State')}:`,
style: 'boldText'
},
{
text: this.motionRepo.getExtendedStateLabel(motion)
}
]);
if (!infoToExport || infoToExport.includes('state')) {
metaTableBody.push([
{
text: `${this.translate.instant('State')}:`,
style: 'boldText'
},
{
text: this.motionRepo.getExtendedStateLabel(motion)
}
]);
}
// recommendation
if (motion.recommendation) {
if (motion.recommendation && (!infoToExport || infoToExport.includes('recommendation'))) {
metaTableBody.push([
{
text: `${this.translate.instant('Recommendation')}:`,
@ -164,7 +192,7 @@ export class MotionPdfService {
}
// category
if (motion.category) {
if (motion.category && (!infoToExport || infoToExport.includes('category'))) {
metaTableBody.push([
{
text: `${this.translate.instant('Category')}:`,
@ -177,7 +205,7 @@ export class MotionPdfService {
}
// motion block
if (motion.origin) {
if (motion.motion_block && (!infoToExport || infoToExport.includes('block'))) {
metaTableBody.push([
{
text: `${this.translate.instant('Motion block')}:`,
@ -190,11 +218,11 @@ export class MotionPdfService {
}
// origin
if (motion.origin) {
if (motion.origin && (!infoToExport || infoToExport.includes('origin'))) {
metaTableBody.push([
{
text: `${this.translate.instant('Origin')}:`,
style: ['boldText', 'greyBackground']
style: 'boldText'
},
{
text: motion.origin
@ -259,28 +287,30 @@ export class MotionPdfService {
]);
}
return {
table: {
widths: ['35%', '65%'],
body: metaTableBody
},
margin: [0, 0, 0, 20],
// That did not work too well in the past. Perhaps substitution by a pdfWorker the worker will be necessary
layout: {
fillColor: () => {
return '#dddddd';
if (metaTableBody.length > 0) {
return {
table: {
widths: ['35%', '65%'],
body: metaTableBody
},
hLineWidth: (i, node) => {
return i === 0 || i === node.table.body.length ? 0 : 0.5;
},
vLineWidth: () => {
return 0;
},
hLineColor: () => {
return 'white';
margin: [0, 0, 0, 20],
// That did not work too well in the past. Perhaps substitution by a pdfWorker the worker will be necessary
layout: {
fillColor: () => {
return '#dddddd';
},
hLineWidth: (i, node) => {
return i === 0 || i === node.table.body.length ? 0 : 0.5;
},
vLineWidth: () => {
return 0;
},
hLineColor: () => {
return 'white';
}
}
}
};
};
}
}
/**