diff --git a/ui/src/components/NewIssueDialog.test.tsx b/ui/src/components/NewIssueDialog.test.tsx index 0285c4306ed..f938af87e53 100644 --- a/ui/src/components/NewIssueDialog.test.tsx +++ b/ui/src/components/NewIssueDialog.test.tsx @@ -372,6 +372,56 @@ describe("NewIssueDialog", () => { act(() => root.unmount()); }); + it("submits the latest locally typed title and description", async () => { + const { root } = renderDialog(container); + await flush(); + + const titleInput = container.querySelector('textarea[placeholder="Issue title"]') as HTMLTextAreaElement | null; + const descriptionInput = container.querySelector('textarea[aria-label="Add description..."]') as HTMLTextAreaElement | null; + expect(titleInput).not.toBeNull(); + expect(descriptionInput).not.toBeNull(); + + await act(async () => { + const valueSetter = Object.getOwnPropertyDescriptor( + HTMLTextAreaElement.prototype, + "value", + )?.set; + valueSetter?.call(titleInput, "Typed issue"); + titleInput!.dispatchEvent(new Event("input", { bubbles: true })); + }); + await flush(); + + await act(async () => { + const valueSetter = Object.getOwnPropertyDescriptor( + HTMLTextAreaElement.prototype, + "value", + )?.set; + valueSetter?.call(descriptionInput, "Typed description"); + descriptionInput!.dispatchEvent(new Event("input", { bubbles: true })); + }); + await flush(); + + const submitButton = Array.from(container.querySelectorAll("button")) + .find((button) => button.textContent?.includes("Create Issue")); + expect(submitButton).not.toBeUndefined(); + expect(submitButton?.hasAttribute("disabled")).toBe(false); + + await act(async () => { + submitButton!.dispatchEvent(new MouseEvent("click", { bubbles: true })); + }); + await flush(); + + expect(mockIssuesApi.create).toHaveBeenCalledWith( + "company-1", + expect.objectContaining({ + title: "Typed issue", + description: "Typed description", + }), + ); + + act(() => root.unmount()); + }); + it("submits the parent assignee when a sub-issue opens with inherited defaults", async () => { dialogState.newIssueDefaults = { parentId: "issue-1", diff --git a/ui/src/components/NewIssueDialog.tsx b/ui/src/components/NewIssueDialog.tsx index fa91a9c1434..b5623322acf 100644 --- a/ui/src/components/NewIssueDialog.tsx +++ b/ui/src/components/NewIssueDialog.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useRef, useCallback, useMemo, type ChangeEvent, type DragEvent } from "react"; +import { memo, useState, useEffect, useRef, useCallback, useMemo, type ChangeEvent, type DragEvent, type RefObject } from "react"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { pickTextColorForSolidBg } from "@/lib/color-contrast"; import { useDialog } from "../context/DialogContext"; @@ -269,6 +269,111 @@ function defaultExecutionWorkspaceModeForProject(project: { executionWorkspacePo return "shared_workspace"; } +const IssueTitleTextarea = memo(function IssueTitleTextarea({ + value, + pending, + assigneeValue, + projectId, + descriptionEditorRef, + assigneeSelectorRef, + projectSelectorRef, + onChange, +}: { + value: string; + pending: boolean; + assigneeValue: string; + projectId: string; + descriptionEditorRef: RefObject; + assigneeSelectorRef: RefObject; + projectSelectorRef: RefObject; + onChange: (value: string) => void; +}) { + const [draftValue, setDraftValue] = useState(value); + + useEffect(() => { + setDraftValue(value); + }, [value]); + + return ( +