Compare commits

...

8 Commits

Author SHA1 Message Date
f1779131e8 Also load/init lists in anonymous mode
Some checks failed
CI / Lint (pull_request) Failing after 22s
CI / Test (pull_request) Successful in 34s
2026-03-13 17:04:29 +04:00
37cf47b3dd Properly handle place removals
Some checks failed
CI / Lint (pull_request) Failing after 23s
CI / Test (pull_request) Successful in 36s
* Transition to OSM route or index instead of staying on ghost route/ID
  (closes sidebar if it was a custom place)
* Ensure save button and lists are in the correct state
2026-03-13 15:33:29 +04:00
ff68b5addc Move default yellow to var, add in list UI 2026-03-13 14:56:12 +04:00
990f3afa88 Fix lint errors
All checks were successful
CI / Lint (pull_request) Successful in 21s
CI / Test (pull_request) Successful in 34s
2026-03-13 13:51:49 +04:00
b2220b8310 Close list dropdown when clicking outside of it
Some checks failed
CI / Lint (pull_request) Failing after 25s
CI / Test (pull_request) Successful in 34s
2026-03-13 13:40:28 +04:00
a8613ab81a Remove confirmation dialog when deleting place bookmarks 2026-03-13 13:27:01 +04:00
bcb9b20e85 WIP Add places to lists 2026-03-13 12:22:51 +04:00
466b1d5383 Comment dev config for remote access
All checks were successful
CI / Lint (push) Successful in 20s
CI / Test (push) Successful in 34s
2026-03-11 18:26:45 +04:00
13 changed files with 761 additions and 61 deletions

View File

