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