From 08f1c6f93fb15f62cbd49f8a19f550e9f0688c27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20Ha=C3=9Fel?= Date: Mon, 1 Nov 2021 10:05:08 +0100 Subject: [PATCH] implemented in-place-search for properties --- .../angular/src/app/api/ISearchService.ts | 9 ++ src/main/angular/src/app/api/KeyValuePair.ts | 26 +++++ .../app/api/property/property.service.spec.ts | 16 +++ .../src/app/api/property/property.service.ts | 54 ++++++++++ src/main/angular/src/app/app.component.html | 6 +- src/main/angular/src/app/app.module.ts | 2 + .../pages/schedule/schedule.component.html | 2 +- .../app/pages/schedule/schedule.component.ts | 2 + .../app/shared/search/search.component.html | 20 ++++ .../app/shared/search/search.component.less | 14 +++ .../shared/search/search.component.spec.ts | 25 +++++ .../src/app/shared/search/search.component.ts | 100 ++++++++++++++++++ .../ph87/homeautomation/DemoDataService.java | 2 +- .../homeautomation/knx/group/KnxGroup.java | 14 +-- .../homeautomation/knx/group/KnxGroupDto.java | 2 +- .../knx/group/KnxGroupRepository.java | 4 + .../knx/group/KnxGroupSetService.java | 14 ++- .../knx/group/KnxGroupWriteService.java | 2 +- .../property/IPropertyOwner.java | 5 + .../property/PropertyController.java | 20 +++- .../homeautomation/property/PropertyDto.java | 5 +- .../property/PropertyService.java | 29 +++-- .../shared/ISearchController.java | 11 ++ .../homeautomation/shared/KeyValuePair.java | 17 +++ 24 files changed, 376 insertions(+), 25 deletions(-) create mode 100644 src/main/angular/src/app/api/ISearchService.ts create mode 100644 src/main/angular/src/app/api/KeyValuePair.ts create mode 100644 src/main/angular/src/app/api/property/property.service.spec.ts create mode 100644 src/main/angular/src/app/api/property/property.service.ts create mode 100644 src/main/angular/src/app/shared/search/search.component.html create mode 100644 src/main/angular/src/app/shared/search/search.component.less create mode 100644 src/main/angular/src/app/shared/search/search.component.spec.ts create mode 100644 src/main/angular/src/app/shared/search/search.component.ts create mode 100644 src/main/java/de/ph87/homeautomation/shared/ISearchController.java create mode 100644 src/main/java/de/ph87/homeautomation/shared/KeyValuePair.java diff --git a/src/main/angular/src/app/api/ISearchService.ts b/src/main/angular/src/app/api/ISearchService.ts new file mode 100644 index 0000000..ef04432 --- /dev/null +++ b/src/main/angular/src/app/api/ISearchService.ts @@ -0,0 +1,9 @@ +import {KeyValuePair} from "./KeyValuePair"; + +export interface ISearchService { + + get(id: string, next: (results: KeyValuePair) => void, error: (error: any) => void): void; + + search(term: string, next: (results: KeyValuePair[]) => void, error: (error: any) => void): void; + +} diff --git a/src/main/angular/src/app/api/KeyValuePair.ts b/src/main/angular/src/app/api/KeyValuePair.ts new file mode 100644 index 0000000..7783f3f --- /dev/null +++ b/src/main/angular/src/app/api/KeyValuePair.ts @@ -0,0 +1,26 @@ +import {validateStringNotEmptyNotNull} from "./validators"; + +export class KeyValuePair { + + constructor( + readonly key: string, + readonly value: string, + ) { + } + + static fromJson(json: any): KeyValuePair { + return new KeyValuePair( + validateStringNotEmptyNotNull(json['key']), + validateStringNotEmptyNotNull(json['value']), + ); + } + + public static trackBy(index: number, item: KeyValuePair): string { + return item.value; + } + + public static compareKey(a: KeyValuePair, b: KeyValuePair): number { + return a.value.localeCompare(b.value); + } + +} diff --git a/src/main/angular/src/app/api/property/property.service.spec.ts b/src/main/angular/src/app/api/property/property.service.spec.ts new file mode 100644 index 0000000..d3f54bc --- /dev/null +++ b/src/main/angular/src/app/api/property/property.service.spec.ts @@ -0,0 +1,16 @@ +import {TestBed} from '@angular/core/testing'; + +import {PropertyService} from './property.service'; + +describe('PropertyService', () => { + let service: PropertyService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(PropertyService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/main/angular/src/app/api/property/property.service.ts b/src/main/angular/src/app/api/property/property.service.ts new file mode 100644 index 0000000..e810cb7 --- /dev/null +++ b/src/main/angular/src/app/api/property/property.service.ts @@ -0,0 +1,54 @@ +import {Injectable} from '@angular/core'; +import {ApiService, NO_OP} from "../api.service"; +import {validateNumberNotNull, validateStringNotEmptyNotNull} from "../validators"; +import {ISearchService} from "../ISearchService"; +import {KeyValuePair} from "../KeyValuePair"; + +export class Property { + + constructor( + public name: string, + public type: string, + public value: number, + ) { + // nothing + } + + static fromJson(json: any): Property { + return new Property( + validateStringNotEmptyNotNull(json['name']), + validateStringNotEmptyNotNull(json['type']), + validateNumberNotNull(json['value']), + ); + } + + public static trackBy(index: number, item: Property): string { + return item.name; + } + + public static compareName(a: Property, b: Property): number { + return a.name.localeCompare(b.name); + } + +} + +@Injectable({ + providedIn: 'root' +}) +export class PropertyService implements ISearchService { + + constructor( + readonly api: ApiService, + ) { + // nothing + } + + get(id: string, next: (results: KeyValuePair) => void, error: (error: any) => void): void { + this.api.postReturnItem("property/getById", id, KeyValuePair.fromJson, next, error); + } + + search(term: string, next: (results: KeyValuePair[]) => void = NO_OP, error: (error: any) => void = NO_OP): void { + this.api.postReturnList("property/searchLike", term, KeyValuePair.fromJson, next, error); + } + +} diff --git a/src/main/angular/src/app/app.component.html b/src/main/angular/src/app/app.component.html index d089a1c..0ae728b 100644 --- a/src/main/angular/src/app/app.component.html +++ b/src/main/angular/src/app/app.component.html @@ -4,9 +4,9 @@ Zeitpläne - + + + diff --git a/src/main/angular/src/app/app.module.ts b/src/main/angular/src/app/app.module.ts index b9a730f..4b869ff 100644 --- a/src/main/angular/src/app/app.module.ts +++ b/src/main/angular/src/app/app.module.ts @@ -10,6 +10,7 @@ import {ScheduleListComponent} from './pages/schedule-list/schedule-list.compone import {FontAwesomeModule} from '@fortawesome/angular-fontawesome'; import {NumberComponent} from './shared/number/number.component'; import {ScheduleComponent} from "./pages/schedule/schedule.component"; +import {SearchComponent} from './shared/search/search.component'; @NgModule({ declarations: [ @@ -18,6 +19,7 @@ import {ScheduleComponent} from "./pages/schedule/schedule.component"; ScheduleComponent, ScheduleListComponent, NumberComponent, + SearchComponent, ], imports: [ BrowserModule, diff --git a/src/main/angular/src/app/pages/schedule/schedule.component.html b/src/main/angular/src/app/pages/schedule/schedule.component.html index 5cd6c5c..bc43bd3 100644 --- a/src/main/angular/src/app/pages/schedule/schedule.component.html +++ b/src/main/angular/src/app/pages/schedule/schedule.component.html @@ -14,7 +14,7 @@ - + + +
+
{{result.value}}
+
diff --git a/src/main/angular/src/app/shared/search/search.component.less b/src/main/angular/src/app/shared/search/search.component.less new file mode 100644 index 0000000..8225b4e --- /dev/null +++ b/src/main/angular/src/app/shared/search/search.component.less @@ -0,0 +1,14 @@ +.resultList { + position: absolute; + background-color: lightgray; + min-width: 200px; + border: 1px solid black; + + .result { + padding: 5px; + } + + .result:hover { + background-color: lightyellow; + } +} diff --git a/src/main/angular/src/app/shared/search/search.component.spec.ts b/src/main/angular/src/app/shared/search/search.component.spec.ts new file mode 100644 index 0000000..b17eced --- /dev/null +++ b/src/main/angular/src/app/shared/search/search.component.spec.ts @@ -0,0 +1,25 @@ +import {ComponentFixture, TestBed} from '@angular/core/testing'; + +import {SearchComponent} from './search.component'; + +describe('SearchComponent', () => { + let component: SearchComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [SearchComponent] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(SearchComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/main/angular/src/app/shared/search/search.component.ts b/src/main/angular/src/app/shared/search/search.component.ts new file mode 100644 index 0000000..838eeae --- /dev/null +++ b/src/main/angular/src/app/shared/search/search.component.ts @@ -0,0 +1,100 @@ +import {Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild} from '@angular/core'; +import {KeyValuePair} from "../../api/KeyValuePair"; +import {ISearchService} from "../../api/ISearchService"; + +@Component({ + selector: 'app-search', + templateUrl: './search.component.html', + styleUrls: ['./search.component.less'] +}) +export class SearchComponent implements OnInit { + + private timeout: number | undefined; + + @ViewChild('input') + input2?: ElementRef; + + @ViewChild('input') + input?: HTMLInputElement; + + @ViewChild('resultList') + resultList?: HTMLDivElement; + + @Input() + searchService!: ISearchService; + + @Input() + initial!: string; + + @Output() + valueChange: EventEmitter = new EventEmitter(); + + term: string = ""; + + results: KeyValuePair[] = []; + + selected?: KeyValuePair; + + searching: boolean = false; + + constructor() { + } + + ngOnInit(): void { + this.searchService.get(this.initial, result => this.selected = result, _ => _); + } + + changed(): void { + this.clearTimeout(); + this.timeout = setTimeout(() => this.doSearch(), 400); + } + + private clearTimeout(): void { + if (this.timeout) { + clearTimeout(this.timeout); + this.timeout = undefined; + } + } + + startSearch(): void { + this.term = this.initial; + if (this.resultList && this.input) { + this.resultList.style.left = this.input.style.left; + } + this.searching = true; + setTimeout(() => this.input2?.nativeElement.focus(), 0); + this.doSearch(); + } + + inputKeyPress($event: KeyboardEvent): void { + switch ($event.key) { + case 'Enter': + this.doSearch(); + break; + case 'Escape': + this.cancelSearch(); + break; + } + } + + doSearch(): void { + this.clearTimeout(); + if (!this.term) { + this.results = []; + } else { + this.searchService.search(this.term, results => this.results = results, _ => _); + } + } + + cancelSearch(): void { + setTimeout(() => this.searching = false, 10); + } + + select(result: KeyValuePair): void { + console.log(result); + this.searching = false; + this.selected = result; + this.valueChange.emit(this.selected?.key); + } + +} diff --git a/src/main/java/de/ph87/homeautomation/DemoDataService.java b/src/main/java/de/ph87/homeautomation/DemoDataService.java index 66df8a4..77548dd 100644 --- a/src/main/java/de/ph87/homeautomation/DemoDataService.java +++ b/src/main/java/de/ph87/homeautomation/DemoDataService.java @@ -78,7 +78,7 @@ public class DemoDataService { createSunset(scheduleSchlafzimmerRollladen, Zenith.CIVIL, 0, 100); scheduleRepository.save(scheduleSchlafzimmerRollladen); - final Schedule scheduleFlurRollladen = createSchedule("Rollläden Flur", flur_rollladen_position_anfahren, PropertyType.SHUTTER); + final Schedule scheduleFlurRollladen = createSchedule("Rollladen Flur", flur_rollladen_position_anfahren, PropertyType.SHUTTER); createSunrise(scheduleFlurRollladen, Zenith.CIVIL, 0, 0); createSunset(scheduleFlurRollladen, Zenith.CIVIL, 0, 100); scheduleRepository.save(scheduleFlurRollladen); diff --git a/src/main/java/de/ph87/homeautomation/knx/group/KnxGroup.java b/src/main/java/de/ph87/homeautomation/knx/group/KnxGroup.java index 23a0252..228e41a 100644 --- a/src/main/java/de/ph87/homeautomation/knx/group/KnxGroup.java +++ b/src/main/java/de/ph87/homeautomation/knx/group/KnxGroup.java @@ -21,17 +21,23 @@ public class KnxGroup { @Setter(AccessLevel.NONE) private Long id; + @Setter(AccessLevel.NONE) @Column(nullable = false, unique = true) private int addressRaw; + @Setter(AccessLevel.NONE) @Column(nullable = false, unique = true) private String addressStr; + @Setter(AccessLevel.NONE) + @Column(nullable = false, unique = true) + private String propertyName; + @Column(nullable = false) private String dpt; @Column(nullable = false) - private String name; + private String title; @Column(nullable = false) @Enumerated(EnumType.STRING) @@ -69,15 +75,11 @@ public class KnxGroup { public void setAddress(final GroupAddress groupAddress) { this.addressRaw = groupAddress.getRawAddress(); this.addressStr = groupAddress.toString(); + this.propertyName = "knx.group." + groupAddress.getMainGroup() + "." + groupAddress.getMiddleGroup() + "." + groupAddress.getSubGroup8(); } public GroupAddress getAddress() { return new GroupAddress(addressRaw); } - public String getPropertyName() { - final GroupAddress address = getAddress(); - return "knx.group." + address.getMainGroup() + "." + address.getMiddleGroup() + "." + address.getSubGroup8(); - } - } diff --git a/src/main/java/de/ph87/homeautomation/knx/group/KnxGroupDto.java b/src/main/java/de/ph87/homeautomation/knx/group/KnxGroupDto.java index 883bd40..700df02 100644 --- a/src/main/java/de/ph87/homeautomation/knx/group/KnxGroupDto.java +++ b/src/main/java/de/ph87/homeautomation/knx/group/KnxGroupDto.java @@ -34,7 +34,7 @@ public class KnxGroupDto { addressStr = knxGroup.getAddressStr(); propertyName = knxGroup.getPropertyName(); dpt = knxGroup.getDpt(); - name = knxGroup.getName(); + name = knxGroup.getTitle(); propertyType = knxGroup.getPropertyType(); booleanValue = knxGroup.getBooleanValue(); diff --git a/src/main/java/de/ph87/homeautomation/knx/group/KnxGroupRepository.java b/src/main/java/de/ph87/homeautomation/knx/group/KnxGroupRepository.java index 8d1eaf1..7862c6a 100644 --- a/src/main/java/de/ph87/homeautomation/knx/group/KnxGroupRepository.java +++ b/src/main/java/de/ph87/homeautomation/knx/group/KnxGroupRepository.java @@ -22,4 +22,8 @@ public interface KnxGroupRepository extends CrudRepository { boolean existsByAddressRaw(int rawAddress); + List findAllByPropertyNameLikeIgnoreCaseOrTitleLikeIgnoreCase(String propertyNameLike, final String titleLike); + + Optional findByPropertyName(String propertyName); + } diff --git a/src/main/java/de/ph87/homeautomation/knx/group/KnxGroupSetService.java b/src/main/java/de/ph87/homeautomation/knx/group/KnxGroupSetService.java index b633713..6ba038f 100644 --- a/src/main/java/de/ph87/homeautomation/knx/group/KnxGroupSetService.java +++ b/src/main/java/de/ph87/homeautomation/knx/group/KnxGroupSetService.java @@ -14,6 +14,7 @@ import org.springframework.stereotype.Service; import tuwien.auto.calimero.GroupAddress; import java.util.List; +import java.util.Optional; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -66,14 +67,25 @@ public class KnxGroupSetService implements IPropertyOwner { return knxGroupRepository.findByAddressRaw(parseGroupAddress(propertyName).getRawAddress()).map(KnxGroup::getNumberValue).orElse(null); } + @Override + public Optional findPropertyByName(final String propertyName) { + return knxGroupRepository.findByPropertyName(propertyName).map(this::toPropertyDto); + } + @Override public List findAllProperties() { return knxGroupRepository.findAll().stream().map(this::toPropertyDto).collect(Collectors.toList()); } + @Override + public List findAllPropertiesLike(final String like) { + return knxGroupRepository.findAllByPropertyNameLikeIgnoreCaseOrTitleLikeIgnoreCase(like, like).stream().map(this::toPropertyDto).collect(Collectors.toList()); + } + private PropertyDto toPropertyDto(final KnxGroup knxGroup) { return new PropertyDto( - knxGroup.getName(), + knxGroup.getPropertyName(), + knxGroup.getTitle(), knxGroup.getBooleanValue(), knxGroup.getNumberValue(), knxGroup.getValueTimestamp() diff --git a/src/main/java/de/ph87/homeautomation/knx/group/KnxGroupWriteService.java b/src/main/java/de/ph87/homeautomation/knx/group/KnxGroupWriteService.java index 50473cc..6a93bc9 100644 --- a/src/main/java/de/ph87/homeautomation/knx/group/KnxGroupWriteService.java +++ b/src/main/java/de/ph87/homeautomation/knx/group/KnxGroupWriteService.java @@ -113,7 +113,7 @@ public class KnxGroupWriteService { trans.setAddress(address); trans.setDpt(dpt); trans.setMultiGroup(multiGroup); - trans.setName(name); + trans.setTitle(name); trans.setPropertyType(type); trans.getRead().setAble(readable); return new KnxGroupDto(knxGroupRepository.save(trans)); diff --git a/src/main/java/de/ph87/homeautomation/property/IPropertyOwner.java b/src/main/java/de/ph87/homeautomation/property/IPropertyOwner.java index 00293cb..eb4aa61 100644 --- a/src/main/java/de/ph87/homeautomation/property/IPropertyOwner.java +++ b/src/main/java/de/ph87/homeautomation/property/IPropertyOwner.java @@ -1,6 +1,7 @@ package de.ph87.homeautomation.property; import java.util.List; +import java.util.Optional; import java.util.regex.Pattern; public interface IPropertyOwner { @@ -15,4 +16,8 @@ public interface IPropertyOwner { List findAllProperties(); + List findAllPropertiesLike(final String like); + + Optional findPropertyByName(final String propertyName); + } diff --git a/src/main/java/de/ph87/homeautomation/property/PropertyController.java b/src/main/java/de/ph87/homeautomation/property/PropertyController.java index 84954b3..93765a0 100644 --- a/src/main/java/de/ph87/homeautomation/property/PropertyController.java +++ b/src/main/java/de/ph87/homeautomation/property/PropertyController.java @@ -1,16 +1,17 @@ package de.ph87.homeautomation.property; +import de.ph87.homeautomation.shared.ISearchController; +import de.ph87.homeautomation.shared.KeyValuePair; import lombok.RequiredArgsConstructor; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; import java.util.List; +import java.util.stream.Collectors; @RestController @RequestMapping("property") @RequiredArgsConstructor -public class PropertyController { +public class PropertyController implements ISearchController { private final PropertyService propertyService; @@ -19,4 +20,15 @@ public class PropertyController { return propertyService.findAll(); } + @PostMapping("getById") + public KeyValuePair getById(@RequestBody final String id) { + final PropertyDto propertyDto = propertyService.getById(id); + return new KeyValuePair(propertyDto.name, propertyDto.title); + } + + @PostMapping("searchLike") + public List searchLike(@RequestBody final String term) { + return propertyService.findAllLike("%" + term + "%").stream().map(propertyDto -> new KeyValuePair(propertyDto.name, propertyDto.title)).collect(Collectors.toList()); + } + } diff --git a/src/main/java/de/ph87/homeautomation/property/PropertyDto.java b/src/main/java/de/ph87/homeautomation/property/PropertyDto.java index e76ede1..a8452a1 100644 --- a/src/main/java/de/ph87/homeautomation/property/PropertyDto.java +++ b/src/main/java/de/ph87/homeautomation/property/PropertyDto.java @@ -9,14 +9,17 @@ public class PropertyDto { public final String name; + public final String title; + public final Boolean booleanValue; public final Number numberValue; public final ZonedDateTime timestamp; - public PropertyDto(final String name, final Boolean booleanValue, final Number numberValue, final ZonedDateTime timestamp) { + public PropertyDto(final String name, final String title, final Boolean booleanValue, final Number numberValue, final ZonedDateTime timestamp) { this.name = name; + this.title = title; this.booleanValue = booleanValue; this.numberValue = numberValue; this.timestamp = timestamp; diff --git a/src/main/java/de/ph87/homeautomation/property/PropertyService.java b/src/main/java/de/ph87/homeautomation/property/PropertyService.java index 5ce1248..d027098 100644 --- a/src/main/java/de/ph87/homeautomation/property/PropertyService.java +++ b/src/main/java/de/ph87/homeautomation/property/PropertyService.java @@ -1,13 +1,12 @@ package de.ph87.homeautomation.property; import de.ph87.homeautomation.shared.Helpers; +import de.ph87.office.web.NotFoundException; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; -import java.util.ArrayList; -import java.util.List; -import java.util.Set; +import java.util.*; @Slf4j @Service @@ -30,9 +29,7 @@ public class PropertyService { } private IPropertyOwner getOwnerOrThrow(final String propertyName) { - return propertyOwners.stream() - .filter(iPropertyOwner -> iPropertyOwner.getPropertyNamePattern().matcher(propertyName).matches()) - .findFirst() + return findOwner(propertyName) .orElseThrow(() -> new RuntimeException("No IPropertyOwner found for propertyName: " + propertyName)); } @@ -40,4 +37,24 @@ public class PropertyService { return propertyOwners.stream().map(IPropertyOwner::findAllProperties).reduce(new ArrayList<>(), Helpers::merge); } + public List findAllLike(final String like) { + return propertyOwners.stream().map(iProperyOwner -> iProperyOwner.findAllPropertiesLike(like)).reduce(PropertyService::merge).orElse(Collections.emptyList()); + } + + private static List merge(final List a, final List b) { + final ArrayList c = new ArrayList<>(a); + c.addAll(b); + return c; + } + + public PropertyDto getById(final String propertyName) { + return findOwner(propertyName).flatMap(iPropertyOwner -> iPropertyOwner.findPropertyByName(propertyName)).orElseThrow(() -> new NotFoundException("Property.name=%s", propertyName)); + } + + private Optional findOwner(final String propertyName) { + return propertyOwners.stream() + .filter(iPropertyOwner -> iPropertyOwner.getPropertyNamePattern().matcher(propertyName).matches()) + .findFirst(); + } + } diff --git a/src/main/java/de/ph87/homeautomation/shared/ISearchController.java b/src/main/java/de/ph87/homeautomation/shared/ISearchController.java new file mode 100644 index 0000000..04404d0 --- /dev/null +++ b/src/main/java/de/ph87/homeautomation/shared/ISearchController.java @@ -0,0 +1,11 @@ +package de.ph87.homeautomation.shared; + +import java.util.List; + +public interface ISearchController { + + KeyValuePair getById(final String id); + + List searchLike(final String term); + +} diff --git a/src/main/java/de/ph87/homeautomation/shared/KeyValuePair.java b/src/main/java/de/ph87/homeautomation/shared/KeyValuePair.java new file mode 100644 index 0000000..e54268c --- /dev/null +++ b/src/main/java/de/ph87/homeautomation/shared/KeyValuePair.java @@ -0,0 +1,17 @@ +package de.ph87.homeautomation.shared; + +import lombok.Getter; + +@Getter +public class KeyValuePair { + + public final String key; + + public final String value; + + public KeyValuePair(final String key, final String value) { + this.key = key; + this.value = value; + } + +}