Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

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' &amp;&amp; (!document.activeElement || !document.activeElement.closest('#request-aside')) &amp;&amp; !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">