added search response into searchmodal ui, restructured folders

This commit is contained in:
2026-06-24 15:57:10 +02:00
parent a1ce085123
commit 6e3d3347e2
16 changed files with 110 additions and 34 deletions
@@ -64,10 +64,11 @@ public class HomeboxSearchProvider implements SearchProvider {
} }
List<SearchResponse> items = hbResponse.items().stream().map(i -> { List<SearchResponse> items = hbResponse.items().stream().map(i -> {
String id = i.id();
String title = i.name(); String title = i.name();
String description = i.description(); String description = i.description();
Map<String, Object> extraSearchResponseData = Map.of("location", i.parent()); Map<String, Object> extraSearchResponseData = Map.of("location", i.parent());
return new SearchResponse(title, description, extraSearchResponseData); return new SearchResponse(id, title, description, extraSearchResponseData);
}).toList(); }).toList();
log.info("Search results for query '{}': {}", request.query(), items); log.info("Search results for query '{}': {}", request.query(), items);
@@ -79,7 +80,7 @@ public class HomeboxSearchProvider implements SearchProvider {
List<HomeboxItem> items) { List<HomeboxItem> items) {
} }
private record HomeboxItem(String name, String description, HomeboxLocation parent) { private record HomeboxItem(String id, String name, String description, HomeboxLocation parent) {
} }
private record HomeboxLocation(String name, String description) { private record HomeboxLocation(String name, String description) {
@@ -2,7 +2,8 @@ package com.vaessl.app.search;
import java.util.Map; import java.util.Map;
public record SearchResponse(String title, String description, Map<String, Object> extraData) { public record SearchResponse(String id, String title, String description,
Map<String, Object> extraData) {
public String getExtra(String key) { public String getExtra(String key) {
if (extraData == null) { if (extraData == null) {
@@ -8,6 +8,7 @@ public final class Mockdata {
public static final String MOCK_SERVICE_TYPE = "SERVICE_TYPE"; public static final String MOCK_SERVICE_TYPE = "SERVICE_TYPE";
public static final String MOCK_USER = "user"; public static final String MOCK_USER = "user";
public static final String MOCK_PASS = "pw"; public static final String MOCK_PASS = "pw";
public static final String MOCK_ID = "item-1";
public static final String MOCK_TITLE = "title"; public static final String MOCK_TITLE = "title";
public static final String MOCK_DESCRIPTION = "desc"; public static final String MOCK_DESCRIPTION = "desc";
} }
@@ -9,7 +9,7 @@ class SearchResponseTest {
@Test @Test
void shouldReturnNullWhenExtraDataIsNull() { void shouldReturnNullWhenExtraDataIsNull() {
SearchResponse response = new SearchResponse(MOCK_TITLE, MOCK_DESCRIPTION, null); SearchResponse response = new SearchResponse(MOCK_ID, MOCK_TITLE, MOCK_DESCRIPTION, null);
assertThat(response.getExtra(null)).isNull(); assertThat(response.getExtra(null)).isNull();
} }
@@ -17,7 +17,7 @@ class SearchResponseTest {
@Test @Test
void shouldReturnNullWhenExtraDataKeyIsMissing() { void shouldReturnNullWhenExtraDataKeyIsMissing() {
SearchResponse response = new SearchResponse(MOCK_TITLE, MOCK_DESCRIPTION, Map.of("key", "value")); SearchResponse response = new SearchResponse(MOCK_ID, MOCK_TITLE, MOCK_DESCRIPTION, Map.of("key", "value"));
assertThat(response.getExtra("missing")).isNull(); assertThat(response.getExtra("missing")).isNull();
} }
@@ -25,8 +25,9 @@ class SearchResponseTest {
@Test @Test
void shouldReturnExtraDataValue() { void shouldReturnExtraDataValue() {
SearchResponse response = new SearchResponse(MOCK_TITLE, MOCK_DESCRIPTION, Map.of("key", "value")); SearchResponse response = new SearchResponse(MOCK_ID, MOCK_TITLE, MOCK_DESCRIPTION, Map.of("key", "value"));
assertThat(response.id()).isEqualTo(MOCK_ID);
assertThat(response.getExtra("key")).contains("value"); assertThat(response.getExtra("key")).contains("value");
} }
+1 -1
View File
@@ -1,5 +1,5 @@
import { Dashboard } from './components/Dashboard' import { Dashboard } from './components/connections/Dashboard'
function App() { function App() {
return ( return (
+2 -3
View File
@@ -1,9 +1,8 @@
import { apiFetch } from "./client"; import { apiFetch } from "./client";
import type { AuthResponse } from "../types/connection"; import type { PagedSearchResponse, SearchRequest, SearchResponse } from "../types/search";
import type { SearchRequest } from "../types/search";
export const search = (req: SearchRequest) => export const search = (req: SearchRequest) =>
apiFetch<AuthResponse>("/search", { apiFetch<PagedSearchResponse<SearchResponse>>("/search", {
method: "POST", method: "POST",
body: JSON.stringify(req), body: JSON.stringify(req),
}); });
@@ -1,7 +1,7 @@
import { useEffect, useRef, useState, type SyntheticEvent } from 'react' import { useEffect, useRef, useState, type SyntheticEvent } from 'react'
import { login } from '../api/connections' import { login } from '../../api/connections'
import type { LoginRequest } from '../types/connection' import type { LoginRequest } from '../../types/connection'
import './ConnectModal.scss' import '../ui/Modal.scss'
interface Props { interface Props {
serviceType: string serviceType: string
@@ -1,10 +1,10 @@
import { useState, useEffect } from "react" import { useState, useEffect } from "react"
import { getStatuses, logout } from "../api/connections" import { getStatuses, logout } from "../../api/connections"
import type { ConnectionStatus } from "../types/connection" import type { ConnectionStatus } from "../../types/connection"
import { ServiceCard } from "./ServiceCard" import { ServiceCard } from "./ServiceCard"
import { ConnectModal } from "./ConnectModal" import { ConnectModal } from "./ConnectModal"
import "./Dashboard.scss" import "./Dashboard.scss"
import { SearchModal } from "./SearchModal" import { SearchModal } from "../search/SearchModal"
const SERVICES = [ const SERVICES = [
{ serviceType: 'HOMEBOX', label: 'Homebox', icon: '📦' }, { serviceType: 'HOMEBOX', label: 'Homebox', icon: '📦' },
@@ -1,5 +1,5 @@
import type { ConnectionStatus } from '../types/connection' import type { ConnectionStatus } from '../../types/connection'
import { ActionButton } from './ActionButton' import { ActionButton } from '../ui/ActionButton'
import './ServiceCard.scss' import './ServiceCard.scss'
interface Props { interface Props {
@@ -1,7 +1,7 @@
import { useEffect, useRef, useState, type SyntheticEvent } from 'react' import { useEffect, useRef, useState, type SyntheticEvent } from 'react'
import './ConnectModal.scss' import '../ui/Modal.scss'
import type { SearchRequest } from '../types/search' import { type PagedSearchResponse, type SearchResponse, type SearchRequest } from '../../types/search'
import { search } from '../api/searches' import { search } from '../../api/searches'
interface Props { interface Props {
serviceType: string serviceType: string
@@ -12,7 +12,8 @@ interface Props {
} }
export function SearchModal({ serviceType, label, appUrl, username, onClose }: Readonly<Props>) { export function SearchModal({ serviceType, label, appUrl, username, onClose }: Readonly<Props>) {
const [query, setQuery] = useState(''); const [query, setQuery] = useState('')
const [results, setResults] = useState<PagedSearchResponse<SearchResponse> | null>(null)
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null) const [error, setError] = useState<string | null>(null)
const firstInputRef = useRef<HTMLInputElement>(null) const firstInputRef = useRef<HTMLInputElement>(null)
@@ -40,7 +41,10 @@ export function SearchModal({ serviceType, label, appUrl, username, onClose }: R
setLoading(true) setLoading(true)
try { try {
const req: SearchRequest = { appUrl, serviceType, username, query} const req: SearchRequest = { appUrl, serviceType, username, query}
await search(req) const res = await search(req)
console.log(req)
setResults(res)
console.log('results', results)
} catch (err) { } catch (err) {
setError(err instanceof Error ? err.message : 'Search failed') setError(err instanceof Error ? err.message : 'Search failed')
} finally { } finally {
@@ -49,21 +53,31 @@ export function SearchModal({ serviceType, label, appUrl, username, onClose }: R
} }
return ( return (
<dialog className="modal" ref={dialogRef}> <dialog className='modal' ref={dialogRef}>
<div className="modal__header"> <div className='modal__header'>
<h2 className="modal__title" id="modal-title">Search in {label}</h2> <h2 className='modal__title' id='modal-title'>Search in {label}</h2>
<button className="modal__close" onClick={onClose} aria-label="Close">×</button> <button className='modal__close' onClick={onClose} aria-label='Close'>×</button>
</div> </div>
<form className="modal__form" onSubmit={handleSubmit}> <form className='modal__form' onSubmit={handleSubmit}>
<div className="modal__field"> <div className='modal__field'>
<input id="search" className="modal__input" <input id='search' className='modal__input'
value={query} onChange={e => setQuery(e.target.value)} /> value={query} onChange={e => setQuery(e.target.value)} />
</div> </div>
{error && <p className="modal__error">{error}</p>} {error && <p className='modal__error'>{error}</p>}
<button className="modal__submit" type="submit" disabled={loading}> <button className='modal__submit' type='submit' disabled={loading}>
Search Search
</button> </button>
</form> </form>
{results && (
<div className='modal__results'>
<p className='modal__results-count'>{results.totalElements}</p>
{results.content.map((item) => (<div key={item.id} className='modal__result-item'>
<p className='"modal__result-title'>{item.title}</p>
{item.description && <p className='modal__result-desc'>{item.description}</p>}
</div>))}
</div>
)}
</dialog> </dialog>
) )
} }
@@ -17,6 +17,8 @@
width: 100%; width: 100%;
max-width: 420px; max-width: 420px;
box-shadow: var(--shadow); box-shadow: var(--shadow);
display: flex;
flex-direction: column;
&__header { &__header {
display: flex; display: flex;
@@ -87,7 +89,7 @@
font-size: 13px; font-size: 13px;
color: #ef4444; color: #ef4444;
padding: 8px 12px; padding: 8px 12px;
background: rgba(239, 68, 68, 0.08); background: rgba(11, 0, 0, 0.499);
border-radius: 6px; border-radius: 6px;
border: 1px solid rgba(239, 68, 68, 0.2); border: 1px solid rgba(239, 68, 68, 0.2);
} }
@@ -110,4 +112,44 @@
&:disabled { opacity: 0.6; cursor: not-allowed; } &:disabled { opacity: 0.6; cursor: not-allowed; }
&:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; } &:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
} }
&--expanded {
max-height: 70vh;
}
&__results {
overflow-y: auto;
flex: 1;
min-height: 0; // required: flex children won't shrink without this
margin-top: 20px;
display: flex;
flex-direction: column;
gap: 8px;
}
&__results-count {
font-size: 12px;
color: var(--text);
margin: 0 0 8px 0;
}
&__result-item {
padding: 10px 12px;
border: 1px solid var(--border);
border-radius: 6px;
background: var(--bg);
}
&__result-title {
font-weight: 500;
font-size: 14px;
color: var(--text-h);
margin: 0;
}
&__result-desc {
font-size: 13px;
color: var(--text);
margin: 4px 0 0 0;
}
} }
+18 -1
View File
@@ -2,5 +2,22 @@ export interface SearchRequest {
appUrl: string; appUrl: string;
serviceType: string; serviceType: string;
username: string; username: string;
query: string; query: string | null;
}
export interface SearchResponse {
id: string;
title: string;
description: string | null;
extraData: Record<string, unknown>;
}
export interface PagedSearchResponse<T> {
content: T[];
page: number;
pageSize: number;
totalElements: number;
first: boolean;
last: boolean;
sort: string;
} }