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 977ecb187..a95042cc0 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 @@ -421,6 +421,9 @@ RED.history = (function() { ev.node[i] = ev.changes[i]; } } + ev.node.dirty = true; + ev.node.changed = ev.changed; + var eventType; switch(ev.node.type) { case 'tab': eventType = "flows"; break; @@ -511,8 +514,6 @@ RED.history = (function() { inverseEv.links.push(ev.createdLinks[i]); } } - ev.node.dirty = true; - ev.node.changed = ev.changed; } else if (ev.t == "createSubflow") { inverseEv = { t: "deleteSubflow", diff --git a/packages/node_modules/@node-red/editor-client/src/js/nodes.js b/packages/node_modules/@node-red/editor-client/src/js/nodes.js index 5f054782e..34fb8512c 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/nodes.js +++ b/packages/node_modules/@node-red/editor-client/src/js/nodes.js @@ -46,6 +46,9 @@ RED.nodes = (function() { function setDirty(d) { dirty = d; + if (!d) { + allNodes.clearState() + } RED.events.emit("workspace:dirty",{dirty:dirty}); } @@ -238,22 +241,36 @@ RED.nodes = (function() { // allNodes holds information about the Flow nodes. var allNodes = (function() { + // Map node.id -> node var nodes = {}; + // Map tab.id -> Array of nodes on that tab var tabMap = {}; + // Map tab.id -> Set of dirty object ids on that tab + var tabDirtyMap = {}; + // Map tab.id -> Set of object ids of things deleted from the tab that weren't otherwise dirty + var tabDeletedNodesMap = {}; + // Set of object ids of things added to a tab after initial import + var addedDirtyObjects = new Set() + var api = { addTab: function(id) { tabMap[id] = []; + tabDirtyMap[id] = new Set(); + tabDeletedNodesMap[id] = new Set(); }, hasTab: function(z) { return tabMap.hasOwnProperty(z) }, removeTab: function(id) { delete tabMap[id]; + delete tabDirtyMap[id]; + delete tabDeletedNodesMap[id]; }, addNode: function(n) { nodes[n.id] = n; if (tabMap.hasOwnProperty(n.z)) { tabMap[n.z].push(n); + api.addObjectToWorkspace(n.z, n.id, n.changed || n.moved) } else { console.warn("Node added to unknown tab/subflow:",n); tabMap["_"] = tabMap["_"] || []; @@ -267,8 +284,37 @@ RED.nodes = (function() { if (i > -1) { tabMap[n.z].splice(i,1); } + api.removeObjectFromWorkspace(n.z, n.id) } }, + /** + * Add an object to our dirty/clean tracking state + * @param {String} z + * @param {String} id + * @param {Boolean} isDirty + */ + addObjectToWorkspace: function (z, id, isDirty) { + if (isDirty) { + addedDirtyObjects.add(id) + } + if (tabDeletedNodesMap[z].has(id)) { + tabDeletedNodesMap[z].delete(id) + } + api.markNodeDirty(z, id, isDirty) + }, + /** + * Remove an object from our dirty/clean tracking state + * @param {String} z + * @param {String} id + */ + removeObjectFromWorkspace: function (z, id) { + if (!addedDirtyObjects.has(id)) { + tabDeletedNodesMap[z].add(id) + } else { + addedDirtyObjects.delete(id) + } + api.markNodeDirty(z, id, false) + }, hasNode: function(id) { return nodes.hasOwnProperty(id); }, @@ -433,6 +479,33 @@ RED.nodes = (function() { clear: function() { nodes = {}; tabMap = {}; + tabDirtyMap = {}; + tabDeletedNodesMap = {}; + addedDirtyObjects = new Set(); + }, + /** + * Clear all internal state on what is dirty. + */ + clearState: function () { + // Called when a deploy happens, we can forget about added/remove + // items as they have now been deployed. + addedDirtyObjects = new Set() + const flowsToCheck = new Set() + for (const [z, set] of Object.entries(tabDeletedNodesMap)) { + if (set.size > 0) { + set.clear() + flowsToCheck.add(z) + } + } + for (const [z, set] of Object.entries(tabDirtyMap)) { + if (set.size > 0) { + set.clear() + flowsToCheck.add(z) + } + } + for (const z of flowsToCheck) { + api.checkTabState(z) + } }, eachNode: function(cb) { var nodeList,i,j; @@ -510,6 +583,36 @@ RED.nodes = (function() { B._reordered = true; return orderMap[A.id] - orderMap[B.id]; }) + }, + /** + * Update our records if an object is dirty or not + * @param {String} z tab id + * @param {String} id object id + * @param {Boolean} dirty whether the object is dirty or not + */ + markNodeDirty: function(z, id, dirty) { + if (tabDirtyMap[z]) { + if (dirty) { + tabDirtyMap[z].add(id) + } else { + tabDirtyMap[z].delete(id) + } + api.checkTabState(z) + } + }, + /** + * Check if a tab should update its contentsChange flag + * @param {String} z tab id + */ + checkTabState: function (z) { + const ws = workspaces[z] + if (ws) { + const contentsChanged = tabDirtyMap[z].size > 0 || tabDeletedNodesMap[z].size > 0 + if (Boolean(ws.contentsChanged) !== contentsChanged) { + ws.contentsChanged = contentsChanged + RED.events.emit("flows:change", ws); + } + } } } return api; @@ -597,6 +700,11 @@ RED.nodes = (function() { throw new Error(`Cannot modified property '${prop}' of locked object '${node.type}:${node.id}'`) } } + if (node.z && (prop === 'changed' || prop === 'moved')) { + setTimeout(() => { + allNodes.markNodeDirty(node.z, node.id, node.changed || node.moved) + }, 0) + } node[prop] = value; return true } @@ -666,10 +774,16 @@ RED.nodes = (function() { } if (l.source.z === l.target.z && linkTabMap[l.source.z]) { linkTabMap[l.source.z].push(l); + allNodes.addObjectToWorkspace(l.source.z, getLinkId(l), true) } RED.events.emit("links:add",l); } + function getLinkId(link) { + return link.source.id + ':' + link.sourcePort + ':' + link.target.id + } + + function getNode(id) { if (id in configNodes) { return configNodes[id]; @@ -864,6 +978,7 @@ RED.nodes = (function() { if (index !== -1) { linkTabMap[l.source.z].splice(index,1) } + allNodes.removeObjectFromWorkspace(l.source.z, getLinkId(l)) } } RED.events.emit("links:remove",l); @@ -1688,6 +1803,7 @@ RED.nodes = (function() { * Options: * - generateIds - whether to replace all node ids * - addFlow - whether to import nodes to a new tab + * - markChanged - whether to set changed=true on all newly imported objects * - reimport - if node has a .z property, dont overwrite it * Only applicible when `generateIds` is false * - importMap - how to resolve any conflicts. @@ -1696,7 +1812,7 @@ RED.nodes = (function() { * - id:replace - import over the top of existing */ function importNodes(newNodesObj,options) { // createNewIds,createMissingWorkspace) { - const defOpts = { generateIds: false, addFlow: false, reimport: false, importMap: {} } + const defOpts = { generateIds: false, addFlow: false, markChanged: false, reimport: false, importMap: {} } options = Object.assign({}, defOpts, options) options.importMap = options.importMap || {} const createNewIds = options.generateIds; @@ -1722,7 +1838,7 @@ RED.nodes = (function() { newNodes = newNodesObj; } - if (!$.isArray(newNodes)) { + if (!Array.isArray(newNodes)) { newNodes = [newNodes]; } @@ -2020,6 +2136,9 @@ RED.nodes = (function() { if (!n.z) { delete configNode.z; } + if (options.markChanged) { + configNode.changed = true + } if (n.hasOwnProperty('d')) { configNode.d = n.d; } @@ -2082,6 +2201,9 @@ RED.nodes = (function() { if (n.hasOwnProperty('g')) { node.g = n.g; } + if (options.markChanged) { + node.changed = true + } if (createNewIds || options.importMap[n.id] === "copy") { if (subflow_denylist[n.z]) { continue; @@ -2595,6 +2717,7 @@ RED.nodes = (function() { groupsByZ[group.z] = groupsByZ[group.z] || []; groupsByZ[group.z].push(group); groups[group.id] = group; + allNodes.addObjectToWorkspace(group.z, group.id, group.changed || group.moved) RED.events.emit("groups:add",group); return group } @@ -2611,7 +2734,7 @@ RED.nodes = (function() { } } RED.group.markDirty(group); - + allNodes.removeObjectFromWorkspace(group.z, group.id) delete groups[group.id]; RED.events.emit("groups:remove",group); } @@ -2626,6 +2749,7 @@ RED.nodes = (function() { if (!nodeLinks[junction.id]) { nodeLinks[junction.id] = {in:[],out:[]}; } + allNodes.addObjectToWorkspace(junction.z, junction.id, junction.changed || junction.moved) RED.events.emit("junctions:add", junction) return junction } @@ -2637,6 +2761,7 @@ RED.nodes = (function() { } delete junctions[junction.id] delete nodeLinks[junction.id]; + allNodes.removeObjectFromWorkspace(junction.z, junction.id) RED.events.emit("junctions:remove", junction) var removedLinks = links.filter(function(l) { return (l.source === junction) || (l.target === junction); }); @@ -2874,6 +2999,9 @@ RED.nodes = (function() { RED.view.redraw(true); } }); + RED.events.on('deploy', function () { + allNodes.clearState() + }) }, registry:registry, setNodeList: registry.setNodeList, @@ -2976,6 +3104,20 @@ RED.nodes = (function() { } } }, + eachGroup: function(cb) { + for (var group of Object.values(groups)) { + if (cb(group) === false) { + break + } + } + }, + eachJunction: function(cb) { + for (var junction of Object.values(junctions)) { + if (cb(junction) === false) { + break + } + } + }, node: getNode, diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/common/tabs.js b/packages/node_modules/@node-red/editor-client/src/js/ui/common/tabs.js index fd8c5c62d..abb76e622 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/common/tabs.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/common/tabs.js @@ -845,7 +845,6 @@ RED.tabs = (function() { var badges = $('').appendTo(li); if (options.onselect) { - $('').appendTo(badges); $('').appendTo(badges); } diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/contextMenu.js b/packages/node_modules/@node-red/editor-client/src/js/ui/contextMenu.js index 30fc7fd3b..d122b04d1 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/contextMenu.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/contextMenu.js @@ -79,7 +79,8 @@ RED.contextMenu = (function () { w: 0, h: 0, outputs: 1, inputs: 1, - dirty: true + dirty: true, + moved: true } const historyEvent = { dirty: RED.nodes.dirty(), 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 32b604235..a09fdeb01 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 @@ -557,12 +557,17 @@ RED.deploy = (function() { } else { RED.notify('
' + RED._("deploy.successfulDeploy") + '
', "success"); } - RED.nodes.eachNode(function (node) { - const flow = node.z && (RED.nodes.workspace(node.z) || RED.nodes.subflow(node.z) || null); + 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) } + } + RED.nodes.eachNode(function (node) { + ensureUnlocked(node.z) if (node.changed) { node.dirty = true; node.changed = false; @@ -574,11 +579,33 @@ RED.deploy = (function() { if (node.credentials) { delete node.credentials; } - if (flow && isLocked) { - flow.locked = isLocked; - } }); + RED.nodes.eachGroup(function (node) { + ensureUnlocked(node.z) + if (node.changed) { + node.dirty = true; + node.changed = false; + } + if (node.moved) { + node.dirty = true; + node.moved = false; + } + }) + RED.nodes.eachJunction(function (node) { + ensureUnlocked(node.z) + if (node.changed) { + node.dirty = true; + node.changed = false; + } + if (node.moved) { + node.dirty = true; + node.moved = false; + } + }) RED.nodes.eachConfig(function (confNode) { + if (confNode.z) { + ensureUnlocked(confNode.z) + } confNode.changed = false; if (confNode.credentials) { delete confNode.credentials; @@ -588,8 +615,16 @@ RED.deploy = (function() { subflow.changed = false; }); RED.nodes.eachWorkspace(function (ws) { - ws.changed = false; + if (ws.changed || ws.added) { + ensureUnlocked(ws.z) + ws.changed = false; + delete ws.added + RED.events.emit("flows:change", ws) + } }); + flowsToLock.forEach(flow => { + flow.locked = true + }) // Once deployed, cannot undo back to a clean state RED.history.markAllDirty(); RED.view.redraw(); diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/group.js b/packages/node_modules/@node-red/editor-client/src/js/ui/group.js index bee533f98..f17852c58 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/group.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/group.js @@ -488,7 +488,8 @@ RED.group = (function() { y: Number.POSITIVE_INFINITY, w: 0, h: 0, - _def: RED.group.def + _def: RED.group.def, + changed: true } group.z = nodes[0].z; diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/tab-config.js b/packages/node_modules/@node-red/editor-client/src/js/ui/tab-config.js index e69674b93..90ecf6093 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/tab-config.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/tab-config.js @@ -145,17 +145,19 @@ RED.sidebar.config = (function() { } else { var currentType = ""; nodes.forEach(function(node) { - var label = RED.utils.getNodeLabel(node,node.id); + var labelText = RED.utils.getNodeLabel(node,node.id); if (node.type != currentType) { $('