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 mapUi;
|
||||||
@service storage;
|
@service storage;
|
||||||
@service router;
|
@service router;
|
||||||
|
@service toast;
|
||||||
|
|
||||||
queryParams = {
|
queryParams = {
|
||||||
lat: { refreshModel: true },
|
lat: { refreshModel: true },
|
||||||
@@ -199,8 +200,12 @@ export default class SearchRoute extends Route {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
error() {
|
error(error, transition) {
|
||||||
this.mapUi.stopSearch();
|
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 [];
|
return [];
|
||||||
}
|
}
|
||||||
console.error('Category search failed', e);
|
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;
|
cursor: not-allowed;
|
||||||
pointer-events: none;
|
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 Map from '#components/map';
|
||||||
import AppHeader from '#components/app-header';
|
import AppHeader from '#components/app-header';
|
||||||
import AppMenu from '#components/app-menu/index';
|
import AppMenu from '#components/app-menu/index';
|
||||||
|
import Toast from '#components/toast';
|
||||||
import { service } from '@ember/service';
|
import { service } from '@ember/service';
|
||||||
import { tracked } from '@glimmer/tracking';
|
import { tracked } from '@glimmer/tracking';
|
||||||
import { action } from '@ember/object';
|
import { action } from '@ember/object';
|
||||||
@@ -89,6 +90,8 @@ export default class ApplicationComponent extends Component {
|
|||||||
<AppMenu @onClose={{this.closeAppMenu}} />
|
<AppMenu @onClose={{this.closeAppMenu}} />
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
|
<Toast />
|
||||||
|
|
||||||
{{outlet}}
|
{{outlet}}
|
||||||
</template>
|
</template>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -218,4 +218,56 @@ module('Acceptance | search', function (hooks) {
|
|||||||
// Ensure it shows "Results" not "Nearby"
|
// Ensure it shows "Results" not "Nearby"
|
||||||
assert.dom('.sidebar-header h2').includesText('Results');
|
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