From 6edf8314bcc454d1b245df9a0f40611d9fdbe88e Mon Sep 17 00:00:00 2001 From: Mix Date: Sun, 18 Oct 2026 23:30:44 +0300 Subject: [PATCH] user form, backup: two gaps the mockup comparison found MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Owner, 2026-10-18: "Что ещё из макетов мы также прошляпили?" - every mockup held against the panel; two small gaps beyond the queues: - user.html: with All ticked, a list's single domains are disabled (mockup user.html:34-36). A panel.js show-when rule with disable: the boxes data-single-out / data-single-in follow data-all-out / data-all-in, at load and on change; a disabled box is not submitted, which the server ignores with All anyway. node test testdata/alldomains_test.js (fails on the previous panel.js). - backup.html: "domain's settings" is a link, to Outbound › Domains (mockup backup.html:35 links a domain's settings; a server page has no one domain). Tests: go test ./internal/web/... pass, TestPanelJS with node 24. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 13 +++++ .../web/handlers/handlers_delegation_test.go | 2 +- internal/web/view/paneljs_test.go | 2 +- internal/web/view/server_test.go | 11 +++- internal/web/view/static/panel.js | 22 ++++++++ internal/web/view/templates/backup.html | 2 +- internal/web/view/templates/user.html | 8 +-- internal/web/view/testdata/alldomains_test.js | 56 +++++++++++++++++++ 8 files changed, 106 insertions(+), 10 deletions(-) create mode 100644 internal/web/view/testdata/alldomains_test.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 884d2b5..1c201bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,19 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); version must be named in `docs/roadmap.md`. The two queues had been deferred on 2026-10-09 with no roadmap row and were forgotten; development.md says so. +### Fixed + +Two gaps a comparison of every mockup with the panel found (the rest +matches): + +- **Server › Users, the user form:** with *All outbound domains* (or *All + inbound domains*) ticked, that list's single domains are disabled, as the + mockup draws them - the server ignored them already, which the help line + says. panel.js does it; without scripts they stay clickable. +- **Server › Backup, Import a domain:** "the domain's settings" links to + Outbound › Domains, where each domain's settings are, as the mockup's link + does. + ## [2.15.0] - 2026-10-18 The two proposals the security audit left as product choices. Migration diff --git a/internal/web/handlers/handlers_delegation_test.go b/internal/web/handlers/handlers_delegation_test.go index b5d336e..61f647f 100644 --- a/internal/web/handlers/handlers_delegation_test.go +++ b/internal/web/handlers/handlers_delegation_test.go @@ -431,7 +431,7 @@ func TestUserFormAssignsBothDirections(t *testing.T) { body := send(h.HandleUserEdit, &root, "GET", "/server/users/"+idStr(other.ID), map[string]string{"uid": idStr(other.ID)}, nil).Body.String() for _, want := range []string{ `name="all_domains"`, `name="all_inbound_domains"`, "out.example.com", - `name="inbound_domain_ids" value="` + idStr(in.ID) + `" checked`, + `name="inbound_domain_ids" value="` + idStr(in.ID) + `" data-single-in checked`, } { if !strings.Contains(body, want) { t.Errorf("the user form is missing %q", want) diff --git a/internal/web/view/paneljs_test.go b/internal/web/view/paneljs_test.go index 4b489a5..24bb3eb 100644 --- a/internal/web/view/paneljs_test.go +++ b/internal/web/view/paneljs_test.go @@ -17,7 +17,7 @@ func TestPanelJS(t *testing.T) { if err != nil { t.Skip("node is not installed") } - for _, script := range []string{"poll_test.js", "unsaved_test.js", "filename_test.js"} { + for _, script := range []string{"poll_test.js", "unsaved_test.js", "filename_test.js", "alldomains_test.js"} { t.Run(script, func(t *testing.T) { out, err := exec.Command(node, filepath.Join("testdata", script), filepath.Join("static", "panel.js")).CombinedOutput() if err != nil { diff --git a/internal/web/view/server_test.go b/internal/web/view/server_test.go index ca0dbff..7c1183b 100644 --- a/internal/web/view/server_test.go +++ b/internal/web/view/server_test.go @@ -59,6 +59,7 @@ func TestBackupHasBothFormsAndTheEncryptionHooks(t *testing.T) { `name="password"`, `name="password_confirm"`, `minlength="12"`, `placeholder="at least 12 characters"`, `name="file"`, `accept=".json,.spde,application/json"`, `required data-import-file`, `data-import-password-fields`, `name="import_password"`, `
`, + `The export itself is made on the domain's settings.`, ) pageLacks(t, "Backup", out, `notification`) } @@ -157,8 +158,8 @@ func TestUserFormShowsWhatIsStoredUnderTheHandlersFieldNames(t *testing.T) { `name="password" type="password" autocomplete="new-password">`, `Leave empty to keep the current one.`, ``) + pageHas(t, "User", out, `Choose a file…No file chosen
diff --git a/internal/web/view/templates/user.html b/internal/web/view/templates/user.html index 16f6e2a..39f6fa4 100644 --- a/internal/web/view/templates/user.html +++ b/internal/web/view/templates/user.html @@ -34,16 +34,16 @@
{{template "box_open" .Outbound}} {{template "body_open"}} -
{{range .OutDomains}} -
{{end}} +
{{range .OutDomains}} +
{{end}}

