Skip to content

Commit 6d0eda6

Browse files
committed
add AvatarWithFallback component and update UserCard to use it; enhance CreateIssueApp to manage existing issue data
1 parent c927390 commit 6d0eda6

File tree

3 files changed

+106
-58
lines changed

3 files changed

+106
-58
lines changed

ui/src/apps/get-me/App.tsx

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { StrictMode } from "react";
1+
import { StrictMode, useState } from "react";
22
import { createRoot } from "react-dom/client";
33
import { Avatar, Box, Text, Link, Heading, Spinner } from "@primer/react";
44
import {
@@ -8,6 +8,7 @@ import {
88
MailIcon,
99
PeopleIcon,
1010
RepoIcon,
11+
PersonIcon,
1112
} from "@primer/octicons-react";
1213
import { AppProvider } from "../../components/AppProvider";
1314
import { useMcpApp } from "../../hooks/useMcpApp";
@@ -28,6 +29,39 @@ interface UserData {
2829
};
2930
}
3031

32+
function AvatarWithFallback({ src, login, size }: { src?: string; login: string; size: number }) {
33+
const [imgError, setImgError] = useState(false);
34+
35+
if (!src || imgError) {
36+
return (
37+
<Box
38+
sx={{
39+
width: size,
40+
height: size,
41+
borderRadius: "50%",
42+
bg: "accent.subtle",
43+
display: "flex",
44+
alignItems: "center",
45+
justifyContent: "center",
46+
mr: 3,
47+
flexShrink: 0,
48+
}}
49+
>
50+
<PersonIcon size={size * 0.6} />
51+
</Box>
52+
);
53+
}
54+
55+
return (
56+
<Avatar
57+
src={src}
58+
size={size}
59+
sx={{ mr: 3 }}
60+
onError={() => setImgError(true)}
61+
/>
62+
);
63+
}
64+
3165
function UserCard({ user }: { user: UserData }) {
3266
const d = user.details || {};
3367

@@ -43,9 +77,7 @@ function UserCard({ user }: { user: UserData }) {
4377
>
4478
{/* Header with avatar and name */}
4579
<Box display="flex" alignItems="center" mb={3} pb={3} borderBottomWidth={1} borderBottomStyle="solid" borderBottomColor="border.default">
46-
{user.avatar_url && (
47-
<Avatar src={user.avatar_url} size={48} sx={{ mr: 3 }} />
48-
)}
80+
<AvatarWithFallback src={user.avatar_url} login={user.login} size={48} />
4981
<Box>
5082
<Heading as="h2" sx={{ fontSize: 2, mb: 0 }}>
5183
{d.name || user.login}

ui/src/apps/get-me/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
<head>
44
<meta charset="UTF-8" />
55
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+
<meta http-equiv="Content-Security-Policy" content="default-src 'self' 'unsafe-inline' 'unsafe-eval' data:; img-src 'self' data: https://avatars.githubusercontent.com https://*.githubusercontent.com; connect-src *;" />
67
<title>GitHub User Profile</title>
78
</head>
89
<body>

ui/src/apps/issue-write/App.tsx

Lines changed: 69 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -426,18 +426,24 @@ function CreateIssueApp() {
426426
type: boolean;
427427
}>({ title: false, body: false, labels: false, assignees: false, milestone: false, type: false });
428428

429-
// Track if we've loaded existing issue data for update mode
430-
const existingIssueLoaded = useRef(false);
429+
// Store existing issue data for matching when available lists load
430+
interface ExistingIssueData {
431+
labels: string[];
432+
assignees: string[];
433+
milestoneNumber: number | null;
434+
issueType: string | null;
435+
}
436+
const [existingIssueData, setExistingIssueData] = useState<ExistingIssueData | null>(null);
431437

432438
// Reset prefill tracking when toolInput changes (new invocation)
433439
useEffect(() => {
434440
prefillApplied.current = { title: false, body: false, labels: false, assignees: false, milestone: false, type: false };
435-
existingIssueLoaded.current = false;
441+
setExistingIssueData(null);
436442
}, [toolInput]);
437443

438444
// Load existing issue data when in update mode
439445
useEffect(() => {
440-
if (!isUpdateMode || !owner || !repo || !issueNumber || !app || existingIssueLoaded.current) {
446+
if (!isUpdateMode || !owner || !repo || !issueNumber || !app || existingIssueData !== null) {
441447
return;
442448
}
443449

@@ -459,7 +465,7 @@ function CreateIssueApp() {
459465
const issueData = JSON.parse(textContent.text);
460466
console.log("Loaded issue data:", issueData);
461467

462-
// Pre-fill title and body
468+
// Pre-fill title and body immediately
463469
if (issueData.title && !prefillApplied.current.title) {
464470
setTitle(issueData.title);
465471
prefillApplied.current.title = true;
@@ -469,53 +475,22 @@ function CreateIssueApp() {
469475
prefillApplied.current.body = true;
470476
}
471477

472-
// Pre-fill labels (wait for available labels to be loaded)
473-
if (issueData.labels && Array.isArray(issueData.labels)) {
474-
const labelNames = issueData.labels.map((l: { name?: string } | string) =>
475-
typeof l === 'string' ? l : l.name
476-
).filter(Boolean);
477-
if (labelNames.length > 0 && availableLabels.length > 0 && !prefillApplied.current.labels) {
478-
const matchedLabels = availableLabels.filter((l) =>
479-
labelNames.includes(l.text)
480-
);
481-
if (matchedLabels.length > 0) {
482-
setSelectedLabels(matchedLabels);
483-
prefillApplied.current.labels = true;
484-
}
485-
}
486-
}
487-
488-
// Pre-fill assignees
489-
if (issueData.assignees && Array.isArray(issueData.assignees)) {
490-
const assigneeLogins = issueData.assignees.map((a: { login?: string } | string) =>
491-
typeof a === 'string' ? a : a.login
492-
).filter(Boolean);
493-
if (assigneeLogins.length > 0 && availableAssignees.length > 0 && !prefillApplied.current.assignees) {
494-
const matchedAssignees = availableAssignees.filter((a) =>
495-
assigneeLogins.includes(a.text)
496-
);
497-
if (matchedAssignees.length > 0) {
498-
setSelectedAssignees(matchedAssignees);
499-
prefillApplied.current.assignees = true;
500-
}
501-
}
502-
}
503-
504-
// Pre-fill milestone
505-
if (issueData.milestone && availableMilestones.length > 0 && !prefillApplied.current.milestone) {
506-
const milestoneNumber = typeof issueData.milestone === 'object'
507-
? issueData.milestone.number
508-
: issueData.milestone;
509-
const matchedMilestone = availableMilestones.find(
510-
(m) => m.number === milestoneNumber
511-
);
512-
if (matchedMilestone) {
513-
setSelectedMilestone(matchedMilestone);
514-
prefillApplied.current.milestone = true;
515-
}
516-
}
517-
518-
existingIssueLoaded.current = true;
478+
// Extract data for deferred matching when available lists load
479+
const labelNames = (issueData.labels || [])
480+
.map((l: { name?: string } | string) => typeof l === 'string' ? l : l.name)
481+
.filter(Boolean) as string[];
482+
483+
const assigneeLogins = (issueData.assignees || [])
484+
.map((a: { login?: string } | string) => typeof a === 'string' ? a : a.login)
485+
.filter(Boolean) as string[];
486+
487+
const milestoneNumber = issueData.milestone
488+
? (typeof issueData.milestone === 'object' ? issueData.milestone.number : issueData.milestone)
489+
: null;
490+
491+
const issueType = issueData.issue_type?.name || issueData.type || null;
492+
493+
setExistingIssueData({ labels: labelNames, assignees: assigneeLogins, milestoneNumber, issueType });
519494
}
520495
}
521496
} catch (e) {
@@ -524,7 +499,47 @@ function CreateIssueApp() {
524499
};
525500

526501
loadExistingIssue();
527-
}, [isUpdateMode, owner, repo, issueNumber, app, callTool, availableLabels, availableAssignees, availableMilestones]);
502+
}, [isUpdateMode, owner, repo, issueNumber, app, callTool, existingIssueData]);
503+
504+
// Apply existing labels when available labels load
505+
useEffect(() => {
506+
if (!existingIssueData?.labels.length || !availableLabels.length || prefillApplied.current.labels) return;
507+
const matched = availableLabels.filter((l) => existingIssueData.labels.includes(l.text));
508+
if (matched.length > 0) {
509+
setSelectedLabels(matched);
510+
prefillApplied.current.labels = true;
511+
}
512+
}, [existingIssueData, availableLabels]);
513+
514+
// Apply existing assignees when available assignees load
515+
useEffect(() => {
516+
if (!existingIssueData?.assignees.length || !availableAssignees.length || prefillApplied.current.assignees) return;
517+
const matched = availableAssignees.filter((a) => existingIssueData.assignees.includes(a.text));
518+
if (matched.length > 0) {
519+
setSelectedAssignees(matched);
520+
prefillApplied.current.assignees = true;
521+
}
522+
}, [existingIssueData, availableAssignees]);
523+
524+
// Apply existing milestone when available milestones load
525+
useEffect(() => {
526+
if (!existingIssueData?.milestoneNumber || !availableMilestones.length || prefillApplied.current.milestone) return;
527+
const matched = availableMilestones.find((m) => m.number === existingIssueData.milestoneNumber);
528+
if (matched) {
529+
setSelectedMilestone(matched);
530+
prefillApplied.current.milestone = true;
531+
}
532+
}, [existingIssueData, availableMilestones]);
533+
534+
// Apply existing issue type when available issue types load
535+
useEffect(() => {
536+
if (!existingIssueData?.issueType || !availableIssueTypes.length || prefillApplied.current.type) return;
537+
const matched = availableIssueTypes.find((t) => t.text === existingIssueData.issueType);
538+
if (matched) {
539+
setSelectedIssueType(matched);
540+
prefillApplied.current.type = true;
541+
}
542+
}, [existingIssueData, availableIssueTypes]);
528543

529544
// Pre-fill title and body immediately (don't wait for data loading)
530545
useEffect(() => {
@@ -855,7 +870,7 @@ function CreateIssueApp() {
855870
</Box>
856871

857872
{/* Metadata section */}
858-
<Box display="flex" gap={3} mb={3} flexWrap="wrap" sx={{ "& > *": { flex: "1 1 auto" } }}>
873+
<Box display="flex" gap={2} mb={3} sx={{ flexWrap: "nowrap", "& > *": { flexShrink: 0 } }}>
859874
{/* Labels dropdown */}
860875
<ActionMenu>
861876
<ActionMenu.Button size="small" leadingVisual={TagIcon}>

0 commit comments

Comments
 (0)