Export a list of motions
Ports the "Motion Catalog Content Provider" to OpenSlides 3. Categories and Prefixes are considered as before. Updated to Code to ES6/Typescript. Allows to export all motions from the motion list view.
This commit is contained in:
parent
c48148fc01
commit
0e77ec79c2
@ -93,7 +93,7 @@ export class HtmlToPdfService {
|
|||||||
const parser = new DOMParser();
|
const parser = new DOMParser();
|
||||||
const parsedHtml = parser.parseFromString(htmlText, 'text/html');
|
const parsedHtml = parser.parseFromString(htmlText, 'text/html');
|
||||||
// Since the spread operator did not work for HTMLCollection, use Array.from
|
// Since the spread operator did not work for HTMLCollection, use Array.from
|
||||||
const htmlArray = Array.from(parsedHtml.body.children);
|
const htmlArray = Array.from(parsedHtml.body.childNodes);
|
||||||
|
|
||||||
// Parse the children of the current HTML element
|
// Parse the children of the current HTML element
|
||||||
for (const child of htmlArray) {
|
for (const child of htmlArray) {
|
||||||
@ -219,6 +219,7 @@ export class HtmlToPdfService {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const children = this.parseChildren(element, styles);
|
const children = this.parseChildren(element, styles);
|
||||||
|
|
||||||
newParagraph = {
|
newParagraph = {
|
||||||
...this.create('text'),
|
...this.create('text'),
|
||||||
...this.computeStyle(styles)
|
...this.computeStyle(styles)
|
||||||
|
@ -308,13 +308,18 @@ export class PdfDocumentService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Downloads a pdf. Does not seem to work.
|
* Downloads a pdf.
|
||||||
*
|
*
|
||||||
* @param docDefinition the structure of the PDF document
|
* @param docDefinition the structure of the PDF document
|
||||||
*/
|
*/
|
||||||
public download(docDefinition: object, filename: string, metadata?: object): void {
|
public async download(docDefinition: object, filename: string, metadata?: object): Promise<void> {
|
||||||
this.getStandardPaper(docDefinition, metadata).then(doc => {
|
const doc = await this.getStandardPaper(docDefinition, metadata);
|
||||||
pdfMake.createPdf(doc).getBlob(blob => saveAs(blob, `${filename}.pdf`, { autoBOM: true }));
|
await new Promise<boolean>(resolve => {
|
||||||
|
const pdf = pdfMake.createPdf(doc);
|
||||||
|
pdf.getBlob(blob => {
|
||||||
|
saveAs(blob, `${filename}.pdf`, { autoBOM: true });
|
||||||
|
resolve(true);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -347,6 +352,9 @@ export class PdfDocumentService {
|
|||||||
margin: [0, -20, 0, 20],
|
margin: [0, -20, 0, 20],
|
||||||
color: 'grey'
|
color: 'grey'
|
||||||
},
|
},
|
||||||
|
preamble: {
|
||||||
|
margin: [0, 0, 0, 10]
|
||||||
|
},
|
||||||
headerText: {
|
headerText: {
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
margin: [0, 10, 0, 0]
|
margin: [0, 10, 0, 0]
|
||||||
@ -362,10 +370,33 @@ export class PdfDocumentService {
|
|||||||
smallText: {
|
smallText: {
|
||||||
fontSize: 8
|
fontSize: 8
|
||||||
},
|
},
|
||||||
|
heading2: {
|
||||||
|
fontSize: 14,
|
||||||
|
margin: [0, 0, 0, 10],
|
||||||
|
bold: true
|
||||||
|
},
|
||||||
heading3: {
|
heading3: {
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
margin: [0, 10, 0, 0],
|
margin: [0, 10, 0, 0],
|
||||||
bold: true
|
bold: true
|
||||||
|
},
|
||||||
|
tocEntry: {
|
||||||
|
fontSize: 12,
|
||||||
|
margin: [0, 0, 0, 0],
|
||||||
|
bold: false
|
||||||
|
},
|
||||||
|
tocCategoryEntry: {
|
||||||
|
fontSize: 12,
|
||||||
|
margin: [10, 0, 0, 0],
|
||||||
|
bold: false
|
||||||
|
},
|
||||||
|
tocCategoryTitle: {
|
||||||
|
fontSize: 12,
|
||||||
|
margin: [0, 0, 0, 4],
|
||||||
|
bold: true
|
||||||
|
},
|
||||||
|
tocCategorySection: {
|
||||||
|
margin: [0, 0, 0, 10]
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -178,6 +178,10 @@
|
|||||||
<mat-icon>archive</mat-icon>
|
<mat-icon>archive</mat-icon>
|
||||||
<span translate>Export as CSV</span>
|
<span translate>Export as CSV</span>
|
||||||
</button>
|
</button>
|
||||||
|
<button mat-menu-item (click)="onExportAsPdf()">
|
||||||
|
<mat-icon>picture_as_pdf</mat-icon>
|
||||||
|
<span translate>Export all as PDF</span>
|
||||||
|
</button>
|
||||||
<button mat-menu-item routerLink="import">
|
<button mat-menu-item routerLink="import">
|
||||||
<mat-icon>save_alt</mat-icon>
|
<mat-icon>save_alt</mat-icon>
|
||||||
<span translate>Import</span><span> ...</span>
|
<span translate>Import</span><span> ...</span>
|
||||||
|
@ -22,6 +22,7 @@ import { ViewTag } from 'app/site/tags/models/view-tag';
|
|||||||
import { ViewWorkflow } from '../../models/view-workflow';
|
import { ViewWorkflow } from '../../models/view-workflow';
|
||||||
import { WorkflowState } from '../../../../shared/models/motions/workflow-state';
|
import { WorkflowState } from '../../../../shared/models/motions/workflow-state';
|
||||||
import { WorkflowRepositoryService } from '../../services/workflow-repository.service';
|
import { WorkflowRepositoryService } from '../../services/workflow-repository.service';
|
||||||
|
import { MotionPdfExportService } from '../../services/motion-pdf-export.service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component that displays all the motions in a Table using DataSource.
|
* Component that displays all the motions in a Table using DataSource.
|
||||||
@ -74,6 +75,7 @@ export class MotionListComponent extends ListViewBaseComponent<ViewMotion> imple
|
|||||||
* @param categoryRepo: Repo for categories. Used to define filters
|
* @param categoryRepo: Repo for categories. Used to define filters
|
||||||
* @param workflowRepo: Repo for Workflows. Used to define filters
|
* @param workflowRepo: Repo for Workflows. Used to define filters
|
||||||
* @param motionCsvExport
|
* @param motionCsvExport
|
||||||
|
* @param pdfExport To export motions as PDF
|
||||||
* @param multiselectService Service for the multiSelect actions
|
* @param multiselectService Service for the multiSelect actions
|
||||||
* @param userRepo
|
* @param userRepo
|
||||||
* @param sortService
|
* @param sortService
|
||||||
@ -93,6 +95,7 @@ export class MotionListComponent extends ListViewBaseComponent<ViewMotion> imple
|
|||||||
private workflowRepo: WorkflowRepositoryService,
|
private workflowRepo: WorkflowRepositoryService,
|
||||||
private motionRepo: MotionRepositoryService,
|
private motionRepo: MotionRepositoryService,
|
||||||
private motionCsvExport: MotionCsvExportService,
|
private motionCsvExport: MotionCsvExportService,
|
||||||
|
private pdfExport: MotionPdfExportService,
|
||||||
public multiselectService: MotionMultiselectService,
|
public multiselectService: MotionMultiselectService,
|
||||||
public sortService: MotionSortListService,
|
public sortService: MotionSortListService,
|
||||||
public filterService: MotionFilterListService,
|
public filterService: MotionFilterListService,
|
||||||
@ -192,6 +195,13 @@ export class MotionListComponent extends ListViewBaseComponent<ViewMotion> imple
|
|||||||
this.motionCsvExport.exportMotionList(this.dataSource.data);
|
this.motionCsvExport.exportMotionList(this.dataSource.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exports motions as PDF.
|
||||||
|
*/
|
||||||
|
public onExportAsPdf(): void {
|
||||||
|
this.pdfExport.exportMotionCatalog(this.dataSource.data);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns current definitions for the listView table
|
* Returns current definitions for the listView table
|
||||||
*/
|
*/
|
||||||
|
@ -0,0 +1,17 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { MotionPdfCatalogService } from './motion-pdf-catalog.service';
|
||||||
|
import { E2EImportsModule } from 'e2e-imports.module';
|
||||||
|
|
||||||
|
describe('MotionPdfCatalogService', () => {
|
||||||
|
beforeEach(() =>
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [E2EImportsModule]
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
const service: MotionPdfCatalogService = TestBed.get(MotionPdfCatalogService);
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,240 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
|
||||||
|
import { ViewMotion } 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';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service to export a list of motions.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* const docDef = this.motionPdfCatalogService.motionListToDocDef(myListOfViewMotions);
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class MotionPdfCatalogService {
|
||||||
|
/**
|
||||||
|
* Helper to add page breaks to documents
|
||||||
|
*/
|
||||||
|
private pageBreak = {
|
||||||
|
text: '',
|
||||||
|
pageBreak: 'after'
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor
|
||||||
|
*
|
||||||
|
* @param translate handle translations
|
||||||
|
* @param configService read out config variables
|
||||||
|
* @param motionPdfService handle motion to pdf conversion
|
||||||
|
*/
|
||||||
|
public constructor(
|
||||||
|
private translate: TranslateService,
|
||||||
|
private configService: ConfigService,
|
||||||
|
private motionPdfService: MotionPdfService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts the list of motions to pdfmake doc definition.
|
||||||
|
* Public entry point to conversion of multiple motions
|
||||||
|
*
|
||||||
|
* @param motions the list of view motions to convert
|
||||||
|
* @returns pdfmake doc definition as object
|
||||||
|
*/
|
||||||
|
public motionListToDocDef(motions: ViewMotion[]): object {
|
||||||
|
let doc = [];
|
||||||
|
const motionDocList = [];
|
||||||
|
|
||||||
|
for (let motionIndex = 0; motionIndex < motions.length; ++motionIndex) {
|
||||||
|
const motionDocDef: any = this.motionPdfService.motionToDocDef(motions[motionIndex]);
|
||||||
|
|
||||||
|
// add id field to the first page of a motion to make it findable over TOC
|
||||||
|
motionDocDef[0].id = `${motions[motionIndex].id}`;
|
||||||
|
|
||||||
|
motionDocList.push(motionDocDef);
|
||||||
|
|
||||||
|
if (motionIndex < motions.length - 1) {
|
||||||
|
motionDocList.push(this.pageBreak);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// print extra data (title, preamble, categories, toc) only if there are more than 1 motion
|
||||||
|
if (motions.length > 1) {
|
||||||
|
doc.push(this.createTitle(), this.createPreamble(), this.createToc(motions));
|
||||||
|
}
|
||||||
|
|
||||||
|
doc = doc.concat(motionDocList);
|
||||||
|
|
||||||
|
return doc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the title for the motion list as pdfmake doc definition
|
||||||
|
*
|
||||||
|
* @returns The motion list title for the PDF document
|
||||||
|
*/
|
||||||
|
private createTitle(): object {
|
||||||
|
const titleText = this.configService.instant<string>('motions_export_title');
|
||||||
|
return {
|
||||||
|
text: titleText,
|
||||||
|
style: 'title'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the preamble for the motion list as pdfmake doc definition
|
||||||
|
*
|
||||||
|
* @returns The motion list preamble for the PDF document
|
||||||
|
*/
|
||||||
|
private createPreamble(): object {
|
||||||
|
const preambleText = this.configService.instant<string>('motions_export_preamble');
|
||||||
|
|
||||||
|
if (preambleText) {
|
||||||
|
return {
|
||||||
|
text: preambleText,
|
||||||
|
style: 'preamble'
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the table of contents for the motion book.
|
||||||
|
* Considers sorting by categories and no sorting.
|
||||||
|
*
|
||||||
|
* @param motions The motions to add in the TOC
|
||||||
|
* @param sorting The optional sorting strategy
|
||||||
|
* @returns the table of contents as document definition
|
||||||
|
*/
|
||||||
|
private createToc(motions: ViewMotion[], sorting?: string): object {
|
||||||
|
const toc = [];
|
||||||
|
const categories: Category[] = this.getUniqueCategories(motions);
|
||||||
|
|
||||||
|
// Create the toc title
|
||||||
|
const tocTitle = {
|
||||||
|
text: this.translate.instant('Table of contents'),
|
||||||
|
style: 'heading2'
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!sorting) {
|
||||||
|
sorting = this.configService.instant<string>('motions_export_category_sorting');
|
||||||
|
}
|
||||||
|
const exportCategory = sorting === 'identifier' || sorting === 'prefix';
|
||||||
|
|
||||||
|
if (exportCategory && categories) {
|
||||||
|
const catTocBody = [];
|
||||||
|
for (const category of categories) {
|
||||||
|
// push the name of the category
|
||||||
|
// make a table for correct alignment
|
||||||
|
catTocBody.push({
|
||||||
|
table: {
|
||||||
|
body: [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
text: category.prefix + ' - ' + category.name,
|
||||||
|
style: 'tocCategoryTitle'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
layout: 'noBorders'
|
||||||
|
});
|
||||||
|
|
||||||
|
const tocBody = motions
|
||||||
|
.filter(motion => category === motion.category)
|
||||||
|
.map(motion => this.createTocLine(motion, 'tocCategoryEntry'));
|
||||||
|
|
||||||
|
catTocBody.push(this.createTocTableDef(tocBody));
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle those without category
|
||||||
|
const uncatTocBody = motions
|
||||||
|
.filter(motion => !motion.category)
|
||||||
|
.map(motion => this.createTocLine(motion, 'tocEntry'));
|
||||||
|
|
||||||
|
// only push this array if there is at least one entry
|
||||||
|
if (uncatTocBody.length > 0) {
|
||||||
|
catTocBody.push(this.createTocTableDef(uncatTocBody));
|
||||||
|
}
|
||||||
|
|
||||||
|
toc.push(catTocBody);
|
||||||
|
} else {
|
||||||
|
// all motions in the same table
|
||||||
|
const tocBody = motions.map(motion => this.createTocLine(motion, 'tocEntry'));
|
||||||
|
toc.push(this.createTocTableDef(tocBody));
|
||||||
|
}
|
||||||
|
|
||||||
|
return [tocTitle, toc, this.pageBreak];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates the table definition for the TOC
|
||||||
|
*
|
||||||
|
* @param tocBody the body of the table
|
||||||
|
* @returns The table of contents as doc definition
|
||||||
|
*/
|
||||||
|
private createTocTableDef(tocBody: object): object {
|
||||||
|
return {
|
||||||
|
table: {
|
||||||
|
widths: ['auto', '*', 'auto'],
|
||||||
|
body: tocBody
|
||||||
|
},
|
||||||
|
layout: 'noBorders',
|
||||||
|
style: 'tocCategorySection'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a line in the TOC as list object
|
||||||
|
*
|
||||||
|
* @param motion motion to make a toc entry to
|
||||||
|
* @param style the desired style
|
||||||
|
*/
|
||||||
|
private createTocLine(motion: ViewMotion, style: string): object {
|
||||||
|
const firstColumn = motion.identifier;
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
text: firstColumn,
|
||||||
|
style: style
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: motion.title,
|
||||||
|
style: 'tocEntry'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pageReference: `${motion.id}`,
|
||||||
|
style: 'tocEntry',
|
||||||
|
alignment: 'right'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract the used categories from the given motion list.
|
||||||
|
*
|
||||||
|
* @param motions the list of motions
|
||||||
|
* @returns Unique list of categories
|
||||||
|
*/
|
||||||
|
private getUniqueCategories(motions: ViewMotion[]): Category[] {
|
||||||
|
const categories: Category[] = motions
|
||||||
|
// remove motions without category
|
||||||
|
.filter(motion => (motion.category ? motion : null))
|
||||||
|
// map motions their categories
|
||||||
|
.map(motion => motion.category)
|
||||||
|
// remove redundancies
|
||||||
|
.filter(
|
||||||
|
(category, index, self) =>
|
||||||
|
index ===
|
||||||
|
self.findIndex(compare => compare.prefix === category.prefix && compare.name === category.name)
|
||||||
|
);
|
||||||
|
|
||||||
|
return categories;
|
||||||
|
}
|
||||||
|
}
|
@ -5,6 +5,8 @@ import { TranslateService } from '@ngx-translate/core';
|
|||||||
import { MotionPdfService } from './motion-pdf.service';
|
import { MotionPdfService } from './motion-pdf.service';
|
||||||
import { PdfDocumentService } from 'app/core/services/pdf-document.service';
|
import { PdfDocumentService } from 'app/core/services/pdf-document.service';
|
||||||
import { ViewMotion, LineNumberingMode, ChangeRecoMode } from '../models/view-motion';
|
import { ViewMotion, LineNumberingMode, ChangeRecoMode } from '../models/view-motion';
|
||||||
|
import { ConfigService } from 'app/core/services/config.service';
|
||||||
|
import { MotionPdfCatalogService } from './motion-pdf-catalog.service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Export service to handle various kind of exporting necessities.
|
* Export service to handle various kind of exporting necessities.
|
||||||
@ -17,12 +19,15 @@ export class MotionPdfExportService {
|
|||||||
* Constructor
|
* Constructor
|
||||||
*
|
*
|
||||||
* @param translate handle translations
|
* @param translate handle translations
|
||||||
|
* @param configService Read out Config variables
|
||||||
* @param motionPdfService Converting actual motions to PDF
|
* @param motionPdfService Converting actual motions to PDF
|
||||||
* @param pdfDocumentService Actual pdfmake functions and global doc definitions
|
* @param pdfDocumentService Actual pdfmake functions and global doc definitions
|
||||||
*/
|
*/
|
||||||
public constructor(
|
public constructor(
|
||||||
private translate: TranslateService,
|
private translate: TranslateService,
|
||||||
|
private configService: ConfigService,
|
||||||
private motionPdfService: MotionPdfService,
|
private motionPdfService: MotionPdfService,
|
||||||
|
private pdfCatalogService: MotionPdfCatalogService,
|
||||||
private pdfDocumentService: PdfDocumentService
|
private pdfDocumentService: PdfDocumentService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@ -41,4 +46,18 @@ export class MotionPdfExportService {
|
|||||||
};
|
};
|
||||||
this.pdfDocumentService.download(doc, filename, metadata);
|
this.pdfDocumentService.download(doc, filename, metadata);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exports multiple motions to a collection of PDFs
|
||||||
|
*
|
||||||
|
* @param motions
|
||||||
|
*/
|
||||||
|
public exportMotionCatalog(motions: ViewMotion[]): void {
|
||||||
|
const doc = this.pdfCatalogService.motionListToDocDef(motions);
|
||||||
|
const filename = this.translate.instant(this.configService.instant<string>('motions_export_title'));
|
||||||
|
const metadata = {
|
||||||
|
title: filename
|
||||||
|
};
|
||||||
|
this.pdfDocumentService.download(doc, filename, metadata);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -57,7 +57,7 @@ export class MotionPdfService {
|
|||||||
|
|
||||||
// determine the default crMode if not explicitly given
|
// determine the default crMode if not explicitly given
|
||||||
if (!crMode) {
|
if (!crMode) {
|
||||||
lnMode = this.configService.instant('motions_recommendation_text_mode');
|
crMode = this.configService.instant('motions_recommendation_text_mode');
|
||||||
}
|
}
|
||||||
|
|
||||||
const title = this.createTitle(motion);
|
const title = this.createTitle(motion);
|
||||||
|
@ -20,7 +20,7 @@ describe('FullscreenProjectorComponent', () => {
|
|||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
|
|
||||||
fit('should create', () => {
|
it('should create', () => {
|
||||||
expect(component).toBeTruthy();
|
expect(component).toBeTruthy();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user