@@ -60,9 +60,30 @@ export default class MapComponent extends Component {
// Create a vector source and layer for bookmarks
this.bookmarkSource = new VectorSource();
const bookmarkLayer = new VectorLayer({
source: this.bookmarkSource,
style: [
const bookmarkStyleFunction = (feature) => {
const originalPlace = feature.get('originalPlace');
let color =
getComputedStyle(document.documentElement)
.getPropertyValue('--default-list-color')
.trim() || '#000000'; // Fallback to black if variable is missing to make error obvious
if (
originalPlace &&
originalPlace._listIds &&
originalPlace._listIds.length > 0
) {
// Find the first list color
// We need access to storage.lists.
// Since this is inside setupMap, 'this' refers to the component instance.
const firstListId = originalPlace._listIds[0];
const list = this.storage.lists.find((l) => l.id === firstListId);
if (list && list.color) {
color = list.color;
}
}
return [
new Style({
image: new Circle({
radius: 10,
@@ -73,14 +94,19 @@ export default class MapComponent extends Component {
new Style({
image: new Circle({
radius: 9,
fill: new Fill({ color: '#ffcc33' }), // Gold/Yellow
fill: new Fill({ color: color }),
stroke: new Stroke({
color: '#fff',
width: 2,
}),
}),
}),
],
];
};
const bookmarkLayer = new VectorLayer({
source: this.bookmarkSource,
style: bookmarkStyleFunction,
zIndex: 10, // Ensure it sits above the map tiles
});

View File

@@ -1,23 +1,39 @@
import Component from '@glimmer/component';
import { fn } from '@ember/helper';
import { service } from '@ember/service';
import { on } from '@ember/modifier';
import { htmlSafe } from '@ember/template';
import { humanizeOsmTag } from '../utils/format-text';
import { getLocalizedName, getPlaceType } from '../utils/osm';
import { mapToStorageSchema } from '../utils/place-mapping';
import { getSocialInfo } from '../utils/social-links';
import Icon from '../components/icon';
import PlaceEditForm from './place-edit-form';
import PlaceListsManager from './place-lists-manager';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
export default class PlaceDetails extends Component {
@service storage;
@tracked isEditing = false;
@tracked showLists = false;
get isSaved() {
return this.storage.isPlaceSaved(this.place.id || this.place.osmId);
}
get place() {
return this.args.place || {};
}
get saveablePlace() {
if (this.place.createdAt) {
return this.place;
}
return mapToStorageSchema(this.place);
}
get tags() {
return this.place.osmTags || {};
}
@@ -28,7 +44,7 @@ export default class PlaceDetails extends Component {
@action
startEditing() {
if (!this.place.createdAt) return; // Only allow editing saved places
if (!this.isSaved) return; // Only allow editing saved places
this.isEditing = true;
}
@@ -37,6 +53,21 @@ export default class PlaceDetails extends Component {
this.isEditing = false;
}
@action
toggleLists(event) {
// Prevent this click from propagating to the document listener
// which handles the "click outside" logic.
if (event) {
event.stopPropagation();
}
this.showLists = !this.showLists;
}
@action
closeLists() {
this.showLists = false;
}
@action
async saveChanges(changes) {
if (this.args.onSave) {
@@ -247,23 +278,33 @@ export default class PlaceDetails extends Component {
{{/if}}
<div class="actions">
<button
type="button"
class={{if
this.place.createdAt
"btn btn-secondary"
"btn btn-outline"
}}
{{on "click" (fn @onToggleSave this.place)}}
>
<Icon
@name="bookmark"
@color={{if this.place.createdAt "currentColor" "#007bff"}}
/>
{{if this.place.createdAt "Saved" "Save"}}
</button>
<div class="save-button-wrapper">
<button
type="button"
class={{if
this.isSaved
"btn btn-secondary"
"btn btn-outline"
}}
{{on "click" this.toggleLists}}
>
<Icon
@name="bookmark"
@color={{if this.isSaved "currentColor" "#007bff"}}
/>
{{if this.isSaved "Saved" "Save"}}
</button>
{{#if this.place.createdAt}}
{{#if this.showLists}}
<PlaceListsManager
@place={{this.saveablePlace}}
@onClose={{this.closeLists}}
@isSaved={{this.isSaved}}
/>
{{/if}}
</div>
{{#if this.isSaved}}
<button
type="button"
class="btn btn-outline"

View File

@@ -0,0 +1,135 @@
import Component from '@glimmer/component';
import { service } from '@ember/service';
import { action } from '@ember/object';
import { tracked } from '@glimmer/tracking';
import { on } from '@ember/modifier';
import { fn } from '@ember/helper';
import { htmlSafe } from '@ember/template';
import onClickOutside from '../modifiers/on-click-outside';
export default class PlaceListsManager extends Component {
@service storage;
@service router;
@tracked _forceClear = false;
get isSaved() {
return this.args.isSaved;
}
get placeListIds() {
if (this._forceClear) return [];
return this.args.place._listIds || [];
}
styleFor(color) {
return htmlSafe(`background-color: ${color}`);
}
@action
isInList(list) {
if (!this.placeListIds) return false;
return this.placeListIds.includes(list.id);
}
@action
async toggleSaved() {
if (this.isSaved) {
const { osmId, osmType } = this.args.place;
await this.storage.removePlace(this.args.place);
// Clean up the local object reference immediately to prevent UI flicker
// or stale state if the transition is delayed/cancelled.
if (this.args.place) {
this.args.place.id = null;
this.args.place.createdAt = null;
this.args.place._listIds = [];
this._forceClear = true;
}
// Transition immediately to the canonical state
if (osmId && osmType) {
// Create a transient copy that looks like a fresh OSM result
const rawPlace = { ...this.args.place };
delete rawPlace.id;
delete rawPlace.createdAt;
delete rawPlace._listIds;
// Transition to the place route using the raw object
// This updates the URL to 'osm:...' and renders immediately
this.router.transitionTo('place', rawPlace);
} else {
// Custom place deleted -> go home
this.router.transitionTo('index');
}
if (this.args.onClose) this.args.onClose();
} else {
await this.storage.storePlace(this.args.place);
}
}
@action
async toggleList(list) {
const isMember = this.placeListIds.includes(list.id);
const shouldAdd = !isMember;
if (shouldAdd && !this.isSaved) {
// Auto-save if adding to list
await this.storage.storePlace(this.args.place);
}
try {
// Toggle membership
// We must pass the SAVED place (with ID) to the toggle function
// If we just saved it above, the args.place might still be the old object reference unless storage updates it in-place?
// StorageService.storePlace returns the new object.
// But togglePlaceList handles saving internally if ID is missing.
// Let's rely on storage.togglePlaceList to handle the "save if needed" part.
await this.storage.togglePlaceList(this.args.place, list.id, shouldAdd);
} catch (e) {
console.error(e);
alert('Failed to update list: ' + e.message);
}
}
<template>
<div class="place-lists-manager" {{onClickOutside @onClose}}>
<div class="list-item master-toggle">
<label>
<input
type="checkbox"
checked={{this.isSaved}}
{{on "change" this.toggleSaved}}
/>
<span class="list-color"></span>
<span class="list-name">Saved places</span>
</label>
</div>
<div class="divider"></div>
<div class="lists-container">
{{#each this.storage.lists as |list|}}
<div class="list-item">
<label>
<input
type="checkbox"
checked={{this.isInList list}}
{{on "change" (fn this.toggleList list)}}
disabled={{unless this.isSaved true}}
/>
{{! template-lint-disable no-inline-styles }}
<span
class="list-color"
style={{this.styleFor list.color}}
></span>
<span class="list-name">{{list.title}}</span>
</label>
</div>
{{/each}}
</div>
</div>
</template>
}

View File

@@ -51,40 +51,39 @@ export default class PlacesSidebar extends Component {
if (!place) return;
if (place.createdAt) {
if (confirm(`Delete "${place.title}"?`)) {
try {
await this.storage.removePlace(place);
console.debug('Place deleted:', place.title);
// Direct delete without confirmation
try {
await this.storage.removePlace(place);
console.debug('Place deleted:', place.title);
// Notify parent to refresh map bookmarks
if (this.args.onBookmarkChange) {
this.args.onBookmarkChange();
}
if (this.args.onUpdate) {
// Reconstruct the "original" place without ID/Geohash/CreatedAt
const freshPlace = {
...place,
id: undefined,
geohash: undefined,
createdAt: undefined,
};
this.args.onUpdate(freshPlace);
}
// Also fire onSelect if it exists (for list view)
if (this.args.onSelect) {
this.args.onSelect(null);
}
// Close sidebar after delete
if (this.args.onClose) {
this.args.onClose();
}
} catch (e) {
console.error('Failed to delete:', e);
alert('Failed to delete: ' + e.message);
// Notify parent to refresh map bookmarks
if (this.args.onBookmarkChange) {
this.args.onBookmarkChange();
}
if (this.args.onUpdate) {
// Reconstruct the "original" place without ID/Geohash/CreatedAt
const freshPlace = {
...place,
id: undefined,
geohash: undefined,
createdAt: undefined,
};
this.args.onUpdate(freshPlace);
}
// Also fire onSelect if it exists (for list view)
if (this.args.onSelect) {
this.args.onSelect(null);
}
// Close sidebar after delete
if (this.args.onClose) {
this.args.onClose();
}
} catch (e) {
console.error('Failed to delete:', e);
alert('Failed to delete: ' + e.message);
}
} else {
// It's a fresh POI -> Save it

View File

@@ -0,0 +1,21 @@
import { modifier } from 'ember-modifier';
export default modifier((element, [callback]) => {
const handler = (event) => {
// Check if the click target is contained within the element
if (element && !element.contains(event.target)) {
callback(event);
}
};
// Delay attaching the listener to avoid catching the opening click
// (using a microtask or setTimeout 0)
const timer = setTimeout(() => {
document.addEventListener('click', handler);
}, 0);
return () => {
clearTimeout(timer);
document.removeEventListener('click', handler);
};
});

View File

@@ -15,6 +15,7 @@ export default class StorageService extends Service {
@tracked savedPlaces = [];
@tracked loadedPrefixes = [];
@tracked currentBbox = null;
@tracked lists = [];
@tracked version = 0; // Shared version tracker for bookmarks
@tracked initialSyncDone = false;
@tracked connected = false;
@@ -46,6 +47,11 @@ export default class StorageService extends Service {
this.rs.on('connected', () => {
this.connected = true;
this.userAddress = this.rs.remote.userAddress;
this.loadLists();
});
this.rs.on('not-connected', () => {
this.loadLists();
});
this.rs.on('disconnected', () => {
@@ -54,6 +60,7 @@ export default class StorageService extends Service {
this.placesInView = [];
this.savedPlaces = [];
this.loadedPrefixes = [];
this.lists = [];
this.initialSyncDone = false;
});
@@ -61,13 +68,18 @@ export default class StorageService extends Service {
// console.debug('[rs] sync done:', result);
if (!this.initialSyncDone) {
this.initialSyncDone = true;
this.loadLists();
}
});
this.rs.scope('/places/').on('change', (event) => {
// console.debug(event);
this.handlePlaceChange(event);
debounceTask(this, 'reloadCurrentView', 200);
if (event.relativePath.startsWith('_lists/')) {
this.loadLists();
} else {
this.handlePlaceChange(event);
debounceTask(this, 'reloadCurrentView', 200);
}
});
}
@@ -120,6 +132,88 @@ export default class StorageService extends Service {
this.loadAllPlaces(required);
}
async loadLists() {
try {
if (!this.places.lists) return; // Wait for module init
// Ensure defaults exist first
await this.places.lists.initDefaults();
const lists = await this.places.lists.getAll();
this.lists = lists || [];
this.refreshPlaceListAssociations();
} catch (e) {
console.error('Failed to load lists:', e);
}
}
refreshPlaceListAssociations() {
// 1. Build an index of PlaceID -> ListID[]
const placeToListMap = new Map();
this.lists.forEach((list) => {
if (list.placeRefs && Array.isArray(list.placeRefs)) {
list.placeRefs.forEach((ref) => {
if (!ref.id) return;
if (!placeToListMap.has(ref.id)) {
placeToListMap.set(ref.id, []);
}
placeToListMap.get(ref.id).push(list.id);
});
}
});
// 2. Helper to attach lists to a place object
const attachLists = (place) => {
const listIds = placeToListMap.get(place.id) || [];
// Assign directly to object property (non-tracked mutation is fine as we trigger updates below)
place._listIds = listIds;
return place;
};
// 3. Update savedPlaces
this.savedPlaces = this.savedPlaces.map((p) => attachLists({ ...p }));
// 4. Update placesInView
this.placesInView = this.placesInView.map((p) => attachLists({ ...p }));
}
async togglePlaceList(place, listId, shouldBeInList) {
if (!place) return;
// Ensure place is saved first if it's new
let savedPlace = place;
if (!place.id || !place.geohash) {
if (shouldBeInList) {
// If adding to a list, we must save the place first
savedPlace = await this.storePlace(place);
} else {
return; // Can't remove an unsaved place from a list
}
}
try {
if (shouldBeInList) {
await this.places.lists.addPlace(
listId,
savedPlace.id,
savedPlace.geohash
);
} else {
await this.places.lists.removePlace(listId, savedPlace.id);
}
// Reload lists to reflect changes
await this.loadLists();
// Return the updated place
return this.findPlaceById(savedPlace.id);
} catch (e) {
console.error('Failed to toggle place in list:', e);
throw e;
}
}
async loadPlacesInBounds(bbox) {
// 1. Calculate required prefixes
const requiredPrefixes = getGeohashPrefixesInBbox(bbox);
@@ -173,6 +267,8 @@ export default class StorageService extends Service {
// Full reload
this.placesInView = places;
}
// Refresh list associations
this.refreshPlaceListAssociations();
} else {
if (!prefixes) this.placesInView = [];
}
@@ -190,11 +286,22 @@ export default class StorageService extends Service {
let place = this.savedPlaces.find((p) => p.id && String(p.id) === strId);
if (place) return place;
// Check placesInView as fallback
place = this.placesInView.find((p) => p.id && String(p.id) === strId);
if (place) return place;
// Then search by OSM ID
place = this.savedPlaces.find((p) => p.osmId && String(p.osmId) === strId);
if (place) return place;
place = this.placesInView.find((p) => p.osmId && String(p.osmId) === strId);
return place;
}
isPlaceSaved(id) {
return !!this.findPlaceById(id);
}
async storePlace(placeData) {
const savedPlace = await this.places.store(placeData);

View File

@@ -1,5 +1,9 @@
/* Ember supports plain CSS out of the box. More info: https://cli.emberjs.com/release/advanced-use/stylesheets/ */
:root {
--default-list-color: #ffcc33;
}
html,
body {
height: 100%;
@@ -981,3 +985,64 @@ button.create-place {
text-overflow: ellipsis;
margin-top: 2px;
}
/* Place Lists Manager */
.save-button-wrapper {
position: relative;
}
.place-lists-manager {
position: absolute;
top: 100%;
left: 0;
margin-top: 0.5rem;
background: white;
border-radius: 8px;
box-shadow: 0 4px 12px rgb(0 0 0 / 15%);
width: 220px;
z-index: 10;
padding: 0.5rem 0;
}
.place-lists-manager .list-item {
padding: 0.5rem 1rem;
display: flex;
align-items: center;
}
.place-lists-manager .list-item:hover {
background: #f8f9fa;
}
.place-lists-manager label {
display: flex;
align-items: center;
gap: 0.75rem;
width: 100%;
cursor: pointer;
margin: 0;
font-size: 0.95rem;
color: #333;
}
.place-lists-manager input[type='checkbox'] {
accent-color: #007bff;
width: 16px;
height: 16px;
cursor: pointer;
}
.place-lists-manager .list-color {
width: 12px;
height: 12px;
background-color: var(--default-list-color);
border-radius: 50%;
flex-shrink: 0;
border: 1px solid rgb(0 0 0 / 10%);
}
.place-lists-manager .divider {
height: 1px;
background: #eee;
margin: 0.5rem 0;
}

View File

@@ -0,0 +1,15 @@
import { getLocalizedName } from './osm';
export function mapToStorageSchema(place) {
return {
title: place.title || getLocalizedName(place.osmTags, 'Untitled Place'),
lat: place.lat,
lon: place.lon,
tags: [],
url: place.osmTags?.website,
osmId: String(place.osmId || place.id),
osmType: place.osmType,
osmTags: place.osmTags || {},
description: place.description,
};
}

View File

@@ -42,6 +42,9 @@ class MockStorageService extends Service {
findPlaceById() {
return null;
}
isPlaceSaved() {
return false;
}
loadPlacesInBounds() {
return [];
}

View File

@@ -41,6 +41,9 @@ module('Acceptance | search', function (hooks) {
findPlaceById() {
return null;
}
isPlaceSaved() {
return false;
}
rs = {
on: () => {},
};
@@ -85,6 +88,9 @@ module('Acceptance | search', function (hooks) {
findPlaceById() {
return null;
}
isPlaceSaved() {
return false;
}
rs = {
on: () => {},
};
@@ -130,6 +136,9 @@ module('Acceptance | search', function (hooks) {
if (id === '999') return this.savedPlaces[0];
return null;
}
isPlaceSaved(id) {
return !!this.findPlaceById(id);
}
rs = {
on: () => {},
};

View File

@@ -1,11 +1,49 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from 'marco/tests/helpers';
import { render } from '@ember/test-helpers';
import { render, click } from '@ember/test-helpers';
import Service from '@ember/service';
import PlaceDetails from 'marco/components/place-details';
module('Integration | Component | place-details', function (hooks) {
setupRenderingTest(hooks);
class StorageService extends Service {
lists = [
{ id: 'to-go', title: 'Want to go', color: '#ff00ff' },
{ id: 'to-do', title: 'To do', color: '#008000' },
];
isPlaceSaved(id) {
return false;
}
findPlaceById(id) {
return null;
}
async storePlace(place) {
return { ...place, id: '123', createdAt: new Date().toISOString() };
}
async removePlace() {
return true;
}
async togglePlaceList() {
return true;
}
}
hooks.beforeEach(function () {
this.owner.register('service:storage', StorageService);
// Mock Router for all tests
class MockRouter extends Service {
transitionTo() {}
}
this.owner.register('service:router', MockRouter);
});
test('it formats coordinates correctly', async function (assert) {
const place = {
title: 'Test Place',
@@ -34,4 +72,187 @@ module('Integration | Component | place-details', function (hooks) {
assert.dom('.place-details h3').hasText('Place without Coords');
assert.dom('.meta-info a[href*="geo:"]').doesNotExist();
});
test('it reveals the list manager when save is clicked', async function (assert) {
const place = {
title: 'Cool Cafe',
lat: 10,
lon: 10,
};
await render(<template><PlaceDetails @place={{place}} /></template>);
// Manager is initially hidden
assert.dom('.place-lists-manager').doesNotExist();
// Find the Save button
// It's the first button in .actions
const saveBtn = this.element.querySelector('.actions button');
await click(saveBtn);
// Manager should be visible now
assert.dom('.place-lists-manager').exists();
// Check for default lists from mock service
assert.dom('.place-lists-manager').includesText('Want to go');
assert.dom('.place-lists-manager').includesText('To do');
assert.dom('.place-lists-manager').includesText('Saved');
});
test('it handles saving a new place via master toggle', async function (assert) {
let storedPlace = null;
// Override mock service specifically for this test to spy on storePlace
class MockStorage extends Service {
lists = [];
async storePlace(place) {
storedPlace = place;
return { ...place, id: 'new-id', createdAt: new Date().toISOString() };
}
isPlaceSaved() {
return false;
}
findPlaceById() {
return null;
}
}
this.owner.register('service:storage', MockStorage);
const place = {
title: 'New Spot',
lat: 20,
lon: 20,
};
await render(<template><PlaceDetails @place={{place}} /></template>);
// Open manager
await click('.actions button');
// Find master "Saved" toggle
const masterToggle = this.element.querySelector(
'.place-lists-manager .master-toggle input'
);
// It should be unchecked initially for a new place
assert.dom(masterToggle).isNotChecked();
// Click it to save
await click(masterToggle);
// Verify storePlace was called
assert.ok(storedPlace, 'storePlace was called');
assert.strictEqual(storedPlace.title, 'New Spot');
});
test('it handles removing a saved place via master toggle', async function (assert) {
let removedPlaceId = null;
class MockStorage extends Service {
lists = [];
async removePlace(place) {
removedPlaceId = place.id;
}
isPlaceSaved() {
return true;
}
}
this.owner.register('service:storage', MockStorage);
const place = {
id: 'saved-id',
title: 'Saved Spot',
lat: 30,
lon: 30,
createdAt: '2023-01-01', // Marks it as saved
};
await render(<template><PlaceDetails @place={{place}} /></template>);
// Open manager
await click('.actions button');
// Find master "Saved" toggle
const masterToggle = this.element.querySelector(
'.place-lists-manager .master-toggle input'
);
// It should be checked initially for a saved place
assert.dom(masterToggle).isChecked();
// Click it to remove
await click(masterToggle);
assert.strictEqual(removedPlaceId, 'saved-id', 'removePlace was called');
assert.deepEqual(place._listIds, [], '_listIds was cleared on the object');
});
test('it adds place to a list', async function (assert) {
let listId = null;
let placeArg = null;
let shouldAdd = null;
class MockStorage extends Service {
lists = [{ id: 'favs', title: 'Favorites', color: 'red' }];
async togglePlaceList(place, id, add) {
placeArg = place;
listId = id;
shouldAdd = add;
}
isPlaceSaved() {
return true;
}
}
this.owner.register('service:storage', MockStorage);
// Provide a place that is already saved
const place = {
id: 'p1',
title: 'My Spot',
createdAt: '2023-01-01',
_listIds: [], // Not in any list yet
};
await render(<template><PlaceDetails @place={{place}} /></template>);
// Open manager
await click('.actions button');
// Find the checkbox for "Favorites"
const checkbox = this.element.querySelectorAll(
'.place-lists-manager input[type="checkbox"]'
)[1]; // Index 1 because 0 is master toggle
await click(checkbox);
assert.strictEqual(listId, 'favs');
assert.strictEqual(placeArg.id, 'p1');
assert.true(shouldAdd);
});
test('it respects storage service state over stale place object', async function (assert) {
class MockStorage extends Service {
lists = [];
isPlaceSaved() {
return false;
}
findPlaceById() {
return null;
}
}
this.owner.register('service:storage', MockStorage);
const place = {
id: 'stale-id',
title: 'Stale Place',
createdAt: '2023-01-01', // Looks saved
};
await render(<template><PlaceDetails @place={{place}} /></template>);
// Button should say "Save", not "Saved" because isPlaceSaved returns false
assert.dom('.actions button').hasText('Save');
assert.dom('.actions button').doesNotHaveClass('btn-secondary');
});
});

View File

@@ -0,0 +1,58 @@
import { mapToStorageSchema } from 'marco/utils/place-mapping';
import { module, test } from 'qunit';
module('Unit | Utility | place-mapping', function () {
test('it maps a raw place object to the storage schema', function (assert) {
const rawPlace = {
osmId: 12345,
osmType: 'node',
lat: 52.52,
lon: 13.405,
osmTags: {
name: 'Test Place',
website: 'https://example.com',
},
description: 'A test description',
};
const result = mapToStorageSchema(rawPlace);
assert.strictEqual(result.title, 'Test Place');
assert.strictEqual(result.lat, 52.52);
assert.strictEqual(result.lon, 13.405);
assert.strictEqual(result.osmId, '12345');
assert.strictEqual(result.osmType, 'node');
assert.strictEqual(result.url, 'https://example.com');
assert.strictEqual(result.description, 'A test description');
assert.deepEqual(result.osmTags, rawPlace.osmTags);
assert.deepEqual(result.tags, []);
});
test('it prioritizes place.title over osmTags.name', function (assert) {
const rawPlace = {
osmId: 123,
lat: 0,
lon: 0,
title: 'Custom Title',
osmTags: {
name: 'OSM Name',
},
};
const result = mapToStorageSchema(rawPlace);
assert.strictEqual(result.title, 'Custom Title');
});
test('it handles fallback title correctly when no name is present', function (assert) {
const rawPlace = {
id: 987,
lat: 10,
lon: 20,
osmTags: {},
};
const result = mapToStorageSchema(rawPlace);
assert.strictEqual(result.title, 'Untitled Place');
assert.strictEqual(result.osmId, '987');
});
});

View File

@@ -3,9 +3,9 @@ import { extensions, ember } from '@embroider/vite';
import { babel } from '@rollup/plugin-babel';
export default defineConfig({
server: {
host: '0.0.0.0'
},
// server: {
// host: '0.0.0.0',
// },
plugins: [
ember(),
// extra plugins here