The user form had Back to users at the bottom of the card; all drill-down pages now use a shared back_link template with a structural test. Co-Authored-By: Claude <Composer 2.5> <noreply@anthropic.com> Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
<span class="st st-{{.Level}}">{{.Row.Status}}</span>
|
||||
</p>
|
||||
|
||||
<a class="back" href="{{.BackURL}}">← Back to deliveries</a>
|
||||
{{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
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{{define "content"}}
|
||||
<h1>Delete {{.Domain.Name}}</h1>
|
||||
|
||||
<a class="back" href="/domains/{{.Domain.ID}}">← Back to {{.Domain.Name}}</a>
|
||||
{{template "back_link" (back (printf "/domains/%d" .Domain.ID) (printf "Back to %s" .Domain.Name))}}
|
||||
|
||||
<div class="card">
|
||||
<h2>Confirm deletion</h2>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{{define "content"}}
|
||||
<h1>{{.Domain.Name}}</h1>
|
||||
|
||||
<a class="back" href="/domains">← All domains</a>
|
||||
{{template "back_link" (back "/domains" "All domains")}}
|
||||
|
||||
{{if .Flash}}<div class="flash">{{.Flash}}</div>{{end}}
|
||||
{{if .RateLimitErr}}<div class="flash error">{{.RateLimitErr}}</div>{{end}}
|
||||
|
||||
@@ -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 <h1> and above the cards (see .back in
|
||||
panel.css). TestDrillDownPagesPlaceBackLinkAboveContent guards this. */}}
|
||||
{{define "back_link"}}
|
||||
<a class="back" href="{{.Href}}">← {{.Label}}</a>
|
||||
{{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
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
{{define "content"}}
|
||||
<h1>{{if .IsEdit}}Edit user{{else}}Create user{{end}}</h1>
|
||||
|
||||
{{template "back_link" (back "/users" "Back to users")}}
|
||||
|
||||
<div class="card narrow">
|
||||
{{if .Error}}<p class="error">{{.Error}}</p>{{end}}
|
||||
<form method="post" action="{{if .IsEdit}}/users/{{.UserID}}{{else}}/users/new{{end}}">
|
||||
@@ -33,6 +35,5 @@
|
||||
<button type="submit" name="action" value="delete" class="danger">Delete user</button>
|
||||
{{end}}
|
||||
</form>
|
||||
<p class="muted"><a href="/users">Back to users</a></p>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 <a class="back"> 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 {
|
||||
|
||||
Reference in New Issue
Block a user