Skip to content

Commit 14e5967

Browse files
authored
fix: prevent invalid dropdown options to be added (#2493)
* fix: prevent invalid dropdown options to be added fixes #2491
1 parent 6627f7c commit 14e5967

File tree

5 files changed

+161
-11
lines changed

5 files changed

+161
-11
lines changed
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { ConfigurableEnumValue } from "./configurable-enum.interface";
2+
import { ConfigurableEnum } from "./configurable-enum";
3+
4+
describe("ConfigurableEnum", () => {
5+
let sampleValues: ConfigurableEnumValue[];
6+
let testEnum: ConfigurableEnum;
7+
8+
beforeEach(async () => {
9+
sampleValues = [
10+
{ id: "1", label: "one" },
11+
{ id: "2", label: "two" },
12+
];
13+
testEnum = new ConfigurableEnum("test-enum");
14+
testEnum.values = JSON.parse(JSON.stringify(sampleValues));
15+
});
16+
17+
it("should create", () => {
18+
expect(testEnum).toBeTruthy();
19+
});
20+
21+
it("should add option from value object", () => {
22+
const newOption: ConfigurableEnumValue = {
23+
id: "3",
24+
label: "three",
25+
color: "red",
26+
};
27+
const returnedOption = testEnum.addOption(newOption);
28+
expect(returnedOption).toEqual(newOption);
29+
expect(testEnum.values).toContain(newOption);
30+
expect(testEnum.values.length).toBe(sampleValues.length + 1);
31+
});
32+
33+
it("should add option from string", () => {
34+
const newOption: string = "three";
35+
const returnedOption = testEnum.addOption(newOption);
36+
expect(returnedOption.label).toEqual(newOption);
37+
expect(testEnum.values).toContain(
38+
jasmine.objectContaining({ id: "THREE", label: "three" }),
39+
);
40+
expect(testEnum.values.length).toBe(sampleValues.length + 1);
41+
});
42+
43+
it("should not add option for empty values", () => {
44+
testEnum.addOption("");
45+
testEnum.addOption(undefined);
46+
expect(testEnum.values).toEqual(sampleValues);
47+
});
48+
49+
it("should not add option for duplicate label", () => {
50+
expect(() => testEnum.addOption(sampleValues[0].label)).toThrowError();
51+
expect(testEnum.values).toEqual(sampleValues);
52+
});
53+
54+
it("should adapt generated id if duplicate", () => {
55+
const newOption: string = "1"; // already exists as id in sampleValues (but with different label)
56+
const returnedOption = testEnum.addOption(newOption);
57+
58+
expect(returnedOption.label).toEqual(newOption);
59+
expect(returnedOption.id).toEqual("1_");
60+
expect(testEnum.values).toContain(returnedOption);
61+
expect(testEnum.values.length).toBe(sampleValues.length + 1);
62+
});
63+
});

src/app/core/basic-datatypes/configurable-enum/configurable-enum.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,73 @@ import { Entity } from "../../entity/model/entity";
22
import { DatabaseEntity } from "../../entity/database-entity.decorator";
33
import { ConfigurableEnumValue } from "./configurable-enum.interface";
44
import { DatabaseField } from "../../entity/database-field.decorator";
5+
import { Logging } from "../../logging/logging.service";
56

67
@DatabaseEntity("ConfigurableEnum")
78
export class ConfigurableEnum extends Entity {
89
@DatabaseField() values: ConfigurableEnumValue[] = [];
10+
911
constructor(id?: string, values: ConfigurableEnumValue[] = []) {
1012
super(id);
1113
this.values = values;
1214
}
15+
16+
/**
17+
* Add a new valid option to the enum values, if it is not a duplicate or invalid.
18+
* Returns the newly added option upon success.
19+
* @param newOptionInput String or option object to be added
20+
*/
21+
addOption(
22+
newOptionInput: ConfigurableEnumValue | string,
23+
): ConfigurableEnumValue | undefined {
24+
const option: ConfigurableEnumValue =
25+
typeof newOptionInput === "string"
26+
? this.convertStringToOption(newOptionInput)
27+
: newOptionInput;
28+
29+
if (!option || !(option?.id && option?.label)) {
30+
Logging.debug(
31+
"Trying to add invalid enum option",
32+
newOptionInput,
33+
option,
34+
);
35+
return;
36+
}
37+
38+
// check for duplicates
39+
if (this.values.some((v) => v.label === option.label)) {
40+
throw new DuplicateEnumOptionException(newOptionInput);
41+
}
42+
if (this.values.some((v) => v.id === option.id)) {
43+
option.id = option.id + "_";
44+
}
45+
46+
this.values.push(option);
47+
return option;
48+
}
49+
50+
private convertStringToOption(
51+
newOption: string,
52+
): ConfigurableEnumValue | undefined {
53+
newOption = newOption.trim();
54+
if (newOption.length === 0) {
55+
return;
56+
}
57+
58+
return {
59+
id: newOption.toUpperCase(),
60+
label: newOption,
61+
};
62+
}
63+
}
64+
65+
/**
66+
* Error thrown when trying to add an option that already exists in the enum values.
67+
*/
68+
export class DuplicateEnumOptionException extends Error {
69+
constructor(newOptionInput) {
70+
super("Enum Option already exists");
71+
72+
this["newOptionInput"] = newOptionInput;
73+
}
1374
}

src/app/core/basic-datatypes/configurable-enum/configure-enum-popup/configure-enum-popup.component.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { MatButtonModule } from "@angular/material/button";
2323
import { ConfirmationDialogService } from "../../../common-components/confirmation-dialog/confirmation-dialog.service";
2424
import { EntityRegistry } from "../../../entity/database-entity.decorator";
2525
import { Entity } from "../../../entity/model/entity";
26+
import { OkButton } from "../../../common-components/confirmation-dialog/confirmation-dialog/confirmation-dialog.component";
2627

2728
@Component({
2829
selector: "app-configure-enum-popup",
@@ -126,11 +127,18 @@ export class ConfigureEnumPopupComponent {
126127
);
127128
}
128129

129-
createNewOption() {
130-
this.enumEntity.values.push({
131-
id: this.newOptionInput,
132-
label: this.newOptionInput,
133-
});
130+
async createNewOption() {
131+
try {
132+
this.enumEntity.addOption(this.newOptionInput);
133+
} catch (error) {
134+
await this.confirmationService.getConfirmation(
135+
$localize`Failed to create new option`,
136+
$localize`Couldn't create this new option. Please check if the value already exists.`,
137+
OkButton,
138+
);
139+
return;
140+
}
141+
134142
this.newOptionInput = "";
135143
}
136144
}

src/app/core/basic-datatypes/configurable-enum/enum-dropdown/enum-dropdown.component.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,10 +97,10 @@ describe("EnumDropdownComponent", () => {
9797
const res = await component.createNewOption("second");
9898

9999
expect(confirmationSpy).toHaveBeenCalled();
100-
expect(res).toEqual({ id: "second", label: "second" });
100+
expect(res).toEqual({ id: "SECOND", label: "second" });
101101
expect(enumEntity.values).toEqual([
102102
{ id: "1", label: "first" },
103-
{ id: "second", label: "second" },
103+
{ id: "SECOND", label: "second" },
104104
]);
105105
expect(saveSpy).toHaveBeenCalledWith(enumEntity);
106106
});

src/app/core/basic-datatypes/configurable-enum/enum-dropdown/enum-dropdown.component.ts

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { FontAwesomeModule } from "@fortawesome/angular-fontawesome";
1515
import { ErrorHintComponent } from "../../../common-components/error-hint/error-hint.component";
1616
import { MatButtonModule } from "@angular/material/button";
1717
import { ConfirmationDialogService } from "../../../common-components/confirmation-dialog/confirmation-dialog.service";
18+
import { OkButton } from "../../../common-components/confirmation-dialog/confirmation-dialog/confirmation-dialog.component";
1819

1920
@Component({
2021
selector: "app-enum-dropdown",
@@ -80,18 +81,35 @@ export class EnumDropdownComponent implements OnChanges {
8081
}
8182

8283
private async addNewOption(name: string) {
84+
const prevValues = JSON.stringify(this.enumEntity.values);
85+
let addedOption: ConfigurableEnumValue;
86+
87+
try {
88+
addedOption = this.enumEntity.addOption(name);
89+
} catch (error) {
90+
await this.confirmation.getConfirmation(
91+
$localize`Failed to create new option`,
92+
$localize`Couldn't create this new option. Please check if the value already exists.`,
93+
OkButton,
94+
);
95+
return undefined;
96+
}
97+
98+
if (!addedOption) {
99+
return undefined;
100+
}
101+
83102
const userConfirmed = await this.confirmation.getConfirmation(
84103
$localize`Create new option`,
85-
$localize`Do you want to create the new option "${name}"?`,
104+
$localize`Do you want to create the new option "${addedOption.label}"?`,
86105
);
87106
if (!userConfirmed) {
107+
this.enumEntity.values = JSON.parse(prevValues);
88108
return undefined;
89109
}
90110

91-
const option = { id: name, label: name };
92-
this.enumEntity.values.push(option);
93111
await this.entityMapper.save(this.enumEntity);
94-
return option;
112+
return addedOption;
95113
}
96114

97115
openSettings(event: Event) {

0 commit comments

Comments
 (0)