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}`} safe>
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 safe>
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 safe
191 >
192 {issue.body}
193 </textarea>
194 </div>
195 <div class="form-actions">
196 <button
197 type="submit"
198 class="btn btn-sm btn-primary"
199 >
200 Save
201 </button>
202 </div>
203 </form>
204 </div>
205 )}
206 <div class="timeline-body markdown-body">
207 {(bodyHtml as "safe") || (
208 <em class="text-muted">
209 No description provided.
210 </em>
211 )}
212 </div>
213 <ReactionBar
214 reactions={reactions}
215 postUrl={`${baseUrl}/react`}
216 user={user}
217 />
218 </div>
219
220 <CommentThread
221 user={user}
222 comments={comments}
223 commentReactions={commentReactions}
224 baseUrl={baseUrl}
225 parentStatus={issue.status}
226 />
227 </div>
228 </div>
229 </Layout>
230 );
231}
232