diff --git a/packages/editor-ui/src/components/MainHeader.vue b/packages/editor-ui/src/components/MainHeader.vue index 2b9696e2b3..4624ad4034 100644 --- a/packages/editor-ui/src/components/MainHeader.vue +++ b/packages/editor-ui/src/components/MainHeader.vue @@ -12,7 +12,7 @@ - of + of "{{workflowName}}" @@ -154,6 +154,9 @@ export default mixins( workflowRunning (): boolean { return this.$store.getters.isActionActive('workflowRunning'); }, + isDirty () : boolean { + return this.$store.getters.getStateIsDirty; + }, }, methods: { async openWorkflow (workflowId: string) { diff --git a/packages/editor-ui/src/components/MainSidebar.vue b/packages/editor-ui/src/components/MainSidebar.vue index ea2a8b4cef..92645942b2 100644 --- a/packages/editor-ui/src/components/MainSidebar.vue +++ b/packages/editor-ui/src/components/MainSidebar.vue @@ -398,7 +398,7 @@ export default mixins( return; } - this.$store.commit('setWorkflowName', workflowName); + this.$store.commit('setWorkflowName', {newName: workflowName, setStateDirty: true}); this.$showMessage({ title: 'Workflow renamed', @@ -440,18 +440,15 @@ export default mixins( saveAs(blob, workflowName + '.json'); } else if (key === 'workflow-save') { - console.log("saving......"); this.saveCurrentWorkflow(); } else if (key === 'workflow-save-as') { - console.log("saving......"); this.saveCurrentWorkflow(true); } else if (key === 'help-about') { this.aboutDialogVisible = true; } else if (key === 'workflow-settings') { this.workflowSettingsDialogVisible = true; } else if (key === 'workflow-new') { - const workflowId = this.$store.getters.workflowId; - const result = await this.dataHasChanged(workflowId); + const result = this.$store.getters.getStateIsDirty; if(result) { const importConfirm = await this.confirmMessage(`When you switch workflows your current workflow changes will be lost.`, 'Save your Changes?', 'warning', 'Yes, switch workflows and forget changes'); if (importConfirm === true) { diff --git a/packages/editor-ui/src/components/WorkflowOpen.vue b/packages/editor-ui/src/components/WorkflowOpen.vue index 45f1cfa0ff..5ef981ef5b 100644 --- a/packages/editor-ui/src/components/WorkflowOpen.vue +++ b/packages/editor-ui/src/components/WorkflowOpen.vue @@ -92,8 +92,7 @@ export default mixins( }, async openWorkflow (data: IWorkflowShortResponse, column: any) { // tslint:disable-line:no-any if (column.label !== 'Active') { - const workflowId = this.$store.getters.workflowId; - const result = await this.dataHasChanged(workflowId); + const result = this.$store.getters.getStateIsDirty; if(result) { const importConfirm = await this.confirmMessage(`When you switch workflows your current workflow changes will be lost.`, 'Save your Changes?', 'warning', 'Yes, switch workflows and forget changes'); if (importConfirm === false) { diff --git a/packages/editor-ui/src/components/mixins/moveNodeWorkflow.ts b/packages/editor-ui/src/components/mixins/moveNodeWorkflow.ts index b526dd5c75..9dcf86aeac 100644 --- a/packages/editor-ui/src/components/mixins/moveNodeWorkflow.ts +++ b/packages/editor-ui/src/components/mixins/moveNodeWorkflow.ts @@ -31,7 +31,7 @@ export const moveNodeWorkflow = mixins(nodeIndex).extend({ const nodeViewOffsetPositionX = offsetPosition[0] + (e.pageX - this.moveLastPosition[0]); const nodeViewOffsetPositionY = offsetPosition[1] + (e.pageY - this.moveLastPosition[1]); - this.$store.commit('setNodeViewOffsetPosition', [nodeViewOffsetPositionX, nodeViewOffsetPositionY]); + this.$store.commit('setNodeViewOffsetPosition', {newOffset: [nodeViewOffsetPositionX, nodeViewOffsetPositionY], setStateDirty: true}); // Update the last position this.moveLastPosition[0] = e.pageX; @@ -87,7 +87,7 @@ export const moveNodeWorkflow = mixins(nodeIndex).extend({ const offsetPosition = this.$store.getters.getNodeViewOffsetPosition; const nodeViewOffsetPositionX = offsetPosition[0] - e.deltaX; const nodeViewOffsetPositionY = offsetPosition[1] - e.deltaY; - this.$store.commit('setNodeViewOffsetPosition', [nodeViewOffsetPositionX, nodeViewOffsetPositionY]); + this.$store.commit('setNodeViewOffsetPosition', {newOffset: [nodeViewOffsetPositionX, nodeViewOffsetPositionY], setStateDirty: true}); }, }, }); diff --git a/packages/editor-ui/src/components/mixins/workflowHelpers.ts b/packages/editor-ui/src/components/mixins/workflowHelpers.ts index b3c2416b28..d5354bf2d8 100644 --- a/packages/editor-ui/src/components/mixins/workflowHelpers.ts +++ b/packages/editor-ui/src/components/mixins/workflowHelpers.ts @@ -420,7 +420,7 @@ export const workflowHelpers = mixins( this.$store.commit('setActive', workflowData.active || false); this.$store.commit('setWorkflowId', workflowData.id); - this.$store.commit('setWorkflowName', workflowData.name); + this.$store.commit('setWorkflowName', {newName: workflowData.name, setStateDirty: false}); this.$store.commit('setWorkflowSettings', workflowData.settings || {}); } else { // Workflow exists already so update it @@ -435,7 +435,7 @@ export const workflowHelpers = mixins( } this.$store.commit('removeActiveAction', 'workflowSaving'); - + this.$store.commit('setStateDirty', false); this.$showMessage({ title: 'Workflow saved', message: `The workflow "${workflowData.name}" got saved!`, @@ -492,13 +492,13 @@ export const workflowHelpers = mixins( nodes: data.nodes, connections: data.connections, settings: data.settings, - name: data.name + name: data.name, }; const y = { nodes: currentData.nodes, connections: currentData.connections, settings: currentData.settings, - name: currentData.name + name: currentData.name, }; return !isEqual(x, y); } diff --git a/packages/editor-ui/src/components/mixins/workflowSave.ts b/packages/editor-ui/src/components/mixins/workflowSave.ts index 1584d9a7c8..c4d4001047 100644 --- a/packages/editor-ui/src/components/mixins/workflowSave.ts +++ b/packages/editor-ui/src/components/mixins/workflowSave.ts @@ -74,7 +74,7 @@ export const workflowSave = mixins( this.$store.commit('setActive', workflowData.active || false); this.$store.commit('setWorkflowId', workflowData.id); - this.$store.commit('setWorkflowName', workflowData.name); + this.$store.commit('setWorkflowName', {newName: workflowData.name, setStateDirty: false}); this.$store.commit('setWorkflowSettings', workflowData.settings || {}); } else { // Workflow exists already so update it @@ -89,7 +89,7 @@ export const workflowSave = mixins( } this.$store.commit('removeActiveAction', 'workflowSaving'); - + this.$store.commit('setStateDirty', false); this.$showMessage({ title: 'Workflow saved', message: `The workflow "${workflowData.name}" got saved!`, diff --git a/packages/editor-ui/src/store.ts b/packages/editor-ui/src/store.ts index 1b54f3c1ad..9359478345 100644 --- a/packages/editor-ui/src/store.ts +++ b/packages/editor-ui/src/store.ts @@ -52,6 +52,7 @@ export const store = new Vuex.Store({ saveDataSuccessExecution: 'all', saveManualExecutions: false, timezone: 'America/New_York', + stateIsDirty: false, executionTimeout: -1, maxExecutionTimeout: Number.MAX_SAFE_INTEGER, versionCli: '0.0.0', @@ -83,6 +84,7 @@ export const store = new Vuex.Store({ state.activeActions.push(action); } }, + removeActiveAction (state, action: string) { const actionIndex = state.activeActions.indexOf(action); if (actionIndex !== -1) { @@ -92,6 +94,7 @@ export const store = new Vuex.Store({ // Active Executions addActiveExecution (state, newActiveExecution: IExecutionsCurrentSummaryExtended) { + state.stateIsDirty = true; // Check if the execution exists already const activeExecution = state.activeExecutions.find(execution => { return execution.idActive === newActiveExecution.idActive; @@ -108,6 +111,7 @@ export const store = new Vuex.Store({ state.activeExecutions.unshift(newActiveExecution); }, finishActiveExecution (state, finishedActiveExecution: IPushDataExecutionFinished) { + state.stateIsDirty = true; // Find the execution to set to finished const activeExecution = state.activeExecutions.find(execution => { return execution.idActive === finishedActiveExecution.executionIdActive; @@ -126,6 +130,7 @@ export const store = new Vuex.Store({ Vue.set(activeExecution, 'stoppedAt', finishedActiveExecution.data.stoppedAt); }, setActiveExecutions (state, newActiveExecutions: IExecutionsCurrentSummaryExtended[]) { + state.stateIsDirty = true; Vue.set(state, 'activeExecutions', newActiveExecutions); }, @@ -134,23 +139,32 @@ export const store = new Vuex.Store({ state.activeWorkflows = newActiveWorkflows; }, setWorkflowActive (state, workflowId: string) { + state.stateIsDirty = true; const index = state.activeWorkflows.indexOf(workflowId); if (index === -1) { state.activeWorkflows.push(workflowId); } }, setWorkflowInactive (state, workflowId: string) { + state.stateIsDirty = true; const index = state.activeWorkflows.indexOf(workflowId); if (index !== -1) { state.selectedNodes.splice(index, 1); } }, + // Set state condition dirty or not + // ** Dirty: if current workflow state has been synchronized with database AKA has it been saved + setStateDirty (state, dirty : boolean) { + state.stateIsDirty = dirty; + }, // Selected Nodes addSelectedNode (state, node: INodeUi) { + state.stateIsDirty = true; state.selectedNodes.push(node); }, removeNodeFromSelection (state, node: INodeUi) { + state.stateIsDirty = true; let index; for (index in state.selectedNodes) { if (state.selectedNodes[index].name === node.name) { @@ -176,6 +190,10 @@ export const store = new Vuex.Store({ return; } + if (data.setStateDirty) { + state.stateIsDirty = true; + } + const sourceData: IConnection = data.connection[0]; const destinationData: IConnection = data.connection[1]; @@ -211,6 +229,7 @@ export const store = new Vuex.Store({ if (connectionExists === false) { state.workflow.connections[sourceData.node][sourceData.type][sourceData.index].push(destinationData); } + }, removeConnection (state, data) { const sourceData = data.connection[0]; @@ -226,6 +245,8 @@ export const store = new Vuex.Store({ return; } + state.stateIsDirty = true; + const connections = state.workflow.connections[sourceData.node][sourceData.type][sourceData.index]; for (const index in connections) { if (connections[index].node === destinationData.node && connections[index].type === destinationData.type && connections[index].index === destinationData.index) { @@ -233,11 +254,16 @@ export const store = new Vuex.Store({ connections.splice(parseInt(index, 10), 1); } } + }, - removeAllConnections (state) { + removeAllConnections (state, data) { + if (data.setStateDirty === true) { + state.stateIsDirty = true; + } state.workflow.connections = {}; }, removeAllNodeConnection (state, node: INodeUi) { + state.stateIsDirty = true; // Remove all source connections if (state.workflow.connections.hasOwnProperty(node.name)) { delete state.workflow.connections[node.name]; @@ -275,6 +301,7 @@ export const store = new Vuex.Store({ if (state.credentials === null) { return; } + for (let i = 0; i < state.credentials.length; i++) { if (state.credentials[i].id === credentialData.id) { state.credentials.splice(i, 1); @@ -286,6 +313,7 @@ export const store = new Vuex.Store({ if (state.credentials === null) { return; } + for (let i = 0; i < state.credentials.length; i++) { if (state.credentials[i].id === credentialData.id) { state.credentials[i] = credentialData; @@ -301,6 +329,7 @@ export const store = new Vuex.Store({ }, renameNodeSelectedAndExecution (state, nameData) { + state.stateIsDirty = true; // If node has any WorkflowResultData rename also that one that the data // does still get displayed also after node got renamed if (state.workflowExecutionData !== null && state.workflowExecutionData.data.resultData.runData.hasOwnProperty(nameData.old)) { @@ -318,10 +347,12 @@ export const store = new Vuex.Store({ state.workflow.nodes.forEach((node) => { node.issues = undefined; }); + return true; }, setNodeIssue (state, nodeIssueData: INodeIssueData) { + const node = state.workflow.nodes.find(node => { return node.name === nodeIssueData.node; }); @@ -345,6 +376,7 @@ export const store = new Vuex.Store({ // Set/Overwrite the value Vue.set(node.issues!, nodeIssueData.type, nodeIssueData.value); + state.stateIsDirty = true; } return true; @@ -356,8 +388,11 @@ export const store = new Vuex.Store({ }, // Name - setWorkflowName (state, newName: string) { - state.workflow.name = newName; + setWorkflowName (state, data) { + if (data.setStateDirty === true) { + state.stateIsDirty = true; + } + state.workflow.name = data.newName; }, // Nodes @@ -374,11 +409,15 @@ export const store = new Vuex.Store({ for (let i = 0; i < state.workflow.nodes.length; i++) { if (state.workflow.nodes[i].name === node.name) { state.workflow.nodes.splice(i, 1); + state.stateIsDirty = true; return; } } }, - removeAllNodes (state) { + removeAllNodes (state, data) { + if (data.setStateDirty === true) { + state.stateIsDirty = true; + } state.workflow.nodes.splice(0, state.workflow.nodes.length); }, updateNodeProperties (state, updateInformation: INodeUpdatePropertiesInformation) { @@ -388,6 +427,7 @@ export const store = new Vuex.Store({ }); if (node) { + state.stateIsDirty = true; for (const key of Object.keys(updateInformation.properties)) { Vue.set(node, key, updateInformation.properties[key]); } @@ -403,6 +443,7 @@ export const store = new Vuex.Store({ throw new Error(`Node with the name "${updateInformation.name}" could not be found to set parameter.`); } + state.stateIsDirty = true; Vue.set(node, updateInformation.key, updateInformation.value); }, setNodeParameters (state, updateInformation: IUpdateInformation) { @@ -415,6 +456,7 @@ export const store = new Vuex.Store({ throw new Error(`Node with the name "${updateInformation.name}" could not be found to set parameter.`); } + state.stateIsDirty = true; Vue.set(node, 'parameters', updateInformation.value); }, @@ -423,6 +465,7 @@ export const store = new Vuex.Store({ state.nodeIndex.push(nodeName); }, setNodeIndex (state, newData: { index: number, name: string | null}) { + state.stateIsDirty = true; state.nodeIndex[newData.index] = newData.name; }, resetNodeIndex (state) { @@ -433,8 +476,11 @@ export const store = new Vuex.Store({ setNodeViewMoveInProgress (state, value: boolean) { state.nodeViewMoveInProgress = value; }, - setNodeViewOffsetPosition (state, newOffset: XYPositon) { - state.nodeViewOffsetPosition = newOffset; + setNodeViewOffsetPosition (state, data) { + if (data.setStateDirty === true) { + state.stateIsDirty = true; + } + state.nodeViewOffsetPosition = data.newOffset; }, // Node-Types @@ -497,19 +543,22 @@ export const store = new Vuex.Store({ // TODO: Check if there is an error or whatever that is supposed to be returned return; } - + state.stateIsDirty = true; state.nodeTypes.push(typeData); }, setActiveNode (state, nodeName: string) { + state.stateIsDirty = true; state.activeNode = nodeName; }, setLastSelectedNode (state, nodeName: string) { + state.stateIsDirty = true; state.lastSelectedNode = nodeName; }, setLastSelectedNodeOutputIndex (state, outputIndex: number | null) { + state.stateIsDirty = true; state.lastSelectedNodeOutputIndex = outputIndex; }, @@ -523,7 +572,7 @@ export const store = new Vuex.Store({ if (state.workflowExecutionData.data.resultData.runData[pushData.nodeName] === undefined) { Vue.set(state.workflowExecutionData.data.resultData.runData, pushData.nodeName, []); } - + state.stateIsDirty = true; state.workflowExecutionData.data.resultData.runData[pushData.nodeName].push(pushData.data); }, @@ -588,6 +637,10 @@ export const store = new Vuex.Store({ return `${state.urlBaseWebhook}${state.endpointWebhookTest}`; }, + getStateIsDirty: (state) : boolean => { + return state.stateIsDirty; + }, + saveDataErrorExecution: (state): string => { return state.saveDataErrorExecution; }, diff --git a/packages/editor-ui/src/views/NodeView.vue b/packages/editor-ui/src/views/NodeView.vue index 4b12a7d7bc..914f42e670 100644 --- a/packages/editor-ui/src/views/NodeView.vue +++ b/packages/editor-ui/src/views/NodeView.vue @@ -127,7 +127,7 @@ import NodeSettings from '@/components/NodeSettings.vue'; import RunData from '@/components/RunData.vue'; import mixins from 'vue-typed-mixins'; - +import { uuid } from 'uuidv4'; import { debounce, isEqual } from 'lodash'; import axios from 'axios'; import { @@ -196,13 +196,8 @@ export default mixins( if (this.$route && this.$route.params.name) { workflowId = this.$route.params.name; } - if(workflowId !== null) { - this.isDirty = await this.dataHasChanged(workflowId); - } else { - this.isDirty = true; - } }, - deep: true + deep: true, }, connections: { async handler (val, oldVal) { @@ -211,13 +206,8 @@ export default mixins( if (this.$route && this.$route.params.name) { workflowId = this.$route.params.name; } - if(workflowId !== null) { - this.isDirty = await this.dataHasChanged(workflowId); - } else { - this.isDirty = true; - } }, - deep: true + deep: true, }, }, computed: { @@ -292,7 +282,6 @@ export default mixins( ctrlKeyPressed: false, debouncedFunctions: [] as any[], // tslint:disable-line:no-any stopExecutionInProgress: false, - isDirty: false, }; }, beforeDestroy () { @@ -336,7 +325,7 @@ export default mixins( throw new Error(`Execution with id "${executionId}" could not be found!`); } - this.$store.commit('setWorkflowName', data.workflowData.name); + this.$store.commit('setWorkflowName', {newName: data.workflowData.name, setStateDirty: false}); this.$store.commit('setWorkflowId', PLACEHOLDER_EMPTY_WORKFLOW_ID); this.$store.commit('setWorkflowExecutionData', data); @@ -360,7 +349,7 @@ export default mixins( this.$store.commit('setActive', data.active || false); this.$store.commit('setWorkflowId', workflowId); - this.$store.commit('setWorkflowName', data.name); + this.$store.commit('setWorkflowName', {newName: data.name, setStateDirty: false}); this.$store.commit('setWorkflowSettings', data.settings || {}); await this.addNodes(data.nodes, data.connections); @@ -467,7 +456,7 @@ export default mixins( e.stopPropagation(); e.preventDefault(); - this.isDirty = false; + this.$store.commit('setStateDirty', false); this.callDebounced('saveCurrentWorkflow', 1000); } else if (e.key === 'Enter') { @@ -985,7 +974,7 @@ export default mixins( newNodeData.name = this.getUniqueNodeName(newNodeData.name); if (nodeTypeData.webhooks && nodeTypeData.webhooks.length) { - newNodeData.webhookId = uuidv4(); + newNodeData.webhookId = uuid(); } await this.addNodes([newNodeData]); @@ -1345,7 +1334,7 @@ export default mixins( if (this.$route.params.action === 'workflowSave') { // In case the workflow got saved we do not have to run init // as only the route changed but all the needed data is already loaded - this.isDirty = false; + this.$store.commit('setStateDirty', false); return Promise.resolve(); } @@ -1375,7 +1364,7 @@ export default mixins( document.addEventListener('keyup', this.keyUp); window.addEventListener("beforeunload", (e) => { - if(this.isDirty === true) { + if(this.$store.getters.getStateIsDirty === true) { const confirmationMessage = 'It looks like you have been editing something. ' + 'If you leave before saving, your changes will be lost.'; (e || window.event).returnValue = confirmationMessage; //Gecko + IE @@ -1399,6 +1388,8 @@ export default mixins( detachable: !this.isReadOnly, }); } else { + // @ts-ignore + 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.$store.commit('addConnection', { connection }); @@ -1586,7 +1577,7 @@ export default mixins( this.instance.deleteEveryEndpoint(); } this.$store.commit('removeAllConnections'); - this.$store.commit('removeAllNodes'); + this.$store.commit('removeAllNodes', {setStateDirty: true}); // Wait a tick that the old nodes had time to get removed await Vue.nextTick(); @@ -1876,8 +1867,8 @@ export default mixins( }); } - this.$store.commit('removeAllConnections'); - this.$store.commit('removeAllNodes'); + this.$store.commit('removeAllConnections', {setStateDirty: false}); + this.$store.commit('removeAllNodes', {setStateDirty: false}); // Reset workflow execution data this.$store.commit('setWorkflowExecutionData', null); @@ -1886,7 +1877,7 @@ export default mixins( this.$store.commit('setActive', false); this.$store.commit('setWorkflowId', PLACEHOLDER_EMPTY_WORKFLOW_ID); - this.$store.commit('setWorkflowName', ''); + this.$store.commit('setWorkflowName', {newName: '', setStateDirty: false}); this.$store.commit('setWorkflowSettings', {}); this.$store.commit('setActiveExecutionId', null); @@ -1897,7 +1888,7 @@ export default mixins( this.$store.commit('resetNodeIndex'); this.$store.commit('resetSelectedNodes'); - this.$store.commit('setNodeViewOffsetPosition', [0, 0]); + this.$store.commit('setNodeViewOffsetPosition', {newOffset: [0, 0], setStateDirty: false}); return Promise.resolve(); },