Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Inactive records in entity select #2056

Merged
merged 27 commits into from
Feb 2, 2024
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
cb4e0d9
WIP
christophscheuing Sep 13, 2023
5067130
Cleanup
christophscheuing Oct 5, 2023
ff50dfc
Displaying "None" and number of inactive
christophscheuing Oct 12, 2023
542bc68
Bugfix and moved checkbox to bottom
christophscheuing Oct 19, 2023
6a378a0
WIP: Writing tests
christophscheuing Oct 19, 2023
d677be4
Completed tests
christophscheuing Oct 26, 2023
1dab951
Removed f at fdescribe
christophscheuing Oct 26, 2023
4e65cf0
Fixed tests
christophscheuing Oct 26, 2023
665f62d
Merge remote-tracking branch 'origin/master' into inactive_records_in…
christophscheuing Oct 26, 2023
ac52cc0
Merge branch 'master' into inactive_records_in_entity_select
TheSlimvReal Oct 26, 2023
d63f7f5
Merge branch 'master' into inactive_records_in_entity_select
sleidig Nov 9, 2023
22f980b
undone changes
TheSlimvReal Nov 13, 2023
c224aaf
Added relevantEntities and inactiveFilteredEntities
christophscheuing Nov 23, 2023
33e2e31
Merge remote-tracking branch 'origin/master' into inactive_records_in…
christophscheuing Nov 30, 2023
78f861f
Merge remote-tracking branch 'origin/master' into inactive_records_in…
christophscheuing Nov 30, 2023
0d436bc
Fixed tests – but three of them occasionally still failing
christophscheuing Nov 30, 2023
69e19bd
Fixed tests
christophscheuing Dec 14, 2023
7443150
Merge branch 'master' into inactive_records_in_entity_select
christophscheuing Dec 14, 2023
cb15231
Merge branch 'master' into inactive_records_in_entity_select
sleidig Jan 4, 2024
5a0d0ab
Removed erroneous import in entity-select.component.ts
christophscheuing Jan 4, 2024
4cd9aa7
Further cleaning up and making code more explicit
christophscheuing Jan 18, 2024
1d9e568
discarding non existing IDs from initial selection
christophscheuing Jan 18, 2024
d3686d2
Merge branch 'master' into inactive_records_in_entity_select
christophscheuing Jan 18, 2024
54a7937
Merge branch 'master' into inactive_records_in_entity_select
sleidig Jan 23, 2024
2bbfb42
Merge branch 'master' into inactive_records_in_entity_select
sleidig Feb 2, 2024
c6ac133
code cleanup
sleidig Feb 2, 2024
da14f0c
Merge branch 'master' into inactive_records_in_entity_select
sleidig Feb 2, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,29 @@
(optionSelected)="selectEntity($event.option.value)"
autoActiveFirstOption
>
<ng-content select="mat-option"></ng-content>
<mat-option *ngFor="let res of filteredEntities" [value]="res">
<app-display-entity
[entityToDisplay]="res"
[linkDisabled]="true"
></app-display-entity>
</mat-option>
<mat-option
*ngIf="
filteredEntities.length < 1 &&
!includeInactive &&
inactiveFilteredEntities.length > 0
"
[disabled]="true"
style="display: none"
></mat-option>
christophscheuing marked this conversation as resolved.
Show resolved Hide resolved
<mat-checkbox
*ngIf="inactiveFilteredEntities.length > 0"
labelPosition="after"
(change)="toggleIncludeInactive()"
[checked]="includeInactive"
i18n="Label for checkbox|e.g. include inactive children"
>
Also show {{ inactiveFilteredEntities.length }} inactive
</mat-checkbox>
</mat-autocomplete>
</mat-form-field>
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,26 @@ import { Child } from "../../../child-dev-project/children/model/child";
import { School } from "../../../child-dev-project/schools/model/school";
import { MockedTestingModule } from "../../../utils/mocked-testing.module";
import { LoginState } from "../../session/session-states/login-state.enum";
import { HarnessLoader } from "@angular/cdk/testing";
import { TestbedHarnessEnvironment } from "@angular/cdk/testing/testbed";
import { MatAutocompleteHarness } from "@angular/material/autocomplete/testing";
import { EntityMapperService } from "app/core/entity/entity-mapper/entity-mapper.service";

