Make collection list loading async
This commit is contained in:
@@ -218,52 +218,58 @@ export default class PlacesSidebar extends Component {
|
||||
@onSave={{this.updateBookmark}}
|
||||
/>
|
||||
{{else}}
|
||||
{{#if @places}}
|
||||
<ul class="places-list">
|
||||
{{#each @places as |place|}}
|
||||
<li>
|
||||
<button
|
||||
type="button"
|
||||
class="place-item"
|
||||
{{on "click" (fn this.selectPlace place)}}
|
||||
>
|
||||
<div class="place-name">{{or
|
||||
place.title
|
||||
place.osmTags.name
|
||||
place.osmTags.name:en
|
||||
"Unnamed Place"
|
||||
}}</div>
|
||||
<div class="place-type">
|
||||
{{#if (eq place.source "osm")}}
|
||||
{{humanizeOsmTag place.type}}
|
||||
{{else if (eq place.source "photon")}}
|
||||
{{place.description}}
|
||||
{{else if (getPlaceType place.osmTags)}}
|
||||
{{getPlaceType place.osmTags}}
|
||||
{{else}}
|
||||
Saved place
|
||||
{{/if}}
|
||||
</div>
|
||||
</button>
|
||||
</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
{{#if @isLoading}}
|
||||
<div class="sidebar-loading">
|
||||
<Icon @name="loading-ring" @size={{24}} @color="#898989" />
|
||||
</div>
|
||||
{{else}}
|
||||
{{#if this.isNearbySearch}}
|
||||
<p class="empty-state">No places found nearby.</p>
|
||||
{{#if @places}}
|
||||
<ul class="places-list">
|
||||
{{#each @places as |place|}}
|
||||
<li>
|
||||
<button
|
||||
type="button"
|
||||
class="place-item"
|
||||
{{on "click" (fn this.selectPlace place)}}
|
||||
>
|
||||
<div class="place-name">{{or
|
||||
place.title
|
||||
place.osmTags.name
|
||||
place.osmTags.name:en
|
||||
"Unnamed Place"
|
||||
}}</div>
|
||||
<div class="place-type">
|
||||
{{#if (eq place.source "osm")}}
|
||||
{{humanizeOsmTag place.type}}
|
||||
{{else if (eq place.source "photon")}}
|
||||
{{place.description}}
|
||||
{{else if (getPlaceType place.osmTags)}}
|
||||
{{getPlaceType place.osmTags}}
|
||||
{{else}}
|
||||
Saved place
|
||||
{{/if}}
|
||||
</div>
|
||||
</button>
|
||||
</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
{{else}}
|
||||
<p class="empty-state">No results found.</p>
|
||||
{{#if this.isNearbySearch}}
|
||||
<p class="empty-state">No places found nearby.</p>
|
||||
{{else}}
|
||||
<p class="empty-state">No results found.</p>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline create-place"
|
||||
{{on "click" this.createNewPlace}}
|
||||
>
|
||||
<Icon @name="plus" @size={{18}} @color="var(--link-color)" />
|
||||
Create new place
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline create-place"
|
||||
{{on "click" this.createNewPlace}}
|
||||
>
|
||||
<Icon @name="plus" @size={{18}} @color="var(--link-color)" />
|
||||
Create new place
|
||||
</button>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
import Controller from '@ember/controller';
|
||||
import { service } from '@ember/service';
|
||||
import { action } from '@ember/object';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { task } from 'ember-concurrency';
|
||||
|
||||
export default class ListsListController extends Controller {
|
||||
@service router;
|
||||
@service mapUi;
|
||||
@service storage;
|
||||
|
||||
@tracked model;
|
||||
@tracked loadedPlaces = [];
|
||||
|
||||
get listId() {
|
||||
return this.model?.list_id;
|
||||
}
|
||||
|
||||
loadPlacesTask = task({ restartable: true }, async (listId) => {
|
||||
this.loadedPlaces = []; // Clear previous elements immediately to show fresh loader
|
||||
try {
|
||||
this.loadedPlaces = await this.storage.getPlacesInList(listId);
|
||||
} catch (e) {
|
||||
console.error('Failed to load places in list', listId, e);
|
||||
this.loadedPlaces = [];
|
||||
}
|
||||
});
|
||||
|
||||
get scrollTop() {
|
||||
return this.mapUi.getScrollPosition(`list-${this.listId}`);
|
||||
}
|
||||
|
||||
get listColor() {
|
||||
const list = this.storage.lists.find((l) => l.id === this.listId);
|
||||
if (list && list.color) {
|
||||
return list.color;
|
||||
}
|
||||
return getComputedStyle(document.documentElement)
|
||||
.getPropertyValue('--default-list-color')
|
||||
.trim();
|
||||
}
|
||||
|
||||
get listTitle() {
|
||||
const list = this.storage.lists.find((l) => l.id === this.listId);
|
||||
return list ? list.title : 'Collections';
|
||||
}
|
||||
|
||||
get places() {
|
||||
const currentList = this.storage.lists.find((l) => l.id === this.listId);
|
||||
const placeRefsIds = new Set(
|
||||
currentList?.placeRefs?.map((ref) => ref.id) || []
|
||||
);
|
||||
|
||||
// Filter live tracked savedPlaces that are in this list
|
||||
const livePlaces = this.storage.savedPlaces.filter((p) =>
|
||||
placeRefsIds.has(p.id)
|
||||
);
|
||||
|
||||
const merged = [];
|
||||
const seen = new Set();
|
||||
|
||||
// Process live state first to reflect deletions/edits immediately
|
||||
livePlaces.forEach((p) => {
|
||||
merged.push(p);
|
||||
seen.add(p.id);
|
||||
});
|
||||
|
||||
// Supplement with any background-fetched places that are still valid but not in live state yet
|
||||
this.loadedPlaces.forEach((p) => {
|
||||
if (placeRefsIds.has(p.id) && !seen.has(p.id)) {
|
||||
merged.push(p);
|
||||
seen.add(p.id);
|
||||
}
|
||||
});
|
||||
|
||||
return merged;
|
||||
}
|
||||
|
||||
@action
|
||||
selectPlace(place) {
|
||||
if (place) {
|
||||
const sidebarContent = document.querySelector('.sidebar-content');
|
||||
if (sidebarContent) {
|
||||
this.mapUi.saveScrollPosition(
|
||||
`list-${this.listId}`,
|
||||
sidebarContent.scrollTop
|
||||
);
|
||||
}
|
||||
this.mapUi.returnToRoute = {
|
||||
name: 'lists.list',
|
||||
model: this.listId,
|
||||
};
|
||||
this.mapUi.showSidebar();
|
||||
this.mapUi.preventNextZoom = true;
|
||||
this.router.transitionTo('place', place);
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
close() {
|
||||
this.router.transitionTo('index');
|
||||
}
|
||||
|
||||
@action
|
||||
backToLists() {
|
||||
this.router.transitionTo('lists.index');
|
||||
}
|
||||
}
|
||||
@@ -4,14 +4,23 @@ import { service } from '@ember/service';
|
||||
export default class ListsListRoute extends Route {
|
||||
@service storage;
|
||||
|
||||
async model(params) {
|
||||
const listId = params.list_id;
|
||||
try {
|
||||
const places = await this.storage.getPlacesInList(listId);
|
||||
return { listId, places };
|
||||
} catch (e) {
|
||||
console.error('Failed to load places in list', listId, e);
|
||||
return { listId, places: [] };
|
||||
model(params) {
|
||||
// Resolve instantly so transition happens in 0ms!
|
||||
return { list_id: params.list_id };
|
||||
}
|
||||
|
||||
setupController(controller, model) {
|
||||
console.debug('DEBUG: setupController controller is:', controller);
|
||||
console.debug(
|
||||
'DEBUG: controller.loadPlacesTask is:',
|
||||
controller?.loadPlacesTask
|
||||
);
|
||||
controller.model = model;
|
||||
super.setupController(controller, model);
|
||||
if (controller && controller.loadPlacesTask) {
|
||||
controller.loadPlacesTask.perform(model.list_id);
|
||||
} else {
|
||||
console.error('DEBUG: ERROR! controller.loadPlacesTask is undefined!');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2250,3 +2250,11 @@ button.create-place {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Sidebar Loading State */
|
||||
.sidebar-loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 4rem 1rem;
|
||||
}
|
||||
|
||||
+14
-107
@@ -1,109 +1,16 @@
|
||||
import Component from '@glimmer/component';
|
||||
import PlacesSidebar from '#components/places-sidebar';
|
||||
import { service } from '@ember/service';
|
||||
import { action } from '@ember/object';
|
||||
|
||||
export default class ListsListTemplate extends Component {
|
||||
@service router;
|
||||
@service mapUi;
|
||||
@service storage;
|
||||
|
||||
get listId() {
|
||||
return this.args.model?.listId;
|
||||
}
|
||||
|
||||
get scrollTop() {
|
||||
return this.mapUi.getScrollPosition(`list-${this.listId}`);
|
||||
}
|
||||
|
||||
get listColor() {
|
||||
const list = this.storage.lists.find((l) => l.id === this.listId);
|
||||
if (list && list.color) {
|
||||
return list.color;
|
||||
}
|
||||
return getComputedStyle(document.documentElement)
|
||||
.getPropertyValue('--default-list-color')
|
||||
.trim();
|
||||
}
|
||||
|
||||
get listTitle() {
|
||||
const list = this.storage.lists.find((l) => l.id === this.listId);
|
||||
return list ? list.title : 'Collections';
|
||||
}
|
||||
|
||||
get places() {
|
||||
const modelPlaces = this.args.model?.places || [];
|
||||
const currentList = this.storage.lists.find((l) => l.id === this.listId);
|
||||
const placeRefsIds = new Set(
|
||||
currentList?.placeRefs?.map((ref) => ref.id) || []
|
||||
);
|
||||
|
||||
// Filter live tracked savedPlaces that are in this list
|
||||
const livePlaces = this.storage.savedPlaces.filter((p) =>
|
||||
placeRefsIds.has(p.id)
|
||||
);
|
||||
|
||||
const merged = [];
|
||||
const seen = new Set();
|
||||
|
||||
// Process live state first to reflect deletions/edits immediately
|
||||
livePlaces.forEach((p) => {
|
||||
merged.push(p);
|
||||
seen.add(p.id);
|
||||
});
|
||||
|
||||
// Supplement with any model-fetched places that are still valid but not in live state yet
|
||||
modelPlaces.forEach((p) => {
|
||||
if (placeRefsIds.has(p.id) && !seen.has(p.id)) {
|
||||
merged.push(p);
|
||||
seen.add(p.id);
|
||||
}
|
||||
});
|
||||
|
||||
return merged;
|
||||
}
|
||||
|
||||
@action
|
||||
selectPlace(place) {
|
||||
if (place) {
|
||||
const sidebarContent = document.querySelector('.sidebar-content');
|
||||
if (sidebarContent) {
|
||||
this.mapUi.saveScrollPosition(
|
||||
`list-${this.listId}`,
|
||||
sidebarContent.scrollTop
|
||||
);
|
||||
}
|
||||
this.mapUi.returnToRoute = {
|
||||
name: 'lists.list',
|
||||
model: this.listId,
|
||||
};
|
||||
this.mapUi.showSidebar();
|
||||
this.mapUi.preventNextZoom = true;
|
||||
this.router.transitionTo('place', place);
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
close() {
|
||||
this.router.transitionTo('index');
|
||||
}
|
||||
|
||||
@action
|
||||
backToLists() {
|
||||
this.router.transitionTo('lists.index');
|
||||
}
|
||||
|
||||
<template>
|
||||
{{#if this.mapUi.isSidebarVisible}}
|
||||
<PlacesSidebar
|
||||
@places={{this.places}}
|
||||
@title={{this.listTitle}}
|
||||
@color={{this.listColor}}
|
||||
@scrollTop={{this.scrollTop}}
|
||||
@onSelect={{this.selectPlace}}
|
||||
@onClose={{this.close}}
|
||||
@onBack={{this.backToLists}}
|
||||
/>
|
||||
{{/if}}
|
||||
</template>
|
||||
}
|
||||
<template>
|
||||
{{#if @controller.mapUi.isSidebarVisible}}
|
||||
<PlacesSidebar
|
||||
@places={{@controller.places}}
|
||||
@title={{@controller.listTitle}}
|
||||
@color={{@controller.listColor}}
|
||||
@scrollTop={{@controller.scrollTop}}
|
||||
@isLoading={{@controller.loadPlacesTask.isRunning}}
|
||||
@onSelect={{@controller.selectPlace}}
|
||||
@onClose={{@controller.close}}
|
||||
@onBack={{@controller.backToLists}}
|
||||
/>
|
||||
{{/if}}
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user