WIP Add places to lists
This commit is contained in:
@@ -60,9 +60,27 @@ 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 = '#ffcc33'; // Default Yellow
|
||||
|
||||
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 +91,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
|
||||
});
|
||||
|
||||
|
||||
@@ -4,20 +4,31 @@ 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 {
|
||||
@tracked isEditing = false;
|
||||
@tracked showLists = false;
|
||||
|
||||
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 || {};
|
||||
}
|
||||
@@ -37,6 +48,16 @@ export default class PlaceDetails extends Component {
|
||||
this.isEditing = false;
|
||||
}
|
||||
|
||||
@action
|
||||
toggleLists() {
|
||||
this.showLists = !this.showLists;
|
||||
}
|
||||
|
||||
@action
|
||||
closeLists() {
|
||||
this.showLists = false;
|
||||
}
|
||||
|
||||
@action
|
||||
async saveChanges(changes) {
|
||||
if (this.args.onSave) {
|
||||
@@ -247,21 +268,30 @@ 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.place.createdAt
|
||||
"btn btn-secondary"
|
||||
"btn btn-outline"
|
||||
}}
|
||||
{{on "click" this.toggleLists}}
|
||||
>
|
||||
<Icon
|
||||
@name="bookmark"
|
||||
@color={{if this.place.createdAt "currentColor" "#007bff"}}
|
||||
/>
|
||||
{{if this.place.createdAt "Saved" "Save"}}
|
||||
</button>
|
||||
|
||||
{{#if this.showLists}}
|
||||
<PlaceListsManager
|
||||
@place={{this.saveablePlace}}
|
||||
@onClose={{this.closeLists}}
|
||||
/>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
{{#if this.place.createdAt}}
|
||||
<button
|
||||
|
||||
95
app/components/place-lists-manager.gjs
Normal file
95
app/components/place-lists-manager.gjs
Normal file
@@ -0,0 +1,95 @@
|
||||
import Component from '@glimmer/component';
|
||||
import { service } from '@ember/service';
|
||||
import { action } from '@ember/object';
|
||||
import { on } from '@ember/modifier';
|
||||
import { fn } from '@ember/helper';
|
||||
import Icon from './icon';
|
||||
|
||||
export default class PlaceListsManager extends Component {
|
||||
@service storage;
|
||||
|
||||
get isSaved() {
|
||||
return !!this.args.place.createdAt;
|
||||
}
|
||||
|
||||
get placeListIds() {
|
||||
return this.args.place._listIds || [];
|
||||
}
|
||||
|
||||
@action
|
||||
isInList(list) {
|
||||
if (!this.placeListIds) return false;
|
||||
return this.placeListIds.includes(list.id);
|
||||
}
|
||||
|
||||
@action
|
||||
async toggleSaved() {
|
||||
if (this.isSaved) {
|
||||
if (confirm(`Remove "${this.args.place.title}" from saved places?`)) {
|
||||
await this.storage.removePlace(this.args.place);
|
||||
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">
|
||||
<div class="list-item master-toggle">
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={{this.isSaved}}
|
||||
{{on "change" this.toggleSaved}}
|
||||
/>
|
||||
<span class="list-name">Saved</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}}
|
||||
/>
|
||||
<span class="list-color" style="background-color: {{list.color}}"></span>
|
||||
<span class="list-name">{{list.title}}</span>
|
||||
</label>
|
||||
</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
}
|
||||
@@ -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,7 @@ export default class StorageService extends Service {
|
||||
this.rs.on('connected', () => {
|
||||
this.connected = true;
|
||||
this.userAddress = this.rs.remote.userAddress;
|
||||
this.loadLists();
|
||||
});
|
||||
|
||||
this.rs.on('disconnected', () => {
|
||||
@@ -54,6 +56,7 @@ export default class StorageService extends Service {
|
||||
this.placesInView = [];
|
||||
this.savedPlaces = [];
|
||||
this.loadedPrefixes = [];
|
||||
this.lists = [];
|
||||
this.initialSyncDone = false;
|
||||
});
|
||||
|
||||
@@ -61,13 +64,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 +128,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 +263,8 @@ export default class StorageService extends Service {
|
||||
// Full reload
|
||||
this.placesInView = places;
|
||||
}
|
||||
// Refresh list associations
|
||||
this.refreshPlaceListAssociations();
|
||||
} else {
|
||||
if (!prefixes) this.placesInView = [];
|
||||
}
|
||||
|
||||
@@ -981,3 +981,63 @@ 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;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
border: 1px solid rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.place-lists-manager .divider {
|
||||
height: 1px;
|
||||
background: #eee;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
15
app/utils/place-mapping.js
Normal file
15
app/utils/place-mapping.js
Normal 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,
|
||||
};
|
||||
}
|
||||
@@ -1,11 +1,35 @@
|
||||
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' },
|
||||
];
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
test('it formats coordinates correctly', async function (assert) {
|
||||
const place = {
|
||||
title: 'Test Place',
|
||||
@@ -34,4 +58,160 @@ 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() };
|
||||
}
|
||||
}
|
||||
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 removedPlace = null;
|
||||
let confirmCalled = false;
|
||||
|
||||
// Mock confirm
|
||||
const originalConfirm = window.confirm;
|
||||
window.confirm = () => {
|
||||
confirmCalled = true;
|
||||
return true;
|
||||
};
|
||||
|
||||
class MockStorage extends Service {
|
||||
lists = [];
|
||||
async removePlace(place) {
|
||||
removedPlace = place;
|
||||
}
|
||||
}
|
||||
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.ok(confirmCalled, 'confirm dialog was shown');
|
||||
assert.strictEqual(removedPlace.id, 'saved-id', 'removePlace was called');
|
||||
|
||||
// Restore confirm
|
||||
window.confirm = originalConfirm;
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
58
tests/unit/utils/place-mapping-test.js
Normal file
58
tests/unit/utils/place-mapping-test.js
Normal 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');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user