Skip to content

Commit

Permalink
feat(export): export only human-readable fields and use better headers (
Browse files Browse the repository at this point in the history
#2033)

closes #1918

---------
Co-authored-by: Sebastian Leidig <sebastian@aam-digital.com>
  • Loading branch information
brajesh-lab authored Oct 31, 2023
1 parent ecfcab0 commit 58cd3a9
Show file tree
Hide file tree
Showing 2 changed files with 113 additions and 11 deletions.
66 changes: 61 additions & 5 deletions src/app/core/export/download-service/download.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ describe("DownloadService", () => {
// reset createElement otherwise results in: 'an Error was thrown after all'
document.createElement = oldCreateElement;
});

it("should contain a column for every property", async () => {
const docs = [
{ _id: "Test:1", test: 1 },
Expand Down Expand Up @@ -77,11 +78,12 @@ describe("DownloadService", () => {
'"_id","_rev","propOne","propTwo"' +
DownloadService.SEPARATOR_ROW +
'"TestForCsvEntity:1","2","first","second"';
spyOn(service, "exportFile").and.returnValue(expected);
const result = await service.createCsv([test]);
expect(result).toEqual(expected);
});

it("should transform object properties to their label for export", async () => {
it("should transform object values to their label for export when available (e.g. configurable-enum)", async () => {
const testEnumValue: ConfigurableEnumValue = {
id: "ID VALUE",
label: "label value",
Expand All @@ -90,9 +92,10 @@ describe("DownloadService", () => {

@DatabaseEntity("TestEntity")
class TestEntity extends Entity {
@DatabaseField() enumProperty: ConfigurableEnumValue;
@DatabaseField() dateProperty: Date;
@DatabaseField() boolProperty: boolean;
@DatabaseField({ label: "test enum" })
enumProperty: ConfigurableEnumValue;
@DatabaseField({ label: "test date" }) dateProperty: Date;
@DatabaseField({ label: "test boolean" }) boolProperty: boolean;
}

const testEntity = new TestEntity();
Expand All @@ -105,12 +108,65 @@ describe("DownloadService", () => {
const rows = csvExport.split(DownloadService.SEPARATOR_ROW);
expect(rows).toHaveSize(1 + 1); // includes 1 header line
const columnValues = rows[1].split(DownloadService.SEPARATOR_COL);
expect(columnValues).toHaveSize(3 + 1); // Properties + _id
expect(columnValues).toHaveSize(3); // Properties (_id is filter out by default)
expect(columnValues).toContain('"' + testEnumValue.label + '"');
expect(columnValues).toContain('"' + testDate + '"');
expect(columnValues).toContain('"true"');
});

it("should export all properties using object keys as headers, if no schema is available", async () => {
const docs = [
{ _id: "Test:1", name: "Child 1" },
{ _id: "Test:2", name: "Child 2" },
];

const csvExport = await service.createCsv(docs);

const rows = csvExport.split(DownloadService.SEPARATOR_ROW);
expect(rows).toHaveSize(2 + 1); // includes 1 header line
const columnHeaders = rows[0].split(DownloadService.SEPARATOR_COL);
const columnValues = rows[1].split(DownloadService.SEPARATOR_COL);

expect(columnValues).toHaveSize(2);
expect(columnHeaders).toHaveSize(2);
expect(columnHeaders).toContain('"_id"');
});

it("should only export columns that have labels defined in entity schema and use the schema labels as export headers", async () => {
const testString: string = "Test 1";

@DatabaseEntity("LabelTestEntity")
class LabelTestEntity extends Entity {
@DatabaseField({ label: "test string" }) stringProperty: string;
@DatabaseField({ label: "test date" }) otherProperty: string;
@DatabaseField() boolProperty: boolean;
}

const labelTestEntity = new LabelTestEntity();
labelTestEntity.stringProperty = testString;
labelTestEntity.otherProperty = "x";
labelTestEntity.boolProperty = true;

const incompleteTestEntity = new LabelTestEntity();
incompleteTestEntity.otherProperty = "second row";

const csvExport = await service.createCsv([
labelTestEntity,
incompleteTestEntity,
]);

const rows = csvExport.split(DownloadService.SEPARATOR_ROW);
expect(rows).toHaveSize(1 + 2); // includes 1 header line

const columnHeaders = rows[0].split(DownloadService.SEPARATOR_COL);
expect(columnHeaders).toHaveSize(2);
expect(columnHeaders).toContain('"test string"');
expect(columnHeaders).toContain('"test date"');

const entity2Values = rows.find((r) => r.includes("second row"));
expect(entity2Values).toEqual(',"second row"'); // first column empty!
});

it("should export a date as YYYY-MM-dd only", async () => {
const dateString = "2021-01-01";
const dateObject = moment(dateString).toDate();
Expand Down
58 changes: 52 additions & 6 deletions src/app/core/export/download-service/download.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { LoggingService } from "../../logging/logging.service";
import { DataTransformationService } from "../data-transformation-service/data-transformation.service";
import { transformToReadableFormat } from "../../common-components/entity-subrecord/entity-subrecord/value-accessor";
import { Papa } from "ngx-papaparse";
import { EntitySchemaField } from "app/core/entity/schema/entity-schema-field";

/**
* This service allows to start a download process from the browser.
Expand Down Expand Up @@ -90,17 +91,62 @@ export class DownloadService {
* @returns string a valid CSV string of the input data
*/
async createCsv(data: any[]): Promise<string> {
// Collect all properties because papa only uses the properties of the first object
let entityConstructor: any;

if (data.length > 0 && typeof data[0]?.getConstructor === "function") {
entityConstructor = data[0].getConstructor();
}
const keys = new Set<string>();
data.forEach((row) => Object.keys(row).forEach((key) => keys.add(key)));

data = data.map(transformToReadableFormat);

return this.papa.unparse(data, {
quotes: true,
header: true,
newline: DownloadService.SEPARATOR_ROW,
columns: [...keys],
if (!entityConstructor) {
return this.papa.unparse(data, {
quotes: true,
header: true,
newline: DownloadService.SEPARATOR_ROW,
columns: [...keys],
});
}

const result = this.exportFile(data, entityConstructor);
return result;
}

exportFile(data: any[], entityConstructor: { schema: any }) {
const entitySchema = entityConstructor.schema;
const columnLabels = new Map<string, EntitySchemaField>();

entitySchema.forEach((value: { label: EntitySchemaField }, key: string) => {
if (value.label) columnLabels.set(key, value.label);
});

const exportEntities = data.map((item) => {
let newItem = {};
for (const key in item) {
if (columnLabels.has(key)) {
newItem[key] = item[key];
}
}
return newItem;
});

const columnKeys: string[] = Array.from(columnLabels.keys());
const labels: any[] = Array.from(columnLabels.values());
const orderedData: any[] = exportEntities.map((item) =>
columnKeys.map((key) => item[key]),
);

return this.papa.unparse(
{
fields: labels,
data: orderedData,
},
{
quotes: true,
newline: DownloadService.SEPARATOR_ROW,
},
);
}
}

0 comments on commit 58cd3a9

Please sign in to comment.