Skip to content

Commit

Permalink
feat: create a new entity from entity-select fields
Browse files Browse the repository at this point in the history
closes #2213
  • Loading branch information
sleidig committed Mar 14, 2024
1 parent b5c84d7 commit 8b1d7d1
Show file tree
Hide file tree
Showing 8 changed files with 121 additions and 16 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ <h2 mat-dialog-title i18n>Configure Field "{{ entitySchemaField.label }}"</h2>
[options]="typeAdditionalOptions"
[optionToString]="objectToLabel"
[valueMapper]="objectToValue"
[createOption]="createNewAdditionalOption"
[createOption]="createNewAdditionalOptionAsync"
></app-basic-autocomplete>

<button
Expand Down Expand Up @@ -132,7 +132,7 @@ <h2 mat-dialog-title i18n>Configure Field "{{ entitySchemaField.label }}"</h2>
[options]="typeAdditionalOptions"
[optionToString]="objectToLabel"
[valueMapper]="objectToValue"
[createOption]="createNewAdditionalOption"
[createOption]="createNewAdditionalOptionAsync"
></app-basic-autocomplete>
</mat-form-field>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,8 @@ export class AdminEntityFieldComponent implements OnChanges {
objectToLabel = (v: SimpleDropdownValue) => v?.label;
objectToValue = (v: SimpleDropdownValue) => v?.value;
createNewAdditionalOption: (input: string) => SimpleDropdownValue;
createNewAdditionalOptionAsync = async (input) =>
this.createNewAdditionalOptionAsync(input);

private updateDataTypeAdditional(dataType: string) {
this.resetAdditional();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,26 +34,34 @@ const Template: StoryFn<FormComponent<any>> = (args: FormComponent<any>) => ({
});

const fieldConfig: FormFieldConfig = {
id: "relatedEntity",
label: "related entity",
description: "test tooltip",
};
const fieldMultiConfig: FormFieldConfig = {
id: "relatedEntities",
viewComponent: "DisplayEntityArray",
editComponent: "EditEntity",
label: "test related entities label",
label: "related entities (multi select)",
description: "test tooltip",
};
const otherField: FormFieldConfig = {
id: "x",
viewComponent: "DisplayNumber",
editComponent: "EditNumber",
label: "other label",
};

@DatabaseEntity("TestEntityReferenceArrayEntity")
class TestEntity extends Entity {
@DatabaseField({
dataType: "entity-reference-array",
dataType: "entity-array",
additional: User.ENTITY_TYPE,
})
relatedEntities: string[];

@DatabaseField({
dataType: "entity",
additional: User.ENTITY_TYPE,
})
relatedEntity: string;

@DatabaseField() x: number;
}

Expand All @@ -62,6 +70,8 @@ testEntity.relatedEntities = [testUser.getId()];

export const Primary = Template.bind({});
Primary.args = {
fieldGroups: [{ fields: [otherField, fieldConfig, otherField, otherField] }],
fieldGroups: [
{ fields: [otherField, fieldConfig, fieldMultiConfig, otherField] },
],
entity: testEntity,
};
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,8 @@
[value]="inputElement.value"
>
<em
i18n="
Label for adding an option in a dropdown|e.g. Add option My new Option
"
>Add option</em
i18n="Label for adding an option in a dropdown|e.g. Add new My new Option"
>Add new</em
>
{{ inputElement.value }}
</mat-option>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
[display]="showEntities ? 'chips' : 'none'"
[options]="availableOptions | async"
[placeholder]="(loading | async) ? loadingPlaceholder : placeholder"
[createOption]="createNewEntity"
>
<ng-template let-item>
<app-display-entity
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ import {
tick,
waitForAsync,
} from "@angular/core/testing";
import { EntitySelectComponent } from "./entity-select.component";
import {
applyTextToCreatedEntity,
EntitySelectComponent,
} from "./entity-select.component";
import { Entity } from "../../entity/model/entity";
import { User } from "../../user/user";
import { Child } from "../../../child-dev-project/children/model/child";
Expand All @@ -15,6 +18,8 @@ import { LoginState } from "../../session/session-states/login-state.enum";
import { LoggingService } from "../../logging/logging.service";
import { FormControl } from "@angular/forms";
import { EntityMapperService } from "../../entity/entity-mapper/entity-mapper.service";
import { FormDialogService } from "../../form-dialog/form-dialog.service";
import { of } from "rxjs";

describe("EntitySelectComponent", () => {
let component: EntitySelectComponent<any, any>;
Expand Down Expand Up @@ -168,4 +173,46 @@ describe("EntitySelectComponent", () => {
jasmine.arrayWithExactContents([...testUsers, testChildren[0]]),
);
}));

it("should create a new entity when user selects 'add new' and apply input text", async () => {
component.entityType = [Child.ENTITY_TYPE];
const formDialogSpy = spyOn(
TestBed.inject(FormDialogService),
"openFormPopup",
);
const savedEntity = new Child("123");

formDialogSpy.and.returnValue({ afterClosed: () => of(undefined) } as any);
const resultCancel = await component.createNewEntity("my new record");
expect(resultCancel).toBeUndefined();

formDialogSpy.and.returnValue({
afterClosed: () => of(savedEntity),
} as any);
const resultSave = await component.createNewEntity("my new record");
expect(resultSave).toEqual(savedEntity);
expect(formDialogSpy).toHaveBeenCalledWith(
jasmine.objectContaining({ name: "my new record" }),
);
});

it("should smartly distribute the input text to all toStringAttributes when create a new record", async () => {
class TestEntity extends Entity {
static override toStringAttributes = ["firstName", "lastName"];
}

expect(applyTextToCreatedEntity(new TestEntity(), "one")).toEqual(
jasmine.objectContaining({ firstName: "one" }),
);
expect(applyTextToCreatedEntity(new TestEntity(), "one two")).toEqual(
jasmine.objectContaining({ firstName: "one", lastName: "two" }),
);
expect(applyTextToCreatedEntity(new TestEntity(), "one two")).toEqual(
jasmine.objectContaining({ firstName: "one", lastName: "two" }),
);
// if more input parts than toStringAttributes, put all remaining parts into last property:
expect(applyTextToCreatedEntity(new TestEntity(), "one two three")).toEqual(
jasmine.objectContaining({ firstName: "one", lastName: "two three" }),
);
});
});
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Component, Input } from "@angular/core";
import { Entity } from "../../entity/model/entity";
import { BehaviorSubject } from "rxjs";
import { BehaviorSubject, lastValueFrom } from "rxjs";
import { FormControl, FormsModule, ReactiveFormsModule } from "@angular/forms";
import { MatChipsModule } from "@angular/material/chips";
import { MatAutocompleteModule } from "@angular/material/autocomplete";
Expand All @@ -18,6 +18,8 @@ import { BasicAutocompleteComponent } from "../basic-autocomplete/basic-autocomp
import { MatSlideToggle } from "@angular/material/slide-toggle";
import { asArray } from "../../../utils/utils";
import { LoggingService } from "../../logging/logging.service";
import { FormDialogService } from "../../form-dialog/form-dialog.service";
import { EntityRegistry } from "../../entity/database-entity.decorator";

