feat(web): show releases as one card with prose notes and the latest release in About
- commit
- e08572cdbcb35f78e9849a172ea0dd69ef5105b9
- author
- brice · · 2026-10-22 05:47 UTC
- committer
- brice · · 2026-10-22 08:51 UTC
- parent
- 5d69f2b
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
10 files · +518 -304
M internal/adapters/web/fragments.go +1 -1
| @@ -167,7 +167,7 @@ func (l *liveStream) gone(ctx context.Context, name string) ([]fragment, error) | |||
| 167 | 167 | func (l *liveStream) repoPage(ctx context.Context, we ports.WatchEvent) ([]fragment, error) { | |
| 168 | 168 | switch ev := we.Event.(type) { | |
| 169 | 169 | case domain.ReleasePublished, domain.ReleaseUpdated, domain.ReleaseDeleted, domain.ReleaseUnpublished: | |
| 170 | - | if l.view == "releases" || l.view == "release" { | |
| 170 | + | if l.view == "releases" || l.view == "release" || l.view == "summary" { | |
| 171 | 171 | return gap, nil // notes, assets, latest or visibility changed: reload | |
| 172 | 172 | } | |
| 173 | 173 | return nil, nil | |
| @@ -167,7 +167,7 @@ func (l *liveStream) gone(ctx context.Context, name string) ([]fragment, error) | |||||
| 167 | func (l *liveStream) repoPage(ctx context.Context, we ports.WatchEvent) ([]fragment, error) { | 167 | func (l *liveStream) repoPage(ctx context.Context, we ports.WatchEvent) ([]fragment, error) { | ||
| 168 | switch ev := we.Event.(type) { | 168 | switch ev := we.Event.(type) { | ||
| 169 | case domain.ReleasePublished, domain.ReleaseUpdated, domain.ReleaseDeleted, domain.ReleaseUnpublished: | 169 | case domain.ReleasePublished, domain.ReleaseUpdated, domain.ReleaseDeleted, domain.ReleaseUnpublished: | ||
| 170 | - | if l.view == "releases" || l.view == "release" { | 170 | + | if l.view == "releases" || l.view == "release" || l.view == "summary" { |
| 171 | return gap, nil // notes, assets, latest or visibility changed: reload | 171 | return gap, nil // notes, assets, latest or visibility changed: reload | ||
| 172 | } | 172 | } | ||
| 173 | return nil, nil | 173 | return nil, nil | ||
M internal/adapters/web/fragments_test.go +3 -3
| @@ -101,8 +101,8 @@ func TestMRPushedHeadSignal(t *testing.T) { | |||
| 101 | 101 | } | |
| 102 | 102 | } | |
| 103 | 103 | ||
| 104 | - | // Release lifecycle events invalidate the release pages without putting notes | |
| 105 | - | // or asset metadata in SSE payloads. Other views need no release fragments. | |
| 104 | + | // Release lifecycle events invalidate release pages and the summary's release | |
| 105 | + | // fact without putting notes or asset metadata in SSE payloads. | |
| 106 | 106 | func TestReleaseLiveUpdates(t *testing.T) { | |
| 107 | 107 | for _, ev := range []domain.Event{domain.ReleasePublished{}, domain.ReleaseUpdated{}, domain.ReleaseDeleted{}, domain.ReleaseUnpublished{}} { | |
| 108 | 108 | we := ports.WatchEvent{Event: ev} | |
| @@ -111,7 +111,7 @@ func TestReleaseLiveUpdates(t *testing.T) { | |||
| 111 | 111 | st := &liveStream{repo: "proj", view: view} | |
| 112 | 112 | got, err := st.render(t.Context(), we) | |
| 113 | 113 | require.NoError(t, err) | |
| 114 | - | if view == "release" || view == "releases" { | |
| 114 | + | if view == "release" || view == "releases" || view == "summary" { | |
| 115 | 115 | assert.Equal(t, gap, got) | |
| 116 | 116 | } else { | |
| 117 | 117 | assert.Empty(t, got) | |
| @@ -101,8 +101,8 @@ func TestMRPushedHeadSignal(t *testing.T) { | |||||
| 101 | } | 101 | } | ||
| 102 | } | 102 | } | ||
| 103 | 103 | ||||
| 104 | - | // Release lifecycle events invalidate the release pages without putting notes | 104 | + | // Release lifecycle events invalidate release pages and the summary's release |
| 105 | - | // or asset metadata in SSE payloads. Other views need no release fragments. | 105 | + | // fact without putting notes or asset metadata in SSE payloads. |
| 106 | func TestReleaseLiveUpdates(t *testing.T) { | 106 | func TestReleaseLiveUpdates(t *testing.T) { | ||
| 107 | for _, ev := range []domain.Event{domain.ReleasePublished{}, domain.ReleaseUpdated{}, domain.ReleaseDeleted{}, domain.ReleaseUnpublished{}} { | 107 | for _, ev := range []domain.Event{domain.ReleasePublished{}, domain.ReleaseUpdated{}, domain.ReleaseDeleted{}, domain.ReleaseUnpublished{}} { | ||
| 108 | we := ports.WatchEvent{Event: ev} | 108 | we := ports.WatchEvent{Event: ev} | ||
| @@ -111,7 +111,7 @@ func TestReleaseLiveUpdates(t *testing.T) { | |||||
| 111 | st := &liveStream{repo: "proj", view: view} | 111 | st := &liveStream{repo: "proj", view: view} | ||
| 112 | got, err := st.render(t.Context(), we) | 112 | got, err := st.render(t.Context(), we) | ||
| 113 | require.NoError(t, err) | 113 | require.NoError(t, err) | ||
| 114 | - | if view == "release" || view == "releases" { | 114 | + | if view == "release" || view == "releases" || view == "summary" { |
| 115 | assert.Equal(t, gap, got) | 115 | assert.Equal(t, gap, got) | ||
| 116 | } else { | 116 | } else { | ||
| 117 | assert.Empty(t, got) | 117 | assert.Empty(t, got) | ||
M internal/adapters/web/pages.templ +8 -0
| @@ -115,6 +115,14 @@ templ summaryRail(p SummaryPage) { | |||
| 115 | 115 | if p.Latest != nil { | |
| 116 | 116 | @latestTagFact(*p.Latest) | |
| 117 | 117 | } | |
| 118 | + | if r := p.Release; r != nil { | |
| 119 | + | <dt>release</dt> | |
| 120 | + | <dd> | |
| 121 | + | <a href={ templ.SafeURL(r.Href) }>{ r.Tag }</a>{ " · " } | |
| 122 | + | <time datetime={ r.WhenAt } title={ r.WhenAt }>{ r.When }</time>{ " · " } | |
| 123 | + | <a href={ templ.SafeURL(r.AllHref) }>all releases</a> | |
| 124 | + | </dd> | |
| 125 | + | } | |
| 118 | 126 | </dl> | |
| 119 | 127 | </section> | |
| 120 | 128 | if len(p.Recent) > 0 { | |
| @@ -115,6 +115,14 @@ templ summaryRail(p SummaryPage) { | |||||
| 115 | if p.Latest != nil { | 115 | if p.Latest != nil { | ||
| 116 | @latestTagFact(*p.Latest) | 116 | @latestTagFact(*p.Latest) | ||
| 117 | } | 117 | } | ||
| 118 | + | if r := p.Release; r != nil { | |||
| 119 | + | <dt>release</dt> | |||
| 120 | + | <dd> | |||
| 121 | + | <a href={ templ.SafeURL(r.Href) }>{ r.Tag }</a>{ " · " } | |||
| 122 | + | <time datetime={ r.WhenAt } title={ r.WhenAt }>{ r.When }</time>{ " · " } | |||
| 123 | + | <a href={ templ.SafeURL(r.AllHref) }>all releases</a> | |||
| 124 | + | </dd> | |||
| 125 | + | } | |||
| 118 | </dl> | 126 | </dl> | ||
| 119 | </section> | 127 | </section> | ||
| 120 | if len(p.Recent) > 0 { | 128 | if len(p.Recent) > 0 { | ||
M internal/adapters/web/pages_templ.go +371 -261 generated file: collapsed
M internal/adapters/web/releases.templ +16 -9
| @@ -40,15 +40,22 @@ templ Releases(l Layout, p ReleaseListPage) { | |||
| 40 | 40 | ||
| 41 | 41 | templ ReleaseDetail(l Layout, p ReleaseDetailPage) { | |
| 42 | 42 | @Shell(l, nil) { | |
| 43 | - | <h1>{ p.Title }</h1> | |
| 44 | - | <p> | |
| 45 | - | <span class="pill">{ p.Tag }</span> @ReleaseBadges(p.ReleaseRow) | |
| 46 | - | if p.SHA != "" { | |
| 47 | - | <a href={ templ.SafeURL(p.CommitHref) }><code>{ p.SHA }</code></a> | |
| 48 | - | } | |
| 49 | - | <span class="dim">{ p.Published }</span> | |
| 50 | - | </p> | |
| 51 | - | <div class="md">@templ.Raw(string(p.Notes))</div> | |
| 43 | + | <section class="mr-card"> | |
| 44 | + | <header class="show mr-h"> | |
| 45 | + | <h1>{ p.Title }</h1> | |
| 46 | + | <dl class="kv show-kv"> | |
| 47 | + | <dt>tag</dt> | |
| 48 | + | <dd><span class="pill">{ p.Tag }</span> @ReleaseBadges(p.ReleaseRow)</dd> | |
| 49 | + | if p.SHA != "" { | |
| 50 | + | <dt>commit</dt> | |
| 51 | + | <dd><a href={ templ.SafeURL(p.CommitHref) }><code>{ p.SHA }</code></a></dd> | |
| 52 | + | } | |
| 53 | + | <dt>published</dt> | |
| 54 | + | <dd>{ p.Published }</dd> | |
| 55 | + | </dl> | |
| 56 | + | </header> | |
| 57 | + | <article class="mr-desc prose">@templ.Raw(string(p.Notes))</article> | |
| 58 | + | </section> | |
| 52 | 59 | <h2>Assets</h2> | |
| 53 | 60 | if len(p.Assets) == 0 { | |
| 54 | 61 | <p class="dim">no assets.</p> | |
| @@ -40,15 +40,22 @@ templ Releases(l Layout, p ReleaseListPage) { | |||||
| 40 | 40 | ||||
| 41 | templ ReleaseDetail(l Layout, p ReleaseDetailPage) { | 41 | templ ReleaseDetail(l Layout, p ReleaseDetailPage) { | ||
| 42 | @Shell(l, nil) { | 42 | @Shell(l, nil) { | ||
| 43 | - | <h1>{ p.Title }</h1> | 43 | + | <section class="mr-card"> |
| 44 | - | <p> | 44 | + | <header class="show mr-h"> |
| 45 | - | <span class="pill">{ p.Tag }</span> @ReleaseBadges(p.ReleaseRow) | 45 | + | <h1>{ p.Title }</h1> |
| 46 | - | if p.SHA != "" { | 46 | + | <dl class="kv show-kv"> |
| 47 | - | <a href={ templ.SafeURL(p.CommitHref) }><code>{ p.SHA }</code></a> | 47 | + | <dt>tag</dt> |
| 48 | - | } | 48 | + | <dd><span class="pill">{ p.Tag }</span> @ReleaseBadges(p.ReleaseRow)</dd> |
| 49 | - | <span class="dim">{ p.Published }</span> | 49 | + | if p.SHA != "" { |
| 50 | - | </p> | 50 | + | <dt>commit</dt> |
| 51 | - | <div class="md">@templ.Raw(string(p.Notes))</div> | 51 | + | <dd><a href={ templ.SafeURL(p.CommitHref) }><code>{ p.SHA }</code></a></dd> |
| 52 | + | } | |||
| 53 | + | <dt>published</dt> | |||
| 54 | + | <dd>{ p.Published }</dd> | |||
| 55 | + | </dl> | |||
| 56 | + | </header> | |||
| 57 | + | <article class="mr-desc prose">@templ.Raw(string(p.Notes))</article> | |||
| 58 | + | </section> | |||
| 52 | <h2>Assets</h2> | 59 | <h2>Assets</h2> | ||
| 53 | if len(p.Assets) == 0 { | 60 | if len(p.Assets) == 0 { | ||
| 54 | <p class="dim">no assets.</p> | 61 | <p class="dim">no assets.</p> | ||
M internal/adapters/web/releases_templ.go +32 -28
| @@ -245,27 +245,27 @@ func ReleaseDetail(l Layout, p ReleaseDetailPage) templ.Component { | |||
| 245 | 245 | }() | |
| 246 | 246 | } | |
| 247 | 247 | ctx = templ.InitializeContext(ctx) | |
| 248 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<h1>") | |
| 248 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<section class=\"mr-card\"><header class=\"show mr-h\"><h1>") | |
| 249 | 249 | if templ_7745c5c3_Err != nil { | |
| 250 | 250 | return templ_7745c5c3_Err | |
| 251 | 251 | } | |
| 252 | 252 | var templ_7745c5c3_Var12 string | |
| 253 | 253 | templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(p.Title) | |
| 254 | 254 | if templ_7745c5c3_Err != nil { | |
| 255 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/releases.templ`, Line: 43, Col: 15} | |
| 255 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/releases.templ`, Line: 45, Col: 17} | |
| 256 | 256 | } | |
| 257 | 257 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12)) | |
| 258 | 258 | if templ_7745c5c3_Err != nil { | |
| 259 | 259 | return templ_7745c5c3_Err | |
| 260 | 260 | } | |
| 261 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</h1><p><span class=\"pill\">") | |
| 261 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</h1><dl class=\"kv show-kv\"><dt>tag</dt><dd><span class=\"pill\">") | |
| 262 | 262 | if templ_7745c5c3_Err != nil { | |
| 263 | 263 | return templ_7745c5c3_Err | |
| 264 | 264 | } | |
| 265 | 265 | var templ_7745c5c3_Var13 string | |
| 266 | 266 | templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(p.Tag) | |
| 267 | 267 | if templ_7745c5c3_Err != nil { | |
| 268 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/releases.templ`, Line: 45, Col: 29} | |
| 268 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/releases.templ`, Line: 48, Col: 35} | |
| 269 | 269 | } | |
| 270 | 270 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13)) | |
| 271 | 271 | if templ_7745c5c3_Err != nil { | |
| @@ -279,52 +279,56 @@ func ReleaseDetail(l Layout, p ReleaseDetailPage) templ.Component { | |||
| 279 | 279 | if templ_7745c5c3_Err != nil { | |
| 280 | 280 | return templ_7745c5c3_Err | |
| 281 | 281 | } | |
| 282 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</dd>") | |
| 283 | + | if templ_7745c5c3_Err != nil { | |
| 284 | + | return templ_7745c5c3_Err | |
| 285 | + | } | |
| 282 | 286 | if p.SHA != "" { | |
| 283 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "<a href=\"") | |
| 287 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "<dt>commit</dt><dd><a href=\"") | |
| 284 | 288 | if templ_7745c5c3_Err != nil { | |
| 285 | 289 | return templ_7745c5c3_Err | |
| 286 | 290 | } | |
| 287 | 291 | var templ_7745c5c3_Var14 templ.SafeURL | |
| 288 | 292 | templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.CommitHref)) | |
| 289 | 293 | if templ_7745c5c3_Err != nil { | |
| 290 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/releases.templ`, Line: 47, Col: 41} | |
| 294 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/releases.templ`, Line: 51, Col: 47} | |
| 291 | 295 | } | |
| 292 | 296 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14)) | |
| 293 | 297 | if templ_7745c5c3_Err != nil { | |
| 294 | 298 | return templ_7745c5c3_Err | |
| 295 | 299 | } | |
| 296 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "\"><code>") | |
| 300 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "\"><code>") | |
| 297 | 301 | if templ_7745c5c3_Err != nil { | |
| 298 | 302 | return templ_7745c5c3_Err | |
| 299 | 303 | } | |
| 300 | 304 | var templ_7745c5c3_Var15 string | |
| 301 | 305 | templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(p.SHA) | |
| 302 | 306 | if templ_7745c5c3_Err != nil { | |
| 303 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/releases.templ`, Line: 47, Col: 57} | |
| 307 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/releases.templ`, Line: 51, Col: 63} | |
| 304 | 308 | } | |
| 305 | 309 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15)) | |
| 306 | 310 | if templ_7745c5c3_Err != nil { | |
| 307 | 311 | return templ_7745c5c3_Err | |
| 308 | 312 | } | |
| 309 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "</code></a> ") | |
| 313 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "</code></a></dd>") | |
| 310 | 314 | if templ_7745c5c3_Err != nil { | |
| 311 | 315 | return templ_7745c5c3_Err | |
| 312 | 316 | } | |
| 313 | 317 | } | |
| 314 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "<span class=\"dim\">") | |
| 318 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "<dt>published</dt><dd>") | |
| 315 | 319 | if templ_7745c5c3_Err != nil { | |
| 316 | 320 | return templ_7745c5c3_Err | |
| 317 | 321 | } | |
| 318 | 322 | var templ_7745c5c3_Var16 string | |
| 319 | 323 | templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(p.Published) | |
| 320 | 324 | if templ_7745c5c3_Err != nil { | |
| 321 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/releases.templ`, Line: 49, Col: 34} | |
| 325 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/releases.templ`, Line: 54, Col: 22} | |
| 322 | 326 | } | |
| 323 | 327 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16)) | |
| 324 | 328 | if templ_7745c5c3_Err != nil { | |
| 325 | 329 | return templ_7745c5c3_Err | |
| 326 | 330 | } | |
| 327 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "</span></p><div class=\"md\">") | |
| 331 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "</dd></dl></header><article class=\"mr-desc prose\">") | |
| 328 | 332 | if templ_7745c5c3_Err != nil { | |
| 329 | 333 | return templ_7745c5c3_Err | |
| 330 | 334 | } | |
| @@ -332,97 +336,97 @@ func ReleaseDetail(l Layout, p ReleaseDetailPage) templ.Component { | |||
| 332 | 336 | if templ_7745c5c3_Err != nil { | |
| 333 | 337 | return templ_7745c5c3_Err | |
| 334 | 338 | } | |
| 335 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "</div><h2>Assets</h2>") | |
| 339 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "</article></section><h2>Assets</h2>") | |
| 336 | 340 | if templ_7745c5c3_Err != nil { | |
| 337 | 341 | return templ_7745c5c3_Err | |
| 338 | 342 | } | |
| 339 | 343 | if len(p.Assets) == 0 { | |
| 340 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "<p class=\"dim\">no assets.</p>") | |
| 344 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "<p class=\"dim\">no assets.</p>") | |
| 341 | 345 | if templ_7745c5c3_Err != nil { | |
| 342 | 346 | return templ_7745c5c3_Err | |
| 343 | 347 | } | |
| 344 | 348 | } else { | |
| 345 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "<table class=\"list\" aria-label=\"Release assets\"><thead><tr><th>name</th><th>size</th><th>content type</th></tr></thead> <tbody>") | |
| 349 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "<table class=\"list\" aria-label=\"Release assets\"><thead><tr><th>name</th><th>size</th><th>content type</th></tr></thead> <tbody>") | |
| 346 | 350 | if templ_7745c5c3_Err != nil { | |
| 347 | 351 | return templ_7745c5c3_Err | |
| 348 | 352 | } | |
| 349 | 353 | for _, a := range p.Assets { | |
| 350 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "<tr><td><a href=\"") | |
| 354 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "<tr><td><a href=\"") | |
| 351 | 355 | if templ_7745c5c3_Err != nil { | |
| 352 | 356 | return templ_7745c5c3_Err | |
| 353 | 357 | } | |
| 354 | 358 | var templ_7745c5c3_Var17 templ.SafeURL | |
| 355 | 359 | templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(a.Href)) | |
| 356 | 360 | if templ_7745c5c3_Err != nil { | |
| 357 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/releases.templ`, Line: 60, Col: 45} | |
| 361 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/releases.templ`, Line: 67, Col: 45} | |
| 358 | 362 | } | |
| 359 | 363 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17)) | |
| 360 | 364 | if templ_7745c5c3_Err != nil { | |
| 361 | 365 | return templ_7745c5c3_Err | |
| 362 | 366 | } | |
| 363 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "\" download>") | |
| 367 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "\" download>") | |
| 364 | 368 | if templ_7745c5c3_Err != nil { | |
| 365 | 369 | return templ_7745c5c3_Err | |
| 366 | 370 | } | |
| 367 | 371 | var templ_7745c5c3_Var18 string | |
| 368 | 372 | templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(a.Name) | |
| 369 | 373 | if templ_7745c5c3_Err != nil { | |
| 370 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/releases.templ`, Line: 60, Col: 65} | |
| 374 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/releases.templ`, Line: 67, Col: 65} | |
| 371 | 375 | } | |
| 372 | 376 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18)) | |
| 373 | 377 | if templ_7745c5c3_Err != nil { | |
| 374 | 378 | return templ_7745c5c3_Err | |
| 375 | 379 | } | |
| 376 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "</a></td><td>") | |
| 380 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "</a></td><td>") | |
| 377 | 381 | if templ_7745c5c3_Err != nil { | |
| 378 | 382 | return templ_7745c5c3_Err | |
| 379 | 383 | } | |
| 380 | 384 | var templ_7745c5c3_Var19 string | |
| 381 | 385 | templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(a.Size) | |
| 382 | 386 | if templ_7745c5c3_Err != nil { | |
| 383 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/releases.templ`, Line: 60, Col: 88} | |
| 387 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/releases.templ`, Line: 67, Col: 88} | |
| 384 | 388 | } | |
| 385 | 389 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19)) | |
| 386 | 390 | if templ_7745c5c3_Err != nil { | |
| 387 | 391 | return templ_7745c5c3_Err | |
| 388 | 392 | } | |
| 389 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "</td><td>") | |
| 393 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "</td><td>") | |
| 390 | 394 | if templ_7745c5c3_Err != nil { | |
| 391 | 395 | return templ_7745c5c3_Err | |
| 392 | 396 | } | |
| 393 | 397 | var templ_7745c5c3_Var20 string | |
| 394 | 398 | templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(a.ContentType) | |
| 395 | 399 | if templ_7745c5c3_Err != nil { | |
| 396 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/releases.templ`, Line: 60, Col: 114} | |
| 400 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/releases.templ`, Line: 67, Col: 114} | |
| 397 | 401 | } | |
| 398 | 402 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20)) | |
| 399 | 403 | if templ_7745c5c3_Err != nil { | |
| 400 | 404 | return templ_7745c5c3_Err | |
| 401 | 405 | } | |
| 402 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "</td></tr>") | |
| 406 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "</td></tr>") | |
| 403 | 407 | if templ_7745c5c3_Err != nil { | |
| 404 | 408 | return templ_7745c5c3_Err | |
| 405 | 409 | } | |
| 406 | 410 | } | |
| 407 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "</tbody></table>") | |
| 411 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "</tbody></table>") | |
| 408 | 412 | if templ_7745c5c3_Err != nil { | |
| 409 | 413 | return templ_7745c5c3_Err | |
| 410 | 414 | } | |
| 411 | 415 | } | |
| 412 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, " <p><a href=\"") | |
| 416 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, " <p><a href=\"") | |
| 413 | 417 | if templ_7745c5c3_Err != nil { | |
| 414 | 418 | return templ_7745c5c3_Err | |
| 415 | 419 | } | |
| 416 | 420 | var templ_7745c5c3_Var21 templ.SafeURL | |
| 417 | 421 | templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.SourceHref)) | |
| 418 | 422 | if templ_7745c5c3_Err != nil { | |
| 419 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/releases.templ`, Line: 65, Col: 42} | |
| 423 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/releases.templ`, Line: 72, Col: 42} | |
| 420 | 424 | } | |
| 421 | 425 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21)) | |
| 422 | 426 | if templ_7745c5c3_Err != nil { | |
| 423 | 427 | return templ_7745c5c3_Err | |
| 424 | 428 | } | |
| 425 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "\">Source</a></p>") | |
| 429 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "\">Source</a></p>") | |
| 426 | 430 | if templ_7745c5c3_Err != nil { | |
| 427 | 431 | return templ_7745c5c3_Err | |
| 428 | 432 | } | |
| @@ -245,27 +245,27 @@ func ReleaseDetail(l Layout, p ReleaseDetailPage) templ.Component { | |||||
| 245 | }() | 245 | }() | ||
| 246 | } | 246 | } | ||
| 247 | ctx = templ.InitializeContext(ctx) | 247 | ctx = templ.InitializeContext(ctx) | ||
| 248 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<h1>") | 248 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<section class=\"mr-card\"><header class=\"show mr-h\"><h1>") |
| 249 | if templ_7745c5c3_Err != nil { | 249 | if templ_7745c5c3_Err != nil { | ||
| 250 | return templ_7745c5c3_Err | 250 | return templ_7745c5c3_Err | ||
| 251 | } | 251 | } | ||
| 252 | var templ_7745c5c3_Var12 string | 252 | var templ_7745c5c3_Var12 string | ||
| 253 | templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(p.Title) | 253 | templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(p.Title) | ||
| 254 | if templ_7745c5c3_Err != nil { | 254 | if templ_7745c5c3_Err != nil { | ||
| 255 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/releases.templ`, Line: 43, Col: 15} | 255 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/releases.templ`, Line: 45, Col: 17} |
| 256 | } | 256 | } | ||
| 257 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12)) | 257 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12)) | ||
| 258 | if templ_7745c5c3_Err != nil { | 258 | if templ_7745c5c3_Err != nil { | ||
| 259 | return templ_7745c5c3_Err | 259 | return templ_7745c5c3_Err | ||
| 260 | } | 260 | } | ||
| 261 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</h1><p><span class=\"pill\">") | 261 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</h1><dl class=\"kv show-kv\"><dt>tag</dt><dd><span class=\"pill\">") |
| 262 | if templ_7745c5c3_Err != nil { | 262 | if templ_7745c5c3_Err != nil { | ||
| 263 | return templ_7745c5c3_Err | 263 | return templ_7745c5c3_Err | ||
| 264 | } | 264 | } | ||
| 265 | var templ_7745c5c3_Var13 string | 265 | var templ_7745c5c3_Var13 string | ||
| 266 | templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(p.Tag) | 266 | templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(p.Tag) | ||
| 267 | if templ_7745c5c3_Err != nil { | 267 | if templ_7745c5c3_Err != nil { | ||
| 268 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/releases.templ`, Line: 45, Col: 29} | 268 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/releases.templ`, Line: 48, Col: 35} |
| 269 | } | 269 | } | ||
| 270 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13)) | 270 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13)) | ||
| 271 | if templ_7745c5c3_Err != nil { | 271 | if templ_7745c5c3_Err != nil { | ||
| @@ -279,52 +279,56 @@ func ReleaseDetail(l Layout, p ReleaseDetailPage) templ.Component { | |||||
| 279 | if templ_7745c5c3_Err != nil { | 279 | if templ_7745c5c3_Err != nil { | ||
| 280 | return templ_7745c5c3_Err | 280 | return templ_7745c5c3_Err | ||
| 281 | } | 281 | } | ||
| 282 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</dd>") | |||
| 283 | + | if templ_7745c5c3_Err != nil { | |||
| 284 | + | return templ_7745c5c3_Err | |||
| 285 | + | } | |||
| 282 | if p.SHA != "" { | 286 | if p.SHA != "" { | ||
| 283 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "<a href=\"") | 287 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "<dt>commit</dt><dd><a href=\"") |
| 284 | if templ_7745c5c3_Err != nil { | 288 | if templ_7745c5c3_Err != nil { | ||
| 285 | return templ_7745c5c3_Err | 289 | return templ_7745c5c3_Err | ||
| 286 | } | 290 | } | ||
| 287 | var templ_7745c5c3_Var14 templ.SafeURL | 291 | var templ_7745c5c3_Var14 templ.SafeURL | ||
| 288 | templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.CommitHref)) | 292 | templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.CommitHref)) | ||
| 289 | if templ_7745c5c3_Err != nil { | 293 | if templ_7745c5c3_Err != nil { | ||
| 290 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/releases.templ`, Line: 47, Col: 41} | 294 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/releases.templ`, Line: 51, Col: 47} |
| 291 | } | 295 | } | ||
| 292 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14)) | 296 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14)) | ||
| 293 | if templ_7745c5c3_Err != nil { | 297 | if templ_7745c5c3_Err != nil { | ||
| 294 | return templ_7745c5c3_Err | 298 | return templ_7745c5c3_Err | ||
| 295 | } | 299 | } | ||
| 296 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "\"><code>") | 300 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "\"><code>") |
| 297 | if templ_7745c5c3_Err != nil { | 301 | if templ_7745c5c3_Err != nil { | ||
| 298 | return templ_7745c5c3_Err | 302 | return templ_7745c5c3_Err | ||
| 299 | } | 303 | } | ||
| 300 | var templ_7745c5c3_Var15 string | 304 | var templ_7745c5c3_Var15 string | ||
| 301 | templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(p.SHA) | 305 | templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(p.SHA) | ||
| 302 | if templ_7745c5c3_Err != nil { | 306 | if templ_7745c5c3_Err != nil { | ||
| 303 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/releases.templ`, Line: 47, Col: 57} | 307 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/releases.templ`, Line: 51, Col: 63} |
| 304 | } | 308 | } | ||
| 305 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15)) | 309 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15)) | ||
| 306 | if templ_7745c5c3_Err != nil { | 310 | if templ_7745c5c3_Err != nil { | ||
| 307 | return templ_7745c5c3_Err | 311 | return templ_7745c5c3_Err | ||
| 308 | } | 312 | } | ||
| 309 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "</code></a> ") | 313 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "</code></a></dd>") |
| 310 | if templ_7745c5c3_Err != nil { | 314 | if templ_7745c5c3_Err != nil { | ||
| 311 | return templ_7745c5c3_Err | 315 | return templ_7745c5c3_Err | ||
| 312 | } | 316 | } | ||
| 313 | } | 317 | } | ||
| 314 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "<span class=\"dim\">") | 318 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "<dt>published</dt><dd>") |
| 315 | if templ_7745c5c3_Err != nil { | 319 | if templ_7745c5c3_Err != nil { | ||
| 316 | return templ_7745c5c3_Err | 320 | return templ_7745c5c3_Err | ||
| 317 | } | 321 | } | ||
| 318 | var templ_7745c5c3_Var16 string | 322 | var templ_7745c5c3_Var16 string | ||
| 319 | templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(p.Published) | 323 | templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(p.Published) | ||
| 320 | if templ_7745c5c3_Err != nil { | 324 | if templ_7745c5c3_Err != nil { | ||
| 321 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/releases.templ`, Line: 49, Col: 34} | 325 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/releases.templ`, Line: 54, Col: 22} |
| 322 | } | 326 | } | ||
| 323 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16)) | 327 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16)) | ||
| 324 | if templ_7745c5c3_Err != nil { | 328 | if templ_7745c5c3_Err != nil { | ||
| 325 | return templ_7745c5c3_Err | 329 | return templ_7745c5c3_Err | ||
| 326 | } | 330 | } | ||
| 327 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "</span></p><div class=\"md\">") | 331 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "</dd></dl></header><article class=\"mr-desc prose\">") |
| 328 | if templ_7745c5c3_Err != nil { | 332 | if templ_7745c5c3_Err != nil { | ||
| 329 | return templ_7745c5c3_Err | 333 | return templ_7745c5c3_Err | ||
| 330 | } | 334 | } | ||
| @@ -332,97 +336,97 @@ func ReleaseDetail(l Layout, p ReleaseDetailPage) templ.Component { | |||||
| 332 | if templ_7745c5c3_Err != nil { | 336 | if templ_7745c5c3_Err != nil { | ||
| 333 | return templ_7745c5c3_Err | 337 | return templ_7745c5c3_Err | ||
| 334 | } | 338 | } | ||
| 335 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "</div><h2>Assets</h2>") | 339 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "</article></section><h2>Assets</h2>") |
| 336 | if templ_7745c5c3_Err != nil { | 340 | if templ_7745c5c3_Err != nil { | ||
| 337 | return templ_7745c5c3_Err | 341 | return templ_7745c5c3_Err | ||
| 338 | } | 342 | } | ||
| 339 | if len(p.Assets) == 0 { | 343 | if len(p.Assets) == 0 { | ||
| 340 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "<p class=\"dim\">no assets.</p>") | 344 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "<p class=\"dim\">no assets.</p>") |
| 341 | if templ_7745c5c3_Err != nil { | 345 | if templ_7745c5c3_Err != nil { | ||
| 342 | return templ_7745c5c3_Err | 346 | return templ_7745c5c3_Err | ||
| 343 | } | 347 | } | ||
| 344 | } else { | 348 | } else { | ||
| 345 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "<table class=\"list\" aria-label=\"Release assets\"><thead><tr><th>name</th><th>size</th><th>content type</th></tr></thead> <tbody>") | 349 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "<table class=\"list\" aria-label=\"Release assets\"><thead><tr><th>name</th><th>size</th><th>content type</th></tr></thead> <tbody>") |
| 346 | if templ_7745c5c3_Err != nil { | 350 | if templ_7745c5c3_Err != nil { | ||
| 347 | return templ_7745c5c3_Err | 351 | return templ_7745c5c3_Err | ||
| 348 | } | 352 | } | ||
| 349 | for _, a := range p.Assets { | 353 | for _, a := range p.Assets { | ||
| 350 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "<tr><td><a href=\"") | 354 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "<tr><td><a href=\"") |
| 351 | if templ_7745c5c3_Err != nil { | 355 | if templ_7745c5c3_Err != nil { | ||
| 352 | return templ_7745c5c3_Err | 356 | return templ_7745c5c3_Err | ||
| 353 | } | 357 | } | ||
| 354 | var templ_7745c5c3_Var17 templ.SafeURL | 358 | var templ_7745c5c3_Var17 templ.SafeURL | ||
| 355 | templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(a.Href)) | 359 | templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(a.Href)) | ||
| 356 | if templ_7745c5c3_Err != nil { | 360 | if templ_7745c5c3_Err != nil { | ||
| 357 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/releases.templ`, Line: 60, Col: 45} | 361 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/releases.templ`, Line: 67, Col: 45} |
| 358 | } | 362 | } | ||
| 359 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17)) | 363 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17)) | ||
| 360 | if templ_7745c5c3_Err != nil { | 364 | if templ_7745c5c3_Err != nil { | ||
| 361 | return templ_7745c5c3_Err | 365 | return templ_7745c5c3_Err | ||
| 362 | } | 366 | } | ||
| 363 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "\" download>") | 367 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "\" download>") |
| 364 | if templ_7745c5c3_Err != nil { | 368 | if templ_7745c5c3_Err != nil { | ||
| 365 | return templ_7745c5c3_Err | 369 | return templ_7745c5c3_Err | ||
| 366 | } | 370 | } | ||
| 367 | var templ_7745c5c3_Var18 string | 371 | var templ_7745c5c3_Var18 string | ||
| 368 | templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(a.Name) | 372 | templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(a.Name) | ||
| 369 | if templ_7745c5c3_Err != nil { | 373 | if templ_7745c5c3_Err != nil { | ||
| 370 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/releases.templ`, Line: 60, Col: 65} | 374 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/releases.templ`, Line: 67, Col: 65} |
| 371 | } | 375 | } | ||
| 372 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18)) | 376 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18)) | ||
| 373 | if templ_7745c5c3_Err != nil { | 377 | if templ_7745c5c3_Err != nil { | ||
| 374 | return templ_7745c5c3_Err | 378 | return templ_7745c5c3_Err | ||
| 375 | } | 379 | } | ||
| 376 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "</a></td><td>") | 380 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "</a></td><td>") |
| 377 | if templ_7745c5c3_Err != nil { | 381 | if templ_7745c5c3_Err != nil { | ||
| 378 | return templ_7745c5c3_Err | 382 | return templ_7745c5c3_Err | ||
| 379 | } | 383 | } | ||
| 380 | var templ_7745c5c3_Var19 string | 384 | var templ_7745c5c3_Var19 string | ||
| 381 | templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(a.Size) | 385 | templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(a.Size) | ||
| 382 | if templ_7745c5c3_Err != nil { | 386 | if templ_7745c5c3_Err != nil { | ||
| 383 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/releases.templ`, Line: 60, Col: 88} | 387 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/releases.templ`, Line: 67, Col: 88} |
| 384 | } | 388 | } | ||
| 385 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19)) | 389 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19)) | ||
| 386 | if templ_7745c5c3_Err != nil { | 390 | if templ_7745c5c3_Err != nil { | ||
| 387 | return templ_7745c5c3_Err | 391 | return templ_7745c5c3_Err | ||
| 388 | } | 392 | } | ||
| 389 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "</td><td>") | 393 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "</td><td>") |
| 390 | if templ_7745c5c3_Err != nil { | 394 | if templ_7745c5c3_Err != nil { | ||
| 391 | return templ_7745c5c3_Err | 395 | return templ_7745c5c3_Err | ||
| 392 | } | 396 | } | ||
| 393 | var templ_7745c5c3_Var20 string | 397 | var templ_7745c5c3_Var20 string | ||
| 394 | templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(a.ContentType) | 398 | templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(a.ContentType) | ||
| 395 | if templ_7745c5c3_Err != nil { | 399 | if templ_7745c5c3_Err != nil { | ||
| 396 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/releases.templ`, Line: 60, Col: 114} | 400 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/releases.templ`, Line: 67, Col: 114} |
| 397 | } | 401 | } | ||
| 398 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20)) | 402 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20)) | ||
| 399 | if templ_7745c5c3_Err != nil { | 403 | if templ_7745c5c3_Err != nil { | ||
| 400 | return templ_7745c5c3_Err | 404 | return templ_7745c5c3_Err | ||
| 401 | } | 405 | } | ||
| 402 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "</td></tr>") | 406 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "</td></tr>") |
| 403 | if templ_7745c5c3_Err != nil { | 407 | if templ_7745c5c3_Err != nil { | ||
| 404 | return templ_7745c5c3_Err | 408 | return templ_7745c5c3_Err | ||
| 405 | } | 409 | } | ||
| 406 | } | 410 | } | ||
| 407 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "</tbody></table>") | 411 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "</tbody></table>") |
| 408 | if templ_7745c5c3_Err != nil { | 412 | if templ_7745c5c3_Err != nil { | ||
| 409 | return templ_7745c5c3_Err | 413 | return templ_7745c5c3_Err | ||
| 410 | } | 414 | } | ||
| 411 | } | 415 | } | ||
| 412 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, " <p><a href=\"") | 416 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, " <p><a href=\"") |
| 413 | if templ_7745c5c3_Err != nil { | 417 | if templ_7745c5c3_Err != nil { | ||
| 414 | return templ_7745c5c3_Err | 418 | return templ_7745c5c3_Err | ||
| 415 | } | 419 | } | ||
| 416 | var templ_7745c5c3_Var21 templ.SafeURL | 420 | var templ_7745c5c3_Var21 templ.SafeURL | ||
| 417 | templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.SourceHref)) | 421 | templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.SourceHref)) | ||
| 418 | if templ_7745c5c3_Err != nil { | 422 | if templ_7745c5c3_Err != nil { | ||
| 419 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/releases.templ`, Line: 65, Col: 42} | 423 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/releases.templ`, Line: 72, Col: 42} |
| 420 | } | 424 | } | ||
| 421 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21)) | 425 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21)) | ||
| 422 | if templ_7745c5c3_Err != nil { | 426 | if templ_7745c5c3_Err != nil { | ||
| 423 | return templ_7745c5c3_Err | 427 | return templ_7745c5c3_Err | ||
| 424 | } | 428 | } | ||
| 425 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "\">Source</a></p>") | 429 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "\">Source</a></p>") |
| 426 | if templ_7745c5c3_Err != nil { | 430 | if templ_7745c5c3_Err != nil { | ||
| 427 | return templ_7745c5c3_Err | 431 | return templ_7745c5c3_Err | ||
| 428 | } | 432 | } | ||
M internal/adapters/web/releases_test.go +10 -1
| @@ -38,7 +38,7 @@ func newReleaseFixture(t *testing.T) *releaseFixture { | |||
| 38 | 38 | t.Helper() | |
| 39 | 39 | repo := repoName(t, "proj") | |
| 40 | 40 | f := &releaseFixture{svc: mocks.NewReleaseService(t), data: "0123456789"} | |
| 41 | - | f.published = domain.Release{ID: 1, Repo: repo, Tag: "stable/v1", Title: "First release", Notes: "# Notes\n\n**Ready** [bad](javascript:alert(1)) <script>alert(1)</script>\n\n[guide](guide.md)", TargetCommitish: strings.Repeat("a", 40), PublishedAt: fixedNow, Prerelease: true} | |
| 41 | + | f.published = domain.Release{ID: 1, Repo: repo, Tag: "stable/v1", Title: "First release", Notes: "# Notes\n\n**Ready** [bad](javascript:alert(1)) <script>alert(1)</script>\n\n[guide](guide.md)\n\n```sh\ncurl https://example.com\n```", TargetCommitish: strings.Repeat("a", 40), PublishedAt: fixedNow, Prerelease: true} | |
| 42 | 42 | f.draft = domain.Release{ID: 2, Repo: repo, Tag: "unpublished", Title: "Secret draft", Draft: true, MakeLatest: true} | |
| 43 | 43 | f.asset = domain.ReleaseAsset{ID: 7, ReleaseID: 1, Repo: repo, Name: "build.zip", Size: 10, ContentType: "application/zip", Digest: "sha256:" + strings.Repeat("b", 64)} | |
| 44 | 44 | f.svc.EXPECT().GetRelease(mock.Anything, mock.Anything, repo, mock.Anything).RunAndReturn(func(_ context.Context, p domain.Principal, _ domain.RepoName, ref ports.ReleaseRef) (domain.Release, error) { | |
| @@ -137,6 +137,15 @@ func TestReleasePages(t *testing.T) { | |||
| 137 | 137 | for _, text := range []string{"<strong>Ready</strong>", "build.zip", humanSize(10), "application/zip", `href="/proj/releases/assets/7"`, `href="/proj/tree/stable/v1"`, `href="/proj/commit/` + strings.Repeat("a", 40) + `"`, "view=release"} { | |
| 138 | 138 | assert.Contains(t, body, text) | |
| 139 | 139 | } | |
| 140 | + | notes := strings.SplitN(body, `<article class="mr-desc prose">`, 2) | |
| 141 | + | require.Len(t, notes, 2) | |
| 142 | + | notesBody := strings.SplitN(notes[1], "</article>", 2)[0] | |
| 143 | + | assert.Contains(t, notesBody, "<pre") | |
| 144 | + | assert.Contains(t, notesBody, "<code") | |
| 145 | + | assert.Contains(t, notesBody, "curl") | |
| 146 | + | assert.Contains(t, notesBody, "</code></pre>") | |
| 147 | + | assert.Contains(t, notesBody, `<h1 id="md-notes">Notes</h1>`) | |
| 148 | + | assert.Contains(t, body, `<section class="mr-card">`) | |
| 140 | 149 | assert.NotContains(t, body, "<script>alert") | |
| 141 | 150 | assert.NotContains(t, body, `href="javascript:`) | |
| 142 | 151 | assert.Contains(t, body, `href="/proj/blob/stable/v1/guide.md"`) | |
| @@ -38,7 +38,7 @@ func newReleaseFixture(t *testing.T) *releaseFixture { | |||||
| 38 | t.Helper() | 38 | t.Helper() | ||
| 39 | repo := repoName(t, "proj") | 39 | repo := repoName(t, "proj") | ||
| 40 | f := &releaseFixture{svc: mocks.NewReleaseService(t), data: "0123456789"} | 40 | f := &releaseFixture{svc: mocks.NewReleaseService(t), data: "0123456789"} | ||
| 41 | - | f.published = domain.Release{ID: 1, Repo: repo, Tag: "stable/v1", Title: "First release", Notes: "# Notes\n\n**Ready** [bad](javascript:alert(1)) <script>alert(1)</script>\n\n[guide](guide.md)", TargetCommitish: strings.Repeat("a", 40), PublishedAt: fixedNow, Prerelease: true} | 41 | + | f.published = domain.Release{ID: 1, Repo: repo, Tag: "stable/v1", Title: "First release", Notes: "# Notes\n\n**Ready** [bad](javascript:alert(1)) <script>alert(1)</script>\n\n[guide](guide.md)\n\n```sh\ncurl https://example.com\n```", TargetCommitish: strings.Repeat("a", 40), PublishedAt: fixedNow, Prerelease: true} |
| 42 | f.draft = domain.Release{ID: 2, Repo: repo, Tag: "unpublished", Title: "Secret draft", Draft: true, MakeLatest: true} | 42 | f.draft = domain.Release{ID: 2, Repo: repo, Tag: "unpublished", Title: "Secret draft", Draft: true, MakeLatest: true} | ||
| 43 | f.asset = domain.ReleaseAsset{ID: 7, ReleaseID: 1, Repo: repo, Name: "build.zip", Size: 10, ContentType: "application/zip", Digest: "sha256:" + strings.Repeat("b", 64)} | 43 | f.asset = domain.ReleaseAsset{ID: 7, ReleaseID: 1, Repo: repo, Name: "build.zip", Size: 10, ContentType: "application/zip", Digest: "sha256:" + strings.Repeat("b", 64)} | ||
| 44 | f.svc.EXPECT().GetRelease(mock.Anything, mock.Anything, repo, mock.Anything).RunAndReturn(func(_ context.Context, p domain.Principal, _ domain.RepoName, ref ports.ReleaseRef) (domain.Release, error) { | 44 | f.svc.EXPECT().GetRelease(mock.Anything, mock.Anything, repo, mock.Anything).RunAndReturn(func(_ context.Context, p domain.Principal, _ domain.RepoName, ref ports.ReleaseRef) (domain.Release, error) { | ||
| @@ -137,6 +137,15 @@ func TestReleasePages(t *testing.T) { | |||||
| 137 | for _, text := range []string{"<strong>Ready</strong>", "build.zip", humanSize(10), "application/zip", `href="/proj/releases/assets/7"`, `href="/proj/tree/stable/v1"`, `href="/proj/commit/` + strings.Repeat("a", 40) + `"`, "view=release"} { | 137 | for _, text := range []string{"<strong>Ready</strong>", "build.zip", humanSize(10), "application/zip", `href="/proj/releases/assets/7"`, `href="/proj/tree/stable/v1"`, `href="/proj/commit/` + strings.Repeat("a", 40) + `"`, "view=release"} { | ||
| 138 | assert.Contains(t, body, text) | 138 | assert.Contains(t, body, text) | ||
| 139 | } | 139 | } | ||
| 140 | + | notes := strings.SplitN(body, `<article class="mr-desc prose">`, 2) | |||
| 141 | + | require.Len(t, notes, 2) | |||
| 142 | + | notesBody := strings.SplitN(notes[1], "</article>", 2)[0] | |||
| 143 | + | assert.Contains(t, notesBody, "<pre") | |||
| 144 | + | assert.Contains(t, notesBody, "<code") | |||
| 145 | + | assert.Contains(t, notesBody, "curl") | |||
| 146 | + | assert.Contains(t, notesBody, "</code></pre>") | |||
| 147 | + | assert.Contains(t, notesBody, `<h1 id="md-notes">Notes</h1>`) | |||
| 148 | + | assert.Contains(t, body, `<section class="mr-card">`) | |||
| 140 | assert.NotContains(t, body, "<script>alert") | 149 | assert.NotContains(t, body, "<script>alert") | ||
| 141 | assert.NotContains(t, body, `href="javascript:`) | 150 | assert.NotContains(t, body, `href="javascript:`) | ||
| 142 | assert.Contains(t, body, `href="/proj/blob/stable/v1/guide.md"`) | 151 | assert.Contains(t, body, `href="/proj/blob/stable/v1/guide.md"`) | ||
M internal/adapters/web/summary.go +21 -0
| @@ -68,6 +68,7 @@ func (s *Server) summary(ctx context.Context, r *http.Request) (templ.Component, | |||
| 68 | 68 | ||
| 69 | 69 | p.Facts = s.facts(d, now) | |
| 70 | 70 | p.Latest = s.latest(ctx, d) | |
| 71 | + | p.Release = s.summaryRelease(ctx, d.repo.Name, now) | |
| 71 | 72 | p.Actions = s.recentActions(ctx, d.repo.Name, now) | |
| 72 | 73 | ||
| 73 | 74 | if rd, err := s.readme(ctx, d, ref); err != nil { | |
| @@ -272,3 +273,23 @@ func (s *Server) listRecentActions(ctx context.Context, repo domain.RepoName, no | |||
| 272 | 273 | } | |
| 273 | 274 | return a, nil | |
| 274 | 275 | } | |
| 276 | + | ||
| 277 | + | // summaryRelease uses the same Latest selection as the releases page. A | |
| 278 | + | // missing service or an unavailable release must not prevent repository browsing. | |
| 279 | + | func (s *Server) summaryRelease(ctx context.Context, repo domain.RepoName, now time.Time) *LatestRelease { | |
| 280 | + | if s.cfg.Releases == nil { | |
| 281 | + | return nil | |
| 282 | + | } | |
| 283 | + | rel, err := s.cfg.Releases.GetRelease(ctx, principal(ctx), repo, ports.ReleaseRef{Latest: true}) | |
| 284 | + | if err != nil { | |
| 285 | + | if !errors.Is(err, domain.ErrNotFound) { | |
| 286 | + | log := zerowrap.FromCtx(ctx) | |
| 287 | + | log.Warn().Err(err).Str("repo", repo.String()).Msg("read the latest release") | |
| 288 | + | } | |
| 289 | + | return nil | |
| 290 | + | } | |
| 291 | + | if rel.Draft || rel.PublishedAt.IsZero() { | |
| 292 | + | return nil | |
| 293 | + | } | |
| 294 | + | return &LatestRelease{Tag: rel.Tag, Href: releaseHref(repo.String(), rel.Tag), When: ago(now, rel.PublishedAt), WhenAt: rfc3339(rel.PublishedAt), AllHref: releasesBase(repo.String())} | |
| 295 | + | } | |
| @@ -68,6 +68,7 @@ func (s *Server) summary(ctx context.Context, r *http.Request) (templ.Component, | |||||
| 68 | 68 | ||||
| 69 | p.Facts = s.facts(d, now) | 69 | p.Facts = s.facts(d, now) | ||
| 70 | p.Latest = s.latest(ctx, d) | 70 | p.Latest = s.latest(ctx, d) | ||
| 71 | + | p.Release = s.summaryRelease(ctx, d.repo.Name, now) | |||
| 71 | p.Actions = s.recentActions(ctx, d.repo.Name, now) | 72 | p.Actions = s.recentActions(ctx, d.repo.Name, now) | ||
| 72 | 73 | ||||
| 73 | if rd, err := s.readme(ctx, d, ref); err != nil { | 74 | if rd, err := s.readme(ctx, d, ref); err != nil { | ||
| @@ -272,3 +273,23 @@ func (s *Server) listRecentActions(ctx context.Context, repo domain.RepoName, no | |||||
| 272 | } | 273 | } | ||
| 273 | return a, nil | 274 | return a, nil | ||
| 274 | } | 275 | } | ||
| 276 | + | ||||
| 277 | + | // summaryRelease uses the same Latest selection as the releases page. A | |||
| 278 | + | // missing service or an unavailable release must not prevent repository browsing. | |||
| 279 | + | func (s *Server) summaryRelease(ctx context.Context, repo domain.RepoName, now time.Time) *LatestRelease { | |||
| 280 | + | if s.cfg.Releases == nil { | |||
| 281 | + | return nil | |||
| 282 | + | } | |||
| 283 | + | rel, err := s.cfg.Releases.GetRelease(ctx, principal(ctx), repo, ports.ReleaseRef{Latest: true}) | |||
| 284 | + | if err != nil { | |||
| 285 | + | if !errors.Is(err, domain.ErrNotFound) { | |||
| 286 | + | log := zerowrap.FromCtx(ctx) | |||
| 287 | + | log.Warn().Err(err).Str("repo", repo.String()).Msg("read the latest release") | |||
| 288 | + | } | |||
| 289 | + | return nil | |||
| 290 | + | } | |||
| 291 | + | if rel.Draft || rel.PublishedAt.IsZero() { | |||
| 292 | + | return nil | |||
| 293 | + | } | |||
| 294 | + | return &LatestRelease{Tag: rel.Tag, Href: releaseHref(repo.String(), rel.Tag), When: ago(now, rel.PublishedAt), WhenAt: rfc3339(rel.PublishedAt), AllHref: releasesBase(repo.String())} | |||
| 295 | + | } | |||
A internal/adapters/web/summary_releases_test.go +49 -0
| @@ -0,0 +1,49 @@ | |||
| 1 | + | // SPDX-License-Identifier: AGPL-3.0-or-later | |
| 2 | + | ||
| 3 | + | package web | |
| 4 | + | ||
| 5 | + | import ( | |
| 6 | + | "strings" | |
| 7 | + | "testing" | |
| 8 | + | "time" | |
| 9 | + | ||
| 10 | + | "github.com/stretchr/testify/assert" | |
| 11 | + | "github.com/stretchr/testify/mock" | |
| 12 | + | "github.com/stretchr/testify/require" | |
| 13 | + | ||
| 14 | + | "bnema.dev/ermite/internal/domain" | |
| 15 | + | "bnema.dev/ermite/internal/ports" | |
| 16 | + | "bnema.dev/ermite/internal/ports/mocks" | |
| 17 | + | ) | |
| 18 | + | ||
| 19 | + | func TestSummaryRelease(t *testing.T) { | |
| 20 | + | repo := repoName(t, "proj") | |
| 21 | + | for _, tc := range []struct { | |
| 22 | + | name string | |
| 23 | + | release domain.Release | |
| 24 | + | err error | |
| 25 | + | visible bool | |
| 26 | + | }{ | |
| 27 | + | {name: "published", release: domain.Release{Tag: "v0.9.1", PublishedAt: fixedNow.Add(-6 * time.Minute)}, visible: true}, | |
| 28 | + | {name: "none", err: domain.ErrNotFound}, | |
| 29 | + | {name: "draft", release: domain.Release{Tag: "secret", Draft: true, PublishedAt: fixedNow}}, | |
| 30 | + | } { | |
| 31 | + | t.Run(tc.name, func(t *testing.T) { | |
| 32 | + | svc := mocks.NewReleaseService(t) | |
| 33 | + | svc.EXPECT().GetRelease(mock.Anything, mock.Anything, repo, ports.ReleaseRef{Latest: true}).Return(tc.release, tc.err).Once() | |
| 34 | + | srv := newTestServer(t, Config{Repos: reposServing(t, publicRepo(t, "proj")), Browse: mocks.NewBrowseService(t), Releases: svc}) | |
| 35 | + | rel := srv.summaryRelease(t.Context(), repo, fixedNow) | |
| 36 | + | out := strings.Join(strings.Fields(renderRail(t, SummaryPage{Latest: &LatestTag{Name: "v1", Count: 1}, Release: rel})), " ") | |
| 37 | + | if !tc.visible { | |
| 38 | + | assert.Nil(t, rel) | |
| 39 | + | assert.NotContains(t, out, "<dt>release</dt>") | |
| 40 | + | return | |
| 41 | + | } | |
| 42 | + | require.NotNil(t, rel) | |
| 43 | + | assert.Contains(t, out, `<a href="/proj/releases/tag/v0.9.1">v0.9.1</a> · <time`) | |
| 44 | + | assert.Contains(t, out, `datetime="`+rfc3339(tc.release.PublishedAt)+`"`) | |
| 45 | + | assert.Contains(t, out, `>`+ago(fixedNow, tc.release.PublishedAt)+`</time> · <a href="/proj/releases">all releases</a>`) | |
| 46 | + | assert.Regexp(t, `(?s)<dt>latest tag</dt>.*<dt>release</dt>`, out) | |
| 47 | + | }) | |
| 48 | + | } | |
| 49 | + | } | |
| @@ -0,0 +1,49 @@ | |||||
| 1 | + | // SPDX-License-Identifier: AGPL-3.0-or-later | |||
| 2 | + | ||||
| 3 | + | package web | |||
| 4 | + | ||||
| 5 | + | import ( | |||
| 6 | + | "strings" | |||
| 7 | + | "testing" | |||
| 8 | + | "time" | |||
| 9 | + | ||||
| 10 | + | "github.com/stretchr/testify/assert" | |||
| 11 | + | "github.com/stretchr/testify/mock" | |||
| 12 | + | "github.com/stretchr/testify/require" | |||
| 13 | + | ||||
| 14 | + | "bnema.dev/ermite/internal/domain" | |||
| 15 | + | "bnema.dev/ermite/internal/ports" | |||
| 16 | + | "bnema.dev/ermite/internal/ports/mocks" | |||
| 17 | + | ) | |||
| 18 | + | ||||
| 19 | + | func TestSummaryRelease(t *testing.T) { | |||
| 20 | + | repo := repoName(t, "proj") | |||
| 21 | + | for _, tc := range []struct { | |||
| 22 | + | name string | |||
| 23 | + | release domain.Release | |||
| 24 | + | err error | |||
| 25 | + | visible bool | |||
| 26 | + | }{ | |||
| 27 | + | {name: "published", release: domain.Release{Tag: "v0.9.1", PublishedAt: fixedNow.Add(-6 * time.Minute)}, visible: true}, | |||
| 28 | + | {name: "none", err: domain.ErrNotFound}, | |||
| 29 | + | {name: "draft", release: domain.Release{Tag: "secret", Draft: true, PublishedAt: fixedNow}}, | |||
| 30 | + | } { | |||
| 31 | + | t.Run(tc.name, func(t *testing.T) { | |||
| 32 | + | svc := mocks.NewReleaseService(t) | |||
| 33 | + | svc.EXPECT().GetRelease(mock.Anything, mock.Anything, repo, ports.ReleaseRef{Latest: true}).Return(tc.release, tc.err).Once() | |||
| 34 | + | srv := newTestServer(t, Config{Repos: reposServing(t, publicRepo(t, "proj")), Browse: mocks.NewBrowseService(t), Releases: svc}) | |||
| 35 | + | rel := srv.summaryRelease(t.Context(), repo, fixedNow) | |||
| 36 | + | out := strings.Join(strings.Fields(renderRail(t, SummaryPage{Latest: &LatestTag{Name: "v1", Count: 1}, Release: rel})), " ") | |||
| 37 | + | if !tc.visible { | |||
| 38 | + | assert.Nil(t, rel) | |||
| 39 | + | assert.NotContains(t, out, "<dt>release</dt>") | |||
| 40 | + | return | |||
| 41 | + | } | |||
| 42 | + | require.NotNil(t, rel) | |||
| 43 | + | assert.Contains(t, out, `<a href="/proj/releases/tag/v0.9.1">v0.9.1</a> · <time`) | |||
| 44 | + | assert.Contains(t, out, `datetime="`+rfc3339(tc.release.PublishedAt)+`"`) | |||
| 45 | + | assert.Contains(t, out, `>`+ago(fixedNow, tc.release.PublishedAt)+`</time> · <a href="/proj/releases">all releases</a>`) | |||
| 46 | + | assert.Regexp(t, `(?s)<dt>latest tag</dt>.*<dt>release</dt>`, out) | |||
| 47 | + | }) | |||
| 48 | + | } | |||
| 49 | + | } | |||
M internal/adapters/web/view.go +7 -1
| @@ -211,12 +211,18 @@ type SummaryPage struct { | |||
| 211 | 211 | Outline []Heading | |
| 212 | 212 | AllHref string | |
| 213 | 213 | // Latest is the newest tag, nil when the repository has none. | |
| 214 | - | Latest *LatestTag | |
| 214 | + | Latest *LatestTag | |
| 215 | + | Release *LatestRelease | |
| 215 | 216 | // Actions is the recent runs of the rail, nil when the viewer cannot read | |
| 216 | 217 | // CI (or the server runs none). | |
| 217 | 218 | Actions *RecentActions | |
| 218 | 219 | } | |
| 219 | 220 | ||
| 221 | + | // LatestRelease is the published release selected as Latest by the release service. | |
| 222 | + | type LatestRelease struct { | |
| 223 | + | Tag, Href, When, WhenAt, AllHref string | |
| 224 | + | } | |
| 225 | + | ||
| 220 | 226 | // RecentActions is the "Recent actions" block of the summary rail: the newest | |
| 221 | 227 | // runs. With no run the block is in the page but hidden, so that the first | |
| 222 | 228 | // run reaches it live. | |
| @@ -211,12 +211,18 @@ type SummaryPage struct { | |||||
| 211 | Outline []Heading | 211 | Outline []Heading | ||
| 212 | AllHref string | 212 | AllHref string | ||
| 213 | // Latest is the newest tag, nil when the repository has none. | 213 | // Latest is the newest tag, nil when the repository has none. | ||
| 214 | - | Latest *LatestTag | 214 | + | Latest *LatestTag |
| 215 | + | Release *LatestRelease | |||
| 215 | // Actions is the recent runs of the rail, nil when the viewer cannot read | 216 | // Actions is the recent runs of the rail, nil when the viewer cannot read | ||
| 216 | // CI (or the server runs none). | 217 | // CI (or the server runs none). | ||
| 217 | Actions *RecentActions | 218 | Actions *RecentActions | ||
| 218 | } | 219 | } | ||
| 219 | 220 | ||||
| 221 | + | // LatestRelease is the published release selected as Latest by the release service. | |||
| 222 | + | type LatestRelease struct { | |||
| 223 | + | Tag, Href, When, WhenAt, AllHref string | |||
| 224 | + | } | |||
| 225 | + | ||||
| 220 | // RecentActions is the "Recent actions" block of the summary rail: the newest | 226 | // RecentActions is the "Recent actions" block of the summary rail: the newest | ||
| 221 | // runs. With no run the block is in the page but hidden, so that the first | 227 | // runs. With no run the block is in the page but hidden, so that the first | ||
| 222 | // run reaches it live. | 228 | // run reaches it live. | ||