diff --git a/editor/js/main.js b/editor/js/main.js index 4573d57aa..e9eea4b58 100644 --- a/editor/js/main.js +++ b/editor/js/main.js @@ -44,10 +44,21 @@ $("#palette-search").removeClass("hide"); loadFlows(function() { if (RED.settings.theme("projects.enabled",true)) { - RED.projects.refresh(function() { + 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); + RED.menu.setDisabled('menu-item-projects-delete',true); + if (activeProject === false) { + // User previously decline the migration to projects. + } else { // null/undefined + RED.projects.showStartup(); + } + } }); } else { + // Projects disabled by the user RED.sidebar.info.refresh() } @@ -218,7 +229,7 @@ function loadEditor() { var menuOptions = []; if (RED.settings.theme("projects.enabled",true)) { - menuOptions.push({id:"menu-item-projects-menu",label:"NLS: Projects",options:[ + 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"}, {id:"menu-item-projects-delete",label:"Delete...",disabled:false,onselect:"core:delete-project"}, diff --git a/editor/js/ui/projects.js b/editor/js/ui/projects.js index 903ead70b..72ed1a5af 100644 --- a/editor/js/ui/projects.js +++ b/editor/js/ui/projects.js @@ -22,24 +22,435 @@ RED.projects = (function() { var screens = {}; function initScreens() { + var migrateProjectHeader = $('
'); + $('         ').appendTo(migrateProjectHeader) + $('
').appendTo(migrateProjectHeader); + + var createProjectOptions = {}; + screens = { 'welcome': { - content: function() { + content: function(options) { + var container = $('
'); - var buttons = $('
').appendTo(container); - var createNew = $('').appendTo(buttons); - createNew.click(function(e) { - e.preventDefault(); - show('create'); - }); - var openExisting = $('').appendTo(buttons); - openExisting.click(function(e) { - e.preventDefault(); - show('open') - }); + + migrateProjectHeader.appendTo(container); + + var body = $('
').appendTo(container); + $('

').text("Hello! We have introduced 'projects' to Node-RED.").appendTo(body); + $('

').text("This is a new way for you to manage your flow files and includes version control of your flows.").appendTo(body); + $('

').text("To get started you can create your first project using your current flow files in a few easy steps.").appendTo(body); + $('

').text("If you are not sure, you can skip this for now. You will still be able to create your first project from the 'Projects' menu option at any time.").appendTo(body); + + // var buttons = $('

').appendTo(container); + // var createNew = $('').appendTo(buttons); + // createNew.click(function(e) { + // e.preventDefault(); + // show('create'); + // }); + // var openExisting = $('').appendTo(buttons); + // openExisting.click(function(e) { + // e.preventDefault(); + // show('open') + // }); return container; }, buttons: [ + { + // id: "clipboard-dialog-cancel", + text: "Not right now", + click: function() { + createProjectOptions = {}; + $( this ).dialog( "close" ); + } + }, + { + text: "Create your first project", // TODO: nls + class: "primary", + click: function() { + show('git-config'); + } + } + ] + }, + 'git-config': (function() { + var gitUsernameInput; + var gitEmailInput; + return { + content: function(options) { + + var container = $('
'); + migrateProjectHeader.appendTo(container); + var body = $('
').appendTo(container); + + $('

').text("Step 1 : Setup your version control client").appendTo(body); + $('

').text("Node-RED uses the open source tool Git for version control. It tracks changes to your project files and lets you push them to remote repositories.").appendTo(body); + $('

').text("When you commit a set of changes, Git records who made the changes with a username and email address. The Username can be anything you want - it does not need to be your real name.").appendTo(body); + $('

').text("If your Git client is already configured, you can skip this step.").appendTo(body); + + var currentGitSettings = RED.settings.get('git') || {}; + currentGitSettings.user = currentGitSettings.user || {}; + + + var row = $('

').appendTo(body); + $('').appendTo(row); + gitUsernameInput = $('').val(currentGitSettings.user.name||"").appendTo(row); + // $('
').text("This does not need to be your real name").appendTo(row); + + row = $('
').appendTo(body); + $('').appendTo(row); + gitEmailInput = $('').val(currentGitSettings.user.email||"").appendTo(row); + // $('
').text("Something something email").appendTo(row); + setTimeout(function() { + gitUsernameInput.focus(); + },50); + return container; + }, + buttons: [ + { + // id: "clipboard-dialog-cancel", + text: "Back", + click: function() { + show('welcome'); + } + }, + { + text: "Next", // TODO: nls + class: "primary", + click: function() { + var currentGitSettings = RED.settings.get('git') || {}; + currentGitSettings.user = currentGitSettings.user || {}; + currentGitSettings.user.name = gitUsernameInput.val(); + currentGitSettings.user.email = gitEmailInput.val(); + RED.settings.set('git', currentGitSettings); + show('project-details'); + } + } + ] + }; + })(), + 'project-details': (function() { + var projectNameInput; + var projectSummaryInput; + return { + content: function(options) { + + var container = $('
'); + migrateProjectHeader.appendTo(container); + var body = $('
').appendTo(container); + + $('

').text("Step 2 : Create your project").appendTo(body); + $('

').text("A project contains your flow files, a README file, a package.json file and a settings file. It makes it much easier to share your flows with others and to collaborate on them.").appendTo(body); + $('

').text("You can create multiple projects and quickly switch between them from the editor.").appendTo(body); + $('

').text("To begin, your project needs a name and an optional description.").appendTo(body); + + var validateForm = function() { + var projectName = projectNameInput.val(); + var valid = true; + if (projectNameInputChanged) { + projectNameStatus.empty(); + if (!/^[a-zA-Z0-9\-_]+$/.test(projectName)) { + projectNameInput.addClass("input-error"); + $('').appendTo(projectNameStatus); + projectNameValid = false; + valid = false; + } else { + projectNameInput.removeClass("input-error"); + $('').appendTo(projectNameStatus); + projectNameValid = true; + } + projectNameLastChecked = projectName; + } + valid = projectNameValid; + $("#projects-dialog-create-name").prop('disabled',!valid).toggleClass('disabled ui-button-disabled ui-state-disabled',!valid); + } + + var row = $('

').appendTo(body); + $('').appendTo(row); + + var subrow = $('
').appendTo(row); + projectNameInput = $('').val(createProjectOptions.name||"").appendTo(subrow); + var projectNameStatus = $('
').appendTo(subrow); + + var projectNameInputChanged = false; + var projectNameLastChecked = ""; + var projectNameValid; + var checkProjectName; + var autoInsertedName = ""; + + + projectNameInput.on("change keyup paste",function() { + projectNameInputChanged = (projectNameInput.val() !== projectNameLastChecked); + if (checkProjectName) { + clearTimeout(checkProjectName); + } else if (projectNameInputChanged) { + projectNameStatus.empty(); + $('').appendTo(projectNameStatus); + if (projectNameInput.val() === '') { + validateForm(); + return; + } + } + checkProjectName = setTimeout(function() { + validateForm(); + checkProjectName = null; + },300) + }); + projectNameSublabel = $('').appendTo(row).find("small"); + + // Empty Project + row = $('
').appendTo(body); + $('').appendTo(row); + projectSummaryInput = $('').val(createProjectOptions.summary||"").appendTo(row); + $('').appendTo(row); + + setTimeout(function() { + projectNameInput.focus(); + },50); + return container; + }, + buttons: [ + { + text: "Back", + click: function() { + show('git-config'); + } + }, + { + id: "projects-dialog-create-name", + disabled: true, + text: "Next", // TODO: nls + class: "primary disabled", + click: function() { + createProjectOptions.name = projectNameInput.val(); + createProjectOptions.summary = projectSummaryInput.val(); + createProjectOptions.files = { migrateFiles: true }; + show('encryption-config'); + } + } + ] + }; + })(), + + 'encryption-config': (function() { + var emptyProjectCredentialInput; + return { + content: function(options) { + + var container = $('
'); + migrateProjectHeader.appendTo(container); + var body = $('
').appendTo(container); + + $('

').text("Step 3 : Setup encryption of your credentials file").appendTo(body); + // $('

').text("Your flow credentials file can be encrypted to keep its contents secure.").appendTo(body); + + if (RED.settings.flowEncryptionType === 'disabled') { + $('

').text("Your flow credentials file is not currently encrypted.").appendTo(body); + $('

').text("That means its contents, such as passwords and access tokens, can be read by anyone with access to the file.").appendTo(body); + $('

').text("If you want to store these credentials in a public Git repository, you must encrypt them by providing a secret key phrase.").appendTo(body); + } else { + if (RED.settings.flowEncryptionType === 'user') { + $('

').text("Your flow credentials file is currently encrypted using the credentialSecret property from your settings file as the key.").appendTo(body); + } else if (RED.settings.flowEncryptionType === 'system') { + $('

').text("Your flow credentials file is currently encrypted using a system-generated secret as the key. You should provide a new secret key for this project.").appendTo(body); + } + $('

').text("The secret will be copied into the settings for your new project. You can then manage the secret within the editor.").appendTo(body); + } + + // var row = $('

').appendTo(body); + // $('').appendTo(row); + // var gitUsernameInput = $('').val(currentGitSettings.user.name||"").appendTo(row); + // // $('
').text("This does not need to be your real name").appendTo(row); + // + // row = $('
').appendTo(body); + // $('').appendTo(row); + // var gitEmailInput = $('').val(currentGitSettings.user.email||"").appendTo(row); + // // $('
').text("Something something email").appendTo(row); + + var validateForm = function() { + var valid = true; + var encryptionState = $("input[name=projects-encryption-type]:checked").val(); + if (encryptionState === 'enabled') { + var encryptionKeyType = $("input[name=projects-encryption-key]:checked").val(); + if (encryptionKeyType === 'custom') { + valid = valid && emptyProjectCredentialInput.val()!==''; + } + } + $("#projects-dialog-create-encryption").prop('disabled',!valid).toggleClass('disabled ui-button-disabled ui-state-disabled',!valid); + } + + + var row = $('
').appendTo(body); + $('').appendTo(row); + + var credentialsBox = $('
').appendTo(row); + var credentialsRightBox = $('
').appendTo(credentialsBox); + var credentialsLeftBox = $('
').appendTo(credentialsBox); + + var credentialsEnabledBox = $('
').appendTo(credentialsLeftBox); + $('').appendTo(credentialsEnabledBox); + var credentialsDisabledBox = $('
').appendTo(credentialsLeftBox); + $('').appendTo(credentialsDisabledBox); + + credentialsLeftBox.find("input[name=projects-encryption-type]").click(function(e) { + var val = $(this).val(); + var toEnable; + var toDisable; + if (val === 'enabled') { + toEnable = credentialsEnabledBox; + toDisable = credentialsDisabledBox; + $(".projects-encryption-enabled-row").show(); + $(".projects-encryption-disabled-row").hide(); + if ($("input[name=projects-encryption-key]:checked").val() === 'custom') { + emptyProjectCredentialInput.focus(); + } + + } else { + toDisable = credentialsEnabledBox; + toEnable = credentialsDisabledBox; + $(".projects-encryption-enabled-row").hide(); + $(".projects-encryption-disabled-row").show(); + } + + toEnable.css({ + borderColor: "#ccc", + borderRightColor: "white" + }); + toDisable.css({ + borderColor: "white", + borderRightColor: "#ccc" + }); + + validateForm(); + }) + + row = $('
').appendTo(credentialsRightBox); + $('').appendTo(row); + row = $('
').appendTo(credentialsRightBox); + $('').appendTo(row); + row = $('
').appendTo(credentialsRightBox); + emptyProjectCredentialInput = $('').appendTo(row); + emptyProjectCredentialInput.on("change keyup paste", validateForm); + + row = $('
').hide().appendTo(credentialsRightBox); + $('
The credentials file will not be encrypted and its contents easily read
').appendTo(row); + + credentialsRightBox.find("input[name=projects-encryption-key]").click(function() { + var val = $(this).val(); + emptyProjectCredentialInput.attr("disabled",val === 'default'); + if (val === "custom") { + emptyProjectCredentialInput.focus(); + } + validateForm(); + }); + + setTimeout(function() { + credentialsLeftBox.find("input[name=projects-encryption-type][value=enabled]").click(); + if (RED.settings.flowEncryptionType !== 'user') { + credentialsRightBox.find("input[name=projects-encryption-key][value=custom]").click(); + } else { + credentialsRightBox.find("input[name=projects-encryption-key][value=default]").click(); + } + validateForm(); + },100); + + return container; + }, + buttons: [ + { + // id: "clipboard-dialog-cancel", + text: "Back", + click: function() { + show('project-details'); + } + }, + { + id: "projects-dialog-create-encryption", + text: "Create project", // TODO: nls + class: "primary disabled", + disabled: true, + click: function() { + var encryptionState = $("input[name=projects-encryption-type]:checked").val(); + if (encryptionState === 'enabled') { + var encryptionKeyType = $("input[name=projects-encryption-key]:checked").val(); + if (encryptionKeyType === 'custom') { + createProjectOptions.credentialSecret = emptyProjectCredentialInput.val(); + } else { + // If 'use existing', leave createProjectOptions.credentialSecret blank + // - that will trigger it to use the existing key + // TODO: this option should be disabled if encryption is disabled + } + } else { + // Disabled encryption by explicitly setting credSec to false + createProjectOptions.credentialSecret = false; + } + + RED.deploy.setDeployInflight(true); + RED.projects.settings.switchProject(createProjectOptions.name); + + sendRequest({ + url: "projects", + type: "POST", + requireCleanWorkspace: true, + handleAuthFail: false, + responses: { + 200: function(data) { + createProjectOptions = {}; + show('create-success'); + }, + 400: { + 'project_exists': function(error) { + console.log("already exists"); + }, + 'git_error': function(error) { + console.log("git error",error); + }, + 'git_connection_failed': function(error) { + projectRepoInput.addClass("input-error"); + }, + 'git_auth_failed': function(error) { + projectRepoUserInput.addClass("input-error"); + projectRepoPasswordInput.addClass("input-error"); + // getRepoAuthDetails(req); + console.log("git auth error",error); + }, + 'unexpected_error': function(error) { + console.log("unexpected_error",error) + } + } + } + },createProjectOptions).always(function() { + RED.deploy.setDeployInflight(false); + }) + } + } + ] + } + })(), + 'create-success': { + content: function(options) { + + var container = $('
'); + migrateProjectHeader.appendTo(container); + var body = $('
').appendTo(container); + + $('

').text("You have successfully created your first project!").appendTo(body); + $('

').text("You can now continue to use Node-RED just as you always have.").appendTo(body); + $('

').text("The 'info' tab in the sidebar shows you what your current active project is. "+ + "The button next to the name can be used to access the project settings view.").appendTo(body); + $('

').text("The new 'history' tab in the sidebar can be used to view files that have changed "+ + "in your project and to commit them. It shows you a complete history of your commits and "+ + "allows you to push your changes to a remote repository.").appendTo(body); + + return container; + }, + buttons: [ + { + text: "Done", + click: function() { + $( this ).dialog( "close" ); + } + } ] }, 'create': (function() { @@ -245,6 +656,9 @@ RED.projects = (function() { toDisable = credentialsDisabledBox; $(".projects-encryption-enabled-row").show(); $(".projects-encryption-disabled-row").hide(); + if ($("input[name=projects-encryption-key]:checked").val() === 'custom') { + emptyProjectCredentialInput.focus(); + } } else { toDisable = credentialsEnabledBox; toEnable = credentialsDisabledBox; @@ -278,6 +692,9 @@ RED.projects = (function() { credentialsRightBox.find("input[name=projects-encryption-key]").click(function() { var val = $(this).val(); emptyProjectCredentialInput.attr("disabled",val === 'default'); + if (val === "custom") { + emptyProjectCredentialInput.focus(); + } validateForm(); }) @@ -633,7 +1050,7 @@ RED.projects = (function() { RED.projects.init(); } var screen = screens[s]; - var container = screen.content(); + var container = screen.content(options); dialogBody.empty(); dialog.dialog('option','buttons',screen.buttons); @@ -1030,9 +1447,13 @@ RED.projects = (function() { // updateProjectDependencies(); RED.sidebar.versionControl.refresh(true); if (done) { - done(); + done(activeProject); } }); + } else { + if (done) { + done(null); + } } }); } @@ -1045,7 +1466,11 @@ RED.projects = (function() { show('welcome'); }, newProject: function() { - show('create') + if (!activeProject) { + show('welcome'); + } else { + show('create') + } }, selectProject: function() { show('open') diff --git a/editor/js/ui/tab-versionControl.js b/editor/js/ui/tab-versionControl.js index 2ed577b45..8c03e0004 100644 --- a/editor/js/ui/tab-versionControl.js +++ b/editor/js/ui/tab-versionControl.js @@ -897,6 +897,7 @@ RED.sidebar.versionControl = (function() { }); }); + $('