fix: Prompt to save before manual run with autosave disabled (#32513)

Co-authored-by: n8n-cat-bot[bot] <n8n-cat-bot[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
n8n-cat-bot[bot]
2026-06-18 14:31:42 +00:00
committed by GitHub
parent 5582bb2604
commit 2124c08c05
3 changed files with 65 additions and 2 deletions
@@ -4002,6 +4002,10 @@
"workflowPreview.executionMode.showError.previewError.message": "Unable to preview workflow execution",
"workflowPreview.showError.previewError.title": "Preview error",
"workflowRun.noActiveConnectionToTheServer": "Lost connection to the server",
"workflowRun.saveBeforeRun.headline": "Save before running?",
"workflowRun.saveBeforeRun.message": "You have to save your workflow before running a manual execution.",
"workflowRun.saveBeforeRun.confirmButtonText": "Save & run",
"workflowRun.saveBeforeRun.cancelButtonText": "Cancel",
"workflowRun.showError.deactivate": "Deactivate workflow to execute",
"workflowRun.showError.title": "Problem running workflow",
"workflowRun.showError.payloadTooLarge": "Please execute the whole workflow, rather than just the node. (Existing execution data is too large.)",
@@ -215,6 +215,14 @@ vi.mock('@/app/composables/useToast', () => ({
}),
}));
vi.mock('@/app/composables/useMessage', () => ({
useMessage: vi.fn().mockReturnValue({
confirm: vi.fn().mockResolvedValue('confirm'),
alert: vi.fn(),
prompt: vi.fn(),
}),
}));
vi.mock('@/app/composables/useWorkflowHelpers', () => ({
useWorkflowHelpers: vi.fn().mockReturnValue({
saveCurrentWorkflow: vi.fn(),
@@ -448,7 +456,11 @@ describe('useRunWorkflow({ router })', () => {
expect(workflowSaving.saveCurrentWorkflow).toHaveBeenCalledTimes(1);
});
it('should not save before execute when autosave is disabled and state is dirty', async () => {
it('should prompt the user and save when they confirm if autosave is disabled and state is dirty (ADO-5328)', async () => {
const { useMessage } = await import('@/app/composables/useMessage');
const messageMock = vi.mocked(useMessage)();
vi.mocked(messageMock.confirm).mockResolvedValueOnce('confirm');
vi.spyOn(settingsStore, 'isAutosaveEnabled', 'get').mockReturnValue(false);
vi.mocked(uiStore).stateIsDirty = true;
vi.mocked(workflowsStore).isWorkflowSaved = { '123': true };
@@ -457,7 +469,27 @@ describe('useRunWorkflow({ router })', () => {
const { runWorkflow } = useRunWorkflow({ router });
await runWorkflow({});
expect(messageMock.confirm).toHaveBeenCalledTimes(1);
expect(workflowSaving.saveCurrentWorkflow).toHaveBeenCalledTimes(1);
});
it('should not run when the user cancels the save-before-run prompt (ADO-5328)', async () => {
const { useMessage } = await import('@/app/composables/useMessage');
const messageMock = vi.mocked(useMessage)();
vi.mocked(messageMock.confirm).mockResolvedValueOnce('cancel');
vi.spyOn(settingsStore, 'isAutosaveEnabled', 'get').mockReturnValue(false);
vi.mocked(uiStore).stateIsDirty = true;
vi.mocked(workflowsStore).isWorkflowSaved = { '123': true };
const workflowSaving = useWorkflowSaving({ router });
const { runWorkflow } = useRunWorkflow({ router });
const result = await runWorkflow({});
expect(messageMock.confirm).toHaveBeenCalledTimes(1);
expect(workflowSaving.saveCurrentWorkflow).not.toHaveBeenCalled();
expect(workflowsStore.runWorkflow).not.toHaveBeenCalled();
expect(result).toBeUndefined();
});
it('should save new workflow before execute even when autosave is disabled', async () => {
@@ -25,6 +25,7 @@ import { retry } from '@n8n/utils/retry';
import { computed, getCurrentInstance, type Ref } from 'vue';
import { useToast } from '@/app/composables/useToast';
import { useMessage } from '@/app/composables/useMessage';
import { useNodeHelpers } from '@/app/composables/useNodeHelpers';
import {
@@ -33,6 +34,7 @@ import {
CHAT_HITL_TOOL_NODE_TYPE,
CHAT_TRIGGER_NODE_TYPE,
IN_PROGRESS_EXECUTION_ID,
MODAL_CONFIRM,
RESPOND_TO_WEBHOOK_NODE_TYPE,
} from '@/app/constants';
@@ -78,6 +80,7 @@ export function useRunWorkflow(useRunWorkflowOpts: {
const workflowHelpers = useWorkflowHelpers();
const i18n = useI18n();
const toast = useToast();
const message = useMessage();
const telemetry = useTelemetry();
const externalHooks = useExternalHooks();
const settingsStore = useSettingsStore();
@@ -182,7 +185,31 @@ export function useRunWorkflow(useRunWorkflowOpts: {
const runData = workflowsStore.getWorkflowRunData;
const isNewWorkflow = !workflowsStore.isWorkflowSaved[workflowDocumentStore.value.workflowId];
if (isNewWorkflow || (uiStore.stateIsDirty && settingsStore.isAutosaveEnabled)) {
// With N8N_WORKFLOWS_AUTOSAVE_DISABLED=true the editor no longer
// force-saves before executing, so canvas-only edits would be
// dropped by the executor (it only ever runs the DB copy). Prompt
// the user to save first so the run reflects the canvas. ADO-5328.
if (!isNewWorkflow && uiStore.stateIsDirty && !settingsStore.isAutosaveEnabled) {
const response = await message.confirm(i18n.baseText('workflowRun.saveBeforeRun.message'), {
title: i18n.baseText('workflowRun.saveBeforeRun.headline'),
type: 'info',
confirmButtonText: i18n.baseText('workflowRun.saveBeforeRun.confirmButtonText'),
cancelButtonText: i18n.baseText('workflowRun.saveBeforeRun.cancelButtonText'),
showClose: true,
});
if (response !== MODAL_CONFIRM) {
return undefined;
}
const saved = await workflowSaving.saveCurrentWorkflow({
id: workflowDocumentStore.value.workflowId,
});
if (!saved) {
return undefined;
}
} else if (isNewWorkflow || (uiStore.stateIsDirty && settingsStore.isAutosaveEnabled)) {
await workflowSaving.saveCurrentWorkflow({ id: workflowDocumentStore.value.workflowId });
}