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 2fa4e4427..df29b354e 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 @@ -18,17 +18,18 @@ * An API for undo / redo history buffer * @namespace RED.history */ -RED.history = (function() { - var undoHistory = []; - var redoHistory = []; +RED.history = (function () { + let undoHistory = []; + let redoHistory = []; function nodeOrJunction(id) { - var node = RED.nodes.node(id); + const node = RED.nodes.node(id); if (node) { return node; } return RED.nodes.junction(id); } + function ensureUnlocked(id, flowsToLock) { const flow = id && (RED.nodes.workspace(id) || RED.nodes.subflow(id) || null); const isLocked = flow ? flow.locked : false; @@ -37,693 +38,980 @@ RED.history = (function() { flowsToLock.add(flow) } } - function undoEvent(ev) { - var i; - var len; - var node; - var group; - var subflow; - var modifiedTabs = {}; - var inverseEv; - if (ev) { - if (ev.t == 'multi') { - inverseEv = { - t: 'multi', - events: [] - }; - len = ev.events.length; - for (i=len-1;i>=0;i--) { - var r = undoEvent(ev.events[i]); - inverseEv.events.push(r); - } - } else if (ev.t == 'replace') { - if (ev.complete) { - // This is a replace of everything. We can short-cut - // the logic by clearing everyting first, then importing - // the ev.config. - // Used by RED.diff.mergeDiff - inverseEv = { - t: 'replace', - config: RED.nodes.createCompleteNodeSet(), - changed: {}, - 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() - - imported.nodes.forEach(function(n) { - if (ev.changed[n.id]) { - ensureUnlocked(n.z, flowsToLock) - n.changed = true; - } - if (ev.moved[n.id]) { - ensureUnlocked(n.z, flowsToLock) - n.moved = true; - } - }) - flowsToLock.forEach(flow => { - flow.locked = true - }) + /** + * @typedef {"add"|"delete"|"edit"|"move"|"multi"|"remove"|"reorder"|"replace"| + * "createSubflow"|"deleteSubflow"|"addToGroup"|"createGroup"|"ungroup"|"removeFromGroup"} HistoryType + * @typedef {{ t: HistoryType; dirty: boolean; callback?: (ev: HistoryEvent) => void; } & {}} HistoryEvent + * + * @typedef {{ source: Node; sourcePort: number; target: Node; }} Link + * @typedef {Node & {}} Group + * @typedef {Node & {}} Junction + * @typedef {Node & { instances: Array; }} Subflow + * @typedef {Node & {}} Workspace + */ - 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) { - importMap[n.id] = "replace"; - }) - var importedResult = RED.nodes.import(ev.config,{importMap: importMap}) - inverseEv = { - t: 'replace', - config: importedResult.removedNodes, - dirty: RED.nodes.dirty() - } - } - } else if (ev.t == 'add') { - inverseEv = { - t: "delete", - dirty: RED.nodes.dirty() - }; - if (ev.nodes) { - inverseEv.nodes = []; - for (i=0;i} [ev.groups] + * @param {Array} [ev.junctions] + * @param {Array} [ev.links] + * @param {Array} [ev.nodes] + * @param {Array} [ev.removedLinks] + * @param {Subflow} [ev.subflow] + * @param {Array} [ev.subflows] + * @param {Array} [ev.workspaces] + * @param {object} [modifiedTabs] + * @returns {HistoryEvent} The generated history event to redo + */ + function onAdd(ev, modifiedTabs) { + const inverseEv = { + t: "delete", + dirty: RED.nodes.dirty() + }; + if (ev.nodes) { + inverseEv.nodes = []; + for (let i = 0; i < ev.nodes.length; i++) { + const node = RED.nodes.node(ev.nodes[i]); + if (node.z) { + modifiedTabs[node.z] = true; + } + inverseEv.nodes.push(node); + RED.nodes.remove(ev.nodes[i]); + if (node.g) { + const group = RED.nodes.group(node.g); + const index = group.nodes.indexOf(node); + if (index !== -1) { + group.nodes.splice(index, 1); + RED.group.markDirty(group); + } + } + } + } - } - } - if (ev.groups) { - inverseEv.groups = []; - for (i = ev.groups.length - 1;i>=0;i--) { - group = ev.groups[i]; - modifiedTabs[group.z] = true; - // The order of groups is important - // - to invert the action, the order is reversed - inverseEv.groups.unshift(group); - RED.nodes.removeGroup(group); - } - } - if (ev.workspaces) { - inverseEv.workspaces = []; - for (i=0;i= 0; i--) { + const group = ev.groups[i]; + modifiedTabs[group.z] = true; + // The order of groups is important + // - to invert the action, the order is reversed + inverseEv.groups.unshift(group); + RED.nodes.removeGroup(group); + } + } + + if (ev.workspaces) { + inverseEv.workspaces = []; + for (let i = 0; i < ev.workspaces.length; i++) { + const workspaceOrder = RED.nodes.getWorkspaceOrder(); + ev.workspaces[i]._index = workspaceOrder.indexOf(ev.workspaces[i].id); + inverseEv.workspaces.push(ev.workspaces[i]); + RED.nodes.removeWorkspace(ev.workspaces[i].id); + RED.workspaces.remove(ev.workspaces[i]); + } + } + + if (ev.subflows) { + inverseEv.subflows = []; + for (let i = 0; i < ev.subflows.length; i++) { + inverseEv.subflows.push(ev.subflows[i]); + RED.nodes.removeSubflow(ev.subflows[i]); + RED.workspaces.remove(ev.subflows[i]); + } + } + + if (ev.subflow) { + inverseEv.subflow = {}; + if (ev.subflow.instances) { + inverseEv.subflow.instances = []; + ev.subflow.instances.forEach(function (n) { + inverseEv.subflow.instances.push(n); + const node = RED.nodes.node(n.id); + if (node) { + node.changed = n.changed; + node.dirty = true; } - } - if (ev.subflowInputs && ev.subflowInputs.length > 0) { - subflow = RED.nodes.subflow(ev.subflowInputs[0].z); - subflow.in.push(ev.subflowInputs[0]); - subflow.in[0].dirty = true; - } - if (ev.subflowOutputs && ev.subflowOutputs.length > 0) { - subflow = RED.nodes.subflow(ev.subflowOutputs[0].z); - ev.subflowOutputs.sort(function(a,b) { return a.i-b.i}); - for (i=0;i= output.i) { - l.sourcePort++; - } - } - }); - } - } - if (ev.subflow) { - inverseEv.subflow = {}; - if (ev.subflow.hasOwnProperty('instances')) { - inverseEv.subflow.instances = []; - ev.subflow.instances.forEach(function(n) { - inverseEv.subflow.instances.push(n); - var node = RED.nodes.node(n.id); - if (node) { - node.changed = n.changed; - node.dirty = true; - } - }); - } - if (ev.subflow.hasOwnProperty('status')) { - subflow = RED.nodes.subflow(ev.subflow.id); - subflow.status = ev.subflow.status; - } - } + }); + } + + if (ev.subflow.hasOwnProperty("changed")) { + const subflow = RED.nodes.subflow(ev.subflow.id); if (subflow) { - RED.nodes.filterNodes({type:"subflow:"+subflow.id}).forEach(function(n) { - n.inputs = subflow.in.length; - n.outputs = subflow.out.length; - n.resize = true; - n.dirty = true; + subflow.changed = ev.subflow.changed; + } + } + } + + if (ev.removedLinks) { + inverseEv.createdLinks = []; + for (let i = 0; i < ev.removedLinks.length; i++) { + inverseEv.createdLinks.push(ev.removedLinks[i]); + RED.nodes.addLink(ev.removedLinks[i]); + } + } + + return inverseEv; + } + + /** + * Called on the `delete` history event + * @param {object} ev The history event to undo + * @param {"delete"} ev.t The history event type + * @param {Record} [ev.changes] + * @param {Array} [ev.createdLinks] + * @param {boolean} [ev.dirty] + * @param {Array} [ev.groups] + * @param {Array} [ev.junctions] + * @param {Array} [ev.links] + * @param {Array} [ev.nodes] + * @param {Subflow} [ev.subflow] + * @param {Array} [ev.subflowInputs] + * @param {Array} [ev.subflowOutputs] + * @param {Array} [ev.subflows] + * @param {Array} [ev.workspaces] + * @param {object} [modifiedTabs] + * @returns {HistoryEvent} The generated history event to redo + */ + function onDelete(ev, modifiedTabs) { + // TODO: check why global + let subflow; + const inverseEv = { + t: "add", + dirty: RED.nodes.dirty() + }; + + if (ev.workspaces) { + inverseEv.workspaces = []; + for (let i = 0; i < ev.workspaces.length; i++) { + inverseEv.workspaces.push(ev.workspaces[i]); + RED.nodes.addWorkspace(ev.workspaces[i], ev.workspaces[i]._index); + RED.workspaces.add(ev.workspaces[i], undefined, ev.workspaces[i]._index); + delete ev.workspaces[i]._index; + } + } + + if (ev.subflows) { + inverseEv.subflows = []; + for (let i = 0; i < ev.subflows.length; i++) { + inverseEv.subflows.push(ev.subflows[i]); + RED.nodes.addSubflow(ev.subflows[i]); + } + } + + if (ev.subflowInputs && ev.subflowInputs.length > 0) { + subflow = RED.nodes.subflow(ev.subflowInputs[0].z); + subflow.in.push(ev.subflowInputs[0]); + subflow.in[0].dirty = true; + } + + if (ev.subflowOutputs && ev.subflowOutputs.length > 0) { + subflow = RED.nodes.subflow(ev.subflowOutputs[0].z); + ev.subflowOutputs.sort(function (a, b) { return a.i - b.i }); + for (let i = 0; i < ev.subflowOutputs.length; i++) { + const output = ev.subflowOutputs[i]; + subflow.out.splice(output.i, 0, output); + for (let j = output.i + 1; j < subflow.out.length; j++) { + subflow.out[j].i++; + subflow.out[j].dirty = true; + } + RED.nodes.eachLink(function (l) { + if (l.source.type == "subflow:" + subflow.id) { + if (l.sourcePort >= output.i) { + l.sourcePort++; + } + } + }); + } + } + + if (ev.subflow) { + inverseEv.subflow = {}; + if (ev.subflow.hasOwnProperty("instances")) { + inverseEv.subflow.instances = []; + ev.subflow.instances.forEach(function (n) { + inverseEv.subflow.instances.push(n); + const node = RED.nodes.node(n.id); + if (node) { + node.changed = n.changed; + node.dirty = true; + } + }); + } + + if (ev.subflow.hasOwnProperty("status")) { + const subflow = RED.nodes.subflow(ev.subflow.id); + subflow.status = ev.subflow.status; + } + } + + if (subflow) { + RED.nodes.filterNodes({ type: "subflow:" + subflow.id }).forEach(function (n) { + n.inputs = subflow.in.length; + n.outputs = subflow.out.length; + n.resize = true; + n.dirty = true; + }); + } + + if (ev.groups) { + inverseEv.groups = []; + const groupsToAdd = {}; + ev.groups.forEach(function (g) { groupsToAdd[g.id] = g; }); + for (let i = ev.groups.length - 1; i >= 0; i--) { + RED.nodes.addGroup(ev.groups[i]) + modifiedTabs[ev.groups[i].z] = true; + // The order of groups is important + // - to invert the action, the order is reversed + inverseEv.groups.unshift(ev.groups[i]); + if (ev.groups[i].g) { + let group; + if (!groupsToAdd[ev.groups[i].g]) { + group = RED.nodes.group(ev.groups[i].g); + } else { + group = groupsToAdd[ev.groups[i].g]; + } + if (group.nodes.indexOf(ev.groups[i]) === -1) { + group.nodes.push(ev.groups[i]); + } + RED.group.markDirty(ev.groups[i]) + } + } + } + + if (ev.nodes) { + inverseEv.nodes = []; + for (let i = 0; i < ev.nodes.length; i++) { + RED.nodes.add(ev.nodes[i]); + modifiedTabs[ev.nodes[i].z] = true; + inverseEv.nodes.push(ev.nodes[i].id); + if (ev.nodes[i].g) { + const group = RED.nodes.group(ev.nodes[i].g); + if (group.nodes.indexOf(ev.nodes[i]) === -1) { + group.nodes.push(ev.nodes[i]); + } + RED.group.markDirty(group) + } + } + } + if (ev.junctions) { + inverseEv.junctions = []; + for (let i = 0; i < ev.junctions.length; i++) { + inverseEv.junctions.push(ev.junctions[i]); + RED.nodes.addJunction(ev.junctions[i]); + if (ev.junctions[i].g) { + const group = RED.nodes.group(ev.junctions[i].g); + if (group.nodes.indexOf(ev.junctions[i]) === -1) { + group.nodes.push(ev.junctions[i]); + } + RED.group.markDirty(group); + } + } + } + + if (ev.links) { + inverseEv.links = []; + for (let i = 0; i < ev.links.length; i++) { + RED.nodes.addLink(ev.links[i]); + inverseEv.links.push(ev.links[i]); + } + } + + if (ev.createdLinks) { + inverseEv.removedLinks = []; + for (let i = 0; i < ev.createdLinks.length; i++) { + inverseEv.removedLinks.push(ev.createdLinks[i]); + RED.nodes.removeLink(ev.createdLinks[i]); + } + } + + if (ev.changes) { + for (const i in ev.changes) { + if (ev.changes.hasOwnProperty(i)) { + const node = RED.nodes.node(i); + if (node) { + for (const d in ev.changes[i]) { + if (ev.changes[i].hasOwnProperty(d)) { + node[d] = ev.changes[i][d]; + } + } + node.dirty = true; + } + RED.events.emit("nodes:change", node); + } + } + } + + if (subflow) { + RED.events.emit("subflows:change", subflow); + } + + return inverseEv; + } + + /** + * Called on the `move` history event + * @param {object} ev The history event to undo + * @param {"move"} ev.t The history event type + * @param {Group} [ev.addToGroup] + * @param {Array} [ev.links] + * @param {Array<{ n: Group; ox: number; oy: number; dx: number; dy: number; }>} ev.nodes + * @param {Array} [ev.removedLinks] + * @param {Group} [ev.removeFromGroup] + * @returns {HistoryEvent} The generated history event to redo + */ + function onMove(ev) { + const inverseEv = { + t: "move", + nodes: [], + dirty: RED.nodes.dirty() + }; + + for (let i = 0; i < ev.nodes.length; i++) { + const n = ev.nodes[i]; + const rn = { n: n.n, ox: n.n.x, oy: n.n.y, dirty: true, moved: n.n.moved }; + inverseEv.nodes.push(rn); + n.n.x = n.ox; + n.n.y = n.oy; + n.n.dirty = true; + n.n.moved = n.moved; + } + + // A move could have caused a link splice + if (ev.links) { + inverseEv.removedLinks = []; + for (let i = 0; i < ev.links.length; i++) { + inverseEv.removedLinks.push(ev.links[i]); + RED.nodes.removeLink(ev.links[i]); + } + } + + if (ev.removedLinks) { + inverseEv.links = []; + for (let i = 0; i < ev.removedLinks.length; i++) { + inverseEv.links.push(ev.removedLinks[i]); + RED.nodes.addLink(ev.removedLinks[i]); + } + } + + if (ev.addToGroup) { + RED.group.removeFromGroup(ev.addToGroup, ev.nodes.map(function (n) { return n.n }), false); + inverseEv.removeFromGroup = ev.addToGroup; + } + + if (ev.removeFromGroup) { + RED.group.addToGroup(ev.removeFromGroup, ev.nodes.map(function (n) { return n.n })); + inverseEv.addToGroup = ev.removeFromGroup; + } + + return inverseEv; + } + + /** + * Called on the `createSubflow` history event + * @param {object} ev The history event to undo + * @param {"createSubflow"} ev.t The history event type + * @param {string} ev.activeWorkspace + * @param {Array} [ev.links] + * @param {Array} [ev.nodes] + * @param {Array} [ev.removedLinks] + * @param {{ subflow: Subflow; offsetX?: number; offsetY?: number; }} ev.subflow + * @returns {HistoryEvent} The generated history event to redo + */ + function onCreateSubflow(ev) { + const inverseEv = { + t: "deleteSubflow", + activeWorkspace: ev.activeWorkspace, + dirty: RED.nodes.dirty() + }; + + if (ev.nodes) { + inverseEv.movedNodes = []; + const z = ev.activeWorkspace; + let fullNodeList = RED.nodes.filterNodes({ z: ev.subflow.subflow.id }); + fullNodeList = fullNodeList.concat(RED.nodes.groups(ev.subflow.subflow.id)); + fullNodeList = fullNodeList.concat(RED.nodes.junctions(ev.subflow.subflow.id)); + fullNodeList.forEach(function (n) { + n.x += ev.subflow.offsetX; + n.y += ev.subflow.offsetY; + n.dirty = true; + inverseEv.movedNodes.push(n.id); + RED.nodes.moveNodeToTab(n, z); + }); + inverseEv.subflows = []; + for (let i = 0; i < ev.nodes.length; i++) { + inverseEv.subflows.push(nodeOrJunction(ev.nodes[i])); + RED.nodes.remove(ev.nodes[i]); + } + } + if (ev.links) { + inverseEv.links = []; + for (let i = 0; i < ev.links.length; i++) { + inverseEv.links.push(ev.links[i]); + RED.nodes.removeLink(ev.links[i]); + } + } + + inverseEv.subflow = ev.subflow; + RED.nodes.removeSubflow(ev.subflow.subflow); + RED.workspaces.remove(ev.subflow.subflow); + + if (ev.removedLinks) { + inverseEv.createdLinks = []; + for (let i = 0; i < ev.removedLinks.length; i++) { + inverseEv.createdLinks.push(ev.removedLinks[i]); + RED.nodes.addLink(ev.removedLinks[i]); + } + } + + return inverseEv; + } + + /** + * Called on the `deleteSubflow` history event + * @param {object} ev The history event to undo + * @param {"deleteSubflow"} ev.t The history event type + * @param {string} ev.activeWorkspace + * @param {Array} [ev.createdLinks] + * @param {Array} [ev.links] + * @param {Array} [ev.movedNodes] + * @param {{ subflow: Subflow; offsetX?: number; offsetY?: number; }} ev.subflow + * @param {Array} [ev.subflows] + * @returns {HistoryEvent} The generated history event to redo + */ + function onDeleteSubflow(ev) { + const inverseEv = { + t: "createSubflow", + activeWorkspace: ev.activeWorkspace, + dirty: RED.nodes.dirty(), + }; + + if (ev.subflow) { + RED.nodes.addSubflow(ev.subflow.subflow); + inverseEv.subflow = ev.subflow; + if (ev.subflow.subflow.g) { + RED.group.addToGroup(RED.nodes.group(ev.subflow.subflow.g), ev.subflow.subflow); + } + } + + if (ev.subflows) { + inverseEv.nodes = []; + for (let i = 0; i < ev.subflows.length; i++) { + RED.nodes.add(ev.subflows[i]); + inverseEv.nodes.push(ev.subflows[i].id); + } + } + + if (ev.movedNodes) { + ev.movedNodes.forEach(function (nid) { + let nn = RED.nodes.node(nid); + if (!nn) { + nn = RED.nodes.group(nid); + } + nn.x -= ev.subflow.offsetX; + nn.y -= ev.subflow.offsetY; + nn.dirty = true; + RED.nodes.moveNodeToTab(nn, ev.subflow.subflow.id); + }); + } + + if (ev.links) { + inverseEv.links = []; + for (let i = 0; i < ev.links.length; i++) { + inverseEv.links.push(ev.links[i]); + RED.nodes.addLink(ev.links[i]); + } + } + + if (ev.createdLinks) { + inverseEv.removedLinks = []; + for (let i = 0; i < ev.createdLinks.length; i++) { + inverseEv.removedLinks.push(ev.createdLinks[i]); + RED.nodes.removeLink(ev.createdLinks[i]); + } + } + + return inverseEv; + } + + /** + * Called on the `reorder` history event + * @param {object} ev The history event to undo + * @param {"reorder"} ev.t The history event type + * @param {{ from: string; to: string; z: string; }} [ev.nodes] + * @param {{ from: Array; to: Array; }} [ev.workspaces] + * @returns {HistoryEvent} The generated history event to redo + */ + function onReoder(ev) { + const inverseEv = { + t: "reorder", + dirty: RED.nodes.dirty() + }; + + if (ev.workspaces) { + inverseEv.workspaces = { + from: ev.workspaces.to, + to: ev.workspaces.from + } + RED.workspaces.order(ev.workspaces.from); + } + + if (ev.nodes) { + inverseEv.nodes = { + z: ev.nodes.z, + from: ev.nodes.to, + to: ev.nodes.from + } + RED.nodes.setNodeOrder(ev.nodes.z, ev.nodes.from); + } + + return inverseEv; + } + + /** + * Called on the `createGroup` history event + * @param {object} ev The history event to undo + * @param {"createGroup"} ev.t The history event type + * @param {Array} [ev.groups] + * @returns {HistoryEvent} The generated history event to redo + */ + function onCreateGroup(ev) { + const inverseEv = { + t: "ungroup", + dirty: RED.nodes.dirty(), + groups: [] + }; + + if (ev.groups) { + for (let i = 0; i < ev.groups.length; i++) { + inverseEv.groups.push(ev.groups[i]); + RED.group.ungroup(ev.groups[i]); + } + } + + return inverseEv; + } + + /** + * Called on the `ungroup` history event + * @param {object} ev The history event to undo + * @param {"ungroup"} ev.t The history event type + * @param {Array} [ev.groups] + * @returns {HistoryEvent} The generated history event to redo + */ + function onUngroup(ev) { + const inverseEv = { + t: "createGroup", + dirty: RED.nodes.dirty(), + groups: [] + }; + + if (ev.groups) { + for (let i = 0; i < ev.groups.length; i++) { + inverseEv.groups.push(ev.groups[i]); + const nodes = ev.groups[i].nodes.slice(); + ev.groups[i].nodes = []; + RED.nodes.addGroup(ev.groups[i]); + RED.group.addToGroup(ev.groups[i], nodes); + if (ev.groups[i].g) { + const parentGroup = RED.nodes.group(ev.groups[i].g); + if (parentGroup) { + RED.group.addToGroup(parentGroup, ev.groups[i]); + } + } + } + } + + return inverseEv; + } + + /** + * Called on the `addToGroup` history event + * @param {object} ev The history event to undo + * @param {"addToGroup"} ev.t The history event type + * @param {Group} [ev.group] + * @param {Array | Node} [ev.nodes] + * @param {boolean} [ev.reparent] + * @returns {HistoryEvent} The generated history event to redo + */ + function onAddTogroup(ev) { + const inverseEv = { + t: "removeFromGroup", + dirty: RED.nodes.dirty(), + group: ev.group, + nodes: ev.nodes, + reparent: ev.reparent + }; + + if (ev.nodes) { + RED.group.removeFromGroup(ev.group, ev.nodes, (ev.hasOwnProperty("reparent") && ev.hasOwnProperty("reparent") !== undefined) ? ev.reparent : true); + } + + return inverseEv; + } + + /** + * Called on the `removeFromGroup` history event + * @param {object} ev The history event to undo + * @param {"removeFromGroup"} ev.t The history event type + * @param {Group} [ev.group] + * @param {Array | Node} [ev.nodes] + * @param {boolean} [ev.reparent] + * @returns {HistoryEvent} The generated history event to redo + */ + function onRemoveFromgroup(ev) { + const inverseEv = { + t: "addToGroup", + dirty: RED.nodes.dirty(), + group: ev.group, + nodes: ev.nodes, + reparent: ev.reparent + }; + + if (ev.nodes) { + RED.group.addToGroup(ev.group, ev.nodes); + } + + return inverseEv; + } + + /** + * Called on the `edit` history event + * @param {object} ev The history event to undo + * @param {"edit"} ev.t The history event type + * @param {object} ev.changes + * @param {Array} [ev.createdLinks] + * @param {Array} [ev.links] + * @param {Node} ev.node + * @param {object} [ev.outputMap] + * @param {Subflow} [ev.subflow] + * @returns {HistoryEvent} The generated history event to redo + */ + function onEdit(ev) { + const inverseEv = { + t: "edit", + changes: {}, + changed: ev.node.changed, + dirty: RED.nodes.dirty() + }; + + inverseEv.node = ev.node; + for (const i in ev.changes) { + if (ev.changes.hasOwnProperty(i)) { + inverseEv.changes[i] = ev.node[i]; + if (ev.node._def.defaults && ev.node._def.defaults[i] && ev.node._def.defaults[i].type) { + // This property is a reference to another node or nodes. + let nodeList = ev.node[i]; + if (!Array.isArray(nodeList)) { + nodeList = [nodeList]; + } + + nodeList.forEach(function (id) { + const currentConfigNode = RED.nodes.node(id); + if (currentConfigNode && currentConfigNode._def.category === "config") { + currentConfigNode.users.splice(currentConfigNode.users.indexOf(ev.node), 1); + RED.events.emit("nodes:change", currentConfigNode); + } + }); + + nodeList = ev.changes[i]; + if (!Array.isArray(nodeList)) { + nodeList = [nodeList]; + } + + nodeList.forEach(function (id) { + const newConfigNode = RED.nodes.node(id); + if (newConfigNode && newConfigNode._def.category === "config") { + newConfigNode.users.push(ev.node); + RED.events.emit("nodes:change", newConfigNode); + } }); } - if (ev.groups) { - inverseEv.groups = []; - var groupsToAdd = {}; - ev.groups.forEach(function(g) { groupsToAdd[g.id] = g; }); - for (i = ev.groups.length - 1;i>=0;i--) { - RED.nodes.addGroup(ev.groups[i]) - modifiedTabs[ev.groups[i].z] = true; - // The order of groups is important - // - to invert the action, the order is reversed - inverseEv.groups.unshift(ev.groups[i]); - if (ev.groups[i].g) { - if (!groupsToAdd[ev.groups[i].g]) { - group = RED.nodes.group(ev.groups[i].g); - } else { - group = groupsToAdd[ev.groups[i].g]; - } - if (group.nodes.indexOf(ev.groups[i]) === -1) { - group.nodes.push(ev.groups[i]); - } - RED.group.markDirty(ev.groups[i]) - } - } - } - if (ev.nodes) { - inverseEv.nodes = []; - for (i=0;i ev.subflow.inputCount) { - inverseEv.subflow.inputs = ev.node.in.slice(ev.subflow.inputCount); - ev.node.in.splice(ev.subflow.inputCount); - } else if (ev.subflow.inputs.length > 0) { - ev.node.in = ev.node.in.concat(ev.subflow.inputs); - } - } - if (ev.subflow.hasOwnProperty('outputCount')) { - inverseEv.subflow.outputCount = ev.node.out.length; - if (ev.node.out.length > ev.subflow.outputCount) { - inverseEv.subflow.outputs = ev.node.out.slice(ev.subflow.outputCount); - ev.node.out.splice(ev.subflow.outputCount); - } else if (ev.subflow.outputs.length > 0) { - ev.node.out = ev.node.out.concat(ev.subflow.outputs); - } - } - if (ev.subflow.hasOwnProperty('instances')) { - inverseEv.subflow.instances = []; - ev.subflow.instances.forEach(function(n) { - inverseEv.subflow.instances.push(n); - var node = RED.nodes.node(n.id); - if (node) { - node.changed = n.changed; - node.dirty = true; - } - }); - } - if (ev.subflow.hasOwnProperty('status')) { - if (ev.subflow.status) { - delete ev.node.status; - } - } - RED.editor.validateNode(ev.node); - RED.nodes.filterNodes({type:"subflow:"+ev.node.id}).forEach(function(n) { - n.inputs = ev.node.in.length; - n.outputs = ev.node.out.length; - RED.editor.updateNodeProperties(n); - RED.editor.validateNode(n); - }); - } else { - var outputMap; - if (ev.outputMap) { - outputMap = {}; - inverseEv.outputMap = {}; - for (var port in ev.outputMap) { - if (ev.outputMap.hasOwnProperty(port) && ev.outputMap[port] !== "-1") { - outputMap[ev.outputMap[port]] = port; - inverseEv.outputMap[ev.outputMap[port]] = port; - } - } - } - ev.node.__outputs = inverseEv.changes.outputs; - RED.editor.updateNodeProperties(ev.node,outputMap); - RED.editor.validateNode(ev.node); - } - if (ev.links) { - inverseEv.createdLinks = []; - for (i=0;i ev.subflow.inputCount) { + inverseEv.subflow.inputs = ev.node.in.slice(ev.subflow.inputCount); + ev.node.in.splice(ev.subflow.inputCount); + } else if (ev.subflow.inputs.length > 0) { + ev.node.in = ev.node.in.concat(ev.subflow.inputs); } } - if(ev.callback && typeof ev.callback === 'function') { + if (ev.subflow.hasOwnProperty("outputCount")) { + inverseEv.subflow.outputCount = ev.node.out.length; + if (ev.node.out.length > ev.subflow.outputCount) { + inverseEv.subflow.outputs = ev.node.out.slice(ev.subflow.outputCount); + ev.node.out.splice(ev.subflow.outputCount); + } else if (ev.subflow.outputs.length > 0) { + ev.node.out = ev.node.out.concat(ev.subflow.outputs); + } + } + + if (ev.subflow.hasOwnProperty("instances")) { + inverseEv.subflow.instances = []; + ev.subflow.instances.forEach(function (n) { + inverseEv.subflow.instances.push(n); + const node = RED.nodes.node(n.id); + if (node) { + node.changed = n.changed; + node.dirty = true; + } + }); + } + + if (ev.subflow.hasOwnProperty("status")) { + if (ev.subflow.status) { + delete ev.node.status; + } + } + + RED.editor.validateNode(ev.node); + RED.nodes.filterNodes({ type: "subflow:" + ev.node.id }).forEach(function (n) { + n.inputs = ev.node.in.length; + n.outputs = ev.node.out.length; + RED.editor.updateNodeProperties(n); + RED.editor.validateNode(n); + }); + } else { + let outputMap; + if (ev.outputMap) { + outputMap = {}; + inverseEv.outputMap = {}; + for (const port in ev.outputMap) { + if (ev.outputMap.hasOwnProperty(port) && ev.outputMap[port] !== "-1") { + outputMap[ev.outputMap[port]] = port; + inverseEv.outputMap[ev.outputMap[port]] = port; + } + } + } + ev.node.__outputs = inverseEv.changes.outputs; + RED.editor.updateNodeProperties(ev.node, outputMap); + RED.editor.validateNode(ev.node); + } + + if (ev.links) { + inverseEv.createdLinks = []; + for (let i = 0; i < ev.links.length; i++) { + RED.nodes.addLink(ev.links[i]); + inverseEv.createdLinks.push(ev.links[i]); + } + } + + if (ev.createdLinks) { + inverseEv.links = []; + for (let i = 0; i < ev.createdLinks.length; i++) { + RED.nodes.removeLink(ev.createdLinks[i]); + inverseEv.links.push(ev.createdLinks[i]); + } + } + + return inverseEv; + } + + /** + * Called on the `replace` history event + * @param {object} ev The history event to undo + * @param {"replace"} ev.t The history event type + * @param {Record} ev.changed + * @param {boolean} [ev.complete] + * @param {Array} ev.config + * @param {Record} ev.moved + * @param {string} [ev.rev] + * @returns {HistoryEvent} The generated history event to redo + */ + function onReplace(ev) { + let inverseEv; + + if (ev.complete) { + // This is a replace of everything. We can short-cut + // the logic by clearing everyting first, then importing + // the ev.config. + // Used by RED.diff.mergeDiff + inverseEv = { + t: "replace", + config: RED.nodes.createCompleteNodeSet(), + changed: {}, + moved: {}, + complete: true, + rev: RED.nodes.version(), + dirty: RED.nodes.dirty() + }; + + const 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(); + const flowsToLock = new Set(); + const imported = RED.nodes.import(ev.config); + // Clear all change flags from the import + RED.nodes.dirty(false); + + imported.nodes.forEach(function (n) { + if (ev.changed[n.id]) { + ensureUnlocked(n.z, flowsToLock) + n.changed = true; + } + if (ev.moved[n.id]) { + ensureUnlocked(n.z, flowsToLock) + 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 { + const importMap = {}; + + ev.config.forEach(function (n) { + importMap[n.id] = "replace"; + }); + + const importedResult = RED.nodes.import(ev.config, { importMap: importMap }); + inverseEv = { + t: "replace", + config: importedResult.removedNodes, + dirty: RED.nodes.dirty() + }; + } + + return inverseEv; + } + + /** + * Called on the `multi` history event + * @param {object} ev The history event to undo + * @param {"multi"} ev.t The history event type + * @param {Array} ev.events + * @returns {HistoryEvent} The generated history event to redo + */ + function onMultipleEvents(ev) { + const inverseEv = { + t: "multi", + events: [] + }; + + const len = ev.events.length; + for (let i = len - 1; i >= 0; i--) { + const redoEvent = undoEvent(ev.events[i]); + inverseEv.events.push(redoEvent); + } + + return inverseEv; + } + + /** + * Called to undo the history event + * @param {HistoryEvent} ev The history event to undo + * @returns {HistoryEvent} The generated history event to redo + */ + function undoEvent(ev) { + let modifiedTabs = {}; + let inverseEv; + + if (ev) { + if (ev.t == "multi") { + inverseEv = onMultipleEvents(ev); + } else if (ev.t == "replace") { + inverseEv = onReplace(ev); + } else if (ev.t == "add") { + inverseEv = onAdd(ev, modifiedTabs); + } else if (ev.t == "delete") { + inverseEv = onDelete(ev, modifiedTabs); + } else if (ev.t == "move") { + inverseEv = onMove(ev); + } else if (ev.t == "edit") { + inverseEv = onEdit(ev); + } else if (ev.t == "createSubflow") { + inverseEv = onCreateSubflow(ev); + } else if (ev.t == "deleteSubflow") { + inverseEv = onDeleteSubflow(ev); + } else if (ev.t == "reorder") { + inverseEv = onReoder(ev); + } else if (ev.t == "createGroup") { + inverseEv = onCreateGroup(ev); + } else if (ev.t == "ungroup") { + inverseEv = onUngroup(ev); + } else if (ev.t == "addToGroup") { + inverseEv = onAddTogroup(ev); + } else if (ev.t == "removeFromGroup") { + inverseEv = onRemoveFromgroup(ev); + } + + if (ev.callback && typeof ev.callback === "function") { inverseEv.callback = ev.callback; ev.callback(ev); } - Object.keys(modifiedTabs).forEach(function(id) { - var subflow = RED.nodes.subflow(id); + Object.keys(modifiedTabs).forEach(function (id) { + const subflow = RED.nodes.subflow(id); if (subflow) { RED.editor.validateNode(subflow); } @@ -738,67 +1026,70 @@ RED.history = (function() { return inverseEv; } - } return { //TODO: this function is a placeholder until there is a 'save' event that can be listened to - markAllDirty: function() { - for (var i=0;i