From 3cb52594942545e27494e212c62b5b29e7c73762 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Mon, 19 Sep 2022 21:09:00 +0100 Subject: [PATCH 01/11] Initial locking flows UX --- .../@node-red/editor-client/src/js/nodes.js | 4 ++ .../editor-client/src/js/ui/editor.js | 23 ++++++++ .../src/js/ui/tab-info-outliner.js | 19 ++++++ .../editor-client/src/js/ui/workspaces.js | 59 ++++++++++++++++++- .../editor-client/src/sass/tab-info.scss | 14 +++++ .../editor-client/src/sass/workspace.scss | 22 +++++++ 6 files changed, 140 insertions(+), 1 deletion(-) 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 6dd500581..adbf86741 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 @@ -63,6 +63,7 @@ RED.nodes = (function() { defaults: { label: {value:""}, disabled: {value: false}, + locked: {value: false}, info: {value: ""}, env: {value: []} } @@ -1052,6 +1053,9 @@ RED.nodes = (function() { node.type = n.type; for (var d in n._def.defaults) { if (n._def.defaults.hasOwnProperty(d)) { + if (d === 'locked' && !n.locked) { + continue + } node[d] = n[d]; } } diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/editor.js b/packages/node_modules/@node-red/editor-client/src/js/ui/editor.js index fb4c200f5..0a644ba42 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/editor.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/editor.js @@ -1852,6 +1852,16 @@ RED.editor = (function() { } } + var locked = $("#node-input-locked").prop("checked"); + if (workspace.locked !== locked) { + editState.changes.locked = workspace.locked; + editState.changed = true; + workspace.locked = locked; + $("#red-ui-tab-"+(workspace.id.replace(".","-"))).toggleClass('red-ui-workspace-locked',!!workspace.locked); + // if (workspace.id === RED.workspaces.active()) { + // $("#red-ui-workspace").toggleClass("red-ui-workspace-locked",!!workspace.locked); + // } + } if (editState.changed) { var historyEvent = { t: "edit", @@ -1892,6 +1902,7 @@ RED.editor = (function() { var trayBody = tray.find('.red-ui-tray-body'); trayBody.parent().css('overflow','hidden'); var trayFooterLeft = $('').appendTo(trayFooter) + var trayFooterRight = $('').appendTo(trayFooter) var nodeEditPanes = [ 'editor-tab-flow-properties', @@ -1906,6 +1917,18 @@ RED.editor = (function() { disabledIcon: "fa-ban", invertState: true }) + + if (!workspace.hasOwnProperty("locked")) { + workspace.locked = false; + } + $('').prop("checked",workspace.locked).appendTo(trayFooterRight).toggleButton({ + enabledLabel: 'Unlocked', + enabledIcon: "fa-unlock-alt", + disabledLabel: 'Locked', + disabledIcon: "fa-lock", + invertState: true + }) + prepareEditDialog(trayBody, nodeEditPanes, workspace, {}, "node-input", defaultTab, function(_activeEditPanes) { activeEditPanes = _activeEditPanes; trayBody.i18n(); diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/tab-info-outliner.js b/packages/node_modules/@node-red/editor-client/src/js/ui/tab-info-outliner.js index 32491f297..20bf627f0 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/tab-info-outliner.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/tab-info-outliner.js @@ -221,6 +221,22 @@ RED.sidebar.info.outliner = (function() { } else { $('
').appendTo(controls) } + if (n.type === 'tab') { + var lockToggleButton = $('').appendTo(controls).on("click",function(evt) { + evt.preventDefault(); + evt.stopPropagation(); + if (n.locked) { + RED.workspaces.unlock(n.id) + } else { + RED.workspaces.lock(n.id) + } + }) + RED.popover.tooltip(lockToggleButton,function() { + return RED._("common.label."+(n.locked?"unlock":"lock")); + }); + } else { + $('
').appendTo(controls) + } controls.find("button").on("dblclick", function(evt) { evt.preventDefault(); evt.stopPropagation(); @@ -364,6 +380,8 @@ RED.sidebar.info.outliner = (function() { flowList.treeList.addChild(objects[ws.id]) objects[ws.id].element.toggleClass("red-ui-info-outline-item-disabled", !!ws.disabled) objects[ws.id].treeList.container.toggleClass("red-ui-info-outline-item-disabled", !!ws.disabled) + objects[ws.id].element.toggleClass("red-ui-info-outline-item-locked", !!ws.locked) + // objects[ws.id].treeList.container.toggleClass("red-ui-info-outline-item-disabled", !!ws.disabled) updateSearch(); } @@ -378,6 +396,7 @@ RED.sidebar.info.outliner = (function() { existingObject.element.find(".red-ui-info-outline-item-label").text(label); existingObject.element.toggleClass("red-ui-info-outline-item-disabled", !!n.disabled) existingObject.treeList.container.toggleClass("red-ui-info-outline-item-disabled", !!n.disabled) + existingObject.element.toggleClass("red-ui-info-outline-item-locked", !!n.locked) updateSearch(); } function onFlowsReorder(order) { diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/workspaces.js b/packages/node_modules/@node-red/editor-client/src/js/ui/workspaces.js index d274ba519..23171fb5a 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/workspaces.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/workspaces.js @@ -58,6 +58,9 @@ RED.workspaces = (function() { if (!ws.closeable) { ws.hideable = true; } + if (!ws.hasOwnProperty('locked')) { + ws.locked = false + } workspace_tabs.addTab(ws,targetIndex); var hiddenTabs = JSON.parse(RED.settings.getLocal("hiddenTabs")||"{}"); @@ -75,6 +78,7 @@ RED.workspaces = (function() { type: "tab", id: tabId, disabled: false, + locked: false, info: "", label: RED._('workspace.defaultName',{number:workspaceIndex}), env: [], @@ -329,6 +333,12 @@ RED.workspaces = (function() { if (tab.disabled) { $("#red-ui-tab-"+(tab.id.replace(".","-"))).addClass('red-ui-workspace-disabled'); } + $(' ').prependTo("#red-ui-tab-"+(tab.id.replace(".","-"))+" .red-ui-tab-label"); + if (tab.locked) { + $("#red-ui-tab-"+(tab.id.replace(".","-"))).addClass('red-ui-workspace-locked'); + } + + RED.menu.setDisabled("menu-item-workspace-delete",activeWorkspace === 0 || workspaceTabCount <= 1); if (workspaceTabCount === 1) { showWorkspace(); @@ -465,6 +475,8 @@ RED.workspaces = (function() { RED.actions.add("core:remove-flow",removeWorkspace); RED.actions.add("core:enable-flow",enableWorkspace); RED.actions.add("core:disable-flow",disableWorkspace); + RED.actions.add("core:lock-flow",lockWorkspace); + RED.actions.add("core:unlock-flow",unlockWorkspace); RED.actions.add("core:move-flow-to-start", function(id) { moveWorkspace(id, 'start') }); RED.actions.add("core:move-flow-to-end", function(id) { moveWorkspace(id, 'end') }); @@ -638,6 +650,49 @@ RED.workspaces = (function() { } } } + function lockWorkspace(id) { + setWorkspaceLockState(id,true); + } + function unlockWorkspace(id) { + setWorkspaceLockState(id,false); + } + function setWorkspaceLockState(id,locked) { + var workspace = RED.nodes.workspace(id||activeWorkspace); + if (!workspace) { + return; + } + if (workspace.locked !== locked) { + var changes = { locked: workspace.locked }; + workspace.locked = locked; + $("#red-ui-tab-"+(workspace.id.replace(".","-"))).toggleClass('red-ui-workspace-locked',!!workspace.locked); + if (!id || (id === activeWorkspace)) { + $("#red-ui-workspace").toggleClass("red-ui-workspace-locked",!!workspace.locked); + } + var historyEvent = { + t: "edit", + changes:changes, + node: workspace, + dirty: RED.nodes.dirty() + } + workspace.changed = true; + RED.history.push(historyEvent); + RED.events.emit("flows:change",workspace); + RED.nodes.dirty(true); + // RED.sidebar.config.refresh(); + // var selection = RED.view.selection(); + // if (!selection.nodes && !selection.links && workspace.id === activeWorkspace) { + // RED.sidebar.info.refresh(workspace); + // } + // if (changes.hasOwnProperty('disabled')) { + // RED.nodes.eachNode(function(n) { + // if (n.z === workspace.id) { + // n.dirty = true; + // } + // }); + // RED.view.redraw(); + // } + } + } function removeWorkspace(ws) { if (!ws) { @@ -793,6 +848,8 @@ RED.workspaces = (function() { workspace_tabs.resize(); }, enable: enableWorkspace, - disable: disableWorkspace + disable: disableWorkspace, + lock: lockWorkspace, + unlock: unlockWorkspace } })(); diff --git a/packages/node_modules/@node-red/editor-client/src/sass/tab-info.scss b/packages/node_modules/@node-red/editor-client/src/sass/tab-info.scss index 57dc7d6e3..ed526b263 100644 --- a/packages/node_modules/@node-red/editor-client/src/sass/tab-info.scss +++ b/packages/node_modules/@node-red/editor-client/src/sass/tab-info.scss @@ -467,6 +467,9 @@ div.red-ui-info-table { .fa-eye { display: none; } + .fa-unlock-alt { + display: none; + } } .red-ui-info-outline-item-control-reveal, .red-ui-info-outline-item-control-action { @@ -500,6 +503,17 @@ div.red-ui-info-table { display: none; } } + .fa-lock { + display: none; + } + .red-ui-info-outline-item.red-ui-info-outline-item-locked & { + .fa-lock { + display: inline-block; + } + .fa-unlock-alt { + display: none; + } + } button { margin-right: 3px } diff --git a/packages/node_modules/@node-red/editor-client/src/sass/workspace.scss b/packages/node_modules/@node-red/editor-client/src/sass/workspace.scss index 24e156b1e..c458d03d1 100644 --- a/packages/node_modules/@node-red/editor-client/src/sass/workspace.scss +++ b/packages/node_modules/@node-red/editor-client/src/sass/workspace.scss @@ -106,6 +106,28 @@ } } +.red-ui-workspace-locked-icon { + display: none; +} +.red-ui-workspace-locked { + &.red-ui-tab { + // border-top-style: dashed; + // border-left-style: dashed; + // border-right-style: dashed; + + // a { + // font-style: italic; + // color: var(--red-ui-tab-text-color-disabled-inactive) !important; + // } + // &.active a { + // font-weight: normal; + // color: var(--red-ui-tab-text-color-disabled-active) !important; + // } + .red-ui-workspace-locked-icon { + display: inline; + } + } +} #red-ui-navigator-canvas { position: absolute; From f12d36b5ede4898fa8d3fc8a89da79870b6d01f9 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Tue, 1 Nov 2022 10:48:48 +0000 Subject: [PATCH 02/11] Locking flows fixes and context menu options --- .../editor-client/locales/en-US/editor.json | 2 + .../@node-red/editor-client/src/js/history.js | 6 +- .../@node-red/editor-client/src/js/nodes.js | 107 +++++++++--- .../editor-client/src/js/ui/clipboard.js | 50 +++--- .../editor-client/src/js/ui/contextMenu.js | 34 ++-- .../editor-client/src/js/ui/editor.js | 6 - .../src/js/ui/editors/panes/flowProperties.js | 2 - .../editor-client/src/js/ui/group.js | 22 ++- .../editor-client/src/js/ui/palette.js | 164 +++++++++--------- .../editor-client/src/js/ui/subflow.js | 5 +- .../editor-client/src/js/ui/tab-config.js | 20 ++- .../src/js/ui/tab-info-outliner.js | 3 +- .../editor-client/src/js/ui/view-tools.js | 47 ++++- .../@node-red/editor-client/src/js/ui/view.js | 138 ++++++++++----- .../editor-client/src/js/ui/workspaces.js | 43 ++++- .../editor-client/src/sass/flow.scss | 11 +- .../editor-client/src/sass/tab-info.scss | 10 +- 17 files changed, 447 insertions(+), 223 deletions(-) diff --git a/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json b/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json index 44d370aac..e74dbdbaa 100755 --- a/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json +++ b/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json @@ -71,6 +71,8 @@ "selectNodes": "Click nodes to select", "enableFlow": "Enable flow", "disableFlow": "Disable flow", + "lockFlow": "Lock flow", + "unlockFlow": "Unlock flow", "moveToStart": "Move flow to start", "moveToEnd": "Move flow to end" }, 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 b23071239..977ecb187 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 @@ -14,7 +14,7 @@ * limitations under the License. **/ -/** +/** * An API for undo / redo history buffer * @namespace RED.history */ @@ -434,7 +434,9 @@ RED.history = (function() { if (ev.node.type === 'tab' && ev.changes.hasOwnProperty('disabled')) { $("#red-ui-tab-"+(ev.node.id.replace(".","-"))).toggleClass('red-ui-workspace-disabled',!!ev.node.disabled); - $("#red-ui-workspace").toggleClass("red-ui-workspace-disabled",!!ev.node.disabled); + } + if (ev.node.type === 'tab' && ev.changes.hasOwnProperty('locked')) { + $("#red-ui-tab-"+(ev.node.id.replace(".","-"))).toggleClass('red-ui-workspace-locked',!!ev.node.locked); } if (ev.subflow) { inverseEv.subflow = {}; 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 adbf86741..715ae9c49 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 @@ -19,7 +19,6 @@ * @namespace RED.nodes */ RED.nodes = (function() { - var PORT_TYPE_INPUT = 1; var PORT_TYPE_OUTPUT = 0; @@ -576,8 +575,41 @@ RED.nodes = (function() { } } + const nodeProxyHandler = { + get(node, prop) { + if (prop === '__isProxy__') { + return true + } else if (prop == '__node__') { + return node + } + return node[prop] + }, + set(node, prop, value) { + if (node.z && (RED.nodes.workspace(node.z)?.locked || RED.nodes.subflow(node.z)?.locked)) { + if ( + node._def.defaults[prop] || + prop === 'z' || + prop === 'l' || + prop === 'd' || + (prop === 'changed' && !!node.changed !== !!value) || + ((prop === 'x' || prop === 'y') && !node.resize && node.type !== 'group') + ) { + throw new Error(`Cannot modified property '${prop}' of locked object '${node.type}:${node.id}'`) + } + } + node[prop] = value; + return true + } + } function addNode(n) { + let newNode + if (!n.__isProxy__) { + newNode = new Proxy(n, nodeProxyHandler) + } else { + newNode = n + } + if (n.type.indexOf("subflow") !== 0) { n["_"] = n._def._; } else { @@ -601,12 +633,13 @@ RED.nodes = (function() { }); n.i = nextId+1; } - allNodes.addNode(n); + allNodes.addNode(newNode); if (!nodeLinks[n.id]) { nodeLinks[n.id] = {in:[],out:[]}; } } - RED.events.emit('nodes:add',n); + RED.events.emit('nodes:add',newNode); + return newNode } function addLink(l) { if (nodeLinks[l.source.id]) { @@ -1335,7 +1368,6 @@ RED.nodes = (function() { } else { nodeSet = [sf]; } - console.log(nodeSet); return createExportableNodeSet(nodeSet); } /** @@ -2322,19 +2354,6 @@ RED.nodes = (function() { if (n.g && !new_group_set.has(n.g)) { delete n.g; } - n.nodes = n.nodes.map(function(id) { - return node_map[id]; - }) - // Just in case the group references a node that doesn't exist for some reason - n.nodes = n.nodes.filter(function(v) { - if (v) { - // Repair any nodes that have forgotten they are in this group - if (v.g !== n.id) { - v.g = n.id; - } - } - return !!v - }); if (!n.g) { groupDepthMap[n.id] = 0; } @@ -2357,21 +2376,22 @@ RED.nodes = (function() { return groupDepthMap[A.id] - groupDepthMap[B.id]; }); for (i=0;i { + const mappedNode = node_map[id] + if (!mappedNode) { + return null + } + if (mappedNode.__isProxy__) { + return mappedNode + } else { + return node_map[mappedNode.id] + } + } + // Update groups to reference proxy node objects + for (i=0;i 1 const canDelete = hasSelection || hasLinks const isGroup = hasSelection && selection.nodes.length === 1 && selection.nodes[0].type === 'group' - + const canEdit = !RED.workspaces.isActiveLocked() const canRemoveFromGroup = hasSelection && !!selection.nodes[0].g + const offset = $("#red-ui-workspace-chart").offset() let addX = options.x - offset.left + $("#red-ui-workspace-chart").scrollLeft() @@ -55,12 +56,13 @@ RED.contextMenu = (function () { splice: isSingleLink ? selection.links[0] : undefined, // spliceMultiple: isMultipleLinks }) - } + }, + disabled: !canEdit }, (hasLinks) ? { // has least 1 wire selected label: RED._("contextMenu.junction"), onselect: 'core:split-wires-with-junctions', - disabled: !hasLinks + disabled: !canEdit || !hasLinks } : { label: RED._("contextMenu.junction"), onselect: function () { @@ -86,41 +88,39 @@ RED.contextMenu = (function () { RED.nodes.dirty(true); RED.view.select({nodes: [nn] }); RED.view.redraw(true) - } + }, + disabled: !canEdit }, { label: RED._("contextMenu.linkNodes"), onselect: 'core:split-wire-with-link-nodes', - disabled: !hasLinks + disabled: !canEdit || !hasLinks } ] - } - ) - - menuItems.push( + }, null, { onselect: 'core:undo', disabled: RED.history.list().length === 0 }, { onselect: 'core:redo', disabled: RED.history.listRedo().length === 0 }, null, - { onselect: 'core:cut-selection-to-internal-clipboard', label: RED._("keyboard.cutNode"), disabled: !hasSelection }, + { onselect: 'core:cut-selection-to-internal-clipboard', label: RED._("keyboard.cutNode"), disabled: !canEdit || !hasSelection }, { onselect: 'core:copy-selection-to-internal-clipboard', label: RED._("keyboard.copyNode"), disabled: !hasSelection }, - { onselect: 'core:paste-from-internal-clipboard', label: RED._("keyboard.pasteNode"), disabled: !RED.view.clipboard() }, - { onselect: 'core:delete-selection', disabled: !canDelete }, + { onselect: 'core:paste-from-internal-clipboard', label: RED._("keyboard.pasteNode"), disabled: !canEdit || !RED.view.clipboard() }, + { onselect: 'core:delete-selection', disabled: !canEdit || !canDelete }, { onselect: 'core:show-export-dialog', label: RED._("menu.label.export") }, - { onselect: 'core:select-all-nodes' } + { onselect: 'core:select-all-nodes' }, ) - if (hasSelection) { + if (hasSelection && canEdit) { menuItems.push( null, isGroup ? - { onselect: 'core:ungroup-selection', disabled: !isGroup } - : { onselect: 'core:group-selection', disabled: !hasSelection } + { onselect: 'core:ungroup-selection', disabled: !canEdit || !isGroup } + : { onselect: 'core:group-selection', disabled: !canEdit || !hasSelection } ) - if (canRemoveFromGroup) { + if (canRemoveFromGroup && canEdit) { menuItems.push({ onselect: 'core:remove-selection-from-group', label: RED._("menu.label.groupRemoveSelection") }) } diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/editor.js b/packages/node_modules/@node-red/editor-client/src/js/ui/editor.js index 0a644ba42..29202a0ab 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/editor.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/editor.js @@ -1847,9 +1847,6 @@ RED.editor = (function() { workspace.disabled = disabled; $("#red-ui-tab-"+(workspace.id.replace(".","-"))).toggleClass('red-ui-workspace-disabled',!!workspace.disabled); - if (workspace.id === RED.workspaces.active()) { - $("#red-ui-workspace").toggleClass("red-ui-workspace-disabled",!!workspace.disabled); - } } var locked = $("#node-input-locked").prop("checked"); @@ -1858,9 +1855,6 @@ RED.editor = (function() { editState.changed = true; workspace.locked = locked; $("#red-ui-tab-"+(workspace.id.replace(".","-"))).toggleClass('red-ui-workspace-locked',!!workspace.locked); - // if (workspace.id === RED.workspaces.active()) { - // $("#red-ui-workspace").toggleClass("red-ui-workspace-locked",!!workspace.locked); - // } } if (editState.changed) { var historyEvent = { diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/editors/panes/flowProperties.js b/packages/node_modules/@node-red/editor-client/src/js/ui/editors/panes/flowProperties.js index 2db4d0c85..214335f1b 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/editors/panes/flowProperties.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/editors/panes/flowProperties.js @@ -52,8 +52,6 @@ node.info = info; } $("#red-ui-tab-"+(node.id.replace(".","-"))).toggleClass('red-ui-workspace-disabled',!!node.disabled); - $("#red-ui-workspace").toggleClass("red-ui-workspace-disabled",!!node.disabled); - } } }); 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 add6da6c9..15eda31d5 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 @@ -185,6 +185,8 @@ RED.group = (function() { var activateMerge = false; var activateRemove = false; var singleGroupSelected = false; + var locked = RED.workspaces.isActiveLocked() + if (activateGroup) { singleGroupSelected = selection.nodes.length === 1 && selection.nodes[0].type === 'group'; selection.nodes.forEach(function (n) { @@ -199,12 +201,12 @@ RED.group = (function() { activateMerge = (selection.nodes.length > 1); } } - RED.menu.setDisabled("menu-item-group-group", !activateGroup); - RED.menu.setDisabled("menu-item-group-ungroup", !activateUngroup); - RED.menu.setDisabled("menu-item-group-merge", !activateMerge); - RED.menu.setDisabled("menu-item-group-remove", !activateRemove); + RED.menu.setDisabled("menu-item-group-group", locked || !activateGroup); + RED.menu.setDisabled("menu-item-group-ungroup", locked || !activateUngroup); + RED.menu.setDisabled("menu-item-group-merge", locked || !activateMerge); + RED.menu.setDisabled("menu-item-group-remove", locked || !activateRemove); RED.menu.setDisabled("menu-item-edit-copy-group-style", !singleGroupSelected); - RED.menu.setDisabled("menu-item-edit-paste-group-style", !activateUngroup); + RED.menu.setDisabled("menu-item-edit-paste-group-style", locked || !activateUngroup); }); RED.actions.add("core:group-selection", function() { groupSelection() }) @@ -261,6 +263,7 @@ RED.group = (function() { } } function pasteGroupStyle() { + if (RED.workspaces.isActiveLocked()) { return } if (RED.view.state() !== RED.state.DEFAULT) { return } if (groupStyleClipboard) { var selection = RED.view.selection(); @@ -295,6 +298,7 @@ RED.group = (function() { } function groupSelection() { + if (RED.workspaces.isActiveLocked()) { return } if (RED.view.state() !== RED.state.DEFAULT) { return } var selection = RED.view.selection(); if (selection.nodes) { @@ -313,6 +317,7 @@ RED.group = (function() { } } function ungroupSelection() { + if (RED.workspaces.isActiveLocked()) { return } if (RED.view.state() !== RED.state.DEFAULT) { return } var selection = RED.view.selection(); if (selection.nodes) { @@ -336,6 +341,7 @@ RED.group = (function() { } function ungroup(g) { + if (RED.workspaces.isActiveLocked()) { return } var nodes = []; var parentGroup = RED.nodes.group(g.g); g.nodes.forEach(function(n) { @@ -362,6 +368,7 @@ RED.group = (function() { } function mergeSelection() { + if (RED.workspaces.isActiveLocked()) { return } if (RED.view.state() !== RED.state.DEFAULT) { return } var selection = RED.view.selection(); if (selection.nodes) { @@ -431,6 +438,7 @@ RED.group = (function() { } function removeSelection() { + if (RED.workspaces.isActiveLocked()) { return } if (RED.view.state() !== RED.state.DEFAULT) { return } var selection = RED.view.selection(); if (selection.nodes) { @@ -458,6 +466,7 @@ RED.group = (function() { } } function createGroup(nodes) { + if (RED.workspaces.isActiveLocked()) { return } if (nodes.length === 0) { return; } @@ -480,7 +489,7 @@ RED.group = (function() { } group.z = nodes[0].z; - RED.nodes.addGroup(group); + group = RED.nodes.addGroup(group); try { addToGroup(group,nodes); @@ -563,6 +572,7 @@ RED.group = (function() { markDirty(group); } function removeFromGroup(group, nodes, reparent) { + if (RED.workspaces.isActiveLocked()) { return } if (!Array.isArray(nodes)) { nodes = [nodes]; } diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/palette.js b/packages/node_modules/@node-red/editor-client/src/js/ui/palette.js index 9f20cc674..ceaa33775 100755 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/palette.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/palette.js @@ -282,6 +282,7 @@ RED.palette = (function() { var hoverGroup; var paletteWidth; var paletteTop; + var dropEnabled; $(d).draggable({ helper: 'clone', appendTo: '#red-ui-editor', @@ -289,6 +290,7 @@ RED.palette = (function() { revertDuration: 200, containment:'#red-ui-main-container', start: function() { + dropEnabled = !RED.nodes.workspace(RED.workspaces.active()).locked; paletteWidth = $("#red-ui-palette").width(); paletteTop = $("#red-ui-palette").parent().position().top + $("#red-ui-palette-container").position().top; hoverGroup = null; @@ -299,96 +301,100 @@ RED.palette = (function() { RED.view.focus(); }, stop: function() { - d3.select('.red-ui-flow-link-splice').classed('red-ui-flow-link-splice',false); - if (hoverGroup) { - document.getElementById("group_select_"+hoverGroup.id).classList.remove("red-ui-flow-group-hovered"); + if (dropEnabled) { + d3.select('.red-ui-flow-link-splice').classed('red-ui-flow-link-splice',false); + if (hoverGroup) { + document.getElementById("group_select_"+hoverGroup.id).classList.remove("red-ui-flow-group-hovered"); + } + if (activeGroup) { + document.getElementById("group_select_"+activeGroup.id).classList.remove("red-ui-flow-group-active-hovered"); + } + if (spliceTimer) { clearTimeout(spliceTimer); spliceTimer = null; } + if (groupTimer) { clearTimeout(groupTimer); groupTimer = null; } } - if (activeGroup) { - document.getElementById("group_select_"+activeGroup.id).classList.remove("red-ui-flow-group-active-hovered"); - } - if (spliceTimer) { clearTimeout(spliceTimer); spliceTimer = null; } - if (groupTimer) { clearTimeout(groupTimer); groupTimer = null; } }, drag: function(e,ui) { var paletteNode = getPaletteNode(nt); ui.originalPosition.left = paletteNode.offset().left; - mouseX = ui.position.left - paletteWidth + (ui.helper.width()/2) + chart.scrollLeft(); - mouseY = ui.position.top - paletteTop + (ui.helper.height()/2) + chart.scrollTop() + 10; - if (!groupTimer) { - groupTimer = setTimeout(function() { - var mx = mouseX / RED.view.scale(); - var my = mouseY / RED.view.scale(); - var group = RED.view.getGroupAtPoint(mx,my); - if (group !== hoverGroup) { - if (hoverGroup) { - document.getElementById("group_select_"+hoverGroup.id).classList.remove("red-ui-flow-group-hovered"); - } - if (group) { - document.getElementById("group_select_"+group.id).classList.add("red-ui-flow-group-hovered"); - } - hoverGroup = group; - if (hoverGroup) { - $(ui.helper).data('group',hoverGroup); - } else { - $(ui.helper).removeData('group'); - } - } - groupTimer = null; - - },200) - } - if (def.inputs > 0 && def.outputs > 0) { - if (!spliceTimer) { - spliceTimer = setTimeout(function() { - var nodes = []; - var bestDistance = Infinity; - var bestLink = null; - if (chartSVG.getIntersectionList) { - var svgRect = chartSVG.createSVGRect(); - svgRect.x = mouseX; - svgRect.y = mouseY; - svgRect.width = 1; - svgRect.height = 1; - nodes = chartSVG.getIntersectionList(svgRect,chartSVG); - } else { - // Firefox doesn't do getIntersectionList and that - // makes us sad - nodes = RED.view.getLinksAtPoint(mouseX,mouseY); - } + if (dropEnabled) { + mouseX = ui.position.left - paletteWidth + (ui.helper.width()/2) + chart.scrollLeft(); + mouseY = ui.position.top - paletteTop + (ui.helper.height()/2) + chart.scrollTop() + 10; + if (!groupTimer) { + groupTimer = setTimeout(function() { var mx = mouseX / RED.view.scale(); var my = mouseY / RED.view.scale(); - for (var i=0;i 0 && def.outputs > 0) { + if (!spliceTimer) { + spliceTimer = setTimeout(function() { + var nodes = []; + var bestDistance = Infinity; + var bestLink = null; + if (chartSVG.getIntersectionList) { + var svgRect = chartSVG.createSVGRect(); + svgRect.x = mouseX; + svgRect.y = mouseY; + svgRect.width = 1; + svgRect.height = 1; + nodes = chartSVG.getIntersectionList(svgRect,chartSVG); + } else { + // Firefox doesn't do getIntersectionList and that + // makes us sad + nodes = RED.view.getLinksAtPoint(mouseX,mouseY); + } + var mx = mouseX / RED.view.scale(); + var my = mouseY / RED.view.scale(); + for (var i=0;i
').appendTo(parent); var header = $('
').appendTo(container); + let lockIcon if (label) { + lockIcon = $('').appendTo(header) + lockIcon.toggle(!!isLocked) $('').text(label).appendTo(header); } else { $('').appendTo(header); @@ -62,6 +65,7 @@ RED.sidebar.config = (function() { var icon = header.find("i"); var result = { label: label, + lockIcon, list: category, size: function() { return result.list.find("li:not(.red-ui-palette-node-config-none)").length @@ -100,6 +104,9 @@ RED.sidebar.config = (function() { }); categories[name] = result; } else { + if (isLocked !== undefined && categories[name].lockIcon) { + categories[name].lockIcon.toggle(!!isLocked) + } if (categories[name].label !== label) { categories[name].list.parent().find('.red-ui-palette-node-config-label').text(label); categories[name].label = label; @@ -216,7 +223,7 @@ RED.sidebar.config = (function() { RED.nodes.eachWorkspace(function(ws) { validList[ws.id.replace(/\./g,"-")] = true; - getOrCreateCategory(ws.id,flowCategories,ws.label); + getOrCreateCategory(ws.id,flowCategories,ws.label, ws.locked); }) RED.nodes.eachSubflow(function(sf) { validList[sf.id.replace(/\./g,"-")] = true; @@ -274,6 +281,15 @@ RED.sidebar.config = (function() { changes: {}, dirty: RED.nodes.dirty() } + for (let i = 0; i < selectedNodes.length; i++) { + let node = RED.nodes.node(selectedNodes[i]) + if (node.z) { + let ws = RED.nodes.workspace(node.z) + if (ws && ws.locked) { + return + } + } + } selectedNodes.forEach(function(id) { var node = RED.nodes.node(id); try { diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/tab-info-outliner.js b/packages/node_modules/@node-red/editor-client/src/js/ui/tab-info-outliner.js index 20bf627f0..d398cc2d0 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/tab-info-outliner.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/tab-info-outliner.js @@ -381,7 +381,7 @@ RED.sidebar.info.outliner = (function() { objects[ws.id].element.toggleClass("red-ui-info-outline-item-disabled", !!ws.disabled) objects[ws.id].treeList.container.toggleClass("red-ui-info-outline-item-disabled", !!ws.disabled) objects[ws.id].element.toggleClass("red-ui-info-outline-item-locked", !!ws.locked) - // objects[ws.id].treeList.container.toggleClass("red-ui-info-outline-item-disabled", !!ws.disabled) + objects[ws.id].treeList.container.toggleClass("red-ui-info-outline-item-locked", !!ws.locked) updateSearch(); } @@ -397,6 +397,7 @@ RED.sidebar.info.outliner = (function() { existingObject.element.toggleClass("red-ui-info-outline-item-disabled", !!n.disabled) existingObject.treeList.container.toggleClass("red-ui-info-outline-item-disabled", !!n.disabled) existingObject.element.toggleClass("red-ui-info-outline-item-locked", !!n.locked) + existingObject.treeList.container.toggleClass("red-ui-info-outline-item-locked", !!n.locked) updateSearch(); } function onFlowsReorder(order) { diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/view-tools.js b/packages/node_modules/@node-red/editor-client/src/js/ui/view-tools.js index 1ed5791a6..27d45dcd3 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/view-tools.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/view-tools.js @@ -15,7 +15,7 @@ **/ RED.view.tools = (function() { - + 'use strict'; function selectConnected(type) { var selection = RED.view.selection(); var visited = new Set(); @@ -39,6 +39,9 @@ RED.view.tools = (function() { } function alignToGrid() { + if (RED.workspaces.isActiveLocked()) { + return + } var selection = RED.view.selection(); if (selection.nodes) { var changedNodes = []; @@ -87,6 +90,9 @@ RED.view.tools = (function() { } function moveSelection(dx,dy) { + if (RED.workspaces.isActiveLocked()) { + return + } if (moving_set === null) { moving_set = []; var selection = RED.view.selection(); @@ -153,6 +159,9 @@ RED.view.tools = (function() { } function setSelectedNodeLabelState(labelShown) { + if (RED.workspaces.isActiveLocked()) { + return + } var selection = RED.view.selection(); var historyEvents = []; var nodes = []; @@ -439,6 +448,9 @@ RED.view.tools = (function() { } function alignSelectionToEdge(direction) { + // if (RED.workspaces.isActiveLocked()) { + // return + // } var selection = RED.view.selection(); if (selection.nodes && selection.nodes.length > 1) { @@ -539,8 +551,10 @@ RED.view.tools = (function() { } } - function distributeSelection(direction) { + if (RED.workspaces.isActiveLocked()) { + return + } var selection = RED.view.selection(); if (selection.nodes && selection.nodes.length > 2) { @@ -699,6 +713,9 @@ RED.view.tools = (function() { } function reorderSelection(dir) { + if (RED.workspaces.isActiveLocked()) { + return + } var selection = RED.view.selection(); if (selection.nodes) { var nodesToMove = []; @@ -734,8 +751,10 @@ RED.view.tools = (function() { } } - function wireSeriesOfNodes() { + if (RED.workspaces.isActiveLocked()) { + return + } var selection = RED.view.selection(); if (selection.nodes) { if (selection.nodes.length > 1) { @@ -776,6 +795,9 @@ RED.view.tools = (function() { } function wireNodeToMultiple() { + if (RED.workspaces.isActiveLocked()) { + return + } var selection = RED.view.selection(); if (selection.nodes) { if (selection.nodes.length > 1) { @@ -823,6 +845,9 @@ RED.view.tools = (function() { * @param {Object || Object[]} wires The wire(s) to split and replace with link-out, link-in nodes. */ function splitWiresWithLinkNodes(wires) { + if (RED.workspaces.isActiveLocked()) { + return + } let wiresToSplit = wires || (RED.view.selection().links && RED.view.selection().links.filter(e => !e.link)); if (!wiresToSplit) { return @@ -877,7 +902,6 @@ RED.view.tools = (function() { if(!nnLinkOut) { const nLinkOut = RED.view.createNode("link out"); //create link node nnLinkOut = nLinkOut.node; - nodeSrcMap[linkOutMapId] = nnLinkOut; let yOffset = 0; if(nSrc.outputs > 1) { @@ -892,7 +916,8 @@ RED.view.tools = (function() { updateNewNodePosXY(nSrc, nnLinkOut, false, RED.view.snapGrid, yOffset); } //add created node - RED.nodes.add(nnLinkOut); + nnLinkOut = RED.nodes.add(nnLinkOut); + nodeSrcMap[linkOutMapId] = nnLinkOut; RED.editor.validateNode(nnLinkOut); history.events.push(nLinkOut.historyEvent); //connect node to link node @@ -913,10 +938,10 @@ RED.view.tools = (function() { if(!nnLinkIn) { const nLinkIn = RED.view.createNode("link in"); //create link node nnLinkIn = nLinkIn.node; - nodeTrgMap[nTrg.id] = nnLinkIn; updateNewNodePosXY(nTrg, nnLinkIn, true, RED.view.snapGrid, 0); //add created node - RED.nodes.add(nnLinkIn); + nnLinkIn = RED.nodes.add(nnLinkIn); + nodeTrgMap[nTrg.id] = nnLinkIn; RED.editor.validateNode(nnLinkIn); history.events.push(nLinkIn.historyEvent); //connect node to link node @@ -991,6 +1016,9 @@ RED.view.tools = (function() { * @param {{ renameBlank: boolean, renameClash: boolean, generateHistory: boolean }} options Possible options are `renameBlank`, `renameClash` and `generateHistory` */ function generateNodeNames(node, options) { + if (RED.workspaces.isActiveLocked()) { + return + } options = Object.assign({ renameBlank: true, renameClash: true, @@ -1061,6 +1089,9 @@ RED.view.tools = (function() { } function addJunctionsToWires(wires) { + if (RED.workspaces.isActiveLocked()) { + return + } let wiresToSplit = wires || (RED.view.selection().links && RED.view.selection().links.filter(e => !e.link)); if (!wiresToSplit) { return @@ -1131,7 +1162,7 @@ RED.view.tools = (function() { var nodeGroups = new Set() - RED.nodes.addJunction(junction) + junction = RED.nodes.addJunction(junction) addedJunctions.push(junction) let newLink if (gid === links[0].source.id+":"+links[0].sourcePort) { diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/view.js b/packages/node_modules/@node-red/editor-client/src/js/ui/view.js index 83215afe4..d783ce941 100755 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/view.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/view.js @@ -54,6 +54,7 @@ RED.view = (function() { var spliceTimer; var groupHoverTimer; + var activeFlowLocked = false; var activeSubflow = null; var activeNodes = []; var activeLinks = []; @@ -411,6 +412,17 @@ RED.view = (function() { activeSubflow = RED.nodes.subflow(event.workspace); + if (activeSubflow) { + activeFlowLocked = activeSubflow.locked + } else { + var activeWorkspace = RED.nodes.workspace(event.workspace) + if (activeWorkspace) { + activeFlowLocked = activeWorkspace.locked + } else { + activeFlowLocked = true + } + } + RED.menu.setDisabled("menu-item-workspace-edit", activeSubflow || event.workspace === 0); RED.menu.setDisabled("menu-item-workspace-delete",event.workspace === 0 || RED.workspaces.count() == 1 || activeSubflow); @@ -439,6 +451,15 @@ RED.view = (function() { redraw(); }); + RED.events.on("flows:change", function(workspace) { + if (workspace.id === RED.workspaces.active()) { + activeFlowLocked = !!workspace.locked + $("#red-ui-workspace").toggleClass("red-ui-workspace-disabled",!!workspace.disabled); + $("#red-ui-workspace").toggleClass("red-ui-workspace-locked",!!workspace.locked); + + } + }) + RED.statusBar.add({ id: "view-zoom-controls", align: "right", @@ -496,6 +517,9 @@ RED.view = (function() { chart.droppable({ accept:".red-ui-palette-node", drop: function( event, ui ) { + if (activeFlowLocked) { + return + } d3.event = event; var selected_tool = $(ui.draggable[0]).attr("data-palette-type"); var result = createNode(selected_tool); @@ -503,9 +527,7 @@ RED.view = (function() { return; } var historyEvent = result.historyEvent; - var nn = result.node; - - RED.nodes.add(nn); + var nn = RED.nodes.add(result.node); var showLabel = RED.utils.getMessageProperty(RED.settings.get('editor'),"view.view-node-show-label"); if (showLabel !== undefined && (nn._def.hasOwnProperty("showLabel")?nn._def.showLabel:true) && !nn._def.defaults.hasOwnProperty("l")) { @@ -632,6 +654,9 @@ RED.view = (function() { RED.actions.add("core:copy-selection-to-internal-clipboard",copySelection); RED.actions.add("core:cut-selection-to-internal-clipboard",function(){copySelection(true);deleteSelection();}); RED.actions.add("core:paste-from-internal-clipboard",function(){ + if (RED.workspaces.isActiveLocked()) { + return + } importNodes(clipboard,{generateIds: clipboardSource === 'copy', generateDefaultNames: clipboardSource === 'copy'}); }); @@ -640,22 +665,27 @@ RED.view = (function() { RED.events.on("view:selection-changed", function(selection) { var hasSelection = (selection.nodes && selection.nodes.length > 0); var hasMultipleSelection = hasSelection && selection.nodes.length > 1; - RED.menu.setDisabled("menu-item-edit-cut",!hasSelection); - RED.menu.setDisabled("menu-item-edit-copy",!hasSelection); - RED.menu.setDisabled("menu-item-edit-select-connected",!hasSelection); - RED.menu.setDisabled("menu-item-view-tools-move-to-back",!hasSelection); - RED.menu.setDisabled("menu-item-view-tools-move-to-front",!hasSelection); - RED.menu.setDisabled("menu-item-view-tools-move-backwards",!hasSelection); - RED.menu.setDisabled("menu-item-view-tools-move-forwards",!hasSelection); + var hasLinkSelected = selection.links && selection.links.length > 0; + var canEdit = !activeFlowLocked && hasSelection + var canEditMultiple = !activeFlowLocked && hasMultipleSelection + RED.menu.setDisabled("menu-item-edit-cut", !canEdit); + RED.menu.setDisabled("menu-item-edit-copy", !hasSelection); + RED.menu.setDisabled("menu-item-edit-select-connected", !hasSelection); + RED.menu.setDisabled("menu-item-view-tools-move-to-back", !canEdit); + RED.menu.setDisabled("menu-item-view-tools-move-to-front", !canEdit); + RED.menu.setDisabled("menu-item-view-tools-move-backwards", !canEdit); + RED.menu.setDisabled("menu-item-view-tools-move-forwards", !canEdit); - RED.menu.setDisabled("menu-item-view-tools-align-left",!hasMultipleSelection); - RED.menu.setDisabled("menu-item-view-tools-align-center",!hasMultipleSelection); - RED.menu.setDisabled("menu-item-view-tools-align-right",!hasMultipleSelection); - RED.menu.setDisabled("menu-item-view-tools-align-top",!hasMultipleSelection); - RED.menu.setDisabled("menu-item-view-tools-align-middle",!hasMultipleSelection); - RED.menu.setDisabled("menu-item-view-tools-align-bottom",!hasMultipleSelection); - RED.menu.setDisabled("menu-item-view-tools-distribute-horizontally",!hasMultipleSelection); - RED.menu.setDisabled("menu-item-view-tools-distribute-veritcally",!hasMultipleSelection); + RED.menu.setDisabled("menu-item-view-tools-align-left", !canEditMultiple); + RED.menu.setDisabled("menu-item-view-tools-align-center", !canEditMultiple); + RED.menu.setDisabled("menu-item-view-tools-align-right", !canEditMultiple); + RED.menu.setDisabled("menu-item-view-tools-align-top", !canEditMultiple); + RED.menu.setDisabled("menu-item-view-tools-align-middle", !canEditMultiple); + RED.menu.setDisabled("menu-item-view-tools-align-bottom", !canEditMultiple); + RED.menu.setDisabled("menu-item-view-tools-distribute-horizontally", !canEditMultiple); + RED.menu.setDisabled("menu-item-view-tools-distribute-veritcally", !canEditMultiple); + + RED.menu.setDisabled("menu-item-edit-split-wire-with-links", activeFlowLocked || !hasLinkSelected); }) RED.actions.add("core:delete-selection",deleteSelection); @@ -1045,7 +1075,7 @@ RED.view = (function() { .attr("class", "nr-ui-view-lasso"); d3.event.preventDefault(); } - } else if (d3.event.altKey) { + } else if (d3.event.altKey && !activeFlowLocked) { //Alt [+shift] held - Begin slicing clearSelection(); mouse_mode = (d3.event.shiftKey) ? RED.state.SLICING_JUNCTION : RED.state.SLICING; @@ -1059,6 +1089,9 @@ RED.view = (function() { } function showQuickAddDialog(options) { + if (activeFlowLocked) { + return + } options = options || {}; var point = options.position || lastClickPosition; var spliceLink = options.splice; @@ -1238,6 +1271,11 @@ RED.view = (function() { if (showLabel !== undefined && (nn._def.hasOwnProperty("showLabel")?nn._def.showLabel:true) && !nn._def.defaults.hasOwnProperty("l")) { nn.l = showLabel; } + if (nn.type === 'junction') { + nn = RED.nodes.addJunction(nn); + } else { + nn = RED.nodes.add(nn); + } if (quickAddLink) { var drag_line = quickAddLink; var src = null,dst,src_port; @@ -1340,11 +1378,7 @@ RED.view = (function() { } } } - if (nn.type === 'junction') { - RED.nodes.addJunction(nn); - } else { - RED.nodes.add(nn); - } + RED.editor.validateNode(nn); if (targetGroup) { @@ -1602,16 +1636,18 @@ RED.view = (function() { } var d = (mouse_offset[0]-mousePos[0])*(mouse_offset[0]-mousePos[0]) + (mouse_offset[1]-mousePos[1])*(mouse_offset[1]-mousePos[1]); if ((d > 3 && !dblClickPrimed) || (dblClickPrimed && d > 10)) { - mouse_mode = RED.state.MOVING_ACTIVE; clickElapsed = 0; - spliceActive = false; - if (movingSet.length() === 1) { - node = movingSet.get(0); - spliceActive = node.n.hasOwnProperty("_def") && - ((node.n.hasOwnProperty("inputs") && node.n.inputs > 0) || (!node.n.hasOwnProperty("inputs") && node.n._def.inputs > 0)) && - ((node.n.hasOwnProperty("outputs") && node.n.outputs > 0) || (!node.n.hasOwnProperty("outputs") && node.n._def.outputs > 0)) && - RED.nodes.filterLinks({ source: node.n }).length === 0 && - RED.nodes.filterLinks({ target: node.n }).length === 0; + if (!activeFlowLocked) { + mouse_mode = RED.state.MOVING_ACTIVE; + spliceActive = false; + if (movingSet.length() === 1) { + node = movingSet.get(0); + spliceActive = node.n.hasOwnProperty("_def") && + ((node.n.hasOwnProperty("inputs") && node.n.inputs > 0) || (!node.n.hasOwnProperty("inputs") && node.n._def.inputs > 0)) && + ((node.n.hasOwnProperty("outputs") && node.n.outputs > 0) || (!node.n.hasOwnProperty("outputs") && node.n._def.outputs > 0)) && + RED.nodes.filterLinks({ source: node.n }).length === 0 && + RED.nodes.filterLinks({ target: node.n }).length === 0; + } } } } else if (mouse_mode == RED.state.MOVING_ACTIVE || mouse_mode == RED.state.IMPORT_DRAGGING || mouse_mode == RED.state.DETACHED_DRAGGING) { @@ -2456,6 +2492,7 @@ RED.view = (function() { } function editSelection() { + if (RED.workspaces.isActiveLocked()) { return } if (movingSet.length() > 0) { var node = movingSet.get(0).n; if (node.type === "subflow") { @@ -2471,6 +2508,9 @@ RED.view = (function() { if (mouse_mode === RED.state.SELECTING_NODE) { return; } + if (activeFlowLocked) { + return + } if (portLabelHover) { portLabelHover.remove(); portLabelHover = null; @@ -2786,6 +2826,7 @@ RED.view = (function() { function detachSelectedNodes() { + if (RED.workspaces.isActiveLocked()) { return } var selection = RED.view.selection(); if (selection.nodes) { const {newLinks, removedLinks} = RED.nodes.detachNodes(selection.nodes); @@ -2927,7 +2968,7 @@ RED.view = (function() { mousedown_node = d; mousedown_port_type = portType; mousedown_port_index = portIndex || 0; - if (mouse_mode !== RED.state.QUICK_JOINING) { + if (mouse_mode !== RED.state.QUICK_JOINING && !activeFlowLocked) { mouse_mode = RED.state.JOINING; document.body.style.cursor = "crosshair"; if (evt.ctrlKey || evt.metaKey) { @@ -3367,6 +3408,11 @@ RED.view = (function() { } if (dblClickPrimed && mousedown_node == d && clickElapsed > 0 && clickElapsed < dblClickInterval) { mouse_mode = RED.state.DEFAULT; + if (RED.workspaces.isActiveLocked()) { + clickElapsed = 0; + d3.event.stopPropagation(); + return + } if (d.type != "subflow") { if (/^subflow:/.test(d.type) && (d3.event.ctrlKey || d3.event.metaKey)) { RED.workspaces.show(d.type.substring(8)); @@ -3690,7 +3736,6 @@ RED.view = (function() { } // selectedLinks.clear(); if (d3.event.button != 2) { - mouse_mode = RED.state.MOVING; var mouse = d3.touches(this)[0]||d3.mouse(this); mouse[0] += d.x-d.w/2; mouse[1] += d.y-d.h/2; @@ -3883,6 +3928,7 @@ RED.view = (function() { if (RED.view.DEBUG) { console.warn("groupMouseUp", { mouse_mode, event: d3.event }); } + if (RED.workspaces.isActiveLocked()) { return } if (dblClickPrimed && mousedown_group == g && clickElapsed > 0 && clickElapsed < dblClickInterval) { mouse_mode = RED.state.DEFAULT; RED.editor.editGroup(g); @@ -4053,7 +4099,7 @@ RED.view = (function() { function isButtonEnabled(d) { var buttonEnabled = true; var ws = RED.nodes.workspace(RED.workspaces.active()); - if (ws && !ws.disabled && !d.d) { + if (ws && !ws.disabled && !d.d && !ws.locked) { if (d._def.button.hasOwnProperty('enabled')) { if (typeof d._def.button.enabled === "function") { buttonEnabled = d._def.button.enabled.call(d); @@ -4076,7 +4122,7 @@ RED.view = (function() { } var activeWorkspace = RED.workspaces.active(); var ws = RED.nodes.workspace(activeWorkspace); - if (ws && !ws.disabled && !d.d) { + if (ws && !ws.disabled && !d.d && !ws.locked) { if (d._def.button.toggle) { d[d._def.button.toggle] = !d[d._def.button.toggle]; d.dirty = true; @@ -4091,7 +4137,7 @@ RED.view = (function() { if (d.dirty) { redraw(); } - } else { + } else if (!ws || !ws.locked){ if (activeSubflow) { RED.notify(RED._("notification.warning", {message:RED._("notification.warnings.nodeActionDisabledSubflow")}),"warning"); } else { @@ -4106,14 +4152,15 @@ RED.view = (function() { function showTouchMenu(obj,pos) { var mdn = mousedown_node; var options = []; - options.push({name:"delete",disabled:(movingSet.length()===0 && selectedLinks.length() === 0),onselect:function() {deleteSelection();}}); - options.push({name:"cut",disabled:(movingSet.length()===0),onselect:function() {copySelection(true);deleteSelection();}}); - options.push({name:"copy",disabled:(movingSet.length()===0),onselect:function() {copySelection();}}); - options.push({name:"paste",disabled:(clipboard.length===0),onselect:function() {importNodes(clipboard, {generateIds: true, touchImport: true});}}); - options.push({name:"edit",disabled:(movingSet.length() != 1),onselect:function() { RED.editor.edit(mdn);}}); + const isActiveLocked = RED.workspaces.isActiveLocked() + options.push({name:"delete",disabled:(isActiveLocked || movingSet.length()===0 && selectedLinks.length() === 0),onselect:function() {deleteSelection();}}); + options.push({name:"cut",disabled:(isActiveLocked || movingSet.length()===0),onselect:function() {copySelection(true);deleteSelection();}}); + options.push({name:"copy",disabled:(isActiveLocked || movingSet.length()===0),onselect:function() {copySelection();}}); + options.push({name:"paste",disabled:(isActiveLocked || clipboard.length===0),onselect:function() {importNodes(clipboard, {generateIds: true, touchImport: true});}}); + options.push({name:"edit",disabled:(isActiveLocked || movingSet.length() != 1),onselect:function() { RED.editor.edit(mdn);}}); options.push({name:"select",onselect:function() {selectAll();}}); options.push({name:"undo",disabled:(RED.history.depth() === 0),onselect:function() {RED.history.pop();}}); - options.push({name:"add",onselect:function() { + options.push({name:"add",disabled:isActiveLocked, onselect:function() { chartPos = chart.offset(); showQuickAddDialog({ position:[pos[0]-chartPos.left+chart.scrollLeft(),pos[1]-chartPos.top+chart.scrollTop()], @@ -5811,6 +5858,9 @@ RED.view = (function() { if (mouse_mode === RED.state.SELECTING_NODE) { return; } + if (activeFlowLocked) { + return + } var workspaceSelection = RED.workspaces.selection(); var changed = false; if (workspaceSelection.length > 0) { diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/workspaces.js b/packages/node_modules/@node-red/editor-client/src/js/ui/workspaces.js index 23171fb5a..79f361a39 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/workspaces.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/workspaces.js @@ -103,6 +103,9 @@ RED.workspaces = (function() { if (workspaceTabCount === 1) { return; } + if (ws.locked) { + return + } var workspaceOrder = RED.nodes.getWorkspaceOrder(); ws._index = workspaceOrder.indexOf(ws.id); removeWorkspace(ws); @@ -123,7 +126,9 @@ RED.workspaces = (function() { RED.editor.editSubflow(subflow); } } else { - RED.editor.editFlow(workspace); + if (!workspace.locked) { + RED.editor.editFlow(workspace); + } } } @@ -148,6 +153,11 @@ RED.workspaces = (function() { let activeWorkspace = tab || RED.nodes.workspace(RED.workspaces.active()) || RED.nodes.subflow(RED.workspaces.active()) let isFlowDisabled = activeWorkspace ? activeWorkspace.disabled : false + let isCurrentLocked = RED.workspaces.isActiveLocked() + if (tab) { + isCurrentLocked = tab.locked + } + var menuItems = [] if (isMenuButton) { menuItems.push({ @@ -188,14 +198,30 @@ RED.workspaces = (function() { shortcut: RED.keyboard.getShortcut("core:enable-flow"), onselect: function() { RED.actions.invoke("core:enable-flow", tab?tab.id:undefined) - } + }, + disabled: isCurrentLocked } : { label: RED._("workspace.disableFlow"), shortcut: RED.keyboard.getShortcut("core:disable-flow"), onselect: function() { RED.actions.invoke("core:disable-flow", tab?tab.id:undefined) + }, + disabled: isCurrentLocked + }, + isCurrentLocked? { + label: RED._("workspace.unlockFlow"), + shortcut: RED.keyboard.getShortcut("core:unlock-flow"), + onselect: function() { + RED.actions.invoke('core:unlock-flow', tab?tab.id:undefined) } - } + } : { + label: RED._("workspace.lockFlow"), + shortcut: RED.keyboard.getShortcut("core:lock-flow"), + onselect: function() { + RED.actions.invoke('core:lock-flow', tab?tab.id:undefined) + } + }, + null ) } const currentTabs = workspace_tabs.listTabs() @@ -239,6 +265,7 @@ RED.workspaces = (function() { } } ) + } menuItems.push( { @@ -264,6 +291,7 @@ RED.workspaces = (function() { null, { label: RED._("common.label.delete"), + disabled: isCurrentLocked, onselect: function() { if (tab.type === 'tab') { RED.workspaces.delete(tab) @@ -302,6 +330,7 @@ RED.workspaces = (function() { activeWorkspace = tab.id; window.location.hash = 'flow/'+tab.id; $("#red-ui-workspace").toggleClass("red-ui-workspace-disabled",!!tab.disabled); + $("#red-ui-workspace").toggleClass("red-ui-workspace-locked",!!tab.locked); } else { $("#red-ui-workspace-chart").hide(); activeWorkspace = 0; @@ -615,7 +644,7 @@ RED.workspaces = (function() { } function setWorkspaceState(id,disabled) { var workspace = RED.nodes.workspace(id||activeWorkspace); - if (!workspace) { + if (!workspace || workspace.locked) { return; } if (workspace.disabled !== disabled) { @@ -695,6 +724,8 @@ RED.workspaces = (function() { } function removeWorkspace(ws) { + if (ws.locked) { return } + if (!ws) { deleteWorkspace(RED.nodes.workspace(activeWorkspace)); } else { @@ -792,6 +823,10 @@ RED.workspaces = (function() { active: function() { return activeWorkspace }, + isActiveLocked: function() { + var ws = RED.nodes.workspace(activeWorkspace) || RED.nodes.subflow(activeWorkspace) + return ws && ws.locked + }, selection: function() { return workspace_tabs.selection(); }, diff --git a/packages/node_modules/@node-red/editor-client/src/sass/flow.scss b/packages/node_modules/@node-red/editor-client/src/sass/flow.scss index be8db6c93..3e5be0645 100644 --- a/packages/node_modules/@node-red/editor-client/src/sass/flow.scss +++ b/packages/node_modules/@node-red/editor-client/src/sass/flow.scss @@ -68,6 +68,9 @@ stroke: var(--red-ui-node-border); cursor: move; stroke-width: 1; + .red-ui-workspace-locked & { + cursor: pointer; + } } .red-ui-workspace-select-mode { g.red-ui-flow-node.red-ui-flow-node-hovered * { @@ -287,9 +290,11 @@ g.red-ui-flow-node-selected { text-anchor:start; } -.red-ui-flow-port-hovered { - stroke: var(--red-ui-port-selected-color); - fill: var(--red-ui-port-selected-color); +#red-ui-workspace:not(.red-ui-workspace-locked) { + .red-ui-flow-port-hovered { + stroke: var(--red-ui-port-selected-color); + fill: var(--red-ui-port-selected-color); + } } .red-ui-flow-subflow-port { diff --git a/packages/node_modules/@node-red/editor-client/src/sass/tab-info.scss b/packages/node_modules/@node-red/editor-client/src/sass/tab-info.scss index ed526b263..f6bf2473c 100644 --- a/packages/node_modules/@node-red/editor-client/src/sass/tab-info.scss +++ b/packages/node_modules/@node-red/editor-client/src/sass/tab-info.scss @@ -514,6 +514,14 @@ div.red-ui-info-table { display: none; } } + // If the parent is locked, do not show the display/action buttons when + // hovering in the outline + .red-ui-info-outline-item-locked .red-ui-info-outline-item & { + .red-ui-info-outline-item-control-disable, + .red-ui-info-outline-item-control-action { + display: none; + } + } button { margin-right: 3px } @@ -531,8 +539,6 @@ div.red-ui-info-table { } } - - .red-ui-icons { display: inline-block; width: 18px; From ce94226c3c82ea505332c9ba01eb6c3de77ed0ac Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Tue, 1 Nov 2022 11:29:23 +0000 Subject: [PATCH 03/11] Disable subflow/flow menu options if active is locked --- .../@node-red/editor-client/src/js/ui/subflow.js | 2 +- .../@node-red/editor-client/src/js/ui/view.js | 4 ++-- .../@node-red/editor-client/src/js/ui/workspaces.js | 12 +++++++----- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/subflow.js b/packages/node_modules/@node-red/editor-client/src/js/ui/subflow.js index de987e468..9ba4cb0bf 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/subflow.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/subflow.js @@ -554,7 +554,7 @@ RED.subflow = (function() { } }); RED.events.on("view:selection-changed",function(selection) { - if (!selection.nodes) { + if (!selection.nodes || RED.workspaces.isActiveLocked()) { RED.menu.setDisabled("menu-item-subflow-convert",true); } else { RED.menu.setDisabled("menu-item-subflow-convert",false); diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/view.js b/packages/node_modules/@node-red/editor-client/src/js/ui/view.js index d783ce941..1520471fd 100755 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/view.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/view.js @@ -423,8 +423,8 @@ RED.view = (function() { } } - RED.menu.setDisabled("menu-item-workspace-edit", activeSubflow || event.workspace === 0); - RED.menu.setDisabled("menu-item-workspace-delete",event.workspace === 0 || RED.workspaces.count() == 1 || activeSubflow); + RED.menu.setDisabled("menu-item-workspace-edit", activeFlowLocked || activeSubflow || event.workspace === 0); + RED.menu.setDisabled("menu-item-workspace-delete",activeFlowLocked || event.workspace === 0 || RED.workspaces.count() == 1 || activeSubflow); if (workspaceScrollPositions[event.workspace]) { chart.scrollLeft(workspaceScrollPositions[event.workspace].left); diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/workspaces.js b/packages/node_modules/@node-red/editor-client/src/js/ui/workspaces.js index 79f361a39..f127b7662 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/workspaces.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/workspaces.js @@ -329,8 +329,8 @@ RED.workspaces = (function() { $("#red-ui-workspace-chart").show(); activeWorkspace = tab.id; window.location.hash = 'flow/'+tab.id; - $("#red-ui-workspace").toggleClass("red-ui-workspace-disabled",!!tab.disabled); - $("#red-ui-workspace").toggleClass("red-ui-workspace-locked",!!tab.locked); + $("#red-ui-workspace").toggleClass("red-ui-workspace-disabled", !!tab.disabled); + $("#red-ui-workspace").toggleClass("red-ui-workspace-locked", !!tab.locked); } else { $("#red-ui-workspace-chart").hide(); activeWorkspace = 0; @@ -724,11 +724,13 @@ RED.workspaces = (function() { } function removeWorkspace(ws) { - if (ws.locked) { return } - if (!ws) { - deleteWorkspace(RED.nodes.workspace(activeWorkspace)); + ws = RED.nodes.workspace(activeWorkspace) + if (ws && !ws.locked) { + deleteWorkspace(RED.nodes.workspace(activeWorkspace)); + } } else { + if (ws.locked) { return } if (workspace_tabs.contains(ws.id)) { workspace_tabs.removeTab(ws.id); } From fe9c630572fe9432fd6de6ff968c5a1905f79e2d Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Tue, 1 Nov 2022 11:42:40 +0000 Subject: [PATCH 04/11] Prevent deleting subflow if instance on locked tab --- packages/node_modules/@node-red/editor-client/src/js/nodes.js | 2 +- .../node_modules/@node-red/editor-client/src/js/ui/subflow.js | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) 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 715ae9c49..bbb6eb82f 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 @@ -616,7 +616,7 @@ RED.nodes = (function() { var subflowId = n.type.substring(8); var sf = RED.nodes.subflow(subflowId); if (sf) { - sf.instances.push(sf); + sf.instances.push(newNode); } n["_"] = RED._; } diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/subflow.js b/packages/node_modules/@node-red/editor-client/src/js/ui/subflow.js index 9ba4cb0bf..110533c1e 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/subflow.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/subflow.js @@ -450,6 +450,9 @@ RED.subflow = (function() { return } if (subflow.instances.length > 0) { + if (subflow.instances.some(sf => { const ws = RED.nodes.workspace(sf.z); return ws?ws.locked:false })) { + return + } const msg = $('
') $('

').text(RED._("subflow.subflowInstances",{count: subflow.instances.length})).appendTo(msg); $('

').text(RED._("subflow.confirmDelete")).appendTo(msg); From 9cb474ea9c6f1c01b8e5c1212bdce453e6fe46f6 Mon Sep 17 00:00:00 2001 From: Hiroyasu Nishiyama Date: Mon, 7 Nov 2022 09:40:36 +0900 Subject: [PATCH 05/11] fix deployment of locked flow --- .../@node-red/editor-client/src/js/ui/deploy.js | 8 ++++++++ 1 file changed, 8 insertions(+) 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 8a8df6837..32b604235 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 @@ -558,6 +558,11 @@ RED.deploy = (function() { 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 isLocked = flow ? flow.locked : false; + if (flow && isLocked) { + flow.locked = false; + } if (node.changed) { node.dirty = true; node.changed = false; @@ -569,6 +574,9 @@ RED.deploy = (function() { if (node.credentials) { delete node.credentials; } + if (flow && isLocked) { + flow.locked = isLocked; + } }); RED.nodes.eachConfig(function (confNode) { confNode.changed = false; From 11ad03b21e12f8d7d4278d0e12007c7dc53d2df7 Mon Sep 17 00:00:00 2001 From: Hiroyasu Nishiyama Date: Mon, 7 Nov 2022 10:42:07 +0900 Subject: [PATCH 06/11] fix to allow es11 for jshint check --- .jshintrc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.jshintrc b/.jshintrc index 719eecb49..0886c1dc0 100644 --- a/.jshintrc +++ b/.jshintrc @@ -15,5 +15,5 @@ "shadow": true, // allow variable shadowing (re-use of names...) "sub": true, // don't warn that foo['bar'] should be written as foo.bar "proto": true, // allow setting of __proto__ in node < v0.12, - "esversion": 6 // allow es6 + "esversion": 11 // allow es11(ES2020) } From f28bc1bff7a269533fb9228a883b2944eb24f09c Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Mon, 7 Nov 2022 21:11:58 +0000 Subject: [PATCH 07/11] Remove jshint warning --- packages/node_modules/@node-red/editor-client/src/js/nodes.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 bbb6eb82f..cdaf0e00a 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 @@ -591,7 +591,7 @@ RED.nodes = (function() { prop === 'z' || prop === 'l' || prop === 'd' || - (prop === 'changed' && !!node.changed !== !!value) || + (prop === 'changed' && (!!node.changed) !== (!!value)) || // jshint ignore:line ((prop === 'x' || prop === 'y') && !node.resize && node.type !== 'group') ) { throw new Error(`Cannot modified property '${prop}' of locked object '${node.type}:${node.id}'`) From 71db79ba53ea3090981fcc13afc975ae1da01be3 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Thu, 17 Nov 2022 12:55:57 +0000 Subject: [PATCH 08/11] More context menu options --- .../@node-red/editor-client/src/js/red.js | 12 +- .../editor-client/src/js/ui/contextMenu.js | 186 +++++++++++------- .../editor-client/src/js/ui/tab-help.js | 16 +- 3 files changed, 137 insertions(+), 77 deletions(-) diff --git a/packages/node_modules/@node-red/editor-client/src/js/red.js b/packages/node_modules/@node-red/editor-client/src/js/red.js index 129c46799..114066d91 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/red.js +++ b/packages/node_modules/@node-red/editor-client/src/js/red.js @@ -668,11 +668,6 @@ var RED = (function() { ]}); menuOptions.push({id:"menu-item-arrange-menu", label:RED._("menu.label.arrange"), options: [ - {id: "menu-item-view-tools-move-to-back", label:RED._("menu.label.moveToBack"), disabled: true, onselect: "core:move-selection-to-back"}, - {id: "menu-item-view-tools-move-to-front", label:RED._("menu.label.moveToFront"), disabled: true, onselect: "core:move-selection-to-front"}, - {id: "menu-item-view-tools-move-backwards", label:RED._("menu.label.moveBackwards"), disabled: true, onselect: "core:move-selection-backwards"}, - {id: "menu-item-view-tools-move-forwards", label:RED._("menu.label.moveForwards"), disabled: true, onselect: "core:move-selection-forwards"}, - null, {id: "menu-item-view-tools-align-left", label:RED._("menu.label.alignLeft"), disabled: true, onselect: "core:align-selection-to-left"}, {id: "menu-item-view-tools-align-center", label:RED._("menu.label.alignCenter"), disabled: true, onselect: "core:align-selection-to-center"}, {id: "menu-item-view-tools-align-right", label:RED._("menu.label.alignRight"), disabled: true, onselect: "core:align-selection-to-right"}, @@ -682,7 +677,12 @@ var RED = (function() { {id: "menu-item-view-tools-align-bottom", label:RED._("menu.label.alignBottom"), disabled: true, onselect: "core:align-selection-to-bottom"}, null, {id: "menu-item-view-tools-distribute-horizontally", label:RED._("menu.label.distributeHorizontally"), disabled: true, onselect: "core:distribute-selection-horizontally"}, - {id: "menu-item-view-tools-distribute-veritcally", label:RED._("menu.label.distributeVertically"), disabled: true, onselect: "core:distribute-selection-vertically"} + {id: "menu-item-view-tools-distribute-veritcally", label:RED._("menu.label.distributeVertically"), disabled: true, onselect: "core:distribute-selection-vertically"}, + null, + {id: "menu-item-view-tools-move-to-back", label:RED._("menu.label.moveToBack"), disabled: true, onselect: "core:move-selection-to-back"}, + {id: "menu-item-view-tools-move-to-front", label:RED._("menu.label.moveToFront"), disabled: true, onselect: "core:move-selection-to-front"}, + {id: "menu-item-view-tools-move-backwards", label:RED._("menu.label.moveBackwards"), disabled: true, onselect: "core:move-selection-backwards"}, + {id: "menu-item-view-tools-move-forwards", label:RED._("menu.label.moveForwards"), disabled: true, onselect: "core:move-selection-forwards"} ]}); menuOptions.push(null); 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 f1b95c324..ef69ea330 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 @@ -30,7 +30,8 @@ RED.contextMenu = (function () { const isGroup = hasSelection && selection.nodes.length === 1 && selection.nodes[0].type === 'group' const canEdit = !RED.workspaces.isActiveLocked() const canRemoveFromGroup = hasSelection && !!selection.nodes[0].g - + const isAllGroups = hasSelection && selection.nodes.filter(n => n.type !== 'group').length === 0 + const hasGroup = hasSelection && selection.nodes.filter(n => n.type === 'group' ).length > 0 const offset = $("#red-ui-workspace-chart").offset() let addX = options.x - offset.left + $("#red-ui-workspace-chart").scrollLeft() @@ -43,64 +44,121 @@ RED.contextMenu = (function () { } menuItems.push( - { onselect: 'core:show-action-list', onpostselect: function () { } }, + { onselect: 'core:show-action-list', onpostselect: function () { } } + ) + + const insertOptions = [] + menuItems.push({ label: RED._("contextMenu.insert"), options: insertOptions }) + insertOptions.push( { - label: RED._("contextMenu.insert"), - options: [ - { - label: RED._("contextMenu.node"), - onselect: function () { - RED.view.showQuickAddDialog({ - position: [addX, addY], - touchTrigger: true, - splice: isSingleLink ? selection.links[0] : undefined, - // spliceMultiple: isMultipleLinks - }) - }, - disabled: !canEdit - }, - (hasLinks) ? { // has least 1 wire selected - label: RED._("contextMenu.junction"), - onselect: 'core:split-wires-with-junctions', - disabled: !canEdit || !hasLinks - } : { - label: RED._("contextMenu.junction"), - onselect: function () { - const nn = { - _def: { defaults: {} }, - type: 'junction', - z: RED.workspaces.active(), - id: RED.nodes.id(), - x: addX, - y: addY, - w: 0, h: 0, - outputs: 1, - inputs: 1, - dirty: true - } - const historyEvent = { - dirty: RED.nodes.dirty(), - t: 'add', - junctions: [nn] - } - RED.nodes.addJunction(nn); - RED.history.push(historyEvent); - RED.nodes.dirty(true); - RED.view.select({nodes: [nn] }); - RED.view.redraw(true) - }, - disabled: !canEdit - }, - { - label: RED._("contextMenu.linkNodes"), - onselect: 'core:split-wire-with-link-nodes', - disabled: !canEdit || !hasLinks - } - ] - - - + label: RED._("contextMenu.node"), + onselect: function () { + RED.view.showQuickAddDialog({ + position: [addX, addY], + touchTrigger: true, + splice: isSingleLink ? selection.links[0] : undefined, + // spliceMultiple: isMultipleLinks + }) + }, + disabled: !canEdit }, + (hasLinks) ? { // has least 1 wire selected + label: RED._("contextMenu.junction"), + onselect: 'core:split-wires-with-junctions', + disabled: !canEdit || !hasLinks + } : { + label: RED._("contextMenu.junction"), + onselect: function () { + const nn = { + _def: { defaults: {} }, + type: 'junction', + z: RED.workspaces.active(), + id: RED.nodes.id(), + x: addX, + y: addY, + w: 0, h: 0, + outputs: 1, + inputs: 1, + dirty: true + } + const historyEvent = { + dirty: RED.nodes.dirty(), + t: 'add', + junctions: [nn] + } + RED.nodes.addJunction(nn); + RED.history.push(historyEvent); + RED.nodes.dirty(true); + RED.view.select({nodes: [nn] }); + RED.view.redraw(true) + }, + disabled: !canEdit + }, + { + label: RED._("contextMenu.linkNodes"), + onselect: 'core:split-wire-with-link-nodes', + disabled: !canEdit || !hasLinks + }, + null, + { onselect: 'core:show-import-dialog', label: RED._('common.label.import')}, + { onselect: 'core:show-examples-import-dialog', label: 'Import Example Flow' } + ) + if (hasSelection && canEdit) { + const nodeOptions = [] + if (!hasMultipleSelection && !isGroup) { + nodeOptions.push( + { onselect: 'core:show-node-help' }, + null + ) + } + nodeOptions.push( + { onselect: 'core:enable-selected-nodes' }, + { onselect: 'core:disable-selected-nodes' }, + null, + { onselect: 'core:show-selected-node-labels' }, + { onselect: 'core:hide-selected-node-labels' } + ) + menuItems.push({ + label: 'Node', + options: nodeOptions + }) + menuItems.push({ + label: 'Group', + options: [ + { onselect: 'core:group-selection' }, + { onselect: 'core:ungroup-selection', disabled: !hasGroup }, + null, + { onselect: 'core:copy-group-style', disabled: !hasGroup }, + { onselect: 'core:paste-group-style', disabled: !hasGroup} + ] + }) + if (canRemoveFromGroup) { + menuItems[menuItems.length - 1].options.push( + null, + { onselect: 'core:remove-selection-from-group', label: RED._("menu.label.groupRemoveSelection") } + ) + } + } + if (canEdit && hasMultipleSelection) { + menuItems.push({ + label: 'Arrange', + options: [ + { label:RED._("menu.label.alignLeft"), onselect: "core:align-selection-to-left"}, + { label:RED._("menu.label.alignCenter"), onselect: "core:align-selection-to-center"}, + { label:RED._("menu.label.alignRight"), onselect: "core:align-selection-to-right"}, + null, + { label:RED._("menu.label.alignTop"), onselect: "core:align-selection-to-top"}, + { label:RED._("menu.label.alignMiddle"), onselect: "core:align-selection-to-middle"}, + { label:RED._("menu.label.alignBottom"), onselect: "core:align-selection-to-bottom"}, + null, + { label:RED._("menu.label.distributeHorizontally"), onselect: "core:distribute-selection-horizontally"}, + { label:RED._("menu.label.distributeVertically"), onselect: "core:distribute-selection-vertically"} + ] + }) + } + + + menuItems.push( null, { onselect: 'core:undo', disabled: RED.history.list().length === 0 }, { onselect: 'core:redo', disabled: RED.history.listRedo().length === 0 }, @@ -109,22 +167,10 @@ RED.contextMenu = (function () { { onselect: 'core:copy-selection-to-internal-clipboard', label: RED._("keyboard.copyNode"), disabled: !hasSelection }, { onselect: 'core:paste-from-internal-clipboard', label: RED._("keyboard.pasteNode"), disabled: !canEdit || !RED.view.clipboard() }, { onselect: 'core:delete-selection', disabled: !canEdit || !canDelete }, + { onselect: 'core:delete-selection-and-reconnect', label: 'Delete and Reconnect', disabled: !canEdit || !canDelete }, { onselect: 'core:show-export-dialog', label: RED._("menu.label.export") }, { onselect: 'core:select-all-nodes' }, ) - - if (hasSelection && canEdit) { - menuItems.push( - null, - isGroup ? - { onselect: 'core:ungroup-selection', disabled: !canEdit || !isGroup } - : { onselect: 'core:group-selection', disabled: !canEdit || !hasSelection } - ) - if (canRemoveFromGroup && canEdit) { - menuItems.push({ onselect: 'core:remove-selection-from-group', label: RED._("menu.label.groupRemoveSelection") }) - } - - } } var direction = "right"; diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/tab-help.js b/packages/node_modules/@node-red/editor-client/src/js/ui/tab-help.js index e5199b5bc..ebd3d4220 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/tab-help.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/tab-help.js @@ -141,7 +141,8 @@ RED.sidebar.help = (function() { RED.events.on('registry:node-type-removed', queueRefresh); RED.events.on('subflows:change', refreshSubflow); - RED.actions.add("core:show-help-tab",show); + RED.actions.add("core:show-help-tab", show); + RED.actions.add("core:show-node-help", showNodeHelp) } @@ -338,6 +339,19 @@ RED.sidebar.help = (function() { resizeStack(); } + function showNodeHelp(node) { + if (!node) { + const selection = RED.view.selection() + if (selection.nodes && selection.nodes.length > 0) { + node = selection.nodes.find(n => n.type !== 'group' && n.type !== 'junction') + } + } + if (node) { + show(node.type, true) + } + } + + // TODO: DRY - projects.js function addTargetToExternalLinks(el) { $(el).find("a").each(function(el) { From 2d6e1d7089583501c95a773d576520e8c637f007 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Sat, 3 Dec 2022 21:29:27 +0000 Subject: [PATCH 09/11] NLS updates for context menu --- .../@node-red/editor-client/locales/en-US/editor.json | 2 ++ .../@node-red/editor-client/src/js/ui/contextMenu.js | 10 +++++----- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json b/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json index e74dbdbaa..6dcbd1d75 100755 --- a/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json +++ b/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json @@ -107,6 +107,7 @@ "displayStatus": "Show node status", "displayConfig": "Configuration nodes", "import": "Import", + "importExample": "Import Example Flow", "export": "Export", "search": "Search flows", "searchInput": "search your flows", @@ -503,6 +504,7 @@ "addRemoveNode": "Add/remove node from selection", "editSelected": "Edit selected node", "deleteSelected": "Delete selected nodes or link", + "deleteReconnect": "Delete and Reconnect", "importNode": "Import nodes", "exportNode": "Export nodes", "nudgeNode": "Move selected nodes (1px)", 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 ef69ea330..57d5bbcf9 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 @@ -101,7 +101,7 @@ RED.contextMenu = (function () { }, null, { onselect: 'core:show-import-dialog', label: RED._('common.label.import')}, - { onselect: 'core:show-examples-import-dialog', label: 'Import Example Flow' } + { onselect: 'core:show-examples-import-dialog', label: RED._('menu.label.importExample') } ) if (hasSelection && canEdit) { const nodeOptions = [] @@ -119,11 +119,11 @@ RED.contextMenu = (function () { { onselect: 'core:hide-selected-node-labels' } ) menuItems.push({ - label: 'Node', + label: RED._('sidebar.info.node'), options: nodeOptions }) menuItems.push({ - label: 'Group', + label: RED._('sidebar.info.group'), options: [ { onselect: 'core:group-selection' }, { onselect: 'core:ungroup-selection', disabled: !hasGroup }, @@ -141,7 +141,7 @@ RED.contextMenu = (function () { } if (canEdit && hasMultipleSelection) { menuItems.push({ - label: 'Arrange', + label: RED._('menu.label.arrange'), options: [ { label:RED._("menu.label.alignLeft"), onselect: "core:align-selection-to-left"}, { label:RED._("menu.label.alignCenter"), onselect: "core:align-selection-to-center"}, @@ -167,7 +167,7 @@ RED.contextMenu = (function () { { onselect: 'core:copy-selection-to-internal-clipboard', label: RED._("keyboard.copyNode"), disabled: !hasSelection }, { onselect: 'core:paste-from-internal-clipboard', label: RED._("keyboard.pasteNode"), disabled: !canEdit || !RED.view.clipboard() }, { onselect: 'core:delete-selection', disabled: !canEdit || !canDelete }, - { onselect: 'core:delete-selection-and-reconnect', label: 'Delete and Reconnect', disabled: !canEdit || !canDelete }, + { onselect: 'core:delete-selection-and-reconnect', label: RED._('keyboard.deleteReconnect'), disabled: !canEdit || !canDelete }, { onselect: 'core:show-export-dialog', label: RED._("menu.label.export") }, { onselect: 'core:select-all-nodes' }, ) From 3630056ed8ad7020299fb39e3f47651503460053 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Sat, 3 Dec 2022 21:44:33 +0000 Subject: [PATCH 10/11] Fix RED.nodes.clear() when handling locked flows --- .../node_modules/@node-red/editor-client/src/js/nodes.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) 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 cdaf0e00a..a7bfc1191 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 @@ -2560,11 +2560,17 @@ RED.nodes = (function() { junctions = {}; junctionsByZ = {}; + var workspaceIds = Object.keys(workspaces); + // Ensure all workspaces are unlocked so we don't get any edit-protection + // preventing removal + workspaceIds.forEach(function(id) { + workspaces[id].locked = false + }); + var subflowIds = Object.keys(subflows); subflowIds.forEach(function(id) { RED.subflow.removeSubflow(id) }); - var workspaceIds = Object.keys(workspaces); workspaceIds.forEach(function(id) { RED.workspaces.remove(workspaces[id]); }); From dce1cccbdeff405a890fc6d8b684d51f138f27f1 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Sat, 3 Dec 2022 22:30:35 +0000 Subject: [PATCH 11/11] Allow subflow to be edited if instance exists on locked flow --- .../@node-red/editor-client/src/js/ui/palette.js | 2 +- .../@node-red/editor-client/src/js/ui/subflow.js | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/palette.js b/packages/node_modules/@node-red/editor-client/src/js/ui/palette.js index ceaa33775..11ec82772 100755 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/palette.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/palette.js @@ -290,7 +290,7 @@ RED.palette = (function() { revertDuration: 200, containment:'#red-ui-main-container', start: function() { - dropEnabled = !RED.nodes.workspace(RED.workspaces.active()).locked; + dropEnabled = !(RED.nodes.workspace(RED.workspaces.active())?.locked); paletteWidth = $("#red-ui-palette").width(); paletteTop = $("#red-ui-palette").parent().position().top + $("#red-ui-palette-container").position().top; hoverGroup = null; diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/subflow.js b/packages/node_modules/@node-red/editor-client/src/js/ui/subflow.js index 110533c1e..106e1a72a 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/subflow.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/subflow.js @@ -273,6 +273,11 @@ RED.subflow = (function() { var subflowInstances = []; if (activeSubflow) { RED.nodes.filterNodes({type:"subflow:"+activeSubflow.id}).forEach(function(n) { + const parentFlow = RED.nodes.workspace(n.z) + const wasLocked = parentFlow && parentFlow.locked + if (wasLocked) { + parentFlow.locked = false + } subflowInstances.push({ id: n.id, changed: n.changed @@ -285,6 +290,9 @@ RED.subflow = (function() { n.resize = true; n.dirty = true; RED.editor.updateNodeProperties(n); + if (wasLocked) { + parentFlow.locked = true + } }); RED.editor.validateNode(activeSubflow); return {