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.

Build out the forge web UI

f09ef39efe44de693748053a7b596ef328fcf06b · Keenan Nemetz · 15d ago · parent 70b70264

Build out the forge web UI

Turn the web surface into a compact, server-rendered forge with HTMX as the
progressive dynamic layer.

- Split templates and assets into embedded files; add a tokenised dark theme,
  Lucide icons, and vendored HTMX.
- Add the repository Overview page and a separate chronological Activity feed
  with type filters and HTMX fragment updates.
- Finish the issue and change workflows: create pages, comments, close/reopen,
  labels and assignees, approval, discussion, line-anchored review comments,
  and HTMX list filters.
- Add syntax-highlighted blobs, contributor/statistics derivation, branch-aware
  code browsing, and a repository Runs list.
- Add a loopback-only development login for local write testing, with explicit
  configuration and listener checks.
- Extend tests for the new pages, fragments, labels, assignees, anchors, and
  development login.

server/web/templates/issues.html +247

@@ -0,0 +1,247 @@
1 {{define "issues"}}
2 {{template "header" .}}
3 {{template "repohead" (nav .Name "issues")}}
4
5 <div class="page-head">
6 <div>
7 <h1>Issues</h1>
8 <p class="page-sub">{{.Open}} open · {{.Closed}} closed · {{.Total}} total</p>
9 </div>
10 {{if .CanDiscuss}}<a class="button" href="/r/{{.Name}}/issues/new">{{icon "plus"}}New issue</a>{{end}}
11 </div>
12
13 {{template "issue_list" .}}
14 {{template "footer" .}}
15 {{end}}
16
17 {{define "issue_list"}}
18 <div id="issue-list">
19 <div class="chips" hx-boost="true" hx-target="#issue-list" hx-swap="outerHTML" hx-push-url="true">
20 <a class="chip{{if eq .State "open"}} active{{end}}" href="/r/{{.Name}}/issues?state=open{{if .Filter.Query}}&amp;q={{urlquery .Filter.Query}}{{end}}{{if .Filter.Label}}&amp;label={{urlquery .Filter.Label}}{{end}}{{if .Filter.Assignee}}&amp;assignee={{urlquery .Filter.Assignee}}{{end}}">Open <span class="chip-count">{{.Open}}</span></a>
21 <a class="chip{{if eq .State "closed"}} active{{end}}" href="/r/{{.Name}}/issues?state=closed{{if .Filter.Query}}&amp;q={{urlquery .Filter.Query}}{{end}}{{if .Filter.Label}}&amp;label={{urlquery .Filter.Label}}{{end}}{{if .Filter.Assignee}}&amp;assignee={{urlquery .Filter.Assignee}}{{end}}">Closed <span class="chip-count">{{.Closed}}</span></a>
22 <a class="chip{{if eq .State "all"}} active{{end}}" href="/r/{{.Name}}/issues?state=all{{if .Filter.Query}}&amp;q={{urlquery .Filter.Query}}{{end}}{{if .Filter.Label}}&amp;label={{urlquery .Filter.Label}}{{end}}{{if .Filter.Assignee}}&amp;assignee={{urlquery .Filter.Assignee}}{{end}}">All <span class="chip-count">{{.Total}}</span></a>
23 </div>
24
25 <form method="get" action="/r/{{.Name}}/issues" class="filters" hx-get="/r/{{.Name}}/issues" hx-target="#issue-list" hx-swap="outerHTML" hx-push-url="true">
26 <input type="hidden" name="state" value="{{.State}}">
27 <input name="q" type="text" placeholder="Search title and body" value="{{.Filter.Query}}">
28 {{if .Labels}}
29 <span class="filter-label">Labels:</span>
30 <select name="label" aria-label="Label">
31 <option value="">all labels</option>
32 {{range .Labels}}<option value="{{.}}"{{if eq . $.Filter.Label}} selected{{end}}>{{.}}</option>{{end}}
33 </select>
34 {{end}}
35 {{if .Assignees}}
36 <span class="filter-label">Assignees:</span>
37 <select name="assignee" aria-label="Assignee">
38 <option value="">all assignees</option>
39 {{range .Assignees}}<option value="{{.}}"{{if eq . $.Filter.Assignee}} selected{{end}}>@{{.}}</option>{{end}}
40 </select>
41 {{end}}
42 <button type="submit">{{icon "sliders-horizontal"}}Filter</button>
43 {{if .Filtered}}<a href="/r/{{.Name}}/issues" hx-get="/r/{{.Name}}/issues" hx-target="#issue-list" hx-swap="outerHTML" hx-push-url="true">clear</a>{{end}}
44 </form>
45
46 {{if .Issues}}
47 <div class="panel">
48 <table class="issue-list">
49 <thead>
50 <tr><th>Status</th><th>Issue</th><th>Author</th><th>Replies</th><th>Age</th></tr>
51 </thead>
52 <tbody>
53 {{range .Issues}}
54 <tr>
55 <td class="fit"><span class="state {{.State}}">{{.State}}</span></td>
56 <td>
57 <a class="row-title" href="/r/{{$.Name}}/issues/{{.ID}}">{{.Title}}</a>
58 <div class="row-meta">
59 <span class="sha">#{{short .ID}}</span>
60 {{range .Labels}}<span class="label">{{.}}</span>{{end}}
61 {{range .Assignees}}<a class="label" href="/r/{{$.Name}}/issues?assignee={{urlquery .}}">@{{.}}</a>{{end}}
62 </div>
63 </td>
64 <td class="muted nowrap">{{who .Author}}</td>
65 <td class="muted">{{.Comments}}</td>
66 <td class="muted nowrap">{{since .Created.Unix}}</td>
67 </tr>
68 {{end}}
69 </tbody>
70 </table>
71 </div>
72 {{else if and (eq .State "open") .Closed}}
73 <div class="empty-state">
74 <p>No open issues. {{.Closed}} closed.</p>
75 <a href="/r/{{.Name}}/issues?state=closed" hx-get="/r/{{.Name}}/issues?state=closed" hx-target="#issue-list" hx-swap="outerHTML" hx-push-url="true">View closed issues</a>
76 </div>
77 {{else if .Filtered}}
78 <div class="empty-state">
79 <p>No issues match this filter.</p>
80 <a href="/r/{{.Name}}/issues" hx-get="/r/{{.Name}}/issues" hx-target="#issue-list" hx-swap="outerHTML" hx-push-url="true">Clear filters</a>
81 </div>
82 {{else}}
83 <div class="empty-state">
84 <p>No issues yet.</p>
85 <p class="muted">From a clone of this repository:</p>
86 <pre>vole issue new --title "..." -m "..."</pre>
87 <p class="muted">An issue is also just a file on the <code>vole</code> branch:
88 <code>issues/&lt;ulid&gt;/issue.md</code> — frontmatter with the title, state and labels,
89 then the body as markdown.</p>
90 </div>
91 {{end}}
92 </div>
93 {{end}}
94
95 {{define "issue_state"}}
96 <section id="issue-state">
97 <h2>{{icon "circle-dot"}}State</h2>
98 {{if .Error}}<p class="error">{{.Error}}</p>{{end}}
99 <p><span class="state {{.Issue.State}}">{{.Issue.State}}</span></p>
100 {{if .CanClose}}
101 <form method="post" action="/r/{{.Name}}/issues/{{.Issue.ID}}/state" hx-post="/r/{{.Name}}/issues/{{.Issue.ID}}/state" hx-target="#issue-state" hx-swap="outerHTML" hx-disabled-elt="find button[type='submit']">
102 <input type="hidden" name="csrf" value="{{.CSRF}}">
103 <input type="hidden" name="state" value="{{if eq .Issue.State "open"}}closed{{else}}open{{end}}">
104 <button class="secondary" type="submit">{{if eq .Issue.State "open"}}{{icon "circle-x"}}Close issue{{else}}{{icon "rotate-ccw"}}Reopen issue{{end}}</button>
105 </form>
106 {{end}}
107 </section>
108 {{end}}
109
110 {{define "issue_labels"}}
111 <section id="issue-labels">
112 <h2>{{icon "tag"}}Labels</h2>
113 {{if .Error}}<p class="error">{{.Error}}</p>{{end}}
114 {{if .CanEditLabels}}
115 <form method="post" action="/r/{{.Name}}/issues/{{.Issue.ID}}/labels" class="label-form" hx-post="/r/{{.Name}}/issues/{{.Issue.ID}}/labels" hx-target="#issue-labels" hx-swap="outerHTML" hx-trigger="token-change" hx-sync="this:replace">
116 <input type="hidden" name="csrf" value="{{.CSRF}}">
117 {{template "token_input" (tokenPicker .Issue.Labels .AllLabels "labels" "label" "Add labels…")}}
118 <noscript><div class="form-actions"><button class="secondary" type="submit">{{icon "check"}}Save labels</button></div></noscript>
119 </form>
120 {{else}}
121 {{if .Issue.Labels}}
122 <div class="label-row">{{range .Issue.Labels}}<span class="label">{{.}}</span>{{end}}</div>
123 {{else}}
124 <p class="muted">No labels.</p>
125 {{end}}
126 {{end}}
127 </section>
128 {{end}}
129
130 {{define "issue_assignees"}}
131 <section id="issue-assignees">
132 <h2>{{icon "users"}}Assignees</h2>
133 {{if .Error}}<p class="error">{{.Error}}</p>{{end}}
134 {{if .CanEditAssignees}}
135 <form method="post" action="/r/{{.Name}}/issues/{{.Issue.ID}}/assignees" class="label-form" hx-post="/r/{{.Name}}/issues/{{.Issue.ID}}/assignees" hx-target="#issue-assignees" hx-swap="outerHTML" hx-trigger="token-change" hx-sync="this:replace">
136 <input type="hidden" name="csrf" value="{{.CSRF}}">
137 {{template "token_input" (tokenPicker .Issue.Assignees .Assignable "assignees" "assignee" "Add assignees…")}}
138 <noscript><div class="form-actions"><button class="secondary" type="submit">{{icon "check"}}Save assignees</button></div></noscript>
139 </form>
140 {{else}}
141 {{if .Issue.Assignees}}
142 <div class="label-row">{{range .Issue.Assignees}}<a class="label" href="/r/{{$.Name}}/issues?assignee={{urlquery .}}">@{{.}}</a>{{end}}</div>
143 {{else}}
144 <p class="muted">No assignees.</p>
145 {{end}}
146 {{end}}
147 </section>
148 {{end}}
149
150 {{define "issue_thread"}}
151 <div id="issue-thread">
152 {{if .ThreadError}}<p class="error">{{.ThreadError}}</p>{{end}}
153 <h2>{{icon "message-square"}}Comments <span class="muted">({{len .Comments}})</span></h2>
154 {{range .Comments}}
155 <article class="comment" id="c-{{.ID}}">
156 <header class="comment-head">
157 <span class="avatar c{{tone .Author}}" aria-hidden="true">{{initial (who .Author)}}</span>
158 <span class="comment-author">{{who .Author}}</span>
159 <span class="muted">{{since .Created.Unix}}</span>
160 {{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}}
161 </header>
162 <div class="comment-body md">{{.Body}}</div>
163 </article>
164 {{else}}
165 <p class="muted">No comments yet. From a clone of this repository:
166 <code>vole issue comment {{.Issue.ID}} -m "..."</code></p>
167 {{end}}
168
169 {{if .CanDiscuss}}
170 <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']">
171 <input type="hidden" name="csrf" value="{{.CSRF}}">
172 <div class="card form-card">
173 <label>Comment <textarea name="body" required placeholder="Leave a comment. Markdown is welcome."></textarea></label>
174 </div>
175 <div class="form-actions"><button type="submit">{{icon "message-square"}}Comment</button></div>
176 </form>
177 {{end}}
178
179 </div>
180 {{end}}
181
182 {{define "issue"}}
183 {{template "header" .}}
184 {{template "repohead" (nav .Name "issues")}}
185
186 <div class="page-head">
187 <div>
188 <h1>{{.Issue.Title}}</h1>
189 <p class="page-sub">
190 <span class="state {{.Issue.State}}">{{.Issue.State}}</span>
191 · <span class="sha">#{{short .Issue.ID}}</span>
192 · opened by {{who .Issue.Author}} {{since .Issue.Created.Unix}}
193 </p>
194 </div>
195 </div>
196
197 <div class="thread">
198 <div class="thread-main">
199 <article class="comment">
200 <header class="comment-head">
201 <span class="avatar c{{tone .Issue.Author}}" aria-hidden="true">{{initial (who .Issue.Author)}}</span>
202 <span class="comment-author">{{who .Issue.Author}}</span>
203 <span class="muted">opened this {{since .Issue.Created.Unix}}</span>
204 </header>
205 <div class="comment-body md">
206 {{if .Description}}{{.Description}}{{else}}<p class="muted">No description.</p>{{end}}
207 </div>
208 </article>
209
210 {{template "issue_thread" .}}
211
212 {{if .Backlinks}}
213 <h2>{{icon "git-branch"}}Referenced in commits</h2>
214 <div class="panel">
215 <table>
216 <tbody>
217 {{range .Backlinks}}
218 <tr>
219 <td class="sha"><a href="/r/{{$.Name}}/commit/{{.SHA}}">{{short .SHA}}</a></td>
220 <td><a href="/r/{{$.Name}}/commit/{{.SHA}}">{{.Subject}}</a></td>
221 <td class="muted">{{.Author}}</td>
222 <td class="muted nowrap">{{since .When}}</td>
223 </tr>
224 {{end}}
225 </tbody>
226 </table>
227 </div>
228 {{end}}
229 </div>
230
231 <aside class="thread-side">
232 {{template "issue_state" .}}
233 {{template "issue_labels" .}}
234 {{template "issue_assignees" .}}
235
236 <section>
237 <h2>{{icon "info"}}Details</h2>
238 <dl class="meta-list">
239 <dt>Author</dt><dd>{{who .Issue.Author}}</dd>
240 <dt>Opened</dt><dd>{{since .Issue.Created.Unix}}</dd>
241 <dt>Reference</dt><dd class="sha">#{{short .Issue.ID}}</dd>
242 </dl>
243 </section>
244 </aside>
245 </div>
246 {{template "footer" .}}
247 {{end}}