From 13538f6ca9171fcb8692924fc752b92aaa5104f7 Mon Sep 17 00:00:00 2001 From: Florian Zumpe Date: Sun, 27 Sep 2026 22:32:54 +0200 Subject: [PATCH] waypoint management fix --- public/js/editor-ui.js | 33 +++++++++++++++++++++++++++----- test/editor-ui.test.js | 43 +++++++++++++++++++++++++++++++++++++++--- 2 files changed, 68 insertions(+), 8 deletions(-) diff --git a/public/js/editor-ui.js b/public/js/editor-ui.js index f75c4dc..e0d8dc0 100644 --- a/public/js/editor-ui.js +++ b/public/js/editor-ui.js @@ -126,6 +126,26 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {}; return text(poi?.id ?? poi?.poiId); } + function numericPoiId(value) { + const raw = text(value).trim(); + if (!raw) return null; + const parsed = Number(raw); + return Number.isSafeInteger(parsed) && parsed >= 0 ? parsed : null; + } + + function nextLocalPoiId() { + let highest = -1; + for (const poi of state.pois) { + const identifier = numericPoiId(poi?.id ?? poi?.poiId); + if (identifier != null && identifier > highest) highest = identifier; + } + const next = highest + 1; + if (!Number.isSafeInteger(next)) { + throw new Error('Für diese lokale Strecke kann keine weitere Streckenpunkt-ID vergeben werden.'); + } + return next; + } + function routeName(route) { return text(route?.name || route?.title || 'Unbenannte Strecke'); } @@ -479,7 +499,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {}; byId(ids.routeName).value = routeName(route) === 'Unbenannte Strecke' ? '' : routeName(route); byId(ids.routeDescription).value = text(route?.description); byId(ids.routeGpx).value = ''; - setHidden(byId(ids.routeGpxGroup), !routeId(route)); + setHidden(byId(ids.routeGpxGroup), state.currentSource !== 'server'); const uploadButton = byId(ids.routeUpload); if (state.currentSource === 'local') { @@ -983,7 +1003,9 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {}; const accuracy = Number(position?.accuracy); state.currentPoi = null; + const identifier = state.currentSource === 'local' ? nextLocalPoiId() : null; fillPoiForm({ + ...(identifier == null ? {} : { id: identifier }), lat, lon, triggerRadiusM: Number.isFinite(accuracy) && accuracy > 0 ? accuracy : 25 @@ -1062,9 +1084,10 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {}; if (!Number.isFinite(values.lat) || !Number.isFinite(values.lon)) { throw new TypeError('Für einen Streckenpunkt sind gültige Breiten- und Längengrade erforderlich.'); } - const currentId = Number(byId(ids.poiId).value); - const identifier = Number.isSafeInteger(currentId) ? currentId : ns.LocalRoutes.createPoiId(); - const existing = state.pois.find(poi => Number(poi.id) === identifier) || state.currentPoi; + const currentId = numericPoiId(byId(ids.poiId).value); + const identifier = currentId ?? nextLocalPoiId(); + byId(ids.poiId).value = String(identifier); + const existing = state.pois.find(poi => numericPoiId(poi?.id ?? poi?.poiId) === identifier) || state.currentPoi; const media = await mediaFromLocalForm(existing, identifier); const savedPoi = { ...existing, @@ -1074,7 +1097,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {}; id: identifier, sequence: existing?.sequence ?? state.pois.length }; - const index = state.pois.findIndex(poi => Number(poi.id) === identifier); + const index = state.pois.findIndex(poi => numericPoiId(poi?.id ?? poi?.poiId) === identifier); if (index >= 0) state.pois[index] = savedPoi; else state.pois.push(savedPoi); diff --git a/test/editor-ui.test.js b/test/editor-ui.test.js index b1ed6d5..b136fd8 100644 --- a/test/editor-ui.test.js +++ b/test/editor-ui.test.js @@ -78,7 +78,7 @@ test('editor markup and source describe local draft creation instead of immediat assert.match(ui, /sendForm\('\/routes',\s*'POST'/); assert.match(ui, /serverRouteId/); assert.match(ui, /position\?\.accuracy/); - assert.match(ui, /setHidden\(byId\(ids\.routeGpxGroup\), !routeId\(route\)\)/); + assert.match(ui, /setHidden\(byId\(ids\.routeGpxGroup\), state\.currentSource !== 'server'\)/); }); class FakeElement extends EventTarget { @@ -386,7 +386,7 @@ test('public editor workflow saves new route and GPX Streckenpunkt locally witho assert.equal(elements.get('editor-status').textContent, 'Speichern erfolgreich.'); assert.equal(elements.get('editor-route-upload').hidden, false); assert.equal(elements.get('editor-route-upload').disabled, false); - assert.equal(elements.get('editor-route-gpx-group').hidden, false); + assert.equal(elements.get('editor-route-gpx-group').hidden, true); elements.get('editor-add-poi').dispatchEvent(pointerEvent()); assert.equal(elements.get('editor-poi-audio').clickCount, 0); @@ -399,7 +399,7 @@ test('public editor workflow saves new route and GPX Streckenpunkt locally witho assert.equal(elements.get('editor-poi-lon').value, '10.987654321'); assert.equal(elements.get('editor-poi-radius').value, '4.321'); elements.get('editor-poi-title').value = 'Aussicht'; - elements.get('editor-poi-description').value = 'Text zur Station'; + elements.get('editor-poi-description').value = 'Text zum Streckenpunkt'; elements.get('editor-poi-audio').files = [{ name: 'aussicht.mp3', size: 5, type: 'audio/mpeg' }]; elements.get('editor-poi-audio').dispatchEvent(new Event('change')); elements.get('editor-poi-images').files = [{ name: 'aussicht.jpg', size: 7, type: 'image/jpeg' }]; @@ -488,6 +488,42 @@ test('public editor workflow saves new route and GPX Streckenpunkt locally witho assert.equal(elements.get('editor-button').attributes.get('aria-expanded'), 'false'); }); +test('local Streckenpunkt ids start at zero and increment from the highest existing id', async () => { + const sourcePath = path.join(root, 'public/js/editor-ui.js'); + const source = await fs.readFile(sourcePath, 'utf8'); + const { context, elements, packages } = buildEditorContext(); + + vm.runInNewContext(source, context, { filename: sourcePath }); + await context.Wegwichtel.EditorUi.openNewLocalRoute(); + elements.get('editor-route-name').value = 'ID-Test'; + elements.get('editor-route-form').dispatchEvent(submitEvent()); + await tick(); + await tick(); + + elements.get('editor-add-poi').dispatchEvent(pointerEvent()); + assert.equal(elements.get('editor-poi-id').value, '0'); + elements.get('editor-poi-title').value = 'Erster Streckenpunkt'; + elements.get('editor-poi-form').dispatchEvent(submitEvent()); + await tick(); + await tick(); + + elements.get('editor-add-poi').dispatchEvent(pointerEvent()); + assert.equal(elements.get('editor-poi-id').value, '1'); + elements.get('editor-poi-title').value = 'Zweiter Streckenpunkt'; + elements.get('editor-poi-form').dispatchEvent(submitEvent()); + await tick(); + await tick(); + + const saved = packages.get('local-new'); + assert.equal(saved.pois.length, 2); + assert.deepEqual(Array.from(saved.pois, poi => poi.id), [0, 1]); + assert.deepEqual(Array.from(saved.pois, poi => poi.title), ['Erster Streckenpunkt', 'Zweiter Streckenpunkt']); + + const pointItems = elements.get('editor-poi-list').children.filter(child => child.dataset.poiId); + assert.equal(pointItems.length, 2); +}); + + test('route and Streckenpunkt list expose red delete actions and remove local data', async () => { const sourcePath = path.join(root, 'public/js/editor-ui.js'); const source = await fs.readFile(sourcePath, 'utf8'); @@ -560,6 +596,7 @@ test('editing an activated server route uses PUT and never writes activation sta await tick(); assert.equal(elements.get('editor-route-id').value, '1'); + assert.equal(elements.get('editor-route-gpx-group').hidden, false); assert.match(elements.get('editor-route-activated').textContent, /Veröffentlicht am/); assert.equal(elements.get('editor-route-upload').hidden, true);