With All ticked the single domains are ignored.

{{template "body_close"}} {{template "box_close"}} {{if .ShowInbound}} {{template "box_open" .Inbound}} {{template "body_open"}} -
{{range .InDomains}} -
{{end}} +
{{range .InDomains}} +
{{end}}

Granted separately from outbound: the same name on both lists is two assignments. The domain role needs at least one domain, or All, on either list.

{{template "body_close"}} {{template "box_close"}} diff --git a/internal/web/view/testdata/alldomains_test.js b/internal/web/view/testdata/alldomains_test.js new file mode 100644 index 0000000..993c425 --- /dev/null +++ b/internal/web/view/testdata/alldomains_test.js @@ -0,0 +1,56 @@ +// The user form (Server › Users): with "All outbound domains" ticked the +// single outbound domains are disabled, as the mockup user.html draws them, and +// enabled again when it is unticked; the inbound list follows its own All the +// same way, and neither touches the other. Run by TestPanelJS +// (paneljs_test.go): node alldomains_test.js . Exits non-zero with +// what went wrong. +"use strict"; +const fs = require("fs"); +const vm = require("vm"); + +const failures = []; +function check(cond, what) { + if (!cond) failures.push(what); +} + +const listeners = {}; +const box = (attr, checked) => ({ attr, checked, disabled: false, matches: (sel) => sel === "input[" + attr + "]", closest: () => form }); +const allOut = box("data-all-out", true); +const allIn = box("data-all-in", false); +const singlesOut = [box("data-single-out", true), box("data-single-out", false)]; +const singlesIn = [box("data-single-in", true)]; +const form = { + querySelectorAll: (sel) => + sel === "input[data-single-out]" ? singlesOut : sel === "input[data-single-in]" ? singlesIn : [], +}; +const document = { + hidden: false, + addEventListener: (type, fn) => (listeners[type] = listeners[type] || []).push(fn), + body: { addEventListener() {} }, + getElementById: () => null, + querySelectorAll: (sel) => + sel === "input[data-all-out]" ? [allOut] : sel === "input[data-all-in]" ? [allIn] : [], +}; +vm.runInNewContext(fs.readFileSync(process.argv[2], "utf8"), { + document, window: { addEventListener() {} }, htmx: { config: {}, ajax() {} }, setTimeout, clearTimeout, Date, Object, Math, console, +}); +const fire = (type, ev) => (listeners[type] || []).forEach((fn) => fn(ev)); + +fire("DOMContentLoaded", {}); +check(singlesOut.every((b) => b.disabled), "at load, with All outbound ticked, the single outbound domains are not disabled"); +check(singlesIn.every((b) => !b.disabled), "at load, with All inbound unticked, a single inbound domain is disabled"); + +allOut.checked = false; +fire("change", { target: allOut }); +check(singlesOut.every((b) => !b.disabled), "unticking All outbound leaves the single outbound domains disabled"); +check(singlesOut[0].checked && !singlesOut[1].checked, "unticking All outbound changed which single domains are ticked"); + +allIn.checked = true; +fire("change", { target: allIn }); +check(singlesIn.every((b) => b.disabled), "ticking All inbound does not disable the single inbound domains"); +check(singlesOut.every((b) => !b.disabled), "ticking All inbound disabled the outbound list"); + +if (failures.length) { + console.error(failures.join("\n")); + process.exit(1); +}