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/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">&#8943;</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}}