diff --git a/frontend/src/components/ScheduledOperationsView.tsx b/frontend/src/components/ScheduledOperationsView.tsx index 2a766662..b8d87aec 100644 --- a/frontend/src/components/ScheduledOperationsView.tsx +++ b/frontend/src/components/ScheduledOperationsView.tsx @@ -201,12 +201,13 @@ export default function ScheduledOperationsView({ filterNodeId, onClearFilter, p return () => { cancelled = true; }; }, [formAction, formTargetId, formNodeId]); - // Re-fetch stacks when node changes + // Re-fetch stacks when the node changes. Clearing a stale stack selection is + // done in the Node picker's onValueChange (a user-driven change), not here, so + // a prefilled or edited node keeps its stack instead of being wiped on open. useEffect(() => { if (!dialogOpen) return; if (formNodeId) { fetchStacks(formNodeId); - setFormTargetId(''); } else { setStacks([]); } @@ -790,7 +791,7 @@ export default function ScheduledOperationsView({ filterNodeId, onClearFilter, p { setFormNodeId(val); setFormTargetId(''); }} placeholder="Select node..." /> diff --git a/frontend/src/components/__tests__/ScheduledOperationsView.test.tsx b/frontend/src/components/__tests__/ScheduledOperationsView.test.tsx index 56104eb3..81212605 100644 --- a/frontend/src/components/__tests__/ScheduledOperationsView.test.tsx +++ b/frontend/src/components/__tests__/ScheduledOperationsView.test.tsx @@ -125,6 +125,39 @@ describe('ScheduledOperationsView', () => { expect(onPrefillConsumed).toHaveBeenCalledTimes(1); // The prefilled stack drives a node-scoped stack fetch through the proxy. await waitFor(() => expect(mockedFetchForNode).toHaveBeenCalledWith('/stacks', 1)); + // The prefilled stack stays selected; the node-change effect must not wipe it. + // Comboboxes render in order: action, node, stack. + await waitFor(() => expect(screen.getAllByRole('combobox')[2]).toHaveTextContent('web')); + }); + + it('keeps the stack selected when editing a stack-targeted task', async () => { + tasksFixture = [makeTask({ + id: 9, name: 'restart-web', action: 'restart', target_type: 'stack', target_id: 'web', node_id: 1, + })]; + render(); + + await userEvent.click(await screen.findByRole('button', { name: /All tasks/ })); + await userEvent.click(await screen.findByTitle('Edit')); + await waitFor(() => expect(screen.getAllByRole('combobox')[2]).toHaveTextContent('web')); + }); + + it('clears the stale stack when the user changes the node', async () => { + render(); + + await userEvent.click(await screen.findByRole('button', { name: /New Schedule/ })); + // Default action Restart Stack: choose a node, then a stack on it. + await userEvent.click(screen.getAllByRole('combobox')[1]); + await userEvent.click(await screen.findByRole('button', { name: 'hub' })); + await userEvent.click(screen.getAllByRole('combobox')[2]); + await userEvent.click(await screen.findByRole('button', { name: 'web' })); + expect(screen.getAllByRole('combobox')[2]).toHaveTextContent('web'); + + // Switching node is a user-driven change, so the now-stale stack clears + // and the user must re-pick one before the schedule can be saved. + await userEvent.click(screen.getAllByRole('combobox')[1]); + await userEvent.click(await screen.findByRole('button', { name: 'edge' })); + expect(screen.getAllByRole('combobox')[2]).toHaveTextContent('Select stack...'); + expect(screen.getByRole('button', { name: 'Create' })).toBeDisabled(); }); it('filters the table to the selected node and clears the filter', async () => {