Export motions as excel document (.xlsx)

Adds exceljs library.
Extends the "motion export dialog" to support xlsx export with a specific set of rules similar
to CSV
This commit is contained in:
Sean Engelhardt 2019-03-26 17:32:37 +01:00
parent 55c162809c
commit a973ad1719
9 changed files with 206 additions and 6 deletions

View File

@ -43,6 +43,7 @@
"@tinymce/tinymce-angular": "^3.0.0", "@tinymce/tinymce-angular": "^3.0.0",
"core-js": "^2.6.5", "core-js": "^2.6.5",
"css-element-queries": "^1.1.1", "css-element-queries": "^1.1.1",
"exceljs": "1.8.0",
"file-saver": "^2.0.1", "file-saver": "^2.0.1",
"hammerjs": "^2.0.8", "hammerjs": "^2.0.8",
"material-icon-font": "git+https://github.com/petergng/materialIconFont.git", "material-icon-font": "git+https://github.com/petergng/materialIconFont.git",
@ -84,7 +85,7 @@
"terser": "3.16.1", "terser": "3.16.1",
"ts-node": "~8.0.2", "ts-node": "~8.0.2",
"tslint": "~5.12.1", "tslint": "~5.12.1",
"tsutils": "^3.8.0", "tsutils": "3.8.0",
"typescript": "~3.2.0", "typescript": "~3.2.0",
"webpack-bundle-analyzer": "^3.0.4" "webpack-bundle-analyzer": "^3.0.4"
} }

View File

@ -0,0 +1,12 @@
import { TestBed } from '@angular/core/testing';
import { XlsxExportServiceService } from './xlsx-export-service.service';
describe('XlsxExportServiceService', () => {
beforeEach(() => TestBed.configureTestingModule({}));
it('should be created', () => {
const service: XlsxExportServiceService = TestBed.get(XlsxExportServiceService);
expect(service).toBeTruthy();
});
});

View File

@ -0,0 +1,81 @@
import { Injectable } from '@angular/core';
import { Worksheet, Workbook } from 'exceljs';
import { saveAs } from 'file-saver';
@Injectable({
providedIn: 'root'
})
export class XlsxExportServiceService {
/**
* Correction factor for cell width alignment
*/
private PIXELS_PER_EXCEL_WIDTH_UNIT = 7.5;
/**
* Constructor
*/
public constructor() {}
/**
* Saves the given workflow
*
* @param workbook The workflow to export
* @param fileName The filename to save to workflow to
*/
public saveXlsx(workbook: Workbook, fileName: string): void {
workbook.xlsx.writeBuffer().then(blobData => {
const blob = new Blob([blobData as BlobPart], {
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
});
saveAs(blob, `${fileName}.xlsx`);
});
}
/**
* helper function to automatically resize the columns of the given Worksheet.
* Will manipulate the parameter.
* TODO: Upstream ExcelJS issue for auto column width:
* https://github.com/exceljs/exceljs/issues/83
*
* @param sheet The sheet to resize
* @param fromRow the row number to start detecting the size
*/
public autoSize(sheet: Worksheet, fromRow: number): void {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
if (!ctx) {
return;
}
const maxColumnLengths: Array<number> = [];
sheet.eachRow((row, rowNum) => {
if (rowNum < fromRow) {
return;
}
row.eachCell((cell, num) => {
if (typeof cell.value === 'string') {
if (maxColumnLengths[num] === undefined) {
maxColumnLengths[num] = 0;
}
const fontSize = cell.font && cell.font.size ? cell.font.size : 11;
ctx.font = `${fontSize}pt Arial`;
const metrics = ctx.measureText(cell.value);
const cellWidth = metrics.width;
maxColumnLengths[num] = Math.max(maxColumnLengths[num], cellWidth);
}
});
});
for (let i = 1; i <= sheet.columnCount; i++) {
const col = sheet.getColumn(i);
const width = maxColumnLengths[i];
if (width) {
col.width = width / this.PIXELS_PER_EXCEL_WIDTH_UNIT + 1;
}
}
}
}

