diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b94714..186bcbc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); version ## [Unreleased] +### Fixed + +- panel: the user create/edit form placed «Back to users» at the bottom of the + card instead of under the heading like the delivery, domain, and domain-delete + pages. A shared `back_link` template now renders every drill-down up-link, and + `TestDrillDownPagesPlaceBackLinkAboveContent` guards its position. + ### Changed - panel: **Settings** shows panel credentials and DMARC aggregate reports side diff --git a/internal/web/view/templates/delivery.html b/internal/web/view/templates/delivery.html index f03fccb..ee88623 100644 --- a/internal/web/view/templates/delivery.html +++ b/internal/web/view/templates/delivery.html @@ -18,7 +18,7 @@ {{.Row.Status}}

-← Back to deliveries +{{template "back_link" (back .BackURL "Back to deliveries")}} {{/* The two columns: what was recorded on the left, in what order it happened on the right. They are a pair — the facts are only worth reading against diff --git a/internal/web/view/templates/domain_delete.html b/internal/web/view/templates/domain_delete.html index fcb0978..e0701f4 100644 --- a/internal/web/view/templates/domain_delete.html +++ b/internal/web/view/templates/domain_delete.html @@ -1,7 +1,7 @@ {{define "content"}}

Delete {{.Domain.Name}}

-← Back to {{.Domain.Name}} +{{template "back_link" (back (printf "/domains/%d" .Domain.ID) (printf "Back to %s" .Domain.Name))}}

Confirm deletion

diff --git a/internal/web/view/templates/domain_detail.html b/internal/web/view/templates/domain_detail.html index f9ec373..c2ea521 100644 --- a/internal/web/view/templates/domain_detail.html +++ b/internal/web/view/templates/domain_detail.html @@ -1,7 +1,7 @@ {{define "content"}}

{{.Domain.Name}}

-← All domains +{{template "back_link" (back "/domains" "All domains")}} {{if .Flash}}
{{.Flash}}
{{end}} {{if .RateLimitErr}}
{{.RateLimitErr}}
{{end}} diff --git a/internal/web/view/templates/layout.html b/internal/web/view/templates/layout.html index d17e6e3..7e35be2 100644 --- a/internal/web/view/templates/layout.html +++ b/internal/web/view/templates/layout.html @@ -107,6 +107,14 @@ so its definition replaces the empty one. */}} {{define "wide"}}{{end}} +{{/* back_link — up-navigation on drill-down pages. Invoke with the "back" + function, e.g. {{template "back_link" (back "/users" "Back to users")}}. + Place it directly under the page

and above the cards (see .back in + panel.css). TestDrillDownPagesPlaceBackLinkAboveContent guards this. */}} +{{define "back_link"}} +← {{.Label}} +{{end}} + {{/* Navigation icons. Inline SVG rather than an icon font or sprite file: they inherit the link's colour through currentColor, cost no extra request, and need no exemption from the panel's "default-src 'self'" policy. Each is diff --git a/internal/web/view/templates/user_form.html b/internal/web/view/templates/user_form.html index fe1c3cc..864a859 100644 --- a/internal/web/view/templates/user_form.html +++ b/internal/web/view/templates/user_form.html @@ -1,6 +1,8 @@ {{define "content"}}

{{if .IsEdit}}Edit user{{else}}Create user{{end}}

+{{template "back_link" (back "/users" "Back to users")}} +
{{if .Error}}

{{.Error}}

{{end}}
@@ -33,6 +35,5 @@ {{end}}
-

Back to users

{{end}} diff --git a/internal/web/view/templates_test.go b/internal/web/view/templates_test.go index 837c2ed..d45821d 100644 --- a/internal/web/view/templates_test.go +++ b/internal/web/view/templates_test.go @@ -279,6 +279,32 @@ func TestOnlyThePagesMadeOfDataDeclareThemselvesWide(t *testing.T) { } } +// Drill-down pages carry an up-link directly under the heading and above the +// cards. A link at the bottom of a form is easy to miss and drifts from the +// rest of the panel, so the shared back_link template is mandatory on those +// pages and TestDrillDownPagesPlaceBackLinkAboveContent guards its position. +func TestDrillDownPagesPlaceBackLinkAboveContent(t *testing.T) { + drillDown := map[string]bool{ + "user_form.html": true, + "domain_detail.html": true, + "domain_delete.html": true, + "delivery.html": true, + } + forEachTemplate(t, func(name, body string) { + if !drillDown[name] { + return + } + if !strings.Contains(body, `template "back_link"`) { + t.Errorf("%s is a drill-down page but does not use the shared back_link template", name) + } + backIdx := strings.Index(body, `template "back_link"`) + cardIdx := strings.Index(body, `class="card`) + if cardIdx >= 0 && backIdx > cardIdx { + t.Errorf("%s places the back link after the first card", name) + } + }) +} + // Since the panel root redirects to the status page, a link left pointing at // "/" silently lands on the wrong screen instead of failing — so no template may // contain one. diff --git a/internal/web/view/view.go b/internal/web/view/view.go index 648dc68..cec4935 100644 --- a/internal/web/view/view.go +++ b/internal/web/view/view.go @@ -65,7 +65,7 @@ func New(version string) (*Engine, error) { } for name, files := range pageFiles { patterns := append([]string{"templates/layout.html"}, files...) - tmpl, err := template.New("layout.html").ParseFS(assetsFS, patterns...) + tmpl, err := template.New("layout.html").Funcs(templateFuncs()).ParseFS(assetsFS, patterns...) if err != nil { return nil, fmt.Errorf("parse template %s: %w", name, err) } @@ -81,6 +81,17 @@ func New(version string) (*Engine, error) { return e, nil } +// templateFuncs supplies helpers shared across page templates. +func templateFuncs() template.FuncMap { + return template.FuncMap{ + // back builds the map back_link reads; keeps href and label paired at + // the call site instead of repeating the markup. + "back": func(href, label string) map[string]string { + return map[string]string{"Href": href, "Label": label} + }, + } +} + // Page returns a parsed page template by logical name. It is exported for // template guard tests that assert structural properties across all pages. func (e *Engine) Page(name string) *template.Template {