Edit blocklists and rules in the web UI
An Edit button opens a dialog for a list's name, URL, views and comment, or a rule's action, scope, views and comment. The download-now button is called Refresh. Checkboxes in dialogs are no longer stretched. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
67f14b030f
commit
d4756444ac
3 files changed
+59
-7
No files matched your search
@@ -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.
|
||||
- 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`.
|
||||
- Records table: disabled records are hatched and have a "disabled" badge
|
||||
next to their name, instead of only a muted colour and a badge in the
|
||||
comment column.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Blocking: your block/allow rules and blocklists (docs/blocking.md).
|
||||
import { h, mount, table, input, select, checkbox, toast, errorBox, badge, confirmDialog, relTime, time } from "../ui.js";
|
||||
import { h, mount, table, field, input, select, checkbox, toast, errorBox, badge, confirmDialog, modal, relTime, time } from "../ui.js";
|
||||
import { get, change, describeResult, draftQuery, can, api } from "../api.js";
|
||||
import { rerender } from "../app.js";
|
||||
import { pendingBadge } from "./zones.js";
|
||||
@@ -134,9 +134,9 @@ async function addRule(name, action, exact, views, comment) {
|
||||
|
||||
// viewPicker is a compact choice of views: it shows the choice, and opens
|
||||
// a list of checkboxes (none ticked: every view that offers recursion).
|
||||
function viewPicker(views, autoViews) {
|
||||
if (!views.length) return { el: null, value: () => [] };
|
||||
const boxes = views.map((v) => checkbox(v.name, false));
|
||||
function viewPicker(views, autoViews, selected = []) {
|
||||
if (!views.length) return { el: null, picker: null, value: () => [] };
|
||||
const boxes = views.map((v) => checkbox(v.name, selected.includes(v.name)));
|
||||
const value = () => views.filter((v, i) => boxes[i].input.checked).map((v) => v.name);
|
||||
const summary = h("summary");
|
||||
const show = () => {
|
||||
@@ -147,7 +147,52 @@ function viewPicker(views, autoViews) {
|
||||
show();
|
||||
const picker = h("details", { class: "view-picker" }, summary,
|
||||
h("div", { class: "view-picker-list" }, h("div", { class: "muted small" }, "None ticked: every view that offers recursion"), boxes));
|
||||
return { el: h("div", { class: "f" }, h("label", null, "Views"), picker), value };
|
||||
return { el: h("div", { class: "f" }, h("label", null, "Views"), picker), picker, value };
|
||||
}
|
||||
|
||||
// The views a list or rule names itself ([] when automatic).
|
||||
const ownViews = (x) => (x.auto_views ? [] : x.views);
|
||||
|
||||
// editDialog changes a list or rule: fields are [label, control, help]
|
||||
// rows, and data() the PATCH body.
|
||||
async function editDialog(title, path, rows, data) {
|
||||
const err = h("div");
|
||||
await modal(title, h("div", null, h("div", { class: "form-grid" }, rows.filter(Boolean).map(([l, c, help]) => field(l, c, help))), err), [
|
||||
{ label: "Cancel", value: null },
|
||||
{ label: "Save", kind: "primary", onclick: async () => {
|
||||
try {
|
||||
const r = await change("PATCH", path, data());
|
||||
toast(describeResult(r));
|
||||
rerender();
|
||||
return true;
|
||||
} catch (e) { mount(err, errorBox(e)); return false; }
|
||||
} },
|
||||
], { sticky: true });
|
||||
}
|
||||
|
||||
function editList(l, views, autoViews) {
|
||||
const name = input({ value: l.name, class: "mono" });
|
||||
const url = input({ value: l.url, class: "mono", style: { width: "100%" } });
|
||||
const comment = input({ value: l.comment || "" });
|
||||
const vp = viewPicker(views, autoViews, ownViews(l));
|
||||
editDialog("Edit blocklist " + l.name, "/blocking/lists/" + l.id, [
|
||||
["Name", name], ["URL", url, "A new URL is downloaded right after saving."],
|
||||
vp.picker ? ["Views", vp.picker, "None ticked: every view that offers recursion."] : null,
|
||||
["Comment", comment],
|
||||
], () => ({ name: name.value.trim(), url: url.value.trim(), views: vp.value(), comment: comment.value }));
|
||||
}
|
||||
|
||||
function editRule(r, views, autoViews) {
|
||||
const name = input({ value: r.name, class: "mono" });
|
||||
const action = select([["block", "Block"], ["allow", "Allow"]], r.action);
|
||||
const exact = checkbox("only this exact name", !!r.exact);
|
||||
const comment = input({ value: r.comment || "" });
|
||||
const vp = viewPicker(views, autoViews, ownViews(r));
|
||||
editDialog("Edit the rule for " + r.name, "/blocking/rules/" + r.id, [
|
||||
["Action", action], ["Domain", name], ["", exact],
|
||||
vp.picker ? ["Views", vp.picker, "None ticked: every view that offers recursion."] : null,
|
||||
["Comment", comment],
|
||||
], () => ({ name: name.value.trim(), action: action.value, exact: exact.input.checked, views: vp.value(), comment: comment.value }));
|
||||
}
|
||||
|
||||
// An open view picker closes when you click elsewhere.
|
||||
@@ -190,7 +235,9 @@ function rulesPanel(rules, views, autoViews, writable) {
|
||||
{ label: "Domain", render: (r) => [h("span", { class: "mono" }, r.name), r.exact ? null : h("span", { class: "muted small" }, " and below")] },
|
||||
views.length ? { label: "Views", render: viewsCell } : null,
|
||||
{ label: "Comment", render: (r) => h("span", { class: "muted" }, r.comment || "") },
|
||||
writable ? { label: "", render: (r) => r.pending === "delete" ? "" : h("button", { class: "small danger", onclick: () => del("/blocking/rules/" + r.id, "the rule for " + r.name) }, "Delete"), tdClass: "actions-cell" } : null,
|
||||
writable ? { label: "", render: (r) => r.pending === "delete" ? "" : h("span", null,
|
||||
h("button", { class: "small", onclick: () => editRule(r, views, autoViews) }, "Edit"), " ",
|
||||
h("button", { class: "small danger", onclick: () => del("/blocking/rules/" + r.id, "the rule for " + r.name) }, "Delete")), tdClass: "actions-cell" } : null,
|
||||
].filter(Boolean), rules, { empty: "No rules yet.", rowClass: (r) => r.pending ? "pending-" + r.pending : "" }));
|
||||
}
|
||||
|
||||
@@ -229,7 +276,8 @@ function listsPanel(lists, views, autoViews, writable) {
|
||||
{ label: "Memory", render: (l) => l.memory_mb ? h("span", { title: "named's memory for this list, estimated (all its views)" }, "~" + l.memory_mb + " MB") : "–", tdClass: "small right" },
|
||||
{ label: "URL", render: (l) => h("a", { href: l.url, target: "_blank", rel: "noopener", class: "small mono", title: l.url }, shortURL(l.url)) },
|
||||
writable ? { label: "", render: (l) => l.pending === "delete" ? "" : h("span", null,
|
||||
h("button", { class: "small", disabled: l.state === "disabled" || !!l.pending, title: "Download now", onclick: () => refresh(l) }, "Update"), " ",
|
||||
h("button", { class: "small", onclick: () => editList(l, views, autoViews) }, "Edit"), " ",
|
||||
h("button", { class: "small", disabled: l.state === "disabled" || !!l.pending, title: "Download it now (it is checked daily anyway)", onclick: () => refresh(l) }, "Refresh"), " ",
|
||||
h("button", { class: "small", onclick: () => toggle(l) }, l.disabled ? "Enable" : "Disable"), " ",
|
||||
h("button", { class: "small danger", onclick: () => del("/blocking/lists/" + l.id, "blocklist " + l.name) }, "Delete")), tdClass: "actions-cell" } : null,
|
||||
].filter(Boolean), lists, { empty: "No blocklists yet.", rowClass: (l) => l.pending ? "pending-" + l.pending : l.disabled ? "disabled" : "" }),
|
||||
|
||||
@@ -171,6 +171,7 @@ textarea { width: 100%; min-height: 80px; resize: vertical; }
|
||||
.form-grid > label { padding-top: 6px; color: var(--muted); }
|
||||
.form-grid .help { grid-column: 2; margin-top: -4px; color: var(--muted); font-size: 12px; }
|
||||
.form-grid input, .form-grid select { width: 100%; max-width: 560px; }
|
||||
.form-grid input[type=checkbox] { width: auto; }
|
||||
.inline-form { display: flex; gap: 6px; flex-wrap: wrap; align-items: flex-end; }
|
||||
.inline-form .f { display: flex; flex-direction: column; gap: 2px; }
|
||||
.inline-form .f label:not(.check) { font-size: 11px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.3px; }
|
||||
|
||||
Reference in new issue
Block a user