Skip to content

Commit 674e173

Browse files
all issues changes
1 parent ca1ff7f commit 674e173

4 files changed

Lines changed: 147 additions & 25 deletions

File tree

src/api/github.js

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -251,9 +251,8 @@ export const ISSUE_WITH_TIMELINE = `
251251
milestone { id title url dueOn description }
252252
issueType { id name color }
253253
timelineItems(
254-
first: 50,
255-
after: $after,
256-
itemTypes: [ASSIGNED_EVENT, UNASSIGNED_EVENT, CLOSED_EVENT, REOPENED_EVENT, LABELED_EVENT, UNLABELED_EVENT, ISSUE_COMMENT]
254+
first: 100,
255+
after: $after
257256
) {
258257
nodes {
259258
__typename
@@ -264,6 +263,23 @@ export const ISSUE_WITH_TIMELINE = `
264263
... on AssignedEvent { createdAt actor { login avatarUrl url } assignee { ... on User { login avatarUrl url } } }
265264
... on UnassignedEvent { createdAt actor { login avatarUrl url } assignee { ... on User { login avatarUrl url } } }
266265
... on IssueComment { id createdAt author { login avatarUrl url } body }
266+
... on CrossReferencedEvent {
267+
createdAt
268+
actor { login avatarUrl url }
269+
willCloseTarget
270+
source {
271+
__typename
272+
... on PullRequest {
273+
number
274+
title
275+
url
276+
merged
277+
mergedAt
278+
author { login avatarUrl url }
279+
repository { nameWithOwner }
280+
}
281+
}
282+
}
267283
}
268284
pageInfo { hasNextPage endCursor }
269285
}

src/components/AllIssues.jsx

