From 18d0fa22590b7ef11ab06a56a00a2c59080eadbc Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Mon, 13 May 2024 14:19:24 +0100 Subject: [PATCH] Improve background conflict handling --- .../@node-red/editor-client/src/js/history.js | 39 +++++++- .../editor-client/src/js/ui/deploy.js | 91 ++++++++++--------- .../@node-red/editor-client/src/js/ui/diff.js | 73 ++++++++++----- .../editor-client/src/js/ui/notifications.js | 15 ++- 4 files changed, 149 insertions(+), 69 deletions(-) diff --git a/packages/node_modules/@node-red/editor-client/src/js/history.js b/packages/node_modules/@node-red/editor-client/src/js/history.js index c3a966890..66411264b 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/history.js +++ b/packages/node_modules/@node-red/editor-client/src/js/history.js @@ -59,18 +59,53 @@ RED.history = (function() { t: 'replace', config: RED.nodes.createCompleteNodeSet(), changed: {}, - rev: RED.nodes.version() + moved: {}, + complete: true, + rev: RED.nodes.version(), + dirty: RED.nodes.dirty() }; + var selectedTab = RED.workspaces.active(); + inverseEv.config.forEach(n => { + const node = RED.nodes.node(n.id) + if (node) { + inverseEv.changed[n.id] = node.changed + inverseEv.moved[n.id] = node.moved + } + }) RED.nodes.clear(); var imported = RED.nodes.import(ev.config); + // Clear all change flags from the import + RED.nodes.dirty(false); + + const flowsToLock = new Set() + function ensureUnlocked(id) { + const flow = id && (RED.nodes.workspace(id) || RED.nodes.subflow(id) || null); + const isLocked = flow ? flow.locked : false; + if (flow && isLocked) { + flow.locked = false; + flowsToLock.add(flow) + } + } imported.nodes.forEach(function(n) { if (ev.changed[n.id]) { + ensureUnlocked(n.z) n.changed = true; - inverseEv.changed[n.id] = true; } + if (ev.moved[n.id]) { + ensureUnlocked(n.z) + n.moved = true; + } + }) + flowsToLock.forEach(flow => { + flow.locked = true }) RED.nodes.version(ev.rev); + RED.view.redraw(true); + RED.palette.refresh(); + RED.workspaces.refresh(); + RED.workspaces.show(selectedTab, true); + RED.sidebar.config.refresh(); } else { var importMap = {}; ev.config.forEach(function(n) { diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/deploy.js b/packages/node_modules/@node-red/editor-client/src/js/ui/deploy.js index 83c04d775..b766c43eb 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/deploy.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/deploy.js @@ -34,6 +34,8 @@ RED.deploy = (function() { var currentDiff = null; + var activeBackgroundDeployNotification; + function changeDeploymentType(type) { deploymentType = type; $("#red-ui-header-button-deploy-icon").attr("src",deploymentTypes[type].img); @@ -133,37 +135,38 @@ RED.deploy = (function() { } }); - var activeNotifyMessage; RED.comms.subscribe("notification/runtime-deploy",function(topic,msg) { - if (!activeNotifyMessage) { - var currentRev = RED.nodes.version(); - if (currentRev === null || deployInflight || currentRev === msg.revision) { - return; - } - var message = $('

').text(RED._('deploy.confirm.backgroundUpdate')); - activeNotifyMessage = RED.notify(message,{ - modal: true, - fixed: true, - buttons: [ - { - text: RED._('deploy.confirm.button.ignore'), - click: function() { - activeNotifyMessage.close(); - activeNotifyMessage = null; - } - }, - { - text: RED._('deploy.confirm.button.review'), - class: "primary", - click: function() { - activeNotifyMessage.close(); - var nns = RED.nodes.createCompleteNodeSet(); - resolveConflict(nns,false); - activeNotifyMessage = null; - } + var currentRev = RED.nodes.version(); + if (currentRev === null || deployInflight || currentRev === msg.revision) { + return; + } + if (activeBackgroundDeployNotification?.hidden) { + activeBackgroundDeployNotification.showNotification() + return + } + const message = $('

').text(RED._('deploy.confirm.backgroundUpdate')); + const options = { + id: 'background-update', + type: 'compact', + modal: false, + fixed: true, + timeout: 10000, + buttons: [ + { + text: RED._('deploy.confirm.button.review'), + class: "primary", + click: function() { + activeBackgroundDeployNotification.hideNotification(); + var nns = RED.nodes.createCompleteNodeSet(); + resolveConflict(nns,false); } - ] - }); + } + ] + } + if (!activeBackgroundDeployNotification || activeBackgroundDeployNotification.closed) { + activeBackgroundDeployNotification = RED.notify(message, options) + } else { + activeBackgroundDeployNotification.update(message, options) } }); } @@ -220,7 +223,11 @@ RED.deploy = (function() { class: "primary disabled", click: function() { if (!$("#red-ui-deploy-dialog-confirm-deploy-review").hasClass('disabled')) { - RED.diff.showRemoteDiff(); + RED.diff.showRemoteDiff(null, { + onmerge: function () { + activeBackgroundDeployNotification.close() + } + }); conflictNotification.close(); } } @@ -233,6 +240,7 @@ RED.deploy = (function() { if (!$("#red-ui-deploy-dialog-confirm-deploy-merge").hasClass('disabled')) { RED.diff.mergeDiff(currentDiff); conflictNotification.close(); + activeBackgroundDeployNotification.close() } } } @@ -245,6 +253,7 @@ RED.deploy = (function() { click: function() { save(true,activeDeploy); conflictNotification.close(); + activeBackgroundDeployNotification.close() } }) } @@ -255,21 +264,17 @@ RED.deploy = (function() { buttons: buttons }); - var now = Date.now(); RED.diff.getRemoteDiff(function(diff) { - var ellapsed = Math.max(1000 - (Date.now()-now), 0); currentDiff = diff; - setTimeout(function() { - conflictCheck.hide(); - var d = Object.keys(diff.conflicts); - if (d.length === 0) { - conflictAutoMerge.show(); - $("#red-ui-deploy-dialog-confirm-deploy-merge").removeClass('disabled') - } else { - conflictManualMerge.show(); - } - $("#red-ui-deploy-dialog-confirm-deploy-review").removeClass('disabled') - },ellapsed); + conflictCheck.hide(); + var d = Object.keys(diff.conflicts); + if (d.length === 0) { + conflictAutoMerge.show(); + $("#red-ui-deploy-dialog-confirm-deploy-merge").removeClass('disabled') + } else { + conflictManualMerge.show(); + } + $("#red-ui-deploy-dialog-confirm-deploy-review").removeClass('disabled') }) } function cropList(list) { diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/diff.js b/packages/node_modules/@node-red/editor-client/src/js/ui/diff.js index 3f73e29aa..12c19cf71 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/diff.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/diff.js @@ -1099,11 +1099,11 @@ RED.diff = (function() { // var diff = generateDiff(originalFlow,nns); // showDiff(diff); // } - function showRemoteDiff(diff) { - if (diff === undefined) { - getRemoteDiff(showRemoteDiff); + function showRemoteDiff(diff, options = {}) { + if (!diff) { + getRemoteDiff((remoteDiff) => showRemoteDiff(remoteDiff, options)); } else { - showDiff(diff,{mode:'merge'}); + showDiff(diff,{...options, mode:'merge'}); } } function parseNodes(nodeList) { @@ -1240,7 +1240,7 @@ RED.diff = (function() { return diff; } - function showDiff(diff,options) { + function showDiff(diff, options) { if (diffVisible) { return; } @@ -1315,6 +1315,9 @@ RED.diff = (function() { if (!$("#red-ui-diff-view-diff-merge").hasClass('disabled')) { refreshConflictHeader(diff); mergeDiff(diff); + if (options.onmerge) { + options.onmerge() + } RED.tray.close(); } } @@ -1345,6 +1348,7 @@ RED.diff = (function() { var newConfig = []; var node; var nodeChangedStates = {}; + var nodeMovedStates = {}; var localChangedStates = {}; for (id in localDiff.newConfig.all) { if (localDiff.newConfig.all.hasOwnProperty(id)) { @@ -1352,12 +1356,14 @@ RED.diff = (function() { if (resolutions[id] === 'local') { if (node) { nodeChangedStates[id] = node.changed; + nodeMovedStates[id] = node.moved; } newConfig.push(localDiff.newConfig.all[id]); } else if (resolutions[id] === 'remote') { if (!remoteDiff.deleted[id] && remoteDiff.newConfig.all.hasOwnProperty(id)) { if (node) { nodeChangedStates[id] = node.changed; + nodeMovedStates[id] = node.moved; } localChangedStates[id] = 1; newConfig.push(remoteDiff.newConfig.all[id]); @@ -1381,8 +1387,9 @@ RED.diff = (function() { } return { config: newConfig, - nodeChangedStates: nodeChangedStates, - localChangedStates: localChangedStates + nodeChangedStates, + nodeMovedStates, + localChangedStates } } @@ -1393,6 +1400,7 @@ RED.diff = (function() { var newConfig = appliedDiff.config; var nodeChangedStates = appliedDiff.nodeChangedStates; + var nodeMovedStates = appliedDiff.nodeMovedStates; var localChangedStates = appliedDiff.localChangedStates; var isDirty = RED.nodes.dirty(); @@ -1401,33 +1409,56 @@ RED.diff = (function() { t:"replace", config: RED.nodes.createCompleteNodeSet(), changed: nodeChangedStates, + moved: nodeMovedStates, + complete: true, dirty: isDirty, rev: RED.nodes.version() } RED.history.push(historyEvent); - var originalFlow = RED.nodes.originalFlow(); - // originalFlow is what the editor things it loaded - // - add any newly added nodes from remote diff as they are now part of the record - for (var id in diff.remoteDiff.added) { - if (diff.remoteDiff.added.hasOwnProperty(id)) { - if (diff.remoteDiff.newConfig.all.hasOwnProperty(id)) { - originalFlow.push(JSON.parse(JSON.stringify(diff.remoteDiff.newConfig.all[id]))); - } - } - } + // var originalFlow = RED.nodes.originalFlow(); + // // originalFlow is what the editor thinks it loaded + // // - add any newly added nodes from remote diff as they are now part of the record + // for (var id in diff.remoteDiff.added) { + // if (diff.remoteDiff.added.hasOwnProperty(id)) { + // if (diff.remoteDiff.newConfig.all.hasOwnProperty(id)) { + // originalFlow.push(JSON.parse(JSON.stringify(diff.remoteDiff.newConfig.all[id]))); + // } + // } + // } RED.nodes.clear(); var imported = RED.nodes.import(newConfig); - // Restore the original flow so subsequent merge resolutions can properly - // identify new-vs-old - RED.nodes.originalFlow(originalFlow); + // // Restore the original flow so subsequent merge resolutions can properly + // // identify new-vs-old + // RED.nodes.originalFlow(originalFlow); + + // Clear all change flags from the import + RED.nodes.dirty(false); + + const flowsToLock = new Set() + function ensureUnlocked(id) { + const flow = id && (RED.nodes.workspace(id) || RED.nodes.subflow(id) || null); + const isLocked = flow ? flow.locked : false; + if (flow && isLocked) { + flow.locked = false; + flowsToLock.add(flow) + } + } imported.nodes.forEach(function(n) { - if (nodeChangedStates[n.id] || localChangedStates[n.id]) { + if (nodeChangedStates[n.id]) { + ensureUnlocked(n.z) n.changed = true; } + if (nodeMovedStates[n.id]) { + ensureUnlocked(n.z) + n.moved = true; + } + }) + flowsToLock.forEach(flow => { + flow.locked = true }) RED.nodes.version(diff.remoteDiff.rev); diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/notifications.js b/packages/node_modules/@node-red/editor-client/src/js/ui/notifications.js index 30dcc4bd5..ef75ff8c4 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/notifications.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/notifications.js @@ -221,12 +221,12 @@ RED.notifications = (function() { if (newType) { n.className = "red-ui-notification red-ui-notification-"+newType; } - + newTimeout = newOptions.hasOwnProperty('timeout')?newOptions.timeout:timeout if (!fixed || newOptions.fixed === false) { - newTimeout = (newOptions.hasOwnProperty('timeout')?newOptions.timeout:timeout)||5000; + newTimeout === newTimeout || 5000 } if (newOptions.buttons) { - var buttonSet = $('

').appendTo(nn) + var buttonSet = $('
').appendTo(nn) newOptions.buttons.forEach(function(buttonDef) { var b = $('