vole
Clone
Read only, no credentials:
https://rodent.software/git/vole.git
Write over SSH with a registered key:
git clone ssh://git@0.0.0.0:2222/vole.git
A free and open-source forge that lives in your Git repo.
Update the change page as live regions, not one sidebar swap
707dfd10f309461a77a4a8f73dcce74b07c91fd6 · Keenan Nemetz · 10d ago · parent 9f5efa3c
Update the change page as live regions, not one sidebar swap The 20-second poll replaced the entire sidebar, so every new widget needed its own reason to opt out and live updates stopped while it was in use. The sidebar is now a set of regions: review status, changes, approvals, checks, and the combined result are data-only live regions returned with hx-swap-oob; the target picker, issue search, and composers are outside every polled swap. Approval and the other multi-region writes use the same out-of-band group, and the page action group refreshes from the write without replacing unrelated state.
server/web/templates/requests.html +143−91
| @@ -85,7 +85,7 @@ | |||
| 85 | 85 | {{define "help"}}<span class="help" tabindex="0" role="note" aria-label="{{.}}" data-tip="{{.}}">?</span>{{end}} | |
| 86 | 86 | ||
| 87 | 87 | {{define "request_changes"}} | |
| 88 | <section> | ||
| 88 | <section id="request-changes"{{if .Live}} hx-swap-oob="outerHTML"{{end}}> | ||
| 89 | 89 | <h2 class="with-more"><span>{{icon "file"}}Changes</span>{{if .FileCount}} <a class="more" href="/r/{{.Name}}/requests/{{.Request.ID}}/files">View changes</a>{{end}}</h2> | |
| 90 | 90 | {{if .FileCount}} | |
| 91 | 91 | <p class="muted">{{.FileCount}} file(s) changed{{if .Added}} · <span class="adds">+{{.Added}}</span>{{end}}{{if .Removed}} · <span class="dels">−{{.Removed}}</span>{{end}}{{if .AlreadyMerged}} · merged into {{.Request.Base}}{{end}}</p> | |
| @@ -97,70 +97,108 @@ | |||
| 97 | 97 | </section> | |
| 98 | 98 | {{end}} | |
| 99 | 99 | ||
| 100 | {{/* The whole aside is one polling unit: an approval, a finished check or a | ||
| 101 | push from elsewhere should show up without a reload. The swap skips while | ||
| 102 | the reader is inside the panel or has an issue search open, so a half-made | ||
| 103 | branch selection or a result list is not thrown away under them. */}} | ||
| 100 | {{/* The aside is the page's live coordinator. The poll never replaces this element: | ||
| 101 | it returns out-of-band updates for the data-only regions, so a search, a | ||
| 102 | target picker, or an open composer is not thrown away. */}} | ||
| 104 | 103 | {{define "request_aside"}} | |
| 105 | 104 | <div id="request-aside" | |
| 106 | hx-get="/r/{{.Name}}/requests/{{.Request.ID}}/aside" | ||
| 107 | hx-trigger="every 20s [document.visibilityState === 'visible' && (!document.activeElement || !document.activeElement.closest('#request-aside')) && !document.querySelector('#request-issue-search')?.value]" | ||
| 105 | hx-get="/r/{{.Name}}/requests/{{.Request.ID}}/live" | ||
| 106 | hx-trigger="every 20s [document.visibilityState === 'visible']" | ||
| 108 | 107 | hx-sync="this:replace" | |
| 109 | hx-swap="outerHTML"> | ||
| 108 | hx-swap="none"> | ||
| 110 | 109 | {{template "request_sidebar" .}} | |
| 111 | 110 | </div> | |
| 112 | 111 | {{end}} | |
| 113 | 112 | ||
| 114 | 113 | {{define "request_sidebar"}} | |
| 115 | 114 | <div id="request-sidebar"> | |
| 116 | {{if .Error}}<p class="error">{{.Error}}</p>{{end}} | ||
| 117 | {{if .Notice}}<p class="muted">{{.Notice}}</p>{{end}} | ||
| 118 | <section> | ||
| 119 | <h2 class="with-more"><span>{{icon "git-branch"}}Review{{template "help" "The request follows its head branch, so a push moves the head with it. Changing the base or head starts approvals over. A conflict here blocks landing."}}</span> <a class="more sha" href="/r/{{.Name}}/commit/{{.Request.Head}}" title="commit under review">{{short .Request.Head}}</a></h2> | ||
| 120 | {{if .MergeChecked}}{{if .Conflicts}} | ||
| 121 | <p class="conflict">This branch no longer merges into {{.Request.Base}} cleanly: | ||
| 122 | {{range .Conflicts}}<code>{{.}}</code> {{end}}— rebase or merge {{.Request.Base}} before landing it.</p> | ||
| 123 | {{else}} | ||
| 124 | <p class="muted">Merges into {{.Request.Base}} cleanly.</p> | ||
| 125 | {{end}}{{end}} | ||
| 126 | {{if .CanRetarget}} | ||
| 127 | <form method="post" action="/r/{{.Name}}/requests/{{.Request.ID}}/target" class="request-target"> | ||
| 128 | <input type="hidden" name="csrf" value="{{.CSRF}}"> | ||
| 129 | <div class="target-fields"> | ||
| 130 | <label class="target-field"> | ||
| 131 | <span class="target-label">Base branch</span> | ||
| 132 | <select name="base"> | ||
| 133 | {{range .BaseOptions}}<option value="{{.Name}}"{{if .Selected}} selected{{end}}>{{.Name}}</option>{{end}} | ||
| 134 | </select> | ||
| 135 | </label> | ||
| 136 | <label class="target-field"> | ||
| 137 | <span class="target-label">Head branch</span> | ||
| 138 | <select name="head"> | ||
| 139 | {{if not .Request.HeadRef}}<option value="" selected>pinned commit</option>{{end}} | ||
| 140 | {{range .HeadOptions}}<option value="{{.Name}}"{{if .Selected}} selected{{end}}>{{.Name}}</option>{{end}} | ||
| 141 | </select> | ||
| 142 | </label> | ||
| 143 | </div> | ||
| 144 | <div class="target-actions"> | ||
| 145 | {{if .HeadRefNote}}<p class="muted small">{{.HeadRefNote}}.</p>{{end}} | ||
| 146 | <button class="secondary" type="submit" title="Update base and head" aria-label="Update base and head">{{icon "rotate-ccw"}}Update</button> | ||
| 147 | </div> | ||
| 148 | </form> | ||
| 149 | {{else}} | ||
| 150 | <p class="target-summary"> | ||
| 151 | <code>{{.Request.Base}}</code> | ||
| 152 | <span class="muted" aria-hidden="true">←</span> | ||
| 153 | {{if .Request.HeadRef}}<code>{{.Request.HeadRef}}</code>{{else}}<span class="muted">pinned commit</span>{{end}} | ||
| 154 | </p> | ||
| 155 | {{if .HeadRefNote}}<p class="muted">{{.HeadRefNote}}.</p>{{end}} | ||
| 156 | {{end}} | ||
| 115 | <section id="request-review"> | ||
| 116 | {{template "request_review_status" .}} | ||
| 117 | {{template "request_target" .}} | ||
| 157 | 118 | </section> | |
| 158 | 119 | ||
| 159 | 120 | {{template "request_changes" .}} | |
| 160 | 121 | ||
| 161 | 122 | {{template "request_issues" .}} | |
| 162 | 123 | ||
| 163 | <section> | ||
| 124 | {{template "request_approvals" .}} | ||
| 125 | |||
| 126 | {{template "request_checks" .}} | ||
| 127 | |||
| 128 | {{template "request_combined" .}} | ||
| 129 | </div> | ||
| 130 | {{end}} | ||
| 131 | |||
| 132 | {{define "request_alert"}} | ||
| 133 | <div id="request-alert"{{if .Live}} hx-swap-oob="outerHTML"{{end}}> | ||
| 134 | {{if .Error}}<p class="error">{{.Error}}</p>{{end}} | ||
| 135 | {{if .Notice}}<p class="muted">{{.Notice}}</p>{{end}} | ||
| 136 | </div> | ||
| 137 | {{end}} | ||
| 138 | |||
| 139 | {{define "request_live"}} | ||
| 140 | {{template "request_review_status" .}} | ||
| 141 | {{template "request_changes" .}} | ||
| 142 | {{template "request_approvals" .}} | ||
| 143 | {{template "request_checks" .}} | ||
| 144 | {{template "request_combined" .}} | ||
| 145 | {{template "request_merge_note" .}} | ||
| 146 | {{end}} | ||
| 147 | |||
| 148 | {{define "request_live_write"}} | ||
| 149 | {{template "request_alert" .}} | ||
| 150 | {{template "request_actions" .}} | ||
| 151 | {{template "request_live" .}} | ||
| 152 | {{end}} | ||
| 153 | |||
| 154 | {{define "request_review_status"}} | ||
| 155 | <div id="request-review-status"{{if .Live}} hx-swap-oob="outerHTML"{{end}}> | ||
| 156 | <h2 class="with-more"><span>{{icon "git-branch"}}Review{{template "help" "The request follows its head branch, so a push moves the head with it. Changing the base or head starts approvals over. A conflict here blocks landing."}}</span> <a class="more sha" href="/r/{{.Name}}/commit/{{.Request.Head}}" title="commit under review">{{short .Request.Head}}</a></h2> | ||
| 157 | {{if .MergeChecked}}{{if .Conflicts}} | ||
| 158 | <p class="conflict">This branch no longer merges into {{.Request.Base}} cleanly: | ||
| 159 | {{range .Conflicts}}<code>{{.}}</code> {{end}}— rebase or merge {{.Request.Base}} before landing it.</p> | ||
| 160 | {{else}} | ||
| 161 | <p class="muted">Merges into {{.Request.Base}} cleanly.</p> | ||
| 162 | {{end}}{{end}} | ||
| 163 | </div> | ||
| 164 | {{end}} | ||
| 165 | |||
| 166 | {{define "request_target"}} | ||
| 167 | {{if .CanRetarget}} | ||
| 168 | <form method="post" action="/r/{{.Name}}/requests/{{.Request.ID}}/target" class="request-target"> | ||
| 169 | <input type="hidden" name="csrf" value="{{.CSRF}}"> | ||
| 170 | <div class="target-fields"> | ||
| 171 | <label class="target-field"> | ||
| 172 | <span class="target-label">Base branch</span> | ||
| 173 | <select name="base"> | ||
| 174 | {{range .BaseOptions}}<option value="{{.Name}}"{{if .Selected}} selected{{end}}>{{.Name}}</option>{{end}} | ||
| 175 | </select> | ||
| 176 | </label> | ||
| 177 | <label class="target-field"> | ||
| 178 | <span class="target-label">Head branch</span> | ||
| 179 | <select name="head"> | ||
| 180 | {{if not .Request.HeadRef}}<option value="" selected>pinned commit</option>{{end}} | ||
| 181 | {{range .HeadOptions}}<option value="{{.Name}}"{{if .Selected}} selected{{end}}>{{.Name}}</option>{{end}} | ||
| 182 | </select> | ||
| 183 | </label> | ||
| 184 | </div> | ||
| 185 | <div class="target-actions"> | ||
| 186 | {{if .HeadRefNote}}<p class="muted small">{{.HeadRefNote}}.</p>{{end}} | ||
| 187 | <button class="secondary" type="submit" title="Update base and head" aria-label="Update base and head">{{icon "rotate-ccw"}}Update</button> | ||
| 188 | </div> | ||
| 189 | </form> | ||
| 190 | {{else}} | ||
| 191 | <p class="target-summary"> | ||
| 192 | <code>{{.Request.Base}}</code> | ||
| 193 | <span class="muted" aria-hidden="true">←</span> | ||
| 194 | {{if .Request.HeadRef}}<code>{{.Request.HeadRef}}</code>{{else}}<span class="muted">pinned commit</span>{{end}} | ||
| 195 | </p> | ||
| 196 | {{if .HeadRefNote}}<p class="muted">{{.HeadRefNote}}.</p>{{end}} | ||
| 197 | {{end}} | ||
| 198 | {{end}} | ||
| 199 | |||
| 200 | {{define "request_approvals"}} | ||
| 201 | <section id="request-approvals"{{if .Live}} hx-swap-oob="outerHTML"{{end}}> | ||
| 164 | 202 | <h2>{{icon "check"}}Approvals{{template "help" "Approving names the current base and head; a new base or head starts over. An approval stops counting if its author is retired or no longer maintains the repository."}}</h2> | |
| 165 | 203 | <p class="muted">{{if .Required}}{{.Current}} of {{.Required}} required approval(s).{{else}}No approvals required.{{end}}</p> | |
| 166 | 204 | {{if .Approvals}} | |
| @@ -175,8 +213,10 @@ | |||
| 175 | 213 | </ul> | |
| 176 | 214 | {{end}} | |
| 177 | 215 | </section> | |
| 216 | {{end}} | ||
| 178 | 217 | ||
| 179 | <section> | ||
| 218 | {{define "request_checks"}} | ||
| 219 | <section id="request-checks"{{if .Live}} hx-swap-oob="outerHTML"{{end}}> | ||
| 180 | 220 | <h2 class="with-more"><span>{{icon "circle-check"}}Checks{{template "help" "Workflow runs for the commit under review. If the base branch requires checks, a failing or missing run blocks landing."}}</span> <a class="more" href="/r/{{.Name}}/requests/{{.Request.ID}}/runs">View runs</a></h2> | |
| 181 | 221 | {{with .ContributorCI}} | |
| 182 | 222 | <p class="muted">CI is waiting for a maintainer to allow <code>{{.Ref}}</code> at this commit.</p> | |
| @@ -197,9 +237,11 @@ | |||
| 197 | 237 | <p class="muted">No CI run for this commit yet.</p> | |
| 198 | 238 | {{end}} | |
| 199 | 239 | </section> | |
| 240 | {{end}} | ||
| 200 | 241 | ||
| 242 | {{define "request_combined"}} | ||
| 201 | 243 | {{if .CombinedNeeded}} | |
| 202 | <section> | ||
| 244 | <section id="request-combined"{{if .Live}} hx-swap-oob="outerHTML"{{end}}> | ||
| 203 | 245 | <h2 class="with-more"><span>{{icon "git-merge"}}Combined result{{template "help" "The merge that would land, built from this change and the latest base. It is tested automatically so the branch is ready when the review is. Only a conflict needs work on the branch."}}</span> {{if .CombinedSHA}}<a class="more sha" href="/r/{{.Name}}/commit/{{.CombinedSHA}}" title="combined result commit">{{short .CombinedSHA}}</a>{{end}}</h2> | |
| 204 | 246 | {{if eq .CombinedState "passed"}} | |
| 205 | 247 | <p class="muted">Passed with the latest <code>{{.Request.Base}}</code>.</p> | |
| @@ -224,8 +266,9 @@ | |||
| 224 | 266 | </div> | |
| 225 | 267 | {{end}} | |
| 226 | 268 | </section> | |
| 269 | {{else if .Live}} | ||
| 270 | <section id="request-combined" hx-swap-oob="outerHTML" hidden></section> | ||
| 227 | 271 | {{end}} | |
| 228 | </div> | ||
| 229 | 272 | {{end}} | |
| 230 | 273 | ||
| 231 | 274 | {{define "request_issues"}} | |
| @@ -322,47 +365,56 @@ | |||
| 322 | 365 | {{end}} | |
| 323 | 366 | </p> | |
| 324 | 367 | </div> | |
| 325 | {{if or .CanApprove .CanClose .CanMerge .MergeArmed .ContributorCI}} | ||
| 326 | <div class="page-head-actions"> | ||
| 327 | {{with .ContributorCI}}{{if .CanAllow}} | ||
| 328 | <form method="post" action="/r/{{$.Name}}/ci/allow"> | ||
| 329 | <input type="hidden" name="csrf" value="{{$.CSRF}}"> | ||
| 330 | <input type="hidden" name="request" value="{{$.Request.ID}}"> | ||
| 331 | <input type="hidden" name="ref" value="refs/heads/{{.Ref}}"> | ||
| 332 | <button class="warn" type="submit">{{icon "play"}}Allow CI</button> | ||
| 333 | </form> | ||
| 334 | {{end}}{{end}} | ||
| 335 | {{if .CanApprove}} | ||
| 336 | <form method="post" action="/r/{{.Name}}/requests/{{.Request.ID}}/approve"> | ||
| 337 | <input type="hidden" name="csrf" value="{{.CSRF}}"> | ||
| 338 | <input type="hidden" name="base" value="{{.Request.Base}}"> | ||
| 339 | <input type="hidden" name="head" value="{{.Request.Head}}"> | ||
| 340 | <button class="success" type="submit">{{icon "check"}}Approve</button> | ||
| 341 | </form> | ||
| 342 | {{end}} | ||
| 343 | {{if .MergeArmed}} | ||
| 344 | <form method="post" action="/r/{{.Name}}/requests/{{.Request.ID}}/merge/cancel"> | ||
| 345 | <input type="hidden" name="csrf" value="{{.CSRF}}"> | ||
| 346 | <button class="secondary" type="submit">{{icon "circle-x"}}Cancel merge</button> | ||
| 347 | </form> | ||
| 348 | {{else if .CanMerge}} | ||
| 349 | <form method="post" action="/r/{{.Name}}/requests/{{.Request.ID}}/merge"> | ||
| 350 | <input type="hidden" name="csrf" value="{{.CSRF}}"> | ||
| 351 | <button class="success" type="submit"{{if .MergeBlocked}} disabled{{end}}>{{icon "git-merge"}}{{if .MergePending}}Merge when ready{{else}}Merge{{end}}</button> | ||
| 352 | </form> | ||
| 353 | {{end}} | ||
| 354 | {{if .CanClose}} | ||
| 355 | <form method="post" action="/r/{{.Name}}/requests/{{.Request.ID}}/state"> | ||
| 356 | <input type="hidden" name="csrf" value="{{.CSRF}}"> | ||
| 357 | <input type="hidden" name="state" value="{{if eq .Request.State "open"}}closed{{else}}open{{end}}"> | ||
| 358 | <button class="{{if eq .Request.State "open"}}danger{{else}}secondary{{end}}" type="submit">{{if eq .Request.State "open"}}{{icon "circle-x"}}Close{{else}}{{icon "rotate-ccw"}}Reopen{{end}}</button> | ||
| 359 | </form> | ||
| 360 | {{end}} | ||
| 361 | </div> | ||
| 368 | {{template "request_actions" .}} | ||
| 369 | {{template "request_merge_note" .}} | ||
| 370 | </div> | ||
| 371 | {{template "request_alert" .}} | ||
| 372 | {{end}} | ||
| 373 | |||
| 374 | {{define "request_merge_note"}} | ||
| 375 | <p id="request-merge-note" class="muted small"{{if .Live}} hx-swap-oob="outerHTML"{{end}}{{if not .MergeBlocked}} hidden{{end}}>{{.MergeBlocked}}</p> | ||
| 376 | {{end}} | ||
| 377 | |||
| 378 | {{define "request_actions"}} | ||
| 379 | {{if or .CanApprove .CanClose .CanMerge .MergeArmed .ContributorCI .Live}} | ||
| 380 | <div id="request-actions" class="page-head-actions"{{if .Live}} hx-swap-oob="outerHTML"{{end}}> | ||
| 381 | {{with .ContributorCI}}{{if .CanAllow}} | ||
| 382 | <form method="post" action="/r/{{$.Name}}/ci/allow"> | ||
| 383 | <input type="hidden" name="csrf" value="{{$.CSRF}}"> | ||
| 384 | <input type="hidden" name="request" value="{{$.Request.ID}}"> | ||
| 385 | <input type="hidden" name="ref" value="refs/heads/{{.Ref}}"> | ||
| 386 | <button class="warn" type="submit">{{icon "play"}}Allow CI</button> | ||
| 387 | </form> | ||
| 388 | {{end}}{{end}} | ||
| 389 | {{if .CanApprove}} | ||
| 390 | <form method="post" action="/r/{{.Name}}/requests/{{.Request.ID}}/approve" hx-post="/r/{{.Name}}/requests/{{.Request.ID}}/approve" hx-swap="none" hx-disabled-elt="find button[type='submit']"> | ||
| 391 | <input type="hidden" name="csrf" value="{{.CSRF}}"> | ||
| 392 | <input type="hidden" name="base" value="{{.Request.Base}}"> | ||
| 393 | <input type="hidden" name="head" value="{{.Request.Head}}"> | ||
| 394 | <button class="success" type="submit">{{icon "check"}}Approve</button> | ||
| 395 | </form> | ||
| 396 | {{end}} | ||
| 397 | {{if .MergeArmed}} | ||
| 398 | <form method="post" action="/r/{{.Name}}/requests/{{.Request.ID}}/merge/cancel"> | ||
| 399 | <input type="hidden" name="csrf" value="{{.CSRF}}"> | ||
| 400 | <button class="secondary" type="submit">{{icon "circle-x"}}Cancel merge</button> | ||
| 401 | </form> | ||
| 402 | {{else if .CanMerge}} | ||
| 403 | <form method="post" action="/r/{{.Name}}/requests/{{.Request.ID}}/merge"> | ||
| 404 | <input type="hidden" name="csrf" value="{{.CSRF}}"> | ||
| 405 | <button class="success" type="submit"{{if .MergeBlocked}} disabled{{end}}>{{icon "git-merge"}}{{if .MergePending}}Merge when ready{{else}}Merge{{end}}</button> | ||
| 406 | </form> | ||
| 407 | {{end}} | ||
| 408 | {{if .CanClose}} | ||
| 409 | <form method="post" action="/r/{{.Name}}/requests/{{.Request.ID}}/state"> | ||
| 410 | <input type="hidden" name="csrf" value="{{.CSRF}}"> | ||
| 411 | <input type="hidden" name="state" value="{{if eq .Request.State "open"}}closed{{else}}open{{end}}"> | ||
| 412 | <button class="{{if eq .Request.State "open"}}danger{{else}}secondary{{end}}" type="submit">{{if eq .Request.State "open"}}{{icon "circle-x"}}Close{{else}}{{icon "rotate-ccw"}}Reopen{{end}}</button> | ||
| 413 | </form> | ||
| 362 | 414 | {{end}} | |
| 363 | {{if .MergeBlocked}}<p class="muted small">{{.MergeBlocked}}</p>{{end}} | ||
| 364 | 415 | </div> | |
| 365 | 416 | {{end}} | |
| 417 | {{end}} | ||
| 366 | 418 | ||
| 367 | 419 | {{define "request_tabs"}} | |
| 368 | 420 | <nav class="subtabs" aria-label="Change sections"> | |