From 58d5adc1c7502171fe5b71deeb4e8b4f18ba96d0 Mon Sep 17 00:00:00 2001 From: Dotta Date: Fri, 24 Apr 2026 18:31:53 -0500 Subject: [PATCH] Wire external object surfaces across markdown, properties, related work, sidebar, list, filters - MarkdownBody/InlineEditor accept an externalReferences map and render resolved URLs with a host-rendered status icon prefix (no inline plugin React, per Phase 1B security review) - IssueProperties shows a read-only "External objects" row above Parent, with skeleton/error fallbacks - IssueRelatedWorkPanel adds a third "External objects" section between References and Referenced by, severity-sorted with mention-source chips - SidebarProjects renders a per-project ExternalObjectStatusSummary marker to the right of the project name - IssueRow accepts an externalObjectSummary in the desktop trailing slot - IssueFiltersPopover gains an "External object status" section with failed/waiting/running/auth_required/unreachable/stale/none options - Add useIssueExternalObjects, useIssueExternalObjectSummary, useProjectExternalObjectSummary hooks plus queryKeys.externalObjects - IssueDetail loads external objects once and forwards to description, properties, mobile properties drawer, and the related-work tab Co-Authored-By: Paperclip --- ui/src/components/InlineEditor.tsx | 18 ++- ui/src/components/IssueFiltersPopover.tsx | 35 +++++ ui/src/components/IssueProperties.tsx | 54 +++++++ ui/src/components/IssueRelatedWorkPanel.tsx | 100 +++++++++++++ ui/src/components/IssueRow.tsx | 14 +- ui/src/components/MarkdownBody.tsx | 99 +++++++++++- ui/src/components/SidebarProjects.tsx | 5 + ui/src/hooks/useIssueExternalObjects.ts | 158 ++++++++++++++++++++ ui/src/lib/inbox.test.ts | 8 + ui/src/lib/issue-filters.ts | 42 ++++++ ui/src/lib/queryKeys.ts | 5 + ui/src/pages/IssueDetail.tsx | 23 ++- 12 files changed, 554 insertions(+), 7 deletions(-) create mode 100644 ui/src/hooks/useIssueExternalObjects.ts diff --git a/ui/src/components/InlineEditor.tsx b/ui/src/components/InlineEditor.tsx index 192e8260d8e..094566243a1 100644 --- a/ui/src/components/InlineEditor.tsx +++ b/ui/src/components/InlineEditor.tsx @@ -1,6 +1,6 @@ import { useState, useRef, useEffect, useCallback } from "react"; import { cn } from "../lib/utils"; -import { MarkdownBody } from "./MarkdownBody"; +import { MarkdownBody, type MarkdownExternalReferenceMap } from "./MarkdownBody"; import { MarkdownEditor, type MarkdownEditorRef, type MentionOption } from "./MarkdownEditor"; import { useAutosaveIndicator } from "../hooks/useAutosaveIndicator"; import { FoldCurtain } from "./FoldCurtain"; @@ -19,6 +19,11 @@ interface InlineEditorProps { nullable?: boolean; /** When true, long display-mode markdown is clipped with a fade curtain that expands on click. */ foldable?: boolean; + /** + * Optional host-resolved external object metadata. Forwarded to the read-mode + * `MarkdownBody` so resolved URLs render with the inline status icon prefix. + */ + externalReferences?: MarkdownExternalReferenceMap; } /** Shared padding so display and edit modes occupy the exact same box. */ @@ -55,6 +60,7 @@ export function InlineEditor({ onDropFile, mentions, foldable = false, + externalReferences, }: InlineEditorProps) { const [editing, setEditing] = useState(false); const [multilineEditing, setMultilineEditing] = useState(false); @@ -288,12 +294,18 @@ export function InlineEditor({ > {foldable ? ( - + {previewValue} ) : ( - + {previewValue} )} diff --git a/ui/src/components/IssueFiltersPopover.tsx b/ui/src/components/IssueFiltersPopover.tsx index 19a844ad2c3..473689fb6d5 100644 --- a/ui/src/components/IssueFiltersPopover.tsx +++ b/ui/src/components/IssueFiltersPopover.tsx @@ -9,6 +9,8 @@ import { PriorityIcon } from "./PriorityIcon"; import { StatusIcon } from "./StatusIcon"; import { defaultIssueFilterState, + externalObjectFilterLabel, + externalObjectFilterOrder, issueFilterArraysEqual, issueFilterLabel, issuePriorityOrder, @@ -17,6 +19,8 @@ import { toggleIssueFilterValue, type IssueFilterState, } from "../lib/issue-filters"; +import { externalObjectIconForCategory } from "../lib/external-objects"; +import { externalObjectStatusIcon } from "../lib/status-colors"; import { formatAssigneeUserLabel } from "../lib/assignees"; type AgentOption = { @@ -344,6 +348,37 @@ export function IssueFiltersPopover({ ) : null} +
+ External object status +
+ {externalObjectFilterOrder.map((value) => { + const iconCategory = value === "failed" ? "failed" + : value === "waiting" ? "waiting" + : value === "running" ? "running" + : value === "auth_required" ? "auth_required" + : value === "unreachable" ? "unreachable" + : value === "stale" ? "unknown" + : "closed"; + const Icon = externalObjectIconForCategory(iconCategory); + const tone = externalObjectStatusIcon[iconCategory] ?? ""; + const textTone = tone.split(" ").filter((c) => c.startsWith("text-")).join(" "); + return ( + + ); + })} +
+
+
Visibility