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:
@@ -47,6 +47,21 @@ export function activityRoutes(db: Db) {
|
||||
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) => {
|
||||
const id = req.params.id as string;
|
||||
const issue = await issueSvc.getById(id);
|
||||
|
||||
@@ -1017,9 +1017,10 @@ export function agentRoutes(db: Db) {
|
||||
});
|
||||
|
||||
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 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) {
|
||||
res.status(404).json({ error: "Issue not found" });
|
||||
return;
|
||||
@@ -1045,7 +1046,7 @@ export function agentRoutes(db: Db) {
|
||||
and(
|
||||
eq(heartbeatRuns.companyId, issue.companyId),
|
||||
inArray(heartbeatRuns.status, ["queued", "running"]),
|
||||
sql`${heartbeatRuns.contextSnapshot} ->> 'issueId' = ${id}`,
|
||||
sql`${heartbeatRuns.contextSnapshot} ->> 'issueId' = ${issue.id}`,
|
||||
),
|
||||
)
|
||||
.orderBy(desc(heartbeatRuns.createdAt));
|
||||
@@ -1054,9 +1055,10 @@ export function agentRoutes(db: Db) {
|
||||
});
|
||||
|
||||
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 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) {
|
||||
res.status(404).json({ error: "Issue not found" });
|
||||
return;
|
||||
|
||||
@@ -106,6 +106,21 @@ export function issueRoutes(db: Db, storage: StorageService) {
|
||||
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) => {
|
||||
const companyId = req.params.companyId as string;
|
||||
assertCompanyAccess(req, companyId);
|
||||
@@ -119,8 +134,7 @@ export function issueRoutes(db: Db, storage: StorageService) {
|
||||
|
||||
router.get("/issues/:id", async (req, res) => {
|
||||
const id = req.params.id as string;
|
||||
const isIdentifier = /^[A-Z]+-\d+$/i.test(id);
|
||||
const issue = isIdentifier ? await svc.getByIdentifier(id) : await svc.getById(id);
|
||||
const issue = await svc.getById(id);
|
||||
if (!issue) {
|
||||
res.status(404).json({ error: "Issue not found" });
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user