Lines changed: 56 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { Card, CardContent } from "./ui/card";
33
import { Button } from "./ui/button";
44
import { Input } from "./ui/input";
55
import MultiSelect from "./ui/MultiSelect";
6-
import { Download, Search } from "lucide-react";
6+
import { Download, Search, X } from "lucide-react";
77
import { downloadIssuesExcel } from "../utils/exportExcel";
88
import IssueCard, { IssueOverlayCard } from "./IssueCard";
99
import useAppStore from "../store";
@@ -97,6 +97,13 @@ export default function AllIssues({
9797
(filterTag?.length || 0) +
9898
(filterMilestone?.length || 0) > 0;
9999

100+
const removeState = (v) => setFilterState(filterState.filter(x => x !== v));
101+
const removeProjectStatus = (v) => setFilterProjectStatus(filterProjectStatus.filter(x => x !== v));
102+
const removeAssignee = (v) => setFilterAssignee(filterAssignee.filter(x => x !== v));
103+
const removeIssueType = (v) => setFilterIssueType(filterIssueType.filter(x => x !== v));
104+
const removeTag = (v) => setFilterTag(filterTag.filter(x => x !== v));
105+
const removeMilestone = (v) => setFilterMilestone(filterMilestone.filter(x => x !== v));
106+
100107
return (
101108
<div className="space-y-6">
102109
<div className="mb-1 flex flex-wrap items-center gap-2">
@@ -116,12 +123,54 @@ export default function AllIssues({
116123
</div>
117124
{hasAnyFilter ? (
118125
<div className="mb-3 flex flex-wrap gap-2 text-xs">
119-
{filterState.map(v => <span key={"st-"+v} className="px-2 py-1 rounded-full bg-blue-50 text-blue-700 border border-blue-200">State: {v}</span>)}
120-
{filterProjectStatus.map(v => <span key={"ps-"+v} className="px-2 py-1 rounded-full bg-purple-50 text-purple-700 border border-purple-200">Status: {v}</span>)}
121-
{filterAssignee.map(v => <span key={"as-"+v} className="px-2 py-1 rounded-full bg-emerald-50 text-emerald-700 border border-emerald-200">Assignee: {v}</span>)}
122-
{filterIssueType.map(v => <span key={"it-"+v} className="px-2 py-1 rounded-full bg-amber-50 text-amber-700 border border-amber-200">Type: {v}</span>)}
123-
{filterTag.map(v => <span key={"tg-"+v} className="px-2 py-1 rounded-full bg-gray-100 text-gray-700 border">Tag: {v}</span>)}
124-
{filterMilestone.map(v => <span key={"ms-"+v} className="px-2 py-1 rounded-full bg-pink-50 text-pink-700 border border-pink-200">Milestone: {v}</span>)}
126+
{filterState.map(v => (
127+
<div key={"st-"+v} className="inline-flex items-center gap-1 px-2 py-1 rounded-full bg-blue-50 text-blue-700 border border-blue-200">
128+
<span>State: {v}</span>
129+
<button aria-label={`Remove state ${v}`} className="hover:bg-blue-100 rounded-full p-0.5" onClick={() => removeState(v)}>
130+
<X className="w-3 h-3"/>
131+
</button>
132+
</div>
133+
))}
134+
{filterProjectStatus.map(v => (
135+
<div key={"ps-"+v} className="inline-flex items-center gap-1 px-2 py-1 rounded-full bg-purple-50 text-purple-700 border border-purple-200">
136+
<span>Status: {v}</span>
137+
<button aria-label={`Remove status ${v}`} className="hover:bg-purple-100 rounded-full p-0.5" onClick={() => removeProjectStatus(v)}>
138+
<X className="w-3 h-3"/>
139+
</button>
140+
</div>
141+
))}
142+
{filterAssignee.map(v => (
143+
<div key={"as-"+v} className="inline-flex items-center gap-1 px-2 py-1 rounded-full bg-emerald-50 text-emerald-700 border border-emerald-200">
144+
<span>Assignee: {v}</span>
145+
<button aria-label={`Remove assignee ${v}`} className="hover:bg-emerald-100 rounded-full p-0.5" onClick={() => removeAssignee(v)}>
146+
<X className="w-3 h-3"/>
147+
</button>
148+
</div>
149+
))}
150+
{filterIssueType.map(v => (
151+
<div key={"it-"+v} className="inline-flex items-center gap-1 px-2 py-1 rounded-full bg-amber-50 text-amber-700 border border-amber-200">
152+
<span>Type: {v}</span>
153+
<button aria-label={`Remove type ${v}`} className="hover:bg-amber-100 rounded-full p-0.5" onClick={() => removeIssueType(v)}>
154+
<X className="w-3 h-3"/>
155+
</button>
156+
</div>
157+
))}
158+
{filterTag.map(v => (
159+
<div key={"tg-"+v} className="inline-flex items-center gap-1 px-2 py-1 rounded-full bg-gray-100 text-gray-700 border">
160+
<span>Tag: {v}</span>
161+
<button aria-label={`Remove tag ${v}`} className="hover:bg-gray-200 rounded-full p-0.5" onClick={() => removeTag(v)}>
162+
<X className="w-3 h-3"/>
163+
</button>
164+
</div>
165+
))}
166+
{filterMilestone.map(v => (
167+
<div key={"ms-"+v} className="inline-flex items-center gap-1 px-2 py-1 rounded-full bg-pink-50 text-pink-700 border border-pink-200">
168+
<span>Milestone: {v}</span>
169+
<button aria-label={`Remove milestone ${v}`} className="hover:bg-pink-100 rounded-full p-0.5" onClick={() => removeMilestone(v)}>
170+
<X className="w-3 h-3"/>
171+
</button>
172+
</div>
173+
))}
125174
<button className="ml-2 underline text-gray-500" onClick={() => { setFilterState([]); setFilterProjectStatus([]); setFilterAssignee([]); setFilterIssueType([]); setFilterTag([]); setFilterMilestone([]); }}>Clear all</button>
126175
</div>
127176
) : null}

src/components/IssueCard.jsx

Lines changed: 65 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,6 @@ export function ExpandedIssueCard({ issue }) {
5656
const displayBody = processedBody;
5757

5858
const renderTimelineItem = (item, idx) => {
59-
const time = item.createdAt ? new Date(item.createdAt).toLocaleString() : "";
6059
switch (item.__typename) {
6160
case "IssueComment":
6261
return (
@@ -68,7 +67,7 @@ export function ExpandedIssueCard({ issue }) {
6867
<AvatarFallback className="text-[10px]">{initials(item.author?.login)}</AvatarFallback>
6968
</Avatar>
7069
<div className="text-gray-700">
71-
<span className="font-medium">{item.author?.login}</span> commented {time}
70+
<span className="font-medium">{item.author?.login}</span> commented <TimeAgo iso={item.createdAt} />
7271
</div>
7372
</div>
7473
{item.body && <div className="ml-7 text-gray-600">{item.body}</div>}
@@ -83,7 +82,7 @@ export function ExpandedIssueCard({ issue }) {
8382
<AvatarImage src={item.actor?.avatarUrl} />
8483
<AvatarFallback className="text-[10px]">{initials(item.actor?.login)}</AvatarFallback>
8584
</Avatar>
86-
<div><span className="font-medium">{item.actor?.login}</span> closed this issue {time}</div>
85+
<div><span className="font-medium">{item.actor?.login}</span> closed this issue <TimeAgo iso={item.createdAt} /></div>
8786
</div>
8887
</li>
8988
);
@@ -96,7 +95,7 @@ export function ExpandedIssueCard({ issue }) {
9695
<AvatarImage src={item.actor?.avatarUrl} />
9796
<AvatarFallback className="text-[10px]">{initials(item.actor?.login)}</AvatarFallback>
9897
</Avatar>
99-
<div><span className="font-medium">{item.actor?.login}</span> reopened this issue {time}</div>
98+
<div><span className="font-medium">{item.actor?.login}</span> reopened this issue <TimeAgo iso={item.createdAt} /></div>
10099
</div>
101100
</li>
102101
);
@@ -119,7 +118,7 @@ export function ExpandedIssueCard({ issue }) {
119118
{item.label.name}
120119
</span>
121120
)}
122-
<span>{time}</span>
121+
<span><TimeAgo iso={item.createdAt} /></span>
123122
</div>
124123
</div>
125124
</li>
@@ -143,7 +142,7 @@ export function ExpandedIssueCard({ issue }) {
143142
{item.label.name}
144143
</span>
145144
)}
146-
<span>{time}</span>
145+
<span><TimeAgo iso={item.createdAt} /></span>
147146
</div>
148147
</div>
149148
</li>
@@ -169,7 +168,7 @@ export function ExpandedIssueCard({ issue }) {
169168
<span className="font-medium">{item.assignee?.login}</span>
170169
</>
171170
)}
172-
<span>{time}</span>
171+
<span><TimeAgo iso={item.createdAt} /></span>
173172
</div>
174173
</div>
175174
</li>
@@ -195,11 +194,63 @@ export function ExpandedIssueCard({ issue }) {
195194
<span className="font-medium">{item.assignee?.login}</span>
196195
</>
197196
)}
198-
<span>{time}</span>
197+
<span><TimeAgo iso={item.createdAt} /></span>
199198
</div>
200199
</div>
201200
</li>
202201
);
202+
case "CrossReferencedEvent": {
203+
const src = item.source;
204+
if (src?.__typename === 'PullRequest') {
205+
return (
206+
<li key={idx} className="relative pl-6">
207+
<span className="absolute rounded-full bg-blue-500 border-2 border-white" style={{ left: -6, top: 10, width: 12, height: 12 }} />
208+
<div className="flex items-start gap-2 text-gray-700">
209+
<Avatar className="w-5 h-5 mt-0.5">
210+
<AvatarImage src={item.actor?.avatarUrl} />
211+
<AvatarFallback className="text-[10px]">{initials(item.actor?.login)}</AvatarFallback>
212+
</Avatar>
213+
<div className="flex-1">
214+
<div>
215+
<span className="font-medium">{item.actor?.login}</span> linked pull request{' '}
216+
<a className="text-blue-600 underline" href={src.url} target="_blank" rel="noreferrer">
217+
{src.repository?.nameWithOwner}#{src.number}
218+
</a>{' '}{src.title} {src.merged ? <span className="ml-1 text-purple-700">(merged)</span> : null}
219+
</div>
220+
<div className="text-xs text-gray-500"><TimeAgo iso={item.createdAt} /></div>
221+
</div>
222+
</div>
223+
</li>
224+
);
225+
}
226+
return null;
227+
}
228+
case "CrossReferencedEvent": {
229+
const src = item.source;
230+
if (src?.__typename === 'PullRequest') {
231+
return (
232+
<li key={idx} className="relative pl-6">
233+
<span className="absolute rounded-full bg-blue-500 border-2 border-white" style={{ left: -6, top: 10, width: 12, height: 12 }} />
234+
<div className="flex items-start gap-2 text-gray-700">
235+
<Avatar className="w-5 h-5 mt-0.5">
236+
<AvatarImage src={item.actor?.avatarUrl} />
237+
<AvatarFallback className="text-[10px]">{initials(item.actor?.login)}</AvatarFallback>
238+
</Avatar>
239+
<div className="flex-1">
240+
<div>
241+
<span className="font-medium">{item.actor?.login}</span> linked pull request{' '}
242+
<a className="text-blue-600 underline" href={src.url} target="_blank" rel="noreferrer">
243+
{src.repository?.nameWithOwner}#{src.number}
244+
</a>{' '}{src.title} {src.merged ? <span className="ml-1 text-purple-700">(merged)</span> : null}
245+
</div>
246+
<div className="text-xs text-gray-500"><TimeAgo iso={item.createdAt} /></div>
247+
</div>
248+
</div>
249+
</li>
250+
);
251+
}
252+
return null;
253+
}
203254
default:
204255
return null;
205256
}
@@ -380,7 +431,7 @@ export function IssueOverlayCard({ issue }) {
380431
<AvatarFallback className="text-[10px]">{initials(item.author?.login)}</AvatarFallback>
381432
</Avatar>
382433
<div className="text-gray-700">
383-
<span className="font-medium">{item.author?.login}</span> commented {time}
434+
<span className="font-medium">{item.author?.login}</span> commented <TimeAgo iso={item.createdAt} />
384435
</div>
385436
</div>
386437
{item.body && <div className="ml-7 text-gray-600">{item.body}</div>}
@@ -395,7 +446,7 @@ export function IssueOverlayCard({ issue }) {
395446
<AvatarImage src={item.actor?.avatarUrl} />
396447
<AvatarFallback className="text-[10px]">{initials(item.actor?.login)}</AvatarFallback>
397448
</Avatar>
398-
<div><span className="font-medium">{item.actor?.login}</span> closed this issue {time}</div>
449+
<div><span className="font-medium">{item.actor?.login}</span> closed this issue <TimeAgo iso={item.createdAt} /></div>
399450
</div>
400451
</li>
401452
);
@@ -408,7 +459,7 @@ export function IssueOverlayCard({ issue }) {
408459
<AvatarImage src={item.actor?.avatarUrl} />
409460
<AvatarFallback className="text-[10px]">{initials(item.actor?.login)}</AvatarFallback>
410461
</Avatar>
411-
<div><span className="font-medium">{item.actor?.login}</span> reopened this issue {time}</div>
462+
<div><span className="font-medium">{item.actor?.login}</span> reopened this issue <TimeAgo iso={item.createdAt} /></div>
412463
</div>
413464
</li>
414465
);
@@ -431,7 +482,7 @@ export function IssueOverlayCard({ issue }) {
431482
{item.label.name}
432483
</span>
433484
)}
434-
<span>{time}</span>
485+
<span><TimeAgo iso={item.createdAt} /></span>
435486
</div>
436487
</div>
437488
</li>
@@ -455,7 +506,7 @@ export function IssueOverlayCard({ issue }) {
455506
{item.label.name}
456507
</span>
457508
)}
458-
<span>{time}</span>
509+
<span><TimeAgo iso={item.createdAt} /></span>
459510
</div>
460511
</div>
461512
</li>
@@ -481,7 +532,7 @@ export function IssueOverlayCard({ issue }) {
481532
<span className="font-medium">{item.assignee?.login}</span>
482533
</>
483534
)}
484-
<span>{time}</span>
535+
<span><TimeAgo iso={item.createdAt} /></span>
485536
</div>
486537
</div>
487538
</li>

src/components/TimeAgo.jsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,15 @@ export default function TimeAgo({ iso }) {
1212
} else if (minutes < 1440) {
1313
const hours = Math.floor(minutes / 60);
1414
text = `${hours}h ago`;
15-
} else {
15+
} else if (minutes < 10080) { // < 7 days
1616
const days = Math.floor(minutes / 1440);
1717
text = `${days}d ago`;
18+
} else if (minutes < 43800) { // < ~1 month (30.4 days)
19+
const weeks = Math.floor(minutes / 10080);
20+
text = `${weeks}w ago`;
21+
} else {
22+
const months = Math.floor(minutes / 43800);
23+
text = `${months}mo ago`;
1824
}
1925
return (
2026
<time dateTime={iso} title={date.toLocaleString()}>{text}</time>

0 commit comments

Comments
 (0)