describe("EntitySelectComponent", () => {
let component: EntitySelectComponent<any>;
let fixture: ComponentFixture<EntitySelectComponent<any>>;

const testUsers: Entity[] = ["Abc", "Bcd", "Abd", "Aba"].map((s) => {
const user = new User();
user.name = s;
return user;
});
const testChildren: Entity[] = [new Child(), new Child()];
const otherEntities: Entity[] = [new School()];
let loader: HarnessLoader;
let testUsers: Entity[];
let testChildren: Entity[];

beforeEach(waitForAsync(() => {
testUsers = ["Abc", "Bcd", "Abd", "Aba"].map((s) => {
const user = new User();
user.name = s;
return user;
});
testChildren = [new Child(), new Child()];
const otherEntities: Entity[] = [new School()];
TestBed.configureTestingModule({
imports: [
EntitySelectComponent,
Expand All @@ -40,6 +46,7 @@ describe("EntitySelectComponent", () => {

beforeEach(() => {
fixture = TestBed.createComponent(EntitySelectComponent);
loader = TestbedHarnessEnvironment.loader(fixture);
component = fixture.componentInstance;
fixture.detectChanges();
});
Expand All @@ -65,6 +72,10 @@ describe("EntitySelectComponent", () => {
component.entityType = User.ENTITY_TYPE;
tick();
fixture.detectChanges();
console.log(
"should suggest ... filteredEntities",
component.filteredEntities,
);
christophscheuing marked this conversation as resolved.
Show resolved Hide resolved
expect(component.filteredEntities.length).toBe(testUsers.length);
}));

Expand Down Expand Up @@ -148,7 +159,10 @@ describe("EntitySelectComponent", () => {
});

it("adds a new entity if it matches a known entity", fakeAsync(() => {
component.entityType = User.ENTITY_TYPE;
component.allEntities = testUsers;
tick();

component.select({ value: testUsers[0]["name"] });
expect(component.selectedEntities).toEqual([testUsers[0]]);
tick();
Expand All @@ -162,36 +176,119 @@ describe("EntitySelectComponent", () => {
}));

it("autocompletes with the default accessor", fakeAsync(() => {
component.entityType = User.ENTITY_TYPE;
tick();
component.allEntities = testUsers;
component.loading.next(false);

component.formControl.setValue(null);
tick();
expect(component.filteredEntities.length).toEqual(4);

component.formControl.setValue("A");
tick();
expect(component.filteredEntities.length).toEqual(3);

component.formControl.setValue("c");
tick();
expect(component.filteredEntities.length).toEqual(2);

component.formControl.setValue("Abc");
tick();
expect(component.filteredEntities.length).toEqual(1);

component.formControl.setValue("z");
tick();
expect(component.filteredEntities.length).toEqual(0);
tick();
}));

it("shows inactive entites according to the includeInactive state", fakeAsync(() => {
testUsers[0].isActive = false;
testUsers[2].isActive = false;
component.entityType = User.ENTITY_TYPE;
tick();
component.allEntities = testUsers;
component.loading.next(false);

component.formControl.setValue(null);
expect(component.filteredEntities.length).toEqual(2);

component.toggleIncludeInactive();
expect(component.filteredEntities.length).toEqual(4);

testUsers[2].isActive = true;
component.toggleIncludeInactive();
expect(component.filteredEntities.length).toEqual(3);
}));

it("shows the autocomplete options and eventually the hidden autocomplete option in case of corresponding inactive entities appropriately", fakeAsync(async () => {
const testEntities = [
"Aaa",
"Aab",
"Aac",
"Baa",
"Bab",
"Bac",
"Caa",
"Cab",
].map((s) => {
const user = new User();
user.name = s;
return user;
});
testEntities[6].isActive = false;
testEntities[7].isActive = false;

const mockEntityMapper = TestBed.inject(EntityMapperService);
spyOn(mockEntityMapper, "loadType").and.resolveTo(testEntities);
component.entityType = User.ENTITY_TYPE;
tick();
component.allEntities = testEntities;

component.loading.next(false);
const autocomplete = await loader.getHarness(MatAutocompleteHarness);
let options;

console.log(
"shows the autocomplete ... allEntities:",
component.allEntities,
);
console.log(
"shows the autocomplete ... RelevantEntities:",
component.relevantEntities,
);

autocomplete.enterText("X");
options = await autocomplete.getOptions();
expect(options.length).toEqual(0);

autocomplete.clear();
autocomplete.enterText("Ba");
options = await autocomplete.getOptions();
expect(options.length).toEqual(3);

autocomplete.clear();
autocomplete.enterText("Ca");
options = await autocomplete.getOptions();
expect(options.length).toEqual(1);

tick();
}));

it("should use the configurable toStringAttributes for comparing values", fakeAsync(() => {
class Person extends Entity {
static toStringAttributes = ["firstname", "lastname"];

firstname: string;
lastname: string;
}

const p1 = Object.assign(new Person(), { firstname: "Aa", lastname: "bb" });
const p2 = Object.assign(new Person(), { firstname: "Aa", lastname: "cc" });
const mockEntityMapper = TestBed.inject(EntityMapperService);
spyOn(mockEntityMapper, "loadType").and.resolveTo([p1, p2]);
component.entityType = Person.ENTITY_TYPE;
tick();
component.allEntities = [p1, p2];
component.loading.next(false);

Expand All @@ -205,9 +302,11 @@ describe("EntitySelectComponent", () => {
}));

it("should add an unselected entity to the filtered entities array", fakeAsync(() => {
component.entityType = User.ENTITY_TYPE;
component.allEntities = testUsers;
const selectedUser = testUsers[1];
tick();

const selectedUser = testUsers[1];
component.selectEntity(selectedUser);
expect(component.filteredEntities).not.toContain(selectedUser);

Expand All @@ -220,8 +319,24 @@ describe("EntitySelectComponent", () => {
component.entityType = [User.ENTITY_TYPE, Child.ENTITY_TYPE];
tick();
fixture.detectChanges();
expect(component.allEntities).toEqual([...testUsers, ...testChildren]);
expect(component.filteredEntities).toEqual([...testUsers, ...testChildren]);
console.log(
"suggests all entities ... allEntities:",
component.allEntities,
);
console.log(
"suggests all entities ... relevantEntities:",
component.relevantEntities,
);
expect(component.allEntities).toEqual(
[...testUsers, ...testChildren].sort((a, b) =>
a.toString().localeCompare(b.toString()),
),
);
expect(component.filteredEntities).toEqual(
[...testUsers, ...testChildren].sort((a, b) =>
a.toString().localeCompare(b.toString()),
),
);
}));

it("should be able to select entities from different types", fakeAsync(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { DisplayEntityComponent } from "../../basic-datatypes/entity/display-ent
import { FontAwesomeModule } from "@fortawesome/angular-fontawesome";
import { MatTooltipModule } from "@angular/material/tooltip";
import { MatInputModule } from "@angular/material/input";
import { MatCheckboxModule } from "@angular/material/checkbox";

@Component({
selector: "app-entity-select",
Expand All @@ -42,6 +43,7 @@ import { MatInputModule } from "@angular/material/input";
FontAwesomeModule,
MatTooltipModule,
MatInputModule,
MatCheckboxModule,
],
standalone: true,
})
Expand All @@ -58,6 +60,9 @@ export class EntitySelectComponent<E extends Entity> implements OnChanges {
*/
@Input() withPrefix: boolean = false;

includeInactive: boolean = false;
filterValue: string;

/**
* The entity-type (e.g. 'Child', 'School', e.t.c.) to set.
* @param type The ENTITY_TYPE of a Entity. This affects the entities which will be loaded and the component
Expand Down Expand Up @@ -89,8 +94,10 @@ export class EntitySelectComponent<E extends Entity> implements OnChanges {
filter((isLoading) => !isLoading),
)
.subscribe((_) => {
this.selectedEntities = this.allEntities.filter((e) =>
sel.find((s) => s === e.getId(true) || s === e.getId()),
this.selectedEntities = sel.map((id) =>
this.allEntities.find(
(s) => id === s.getId(true) || id === s.getId(),
),
christophscheuing marked this conversation as resolved.
Show resolved Hide resolved
);
});
}
Expand Down Expand Up @@ -156,7 +163,10 @@ export class EntitySelectComponent<E extends Entity> implements OnChanges {
inputPlaceholder = this.loadingPlaceholder;

allEntities: E[] = [];
relevantEntities: E[] = [];
christophscheuing marked this conversation as resolved.
Show resolved Hide resolved
filteredEntities: E[] = [];
inactiveFilteredEntities: E[] = [];

formControl = new FormControl("");

@ViewChild("inputField") inputField: ElementRef<HTMLInputElement>;
Expand All @@ -183,6 +193,9 @@ export class EntitySelectComponent<E extends Entity> implements OnChanges {
if (changes.hasOwnProperty("additionalFilter")) {
// update whenever additional filters are being set
this.formControl.setValue(this.formControl.value);
this.relevantEntities = this.allEntities.filter((e) =>
this.additionalFilter(e),
);
}
}

Expand All @@ -203,8 +216,11 @@ export class EntitySelectComponent<E extends Entity> implements OnChanges {
for (const type of types) {
entities.push(...(await this.entityMapperService.loadType<E>(type)));
}

this.allEntities = entities;
this.allEntities.sort((a, b) => a.toString().localeCompare(b.toString()));
this.relevantEntities = this.allEntities.filter((e) =>
this.additionalFilter(e),
);
this.loading.next(false);
this.formControl.setValue(null);
}
Expand All @@ -214,11 +230,13 @@ export class EntitySelectComponent<E extends Entity> implements OnChanges {
* @param entity the entity to select
*/
selectEntity(entity: E) {
this.selectedEntities.push(entity);
this.emitChange();
this.inputField.nativeElement.value = "";
this.formControl.setValue(null);
setTimeout(() => this.autocomplete.openPanel());
if (entity) {
this.selectedEntities.push(entity);
this.emitChange();
this.inputField.nativeElement.value = "";
this.formControl.setValue(null);
setTimeout(() => this.autocomplete.openPanel());
}
}

/**
Expand All @@ -231,7 +249,7 @@ export class EntitySelectComponent<E extends Entity> implements OnChanges {
const value = event.value;

if (value) {
const entity = this.allEntities.find(
const entity = this.relevantEntities.find(
(e) => this.accessor(e) === value.trim(),
);
if (entity) {
Expand All @@ -248,19 +266,33 @@ export class EntitySelectComponent<E extends Entity> implements OnChanges {
* this will return all entities (with the aforementioned additional filters).
* @param value The value to look for in all entities
*/
private filter(value?: string): E[] {
let filteredEntities: E[] = this.allEntities.filter(
(e) => this.additionalFilter(e) && !this.isSelected(e),
private filter(value: string): E[] {
let filteredEntities: E[] = this.relevantEntities.filter(
(e) => !this.isSelected(e) && (this.includeInactive ? true : e.isActive),
);
let inactiveFilteredEntities: E[] = this.relevantEntities.filter(
(e) => !this.isSelected(e) && !e.isActive,
);
this.filterValue = value;

if (value) {
const filterValue = value.toLowerCase();
filteredEntities = filteredEntities.filter((entity) =>
this.accessor(entity).toLowerCase().includes(filterValue),
);
inactiveFilteredEntities = inactiveFilteredEntities.filter((entity) =>
this.accessor(entity).toLowerCase().includes(filterValue),
);
}
this.inactiveFilteredEntities = inactiveFilteredEntities;
return filteredEntities;
}

toggleIncludeInactive() {
this.includeInactive = !this.includeInactive;
this.filteredEntities = this.filter(this.filterValue);
}

/**
* removes a given entity from the records (if it exists) and emits changes
* @param entity The entity to remove
Expand Down