feat: Implement recipe copy and delete functionality

Backend changes (C#):
- Add CopyRecipe method to MachineBridge
  - Generates new GUID for copied recipe
  - Returns new recipe with current timestamp
- Add DeleteRecipe method to MachineBridge
  - Prevents deletion of currently selected recipe
  - Returns success/failure status
- Add COPY_RECIPE and DELETE_RECIPE handlers in WebSocketServer

Frontend changes (React/TypeScript):
- Add CopyRecipe and DeleteRecipe to Window interface types
- Add copyRecipe and deleteRecipe methods to communication layer
- Implement handleCopy in RecipePage
  - Prompts user for new name
  - Adds copied recipe to list
  - Selects newly copied recipe
- Implement handleDelete in RecipePage
  - Shows confirmation dialog
  - Removes recipe from list
  - Selects next available recipe
- Connect Copy and Delete buttons with handlers
- Disable buttons when no recipe is selected

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-24 23:53:35 +09:00
parent 8c1a87ded0
commit 27cc2507cf
5 changed files with 206 additions and 2 deletions

View File

@@ -221,6 +221,66 @@ class CommunicationLayer {
});
}
}
public async copyRecipe(recipeId: string, newName: string): Promise<{ success: boolean; message: string; newRecipe?: any }> {
if (isWebView) {
const resultJson = await window.chrome!.webview!.hostObjects.machine.CopyRecipe(recipeId, newName);
return JSON.parse(resultJson);
} else {
return new Promise((resolve, reject) => {
if (!this.isConnected) {
setTimeout(() => {
if (!this.isConnected) reject({ success: false, message: "WebSocket connection timeout" });
}, 2000);
}
const timeoutId = setTimeout(() => {
this.listeners = this.listeners.filter(cb => cb !== handler);
reject({ success: false, message: "Recipe copy timeout" });
}, 10000);
const handler = (data: any) => {
if (data.type === 'RECIPE_COPIED') {
clearTimeout(timeoutId);
this.listeners = this.listeners.filter(cb => cb !== handler);
resolve(data.data);
}
};
this.listeners.push(handler);
this.ws?.send(JSON.stringify({ type: 'COPY_RECIPE', recipeId, newName }));
});
}
}
public async deleteRecipe(recipeId: string): Promise<{ success: boolean; message: string; recipeId?: string }> {
if (isWebView) {
const resultJson = await window.chrome!.webview!.hostObjects.machine.DeleteRecipe(recipeId);
return JSON.parse(resultJson);
} else {
return new Promise((resolve, reject) => {
if (!this.isConnected) {
setTimeout(() => {
if (!this.isConnected) reject({ success: false, message: "WebSocket connection timeout" });
}, 2000);
}
const timeoutId = setTimeout(() => {
this.listeners = this.listeners.filter(cb => cb !== handler);
reject({ success: false, message: "Recipe delete timeout" });
}, 10000);
const handler = (data: any) => {
if (data.type === 'RECIPE_DELETED') {
clearTimeout(timeoutId);
this.listeners = this.listeners.filter(cb => cb !== handler);
resolve(data.data);
}
};
this.listeners.push(handler);
this.ws?.send(JSON.stringify({ type: 'DELETE_RECIPE', recipeId }));
});
}
}
}
export const comms = new CommunicationLayer();

View File

@@ -41,6 +41,66 @@ export const RecipePage: React.FC = () => {
// In real app: await comms.saveRecipe(editedRecipe);
};
const handleCopy = async () => {
if (!selectedId) return;
const selectedRecipe = recipes.find(r => r.id === selectedId);
if (!selectedRecipe) return;
const newName = prompt(`Copy "${selectedRecipe.name}" as:`, `${selectedRecipe.name}_Copy`);
if (!newName || newName.trim() === '') return;
try {
const result = await comms.copyRecipe(selectedId, newName.trim());
if (result.success && result.newRecipe) {
const newRecipeList = [...recipes, result.newRecipe];
setRecipes(newRecipeList);
setSelectedId(result.newRecipe.id);
setEditedRecipe(result.newRecipe);
console.log("Recipe copied successfully:", result.newRecipe);
} else {
alert(`Failed to copy recipe: ${result.message}`);
}
} catch (error: any) {
alert(`Error copying recipe: ${error.message || 'Unknown error'}`);
console.error('Recipe copy error:', error);
}
};
const handleDelete = async () => {
if (!selectedId) return;
const selectedRecipe = recipes.find(r => r.id === selectedId);
if (!selectedRecipe) return;
const confirmed = window.confirm(`Are you sure you want to delete "${selectedRecipe.name}"?`);
if (!confirmed) return;
try {
const result = await comms.deleteRecipe(selectedId);
if (result.success) {
const newRecipeList = recipes.filter(r => r.id !== selectedId);
setRecipes(newRecipeList);
// Select first recipe or clear selection
if (newRecipeList.length > 0) {
setSelectedId(newRecipeList[0].id);
setEditedRecipe(newRecipeList[0]);
} else {
setSelectedId(null);
setEditedRecipe(null);
}
console.log("Recipe deleted successfully");
} else {
alert(`Failed to delete recipe: ${result.message}`);
}
} catch (error: any) {
alert(`Error deleting recipe: ${error.message || 'Unknown error'}`);
console.error('Recipe delete error:', error);
}
};
return (
<div className="w-full h-full p-6 flex flex-col gap-4">
{/* Header */}
@@ -93,10 +153,22 @@ export const RecipePage: React.FC = () => {
<TechButton variant="default" className="flex justify-center" title="Add New">
<Plus className="w-4 h-4" />
</TechButton>
<TechButton variant="default" className="flex justify-center" title="Copy Selected">
<TechButton
variant="default"
className="flex justify-center"
title="Copy Selected"
onClick={handleCopy}
disabled={!selectedId}
>
<Copy className="w-4 h-4" />
</TechButton>
<TechButton variant="danger" className="flex justify-center" title="Delete Selected">
<TechButton
variant="danger"
className="flex justify-center"
title="Delete Selected"
onClick={handleDelete}
disabled={!selectedId}
>
<Trash2 className="w-4 h-4" />
</TechButton>
</div>

View File

@@ -60,6 +60,8 @@ declare global {
SetIO(id: number, isInput: boolean, state: boolean): Promise<void>;
SystemControl(command: string): Promise<void>;
SelectRecipe(recipeId: string): Promise<string>;
CopyRecipe(recipeId: string, newName: string): Promise<string>;
DeleteRecipe(recipeId: string): Promise<string>;
GetConfig(): Promise<string>;
GetIOList(): Promise<string>;
GetRecipeList(): Promise<string>;