Merge pull request #3931 from MaximilianKrambach/os3/export
CSV export (example implementation in user list)
This commit is contained in:
commit
f48410024e
@ -29,6 +29,7 @@
|
|||||||
"@ngx-translate/core": "^10.0.2",
|
"@ngx-translate/core": "^10.0.2",
|
||||||
"@ngx-translate/http-loader": "^3.0.1",
|
"@ngx-translate/http-loader": "^3.0.1",
|
||||||
"core-js": "^2.5.4",
|
"core-js": "^2.5.4",
|
||||||
|
"file-saver": "^2.0.0-rc.3",
|
||||||
"material-design-icons": "^3.0.1",
|
"material-design-icons": "^3.0.1",
|
||||||
"ngx-mat-select-search": "^1.4.0",
|
"ngx-mat-select-search": "^1.4.0",
|
||||||
"roboto-fontface": "^0.10.0",
|
"roboto-fontface": "^0.10.0",
|
||||||
|
@ -54,6 +54,7 @@ export class AppComponent {
|
|||||||
* TODO: Overloading can be extended to more functions.
|
* TODO: Overloading can be extended to more functions.
|
||||||
*/
|
*/
|
||||||
private overloadArrayToString(): void {
|
private overloadArrayToString(): void {
|
||||||
|
|
||||||
Array.prototype.toString = function(): string {
|
Array.prototype.toString = function(): string {
|
||||||
let string = '';
|
let string = '';
|
||||||
const iterations = Math.min(this.length, 3);
|
const iterations = Math.min(this.length, 3);
|
||||||
|
17
client/src/app/core/services/csv-export.service.spec.ts
Normal file
17
client/src/app/core/services/csv-export.service.spec.ts
Normal 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();
|
||||||
|
}));
|
||||||
|
});
|
119
client/src/app/core/services/csv-export.service.ts
Normal file
119
client/src/app/core/services/csv-export.service.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
17
client/src/app/core/services/file-export.service.spec.ts
Normal file
17
client/src/app/core/services/file-export.service.spec.ts
Normal 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();
|
||||||
|
}));
|
||||||
|
});
|
33
client/src/app/core/services/file-export.service.ts
Normal file
33
client/src/app/core/services/file-export.service.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
@ -75,8 +75,8 @@
|
|||||||
<span translate>Import ...</span>
|
<span translate>Import ...</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button mat-menu-item>
|
<button mat-menu-item (click)="csvExportUserList()">
|
||||||
<mat-icon>archive</mat-icon>
|
<mat-icon>archive</mat-icon>
|
||||||
<span translate>Export ...</span>
|
<span translate>Export as csv</span>
|
||||||
</button>
|
</button>
|
||||||
</mat-menu>
|
</mat-menu>
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { Component, OnInit } from '@angular/core';
|
import { Component, OnInit } from '@angular/core';
|
||||||
import { Title } from '@angular/platform-browser';
|
import { Title } from '@angular/platform-browser';
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
import { CsvExportService } from '../../../../core/services/csv-export.service';
|
||||||
|
|
||||||
import { ViewUser } from '../../models/view-user';
|
import { ViewUser } from '../../models/view-user';
|
||||||
import { UserRepositoryService } from '../../services/user-repository.service';
|
import { UserRepositoryService } from '../../services/user-repository.service';
|
||||||
@ -17,6 +18,7 @@ import { Router, ActivatedRoute } from '@angular/router';
|
|||||||
styleUrls: ['./user-list.component.scss']
|
styleUrls: ['./user-list.component.scss']
|
||||||
})
|
})
|
||||||
export class UserListComponent extends ListViewBaseComponent<ViewUser> implements OnInit {
|
export class UserListComponent extends ListViewBaseComponent<ViewUser> implements OnInit {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The usual constructor for components
|
* The usual constructor for components
|
||||||
* @param repo the user repository
|
* @param repo the user repository
|
||||||
@ -28,7 +30,8 @@ export class UserListComponent extends ListViewBaseComponent<ViewUser> implement
|
|||||||
protected titleService: Title,
|
protected titleService: Title,
|
||||||
protected translate: TranslateService,
|
protected translate: TranslateService,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
private route: ActivatedRoute
|
private route: ActivatedRoute,
|
||||||
|
protected csvExport: CsvExportService
|
||||||
) {
|
) {
|
||||||
super(titleService, translate);
|
super(titleService, translate);
|
||||||
}
|
}
|
||||||
@ -69,4 +72,26 @@ export class UserListComponent extends ListViewBaseComponent<ViewUser> implement
|
|||||||
public onPlusButton(): void {
|
public onPlusButton(): void {
|
||||||
this.router.navigate(['./new'], { relativeTo: this.route });
|
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'
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user