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:
@@ -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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user