From a5b33d11fc378471ad9c7bbc2d1133b935550920 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Mon, 27 Apr 2020 11:17:19 +0100 Subject: [PATCH] [outline] Add outline section to info sidebar --- Gruntfile.js | 1 + .../editor-client/locales/en-US/editor.json | 3 +- .../src/js/ui/tab-info-outliner.js | 426 ++++++++++++++++++ .../editor-client/src/js/ui/tab-info.js | 8 + .../editor-client/src/js/ui/utils.js | 4 +- .../editor-client/src/sass/search.scss | 4 +- .../editor-client/src/sass/tab-info.scss | 170 +++++++ 7 files changed, 613 insertions(+), 3 deletions(-) create mode 100644 packages/node_modules/@node-red/editor-client/src/js/ui/tab-info-outliner.js diff --git a/Gruntfile.js b/Gruntfile.js index b6474182a..b39256d88 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -165,6 +165,7 @@ module.exports = function(grunt) { "packages/node_modules/@node-red/editor-client/src/js/ui/sidebar.js", "packages/node_modules/@node-red/editor-client/src/js/ui/palette.js", "packages/node_modules/@node-red/editor-client/src/js/ui/tab-info.js", + "packages/node_modules/@node-red/editor-client/src/js/ui/tab-info-outliner.js", "packages/node_modules/@node-red/editor-client/src/js/ui/tab-config.js", "packages/node_modules/@node-red/editor-client/src/js/ui/tab-context.js", "packages/node_modules/@node-red/editor-client/src/js/ui/palette-editor.js", 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 f67a3de9a..7567ada3a 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 @@ -583,7 +583,8 @@ "nodeHelp": "Node Help", "none":"None", "arrayItems": "__count__ items", - "showTips":"You can open the tips from the settings panel" + "showTips":"You can open the tips from the settings panel", + "outline": "Outline" }, "config": { "name": "Configuration nodes", 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 new file mode 100644 index 000000000..82d3ce1c0 --- /dev/null +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/tab-info-outliner.js @@ -0,0 +1,426 @@ +RED.sidebar.info.outliner = (function() { + + var treeList; + + var flowList; + var subflowList; + var globalConfigNodes; + + var objects = {}; + + var objectBacklog = {}; + + function getFlowData(project) { + var flowData = [ + { + label: "Flows", + expanded: true, + children: [ + { + id: "__global__", + label: "Global", + children: [] + } + ] + }, + { + label: "Subflows", + children: [] + } + ] + flowList = flowData[0]; + subflowList = flowData[1]; + globalConfigNodes = flowList.children[0]; + + if (project) { + flowData = [ + { + element: getProjectLabel(project), + icon: "fa fa-archive", + children: flowData, + expanded: true + } + ] + } + return flowData; + } + + function getProjectLabel(p) { + var div = $('
',{class:"red-ui-info-outline-item red-ui-info-outline-item-flow"}); + var contentDiv = $('
',{class:"red-ui-search-result-description red-ui-info-outline-item-label"}).appendTo(div); + contentDiv.text(p.name); + var controls = $('
',{class:"red-ui-info-outline-item-controls"}).appendTo(div); + var editProjectButton = $('') + .appendTo(controls) + .on("click", function(evt) { + evt.preventDefault(); + RED.projects.editProject(); + }); + RED.popover.tooltip(editProjectButton,RED._('sidebar.project.showProjectSettings')); + return div; + } + + var empties = {}; + function getEmptyItem(id) { + var item = { + empty: true, + element: $('
').text("empty") + } + empties[id] = item; + return item; + } + + function getNodeLabelText(n) { + var label = n.name || n.id; + if (n._def.label) { + try { + label = (typeof n._def.label === "function" ? n._def.label.call(n) : n._def.label)||""; + } catch(err) { + console.log("Definition error: "+type+".label",err); + } + } + return label; + } + + function getNodeLabel(n) { + var div = $('
',{class:"red-ui-info-outline-item"}); + var nodeDiv = $('
',{class:"red-ui-search-result-node"}).appendTo(div); + if (n.type === "group") { + div.addClass('red-ui-info-outline-item-group') + } else { + var colour = RED.utils.getNodeColor(n.type,n._def); + nodeDiv.css('backgroundColor',colour); + } + var icon_url = RED.utils.getNodeIcon(n._def,n); + var iconContainer = $('
',{class:"red-ui-palette-icon-container"}).appendTo(nodeDiv); + RED.utils.createIconElement(icon_url, iconContainer, false); + var contentDiv = $('
',{class:"red-ui-search-result-description"}).appendTo(div); + var labelText = getNodeLabelText(n); + var label = $('
',{class:"red-ui-search-result-node-label red-ui-info-outline-item-label"}).appendTo(contentDiv); + if (labelText) { + label.text(labelText) + } else { + label.html(" ") + } + + addControls(n, div); + + return div; + } + + function getFlowLabel(n) { + var div = $('
',{class:"red-ui-info-outline-item red-ui-info-outline-item-flow"}); + var contentDiv = $('
',{class:"red-ui-search-result-description red-ui-info-outline-item-label"}).appendTo(div); + contentDiv.text(typeof n === "string"? n : n.label); + addControls(n, div); + return div; + } + + function getSubflowLabel(n) { + + var div = $('
',{class:"red-ui-info-outline-item"}); + var nodeDiv = $('
',{class:"red-ui-search-result-node"}).appendTo(div); + if (n.type === "group") { + div.addClass('red-ui-info-outline-item-group') + } else { + var colour = RED.utils.getNodeColor(n.type,n._def); + nodeDiv.css('backgroundColor',colour); + } + var icon_url = RED.utils.getNodeIcon(n._def,n); + var iconContainer = $('
',{class:"red-ui-palette-icon-container"}).appendTo(nodeDiv); + RED.utils.createIconElement(icon_url, iconContainer, false); + var contentDiv = $('
',{class:"red-ui-search-result-description"}).appendTo(div); + var labelText = getNodeLabelText(n); + var label = $('
',{class:"red-ui-search-result-node-label red-ui-info-outline-item-label"}).appendTo(contentDiv); + if (labelText) { + label.text(labelText) + } else { + label.html(" ") + } + + addControls(n, div); + + return div; + + + // var div = $('
',{class:"red-ui-info-outline-item red-ui-info-outline-item-flow"}); + // var contentDiv = $('
',{class:"red-ui-search-result-description red-ui-info-outline-item-label"}).appendTo(div); + // contentDiv.text(n.name || n.id); + // addControls(n, div); + // return div; + } + + function addControls(n,div) { + if (n.type === 'group') { + return; + } + var controls = $('
',{class:"red-ui-info-outline-item-controls red-ui-info-outline-item-hover-controls"}).appendTo(div); + if (n._def.button) { + $('').appendTo(controls).on("click",function(evt) { + evt.preventDefault(); + evt.stopPropagation(); + RED.view.clickNodeButton(n); + }) + } + + + if (n.type !== 'group') { + $('').appendTo(controls).on("click",function(evt) { + evt.preventDefault(); + evt.stopPropagation(); + RED.view.reveal(n.id); + }) + } + if (n.type !== 'group' && n.type !== 'subflow') { + $('').appendTo(controls).on("click",function(evt) { + evt.preventDefault(); + evt.stopPropagation(); + if (n.type === 'tab') { + if (n.disabled) { + RED.workspaces.enable(n.id) + } else { + RED.workspaces.disable(n.id) + } + } else { + // TODO: this ought to be a utility function in RED.nodes + var historyEvent = { + t: "edit", + node: n, + changed: n.changed, + changes: { + d: n.d + }, + dirty:RED.nodes.dirty() + } + if (n.d) { + delete n.d; + } else { + n.d = true; + } + n.dirty = true; + n.changed = true; + RED.events.emit("nodes:change",n); + RED.nodes.dirty(true) + RED.view.redraw(); + } + }); + } else { + $('
').appendTo(controls) + } + controls.find("button").on("dblclick", function(evt) { + evt.preventDefault(); + evt.stopPropagation(); + }) + } + + function onProjectLoad(activeProject) { + var newFlowData = getFlowData(activeProject); + treeList.treeList('data',newFlowData); + } + + function build() { + var container = $("
", {class:"red-ui-info-outline"}).css({'height': '400px'}); + var toolbar = $("
", {class:"red-ui-info-outline-toolbar"}).appendTo(container); + + var searchInput = $('').appendTo(toolbar).searchBox({ + delay: 100, + change: function() { + var val = $(this).val().trim().toLowerCase(); + if (val) { + var c = treeList.treeList('filter',function(item) { + if (item.depth === 0) { + return true; + } + if (item.id && objects[item.id]) { + var l = ((objects[item.id].type||"")+" "+(objects[item.id].name||"")+" "+(objects[item.id].id||"")+" "+(objects[item.id].label||"")).toLowerCase(); + var isMatch = l.indexOf(val) > -1; + if (isMatch) { + return true; + } + } + return false; + }) + } else { + treeList.treeList('filter',null); + + } + } + }); + + treeList = $("
").css({width: "100%"}).appendTo(container).treeList({ + data:getFlowData() + }) + // treeList.on('treelistselect', function(e,item) { + // console.log(item) + // RED.view.reveal(item.id); + // }) + // treeList.treeList('data',[ ... ] ) + treeList.on('treelistconfirm', function(e,item) { + var node = RED.nodes.node(item.id); + if (node) { + if (node._def.category === "config") { + RED.editor.editConfig("", node.type, node.id); + } else { + RED.editor.edit(node); + } + } + }) + + RED.events.on("projects:load", onProjectLoad) + + RED.events.on("flows:add", onFlowAdd) + RED.events.on("flows:remove", onObjectRemove) + RED.events.on("flows:change", onFlowChange) + RED.events.on("flows:reorder", onFlowsReorder) + + RED.events.on("subflows:add", onSubflowAdd) + RED.events.on("subflows:remove", onObjectRemove) + RED.events.on("subflows:change", onSubflowChange) + + RED.events.on("nodes:add",onNodeAdd); + RED.events.on("nodes:remove",onObjectRemove); + RED.events.on("nodes:change",onNodeChange); + + RED.events.on("groups:add",onNodeAdd); + RED.events.on("groups:remove",onObjectRemove); + RED.events.on("groups:change",onNodeChange); + + RED.events.on("view:selection-changed", onSelectionChanged); + + + // ["links","nodes","flows","subflows","groups"].forEach(function(t) { + // ["add","remove","change"].forEach(function(v) { + // RED.events.on(t+":"+v, function(n) { console.log(t+":"+v,n)}) + // }) + // }) + // RED.events.on("workspace:clear", function() { console.log("workspace:clear")}) + + return container; + } + function onFlowAdd(ws) { + objects[ws.id] = { + id: ws.id, + element: getFlowLabel(ws), + children:[getEmptyItem(ws.id)], + deferBuild: true, + icon: "red-ui-icons red-ui-icons-flow" + } + 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) + + + } + function onFlowChange(n) { + var existingObject = objects[n.id]; + existingObject.element.find(".red-ui-info-outline-item-label").text(n.label || n.id); + existingObject.element.toggleClass("red-ui-info-outline-item-disabled", !!n.disabled) + existingObject.treeList.container.toggleClass("red-ui-info-outline-item-disabled", !!n.disabled) + } + function onFlowsReorder(order) { + var indexMap = {}; + order.forEach(function(id,index) { + indexMap[id] = index; + }) + + flowList.treeList.sortChildren(function(A,B) { + if (A.id === "__global__") { return -1 } + if (B.id === "__global__") { return 1 } + return indexMap[A.id] - indexMap[B.id] + }) + } + function onSubflowAdd(sf) { + objects[sf.id] = { + id: sf.id, + element: getSubflowLabel(sf), + children:[getEmptyItem(sf.id)], + deferBuild: true + } + subflowList.treeList.addChild(objects[sf.id]) + } + function onSubflowChange(n) { + var existingObject = objects[n.id]; + + existingObject.treeList.replaceElement(getSubflowLabel(n)); + // existingObject.element.find(".red-ui-info-outline-item-label").text(n.name || n.id); + + } + + function onNodeChange(n) { + var existingObject = objects[n.id]; + var parent = n.g||n.z; + + var nodeLabelText = getNodeLabelText(n); + if (nodeLabelText) { + existingObject.element.find(".red-ui-info-outline-item-label").text(nodeLabelText); + } else { + existingObject.element.find(".red-ui-info-outline-item-label").html(" "); + } + + if (parent !== existingObject.parent.id) { + existingObject.treeList.remove(); + if (!parent) { + globalConfigNodes.treeList.addChild(existingObject); + } else { + objects[parent].treeList.addChild(existingObject) + } + } + existingObject.element.toggleClass("red-ui-info-outline-item-disabled", !!n.d) + } + function onObjectRemove(n) { + var existingObject = objects[n.id]; + existingObject.treeList.remove(); + delete objects[n.d] + var parent = existingObject.parent; + if (parent.children.length === 0) { + parent.treeList.addChild(getEmptyItem(parent.id)); + } + } + + function onNodeAdd(n) { + objects[n.id] = { + id: n.id, + element: getNodeLabel(n) + } + if (n.type === "group") { + objects[n.id].children = []; + objects[n.id].deferBuild = true; + } + var parent = n.g||n.z; + if (parent) { + if (objects[parent]) { + if (empties[parent]) { + empties[parent].treeList.remove(); + delete empties[parent]; + } + objects[parent].treeList.addChild(objects[n.id]) + } else { + // The parent hasn't been added yet + console.log("missing",parent) + } + } else { + // No parent - add to Global flow list + globalConfigNodes.treeList.addChild(objects[n.id]) + } + objects[n.id].element.toggleClass("red-ui-info-outline-item-disabled", !!n.d) + } + + function onSelectionChanged(selection) { + // treeList.treeList('clearSelection'); + // console.log(selection); + if (selection.nodes) { + selection.nodes.forEach(function(n) { + // console.log("..",n.id); + treeList.treeList('show',n.id); + if (objects[n.id].treeList) { + objects[n.id].treeList.select(true); + } + + }); + } + } + + return { + build: build + } +})(); diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/tab-info.js b/packages/node_modules/@node-red/editor-client/src/js/ui/tab-info.js index 39683e41e..c6e9aec52 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/tab-info.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/tab-info.js @@ -18,6 +18,7 @@ RED.sidebar.info = (function() { var content; var sections; var propertiesSection; + var outlinerSection; var infoSection; var helpSection; var tipBox; @@ -39,6 +40,13 @@ RED.sidebar.info = (function() { container: stackContainer }).hide(); + outlinerSection = sections.add({ + title: RED._("sidebar.info.outline"), + collapsible: true + }) + outlinerSection.expand(); + RED.sidebar.info.outliner.build().appendTo(outlinerSection.content); + propertiesSection = sections.add({ title: RED._("sidebar.info.info"), collapsible: true diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/utils.js b/packages/node_modules/@node-red/editor-client/src/js/ui/utils.js index a9c1c9500..7e3d72b89 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/utils.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/utils.js @@ -859,7 +859,9 @@ RED.utils = (function() { } function getNodeIcon(def,node) { - if (def.category === 'config') { + if (node && node.type === 'group') { + return "font-awesome/fa-object-group" + } else if (def.category === 'config') { return RED.settings.apiRootUrl+"icons/node-red/cog.svg" } else if (node && node.type === 'tab') { return RED.settings.apiRootUrl+"icons/node-red/subflow.svg" diff --git a/packages/node_modules/@node-red/editor-client/src/sass/search.scss b/packages/node_modules/@node-red/editor-client/src/sass/search.scss index 0ec8b6525..273793896 100644 --- a/packages/node_modules/@node-red/editor-client/src/sass/search.scss +++ b/packages/node_modules/@node-red/editor-client/src/sass/search.scss @@ -87,6 +87,8 @@ } .red-ui-palette-icon { width: 15px; + position:relative; + left: -1px; } .red-ui-search-result-description { margin-left:28px; @@ -153,7 +155,7 @@ width: 30px; float:left; height: 25px; - border-radius: 5px; + border-radius: 3px; border: 1px solid $node-border; background-position: 5% 50%; background-repeat: no-repeat; 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 bc72f7532..1605c9c76 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 @@ -279,3 +279,173 @@ div.red-ui-info-table { border-radius: 4px; padding: 2px 4px 2px; } + +.red-ui-help-search { + border-bottom: 1px solid $secondary-border-color; +} + + +.red-ui-info-outline { + display: flex; + flex-direction: column; + + .red-ui-treeList { + flex-grow: 1; + } + + .red-ui-treeList-container,.red-ui-editableList-border { + border: none; + border-radius: 0; + } + .red-ui-treeList-label { + font-size: 13px; + padding: 2px 0; + overflow: hidden; + } + .red-ui-info-outline-item { + display: inline-block; + padding: 0; + font-size: 13px; + border: none; + .red-ui-palette-icon-fa { + position: relative; + top: 1px; + left: 0px; + } + &:hover { + background: inherit + } + + &.red-ui-info-outline-item-flow { + .red-ui-search-result-description { + margin-left: 4px; + } + } + &.red-ui-info-outline-item-group .red-ui-search-result-node { + background: none; + border-color: transparent; + .red-ui-palette-icon-container { + background: none; + } + .red-ui-palette-icon-fa { + color: $secondary-text-color; + font-size: 18px; + } + } + &.red-ui-info-outline-item-empty { + font-style: italic; + color: $form-placeholder-color; + } + } + + .red-ui-search-result-node { + width: 24px; + height: 20px; + margin-top: 1px; + } + + .red-ui-palette-icon-container { + width: 24px; + } + .red-ui-palette-icon { + width: 20px; + } + .red-ui-search-result-description { + margin-left: 32px; + line-height: 22px; + white-space: nowrap; + } + .red-ui-search-result-node-label { + color: $secondary-text-color; + } +} +.red-ui-info-outline-item-control-spacer { + display: inline-block; + width: 23px; +} + +.red-ui-info-outline-item-controls { + position: absolute; + top:0; + bottom: 0; + right: 0px; + padding: 2px 0 0 1px; + text-align: right; + background: $list-item-background; + + .red-ui-treeList-label:hover & { + background: $list-item-background-hover; + } + .red-ui-treeList-label.selected & { + background: $list-item-background-selected; + } + + + &.red-ui-info-outline-item-hover-controls button { + min-width: 23px; + } + + .red-ui-treeList-label:not(:hover) &.red-ui-info-outline-item-hover-controls { + button { + border: none; + background: none; + } + } + .red-ui-info-outline-item-control-reveal, + .red-ui-info-outline-item-control-action { + display: none; + } + .red-ui-treeList-label:hover & { + .red-ui-info-outline-item-control-reveal, + .red-ui-info-outline-item-control-action { + display: inline-block; + } + } + + .fa-ban { + display: none; + } + .red-ui-info-outline-item.red-ui-info-outline-item-disabled & { + .fa-ban { + display: inline-block; + } + .fa-circle-thin { + display: none; + } + } + button { + margin-right: 3px + } +} +.red-ui-info-outline-item-disabled { + .red-ui-search-result-node { + opacity: 0.4; + } + .red-ui-info-outline-item-label { + font-style: italic; + color: $secondary-text-color-disabled; + } + .red-ui-icons-flow { + opacity: 0.4; + } +} +.red-ui-icons { + display: inline-block; + width: 18px; + &:before { + white-space: pre; + content: ' ' + } + +} + +.red-ui-icons-flow { + background-image: url('images/subflow_tab.svg'); + background-repeat: no-repeat; + background-size: contain; + filter: brightness(2.5); +} + +.red-ui-info-outline-toolbar { + border-bottom: 1px solid $secondary-border-color; +}