@Component({
selector: "app-entity-select",
Expand Down Expand Up @@ -111,6 +113,8 @@ export class EntitySelectComponent<
constructor(
private entityMapperService: EntityMapperService,
private logger: LoggingService,
private formDialog: FormDialogService,
private entityRegistry: EntityRegistry,
) {}

/**
Expand Down Expand Up @@ -226,10 +230,53 @@ export class EntitySelectComponent<
(e) => !e.isActive && this.autocompleteFilter(e),
).length;
}

createNewEntity = async (input: string): Promise<E> => {
if (this._entityType?.length < 1) {
return;
}
if (this._entityType.length > 0) {
this.logger.warn(
"EntitySelect with multiple types is always creating a new entity of the first listed type only.",
);
// TODO: maybe display an additional popup asking the user to select which type should be created?
}

const newEntity = new (this.entityRegistry.get(this._entityType[0]))();
applyTextToCreatedEntity(newEntity, input);

const dialogRef = this.formDialog.openFormPopup(newEntity);
return lastValueFrom<E | undefined>(dialogRef.afterClosed());
};
}

function isMulti(
cmp: EntitySelectComponent<any, string | string[]>,
): cmp is EntitySelectComponent<any, string[]> {
return cmp.multi;
}

/**
* Update the given entity by applying the text entered by a user
* to the most likely appropriate entity field, inferred from the toString representation.
*/
export function applyTextToCreatedEntity(entity: Entity, input: string) {
const toStringFields = entity.getConstructor().toStringAttributes;
if (!toStringFields || toStringFields.length < 1) {
return;
}

const inputParts = input.split(/\s+/);
for (let i = 0; i < inputParts.length; i++) {
const targetProperty =
toStringFields[i < toStringFields.length ? i : toStringFields.length - 1];

entity[targetProperty] = (
(entity[targetProperty] ?? "") +
" " +
inputParts[i]
).trim();
}

return entity;
}
2 changes: 1 addition & 1 deletion src/app/core/form-dialog/form-dialog.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export class FormDialogService {
*/
openFormPopup<E extends Entity, T = RowDetailsComponent>(
entity: E,
columnsOverall: ColumnConfig[],
columnsOverall?: ColumnConfig[],
component: ComponentType<T> = RowDetailsComponent as ComponentType<T>,
): MatDialogRef<T> {
if (!columnsOverall) {
Expand Down

0 comments on commit 8b1d7d1

Please sign in to comment.