diff --git a/messages/en.json b/messages/en.json index 4fea863d..56e83152 100644 --- a/messages/en.json +++ b/messages/en.json @@ -79,6 +79,7 @@ "dashboard_notifications_alt": "Notifications", "dashboard_loading_view": "Loading dashboard view...", "dashboard_loading_devices": "Loading devices…", + "dashboard_search_placeholder": "Search by location, device name or EUI…", "error_bad_request_title": "Bad Request", "error_bad_request_description": "The server could not understand your request. Please check the URL and try again.", "error_unauthorized_title": "Unauthorized", diff --git a/messages/ja.json b/messages/ja.json index 19c0e17f..d8abf100 100644 --- a/messages/ja.json +++ b/messages/ja.json @@ -79,6 +79,7 @@ "dashboard_notifications_alt": "通知", "dashboard_loading_view": "ダッシュボードを読み込み中...", "dashboard_loading_devices": "デバイスを読み込み中…", + "dashboard_search_placeholder": "ロケーション、デバイス名、EUIで検索…", "error_bad_request_title": "不正なリクエスト", "error_bad_request_description": "サーバーがリクエストを理解できませんでした。URL を確認して再度お試しください。", "error_unauthorized_title": "認証が必要です", diff --git a/src/lib/components/dashboard/DashboardTable.svelte b/src/lib/components/dashboard/DashboardTable.svelte index 05586bff..ffb0b8f4 100644 --- a/src/lib/components/dashboard/DashboardTable.svelte +++ b/src/lib/components/dashboard/DashboardTable.svelte @@ -75,22 +75,33 @@ group: filters.group || undefined, locationGroup: filters.locationGroup || undefined, location: filters.location || undefined, - name: query.search?.trim() || filters.name || undefined + name: filters.name || undefined }, { signal: query.signal } ); return { rows: page.rows, total: page.total }; } + + // Mapped into CwDataTable's `Record` filter shape so the + // table re-runs `loadData` whenever the dashboard filters (incl. the search + // box) change. The actual values are read from `filters` inside `loadData`. + const tableFilters = $derived({ + group: filters.group ? [filters.group] : [], + locationGroup: filters.locationGroup ? [filters.locationGroup] : [], + location: filters.location ? [filters.location] : [], + name: filters.name ? [filters.name] : [] + }); { if (row.location?.location_id != null) { goto(resolve(`/locations/${row.location.location_id}/devices/${row.dev_eui}`)); diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 0633c747..e1b2c007 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -4,7 +4,7 @@ import { onMount } from 'svelte'; import Icon from '$lib/components/Icon.svelte'; import { AppPage } from '$lib/components/layout'; - import { CwButton } from '@cropwatchdevelopment/cwui'; + import { CwButton, CwSearchInput } from '@cropwatchdevelopment/cwui'; import DashboardCards from '$lib/components/dashboard/DashboardCards.svelte'; import DashboardTable from '$lib/components/dashboard/DashboardTable.svelte'; import { getAppContext } from '$lib/appContext.svelte'; @@ -17,17 +17,30 @@ const VIEW_STORAGE_KEY = 'cropwatch.dashboard.view'; const MOBILE_QUERY = '(max-width: 767px)'; + const SEARCH_DEBOUNCE_MS = 300; const app = getAppContext(); let view = $state('table'); let viewReady = $state(!browser); + // Free-text search box. `searchName` updates on every keystroke; `debouncedName` + // trails it so the views re-fetch once the user pauses, not on every key. + let searchName = $state(page.url.searchParams.get('name') ?? ''); + let debouncedName = $state(page.url.searchParams.get('name') ?? ''); + $effect(() => { + const next = searchName; + const timer = setTimeout(() => { + debouncedName = next; + }, SEARCH_DEBOUNCE_MS); + return () => clearTimeout(timer); + }); + const filters = $derived({ group: page.url.searchParams.get('group') ?? '', locationGroup: page.url.searchParams.get('locationGroup') ?? '', location: page.url.searchParams.get('location') ?? '', - name: page.url.searchParams.get('name') ?? '' + name: debouncedName.trim() }); function setView(next: DashboardView) { @@ -55,12 +68,22 @@ if (!token || sidebarDataLoaded) return; sidebarDataLoaded = true; const api = new ApiService({ authToken: token }); - api.getLocationGroups().then((groups) => { - app.locationGroups = groups; - }).catch(() => { /* sidebar tolerates an empty list */ }); - api.getLocations().then((locations) => { - app.locations = locations; - }).catch(() => { /* sidebar tolerates an empty list */ }); + api + .getLocationGroups() + .then((groups) => { + app.locationGroups = groups; + }) + .catch(() => { + /* sidebar tolerates an empty list */ + }); + api + .getLocations() + .then((locations) => { + app.locations = locations; + }) + .catch(() => { + /* sidebar tolerates an empty list */ + }); }); @@ -71,8 +94,20 @@
-
-
+
+ + +