Skip to content

Commit 200cd0c

Browse files
Merge pull request #53 from Evolvus/codex/add-expandable-view-for-issue-card
feat: expand issue card with full history
2 parents 76b9e2b + 9d205d7 commit 200cd0c

6 files changed

Lines changed: 135 additions & 6 deletions

File tree

src/App.jsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,7 @@ export default function App() {
342342
tagOptions={tagOptions}
343343
milestoneOptions={milestoneOptions}
344344
issueTypeOptions={issueTypeOptions}
345+
token={token}
345346
/>
346347
} />
347348
<Route path="*" element={<Navigate to="/" replace />} />

src/api/github.js

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,3 +194,65 @@ export async function fetchIssueTypes(token, org) {
194194
return data?.issue_types || data;
195195
}
196196

197+
export const ISSUE_WITH_TIMELINE = `
198+
query IssueWithTimeline($owner: String!, $name: String!, $number: Int!, $after: String) {
199+
repository(owner: $owner, name: $name) {
200+
issue(number: $number) {
201+
id
202+
number
203+
title
204+
body
205+
url
206+
state
207+
createdAt
208+
closedAt
209+
repository { nameWithOwner url }
210+
assignees(first: 10) { nodes { login avatarUrl url } }
211+
labels(first: 20) { nodes { id name color } }
212+
milestone { id title url dueOn description }
213+
issueType { id name color }
214+
timelineItems(
215+
first: 50,
216+
after: $after,
217+
itemTypes: [ASSIGNED_EVENT, UNASSIGNED_EVENT, CLOSED_EVENT, REOPENED_EVENT, LABELED_EVENT, UNLABELED_EVENT, ISSUE_COMMENT]
218+
) {
219+
nodes {
220+
__typename
221+
... on ClosedEvent { createdAt actor { login avatarUrl url } }
222+
... on ReopenedEvent { createdAt actor { login avatarUrl url } }
223+
... on LabeledEvent { createdAt actor { login avatarUrl url } label { name color } }
224+
... on UnlabeledEvent { createdAt actor { login avatarUrl url } label { name color } }
225+
... on AssignedEvent { createdAt actor { login avatarUrl url } assignee { ... on User { login avatarUrl url } } }
226+
... on UnassignedEvent { createdAt actor { login avatarUrl url } assignee { ... on User { login avatarUrl url } } }
227+
... on IssueComment { id createdAt author { login avatarUrl url } body }
228+
}
229+
pageInfo { hasNextPage endCursor }
230+
}
231+
}
232+
}
233+
}
234+
`;
235+
236+
export async function fetchIssueWithTimeline(token, owner, repo, number) {
237+
let after = null;
238+
let issue = null;
239+
let timeline = [];
240+
while (true) {
241+
const data = await githubGraphQL(token, ISSUE_WITH_TIMELINE, { owner, name: repo, number, after });
242+
const node = data?.repository?.issue;
243+
if (!node) break;
244+
if (!issue) {
245+
issue = { ...node };
246+
delete issue.timelineItems;
247+
}
248+
const nodes = node.timelineItems?.nodes || [];
249+
timeline = timeline.concat(nodes);
250+
if (!node.timelineItems?.pageInfo?.hasNextPage) break;
251+
after = node.timelineItems.pageInfo.endCursor;
252+
}
253+
if (issue) {
254+
issue.timelineItems = timeline;
255+
}
256+
return issue;
257+
}
258+

src/components/AllIssues.jsx

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { Input } from "./ui/input";
55
import { Download, Search } from "lucide-react";
66
import IssueCard, { ExpandedIssueCard } from "./IssueCard";
77
import useAppStore from "../store";
8+
import { fetchIssueWithTimeline } from "../api/github";
89

