feat: skip pre-filled assignee/project fields when tabbing in new issue dialog

When creating a new issue with a pre-filled assignee or project (e.g. from
a project page), Tab from the title field now skips over fields that already
have values, going directly to the next empty field or description.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Dotta
2026-03-12 16:11:37 -05:00
parent c9259bbec0
commit 575a2fd83f

View File

@@ -10,6 +10,11 @@ import { assetsApi } from "../api/assets";
import { queryKeys } from "../lib/queryKeys"; import { queryKeys } from "../lib/queryKeys";
import { useProjectOrder } from "../hooks/useProjectOrder"; import { useProjectOrder } from "../hooks/useProjectOrder";
import { getRecentAssigneeIds, sortAgentsByRecency, trackRecentAssignee } from "../lib/recent-assignees"; import { getRecentAssigneeIds, sortAgentsByRecency, trackRecentAssignee } from "../lib/recent-assignees";
import {
assigneeValueFromSelection,
currentUserAssigneeOption,
parseAssigneeValue,
} from "../lib/assignees";
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@@ -63,7 +68,8 @@ interface IssueDraft {
description: string; description: string;
status: string; status: string;
priority: string; priority: string;
assigneeId: string; assigneeValue: string;
assigneeId?: string;
projectId: string; projectId: string;
assigneeModelOverride: string; assigneeModelOverride: string;
assigneeThinkingEffort: string; assigneeThinkingEffort: string;
@@ -173,7 +179,7 @@ export function NewIssueDialog() {
const [description, setDescription] = useState(""); const [description, setDescription] = useState("");
const [status, setStatus] = useState("todo"); const [status, setStatus] = useState("todo");
const [priority, setPriority] = useState(""); const [priority, setPriority] = useState("");
const [assigneeId, setAssigneeId] = useState(""); const [assigneeValue, setAssigneeValue] = useState("");
const [projectId, setProjectId] = useState(""); const [projectId, setProjectId] = useState("");
const [assigneeOptionsOpen, setAssigneeOptionsOpen] = useState(false); const [assigneeOptionsOpen, setAssigneeOptionsOpen] = useState(false);
const [assigneeModelOverride, setAssigneeModelOverride] = useState(""); const [assigneeModelOverride, setAssigneeModelOverride] = useState("");
@@ -220,7 +226,11 @@ export function NewIssueDialog() {
userId: currentUserId, userId: currentUserId,
}); });
const assigneeAdapterType = (agents ?? []).find((agent) => agent.id === assigneeId)?.adapterType ?? null; const selectedAssignee = useMemo(() => parseAssigneeValue(assigneeValue), [assigneeValue]);
const selectedAssigneeAgentId = selectedAssignee.assigneeAgentId;
const selectedAssigneeUserId = selectedAssignee.assigneeUserId;
const assigneeAdapterType = (agents ?? []).find((agent) => agent.id === selectedAssigneeAgentId)?.adapterType ?? null;
const supportsAssigneeOverrides = Boolean( const supportsAssigneeOverrides = Boolean(
assigneeAdapterType && ISSUE_OVERRIDE_ADAPTER_TYPES.has(assigneeAdapterType), assigneeAdapterType && ISSUE_OVERRIDE_ADAPTER_TYPES.has(assigneeAdapterType),
); );
@@ -295,7 +305,7 @@ export function NewIssueDialog() {
description, description,
status, status,
priority, priority,
assigneeId, assigneeValue,
projectId, projectId,
assigneeModelOverride, assigneeModelOverride,
assigneeThinkingEffort, assigneeThinkingEffort,
@@ -307,7 +317,7 @@ export function NewIssueDialog() {
description, description,
status, status,
priority, priority,
assigneeId, assigneeValue,
projectId, projectId,
assigneeModelOverride, assigneeModelOverride,
assigneeThinkingEffort, assigneeThinkingEffort,
@@ -330,7 +340,7 @@ export function NewIssueDialog() {
setStatus(newIssueDefaults.status ?? "todo"); setStatus(newIssueDefaults.status ?? "todo");
setPriority(newIssueDefaults.priority ?? ""); setPriority(newIssueDefaults.priority ?? "");
setProjectId(newIssueDefaults.projectId ?? ""); setProjectId(newIssueDefaults.projectId ?? "");
setAssigneeId(newIssueDefaults.assigneeAgentId ?? ""); setAssigneeValue(assigneeValueFromSelection(newIssueDefaults));
setAssigneeModelOverride(""); setAssigneeModelOverride("");
setAssigneeThinkingEffort(""); setAssigneeThinkingEffort("");
setAssigneeChrome(false); setAssigneeChrome(false);
@@ -340,7 +350,11 @@ export function NewIssueDialog() {
setDescription(draft.description); setDescription(draft.description);
setStatus(draft.status || "todo"); setStatus(draft.status || "todo");
setPriority(draft.priority); setPriority(draft.priority);
setAssigneeId(newIssueDefaults.assigneeAgentId ?? draft.assigneeId); setAssigneeValue(
newIssueDefaults.assigneeAgentId || newIssueDefaults.assigneeUserId
? assigneeValueFromSelection(newIssueDefaults)
: (draft.assigneeValue ?? draft.assigneeId ?? ""),
);
setProjectId(newIssueDefaults.projectId ?? draft.projectId); setProjectId(newIssueDefaults.projectId ?? draft.projectId);
setAssigneeModelOverride(draft.assigneeModelOverride ?? ""); setAssigneeModelOverride(draft.assigneeModelOverride ?? "");
setAssigneeThinkingEffort(draft.assigneeThinkingEffort ?? ""); setAssigneeThinkingEffort(draft.assigneeThinkingEffort ?? "");
@@ -350,7 +364,7 @@ export function NewIssueDialog() {
setStatus(newIssueDefaults.status ?? "todo"); setStatus(newIssueDefaults.status ?? "todo");
setPriority(newIssueDefaults.priority ?? ""); setPriority(newIssueDefaults.priority ?? "");
setProjectId(newIssueDefaults.projectId ?? ""); setProjectId(newIssueDefaults.projectId ?? "");
setAssigneeId(newIssueDefaults.assigneeAgentId ?? ""); setAssigneeValue(assigneeValueFromSelection(newIssueDefaults));
setAssigneeModelOverride(""); setAssigneeModelOverride("");
setAssigneeThinkingEffort(""); setAssigneeThinkingEffort("");
setAssigneeChrome(false); setAssigneeChrome(false);
@@ -390,7 +404,7 @@ export function NewIssueDialog() {
setDescription(""); setDescription("");
setStatus("todo"); setStatus("todo");
setPriority(""); setPriority("");
setAssigneeId(""); setAssigneeValue("");
setProjectId(""); setProjectId("");
setAssigneeOptionsOpen(false); setAssigneeOptionsOpen(false);
setAssigneeModelOverride(""); setAssigneeModelOverride("");
@@ -406,7 +420,7 @@ export function NewIssueDialog() {
function handleCompanyChange(companyId: string) { function handleCompanyChange(companyId: string) {
if (companyId === effectiveCompanyId) return; if (companyId === effectiveCompanyId) return;
setDialogCompanyId(companyId); setDialogCompanyId(companyId);
setAssigneeId(""); setAssigneeValue("");
setProjectId(""); setProjectId("");
setAssigneeModelOverride(""); setAssigneeModelOverride("");
setAssigneeThinkingEffort(""); setAssigneeThinkingEffort("");
@@ -443,7 +457,8 @@ export function NewIssueDialog() {
description: description.trim() || undefined, description: description.trim() || undefined,
status, status,
priority: priority || "medium", priority: priority || "medium",
...(assigneeId ? { assigneeAgentId: assigneeId } : {}), ...(selectedAssigneeAgentId ? { assigneeAgentId: selectedAssigneeAgentId } : {}),
...(selectedAssigneeUserId ? { assigneeUserId: selectedAssigneeUserId } : {}),
...(projectId ? { projectId } : {}), ...(projectId ? { projectId } : {}),
...(assigneeAdapterOverrides ? { assigneeAdapterOverrides } : {}), ...(assigneeAdapterOverrides ? { assigneeAdapterOverrides } : {}),
...(executionWorkspaceSettings ? { executionWorkspaceSettings } : {}), ...(executionWorkspaceSettings ? { executionWorkspaceSettings } : {}),
@@ -475,7 +490,9 @@ export function NewIssueDialog() {
const hasDraft = title.trim().length > 0 || description.trim().length > 0; const hasDraft = title.trim().length > 0 || description.trim().length > 0;
const currentStatus = statuses.find((s) => s.value === status) ?? statuses[1]!; const currentStatus = statuses.find((s) => s.value === status) ?? statuses[1]!;
const currentPriority = priorities.find((p) => p.value === priority); const currentPriority = priorities.find((p) => p.value === priority);
const currentAssignee = (agents ?? []).find((a) => a.id === assigneeId); const currentAssignee = selectedAssigneeAgentId
? (agents ?? []).find((a) => a.id === selectedAssigneeAgentId)
: null;
const currentProject = orderedProjects.find((project) => project.id === projectId); const currentProject = orderedProjects.find((project) => project.id === projectId);
const currentProjectExecutionWorkspacePolicy = SHOW_EXPERIMENTAL_ISSUE_WORKTREE_UI const currentProjectExecutionWorkspacePolicy = SHOW_EXPERIMENTAL_ISSUE_WORKTREE_UI
? currentProject?.executionWorkspacePolicy ?? null ? currentProject?.executionWorkspacePolicy ?? null
@@ -497,16 +514,18 @@ export function NewIssueDialog() {
: ISSUE_THINKING_EFFORT_OPTIONS.claude_local; : ISSUE_THINKING_EFFORT_OPTIONS.claude_local;
const recentAssigneeIds = useMemo(() => getRecentAssigneeIds(), [newIssueOpen]); const recentAssigneeIds = useMemo(() => getRecentAssigneeIds(), [newIssueOpen]);
const assigneeOptions = useMemo<InlineEntityOption[]>( const assigneeOptions = useMemo<InlineEntityOption[]>(
() => () => [
sortAgentsByRecency( ...currentUserAssigneeOption(currentUserId),
...sortAgentsByRecency(
(agents ?? []).filter((agent) => agent.status !== "terminated"), (agents ?? []).filter((agent) => agent.status !== "terminated"),
recentAssigneeIds, recentAssigneeIds,
).map((agent) => ({ ).map((agent) => ({
id: agent.id, id: assigneeValueFromSelection({ assigneeAgentId: agent.id }),
label: agent.name, label: agent.name,
searchText: `${agent.name} ${agent.role} ${agent.title ?? ""}`, searchText: `${agent.name} ${agent.role} ${agent.title ?? ""}`,
})), })),
[agents, recentAssigneeIds], ],
[agents, currentUserId, recentAssigneeIds],
); );
const projectOptions = useMemo<InlineEntityOption[]>( const projectOptions = useMemo<InlineEntityOption[]>(
() => () =>
@@ -710,7 +729,16 @@ export function NewIssueDialog() {
} }
if (e.key === "Tab" && !e.shiftKey) { if (e.key === "Tab" && !e.shiftKey) {
e.preventDefault(); e.preventDefault();
assigneeSelectorRef.current?.focus(); if (assigneeValue) {
// Assignee already set — skip to project or description
if (projectId) {
descriptionEditorRef.current?.focus();
} else {
projectSelectorRef.current?.focus();
}
} else {
assigneeSelectorRef.current?.focus();
}
} }
}} }}
autoFocus autoFocus
@@ -723,33 +751,49 @@ export function NewIssueDialog() {
<span>For</span> <span>For</span>
<InlineEntitySelector <InlineEntitySelector
ref={assigneeSelectorRef} ref={assigneeSelectorRef}
value={assigneeId} value={assigneeValue}
options={assigneeOptions} options={assigneeOptions}
placeholder="Assignee" placeholder="Assignee"
disablePortal disablePortal
noneLabel="No assignee" noneLabel="No assignee"
searchPlaceholder="Search assignees..." searchPlaceholder="Search assignees..."
emptyMessage="No assignees found." emptyMessage="No assignees found."
onChange={(id) => { if (id) trackRecentAssignee(id); setAssigneeId(id); }} onChange={(value) => {
const nextAssignee = parseAssigneeValue(value);
if (nextAssignee.assigneeAgentId) {
trackRecentAssignee(nextAssignee.assigneeAgentId);
}
setAssigneeValue(value);
}}
onConfirm={() => { onConfirm={() => {
projectSelectorRef.current?.focus(); if (projectId) {
descriptionEditorRef.current?.focus();
} else {
projectSelectorRef.current?.focus();
}
}} }}
renderTriggerValue={(option) => renderTriggerValue={(option) =>
option && currentAssignee ? ( option ? (
<> currentAssignee ? (
<AgentIcon icon={currentAssignee.icon} className="h-3.5 w-3.5 shrink-0 text-muted-foreground" /> <>
<AgentIcon icon={currentAssignee.icon} className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
<span className="truncate">{option.label}</span>
</>
) : (
<span className="truncate">{option.label}</span> <span className="truncate">{option.label}</span>
</> )
) : ( ) : (
<span className="text-muted-foreground">Assignee</span> <span className="text-muted-foreground">Assignee</span>
) )
} }
renderOption={(option) => { renderOption={(option) => {
if (!option.id) return <span className="truncate">{option.label}</span>; if (!option.id) return <span className="truncate">{option.label}</span>;
const assignee = (agents ?? []).find((agent) => agent.id === option.id); const assignee = parseAssigneeValue(option.id).assigneeAgentId
? (agents ?? []).find((agent) => agent.id === parseAssigneeValue(option.id).assigneeAgentId)
: null;
return ( return (
<> <>
<AgentIcon icon={assignee?.icon} className="h-3.5 w-3.5 shrink-0 text-muted-foreground" /> {assignee ? <AgentIcon icon={assignee.icon} className="h-3.5 w-3.5 shrink-0 text-muted-foreground" /> : null}
<span className="truncate">{option.label}</span> <span className="truncate">{option.label}</span>
</> </>
); );