diff --git a/editor/js/ui/common/editableList.js b/editor/js/ui/common/editableList.js index 79be3cfcb..975c8c300 100644 --- a/editor/js/ui/common/editableList.js +++ b/editor/js/ui/common/editableList.js @@ -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/projectSettings.js b/editor/js/ui/projectSettings.js index d495b42f6..2c3e1f54d 100644 --- a/editor/js/ui/projectSettings.js +++ b/editor/js/ui/projectSettings.js @@ -164,7 +164,7 @@ RED.projects.settings = (function() { var editButton = container.prev(); editButton.hide(); container.empty(); - var bg = $('').appendTo(container); + var bg = $('').appendTo(container); var input = $('').val(summary||"").appendTo(container); $('') .appendTo(bg) @@ -853,7 +853,7 @@ RED.projects.settings = (function() { } - var formButtons = $('').hide().appendTo(filesContainer); + var formButtons = $('').hide().appendTo(filesContainer); $('') .appendTo(formButtons) .click(function(evt) { @@ -955,35 +955,40 @@ RED.projects.settings = (function() { var localBranchContainer = $('
').appendTo(pane); $('

').text("Branches").appendTo(localBranchContainer); - var row = $('
').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); - $('').appendTo(container); - $('').text(entry.name).appendTo(container); - // if (entry.commit) { - // $('').text(entry.commit.sha).appendTo(container); - // } - + var container = $('
    ').appendTo(row); + 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) { - $('').text(entry.remote||"").appendTo(container); + var bottomRow = $('
    ').appendTo(content); + + $('').text(entry.remote||"").appendTo(bottomRow); if (entry.status.ahead+entry.status.behind > 0) { - $(''+ + $(''+ ' '+entry.status.ahead+' '+ ' '+entry.status.behind+''+ - '').appendTo(container); + '').appendTo(bottomRow); } } - var tools = $('').appendTo(container); - if (entry.current) { - tools.text('current'); - } else { - $('') + if (!entry.current) { + var tools = $('').appendTo(container); + $('') .appendTo(tools) .click(function(e) { e.preventDefault(); @@ -1056,6 +1061,7 @@ RED.projects.settings = (function() { } }); + $.getJSON("projects/"+activeProject.name+"/branches",function(result) { if (result.branches) { result.branches.sort(function(A,B) { @@ -1069,6 +1075,7 @@ RED.projects.settings = (function() { } }) } + function createRemoteRepositorySection(activeProject,pane) { $('

    ').text("Version Control").appendTo(pane); @@ -1077,152 +1084,195 @@ RED.projects.settings = (function() { var repoContainer = $('').appendTo(pane); var title = $('

    ').text("Git remotes").appendTo(repoContainer); - var editRepoButton = $('') + var editRepoButton = $('') .appendTo(title) .click(function(evt) { - editRepoButton.hide(); - formButtons.show(); - - $('.projects-dialog-remote-list-entry-delete').show(); - remoteListAddButton.show(); + editRepoButton.attr('disabled',true); + addBranchDialog.slideDown(200, function() { + addBranchDialog[0].scrollIntoView(); + }); }); + var emptyItem = { empty: true }; - - row = $('').appendTo(repoContainer); + row = $('').appendTo(repoContainer); var remotesList = $('
      ').appendTo(row); remotesList.editableList({ - addButton: 'add remote repository', + addButton: false, height: 'auto', - addItem: function(outer,index,entry) { + addItem: function(row,index,entry) { - var header = $('
      ').appendTo(outer); - entry.header = $('').text(entry.name||"Add new remote").appendTo(header); - var body = $('
      ').appendTo(outer); - entry.body = body; - if (entry.name) { - entry.removeButton = $('') - .hide() - .appendTo(header) - .click(function(e) { - entry.removed = true; - body.fadeOut(100); - entry.header.css("text-decoration","line-through") - entry.header.css("font-style","italic") - $(this).hide(); - }); - if (entry.urls.fetch === entry.urls.push) { - row = $('').appendTo(body); - $('').text('URL').appendTo(row); - $('
      ').text(entry.urls.fetch).appendTo(row); - } else { - row = $('').appendTo(body); - $('').text('Fetch URL').appendTo(row); - $('
      ').text(entry.urls.fetch).appendTo(row); - row = $('').appendTo(body); - $('').text('Push URL').appendTo(row); - $('
      ').text(entry.urls.push).appendTo(row); - } + var container = $('
      ').appendTo(row); + if (entry.empty) { + container.addClass('red-ui-search-empty'); + container.text("No remotes"); + return; } else { - row = $('').appendTo(body); - $('').text('Remote name').appendTo(row); - entry.nameInput = $('').appendTo(row); + $('').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); - row = $('').appendTo(body); - var fetchLabel = $('').text('URL').appendTo(row); - entry.urlInput = $('').appendTo(row); + } + 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 remoteListAddButton = row.find(".red-ui-editableList-addButton").hide(); + 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; + var addRemoteDialog = $('
      ').hide().appendTo(row); + $('
      ').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); + var fetchLabel = $('').text('URL').appendTo(row); + var remoteURLInput = $('').appendTo(row).on("change keyup paste",validateForm); var hideEditForm = function() { - editRepoButton.show(); - formButtons.hide(); - $('.projects-dialog-remote-list-entry-delete').hide(); - remoteListAddButton.hide(); + editRepoButton.attr('disabled',false); + addRemoteDialog.hide(); + remoteNameInput.val(""); + remoteURLInput.val(""); + if (popover) { + popover.close(); + popover = null; + } } - - var formButtons = $('') - .hide().appendTo(repoContainer); + var formButtons = $('') + .appendTo(addRemoteDialog); $('') .appendTo(formButtons) .click(function(evt) { evt.preventDefault(); - - var items = remotesList.editableList('items'); - items.each(function() { - var data = $(this).data('data'); - if (!data.name) { - remotesList.editableList('removeItem',data); - } else if (data.removed) { - delete data.removed; - data.body.show(); - data.header.css("text-decoration",""); - data.header.css("font-style",""); - } - }) - - hideEditForm(); }); - var saveButton = $('') + var saveButton = $('') .appendTo(formButtons) .click(function(evt) { evt.preventDefault(); - var spinner = utils.addSpinnerOverlay(repoContainer); + var spinner = utils.addSpinnerOverlay(addRemoteDialog).addClass('projects-dialog-spinner-contain'); - var body = { - remotes: {} + var payload = { + name: remoteNameInput.val(), + url: remoteURLInput.val() } - - var items = remotesList.editableList('items'); - items.each(function() { - var data = $(this).data('data'); - if (!data.name) { - body.remotes[data.nameInput.val()] = { - url: data.urlInput.val() - }; - remotesList.editableList('removeItem',data); - } else if (data.removed) { - body.remotes[data.name] = { - removed: true - }; - delete data.removed; - data.body.show(); - data.header.css("text-decoration",""); - data.header.css("font-style",""); - } - }) - var done = function(err) { spinner.remove(); if (err) { - console.log(err); return; } hideEditForm(); } - var payload = { git: body }; - // console.log(JSON.stringify(payload,null,4)); RED.deploy.setDeployInflight(true); utils.sendRequest({ - url: "projects/"+activeProject.name, - type: "PUT", + url: "projects/"+activeProject.name+"/remotes", + type: "POST", responses: { 0: function(error) { done(error); }, 200: function(data) { - activeProject.git.remotes = data.git.remotes; - + 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); @@ -1233,13 +1283,18 @@ RED.projects.settings = (function() { }); 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(); } diff --git a/editor/js/ui/projects.js b/editor/js/ui/projects.js index 72ed1a5af..894dae4e0 100644 --- a/editor/js/ui/projects.js +++ b/editor/js/ui/projects.js @@ -584,6 +584,7 @@ RED.projects = (function() { $(".projects-dialog-screen-create-row").hide(); $(".projects-dialog-screen-create-row-"+$(this).data('type')).show(); validateForm(); + projectNameInput.focus(); }) @@ -679,12 +680,12 @@ RED.projects = (function() { }) row = $('
      ').appendTo(credentialsRightBox); - $('').appendTo(row); - row = $('
      ').appendTo(credentialsRightBox); - $('').appendTo(row); - row = $('
      ').appendTo(credentialsRightBox); - emptyProjectCredentialInput = $('').appendTo(row); + $('').appendTo(row); + // row = $('
      ').appendTo(credentialsRightBox); + emptyProjectCredentialInput = $('').appendTo(row); emptyProjectCredentialInput.on("change keyup paste", validateForm); + $('').appendTo(row); + row = $('
      ').hide().appendTo(credentialsRightBox); $('
      The credentials file will not be encrypted and its contents easily read
      ').appendTo(row); @@ -770,6 +771,9 @@ RED.projects = (function() { createAsEmpty.click(); + setTimeout(function() { + projectNameInput.focus(); + },50); return container; }, buttons: [ @@ -1057,7 +1061,7 @@ RED.projects = (function() { dialogBody.append(container); dialog.dialog('option','title',screen.title||""); dialog.dialog("open"); - dialog.dialog({position: { 'my': 'center', 'at': 'center', 'of': window }}); + dialog.dialog({position: { 'my': 'center top', 'at': 'center top+10%', 'of': window }}); } var selectedProject = null; @@ -1179,7 +1183,7 @@ RED.projects = (function() { resultCallbackArgs = {error:responses.statusText}; return; } else if (options.handleAuthFail !== false && xhr.responseJSON.error === 'git_auth_failed') { - var url = activeProject.git.remotes.origin.fetch; + var url = activeProject.git.remotes[options.remote||'origin'].fetch; var message = $('
      '+ '
      Authentication required for repository:
      '+ '
      '+url+'
      '+ diff --git a/editor/sass/mixins.scss b/editor/sass/mixins.scss index 85f37603d..1fbdb0c1c 100644 --- a/editor/sass/mixins.scss +++ b/editor/sass/mixins.scss @@ -94,6 +94,10 @@ border-bottom-right-radius: 0; } + .button-row &:not(:first-child) { + margin-left: 15px; + } + &:focus { outline: 1px solid $workspace-button-color-focus-outline; } diff --git a/editor/sass/projects.scss b/editor/sass/projects.scss index 23838d303..fa21b5144 100644 --- a/editor/sass/projects.scss +++ b/editor/sass/projects.scss @@ -675,78 +675,71 @@ } } -.projects-dialog-remote-list-entry-header { - padding: 8px 10px; - background: #f6f6f6; - margin-bottom: 8px; -} -.projects-dialog-remote-list-entry-delete { - float: right; -} -.projects-dialog-remote-list-entry-copy { - float: right; -} - -/* -.expandable-list-entry { - .exandable-list-entry-header { - padding: 15px 0; - cursor: pointer; - &:hover { - background: #f3f3f3; +.projects-dialog-list { + position: relative; + .red-ui-editableList-container { + padding: 1px; + background: #f6f6f6; + li:last-child { + border-bottom: none; } - i { - width: 16px; - text-align: center; - } - .fa-angle-right { - color: #333; - transition: all 0.2s ease-in-out; - } - } - &.expanded .fa-angle-right { - transform: rotate(90deg); - } -} -*/ - -.projects-dialog-branch-list { - .red-ui-editableList-container li:last-child { - border-bottom: none; } } -.projects-dialog-branch-list-entry { +.projects-dialog-list-entry { + &.red-ui-search-empty { + padding: 0; + } + span { display: inline-block; } - span:first-child { + .entry-icon { text-align: center; min-width: 30px; - } - .branch-name { - min-width: 150px; - } - .branch-remote-name { - color: #aaa; - font-size: 0.9em; - min-width: 150px; - } - .branch-remote-status { - color: #aaa; - font-size: 0.9em; - } - .commit { - color: #aaa; - font-size: 0.9em; - padding: 2px 5px; - - } - - .projects-dialog-branch-list-entry-tools { - float: right; - margin-right: 20px; - font-size: 0.9em; + vertical-align: top; color: #999; } + .entry-name { + min-width: 250px; + } + &.current .entry-name { + font-weight: bold; + } + .entry-detail { + color: #aaa; + font-size: 0.9em; + } + + .entry-remote-name { + min-width: 250px; + } + + + .entry-tools { + float: right; + margin-right: 10px; + } +} +.projects-dialog-list-dialog { + position: absolute; + top: 5px; + right: 10px; + left: 10px; + background: white; + border-radius: 4px; + border: 1px solid $secondary-border-color; + .projects-edit-form-sublabel { + margin-top: -8px !important; + display: block !important; + width: auto !important; + } + + .projects-dialog-list-dialog-header { + font-weight: bold; + background: #f3f3f3; + margin-top: 0 !important; + padding: 5px 10px; + margin-bottom: 10px; + } } diff --git a/red/api/editor/projects/index.js b/red/api/editor/projects/index.js index 35808e54f..a3fdc29a2 100644 --- a/red/api/editor/projects/index.js +++ b/red/api/editor/projects/index.js @@ -77,7 +77,7 @@ module.exports = { //TODO: validate the payload properly if (req.body.active) { var currentProject = runtime.storage.projects.getActiveProject(req.user); - if (req.params.id !== currentProject.name) { + if (!currentProject || req.params.id !== currentProject.name) { runtime.storage.projects.setActiveProject(req.user, req.params.id).then(function() { res.redirect(303,req.baseUrl + '/'); }).catch(function(err) { @@ -465,8 +465,52 @@ module.exports = { }) }); + // Get a list of remotes + app.get("/:id/remotes", needsPermission("projects.read"), function(req, res) { + var projectName = req.params.id; + runtime.storage.projects.getRemotes(req.user, projectName).then(function(data) { + res.json(data); + }) + .catch(function(err) { + console.log(err.stack); + if (err.code) { + res.status(400).json({error:err.code, message: err.message}); + } else { + res.status(400).json({error:"unexpected_error", message:err.toString()}); + } + }) + }); + // Add a remote + app.post("/:id/remotes", needsPermission("projects.write"), function(req,res) { + var projectName = req.params.id; + runtime.storage.projects.addRemote(req.user, projectName, req.body).then(function() { + res.redirect(303,req.baseUrl+"/"+projectName+"/remotes"); + }).catch(function(err) { + console.log(err.stack); + if (err.code) { + res.status(400).json({error:err.code, message: err.message}); + } else { + res.status(400).json({error:"unexpected_error", message:err.toString()}); + } + }) + }); + // Delete a remote + app.delete("/:id/remotes/:remoteName", needsPermission("projects.write"), function(req, res) { + var projectName = req.params.id; + var remoteName = req.params.remoteName; + runtime.storage.projects.removeRemote(req.user, projectName, remoteName).then(function(data) { + res.redirect(303,req.baseUrl+"/"+projectName+"/remotes"); + }) + .catch(function(err) { + if (err.code) { + res.status(400).json({error:err.code, message: err.message}); + } else { + res.status(400).json({error:"unexpected_error", message:err.toString()}); + } + }); + }); return app; } diff --git a/red/runtime/storage/localfilesystem/projects/Project.js b/red/runtime/storage/localfilesystem/projects/Project.js index 056711549..e4d8aa752 100644 --- a/red/runtime/storage/localfilesystem/projects/Project.js +++ b/red/runtime/storage/localfilesystem/projects/Project.js @@ -117,7 +117,6 @@ Project.prototype.loadRemotes = function() { }).then(function() { return project.loadBranches(); }).then(function() { - var allRemotes = Object.keys(project.remotes); var match = ""; allRemotes.forEach(function(remote) { @@ -519,6 +518,41 @@ Project.prototype.setBranch = function (branchName, isCreate) { Project.prototype.getBranchStatus = function (branchName) { return gitTools.getBranchStatus(this.path,branchName); }; + + + +Project.prototype.getRemotes = function (user) { + return gitTools.getRemotes(this.path).then(function(remotes) { + var result = []; + for (var name in remotes) { + if (remotes.hasOwnProperty(name)) { + remotes[name].name = name; + result.push(remotes[name]); + } + } + return {remotes:result}; + }) +}; +Project.prototype.addRemote = function(user,remote,options) { + var project = this; + return gitTools.addRemote(this.path,remote,options).then(function() { + return project.loadRemotes() + }); +} +Project.prototype.updateRemote = function(user,remote,options) { + // TODO: once the sshkey support is added, move the updating of remotes, + // including their auth details, down here. +} +Project.prototype.removeRemote = function(user, remote) { + // TODO: if this was the last remote using this url, then remove the authCache + // details. + var project = this; + return gitTools.removeRemote(this.path,remote).then(function() { + return project.loadRemotes() + }); +} + + Project.prototype.getFlowFile = function() { console.log("Project.getFlowFile = ",this.paths.flowFile); if (this.paths.flowFile) { diff --git a/red/runtime/storage/localfilesystem/projects/git/index.js b/red/runtime/storage/localfilesystem/projects/git/index.js index a706b410e..a7a8d87b7 100644 --- a/red/runtime/storage/localfilesystem/projects/git/index.js +++ b/red/runtime/storage/localfilesystem/projects/git/index.js @@ -58,6 +58,8 @@ function runGitCommand(args,cwd,env) { err.code = "git_pull_merge_conflict"; } else if (/not fully merged/.test(stderr)) { err.code = "git_delete_branch_unmerged"; + } else if (/remote .* already exists/.test(stderr)) { + err.code = "git_remote_already_exists"; } return reject(err); } diff --git a/red/runtime/storage/localfilesystem/projects/index.js b/red/runtime/storage/localfilesystem/projects/index.js index 1b895912f..5d9218bdf 100644 --- a/red/runtime/storage/localfilesystem/projects/index.js +++ b/red/runtime/storage/localfilesystem/projects/index.js @@ -252,6 +252,21 @@ function getBranchStatus(user, project,branchName) { checkActiveProject(project); return activeProject.getBranchStatus(branchName); } + + +function getRemotes(user, project) { + checkActiveProject(project); + return activeProject.getRemotes(user); +} +function addRemote(user, project, options) { + checkActiveProject(project); + return activeProject.addRemote(user, options.name, options); +} +function removeRemote(user, project, remote) { + checkActiveProject(project); + return activeProject.removeRemote(user, remote); +} + function getActiveProject(user) { return activeProject; } @@ -473,6 +488,9 @@ module.exports = { deleteBranch: deleteBranch, setBranch: setBranch, getBranchStatus:getBranchStatus, + getRemotes: getRemotes, + addRemote: addRemote, + removeRemote: removeRemote, getFlows: getFlows, saveFlows: saveFlows,