Fix input focus loss when creating a project
DetailView was defined as a component inside ProjectsTab's render, causing React to unmount/remount it on every keystroke. Replace with inline JSX so the input element identity stays stable across renders.
This commit is contained in:
@@ -987,92 +987,82 @@ function ProjectsTab({ projects, setProjects, mobile }: {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const DetailView = () => {
|
const detailView = creating ? (
|
||||||
if (creating) {
|
<div style={{ flex: 1, overflow: "auto", display: "flex", flexDirection: "column" }}>
|
||||||
return (
|
<div style={{ padding: `${tokens.space[3]}px ${tokens.space[4]}px`, borderBottom: `1px solid ${tokens.color.border0}`, flexShrink: 0, background: tokens.color.bg0 }}>
|
||||||
<div style={{ flex: 1, overflow: "auto", display: "flex", flexDirection: "column" }}>
|
{mobile && <BackBtn onBack={() => setCreating(false)} label="PROJECTS" />}
|
||||||
<div style={{ padding: `${tokens.space[3]}px ${tokens.space[4]}px`, borderBottom: `1px solid ${tokens.color.border0}`, flexShrink: 0, background: tokens.color.bg0 }}>
|
<Label color={tokens.color.accent} style={{ fontSize: tokens.size.md, letterSpacing: tokens.tracking.wider, marginTop: mobile ? tokens.space[2] : 0, display: "block" }}>NEW PROJECT</Label>
|
||||||
{mobile && <BackBtn onBack={() => setCreating(false)} label="PROJECTS" />}
|
</div>
|
||||||
<Label color={tokens.color.accent} style={{ fontSize: tokens.size.md, letterSpacing: tokens.tracking.wider, marginTop: mobile ? tokens.space[2] : 0, display: "block" }}>NEW PROJECT</Label>
|
<div style={{ padding: tokens.space[4], display: "flex", flexDirection: "column", gap: tokens.space[4], maxWidth: 480 }}>
|
||||||
</div>
|
<div style={{ display: "flex", flexDirection: "column", gap: tokens.space[1] }}>
|
||||||
<div style={{ padding: tokens.space[4], display: "flex", flexDirection: "column", gap: tokens.space[4], maxWidth: 480 }}>
|
<Label color={tokens.color.text2}>Project Name</Label>
|
||||||
<div style={{ display: "flex", flexDirection: "column", gap: tokens.space[1] }}>
|
<Input value={newName} onChange={e => setNewName(e.target.value)} placeholder="My Project" />
|
||||||
<Label color={tokens.color.text2}>Project Name</Label>
|
|
||||||
<Input value={newName} onChange={e => setNewName(e.target.value)} placeholder="My Project" />
|
|
||||||
</div>
|
|
||||||
<div style={{ display: "flex", gap: tokens.space[3] }}>
|
|
||||||
<Btn variant="ghost" onClick={() => { setCreating(false); setNewName(""); }}>CANCEL</Btn>
|
|
||||||
<Btn variant="primary" onClick={handleCreate} disabled={!newName.trim()}>CREATE PROJECT</Btn>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
<div style={{ display: "flex", gap: tokens.space[3] }}>
|
||||||
}
|
<Btn variant="ghost" onClick={() => { setCreating(false); setNewName(""); }}>CANCEL</Btn>
|
||||||
|
<Btn variant="primary" onClick={handleCreate} disabled={!newName.trim()}>CREATE PROJECT</Btn>
|
||||||
if (!selectedProject) {
|
|
||||||
return (
|
|
||||||
<div style={{ flex: 1, display: "flex", alignItems: "center", justifyContent: "center" }}>
|
|
||||||
<Label color={tokens.color.text3}>SELECT A PROJECT</Label>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div style={{ flex: 1, overflow: "hidden", display: "flex", flexDirection: "column" }}>
|
|
||||||
<div style={{ padding: `${tokens.space[3]}px ${tokens.space[4]}px`, borderBottom: `1px solid ${tokens.color.border0}`, flexShrink: 0, background: tokens.color.bg0 }}>
|
|
||||||
{mobile && <BackBtn onBack={() => setSelectedId(null)} label="PROJECTS" />}
|
|
||||||
<div style={{ marginTop: mobile ? tokens.space[2] : 0, display: "flex", alignItems: "center", justifyContent: "space-between" }}>
|
|
||||||
<Mono size={tokens.size.lg} color={tokens.color.text0}>{selectedProject.name}</Mono>
|
|
||||||
<Btn variant="danger" onClick={() => handleDelete(selectedProject.id)} style={{ fontSize: tokens.size.xs, minHeight: 32 }}>DELETE PROJECT</Btn>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style={{ flex: 1, overflow: "auto", padding: tokens.space[4], display: "flex", flexDirection: "column", gap: tokens.space[5] }}>
|
|
||||||
{/* Workspaces list */}
|
|
||||||
<div>
|
|
||||||
<Label color={tokens.color.text2} style={{ display: "block", marginBottom: tokens.space[3] }}>
|
|
||||||
WORKSPACES · {selectedProject.workspaces.length}
|
|
||||||
</Label>
|
|
||||||
<div style={{ display: "flex", flexDirection: "column", gap: tokens.space[2] }}>
|
|
||||||
{selectedProject.workspaces.length === 0 ? (
|
|
||||||
<Mono size={tokens.size.sm} color={tokens.color.text3}>No workspaces configured. Search for a repository below.</Mono>
|
|
||||||
) : (
|
|
||||||
selectedProject.workspaces.map(w => (
|
|
||||||
<div key={w.name} style={{
|
|
||||||
background: tokens.color.bg2, border: `1px solid ${tokens.color.border0}`,
|
|
||||||
padding: `${tokens.space[3]}px ${tokens.space[4]}px`,
|
|
||||||
display: "flex", alignItems: mobile ? "flex-start" : "center",
|
|
||||||
flexDirection: mobile ? "column" : "row",
|
|
||||||
justifyContent: "space-between", gap: tokens.space[3],
|
|
||||||
}}>
|
|
||||||
<div style={{ display: "flex", flexDirection: "column", gap: tokens.space[1], minWidth: 0 }}>
|
|
||||||
<Mono size={tokens.size.base} color={tokens.color.text0}>{w.name}</Mono>
|
|
||||||
<Mono size={tokens.size.sm} color={tokens.color.text2} style={{ wordBreak: "break-all" }}>{w.repo}</Mono>
|
|
||||||
</div>
|
|
||||||
<Btn variant="ghost" onClick={() => handleRemoveWorkspace(selectedProject.id, w.name)}
|
|
||||||
style={{ fontSize: tokens.size.xs, minHeight: 32, color: tokens.color.fail, flexShrink: 0 }}>
|
|
||||||
REMOVE
|
|
||||||
</Btn>
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Repo search */}
|
|
||||||
<div>
|
|
||||||
<Label color={tokens.color.text2} style={{ display: "block", marginBottom: tokens.space[3] }}>ADD WORKSPACE</Label>
|
|
||||||
<RepoSearch onSelect={handleAddRepo} mobile={mobile} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Divider />
|
|
||||||
|
|
||||||
{/* Credentials management */}
|
|
||||||
<CredentialManager kind="git" mobile={mobile} />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
</div>
|
||||||
};
|
) : selectedProject ? (
|
||||||
|
<div style={{ flex: 1, overflow: "hidden", display: "flex", flexDirection: "column" }}>
|
||||||
|
<div style={{ padding: `${tokens.space[3]}px ${tokens.space[4]}px`, borderBottom: `1px solid ${tokens.color.border0}`, flexShrink: 0, background: tokens.color.bg0 }}>
|
||||||
|
{mobile && <BackBtn onBack={() => setSelectedId(null)} label="PROJECTS" />}
|
||||||
|
<div style={{ marginTop: mobile ? tokens.space[2] : 0, display: "flex", alignItems: "center", justifyContent: "space-between" }}>
|
||||||
|
<Mono size={tokens.size.lg} color={tokens.color.text0}>{selectedProject.name}</Mono>
|
||||||
|
<Btn variant="danger" onClick={() => handleDelete(selectedProject.id)} style={{ fontSize: tokens.size.xs, minHeight: 32 }}>DELETE PROJECT</Btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ flex: 1, overflow: "auto", padding: tokens.space[4], display: "flex", flexDirection: "column", gap: tokens.space[5] }}>
|
||||||
|
{/* Workspaces list */}
|
||||||
|
<div>
|
||||||
|
<Label color={tokens.color.text2} style={{ display: "block", marginBottom: tokens.space[3] }}>
|
||||||
|
WORKSPACES · {selectedProject.workspaces.length}
|
||||||
|
</Label>
|
||||||
|
<div style={{ display: "flex", flexDirection: "column", gap: tokens.space[2] }}>
|
||||||
|
{selectedProject.workspaces.length === 0 ? (
|
||||||
|
<Mono size={tokens.size.sm} color={tokens.color.text3}>No workspaces configured. Search for a repository below.</Mono>
|
||||||
|
) : (
|
||||||
|
selectedProject.workspaces.map(w => (
|
||||||
|
<div key={w.name} style={{
|
||||||
|
background: tokens.color.bg2, border: `1px solid ${tokens.color.border0}`,
|
||||||
|
padding: `${tokens.space[3]}px ${tokens.space[4]}px`,
|
||||||
|
display: "flex", alignItems: mobile ? "flex-start" : "center",
|
||||||
|
flexDirection: mobile ? "column" : "row",
|
||||||
|
justifyContent: "space-between", gap: tokens.space[3],
|
||||||
|
}}>
|
||||||
|
<div style={{ display: "flex", flexDirection: "column", gap: tokens.space[1], minWidth: 0 }}>
|
||||||
|
<Mono size={tokens.size.base} color={tokens.color.text0}>{w.name}</Mono>
|
||||||
|
<Mono size={tokens.size.sm} color={tokens.color.text2} style={{ wordBreak: "break-all" }}>{w.repo}</Mono>
|
||||||
|
</div>
|
||||||
|
<Btn variant="ghost" onClick={() => handleRemoveWorkspace(selectedProject.id, w.name)}
|
||||||
|
style={{ fontSize: tokens.size.xs, minHeight: 32, color: tokens.color.fail, flexShrink: 0 }}>
|
||||||
|
REMOVE
|
||||||
|
</Btn>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Repo search */}
|
||||||
|
<div>
|
||||||
|
<Label color={tokens.color.text2} style={{ display: "block", marginBottom: tokens.space[3] }}>ADD WORKSPACE</Label>
|
||||||
|
<RepoSearch onSelect={handleAddRepo} mobile={mobile} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
{/* Credentials management */}
|
||||||
|
<CredentialManager kind="git" mobile={mobile} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div style={{ flex: 1, display: "flex", alignItems: "center", justifyContent: "center" }}>
|
||||||
|
<Label color={tokens.color.text3}>SELECT A PROJECT</Label>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ flex: 1, display: "flex", overflow: "hidden" }}>
|
<div style={{ flex: 1, display: "flex", overflow: "hidden" }}>
|
||||||
@@ -1090,8 +1080,8 @@ function ProjectsTab({ projects, setProjects, mobile }: {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!mobile && <DetailView />}
|
{!mobile && detailView}
|
||||||
{mobile && showDetail && <DetailView />}
|
{mobile && showDetail && detailView}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user