Skip to content

Commit 48f0c91

Browse files
authored
fix(editor): Close saving modal when workflow is new (#14836)
1 parent a33e3a8 commit 48f0c91

File tree

5 files changed

+312
-67
lines changed

5 files changed

+312
-67
lines changed

packages/frontend/editor-ui/src/components/executions/workflow/WorkflowExecutionsList.vue

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<script setup lang="ts">
22
import WorkflowExecutionsSidebar from '@/components/executions/workflow/WorkflowExecutionsSidebar.vue';
3-
import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
3+
import { useWorkflowSaving } from '@/composables/useWorkflowSaving';
44
import { MAIN_HEADER_TABS } from '@/constants';
55
import type { ExecutionFilterType, IWorkflowDb } from '@/Interface';
66
import { getNodeViewTab } from '@/utils/nodeViewUtils';
@@ -33,7 +33,8 @@ const emit = defineEmits<{
3333
reload: [];
3434
}>();
3535
36-
const workflowHelpers = useWorkflowHelpers({ router: useRouter() });
36+
const router = useRouter();
37+
const { promptSaveUnsavedWorkflowChanges } = useWorkflowSaving({ router });
3738
3839
const temporaryExecution = computed<ExecutionSummary | undefined>(() =>
3940
props.executions.find((execution) => execution.id === props.execution?.id)
@@ -72,7 +73,7 @@ onBeforeRouteLeave(async (to, _, next) => {
7273
return;
7374
}
7475
75-
await workflowHelpers.promptSaveUnsavedWorkflowChanges(next);
76+
await promptSaveUnsavedWorkflowChanges(next);
7677
});
7778
</script>
7879

packages/frontend/editor-ui/src/composables/useWorkflowHelpers.ts

Lines changed: 1 addition & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
import {
22
HTTP_REQUEST_NODE_TYPE,
3-
MODAL_CANCEL,
4-
MODAL_CLOSE,
53
MODAL_CONFIRM,
64
PLACEHOLDER_EMPTY_WORKFLOW_ID,
75
PLACEHOLDER_FILLED_AT_EXECUTION_TIME,
@@ -70,12 +68,11 @@ import { useCanvasStore } from '@/stores/canvas.store';
7068
import { useSourceControlStore } from '@/stores/sourceControl.store';
7169
import { tryToParseNumber } from '@/utils/typesUtils';
7270
import { useI18n } from '@/composables/useI18n';
73-
import type { useRouter, NavigationGuardNext } from 'vue-router';
71+
import type { useRouter } from 'vue-router';
7472
import { useTelemetry } from '@/composables/useTelemetry';
7573
import { useProjectsStore } from '@/stores/projects.store';
7674
import { useTagsStore } from '@/stores/tags.store';
7775
import { useWorkflowsEEStore } from '@/stores/workflows.ee.store';
78-
import { useNpsSurveyStore } from '@/stores/npsSurvey.store';
7976
import { findWebhook } from '../api/webhooks';
8077

8178
export type ResolveParameterOptions = {
@@ -1161,63 +1158,6 @@ export function useWorkflowHelpers(options: { router: ReturnType<typeof useRoute
11611158
}
11621159
}
11631160

1164-
async function promptSaveUnsavedWorkflowChanges(
1165-
next: NavigationGuardNext,
1166-
{
1167-
confirm = async () => true,
1168-
cancel = async () => {},
1169-
}: {
1170-
confirm?: () => Promise<boolean>;
1171-
cancel?: () => Promise<void>;
1172-
} = {},
1173-
) {
1174-
if (uiStore.stateIsDirty) {
1175-
const npsSurveyStore = useNpsSurveyStore();
1176-
1177-
const confirmModal = await message.confirm(
1178-
i18n.baseText('generic.unsavedWork.confirmMessage.message'),
1179-
{
1180-
title: i18n.baseText('generic.unsavedWork.confirmMessage.headline'),
1181-
type: 'warning',
1182-
confirmButtonText: i18n.baseText('generic.unsavedWork.confirmMessage.confirmButtonText'),
1183-
cancelButtonText: i18n.baseText('generic.unsavedWork.confirmMessage.cancelButtonText'),
1184-
showClose: true,
1185-
},
1186-
);
1187-
if (confirmModal === MODAL_CONFIRM) {
1188-
const saved = await saveCurrentWorkflow({}, false);
1189-
if (saved) {
1190-
await npsSurveyStore.fetchPromptsData();
1191-
uiStore.stateIsDirty = false;
1192-
const goToNext = await confirm();
1193-
next(goToNext);
1194-
} else {
1195-
next(
1196-
router.resolve({
1197-
name: VIEWS.WORKFLOW,
1198-
params: { name: workflowsStore.workflow.id },
1199-
}),
1200-
);
1201-
}
1202-
} else if (confirmModal === MODAL_CANCEL) {
1203-
await cancel();
1204-
1205-
uiStore.stateIsDirty = false;
1206-
next();
1207-
} else if (confirmModal === MODAL_CLOSE) {
1208-
// The route may have already changed due to the browser back button, so let's restore it
1209-
next(
1210-
router.resolve({
1211-
name: VIEWS.WORKFLOW,
1212-
params: { name: workflowsStore.workflow.id },
1213-
}),
1214-
);
1215-
}
1216-
} else {
1217-
next();
1218-
}
1219-
}
1220-
12211161
function initState(workflowData: IWorkflowDb) {
12221162
workflowsStore.addWorkflow(workflowData);
12231163
workflowsStore.setActive(workflowData.active || false);
@@ -1310,7 +1250,6 @@ export function useWorkflowHelpers(options: { router: ReturnType<typeof useRoute
13101250
updateNodePositions,
13111251
removeForeignCredentialsFromWorkflow,
13121252
getWorkflowProjectRole,
1313-
promptSaveUnsavedWorkflowChanges,
13141253
initState,
13151254
getNodeParametersWithResolvedExpressions,
13161255
containsNodeFromPackage,
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
import { useUIStore } from '@/stores/ui.store';
2+
import { MODAL_CANCEL, MODAL_CONFIRM, PLACEHOLDER_EMPTY_WORKFLOW_ID, VIEWS } from '@/constants';
3+
import { useWorkflowSaving } from './useWorkflowSaving';
4+
import router from '@/router';
5+
import { createTestingPinia } from '@pinia/testing';
6+
import { setActivePinia } from 'pinia';
7+
import { useNpsSurveyStore } from '@/stores/npsSurvey.store';
8+
import { useWorkflowsStore } from '@/stores/workflows.store';
9+
10+
const modalConfirmSpy = vi.fn();
11+
const saveCurrentWorkflowSpy = vi.fn();
12+
13+
vi.mock('@/composables/useMessage', () => {
14+
return {
15+
useMessage: () => ({
16+
confirm: modalConfirmSpy,
17+
}),
18+
};
19+
});
20+
21+
vi.mock('@/composables/useWorkflowHelpers', () => {
22+
return {
23+
useWorkflowHelpers: () => ({
24+
saveCurrentWorkflow: saveCurrentWorkflowSpy,
25+
}),
26+
};
27+
});
28+
29+
describe('promptSaveUnsavedWorkflowChanges', () => {
30+
beforeAll(() => {
31+
setActivePinia(createTestingPinia());
32+
});
33+
34+
beforeEach(() => {
35+
vi.resetAllMocks();
36+
});
37+
38+
it('should prompt the user to save changes and proceed if confirmed', async () => {
39+
const { promptSaveUnsavedWorkflowChanges } = useWorkflowSaving({ router });
40+
const next = vi.fn();
41+
const confirm = vi.fn().mockResolvedValue(true);
42+
const cancel = vi.fn();
43+
44+
// Mock state
45+
const uiStore = useUIStore();
46+
uiStore.stateIsDirty = true;
47+
48+
const npsSurveyStore = useNpsSurveyStore();
49+
vi.spyOn(npsSurveyStore, 'fetchPromptsData').mockResolvedValue();
50+
51+
saveCurrentWorkflowSpy.mockResolvedValue(true);
52+
53+
// Mock message.confirm
54+
modalConfirmSpy.mockResolvedValue(MODAL_CONFIRM);
55+
56+
await promptSaveUnsavedWorkflowChanges(next, { confirm, cancel });
57+
58+
expect(modalConfirmSpy).toHaveBeenCalled();
59+
expect(npsSurveyStore.fetchPromptsData).toHaveBeenCalled();
60+
expect(saveCurrentWorkflowSpy).toHaveBeenCalledWith({}, false);
61+
expect(uiStore.stateIsDirty).toEqual(false);
62+
63+
expect(confirm).toHaveBeenCalled();
64+
expect(next).toHaveBeenCalledWith(true);
65+
expect(cancel).not.toHaveBeenCalled();
66+
});
67+
68+
it('should not proceed if the user cancels the confirmation modal', async () => {
69+
const { promptSaveUnsavedWorkflowChanges } = useWorkflowSaving({ router });
70+
const next = vi.fn();
71+
const confirm = vi.fn();
72+
const cancel = vi.fn();
73+
74+
// Mock state
75+
const uiStore = useUIStore();
76+
uiStore.stateIsDirty = true;
77+
78+
// Mock message.confirm
79+
modalConfirmSpy.mockResolvedValue(MODAL_CANCEL);
80+
81+
await promptSaveUnsavedWorkflowChanges(next, { confirm, cancel });
82+
83+
expect(modalConfirmSpy).toHaveBeenCalled();
84+
expect(saveCurrentWorkflowSpy).not.toHaveBeenCalled();
85+
expect(uiStore.stateIsDirty).toEqual(false);
86+
87+
expect(confirm).not.toHaveBeenCalled();
88+
expect(cancel).toHaveBeenCalled();
89+
expect(next).toHaveBeenCalledWith();
90+
});
91+
92+
it('should restore the route if the modal is closed and the workflow is not new', async () => {
93+
const { promptSaveUnsavedWorkflowChanges } = useWorkflowSaving({ router });
94+
const next = vi.fn();
95+
const confirm = vi.fn();
96+
const cancel = vi.fn();
97+
98+
// Mock state
99+
const uiStore = useUIStore();
100+
uiStore.stateIsDirty = true;
101+
102+
const workflowStore = useWorkflowsStore();
103+
const MOCK_ID = 'existing-workflow-id';
104+
workflowStore.workflow.id = MOCK_ID;
105+
106+
// Mock message.confirm
107+
modalConfirmSpy.mockResolvedValue('close');
108+
109+
await promptSaveUnsavedWorkflowChanges(next, { confirm, cancel });
110+
111+
expect(modalConfirmSpy).toHaveBeenCalled();
112+
expect(saveCurrentWorkflowSpy).not.toHaveBeenCalled();
113+
expect(uiStore.stateIsDirty).toEqual(true);
114+
115+
expect(confirm).not.toHaveBeenCalled();
116+
expect(cancel).not.toHaveBeenCalled();
117+
expect(next).toHaveBeenCalledWith(
118+
router.resolve({
119+
name: VIEWS.WORKFLOW,
120+
params: { name: MOCK_ID },
121+
}),
122+
);
123+
});
124+
125+
it('should close modal if workflow is not new', async () => {
126+
const { promptSaveUnsavedWorkflowChanges } = useWorkflowSaving({ router });
127+
const next = vi.fn();
128+
const confirm = vi.fn();
129+
const cancel = vi.fn();
130+
131+
// Mock state
132+
const uiStore = useUIStore();
133+
uiStore.stateIsDirty = true;
134+
135+
const workflowStore = useWorkflowsStore();
136+
workflowStore.workflow.id = PLACEHOLDER_EMPTY_WORKFLOW_ID;
137+
138+
// Mock message.confirm
139+
modalConfirmSpy.mockResolvedValue('close');
140+
141+
await promptSaveUnsavedWorkflowChanges(next, { confirm, cancel });
142+
143+
expect(modalConfirmSpy).toHaveBeenCalled();
144+
expect(saveCurrentWorkflowSpy).not.toHaveBeenCalled();
145+
expect(uiStore.stateIsDirty).toEqual(true);
146+
147+
expect(confirm).not.toHaveBeenCalled();
148+
expect(cancel).not.toHaveBeenCalled();
149+
expect(next).not.toHaveBeenCalled();
150+
});
151+
152+
it('should proceed without prompting if there are no unsaved changes', async () => {
153+
const { promptSaveUnsavedWorkflowChanges } = useWorkflowSaving({ router });
154+
const next = vi.fn();
155+
const confirm = vi.fn();
156+
const cancel = vi.fn();
157+
158+
// Mock state
159+
const uiStore = useUIStore();
160+
uiStore.stateIsDirty = false;
161+
162+
await promptSaveUnsavedWorkflowChanges(next, { confirm, cancel });
163+
164+
expect(modalConfirmSpy).not.toHaveBeenCalled();
165+
expect(saveCurrentWorkflowSpy).not.toHaveBeenCalled();
166+
expect(uiStore.stateIsDirty).toEqual(false);
167+
168+
expect(confirm).not.toHaveBeenCalled();
169+
expect(cancel).not.toHaveBeenCalled();
170+
expect(next).toHaveBeenCalledWith();
171+
});
172+
173+
it('should handle save failure and restore the route', async () => {
174+
const { promptSaveUnsavedWorkflowChanges } = useWorkflowSaving({ router });
175+
const next = vi.fn();
176+
const confirm = vi.fn();
177+
const cancel = vi.fn();
178+
179+
// Mock state
180+
const uiStore = useUIStore();
181+
uiStore.stateIsDirty = true;
182+
183+
const workflowStore = useWorkflowsStore();
184+
const MOCK_ID = 'existing-workflow-id';
185+
workflowStore.workflow.id = MOCK_ID;
186+
187+
saveCurrentWorkflowSpy.mockResolvedValue(false);
188+
189+
// Mock message.confirm
190+
modalConfirmSpy.mockResolvedValue(MODAL_CONFIRM);
191+
192+
await promptSaveUnsavedWorkflowChanges(next, { confirm, cancel });
193+
194+
expect(modalConfirmSpy).toHaveBeenCalled();
195+
expect(saveCurrentWorkflowSpy).toHaveBeenCalledWith({}, false);
196+
expect(uiStore.stateIsDirty).toEqual(true);
197+
198+
expect(confirm).not.toHaveBeenCalled();
199+
expect(cancel).not.toHaveBeenCalled();
200+
expect(next).toHaveBeenCalledWith(
201+
router.resolve({
202+
name: VIEWS.WORKFLOW,
203+
params: { name: MOCK_ID },
204+
}),
205+
);
206+
});
207+
});

0 commit comments

Comments
 (0)