Merge pull request 'When search requests fail, show error in toast notifications instead of empty search results' (#38) from feature/failed_requests into master
Reviewed-on: #38
This commit was merged in pull request #38.
This commit is contained in:
14
app/components/toast.gjs
Normal file
14
app/components/toast.gjs
Normal file
@@ -0,0 +1,14 @@
|
||||
import Component from '@glimmer/component';
|
||||
import { service } from '@ember/service';
|
||||
|
||||
export default class ToastComponent extends Component {
|
||||
@service toast;
|
||||
|
||||
<template>
|
||||
{{#if this.toast.isVisible}}
|
||||
<div class="toast-notification">
|
||||
{{this.toast.message}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</template>
|
||||
}
|
||||
@@ -9,6 +9,7 @@ export default class SearchRoute extends Route {
|
||||
@service mapUi;
|
||||
@service storage;
|
||||
@service router;
|
||||
@service toast;
|
||||
|
||||
queryParams = {
|
||||
lat: { refreshModel: true },
|
||||
@@ -199,8 +200,12 @@ export default class SearchRoute extends Route {
|
||||
}
|
||||
|
||||
@action
|
||||
error() {
|
||||
error(error, transition) {
|
||||
this.mapUi.stopSearch();
|
||||
return true; // Bubble error
|
||||
this.toast.show('Search request failed. Please try again.');
|
||||
if (transition) {
|
||||
transition.abort();
|
||||
}
|
||||
return false; // Prevent bubble and stop transition
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,7 +155,7 @@ out center;
|
||||
return [];
|
||||
}
|
||||
console.error('Category search failed', e);
|
||||
return [];
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
21
app/services/toast.js
Normal file
21
app/services/toast.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import Service from '@ember/service';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
|
||||
export default class ToastService extends Service {
|
||||
@tracked message = null;
|
||||
@tracked isVisible = false;
|
||||
timeoutId = null;
|
||||
|
||||
show(message, duration = 3000) {
|
||||
this.message = message;
|
||||
this.isVisible = true;
|
||||
|
||||
if (this.timeoutId) {
|
||||
clearTimeout(this.timeoutId);
|
||||
}
|
||||
|
||||
this.timeoutId = setTimeout(() => {
|
||||
this.isVisible = false;
|
||||
}, duration);
|
||||
}
|
||||
}
|
||||
@@ -1321,3 +1321,36 @@ button.create-place {
|
||||
cursor: not-allowed;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Toast Notification */
|
||||
.toast-notification {
|
||||
position: fixed;
|
||||
bottom: 2rem;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background-color: rgb(51 51 51 / 85%);
|
||||
backdrop-filter: blur(4px);
|
||||
color: white;
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: 999px;
|
||||
z-index: 9999;
|
||||
box-shadow: 0 4px 12px rgb(0 0 0 / 15%);
|
||||
animation: fade-in-up 0.3s ease-out forwards;
|
||||
text-align: center;
|
||||
max-width: 90%;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@keyframes fade-in-up {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translate(-50%, 1rem);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translate(-50%, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { pageTitle } from 'ember-page-title';
|
||||
import Map from '#components/map';
|
||||
import AppHeader from '#components/app-header';
|
||||
import AppMenu from '#components/app-menu/index';
|
||||
import Toast from '#components/toast';
|
||||
import { service } from '@ember/service';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { action } from '@ember/object';
|
||||
@@ -89,6 +90,8 @@ export default class ApplicationComponent extends Component {
|
||||
<AppMenu @onClose={{this.closeAppMenu}} />
|
||||
{{/if}}
|
||||
|
||||
<Toast />
|
||||
|
||||
{{outlet}}
|
||||
</template>
|
||||
}
|
||||
|
||||
@@ -218,4 +218,56 @@ module('Acceptance | search', function (hooks) {
|
||||
// Ensure it shows "Results" not "Nearby"
|
||||
assert.dom('.sidebar-header h2').includesText('Results');
|
||||
});
|
||||
|
||||
test('search error handling prevents opening empty panel and shows toast', async function (assert) {
|
||||
// Mock Osm Service to throw an error
|
||||
class MockOsmService extends Service {
|
||||
async getCategoryPois() {
|
||||
throw new Error('Overpass request failed');
|
||||
}
|
||||
}
|
||||
this.owner.register('service:osm', MockOsmService);
|
||||
|
||||
class MockStorageService extends Service {
|
||||
savedPlaces = [];
|
||||
findPlaceById() {
|
||||
return null;
|
||||
}
|
||||
isPlaceSaved() {
|
||||
return false;
|
||||
}
|
||||
rs = { on: () => {} };
|
||||
placesInView = [];
|
||||
loadPlacesInBounds() {
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
this.owner.register('service:storage', MockStorageService);
|
||||
|
||||
class MockMapService extends Service {
|
||||
getBounds() {
|
||||
return {
|
||||
minLat: 52.5,
|
||||
minLon: 13.4,
|
||||
maxLat: 52.6,
|
||||
maxLon: 13.5,
|
||||
};
|
||||
}
|
||||
}
|
||||
this.owner.register('service:map', MockMapService);
|
||||
|
||||
await visit('/');
|
||||
|
||||
try {
|
||||
await visit('/search?category=coffee&lat=52.52&lon=13.405');
|
||||
} catch {
|
||||
// Aborted transition throws, which is expected
|
||||
}
|
||||
|
||||
assert.dom('.toast-notification').exists('Toast should be visible');
|
||||
assert
|
||||
.dom('.toast-notification')
|
||||
.hasText('Search request failed. Please try again.');
|
||||
assert.dom('.places-sidebar').doesNotExist('Results panel should not open');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user