Skip to content

Commit 8961b1b

Browse files
MarkShawn2020claude
andcommitted
feat(workspace): 全局 Feature 序号 + 创建对话框优化
- Feature seq 改为跨项目全局递增 - 后端原子处理 seq,加载时自动迁移 - 展开的 tab group 均显示新增按钮 - CreateFeatureDialog 显示只读 ID - Title 默认空,placeholder 显示序号,Tab 键补全 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 2ebcb3f commit 8961b1b

5 files changed

Lines changed: 87 additions & 45 deletions

File tree

src-tauri/src/workspace_store.rs

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,9 @@ pub struct WorkspaceProject {
116116
pub struct WorkspaceData {
117117
pub projects: Vec<WorkspaceProject>,
118118
pub active_project_id: Option<String>,
119+
/// Global feature counter across all projects
120+
#[serde(default)]
121+
pub feature_counter: Option<u32>,
119122
}
120123

121124
/// Load workspace data from disk
@@ -128,7 +131,21 @@ pub fn load_workspace() -> Result<WorkspaceData, String> {
128131

129132
let content = fs::read_to_string(&path).map_err(|e| format!("Failed to read workspace: {}", e))?;
130133

131-
serde_json::from_str(&content).map_err(|e| format!("Failed to parse workspace: {}", e))
134+
let mut data: WorkspaceData = serde_json::from_str(&content).map_err(|e| format!("Failed to parse workspace: {}", e))?;
135+
136+
// Migrate: initialize global feature_counter from max seq if not set
137+
if data.feature_counter.is_none() {
138+
let max_seq = data.projects.iter()
139+
.flat_map(|p| p.features.iter())
140+
.map(|f| f.seq)
141+
.max()
142+
.unwrap_or(0);
143+
if max_seq > 0 {
144+
data.feature_counter = Some(max_seq);
145+
}
146+
}
147+
148+
Ok(data)
132149
}
133150

134151
/// Save workspace data to disk
@@ -231,6 +248,10 @@ pub fn set_active_project(id: &str) -> Result<(), String> {
231248
pub fn create_feature(project_id: &str, name: String, description: Option<String>) -> Result<Feature, String> {
232249
let mut data = load_workspace()?;
233250

251+
// Increment global feature counter
252+
let seq = data.feature_counter.unwrap_or(0) + 1;
253+
data.feature_counter = Some(seq);
254+
234255
let project = data
235256
.projects
236257
.iter_mut()
@@ -239,7 +260,7 @@ pub fn create_feature(project_id: &str, name: String, description: Option<String
239260

240261
let feature = Feature {
241262
id: uuid::Uuid::new_v4().to_string(),
242-
seq: 0, // Will be set by frontend using feature_counter
263+
seq,
243264
name,
244265
description,
245266
status: FeatureStatus::Pending,

src/components/GlobalHeader/CreateFeatureDialog.tsx

Lines changed: 39 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -14,39 +14,51 @@ import { MarkdownRenderer } from "@/components/MarkdownRenderer";
1414
interface CreateFeatureDialogProps {
1515
open: boolean;
1616
onOpenChange: (open: boolean) => void;
17-
defaultName: string;
17+
seq: number;
1818
onSubmit: (name: string, description: string) => void;
1919
}
2020

2121
export function CreateFeatureDialog({
2222
open,
2323
onOpenChange,
24-
defaultName,
24+
seq,
2525
onSubmit,
2626
}: CreateFeatureDialogProps) {
27-
const [name, setName] = useState(defaultName);
27+
const placeholder = `#${seq}`;
28+
const [name, setName] = useState("");
2829
const [description, setDescription] = useState("");
2930
const [showPreview, setShowPreview] = useState(false);
3031
const nameInputRef = useRef<HTMLInputElement>(null);
3132

3233
useEffect(() => {
3334
if (open) {
34-
setName(defaultName);
35+
setName("");
3536
setDescription("");
3637
setShowPreview(false);
3738
// Focus name input when dialog opens
3839
setTimeout(() => nameInputRef.current?.focus(), 100);
3940
}
40-
}, [open, defaultName]);
41+
}, [open]);
4142

4243
const handleSubmit = (e: React.FormEvent) => {
4344
e.preventDefault();
44-
const trimmedName = name.trim();
45-
if (!trimmedName) return;
46-
onSubmit(trimmedName, description.trim());
45+
// Use placeholder if name is empty
46+
const finalName = name.trim() || placeholder;
47+
onSubmit(finalName, description.trim());
4748
onOpenChange(false);
4849
};
4950

51+
const handleNameKeyDown = (e: React.KeyboardEvent) => {
52+
// Tab to accept placeholder
53+
if (e.key === "Tab" && !name) {
54+
e.preventDefault();
55+
setName(placeholder);
56+
}
57+
if (e.key === "Enter" && e.metaKey) {
58+
handleSubmit(e);
59+
}
60+
};
61+
5062
const handleKeyDown = (e: React.KeyboardEvent) => {
5163
if (e.key === "Enter" && e.metaKey) {
5264
handleSubmit(e);
@@ -61,16 +73,24 @@ export function CreateFeatureDialog({
6173
</DialogHeader>
6274

6375
<form onSubmit={handleSubmit} className="flex flex-col gap-4 flex-1 min-h-0">
64-
<div className="space-y-2">
65-
<Label htmlFor="feature-name">Title</Label>
66-
<Input
67-
ref={nameInputRef}
68-
id="feature-name"
69-
value={name}
70-
onChange={(e) => setName(e.target.value)}
71-
placeholder="Feature title"
72-
onKeyDown={handleKeyDown}
73-
/>
76+
<div className="flex gap-4">
77+
<div className="space-y-2 w-20 flex-shrink-0">
78+
<Label>ID</Label>
79+
<div className="h-9 px-3 flex items-center text-sm text-muted-foreground bg-muted rounded-md">
80+
#{seq}
81+
</div>
82+
</div>
83+
<div className="space-y-2 flex-1">
84+
<Label htmlFor="feature-name">Title</Label>
85+
<Input
86+
ref={nameInputRef}
87+
id="feature-name"
88+
value={name}
89+
onChange={(e) => setName(e.target.value)}
90+
placeholder={placeholder}
91+
onKeyDown={handleNameKeyDown}
92+
/>
93+
</div>
7494
</div>
7595

7696
<div className="flex-1 min-h-0 flex flex-col space-y-2">
@@ -137,7 +157,7 @@ Any additional context..."
137157
<Button type="button" variant="outline" onClick={() => onOpenChange(false)}>
138158
Cancel
139159
</Button>
140-
<Button type="submit" disabled={!name.trim()}>
160+
<Button type="submit">
141161
Create
142162
</Button>
143163
</DialogFooter>

src/components/GlobalHeader/FeatureTabGroup.tsx

Lines changed: 17 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export function FeatureTabGroup({
3737
const [, setView] = useAtom(viewAtom);
3838
const [hasLogo, setHasLogo] = useState(true); // Default true to hide name initially
3939
const [showCreateDialog, setShowCreateDialog] = useState(false);
40-
const [defaultFeatureName, setDefaultFeatureName] = useState("");
40+
const [nextSeq, setNextSeq] = useState(0);
4141

4242
useEffect(() => {
4343
invoke<string | null>("get_project_logo", { projectPath: project.path })
@@ -139,8 +139,8 @@ export function FeatureTabGroup({
139139
e?.stopPropagation();
140140
if (!workspace) return;
141141

142-
const counter = (project.feature_counter ?? 0) + 1;
143-
setDefaultFeatureName(`#${counter}`);
142+
// Use global counter
143+
setNextSeq((workspace.feature_counter ?? 0) + 1);
144144
setShowCreateDialog(true);
145145
};
146146

@@ -149,9 +149,8 @@ export function FeatureTabGroup({
149149

150150
setView({ type: "workspace" });
151151

152-
const counter = (project.feature_counter ?? 0) + 1;
153-
154152
try {
153+
// Backend handles seq and feature_counter atomically (global)
155154
const feature = await invoke<Feature>("workspace_create_feature", {
156155
projectId: project.id,
157156
name,
@@ -162,9 +161,8 @@ export function FeatureTabGroup({
162161
p.id === project.id
163162
? {
164163
...p,
165-
features: [...p.features, { ...feature, seq: counter, description: description || undefined }],
164+
features: [...p.features, feature],
166165
active_feature_id: feature.id,
167-
feature_counter: counter,
168166
view_mode: "features" as const,
169167
}
170168
: p
@@ -174,6 +172,7 @@ export function FeatureTabGroup({
174172
...workspace,
175173
projects: newProjects,
176174
active_project_id: project.id,
175+
feature_counter: feature.seq,
177176
};
178177

179178
setWorkspace(newWorkspace);
@@ -277,7 +276,7 @@ export function FeatureTabGroup({
277276
<CreateFeatureDialog
278277
open={showCreateDialog}
279278
onOpenChange={setShowCreateDialog}
280-
defaultName={defaultFeatureName}
279+
seq={nextSeq}
281280
onSubmit={handleCreateFeature}
282281
/>
283282
</>
@@ -332,17 +331,15 @@ export function FeatureTabGroup({
332331
</SortableContext>
333332
)}
334333

335-
{/* Add button - only show for active project */}
336-
{isActiveProject && (
337-
<button
338-
onClick={handleAddFeature}
339-
onPointerDown={(e) => e.stopPropagation()}
340-
className="p-1 text-muted-foreground hover:text-ink hover:bg-card-alt rounded transition-colors"
341-
title="New Feature"
342-
>
343-
<PlusIcon className="w-3.5 h-3.5" />
344-
</button>
345-
)}
334+
{/* Add button */}
335+
<button
336+
onClick={handleAddFeature}
337+
onPointerDown={(e) => e.stopPropagation()}
338+
className="p-1 text-muted-foreground hover:text-ink hover:bg-card-alt rounded transition-colors"
339+
title="New Feature"
340+
>
341+
<PlusIcon className="w-3.5 h-3.5" />
342+
</button>
346343
</div>
347344

348345
{/* Separator between project groups */}
@@ -351,7 +348,7 @@ export function FeatureTabGroup({
351348
<CreateFeatureDialog
352349
open={showCreateDialog}
353350
onOpenChange={setShowCreateDialog}
354-
defaultName={defaultFeatureName}
351+
seq={nextSeq}
355352
onSubmit={handleCreateFeature}
356353
/>
357354
</>

src/views/Workspace/WorkspaceView.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -130,10 +130,12 @@ export function WorkspaceView() {
130130
: activeProject;
131131
if (!targetProject) return;
132132

133-
const counter = (targetProject.feature_counter ?? 0) + 1;
133+
// Generate name based on global counter (backend will assign actual seq)
134+
const counter = (workspace.feature_counter ?? 0) + 1;
134135
const name = `#${counter}`;
135136

136137
try {
138+
// Backend handles seq and feature_counter atomically (global)
137139
const feature = await invoke<Feature>("workspace_create_feature", {
138140
projectId: targetProject.id,
139141
name,
@@ -143,18 +145,18 @@ export function WorkspaceView() {
143145
p.id === targetProject.id
144146
? {
145147
...p,
146-
features: [...p.features, { ...feature, seq: counter }],
148+
features: [...p.features, feature],
147149
active_feature_id: feature.id,
148-
feature_counter: counter,
149150
}
150151
: p
151152
);
152153
saveWorkspace({
153154
...workspace,
154155
projects: newProjects,
155156
active_project_id: targetProject.id,
157+
feature_counter: feature.seq,
156158
});
157-
return { featureId: feature.id, featureName: name };
159+
return { featureId: feature.id, featureName: feature.name };
158160
} catch (err) {
159161
console.error("Failed to create feature:", err);
160162
return undefined;

src/views/Workspace/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,4 +66,6 @@ export interface WorkspaceProject {
6666
export interface WorkspaceData {
6767
projects: WorkspaceProject[];
6868
active_project_id?: string;
69+
/** Global feature counter across all projects */
70+
feature_counter?: number;
6971
}

0 commit comments

Comments
 (0)