Merge pull request #3931 from MaximilianKrambach/os3/export

CSV export (example implementation in user list)
This commit is contained in:
Emanuel Schütze 2018-11-02 20:09:34 +01:00 committed by GitHub
commit f48410024e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 216 additions and 3 deletions

View File

@ -29,6 +29,7 @@
"@ngx-translate/core": "^10.0.2",
"@ngx-translate/http-loader": "^3.0.1",
"core-js": "^2.5.4",
"file-saver": "^2.0.0-rc.3",
"material-design-icons": "^3.0.1",
"ngx-mat-select-search": "^1.4.0",
"roboto-fontface": "^0.10.0",

View File

@ -54,6 +54,7 @@ export class AppComponent {
* TODO: Overloading can be extended to more functions.
*/
private overloadArrayToString(): void {
Array.prototype.toString = function(): string {
let string = '';
const iterations = Math.min(this.length, 3);

View File

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

View File

@ -0,0 +1,119 @@
import { BaseViewModel } from '../../site/base/base-view-model';
import { Injectable } from '@angular/core';
import { FileExportService } from './file-export.service';
import { TranslateService } from '@ngx-translate/core';
@Injectable({
providedIn: 'root'
})
export class CsvExportService {
/**
* Constructor
*/
public constructor(protected exporter: FileExportService, private translate: TranslateService) {}
/**
* Saves an array of model data to a CSV.
* @param data Array of Model instances to be saved
* @param columns Column definitions
* @param filename name of the resulting file
* @param options optional:
* lineSeparator (defaults to \r\n windows style line separator),
* columnseparator defaults to semicolon (other usual separators are ',' '\T' (tab), ' 'whitespace)
*/
public export<T extends BaseViewModel>(
data: T[],
columns: {
property: keyof T; // name of the property used for export
label?: string;
assemble?: string; // (if property is further child object, the property of these to be used)
}[],
filename: string,
{ lineSeparator = '\r\n', columnSeparator = ';' }: { lineSeparator?: string; columnSeparator?: string } = {}
): void {
const allLines = []; // Array of arrays of entries
const usedColumns = []; // mapped properties to be included
// initial array of usable text separators. The first character not used
// in any text data or as column separator will be used as text separator
let tsList = ['"', "'", '`', '/', '\\', ';', '.'];
if (lineSeparator === columnSeparator) {
throw new Error('lineseparator and columnseparator must differ from each other');
}
tsList = this.checkCsvTextSafety(lineSeparator, tsList);
tsList = this.checkCsvTextSafety(columnSeparator, tsList);
// create header data
const header = [];
columns.forEach(column => {
const rawLabel: string = column.label ? column.label : (column.property as string);
const colLabel = this.capitalizeTranslate(rawLabel);
tsList = this.checkCsvTextSafety(colLabel, tsList);
header.push(colLabel);
usedColumns.push(column.property);
});
allLines.push(header);
// create lines
data.forEach(item => {
const line = [];
for (let i = 0; i < usedColumns.length; i++ ){
const property = usedColumns[i];
let prop: any = item[property];
if (columns[i].assemble){
prop = item[property].map(subitem => this.translate.instant(subitem[columns[i].assemble])).join(',');
}
tsList = this.checkCsvTextSafety(prop, tsList);
line.push(prop);
};
allLines.push(line);
});
// assemble lines, putting text separator in place
if (!tsList.length) {
throw new Error('no usable text separator left for valid csv text');
}
const allLinesAssembled = [];
allLines.forEach(line => {
const assembledLine = [];
line.forEach(item => {
if (typeof item === 'number') {
assembledLine.push(item.toString(10));
} else if (item === null || item === undefined || item === '') {
assembledLine.push('');
} else if (item === true) {
assembledLine.push('1');
} else if (item === false) {
assembledLine.push('0');
} else {
assembledLine.push(tsList[0] + item + tsList[0]);
}
});
allLinesAssembled.push(assembledLine.join(columnSeparator));
});
this.exporter.saveFile(allLinesAssembled.join(lineSeparator), filename);
}
/**
* Checks if a given input contains any of the characters defined in a list
* used for textseparators. The list is then returned without the 'special'
* characters, as they may not be used as text separator in this csv.
* @param input any input to be sent to CSV
* @param tsList The list of special characters to check.
*/
public checkCsvTextSafety(input: any, tsList: string[]): string[] {
if (input === null || input === undefined ) {
return tsList;
}
const inputAsString = String(input);
return tsList.filter(char => inputAsString.indexOf(char) < 0);
}
private capitalizeTranslate(input: string): string {
const temp = input.charAt(0).toUpperCase() + input.slice(1);
return this.translate.instant(temp);
}
}

View File

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

View File

@ -0,0 +1,33 @@
import { Injectable } from '@angular/core';
import { saveAs } from 'file-saver';
@Injectable({
providedIn: 'root'
})
export class FileExportService {
/**
* Constructor
*/
public constructor() {}
/**
* Saves a file
* @param file
* @param filename
*/
public saveFile(file: BlobPart, filename: string): void {
const blob = new Blob([file]);
saveAs(blob, filename, { autoBOM: true });
// autoBOM = automatic byte-order-mark
}
/**
* Validates a file name for characters that might break.
* @param filename A name used to save a file
*/
protected validateFileName(filename: string): boolean {
// everything not containing \/?%*:|"<> and not ending on a dot
const pattern = new RegExp(/^[^\\\/\?%\*:\|\"\<\>]*[^\.]+$/i);
return pattern.test(filename);
}
}

View File

@ -75,8 +75,8 @@
<span translate>Import ...</span>
</button>
<button mat-menu-item>
<button mat-menu-item (click)="csvExportUserList()">
<mat-icon>archive</mat-icon>
<span translate>Export ...</span>
<span translate>Export as csv</span>
</button>
</mat-menu>

View File

@ -1,6 +1,7 @@
import { Component, OnInit } from '@angular/core';
import { Title } from '@angular/platform-browser';
import { TranslateService } from '@ngx-translate/core';
import { CsvExportService } from '../../../../core/services/csv-export.service';
import { ViewUser } from '../../models/view-user';
import { UserRepositoryService } from '../../services/user-repository.service';
@ -17,6 +18,7 @@ import { Router, ActivatedRoute } from '@angular/router';
styleUrls: ['./user-list.component.scss']
})
export class UserListComponent extends ListViewBaseComponent<ViewUser> implements OnInit {
/**
* The usual constructor for components
* @param repo the user repository
@ -28,7 +30,8 @@ export class UserListComponent extends ListViewBaseComponent<ViewUser> implement
protected titleService: Title,
protected translate: TranslateService,
private router: Router,
private route: ActivatedRoute
private route: ActivatedRoute,
protected csvExport: CsvExportService
) {
super(titleService, translate);
}
@ -69,4 +72,26 @@ export class UserListComponent extends ListViewBaseComponent<ViewUser> implement
public onPlusButton(): void {
this.router.navigate(['./new'], { relativeTo: this.route });
}
// TODO save all data from the dataSource
public csvExportUserList(): void {
this.csvExport.export(
this.dataSource.data,
[
{ property: 'title' },
{ property: 'first_name', label: 'First Name' },
{ property: 'last_name', label: 'Last Name' },
{ property: 'structure_level', label: 'Structure Level' },
{ property: 'participant_number', label: 'Participant Number' },
{ property: 'groups', assemble: 'name'},
{ property: 'comment' },
{ property: 'is_active', label: 'Active' },
{ property: 'is_present', label: 'Presence' },
{ property: 'is_committee', label: 'Committee' },
{ property: 'default_password', label: 'Default password' },
{ property: 'email', label: 'E-Mail' }
],
'export.csv'
);
}
}