import { useState, useEffect, useRef, useCallback, useMemo, type ChangeEvent, type DragEvent } from "react"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { useDialog } from "../context/DialogContext"; import { useCompany } from "../context/CompanyContext"; import { issuesApi } from "../api/issues"; import { projectsApi } from "../api/projects"; import { agentsApi } from "../api/agents"; import { authApi } from "../api/auth"; import { assetsApi } from "../api/assets"; import { queryKeys } from "../lib/queryKeys"; import { useProjectOrder } from "../hooks/useProjectOrder"; import { getRecentAssigneeIds, sortAgentsByRecency, trackRecentAssignee } from "../lib/recent-assignees"; import { useToast } from "../context/ToastContext"; import { assigneeValueFromSelection, currentUserAssigneeOption, parseAssigneeValue, } from "../lib/assignees"; import { Dialog, DialogContent, } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; import { Maximize2, Minimize2, MoreHorizontal, ChevronRight, ChevronDown, CircleDot, Minus, ArrowUp, ArrowDown, AlertTriangle, Tag, Calendar, Paperclip, FileText, Loader2, X, } from "lucide-react"; import { cn } from "../lib/utils"; import { extractProviderIdWithFallback } from "../lib/model-utils"; import { issueStatusText, issueStatusTextDefault, priorityColor, priorityColorDefault } from "../lib/status-colors"; import { MarkdownEditor, type MarkdownEditorRef, type MentionOption } from "./MarkdownEditor"; import { AgentIcon } from "./AgentIconPicker"; import { InlineEntitySelector, type InlineEntityOption } from "./InlineEntitySelector"; const DRAFT_KEY = "paperclip:issue-draft"; const DEBOUNCE_MS = 800; // TODO(issue-worktree-support): re-enable this UI once the workflow is ready to ship. const SHOW_EXPERIMENTAL_ISSUE_WORKTREE_UI = false; /** Return black or white hex based on background luminance (WCAG perceptual weights). */ function getContrastTextColor(hexColor: string): string { const hex = hexColor.replace("#", ""); const r = parseInt(hex.substring(0, 2), 16); const g = parseInt(hex.substring(2, 4), 16); const b = parseInt(hex.substring(4, 6), 16); const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255; return luminance > 0.5 ? "#000000" : "#ffffff"; } interface IssueDraft { title: string; description: string; status: string; priority: string; assigneeValue: string; assigneeId?: string; projectId: string; assigneeModelOverride: string; assigneeThinkingEffort: string; assigneeChrome: boolean; useIsolatedExecutionWorkspace: boolean; } type StagedIssueFile = { id: string; file: File; kind: "document" | "attachment"; documentKey?: string; title?: string | null; }; const ISSUE_OVERRIDE_ADAPTER_TYPES = new Set(["claude_local", "codex_local", "opencode_local"]); const STAGED_FILE_ACCEPT = "image/*,application/pdf,text/plain,text/markdown,application/json,text/csv,text/html,.md,.markdown"; const ISSUE_THINKING_EFFORT_OPTIONS = { claude_local: [ { value: "", label: "Default" }, { value: "low", label: "Low" }, { value: "medium", label: "Medium" }, { value: "high", label: "High" }, ], codex_local: [ { value: "", label: "Default" }, { value: "minimal", label: "Minimal" }, { value: "low", label: "Low" }, { value: "medium", label: "Medium" }, { value: "high", label: "High" }, ], opencode_local: [ { value: "", label: "Default" }, { value: "minimal", label: "Minimal" }, { value: "low", label: "Low" }, { value: "medium", label: "Medium" }, { value: "high", label: "High" }, { value: "max", label: "Max" }, ], } as const; function buildAssigneeAdapterOverrides(input: { adapterType: string | null | undefined; modelOverride: string; thinkingEffortOverride: string; chrome: boolean; }): Record | null { const adapterType = input.adapterType ?? null; if (!adapterType || !ISSUE_OVERRIDE_ADAPTER_TYPES.has(adapterType)) { return null; } const adapterConfig: Record = {}; if (input.modelOverride) adapterConfig.model = input.modelOverride; if (input.thinkingEffortOverride) { if (adapterType === "codex_local") { adapterConfig.modelReasoningEffort = input.thinkingEffortOverride; } else if (adapterType === "opencode_local") { adapterConfig.variant = input.thinkingEffortOverride; } else if (adapterType === "claude_local") { adapterConfig.effort = input.thinkingEffortOverride; } else if (adapterType === "opencode_local") { adapterConfig.variant = input.thinkingEffortOverride; } } if (adapterType === "claude_local" && input.chrome) { adapterConfig.chrome = true; } const overrides: Record = {}; if (Object.keys(adapterConfig).length > 0) { overrides.adapterConfig = adapterConfig; } return Object.keys(overrides).length > 0 ? overrides : null; } function loadDraft(): IssueDraft | null { try { const raw = localStorage.getItem(DRAFT_KEY); if (!raw) return null; return JSON.parse(raw) as IssueDraft; } catch { return null; } } function saveDraft(draft: IssueDraft) { localStorage.setItem(DRAFT_KEY, JSON.stringify(draft)); } function clearDraft() { localStorage.removeItem(DRAFT_KEY); } function isTextDocumentFile(file: File) { const name = file.name.toLowerCase(); return ( name.endsWith(".md") || name.endsWith(".markdown") || name.endsWith(".txt") || file.type === "text/markdown" || file.type === "text/plain" ); } function fileBaseName(filename: string) { return filename.replace(/\.[^.]+$/, ""); } function slugifyDocumentKey(input: string) { const slug = input .trim() .toLowerCase() .replace(/[^a-z0-9]+/g, "-") .replace(/^-+|-+$/g, ""); return slug || "document"; } function titleizeFilename(input: string) { return input .split(/[-_ ]+/g) .filter(Boolean) .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) .join(" "); } function createUniqueDocumentKey(baseKey: string, stagedFiles: StagedIssueFile[]) { const existingKeys = new Set( stagedFiles .filter((file) => file.kind === "document") .map((file) => file.documentKey) .filter((key): key is string => Boolean(key)), ); if (!existingKeys.has(baseKey)) return baseKey; let suffix = 2; while (existingKeys.has(`${baseKey}-${suffix}`)) { suffix += 1; } return `${baseKey}-${suffix}`; } function formatFileSize(file: File) { if (file.size < 1024) return `${file.size} B`; if (file.size < 1024 * 1024) return `${(file.size / 1024).toFixed(1)} KB`; return `${(file.size / (1024 * 1024)).toFixed(1)} MB`; } const statuses = [ { value: "backlog", label: "Backlog", color: issueStatusText.backlog ?? issueStatusTextDefault }, { value: "todo", label: "Todo", color: issueStatusText.todo ?? issueStatusTextDefault }, { value: "in_progress", label: "In Progress", color: issueStatusText.in_progress ?? issueStatusTextDefault }, { value: "in_review", label: "In Review", color: issueStatusText.in_review ?? issueStatusTextDefault }, { value: "done", label: "Done", color: issueStatusText.done ?? issueStatusTextDefault }, ]; const priorities = [ { value: "critical", label: "Critical", icon: AlertTriangle, color: priorityColor.critical ?? priorityColorDefault }, { value: "high", label: "High", icon: ArrowUp, color: priorityColor.high ?? priorityColorDefault }, { value: "medium", label: "Medium", icon: Minus, color: priorityColor.medium ?? priorityColorDefault }, { value: "low", label: "Low", icon: ArrowDown, color: priorityColor.low ?? priorityColorDefault }, ]; export function NewIssueDialog() { const { newIssueOpen, newIssueDefaults, closeNewIssue } = useDialog(); const { companies, selectedCompanyId, selectedCompany } = useCompany(); const queryClient = useQueryClient(); const { pushToast } = useToast(); const [title, setTitle] = useState(""); const [description, setDescription] = useState(""); const [status, setStatus] = useState("todo"); const [priority, setPriority] = useState(""); const [assigneeValue, setAssigneeValue] = useState(""); const [projectId, setProjectId] = useState(""); const [assigneeOptionsOpen, setAssigneeOptionsOpen] = useState(false); const [assigneeModelOverride, setAssigneeModelOverride] = useState(""); const [assigneeThinkingEffort, setAssigneeThinkingEffort] = useState(""); const [assigneeChrome, setAssigneeChrome] = useState(false); const [useIsolatedExecutionWorkspace, setUseIsolatedExecutionWorkspace] = useState(false); const [expanded, setExpanded] = useState(false); const [dialogCompanyId, setDialogCompanyId] = useState(null); const [stagedFiles, setStagedFiles] = useState([]); const [isFileDragOver, setIsFileDragOver] = useState(false); const draftTimer = useRef | null>(null); const executionWorkspaceDefaultProjectId = useRef(null); const effectiveCompanyId = dialogCompanyId ?? selectedCompanyId; const dialogCompany = companies.find((c) => c.id === effectiveCompanyId) ?? selectedCompany; // Popover states const [statusOpen, setStatusOpen] = useState(false); const [priorityOpen, setPriorityOpen] = useState(false); const [moreOpen, setMoreOpen] = useState(false); const [companyOpen, setCompanyOpen] = useState(false); const descriptionEditorRef = useRef(null); const stageFileInputRef = useRef(null); const assigneeSelectorRef = useRef(null); const projectSelectorRef = useRef(null); const { data: agents } = useQuery({ queryKey: queryKeys.agents.list(effectiveCompanyId!), queryFn: () => agentsApi.list(effectiveCompanyId!), enabled: !!effectiveCompanyId && newIssueOpen, }); const { data: projects } = useQuery({ queryKey: queryKeys.projects.list(effectiveCompanyId!), queryFn: () => projectsApi.list(effectiveCompanyId!), enabled: !!effectiveCompanyId && newIssueOpen, }); const { data: session } = useQuery({ queryKey: queryKeys.auth.session, queryFn: () => authApi.getSession(), }); const currentUserId = session?.user?.id ?? session?.session?.userId ?? null; const activeProjects = useMemo( () => (projects ?? []).filter((p) => !p.archivedAt), [projects], ); const { orderedProjects } = useProjectOrder({ projects: activeProjects, companyId: effectiveCompanyId, userId: currentUserId, }); 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( assigneeAdapterType && ISSUE_OVERRIDE_ADAPTER_TYPES.has(assigneeAdapterType), ); const mentionOptions = useMemo(() => { const options: MentionOption[] = []; const activeAgents = [...(agents ?? [])] .filter((agent) => agent.status !== "terminated") .sort((a, b) => a.name.localeCompare(b.name)); for (const agent of activeAgents) { options.push({ id: `agent:${agent.id}`, name: agent.name, kind: "agent", }); } for (const project of orderedProjects) { options.push({ id: `project:${project.id}`, name: project.name, kind: "project", projectId: project.id, projectColor: project.color, }); } return options; }, [agents, orderedProjects]); const { data: assigneeAdapterModels } = useQuery({ queryKey: effectiveCompanyId && assigneeAdapterType ? queryKeys.agents.adapterModels(effectiveCompanyId, assigneeAdapterType) : ["agents", "none", "adapter-models", assigneeAdapterType ?? "none"], queryFn: () => agentsApi.adapterModels(effectiveCompanyId!, assigneeAdapterType!), enabled: Boolean(effectiveCompanyId) && newIssueOpen && supportsAssigneeOverrides, }); const createIssue = useMutation({ mutationFn: async ({ companyId, stagedFiles: pendingStagedFiles, ...data }: { companyId: string; stagedFiles: StagedIssueFile[] } & Record) => { const issue = await issuesApi.create(companyId, data); const failures: string[] = []; for (const stagedFile of pendingStagedFiles) { try { if (stagedFile.kind === "document") { const body = await stagedFile.file.text(); await issuesApi.upsertDocument(issue.id, stagedFile.documentKey ?? "document", { title: stagedFile.documentKey === "plan" ? null : stagedFile.title ?? null, format: "markdown", body, baseRevisionId: null, }); } else { await issuesApi.uploadAttachment(companyId, issue.id, stagedFile.file); } } catch { failures.push(stagedFile.file.name); } } return { issue, companyId, failures }; }, onSuccess: ({ issue, companyId, failures }) => { queryClient.invalidateQueries({ queryKey: queryKeys.issues.list(companyId) }); if (draftTimer.current) clearTimeout(draftTimer.current); if (failures.length > 0) { const prefix = (companies.find((company) => company.id === companyId)?.issuePrefix ?? "").trim(); const issueRef = issue.identifier ?? issue.id; pushToast({ title: `Created ${issueRef} with upload warnings`, body: `${failures.length} staged ${failures.length === 1 ? "file" : "files"} could not be added.`, tone: "warn", action: prefix ? { label: `Open ${issueRef}`, href: `/${prefix}/issues/${issueRef}` } : undefined, }); } clearDraft(); reset(); closeNewIssue(); }, }); const uploadDescriptionImage = useMutation({ mutationFn: async (file: File) => { if (!effectiveCompanyId) throw new Error("No company selected"); return assetsApi.uploadImage(effectiveCompanyId, file, "issues/drafts"); }, }); // Debounced draft saving const scheduleSave = useCallback( (draft: IssueDraft) => { if (draftTimer.current) clearTimeout(draftTimer.current); draftTimer.current = setTimeout(() => { if (draft.title.trim()) saveDraft(draft); }, DEBOUNCE_MS); }, [], ); // Save draft on meaningful changes useEffect(() => { if (!newIssueOpen) return; scheduleSave({ title, description, status, priority, assigneeValue, projectId, assigneeModelOverride, assigneeThinkingEffort, assigneeChrome, useIsolatedExecutionWorkspace, }); }, [ title, description, status, priority, assigneeValue, projectId, assigneeModelOverride, assigneeThinkingEffort, assigneeChrome, useIsolatedExecutionWorkspace, newIssueOpen, scheduleSave, ]); // Restore draft or apply defaults when dialog opens useEffect(() => { if (!newIssueOpen) return; setDialogCompanyId(selectedCompanyId); executionWorkspaceDefaultProjectId.current = null; const draft = loadDraft(); if (newIssueDefaults.title) { setTitle(newIssueDefaults.title); setDescription(newIssueDefaults.description ?? ""); setStatus(newIssueDefaults.status ?? "todo"); setPriority(newIssueDefaults.priority ?? ""); setProjectId(newIssueDefaults.projectId ?? ""); setAssigneeValue(assigneeValueFromSelection(newIssueDefaults)); setAssigneeModelOverride(""); setAssigneeThinkingEffort(""); setAssigneeChrome(false); setUseIsolatedExecutionWorkspace(false); } else if (draft && draft.title.trim()) { setTitle(draft.title); setDescription(draft.description); setStatus(draft.status || "todo"); setPriority(draft.priority); setAssigneeValue( newIssueDefaults.assigneeAgentId || newIssueDefaults.assigneeUserId ? assigneeValueFromSelection(newIssueDefaults) : (draft.assigneeValue ?? draft.assigneeId ?? ""), ); setProjectId(newIssueDefaults.projectId ?? draft.projectId); setAssigneeModelOverride(draft.assigneeModelOverride ?? ""); setAssigneeThinkingEffort(draft.assigneeThinkingEffort ?? ""); setAssigneeChrome(draft.assigneeChrome ?? false); setUseIsolatedExecutionWorkspace(draft.useIsolatedExecutionWorkspace ?? false); } else { setStatus(newIssueDefaults.status ?? "todo"); setPriority(newIssueDefaults.priority ?? ""); setProjectId(newIssueDefaults.projectId ?? ""); setAssigneeValue(assigneeValueFromSelection(newIssueDefaults)); setAssigneeModelOverride(""); setAssigneeThinkingEffort(""); setAssigneeChrome(false); setUseIsolatedExecutionWorkspace(false); } }, [newIssueOpen, newIssueDefaults]); useEffect(() => { if (!supportsAssigneeOverrides) { setAssigneeOptionsOpen(false); setAssigneeModelOverride(""); setAssigneeThinkingEffort(""); setAssigneeChrome(false); return; } const validThinkingValues = assigneeAdapterType === "codex_local" ? ISSUE_THINKING_EFFORT_OPTIONS.codex_local : assigneeAdapterType === "opencode_local" ? ISSUE_THINKING_EFFORT_OPTIONS.opencode_local : ISSUE_THINKING_EFFORT_OPTIONS.claude_local; if (!validThinkingValues.some((option) => option.value === assigneeThinkingEffort)) { setAssigneeThinkingEffort(""); } }, [supportsAssigneeOverrides, assigneeAdapterType, assigneeThinkingEffort]); // Cleanup timer on unmount useEffect(() => { return () => { if (draftTimer.current) clearTimeout(draftTimer.current); }; }, []); function reset() { setTitle(""); setDescription(""); setStatus("todo"); setPriority(""); setAssigneeValue(""); setProjectId(""); setAssigneeOptionsOpen(false); setAssigneeModelOverride(""); setAssigneeThinkingEffort(""); setAssigneeChrome(false); setUseIsolatedExecutionWorkspace(false); setExpanded(false); setDialogCompanyId(null); setStagedFiles([]); setIsFileDragOver(false); setCompanyOpen(false); executionWorkspaceDefaultProjectId.current = null; } function handleCompanyChange(companyId: string) { if (companyId === effectiveCompanyId) return; setDialogCompanyId(companyId); setAssigneeValue(""); setProjectId(""); setAssigneeModelOverride(""); setAssigneeThinkingEffort(""); setAssigneeChrome(false); setUseIsolatedExecutionWorkspace(false); } function discardDraft() { clearDraft(); reset(); closeNewIssue(); } function handleSubmit() { if (!effectiveCompanyId || !title.trim() || createIssue.isPending) return; const assigneeAdapterOverrides = buildAssigneeAdapterOverrides({ adapterType: assigneeAdapterType, modelOverride: assigneeModelOverride, thinkingEffortOverride: assigneeThinkingEffort, chrome: assigneeChrome, }); const selectedProject = orderedProjects.find((project) => project.id === projectId); const executionWorkspacePolicy = SHOW_EXPERIMENTAL_ISSUE_WORKTREE_UI ? selectedProject?.executionWorkspacePolicy : null; const executionWorkspaceSettings = executionWorkspacePolicy?.enabled ? { mode: useIsolatedExecutionWorkspace ? "isolated" : "project_primary", } : null; createIssue.mutate({ companyId: effectiveCompanyId, stagedFiles, title: title.trim(), description: description.trim() || undefined, status, priority: priority || "medium", ...(selectedAssigneeAgentId ? { assigneeAgentId: selectedAssigneeAgentId } : {}), ...(selectedAssigneeUserId ? { assigneeUserId: selectedAssigneeUserId } : {}), ...(projectId ? { projectId } : {}), ...(assigneeAdapterOverrides ? { assigneeAdapterOverrides } : {}), ...(executionWorkspaceSettings ? { executionWorkspaceSettings } : {}), }); } function handleKeyDown(e: React.KeyboardEvent) { if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) { e.preventDefault(); handleSubmit(); } } function stageFiles(files: File[]) { if (files.length === 0) return; setStagedFiles((current) => { const next = [...current]; for (const file of files) { if (isTextDocumentFile(file)) { const baseName = fileBaseName(file.name); const documentKey = createUniqueDocumentKey(slugifyDocumentKey(baseName), next); next.push({ id: `${file.name}:${file.size}:${file.lastModified}:${documentKey}`, file, kind: "document", documentKey, title: titleizeFilename(baseName), }); continue; } next.push({ id: `${file.name}:${file.size}:${file.lastModified}`, file, kind: "attachment", }); } return next; }); } function handleStageFilesPicked(evt: ChangeEvent) { stageFiles(Array.from(evt.target.files ?? [])); if (stageFileInputRef.current) { stageFileInputRef.current.value = ""; } } function handleFileDragEnter(evt: DragEvent) { if (!evt.dataTransfer.types.includes("Files")) return; evt.preventDefault(); setIsFileDragOver(true); } function handleFileDragOver(evt: DragEvent) { if (!evt.dataTransfer.types.includes("Files")) return; evt.preventDefault(); evt.dataTransfer.dropEffect = "copy"; setIsFileDragOver(true); } function handleFileDragLeave(evt: DragEvent) { if (evt.currentTarget.contains(evt.relatedTarget as Node | null)) return; setIsFileDragOver(false); } function handleFileDrop(evt: DragEvent) { if (!evt.dataTransfer.files.length) return; evt.preventDefault(); setIsFileDragOver(false); stageFiles(Array.from(evt.dataTransfer.files)); } function removeStagedFile(id: string) { setStagedFiles((current) => current.filter((file) => file.id !== id)); } const hasDraft = title.trim().length > 0 || description.trim().length > 0 || stagedFiles.length > 0; const currentStatus = statuses.find((s) => s.value === status) ?? statuses[1]!; const currentPriority = priorities.find((p) => p.value === priority); const currentAssignee = selectedAssigneeAgentId ? (agents ?? []).find((a) => a.id === selectedAssigneeAgentId) : null; const currentProject = orderedProjects.find((project) => project.id === projectId); const currentProjectExecutionWorkspacePolicy = SHOW_EXPERIMENTAL_ISSUE_WORKTREE_UI ? currentProject?.executionWorkspacePolicy ?? null : null; const currentProjectSupportsExecutionWorkspace = Boolean(currentProjectExecutionWorkspacePolicy?.enabled); const assigneeOptionsTitle = assigneeAdapterType === "claude_local" ? "Claude options" : assigneeAdapterType === "codex_local" ? "Codex options" : assigneeAdapterType === "opencode_local" ? "OpenCode options" : "Agent options"; const thinkingEffortOptions = assigneeAdapterType === "codex_local" ? ISSUE_THINKING_EFFORT_OPTIONS.codex_local : assigneeAdapterType === "opencode_local" ? ISSUE_THINKING_EFFORT_OPTIONS.opencode_local : ISSUE_THINKING_EFFORT_OPTIONS.claude_local; const recentAssigneeIds = useMemo(() => getRecentAssigneeIds(), [newIssueOpen]); const assigneeOptions = useMemo( () => [ ...currentUserAssigneeOption(currentUserId), ...sortAgentsByRecency( (agents ?? []).filter((agent) => agent.status !== "terminated"), recentAssigneeIds, ).map((agent) => ({ id: assigneeValueFromSelection({ assigneeAgentId: agent.id }), label: agent.name, searchText: `${agent.name} ${agent.role} ${agent.title ?? ""}`, })), ], [agents, currentUserId, recentAssigneeIds], ); const projectOptions = useMemo( () => orderedProjects.map((project) => ({ id: project.id, label: project.name, searchText: project.description ?? "", })), [orderedProjects], ); const savedDraft = loadDraft(); const hasSavedDraft = Boolean(savedDraft?.title.trim() || savedDraft?.description.trim()); const canDiscardDraft = hasDraft || hasSavedDraft; const createIssueErrorMessage = createIssue.error instanceof Error ? createIssue.error.message : "Failed to create issue. Try again."; const stagedDocuments = stagedFiles.filter((file) => file.kind === "document"); const stagedAttachments = stagedFiles.filter((file) => file.kind === "attachment"); const handleProjectChange = useCallback((nextProjectId: string) => { setProjectId(nextProjectId); const nextProject = orderedProjects.find((project) => project.id === nextProjectId); const policy = SHOW_EXPERIMENTAL_ISSUE_WORKTREE_UI ? nextProject?.executionWorkspacePolicy : null; executionWorkspaceDefaultProjectId.current = nextProjectId || null; setUseIsolatedExecutionWorkspace(Boolean(policy?.enabled && policy.defaultMode === "isolated")); }, [orderedProjects]); useEffect(() => { if (!newIssueOpen || !projectId || executionWorkspaceDefaultProjectId.current === projectId) { return; } const project = orderedProjects.find((entry) => entry.id === projectId); if (!project) return; executionWorkspaceDefaultProjectId.current = projectId; setUseIsolatedExecutionWorkspace( Boolean( SHOW_EXPERIMENTAL_ISSUE_WORKTREE_UI && project.executionWorkspacePolicy?.enabled && project.executionWorkspacePolicy.defaultMode === "isolated", ), ); }, [newIssueOpen, orderedProjects, projectId]); const modelOverrideOptions = useMemo( () => { return [...(assigneeAdapterModels ?? [])] .sort((a, b) => { const providerA = extractProviderIdWithFallback(a.id); const providerB = extractProviderIdWithFallback(b.id); const byProvider = providerA.localeCompare(providerB); if (byProvider !== 0) return byProvider; return a.id.localeCompare(b.id); }) .map((model) => ({ id: model.id, label: model.label, searchText: `${model.id} ${extractProviderIdWithFallback(model.id)}`, })); }, [assigneeAdapterModels], ); return ( { if (!open && !createIssue.isPending) closeNewIssue(); }} > { if (createIssue.isPending) { event.preventDefault(); } }} onPointerDownOutside={(event) => { if (createIssue.isPending) { event.preventDefault(); return; } // Radix Dialog's modal DismissableLayer calls preventDefault() on // pointerdown events that originate outside the Dialog DOM tree. // Popover portals render at the body level (outside the Dialog), so // touch events on popover content get their default prevented — which // kills scroll gesture recognition on mobile. Telling Radix "this // event is handled" skips that preventDefault, restoring touch scroll. const target = event.detail.originalEvent.target as HTMLElement | null; if (target?.closest("[data-radix-popper-content-wrapper]")) { event.preventDefault(); } }} > {/* Header bar */}
{companies.filter((c) => c.status !== "archived").map((c) => ( ))} New issue
{/* Title */}