diff --git a/src/app/core/core-components.ts b/src/app/core/core-components.ts
index 25b48d3dc0..15f45c8211 100644
--- a/src/app/core/core-components.ts
+++ b/src/app/core/core-components.ts
@@ -201,7 +201,6 @@ export const coreComponents: ComponentTuple[] = [
(c) => c.MarkdownPageComponent
),
],
-
[
"DisplayEntity",
() =>
@@ -209,4 +208,11 @@ export const coreComponents: ComponentTuple[] = [
"./entity-components/entity-select/display-entity/display-entity.component"
).then((c) => c.DisplayEntityComponent),
],
+ [
+ "RelatedEntities",
+ () =>
+ import(
+ "./entity-components/entity-details/related-entities/related-entities.component"
+ ).then((c) => c.RelatedEntitiesComponent),
+ ],
];
diff --git a/src/app/core/entity-components/entity-details/related-entities/related-entities.component.html b/src/app/core/entity-components/entity-details/related-entities/related-entities.component.html
new file mode 100644
index 0000000000..a65beb6f3b
--- /dev/null
+++ b/src/app/core/entity-components/entity-details/related-entities/related-entities.component.html
@@ -0,0 +1,6 @@
+
diff --git a/src/app/core/entity-components/entity-details/related-entities/related-entities.component.spec.ts b/src/app/core/entity-components/entity-details/related-entities/related-entities.component.spec.ts
new file mode 100644
index 0000000000..d8d06e006e
--- /dev/null
+++ b/src/app/core/entity-components/entity-details/related-entities/related-entities.component.spec.ts
@@ -0,0 +1,80 @@
+import { ComponentFixture, TestBed } from "@angular/core/testing";
+
+import { RelatedEntitiesComponent } from "./related-entities.component";
+import { MockedTestingModule } from "../../../../utils/mocked-testing.module";
+import { EntityMapperService } from "../../../entity/entity-mapper.service";
+import { Child } from "../../../../child-dev-project/children/model/child";
+import { ChildSchoolRelation } from "../../../../child-dev-project/children/model/childSchoolRelation";
+
+describe("RelatedEntitiesComponent", () => {
+ let component: RelatedEntitiesComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [RelatedEntitiesComponent, MockedTestingModule.withState()],
+ }).compileComponents();
+
+ fixture = TestBed.createComponent(RelatedEntitiesComponent);
+ component = fixture.componentInstance;
+ await component.onInitFromDynamicConfig({
+ entity: new Child(),
+ config: {
+ entity: ChildSchoolRelation.ENTITY_TYPE,
+ property: "childId",
+ columns: [],
+ },
+ });
+ fixture.detectChanges();
+ });
+
+ it("should create", () => {
+ expect(component).toBeTruthy();
+ });
+
+ it("should load the entities which are linked with the passed one", async () => {
+ const c1 = new Child();
+ const c2 = new Child();
+ const r1 = new ChildSchoolRelation();
+ r1.childId = c1.getId();
+ const r2 = new ChildSchoolRelation();
+ r2.childId = c1.getId();
+ const r3 = new ChildSchoolRelation();
+ r3.childId = c2.getId();
+ const entityMapper = TestBed.inject(EntityMapperService);
+ await entityMapper.saveAll([c1, c2, r1, r2, r3]);
+ const columns = ["start", "end", "schoolId"];
+ const filter = { start: { $exists: true } } as any;
+
+ await component.onInitFromDynamicConfig({
+ entity: c1,
+ config: {
+ entity: ChildSchoolRelation.ENTITY_TYPE,
+ property: "childId",
+ columns,
+ filter,
+ },
+ });
+
+ expect(component.columns).toBe(columns);
+ expect(component.data).toEqual([r1, r2, r3]);
+ expect(component.filter).toEqual({ ...filter, childId: c1.getId() });
+ });
+
+ it("should create a new entity that references the related one", async () => {
+ const related = new Child();
+ await component.onInitFromDynamicConfig({
+ entity: related,
+ config: {
+ entity: ChildSchoolRelation.ENTITY_TYPE,
+ property: "childId",
+ columns: [],
+ },
+ });
+
+ const newEntity = component.createNewRecordFactory()();
+
+ expect(newEntity instanceof ChildSchoolRelation).toBeTrue();
+ expect(newEntity["childId"]).toBe(related.getId());
+ });
+});
diff --git a/src/app/core/entity-components/entity-details/related-entities/related-entities.component.ts b/src/app/core/entity-components/entity-details/related-entities/related-entities.component.ts
new file mode 100644
index 0000000000..38bb1a1faf
--- /dev/null
+++ b/src/app/core/entity-components/entity-details/related-entities/related-entities.component.ts
@@ -0,0 +1,69 @@
+import { Component } from "@angular/core";
+import { DynamicComponent } from "../../../view/dynamic-components/dynamic-component.decorator";
+import { OnInitDynamicComponent } from "../../../view/dynamic-components/on-init-dynamic-component.interface";
+import { PanelConfig } from "../EntityDetailsConfig";
+import { EntityMapperService } from "../../../entity/entity-mapper.service";
+import { Entity, EntityConstructor } from "../../../entity/model/entity";
+import {
+ ColumnConfig,
+ DataFilter,
+} from "../../entity-subrecord/entity-subrecord/entity-subrecord-config";
+import { EntityRegistry } from "../../../entity/database-entity.decorator";
+import { isArrayProperty } from "../../entity-utils/entity-utils";
+import { EntitySubrecordComponent } from "../../entity-subrecord/entity-subrecord/entity-subrecord.component";
+
+@DynamicComponent("RelatedEntities")
+@Component({
+ selector: "app-related-entities",
+ templateUrl: "./related-entities.component.html",
+ standalone: true,
+ imports: [EntitySubrecordComponent],
+})
+export class RelatedEntitiesComponent implements OnInitDynamicComponent {
+ data: Entity[] = [];
+ columns: ColumnConfig[] = [];
+ filter: DataFilter;
+ relatedEntity: Entity;
+ private entityType: EntityConstructor;
+ private property;
+ private isArray = false;
+
+ constructor(
+ private entityMapper: EntityMapperService,
+ private entities: EntityRegistry
+ ) {}
+
+ async onInitFromDynamicConfig(
+ config: PanelConfig<{
+ entity: string;
+ property: string;
+ columns: ColumnConfig[];
+ filter?: DataFilter;
+ }>
+ ) {
+ this.relatedEntity = config.entity;
+ this.entityType = this.entities.get(config.config.entity);
+ this.property = config.config.property;
+ this.isArray = isArrayProperty(this.entityType, this.property);
+
+ this.data = await this.entityMapper.loadType(this.entityType);
+ this.filter = {
+ ...config.config.filter,
+ [this.property]: this.isArray
+ ? { $elemMatch: { $eq: this.relatedEntity.getId() } }
+ : this.relatedEntity.getId(),
+ };
+ this.columns = config.config.columns;
+ }
+
+ createNewRecordFactory() {
+ // TODO has a similar purpose like FilterService.alignEntityWithFilter
+ return () => {
+ const rec = new this.entityType();
+ rec[this.property] = this.isArray
+ ? [this.relatedEntity.getId()]
+ : this.relatedEntity.getId();
+ return rec;
+ };
+ }
+}
diff --git a/src/app/core/entity-components/entity-utils/entity-utils.ts b/src/app/core/entity-components/entity-utils/entity-utils.ts
new file mode 100644
index 0000000000..45a91ac075
--- /dev/null
+++ b/src/app/core/entity-components/entity-utils/entity-utils.ts
@@ -0,0 +1,14 @@
+import { arrayEntitySchemaDatatype } from "../../entity/schema-datatypes/datatype-array";
+import { entityArrayEntitySchemaDatatype } from "../../entity/schema-datatypes/datatype-entity-array";
+import { EntityConstructor } from "../../entity/model/entity";
+
+export function isArrayProperty(
+ entity: EntityConstructor,
+ property: string
+): boolean {
+ const dataType = entity.schema.get(property).dataType;
+ return (
+ dataType === arrayEntitySchemaDatatype.name ||
+ dataType === entityArrayEntitySchemaDatatype.name
+ );
+}
diff --git a/src/app/core/entity/database-indexing/database-indexing.service.ts b/src/app/core/entity/database-indexing/database-indexing.service.ts
index 6fc6c4176e..477d0345a1 100644
--- a/src/app/core/entity/database-indexing/database-indexing.service.ts
+++ b/src/app/core/entity/database-indexing/database-indexing.service.ts
@@ -22,8 +22,7 @@ import { BackgroundProcessState } from "../../sync-status/background-process-sta
import { Entity, EntityConstructor } from "../model/entity";
import { EntitySchemaService } from "../schema/entity-schema.service";
import { first } from "rxjs/operators";
-import { arrayEntitySchemaDatatype } from "../schema-datatypes/datatype-array";
-import { entityArrayEntitySchemaDatatype } from "../schema-datatypes/datatype-entity-array";
+import { isArrayProperty } from "../../entity-components/entity-utils/entity-utils";
/**
* Manage database query index creation and use, working as a facade in front of the Database service.
@@ -115,10 +114,6 @@ export class DatabaseIndexingService {
return `emit(${primaryParam});`;
}
};
- const dataType = entity.schema.get(referenceProperty).dataType;
- const isArrayProperty =
- dataType === arrayEntitySchemaDatatype.name ||
- dataType === entityArrayEntitySchemaDatatype.name;
const simpleEmit = emitParamFormatter("doc." + referenceProperty);
const arrayEmit = `
@@ -134,7 +129,11 @@ export class DatabaseIndexingService {
map: `(doc) => {
if (!doc._id.startsWith("${entity.ENTITY_TYPE}")) return;
- ${isArrayProperty ? arrayEmit : simpleEmit}
+ ${
+ isArrayProperty(entity, referenceProperty)
+ ? arrayEmit
+ : simpleEmit
+ }
}`,
},
},
diff --git a/src/app/core/entity/entity-config.service.spec.ts b/src/app/core/entity/entity-config.service.spec.ts
index 947a78e8cc..4a70f6dfd4 100644
--- a/src/app/core/entity/entity-config.service.spec.ts
+++ b/src/app/core/entity/entity-config.service.spec.ts
@@ -61,18 +61,6 @@ describe("EntityConfigService", () => {
expect(result).toBe(config);
});
- it("throws an error when trying to setting the entities up from config and they are not registered", () => {
- const configWithInvalidEntities: (EntityConfig & { _id: string })[] = [
- {
- _id: "entity:IDoNotExist",
- attributes: [],
- },
- ];
- mockConfigService.getAllConfigs.and.returnValue(configWithInvalidEntities);
-
- expect(() => service.setupEntitiesFromConfig()).toThrowError();
- });
-
it("appends custom definitions for each entity from the config", () => {
const ATTRIBUTE_1_NAME = "test1Attribute";
const ATTRIBUTE_2_NAME = "test2Attribute";
@@ -134,6 +122,55 @@ describe("EntityConfigService", () => {
expect(Test.icon).toBe("users");
expect(Test.color).toBe("red");
});
+
+ it("should create a new subclass with the schema of the extended", () => {
+ const schema = { dataType: "string", label: "Dynamic Property" };
+ mockConfigService.getAllConfigs.and.returnValue([
+ {
+ _id: "entity:DynamicTest",
+ label: "Dynamic Test Entity",
+ extends: "Test",
+ attributes: [{ name: "dynamicProperty", schema }],
+ },
+ ]);
+
+ service.setupEntitiesFromConfig();
+
+ const dynamicEntity = entityRegistry.get("DynamicTest");
+ expect(dynamicEntity.ENTITY_TYPE).toBe("DynamicTest");
+ expect([...dynamicEntity.schema.entries()]).toEqual(
+ jasmine.arrayContaining([...Test.schema.entries()])
+ );
+ expect(dynamicEntity.schema.get("dynamicProperty")).toBe(schema);
+ const dynamicInstance = new dynamicEntity("someId");
+ expect(dynamicInstance instanceof Test).toBeTrue();
+ expect(dynamicInstance.getId(true)).toBe("DynamicTest:someId");
+
+ // it should overwrite anything in the extended entity
+ expect(Test.schema.has("dynamicProperty")).toBeFalse();
+ const parentInstance = new Test("otherId");
+ expect(parentInstance.getId(true)).toBe("Test:otherId");
+ });
+
+ it("should subclass entity if no extension is specified", () => {
+ mockConfigService.getAllConfigs.and.returnValue([
+ {
+ _id: "entity:NoExtends",
+ label: "DynamicTest",
+ attributes: [],
+ },
+ ]);
+
+ service.setupEntitiesFromConfig();
+
+ const dynamicEntity = entityRegistry.get("NoExtends");
+ expect([...dynamicEntity.schema.entries()]).toEqual([
+ ...Entity.schema.entries(),
+ ]);
+ const dynamicInstance = new dynamicEntity("someId");
+ expect(dynamicInstance instanceof Entity).toBeTrue();
+ expect(dynamicInstance.getId(true)).toBe("NoExtends:someId");
+ });
});
@DatabaseEntity("Test")
diff --git a/src/app/core/entity/entity-config.service.ts b/src/app/core/entity/entity-config.service.ts
index 432c69c930..c986909737 100644
--- a/src/app/core/entity/entity-config.service.ts
+++ b/src/app/core/entity/entity-config.service.ts
@@ -37,11 +37,27 @@ export class EntityConfigService {
EntityConfig & { _id: string }
>(ENTITY_CONFIG_PREFIX)) {
const id = config._id.substring(ENTITY_CONFIG_PREFIX.length);
+ if (!this.entities.has(id)) {
+ this.createNewEntity(id, config.extends);
+ }
const ctor = this.entities.get(id);
this.addConfigAttributes(ctor, config);
}
}
+ private createNewEntity(id: string, parent: string) {
+ const parentClass = this.entities.has(parent)
+ ? this.entities.get(parent)
+ : Entity;
+
+ class DynamicClass extends parentClass {
+ static schema = new Map(parentClass.schema.entries());
+ static ENTITY_TYPE = id;
+ }
+
+ this.entities.set(id, DynamicClass);
+ }
+
/**
* Appends the given (dynamic) attributes to the schema of the provided Entity.
* If no arguments are provided, they will be loaded from the config
@@ -135,4 +151,9 @@ export interface EntityConfig {
* base route of views for this entity type
*/
route?: string;
+
+ /**
+ * when a new entity is created, all properties from this class will also be available
+ */
+ extends?: string;
}