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:
parent
55c162809c
commit
a973ad1719
@ -43,6 +43,7 @@
|
||||
"@tinymce/tinymce-angular": "^3.0.0",
|
||||
"core-js": "^2.6.5",
|
||||
"css-element-queries": "^1.1.1",
|
||||
"exceljs": "1.8.0",
|
||||
"file-saver": "^2.0.1",
|
||||
"hammerjs": "^2.0.8",
|
||||
"material-icon-font": "git+https://github.com/petergng/materialIconFont.git",
|
||||
@ -84,7 +85,7 @@
|
||||
"terser": "3.16.1",
|
||||
"ts-node": "~8.0.2",
|
||||
"tslint": "~5.12.1",
|
||||
"tsutils": "^3.8.0",
|
||||
"tsutils": "3.8.0",
|
||||
"typescript": "~3.2.0",
|
||||
"webpack-bundle-analyzer": "^3.0.4"
|
||||
}
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -8,6 +8,7 @@
|
||||
<mat-button-toggle-group class="smaller-buttons" formControlName="format">
|
||||
<mat-button-toggle value="pdf">PDF</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>
|
||||
</div>
|
||||
|
||||
@ -52,7 +53,7 @@
|
||||
<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="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>
|
||||
</mat-button-toggle>
|
||||
<mat-button-toggle value="id"><span translate>Sequential number</span></mat-button-toggle>
|
||||
|
@ -113,7 +113,17 @@ export class MotionExportDialogComponent implements OnInit {
|
||||
*/
|
||||
public ngOnInit(): void {
|
||||
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"
|
||||
this.exportForm.get('lnMode').setValue(this.lnMode.None);
|
||||
this.exportForm.get('lnMode').disable();
|
||||
@ -140,7 +150,7 @@ export class MotionExportDialogComponent implements OnInit {
|
||||
// remove the selection of "votingResult"
|
||||
let metaInfoVal: string[] = this.exportForm.get('metaInfo').value;
|
||||
metaInfoVal = metaInfoVal.filter(info => {
|
||||
return info !== 'votingResult';
|
||||
return info !== 'polls';
|
||||
});
|
||||
this.exportForm.get('metaInfo').setValue(metaInfoVal);
|
||||
|
||||
|
@ -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 { MotionPdfExportService } from 'app/site/motions/services/motion-pdf-export.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 { StorageService } from 'app/core/core-services/storage.service';
|
||||
|
||||
@ -106,7 +107,8 @@ export class MotionListComponent extends ListViewBaseComponent<ViewMotion, Motio
|
||||
private dialog: MatDialog,
|
||||
private vp: ViewportService,
|
||||
public multiselectService: MotionMultiselectService,
|
||||
public perms: LocalPermissionsService
|
||||
public perms: LocalPermissionsService,
|
||||
private motionXlsxExport: MotionXlsxExportService
|
||||
) {
|
||||
super(titleService, translate, matSnackBar, route, storage, filterService, sortService);
|
||||
|
||||
@ -223,6 +225,8 @@ export class MotionListComponent extends ListViewBaseComponent<ViewMotion, Motio
|
||||
result.content,
|
||||
result.metaInfo
|
||||
);
|
||||
} else if (result.format === 'xlsx') {
|
||||
this.motionXlsxExport.exportMotionList(this.dataSource.filteredData, result.metaInfo);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
@ -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'));
|
||||
}
|
||||
}
|
@ -10,6 +10,9 @@
|
||||
"experimentalDecorators": true,
|
||||
"target": "es5",
|
||||
"typeRoots": ["node_modules/@types"],
|
||||
"lib": ["es2017", "dom"]
|
||||
"lib": ["es2017", "dom"],
|
||||
"paths": {
|
||||
"exceljs": ["../node_modules/exceljs/dist/exceljs.min"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user