= (new (
+ id?: string
+) => T) &
typeof Entity;
export const ENTITY_CONFIG_PREFIX = "entity:";
diff --git a/src/app/core/form-dialog/form-dialog-wrapper/form-dialog-wrapper.component.spec.ts b/src/app/core/form-dialog/form-dialog-wrapper/form-dialog-wrapper.component.spec.ts
index 78c77dbb58..e87a5e8473 100644
--- a/src/app/core/form-dialog/form-dialog-wrapper/form-dialog-wrapper.component.spec.ts
+++ b/src/app/core/form-dialog/form-dialog-wrapper/form-dialog-wrapper.component.spec.ts
@@ -9,6 +9,8 @@ import { MatDialogRef } from "@angular/material/dialog";
import { Subject } from "rxjs";
import { MatSnackBarModule } from "@angular/material/snack-bar";
import { MockSessionModule } from "../../session/mock-session.module";
+import { DynamicEntityService } from "../../entity/dynamic-entity.service";
+import { EntitySchemaService } from "../../entity/schema/entity-schema.service";
describe("FormDialogWrapperComponent", () => {
let component: FormDialogWrapperComponent;
@@ -26,7 +28,11 @@ describe("FormDialogWrapperComponent", () => {
MatSnackBarModule,
MockSessionModule.withState(),
],
- providers: [{ provide: MatDialogRef, useValue: {} }],
+ providers: [
+ { provide: MatDialogRef, useValue: {} },
+ DynamicEntityService,
+ EntitySchemaService,
+ ],
}).compileComponents();
saveEntitySpy = spyOn(TestBed.inject(EntityMapperService), "save");
diff --git a/src/app/core/session/login/login.component.html b/src/app/core/session/login/login.component.html
index 23fd388477..8e507ed2c7 100644
--- a/src/app/core/session/login/login.component.html
+++ b/src/app/core/session/login/login.component.html
@@ -27,6 +27,7 @@
diff --git a/src/app/core/session/login/login.component.spec.ts b/src/app/core/session/login/login.component.spec.ts
index 24407ea696..b4b48d97b6 100644
--- a/src/app/core/session/login/login.component.spec.ts
+++ b/src/app/core/session/login/login.component.spec.ts
@@ -109,6 +109,15 @@ describe("LoginComponent", () => {
expect(component.errorMessage).toBeTruthy();
}));
+ it("should focus the first input element on initialization", fakeAsync(() => {
+ component.ngAfterViewInit();
+ tick();
+ fixture.detectChanges();
+
+ const firstInputElement = document.getElementsByTagName("input")[0];
+ expect(document.activeElement).toBe(firstInputElement);
+ }));
+
function expectErrorMessageOnState(loginState: LoginState) {
mockSessionService.login.and.resolveTo(loginState);
expect(component.errorMessage).toBeFalsy();
diff --git a/src/app/core/session/login/login.component.ts b/src/app/core/session/login/login.component.ts
index c14b8e4871..dde8387bb6 100644
--- a/src/app/core/session/login/login.component.ts
+++ b/src/app/core/session/login/login.component.ts
@@ -15,7 +15,7 @@
* along with ndb-core. If not, see .
*/
-import { Component } from "@angular/core";
+import { AfterViewInit, Component, ElementRef, ViewChild } from "@angular/core";
import { SessionService } from "../session-service/session.service";
import { LoginState } from "../session-states/login-state.enum";
import { ActivatedRoute, Router } from "@angular/router";
@@ -29,7 +29,7 @@ import { LoggingService } from "../../logging/logging.service";
templateUrl: "./login.component.html",
styleUrls: ["./login.component.scss"],
})
-export class LoginComponent {
+export class LoginComponent implements AfterViewInit {
/** true while a login is started but result is not received yet */
loginInProgress = false;
@@ -42,6 +42,8 @@ export class LoginComponent {
/** errorMessage displayed in form */
errorMessage: string;
+ @ViewChild("usernameInput") usernameInput: ElementRef;
+
constructor(
private _sessionService: SessionService,
private loggingService: LoggingService,
@@ -49,6 +51,10 @@ export class LoginComponent {
private route: ActivatedRoute
) {}
+ ngAfterViewInit(): void {
+ setTimeout(() => this.usernameInput?.nativeElement.focus());
+ }
+
/**
* Do a login with the SessionService.
*/
diff --git a/src/app/core/session/mock-session.module.ts b/src/app/core/session/mock-session.module.ts
index 7ecb266907..5f365cc2df 100644
--- a/src/app/core/session/mock-session.module.ts
+++ b/src/app/core/session/mock-session.module.ts
@@ -12,6 +12,7 @@ import { AnalyticsService } from "../analytics/analytics.service";
import { NoopAnimationsModule } from "@angular/platform-browser/animations";
import { Angulartics2Module } from "angulartics2";
import { RouterTestingModule } from "@angular/router/testing";
+import { Entity } from "../entity/model/entity";
export const TEST_USER = "test";
export const TEST_PASSWORD = "pass";
@@ -36,9 +37,10 @@ export const TEST_PASSWORD = "pass";
})
export class MockSessionModule {
static withState(
- loginState = LoginState.LOGGED_IN
+ loginState = LoginState.LOGGED_IN,
+ data: Entity[] = []
): ModuleWithProviders {
- const mockedEntityMapper = mockEntityMapper([new User(TEST_USER)]);
+ const mockedEntityMapper = mockEntityMapper([new User(TEST_USER), ...data]);
return {
ngModule: MockSessionModule,
providers: [
diff --git a/src/app/core/translation/translation.service.ts b/src/app/core/translation/translation.service.ts
index eab7decf84..12799625ef 100644
--- a/src/app/core/translation/translation.service.ts
+++ b/src/app/core/translation/translation.service.ts
@@ -21,6 +21,7 @@ export class TranslationService {
this.availableLocales = [
{ locale: "de", regionCode: "de" },
{ locale: "en-US", regionCode: "us" },
+ { locale: "fr", regionCode: "fr" },
];
}
diff --git a/src/app/core/ui/primary-action/primary-action.component.spec.ts b/src/app/core/ui/primary-action/primary-action.component.spec.ts
index 117ef104a7..0ed5517138 100644
--- a/src/app/core/ui/primary-action/primary-action.component.spec.ts
+++ b/src/app/core/ui/primary-action/primary-action.component.spec.ts
@@ -7,6 +7,8 @@ import { FormDialogModule } from "../../form-dialog/form-dialog.module";
import { PermissionsModule } from "../../permissions/permissions.module";
import { MockSessionModule } from "../../session/mock-session.module";
import { FontAwesomeTestingModule } from "@fortawesome/angular-fontawesome/testing";
+import { DynamicEntityService } from "../../entity/dynamic-entity.service";
+import { EntitySchemaService } from "../../entity/schema/entity-schema.service";
describe("PrimaryActionComponent", () => {
let component: PrimaryActionComponent;
@@ -23,6 +25,7 @@ describe("PrimaryActionComponent", () => {
FontAwesomeTestingModule,
MockSessionModule.withState(),
],
+ providers: [DynamicEntityService, EntitySchemaService],
}).compileComponents();
});
diff --git a/src/app/core/ui/search/search.component.spec.ts b/src/app/core/ui/search/search.component.spec.ts
index 4ec78aa878..fbed06f921 100644
--- a/src/app/core/ui/search/search.component.spec.ts
+++ b/src/app/core/ui/search/search.component.spec.ts
@@ -18,6 +18,8 @@ import { DatabaseIndexingService } from "../../entity/database-indexing/database
import { EntityUtilsModule } from "../../entity-components/entity-utils/entity-utils.module";
import { Subscription } from "rxjs";
import { Entity } from "../../entity/model/entity";
+import { DynamicEntityService } from "../../entity/dynamic-entity.service";
+import { EntityMapperService } from "../../entity/entity-mapper.service";
describe("SearchComponent", () => {
let component: SearchComponent;
@@ -52,6 +54,8 @@ describe("SearchComponent", () => {
providers: [
{ provide: EntitySchemaService, useValue: entitySchemaService },
{ provide: DatabaseIndexingService, useValue: mockIndexService },
+ { provide: EntityMapperService, useValue: {} },
+ DynamicEntityService,
],
declarations: [SearchComponent],
}).compileComponents();
diff --git a/src/app/core/ui/search/search.component.ts b/src/app/core/ui/search/search.component.ts
index 5d944583b2..0428c95010 100644
--- a/src/app/core/ui/search/search.component.ts
+++ b/src/app/core/ui/search/search.component.ts
@@ -1,13 +1,12 @@
import { Component } from "@angular/core";
-import { Entity, EntityConstructor } from "../../entity/model/entity";
-import { EntitySchemaService } from "../../entity/schema/entity-schema.service";
+import { Entity } from "../../entity/model/entity";
import { Observable } from "rxjs";
import { concatMap, debounceTime, skipUntil, tap } from "rxjs/operators";
import { DatabaseIndexingService } from "../../entity/database-indexing/database-indexing.service";
import { Router } from "@angular/router";
-import { ENTITY_MAP } from "../../entity-components/entity-details/entity-details.component";
import { fromPromise } from "rxjs/internal-compatibility";
import { FormControl } from "@angular/forms";
+import { DynamicEntityService } from "../../entity/dynamic-entity.service";
/**
* General search box that provides results out of any kind of entities from the system
@@ -39,8 +38,8 @@ export class SearchComponent {
constructor(
private indexingService: DatabaseIndexingService,
- private entitySchemaService: EntitySchemaService,
- private router: Router
+ private router: Router,
+ private dynamicEntityService: DynamicEntityService
) {
this.results = this.formControl.valueChanges.pipe(
debounceTime(this.INPUT_DEBOUNCE_TIME_MS),
@@ -157,10 +156,10 @@ export class SearchComponent {
id: string;
doc: object;
}): Entity {
- const ctor: EntityConstructor =
- ENTITY_MAP.get(Entity.extractTypeFromId(doc.id)) || Entity;
- const entity = new ctor(doc.id);
- this.entitySchemaService.loadDataIntoEntity(entity, doc.doc);
- return entity;
+ return this.dynamicEntityService.instantiateEntity(
+ Entity.extractTypeFromId(doc.id),
+ doc.id,
+ doc.doc
+ );
}
}
diff --git a/src/app/features/data-import/data-import.service.spec.ts b/src/app/features/data-import/data-import.service.spec.ts
index 7ec4769026..4c5e14b7ac 100644
--- a/src/app/features/data-import/data-import.service.spec.ts
+++ b/src/app/features/data-import/data-import.service.spec.ts
@@ -7,6 +7,8 @@ import { ConfirmationDialogService } from "../../core/confirmation-dialog/confir
import { MatSnackBar, MatSnackBarRef } from "@angular/material/snack-bar";
import { MatDialogRef } from "@angular/material/dialog";
import { of } from "rxjs";
+import { EntityMapperService } from "../../core/entity/entity-mapper.service";
+import { EntitySchemaService } from "../../core/entity/schema/entity-schema.service";
describe("DataImportService", () => {
let db: PouchDatabase;
@@ -85,10 +87,11 @@ describe("DataImportService", () => {
provide: MatSnackBar,
useValue: mockSnackBar,
},
+ EntityMapperService,
+ EntitySchemaService,
],
});
service = TestBed.inject(DataImportService);
- spyOn(service, "importCsvContentToDB");
spyOn(db, "put");
});
@@ -105,6 +108,7 @@ describe("DataImportService", () => {
mockBackupService.getJsonExport.and.resolveTo(null);
createDialogMock(true);
createSnackBarMock(false);
+ spyOn(service, "importCsvContentToDB");
service.handleCsvImport(null);
@@ -120,6 +124,7 @@ describe("DataImportService", () => {
const mockFileReader = createFileReaderMock();
mockBackupService.getJsonExport.and.resolveTo(null);
createDialogMock(false);
+ spyOn(service, "importCsvContentToDB");
service.handleCsvImport(null);
@@ -149,7 +154,16 @@ describe("DataImportService", () => {
flush();
}));
- it("should put csv into db", async () => {
- // Todo, missing importCsv Function
+ it("should import csv file and generate searchIndices", async () => {
+ const csvString = "_id,name,projectNumber\n" + 'Child:1,"John Doe",123';
+
+ await service.importCsvContentToDB(csvString);
+
+ expect(db.put).toHaveBeenCalledWith(
+ jasmine.objectContaining({
+ searchIndices: ["John", "Doe", 123],
+ }),
+ jasmine.anything()
+ );
});
});
diff --git a/src/app/features/data-import/data-import.service.ts b/src/app/features/data-import/data-import.service.ts
index 971f1c5b56..63a6bb895f 100644
--- a/src/app/features/data-import/data-import.service.ts
+++ b/src/app/features/data-import/data-import.service.ts
@@ -6,6 +6,7 @@ import { BackupService } from "../../core/admin/services/backup.service";
import { ConfirmationDialogService } from "../../core/confirmation-dialog/confirmation-dialog.service";
import { MatSnackBar } from "@angular/material/snack-bar";
import { readFile } from "../../utils/utils";
+import { DynamicEntityService } from "../../core/entity/dynamic-entity.service";
@Injectable()
@UntilDestroy()
@@ -15,7 +16,8 @@ export class DataImportService {
private papa: Papa,
private backupService: BackupService,
private confirmationDialog: ConfirmationDialogService,
- private snackBar: MatSnackBar
+ private snackBar: MatSnackBar,
+ private dynamicEntityService: DynamicEntityService
) {}
async importCsvContentToDB(csv: string): Promise {
@@ -33,6 +35,14 @@ export class DataImportService {
}
}
+ if (record["_id"] !== undefined) {
+ const entityType = record["_id"].split(":")[0];
+ const ctor = this.dynamicEntityService.getEntityConstructor(entityType);
+ record["searchIndices"] = Object.assign(
+ new ctor(),
+ record
+ ).searchIndices;
+ }
await this.db.put(record, true);
}
}
diff --git a/src/app/features/data-import/data-import/data-import.component.html b/src/app/features/data-import/data-import/data-import.component.html
index be9982043a..8a03d1c983 100644
--- a/src/app/features/data-import/data-import/data-import.component.html
+++ b/src/app/features/data-import/data-import/data-import.component.html
@@ -9,5 +9,5 @@
#csvImport
type="file"
style="display: none"
- (change)="importCsvFile($event.target.files[0])"
+ (change)="importCsvFile($event)"
/>
diff --git a/src/app/features/data-import/data-import/data-import.component.spec.ts b/src/app/features/data-import/data-import/data-import.component.spec.ts
index b420b73e54..5017615f66 100644
--- a/src/app/features/data-import/data-import/data-import.component.spec.ts
+++ b/src/app/features/data-import/data-import/data-import.component.spec.ts
@@ -36,7 +36,7 @@ describe("DataImportComponent", () => {
});
it("should call handleCsvImport() in DataImportService", () => {
- component.importCsvFile(mockCsvFile);
+ component.importCsvFile({ target: { files: [mockCsvFile] } } as any);
expect(mockDataImportService.handleCsvImport).toHaveBeenCalledWith(
mockCsvFile
);
diff --git a/src/app/features/data-import/data-import/data-import.component.ts b/src/app/features/data-import/data-import/data-import.component.ts
index a07ed36e5c..7647e9da92 100644
--- a/src/app/features/data-import/data-import/data-import.component.ts
+++ b/src/app/features/data-import/data-import/data-import.component.ts
@@ -12,7 +12,8 @@ import { DataImportService } from "../data-import.service";
export class DataImportComponent {
constructor(private dataImportService: DataImportService) {}
- importCsvFile(file: Blob): void {
- this.dataImportService.handleCsvImport(file);
+ importCsvFile(inputEvent: Event): void {
+ const target = inputEvent.target as HTMLInputElement;
+ this.dataImportService.handleCsvImport(target.files[0]);
}
}
diff --git a/src/app/features/reporting/reporting/reporting.component.ts b/src/app/features/reporting/reporting/reporting.component.ts
index 6dd0780fb6..aa12c686ea 100644
--- a/src/app/features/reporting/reporting/reporting.component.ts
+++ b/src/app/features/reporting/reporting/reporting.component.ts
@@ -37,7 +37,7 @@ export class ReportingComponent implements OnInit {
ngOnInit() {
this.activatedRoute.data.subscribe(
(data: RouteData) => {
- this.availableReports = data.config.reports;
+ this.availableReports = data.config?.reports;
if (this.availableReports?.length === 1) {
this.selectedReport = this.availableReports[0];
}
diff --git a/src/app/utils/utils.spec.ts b/src/app/utils/utils.spec.ts
index 8fae3607f6..bf53f1ed9f 100644
--- a/src/app/utils/utils.spec.ts
+++ b/src/app/utils/utils.spec.ts
@@ -44,4 +44,22 @@ describe("Utils", () => {
expect(sortedDesc).toEqual([forth, third, second, first]);
});
+
+ it("should sort undefined last", () => {
+ const first = { number: 1 };
+ const second = { number: 10 };
+ const third = { number: undefined };
+
+ const sorted = [second, third, first].sort(
+ sortByAttribute("number", "asc")
+ );
+
+ expect(sorted).toEqual([first, second, third]);
+
+ const sortedDesc = [second, third, first].sort(
+ sortByAttribute("number", "desc")
+ );
+
+ expect(sortedDesc).toEqual([third, second, first]);
+ });
});
diff --git a/src/app/utils/utils.ts b/src/app/utils/utils.ts
index 54845811f0..0eadadfc49 100644
--- a/src/app/utils/utils.ts
+++ b/src/app/utils/utils.ts
@@ -53,8 +53,8 @@ export function calculateAge(dateOfBirth: Date): number {
return age;
}
-export function sortByAttribute