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]
|
## [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
|
### Changed
|
||||||
|
|
||||||
- panel: **Settings** shows panel credentials and DMARC aggregate reports side
|
- panel: **Settings** shows panel credentials and DMARC aggregate reports side
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
<span class="st st-{{.Level}}">{{.Row.Status}}</span>
|
<span class="st st-{{.Level}}">{{.Row.Status}}</span>
|
||||||
</p>
|
</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
|
{{/* 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
|
on the right. They are a pair — the facts are only worth reading against
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{{define "content"}}
|
{{define "content"}}
|
||||||
<h1>Delete {{.Domain.Name}}</h1>
|
<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">
|
<div class="card">
|
||||||
<h2>Confirm deletion</h2>
|
<h2>Confirm deletion</h2>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{{define "content"}}
|
{{define "content"}}
|
||||||
<h1>{{.Domain.Name}}</h1>
|
<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 .Flash}}<div class="flash">{{.Flash}}</div>{{end}}
|
||||||
{{if .RateLimitErr}}<div class="flash error">{{.RateLimitErr}}</div>{{end}}
|
{{if .RateLimitErr}}<div class="flash error">{{.RateLimitErr}}</div>{{end}}
|
||||||
|
|||||||
@@ -107,6 +107,14 @@
|
|||||||
so its definition replaces the empty one. */}}
|
so its definition replaces the empty one. */}}
|
||||||
{{define "wide"}}{{end}}
|
{{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
|
{{/* 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
|
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
|
need no exemption from the panel's "default-src 'self'" policy. Each is
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
{{define "content"}}
|
{{define "content"}}
|
||||||
<h1>{{if .IsEdit}}Edit user{{else}}Create user{{end}}</h1>
|
<h1>{{if .IsEdit}}Edit user{{else}}Create user{{end}}</h1>
|
||||||
|
|
||||||
|
{{template "back_link" (back "/users" "Back to users")}}
|
||||||
|
|
||||||
<div class="card narrow">
|
<div class="card narrow">
|
||||||
{{if .Error}}<p class="error">{{.Error}}</p>{{end}}
|
{{if .Error}}<p class="error">{{.Error}}</p>{{end}}
|
||||||
<form method="post" action="{{if .IsEdit}}/users/{{.UserID}}{{else}}/users/new{{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>
|
<button type="submit" name="action" value="delete" class="danger">Delete user</button>
|
||||||
{{end}}
|
{{end}}
|
||||||
</form>
|
</form>
|
||||||
<p class="muted"><a href="/users">Back to users</a></p>
|
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{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
|
// 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
|
// "/" silently lands on the wrong screen instead of failing — so no template may
|
||||||
// contain one.
|
// contain one.
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ func New(version string) (*Engine, error) {
|
|||||||
}
|
}
|
||||||
for name, files := range pageFiles {
|
for name, files := range pageFiles {
|
||||||
patterns := append([]string{"templates/layout.html"}, files...)
|
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 {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("parse template %s: %w", name, err)
|
return nil, fmt.Errorf("parse template %s: %w", name, err)
|
||||||
}
|
}
|
||||||
@@ -81,6 +81,17 @@ func New(version string) (*Engine, error) {
|
|||||||
return e, nil
|
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
|
// Page returns a parsed page template by logical name. It is exported for
|
||||||
// template guard tests that assert structural properties across all pages.
|
// template guard tests that assert structural properties across all pages.
|
||||||
func (e *Engine) Page(name string) *template.Template {
|
func (e *Engine) Page(name string) *template.Template {
|
||||||
|
|||||||
Reference in New Issue
Block a user