implemented in-place-search for properties

This commit is contained in:
Patrick Haßel 2021-11-01 10:05:08 +01:00
parent ec8adf8643
commit 08f1c6f93f
24 changed files with 376 additions and 25 deletions

View File

@ -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;
}

View File

@ -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);
}
}

View File

@ -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();
});
});

View File

@ -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);
}
}

View File

@ -4,9 +4,9 @@
Zeitpläne
</div>
<div class="item breadcrumb" [routerLink]="['/Schedule', {id: dataService.schedule.id}]" routerLinkActive="itemActive" *ngIf="dataService.schedule">
{{dataService.schedule.name}}
</div>
<!-- <div class="item breadcrumb" [routerLink]="['/Schedule', {id: dataService.schedule.id}]" routerLinkActive="itemActive" *ngIf="dataService.schedule">-->
<!-- {{dataService.schedule.name}}-->
<!-- </div>-->
</div>

View File

@ -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,

View File

@ -14,7 +14,7 @@
<app-edit-field [initial]="schedule.name" (valueChange)="set(null, 'name', $event)"></app-edit-field>
</td>
<td colspan="5">
<app-edit-field [initial]="schedule.propertyName" (valueChange)="set(null, 'propertyName', $event)"></app-edit-field>
<app-search [searchService]="propertyService" [initial]="schedule.propertyName" (valueChange)="set(null, 'propertyName', $event)"></app-search>
</td>
<td colspan="9">
<select [(ngModel)]="schedule.propertyType" (ngModelChange)="set(null,'propertyType', schedule.propertyType)">

View File

@ -6,6 +6,7 @@ import {ScheduleEntryService} from "../../api/schedule/entry/schedule-entry.serv
import {faCheckCircle, faCircle, faTimesCircle} from '@fortawesome/free-regular-svg-icons';
import {ActivatedRoute} from "@angular/router";
import {DataService} from "../../data.service";
import {PropertyService} from "../../api/property/property.service";
@Component({
selector: 'app-schedule',
@ -26,6 +27,7 @@ export class ScheduleComponent implements OnInit {
readonly scheduleService: ScheduleService,
readonly scheduleEntryService: ScheduleEntryService,
readonly dataService: DataService,
readonly propertyService: PropertyService,
) {
// nothing
}

View File

@ -0,0 +1,20 @@
<div
*ngIf="!searching"
(click)="startSearch()"
[class.empty]="!selected"
>
{{selected?.value ? selected?.value : "-LEER-"}}
</div>
<input
#input
type="text"
*ngIf="searching"
[(ngModel)]="term"
(ngModelChange)="changed()"
(keydown)="inputKeyPress($event)"
>
<div #resultList *ngIf="searching" class="resultList">
<div *ngFor="let result of results" class="result" (click)="select(result)">{{result.value}}</div>
</div>

View File

@ -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;
}
}

View File

@ -0,0 +1,25 @@
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {SearchComponent} from './search.component';
describe('SearchComponent', () => {
let component: SearchComponent;
let fixture: ComponentFixture<SearchComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [SearchComponent]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(SearchComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -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<T> 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<string> = new EventEmitter<string>();
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);
}
}

View File

@ -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);

View File

@ -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();
}
}

View File

@ -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();

View File

@ -22,4 +22,8 @@ public interface KnxGroupRepository extends CrudRepository<KnxGroup, Long> {
boolean existsByAddressRaw(int rawAddress);
List<KnxGroup> findAllByPropertyNameLikeIgnoreCaseOrTitleLikeIgnoreCase(String propertyNameLike, final String titleLike);
Optional<KnxGroup> findByPropertyName(String propertyName);
}

View File

@ -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<PropertyDto> findPropertyByName(final String propertyName) {
return knxGroupRepository.findByPropertyName(propertyName).map(this::toPropertyDto);
}
@Override
public List<PropertyDto> findAllProperties() {
return knxGroupRepository.findAll().stream().map(this::toPropertyDto).collect(Collectors.toList());
}
@Override
public List<PropertyDto> 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()

View File

@ -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));

View File

@ -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<PropertyDto> findAllProperties();
List<PropertyDto> findAllPropertiesLike(final String like);
Optional<PropertyDto> findPropertyByName(final String propertyName);
}

View File

@ -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<KeyValuePair> searchLike(@RequestBody final String term) {
return propertyService.findAllLike("%" + term + "%").stream().map(propertyDto -> new KeyValuePair(propertyDto.name, propertyDto.title)).collect(Collectors.toList());
}
}

View File

@ -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;

View File

@ -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<PropertyDto> findAllLike(final String like) {
return propertyOwners.stream().map(iProperyOwner -> iProperyOwner.findAllPropertiesLike(like)).reduce(PropertyService::merge).orElse(Collections.emptyList());
}
private static <T> List<T> merge(final List<T> a, final List<T> b) {
final ArrayList<T> 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<IPropertyOwner> findOwner(final String propertyName) {
return propertyOwners.stream()
.filter(iPropertyOwner -> iPropertyOwner.getPropertyNamePattern().matcher(propertyName).matches())
.findFirst();
}
}

View File

@ -0,0 +1,11 @@
package de.ph87.homeautomation.shared;
import java.util.List;
public interface ISearchController {
KeyValuePair getById(final String id);
List<KeyValuePair> searchLike(final String term);
}

View File

@ -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;
}
}