From 5d3ed0c313ad2eca55b18581289d019b26a7b7d3 Mon Sep 17 00:00:00 2001 From: Bryandero98 Date: Thu, 3 Sep 2026 14:35:39 -0500 Subject: [PATCH] fix: PromptManager stage save/create/delete give no visible confirmation (#6022) saveStage, createStage, and confirmDeleteStage all mutated the stage and silently reloaded, with no toast telling the user the save/create/delete actually happened. createStage also never deep-linked to the stage it just created, leaving the user looking at whatever stage happened to be selected before. All three now toast success, and createStage selects the new stage by name so the URL and the visible editor point at what was just made. Co-Authored-By: Claude Sonnet 5 --- client/src/pages/PromptManager.jsx | 4 ++ client/src/pages/PromptManager.test.jsx | 56 ++++++++++++++++++++++++- 2 files changed, 58 insertions(+), 2 deletions(-) diff --git a/client/src/pages/PromptManager.jsx b/client/src/pages/PromptManager.jsx index 11b8ffd24c..0116ae7ba0 100644 --- a/client/src/pages/PromptManager.jsx +++ b/client/src/pages/PromptManager.jsx @@ -204,6 +204,7 @@ export default function PromptManager() { .catch((err) => { toast.error('Failed to save stage: ' + err.message); return false; }); if (!ok) { setSaving(false); return; } setSaving(false); + toast.success(`Stage "${stages[selectedStage]?.name || selectedStage}" saved`); await loadData(); }; @@ -283,6 +284,8 @@ export default function PromptManager() { variables: [], template: '' }); + toast.success(`Stage "${payload.name || payload.stageName}" created`); + setSelectedStage(payload.stageName); await loadData(); }; @@ -313,6 +316,7 @@ export default function PromptManager() { if (selectedStage === stageName) { setSelectedStage(null); } + toast.success(`Stage "${stageName}" deleted`); await loadData(); }; diff --git a/client/src/pages/PromptManager.test.jsx b/client/src/pages/PromptManager.test.jsx index 0cddea25d7..ec2c9ad4ac 100644 --- a/client/src/pages/PromptManager.test.jsx +++ b/client/src/pages/PromptManager.test.jsx @@ -11,6 +11,8 @@ const getPrompts = vi.fn(); const getPrompt = vi.fn(); const getPromptUsage = vi.fn(); const deletePrompt = vi.fn(); +const savePrompt = vi.fn(); +const createPrompt = vi.fn(); const getPromptVariables = vi.fn(); const deletePromptVariable = vi.fn(); const getJobSkills = vi.fn(() => Promise.resolve({ skills: [] })); @@ -21,8 +23,8 @@ const previewJobSkill = vi.fn(); vi.mock('../services/apiPrompts', () => ({ getPrompts: (...a) => getPrompts(...a), getPrompt: (...a) => getPrompt(...a), - createPrompt: vi.fn(), - savePrompt: vi.fn(), + createPrompt: (...a) => createPrompt(...a), + savePrompt: (...a) => savePrompt(...a), deletePrompt: (...a) => deletePrompt(...a), previewPrompt: vi.fn(), getPromptUsage: (...a) => getPromptUsage(...a), @@ -381,6 +383,56 @@ describe('PromptManager delete demotion', () => { // #3935: the row trash icon used to fire DELETE on the first click, so a // mis-click on a 14px target destroyed the variable with no undo. +describe('PromptManager stage save/create/delete feedback (#6022)', () => { + beforeEach(() => { + getPrompts.mockReset().mockResolvedValue({ stages: STAGES, systemStages: SYSTEM_STAGES }); + getPrompt.mockReset().mockResolvedValue({ name: 'Pipeline — Comic Book Script', template: 'body', variables: [] }); + getPromptUsage.mockReset().mockResolvedValue({ + isSystemStage: false, usedBy: [], referencedBy: [], canDelete: true, + }); + savePrompt.mockReset().mockResolvedValue({ success: true }); + createPrompt.mockReset().mockResolvedValue({ success: true, stageName: 'my-new-stage' }); + deletePrompt.mockReset().mockResolvedValue({ success: true }); + toast.error.mockReset(); + toast.success.mockReset(); + }); + + it('confirms a successful stage save with a named toast', async () => { + renderPage('/prompts?stage=pipeline-comic-script'); + await screen.findByText('Prompt Stages'); + await screen.findByRole('button', { name: /^save$/i }); + + fireEvent.click(screen.getByRole('button', { name: /^save$/i })); + + await waitFor(() => expect(toast.success).toHaveBeenCalledWith('Stage "Pipeline — Comic Book Script" saved')); + expect(toast.error).not.toHaveBeenCalled(); + }); + + it('confirms stage creation with a named toast and deep-links straight to the new stage', async () => { + renderPage('/prompts'); + await screen.findByText('Prompt Stages'); + + fireEvent.click(screen.getByRole('button', { name: 'New Stage' })); + fireEvent.change(screen.getByPlaceholderText('my-stage'), { target: { value: 'my-new-stage' } }); + fireEvent.change(screen.getByPlaceholderText('Pipeline — My Stage'), { target: { value: 'My New Stage' } }); + fireEvent.click(screen.getByRole('button', { name: /create stage/i })); + + await waitFor(() => expect(toast.success).toHaveBeenCalledWith('Stage "My New Stage" created')); + await waitFor(() => expect(currentSearch()).toContain('stage=my-new-stage')); + }); + + it('confirms a successful stage delete with a named toast', async () => { + renderPage('/prompts?stage=pipeline-comic-script'); + await screen.findByText('Prompt Stages'); + + fireEvent.click(await screen.findByRole('button', { name: /^delete$/i })); + expect(await screen.findByText('Delete Stage?')).toBeTruthy(); + fireEvent.click(screen.getAllByRole('button', { name: /^delete$/i }).at(-1)); + + await waitFor(() => expect(toast.success).toHaveBeenCalledWith('Stage "pipeline-comic-script" deleted')); + }); +}); + describe('PromptManager variable deletion', () => { const VARIABLES = { 'tone-guide': { name: 'Tone Guide', category: 'style', content: 'stay wry' },