vole
Clone
Read only, no credentials:
https://rodent.software/vole.git
Write over SSH with a registered key:
git clone ssh://git@rodent.software:2222/vole.git
A free and open-source forge that lives in your Git repo.
Build out change review and threaded discussions
f6f4b1db24292d1df877680714952a9c568e2b78 · Keenan Nemetz · 15d ago · parent 564acd4b
Build out change review and threaded discussions - Add Conversation, Files and Runs tabs to change requests. Files uses the code-browser shape with a changed-file tree and one selected-file diff; Runs lists the head commit's workflow results. - Show anchored review comments beneath the line, with a collapsed excerpt read from the exact blob that was reviewed, and return posted anchors to their line note. - Add comment threads: any comment can be replied to, the UI flattens the descendants under one root, reply context links back to the parent, and reply targets feed a single composer at the foot of the discussion. - Collapse reply groups while keeping the root visible, and scroll to the composer when Reply is selected. - Add comment context menus with Reply, Copy text, Copy link, Edit and Delete. Add web edit/delete routes with author or admin checks; deletion leaves a tombstone so replies keep their thread. - Support --reply-to in the issue and change CLI commands, and treat a missing parent as a tombstone rather than an invalid tree. - Fix the diff comment gutter, action button colours, reply labels, and keep the Files tree and diff pane aligned. Update metadata validation, web templates, CSS, JavaScript, tests, and the CLI/change-request/issue docs.
server/web/templates/issues.html +61−19
| @@ -132,35 +132,77 @@ | |||
| 132 | 132 | </section> | |
| 133 | 133 | {{end}} | |
| 134 | 134 | ||
| 135 | {{define "issue_thread"}} | ||
| 136 | <div id="issue-thread"> | ||
| 137 | {{if .ThreadError}}<p class="error">{{.ThreadError}}</p>{{end}} | ||
| 138 | <h2>{{icon "message-square"}}Comments <span class="muted">({{len .Comments}})</span></h2> | ||
| 139 | {{range .Comments}} | ||
| 140 | <article class="comment" id="c-{{.ID}}"> | ||
| 135 | {{define "issue_comment_card"}} | ||
| 136 | <article class="comment{{if .Deleted}} deleted{{end}}" id="c-{{.ID}}"> | ||
| 137 | {{if .Deleted}} | ||
| 138 | <div class="comment-deleted">{{icon "circle-x"}}Comment deleted</div> | ||
| 139 | {{else}} | ||
| 141 | 140 | <header class="comment-head"> | |
| 142 | 141 | <span class="avatar c{{tone .Author}}" aria-hidden="true">{{initial (who .Author)}}</span> | |
| 143 | 142 | <span class="comment-author">{{who .Author}}</span> | |
| 143 | {{if .ReplyTo}}<span class="reply-context">replying to {{if .ReplyToDeleted}}<a href="#c-{{.ReplyTo}}">a deleted comment</a>{{else}}<a href="#c-{{.ReplyTo}}">{{who .ReplyToName}}</a>{{end}}</span>{{end}} | ||
| 144 | 144 | <span class="muted">{{since .Created.Unix}}</span> | |
| 145 | 145 | {{if .Anchor}}<span class="muted">on <code>{{.Anchor.Path}}:{{.Anchor.Line}}</code> ({{.Anchor.Side}} side){{if not .Current}} · <span class="conflict">the file has changed since this comment</span>{{end}}</span>{{end}} | |
| 146 | <div class="comment-menu" data-comment-menu> | ||
| 147 | <button type="button" class="comment-menu-toggle" data-comment-menu-toggle aria-haspopup="menu" aria-expanded="false" aria-label="Comment actions">⋯</button> | ||
| 148 | <div class="comment-menu-list" data-comment-menu-list role="menu" hidden> | ||
| 149 | {{if .CanReply}}<button type="button" role="menuitem" data-reply-to="{{.ID}}" data-reply-author="{{who .Author}}">Reply</button>{{end}} | ||
| 150 | <button type="button" role="menuitem" data-copy="{{.Source}}">Copy text</button> | ||
| 151 | <button type="button" role="menuitem" data-copy-link="{{.ID}}">Copy link</button> | ||
| 152 | {{if .CanEdit}}<button type="button" role="menuitem" data-comment-edit>Edit</button>{{end}} | ||
| 153 | {{if .CanDelete}} | ||
| 154 | <form method="post" action="{{.DeleteURL}}" data-confirm="Delete this comment?"> | ||
| 155 | <input type="hidden" name="csrf" value="{{.CSRF}}"> | ||
| 156 | <button type="submit" role="menuitem" class="menu-danger">Delete</button> | ||
| 157 | </form> | ||
| 158 | {{end}} | ||
| 159 | </div> | ||
| 160 | </div> | ||
| 146 | 161 | </header> | |
| 147 | 162 | <div class="comment-body md">{{.Body}}</div> | |
| 163 | {{if .CanEdit}} | ||
| 164 | <form class="comment-edit-form" data-comment-edit-form hidden method="post" action="{{.EditURL}}" hx-post="{{.EditURL}}" hx-target="#issue-thread" hx-swap="outerHTML" hx-disabled-elt="find button[type='submit']"> | ||
| 165 | <input type="hidden" name="csrf" value="{{.CSRF}}"> | ||
| 166 | <textarea name="body" required>{{.Source}}</textarea> | ||
| 167 | <div class="form-actions"> | ||
| 168 | <button class="secondary" type="button" data-comment-edit-cancel>Cancel</button> | ||
| 169 | <button type="submit">Save</button> | ||
| 170 | </div> | ||
| 171 | </form> | ||
| 172 | {{end}} | ||
| 173 | {{end}} | ||
| 148 | 174 | </article> | |
| 149 | {{else}} | ||
| 150 | <p class="muted">No comments yet. From a clone of this repository: | ||
| 151 | <code>vole issue comment {{.Issue.ID}} -m "..."</code></p> | ||
| 152 | 175 | {{end}} | |
| 153 | 176 | ||
| 154 | {{if .CanDiscuss}} | ||
| 155 | <form method="post" action="/r/{{.Name}}/issues/{{.Issue.ID}}/comments" class="form-block" hx-post="/r/{{.Name}}/issues/{{.Issue.ID}}/comments" hx-target="#issue-thread" hx-swap="outerHTML" hx-disabled-elt="find button[type='submit']"> | ||
| 156 | <input type="hidden" name="csrf" value="{{.CSRF}}"> | ||
| 157 | <div class="card form-card"> | ||
| 158 | <label>Comment <textarea name="body" required placeholder="Leave a comment. Markdown is welcome."></textarea></label> | ||
| 159 | </div> | ||
| 160 | <div class="form-actions"><button type="submit">{{icon "message-square"}}Comment</button></div> | ||
| 161 | </form> | ||
| 162 | {{end}} | ||
| 177 | {{define "issue_thread"}} | ||
| 178 | <div id="issue-thread"> | ||
| 179 | <h2>{{icon "message-square"}}Comments <span class="muted">({{len .Issue.Comments}})</span></h2> | ||
| 180 | {{if .ThreadError}}<p class="error">{{.ThreadError}}</p>{{end}} | ||
| 181 | {{range .Comments}} | ||
| 182 | {{template "issue_comment_card" .}} | ||
| 183 | {{if .Replies}} | ||
| 184 | <details class="thread-replies" open> | ||
| 185 | <summary><span class="thread-chevron">{{icon "chevron-right"}}</span>{{len .Replies}} {{if eq (len .Replies) 1}}reply{{else}}replies{{end}}</summary> | ||
| 186 | <div class="comment-replies">{{range .Replies}}{{template "issue_comment_card" .}}{{end}}</div> | ||
| 187 | </details> | ||
| 188 | {{end}} | ||
| 189 | {{else}} | ||
| 190 | <p class="muted">No comments yet. From a clone of this repository: | ||
| 191 | <code>vole issue comment {{.Issue.ID}} -m "..."</code></p> | ||
| 192 | {{end}} | ||
| 163 | 193 | ||
| 194 | {{if .CanDiscuss}} | ||
| 195 | <form id="issue-comment-form" method="post" action="/r/{{.Name}}/issues/{{.Issue.ID}}/comments" class="form-block discussion-composer" hx-post="/r/{{.Name}}/issues/{{.Issue.ID}}/comments" hx-target="#issue-thread" hx-swap="outerHTML" hx-disabled-elt="find button[type='submit']"> | ||
| 196 | <input type="hidden" name="csrf" value="{{.CSRF}}"> | ||
| 197 | <input type="hidden" name="reply_to" value="" data-reply-input> | ||
| 198 | <div class="reply-target" data-reply-target hidden> | ||
| 199 | <span>Replying to <strong data-reply-name></strong></span> | ||
| 200 | <button class="link" type="button" data-reply-clear>Cancel reply</button> | ||
| 201 | </div> | ||
| 202 | <textarea name="body" required placeholder="Leave a comment. Markdown is welcome." aria-label="Comment"></textarea> | ||
| 203 | <div class="form-actions"><button type="submit">{{icon "message-square"}}Comment</button></div> | ||
| 204 | </form> | ||
| 205 | {{end}} | ||
| 164 | 206 | </div> | |
| 165 | 207 | {{end}} | |
| 166 | 208 | ||
| @@ -182,7 +224,7 @@ | |||
| 182 | 224 | <form method="post" action="/r/{{.Name}}/issues/{{.Issue.ID}}/state"> | |
| 183 | 225 | <input type="hidden" name="csrf" value="{{.CSRF}}"> | |
| 184 | 226 | <input type="hidden" name="state" value="{{if eq .Issue.State "open"}}closed{{else}}open{{end}}"> | |
| 185 | <button class="secondary" type="submit">{{if eq .Issue.State "open"}}{{icon "circle-x"}}Close issue{{else}}{{icon "rotate-ccw"}}Reopen issue{{end}}</button> | ||
| 227 | <button class="{{if eq .Issue.State "open"}}danger{{else}}secondary{{end}}" type="submit">{{if eq .Issue.State "open"}}{{icon "circle-x"}}Close{{else}}{{icon "rotate-ccw"}}Reopen{{end}}</button> | ||
| 186 | 228 | </form> | |
| 187 | 229 | </div> | |
| 188 | 230 | {{end}} | |