Support issue identifiers (PAP-39) in URLs and prefer them throughout
Backend: - Add router.param middleware in issues, activity, and agents routes to resolve identifiers (e.g. PAP-39) to UUIDs before handlers run - Simplify GET /issues/:id now that param middleware handles resolution - Include identifier in getAncestors response and issuesForRun query - Add identifier field to IssueAncestor shared type Frontend: - Update all issue navigation links across 15+ files to use issue.identifier ?? issue.id instead of bare UUIDs - Add URL redirect in IssueDetail: navigating via UUID automatically replaces the URL with the human-readable identifier - Fix childIssues filter to use issue.id (UUID) instead of URL param so it works correctly with identifier-based URLs - Add issueUrl() utility in lib/utils.ts Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -18,6 +18,7 @@ export interface IssueAncestorGoal {
|
|||||||
|
|
||||||
export interface IssueAncestor {
|
export interface IssueAncestor {
|
||||||
id: string;
|
id: string;
|
||||||
|
identifier: string | null;
|
||||||
title: string;
|
title: string;
|
||||||
description: string | null;
|
description: string | null;
|
||||||
status: string;
|
status: string;
|
||||||
|
|||||||
@@ -47,6 +47,21 @@ export function activityRoutes(db: Db) {
|
|||||||
res.status(201).json(event);
|
res.status(201).json(event);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Resolve issue identifiers (e.g. "PAP-39") to UUIDs
|
||||||
|
router.param("id", async (req, res, next, rawId) => {
|
||||||
|
try {
|
||||||
|
if (/^[A-Z]+-\d+$/i.test(rawId)) {
|
||||||
|
const issue = await issueSvc.getByIdentifier(rawId);
|
||||||
|
if (issue) {
|
||||||
|
req.params.id = issue.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
} catch (err) {
|
||||||
|
next(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
router.get("/issues/:id/activity", async (req, res) => {
|
router.get("/issues/:id/activity", async (req, res) => {
|
||||||
const id = req.params.id as string;
|
const id = req.params.id as string;
|
||||||
const issue = await issueSvc.getById(id);
|
const issue = await issueSvc.getById(id);
|
||||||
|
|||||||
@@ -1017,9 +1017,10 @@ export function agentRoutes(db: Db) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
router.get("/issues/:id/live-runs", async (req, res) => {
|
router.get("/issues/:id/live-runs", async (req, res) => {
|
||||||
const id = req.params.id as string;
|
const rawId = req.params.id as string;
|
||||||
const issueSvc = issueService(db);
|
const issueSvc = issueService(db);
|
||||||
const issue = await issueSvc.getById(id);
|
const isIdentifier = /^[A-Z]+-\d+$/i.test(rawId);
|
||||||
|
const issue = isIdentifier ? await issueSvc.getByIdentifier(rawId) : await issueSvc.getById(rawId);
|
||||||
if (!issue) {
|
if (!issue) {
|
||||||
res.status(404).json({ error: "Issue not found" });
|
res.status(404).json({ error: "Issue not found" });
|
||||||
return;
|
return;
|
||||||
@@ -1045,7 +1046,7 @@ export function agentRoutes(db: Db) {
|
|||||||
and(
|
and(
|
||||||
eq(heartbeatRuns.companyId, issue.companyId),
|
eq(heartbeatRuns.companyId, issue.companyId),
|
||||||
inArray(heartbeatRuns.status, ["queued", "running"]),
|
inArray(heartbeatRuns.status, ["queued", "running"]),
|
||||||
sql`${heartbeatRuns.contextSnapshot} ->> 'issueId' = ${id}`,
|
sql`${heartbeatRuns.contextSnapshot} ->> 'issueId' = ${issue.id}`,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.orderBy(desc(heartbeatRuns.createdAt));
|
.orderBy(desc(heartbeatRuns.createdAt));
|
||||||
@@ -1054,9 +1055,10 @@ export function agentRoutes(db: Db) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
router.get("/issues/:id/active-run", async (req, res) => {
|
router.get("/issues/:id/active-run", async (req, res) => {
|
||||||
const id = req.params.id as string;
|
const rawId = req.params.id as string;
|
||||||
const issueSvc = issueService(db);
|
const issueSvc = issueService(db);
|
||||||
const issue = await issueSvc.getById(id);
|
const isIdentifier = /^[A-Z]+-\d+$/i.test(rawId);
|
||||||
|
const issue = isIdentifier ? await issueSvc.getByIdentifier(rawId) : await issueSvc.getById(rawId);
|
||||||
if (!issue) {
|
if (!issue) {
|
||||||
res.status(404).json({ error: "Issue not found" });
|
res.status(404).json({ error: "Issue not found" });
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -106,6 +106,21 @@ export function issueRoutes(db: Db, storage: StorageService) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Resolve issue identifiers (e.g. "PAP-39") to UUIDs for all /issues/:id routes
|
||||||
|
router.param("id", async (req, res, next, rawId) => {
|
||||||
|
try {
|
||||||
|
if (/^[A-Z]+-\d+$/i.test(rawId)) {
|
||||||
|
const issue = await svc.getByIdentifier(rawId);
|
||||||
|
if (issue) {
|
||||||
|
req.params.id = issue.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
} catch (err) {
|
||||||
|
next(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
router.get("/companies/:companyId/issues", async (req, res) => {
|
router.get("/companies/:companyId/issues", async (req, res) => {
|
||||||
const companyId = req.params.companyId as string;
|
const companyId = req.params.companyId as string;
|
||||||
assertCompanyAccess(req, companyId);
|
assertCompanyAccess(req, companyId);
|
||||||
@@ -119,8 +134,7 @@ export function issueRoutes(db: Db, storage: StorageService) {
|
|||||||
|
|
||||||
router.get("/issues/:id", async (req, res) => {
|
router.get("/issues/:id", async (req, res) => {
|
||||||
const id = req.params.id as string;
|
const id = req.params.id as string;
|
||||||
const isIdentifier = /^[A-Z]+-\d+$/i.test(id);
|
const issue = await svc.getById(id);
|
||||||
const issue = isIdentifier ? await svc.getByIdentifier(id) : await svc.getById(id);
|
|
||||||
if (!issue) {
|
if (!issue) {
|
||||||
res.status(404).json({ error: "Issue not found" });
|
res.status(404).json({ error: "Issue not found" });
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -88,6 +88,7 @@ export function activityService(db: Db) {
|
|||||||
db
|
db
|
||||||
.selectDistinctOn([issueIdAsText], {
|
.selectDistinctOn([issueIdAsText], {
|
||||||
issueId: issues.id,
|
issueId: issues.id,
|
||||||
|
identifier: issues.identifier,
|
||||||
title: issues.title,
|
title: issues.title,
|
||||||
status: issues.status,
|
status: issues.status,
|
||||||
priority: issues.priority,
|
priority: issues.priority,
|
||||||
|
|||||||
@@ -574,7 +574,7 @@ export function issueService(db: Db) {
|
|||||||
|
|
||||||
getAncestors: async (issueId: string) => {
|
getAncestors: async (issueId: string) => {
|
||||||
const raw: Array<{
|
const raw: Array<{
|
||||||
id: string; title: string; description: string | null;
|
id: string; identifier: string | null; title: string; description: string | null;
|
||||||
status: string; priority: string;
|
status: string; priority: string;
|
||||||
assigneeAgentId: string | null; projectId: string | null; goalId: string | null;
|
assigneeAgentId: string | null; projectId: string | null; goalId: string | null;
|
||||||
}> = [];
|
}> = [];
|
||||||
@@ -584,14 +584,14 @@ export function issueService(db: Db) {
|
|||||||
while (currentId && !visited.has(currentId) && raw.length < 50) {
|
while (currentId && !visited.has(currentId) && raw.length < 50) {
|
||||||
visited.add(currentId);
|
visited.add(currentId);
|
||||||
const parent = await db.select({
|
const parent = await db.select({
|
||||||
id: issues.id, title: issues.title, description: issues.description,
|
id: issues.id, identifier: issues.identifier, title: issues.title, description: issues.description,
|
||||||
status: issues.status, priority: issues.priority,
|
status: issues.status, priority: issues.priority,
|
||||||
assigneeAgentId: issues.assigneeAgentId, projectId: issues.projectId,
|
assigneeAgentId: issues.assigneeAgentId, projectId: issues.projectId,
|
||||||
goalId: issues.goalId, parentId: issues.parentId,
|
goalId: issues.goalId, parentId: issues.parentId,
|
||||||
}).from(issues).where(eq(issues.id, currentId)).then(r => r[0] ?? null);
|
}).from(issues).where(eq(issues.id, currentId)).then(r => r[0] ?? null);
|
||||||
if (!parent) break;
|
if (!parent) break;
|
||||||
raw.push({
|
raw.push({
|
||||||
id: parent.id, title: parent.title, description: parent.description ?? null,
|
id: parent.id, identifier: parent.identifier ?? null, title: parent.title, description: parent.description ?? null,
|
||||||
status: parent.status, priority: parent.priority,
|
status: parent.status, priority: parent.priority,
|
||||||
assigneeAgentId: parent.assigneeAgentId ?? null,
|
assigneeAgentId: parent.assigneeAgentId ?? null,
|
||||||
projectId: parent.projectId ?? null, goalId: parent.goalId ?? null,
|
projectId: parent.projectId ?? null, goalId: parent.goalId ?? null,
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export interface RunForIssue {
|
|||||||
|
|
||||||
export interface IssueForRun {
|
export interface IssueForRun {
|
||||||
issueId: string;
|
issueId: string;
|
||||||
|
identifier: string | null;
|
||||||
title: string;
|
title: string;
|
||||||
status: string;
|
status: string;
|
||||||
priority: string;
|
priority: string;
|
||||||
|
|||||||
@@ -359,7 +359,7 @@ function AgentRunCard({
|
|||||||
<div className="px-3 py-1.5 border-b border-border/40 text-xs flex items-center gap-1 min-w-0">
|
<div className="px-3 py-1.5 border-b border-border/40 text-xs flex items-center gap-1 min-w-0">
|
||||||
<span className="text-muted-foreground mr-1">Working on:</span>
|
<span className="text-muted-foreground mr-1">Working on:</span>
|
||||||
<Link
|
<Link
|
||||||
to={`/issues/${run.issueId}`}
|
to={`/issues/${issue?.identifier ?? run.issueId}`}
|
||||||
className="text-blue-400 hover:text-blue-300 hover:underline min-w-0 truncate"
|
className="text-blue-400 hover:text-blue-300 hover:underline min-w-0 truncate"
|
||||||
title={issue?.title ? `${issue?.identifier ?? run.issueId.slice(0, 8)} - ${issue.title}` : issue?.identifier ?? run.issueId.slice(0, 8)}
|
title={issue?.title ? `${issue?.identifier ?? run.issueId.slice(0, 8)} - ${issue.title}` : issue?.identifier ?? run.issueId.slice(0, 8)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -149,7 +149,7 @@ export function CommandPalette() {
|
|||||||
<CommandSeparator />
|
<CommandSeparator />
|
||||||
<CommandGroup heading="Issues">
|
<CommandGroup heading="Issues">
|
||||||
{issues.slice(0, 10).map((issue) => (
|
{issues.slice(0, 10).map((issue) => (
|
||||||
<CommandItem key={issue.id} onSelect={() => go(`/issues/${issue.id}`)}>
|
<CommandItem key={issue.id} onSelect={() => go(`/issues/${issue.identifier ?? issue.id}`)}>
|
||||||
<CircleDot className="mr-2 h-4 w-4" />
|
<CircleDot className="mr-2 h-4 w-4" />
|
||||||
<span className="text-muted-foreground mr-2 font-mono text-xs">
|
<span className="text-muted-foreground mr-2 font-mono text-xs">
|
||||||
{issue.identifier ?? issue.id.slice(0, 8)}
|
{issue.identifier ?? issue.id.slice(0, 8)}
|
||||||
|
|||||||
@@ -211,7 +211,7 @@ export function IssueProperties({ issue, onUpdate }: IssuePropertiesProps) {
|
|||||||
{issue.parentId && (
|
{issue.parentId && (
|
||||||
<PropertyRow label="Parent">
|
<PropertyRow label="Parent">
|
||||||
<Link
|
<Link
|
||||||
to={`/issues/${issue.parentId}`}
|
to={`/issues/${issue.ancestors?.[0]?.identifier ?? issue.parentId}`}
|
||||||
className="text-sm hover:underline"
|
className="text-sm hover:underline"
|
||||||
>
|
>
|
||||||
{issue.ancestors?.[0]?.title ?? issue.parentId.slice(0, 8)}
|
{issue.ancestors?.[0]?.title ?? issue.parentId.slice(0, 8)}
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ export function NewIssueDialog() {
|
|||||||
title: `${issue.identifier ?? "Issue"} created`,
|
title: `${issue.identifier ?? "Issue"} created`,
|
||||||
body: issue.title,
|
body: issue.title,
|
||||||
tone: "success",
|
tone: "success",
|
||||||
action: { label: `View ${issue.identifier ?? "issue"}`, href: `/issues/${issue.id}` },
|
action: { label: `View ${issue.identifier ?? "issue"}`, href: `/issues/${issue.identifier ?? issue.id}` },
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ function resolveIssueToastContext(
|
|||||||
ref,
|
ref,
|
||||||
title,
|
title,
|
||||||
label: title ? `${ref} - ${truncate(title, 72)}` : ref,
|
label: title ? `${ref} - ${truncate(title, 72)}` : ref,
|
||||||
href: `/issues/${issueId}`,
|
href: `/issues/${cachedIssue?.identifier ?? issueId}`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -36,3 +36,8 @@ export function formatTokens(n: number): string {
|
|||||||
if (n >= 1_000) return `${(n / 1_000).toFixed(1)}k`;
|
if (n >= 1_000) return `${(n / 1_000).toFixed(1)}k`;
|
||||||
return String(n);
|
return String(n);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Build an issue URL using the human-readable identifier when available. */
|
||||||
|
export function issueUrl(issue: { id: string; identifier?: string | null }): string {
|
||||||
|
return `/issues/${issue.identifier ?? issue.id}`;
|
||||||
|
}
|
||||||
|
|||||||
@@ -670,7 +670,7 @@ export function AgentDetail() {
|
|||||||
key={issue.id}
|
key={issue.id}
|
||||||
identifier={issue.identifier ?? issue.id.slice(0, 8)}
|
identifier={issue.identifier ?? issue.id.slice(0, 8)}
|
||||||
title={issue.title}
|
title={issue.title}
|
||||||
onClick={() => navigate(`/issues/${issue.id}`)}
|
onClick={() => navigate(`/issues/${issue.identifier ?? issue.id}`)}
|
||||||
trailing={<StatusBadge status={issue.status} />}
|
trailing={<StatusBadge status={issue.status} />}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
@@ -1210,13 +1210,13 @@ function RunDetail({ run, adapterType }: { run: HeartbeatRun; adapterType: strin
|
|||||||
<button
|
<button
|
||||||
key={issue.issueId}
|
key={issue.issueId}
|
||||||
className="flex items-center justify-between w-full px-3 py-2 text-xs hover:bg-accent/20 transition-colors text-left"
|
className="flex items-center justify-between w-full px-3 py-2 text-xs hover:bg-accent/20 transition-colors text-left"
|
||||||
onClick={() => navigate(`/issues/${issue.issueId}`)}
|
onClick={() => navigate(`/issues/${issue.identifier ?? issue.issueId}`)}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 min-w-0">
|
<div className="flex items-center gap-2 min-w-0">
|
||||||
<StatusBadge status={issue.status} />
|
<StatusBadge status={issue.status} />
|
||||||
<span className="truncate">{issue.title}</span>
|
<span className="truncate">{issue.title}</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="font-mono text-muted-foreground shrink-0 ml-2">{issue.issueId.slice(0, 8)}</span>
|
<span className="font-mono text-muted-foreground shrink-0 ml-2">{issue.identifier ?? issue.issueId.slice(0, 8)}</span>
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -149,7 +149,7 @@ export function ApprovalDetail() {
|
|||||||
(linkedIssues?.length ?? 0) > 1
|
(linkedIssues?.length ?? 0) > 1
|
||||||
? "Review linked issues"
|
? "Review linked issues"
|
||||||
: "Review linked issue",
|
: "Review linked issue",
|
||||||
to: `/issues/${primaryLinkedIssue.id}`,
|
to: `/issues/${primaryLinkedIssue.identifier ?? primaryLinkedIssue.id}`,
|
||||||
}
|
}
|
||||||
: linkedAgentId
|
: linkedAgentId
|
||||||
? {
|
? {
|
||||||
@@ -236,7 +236,7 @@ export function ApprovalDetail() {
|
|||||||
{linkedIssues.map((issue) => (
|
{linkedIssues.map((issue) => (
|
||||||
<Link
|
<Link
|
||||||
key={issue.id}
|
key={issue.id}
|
||||||
to={`/issues/${issue.id}`}
|
to={`/issues/${issue.identifier ?? issue.id}`}
|
||||||
className="block text-xs rounded border border-border/70 px-2 py-1.5 hover:bg-accent/20"
|
className="block text-xs rounded border border-border/70 px-2 py-1.5 hover:bg-accent/20"
|
||||||
>
|
>
|
||||||
<span className="font-mono text-muted-foreground mr-2">
|
<span className="font-mono text-muted-foreground mr-2">
|
||||||
|
|||||||
@@ -266,7 +266,7 @@ export function Dashboard() {
|
|||||||
<div
|
<div
|
||||||
key={issue.id}
|
key={issue.id}
|
||||||
className="px-4 py-2 text-sm cursor-pointer hover:bg-accent/50 transition-colors"
|
className="px-4 py-2 text-sm cursor-pointer hover:bg-accent/50 transition-colors"
|
||||||
onClick={() => navigate(`/issues/${issue.id}`)}
|
onClick={() => navigate(`/issues/${issue.identifier ?? issue.id}`)}
|
||||||
>
|
>
|
||||||
<div className="flex gap-3">
|
<div className="flex gap-3">
|
||||||
<div className="flex items-start gap-2 min-w-0 flex-1">
|
<div className="flex items-start gap-2 min-w-0 flex-1">
|
||||||
|
|||||||
@@ -299,7 +299,7 @@ export function Inbox() {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="truncate text-muted-foreground transition-colors hover:text-foreground"
|
className="truncate text-muted-foreground transition-colors hover:text-foreground"
|
||||||
onClick={() => navigate(`/issues/${issue.id}`)}
|
onClick={() => navigate(`/issues/${issue.identifier ?? issue.id}`)}
|
||||||
>
|
>
|
||||||
{issue.identifier ?? issue.id.slice(0, 8)} · {issue.title}
|
{issue.identifier ?? issue.id.slice(0, 8)} · {issue.title}
|
||||||
</button>
|
</button>
|
||||||
@@ -372,7 +372,7 @@ export function Inbox() {
|
|||||||
<div
|
<div
|
||||||
key={issue.id}
|
key={issue.id}
|
||||||
className="flex cursor-pointer items-center gap-3 px-4 py-3 transition-colors hover:bg-accent/50"
|
className="flex cursor-pointer items-center gap-3 px-4 py-3 transition-colors hover:bg-accent/50"
|
||||||
onClick={() => navigate(`/issues/${issue.id}`)}
|
onClick={() => navigate(`/issues/${issue.identifier ?? issue.id}`)}
|
||||||
>
|
>
|
||||||
<Clock className="h-4 w-4 shrink-0 text-muted-foreground" />
|
<Clock className="h-4 w-4 shrink-0 text-muted-foreground" />
|
||||||
<PriorityIcon priority={issue.priority} />
|
<PriorityIcon priority={issue.priority} />
|
||||||
|
|||||||
@@ -189,11 +189,11 @@ export function IssueDetail() {
|
|||||||
}, [agents]);
|
}, [agents]);
|
||||||
|
|
||||||
const childIssues = useMemo(() => {
|
const childIssues = useMemo(() => {
|
||||||
if (!allIssues || !issueId) return [];
|
if (!allIssues || !issue) return [];
|
||||||
return allIssues
|
return allIssues
|
||||||
.filter((i) => i.parentId === issueId)
|
.filter((i) => i.parentId === issue.id)
|
||||||
.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime());
|
.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime());
|
||||||
}, [allIssues, issueId]);
|
}, [allIssues, issue]);
|
||||||
|
|
||||||
const commentsWithRunMeta = useMemo(() => {
|
const commentsWithRunMeta = useMemo(() => {
|
||||||
const runMetaByCommentId = new Map<string, { runId: string; runAgentId: string | null }>();
|
const runMetaByCommentId = new Map<string, { runId: string; runAgentId: string | null }>();
|
||||||
@@ -281,7 +281,7 @@ export function IssueDetail() {
|
|||||||
title: `${issueRef} updated`,
|
title: `${issueRef} updated`,
|
||||||
body: truncate(updated.title, 96),
|
body: truncate(updated.title, 96),
|
||||||
tone: "success",
|
tone: "success",
|
||||||
action: { label: `View ${issueRef}`, href: `/issues/${updated.id}` },
|
action: { label: `View ${issueRef}`, href: `/issues/${updated.identifier ?? updated.id}` },
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -298,7 +298,7 @@ export function IssueDetail() {
|
|||||||
title: `Comment posted on ${issueRef}`,
|
title: `Comment posted on ${issueRef}`,
|
||||||
body: issue?.title ? truncate(issue.title, 96) : undefined,
|
body: issue?.title ? truncate(issue.title, 96) : undefined,
|
||||||
tone: "success",
|
tone: "success",
|
||||||
action: issueId ? { label: `View ${issueRef}`, href: `/issues/${issueId}` } : undefined,
|
action: issueId ? { label: `View ${issueRef}`, href: `/issues/${issue?.identifier ?? issueId}` } : undefined,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -337,6 +337,13 @@ export function IssueDetail() {
|
|||||||
]);
|
]);
|
||||||
}, [setBreadcrumbs, issue, issueId]);
|
}, [setBreadcrumbs, issue, issueId]);
|
||||||
|
|
||||||
|
// Redirect to identifier-based URL if navigated via UUID
|
||||||
|
useEffect(() => {
|
||||||
|
if (issue?.identifier && issueId !== issue.identifier) {
|
||||||
|
navigate(`/issues/${issue.identifier}`, { replace: true });
|
||||||
|
}
|
||||||
|
}, [issue, issueId, navigate]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (issue) {
|
if (issue) {
|
||||||
openPanel(
|
openPanel(
|
||||||
@@ -373,7 +380,7 @@ export function IssueDetail() {
|
|||||||
<span key={ancestor.id} className="flex items-center gap-1">
|
<span key={ancestor.id} className="flex items-center gap-1">
|
||||||
{i > 0 && <ChevronRight className="h-3 w-3 shrink-0" />}
|
{i > 0 && <ChevronRight className="h-3 w-3 shrink-0" />}
|
||||||
<Link
|
<Link
|
||||||
to={`/issues/${ancestor.id}`}
|
to={`/issues/${ancestor.identifier ?? ancestor.id}`}
|
||||||
className="hover:text-foreground transition-colors truncate max-w-[200px]"
|
className="hover:text-foreground transition-colors truncate max-w-[200px]"
|
||||||
title={ancestor.title}
|
title={ancestor.title}
|
||||||
>
|
>
|
||||||
@@ -595,7 +602,7 @@ export function IssueDetail() {
|
|||||||
{childIssues.map((child) => (
|
{childIssues.map((child) => (
|
||||||
<Link
|
<Link
|
||||||
key={child.id}
|
key={child.id}
|
||||||
to={`/issues/${child.id}`}
|
to={`/issues/${child.identifier ?? child.id}`}
|
||||||
className="flex items-center justify-between px-3 py-2 text-xs hover:bg-accent/20 transition-colors"
|
className="flex items-center justify-between px-3 py-2 text-xs hover:bg-accent/20 transition-colors"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 min-w-0">
|
<div className="flex items-center gap-2 min-w-0">
|
||||||
|
|||||||
@@ -157,7 +157,7 @@ export function Issues() {
|
|||||||
key={issue.id}
|
key={issue.id}
|
||||||
identifier={issue.identifier ?? issue.id.slice(0, 8)}
|
identifier={issue.identifier ?? issue.id.slice(0, 8)}
|
||||||
title={issue.title}
|
title={issue.title}
|
||||||
onClick={() => navigate(`/issues/${issue.id}`)}
|
onClick={() => navigate(`/issues/${issue.identifier ?? issue.id}`)}
|
||||||
leading={
|
leading={
|
||||||
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
|
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
|
||||||
<div className="flex items-center gap-2" onClick={(e) => e.stopPropagation()}>
|
<div className="flex items-center gap-2" onClick={(e) => e.stopPropagation()}>
|
||||||
@@ -220,7 +220,7 @@ export function Issues() {
|
|||||||
key={issue.id}
|
key={issue.id}
|
||||||
identifier={issue.identifier ?? issue.id.slice(0, 8)}
|
identifier={issue.identifier ?? issue.id.slice(0, 8)}
|
||||||
title={issue.title}
|
title={issue.title}
|
||||||
onClick={() => navigate(`/issues/${issue.id}`)}
|
onClick={() => navigate(`/issues/${issue.identifier ?? issue.id}`)}
|
||||||
leading={
|
leading={
|
||||||
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
|
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
|
||||||
<div className="flex items-center gap-2" onClick={(e) => e.stopPropagation()}>
|
<div className="flex items-center gap-2" onClick={(e) => e.stopPropagation()}>
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ export function MyIssues() {
|
|||||||
key={issue.id}
|
key={issue.id}
|
||||||
identifier={issue.identifier ?? issue.id.slice(0, 8)}
|
identifier={issue.identifier ?? issue.id.slice(0, 8)}
|
||||||
title={issue.title}
|
title={issue.title}
|
||||||
onClick={() => navigate(`/issues/${issue.id}`)}
|
onClick={() => navigate(`/issues/${issue.identifier ?? issue.id}`)}
|
||||||
leading={
|
leading={
|
||||||
<>
|
<>
|
||||||
<PriorityIcon priority={issue.priority} />
|
<PriorityIcon priority={issue.priority} />
|
||||||
|
|||||||
@@ -132,7 +132,7 @@ export function ProjectDetail() {
|
|||||||
identifier={issue.identifier ?? issue.id.slice(0, 8)}
|
identifier={issue.identifier ?? issue.id.slice(0, 8)}
|
||||||
title={issue.title}
|
title={issue.title}
|
||||||
trailing={<StatusBadge status={issue.status} />}
|
trailing={<StatusBadge status={issue.status} />}
|
||||||
onClick={() => navigate(`/issues/${issue.id}`)}
|
onClick={() => navigate(`/issues/${issue.identifier ?? issue.id}`)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user