Apply pending changes from the banner

An Apply now button on the pending-changes banner applies the draft and
stays on the page; if the changes are refused, a dialog says why.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
adamandClaude Opus 5.5 committed 2026-10-10 12:56:10 +00:00
1 parent d4756444ac
commit 02c393a6bb
3 files changed
+28 -3

No files matched your search

+3
View File
@@ -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`.
+24 -3
View File
@@ -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() {
+1
View File
@@ -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); }