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/request_view.go +25−17
| @@ -168,8 +168,12 @@ type requestSidebarView struct { | |||
| 168 | 168 | CombinedRuns []*runs.Run | |
| 169 | 169 | CombinedState string | |
| 170 | 170 | // Notice is a non-error message a write wants beside the controls. | |
| 171 | Notice string | ||
| 172 | CanDiscuss bool | ||
| 171 | Notice string | ||
| 172 | CanDiscuss bool | ||
| 173 | // Live says this render is an out-of-band update to the data-only regions, | ||
| 174 | // not a page or a targeted section swap. Each live region adds hx-swap-oob so | ||
| 175 | // one poll can update several places without replacing a form or a picker. | ||
| 176 | Live bool | ||
| 173 | 177 | ContributorCI *requestCI | |
| 174 | 178 | Error string | |
| 175 | 179 | } | |
| @@ -189,6 +193,17 @@ func (w *Web) requestSidebarWithLanding(r *http.Request, m *repo.Meta, req *meta | |||
| 189 | 193 | return w.requestSidebar(r, m, req, title, errMsg).withLanding(w.requestLanding(m, req)) | |
| 190 | 194 | } | |
| 191 | 195 | ||
| 196 | // requestLiveWrite is the same data as a full sidebar render, marked so every | ||
| 197 | // region that belongs to the live group carries hx-swap-oob. The response is a | ||
| 198 | // set of targeted updates, not a replacement for the sidebar: a write can update | ||
| 199 | // the status regions without throwing away an issue search or a target picker. | ||
| 200 | func (w *Web) requestLiveWrite(r *http.Request, m *repo.Meta, req *metadata.Request, errMsg string) requestSidebarView { | ||
| 201 | v := w.requestSidebarWithLanding(r, m, req, "", errMsg) | ||
| 202 | v.Live = true | ||
| 203 | return v | ||
| 204 | } | ||
| 205 | |||
| 206 | // requestPageView is the full change page: the sidebar plus the conversation. | ||
| 192 | 207 | type requestPageView struct { | |
| 193 | 208 | requestSidebarView | |
| 194 | 209 | Tab string | |
| @@ -198,13 +213,6 @@ type requestPageView struct { | |||
| 198 | 213 | Thread threadView | |
| 199 | 214 | } | |
| 200 | 215 | ||
| 201 | // requestAsideView is the polling fragment: the Changes block and the sidebar | ||
| 202 | // together, with none of the page shell the handler that serves a full request | ||
| 203 | // needs. | ||
| 204 | type requestAsideView struct { | ||
| 205 | requestSidebarView | ||
| 206 | } | ||
| 207 | |||
| 208 | 216 | func (w *Web) requestSidebar(r *http.Request, m *repo.Meta, req *metadata.Request, title, errMsg string) requestSidebarView { | |
| 209 | 217 | acl, _ := m.ACL() | |
| 210 | 218 | approvals := make([]requestApproval, 0, len(req.Approvals)) | |
| @@ -559,11 +567,11 @@ func (w *Web) RequestView(rw http.ResponseWriter, r *http.Request) { | |||
| 559 | 567 | w.renderRequestPage(rw, r, "", http.StatusOK) | |
| 560 | 568 | } | |
| 561 | 569 | ||
| 562 | // RequestAside serves just the change sidebar for the page's poll. It is the | ||
| 563 | // same fragment the page renders, so an approval, a finished run, or a push from | ||
| 564 | // somewhere else appears without a reload and without the reader losing the | ||
| 565 | // discussion or diff they were reading. | ||
| 566 | func (w *Web) RequestAside(rw http.ResponseWriter, r *http.Request) { | ||
| 570 | // RequestLive serves the out-of-band updates for the change's data-only regions. | ||
| 571 | // The poll element keeps its own DOM (and every form, search, and picker inside | ||
| 572 | // it); the response only names the regions that can change behind the reader's | ||
| 573 | // back. | ||
| 574 | func (w *Web) RequestLive(rw http.ResponseWriter, r *http.Request) { | ||
| 567 | 575 | m, ok := w.open(rw, r) | |
| 568 | 576 | if !ok { | |
| 569 | 577 | return | |
| @@ -573,9 +581,9 @@ func (w *Web) RequestAside(rw http.ResponseWriter, r *http.Request) { | |||
| 573 | 581 | w.notFound(rw, r) | |
| 574 | 582 | return | |
| 575 | 583 | } | |
| 576 | w.render(rw, "request_aside", requestAsideView{ | ||
| 577 | requestSidebarView: w.requestSidebar(r, m, req, "", "").withLanding(w.requestLanding(m, req)), | ||
| 578 | }, 0) | ||
| 584 | view := w.requestSidebar(r, m, req, "", "").withLanding(w.requestLanding(m, req)) | ||
| 585 | view.Live = true | ||
| 586 | w.render(rw, "request_live", view, 0) | ||
| 579 | 587 | } | |
| 580 | 588 | ||
| 581 | 589 | // requestLanding asks the two questions a reviewer has before approving: does the | |