Add Motion Export dialog

Removes the "Export As CSV" and "Export As PDF" options from MotionList
view and adds an export dialog instead (just like OS 2.3)
The exprt Dialog dynamically changes it's content according to the possible
selections.

The current implementation of the CSV exporter is not able to export anything
but the original motion text. The exporter does consider this and disables
this option for now.

While the old exporter showed "state" and "recommendation" during CSV export,
but was in fact not exporting state nor recommendation, the new exporter
will disable these fields during CSV export.

PDF should work as expected
This commit is contained in:
Sean Engelhardt 2019-01-25 17:03:05 +01:00
parent 43e0f7943b
commit 23fea51333
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';
}
}
}
};
};
}
}
/**