diff --git a/ui/src/components/IssueProperties.test.tsx b/ui/src/components/IssueProperties.test.tsx
index c4c37a9053..0427bfb6bc 100644
--- a/ui/src/components/IssueProperties.test.tsx
+++ b/ui/src/components/IssueProperties.test.tsx
@@ -2039,6 +2039,82 @@ describe("IssueProperties", () => {
act(() => rerenderedRoot.unmount());
});
+
+ it("searches the server for parent candidates so a lower-priority match past the default page stays selectable", async () => {
+ const onUpdate = vi.fn();
+ // The default list is priority-first and caps at 500 rows, so a low-priority
+ // match past that cap never enters the client list. Model that gap: the default
+ // page returns only a high-priority candidate, and the server search with `q`
+ // returns the low-priority match that the default page hides.
+ const defaultPageCandidate = createIssue({
+ id: "issue-2",
+ identifier: "PAP-2",
+ title: "High priority candidate",
+ status: "in_progress",
+ priority: "high",
+ });
+ const lowPriorityMatch = createIssue({
+ id: "issue-900",
+ identifier: "PAP-900",
+ title: "Low priority needle",
+ status: "todo",
+ priority: "low",
+ });
+ mockIssuesApi.list.mockImplementation((_companyId: string, filters?: { q?: string; limit?: number }) => {
+ if (filters?.q === "needle") return Promise.resolve([lowPriorityMatch]);
+ return Promise.resolve([defaultPageCandidate]);
+ });
+
+ const root = renderProperties(container, {
+ issue: createIssue(),
+ childIssues: [],
+ onUpdate,
+ inline: true,
+ });
+ await flush();
+
+ const parentTrigger = findRowTrigger(container, "Parent");
+ expect(parentTrigger).not.toBeUndefined();
+ await act(async () => {
+ parentTrigger!.dispatchEvent(new MouseEvent("click", { bubbles: true }));
+ });
+ await flush();
+
+ // With an empty search the picker shows the default page. The low-priority match is absent.
+ await waitForAssertion(() => {
+ expect(container.textContent).toContain("PAP-2 High priority candidate");
+ });
+ expect(container.textContent).not.toContain("PAP-900 Low priority needle");
+
+ const searchInput = container.querySelector('input[placeholder="Search tasks..."]') as HTMLInputElement | null;
+ expect(searchInput).not.toBeNull();
+
+ await act(async () => {
+ const nativeSetter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value")?.set;
+ nativeSetter?.call(searchInput, "needle");
+ searchInput!.dispatchEvent(new Event("input", { bubbles: true }));
+ });
+
+ // The typed text re-queries the server with `q`, and the low-priority match now appears.
+ await waitForAssertion(() => {
+ expect(mockIssuesApi.list).toHaveBeenCalledWith("company-1", { q: "needle", limit: 50 });
+ expect(container.textContent).toContain("PAP-900 Low priority needle");
+ expect(container.textContent).not.toContain("PAP-2 High priority candidate");
+ });
+
+ const candidateButton = Array.from(container.querySelectorAll("button"))
+ .find((button) => button.textContent?.includes("PAP-900 Low priority needle"));
+ expect(candidateButton).not.toBeUndefined();
+
+ await act(async () => {
+ candidateButton!.dispatchEvent(new MouseEvent("click", { bubbles: true }));
+ });
+
+ expect(onUpdate).toHaveBeenCalledWith({ parentId: "issue-900" });
+
+ act(() => root.unmount());
+ });
+
it("shows a run review action after reviewers are configured and starts execution explicitly when clicked", async () => {
const onUpdate = vi.fn();
const root = renderProperties(container, {
diff --git a/ui/src/components/issue-properties/IssueProperties.tsx b/ui/src/components/issue-properties/IssueProperties.tsx
index ff32ed668c..5fa9ada2be 100644
--- a/ui/src/components/issue-properties/IssueProperties.tsx
+++ b/ui/src/components/issue-properties/IssueProperties.tsx
@@ -287,6 +287,7 @@ export function IssueProperties({
const [watchdogAgentInput, setWatchdogAgentInput] = useState(issue.watchdog?.watchdogAgentId ?? "");
const [watchdogInstructionsInput, setWatchdogInstructionsInput] = useState(issue.watchdog?.instructions ?? "");
const normalizedBlockedBySearch = blockedBySearch.trim();
+ const normalizedParentSearch = parentSearch.trim();
useEffect(() => {
setBlockedByExpanded(false);
@@ -349,6 +350,17 @@ export function IssueProperties({
enabled: !!companyId && blockedByOpen && normalizedBlockedBySearch.length > 0,
});
+ const { data: searchedParentIssues, isFetching: isFetchingSearchedParentIssues } = useQuery({
+ queryKey: companyId
+ ? queryKeys.issues.search(companyId, normalizedParentSearch, undefined, ISSUE_BLOCKER_SEARCH_LIMIT)
+ : ["issues", "blocker-search", normalizedParentSearch, ISSUE_BLOCKER_SEARCH_LIMIT],
+ queryFn: () => issuesApi.list(companyId!, {
+ q: normalizedParentSearch,
+ limit: ISSUE_BLOCKER_SEARCH_LIMIT,
+ }),
+ enabled: !!companyId && parentOpen && normalizedParentSearch.length > 0,
+ });
+
const createLabel = useMutation({
mutationFn: (data: { name: string; color: string }) => issuesApi.createLabel(companyId!, data),
onSuccess: async (created) => {
@@ -1898,22 +1910,22 @@ export function IssueProperties({