From 61ffab50bab3e4c61cdfd87cda2afd65bbac95e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20Ha=C3=9Fel?= Date: Thu, 28 Nov 2024 14:37:22 +0100 Subject: [PATCH] Tag, Taggable --- src/main/angular/src/app/api/Device/Device.ts | 4 -- .../src/app/api/Device/device.service.ts | 6 -- .../src/app/api/Group/group.service.ts | 6 -- .../angular/src/app/api/Shutter/Shutter.ts | 4 -- .../src/app/api/Shutter/shutter.service.ts | 6 -- src/main/angular/src/app/api/Tag/Tag.ts | 19 ++++++ .../angular/src/app/api/Tag/tag.service.ts | 17 +++++ .../angular/src/app/api/Taggable/Taggable.ts | 19 ++++++ .../src/app/api/Taggable/TaggableFilter.ts | 7 ++ .../src/app/api/Taggable/taggable.service.ts | 33 +++++++++ .../angular/src/app/api/Tunable/Tunable.ts | 4 -- .../src/app/api/Tunable/tunable.service.ts | 6 -- .../src/app/api/common/CrudLiveList.ts | 34 +++++++--- .../angular/src/app/api/common/CrudService.ts | 6 +- .../angular/src/app/api/common/validators.ts | 8 +++ src/main/angular/src/app/app.component.html | 6 +- src/main/angular/src/app/app.routes.ts | 9 +-- .../pages/dashboard/dashboard.component.html | 8 +-- .../pages/dashboard/dashboard.component.ts | 6 +- .../device-list-page.component.html | 8 --- .../device-list-page.component.less | 5 -- .../device-list-page.component.ts | 68 ------------------- .../knx-group-list-page.component.html | 2 +- .../knx-group-list-page.component.ts | 18 ++--- .../shutter-list-page.component.html | 8 --- .../shutter-list-page.component.less | 5 -- .../shutter-list-page.component.ts | 68 ------------------- .../taggable-list-page.component.html | 8 +++ .../taggable-list-page.component.less | 0 .../taggable-list-page.component.ts | 68 +++++++++++++++++++ .../tunable-list-page.component.html | 8 --- .../tunable-list-page.component.less | 5 -- .../tunable-list-page.component.ts | 68 ------------------- .../knx-group-tile.component.less | 1 - .../app/shared/search/search.component.html | 3 + .../app/shared/search/search.component.less | 6 ++ .../src/app/shared/search/search.component.ts | 35 ++++++++++ .../taggable-list.component.html | 5 ++ .../taggable-list.component.less | 1 + .../taggable-list/taggable-list.component.ts | 35 ++++++++++ .../taggable-tile.component.html | 5 ++ .../taggable-tile.component.less | 0 .../taggable-tile/taggable-tile.component.ts | 55 +++++++++++++++ src/main/angular/src/styles.less | 5 +- src/main/java/de/ph87/home/area/Area.java | 9 ++- .../java/de/ph87/home/area/AreaFilter.java | 4 +- .../java/de/ph87/home/common/ListHelpers.java | 21 ++++++ .../common/crud/AbstractSearchFilter.java | 2 +- .../de/ph87/home/common/crud/ISearch.java | 18 ----- .../ph87/home/common/crud/SearchHelper.java | 30 ++++++++ .../java/de/ph87/home/demo/DemoService.java | 64 +++++++++++------ src/main/java/de/ph87/home/device/Device.java | 26 +++++-- .../java/de/ph87/home/device/DeviceDto.java | 5 ++ .../de/ph87/home/device/DeviceFilter.java | 4 +- .../de/ph87/home/device/DeviceService.java | 51 ++++++++++---- .../de/ph87/home/knx/group/GroupFilter.java | 4 +- .../java/de/ph87/home/search/ISearchable.java | 16 +++++ .../ph87/home/search/ISearchableService.java | 11 +++ .../de/ph87/home/search/SearchController.java | 24 +++++++ .../de/ph87/home/search/SearchService.java | 29 ++++++++ .../de/ph87/home/search/SearchableDto.java | 17 +++++ .../java/de/ph87/home/shutter/Shutter.java | 26 +++++-- .../java/de/ph87/home/shutter/ShutterDto.java | 6 ++ .../de/ph87/home/shutter/ShutterFilter.java | 4 +- .../de/ph87/home/shutter/ShutterService.java | 57 ++++++++++++---- src/main/java/de/ph87/home/tag/Tag.java | 36 ++++++++++ .../java/de/ph87/home/tag/TagController.java | 12 ++++ src/main/java/de/ph87/home/tag/TagDto.java | 20 ++++++ src/main/java/de/ph87/home/tag/TagReader.java | 22 ++++++ .../java/de/ph87/home/tag/TagRepository.java | 7 ++ .../java/de/ph87/home/tag/TagService.java | 22 ++++++ .../java/de/ph87/home/tag/TaggableDto.java | 17 +++++ .../de/ph87/home/tag/taggable/ITaggable.java | 20 ++++++ .../home/tag/taggable/ITaggableService.java | 13 ++++ .../home/tag/taggable/TaggableController.java | 26 +++++++ .../home/tag/taggable/TaggableFilter.java | 15 ++++ .../home/tag/taggable/TaggableService.java | 30 ++++++++ .../java/de/ph87/home/tunable/Tunable.java | 26 +++++-- .../java/de/ph87/home/tunable/TunableDto.java | 5 ++ .../de/ph87/home/tunable/TunableFilter.java | 4 +- .../de/ph87/home/tunable/TunableService.java | 59 ++++++++++++---- 81 files changed, 1035 insertions(+), 425 deletions(-) create mode 100644 src/main/angular/src/app/api/Tag/Tag.ts create mode 100644 src/main/angular/src/app/api/Tag/tag.service.ts create mode 100644 src/main/angular/src/app/api/Taggable/Taggable.ts create mode 100644 src/main/angular/src/app/api/Taggable/TaggableFilter.ts create mode 100644 src/main/angular/src/app/api/Taggable/taggable.service.ts delete mode 100644 src/main/angular/src/app/pages/device-list-page/device-list-page.component.html delete mode 100644 src/main/angular/src/app/pages/device-list-page/device-list-page.component.less delete mode 100644 src/main/angular/src/app/pages/device-list-page/device-list-page.component.ts delete mode 100644 src/main/angular/src/app/pages/shutter-list-page/shutter-list-page.component.html delete mode 100644 src/main/angular/src/app/pages/shutter-list-page/shutter-list-page.component.less delete mode 100644 src/main/angular/src/app/pages/shutter-list-page/shutter-list-page.component.ts create mode 100644 src/main/angular/src/app/pages/taggable-list-page/taggable-list-page.component.html create mode 100644 src/main/angular/src/app/pages/taggable-list-page/taggable-list-page.component.less create mode 100644 src/main/angular/src/app/pages/taggable-list-page/taggable-list-page.component.ts delete mode 100644 src/main/angular/src/app/pages/tunable-list-page/tunable-list-page.component.html delete mode 100644 src/main/angular/src/app/pages/tunable-list-page/tunable-list-page.component.less delete mode 100644 src/main/angular/src/app/pages/tunable-list-page/tunable-list-page.component.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.ts create mode 100644 src/main/angular/src/app/shared/taggable-list/taggable-list.component.html create mode 100644 src/main/angular/src/app/shared/taggable-list/taggable-list.component.less create mode 100644 src/main/angular/src/app/shared/taggable-list/taggable-list.component.ts create mode 100644 src/main/angular/src/app/shared/taggable-tile/taggable-tile.component.html create mode 100644 src/main/angular/src/app/shared/taggable-tile/taggable-tile.component.less create mode 100644 src/main/angular/src/app/shared/taggable-tile/taggable-tile.component.ts create mode 100644 src/main/java/de/ph87/home/common/ListHelpers.java create mode 100644 src/main/java/de/ph87/home/common/crud/SearchHelper.java create mode 100644 src/main/java/de/ph87/home/search/ISearchable.java create mode 100644 src/main/java/de/ph87/home/search/ISearchableService.java create mode 100644 src/main/java/de/ph87/home/search/SearchController.java create mode 100644 src/main/java/de/ph87/home/search/SearchService.java create mode 100644 src/main/java/de/ph87/home/search/SearchableDto.java create mode 100644 src/main/java/de/ph87/home/tag/Tag.java create mode 100644 src/main/java/de/ph87/home/tag/TagController.java create mode 100644 src/main/java/de/ph87/home/tag/TagDto.java create mode 100644 src/main/java/de/ph87/home/tag/TagReader.java create mode 100644 src/main/java/de/ph87/home/tag/TagRepository.java create mode 100644 src/main/java/de/ph87/home/tag/TagService.java create mode 100644 src/main/java/de/ph87/home/tag/TaggableDto.java create mode 100644 src/main/java/de/ph87/home/tag/taggable/ITaggable.java create mode 100644 src/main/java/de/ph87/home/tag/taggable/ITaggableService.java create mode 100644 src/main/java/de/ph87/home/tag/taggable/TaggableController.java create mode 100644 src/main/java/de/ph87/home/tag/taggable/TaggableFilter.java create mode 100644 src/main/java/de/ph87/home/tag/taggable/TaggableService.java diff --git a/src/main/angular/src/app/api/Device/Device.ts b/src/main/angular/src/app/api/Device/Device.ts index 63bd259..986da94 100644 --- a/src/main/angular/src/app/api/Device/Device.ts +++ b/src/main/angular/src/app/api/Device/Device.ts @@ -44,10 +44,6 @@ export class Device { return device.uuid; } - static equals(a: Device, b: Device): boolean { - return a.uuid === b.uuid; - } - static compareByAreaThenName(a: Device, b: Device): number { const area = Area.compareByName(a.area, b.area); if (area !== 0) { diff --git a/src/main/angular/src/app/api/Device/device.service.ts b/src/main/angular/src/app/api/Device/device.service.ts index 1e0a9a5..0a4daa6 100644 --- a/src/main/angular/src/app/api/Device/device.service.ts +++ b/src/main/angular/src/app/api/Device/device.service.ts @@ -4,8 +4,6 @@ import {Device} from './Device'; import {ApiService} from '../common/api.service'; import {Next} from '../common/types'; -import {DeviceFilter} from './DeviceFilter'; - @Injectable({ providedIn: 'root' }) @@ -21,10 +19,6 @@ export class DeviceService extends CrudService { this.getSingle(['getByUuid', uuid], next); } - list(filter: DeviceFilter | null, next: Next): void { - this.postList(['list'], filter, next); - } - setState(device: Device, state: boolean, next?: Next): void { this.getNone(['setState', device.uuid, state], next); } diff --git a/src/main/angular/src/app/api/Group/group.service.ts b/src/main/angular/src/app/api/Group/group.service.ts index 371fb1b..08ffa70 100644 --- a/src/main/angular/src/app/api/Group/group.service.ts +++ b/src/main/angular/src/app/api/Group/group.service.ts @@ -4,8 +4,6 @@ import {Group} from './Group'; import {ApiService} from '../common/api.service'; import {Next} from '../common/types'; -import {GroupFilter} from './GroupFilter'; - @Injectable({ providedIn: 'root' }) @@ -21,8 +19,4 @@ export class GroupService extends CrudService { this.getSingle(['getByAddress', address], next); } - list(filter: GroupFilter | null, next: Next): void { - this.postList(['list'], filter, next); - } - } diff --git a/src/main/angular/src/app/api/Shutter/Shutter.ts b/src/main/angular/src/app/api/Shutter/Shutter.ts index 9a2946e..13ab9f4 100644 --- a/src/main/angular/src/app/api/Shutter/Shutter.ts +++ b/src/main/angular/src/app/api/Shutter/Shutter.ts @@ -45,10 +45,6 @@ export class Shutter { return shutter.uuid; } - static equals(a: Shutter, b: Shutter): boolean { - return a.uuid === b.uuid; - } - static compareByAreaThenName(a: Shutter, b: Shutter): number { const area = Area.compareByName(a.area, b.area); if (area !== 0) { diff --git a/src/main/angular/src/app/api/Shutter/shutter.service.ts b/src/main/angular/src/app/api/Shutter/shutter.service.ts index 0297eec..04999bb 100644 --- a/src/main/angular/src/app/api/Shutter/shutter.service.ts +++ b/src/main/angular/src/app/api/Shutter/shutter.service.ts @@ -4,8 +4,6 @@ import {Shutter} from './Shutter'; import {ApiService} from '../common/api.service'; import {Next} from '../common/types'; -import {ShutterFilter} from './ShutterFilter'; - @Injectable({ providedIn: 'root' }) @@ -21,10 +19,6 @@ export class ShutterService extends CrudService { this.getSingle(['getByUuid', uuid], next); } - list(filter: ShutterFilter | null, next: Next): void { - this.postList(['list'], filter, next); - } - setPosition(shutter: Shutter, position: number, next?: Next): void { this.getNone(['setPosition', shutter.uuid, position], next); } diff --git a/src/main/angular/src/app/api/Tag/Tag.ts b/src/main/angular/src/app/api/Tag/Tag.ts new file mode 100644 index 0000000..bdbf978 --- /dev/null +++ b/src/main/angular/src/app/api/Tag/Tag.ts @@ -0,0 +1,19 @@ +import {validateString} from '../common/validators'; + +export class Tag { + + constructor( + readonly uuid: string, + readonly name: string, + ) { + // + } + + static fromJson(json: any): Tag { + return new Tag( + validateString(json.uuid), + validateString(json.name), + ); + } + +} diff --git a/src/main/angular/src/app/api/Tag/tag.service.ts b/src/main/angular/src/app/api/Tag/tag.service.ts new file mode 100644 index 0000000..388262e --- /dev/null +++ b/src/main/angular/src/app/api/Tag/tag.service.ts @@ -0,0 +1,17 @@ +import {Injectable} from '@angular/core'; +import {ApiService} from '../common/api.service'; +import {CrudService} from '../common/CrudService'; +import {Tag} from './Tag'; + +@Injectable({ + providedIn: 'root' +}) +export class TagService extends CrudService { + + constructor( + apiService: ApiService, + ) { + super(apiService, ['Tag'], Tag.fromJson); + } + +} diff --git a/src/main/angular/src/app/api/Taggable/Taggable.ts b/src/main/angular/src/app/api/Taggable/Taggable.ts new file mode 100644 index 0000000..5d78dd4 --- /dev/null +++ b/src/main/angular/src/app/api/Taggable/Taggable.ts @@ -0,0 +1,19 @@ +import {Device} from "../Device/Device"; +import {Shutter} from "../Shutter/Shutter"; +import {Tunable} from "../Tunable/Tunable"; +import {validateAndRemoveDtoSuffix} from "../common/validators"; + +export type Taggable = Device | Shutter | Tunable; + +export function taggableFromJson(json: any): Taggable { + const _type_ = validateAndRemoveDtoSuffix(json._type_); + switch (_type_) { + case 'Device': + return Device.fromJson(json.payload); + case 'Shutter': + return Shutter.fromJson(json.payload); + case 'Tunable': + return Tunable.fromJson(json.payload); + } + throw new Error("Type not implemented: " + _type_); +} diff --git a/src/main/angular/src/app/api/Taggable/TaggableFilter.ts b/src/main/angular/src/app/api/Taggable/TaggableFilter.ts new file mode 100644 index 0000000..17ff486 --- /dev/null +++ b/src/main/angular/src/app/api/Taggable/TaggableFilter.ts @@ -0,0 +1,7 @@ +export class TaggableFilter { + + tag: string = ""; + + search: string = ""; + +} diff --git a/src/main/angular/src/app/api/Taggable/taggable.service.ts b/src/main/angular/src/app/api/Taggable/taggable.service.ts new file mode 100644 index 0000000..316057f --- /dev/null +++ b/src/main/angular/src/app/api/Taggable/taggable.service.ts @@ -0,0 +1,33 @@ +import {Injectable} from '@angular/core'; +import {ApiService} from '../common/api.service'; +import {CrudService} from '../common/CrudService'; +import {Taggable, taggableFromJson} from './Taggable'; +import {Next} from '../common/types'; +import {Subject, Subscription} from 'rxjs'; +import {DeviceService} from '../Device/device.service'; +import {ShutterService} from '../Shutter/shutter.service'; +import {TunableService} from '../Tunable/tunable.service'; + +@Injectable({ + providedIn: 'root' +}) +export class TaggableService extends CrudService { + + constructor( + apiService: ApiService, + protected readonly deviceService: DeviceService, + protected readonly shutterService: ShutterService, + protected readonly tunableService: TunableService, + ) { + super(apiService, ['Taggable'], taggableFromJson); + } + + override subscribe(next: Next): Subscription { + const subject = new Subject(); + this.deviceService.subscribe(next => subject.next(next)); + this.shutterService.subscribe(next => subject.next(next)); + this.tunableService.subscribe(next => subject.next(next)); + return subject.subscribe(next); + } + +} diff --git a/src/main/angular/src/app/api/Tunable/Tunable.ts b/src/main/angular/src/app/api/Tunable/Tunable.ts index f5a48cd..d504b37 100644 --- a/src/main/angular/src/app/api/Tunable/Tunable.ts +++ b/src/main/angular/src/app/api/Tunable/Tunable.ts @@ -52,10 +52,6 @@ export class Tunable { return tunable.uuid; } - static equals(a: Tunable, b: Tunable): boolean { - return a.uuid === b.uuid; - } - static compareByAreaThenName(a: Tunable, b: Tunable): number { const area = Area.compareByName(a.area, b.area); if (area !== 0) { diff --git a/src/main/angular/src/app/api/Tunable/tunable.service.ts b/src/main/angular/src/app/api/Tunable/tunable.service.ts index 3686a43..8b903a2 100644 --- a/src/main/angular/src/app/api/Tunable/tunable.service.ts +++ b/src/main/angular/src/app/api/Tunable/tunable.service.ts @@ -4,8 +4,6 @@ import {Tunable} from './Tunable'; import {ApiService} from '../common/api.service'; import {Next} from '../common/types'; -import {TunableFilter} from './TunableFilter'; - @Injectable({ providedIn: 'root' }) @@ -21,10 +19,6 @@ export class TunableService extends CrudService { this.getSingle(['getByUuid', uuid], next); } - list(filter: TunableFilter | null, next: Next): void { - this.postList(['list'], filter, next); - } - setState(tunable: Tunable, state: boolean, next?: Next): void { this.getNone(['setState', tunable.uuid, state], next); } diff --git a/src/main/angular/src/app/api/common/CrudLiveList.ts b/src/main/angular/src/app/api/common/CrudLiveList.ts index 7b56397..198b214 100644 --- a/src/main/angular/src/app/api/common/CrudLiveList.ts +++ b/src/main/angular/src/app/api/common/CrudLiveList.ts @@ -1,46 +1,62 @@ import {CrudService} from "./CrudService"; import {Subscription} from "rxjs"; +import {Next} from './types'; -export class CrudLiveList extends Subscription { +export interface UUID { + + uuid: string; + +} + +export class CrudLiveList extends Subscription { private readonly subs: Subscription[] = []; - unfiltered: ENTITY[] = []; + private unfiltered: ENTITY[] = []; - filtered: ENTITY[] = []; + list: ENTITY[] = []; constructor( readonly crudService: CrudService, - readonly equals: (a: ENTITY, b: ENTITY) => boolean, + readonly allowAppending: boolean, readonly filter: (item: ENTITY) => boolean = _ => true, + readonly all: (next: Next) => any = next => this.crudService.list(next), + readonly equals: (a: ENTITY, b: ENTITY) => boolean = (a, b) => a.uuid === b.uuid, ) { super(() => { this.subs.forEach(sub => sub.unsubscribe()); }); - this.fetchAll(); - this.subs.push(crudService.api.connected(_ => this.fetchAll())); + this.subs.push(crudService.api.connected(_ => this.refresh())); this.subs.push(crudService.subscribe(item => this.update(item))); } - private fetchAll() { - this.crudService.all(list => { + refresh() { + this.all(list => { this.unfiltered = list; this.updateFiltered(); }); } + clear() { + this.unfiltered = []; + this.updateFiltered(); + } + private update(item: ENTITY) { const index = this.unfiltered.findIndex(i => this.equals(i, item)); if (index >= 0) { this.unfiltered[index] = item; } else { + if (!this.allowAppending) { + return; + } this.unfiltered.push(item); } this.updateFiltered(); } private updateFiltered() { - this.filtered = this.unfiltered.filter(this.filter); + this.list = this.unfiltered.filter(this.filter); } } diff --git a/src/main/angular/src/app/api/common/CrudService.ts b/src/main/angular/src/app/api/common/CrudService.ts index e2dd345..8016bb2 100644 --- a/src/main/angular/src/app/api/common/CrudService.ts +++ b/src/main/angular/src/app/api/common/CrudService.ts @@ -13,10 +13,14 @@ export abstract class CrudService { // } - all(next: Next) { + list(next: Next): void { this.getList(['list'], next); } + filter(filter: FILTER, next: Next): void { + this.postList(['list'], filter, next); + } + subscribe(next: Next): Subscription { return this.api.subscribe([...this.path], this.fromJson, next); } diff --git a/src/main/angular/src/app/api/common/validators.ts b/src/main/angular/src/app/api/common/validators.ts index 602182a..1610ad9 100644 --- a/src/main/angular/src/app/api/common/validators.ts +++ b/src/main/angular/src/app/api/common/validators.ts @@ -79,6 +79,14 @@ export function orNull(item: T | null | undefined, map: (t: T) => R): R | return map(item); } +export function validateAndRemoveDtoSuffix(json: any): string { + const type = validateString(json); + if (!type.endsWith('Dto')) { + throw Error("Type name does not end with Dto: " + type); + } + return type.substring(0, type.length - 3); +} + export function isSet(value: any) { return value !== null && value !== undefined; } diff --git a/src/main/angular/src/app/app.component.html b/src/main/angular/src/app/app.component.html index 5625b3e..0cfed85 100644 --- a/src/main/angular/src/app/app.component.html +++ b/src/main/angular/src/app/app.component.html @@ -1,9 +1,9 @@
diff --git a/src/main/angular/src/app/app.routes.ts b/src/main/angular/src/app/app.routes.ts index 29dbfbe..2a18082 100644 --- a/src/main/angular/src/app/app.routes.ts +++ b/src/main/angular/src/app/app.routes.ts @@ -1,15 +1,12 @@ import {Routes} from '@angular/router'; import {KnxGroupListPageComponent} from './pages/knx-group-list-page/knx-group-list-page.component'; -import {DeviceListPageComponent} from './pages/device-list-page/device-list-page.component'; -import {ShutterListPageComponent} from './pages/shutter-list-page/shutter-list-page.component'; -import {TunableListPageComponent} from './pages/tunable-list-page/tunable-list-page.component'; import {DashboardComponent} from './pages/dashboard/dashboard.component'; +import {TaggableListPageComponent} from './pages/taggable-list-page/taggable-list-page.component'; export const routes: Routes = [ {path: 'Dashboard', component: DashboardComponent}, - {path: 'DeviceList', component: DeviceListPageComponent}, - {path: 'TunableList', component: TunableListPageComponent}, - {path: 'ShutterList', component: ShutterListPageComponent}, {path: 'GroupList', component: KnxGroupListPageComponent}, + {path: 'TaggableList', component: TaggableListPageComponent}, + {path: 'TaggableList/:tag', component: TaggableListPageComponent}, {path: '**', redirectTo: 'Dashboard'}, ]; diff --git a/src/main/angular/src/app/pages/dashboard/dashboard.component.html b/src/main/angular/src/app/pages/dashboard/dashboard.component.html index 99a2d66..7c5f722 100644 --- a/src/main/angular/src/app/pages/dashboard/dashboard.component.html +++ b/src/main/angular/src/app/pages/dashboard/dashboard.component.html @@ -1,6 +1,6 @@
- - - -
- Nichts -
+ + + +
- Nichts -
diff --git a/src/main/angular/src/app/pages/dashboard/dashboard.component.ts b/src/main/angular/src/app/pages/dashboard/dashboard.component.ts index 5ba714b..670bbbe 100644 --- a/src/main/angular/src/app/pages/dashboard/dashboard.component.ts +++ b/src/main/angular/src/app/pages/dashboard/dashboard.component.ts @@ -52,9 +52,9 @@ export class DashboardComponent implements OnInit, OnDestroy { ngOnInit(): void { this.newDate(); this.subs.push(timer(5000, 5000).subscribe(() => this.newDate())); - this.subs.push(this.deviceList = new CrudLiveList(this.deviceService, Device.equals, device => device.stateProperty?.state?.value === true)); - this.subs.push(this.tunableList = new CrudLiveList(this.tunableService, Tunable.equals, tunable => tunable.stateProperty?.state?.value === true)); - this.subs.push(this.shutterList = new CrudLiveList(this.shutterService, Shutter.equals, shutter => this.shutterFilter(shutter))); + this.subs.push(this.deviceList = new CrudLiveList(this.deviceService, true, device => device.stateProperty?.state?.value === true)); + this.subs.push(this.tunableList = new CrudLiveList(this.tunableService, true, tunable => tunable.stateProperty?.state?.value === true)); + this.subs.push(this.shutterList = new CrudLiveList(this.shutterService, true, shutter => this.shutterFilter(shutter))); } private newDate() { diff --git a/src/main/angular/src/app/pages/device-list-page/device-list-page.component.html b/src/main/angular/src/app/pages/device-list-page/device-list-page.component.html deleted file mode 100644 index 803a3d9..0000000 --- a/src/main/angular/src/app/pages/device-list-page/device-list-page.component.html +++ /dev/null @@ -1,8 +0,0 @@ -
-
- -
-
- -
-
diff --git a/src/main/angular/src/app/pages/device-list-page/device-list-page.component.less b/src/main/angular/src/app/pages/device-list-page/device-list-page.component.less deleted file mode 100644 index e4e2c79..0000000 --- a/src/main/angular/src/app/pages/device-list-page/device-list-page.component.less +++ /dev/null @@ -1,5 +0,0 @@ -@import "../../../config"; - -input { - border-bottom: @border solid lightgray; -} diff --git a/src/main/angular/src/app/pages/device-list-page/device-list-page.component.ts b/src/main/angular/src/app/pages/device-list-page/device-list-page.component.ts deleted file mode 100644 index ccb23c7..0000000 --- a/src/main/angular/src/app/pages/device-list-page/device-list-page.component.ts +++ /dev/null @@ -1,68 +0,0 @@ -import {Component, OnDestroy, OnInit} from '@angular/core'; -import {DeviceListComponent} from '../../shared/device-list/device-list.component'; -import {Device} from '../../api/Device/Device'; -import {DeviceService} from '../../api/Device/device.service'; -import {FormsModule} from '@angular/forms'; -import {DeviceFilter} from '../../api/Device/DeviceFilter'; -import {Subscription} from 'rxjs'; -import {ApiService} from '../../api/common/api.service'; - -@Component({ - selector: 'app-device-list-page', - standalone: true, - imports: [ - DeviceListComponent, - FormsModule - ], - templateUrl: './device-list-page.component.html', - styleUrl: './device-list-page.component.less' -}) -export class DeviceListPageComponent implements OnInit, OnDestroy { - - private readonly subs: Subscription[] = []; - - protected deviceList: Device[] = []; - - protected filter: DeviceFilter = new DeviceFilter(); - - private fetchTimeout: any; - - constructor( - protected readonly deviceService: DeviceService, - protected readonly apiService: ApiService, - ) { - // - } - - ngOnInit(): void { - this.fetch(); - this.subs.push(this.deviceService.subscribe(device => this.updateDevice(device))); - this.apiService.connected(() => this.fetch()); - } - - ngOnDestroy(): void { - this.subs.forEach(sub => sub.unsubscribe()); - } - - fetchDelayed() { - if (this.fetchTimeout) { - clearTimeout(this.fetchTimeout); - this.fetchTimeout = undefined; - } - this.fetchTimeout = setTimeout(() => this.fetch(), 300) - } - - private fetch() { - this.deviceService.list(this.filter, list => this.deviceList = list) - } - - private updateDevice(device: Device) { - const index = this.deviceList.findIndex(d => d.uuid === device.uuid); - if (index >= 0) { - this.deviceList.splice(index, 1, device); - } else { - this.fetch(); - } - } - -} diff --git a/src/main/angular/src/app/pages/knx-group-list-page/knx-group-list-page.component.html b/src/main/angular/src/app/pages/knx-group-list-page/knx-group-list-page.component.html index a5c3c4a..4a876e0 100644 --- a/src/main/angular/src/app/pages/knx-group-list-page/knx-group-list-page.component.html +++ b/src/main/angular/src/app/pages/knx-group-list-page/knx-group-list-page.component.html @@ -1,6 +1,6 @@
- +
diff --git a/src/main/angular/src/app/pages/knx-group-list-page/knx-group-list-page.component.ts b/src/main/angular/src/app/pages/knx-group-list-page/knx-group-list-page.component.ts index ba6b1b7..e10e394 100644 --- a/src/main/angular/src/app/pages/knx-group-list-page/knx-group-list-page.component.ts +++ b/src/main/angular/src/app/pages/knx-group-list-page/knx-group-list-page.component.ts @@ -6,13 +6,15 @@ import {FormsModule} from '@angular/forms'; import {GroupFilter} from '../../api/Group/GroupFilter'; import {Subscription} from 'rxjs'; import {ApiService} from '../../api/common/api.service'; +import {SearchComponent} from '../../shared/search/search.component'; @Component({ selector: 'app-knx-group-list-page', standalone: true, imports: [ KnxGroupListComponent, - FormsModule + FormsModule, + SearchComponent ], templateUrl: './knx-group-list-page.component.html', styleUrl: './knx-group-list-page.component.less' @@ -25,8 +27,6 @@ export class KnxGroupListPageComponent implements OnInit, OnDestroy { protected filter: GroupFilter = new GroupFilter(); - private fetchTimeout: any; - constructor( protected readonly groupService: GroupService, protected readonly apiService: ApiService, @@ -44,16 +44,8 @@ export class KnxGroupListPageComponent implements OnInit, OnDestroy { this.subs.forEach(sub => sub.unsubscribe()); } - fetchDelayed() { - if (this.fetchTimeout) { - clearTimeout(this.fetchTimeout); - this.fetchTimeout = undefined; - } - this.fetchTimeout = setTimeout(() => this.fetch(), 300) - } - - private fetch() { - this.groupService.list(this.filter, list => this.groupList = list) + protected fetch() { + this.groupService.filter(this.filter, list => this.groupList = list) } private updateGroup(group: Group) { diff --git a/src/main/angular/src/app/pages/shutter-list-page/shutter-list-page.component.html b/src/main/angular/src/app/pages/shutter-list-page/shutter-list-page.component.html deleted file mode 100644 index fb20acd..0000000 --- a/src/main/angular/src/app/pages/shutter-list-page/shutter-list-page.component.html +++ /dev/null @@ -1,8 +0,0 @@ -
-
- -
-
- -
-
diff --git a/src/main/angular/src/app/pages/shutter-list-page/shutter-list-page.component.less b/src/main/angular/src/app/pages/shutter-list-page/shutter-list-page.component.less deleted file mode 100644 index e4e2c79..0000000 --- a/src/main/angular/src/app/pages/shutter-list-page/shutter-list-page.component.less +++ /dev/null @@ -1,5 +0,0 @@ -@import "../../../config"; - -input { - border-bottom: @border solid lightgray; -} diff --git a/src/main/angular/src/app/pages/shutter-list-page/shutter-list-page.component.ts b/src/main/angular/src/app/pages/shutter-list-page/shutter-list-page.component.ts deleted file mode 100644 index 31d5f42..0000000 --- a/src/main/angular/src/app/pages/shutter-list-page/shutter-list-page.component.ts +++ /dev/null @@ -1,68 +0,0 @@ -import {Component, OnDestroy, OnInit} from '@angular/core'; -import {ShutterListComponent} from '../../shared/shutter-list/shutter-list.component'; -import {Shutter} from '../../api/Shutter/Shutter'; -import {ShutterService} from '../../api/Shutter/shutter.service'; -import {FormsModule} from '@angular/forms'; -import {ShutterFilter} from '../../api/Shutter/ShutterFilter'; -import {Subscription} from 'rxjs'; -import {ApiService} from '../../api/common/api.service'; - -@Component({ - selector: 'app-shutter-list-page', - standalone: true, - imports: [ - ShutterListComponent, - FormsModule - ], - templateUrl: './shutter-list-page.component.html', - styleUrl: './shutter-list-page.component.less' -}) -export class ShutterListPageComponent implements OnInit, OnDestroy { - - private readonly subs: Subscription[] = []; - - protected shutterList: Shutter[] = []; - - protected filter: ShutterFilter = new ShutterFilter(); - - private fetchTimeout: any; - - constructor( - protected readonly shutterService: ShutterService, - protected readonly apiService: ApiService, - ) { - // - } - - ngOnInit(): void { - this.fetch(); - this.subs.push(this.shutterService.subscribe(shutter => this.updateShutter(shutter))); - this.apiService.connected(() => this.fetch()); - } - - ngOnDestroy(): void { - this.subs.forEach(sub => sub.unsubscribe()); - } - - fetchDelayed() { - if (this.fetchTimeout) { - clearTimeout(this.fetchTimeout); - this.fetchTimeout = undefined; - } - this.fetchTimeout = setTimeout(() => this.fetch(), 300) - } - - private fetch() { - this.shutterService.list(this.filter, list => this.shutterList = list) - } - - private updateShutter(shutter: Shutter) { - const index = this.shutterList.findIndex(d => d.uuid === shutter.uuid); - if (index >= 0) { - this.shutterList.splice(index, 1, shutter); - } else { - this.fetch(); - } - } - -} diff --git a/src/main/angular/src/app/pages/taggable-list-page/taggable-list-page.component.html b/src/main/angular/src/app/pages/taggable-list-page/taggable-list-page.component.html new file mode 100644 index 0000000..4eb25e0 --- /dev/null +++ b/src/main/angular/src/app/pages/taggable-list-page/taggable-list-page.component.html @@ -0,0 +1,8 @@ +
+
+ +
+
+ +
+
diff --git a/src/main/angular/src/app/pages/taggable-list-page/taggable-list-page.component.less b/src/main/angular/src/app/pages/taggable-list-page/taggable-list-page.component.less new file mode 100644 index 0000000..e69de29 diff --git a/src/main/angular/src/app/pages/taggable-list-page/taggable-list-page.component.ts b/src/main/angular/src/app/pages/taggable-list-page/taggable-list-page.component.ts new file mode 100644 index 0000000..3d7a513 --- /dev/null +++ b/src/main/angular/src/app/pages/taggable-list-page/taggable-list-page.component.ts @@ -0,0 +1,68 @@ +import {Component, OnDestroy, OnInit} from '@angular/core'; +import {TaggableListComponent} from '../../shared/taggable-list/taggable-list.component'; +import {Taggable} from '../../api/Taggable/Taggable'; +import {TaggableService} from '../../api/Taggable/taggable.service'; +import {FormsModule} from '@angular/forms'; +import {Subscription} from 'rxjs'; +import {TaggableFilter} from '../../api/Taggable/TaggableFilter'; +import {ActivatedRoute} from '@angular/router'; +import {CrudLiveList} from '../../api/common/CrudLiveList'; +import {SearchComponent} from '../../shared/search/search.component'; + +@Component({ + selector: 'app-taggable-list-page', + standalone: true, + imports: [ + TaggableListComponent, + FormsModule, + SearchComponent + ], + templateUrl: './taggable-list-page.component.html', + styleUrl: './taggable-list-page.component.less' +}) +export class TaggableListPageComponent implements OnInit, OnDestroy { + + private readonly subs: Subscription[] = []; + + protected readonly filter: TaggableFilter = new TaggableFilter(); + + protected readonly liveList: CrudLiveList; + + private tagSet: boolean = false; + + constructor( + protected readonly taggableService: TaggableService, + protected readonly activatedRoute: ActivatedRoute, + ) { + this.subs.push(this.liveList = new CrudLiveList( + this.taggableService, + false, + undefined, + next => { + if (this.tagSet) { + this.taggableService.filter(this.filter, next); + } else { + next([]); + } + }) + ); + } + + ngOnInit(): void { + this.subs.push(this.activatedRoute.params.subscribe(params => { + this.tagSet = 'tag' in params; + if (this.tagSet) { + this.filter.tag = params['tag'] || ''; + console.log(this.filter.tag); + this.liveList.refresh(); + } else { + this.liveList.clear(); + } + })); + } + + ngOnDestroy(): void { + this.subs.forEach(sub => sub.unsubscribe()); + } + +} diff --git a/src/main/angular/src/app/pages/tunable-list-page/tunable-list-page.component.html b/src/main/angular/src/app/pages/tunable-list-page/tunable-list-page.component.html deleted file mode 100644 index 868a31d..0000000 --- a/src/main/angular/src/app/pages/tunable-list-page/tunable-list-page.component.html +++ /dev/null @@ -1,8 +0,0 @@ -
-
- -
-
- -
-
diff --git a/src/main/angular/src/app/pages/tunable-list-page/tunable-list-page.component.less b/src/main/angular/src/app/pages/tunable-list-page/tunable-list-page.component.less deleted file mode 100644 index e4e2c79..0000000 --- a/src/main/angular/src/app/pages/tunable-list-page/tunable-list-page.component.less +++ /dev/null @@ -1,5 +0,0 @@ -@import "../../../config"; - -input { - border-bottom: @border solid lightgray; -} diff --git a/src/main/angular/src/app/pages/tunable-list-page/tunable-list-page.component.ts b/src/main/angular/src/app/pages/tunable-list-page/tunable-list-page.component.ts deleted file mode 100644 index 9d306fa..0000000 --- a/src/main/angular/src/app/pages/tunable-list-page/tunable-list-page.component.ts +++ /dev/null @@ -1,68 +0,0 @@ -import {Component, OnDestroy, OnInit} from '@angular/core'; -import {TunableListComponent} from '../../shared/tunable-list/tunable-list.component'; -import {Tunable} from '../../api/Tunable/Tunable'; -import {TunableService} from '../../api/Tunable/tunable.service'; -import {FormsModule} from '@angular/forms'; -import {TunableFilter} from '../../api/Tunable/TunableFilter'; -import {Subscription} from 'rxjs'; -import {ApiService} from '../../api/common/api.service'; - -@Component({ - selector: 'app-tunable-list-page', - standalone: true, - imports: [ - TunableListComponent, - FormsModule - ], - templateUrl: './tunable-list-page.component.html', - styleUrl: './tunable-list-page.component.less' -}) -export class TunableListPageComponent implements OnInit, OnDestroy { - - private readonly subs: Subscription[] = []; - - protected tunableList: Tunable[] = []; - - protected filter: TunableFilter = new TunableFilter(); - - private fetchTimeout: any; - - constructor( - protected readonly tunableService: TunableService, - protected readonly apiService: ApiService, - ) { - // - } - - ngOnInit(): void { - this.fetch(); - this.apiService.connected(() => this.fetch()); - this.subs.push(this.tunableService.subscribe(tunable => this.updateTunable(tunable))); - } - - ngOnDestroy(): void { - this.subs.forEach(sub => sub.unsubscribe()); - } - - fetchDelayed() { - if (this.fetchTimeout) { - clearTimeout(this.fetchTimeout); - this.fetchTimeout = undefined; - } - this.fetchTimeout = setTimeout(() => this.fetch(), 300) - } - - private fetch() { - this.tunableService.list(this.filter, list => this.tunableList = list) - } - - private updateTunable(tunable: Tunable) { - const index = this.tunableList.findIndex(d => d.uuid === tunable.uuid); - if (index >= 0) { - this.tunableList.splice(index, 1, tunable); - } else { - this.fetch(); - } - } - -} diff --git a/src/main/angular/src/app/shared/knx-group-tile/knx-group-tile.component.less b/src/main/angular/src/app/shared/knx-group-tile/knx-group-tile.component.less index 31c9f8e..d029cce 100644 --- a/src/main/angular/src/app/shared/knx-group-tile/knx-group-tile.component.less +++ b/src/main/angular/src/app/shared/knx-group-tile/knx-group-tile.component.less @@ -3,7 +3,6 @@ .group { .name { - margin-bottom: @space; } } diff --git a/src/main/angular/src/app/shared/search/search.component.html b/src/main/angular/src/app/shared/search/search.component.html new file mode 100644 index 0000000..35be3eb --- /dev/null +++ b/src/main/angular/src/app/shared/search/search.component.html @@ -0,0 +1,3 @@ +
+ +
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..5fb206a --- /dev/null +++ b/src/main/angular/src/app/shared/search/search.component.less @@ -0,0 +1,6 @@ +@import "../../../config"; + +.box { + width: 100%; + padding: @space @space 0 @space; +} 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..d19624e --- /dev/null +++ b/src/main/angular/src/app/shared/search/search.component.ts @@ -0,0 +1,35 @@ +import {Component, EventEmitter, Input, Output} from '@angular/core'; +import {FormsModule} from '@angular/forms'; + +@Component({ + selector: 'app-search', + standalone: true, + imports: [ + FormsModule + ], + templateUrl: './search.component.html', + styleUrl: './search.component.less' +}) +export class SearchComponent { + + @Input() + search: string = ''; + + @Output() + searchChange: EventEmitter = new EventEmitter(); + + @Output() + doSearch: EventEmitter = new EventEmitter(); + + private fetchTimeout: any; + + fetchDelayed() { + if (this.fetchTimeout) { + clearTimeout(this.fetchTimeout); + this.fetchTimeout = undefined; + } + this.searchChange.emit(this.search); + this.fetchTimeout = setTimeout(() => this.doSearch.emit(this.search), 300); + } + +} diff --git a/src/main/angular/src/app/shared/taggable-list/taggable-list.component.html b/src/main/angular/src/app/shared/taggable-list/taggable-list.component.html new file mode 100644 index 0000000..1cd9551 --- /dev/null +++ b/src/main/angular/src/app/shared/taggable-list/taggable-list.component.html @@ -0,0 +1,5 @@ +
+ + + +
diff --git a/src/main/angular/src/app/shared/taggable-list/taggable-list.component.less b/src/main/angular/src/app/shared/taggable-list/taggable-list.component.less new file mode 100644 index 0000000..7adc753 --- /dev/null +++ b/src/main/angular/src/app/shared/taggable-list/taggable-list.component.less @@ -0,0 +1 @@ +@import "../../../config"; diff --git a/src/main/angular/src/app/shared/taggable-list/taggable-list.component.ts b/src/main/angular/src/app/shared/taggable-list/taggable-list.component.ts new file mode 100644 index 0000000..283d15f --- /dev/null +++ b/src/main/angular/src/app/shared/taggable-list/taggable-list.component.ts @@ -0,0 +1,35 @@ +import {Component, Input, OnDestroy, OnInit} from '@angular/core'; +import {NgForOf} from '@angular/common'; +import {Subscription, timer} from 'rxjs'; +import {TaggableTileComponent} from '../taggable-tile/taggable-tile.component'; +import {Taggable} from '../../api/Taggable/Taggable'; + +@Component({ + selector: 'app-taggable-list', + standalone: true, + imports: [ + NgForOf, + TaggableTileComponent + ], + templateUrl: './taggable-list.component.html', + styleUrl: './taggable-list.component.less' +}) +export class TaggableListComponent implements OnInit, OnDestroy { + + private readonly subs: Subscription[] = []; + + protected now: Date = new Date(); + + @Input() + list: Taggable[] = []; + + ngOnInit(): void { + this.now = new Date(); + this.subs.push(timer(1000, 1000).subscribe(() => this.now = new Date())); + } + + ngOnDestroy(): void { + this.subs.forEach(sub => sub.unsubscribe()); + } + +} diff --git a/src/main/angular/src/app/shared/taggable-tile/taggable-tile.component.html b/src/main/angular/src/app/shared/taggable-tile/taggable-tile.component.html new file mode 100644 index 0000000..4db6b41 --- /dev/null +++ b/src/main/angular/src/app/shared/taggable-tile/taggable-tile.component.html @@ -0,0 +1,5 @@ + + + + + diff --git a/src/main/angular/src/app/shared/taggable-tile/taggable-tile.component.less b/src/main/angular/src/app/shared/taggable-tile/taggable-tile.component.less new file mode 100644 index 0000000..e69de29 diff --git a/src/main/angular/src/app/shared/taggable-tile/taggable-tile.component.ts b/src/main/angular/src/app/shared/taggable-tile/taggable-tile.component.ts new file mode 100644 index 0000000..43e4d0d --- /dev/null +++ b/src/main/angular/src/app/shared/taggable-tile/taggable-tile.component.ts @@ -0,0 +1,55 @@ +import {Component, Input} from '@angular/core'; +import {Device} from '../../api/Device/Device'; +import {Tunable} from '../../api/Tunable/Tunable'; +import {Shutter} from '../../api/Shutter/Shutter'; +import {DeviceTileComponent} from '../device-tile/device-tile.component'; +import {NgIf} from '@angular/common'; +import {ShutterTileComponent} from '../shutter-tile/shutter-tile.component'; +import {TunableTileComponent} from '../tunable-tile/tunable-tile.component'; +import {Taggable} from '../../api/Taggable/Taggable'; + +@Component({ + selector: 'app-taggable-tile', + standalone: true, + imports: [ + DeviceTileComponent, + NgIf, + ShutterTileComponent, + TunableTileComponent + ], + templateUrl: './taggable-tile.component.html', + styleUrl: './taggable-tile.component.less' +}) +export class TaggableTileComponent { + + @Input() + now!: Date; + + @Input() + taggable!: Taggable; + + asDevice(): Device { + return this.taggable as Device; + } + + isDevice(): boolean { + return this.taggable instanceof Device; + } + + asShutter(): Shutter { + return this.taggable as Shutter; + } + + isShutter(): boolean { + return this.taggable instanceof Shutter; + } + + asTunable(): Tunable { + return this.taggable as Tunable; + } + + isTunable(): boolean { + return this.taggable instanceof Tunable; + } + +} diff --git a/src/main/angular/src/styles.less b/src/main/angular/src/styles.less index 9e0e04d..74f7bcf 100644 --- a/src/main/angular/src/styles.less +++ b/src/main/angular/src/styles.less @@ -19,9 +19,8 @@ div { input[type=text] { all: unset; - width: calc(100% - 2 * 0.2em - @border); - padding-left: 0.2em; - padding-right: 0.2em; + width: 100%; + border: @border solid lightgray; } .emptyBox{ diff --git a/src/main/java/de/ph87/home/area/Area.java b/src/main/java/de/ph87/home/area/Area.java index 8c397eb..e20e5cb 100644 --- a/src/main/java/de/ph87/home/area/Area.java +++ b/src/main/java/de/ph87/home/area/Area.java @@ -1,5 +1,6 @@ package de.ph87.home.area; +import de.ph87.home.search.ISearchable; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.Id; @@ -8,13 +9,14 @@ import lombok.NoArgsConstructor; import lombok.NonNull; import lombok.ToString; +import java.util.List; import java.util.UUID; @Entity @Getter @ToString @NoArgsConstructor -public class Area { +public class Area implements ISearchable { @Id @NonNull @@ -28,6 +30,11 @@ public class Area { @Column(nullable = false, unique = true) private String slug; + @Override + public List getSearchableValues() { + return List.of(slug, name); + } + public Area(@NonNull final String name, @NonNull final String slug) { this.name = name; this.slug = slug; diff --git a/src/main/java/de/ph87/home/area/AreaFilter.java b/src/main/java/de/ph87/home/area/AreaFilter.java index eccc806..bbf3f77 100644 --- a/src/main/java/de/ph87/home/area/AreaFilter.java +++ b/src/main/java/de/ph87/home/area/AreaFilter.java @@ -6,12 +6,14 @@ import lombok.Getter; import lombok.NonNull; import lombok.ToString; +import static de.ph87.home.common.crud.SearchHelper.search; + @Getter @ToString public class AreaFilter extends AbstractSearchFilter { public boolean filter(@NonNull final AreaDto dto) throws PropertyTypeMismatch { - return search(dto.getName()); + return search(search, dto.getName()); } } diff --git a/src/main/java/de/ph87/home/common/ListHelpers.java b/src/main/java/de/ph87/home/common/ListHelpers.java new file mode 100644 index 0000000..9d97642 --- /dev/null +++ b/src/main/java/de/ph87/home/common/ListHelpers.java @@ -0,0 +1,21 @@ +package de.ph87.home.common; + +import lombok.NonNull; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class ListHelpers { + + @NonNull + @SafeVarargs + public static List merge(@NonNull final List... listList) { + final List merged = new ArrayList<>(Arrays.stream(listList).reduce(0, (total, subList) -> total + subList.size(), Integer::sum)); + for (final List ts : listList) { + merged.addAll(ts); + } + return merged; + } + +} diff --git a/src/main/java/de/ph87/home/common/crud/AbstractSearchFilter.java b/src/main/java/de/ph87/home/common/crud/AbstractSearchFilter.java index ca14e91..b3aaeb9 100644 --- a/src/main/java/de/ph87/home/common/crud/AbstractSearchFilter.java +++ b/src/main/java/de/ph87/home/common/crud/AbstractSearchFilter.java @@ -11,6 +11,6 @@ public abstract class AbstractSearchFilter implements ISearch { @Nullable @JsonProperty - private String search; + protected String search; } diff --git a/src/main/java/de/ph87/home/common/crud/ISearch.java b/src/main/java/de/ph87/home/common/crud/ISearch.java index 5ff4a4c..8946165 100644 --- a/src/main/java/de/ph87/home/common/crud/ISearch.java +++ b/src/main/java/de/ph87/home/common/crud/ISearch.java @@ -13,22 +13,4 @@ public interface ISearch { @Nullable String getSearch(); - default boolean search(@NonNull final String... fields) { - final String term = getSearch(); - if (term == null) { - return true; - } - final List haystack = Arrays.stream(fields).map(String::toString).map(String::toLowerCase).toList(); - return splitWords(term).allMatch(word -> anyMatch(word, haystack)); - } - - @NonNull - default Stream splitWords(@NonNull final String term) { - return Arrays.stream(term.toLowerCase(Locale.ROOT).replaceAll("^\\s|\\s$", "").split("\\s+")); - } - - default boolean anyMatch(@NonNull final String needle, @NonNull final List haystack) { - return haystack.stream().anyMatch(lowerCaseHayStack -> lowerCaseHayStack.contains(needle)); - } - } diff --git a/src/main/java/de/ph87/home/common/crud/SearchHelper.java b/src/main/java/de/ph87/home/common/crud/SearchHelper.java new file mode 100644 index 0000000..d833262 --- /dev/null +++ b/src/main/java/de/ph87/home/common/crud/SearchHelper.java @@ -0,0 +1,30 @@ +package de.ph87.home.common.crud; + +import jakarta.annotation.Nullable; +import lombok.NonNull; + +import java.util.Arrays; +import java.util.List; +import java.util.Locale; +import java.util.stream.Stream; + +public class SearchHelper { + + public static boolean search(@Nullable final String term, @NonNull final Object... haystack) { + if (term == null) { + return true; + } + final List haystackValues = Arrays.stream(haystack).map(Object::toString).map(String::toLowerCase).toList(); + return splitWords(term).allMatch(word -> anyMatch(word, haystackValues)); + } + + @NonNull + public static Stream splitWords(@NonNull final String term) { + return Arrays.stream(term.toLowerCase(Locale.ROOT).replaceAll("^\\s|\\s$", "").split("\\s+")); + } + + public static boolean anyMatch(@NonNull final String needle, @NonNull final List haystack) { + return haystack.stream().anyMatch(lowerCaseHayStack -> lowerCaseHayStack.contains(needle)); + } + +} diff --git a/src/main/java/de/ph87/home/demo/DemoService.java b/src/main/java/de/ph87/home/demo/DemoService.java index 7d4a54e..2cd7670 100644 --- a/src/main/java/de/ph87/home/demo/DemoService.java +++ b/src/main/java/de/ph87/home/demo/DemoService.java @@ -6,6 +6,8 @@ import de.ph87.home.device.DeviceService; import de.ph87.home.knx.property.KnxPropertyService; import de.ph87.home.knx.property.KnxPropertyType; import de.ph87.home.shutter.ShutterService; +import de.ph87.home.tag.TagDto; +import de.ph87.home.tag.TagService; import de.ph87.home.tunable.TunableService; import jakarta.annotation.Nullable; import lombok.NonNull; @@ -17,6 +19,8 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import tuwien.auto.calimero.GroupAddress; +import java.util.Arrays; + @Slf4j @Service @Transactional @@ -34,32 +38,48 @@ public class DemoService { private final AreaService areaService; + private final TagService tagService; + @EventListener(ApplicationStartedEvent.class) public void startup() { + final TagDto tagLight = tagService.create("light", "Licht"); + final TagDto tagDevice = tagService.create("device", "Gerät"); + final TagDto tagDecoration = tagService.create("decoration", "Dekoration"); + final TagDto tagDecorationInside = tagService.create("decoration_inside", "Dekoration Innen"); + final TagDto tagDecorationWindow = tagService.create("decoration_window", "Dekoration Fenster"); + final TagDto tagDecorationOutside = tagService.create("decoration_outside", "Dekoration Außen"); + final TagDto tagAudio = tagService.create("media_audio", "Audio"); + final TagDto tagVideo = tagService.create("media_video", "Video"); + final TagDto tagMedia = tagService.create("media", "Media"); + final TagDto tagShutter = tagService.create("shutter", "Rollladen"); + final TagDto tagFront = tagService.create("house_front", "Haus Vorne"); + final TagDto tagSide = tagService.create("house_side", "Haus Seite"); + final TagDto tagHinten = tagService.create("house_backside", "Haus Hinten"); + final AreaDto eg = area("eg", "EG"); final AreaDto wohnzimmer = area("wohnzimmer", "Wohnzimmer"); - device(wohnzimmer, "fernseher", "Fernseher", 20, 4); - device(wohnzimmer, "verstaerker", "Verstärker", 825, 824); - tunable(wohnzimmer, "haengelampe", "Hängelampe", 1794, 1799, null, null, null, null); - tunable(wohnzimmer, "fensterdeko", "Fenster", 1823, 1822, null, null, null, null); - tunable(wohnzimmer, "spots", "", 28, 828, 2344, 2343, 1825, 1824); - shutter(wohnzimmer, "links", "Links", 1048); - shutter(wohnzimmer, "rechts", "Rechts", 1811); + device(wohnzimmer, "fernseher", "Fernseher", 20, 4, tagDevice, tagMedia, tagVideo); + device(wohnzimmer, "verstaerker", "Verstärker", 825, 824, tagDevice, tagMedia, tagAudio); + device(wohnzimmer, "haengelampe", "Hängelampe", 1794, 1799, tagLight); + device(wohnzimmer, "fensterdeko", "Fenster", 1823, 1822, tagDecoration, tagDecorationWindow); + tunable(wohnzimmer, "spots", "", 28, 828, 2344, 2343, 1825, 1824, tagLight); + shutter(wohnzimmer, "links", "Links", 1048, tagShutter, tagFront); + shutter(wohnzimmer, "rechts", "Rechts", 1811, tagShutter, tagFront); final AreaDto kueche = area("kueche", "Küche"); - tunable(kueche, "kueche_spots", "", 2311, 2304, 2342, 2341, 2321, 2317); - shutter(kueche, "kueche_seite", "Seite", 2316); - shutter(kueche, "kueche_theke", "Theke", 2320); - shutter(kueche, "kueche_tuer", "Tür", 2324); + tunable(kueche, "kueche_spots", "", 2311, 2304, 2342, 2341, 2321, 2317, tagLight); + shutter(kueche, "kueche_seite", "Seite", 2316, tagShutter, tagSide); + shutter(kueche, "kueche_theke", "Theke", 2320, tagShutter, tagHinten); + shutter(kueche, "kueche_tuer", "Tür", 2324, tagShutter, tagHinten); - tunable(eg, "eg_ambiente", "Ambiente", 849, 848, null, null, null, null); + device(eg, "eg_ambiente", "Ambiente", 849, 848, tagLight); final AreaDto arbeitszimmer = area("arbeitszimmer", "Arbeitszimmer"); - tunable(arbeitszimmer, "spots", "", 2058, 2057, 2067, 2069, 2049, 2054); + tunable(arbeitszimmer, "spots", "", 2058, 2057, 2067, 2069, 2049, 2054, tagLight); final AreaDto keller = area("keller", "Keller"); - device(keller, "receiver", "Receiver", 2561, 2560); + device(keller, "receiver", "Receiver", 2561, 2560, tagDevice); } @NonNull @@ -72,23 +92,26 @@ public class DemoService { @NonNull final String subSlug, @NonNull final String name, @Nullable final Integer stateRead, - @Nullable final Integer stateWrite) { + @Nullable final Integer stateWrite, + @NonNull final TagDto... tagList + ) { final String slug = area.getSlug() + "_" + subSlug; final String statePropertyId = slug + "_state"; knxPropertyService.create(statePropertyId, KnxPropertyType.BOOLEAN, adr(stateRead), adr(stateWrite)); - deviceService.create(area.getUuid(), name, slug, statePropertyId); + deviceService.create(area.getUuid(), name, slug, statePropertyId, Arrays.stream(tagList).map(TagDto::getUuid).toList()); } private void shutter( @NonNull final AreaDto area, @NonNull final String subSlug, @NonNull final String name, - @Nullable final Integer positionReadWrite + @Nullable final Integer positionReadWrite, + @NonNull final TagDto... tagList ) { final String slug = area.getSlug() + "_" + subSlug; final String statePropertyId = slug + "_state"; knxPropertyService.create(statePropertyId, KnxPropertyType.DOUBLE, adr(positionReadWrite), adr(positionReadWrite)); - shutterService.create(area.getUuid(), name, slug, statePropertyId); + shutterService.create(area.getUuid(), name, slug, statePropertyId, Arrays.stream(tagList).map(TagDto::getUuid).toList()); } private void tunable( @@ -100,13 +123,14 @@ public class DemoService { @Nullable final Integer brightnessRead, @Nullable final Integer brightnessWrite, @Nullable final Integer coldnessRead, - @Nullable final Integer coldnessWrite + @Nullable final Integer coldnessWrite, + @NonNull final TagDto... tagList ) { final String slug = area.getSlug() + "_" + subSlug; final String stateProperty = knxProperty(slug + "_state", KnxPropertyType.BOOLEAN, stateRead, stateWrite); final String brightnessProperty = knxProperty(slug + "_brightness", KnxPropertyType.DOUBLE, brightnessRead, brightnessWrite); final String coldnessProperty = knxProperty(slug + "_coldness", KnxPropertyType.DOUBLE, coldnessRead, coldnessWrite); - tunableService.create(area.getUuid(), name, slug, stateProperty, brightnessProperty, coldnessProperty); + tunableService.create(area.getUuid(), name, slug, stateProperty, brightnessProperty, coldnessProperty, Arrays.stream(tagList).map(TagDto::getUuid).toList()); } @NonNull diff --git a/src/main/java/de/ph87/home/device/Device.java b/src/main/java/de/ph87/home/device/Device.java index d94dec7..374e847 100644 --- a/src/main/java/de/ph87/home/device/Device.java +++ b/src/main/java/de/ph87/home/device/Device.java @@ -1,19 +1,22 @@ package de.ph87.home.device; import de.ph87.home.area.Area; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.Id; -import jakarta.persistence.ManyToOne; +import de.ph87.home.tag.taggable.ITaggable; +import de.ph87.home.tag.Tag; +import jakarta.persistence.*; import lombok.*; +import java.util.ArrayList; +import java.util.List; import java.util.UUID; +import static de.ph87.home.common.ListHelpers.merge; + @Entity @Getter @ToString @NoArgsConstructor -public class Device { +public class Device implements ITaggable { @Id @NonNull @@ -36,11 +39,22 @@ public class Device { @Column(nullable = false) private String statePropertyId; - public Device(final Area area, @NonNull final String name, @NonNull final String slug, @NonNull final String statePropertyId) { + @Getter + @NonNull + @ManyToMany + private List tagList = new ArrayList<>(); + + @Override + public List getSearchableValues() { + return merge(List.of(area.getSlug(), area.getName(), slug, name), tagList.stream().map(Tag::getSlug).toList(), tagList.stream().map(Tag::getName).toList()); + } + + public Device(final Area area, @NonNull final String name, @NonNull final String slug, @NonNull final String statePropertyId, @NonNull final List tagList) { this.area = area; this.name = name; this.slug = slug; this.statePropertyId = statePropertyId; + this.tagList = new ArrayList<>(tagList); } } diff --git a/src/main/java/de/ph87/home/device/DeviceDto.java b/src/main/java/de/ph87/home/device/DeviceDto.java index 2d250c2..f7e2d31 100644 --- a/src/main/java/de/ph87/home/device/DeviceDto.java +++ b/src/main/java/de/ph87/home/device/DeviceDto.java @@ -3,6 +3,7 @@ package de.ph87.home.device; import de.ph87.home.area.AreaDto; import de.ph87.home.property.PropertyDto; import de.ph87.home.property.PropertyTypeMismatch; +import de.ph87.home.tag.Tag; import de.ph87.home.web.IWebSocketMessage; import jakarta.annotation.Nullable; import lombok.Getter; @@ -37,6 +38,9 @@ public class DeviceDto implements IWebSocketMessage { @ToString.Exclude private final PropertyDto stateProperty; + @NonNull + private final List tagList; + public DeviceDto(@NonNull final Device device, @Nullable final PropertyDto stateProperty) { this.area = new AreaDto(device.getArea()); this.uuid = device.getUuid(); @@ -44,6 +48,7 @@ public class DeviceDto implements IWebSocketMessage { this.slug = device.getSlug(); this.statePropertyId = device.getStatePropertyId(); this.stateProperty = stateProperty; + this.tagList = device.getTagList().stream().map(Tag::getName).toList(); } @Nullable diff --git a/src/main/java/de/ph87/home/device/DeviceFilter.java b/src/main/java/de/ph87/home/device/DeviceFilter.java index 04c05ba..b662eee 100644 --- a/src/main/java/de/ph87/home/device/DeviceFilter.java +++ b/src/main/java/de/ph87/home/device/DeviceFilter.java @@ -7,6 +7,8 @@ import lombok.Getter; import lombok.NonNull; import lombok.ToString; +import static de.ph87.home.common.crud.SearchHelper.search; + @Getter @ToString public class DeviceFilter extends AbstractSearchFilter { @@ -31,7 +33,7 @@ public class DeviceFilter extends AbstractSearchFilter { if (!stateFalse && Boolean.FALSE.equals(value)) { return false; } - return search(dto.getName()); + return search(search, dto.getName()); } } diff --git a/src/main/java/de/ph87/home/device/DeviceService.java b/src/main/java/de/ph87/home/device/DeviceService.java index c5c04b5..38cf006 100644 --- a/src/main/java/de/ph87/home/device/DeviceService.java +++ b/src/main/java/de/ph87/home/device/DeviceService.java @@ -5,6 +5,12 @@ import de.ph87.home.area.AreaService; import de.ph87.home.common.crud.CrudAction; import de.ph87.home.common.crud.EntityNotFound; import de.ph87.home.property.*; +import de.ph87.home.search.SearchableDto; +import de.ph87.home.tag.Tag; +import de.ph87.home.tag.TagReader; +import de.ph87.home.tag.TaggableDto; +import de.ph87.home.tag.taggable.ITaggableService; +import de.ph87.home.tag.taggable.TaggableFilter; import jakarta.annotation.Nullable; import lombok.NonNull; import lombok.RequiredArgsConstructor; @@ -14,14 +20,15 @@ import org.springframework.context.event.EventListener; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.ArrayList; import java.util.List; +import static de.ph87.home.common.crud.SearchHelper.search; + @Slf4j @Service @Transactional @RequiredArgsConstructor -public class DeviceService { +public class DeviceService implements ITaggableService { private final PropertyService propertyService; @@ -31,10 +38,13 @@ public class DeviceService { private final AreaService areaService; + private final TagReader tagReader; + @NonNull - public DeviceDto create(@NonNull final String areaUuid, @NonNull final String name, @NonNull final String slug, @NonNull final String stateProperty) { + public DeviceDto create(@NonNull final String areaUuid, @NonNull final String name, @NonNull final String slug, @NonNull final String stateProperty, @NonNull final List tagUuidList) { final Area area = areaService.getByUuid(areaUuid); - return publish(deviceRepository.save(new Device(area, name, slug, stateProperty)), CrudAction.UPDATED); + final List tagList = tagUuidList.stream().map(tagReader::getByUuid).toList(); + return publish(deviceRepository.save(new Device(area, name, slug, stateProperty, tagList)), CrudAction.UPDATED); } public void setState(@NonNull final String uuidOrSlug, final boolean state) throws PropertyNotFound, PropertyNotWritable, PropertyTypeMismatch { @@ -66,17 +76,13 @@ public class DeviceService { @NonNull public List list(@Nullable final DeviceFilter filter) throws PropertyTypeMismatch { - final List all = deviceRepository.findAll().stream().map(this::toDto).toList(); if (filter == null) { - return all; + return deviceRepository.findAll().stream().map(this::toDto).toList(); } - final List results = new ArrayList<>(); - for (final DeviceDto dto : all) { - if (filter.filter(dto)) { - results.add(dto); - } - } - return results; + return deviceRepository.findAll().stream() + .filter(device -> search(filter.getSearch(), device.getSearchableValues())) + .map(this::toDto) + .toList(); } @EventListener(PropertyDto.class) @@ -97,4 +103,23 @@ public class DeviceService { return toDto(getByUuid(uuid)); } + @Override + public List> findTaggables(final @NonNull TaggableFilter filter) { + return deviceRepository.findAll().stream() + .filter(device -> search(filter.getSearch(), device.getSearchableValues())) + .filter(device -> device.tagListAnyMatch(filter.getTag())) + .map(this::toDto) + .map(TaggableDto::new) + .toList(); + } + + @Override + public List> findSearchables(@NonNull final String term) { + return deviceRepository.findAll().stream() + .filter(device -> device.search(term)) + .map(this::toDto) + .map(SearchableDto::new) + .toList(); + } + } diff --git a/src/main/java/de/ph87/home/knx/group/GroupFilter.java b/src/main/java/de/ph87/home/knx/group/GroupFilter.java index e6a0d66..10c23c3 100644 --- a/src/main/java/de/ph87/home/knx/group/GroupFilter.java +++ b/src/main/java/de/ph87/home/knx/group/GroupFilter.java @@ -5,12 +5,14 @@ import lombok.Getter; import lombok.NonNull; import lombok.ToString; +import static de.ph87.home.common.crud.SearchHelper.search; + @Getter @ToString public class GroupFilter extends AbstractSearchFilter { public boolean filter(@NonNull final Group group) { - return search(group.getName(), group.getAddress().toString()); + return search(search, group.getName(), group.getAddress().toString()); } } diff --git a/src/main/java/de/ph87/home/search/ISearchable.java b/src/main/java/de/ph87/home/search/ISearchable.java new file mode 100644 index 0000000..e4f6a0b --- /dev/null +++ b/src/main/java/de/ph87/home/search/ISearchable.java @@ -0,0 +1,16 @@ +package de.ph87.home.search; + +import de.ph87.home.common.crud.SearchHelper; +import lombok.NonNull; + +import java.util.List; + +public interface ISearchable { + + List getSearchableValues(); + + default boolean search(@NonNull String term) { + return SearchHelper.search(term, getSearchableValues()); + } + +} diff --git a/src/main/java/de/ph87/home/search/ISearchableService.java b/src/main/java/de/ph87/home/search/ISearchableService.java new file mode 100644 index 0000000..a6ddb6b --- /dev/null +++ b/src/main/java/de/ph87/home/search/ISearchableService.java @@ -0,0 +1,11 @@ +package de.ph87.home.search; + +import lombok.NonNull; + +import java.util.List; + +public interface ISearchableService { + + List> findSearchables(@NonNull final String term); + +} diff --git a/src/main/java/de/ph87/home/search/SearchController.java b/src/main/java/de/ph87/home/search/SearchController.java new file mode 100644 index 0000000..08d1a7a --- /dev/null +++ b/src/main/java/de/ph87/home/search/SearchController.java @@ -0,0 +1,24 @@ +package de.ph87.home.search; + +import jakarta.annotation.Nullable; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@RestController +@RequiredArgsConstructor +@RequestMapping("search") +public class SearchController { + + private final SearchService searchService; + + @PostMapping(consumes = "text/plain") + public List> searchSearchables(@RequestBody(required = false) @Nullable final String term) { + return searchService.findSearchables(term == null ? "" : term); + } + +} diff --git a/src/main/java/de/ph87/home/search/SearchService.java b/src/main/java/de/ph87/home/search/SearchService.java new file mode 100644 index 0000000..5495631 --- /dev/null +++ b/src/main/java/de/ph87/home/search/SearchService.java @@ -0,0 +1,29 @@ +package de.ph87.home.search; + +import de.ph87.home.common.ListHelpers; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.List; + +@Slf4j +@Service +@Transactional +@RequiredArgsConstructor +public class SearchService { + + private final List> searchableServices; + + @NonNull + public List> findSearchables(@NonNull final String term) { + return searchableServices.stream() + .map(iSearchableService -> iSearchableService.findSearchables(term)) + .reduce(ListHelpers::merge) + .orElse(new ArrayList<>()); + } + +} diff --git a/src/main/java/de/ph87/home/search/SearchableDto.java b/src/main/java/de/ph87/home/search/SearchableDto.java new file mode 100644 index 0000000..e2d46b0 --- /dev/null +++ b/src/main/java/de/ph87/home/search/SearchableDto.java @@ -0,0 +1,17 @@ +package de.ph87.home.search; + +import lombok.Data; + +@Data +public class SearchableDto { + + private final String _type_; + + private final T payload; + + public SearchableDto(final T payload) { + this.payload = payload; + this._type_ = payload.getClass().getSimpleName(); + } + +} diff --git a/src/main/java/de/ph87/home/shutter/Shutter.java b/src/main/java/de/ph87/home/shutter/Shutter.java index b333c39..51b75a3 100644 --- a/src/main/java/de/ph87/home/shutter/Shutter.java +++ b/src/main/java/de/ph87/home/shutter/Shutter.java @@ -1,19 +1,22 @@ package de.ph87.home.shutter; import de.ph87.home.area.Area; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.Id; -import jakarta.persistence.ManyToOne; +import de.ph87.home.tag.taggable.ITaggable; +import de.ph87.home.tag.Tag; +import jakarta.persistence.*; import lombok.*; +import java.util.ArrayList; +import java.util.List; import java.util.UUID; +import static de.ph87.home.common.ListHelpers.merge; + @Entity @Getter @ToString @NoArgsConstructor -public class Shutter { +public class Shutter implements ITaggable { @Id @NonNull @@ -36,11 +39,22 @@ public class Shutter { @Column(nullable = false) private String positionPropertyId; - public Shutter(@NonNull final Area area, @NonNull final String name, @NonNull final String slug, @NonNull final String positionPropertyId) { + @Getter + @NonNull + @ManyToMany + private List tagList = new ArrayList<>(); + + @Override + public List getSearchableValues() { + return merge(List.of(area.getSlug(), area.getName(), slug, name), tagList.stream().map(Tag::getSlug).toList(), tagList.stream().map(Tag::getName).toList()); + } + + public Shutter(@NonNull final Area area, @NonNull final String name, @NonNull final String slug, @NonNull final String positionPropertyId, final List tagList) { this.area = area; this.name = name; this.slug = slug; this.positionPropertyId = positionPropertyId; + this.tagList = new ArrayList<>(tagList); } } diff --git a/src/main/java/de/ph87/home/shutter/ShutterDto.java b/src/main/java/de/ph87/home/shutter/ShutterDto.java index b1f6539..7d8fdf8 100644 --- a/src/main/java/de/ph87/home/shutter/ShutterDto.java +++ b/src/main/java/de/ph87/home/shutter/ShutterDto.java @@ -3,6 +3,7 @@ package de.ph87.home.shutter; import de.ph87.home.area.AreaDto; import de.ph87.home.property.PropertyDto; import de.ph87.home.property.PropertyTypeMismatch; +import de.ph87.home.tag.Tag; import de.ph87.home.web.IWebSocketMessage; import jakarta.annotation.Nullable; import lombok.Getter; @@ -20,6 +21,7 @@ public class ShutterDto implements IWebSocketMessage { @NonNull private final AreaDto area; + @NonNull private final String uuid; @@ -36,6 +38,9 @@ public class ShutterDto implements IWebSocketMessage { @ToString.Exclude private final PropertyDto positionProperty; + @NonNull + private final List tagList; + public ShutterDto(@NonNull final Shutter shutter, @Nullable final PropertyDto positionProperty) { this.area = new AreaDto(shutter.getArea()); this.uuid = shutter.getUuid(); @@ -43,6 +48,7 @@ public class ShutterDto implements IWebSocketMessage { this.slug = shutter.getSlug(); this.positionPropertyId = shutter.getPositionPropertyId(); this.positionProperty = positionProperty; + this.tagList = shutter.getTagList().stream().map(Tag::getName).toList(); } @Nullable diff --git a/src/main/java/de/ph87/home/shutter/ShutterFilter.java b/src/main/java/de/ph87/home/shutter/ShutterFilter.java index a04b5bc..72dd09a 100644 --- a/src/main/java/de/ph87/home/shutter/ShutterFilter.java +++ b/src/main/java/de/ph87/home/shutter/ShutterFilter.java @@ -9,6 +9,8 @@ import lombok.ToString; import java.util.Objects; +import static de.ph87.home.common.crud.SearchHelper.search; + @Getter @ToString public class ShutterFilter extends AbstractSearchFilter { @@ -39,7 +41,7 @@ public class ShutterFilter extends AbstractSearchFilter { if (!positionClosed && Objects.equals(value, 100.0)) { return false; } - return search(dto.getName()); + return search(search, dto.getName()); } } diff --git a/src/main/java/de/ph87/home/shutter/ShutterService.java b/src/main/java/de/ph87/home/shutter/ShutterService.java index 5ac238d..c510293 100644 --- a/src/main/java/de/ph87/home/shutter/ShutterService.java +++ b/src/main/java/de/ph87/home/shutter/ShutterService.java @@ -5,6 +5,12 @@ import de.ph87.home.area.AreaService; import de.ph87.home.common.crud.CrudAction; import de.ph87.home.common.crud.EntityNotFound; import de.ph87.home.property.*; +import de.ph87.home.search.SearchableDto; +import de.ph87.home.tag.Tag; +import de.ph87.home.tag.TagReader; +import de.ph87.home.tag.TaggableDto; +import de.ph87.home.tag.taggable.ITaggableService; +import de.ph87.home.tag.taggable.TaggableFilter; import jakarta.annotation.Nullable; import lombok.NonNull; import lombok.RequiredArgsConstructor; @@ -14,14 +20,15 @@ import org.springframework.context.event.EventListener; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.ArrayList; import java.util.List; +import static de.ph87.home.common.crud.SearchHelper.search; + @Slf4j @Service @Transactional @RequiredArgsConstructor -public class ShutterService { +public class ShutterService implements ITaggableService { private final AreaService areaService; @@ -31,10 +38,19 @@ public class ShutterService { private final ApplicationEventPublisher applicationEventPublisher; + private final TagReader tagReader; + @NonNull - public ShutterDto create(@NonNull final String areaUuid, @NonNull final String name, @NonNull final String slug, @NonNull final String positionProperty) { + public ShutterDto create( + @NonNull final String areaUuid, + @NonNull final String name, + @NonNull final String slug, + @NonNull final String positionProperty, + @NonNull final List tagUuidList + ) { final Area area = areaService.getByUuid(areaUuid); - return publish(shutterRepository.save(new Shutter(area, name, slug, positionProperty)), CrudAction.CREATED); + final List tagList = tagUuidList.stream().map(tagReader::getByUuid).toList(); + return publish(shutterRepository.save(new Shutter(area, name, slug, positionProperty, tagList)), CrudAction.CREATED); } public void setPosition(@NonNull final String uuidOrSlug, final double position) throws PropertyNotFound, PropertyNotWritable, PropertyTypeMismatch { @@ -66,17 +82,13 @@ public class ShutterService { @NonNull public List list(@Nullable final ShutterFilter filter) throws PropertyTypeMismatch { - final List all = shutterRepository.findAll().stream().map(this::toDto).toList(); if (filter == null) { - return all; + return shutterRepository.findAll().stream().map(this::toDto).toList(); } - final List results = new ArrayList<>(); - for (final ShutterDto dto : all) { - if (filter.filter(dto)) { - results.add(dto); - } - } - return results; + return shutterRepository.findAll().stream() + .filter(shutter -> search(filter.getSearch(), shutter.getSearchableValues())) + .map(this::toDto) + .toList(); } @EventListener(PropertyDto.class) @@ -97,4 +109,23 @@ public class ShutterService { return toDto(getByUuid(uuid)); } + @Override + public List> findTaggables(final @NonNull TaggableFilter filter) { + return shutterRepository.findAll().stream() + .filter(shutter -> shutter.tagListAnyMatch(filter.getTag())) + .filter(device -> search(filter.getSearch(), device.getSearchableValues())) + .map(this::toDto) + .map(TaggableDto::new) + .toList(); + } + + @Override + public List> findSearchables(@NonNull final String term) { + return shutterRepository.findAll().stream() + .filter(shutter -> shutter.search(term)) + .map(this::toDto) + .map(SearchableDto::new) + .toList(); + } + } diff --git a/src/main/java/de/ph87/home/tag/Tag.java b/src/main/java/de/ph87/home/tag/Tag.java new file mode 100644 index 0000000..08eed49 --- /dev/null +++ b/src/main/java/de/ph87/home/tag/Tag.java @@ -0,0 +1,36 @@ +package de.ph87.home.tag; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.NonNull; +import lombok.ToString; + +import java.util.UUID; + +@Entity +@Getter +@ToString +@NoArgsConstructor +public class Tag { + + @Id + @NonNull + private String uuid = UUID.randomUUID().toString(); + + @NonNull + @Column(nullable = false, unique = true) + private String slug; + + @NonNull + @Column(nullable = false, unique = true) + private String name; + + public Tag(@NonNull final String slug, @NonNull final String name) { + this.slug = slug; + this.name = name; + } + +} diff --git a/src/main/java/de/ph87/home/tag/TagController.java b/src/main/java/de/ph87/home/tag/TagController.java new file mode 100644 index 0000000..3882f4b --- /dev/null +++ b/src/main/java/de/ph87/home/tag/TagController.java @@ -0,0 +1,12 @@ +package de.ph87.home.tag; + +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +@RequestMapping("Tag") +public class TagController { + +} diff --git a/src/main/java/de/ph87/home/tag/TagDto.java b/src/main/java/de/ph87/home/tag/TagDto.java new file mode 100644 index 0000000..f933b98 --- /dev/null +++ b/src/main/java/de/ph87/home/tag/TagDto.java @@ -0,0 +1,20 @@ +package de.ph87.home.tag; + +import lombok.Data; +import lombok.NonNull; + +@Data +public class TagDto { + + @NonNull + private final String uuid; + + @NonNull + private final String name; + + public TagDto(@NonNull final Tag tag) { + this.uuid = tag.getUuid(); + this.name = tag.getName(); + } + +} diff --git a/src/main/java/de/ph87/home/tag/TagReader.java b/src/main/java/de/ph87/home/tag/TagReader.java new file mode 100644 index 0000000..9bbe665 --- /dev/null +++ b/src/main/java/de/ph87/home/tag/TagReader.java @@ -0,0 +1,22 @@ +package de.ph87.home.tag; + +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@Service +@Transactional +@RequiredArgsConstructor +public class TagReader { + + private final TagRepository tagRepository; + + @NonNull + public Tag getByUuid(@NonNull final String uuid) { + return tagRepository.findById(uuid).orElseThrow(); + } + +} diff --git a/src/main/java/de/ph87/home/tag/TagRepository.java b/src/main/java/de/ph87/home/tag/TagRepository.java new file mode 100644 index 0000000..bb69fd4 --- /dev/null +++ b/src/main/java/de/ph87/home/tag/TagRepository.java @@ -0,0 +1,7 @@ +package de.ph87.home.tag; + +import org.springframework.data.repository.ListCrudRepository; + +public interface TagRepository extends ListCrudRepository { + +} diff --git a/src/main/java/de/ph87/home/tag/TagService.java b/src/main/java/de/ph87/home/tag/TagService.java new file mode 100644 index 0000000..ed02195 --- /dev/null +++ b/src/main/java/de/ph87/home/tag/TagService.java @@ -0,0 +1,22 @@ +package de.ph87.home.tag; + +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@Service +@Transactional +@RequiredArgsConstructor +public class TagService { + + private final TagRepository tagRepository; + + @NonNull + public TagDto create(final @NonNull String slug, @NonNull final String name) { + return new TagDto(tagRepository.save(new Tag(slug, name))); + } + +} diff --git a/src/main/java/de/ph87/home/tag/TaggableDto.java b/src/main/java/de/ph87/home/tag/TaggableDto.java new file mode 100644 index 0000000..cfa1d01 --- /dev/null +++ b/src/main/java/de/ph87/home/tag/TaggableDto.java @@ -0,0 +1,17 @@ +package de.ph87.home.tag; + +import lombok.Data; + +@Data +public class TaggableDto { + + private final String _type_; + + private final T payload; + + public TaggableDto(final T payload) { + this.payload = payload; + this._type_ = payload.getClass().getSimpleName(); + } + +} diff --git a/src/main/java/de/ph87/home/tag/taggable/ITaggable.java b/src/main/java/de/ph87/home/tag/taggable/ITaggable.java new file mode 100644 index 0000000..f2e8fd2 --- /dev/null +++ b/src/main/java/de/ph87/home/tag/taggable/ITaggable.java @@ -0,0 +1,20 @@ +package de.ph87.home.tag.taggable; + +import de.ph87.home.search.ISearchable; +import de.ph87.home.tag.Tag; +import lombok.NonNull; + +import java.util.List; + +public interface ITaggable extends ISearchable { + + List getTagList(); + + default boolean tagListAnyMatch(@NonNull final String term) { + if (term.isEmpty()) { + return true; + } + return getTagList().stream().anyMatch(tag -> tag.getSlug().equalsIgnoreCase(term)); + } + +} diff --git a/src/main/java/de/ph87/home/tag/taggable/ITaggableService.java b/src/main/java/de/ph87/home/tag/taggable/ITaggableService.java new file mode 100644 index 0000000..605d39f --- /dev/null +++ b/src/main/java/de/ph87/home/tag/taggable/ITaggableService.java @@ -0,0 +1,13 @@ +package de.ph87.home.tag.taggable; + +import de.ph87.home.search.ISearchableService; +import de.ph87.home.tag.TaggableDto; +import lombok.NonNull; + +import java.util.List; + +public interface ITaggableService extends ISearchableService { + + List> findTaggables(final @NonNull TaggableFilter filter); + +} diff --git a/src/main/java/de/ph87/home/tag/taggable/TaggableController.java b/src/main/java/de/ph87/home/tag/taggable/TaggableController.java new file mode 100644 index 0000000..fa5ac9d --- /dev/null +++ b/src/main/java/de/ph87/home/tag/taggable/TaggableController.java @@ -0,0 +1,26 @@ +package de.ph87.home.tag.taggable; + +import de.ph87.home.tag.TaggableDto; +import jakarta.annotation.Nullable; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@RestController +@RequiredArgsConstructor +@RequestMapping("Taggable") +public class TaggableController { + + private final TaggableService taggableService; + + @PostMapping(value = "list") + public List> list(@RequestBody @NonNull final TaggableFilter filter) { + return taggableService.list(filter); + } + +} diff --git a/src/main/java/de/ph87/home/tag/taggable/TaggableFilter.java b/src/main/java/de/ph87/home/tag/taggable/TaggableFilter.java new file mode 100644 index 0000000..76afc69 --- /dev/null +++ b/src/main/java/de/ph87/home/tag/taggable/TaggableFilter.java @@ -0,0 +1,15 @@ +package de.ph87.home.tag.taggable; + +import lombok.Data; +import lombok.NonNull; + +@Data +public class TaggableFilter { + + @NonNull + private final String tag; + + @NonNull + private final String search; + +} diff --git a/src/main/java/de/ph87/home/tag/taggable/TaggableService.java b/src/main/java/de/ph87/home/tag/taggable/TaggableService.java new file mode 100644 index 0000000..92ba415 --- /dev/null +++ b/src/main/java/de/ph87/home/tag/taggable/TaggableService.java @@ -0,0 +1,30 @@ +package de.ph87.home.tag.taggable; + +import de.ph87.home.common.ListHelpers; +import de.ph87.home.tag.TaggableDto; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.List; + +@Slf4j +@Service +@Transactional +@RequiredArgsConstructor +public class TaggableService { + + private final List> taggableServices; + + @NonNull + public List> list(@NonNull final TaggableFilter filter) { + return taggableServices.stream() + .map(iTaggableService -> iTaggableService.findTaggables(filter)) + .reduce(ListHelpers::merge) + .orElse(new ArrayList<>()); + } + +} diff --git a/src/main/java/de/ph87/home/tunable/Tunable.java b/src/main/java/de/ph87/home/tunable/Tunable.java index 1930e55..33aeb41 100644 --- a/src/main/java/de/ph87/home/tunable/Tunable.java +++ b/src/main/java/de/ph87/home/tunable/Tunable.java @@ -1,19 +1,22 @@ package de.ph87.home.tunable; import de.ph87.home.area.Area; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.Id; -import jakarta.persistence.ManyToOne; +import de.ph87.home.tag.taggable.ITaggable; +import de.ph87.home.tag.Tag; +import jakarta.persistence.*; import lombok.*; +import java.util.ArrayList; +import java.util.List; import java.util.UUID; +import static de.ph87.home.common.ListHelpers.merge; + @Entity @Getter @ToString @NoArgsConstructor -public class Tunable { +public class Tunable implements ITaggable { @Id @NonNull @@ -46,13 +49,24 @@ public class Tunable { @Column(nullable = false) private String coldnessPropertyId; - public Tunable(@NonNull final Area area, @NonNull final String name, @NonNull final String slug, @NonNull final String statePropertyId, @NonNull final String brightnessPropertyId, @NonNull final String coldnessPropertyId) { + @Getter + @NonNull + @ManyToMany + private List tagList = new ArrayList<>(); + + @Override + public List getSearchableValues() { + return merge(List.of(area.getSlug(), area.getName(), slug, name), tagList.stream().map(Tag::getSlug).toList(), tagList.stream().map(Tag::getName).toList()); + } + + public Tunable(@NonNull final Area area, @NonNull final String name, @NonNull final String slug, @NonNull final String statePropertyId, @NonNull final String brightnessPropertyId, @NonNull final String coldnessPropertyId, final List tagList) { this.area = area; this.name = name; this.slug = slug; this.statePropertyId = statePropertyId; this.brightnessPropertyId = brightnessPropertyId; this.coldnessPropertyId = coldnessPropertyId; + this.tagList = new ArrayList<>(tagList); } } diff --git a/src/main/java/de/ph87/home/tunable/TunableDto.java b/src/main/java/de/ph87/home/tunable/TunableDto.java index 6d41659..c5e9ecb 100644 --- a/src/main/java/de/ph87/home/tunable/TunableDto.java +++ b/src/main/java/de/ph87/home/tunable/TunableDto.java @@ -3,6 +3,7 @@ package de.ph87.home.tunable; import de.ph87.home.area.AreaDto; import de.ph87.home.property.PropertyDto; import de.ph87.home.property.PropertyTypeMismatch; +import de.ph87.home.tag.Tag; import de.ph87.home.web.IWebSocketMessage; import jakarta.annotation.Nullable; import lombok.Getter; @@ -51,6 +52,9 @@ public class TunableDto implements IWebSocketMessage { @ToString.Exclude private final PropertyDto coldnessProperty; + @NonNull + private final List tagList; + public TunableDto(@NonNull final Tunable tunable, @Nullable final PropertyDto stateProperty, @Nullable final PropertyDto brightnessProperty, @Nullable final PropertyDto coldnessProperty) { this.area = new AreaDto(tunable.getArea()); this.uuid = tunable.getUuid(); @@ -62,6 +66,7 @@ public class TunableDto implements IWebSocketMessage { this.stateProperty = stateProperty; this.brightnessProperty = brightnessProperty; this.coldnessProperty = coldnessProperty; + this.tagList = tunable.getTagList().stream().map(Tag::getName).toList(); } @Nullable diff --git a/src/main/java/de/ph87/home/tunable/TunableFilter.java b/src/main/java/de/ph87/home/tunable/TunableFilter.java index e39987d..172c272 100644 --- a/src/main/java/de/ph87/home/tunable/TunableFilter.java +++ b/src/main/java/de/ph87/home/tunable/TunableFilter.java @@ -7,6 +7,8 @@ import lombok.Getter; import lombok.NonNull; import lombok.ToString; +import static de.ph87.home.common.crud.SearchHelper.search; + @Getter @ToString public class TunableFilter extends AbstractSearchFilter { @@ -31,7 +33,7 @@ public class TunableFilter extends AbstractSearchFilter { if (!stateFalse && Boolean.FALSE.equals(value)) { return false; } - return search(dto.getName()); + return search(search, dto.getName()); } } diff --git a/src/main/java/de/ph87/home/tunable/TunableService.java b/src/main/java/de/ph87/home/tunable/TunableService.java index b67ff6e..b2613b0 100644 --- a/src/main/java/de/ph87/home/tunable/TunableService.java +++ b/src/main/java/de/ph87/home/tunable/TunableService.java @@ -5,6 +5,12 @@ import de.ph87.home.area.AreaService; import de.ph87.home.common.crud.CrudAction; import de.ph87.home.common.crud.EntityNotFound; import de.ph87.home.property.*; +import de.ph87.home.search.SearchableDto; +import de.ph87.home.tag.Tag; +import de.ph87.home.tag.TagReader; +import de.ph87.home.tag.TaggableDto; +import de.ph87.home.tag.taggable.ITaggableService; +import de.ph87.home.tag.taggable.TaggableFilter; import jakarta.annotation.Nullable; import lombok.NonNull; import lombok.RequiredArgsConstructor; @@ -14,14 +20,15 @@ import org.springframework.context.event.EventListener; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.ArrayList; import java.util.List; +import static de.ph87.home.common.crud.SearchHelper.search; + @Slf4j @Service @Transactional @RequiredArgsConstructor -public class TunableService { +public class TunableService implements ITaggableService { private final PropertyService propertyService; @@ -31,10 +38,21 @@ public class TunableService { private final AreaService areaService; + private final TagReader tagReader; + @NonNull - public TunableDto create(@NonNull final String areaUuid, @NonNull final String name, @NonNull final String slug, @NonNull final String stateProperty, @NonNull final String brightnessProperty, @NonNull final String coldnessProperty) { + public TunableDto create( + @NonNull final String areaUuid, + @NonNull final String name, + @NonNull final String slug, + @NonNull final String stateProperty, + @NonNull final String brightnessProperty, + @NonNull final String coldnessProperty, + @NonNull final List tagUuidList + ) { final Area area = areaService.getByUuid(areaUuid); - return publish(tunableRepository.save(new Tunable(area, name, slug, stateProperty, brightnessProperty, coldnessProperty)), CrudAction.UPDATED); + final List tagList = tagUuidList.stream().map(tagReader::getByUuid).toList(); + return publish(tunableRepository.save(new Tunable(area, name, slug, stateProperty, brightnessProperty, coldnessProperty, tagList)), CrudAction.UPDATED); } @NonNull @@ -86,17 +104,13 @@ public class TunableService { @NonNull public List list(@Nullable final TunableFilter filter) throws PropertyTypeMismatch { - final List all = tunableRepository.findAll().stream().map(this::toDto).toList(); if (filter == null) { - return all; + return tunableRepository.findAll().stream().map(this::toDto).toList(); } - final List results = new ArrayList<>(); - for (final TunableDto dto : all) { - if (filter.filter(dto)) { - results.add(dto); - } - } - return results; + return tunableRepository.findAll().stream() + .filter(tunable -> search(filter.getSearch(), tunable.getSearchableValues())) + .map(this::toDto) + .toList(); } @EventListener(PropertyDto.class) @@ -117,4 +131,23 @@ public class TunableService { return toDto(getByUuid(uuid)); } + @Override + public List> findTaggables(final @NonNull TaggableFilter filter) { + return tunableRepository.findAll().stream() + .filter(tunable -> tunable.tagListAnyMatch(filter.getTag())) + .filter(device -> search(filter.getSearch(), device.getSearchableValues())) + .map(this::toDto) + .map(TaggableDto::new) + .toList(); + } + + @Override + public List> findSearchables(@NonNull final String term) { + return tunableRepository.findAll().stream() + .filter(tunable -> tunable.search(term)) + .map(this::toDto) + .map(SearchableDto::new) + .toList(); + } + }