View File

@ -8,6 +8,7 @@
<mat-button-toggle-group class="smaller-buttons" formControlName="format"> <mat-button-toggle-group class="smaller-buttons" formControlName="format">
<mat-button-toggle value="pdf">PDF</mat-button-toggle> <mat-button-toggle value="pdf">PDF</mat-button-toggle>
<mat-button-toggle value="csv">CSV</mat-button-toggle> <mat-button-toggle value="csv">CSV</mat-button-toggle>
<mat-button-toggle value="xlsx">XLSX</mat-button-toggle>
</mat-button-toggle-group> </mat-button-toggle-group>
</div> </div>
@ -52,7 +53,7 @@
<mat-button-toggle value="tags"> <span translate>Tags</span> </mat-button-toggle> <mat-button-toggle value="tags"> <span translate>Tags</span> </mat-button-toggle>
<mat-button-toggle value="origin"> <span translate>Origin</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="block"> <span translate>Motion block</span> </mat-button-toggle>
<mat-button-toggle value="poll" #votingResultButton> <mat-button-toggle value="polls" #votingResultButton>
<span translate>Voting result</span> <span translate>Voting result</span>
</mat-button-toggle> </mat-button-toggle>
<mat-button-toggle value="id"><span translate>Sequential number</span></mat-button-toggle> <mat-button-toggle value="id"><span translate>Sequential number</span></mat-button-toggle>

View File

@ -113,7 +113,17 @@ export class MotionExportDialogComponent implements OnInit {
*/ */
public ngOnInit(): void { public ngOnInit(): void {
this.exportForm.get('format').valueChanges.subscribe((value: string) => { this.exportForm.get('format').valueChanges.subscribe((value: string) => {
if (value === 'csv') { // disable content for xslx
if (value === 'xlsx') {
// disable the content selection
this.exportForm.get('content').disable();
// remove the selection of "content"
this.exportForm.get('content').setValue(null);
} else {
this.exportForm.get('content').enable();
}
if (value === 'csv' || value === 'xlsx') {
// disable and deselect "lnMode" // disable and deselect "lnMode"
this.exportForm.get('lnMode').setValue(this.lnMode.None); this.exportForm.get('lnMode').setValue(this.lnMode.None);
this.exportForm.get('lnMode').disable(); this.exportForm.get('lnMode').disable();
@ -140,7 +150,7 @@ export class MotionExportDialogComponent implements OnInit {
// remove the selection of "votingResult" // remove the selection of "votingResult"
let metaInfoVal: string[] = this.exportForm.get('metaInfo').value; let metaInfoVal: string[] = this.exportForm.get('metaInfo').value;
metaInfoVal = metaInfoVal.filter(info => { metaInfoVal = metaInfoVal.filter(info => {
return info !== 'votingResult'; return info !== 'polls';
}); });
this.exportForm.get('metaInfo').setValue(metaInfoVal); this.exportForm.get('metaInfo').setValue(metaInfoVal);

View File

@ -27,6 +27,7 @@ import { MotionFilterListService } from 'app/site/motions/services/motion-filter
import { MotionCsvExportService } from 'app/site/motions/services/motion-csv-export.service'; import { MotionCsvExportService } from 'app/site/motions/services/motion-csv-export.service';
import { MotionPdfExportService } from 'app/site/motions/services/motion-pdf-export.service'; import { MotionPdfExportService } from 'app/site/motions/services/motion-pdf-export.service';
import { MotionMultiselectService } from 'app/site/motions/services/motion-multiselect.service'; import { MotionMultiselectService } from 'app/site/motions/services/motion-multiselect.service';
import { MotionXlsxExportService } from 'app/site/motions/services/motion-xlsx-export.service';
import { LocalPermissionsService } from 'app/site/motions/services/local-permissions.service'; import { LocalPermissionsService } from 'app/site/motions/services/local-permissions.service';
import { StorageService } from 'app/core/core-services/storage.service'; import { StorageService } from 'app/core/core-services/storage.service';
@ -106,7 +107,8 @@ export class MotionListComponent extends ListViewBaseComponent<ViewMotion, Motio
private dialog: MatDialog, private dialog: MatDialog,
private vp: ViewportService, private vp: ViewportService,
public multiselectService: MotionMultiselectService, public multiselectService: MotionMultiselectService,
public perms: LocalPermissionsService public perms: LocalPermissionsService,
private motionXlsxExport: MotionXlsxExportService
) { ) {
super(titleService, translate, matSnackBar, route, storage, filterService, sortService); super(titleService, translate, matSnackBar, route, storage, filterService, sortService);
@ -223,6 +225,8 @@ export class MotionListComponent extends ListViewBaseComponent<ViewMotion, Motio
result.content, result.content,
result.metaInfo result.metaInfo
); );
} else if (result.format === 'xlsx') {
this.motionXlsxExport.exportMotionList(this.dataSource.filteredData, result.metaInfo);
} }
} }
}); });

