diff --git a/CHANGELOG.md b/CHANGELOG.md index 94b58e7..f75e0ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,6 +46,9 @@ All notable changes to Jarl are documented here. The format follows indents and Enter keeps the indentation (Esc, then Tab, moves on). With a draft open, the tables show the draft's values, so a staged edit stays visible after Save. +- The pending-changes banner has an Apply now button: it applies your + draft without going to the Changes page, and stays on the page you're + on. If the changes are refused, a dialog says why and links to Changes. - Blocking page: blocklists and rules have an Edit button (name, URL, views, comment; a rule's action and scope). The list button that downloads now is called Refresh, like `jarl blocklist refresh`. diff --git a/internal/web/static/assets/app.js b/internal/web/static/assets/app.js index 07b721c..91b8f01 100644 --- a/internal/web/static/assets/app.js +++ b/internal/web/static/assets/app.js @@ -1,7 +1,7 @@ // Jarl web UI: router, layout and login. -import { h, mount, toast, errorBox, plural, lastList } from "./ui.js"; +import { h, mount, toast, errorBox, plural, lastList, modal } from "./ui.js"; import { wordmark, logo } from "./brand.js"; -import { api, get, state, loadDraft, onChange, setUnauthorizedHandler, setImmediate, setCodePrompt } from "./api.js"; +import { api, get, state, loadDraft, onChange, setUnauthorizedHandler, setImmediate, setCodePrompt, useDraft } from "./api.js"; import * as totp from "./totp.js"; import * as dashboard from "./pages/dashboard.js"; import * as zones from "./pages/zones.js"; @@ -207,7 +207,28 @@ function renderBar() { mount(barEl, n > 0 ? h("div", { class: "pending-bar" }, h("strong", null, plural(n, "pending change")), h("span", { class: "muted" }, "Your edits are collected in changeset #" + d.id + " and are not live yet."), - h("a", { class: "button primary small", href: "#/changes" }, "Review & apply")) : null); + h("a", { class: "button small", href: "#/changes" }, "Review"), + h("button", { class: "primary small apply-now", title: "Apply the pending changes now, without reviewing them first", onclick: (ev) => applyDraft(ev.target, d) }, "Apply now")) : null); +} + +// applyDraft applies the draft changeset from the banner and stays on the +// page. If it is refused, a dialog says why and links to Changes. +async function applyDraft(btn, d) { + btn.disabled = true; + btn.textContent = "Applying…"; + try { + const r = await api("POST", "/changesets/" + d.id + "/apply", { body: {} }); + const res = r.result || {}; + toast("Applied changeset #" + d.id + (res.reloaded && res.reloaded.length ? " — named reloaded" : "")); + (r.warnings || []).forEach((w) => toast(w, "warn", 10000)); + useDraft(null); + rerender(); + } catch (e) { + btn.disabled = false; + btn.textContent = "Apply now"; + const review = await modal("The changes were not applied", errorBox(e), [{ label: "Close", value: false }, { label: "Review changes", kind: "primary", value: true }], { wide: true }); + if (review) location.hash = "#/changes"; + } } async function logout() { diff --git a/internal/web/static/assets/style.css b/internal/web/static/assets/style.css index 5e0a61a..b21adf4 100644 --- a/internal/web/static/assets/style.css +++ b/internal/web/static/assets/style.css @@ -116,6 +116,7 @@ main { padding: 16px; max-width: 1400px; margin: 0 auto; } padding: 8px 16px; } .pending-bar strong { color: var(--warn); } +.pending-bar .apply-now { margin-left: auto; } /* The review/live switch in the header. */ .mode-toggle { display: inline-flex; align-items: center; gap: 6px; padding: 3px 10px; border-radius: 999px; font-size: 12px; font-weight: 500; background: transparent; color: var(--muted); border: 1px solid var(--border); } .mode-toggle:hover { color: var(--text); border-color: var(--accent); }