diff --git a/src/app/core/export/download-service/download.service.spec.ts b/src/app/core/export/download-service/download.service.spec.ts index 9ae04ea0ce..c45574a155 100644 --- a/src/app/core/export/download-service/download.service.spec.ts +++ b/src/app/core/export/download-service/download.service.spec.ts @@ -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 }, @@ -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", @@ -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(); @@ -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(); diff --git a/src/app/core/export/download-service/download.service.ts b/src/app/core/export/download-service/download.service.ts index 147df776db..57fe6180fc 100644 --- a/src/app/core/export/download-service/download.service.ts +++ b/src/app/core/export/download-service/download.service.ts @@ -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. @@ -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, + }, + ); } }