Merge pull request #4554 from tsiegleauq/simple-excel-export
Export motions as excel document (.xlsx)
This commit is contained in:
commit
492372d81c
@ -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"
|
||||||
}
|
}
|
||||||
|
@ -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-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>
|
||||||
|
@ -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);
|
||||||
|
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -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,
|
"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"]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user