910
function issuesToCSV(issues) {
1011
const headers = ["Number", "Title", "URL", "State", "Repository", "ProjectStatus", "CreatedAt", "ClosedAt"];
@@ -38,7 +39,8 @@ export default function AllIssues({
3839
assigneeOptions,
3940
issueTypeOptions,
4041
tagOptions,
41-
milestoneOptions
42+
milestoneOptions,
43+
token,
4244
}) {
4345
const {
4446
query,
@@ -92,9 +94,16 @@ export default function AllIssues({
9294
[filteredAllIssues, visibleCount]
9395
);
9496

95-
const handleIssueClick = (issue) => {
97+
const handleIssueClick = async (issue) => {
9698
setClickedIssue(issue.id);
97-
setClickedIssueData(issue);
99+
try {
100+
const [owner, repo] = (issue.repository?.nameWithOwner || "").split("/");
101+
const full = await fetchIssueWithTimeline(token, owner, repo, issue.number);
102+
setClickedIssueData(full || issue);
103+
} catch (e) {
104+
console.error(e);
105+
setClickedIssueData(issue);
106+
}
98107
};
99108

100109
const handleClosePopup = () => {

src/components/IssueCard.jsx

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,45 @@ export function ExpandedIssueCard({ issue }) {
5353
const processedBody = convertImgTagsToMarkdown(issue.body || "");
5454
const displayBody =
5555
processedBody.length > 500 ? `${processedBody.substring(0, 500)}...` : processedBody;
56+
57+
const renderTimelineItem = (item, idx) => {
58+
const time = item.createdAt ? new Date(item.createdAt).toLocaleString() : "";
59+
switch (item.__typename) {
60+
case "IssueComment":
61+
return (
62+
<li key={idx}>
63+
<div className="text-gray-700"><span className="font-medium">{item.author?.login}</span> commented {time}</div>
64+
{item.body && <div className="ml-4 text-gray-600">{item.body.length > 200 ? item.body.substring(0,200) + "..." : item.body}</div>}
65+
</li>
66+
);
67+
case "ClosedEvent":
68+
return (
69+
<li key={idx} className="text-gray-700"><span className="font-medium">{item.actor?.login}</span> closed this issue {time}</li>
70+
);
71+
case "ReopenedEvent":
72+
return (
73+
<li key={idx} className="text-gray-700"><span className="font-medium">{item.actor?.login}</span> reopened this issue {time}</li>
74+
);
75+
case "LabeledEvent":
76+
return (
77+
<li key={idx} className="text-gray-700"><span className="font-medium">{item.actor?.login}</span> added label {item.label?.name} {time}</li>
78+
);
79+
case "UnlabeledEvent":
80+
return (
81+
<li key={idx} className="text-gray-700"><span className="font-medium">{item.actor?.login}</span> removed label {item.label?.name} {time}</li>
82+
);
83+
case "AssignedEvent":
84+
return (
85+
<li key={idx} className="text-gray-700"><span className="font-medium">{item.actor?.login}</span> assigned {item.assignee?.login} {time}</li>
86+
);
87+
case "UnassignedEvent":
88+
return (
89+
<li key={idx} className="text-gray-700"><span className="font-medium">{item.actor?.login}</span> unassigned {item.assignee?.login} {time}</li>
90+
);
91+
default:
92+
return null;
93+
}
94+
};
5695

5796
return (
5897
<div className="bg-white border rounded-lg shadow-xl p-5 max-w-md">
@@ -197,6 +236,14 @@ export function ExpandedIssueCard({ issue }) {
197236
</div>
198237
</div>
199238
)}
239+
{issue.timelineItems && issue.timelineItems.length > 0 && (
240+
<div className="border-t pt-3 mt-3">
241+
<div className="text-sm text-gray-500 font-medium mb-2">History:</div>
242+
<ul className="space-y-2 text-sm max-h-40 overflow-y-auto">
243+
{issue.timelineItems.map((t, i) => renderTimelineItem(t, i))}
244+
</ul>
245+
</div>
246+
)}
200247
</div>
201248
);
202249
}

src/components/SprintBoard.jsx

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { Button } from "./ui/button";
44
import { Download, Maximize2, Minimize2 } from "lucide-react";
55
import IssueCard, { ExpandedIssueCard } from "./IssueCard";
66
import jsPDF from "jspdf";
7+
import { fetchIssueWithTimeline } from "../api/github";
78

89
function formatDateForHeader(date) {
910
const d = new Date(date);
@@ -341,15 +342,23 @@ async function downloadReleaseNotes(sp, orgName) {
341342
doc.save(`${sp.title}-release-notes.pdf`);
342343
}
343344

344-
export default function SprintBoard({ sprint, isFullScreen, toggleFullScreen, handleDrop, orgName }) {
345+
346+
export default function SprintBoard({ sprint, isFullScreen, toggleFullScreen, handleDrop, orgName, token }) {
345347
const [clickedIssue, setClickedIssue] = useState(null);
346348
const [clickedIssueData, setClickedIssueData] = useState(null);
347349

348350
if (!sprint) return null;
349351

350-
const handleIssueClick = (issue) => {
352+
const handleIssueClick = async (issue) => {
351353
setClickedIssue(issue.id);
352-
setClickedIssueData(issue);
354+
try {
355+
const [owner, repo] = (issue.repository?.nameWithOwner || "").split("/");
356+
const full = await fetchIssueWithTimeline(token, owner, repo, issue.number);
357+
setClickedIssueData(full || issue);
358+
} catch (e) {
359+
console.error(e);
360+
setClickedIssueData(issue);
361+
}
353362
};
354363

355364
const handleClosePopup = () => {

src/components/Sprints.jsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ export default function Sprints({ allIssues, orgMeta, projects, token }) {
172172
toggleFullScreen={toggleFullScreen}
173173
handleDrop={handleDrop}
174174
orgName={orgMeta?.name}
175+
token={token}
175176
/>
176177
)}
177178
</div>

0 commit comments

Comments
 (0)