From 346bfe4cffcf9c7604cac9db22ab71b6693f1343 Mon Sep 17 00:00:00 2001 From: Simon Cordt Date: Sun, 28 Sep 2025 13:49:07 +0200 Subject: [PATCH] Add queue export/import functionality to web UI --- web/js/pqueue.api.js | 16 ++++++++++ web/js/pqueue.events.js | 63 +++++++++++++++++++++++++++++++++++++++ web/js/pqueue.ui.queue.js | 19 +++++++++++- 3 files changed, 97 insertions(+), 1 deletion(-) diff --git a/web/js/pqueue.api.js b/web/js/pqueue.api.js index 50683aa..db02d7c 100644 --- a/web/js/pqueue.api.js +++ b/web/js/pqueue.api.js @@ -22,6 +22,22 @@ headers: { "Content-Type": "application/json" }, body: JSON.stringify({ order }), }), + exportQueue: () => + fetch("/api/pqueue/export", { method: "GET" }).then((r) => r.json()), + importQueue: (fileOrJson) => { + try { + if (fileOrJson instanceof File || fileOrJson instanceof Blob) { + const form = new FormData(); + form.append("file", fileOrJson, fileOrJson.name || "queue.json"); + return fetch("/api/pqueue/import", { method: "POST", body: form }).then((r) => r.json()); + } + } catch (err) { /* fall through to JSON */ } + return fetch("/api/pqueue/import", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(fileOrJson), + }).then((r) => r.json()); + }, setPriority: (prompt_id, priority) => fetch("/api/pqueue/priority", { method: "PATCH", diff --git a/web/js/pqueue.events.js b/web/js/pqueue.events.js index 8465ae2..907cf04 100644 --- a/web/js/pqueue.events.js +++ b/web/js/pqueue.events.js @@ -52,6 +52,69 @@ } catch (err) { /* noop */ } }, + async exportQueueToFile() { + refreshRefs(); + try { + const data = await API.exportQueue(); + const text = JSON.stringify(data, null, 2); + const blob = new Blob([text], { type: "application/json" }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + const dt = new Date(); + const pad = (n) => String(n).padStart(2, '0'); + const fname = `pqueue-${dt.getFullYear()}${pad(dt.getMonth()+1)}${pad(dt.getDate())}-${pad(dt.getHours())}${pad(dt.getMinutes())}${pad(dt.getSeconds())}.json`; + a.download = fname; + document.body.appendChild(a); + a.click(); + a.remove(); + URL.revokeObjectURL(url); + setStatusMessage("Queue exported"); + } catch (err) { + console.error("pqueue: export failed", err); + state.error = err?.message || "Failed to export queue"; + UI.updateToolbarStatus(); + } + }, + + openImportDialog() { + refreshRefs(); + try { + let input = document.getElementById('pqueue-import-input'); + if (!input) { + input = document.createElement('input'); + input.type = 'file'; + input.accept = 'application/json,.json'; + input.id = 'pqueue-import-input'; + input.style.display = 'none'; + document.body.appendChild(input); + input.addEventListener('change', Events.handleImportSelected, { once: false }); + } + input.value = ''; + input.click(); + } catch (err) { /* noop */ } + }, + + async handleImportSelected(ev) { + refreshRefs(); + try { + const input = ev.currentTarget || ev.target; + const file = input?.files?.[0]; + if (!file) return; + const result = await API.importQueue(file); + const cnt = Number(result?.count || (Array.isArray(result?.imported) ? result.imported.length : 0)); + setStatusMessage(`Imported ${cnt} item${cnt === 1 ? '' : 's'}`); + await refresh({ force: true }); + // Clear selection after import + state.selectedPending.clear(); + UI.updateSelectionUI(); + } catch (err) { + console.error("pqueue: import failed", err); + state.error = err?.message || "Failed to import queue"; + UI.updateToolbarStatus(); + } + }, + async syncLatestForAsc() { refreshRefs(); try { diff --git a/web/js/pqueue.ui.queue.js b/web/js/pqueue.ui.queue.js index 18ce3e4..4bac827 100644 --- a/web/js/pqueue.ui.queue.js +++ b/web/js/pqueue.ui.queue.js @@ -5,6 +5,7 @@ const API = (window.PQueue && window.PQueue.API) || window.API; const UI = (window.PQueue && window.PQueue.UI) || (window.PQueue = (window.PQueue || {}), window.PQueue.UI = {}, window.PQueue.UI); const Format = (window.PQueue && window.PQueue.Format) || window.Format; + const Events = (window.PQueue && window.PQueue.Events) || window.Events; const setStatusMessage = (window.PQueue && window.PQueue.setStatusMessage) || window.setStatusMessage; // Metrics card removed; metrics now live within the toolbar summary. @@ -79,6 +80,22 @@ state.dom.pendingCount = footerCount; state.dom.pendingUpdated = footerUpdated; + const exportBtn = UI.button({ + id: "pqueue-export", + icon: "ti ti-download", + variant: "ghost", + subtle: true, + title: "Export queue", + onClick: () => { try { (Events && Events.exportQueueToFile) ? Events.exportQueueToFile() : null; } catch (err) {} }, + }); + const importBtn = UI.button({ + id: "pqueue-import", + icon: "ti ti-upload", + variant: "ghost", + subtle: true, + title: "Import queue", + onClick: () => { try { (Events && Events.openImportDialog) ? Events.openImportDialog() : null; } catch (err) {} }, + }); const deleteSelected = UI.button({ id: "pqueue-delete-selected", icon: "ti ti-trash", @@ -91,7 +108,7 @@ const footer = UI.el("div", { class: "pqueue-table__footer" }, [ UI.el("div", { class: "pqueue-table__footer-left" }, [footerCount, footerUpdated]), - UI.el("div", { class: "pqueue-table__footer-right" }, [deleteSelected]), + UI.el("div", { class: "pqueue-table__footer-right" }, [exportBtn, importBtn, deleteSelected]), ]); // Build collapsible card (mirrors metrics behavior)