IssueDetail.tsx
Raw
1import config from "../../config.ts";
2import type {
3 IssueCommentRow,
4 IssueRow,
5 LabelRow,
6 RepositoryRow,
7} from "../../db/index.ts";
8import { displayName } from "../../lib/users.ts";
9import type { SessionUser } from "../../middleware/session.ts";
10import { Avatar } from "../Avatar.tsx";
11import { CommentThread } from "../CommentThread.tsx";
12import { DateWithEdited } from "../DateWithEdited.tsx";
13import { EditableTitle } from "../EditableTitle.tsx";
14import { LabelBadges } from "../LabelBadges.tsx";
15import { Layout } from "../layout.tsx";
16import { ReactionBar, type ReactionCount } from "../ReactionBar.tsx";
17import { RepoHeader } from "../repos/RepoHeader.tsx";
18import { RepoNav } from "../repos/RepoNav.tsx";
19
20interface IssueDetailProps {
21 user: SessionUser | null;
22 repo: RepositoryRow;
23 issue: IssueRow & {
24 author_username: string;
25 author_avatar_version: number | null;
26 };
27 bodyHtml: string;
28 comments: (IssueCommentRow & {
29 author_username: string;
30 author_avatar_version: number | null;
31 bodyHtml: string;
32 })[];
33 reactions: ReactionCount[];
34 commentReactions: Map<number, ReactionCount[]>;
35 issueLabels: LabelRow[];
36 repoLabels: LabelRow[];
37}
38
39export function IssueDetail({
40 user,
41 repo,
42 issue,
43 bodyHtml,
44 comments,
45 reactions,
46 commentReactions,
47 issueLabels,
48 repoLabels,
49}: IssueDetailProps) {
50 const baseUrl = `/${repo.name}/issues/${issue.number}`;
51 const canEditIssue =
52 user != null &&
53 (user.isAdmin ||
54 (user.id === issue.author_id && issue.status === "open"));
55 const canManageLabels =
56 user != null &&
57 (user.isAdmin ||
58 (repo.allow_user_labels === 1 && user.id === issue.author_id));
59 return (
60 <Layout user={user} title={`${issue.title} — ${repo.name}`}>
61 <div class="container">
62 <RepoHeader repo={repo} />
63 <RepoNav repo={repo} active="issues" user={user} />
64 <div class="issue-detail">
65 <div class="issue-detail-header">
66 <span class="issue-number">#{issue.number}</span>
67 <EditableTitle
68 title={issue.title}
69 canEdit={canEditIssue}
70 editAction={`${baseUrl}/edit`}
71 bodyFieldName="edit_body"
72 bodyValue={issue.body}
73 />
74 <span class={`issue-badge ${issue.status}`}>
75 {issue.status}
76 </span>
77 {canEditIssue && (
78 <div class="issue-detail-meta-actions">
79 {user?.isAdmin && issue.status === "open" && (
80 <form
81 method="POST"
82 action={`${baseUrl}/complete`}
83 class="inline-form"
84 >
85 <button
86 type="submit"
87 class="btn btn-sm btn-primary"
88 >
89 Completed
90 </button>
91 </form>
92 )}
93 {user?.isAdmin && (
94 <form
95 method="POST"
96 action={`${baseUrl}/close`}
97 class="inline-form"
98 >
99 <button
100 type="submit"
101 class="btn btn-sm"
102 >
103 {issue.status === "open"
104 ? "Close issue"
105 : "Reopen issue"}
106 </button>
107 </form>
108 )}
109 <details class="confirm-details">
110 <summary class="btn btn-sm btn-danger">
111 Delete
112 </summary>
113 <div class="confirm-popup">
114 Permanently delete this issue?
115 <form
116 method="POST"
117 action={`${baseUrl}/delete`}
118 class="inline-form"
119 >
120 <button
121 type="submit"
122 class="btn btn-sm btn-danger"
123 >
124 Yes, delete
125 </button>
126 </form>
127 </div>
128 </details>
129 </div>
130 )}
131 </div>
132
133 <LabelBadges
134 labels={issueLabels}
135 repoLabels={repoLabels}
136 canManage={canManageLabels}
137 baseUrl={baseUrl}
138 />
139
140 <div class="timeline-item">
141 <div class="timeline-author">
142 <Avatar
143 userId={issue.author_id}
144 version={issue.author_avatar_version}
145 size={24}
146 />
147 <strong>
148 {displayName(issue.author_username)}
149 </strong>
150 <div class="timeline-author-right">
151 {canEditIssue && (
152 <details class="inline-edit-details">
153 <summary class="btn btn-xs">
154 <span class="when-closed">
155 Edit
156 </span>
157 <span class="when-open">
158 Stop editing
159 </span>
160 </summary>
161 </details>
162 )}
163 <DateWithEdited
164 date={issue.created_at}
165 editedAt={issue.edited_at}
166 />
167 </div>
168 </div>
169 {canEditIssue && (
170 <div class="inline-edit-form-area">
171 <form
172 method="POST"
173 action={`${baseUrl}/edit`}
174 class="inline-edit-form"
175 >
176 <input
177 type="hidden"
178 name="title"
179 value={issue.title}
180 />
181 <div class="form-group">
182 <textarea
183 class="form-input"
184 id="edit-issue-body"
185 name="edit_body"
186 rows="6"
187 maxlength={
188 config.MAX_TEXT_BODY_BYTES
189 }
190 >
191 {issue.body}
192 </textarea>
193 </div>
194 <div class="form-actions">
195 <button
196 type="submit"
197 class="btn btn-sm btn-primary"
198 >
199 Save
200 </button>
201 </div>
202 </form>
203 </div>
204 )}
205 <div class="timeline-body markdown-body">
206 {bodyHtml || (
207 <em class="text-muted">
208 No description provided.
209 </em>
210 )}
211 </div>
212 <ReactionBar
213 reactions={reactions}
214 postUrl={`${baseUrl}/react`}
215 user={user}
216 />
217 </div>
218
219 <CommentThread
220 user={user}
221 comments={comments}
222 commentReactions={commentReactions}
223 baseUrl={baseUrl}
224 parentStatus={issue.status}
225 />
226 </div>
227 </div>
228 </Layout>
229 );
230}
231