View File

@ -0,0 +1,17 @@
import { TestBed } from '@angular/core/testing';
import { MotionXlsxExportService } from './motion-xlsx-export.service';
import { E2EImportsModule } from 'e2e-imports.module';
describe('MotionXlsxExportService', () => {
beforeEach(() =>
TestBed.configureTestingModule({
imports: [E2EImportsModule]
})
);
it('should be created', () => {
const service: MotionXlsxExportService = TestBed.get(MotionXlsxExportService);
expect(service).toBeTruthy();
});
});

View File

@ -0,0 +1,71 @@
import { Injectable } from '@angular/core';
import { Workbook } from 'exceljs';
import { InfoToExport } from './motion-pdf.service';
import { ViewMotion } from '../models/view-motion';
import { XlsxExportServiceService } from 'app/core/ui-services/xlsx-export-service.service';
import { TranslateService } from '@ngx-translate/core';
/**
* Service to export motion elements to XLSX
*/
@Injectable({
providedIn: 'root'
})
export class MotionXlsxExportService {
/**
* Constructor
*/
public constructor(private xlsx: XlsxExportServiceService, private translate: TranslateService) {}
/**
* Export motions as XLSX
*
* @param motions
* @param contentToExport
* @param infoToExport
*/
public exportMotionList(motions: ViewMotion[], infoToExport: InfoToExport[]): void {
const workbook = new Workbook();
const worksheet = workbook.addWorksheet(this.translate.instant('Motions'));
const properties = ['identifier', 'title'].concat(infoToExport);
// if the ID was exported as well, shift it to the first position
if (properties[properties.length - 1] === 'id') {
properties.unshift(properties.pop());
}
worksheet.columns = properties.map(property => {
return {
header: this.translate.instant(property.charAt(0).toLocaleUpperCase() + property.slice(1))
};
});
// style the header row
worksheet.getRow(1).font = {
underline: true,
bold: true
};
// map motion data to properties
const motionData = motions.map(motion =>
properties.map(property => {
const motionProp = motion[property];
if (motionProp) {
return this.translate.instant(motionProp.toString());
} else {
return null;
}
})
);
// add to sheet
for (const motion of motionData) {
worksheet.addRow(motion);
}
this.xlsx.autoSize(worksheet, 0);
this.xlsx.saveXlsx(workbook, this.translate.instant('Motions'));
}
}

View File

@ -10,6 +10,9 @@
"experimentalDecorators": true, "experimentalDecorators": true,
"target": "es5", "target": "es5",
"typeRoots": ["node_modules/@types"], "typeRoots": ["node_modules/@types"],
"lib": ["es2017", "dom"] "lib": ["es2017", "dom"],
"paths": {
"exceljs": ["../node_modules/exceljs/dist/exceljs.min"]
}
} }
} }