diff --git a/Gruntfile.js b/Gruntfile.js index 4f7e390c4..0dc1fda52 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -52,10 +52,12 @@ module.exports = function(grunt) { timeout: 3000, ignoreLeaks: false, ui: 'bdd', - reportFormats: ['lcov'], + reportFormats: ['lcov','html'], print: 'both' }, - coverage: { src: ['test/**/*_spec.js'] } + all: { src: ['test/**/*_spec.js'] }, + core: { src: ["test/_spec.js","test/red/**/*_spec.js"]}, + nodes: { src: ["test/nodes/**/*_spec.js"]} }, jshint: { options: { @@ -156,6 +158,10 @@ module.exports = function(grunt) { "editor/js/ui/typeSearch.js", "editor/js/ui/subflow.js", "editor/js/ui/userSettings.js", + "editor/js/ui/projects/projects.js", + "editor/js/ui/projects/projectSettings.js", + "editor/js/ui/projects/projectUserSettings.js", + "editor/js/ui/projects/tab-versionControl.js", "editor/js/ui/touch/radialMenu.js" ], dest: "public/red/red.js" @@ -472,7 +478,7 @@ module.exports = function(grunt) { grunt.registerTask('test-core', 'Runs code style check and unit tests on core runtime code', - ['jshint:core','simplemocha:core']); + ['build','mocha_istanbul:core']); grunt.registerTask('test-editor', 'Runs code style check on editor code', @@ -484,7 +490,7 @@ module.exports = function(grunt) { grunt.registerTask('test-nodes', 'Runs unit tests on core nodes', - ['simplemocha:nodes']); + ['build','mocha_istanbul:nodes']); grunt.registerTask('build', 'Builds editor content', @@ -500,5 +506,5 @@ module.exports = function(grunt) { grunt.registerTask('coverage', 'Run Istanbul code test coverage task', - ['build','mocha_istanbul']); + ['build','mocha_istanbul:all']); }; diff --git a/editor/js/history.js b/editor/js/history.js index 98c2ca918..ac155bd88 100644 --- a/editor/js/history.js +++ b/editor/js/history.js @@ -321,6 +321,9 @@ RED.history = (function() { }, peek: function() { return undo_history[undo_history.length-1]; + }, + clear: function() { + undo_history = []; } } diff --git a/editor/js/keymap.json b/editor/js/keymap.json index b60372b42..4b7eb6b35 100644 --- a/editor/js/keymap.json +++ b/editor/js/keymap.json @@ -13,7 +13,11 @@ "ctrl-e": "core:show-export-dialog", "ctrl-i": "core:show-import-dialog", "ctrl-space": "core:toggle-sidebar", - "ctrl-,": "core:show-user-settings" + "ctrl-,": "core:show-user-settings", + + "ctrl-alt-n": "core:new-project", + "ctrl-alt-o": "core:open-project", + "ctrl-g v": "core:show-version-control-tab" }, "workspace": { "backspace": "core:delete-selection", diff --git a/editor/js/main.js b/editor/js/main.js index 3cf8ca549..542e857bc 100644 --- a/editor/js/main.js +++ b/editor/js/main.js @@ -60,12 +60,32 @@ $("#palette > .palette-spinner").hide(); $(".palette-scroll").removeClass("hide"); $("#palette-search").removeClass("hide"); - loadFlows(); + loadFlows(function() { + if (RED.settings.theme("projects.enabled",false)) { + RED.projects.refresh(function(activeProject) { + RED.sidebar.info.refresh() + if (!activeProject) { + // Projects enabled but no active project + RED.menu.setDisabled('menu-item-projects-open',true); + if (activeProject === false) { + // User previously decline the migration to projects. + } else { // null/undefined + RED.projects.showStartup(); + } + } + completeLoad(); + }); + } else { + // Projects disabled by the user + RED.sidebar.info.refresh() + completeLoad(); + } + }); } }); } - function loadFlows() { + function loadFlows(done) { $.ajax({ headers: { "Accept":"application/json", @@ -73,116 +93,218 @@ cache: false, url: 'flows', success: function(nodes) { - var currentHash = window.location.hash; - RED.nodes.version(nodes.rev); - RED.nodes.import(nodes.flows); - RED.nodes.dirty(false); - RED.view.redraw(true); - if (/^#flow\/.+$/.test(currentHash)) { - RED.workspaces.show(currentHash.substring(6)); + if (nodes) { + var currentHash = window.location.hash; + RED.nodes.version(nodes.rev); + RED.nodes.import(nodes.flows); + RED.nodes.dirty(false); + RED.view.redraw(true); + if (/^#flow\/.+$/.test(currentHash)) { + RED.workspaces.show(currentHash.substring(6)); + } } - - var persistentNotifications = {}; - RED.comms.subscribe("notification/#",function(topic,msg) { - var parts = topic.split("/"); - var notificationId = parts[1]; - if (notificationId === "runtime-deploy") { - // handled in ui/deploy.js - return; - } - if (notificationId === "node") { - // handled below - return; - } - if (msg.text) { - var text = RED._(msg.text,{default:msg.text}); - if (!persistentNotifications.hasOwnProperty(notificationId)) { - persistentNotifications[notificationId] = RED.notify(text,msg.type,msg.timeout === undefined,msg.timeout); - } else { - persistentNotifications[notificationId].update(text,msg.timeout); - } - } else if (persistentNotifications.hasOwnProperty(notificationId)) { - persistentNotifications[notificationId].close(); - delete persistentNotifications[notificationId]; - } - }); - RED.comms.subscribe("status/#",function(topic,msg) { - var parts = topic.split("/"); - var node = RED.nodes.node(parts[1]); - if (node) { - if (msg.hasOwnProperty("text")) { - if (msg.text[0] !== ".") { - msg.text = node._(msg.text.toString(),{defaultValue:msg.text.toString()}); - } - } - node.status = msg; - node.dirty = true; - RED.view.redraw(); - } - }); - RED.comms.subscribe("notification/node/#",function(topic,msg) { - var i,m; - var typeList; - var info; - if (topic == "notification/node/added") { - var addedTypes = []; - msg.forEach(function(m) { - var id = m.id; - RED.nodes.addNodeSet(m); - addedTypes = addedTypes.concat(m.types); - RED.i18n.loadCatalog(id, function() { - $.get('nodes/'+id, function(data) { - $("body").append(data); - }); - }); - }); - if (addedTypes.length) { - typeList = ""; - RED.notify(RED._("palette.event.nodeAdded", {count:addedTypes.length})+typeList,"success"); - } - loadIconList(); - } else if (topic == "notification/node/removed") { - for (i=0;i
  • ")+"
  • "; - RED.notify(RED._("palette.event.nodeRemoved", {count:m.types.length})+typeList,"success"); - } - } - loadIconList(); - } else if (topic == "notification/node/enabled") { - if (msg.types) { - info = RED.nodes.getNodeSet(msg.id); - if (info.added) { - RED.nodes.enableNodeSet(msg.id); - typeList = ""; - RED.notify(RED._("palette.event.nodeEnabled", {count:msg.types.length})+typeList,"success"); - } else { - $.get('nodes/'+msg.id, function(data) { - $("body").append(data); - typeList = ""; - RED.notify(RED._("palette.event.nodeAdded", {count:msg.types.length})+typeList,"success"); - }); - } - } - } else if (topic == "notification/node/disabled") { - if (msg.types) { - RED.nodes.disableNodeSet(msg.id); - typeList = ""; - RED.notify(RED._("palette.event.nodeDisabled", {count:msg.types.length})+typeList,"success"); - } - } else if (topic == "node/upgraded") { - RED.notify(RED._("palette.event.nodeUpgraded", {module:msg.module,version:msg.version}),"success"); - RED.nodes.registry.setModulePendingUpdated(msg.module,msg.version); - } - // Refresh flow library to ensure any examples are updated - RED.library.loadFlowLibrary(); - }); + done(); } }); } + function completeLoad() { + var persistentNotifications = {}; + RED.comms.subscribe("notification/#",function(topic,msg) { + var parts = topic.split("/"); + var notificationId = parts[1]; + if (notificationId === "runtime-deploy") { + // handled in ui/deploy.js + return; + } + if (notificationId === "node") { + // handled below + return; + } + if (notificationId === "project-update") { + RED.nodes.clear(); + RED.history.clear(); + RED.view.redraw(true); + RED.projects.refresh(function() { + loadFlows(function() { + var project = RED.projects.getActiveProject(); + var message = { + "change-branch":"Change to local branch '"+project.git.branches.local+"'", + "abort-merge":"Git merge aborted", + "loaded":"Project '"+msg.project+"' loaded", + "updated":"Project '"+msg.project+"' updated", + "pull":"Project '"+msg.project+"' reloaded", + "revert": "Project '"+msg.project+"' reloaded" + }[msg.action]; + RED.notify("

    "+message+"

    "); + RED.sidebar.info.refresh() + }); + }); + return; + } + + if (msg.text) { + msg.default = msg.text; + var text = RED._(msg.text,msg); + var options = { + type: msg.type, + fixed: msg.timeout === undefined, + timeout: msg.timeout, + id: notificationId + } + if (notificationId === "runtime-state") { + if (msg.error === "missing-types") { + text+=""; + if (!!RED.projects.getActiveProject()) { + options.buttons = [ + { + text: "Manage project dependencies", + click: function() { + persistentNotifications[notificationId].hideNotification(); + RED.projects.settings.show('deps'); + } + } + ] + // } else if (RED.settings.theme('palette.editable') !== false) { + } else { + options.buttons = [ + { + text: "Close", + click: function() { + persistentNotifications[notificationId].hideNotification(); + } + } + ] + } + } else if (msg.error === "credentials_load_failed") { + if (RED.user.hasPermission("projects.write")) { + options.buttons = [ + { + text: "Setup credentials", + click: function() { + persistentNotifications[notificationId].hideNotification(); + RED.projects.showCredentialsPrompt(); + } + } + ] + } + } else if (msg.error === "missing_flow_file") { + if (RED.user.hasPermission("projects.write")) { + options.buttons = [ + { + text: "Setup project files", + click: function() { + persistentNotifications[notificationId].hideNotification(); + RED.projects.showFilesPrompt(); + } + } + ] + } + } else if (msg.error === "project_empty") { + if (RED.user.hasPermission("projects.write")) { + options.buttons = [ + { + text: "No thanks", + click: function() { + persistentNotifications[notificationId].hideNotification(); + } + }, + { + text: "Create default project files", + click: function() { + persistentNotifications[notificationId].hideNotification(); + RED.projects.createDefaultFileSet(); + } + } + ] + } + } + } + if (!persistentNotifications.hasOwnProperty(notificationId)) { + persistentNotifications[notificationId] = RED.notify(text,options); + } else { + persistentNotifications[notificationId].update(text,options); + } + } else if (persistentNotifications.hasOwnProperty(notificationId)) { + persistentNotifications[notificationId].close(); + delete persistentNotifications[notificationId]; + } + }); + RED.comms.subscribe("status/#",function(topic,msg) { + var parts = topic.split("/"); + var node = RED.nodes.node(parts[1]); + if (node) { + if (msg.hasOwnProperty("text")) { + if (msg.text[0] !== ".") { + msg.text = node._(msg.text.toString(),{defaultValue:msg.text.toString()}); + } + } + node.status = msg; + node.dirty = true; + RED.view.redraw(); + } + }); + RED.comms.subscribe("notification/node/#",function(topic,msg) { + var i,m; + var typeList; + var info; + if (topic == "notification/node/added") { + var addedTypes = []; + msg.forEach(function(m) { + var id = m.id; + RED.nodes.addNodeSet(m); + addedTypes = addedTypes.concat(m.types); + RED.i18n.loadCatalog(id, function() { + $.get('nodes/'+id, function(data) { + $("body").append(data); + }); + }); + }); + if (addedTypes.length) { + typeList = ""; + RED.notify(RED._("palette.event.nodeAdded", {count:addedTypes.length})+typeList,"success"); + } + loadIconList(); + } else if (topic == "notification/node/removed") { + for (i=0;i
  • ")+"
  • "; + RED.notify(RED._("palette.event.nodeRemoved", {count:m.types.length})+typeList,"success"); + } + } + loadIconList(); + } else if (topic == "notification/node/enabled") { + if (msg.types) { + info = RED.nodes.getNodeSet(msg.id); + if (info.added) { + RED.nodes.enableNodeSet(msg.id); + typeList = ""; + RED.notify(RED._("palette.event.nodeEnabled", {count:msg.types.length})+typeList,"success"); + } else { + $.get('nodes/'+msg.id, function(data) { + $("body").append(data); + typeList = ""; + RED.notify(RED._("palette.event.nodeAdded", {count:msg.types.length})+typeList,"success"); + }); + } + } + } else if (topic == "notification/node/disabled") { + if (msg.types) { + RED.nodes.disableNodeSet(msg.id); + typeList = ""; + RED.notify(RED._("palette.event.nodeDisabled", {count:msg.types.length})+typeList,"success"); + } + } else if (topic == "node/upgraded") { + RED.notify(RED._("palette.event.nodeUpgraded", {module:msg.module,version:msg.version}),"success"); + RED.nodes.registry.setModulePendingUpdated(msg.module,msg.version); + } + // Refresh flow library to ensure any examples are updated + RED.library.loadFlowLibrary(); + }); + } + function showAbout() { $.get('red/about', function(data) { var aboutHeader = '
    '+ @@ -196,6 +318,14 @@ function loadEditor() { var menuOptions = []; + if (RED.settings.theme("projects.enabled",false)) { + menuOptions.push({id:"menu-item-projects-menu",label:"Projects",options:[ + {id:"menu-item-projects-new",label:"New...",disabled:false,onselect:"core:new-project"}, + {id:"menu-item-projects-open",label:"Open...",disabled:false,onselect:"core:open-project"} + ]}); + } + + menuOptions.push({id:"menu-item-view-menu",label:RED._("menu.label.view.view"),options:[ // {id:"menu-item-view-show-grid",setting:"view-show-grid",label:RED._("menu.label.view.showGrid"),toggle:true,onselect:"core:toggle-show-grid"}, // {id:"menu-item-view-snap-grid",setting:"view-snap-grid",label:RED._("menu.label.view.snapGrid"),toggle:true,onselect:"core:toggle-snap-grid"}, @@ -258,9 +388,18 @@ RED.palette.init(); if (RED.settings.theme('palette.editable') !== false) { RED.palette.editor.init(); + } else { + console.log("Palette editor disabled"); } RED.sidebar.init(); + + if (RED.settings.theme("projects.enabled",false)) { + RED.projects.init(); + } else { + console.log("Projects disabled"); + } + RED.subflow.init(); RED.workspaces.init(); RED.clipboard.init(); @@ -271,6 +410,7 @@ RED.menu.init({id:"btn-sidemenu",options: menuOptions}); RED.deploy.init(RED.settings.theme("deployButton",null)); + RED.notifications.init(); RED.actions.add("core:show-about", showAbout); RED.nodes.init(); diff --git a/editor/js/nodes.js b/editor/js/nodes.js index 6e3a251ac..c227d1856 100644 --- a/editor/js/nodes.js +++ b/editor/js/nodes.js @@ -723,7 +723,9 @@ RED.nodes = (function() { if (!$.isArray(newNodes)) { newNodes = [newNodes]; } + var isInitialLoad = false; if (!initialLoad) { + isInitialLoad = true; initialLoad = JSON.parse(JSON.stringify(newNodes)); } var unknownTypes = []; @@ -744,7 +746,7 @@ RED.nodes = (function() { } } - if (unknownTypes.length > 0) { + if (!isInitialLoad && unknownTypes.length > 0) { var typeList = "
    • "+unknownTypes.join("
    • ")+"
    "; var type = "type"+(unknownTypes.length > 1?"s":""); RED.notify(""+RED._("clipboard.importUnrecognised",{count:unknownTypes.length})+""+typeList,"error",false,10000); @@ -1223,12 +1225,13 @@ RED.nodes = (function() { RED.workspaces.remove(workspaces[id]); }); defaultWorkspace = null; - - RED.nodes.dirty(true); + initialLoad = null; + RED.nodes.dirty(false); RED.view.redraw(true); RED.palette.refresh(); RED.workspaces.refresh(); RED.sidebar.config.refresh(); + RED.sidebar.info.refresh(); // var node_defs = {}; // var nodes = []; diff --git a/editor/js/settings.js b/editor/js/settings.js index b26dfe990..9594bec43 100644 --- a/editor/js/settings.js +++ b/editor/js/settings.js @@ -18,6 +18,9 @@ RED.settings = (function () { var loadedSettings = {}; + var userSettings = {}; + var settingsDirty = false; + var pendingSave; var hasLocalStorage = function () { try { @@ -31,7 +34,12 @@ RED.settings = (function () { if (!hasLocalStorage()) { return; } - localStorage.setItem(key, JSON.stringify(value)); + if (key === "auth-tokens") { + localStorage.setItem(key, JSON.stringify(value)); + } else { + userSettings[key] = value; + saveUserSettings(); + } }; /** @@ -44,14 +52,23 @@ RED.settings = (function () { if (!hasLocalStorage()) { return undefined; } - return JSON.parse(localStorage.getItem(key)); + if (key === "auth-tokens") { + return JSON.parse(localStorage.getItem(key)); + } else { + return userSettings[key]; + } }; var remove = function (key) { if (!hasLocalStorage()) { return; } - localStorage.removeItem(key); + if (key === "auth-tokens") { + localStorage.removeItem(key); + } else { + delete userSettings[key]; + saveUserSettings(); + } }; var setProperties = function(data) { @@ -68,6 +85,10 @@ RED.settings = (function () { loadedSettings = data; }; + var setUserSettings = function(data) { + userSettings = data; + } + var init = function (done) { var accessTokenMatch = /[?&]access_token=(.*?)(?:$|&)/.exec(window.location.search); if (accessTokenMatch) { @@ -106,7 +127,7 @@ RED.settings = (function () { RED.settings.remove("auth-tokens"); } console.log("Node-RED: " + data.version); - done(); + loadUserSettings(done); }, error: function(jqXHR,textStatus,errorThrown) { if (jqXHR.status === 401) { @@ -115,12 +136,52 @@ RED.settings = (function () { } RED.user.login(function() { load(done); }); } else { - console.log("Unexpected error:",jqXHR.status,textStatus); + console.log("Unexpected error loading settings:",jqXHR.status,textStatus); } } }); }; + function loadUserSettings(done) { + $.ajax({ + headers: { + "Accept": "application/json" + }, + dataType: "json", + cache: false, + url: 'settings/user', + success: function (data) { + setUserSettings(data); + done(); + }, + error: function(jqXHR,textStatus,errorThrown) { + console.log("Unexpected error loading user settings:",jqXHR.status,textStatus); + } + }); + } + + function saveUserSettings() { + if (RED.user.hasPermission("settings.write")) { + if (pendingSave) { + clearTimeout(pendingSave); + } + pendingSave = setTimeout(function() { + pendingSave = null; + $.ajax({ + method: 'POST', + contentType: 'application/json', + url: 'settings/user', + data: JSON.stringify(userSettings), + success: function (data) { + }, + error: function(jqXHR,textStatus,errorThrown) { + console.log("Unexpected error saving user settings:",jqXHR.status,textStatus); + } + }); + },300); + } + } + function theme(property,defaultValue) { if (!RED.settings.editorTheme) { return defaultValue; @@ -143,10 +204,10 @@ RED.settings = (function () { return { init: init, load: load, + loadUserSettings: loadUserSettings, set: set, get: get, remove: remove, theme: theme } -}) -(); +})(); diff --git a/editor/js/ui/common/editableList.js b/editor/js/ui/common/editableList.js index c13788f2f..975c8c300 100644 --- a/editor/js/ui/common/editableList.js +++ b/editor/js/ui/common/editableList.js @@ -75,7 +75,7 @@ addLabel = 'add'; } } - $(' '+addLabel+'') + $(' '+addLabel+'') .appendTo(this.topContainer) .click(function(evt) { evt.preventDefault(); @@ -116,6 +116,11 @@ this.uiContainer.css("minHeight",minHeight); this.element.css("minHeight",0); } + var maxHeight = this.element.css("maxHeight"); + if (maxHeight !== '0px') { + this.uiContainer.css("maxHeight",maxHeight); + this.element.css("maxHeight",null); + } if (this.options.height !== 'auto') { this.uiContainer.css("overflow-y","scroll"); if (!isNaN(this.options.height)) { diff --git a/editor/js/ui/common/popover.js b/editor/js/ui/common/popover.js index df586f668..6a0bc9fdf 100644 --- a/editor/js/ui/common/popover.js +++ b/editor/js/ui/common/popover.js @@ -23,7 +23,7 @@ RED.popover = (function() { }, "small": { top: 5, - leftRight: 8, + leftRight: 17, leftLeft: 16 } } @@ -33,6 +33,7 @@ RED.popover = (function() { var trigger = options.trigger; var content = options.content; var delay = options.delay; + var autoClose = options.autoClose; var width = options.width||"auto"; var size = options.size||"default"; if (!deltaSizes[size]) { @@ -43,7 +44,7 @@ RED.popover = (function() { var active; var div; - var openPopup = function() { + var openPopup = function(instant) { if (active) { div = $('
    ').appendTo("body"); if (size !== "default") { @@ -62,7 +63,6 @@ RED.popover = (function() { var targetPos = target.offset(); var targetWidth = target.width(); var targetHeight = target.height(); - var divHeight = div.height(); var divWidth = div.width(); if (direction === 'right') { @@ -70,23 +70,29 @@ RED.popover = (function() { } else if (direction === 'left') { div.css({top: targetPos.top+targetHeight/2-divHeight/2-deltaSizes[size].top,left:targetPos.left-deltaSizes[size].leftLeft-divWidth}); } - - div.fadeIn("fast"); + if (instant) { + div.show(); + } else { + div.fadeIn("fast"); + } } } - var closePopup = function() { + var closePopup = function(instant) { if (!active) { if (div) { - div.fadeOut("fast",function() { + if (instant) { $(this).remove(); - }); + } else { + div.fadeOut("fast",function() { + $(this).remove(); + }); + } div = null; } } } if (trigger === 'hover') { - target.on('mouseenter',function(e) { clearTimeout(timer); active = true; @@ -110,18 +116,26 @@ RED.popover = (function() { openPopup(); } }); + } else if (autoClose) { + setTimeout(function() { + active = false; + closePopup(); + },autoClose); } var res = { setContent: function(_content) { content = _content; + return res; }, - open: function () { + open: function (instant) { active = true; - openPopup(); + openPopup(instant); + return res; }, - close: function () { + close: function (instant) { active = false; - closePopup(); + closePopup(instant); + return res; } } return res; diff --git a/editor/js/ui/common/stack.js b/editor/js/ui/common/stack.js index 6370649a9..eca30bd75 100644 --- a/editor/js/ui/common/stack.js +++ b/editor/js/ui/common/stack.js @@ -17,11 +17,31 @@ RED.stack = (function() { function createStack(options) { var container = options.container; - + container.addClass("red-ui-stack"); + var contentHeight = 0; var entries = []; var visible = true; + // TODO: make this a singleton function - and watch out for stacks no longer + // in the DOM + var resizeStack = function() { + if (entries.length > 0) { + var headerHeight = 0; + entries.forEach(function(entry) { + headerHeight += entry.header.outerHeight(); + }); + var height = container.innerHeight(); + contentHeight = height - headerHeight - (entries.length-1); + entries.forEach(function(e) { + e.contentWrap.height(contentHeight); + }); + } + } + if (options.fill && options.singleExpanded) { + $(window).resize(resizeStack); + $(window).focus(resizeStack); + } return { add: function(entry) { entries.push(entry); @@ -30,7 +50,12 @@ RED.stack = (function() { entry.container.hide(); } var header = $('
    ').appendTo(entry.container); - entry.content = $('
    ').appendTo(entry.container); + entry.header = header; + entry.contentWrap = $('
    ',{style:"position:relative"}).appendTo(entry.container); + if (options.fill) { + entry.contentWrap.css("height",contentHeight); + } + entry.content = $('
    ').appendTo(entry.contentWrap); if (entry.collapsible !== false) { header.click(function() { if (options.singleExpanded) { @@ -49,11 +74,13 @@ RED.stack = (function() { var icon = $('').appendTo(header); if (entry.expanded) { + entry.container.addClass("palette-category-expanded"); icon.addClass("expanded"); } else { - entry.content.hide(); + entry.contentWrap.hide(); } } else { + $('').appendTo(header); header.css("cursor","default"); } entry.title = $('').html(entry.title).appendTo(header); @@ -74,24 +101,35 @@ RED.stack = (function() { if (entry.onexpand) { entry.onexpand.call(entry); } + if (options.singleExpanded) { + entries.forEach(function(e) { + if (e !== entry) { + e.collapse(); + } + }) + } + icon.addClass("expanded"); - entry.content.slideDown(200); + entry.container.addClass("palette-category-expanded"); + entry.contentWrap.slideDown(200); return true; } }; entry.collapse = function() { if (entry.isExpanded()) { icon.removeClass("expanded"); - entry.content.slideUp(200); + entry.container.removeClass("palette-category-expanded"); + entry.contentWrap.slideUp(200); return true; } }; entry.isExpanded = function() { - return icon.hasClass("expanded"); + return entry.container.hasClass("palette-category-expanded"); }; - + if (options.fill && options.singleExpanded) { + resizeStack(); + } return entry; - }, hide: function() { @@ -108,9 +146,13 @@ RED.stack = (function() { entry.container.show(); }); return this; + }, + resize: function() { + if (resizeStack) { + resizeStack(); + } } } - } return { diff --git a/editor/js/ui/deploy.js b/editor/js/ui/deploy.js index b2589342f..63305d2c2 100644 --- a/editor/js/ui/deploy.js +++ b/editor/js/ui/deploy.js @@ -97,113 +97,6 @@ RED.deploy = (function() { RED.actions.add("core:deploy-flows",save); - $( "#node-dialog-confirm-deploy" ).dialog({ - title: RED._('deploy.confirm.button.confirm'), - modal: true, - autoOpen: false, - width: 550, - height: "auto", - buttons: [ - { - text: RED._("common.label.cancel"), - click: function() { - $( this ).dialog( "close" ); - } - }, - { - id: "node-dialog-confirm-deploy-review", - text: RED._("deploy.confirm.button.review"), - class: "primary disabled", - click: function() { - if (!$("#node-dialog-confirm-deploy-review").hasClass('disabled')) { - RED.diff.showRemoteDiff(); - $( this ).dialog( "close" ); - } - } - }, - { - id: "node-dialog-confirm-deploy-merge", - text: RED._("deploy.confirm.button.merge"), - class: "primary disabled", - click: function() { - RED.diff.mergeDiff(currentDiff); - $( this ).dialog( "close" ); - } - }, - { - id: "node-dialog-confirm-deploy-deploy", - text: RED._("deploy.confirm.button.confirm"), - class: "primary", - click: function() { - - var ignoreChecked = $( "#node-dialog-confirm-deploy-hide" ).prop("checked"); - if (ignoreChecked) { - ignoreDeployWarnings[$( "#node-dialog-confirm-deploy-type" ).val()] = true; - } - save(true,/conflict/.test($("#node-dialog-confirm-deploy-type" ).val())); - $( this ).dialog( "close" ); - } - }, - { - id: "node-dialog-confirm-deploy-overwrite", - text: RED._("deploy.confirm.button.overwrite"), - class: "primary", - click: function() { - save(true,/conflict/.test($("#node-dialog-confirm-deploy-type" ).val())); - $( this ).dialog( "close" ); - } - } - ], - create: function() { - $("#node-dialog-confirm-deploy").parent().find("div.ui-dialog-buttonpane") - .prepend('
    '+ - ' '+ - ''+ - ''+ - '
    '); - }, - open: function() { - var deployType = $("#node-dialog-confirm-deploy-type" ).val(); - if (/conflict/.test(deployType)) { - $( "#node-dialog-confirm-deploy" ).dialog('option','title', RED._('deploy.confirm.button.review')); - $("#node-dialog-confirm-deploy-deploy").hide(); - $("#node-dialog-confirm-deploy-review").addClass('disabled').show(); - $("#node-dialog-confirm-deploy-merge").addClass('disabled').show(); - $("#node-dialog-confirm-deploy-overwrite").toggle(deployType === "deploy-conflict"); - currentDiff = null; - $("#node-dialog-confirm-deploy-conflict-checking").show(); - $("#node-dialog-confirm-deploy-conflict-auto-merge").hide(); - $("#node-dialog-confirm-deploy-conflict-manual-merge").hide(); - - var now = Date.now(); - RED.diff.getRemoteDiff(function(diff) { - var ellapsed = Math.max(1000 - (Date.now()-now), 0); - currentDiff = diff; - setTimeout(function() { - $("#node-dialog-confirm-deploy-conflict-checking").hide(); - var d = Object.keys(diff.conflicts); - if (d.length === 0) { - $("#node-dialog-confirm-deploy-conflict-auto-merge").show(); - $("#node-dialog-confirm-deploy-merge").removeClass('disabled') - } else { - $("#node-dialog-confirm-deploy-conflict-manual-merge").show(); - } - $("#node-dialog-confirm-deploy-review").removeClass('disabled') - },ellapsed); - }) - - - $("#node-dialog-confirm-deploy-hide").parent().hide(); - } else { - $( "#node-dialog-confirm-deploy" ).dialog('option','title', RED._('deploy.confirm.button.confirm')); - $("#node-dialog-confirm-deploy-deploy").show(); - $("#node-dialog-confirm-deploy-overwrite").hide(); - $("#node-dialog-confirm-deploy-review").hide(); - $("#node-dialog-confirm-deploy-merge").hide(); - $("#node-dialog-confirm-deploy-hide").parent().show(); - } - } - }); RED.events.on('nodes:change',function(state) { if (state.dirty) { @@ -224,24 +117,30 @@ RED.deploy = (function() { if (currentRev === null || deployInflight || currentRev === msg.revision) { return; } - var message = $('
    '+RED._('deploy.confirm.backgroundUpdate')+ - '

    '+ - ''+ - ''+ - '
    '); - $(message.find('button')[0]).click(function(evt) { - evt.preventDefault(); - activeNotifyMessage.close(); - activeNotifyMessage = null; - }) - $(message.find('button')[1]).click(function(evt) { - evt.preventDefault(); - activeNotifyMessage.close(); - var nns = RED.nodes.createCompleteNodeSet(); - resolveConflict(nns,false); - activeNotifyMessage = null; - }) - activeNotifyMessage = RED.notify(message,null,true); + var message = $('

    ').text(RED._('deploy.confirm.backgroundUpdate')); + activeNotifyMessage = RED.notify(message,{ + modal: true, + fixed: true, + buttons: [ + { + text: RED._('deploy.confirm.button.ignore'), + click: function() { + activeNotifyMessage.close(); + activeNotifyMessage = null; + } + }, + { + text: RED._('deploy.confirm.button.review'), + class: "primary", + click: function() { + activeNotifyMessage.close(); + var nns = RED.nodes.createCompleteNodeSet(); + resolveConflict(nns,false); + activeNotifyMessage = null; + } + } + ] + }); } }); } @@ -271,16 +170,99 @@ RED.deploy = (function() { } function resolveConflict(currentNodes, activeDeploy) { - $( "#node-dialog-confirm-deploy-config" ).hide(); - $( "#node-dialog-confirm-deploy-unknown" ).hide(); - $( "#node-dialog-confirm-deploy-unused" ).hide(); - $( "#node-dialog-confirm-deploy-conflict" ).show(); - $( "#node-dialog-confirm-deploy-type" ).val(activeDeploy?"deploy-conflict":"background-conflict"); - $( "#node-dialog-confirm-deploy" ).dialog( "open" ); - } + var message = $('

    '); + $('

    ').appendTo(message); + var conflictCheck = $('
    '+ + '
    '+ + '
    ').appendTo(message); + var conflictAutoMerge = $('
    '+ + '
    '+ + '
    ').hide().appendTo(message); + var conflictManualMerge = $('
    '+ + '
    '+ + '
    ').hide().appendTo(message); + message.i18n(); + currentDiff = null; + var buttons = [ + { + text: RED._("common.label.cancel"), + click: function() { + conflictNotification.close(); + } + }, + { + id: "node-dialog-confirm-deploy-review", + text: RED._("deploy.confirm.button.review"), + class: "primary disabled", + click: function() { + if (!$("#node-dialog-confirm-deploy-review").hasClass('disabled')) { + RED.diff.showRemoteDiff(); + conflictNotification.close(); + } + } + }, + { + id: "node-dialog-confirm-deploy-merge", + text: RED._("deploy.confirm.button.merge"), + class: "primary disabled", + click: function() { + if (!$("#node-dialog-confirm-deploy-merge").hasClass('disabled')) { + RED.diff.mergeDiff(currentDiff); + conflictNotification.close(); + } + } + } + ]; + if (activeDeploy) { + buttons.push({ + id: "node-dialog-confirm-deploy-overwrite", + text: RED._("deploy.confirm.button.overwrite"), + class: "primary", + click: function() { + save(true,activeDeploy); + conflictNotification.close(); + } + }) + } + var conflictNotification = RED.notify(message,{ + modal: true, + fixed: true, + width: 600, + buttons: buttons + }); + + var now = Date.now(); + RED.diff.getRemoteDiff(function(diff) { + var ellapsed = Math.max(1000 - (Date.now()-now), 0); + currentDiff = diff; + setTimeout(function() { + conflictCheck.hide(); + var d = Object.keys(diff.conflicts); + if (d.length === 0) { + conflictAutoMerge.show(); + $("#node-dialog-confirm-deploy-merge").removeClass('disabled') + } else { + conflictManualMerge.show(); + } + $("#node-dialog-confirm-deploy-review").removeClass('disabled') + },ellapsed); + }) + } + function cropList(list) { + if (list.length > 5) { + var remainder = list.length - 5; + list = list.slice(0,5); + list.push(RED._("deploy.confirm.plusNMore",{count:remainder})); + } + return list; + } function save(skipValidation,force) { if (!$("#btn-deploy").hasClass("disabled")) { + if (!RED.user.hasPermission("flows.write")) { + RED.notify(RED._("user.errors.deploy"),"error"); + return; + } if (!skipValidation) { var hasUnknown = false; var hasInvalid = false; @@ -310,39 +292,62 @@ RED.deploy = (function() { } }); - $( "#node-dialog-confirm-deploy-config" ).hide(); - $( "#node-dialog-confirm-deploy-unknown" ).hide(); - $( "#node-dialog-confirm-deploy-unused" ).hide(); - $( "#node-dialog-confirm-deploy-conflict" ).hide(); - var showWarning = false; - + var notificationMessage; + var notificationButtons = []; + var notification; if (hasUnknown && !ignoreDeployWarnings.unknown) { showWarning = true; - $( "#node-dialog-confirm-deploy-type" ).val("unknown"); - $( "#node-dialog-confirm-deploy-unknown" ).show(); - $( "#node-dialog-confirm-deploy-unknown-list" ) - .html("
  • "+unknownNodes.join("
  • ")+"
  • "); + notificationMessage = "

    "+RED._('deploy.confirm.unknown')+"

    "+ + '
    • '+cropList(unknownNodes).join("
    • ")+"

    "+ + RED._('deploy.confirm.confirm')+ + "

    "; + + notificationButtons= [ + { + id: "node-dialog-confirm-deploy-deploy", + text: RED._("deploy.confirm.button.confirm"), + class: "primary", + click: function() { + save(true); + notification.close(); + } + } + ]; } else if (hasInvalid && !ignoreDeployWarnings.invalid) { showWarning = true; - $( "#node-dialog-confirm-deploy-type" ).val("invalid"); - $( "#node-dialog-confirm-deploy-config" ).show(); invalidNodes.sort(sortNodeInfo); - $( "#node-dialog-confirm-deploy-invalid-list" ) - .html("
  • "+invalidNodes.map(function(A) { return (A.tab?"["+A.tab+"] ":"")+A.label+" ("+A.type+")"}).join("
  • ")+"
  • "); - } else if (hasUnusedConfig && !ignoreDeployWarnings.unusedConfig) { - // showWarning = true; - // $( "#node-dialog-confirm-deploy-type" ).val("unusedConfig"); - // $( "#node-dialog-confirm-deploy-unused" ).show(); - // - // unusedConfigNodes.sort(sortNodeInfo); - // $( "#node-dialog-confirm-deploy-unused-list" ) - // .html("
  • "+unusedConfigNodes.map(function(A) { return (A.tab?"["+A.tab+"] ":"")+A.label+" ("+A.type+")"}).join("
  • ")+"
  • "); + notificationMessage = "

    "+RED._('deploy.confirm.improperlyConfigured')+"

    "+ + '
    • '+cropList(invalidNodes.map(function(A) { return (A.tab?"["+A.tab+"] ":"")+A.label+" ("+A.type+")"})).join("
    • ")+"

    "+ + RED._('deploy.confirm.confirm')+ + "

    "; + notificationButtons= [ + { + id: "node-dialog-confirm-deploy-deploy", + text: RED._("deploy.confirm.button.confirm"), + class: "primary", + click: function() { + save(true); + notification.close(); + } + } + ]; } if (showWarning) { - $( "#node-dialog-confirm-deploy-hide" ).prop("checked",false); - $( "#node-dialog-confirm-deploy" ).dialog( "open" ); + notificationButtons.unshift( + { + text: RED._("common.label.cancel"), + click: function() { + notification.close(); + } + } + ); + notification = RED.notify(notificationMessage,{ + modal: true, + fixed: true, + buttons:notificationButtons + }); return; } } @@ -382,7 +387,7 @@ RED.deploy = (function() { '

    '+RED._("deploy.successfulDeploy")+'

    '+ '

    '+RED._("deploy.unusedConfigNodes")+' '+RED._("deploy.unusedConfigNodesLink")+'

    ',"success",false,6000); } else { - RED.notify(RED._("deploy.successfulDeploy"),"success"); + RED.notify('

    '+RED._("deploy.successfulDeploy")+'

    ',"success"); } RED.nodes.eachNode(function(node) { if (node.changed) { @@ -437,6 +442,10 @@ RED.deploy = (function() { } } return { - init: init + init: init, + setDeployInflight: function(state) { + deployInflight = state; + } + } })(); diff --git a/editor/js/ui/diff.js b/editor/js/ui/diff.js index 26c73c9a0..cfec75e75 100644 --- a/editor/js/ui/diff.js +++ b/editor/js/ui/diff.js @@ -11,16 +11,19 @@ RED.diff = (function() { // RED.keyboard.add("*","ctrl-shift-l","core:show-current-diff"); RED.keyboard.add("*","ctrl-shift-r","core:show-remote-diff"); + + RED.actions.add("core:show-test-flow-diff-1",function(){showTestFlowDiff(1)}); + RED.keyboard.add("*","ctrl-shift-f 1","core:show-test-flow-diff-1"); + + RED.actions.add("core:show-test-flow-diff-2",function(){showTestFlowDiff(2)}); + RED.keyboard.add("*","ctrl-shift-f 2","core:show-test-flow-diff-2"); + RED.actions.add("core:show-test-flow-diff-3",function(){showTestFlowDiff(3)}); + RED.keyboard.add("*","ctrl-shift-f 3","core:show-test-flow-diff-3"); + } - - function buildDiffPanel(container) { - var diffPanel = $('
      ').appendTo(container); - - var toolbar = $('
      '+ - ' '+ - '
      ').prependTo(diffPanel); - - diffList = diffPanel.find("#node-dialog-view-diff-diff").editableList({ + function createDiffTable(container) { + var diffList = $('
        ').appendTo(container); + diffList.editableList({ addButton: false, scrollOnAdd: false, addItem: function(container,i,object) { @@ -281,7 +284,180 @@ RED.diff = (function() { container.i18n(); } }); - return diffPanel; + return diffList; + } + function buildDiffPanel(container,diff,options) { + var diffPanel = $('
        ').appendTo(container); + var diffHeaders = $('
        ').appendTo(diffPanel); + if (options.mode === "merge") { + diffPanel.addClass("node-dialog-view-diff-panel-merge"); + var toolbar = $('
        '+ + ' '+ + '
        ').prependTo(diffPanel); + } + var diffList = createDiffTable(diffPanel); + + var localDiff = diff.localDiff; + var remoteDiff = diff.remoteDiff; + var conflicts = diff.conflicts; + + var currentConfig = localDiff.currentConfig; + var newConfig = localDiff.newConfig; + + + if (remoteDiff !== undefined) { + diffPanel.addClass('node-diff-three-way'); + var localTitle = options.oldRevTitle || RED._('diff.local'); + var remoteTitle = options.newRevTitle || RED._('diff.remote'); + $('
        ').text(localTitle).appendTo(diffHeaders); + $('
        ').text(remoteTitle).appendTo(diffHeaders); + } else { + diffPanel.removeClass('node-diff-three-way'); + } + + return { + list: diffList, + finish: function() { + var el = { + diff: localDiff, + def: { + category: 'config', + color: '#f0f0f0' + }, + tab: { + n: {}, + nodes: currentConfig.globals + }, + newTab: { + n: {}, + nodes: newConfig.globals + } + }; + if (remoteDiff !== undefined) { + el.remoteTab = { + n:{}, + nodes:remoteDiff.newConfig.globals + }; + el.remoteDiff = remoteDiff; + } + diffList.editableList('addItem',el); + + var seenTabs = {}; + + currentConfig.tabOrder.forEach(function(tabId) { + var tab = currentConfig.tabs[tabId]; + var el = { + diff: localDiff, + def: RED.nodes.getType('tab'), + tab:tab + }; + if (newConfig.tabs.hasOwnProperty(tabId)) { + el.newTab = newConfig.tabs[tabId]; + } + if (remoteDiff !== undefined) { + el.remoteTab = remoteDiff.newConfig.tabs[tabId]; + el.remoteDiff = remoteDiff; + } + seenTabs[tabId] = true; + diffList.editableList('addItem',el) + }); + newConfig.tabOrder.forEach(function(tabId) { + if (!seenTabs[tabId]) { + seenTabs[tabId] = true; + var tab = newConfig.tabs[tabId]; + var el = { + diff: localDiff, + def: RED.nodes.getType('tab'), + tab:tab, + newTab: tab + }; + if (remoteDiff !== undefined) { + el.remoteDiff = remoteDiff; + } + diffList.editableList('addItem',el) + } + }); + if (remoteDiff !== undefined) { + remoteDiff.newConfig.tabOrder.forEach(function(tabId) { + if (!seenTabs[tabId]) { + var tab = remoteDiff.newConfig.tabs[tabId]; + // TODO how to recognise this is a remotely added flow + var el = { + diff: localDiff, + remoteDiff: remoteDiff, + def: RED.nodes.getType('tab'), + tab:tab, + remoteTab:tab + }; + diffList.editableList('addItem',el) + } + }); + } + var subflowId; + for (subflowId in currentConfig.subflows) { + if (currentConfig.subflows.hasOwnProperty(subflowId)) { + seenTabs[subflowId] = true; + el = { + diff: localDiff, + def: { + defaults:{}, + icon:"subflow.png", + category: "subflows", + color: "#da9" + }, + tab:currentConfig.subflows[subflowId] + } + if (newConfig.subflows.hasOwnProperty(subflowId)) { + el.newTab = newConfig.subflows[subflowId]; + } + if (remoteDiff !== undefined) { + el.remoteTab = remoteDiff.newConfig.subflows[subflowId]; + el.remoteDiff = remoteDiff; + } + diffList.editableList('addItem',el) + } + } + for (subflowId in newConfig.subflows) { + if (newConfig.subflows.hasOwnProperty(subflowId) && !seenTabs[subflowId]) { + seenTabs[subflowId] = true; + el = { + diff: localDiff, + def: { + defaults:{}, + icon:"subflow.png", + category: "subflows", + color: "#da9" + }, + tab:newConfig.subflows[subflowId], + newTab:newConfig.subflows[subflowId] + } + if (remoteDiff !== undefined) { + el.remoteDiff = remoteDiff; + } + diffList.editableList('addItem',el) + } + } + if (remoteDiff !== undefined) { + for (subflowId in remoteDiff.newConfig.subflows) { + if (remoteDiff.newConfig.subflows.hasOwnProperty(subflowId) && !seenTabs[subflowId]) { + el = { + diff: localDiff, + remoteDiff: remoteDiff, + def: { + defaults:{}, + icon:"subflow.png", + category: "subflows", + color: "#da9" + }, + tab:remoteDiff.newConfig.subflows[subflowId], + remoteTab: remoteDiff.newConfig.subflows[subflowId] + } + diffList.editableList('addItem',el) + } + } + } + } + }; } function formatWireProperty(wires,allNodes) { var result = $("
        ",{class:"node-diff-property-wires"}) @@ -794,6 +970,15 @@ RED.diff = (function() { remoteCell.addClass("node-diff-empty"); } } + if (localNode && remoteNode && typeof localNode[d] === "string") { + if (/\n/.test(localNode[d]) || /\n/.test(remoteNode[d])) { + $('').click(function() { + showTextDiff(localNode[d],remoteNode[d]); + }).appendTo(propertyNameCell); + } + } + + }); return nodePropertiesDiv; } @@ -899,7 +1084,7 @@ RED.diff = (function() { if (diff === undefined) { getRemoteDiff(showRemoteDiff); } else { - showDiff(diff); + showDiff(diff,{mode:'merge'}); } } function parseNodes(nodeList) { @@ -1036,10 +1221,12 @@ RED.diff = (function() { return diff; } - function showDiff(diff) { + function showDiff(diff,options) { if (diffVisible) { return; } + options = options || {}; + var mode = options.mode || 'merge'; var localDiff = diff.localDiff; var remoteDiff = diff.remoteDiff; @@ -1047,15 +1234,56 @@ RED.diff = (function() { currentDiff = diff; var trayOptions = { - title: "Review Changes", //TODO: nls + title: options.title||"Review Changes", //TODO: nls width: Infinity, + overlay: true, buttons: [ { - text: RED._("common.label.cancel"), + text: RED._((options.mode === 'merge')?"common.label.cancel":"common.label.close"), click: function() { RED.tray.close(); } - }, + } + ], + resize: function(dimensions) { + // trayWidth = dimensions.width; + }, + open: function(tray) { + var trayBody = tray.find('.editor-tray-body'); + var diffTable = buildDiffPanel(trayBody,diff,options); + diffTable.list.hide(); + if (remoteDiff) { + $("#node-diff-view-diff-merge").show(); + if (Object.keys(conflicts).length === 0) { + $("#node-diff-view-diff-merge").removeClass('disabled'); + } else { + $("#node-diff-view-diff-merge").addClass('disabled'); + } + } else { + $("#node-diff-view-diff-merge").hide(); + } + refreshConflictHeader(); + // console.log("--------------"); + // console.log(localDiff); + // console.log(remoteDiff); + + setTimeout(function() { + diffTable.finish(); + diffTable.list.show(); + },300); + $("#sidebar-shade").show(); + }, + close: function() { + diffVisible = false; + $("#sidebar-shade").hide(); + + }, + show: function() { + + } + } + if (options.mode === 'merge') { + trayOptions.buttons.push( { id: "node-diff-view-diff-merge", text: RED._("deploy.confirm.button.merge"), @@ -1068,189 +1296,9 @@ RED.diff = (function() { } } } - ], - resize: function(dimensions) { - // trayWidth = dimensions.width; - }, - open: function(tray) { - var trayBody = tray.find('.editor-tray-body'); - var diffPanel = buildDiffPanel(trayBody); - if (remoteDiff) { - $("#node-diff-view-diff-merge").show(); - if (Object.keys(conflicts).length === 0) { - $("#node-diff-view-diff-merge").removeClass('disabled'); - } else { - $("#node-diff-view-diff-merge").addClass('disabled'); - } - } else { - $("#node-diff-view-diff-merge").hide(); - } - refreshConflictHeader(); - - $("#node-dialog-view-diff-headers").empty(); - // console.log("--------------"); - // console.log(localDiff); - // console.log(remoteDiff); - var currentConfig = localDiff.currentConfig; - var newConfig = localDiff.newConfig; - conflicts = conflicts || {}; - - var el = { - diff: localDiff, - def: { - category: 'config', - color: '#f0f0f0' - }, - tab: { - n: {}, - nodes: currentConfig.globals - }, - newTab: { - n: {}, - nodes: newConfig.globals - } - }; - - if (remoteDiff !== undefined) { - diffPanel.addClass('node-diff-three-way'); - - $('
        ').i18n().appendTo("#node-dialog-view-diff-headers"); - el.remoteTab = { - n:{}, - nodes:remoteDiff.newConfig.globals - }; - el.remoteDiff = remoteDiff; - } else { - diffPanel.removeClass('node-diff-three-way'); - } - - diffList.editableList('addItem',el); - - var seenTabs = {}; - - currentConfig.tabOrder.forEach(function(tabId) { - var tab = currentConfig.tabs[tabId]; - var el = { - diff: localDiff, - def: RED.nodes.getType('tab'), - tab:tab - }; - if (newConfig.tabs.hasOwnProperty(tabId)) { - el.newTab = newConfig.tabs[tabId]; - } - if (remoteDiff !== undefined) { - el.remoteTab = remoteDiff.newConfig.tabs[tabId]; - el.remoteDiff = remoteDiff; - } - seenTabs[tabId] = true; - diffList.editableList('addItem',el) - }); - newConfig.tabOrder.forEach(function(tabId) { - if (!seenTabs[tabId]) { - seenTabs[tabId] = true; - var tab = newConfig.tabs[tabId]; - var el = { - diff: localDiff, - def: RED.nodes.getType('tab'), - tab:tab, - newTab: tab - }; - if (remoteDiff !== undefined) { - el.remoteDiff = remoteDiff; - } - diffList.editableList('addItem',el) - } - }); - if (remoteDiff !== undefined) { - remoteDiff.newConfig.tabOrder.forEach(function(tabId) { - if (!seenTabs[tabId]) { - var tab = remoteDiff.newConfig.tabs[tabId]; - // TODO how to recognise this is a remotely added flow - var el = { - diff: localDiff, - remoteDiff: remoteDiff, - def: RED.nodes.getType('tab'), - tab:tab, - remoteTab:tab - }; - diffList.editableList('addItem',el) - } - }); - } - var subflowId; - for (subflowId in currentConfig.subflows) { - if (currentConfig.subflows.hasOwnProperty(subflowId)) { - seenTabs[subflowId] = true; - el = { - diff: localDiff, - def: { - defaults:{}, - icon:"subflow.png", - category: "subflows", - color: "#da9" - }, - tab:currentConfig.subflows[subflowId] - } - if (newConfig.subflows.hasOwnProperty(subflowId)) { - el.newTab = newConfig.subflows[subflowId]; - } - if (remoteDiff !== undefined) { - el.remoteTab = remoteDiff.newConfig.subflows[subflowId]; - el.remoteDiff = remoteDiff; - } - diffList.editableList('addItem',el) - } - } - for (subflowId in newConfig.subflows) { - if (newConfig.subflows.hasOwnProperty(subflowId) && !seenTabs[subflowId]) { - seenTabs[subflowId] = true; - el = { - diff: localDiff, - def: { - defaults:{}, - icon:"subflow.png", - category: "subflows", - color: "#da9" - }, - tab:newConfig.subflows[subflowId], - newTab:newConfig.subflows[subflowId] - } - if (remoteDiff !== undefined) { - el.remoteDiff = remoteDiff; - } - diffList.editableList('addItem',el) - } - } - if (remoteDiff !== undefined) { - for (subflowId in remoteDiff.newConfig.subflows) { - if (remoteDiff.newConfig.subflows.hasOwnProperty(subflowId) && !seenTabs[subflowId]) { - el = { - diff: localDiff, - remoteDiff: remoteDiff, - def: { - defaults:{}, - icon:"subflow.png", - category: "subflows", - color: "#da9" - }, - tab:remoteDiff.newConfig.subflows[subflowId], - remoteTab: remoteDiff.newConfig.subflows[subflowId] - } - diffList.editableList('addItem',el) - } - } - } - $("#sidebar-shade").show(); - }, - close: function() { - diffVisible = false; - $("#sidebar-shade").hide(); - - }, - show: function() { - - } + ); } + RED.tray.show(trayOptions); } @@ -1333,10 +1381,755 @@ RED.diff = (function() { RED.workspaces.refresh(); RED.sidebar.config.refresh(); } + function showTestFlowDiff(index) { + if (index === 1) { + var localFlow = RED.nodes.createCompleteNodeSet(); + var originalFlow = RED.nodes.originalFlow(); + showTextDiff(JSON.stringify(localFlow,null,4),JSON.stringify(originalFlow,null,4)) + } else if (index === 2) { + var local = "1\n2\n3\n4\n5\nA\n6\n7\n8\n9\n"; + var remote = "1\nA\n2\n3\nD\nE\n6\n7\n8\n9\n"; + showTextDiff(local,remote); + } else if (index === 3) { + var local = "1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22"; + var remote = "1\nTWO\nTHREE\nEXTRA\n4\n5\n6\n7\n8\n9\n10\n11\n12\nTHIRTEEN\n14\n15\n16\n17\n18\n19\n20\n21\n22"; + showTextDiff(local,remote); + } + } + + function showTextDiff(textA,textB) { + var trayOptions = { + title: "Compare Changes", //TODO: nls + width: Infinity, + overlay: true, + buttons: [ + { + text: RED._("common.label.close"), + click: function() { + RED.tray.close(); + } + } + ], + resize: function(dimensions) { + // trayWidth = dimensions.width; + }, + open: function(tray) { + var trayBody = tray.find('.editor-tray-body'); + var diffPanel = $('
        ').appendTo(trayBody); + + var codeTable = $("").appendTo(diffPanel); + $('').appendTo(codeTable); + var codeBody = $('').appendTo(codeTable); + var diffSummary = diffText(textA||"",textB||""); + var aIndex = 0; + var bIndex = 0; + var diffLength = Math.max(diffSummary.a.length, diffSummary.b.length); + + var diffLines = []; + var diffBlocks = []; + var currentBlock; + var blockLength = 0; + var blockType = 0; + + for (var i=0;i 3) { + currentBlock.end -= 3; + currentBlock.empty = true; + diffBlocks.push(currentBlock); + currentBlock = {start:i-3,end:i-3}; + } + blockType = 1; + } else if (blockType === 2) { + // we were in unchanged, but hit a change again + blockType = 1; + } + } + } + } + if (blockType === 0) { + currentBlock.empty = true; + } + currentBlock.end = diffLength; + diffBlocks.push(currentBlock); + // console.table(diffBlocks); + var diffRow; + for (var b = 0; b'); + var content = $('').appendTo(diffRow); + var label = $('').appendTo(content); + if (end < diffLines.length-1) { + label.text("@@ -"+(diffLines[end-1].a.i+1)+" +"+(diffLines[end-1].b.i+1)); + } + diffRow.click(function(evt) { + // console.log(start,end,diffLines.length); + if (end - start > 20) { + var startPos = $(this).offset(); + // console.log(startPos); + if (start > 0) { + for (var i=start;iend-11;i--) { + createDiffLine(diffLines[i]).addClass("unchanged").insertAfter($(this)); + } + end -= 10; + } + if (end < diffLines.length-1) { + label.text("@@ -"+(diffLines[end-1].a.i+1)+" +"+(diffLines[end-1].b.i+1)); + } + var endPos = $(this).offset(); + var delta = endPos.top - startPos.top; + $(".node-text-diff").scrollTop($(".node-text-diff").scrollTop() + delta); + } else { + for (var i=start;i'); + var Adiff = diffLine.a; + var Bdiff = diffLine.b; + //console.log(diffLine); + var cellNo = $('
        ').text(Adiff.type === 2?"":Adiff.i).appendTo(diffRow); + var cellLine = $('').text(Adiff.line).appendTo(diffRow); + if (Adiff.type === 2) { + cellNo.addClass('blank'); + cellLine.addClass('blank'); + } else if (Adiff.type === 4) { + cellNo.addClass('added'); + cellLine.addClass('added'); + } else if (Adiff.type === 1) { + cellNo.addClass('removed'); + cellLine.addClass('removed'); + } + cellNo = $('').text(Bdiff.type === 2?"":Bdiff.i).appendTo(diffRow); + cellLine = $('').text(Bdiff.line).appendTo(diffRow); + if (Bdiff.type === 2) { + cellNo.addClass('blank'); + cellLine.addClass('blank'); + } else if (Bdiff.type === 4) { + cellNo.addClass('added'); + cellLine.addClass('added'); + } else if (Bdiff.type === 1) { + cellNo.addClass('removed'); + cellLine.addClass('removed'); + } + return diffRow; + } + + function diffText(string1, string2,ignoreWhitespace) { + var lines1 = string1.split(/\r?\n/); + var lines2 = string2.split(/\r?\n/); + var i = lines1.length; + var j = lines2.length; + var k; + var m; + var diffSummary = {a:[],b:[]}; + var diffMap = []; + for (k = 0; k < i + 1; k++) { + diffMap[k] = []; + for (m = 0; m < j + 1; m++) { + diffMap[k][m] = 0; + } + } + var c = 0; + for (k = i - 1; k >= 0; k--) { + for (m = j - 1; m >=0; m--) { + c++; + if (compareLines(lines1[k],lines2[m],ignoreWhitespace) !== 1) { + diffMap[k][m] = diffMap[k+1][m+1]+1; + } else { + diffMap[k][m] = Math.max(diffMap[(k + 1)][m], diffMap[k][(m + 1)]); + } + } + } + //console.log(c); + k = 0; + m = 0; + + while ((k < i) && (m < j)) { + var n = compareLines(lines1[k],lines2[m],ignoreWhitespace); + if (n !== 1) { + var d = 0; + if (n===0) { + d = 0; + } else if (n==2) { + d = 3; + } + diffSummary.a.push({i:k+1,j:m+1,line:lines1[k],type:d}); + diffSummary.b.push({i:m+1,j:k+1,line:lines2[m],type:d}); + k++; + m++; + } else if (diffMap[(k + 1)][m] >= diffMap[k][(m + 1)]) { + diffSummary.a.push({i:k+1,line:lines1[k],type:1}); + k++; + } else { + diffSummary.b.push({i:m+1,line:lines2[m],type:4}); + m++; + } + } + while ((k < i) || (m < j)) { + if (k == i) { + diffSummary.b.push({i:m+1,line:lines2[m],type:4}); + m++; + } else if (m == j) { + diffSummary.a.push({i:k+1,line:lines1[k],type:1}); + k++; + } + } + return diffSummary; + } + + function compareLines(string1, string2, ignoreWhitespace) { + if (ignoreWhitespace) { + if (string1 === string2) { + return 0; + } + return string1.trim() === string2.trime() ? 2 : 1; + } + return string1 === string2 ? 0 : 1; + } + + function createUnifiedDiffTable(files,commitOptions) { + var diffPanel = $('
        '); + files.forEach(function(file) { + var hunks = file.hunks; + var isBinary = file.binary; + var codeTable = $("").appendTo(diffPanel); + $('').appendTo(codeTable); + var codeBody = $('').appendTo(codeTable); + + var diffFileRow = $('').appendTo(codeBody); + var content = $('').appendTo(diffFileRow); + + var chevron = $('').appendTo(content); + diffFileRow.click(function(e) { + diffFileRow.toggleClass("collapsed"); + var isCollapsed = diffFileRow.hasClass("collapsed"); + diffFileRow.nextUntil(".node-text-diff-file-header").toggle(!isCollapsed); + }) + var label = $('').text(file.file).appendTo(content); + + var conflictHeader; + var unresolvedConflicts = 0; + var resolvedConflicts = 0; + var conflictResolutions = {}; + + if (!commitOptions.unmerged && commitOptions.project.files && commitOptions.project.files.flow === file.file) { + var tools = $('').appendTo(content); + $('').appendTo(tools).click(function(e) { + e.preventDefault(); + e.stopPropagation(); + var projectName = commitOptions.project.name; + var filename = commitOptions.project.files.flow; + var oldVersionUrl = "/projects/"+projectName+"/files/"+commitOptions.oldRev+"/"+filename; + var newVersionUrl = "/projects/"+projectName+"/files/"+commitOptions.newRev+"/"+filename; + $.when($.getJSON(oldVersionUrl),$.getJSON(newVersionUrl)).done(function(oldVersion,newVersion) { + var oldFlow; + var newFlow; + try { + oldFlow = JSON.parse(oldVersion[0].content||"[]"); + } catch(err) { + console.log("Old Version doesn't contain valid JSON:",oldVersionUrl); + console.log(err); + return; + } + try { + newFlow = JSON.parse(newVersion[0].content||"[]"); + } catch(err) { + console.log("New Version doesn't contain valid JSON:",newFlow); + console.log(err); + return; + } + var localDiff = generateDiff(oldFlow,oldFlow); + var remoteDiff = generateDiff(oldFlow,newFlow); + var diff = resolveDiffs(localDiff,remoteDiff); + showDiff(diff,{ + title: filename, + mode: 'view', + oldRevTitle: commitOptions.oldRevTitle, + newRevTitle: commitOptions.newRevTitle + }); + // var flowDiffRow = $("").insertAfter(diffRow); + // var content = $('').appendTo(flowDiffRow); + // currentDiff = diff; + // var diffTable = buildDiffPanel(content,diff,{mode:"view"}).finish(); + }); + }) + } + + if (isBinary) { + var diffBinaryRow = $('').appendTo(codeBody); + var binaryContent = $('').appendTo(diffBinaryRow); + $('').text("Cannot show binary file contents").appendTo(binaryContent); + + } else { + hunks.forEach(function(hunk) { + var diffRow = $('').appendTo(codeBody); + var content = $('').appendTo(diffRow); + var label = $('').text(hunk.header).appendTo(content); + var isConflict = hunk.conflict; + var localLine = hunk.localStartLine; + var remoteLine = hunk.remoteStartLine; + if (isConflict) { + unresolvedConflicts++; + } + + hunk.lines.forEach(function(lineText,lineNumber) { + // if (lineText[0] === '\\' || lineText === "") { + // // Comment line - bail out of this hunk + // break; + // } + + var actualLineNumber = hunk.diffStart + lineNumber; + var isMergeHeader = isConflict && /^..(<<<<<<<|=======$|>>>>>>>)/.test(lineText); + var diffRow = $('').appendTo(codeBody); + var localLineNo = $('
        ').appendTo(diffRow); + var remoteLineNo; + if (!isMergeHeader) { + remoteLineNo = $('').appendTo(diffRow); + } else { + localLineNo.attr('colspan',2); + } + var line = $('').appendTo(diffRow); + var prefixStart = 0; + var prefixEnd = 1; + if (isConflict) { + prefixEnd = 2; + } + if (!isMergeHeader) { + var changeMarker = lineText[0]; + if (isConflict && !commitOptions.unmerged && changeMarker === ' ') { + changeMarker = lineText[1]; + } + $('').text(changeMarker).appendTo(line); + var handledlLine = false; + if (isConflict && commitOptions.unmerged) { + $('').text(lineText[1]).appendTo(line); + if (lineText[0] === '+') { + localLineNo.text(localLine++); + handledlLine = true; + } + if (lineText[1] === '+') { + remoteLineNo.text(remoteLine++); + handledlLine = true; + } + } else { + if (lineText[0] === '+' || (isConflict && lineText[1] === '+')) { + localLineNo.addClass("added"); + remoteLineNo.addClass("added"); + line.addClass("added"); + remoteLineNo.text(remoteLine++); + handledlLine = true; + } else if (lineText[0] === '-' || (isConflict && lineText[1] === '-')) { + localLineNo.addClass("removed"); + remoteLineNo.addClass("removed"); + line.addClass("removed"); + localLineNo.text(localLine++); + handledlLine = true; + } + } + if (!handledlLine) { + line.addClass("unchanged"); + if (localLine > 0 && lineText[0] !== '\\' && lineText !== "") { + localLineNo.text(localLine++); + } + if (remoteLine > 0 && lineText[0] !== '\\' && lineText !== "") { + remoteLineNo.text(remoteLine++); + } + } + $('').text(lineText.substring(prefixEnd)).appendTo(line); + } else { + diffRow.addClass("mergeHeader"); + var isSeparator = /^..(=======$)/.test(lineText); + if (!isSeparator) { + var isOurs = /^..<<<<<<').text("<<<<<<< Local Changes").appendTo(line); + hunk.localChangeStart = actualLineNumber; + } else { + hunk.remoteChangeEnd = actualLineNumber; + $('').text(">>>>>>> Remote Changes").appendTo(line); + + } + diffRow.addClass("mergeHeader-"+(isOurs?"ours":"theirs")); + $('') + .appendTo(line) + .click(function(evt) { + evt.preventDefault(); + resolvedConflicts++; + var addedRows; + var midRow; + if (isOurs) { + addedRows = diffRow.nextUntil(".mergeHeader-separator"); + midRow = addedRows.last().next(); + midRow.nextUntil(".mergeHeader").remove(); + midRow.next().remove(); + } else { + addedRows = diffRow.prevUntil(".mergeHeader-separator"); + midRow = addedRows.last().prev(); + midRow.prevUntil(".mergeHeader").remove(); + midRow.prev().remove(); + } + midRow.remove(); + diffRow.remove(); + addedRows.find(".linetext").addClass('added'); + conflictHeader.empty(); + $(''+resolvedConflicts+' of '+unresolvedConflicts+' conflicts resolved').appendTo(conflictHeader); + + conflictResolutions[file.file] = conflictResolutions[file.file] || {}; + conflictResolutions[file.file][hunk.localChangeStart] = { + changeStart: hunk.localChangeStart, + separator: hunk.changeSeparator, + changeEnd: hunk.remoteChangeEnd, + selection: isOurs?"A":"B" + } + if (commitOptions.resolveConflict) { + commitOptions.resolveConflict({ + conflicts: unresolvedConflicts, + resolved: resolvedConflicts, + resolutions: conflictResolutions + }); + } + }) + } else { + hunk.changeSeparator = actualLineNumber; + diffRow.addClass("mergeHeader-separator"); + } + } + }); + }); + } + if (commitOptions.unmerged) { + conflictHeader = $(''+resolvedConflicts+' of '+unresolvedConflicts+' conflicts resolved').appendTo(content); + } + }); + return diffPanel; + } + + function showCommitDiff(options) { + var commit = parseCommitDiff(options.commit); + var trayOptions = { + title: "View Commit Changes", //TODO: nls + width: Infinity, + overlay: true, + buttons: [ + { + text: RED._("common.label.close"), + click: function() { + RED.tray.close(); + } + } + ], + resize: function(dimensions) { + // trayWidth = dimensions.width; + }, + open: function(tray) { + var trayBody = tray.find('.editor-tray-body'); + var diffPanel = $('
        ').appendTo(trayBody); + + var codeTable = $("").appendTo(diffPanel); + $('').appendTo(codeTable); + var codeBody = $('').appendTo(codeTable); + + var diffRow = $('').appendTo(codeBody); + var content = $('').appendTo(diffRow); + + $("

        ").text(commit.title).appendTo(content); + $('
        ').text(commit.comment).appendTo(content); + var summary = $('
        ').appendTo(content); + $('
        ').text("Commit "+commit.sha).appendTo(summary); + $('
        ').text((commit.authorName||commit.author)+" - "+options.date).appendTo(summary); + + if (commit.files) { + createUnifiedDiffTable(commit.files,options).appendTo(diffPanel); + } + + + }, + close: function() { + diffVisible = false; + }, + show: function() { + + } + } + RED.tray.show(trayOptions); + } + function showUnifiedDiff(options) { + var diff = options.diff; + var title = options.title; + var files = parseUnifiedDiff(diff); + + var currentResolution; + if (options.unmerged) { + options.resolveConflict = function(results) { + currentResolution = results; + if (results.conflicts === results.resolved) { + $("#node-diff-view-resolve-diff").removeClass('disabled'); + } + } + } + + + var trayOptions = { + title: title||"Compare Changes", //TODO: nls + width: Infinity, + overlay: true, + buttons: [ + { + text: RED._((options.unmerged)?"common.label.cancel":"common.label.close"), + click: function() { + if (options.oncancel) { + options.oncancel(); + } + RED.tray.close(); + } + } + ], + resize: function(dimensions) { + // trayWidth = dimensions.width; + }, + open: function(tray) { + var trayBody = tray.find('.editor-tray-body'); + var diffPanel = $('
        ').appendTo(trayBody); + createUnifiedDiffTable(files,options).appendTo(diffPanel); + }, + close: function() { + diffVisible = false; + }, + show: function() { + + } + } + if (options.unmerged) { + trayOptions.buttons.push( + { + id: "node-diff-view-resolve-diff", + text: "Save conflict resolution", + class: "primary disabled", + click: function() { + if (!$("#node-diff-view-resolve-diff").hasClass('disabled')) { + if (options.onresolve) { + options.onresolve(currentResolution); + } + RED.tray.close(); + } + } + } + ); + } + RED.tray.show(trayOptions); + } + + function parseCommitDiff(diff) { + var result = {}; + var lines = diff.split("\n"); + var comment = []; + for (var i=0;i$/.exec(result.author); + if (m) { + result.authorName = m[1]; + result.authorEmail = m[2]; + } + } else if (/^Date: /.test(lines[i])) { + result.date = lines[i].substring(8); + } else if (/^ /.test(lines[i])) { + if (!result.title) { + result.title = lines[i].substring(4); + } else { + if (lines[i].length !== 4 || comment.length > 0) { + comment.push(lines[i].substring(4)); + } + } + } else if (/^diff /.test(lines[i])) { + result.files = parseUnifiedDiff(lines.slice(i)); + break; + } + } + result.comment = comment.join("\n"); + return result; + } + function parseUnifiedDiff(diff) { + var lines; + if (Array.isArray(diff)) { + lines = diff; + } else { + lines = diff.split("\n"); + } + var diffHeader = /^diff --git a\/(.*) b\/(.*)$/; + var fileHeader = /^\+\+\+ b\/(.*)\t?/; + var binaryFile = /^Binary files /; + var hunkHeader = /^@@ -((\d+)(,(\d+))?) \+((\d+)(,(\d+))?) @@ ?(.*)$/; + var conflictHunkHeader = /^@+ -((\d+)(,(\d+))?) -((\d+)(,(\d+))?) \+((\d+)(,(\d+))?) @+/; + var files = []; + var currentFile; + var hunks = []; + var currentHunk; + for (var i=0;i'; } title += ''; - return title; + return label; } function buildEditForm(container,formId,type,ns) { @@ -2119,9 +2122,105 @@ RED.editor = (function() { editStack.push({type:type}); RED.view.state(RED.state.EDITING); var expressionEditor; + var changeTimer; + + var checkValid = function() { + var v = expressionEditor.getValue(); + try { + JSON.parse(v); + $("#node-dialog-ok").removeClass('disabled'); + return true; + } catch(err) { + $("#node-dialog-ok").addClass('disabled'); + return false; + } + } + var trayOptions = { + title: options.title || getEditStackTitle(), + buttons: [ + { + id: "node-dialog-cancel", + text: RED._("common.label.cancel"), + click: function() { + RED.tray.close(); + } + }, + { + id: "node-dialog-ok", + text: RED._("common.label.done"), + class: "primary", + click: function() { + if (options.requireValid && !checkValid()) { + return; + } + onComplete(expressionEditor.getValue()); + RED.tray.close(); + } + } + ], + resize: function(dimensions) { + editTrayWidthCache[type] = dimensions.width; + + var rows = $("#dialog-form>div:not(.node-text-editor-row)"); + var editorRow = $("#dialog-form>div.node-text-editor-row"); + var height = $("#dialog-form").height(); + for (var i=0;i 4) { @@ -75,6 +74,16 @@ RED.notify = (function() { if (type) { n.className = "notification notification-"+type; } + if (options.width) { + var parentWidth = $("#notifications").width(); + if (options.width > parentWidth) { + var margin = -(options.width-parentWidth)/2; + $(n).css({ + width: options.width+"px", + marginLeft: margin+"px" + }) + } + } n.style.display = "none"; if (typeof msg === "string") { n.innerHTML = msg; @@ -85,6 +94,9 @@ RED.notify = (function() { var buttonSet = $('
        ').appendTo(n) options.buttons.forEach(function(buttonDef) { var b = $('') + .appendTo(bg) + .click(function(evt) { + evt.preventDefault(); + updateProjectSummary(activeProject.summary, container); + editButton.show(); + }); + $('') + .appendTo(bg) + .click(function(evt) { + evt.preventDefault(); + var v = input.val(); + updateProjectSummary(v, container); + var spinner = utils.addSpinnerOverlay(container); + var done = function(err,res) { + if (err) { + spinner.remove(); + return editSummary(activeProject, summary, container); + } + activeProject.summary = v; + spinner.remove(); + updateProjectSummary(activeProject.summary, container); + editButton.show(); + } + utils.sendRequest({ + url: "projects/"+activeProject.name, + type: "PUT", + responses: { + 0: function(error) { + done(error,null); + }, + 200: function(data) { + RED.sidebar.versionControl.refresh(true); + done(null,data); + }, + 400: { + 'unexpected_error': function(error) { + done(error,null); + } + }, + } + },{summary:v}); + }); + } + function updateProjectSummary(summary, container) { + container.empty(); + if (summary) { + container.text(summary).removeClass('node-info-node'); + } else { + container.text("No summary available").addClass('node-info-none');// TODO: nls + } + } + + function createMainPane(activeProject) { + + var pane = $('
        '); + $('

        ').text(activeProject.name).appendTo(pane); + var summary = $('
        ').appendTo(pane); + var summaryContent = $('
        ',{style:"color: #999"}).appendTo(summary); + updateProjectSummary(activeProject.summary, summaryContent); + if (RED.user.hasPermission("projects.write")) { + $('') + .prependTo(summary) + .click(function(evt) { + evt.preventDefault(); + editSummary(activeProject, activeProject.summary, summaryContent); + }); + } + $('
        ').appendTo(pane); + + var description = $('
        ').appendTo(pane); + var descriptionContent = $('
        ',{style:"min-height: 200px"}).appendTo(description); + + updateProjectDescription(activeProject, descriptionContent); + + if (RED.user.hasPermission("projects.write")) { + $('') + .prependTo(description) + .click(function(evt) { + evt.preventDefault(); + editDescription(activeProject, descriptionContent); + }); + } + return pane; + } + function updateProjectDependencies(activeProject,depsList) { + depsList.editableList('empty'); + + var totalCount = 0; + var unknownCount = 0; + var unusedCount = 0; + var notInstalledCount = 0; + + for (var m in modulesInUse) { + if (modulesInUse.hasOwnProperty(m)) { + depsList.editableList('addItem',{ + id: modulesInUse[m].module, + version: modulesInUse[m].version, + count: modulesInUse[m].count, + known: activeProject.dependencies.hasOwnProperty(m), + installed: true + }); + totalCount++; + if (modulesInUse[m].count === 0) { + unusedCount++; + } + if (!activeProject.dependencies.hasOwnProperty(m)) { + unknownCount++; + } + } + } + + if (activeProject.dependencies) { + for (var m in activeProject.dependencies) { + if (activeProject.dependencies.hasOwnProperty(m) && !modulesInUse.hasOwnProperty(m)) { + var installed = !!RED.nodes.registry.getModule(m); + depsList.editableList('addItem',{ + id: m, + version: activeProject.dependencies[m], //RED.nodes.registry.getModule(module).version, + count: 0, + known: true, + installed: installed + }); + totalCount++; + if (installed) { + unusedCount++; + } else { + notInstalledCount++; + } + } + } + } + // if (notInstalledCount > 0) { + // depsList.editableList('addItem',{index:1, label:"Missing dependencies"}); // TODO: nls + // } + // if (unknownCount > 0) { + // depsList.editableList('addItem',{index:1, label:"Unlisted dependencies"}); // TODO: nls + // } + // if (unusedCount > 0) { + // depsList.editableList('addItem',{index:3, label:"Unused dependencies"}); // TODO: nls + // } + if (totalCount === 0) { + depsList.editableList('addItem',{index:0, label:"None"}); // TODO: nls + } + + } + + function saveDependencies(depsList,container,dependencies,complete) { + var activeProject = RED.projects.getActiveProject(); + var spinner = utils.addSpinnerOverlay(container).addClass('projects-dialog-spinner-contain'); + var done = function(err,res) { + spinner.remove(); + if (err) { + return complete(err); + } + activeProject.dependencies = dependencies; + RED.sidebar.versionControl.refresh(true); + complete(); + } + utils.sendRequest({ + url: "projects/"+activeProject.name, + type: "PUT", + responses: { + 0: function(error) { + done(error,null); + }, + 200: function(data) { + RED.sidebar.versionControl.refresh(true); + done(null,data); + }, + 400: { + '*': function(error) { + done(error,null); + } + }, + } + },{dependencies:dependencies}); + } + function editDependencies(activeProject,depsJSON,container,depsList) { + var json = depsJSON||JSON.stringify(activeProject.dependencies||{},"",4); + if (json === "{}") { + json = "{\n\n}"; + } + RED.editor.editJSON({ + title: RED._('sidebar.project.editDependencies'), + value: json, + requireValid: true, + complete: function(v) { + try { + var parsed = JSON.parse(v); + saveDependencies(depsList,container,parsed,function(err) { + if (err) { + return editDependencies(activeProject,v,container,depsList); + } + activeProject.dependencies = parsed; + updateProjectDependencies(activeProject,depsList); + }); + } catch(err) { + editDependencies(activeProject,v,container,depsList); + } + } + }); + } + + function createDependenciesPane(activeProject) { + var pane = $('
        '); + if (RED.user.hasPermission("projects.write")) { + $('') + .appendTo(pane) + .click(function(evt) { + evt.preventDefault(); + editDependencies(activeProject,null,pane,depsList) + }); + } + var depsList = $("
          ",{style:"position: absolute;top: 60px;bottom: 20px;left: 20px;right: 20px;"}).appendTo(pane); + depsList.editableList({ + addButton: false, + addItem: function(row,index,entry) { + // console.log(entry); + var headerRow = $('
          ',{class:"palette-module-header"}).appendTo(row); + if (entry.label) { + if (entry.index === 0) { + headerRow.addClass("red-ui-search-empty") + } else { + row.parent().addClass("palette-module-section"); + } + headerRow.text(entry.label); + // if (RED.user.hasPermission("projects.write")) { + // if (entry.index === 1) { + // var addButton = $('').appendTo(headerRow).click(function(evt) { + // evt.preventDefault(); + // var deps = $.extend(true, {}, activeProject.dependencies); + // for (var m in modulesInUse) { + // if (modulesInUse.hasOwnProperty(m) && !modulesInUse[m].known) { + // deps[m] = modulesInUse[m].version; + // } + // } + // editDependencies(activeProject,JSON.stringify(deps,"",4),pane,depsList); + // }); + // } else if (entry.index === 3) { + // var removeButton = $('').appendTo(headerRow).click(function(evt) { + // evt.preventDefault(); + // var deps = $.extend(true, {}, activeProject.dependencies); + // for (var m in activeProject.dependencies) { + // if (activeProject.dependencies.hasOwnProperty(m) && !modulesInUse.hasOwnProperty(m)) { + // delete deps[m]; + // } + // } + // editDependencies(activeProject,JSON.stringify(deps,"",4),pane,depsList); + // }); + // } + // } + } else { + headerRow.addClass("palette-module-header"); + if (!entry.installed) { + headerRow.addClass("palette-module-not-installed"); + } else if (entry.count === 0) { + headerRow.addClass("palette-module-unused"); + } else if (!entry.known) { + headerRow.addClass("palette-module-unknown"); + } + + entry.element = headerRow; + var titleRow = $('
          ').appendTo(headerRow); + var iconClass = "fa-cube"; + if (!entry.installed) { + iconClass = "fa-warning"; + } + var icon = $('').appendTo(titleRow); + entry.icon = icon; + $('').html(entry.id).appendTo(titleRow); + var metaRow = $('
          ').appendTo(headerRow); + var versionSpan = $('').html(entry.version).appendTo(metaRow); + metaRow = $('
          ').appendTo(headerRow); + var buttons = $('
          ').appendTo(metaRow); + if (RED.user.hasPermission("projects.write")) { + if (!entry.installed && RED.settings.theme('palette.editable') !== false) { + $('install').appendTo(buttons) + .click(function(evt) { + evt.preventDefault(); + RED.palette.editor.install(entry,row,function(err) { + if (!err) { + entry.installed = true; + var spinner = RED.utils.addSpinnerOverlay(row,true); + setTimeout(function() { + depsList.editableList('removeItem',entry); + refreshModuleInUseCounts(); + entry.count = modulesInUse[entry.id].count; + depsList.editableList('addItem',entry); + },500); + } + }); + }) + } else if (entry.known && entry.count === 0) { + $('remove from project').appendTo(buttons) + .click(function(evt) { + evt.preventDefault(); + var deps = $.extend(true, {}, activeProject.dependencies); + delete deps[entry.id]; + saveDependencies(depsList,row,deps,function(err) { + if (!err) { + row.fadeOut(200,function() { + depsList.editableList('removeItem',entry); + }); + } else { + console.log(err); + } + }); + }); + } else if (!entry.known) { + $('add to project').appendTo(buttons) + .click(function(evt) { + evt.preventDefault(); + var deps = $.extend(true, {}, activeProject.dependencies); + deps[entry.id] = modulesInUse[entry.id].version; + saveDependencies(depsList,row,deps,function(err) { + if (!err) { + buttons.remove(); + headerRow.removeClass("palette-module-unknown"); + } else { + console.log(err); + } + }); + }); + } + } + } + }, + sort: function(A,B) { + return A.id.localeCompare(B.id); + // if (A.index && B.index) { + // return A.index - B.index; + // } + // var Acategory = A.index?A.index:(A.known?(A.count>0?0:4):2); + // var Bcategory = B.index?B.index:(B.known?(B.count>0?0:4):2); + // if (Acategory === Bcategory) { + // return A.id.localeCompare(B.id); + // } else { + // return Acategory - Bcategory; + // } + } + }); + + updateProjectDependencies(activeProject,depsList); + return pane; + + } + + function showProjectFileListing(row,activeProject,current,filter,done) { + var dialog; + var dialogBody; + var filesList; + var selected; + var container = $('
          ',{style:"position: relative; min-height: 175px; height: 175px;"}).hide().appendTo(row); + var spinner = utils.addSpinnerOverlay(container); + $.getJSON("projects/"+activeProject.name+"/files",function(result) { + var fileNames = Object.keys(result); + fileNames = fileNames.filter(function(fn) { + return !result[fn].status || !/D/.test(result[fn].status); + }) + var files = {}; + fileNames.sort(); + fileNames.forEach(function(file) { + file.split("/").reduce(function(r,v,i,arr) { if (v) { if (i',{class:"projects-dialog-file-list", style:style}).appendTo(container).editableList({ + addButton: false, + scrollOnAdd: false, + addItem: function(row,index,entry) { + var header = $('
          ',{class:"projects-dialog-file-list-entry"}).appendTo(row); + if (entry.children) { + $(' ').appendTo(header); + if (entry.children.length > 0) { + var children = $('
          ',{style:"padding-left: 20px;"}).appendTo(row); + if (current.indexOf(entry.path+"/") === 0) { + header.addClass("expanded"); + } else { + children.hide(); + } + createFileSubList(children,entry.children,current,filter,onselect); + header.addClass("selectable"); + header.click(function(e) { + if ($(this).hasClass("expanded")) { + $(this).removeClass("expanded"); + children.slideUp(200); + } else { + $(this).addClass("expanded"); + children.slideDown(200); + } + + }); + + } + } else { + var fileIcon = "fa-file-o"; + var fileClass = ""; + if (/\.json$/i.test(entry.name)) { + fileIcon = "fa-file-code-o" + } else if (/\.md$/i.test(entry.name)) { + fileIcon = "fa-book"; + } else if (/^\.git/i.test(entry.name)) { + fileIcon = "fa-code-fork"; + header.addClass("projects-dialog-file-list-entry-file-type-git"); + } + $(' ').appendTo(header); + if (filter.test(entry.name)) { + header.addClass("selectable"); + if (entry.path === current) { + header.addClass("selected"); + } + header.click(function(e) { + $(".projects-dialog-file-list-entry.selected").removeClass("selected"); + $(this).addClass("selected"); + onselect(entry.path); + }) + header.dblclick(function(e) { + e.preventDefault(); + onselect(entry.path,true); + }) + } else { + header.addClass("unselectable"); + } + } + $('').text(entry.name).appendTo(header); + } + }); + if (!style) { + list.parent().css("overflow-y",""); + } + files.forEach(function(f) { + list.editableList('addItem',f); + }) + } + + // function editFiles(activeProject, container,flowFile, flowFileLabel) { + // var editButton = container.children().first(); + // editButton.hide(); + // + // var flowFileInput = $('').val(flowFile).insertAfter(flowFileLabel); + // + // var flowFileInputSearch = $('') + // .insertAfter(flowFileInput) + // .click(function(e) { + // showProjectFileListing(activeProject,'Select flow file',flowFileInput.val(),function(result) { + // flowFileInput.val(result); + // checkFiles(); + // }) + // }) + // + // var checkFiles = function() { + // saveButton.toggleClass('disabled',flowFileInput.val()===""); + // saveButton.prop('disabled',flowFileInput.val()===""); + // } + // flowFileInput.on("change keyup paste",checkFiles); + // flowFileLabel.hide(); + // + // var bg = $('').prependTo(container); + // $('') + // .appendTo(bg) + // .click(function(evt) { + // evt.preventDefault(); + // + // flowFileLabel.show(); + // flowFileInput.remove(); + // flowFileInputSearch.remove(); + // bg.remove(); + // editButton.show(); + // }); + // var saveButton = $('') + // .appendTo(bg) + // .click(function(evt) { + // evt.preventDefault(); + // var newFlowFile = flowFileInput.val(); + // var newCredsFile = credentialsFileInput.val(); + // var spinner = utils.addSpinnerOverlay(container); + // var done = function(err,res) { + // if (err) { + // spinner.remove(); + // return; + // } + // activeProject.summary = v; + // spinner.remove(); + // flowFileLabel.text(newFlowFile); + // flowFileLabel.show(); + // flowFileInput.remove(); + // flowFileInputSearch.remove(); + // bg.remove(); + // editButton.show(); + // } + // // utils.sendRequest({ + // // url: "projects/"+activeProject.name, + // // type: "PUT", + // // responses: { + // // 0: function(error) { + // // done(error,null); + // // }, + // // 200: function(data) { + // // done(null,data); + // // }, + // // 400: { + // // 'unexpected_error': function(error) { + // // done(error,null); + // // } + // // }, + // // } + // // },{summary:v}); + // }); + // + // + // checkFiles(); + // + // } + + function createFilesSection(activeProject,pane) { + var title = $('

          ').text("Files").appendTo(pane); + var filesContainer = $('').appendTo(pane); + if (RED.user.hasPermission("projects.write")) { + var editFilesButton = $('') + .appendTo(title) + .click(function(evt) { + evt.preventDefault(); + formButtons.show(); + editFilesButton.hide(); + flowFileLabelText.hide(); + flowFileInput.show(); + flowFileInputSearch.show(); + credFileLabel.hide(); + credFileInput.show(); + flowFileInput.focus(); + // credentialStateLabel.parent().hide(); + credentialStateLabel.addClass("uneditable-input"); + $(".user-settings-row-credentials").show(); + credentialStateLabel.css('height','auto'); + credentialFormRows.hide(); + credentialSecretButtons.show(); + }); + } + var row; + + // Flow files + row = $('').appendTo(filesContainer); + $('').text('Flow').appendTo(row); + var flowFileLabel = $('
          ').appendTo(row); + var flowFileLabelText = $('').text(activeProject.files.flow).appendTo(flowFileLabel); + + var flowFileInput = $('').val(activeProject.files.flow).hide().appendTo(flowFileLabel); + var flowFileInputSearch = $('') + .hide() + .appendTo(flowFileLabel) + .click(function(e) { + if ($(this).hasClass('selected')) { + $(this).removeClass('selected'); + flowFileLabel.find('.project-file-listing-container').slideUp(200,function() { + $(this).remove(); + flowFileLabel.css('height',''); + }); + flowFileLabel.css('color',''); + } else { + $(this).addClass('selected'); + flowFileLabel.css('color','inherit'); + var fileList = showProjectFileListing(flowFileLabel,activeProject,flowFileInput.val(), /.*\.json$/,function(result,isDblClick) { + if (result) { + flowFileInput.val(result); + } + if (isDblClick) { + $(flowFileInputSearch).click(); + } + checkFiles(); + }); + flowFileLabel.css('height','auto'); + setTimeout(function() { + fileList.slideDown(200); + },50); + + } + }) + + row = $('').appendTo(filesContainer); + $('').text('Credentials').appendTo(row); + var credFileLabel = $('
          ').text(activeProject.files.credentials).appendTo(row); + var credFileInput = $('
          ').text(activeProject.files.credentials).hide().insertAfter(credFileLabel); + + var checkFiles = function() { + var saveDisabled; + var currentFlowValue = flowFileInput.val(); + var m = /^(.+?)(\.[^.]*)?$/.exec(currentFlowValue); + if (m) { + credFileInput.text(m[1]+"_cred"+(m[2]||".json")); + } else if (currentFlowValue === "") { + credFileInput.text(""); + } + var isFlowInvalid = currentFlowValue==="" || + /\.\./.test(currentFlowValue) || + /\/$/.test(currentFlowValue); + + saveDisabled = isFlowInvalid || credFileInput.text()===""; + + if (credentialSecretExistingInput.is(":visible")) { + credentialSecretExistingInput.toggleClass("input-error", credentialSecretExistingInput.val() === ""); + saveDisabled = saveDisabled || credentialSecretExistingInput.val() === ""; + } + if (credentialSecretNewInput.is(":visible")) { + credentialSecretNewInput.toggleClass("input-error", credentialSecretNewInput.val() === ""); + saveDisabled = saveDisabled || credentialSecretNewInput.val() === ""; + } + + + flowFileInput.toggleClass("input-error", isFlowInvalid); + credFileInput.toggleClass("input-error",credFileInput.text()===""); + saveButton.toggleClass('disabled',saveDisabled); + saveButton.prop('disabled',saveDisabled); + } + flowFileInput.on("change keyup paste",checkFiles); + + + if (!activeProject.files.flow) { + $(' Missing').appendTo(flowFileLabelText); + } + if (!activeProject.files.credentials) { + $(' Missing').appendTo(credFileLabel); + } + + + row = $('').appendTo(filesContainer); + + $('').appendTo(row); + var credentialStateLabel = $(' ').appendTo(row); + var credentialSecretButtons = $('').hide().appendTo(row); + + credentialStateLabel.css('color','#666'); + credentialSecretButtons.css('vertical-align','top'); + var credentialSecretResetButton = $('') + .appendTo(credentialSecretButtons) + .click(function(e) { + e.preventDefault(); + if (!$(this).hasClass('selected')) { + credentialSecretNewInput.val(""); + credentialSecretExistingRow.hide(); + credentialSecretNewRow.show(); + $(this).addClass("selected"); + credentialSecretEditButton.removeClass("selected"); + credentialResetLabel.show(); + credentialResetWarning.show(); + credentialSetLabel.hide(); + credentialChangeLabel.hide(); + + credentialFormRows.show(); + } else { + $(this).removeClass("selected"); + credentialFormRows.hide(); + } + checkFiles(); + }); + var credentialSecretEditButton = $('') + .appendTo(credentialSecretButtons) + .click(function(e) { + e.preventDefault(); + if (!$(this).hasClass('selected')) { + credentialSecretExistingInput.val(""); + credentialSecretNewInput.val(""); + if (activeProject.settings.credentialSecretInvalid || !activeProject.settings.credentialsEncrypted) { + credentialSetLabel.show(); + credentialChangeLabel.hide(); + credentialSecretExistingRow.hide(); + } else { + credentialSecretExistingRow.show(); + credentialSetLabel.hide(); + credentialChangeLabel.show(); + } + credentialSecretNewRow.show(); + credentialSecretEditButton.addClass("selected"); + credentialSecretResetButton.removeClass("selected"); + + credentialResetLabel.hide(); + credentialResetWarning.hide(); + credentialFormRows.show(); + } else { + $(this).removeClass("selected"); + credentialFormRows.hide(); + } + checkFiles(); + }) + + + row = $('').hide().appendTo(filesContainer); + + + + var credentialFormRows = $('
          ',{style:"margin-top:10px"}).hide().appendTo(credentialStateLabel); + + var credentialSetLabel = $('
          Set the encryption key:
          ').hide().appendTo(credentialFormRows); + var credentialChangeLabel = $('
          Change the encryption key:
          ').hide().appendTo(credentialFormRows); + var credentialResetLabel = $('
          Reset the encryption key:
          ').hide().appendTo(credentialFormRows); + + var credentialSecretExistingRow = $('').appendTo(credentialFormRows); + $('').text('Current key').appendTo(credentialSecretExistingRow); + var credentialSecretExistingInput = $('').appendTo(credentialSecretExistingRow) + .on("change keyup paste",function() { + if (popover) { + popover.close(); + popover = null; + } + checkFiles(); + }); + + var credentialSecretNewRow = $('').appendTo(credentialFormRows); + + + $('').text('New key').appendTo(credentialSecretNewRow); + var credentialSecretNewInput = $('').appendTo(credentialSecretNewRow).on("change keyup paste",checkFiles); + + var credentialResetWarning = $('
          This will delete all existing credentials
          ').hide().appendTo(credentialFormRows); + + + var hideEditForm = function() { + editFilesButton.show(); + formButtons.hide(); + flowFileLabelText.show(); + flowFileInput.hide(); + flowFileInputSearch.hide(); + credFileLabel.show(); + credFileInput.hide(); + // credentialStateLabel.parent().show(); + credentialStateLabel.removeClass("uneditable-input"); + credentialStateLabel.css('height',''); + + flowFileInputSearch.removeClass('selected'); + flowFileLabel.find('.project-file-listing-container').remove(); + flowFileLabel.css('height',''); + flowFileLabel.css('color',''); + + $(".user-settings-row-credentials").hide(); + credentialFormRows.hide(); + credentialSecretButtons.hide(); + credentialSecretResetButton.removeClass("selected"); + credentialSecretEditButton.removeClass("selected"); + + + } + + var formButtons = $('').hide().appendTo(filesContainer); + $('') + .appendTo(formButtons) + .click(function(evt) { + evt.preventDefault(); + hideEditForm(); + }); + var saveButton = $('') + .appendTo(formButtons) + .click(function(evt) { + evt.preventDefault(); + var spinner = utils.addSpinnerOverlay(filesContainer); + var done = function(err) { + spinner.remove(); + if (err) { + console.log(err); + return; + } + flowFileLabelText.text(flowFileInput.val()); + credFileLabel.text(credFileInput.text()); + hideEditForm(); + } + var payload = { + files: { + flow: flowFileInput.val(), + credentials: credFileInput.text() + } + } + + if (credentialSecretResetButton.hasClass('selected')) { + payload.resetCredentialSecret = true; + } + if (credentialSecretResetButton.hasClass('selected') || credentialSecretEditButton.hasClass('selected')) { + payload.credentialSecret = credentialSecretNewInput.val(); + if (credentialSecretExistingInput.is(":visible")) { + payload.currentCredentialSecret = credentialSecretExistingInput.val(); + } + } + + // console.log(JSON.stringify(payload,null,4)); + RED.deploy.setDeployInflight(true); + utils.sendRequest({ + url: "projects/"+activeProject.name, + type: "PUT", + responses: { + 0: function(error) { + done(error); + }, + 200: function(data) { + activeProject = data; + RED.sidebar.versionControl.refresh(true); + updateForm(); + done(); + }, + 400: { + 'credentials_load_failed': function(error) { + done(error); + }, + 'unexpected_error': function(error) { + console.log(error); + done(error); + }, + 'missing_current_credential_key': function(error) { + credentialSecretExistingInput.addClass("input-error"); + popover = RED.popover.create({ + target: credentialSecretExistingInput, + direction: 'right', + size: 'small', + content: "Incorrect key", + autoClose: 3000 + }).open(); + done(error); + } + }, + } + },payload).always(function() { + RED.deploy.setDeployInflight(false); + }); + }); + var updateForm = function() { + if (activeProject.settings.credentialSecretInvalid) { + credentialStateLabel.find(".user-settings-credentials-state-icon").removeClass().addClass("user-settings-credentials-state-icon fa fa-warning"); + credentialStateLabel.find(".user-settings-credentials-state").text("Invalid encryption key"); + } else if (activeProject.settings.credentialsEncrypted) { + credentialStateLabel.find(".user-settings-credentials-state-icon").removeClass().addClass("user-settings-credentials-state-icon fa fa-lock"); + credentialStateLabel.find(".user-settings-credentials-state").text("Encryption enabled"); + } else { + credentialStateLabel.find(".user-settings-credentials-state-icon").removeClass().addClass("user-settings-credentials-state-icon fa fa-unlock"); + credentialStateLabel.find(".user-settings-credentials-state").text("Encryption disabled"); + } + credentialSecretResetButton.toggleClass('disabled',!activeProject.settings.credentialSecretInvalid && !activeProject.settings.credentialsEncrypted); + credentialSecretResetButton.prop('disabled',!activeProject.settings.credentialSecretInvalid && !activeProject.settings.credentialsEncrypted); + } + + checkFiles(); + updateForm(); + } + + function createLocalBranchListSection(activeProject,pane) { + var localBranchContainer = $('').appendTo(pane); + $('

          ').text("Branches").appendTo(localBranchContainer); + + var row = $('').appendTo(localBranchContainer); + + + var branchList = $('
            ').appendTo(row).editableList({ + height: 'auto', + addButton: false, + scrollOnAdd: false, + addItem: function(row,index,entry) { + var container = $('
            ').appendTo(row); + if (entry.empty) { + container.addClass('red-ui-search-empty'); + container.text("No branches"); + return; + } + if (entry.current) { + container.addClass("current"); + } + $('').appendTo(container); + var content = $('').appendTo(container); + var topRow = $('
            ').appendTo(content); + $('').text(entry.name).appendTo(topRow); + if (entry.commit) { + $('').text(entry.commit.sha).appendTo(topRow); + } + if (entry.remote) { + var bottomRow = $('
            ').appendTo(content); + + $('').text(entry.remote||"").appendTo(bottomRow); + if (entry.status.ahead+entry.status.behind > 0) { + $(''+ + ' '+entry.status.ahead+' '+ + ' '+entry.status.behind+''+ + '').appendTo(bottomRow); + } + } + + if (!entry.current) { + var tools = $('').appendTo(container); + $('') + .appendTo(tools) + .click(function(e) { + e.preventDefault(); + var spinner = utils.addSpinnerOverlay(row).addClass('projects-dialog-spinner-contain'); + var notification = RED.notify("Are you sure you want to delete the local branch '"+entry.name+"'? This cannot be undone.", { + type: "warning", + modal: true, + fixed: true, + buttons: [ + { + text: RED._("common.label.cancel"), + click: function() { + spinner.remove(); + notification.close(); + } + },{ + text: 'Delete branch', + click: function() { + notification.close(); + var url = "projects/"+activeProject.name+"/branches/"+entry.name; + var options = { + url: url, + type: "DELETE", + responses: { + 200: function(data) { + row.fadeOut(200,function() { + branchList.editableList('removeItem',entry); + spinner.remove(); + }); + }, + 400: { + 'git_delete_branch_unmerged': function(error) { + notification = RED.notify("The local branch '"+entry.name+"' has unmerged changes that will be lost. Are you sure you want to delete it?", { + type: "warning", + modal: true, + fixed: true, + buttons: [ + { + text: RED._("common.label.cancel"), + click: function() { + spinner.remove(); + notification.close(); + } + },{ + text: 'Delete unmerged branch', + click: function() { + options.url += "?force=true"; + notification.close(); + utils.sendRequest(options); + } + } + ] + }); + }, + 'unexpected_error': function(error) { + console.log(error); + spinner.remove(); + } + }, + } + } + utils.sendRequest(options); + } + } + + ] + }) + }) + } + + } + }); + + $.getJSON("projects/"+activeProject.name+"/branches",function(result) { + if (result.branches) { + if (result.branches.length > 0) { + result.branches.sort(function(A,B) { + if (A.current) { return -1 } + if (B.current) { return 1 } + return A.name.localeCompare(B.name); + }); + result.branches.forEach(function(branch) { + branchList.editableList('addItem',branch); + }) + } else { + branchList.editableList('addItem',{empty:true}); + } + } + }) + } + + function createRemoteRepositorySection(activeProject,pane) { + $('

            ').text("Version Control").appendTo(pane); + + createLocalBranchListSection(activeProject,pane); + + var repoContainer = $('').appendTo(pane); + var title = $('

            ').text("Git remotes").appendTo(repoContainer); + + var editRepoButton = $('') + .appendTo(title) + .click(function(evt) { + editRepoButton.attr('disabled',true); + addRemoteDialog.slideDown(200, function() { + addRemoteDialog[0].scrollIntoView(); + }); + }); + + + var emptyItem = { empty: true }; + var row = $('').appendTo(repoContainer); + var addRemoteDialog = $('
            ').hide().appendTo(row); + row = $('').appendTo(repoContainer); + var remotesList = $('
              ').appendTo(row); + remotesList.editableList({ + addButton: false, + height: 'auto', + addItem: function(row,index,entry) { + + var container = $('
              ').appendTo(row); + if (entry.empty) { + container.addClass('red-ui-search-empty'); + container.text("No remotes"); + return; + } else { + $('').appendTo(container); + var content = $('').appendTo(container); + $('
              ').text(entry.name).appendTo(content); + if (entry.urls.fetch === entry.urls.push) { + $('
              ').text(entry.urls.fetch).appendTo(content); + } else { + $('
              ').text("fetch: "+entry.urls.fetch).appendTo(content); + $('
              ').text("push: "+entry.urls.push).appendTo(content); + + } + var tools = $('').appendTo(container); + $('') + .appendTo(tools) + .click(function(e) { + e.preventDefault(); + var spinner = utils.addSpinnerOverlay(row).addClass('projects-dialog-spinner-contain'); + var notification = RED.notify("Are you sure you want to delete the remote '"+entry.name+"'?", { + type: "warning", + modal: true, + fixed: true, + buttons: [ + { + text: RED._("common.label.cancel"), + click: function() { + spinner.remove(); + notification.close(); + } + },{ + text: 'Delete remote', + click: function() { + notification.close(); + var url = "projects/"+activeProject.name+"/remotes/"+entry.name; + var options = { + url: url, + type: "DELETE", + responses: { + 200: function(data) { + row.fadeOut(200,function() { + remotesList.editableList('removeItem',entry); + setTimeout(spinner.remove, 100); + activeProject.git.remotes = {}; + data.remotes.forEach(function(remote) { + var name = remote.name; + delete remote.name; + activeProject.git.remotes[name] = remote; + }); + if (data.remotes.length === 0) { + remotesList.editableList('addItem',emptyItem); + } + }); + }, + 400: { + 'unexpected_error': function(error) { + console.log(error); + spinner.remove(); + } + }, + } + } + utils.sendRequest(options); + } + } + + ] + }) + }); + } + + + } + }); + + var validateForm = function() { + var validName = /^[a-zA-Z0-9\-_]+$/.test(remoteNameInput.val()); + var validRepo = /^(?:git|ssh|https?|[\d\w\.\-_]+@[\w\.]+):(?:\/\/)?[\w\.@:\/~_-]+\.git(?:\/?|\#[\d\w\.\-_]+?)$/.test(remoteURLInput.val()); + saveButton.attr('disabled',(!validName || !validRepo)) + remoteNameInput.toggleClass('input-error',remoteNameInputChanged&&!validName); + if (popover) { + popover.close(); + popover = null; + } + }; + var popover; + + $('
              ').text('Add remote').appendTo(addRemoteDialog); + + row = $('').appendTo(addRemoteDialog); + $('').text('Remote name').appendTo(row); + var remoteNameInput = $('').appendTo(row).on("change keyup paste",function() { + remoteNameInputChanged = true; + validateForm(); + }); + var remoteNameInputChanged = false; + $('').appendTo(row).find("small"); + row = $('').appendTo(addRemoteDialog); + $('').text('URL').appendTo(row); + var remoteURLInput = $('').appendTo(row).on("change keyup paste",validateForm); + + var hideEditForm = function() { + editRepoButton.attr('disabled',false); + addRemoteDialog.hide(); + remoteNameInput.val(""); + remoteURLInput.val(""); + if (popover) { + popover.close(); + popover = null; + } + } + var formButtons = $('') + .appendTo(addRemoteDialog); + $('') + .appendTo(formButtons) + .click(function(evt) { + evt.preventDefault(); + hideEditForm(); + }); + var saveButton = $('') + .appendTo(formButtons) + .click(function(evt) { + evt.preventDefault(); + var spinner = utils.addSpinnerOverlay(addRemoteDialog).addClass('projects-dialog-spinner-contain'); + + var payload = { + name: remoteNameInput.val(), + url: remoteURLInput.val() + } + var done = function(err) { + spinner.remove(); + if (err) { + return; + } + hideEditForm(); + } + // console.log(JSON.stringify(payload,null,4)); + RED.deploy.setDeployInflight(true); + utils.sendRequest({ + url: "projects/"+activeProject.name+"/remotes", + type: "POST", + responses: { + 0: function(error) { + done(error); + }, + 200: function(data) { + activeProject.git.remotes = {}; + data.remotes.forEach(function(remote) { + var name = remote.name; + delete remote.name; + activeProject.git.remotes[name] = remote; + }); + updateForm(); + done(); + }, + 400: { + 'git_remote_already_exists': function(error) { + popover = RED.popover.create({ + target: remoteNameInput, + direction: 'right', + size: 'small', + content: "Remote already exists", + autoClose: 6000 + }).open(); + remoteNameInput.addClass('input-error'); + done(error); + }, + 'unexpected_error': function(error) { + console.log(error); + done(error); + } + }, + } + },payload); + }); + var updateForm = function() { + remotesList.editableList('empty'); + var count = 0; + if (activeProject.git.hasOwnProperty('remotes')) { + for (var name in activeProject.git.remotes) { + if (activeProject.git.remotes.hasOwnProperty(name)) { + count++; + remotesList.editableList('addItem',{name:name,urls:activeProject.git.remotes[name]}); + } + } + } + if (count === 0) { + remotesList.editableList('addItem',emptyItem); + } + } + updateForm(); + } + + + + function createSettingsPane(activeProject) { + var pane = $('
              '); + createFilesSection(activeProject,pane); + // createLocalRepositorySection(activeProject,pane); + createRemoteRepositorySection(activeProject,pane); + return pane; + } + + function refreshModuleInUseCounts() { + modulesInUse = {}; + RED.nodes.eachNode(_updateModulesInUse); + RED.nodes.eachConfig(_updateModulesInUse); + } + + function _updateModulesInUse(n) { + if (!/^subflow:/.test(n.type)) { + var module = RED.nodes.registry.getNodeSetForType(n.type).module; + if (module !== 'node-red') { + if (!modulesInUse.hasOwnProperty(module)) { + modulesInUse[module] = { + module: module, + version: RED.nodes.registry.getModule(module).version, + count: 0, + known: false + } + } + modulesInUse[module].count++; + } + } + } + + var popover; + var utils; + var modulesInUse = {}; + function init(_utils) { + utils = _utils; + addPane({ + id:'main', + title: "Project", // TODO: nls + get: createMainPane, + close: function() { } + }); + addPane({ + id:'deps', + title: "Dependencies", // TODO: nls + get: createDependenciesPane, + close: function() { } + }); + addPane({ + id:'settings', + title: "Settings", // TODO: nls + get: createSettingsPane, + close: function() { + if (popover) { + popover.close(); + popover = null; + } + } + }); + + RED.events.on('nodes:add', _updateModulesInUse); + RED.events.on('nodes:remove', function(n) { + if (!/^subflow:/.test(n.type)) { + var module = RED.nodes.registry.getNodeSetForType(n.type).module; + if (module !== 'node-red' && modulesInUse.hasOwnProperty(module)) { + modulesInUse[module].count--; + if (modulesInUse[module].count === 0) { + if (!modulesInUse[module].known) { + delete modulesInUse[module]; + } + } + } + } + }) + + + + } + return { + init: init, + show: show, + switchProject: function(name) { + // TODO: not ideal way to trigger this; should there be an editor-wide event? + modulesInUse = {}; + } + }; +})(); diff --git a/editor/js/ui/projects/projectUserSettings.js b/editor/js/ui/projects/projectUserSettings.js new file mode 100644 index 000000000..4c3840461 --- /dev/null +++ b/editor/js/ui/projects/projectUserSettings.js @@ -0,0 +1,418 @@ +/** + * Copyright JS Foundation and other contributors, http://js.foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + **/ + +RED.projects.userSettings = (function() { + + var gitUsernameInput; + var gitEmailInput; + + function createGitUserSection(pane) { + + var currentGitSettings = RED.settings.get('git') || {}; + currentGitSettings.user = currentGitSettings.user || {}; + + var title = $('

              ').text("Committer Details").appendTo(pane); + + var gitconfigContainer = $('').appendTo(pane); + $('
              ').appendTo(gitconfigContainer).text("Leave blank to use system default"); + + var row = $('').appendTo(gitconfigContainer); + $('').text('Username').appendTo(row); + gitUsernameInput = $('').appendTo(row); + gitUsernameInput.val(currentGitSettings.user.name||""); + + row = $('').appendTo(gitconfigContainer); + $('').text('Email').appendTo(row); + gitEmailInput = $('').appendTo(row); + gitEmailInput.val(currentGitSettings.user.email||""); + } + + + function createSSHKeySection(pane) { + var container = $('').appendTo(pane); + var popover; + var title = $('

              ').text("SSH Keys").appendTo(container); + var subtitle = $('
              ').appendTo(container).text("Allows you to create secure connections to remote git repositories."); + + var addKeyButton = $('') + .appendTo(subtitle) + .click(function(evt) { + addKeyButton.attr('disabled',true); + saveButton.attr('disabled',true); + // bg.children().removeClass("selected"); + // addLocalButton.click(); + addKeyDialog.slideDown(200); + keyNameInput.focus(); + }); + + var validateForm = function() { + var valid = /^[a-zA-Z0-9\-_]+$/.test(keyNameInput.val()); + keyNameInput.toggleClass('input-error',keyNameInputChanged&&!valid); + + // var selectedButton = bg.find(".selected"); + // if (selectedButton[0] === addLocalButton[0]) { + // valid = valid && localPublicKeyPathInput.val().length > 0 && localPrivateKeyPathInput.val().length > 0; + // } else if (selectedButton[0] === uploadButton[0]) { + // valid = valid && publicKeyInput.val().length > 0 && privateKeyInput.val().length > 0; + // } else if (selectedButton[0] === generateButton[0]) { + var passphrase = passphraseInput.val(); + var validPassphrase = passphrase.length === 0 || passphrase.length >= 8; + passphraseInput.toggleClass('input-error',!validPassphrase); + if (!validPassphrase) { + passphraseInputSubLabel.text("Passphrase too short"); + } else if (passphrase.length === 0) { + passphraseInputSubLabel.text("Optional"); + } else { + passphraseInputSubLabel.text(""); + } + valid = valid && validPassphrase; + // } + + saveButton.attr('disabled',!valid); + + if (popover) { + popover.close(); + popover = null; + } + }; + + var row = $('').appendTo(container); + var addKeyDialog = $('
              ').hide().appendTo(row); + $('
              ').text('Add SSH Key').appendTo(addKeyDialog); + var addKeyDialogBody = $('
              ').appendTo(addKeyDialog); + + row = $('').appendTo(addKeyDialogBody); + $('
              ').appendTo(row).text("Generate a new public/private key pair"); + // var bg = $('
              ',{class:"button-group", style:"text-align: center"}).appendTo(row); + // var addLocalButton = $('').appendTo(bg); + // var uploadButton = $('').appendTo(bg); + // var generateButton = $('').appendTo(bg); + // bg.children().click(function(e) { + // e.preventDefault(); + // if ($(this).hasClass("selected")) { + // return; + // } + // bg.children().removeClass("selected"); + // $(this).addClass("selected"); + // if (this === addLocalButton[0]) { + // addLocalKeyPane.show(); + // generateKeyPane.hide(); + // uploadKeyPane.hide(); + // } else if (this === uploadButton[0]) { + // addLocalKeyPane.hide(); + // generateKeyPane.hide(); + // uploadKeyPane.show(); + // } else if (this === generateButton[0]){ + // addLocalKeyPane.hide(); + // generateKeyPane.show(); + // uploadKeyPane.hide(); + // } + // validateForm(); + // }) + + + row = $('').appendTo(addKeyDialogBody); + $('').text('Name').appendTo(row); + var keyNameInputChanged = false; + var keyNameInput = $('').appendTo(row).on("change keyup paste",function() { + keyNameInputChanged = true; + validateForm(); + }); + $('').appendTo(row).find("small"); + + var generateKeyPane = $('
              ').appendTo(addKeyDialogBody); + row = $('').appendTo(generateKeyPane); + $('').text('Passphrase').appendTo(row); + var passphraseInput = $('').appendTo(row).on("change keyup paste",validateForm); + var passphraseInputSubLabel = $('').appendTo(row).find("small"); + + // var addLocalKeyPane = $('
              ').hide().appendTo(addKeyDialogBody); + // row = $('').appendTo(addLocalKeyPane); + // $('').text('Public key').appendTo(row); + // var localPublicKeyPathInput = $('').appendTo(row).on("change keyup paste",validateForm); + // $('').appendTo(row).find("small"); + // row = $('').appendTo(addLocalKeyPane); + // $('').text('Private key').appendTo(row); + // var localPrivateKeyPathInput = $('').appendTo(row).on("change keyup paste",validateForm); + // $('').appendTo(row).find("small"); + // + // var uploadKeyPane = $('
              ').hide().appendTo(addKeyDialogBody); + // row = $('').appendTo(uploadKeyPane); + // $('').text('Public key').appendTo(row); + // var publicKeyInput = $('