editor overhaul
Sonarqube Scanner / Prepare environment (push) Successful in 7s
Sonarqube Scanner / Build and test (push) Successful in 23s
Sonarqube Scanner / SonarQube analysis (push) Successful in 56s
Sonarqube Scanner / Deploy to Server (push) Successful in 8s

This commit is contained in:
Florian Zumpe
2026-10-18 14:26:25 +02:00
parent d33a67b8ae
commit 9c4d5f6324
17 changed files with 359 additions and 55 deletions
+78 -16
View File
@@ -368,6 +368,9 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
const label = document.createElement('div');
appendText(label, 'strong', routeName(route));
appendText(label, 'small', routeSource(route) === 'local' ? ns.I18n.t("app.draft") : ns.I18n.t("editor.published-route"));
if (routeSource(route) === 'local' && route.submissionStatus) {
appendText(label, 'small', ns.I18n.t(`editor.${route.submissionStatus}`));
}
const actions = document.createElement('div');
actions.className = classes.iconRow;
@@ -458,6 +461,36 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
setStatus('');
// Local drafts are usable even while this request is waiting for a connection.
void refreshPublicRoutes(requestGeneration);
void refreshSubmittedRoutes(requestGeneration);
}
async function refreshSubmittedRoutes(requestGeneration) {
const submitted = localRouteSummaries().filter(route => route.serverRouteId);
await Promise.allSettled(submitted.map(async route => {
const existing = await findSubmittedRoute(route.serverRouteId);
if (requestGeneration !== routeListGeneration) return;
const latest = ns.LocalRoutes.get(route.id);
if (!latest || String(latest.route.serverRouteId) !== String(route.serverRouteId)) return;
const submissionStatus = submittedStatus(existing);
if ((latest.route.submissionStatus || null) === submissionStatus) return;
latest.route.submissionStatus = submissionStatus;
const saved = ns.LocalRoutes.save(latest);
if (state.currentSource === 'local' && routeId(state.currentRoute) === route.id) {
state.currentRoute.submissionStatus = submissionStatus;
renderLocalSubmissionStatus(saved.route);
}
}));
if (requestGeneration !== routeListGeneration) return;
state.routes = [...localRouteSummaries(), ...state.routes.filter(route => routeSource(route) === 'server')];
renderRouteList();
}
function renderLocalSubmissionStatus(route) {
const uploaded = Boolean(route?.serverRouteId);
const uploadButton = byId(ids.routeUpload);
if (uploadButton) uploadButton.textContent = ns.I18n.t(uploaded ? 'editor.submitted' : 'editor.submit');
const hint = uploaded ? 'editor.submitted-hint' : 'editor.draft-hint';
byId(ids.routeActivated).textContent = ns.I18n.t(route?.submissionStatus ? `editor.${route.submissionStatus}-hint` : hint);
}
async function refreshPublicRoutes(requestGeneration) {
@@ -532,13 +565,9 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
const uploadButton = byId(ids.routeUpload);
if (state.currentSource === 'local') {
const uploaded = Boolean(route?.serverRouteId);
setHidden(uploadButton, false);
setDisabled(uploadButton, false);
if (uploadButton) uploadButton.textContent = uploaded ? ns.I18n.t("editor.submitted") : ns.I18n.t("editor.submit");
byId(ids.routeActivated).textContent = uploaded
? ns.I18n.t("editor.submitted-hint")
: ns.I18n.t("editor.draft-hint");
renderLocalSubmissionStatus(route);
} else {
setHidden(uploadButton, true);
setDisabled(uploadButton, true);
@@ -946,23 +975,56 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
return ns.LocalRoutes.save(routePackage);
}
function submittedStatus(existing) {
if (!existing) return 'removed';
return existing.status === 'deleted' ? 'rejected' : null;
}
async function findSubmittedRoute(id) {
try {
const payload = await readJson(`/routes/${encodeURIComponent(id)}?includeDeleted=true`);
return payload.route || payload;
} catch (error) {
if (error.status === 404) return null;
throw error;
}
}
function clearSubmittedReferences(routePackage) {
routePackage.route.serverRouteId = null;
routePackage.route.uploadedAt = null;
for (const poi of routePackage.pois || []) {
delete poi.serverPoiId;
delete poi.serverAudio;
for (const image of poi.images || []) delete image.serverPictureId;
}
checkpointLocalUpload(routePackage);
}
async function prepareServerRoute(routePackage) {
const id = routePackage.route.serverRouteId;
const replacedSubmission = Boolean(id || routePackage.route.submissionStatus);
if (id) {
const payload = await readJson(`/routes/${encodeURIComponent(id)}`);
const existing = payload.route || payload;
if (existing.activatedAt || existing.activated_at) {
throw new Error(ns.I18n.t("editor.read-only-hint"));
const existing = await findSubmittedRoute(id);
if (!existing || existing.status === 'deleted') {
// Preserve the old trash entry; submit a new draft with fresh references.
routePackage.route.submissionStatus = submittedStatus(existing);
clearSubmittedReferences(routePackage);
} else {
if (existing.activatedAt || existing.activated_at) {
throw new Error(ns.I18n.t("editor.read-only-hint"));
}
await sendForm(`/routes/${encodeURIComponent(id)}`, 'PUT', localRouteUploadForm(routePackage));
return { id, previousPois: existing.pois || [] };
}
await sendForm(`/routes/${encodeURIComponent(id)}`, 'PUT', localRouteUploadForm(routePackage));
return { id, previousPois: existing.pois || [] };
}
const payload = await sendForm('/routes', 'POST', localRouteUploadForm(routePackage));
const createdId = routeId(payload.route || payload);
if (!createdId) throw new Error(ns.I18n.t("editor.error.send-route"));
routePackage.route.serverRouteId = createdId;
routePackage.route.submissionStatus = null;
checkpointLocalUpload(routePackage);
return { id: createdId, previousPois: [] };
return { id: createdId, previousPois: [], recreated: replacedSubmission };
}
async function syncLocalPoi(base, poi, index, routePackage) {
@@ -1011,7 +1073,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
}
async function createServerRouteFromLocal(routePackage) {
const { id, previousPois } = await prepareServerRoute(routePackage);
const { id, previousPois, recreated } = await prepareServerRoute(routePackage);
const base = `/routes/${encodeURIComponent(id)}/pois`;
const orderedPois = [...(routePackage.pois || [])]
.sort((left, right) => Number(left.sequence) - Number(right.sequence));
@@ -1023,7 +1085,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
await syncLocalPictures(mediaBase, poi, oldPoi, routePackage);
}
await removeMissingServerItems(previousPois, orderedPois, 'serverPoiId', base);
return id;
return { id, recreated };
}
async function uploadLocalRoute() {
@@ -1039,7 +1101,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
const routePackage = ns.LocalRoutes.get(localId);
if (!routePackage) throw new Error(ns.I18n.t("editor.error.draft-missing"));
const serverRouteId = await createServerRouteFromLocal(routePackage);
const { id: serverRouteId, recreated } = await createServerRouteFromLocal(routePackage);
const uploadedAt = new Date().toISOString();
const saved = ns.LocalRoutes.save({
...routePackage,
@@ -1053,7 +1115,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
state.pois = saved.pois;
fillRouteForm(state.currentRoute);
await loadRoutes();
setStatus(ns.I18n.t("editor.submit-success"));
setStatus(ns.I18n.t(recreated ? 'editor.resubmit-success' : 'editor.submit-success'));
} finally {
// Keep acknowledged server IDs even if a later media request failed.
const latest = ns.LocalRoutes.get(routeId(state.currentRoute));