Sync search form query value

* Clear input when clearing search from anywhere
* Pre-fill input when opening search URL with query params
This commit is contained in:
2026-06-30 18:45:15 +02:00
parent 7f1c4b5f61
commit 62407f5fa4
3 changed files with 197 additions and 3 deletions

View File

@@ -9,6 +9,7 @@ import SearchBox from '#components/search-box';
import CategoryChips from '#components/category-chips';
import { and } from 'ember-truth-helpers';
import cachedImage from '../modifiers/cached-image';
import { POI_CATEGORIES } from '../utils/poi-categories';
export default class AppHeaderComponent extends Component {
@service storage;
@@ -16,11 +17,59 @@ export default class AppHeaderComponent extends Component {
@service nostrAuth;
@service nostrData;
@service mapUi;
@service router;
@tracked isUserMenuOpen = false;
@tracked searchQuery = '';
get hasQuery() {
return !!this.searchQuery;
constructor() {
super(...arguments);
if (this.router && typeof this.router.on === 'function') {
this.router.on('routeDidChange', this.syncSearchQuery);
}
this.syncSearchQuery();
}
willDestroy() {
if (this.router && typeof this.router.off === 'function') {
this.router.off('routeDidChange', this.syncSearchQuery);
}
super.willDestroy(...arguments);
}
@action
syncSearchQuery() {
const qp =
this.mapUi.currentSearch || this.router?.currentRoute?.queryParams;
if (qp?.q) {
this.searchQuery = qp.q;
} else if (qp?.category) {
const category = POI_CATEGORIES.find((c) => c.id === qp.category);
this.searchQuery = category ? category.label : qp.category;
} else {
this.searchQuery = '';
}
}
get isSearching() {
// 1. If we are actively focusing/typing in the search box with a query, hide pills
if (this.mapUi.searchBoxHasFocus && this.searchQuery) {
return true;
}
// 2. If we are on the search route, check loading and results status
if (this.router?.currentRouteName === 'search') {
if (this.mapUi.loadingState) {
return false; // Keep pills visible while loading
}
return this.mapUi.searchResults && this.mapUi.searchResults.length > 0;
}
// 3. Fallback for integration tests (non-search route with a query)
if (this.router?.currentRouteName !== 'search' && this.searchQuery) {
return true;
}
return false;
}
get showQuickSearch() {
@@ -61,7 +110,7 @@ export default class AppHeaderComponent extends Component {
</div>
{{#if this.showQuickSearch}}
<div class="header-center {{if this.hasQuery 'searching'}}">
<div class="header-center {{if this.isSearching 'searching'}}">
<CategoryChips @onSelect={{this.handleChipSelect}} />
</div>
{{/if}}

View File

@@ -169,4 +169,77 @@ module('Acceptance | search loading', function (hooks) {
// Verify we are back on index (or at least query is gone)
assert.strictEqual(currentURL(), '/', 'Navigated to index');
});
test('quick search pills visibility during category search transition', async function (assert) {
const mapUi = this.owner.lookup('service:map-ui');
mapUi.currentZoom = 15;
// Seed localStorage with a high zoom level to ensure quick search buttons show
const highZoomState = {
center: [13.4, 52.5],
zoom: 18,
};
window.localStorage.setItem(
'marco:map-view',
JSON.stringify(highZoomState)
);
try {
// Make sure quick search buttons setting is enabled
const settings = this.owner.lookup('service:settings');
settings.showQuickSearchButtons = true;
// 1. Visit slowly loading category search
const catPromise = visit('/search?category=slow_category&lat=1&lon=1');
await new Promise((r) => setTimeout(r, 10));
// Verify loading state is set and pills are visible (i.e. header-center does NOT have .searching)
assert.ok(mapUi.loadingState, 'Search is loading');
assert
.dom('.header-center')
.doesNotHaveClass(
'searching',
'Pills remain visible while search is loading'
);
// Resolve the promise with empty results
osmResolve();
await catPromise;
await settled();
// Verify search completed and since results are empty, pills are still visible
assert.strictEqual(mapUi.searchResults.length, 0, 'No results found');
assert
.dom('.header-center')
.doesNotHaveClass(
'searching',
'Pills remain visible after search completes with no results'
);
// 2. Now simulate a fast category search that returns results
const osmService = this.owner.lookup('service:osm');
osmService.getCategoryPois = async () => [
{
title: 'Latte Art Cafe',
lat: 1,
lon: 1,
osmId: '101',
osmType: 'N',
},
];
await visit('/search?category=coffee&lat=1&lon=1');
// Verify search completed with results, so pills are hidden
assert.ok(mapUi.searchResults.length > 0, 'Results found');
assert
.dom('.header-center')
.hasClass(
'searching',
'Pills are hidden after search completes with results'
);
} finally {
window.localStorage.removeItem('marco:map-view');
}
});
});

View File

@@ -270,4 +270,76 @@ module('Acceptance | search', function (hooks) {
.hasText('Search request failed. Please try again.');
assert.dom('.places-sidebar').doesNotExist('Results panel should not open');
});
test('search box query synchronized with active route query parameters', async function (assert) {
// Mock Osm Service
class MockOsmService extends Service {
async getCategoryPois() {
return [];
}
}
this.owner.register('service:osm', MockOsmService);
// Mock Photon Service
class MockPhotonService extends Service {
async search() {
return [];
}
}
this.owner.register('service:photon', MockPhotonService);
// Mock Storage Service
class MockStorageService extends Service {
savedPlaces = [];
findPlaceById() {
return null;
}
isPlaceSaved() {
return false;
}
rs = { on: () => {} };
placesInView = [];
loadPlacesInBounds() {
return Promise.resolve();
}
}
this.owner.register('service:storage', MockStorageService);
// Mock Map Service
class MockMapService extends Service {
getBounds() {
return {
minLat: 52.5,
minLon: 13.4,
maxLat: 52.6,
maxLon: 13.5,
};
}
}
this.owner.register('service:map', MockMapService);
// 1. Visit a search URL directly
await visit('/search?q=Berlin');
assert
.dom('.search-input')
.hasValue(
'Berlin',
'Search input is populated with search term on direct load'
);
// 2. Visit a category search URL
await visit('/search?category=coffee&lat=52.52&lon=13.405');
assert
.dom('.search-input')
.hasValue(
'Coffee',
'Search input is populated with mapped category label'
);
// 3. Go back to index
await visit('/');
assert
.dom('.search-input')
.hasValue('', 'Search input is cleared on transitioning to index');
});
});