Show "Converting..." once an upload's bytes are fully sent
fetch() has no cross-browser upload progress event, so the upload path now uses XMLHttpRequest instead -- its upload.progress firing with loaded === total is the only client-observable signal that the server has moved from receiving bytes to converting them, worth surfacing now that a large POI export's conversion (GpxStore#save) can itself take several seconds on top of the transfer. Multi-file uploads now also show which file (i of N) is in flight, previously just a static "Uploading N files..." for the whole batch. Verified the 401 path still redirects to /login through the new XHR flow the same way fetch/redirectIfUnauthorized did.
This commit is contained in:
1 parent
37bc14f3ba
commit
45567fc520
1 file changed
+34
-6
+34
-6
@@ -720,19 +720,29 @@ export class GpxPanelControl {
|
|||||||
const files = [...fileList].filter((file) => /\.(gpx|fit|geojson|zip)$/i.test(file.name));
|
const files = [...fileList].filter((file) => /\.(gpx|fit|geojson|zip)$/i.test(file.name));
|
||||||
if (files.length === 0) return;
|
if (files.length === 0) return;
|
||||||
|
|
||||||
this._setStatus(files.length === 1 ? "Uploading…" : `Uploading ${files.length} files…`);
|
|
||||||
let tree = null;
|
let tree = null;
|
||||||
let failed = 0;
|
let failed = 0;
|
||||||
|
|
||||||
for (const file of files) {
|
for (const [index, file] of files.entries()) {
|
||||||
|
this._setStatus(files.length === 1 ? "Uploading…" : `Uploading ${index + 1} of ${files.length}…`);
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append("file", file);
|
formData.append("file", file);
|
||||||
formData.append("folder", folder);
|
formData.append("folder", folder);
|
||||||
try {
|
try {
|
||||||
const response = await fetch(UPLOAD_URL, { method: "POST", body: formData });
|
// fetch() has no cross-browser upload progress event, so this one
|
||||||
if (redirectIfUnauthorized(response)) return;
|
// upload uses XMLHttpRequest instead — its upload.progress firing
|
||||||
if (!response.ok) throw new Error("upload failed");
|
// with loaded === total is the only client-observable signal that
|
||||||
({ tree } = await response.json());
|
// the server has (or is about to start) converting rather than
|
||||||
|
// still receiving bytes, worth surfacing now that a several-
|
||||||
|
// thousand-waypoint POI export's conversion can itself take
|
||||||
|
// several more seconds on top of the transfer (GpxStore#save).
|
||||||
|
const xhr = await uploadFile(formData, () => this._setStatus("Converting…"));
|
||||||
|
if (xhr.status === 401) {
|
||||||
|
window.location.href = "/login";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (xhr.status < 200 || xhr.status >= 300) throw new Error("upload failed");
|
||||||
|
({ tree } = JSON.parse(xhr.responseText));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
failed += 1;
|
failed += 1;
|
||||||
}
|
}
|
||||||
@@ -814,6 +824,24 @@ function redirectIfUnauthorized(response) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// XMLHttpRequest rather than fetch() — only so onUploadComplete (the
|
||||||
|
// upload.progress event) is available at all; every other write action in
|
||||||
|
// this file stays on fetch()/redirectIfUnauthorized. Resolves with the xhr
|
||||||
|
// itself (not just responseText) so the caller can still read .status the
|
||||||
|
// same way a fetch Response would let it.
|
||||||
|
function uploadFile(formData, onUploadComplete) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const xhr = new XMLHttpRequest();
|
||||||
|
xhr.open("POST", UPLOAD_URL);
|
||||||
|
xhr.upload.addEventListener("progress", (event) => {
|
||||||
|
if (event.lengthComputable && event.loaded === event.total) onUploadComplete();
|
||||||
|
});
|
||||||
|
xhr.addEventListener("load", () => resolve(xhr));
|
||||||
|
xhr.addEventListener("error", () => reject(new Error("network error")));
|
||||||
|
xhr.send(formData);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Rasterizes a hand-authored SVG icon to ImageData for map.addImage(), at a
|
// Rasterizes a hand-authored SVG icon to ImageData for map.addImage(), at a
|
||||||
// fixed square size (the source SVGs have no explicit width/height, just a
|
// fixed square size (the source SVGs have no explicit width/height, just a
|
||||||
// viewBox, so one is injected — otherwise an <img> falls back to the CSS
|
// viewBox, so one is injected — otherwise an <img> falls back to the CSS
|
||||||
|
|||||||
Reference in new issue
Block a user