mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-25 04:34:06 -08:00
fix(editor): Stop unsaved changes popup display when navigating away from an untouched workflow (#5259)
* fix(editor): Stop unsaved changes popup showing up after loading a workflow * fix(editor): Fix unsaved change confirmation display
This commit is contained in:
parent
87ceb6f4b8
commit
6a93aed3a2
|
@ -24,6 +24,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import ExecutionsSidebar from '@/components/ExecutionsView/ExecutionsSidebar.vue';
|
import ExecutionsSidebar from '@/components/ExecutionsView/ExecutionsSidebar.vue';
|
||||||
import {
|
import {
|
||||||
|
MAIN_HEADER_TABS,
|
||||||
MODAL_CANCEL,
|
MODAL_CANCEL,
|
||||||
MODAL_CLOSE,
|
MODAL_CLOSE,
|
||||||
MODAL_CONFIRMED,
|
MODAL_CONFIRMED,
|
||||||
|
@ -121,36 +122,34 @@ export default mixins(
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
async beforeRouteLeave(to, from, next) {
|
async beforeRouteLeave(to, from, next) {
|
||||||
const nextTab = getNodeViewTab(to);
|
if (getNodeViewTab(to) === MAIN_HEADER_TABS.WORKFLOW) {
|
||||||
// When leaving for a page that's not a workflow view tab, ask to save changes
|
next();
|
||||||
if (!nextTab) {
|
return;
|
||||||
const result = this.uiStore.stateIsDirty;
|
}
|
||||||
if (result) {
|
if (this.uiStore.stateIsDirty) {
|
||||||
const confirmModal = await this.confirmModal(
|
const confirmModal = await this.confirmModal(
|
||||||
this.$locale.baseText('generic.unsavedWork.confirmMessage.message'),
|
this.$locale.baseText('generic.unsavedWork.confirmMessage.message'),
|
||||||
this.$locale.baseText('generic.unsavedWork.confirmMessage.headline'),
|
this.$locale.baseText('generic.unsavedWork.confirmMessage.headline'),
|
||||||
'warning',
|
'warning',
|
||||||
this.$locale.baseText('generic.unsavedWork.confirmMessage.confirmButtonText'),
|
this.$locale.baseText('generic.unsavedWork.confirmMessage.confirmButtonText'),
|
||||||
this.$locale.baseText('generic.unsavedWork.confirmMessage.cancelButtonText'),
|
this.$locale.baseText('generic.unsavedWork.confirmMessage.cancelButtonText'),
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (confirmModal === MODAL_CONFIRMED) {
|
if (confirmModal === MODAL_CONFIRMED) {
|
||||||
const saved = await this.saveCurrentWorkflow({}, false);
|
const saved = await this.saveCurrentWorkflow({}, false);
|
||||||
if (saved) this.settingsStore.fetchPromptsData();
|
if (saved) {
|
||||||
this.uiStore.stateIsDirty = false;
|
await this.settingsStore.fetchPromptsData();
|
||||||
next();
|
|
||||||
} else if (confirmModal === MODAL_CANCEL) {
|
|
||||||
this.uiStore.stateIsDirty = false;
|
|
||||||
next();
|
|
||||||
} else if (confirmModal === MODAL_CLOSE) {
|
|
||||||
next(false);
|
|
||||||
}
|
}
|
||||||
} else {
|
this.uiStore.stateIsDirty = false;
|
||||||
|
next();
|
||||||
|
} else if (confirmModal === MODAL_CANCEL) {
|
||||||
|
this.uiStore.stateIsDirty = false;
|
||||||
next();
|
next();
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
next();
|
||||||
}
|
}
|
||||||
next();
|
|
||||||
},
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
|
|
|
@ -315,7 +315,7 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, {
|
||||||
},
|
},
|
||||||
|
|
||||||
setWorkflowName(data: { newName: string; setStateDirty: boolean }): void {
|
setWorkflowName(data: { newName: string; setStateDirty: boolean }): void {
|
||||||
if (data.setStateDirty === true) {
|
if (data.setStateDirty) {
|
||||||
const uiStore = useUIStore();
|
const uiStore = useUIStore();
|
||||||
uiStore.stateIsDirty = true;
|
uiStore.stateIsDirty = true;
|
||||||
}
|
}
|
||||||
|
@ -532,16 +532,12 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, {
|
||||||
dataPinningEventBus.$emit('unpin-data', { [payload.node.name]: undefined });
|
dataPinningEventBus.$emit('unpin-data', { [payload.node.name]: undefined });
|
||||||
},
|
},
|
||||||
|
|
||||||
addConnection(data: { connection: IConnection[]; setStateDirty: boolean }): void {
|
addConnection(data: { connection: IConnection[] }): void {
|
||||||
if (data.connection.length !== 2) {
|
if (data.connection.length !== 2) {
|
||||||
// All connections need two entries
|
// All connections need two entries
|
||||||
// TODO: Check if there is an error or whatever that is supposed to be returned
|
// TODO: Check if there is an error or whatever that is supposed to be returned
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const uiStore = useUIStore();
|
|
||||||
if (data.setStateDirty === true) {
|
|
||||||
uiStore.stateIsDirty = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const sourceData: IConnection = data.connection[0];
|
const sourceData: IConnection = data.connection[0];
|
||||||
const destinationData: IConnection = data.connection[1];
|
const destinationData: IConnection = data.connection[1];
|
||||||
|
@ -626,7 +622,7 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, {
|
||||||
},
|
},
|
||||||
|
|
||||||
removeAllConnections(data: { setStateDirty: boolean }): void {
|
removeAllConnections(data: { setStateDirty: boolean }): void {
|
||||||
if (data && data.setStateDirty === true) {
|
if (data && data.setStateDirty) {
|
||||||
const uiStore = useUIStore();
|
const uiStore = useUIStore();
|
||||||
uiStore.stateIsDirty = true;
|
uiStore.stateIsDirty = true;
|
||||||
}
|
}
|
||||||
|
@ -771,7 +767,7 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, {
|
||||||
},
|
},
|
||||||
|
|
||||||
removeAllNodes(data: { setStateDirty: boolean; removePinData: boolean }): void {
|
removeAllNodes(data: { setStateDirty: boolean; removePinData: boolean }): void {
|
||||||
if (data.setStateDirty === true) {
|
if (data.setStateDirty) {
|
||||||
const uiStore = useUIStore();
|
const uiStore = useUIStore();
|
||||||
uiStore.stateIsDirty = true;
|
uiStore.stateIsDirty = true;
|
||||||
}
|
}
|
||||||
|
|
|
@ -76,19 +76,14 @@ export const closestNumberDivisibleBy = (inputNumber: number, divisibleBy: numbe
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getNodeViewTab = (route: Route): string | null => {
|
export const getNodeViewTab = (route: Route): string | null => {
|
||||||
const routeMeta = route.meta;
|
if (route.meta?.nodeView) {
|
||||||
if (routeMeta && routeMeta.nodeView === true) {
|
|
||||||
return MAIN_HEADER_TABS.WORKFLOW;
|
return MAIN_HEADER_TABS.WORKFLOW;
|
||||||
} else {
|
} else if (
|
||||||
const executionTabRoutes = [
|
[VIEWS.WORKFLOW_EXECUTIONS, VIEWS.EXECUTION_PREVIEW, VIEWS.EXECUTION_HOME]
|
||||||
VIEWS.WORKFLOW_EXECUTIONS.toString(),
|
.map(String)
|
||||||
VIEWS.EXECUTION_PREVIEW.toString(),
|
.includes(String(route.name))
|
||||||
VIEWS.EXECUTION_HOME.toString(),
|
) {
|
||||||
];
|
return MAIN_HEADER_TABS.EXECUTIONS;
|
||||||
|
|
||||||
if (executionTabRoutes.includes(route.name || '')) {
|
|
||||||
return MAIN_HEADER_TABS.EXECUTIONS;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
|
@ -402,55 +402,45 @@ export default mixins(
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
async beforeRouteLeave(to, from, next) {
|
async beforeRouteLeave(to, from, next) {
|
||||||
const nextTab = getNodeViewTab(to);
|
if (getNodeViewTab(to) === MAIN_HEADER_TABS.EXECUTIONS || from.name === VIEWS.TEMPLATE_IMPORT) {
|
||||||
// Only react if leaving workflow tab and going to a separate page
|
next();
|
||||||
if (!nextTab) {
|
return;
|
||||||
// Skip check if in the middle of template import
|
}
|
||||||
if (from.name === VIEWS.TEMPLATE_IMPORT) {
|
// Make sure workflow id is empty when leaving the editor
|
||||||
next();
|
this.workflowsStore.setWorkflowId(PLACEHOLDER_EMPTY_WORKFLOW_ID);
|
||||||
return;
|
if (this.uiStore.stateIsDirty) {
|
||||||
}
|
const confirmModal = await this.confirmModal(
|
||||||
// Make sure workflow id is empty when leaving the editor
|
this.$locale.baseText('generic.unsavedWork.confirmMessage.message'),
|
||||||
this.workflowsStore.setWorkflowId(PLACEHOLDER_EMPTY_WORKFLOW_ID);
|
this.$locale.baseText('generic.unsavedWork.confirmMessage.headline'),
|
||||||
const result = this.uiStore.stateIsDirty;
|
'warning',
|
||||||
if (result) {
|
this.$locale.baseText('generic.unsavedWork.confirmMessage.confirmButtonText'),
|
||||||
const confirmModal = await this.confirmModal(
|
this.$locale.baseText('generic.unsavedWork.confirmMessage.cancelButtonText'),
|
||||||
this.$locale.baseText('generic.unsavedWork.confirmMessage.message'),
|
true,
|
||||||
this.$locale.baseText('generic.unsavedWork.confirmMessage.headline'),
|
);
|
||||||
'warning',
|
if (confirmModal === MODAL_CONFIRMED) {
|
||||||
this.$locale.baseText('generic.unsavedWork.confirmMessage.confirmButtonText'),
|
const saved = await this.saveCurrentWorkflow({}, false);
|
||||||
this.$locale.baseText('generic.unsavedWork.confirmMessage.cancelButtonText'),
|
if (saved) {
|
||||||
true,
|
await this.settingsStore.fetchPromptsData();
|
||||||
);
|
|
||||||
|
|
||||||
if (confirmModal === MODAL_CONFIRMED) {
|
|
||||||
const saved = await this.saveCurrentWorkflow({}, false);
|
|
||||||
if (saved) await this.settingsStore.fetchPromptsData();
|
|
||||||
this.uiStore.stateIsDirty = false;
|
|
||||||
|
|
||||||
if (from.name === VIEWS.NEW_WORKFLOW) {
|
|
||||||
// Replace the current route with the new workflow route
|
|
||||||
// before navigating to the new route when saving new workflow.
|
|
||||||
this.$router.replace(
|
|
||||||
{ name: VIEWS.WORKFLOW, params: { name: this.currentWorkflow } },
|
|
||||||
() => {
|
|
||||||
// We can't use next() here since vue-router
|
|
||||||
// would prevent the navigation with an error
|
|
||||||
this.$router.push(to as RawLocation);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
next();
|
|
||||||
}
|
|
||||||
} else if (confirmModal === MODAL_CANCEL) {
|
|
||||||
await this.resetWorkspace();
|
|
||||||
this.uiStore.stateIsDirty = false;
|
|
||||||
|
|
||||||
next();
|
|
||||||
} else if (confirmModal === MODAL_CLOSE) {
|
|
||||||
next(false);
|
|
||||||
}
|
}
|
||||||
} else {
|
this.uiStore.stateIsDirty = false;
|
||||||
|
|
||||||
|
if (from.name === VIEWS.NEW_WORKFLOW) {
|
||||||
|
// Replace the current route with the new workflow route
|
||||||
|
// before navigating to the new route when saving new workflow.
|
||||||
|
this.$router.replace(
|
||||||
|
{ name: VIEWS.WORKFLOW, params: { name: this.currentWorkflow } },
|
||||||
|
() => {
|
||||||
|
// We can't use next() here since vue-router
|
||||||
|
// would prevent the navigation with an error
|
||||||
|
this.$router.push(to as RawLocation);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
} else if (confirmModal === MODAL_CANCEL) {
|
||||||
|
await this.resetWorkspace();
|
||||||
|
this.uiStore.stateIsDirty = false;
|
||||||
next();
|
next();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -1979,8 +1969,9 @@ export default mixins(
|
||||||
sourceNodeOutputIndex: number,
|
sourceNodeOutputIndex: number,
|
||||||
targetNodeName: string,
|
targetNodeName: string,
|
||||||
targetNodeOuputIndex: number,
|
targetNodeOuputIndex: number,
|
||||||
trackHistory = false,
|
|
||||||
) {
|
) {
|
||||||
|
this.uiStore.stateIsDirty = true;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
this.getConnection(
|
this.getConnection(
|
||||||
sourceNodeName,
|
sourceNodeName,
|
||||||
|
@ -2039,17 +2030,11 @@ export default mixins(
|
||||||
|
|
||||||
const targetNodeName = lastSelectedConnection.__meta.targetNodeName;
|
const targetNodeName = lastSelectedConnection.__meta.targetNodeName;
|
||||||
const targetOutputIndex = lastSelectedConnection.__meta.targetOutputIndex;
|
const targetOutputIndex = lastSelectedConnection.__meta.targetOutputIndex;
|
||||||
this.connectTwoNodes(
|
this.connectTwoNodes(newNodeData.name, 0, targetNodeName, targetOutputIndex);
|
||||||
newNodeData.name,
|
|
||||||
0,
|
|
||||||
targetNodeName,
|
|
||||||
targetOutputIndex,
|
|
||||||
trackHistory,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Connect active node to the newly created one
|
// Connect active node to the newly created one
|
||||||
this.connectTwoNodes(lastSelectedNode.name, outputIndex, newNodeData.name, 0, trackHistory);
|
this.connectTwoNodes(lastSelectedNode.name, outputIndex, newNodeData.name, 0);
|
||||||
}
|
}
|
||||||
this.historyStore.stopRecordingUndo();
|
this.historyStore.stopRecordingUndo();
|
||||||
},
|
},
|
||||||
|
@ -2102,13 +2087,7 @@ export default mixins(
|
||||||
const sourceNodeName = sourceNode.name;
|
const sourceNodeName = sourceNode.name;
|
||||||
const outputIndex = connection.parameters.index;
|
const outputIndex = connection.parameters.index;
|
||||||
|
|
||||||
this.connectTwoNodes(
|
this.connectTwoNodes(sourceNodeName, outputIndex, this.pullConnActiveNodeName, 0);
|
||||||
sourceNodeName,
|
|
||||||
outputIndex,
|
|
||||||
this.pullConnActiveNodeName,
|
|
||||||
0,
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
this.pullConnActiveNodeName = null;
|
this.pullConnActiveNodeName = null;
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
|
@ -2181,10 +2160,6 @@ export default mixins(
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
this.workflowsStore.addConnection({
|
|
||||||
connection: connectionData,
|
|
||||||
setStateDirty: true,
|
|
||||||
});
|
|
||||||
this.dropPrevented = true;
|
this.dropPrevented = true;
|
||||||
if (!this.suspendRecordingDetachedConnections) {
|
if (!this.suspendRecordingDetachedConnections) {
|
||||||
this.historyStore.pushCommandToUndo(new AddConnectionCommand(connectionData));
|
this.historyStore.pushCommandToUndo(new AddConnectionCommand(connectionData));
|
||||||
|
@ -2331,7 +2306,7 @@ export default mixins(
|
||||||
if (connectionInfo) {
|
if (connectionInfo) {
|
||||||
this.historyStore.pushCommandToUndo(new RemoveConnectionCommand(connectionInfo));
|
this.historyStore.pushCommandToUndo(new RemoveConnectionCommand(connectionInfo));
|
||||||
}
|
}
|
||||||
this.connectTwoNodes(sourceNodeName, outputIndex, this.pullConnActiveNodeName, 0, true);
|
this.connectTwoNodes(sourceNodeName, outputIndex, this.pullConnActiveNodeName, 0);
|
||||||
this.pullConnActiveNodeName = null;
|
this.pullConnActiveNodeName = null;
|
||||||
await this.$nextTick();
|
await this.$nextTick();
|
||||||
this.historyStore.stopRecordingUndo();
|
this.historyStore.stopRecordingUndo();
|
||||||
|
@ -2553,7 +2528,7 @@ export default mixins(
|
||||||
window.addEventListener('beforeunload', (e) => {
|
window.addEventListener('beforeunload', (e) => {
|
||||||
if (this.isDemo) {
|
if (this.isDemo) {
|
||||||
return;
|
return;
|
||||||
} else if (this.uiStore.stateIsDirty === true) {
|
} else if (this.uiStore.stateIsDirty) {
|
||||||
const confirmationMessage = this.$locale.baseText(
|
const confirmationMessage = this.$locale.baseText(
|
||||||
'nodeView.itLooksLikeYouHaveBeenEditingSomething',
|
'nodeView.itLooksLikeYouHaveBeenEditingSomething',
|
||||||
);
|
);
|
||||||
|
@ -2595,12 +2570,8 @@ export default mixins(
|
||||||
uuids: uuid,
|
uuids: uuid,
|
||||||
detachable: !this.isReadOnly,
|
detachable: !this.isReadOnly,
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
const connectionProperties = { connection, setStateDirty: false };
|
|
||||||
// When nodes get connected it gets saved automatically to the storage
|
|
||||||
// so if we do not connect we have to save the connection manually
|
|
||||||
this.workflowsStore.addConnection(connectionProperties);
|
|
||||||
}
|
}
|
||||||
|
this.workflowsStore.addConnection({ connection });
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.addPinDataConnections(this.workflowsStore.pinData);
|
this.addPinDataConnections(this.workflowsStore.pinData);
|
||||||
|
@ -2984,7 +2955,6 @@ export default mixins(
|
||||||
sourceNodeOutputIndex,
|
sourceNodeOutputIndex,
|
||||||
targetNodeName,
|
targetNodeName,
|
||||||
targetNodeOuputIndex,
|
targetNodeOuputIndex,
|
||||||
trackHistory,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (waitForNewConnection) {
|
if (waitForNewConnection) {
|
||||||
|
|
Loading…
Reference in a new issue