From 5a9fcd92679fc4831609afb28ca29172934a7e22 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Tue, 26 Mar 2019 15:51:17 +0000 Subject: [PATCH 001/213] Bump dev branch to 0.21.0-alpha.0 --- package.json | 2 +- .../node_modules/@node-red/editor-api/package.json | 6 +++--- .../node_modules/@node-red/editor-client/package.json | 2 +- packages/node_modules/@node-red/nodes/package.json | 2 +- packages/node_modules/@node-red/registry/package.json | 4 ++-- packages/node_modules/@node-red/runtime/package.json | 6 +++--- packages/node_modules/@node-red/util/package.json | 2 +- packages/node_modules/node-red/package.json | 10 +++++----- 8 files changed, 17 insertions(+), 17 deletions(-) diff --git a/package.json b/package.json index 01b5ea170..bbec9ced4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-red", - "version": "0.20.3", + "version": "0.21.0-alpha.0", "description": "A visual tool for wiring the Internet of Things", "homepage": "http://nodered.org", "license": "Apache-2.0", diff --git a/packages/node_modules/@node-red/editor-api/package.json b/packages/node_modules/@node-red/editor-api/package.json index c47afa77a..90195a177 100644 --- a/packages/node_modules/@node-red/editor-api/package.json +++ b/packages/node_modules/@node-red/editor-api/package.json @@ -1,6 +1,6 @@ { "name": "@node-red/editor-api", - "version": "0.20.3", + "version": "0.21.0-alpha.0", "license": "Apache-2.0", "main": "./lib/index.js", "repository": { @@ -16,8 +16,8 @@ } ], "dependencies": { - "@node-red/util": "0.20.3", - "@node-red/editor-client": "0.20.3", + "@node-red/util": "0.21.0-alpha.0", + "@node-red/editor-client": "0.21.0-alpha.0", "bcryptjs": "2.4.3", "body-parser": "1.18.3", "clone": "2.1.2", diff --git a/packages/node_modules/@node-red/editor-client/package.json b/packages/node_modules/@node-red/editor-client/package.json index 1afd15995..86c05ea6e 100644 --- a/packages/node_modules/@node-red/editor-client/package.json +++ b/packages/node_modules/@node-red/editor-client/package.json @@ -1,6 +1,6 @@ { "name": "@node-red/editor-client", - "version": "0.20.3", + "version": "0.21.0-alpha.0", "license": "Apache-2.0", "repository": { "type": "git", diff --git a/packages/node_modules/@node-red/nodes/package.json b/packages/node_modules/@node-red/nodes/package.json index 14d56cb21..7e3487edb 100644 --- a/packages/node_modules/@node-red/nodes/package.json +++ b/packages/node_modules/@node-red/nodes/package.json @@ -1,6 +1,6 @@ { "name": "@node-red/nodes", - "version": "0.20.3", + "version": "0.21.0-alpha.0", "license": "Apache-2.0", "repository": { "type": "git", diff --git a/packages/node_modules/@node-red/registry/package.json b/packages/node_modules/@node-red/registry/package.json index 71e602001..b3f689c54 100644 --- a/packages/node_modules/@node-red/registry/package.json +++ b/packages/node_modules/@node-red/registry/package.json @@ -1,6 +1,6 @@ { "name": "@node-red/registry", - "version": "0.20.3", + "version": "0.21.0-alpha.0", "license": "Apache-2.0", "main": "./lib/index.js", "repository": { @@ -16,7 +16,7 @@ } ], "dependencies": { - "@node-red/util": "0.20.3", + "@node-red/util": "0.21.0-alpha.0", "semver": "5.6.0", "uglify-js": "3.4.9", "when": "3.7.8" diff --git a/packages/node_modules/@node-red/runtime/package.json b/packages/node_modules/@node-red/runtime/package.json index 3499187f9..d1b1134e5 100644 --- a/packages/node_modules/@node-red/runtime/package.json +++ b/packages/node_modules/@node-red/runtime/package.json @@ -1,6 +1,6 @@ { "name": "@node-red/runtime", - "version": "0.20.3", + "version": "0.21.0-alpha.0", "license": "Apache-2.0", "main": "./lib/index.js", "repository": { @@ -16,8 +16,8 @@ } ], "dependencies": { - "@node-red/registry": "0.20.3", - "@node-red/util": "0.20.3", + "@node-red/registry": "0.21.0-alpha.0", + "@node-red/util": "0.21.0-alpha.0", "clone": "2.1.2", "express": "4.16.4", "fs-extra": "7.0.1", diff --git a/packages/node_modules/@node-red/util/package.json b/packages/node_modules/@node-red/util/package.json index 51e2208d3..a3208a9a6 100644 --- a/packages/node_modules/@node-red/util/package.json +++ b/packages/node_modules/@node-red/util/package.json @@ -1,6 +1,6 @@ { "name": "@node-red/util", - "version": "0.20.3", + "version": "0.21.0-alpha.0", "license": "Apache-2.0", "repository": { "type": "git", diff --git a/packages/node_modules/node-red/package.json b/packages/node_modules/node-red/package.json index 31c158e1f..74d0f26ff 100644 --- a/packages/node_modules/node-red/package.json +++ b/packages/node_modules/node-red/package.json @@ -1,6 +1,6 @@ { "name": "node-red", - "version": "0.20.3", + "version": "0.21.0-alpha.0", "description": "A visual tool for wiring the Internet of Things", "homepage": "http://nodered.org", "license": "Apache-2.0", @@ -31,10 +31,10 @@ "flow" ], "dependencies": { - "@node-red/editor-api": "0.20.3", - "@node-red/runtime": "0.20.3", - "@node-red/util": "0.20.3", - "@node-red/nodes": "0.20.3", + "@node-red/editor-api": "0.21.0-alpha.0", + "@node-red/runtime": "0.21.0-alpha.0", + "@node-red/util": "0.21.0-alpha.0", + "@node-red/nodes": "0.21.0-alpha.0", "basic-auth": "2.0.1", "bcryptjs": "2.4.3", "express": "4.16.4", From 543519d055955f01c0e2315879beeb6ce5cd1204 Mon Sep 17 00:00:00 2001 From: Hiroki Uchikawa Date: Wed, 9 Jan 2019 18:13:33 +0900 Subject: [PATCH 002/213] Add test cases to ensure context API routes are correctly mounted. --- .../editor-api/lib/admin/index_spec.js | 223 ++++++++++++++---- 1 file changed, 180 insertions(+), 43 deletions(-) diff --git a/test/unit/@node-red/editor-api/lib/admin/index_spec.js b/test/unit/@node-red/editor-api/lib/admin/index_spec.js index c20df8d1b..ead5f274a 100644 --- a/test/unit/@node-red/editor-api/lib/admin/index_spec.js +++ b/test/unit/@node-red/editor-api/lib/admin/index_spec.js @@ -26,6 +26,7 @@ var auth = NR_TEST_UTILS.require("@node-red/editor-api/lib/auth"); var nodes = NR_TEST_UTILS.require("@node-red/editor-api/lib/admin/nodes"); var flows = NR_TEST_UTILS.require("@node-red/editor-api/lib/admin/flows"); var flow = NR_TEST_UTILS.require("@node-red/editor-api/lib/admin/flow"); +var context = NR_TEST_UTILS.require("@node-red/editor-api/lib/admin/context"); /** * Ensure all API routes are correctly mounted, with the expected permissions checks @@ -34,8 +35,8 @@ describe("api/admin/index", function() { describe("Ensure all API routes are correctly mounted, with the expected permissions checks", function() { var app; var mockList = [ - flows,flow,nodes - ] + flows,flow,nodes,context + ]; var permissionChecks = {}; var lastRequest; var stubApp = function(req,res,next) { @@ -50,7 +51,7 @@ describe("api/admin/index", function() { return function(req,res,next) { permissionChecks[permission] = (permissionChecks[permission]||0)+1; next(); - } + }; }); sinon.stub(flows,"get",stubApp); @@ -70,6 +71,9 @@ describe("api/admin/index", function() { sinon.stub(nodes,"putSet",stubApp); sinon.stub(nodes,"getModuleCatalog",stubApp); sinon.stub(nodes,"getModuleCatalogs",stubApp); + + sinon.stub(context,"get",stubApp); + sinon.stub(context,"delete",stubApp); }); after(function() { mockList.forEach(function(m) { @@ -92,15 +96,19 @@ describe("api/admin/index", function() { nodes.putSet.restore(); nodes.getModuleCatalog.restore(); nodes.getModuleCatalogs.restore(); + context.get.restore(); + context.delete.restore(); }); before(function() { app = adminApi.init({}); }); + beforeEach(function() { permissionChecks = {}; - }) + }); + it('GET /flows', function(done) { request(app).get("/flows").expect(200).end(function(err,res) { if (err) { @@ -108,8 +116,9 @@ describe("api/admin/index", function() { } permissionChecks.should.have.property('flows.read',1); done(); - }) + }); }); + it('POST /flows', function(done) { request(app).post("/flows").expect(200).end(function(err,res) { if (err) { @@ -117,7 +126,7 @@ describe("api/admin/index", function() { } permissionChecks.should.have.property('flows.write',1); done(); - }) + }); }); it('GET /flow/1234', function(done) { @@ -126,10 +135,11 @@ describe("api/admin/index", function() { return done(err); } permissionChecks.should.have.property('flows.read',1); - lastRequest.params.should.have.property('id','1234') + lastRequest.params.should.have.property('id','1234'); done(); - }) + }); }); + it('POST /flow', function(done) { request(app).post("/flow").expect(200).end(function(err,res) { if (err) { @@ -137,27 +147,29 @@ describe("api/admin/index", function() { } permissionChecks.should.have.property('flows.write',1); done(); - }) + }); }); + it('DELETE /flow/1234', function(done) { request(app).del("/flow/1234").expect(200).end(function(err,res) { if (err) { return done(err); } permissionChecks.should.have.property('flows.write',1); - lastRequest.params.should.have.property('id','1234') + lastRequest.params.should.have.property('id','1234'); done(); - }) + }); }); + it('PUT /flow/1234', function(done) { request(app).put("/flow/1234").expect(200).end(function(err,res) { if (err) { return done(err); } permissionChecks.should.have.property('flows.write',1); - lastRequest.params.should.have.property('id','1234') + lastRequest.params.should.have.property('id','1234'); done(); - }) + }); }); it('GET /nodes', function(done) { @@ -167,8 +179,9 @@ describe("api/admin/index", function() { } permissionChecks.should.have.property('nodes.read',1); done(); - }) + }); }); + it('POST /nodes', function(done) { request(app).post("/nodes").expect(200).end(function(err,res) { if (err) { @@ -176,27 +189,29 @@ describe("api/admin/index", function() { } permissionChecks.should.have.property('nodes.write',1); done(); - }) + }); }); + it('GET /nodes/module', function(done) { request(app).get("/nodes/module").expect(200).end(function(err,res) { if (err) { return done(err); } permissionChecks.should.have.property('nodes.read',1); - lastRequest.params.should.have.property(0,'module') + lastRequest.params.should.have.property(0,'module'); done(); - }) + }); }); + it('GET /nodes/@scope/module', function(done) { request(app).get("/nodes/@scope/module").expect(200).end(function(err,res) { if (err) { return done(err); } permissionChecks.should.have.property('nodes.read',1); - lastRequest.params.should.have.property(0,'@scope/module') + lastRequest.params.should.have.property(0,'@scope/module'); done(); - }) + }); }); it('PUT /nodes/module', function(done) { @@ -205,19 +220,20 @@ describe("api/admin/index", function() { return done(err); } permissionChecks.should.have.property('nodes.write',1); - lastRequest.params.should.have.property(0,'module') + lastRequest.params.should.have.property(0,'module'); done(); - }) + }); }); + it('PUT /nodes/@scope/module', function(done) { request(app).put("/nodes/@scope/module").expect(200).end(function(err,res) { if (err) { return done(err); } permissionChecks.should.have.property('nodes.write',1); - lastRequest.params.should.have.property(0,'@scope/module') + lastRequest.params.should.have.property(0,'@scope/module'); done(); - }) + }); }); it('DELETE /nodes/module', function(done) { @@ -226,19 +242,20 @@ describe("api/admin/index", function() { return done(err); } permissionChecks.should.have.property('nodes.write',1); - lastRequest.params.should.have.property(0,'module') + lastRequest.params.should.have.property(0,'module'); done(); - }) + }); }); + it('DELETE /nodes/@scope/module', function(done) { request(app).del("/nodes/@scope/module").expect(200).end(function(err,res) { if (err) { return done(err); } permissionChecks.should.have.property('nodes.write',1); - lastRequest.params.should.have.property(0,'@scope/module') + lastRequest.params.should.have.property(0,'@scope/module'); done(); - }) + }); }); it('GET /nodes/module/set', function(done) { @@ -247,21 +264,22 @@ describe("api/admin/index", function() { return done(err); } permissionChecks.should.have.property('nodes.read',1); - lastRequest.params.should.have.property(0,'module') - lastRequest.params.should.have.property(2,'set') + lastRequest.params.should.have.property(0,'module'); + lastRequest.params.should.have.property(2,'set'); done(); - }) + }); }); + it('GET /nodes/@scope/module/set', function(done) { request(app).get("/nodes/@scope/module/set").expect(200).end(function(err,res) { if (err) { return done(err); } permissionChecks.should.have.property('nodes.read',1); - lastRequest.params.should.have.property(0,'@scope/module') - lastRequest.params.should.have.property(2,'set') + lastRequest.params.should.have.property(0,'@scope/module'); + lastRequest.params.should.have.property(2,'set'); done(); - }) + }); }); it('PUT /nodes/module/set', function(done) { @@ -270,21 +288,22 @@ describe("api/admin/index", function() { return done(err); } permissionChecks.should.have.property('nodes.write',1); - lastRequest.params.should.have.property(0,'module') - lastRequest.params.should.have.property(2,'set') + lastRequest.params.should.have.property(0,'module'); + lastRequest.params.should.have.property(2,'set'); done(); - }) + }); }); + it('PUT /nodes/@scope/module/set', function(done) { request(app).put("/nodes/@scope/module/set").expect(200).end(function(err,res) { if (err) { return done(err); } permissionChecks.should.have.property('nodes.write',1); - lastRequest.params.should.have.property(0,'@scope/module') - lastRequest.params.should.have.property(2,'set') + lastRequest.params.should.have.property(0,'@scope/module'); + lastRequest.params.should.have.property(2,'set'); done(); - }) + }); }); it('GET /nodes/messages', function(done) { @@ -293,10 +312,10 @@ describe("api/admin/index", function() { return done(err); } permissionChecks.should.have.property('nodes.read',1); - done(); - }) + }); }); + it('GET /nodes/module/set/messages', function(done) { request(app).get("/nodes/module/set/messages").expect(200).end(function(err,res) { if (err) { @@ -305,8 +324,9 @@ describe("api/admin/index", function() { permissionChecks.should.have.property('nodes.read',1); lastRequest.params.should.have.property(0,'module/set'); done(); - }) + }); }); + it('GET /nodes/@scope/module/set/messages', function(done) { request(app).get("/nodes/@scope/module/set/messages").expect(200).end(function(err,res) { if (err) { @@ -315,7 +335,124 @@ describe("api/admin/index", function() { permissionChecks.should.have.property('nodes.read',1); lastRequest.params.should.have.property(0,'@scope/module/set'); done(); - }) + }); + }); + + it('GET /context/global', function(done) { + request(app).get("/context/global").expect(200).end(function(err,res) { + if (err) { + return done(err); + } + permissionChecks.should.have.property('context.read',1); + lastRequest.params.should.have.property('scope','global'); + done(); + }); + }); + + it('GET /context/global/key?store=memory', function(done) { + request(app).get("/context/global/key?store=memory").expect(200).end(function(err,res) { + if (err) { + return done(err); + } + permissionChecks.should.have.property('context.read',1); + lastRequest.params.should.have.property('scope','global'); + lastRequest.params.should.have.property(0,'key'); + lastRequest.query.should.have.property('store','memory'); + done(); + }); + }); + + it('GET /context/flow/1234', function(done) { + request(app).get("/context/flow/1234").expect(200).end(function(err,res) { + if (err) { + return done(err); + } + permissionChecks.should.have.property('context.read',1); + lastRequest.params.should.have.property('scope','flow'); + lastRequest.params.should.have.property('id','1234'); + done(); + }); + }); + + it('GET /context/flow/1234/key?store=memory', function(done) { + request(app).get("/context/flow/1234/key?store=memory").expect(200).end(function(err,res) { + if (err) { + return done(err); + } + permissionChecks.should.have.property('context.read',1); + lastRequest.params.should.have.property('scope','flow'); + lastRequest.params.should.have.property('id','1234'); + lastRequest.params.should.have.property(0,'key'); + lastRequest.query.should.have.property('store','memory'); + done(); + }); + }); + + it('GET /context/node/5678', function(done) { + request(app).get("/context/node/5678").expect(200).end(function(err,res) { + if (err) { + return done(err); + } + permissionChecks.should.have.property('context.read',1); + lastRequest.params.should.have.property('scope','node'); + lastRequest.params.should.have.property('id','5678'); + done(); + }); + }); + + it('GET /context/node/5678/foo?store=memory', function(done) { + request(app).get("/context/node/5678/foo?store=memory").expect(200).end(function(err,res) { + if (err) { + return done(err); + } + permissionChecks.should.have.property('context.read',1); + lastRequest.params.should.have.property('scope','node'); + lastRequest.params.should.have.property('id','5678'); + lastRequest.params.should.have.property(0,'foo'); + lastRequest.query.should.have.property('store','memory'); + done(); + }); + }); + + it('DELETE /context/global/key?store=memory', function(done) { + request(app).del("/context/global/key?store=memory").expect(200).end(function(err,res) { + if (err) { + return done(err); + } + permissionChecks.should.have.property('context.write',1); + lastRequest.params.should.have.property('scope','global'); + lastRequest.params.should.have.property(0,'key'); + lastRequest.query.should.have.property('store','memory'); + done(); + }); + }); + + it('DELETE /context/flow/1234/key?store=memory', function(done) { + request(app).del("/context/flow/1234/key?store=memory").expect(200).end(function(err,res) { + if (err) { + return done(err); + } + permissionChecks.should.have.property('context.write',1); + lastRequest.params.should.have.property('scope','flow'); + lastRequest.params.should.have.property('id','1234'); + lastRequest.params.should.have.property(0,'key'); + lastRequest.query.should.have.property('store','memory'); + done(); + }); + }); + + it('DELETE /context/node/5678/foo?store=memory', function(done) { + request(app).del("/context/node/5678/foo?store=memory").expect(200).end(function(err,res) { + if (err) { + return done(err); + } + permissionChecks.should.have.property('context.write',1); + lastRequest.params.should.have.property('scope','node'); + lastRequest.params.should.have.property('id','5678'); + lastRequest.params.should.have.property(0,'foo'); + lastRequest.query.should.have.property('store','memory'); + done(); + }); }); }); }); From f98f4085bfde0c6114cb1c873233b3627c38b229 Mon Sep 17 00:00:00 2001 From: Hiroki Uchikawa Date: Fri, 18 Jan 2019 18:55:54 +0900 Subject: [PATCH 003/213] Add test cases for context admin API --- .../editor-api/lib/admin/context_spec.js | 359 +++++++++--------- 1 file changed, 176 insertions(+), 183 deletions(-) diff --git a/test/unit/@node-red/editor-api/lib/admin/context_spec.js b/test/unit/@node-red/editor-api/lib/admin/context_spec.js index 979c3e6eb..a9dc0f70d 100644 --- a/test/unit/@node-red/editor-api/lib/admin/context_spec.js +++ b/test/unit/@node-red/editor-api/lib/admin/context_spec.js @@ -19,22 +19,15 @@ var request = require('supertest'); var express = require('express'); var bodyParser = require('body-parser'); var sinon = require('sinon'); -var when = require('when'); var NR_TEST_UTILS = require("nr-test-utils"); var context = NR_TEST_UTILS.require("@node-red/editor-api/lib/admin/context"); -// var Context = require("../../../../red/runtime/nodes/context"); -// var Util = require("../../../../red/runtime/util"); -describe("api/admin/context", function() { - it.skip("NEEDS TESTS WRITING",function() {}); -}); -/* +describe("api/admin/context", function () { var app = undefined; - before(function (done) { - var node_context = undefined; + before(function () { app = express(); app.use(bodyParser.json()); app.get("/context/:scope(global)", context.get); @@ -42,196 +35,196 @@ describe("api/admin/context", function() { app.get("/context/:scope(node|flow)/:id", context.get); app.get("/context/:scope(node|flow)/:id/*", context.get); - context.init({ - settings: { - }, - log:{warn:function(){},_:function(){},audit:function(){}}, - nodes: { - listContextStores: Context.listStores, - getContext: Context.get, - getNode: function(id) { - if (id === 'NID') { - return { - id: 'NID', - context: function () { - return node_context; - } - }; - } - return null; - } - }, - util: Util - }); - - Context.init({ - contextStorage: { - memory0: { - module: "memory" - }, - memory1: { - module: "memory" - } - } - }); - Context.load().then(function () { - var ctx = Context.get("NID", "FID"); - node_context = ctx; - ctx.set("foo", "n_v00", "memory0"); - ctx.set("bar", "n_v01", "memory0"); - ctx.set("baz", "n_v10", "memory1"); - ctx.set("bar", "n_v11", "memory1"); - ctx.flow.set("foo", "f_v00", "memory0"); - ctx.flow.set("bar", "f_v01", "memory0"); - ctx.flow.set("baz", "f_v10", "memory1"); - ctx.flow.set("bar", "f_v11", "memory1"); - ctx.global.set("foo", "g_v00", "memory0"); - ctx.global.set("bar", "g_v01", "memory0"); - ctx.global.set("baz", "g_v10", "memory1"); - ctx.global.set("bar", "g_v11", "memory1"); - done(); - }); - + app.delete("/context/:scope(global)/*", context.delete); + app.delete("/context/:scope(node|flow)/:id/*", context.delete); }); - after(function () { - Context.clean({allNodes:{}}); - Context.close(); - }); + describe("get", function () { + var gContext = { + default: { abc: { msg: '111', format: 'number' } }, + file: { abc: { msg: '222', format: 'number' } } + }; + var fContext = { + default: { bool: { msg: 'true', format: 'boolean' } }, + file: { string: { msg: 'aaaa', format: 'string[7]' } } + }; + var nContext = { msg: "1", format: "number" }; + var stub = sinon.stub(); - function check_mem(body, mem, name, val) { - var mem0 = body[mem]; - mem0.should.have.property(name); - mem0[name].should.deepEqual(val); - } - - function check_scope(scope, prefix, id) { - describe('# '+scope, function () { - var xid = id ? ("/"+id) : ""; - - it('should return '+scope+' contexts', function (done) { - request(app) - .get('/context/'+scope+xid) - .set('Accept', 'application/json') - .expect(200) - .end(function (err, res) { - if (err) { - return done(err); - } - var body = res.body; - body.should.have.key('memory0', 'memory1'); - check_mem(body, 'memory0', - 'foo', {msg:prefix+'_v00', format:'string[5]'}); - check_mem(body, 'memory0', - 'bar', {msg:prefix+'_v01', format:'string[5]'}); - check_mem(body, 'memory1', - 'baz', {msg:prefix+'_v10', format:'string[5]'}); - check_mem(body, 'memory1', - 'bar', {msg:prefix+'_v11', format:'string[5]'}); - done(); - }); - }); - - it('should return a value from default '+scope+' context', function (done) { - request(app) - .get('/context/'+scope+xid+'/foo') - .set('Accept', 'application/json') - .expect(200) - .end(function (err, res) { - if (err) { - return done(err); - } - var body = res.body; - body.should.deepEqual({msg: prefix+'_v00', format: 'string[5]'}); - done(); - }); - }); - - it('should return a value from specified '+scope+' context', function (done) { - request(app) - .get('/context/'+scope+xid+'/bar?store=memory1') - .set('Accept', 'application/json') - .expect(200) - .end(function (err, res) { - if (err) { - return done(err); - } - var body = res.body; - body.should.deepEqual({msg: prefix+'_v11', format: 'string[5]', store: 'memory1'}); - done(); - }); - }); - - it('should return specified '+scope+' store', function (done) { - request(app) - .get('/context/'+scope+xid+'?store=memory1') - .set('Accept', 'application/json') - .expect(200) - .end(function (err, res) { - if (err) { - return done(err); - } - var body = res.body; - body.should.deepEqual({ - memory1: { - baz: { msg: prefix+'_v10', format: 'string[5]' }, - bar: { msg: prefix+'_v11', format: 'string[5]' } - } - }); - done(); - }); - }); - - it('should return undefined for unknown key of default '+scope+' store', function (done) { - request(app) - .get('/context/'+scope+xid+'/unknown') - .set('Accept', 'application/json') - .expect(200) - .end(function (err, res) { - if (err) { - return done(err); - } - var body = res.body; - body.should.deepEqual({msg:'(undefined)', format:'undefined'}); - done(); - - }); - }); - - it('should cause error for unknown '+scope+' store', function (done) { - request(app) - .get('/context/'+scope+xid+'?store=unknown') - .set('Accept', 'application/json') - .expect(200) - .end(function (err, res) { - if (err) { - return done(); - } - done("unexpected"); - }); + before(function () { + context.init({ + context: { + getValue: stub + } }); }); - } - check_scope("global", "g", undefined); - check_scope("node", "n", "NID"); - check_scope("flow", "f", "FID"); + afterEach(function () { + stub.reset(); + }); - describe("# errors", function () { - it('should cause error for unknown scope', function (done) { + it('should call context.getValue to get global contexts', function (done) { + stub.returns(Promise.resolve(gContext)); request(app) - .get('/context/scope') + .get('/context/global') .set('Accept', 'application/json') .expect(200) .end(function (err, res) { if (err) { - return done(); + return done(err); } - done("unexpected"); + stub.args[0][0].should.have.property('user', undefined); + stub.args[0][0].should.have.property('scope', 'global'); + stub.args[0][0].should.have.property('id', undefined); + stub.args[0][0].should.have.property('key', undefined); + stub.args[0][0].should.have.property('store', undefined); + var body = res.body; + body.should.eql(gContext); + done(); }); }); + it('should call context.getValue to get flow contexts', function (done) { + stub.returns(Promise.resolve(fContext)); + request(app) + .get('/context/flow/1234/') + .set('Accept', 'application/json') + .expect(200) + .end(function (err, res) { + if (err) { + return done(err); + } + stub.args[0][0].should.have.property('user', undefined); + stub.args[0][0].should.have.property('scope', 'flow'); + stub.args[0][0].should.have.property('id', '1234'); + stub.args[0][0].should.have.property('key', undefined); + stub.args[0][0].should.have.property('store', undefined); + var body = res.body; + body.should.eql(fContext); + done(); + }); + }); + + it('should call context.getValue to get a node context', function (done) { + stub.returns(Promise.resolve(nContext)); + request(app) + .get('/context/node/5678/foo?store=file') + .set('Accept', 'application/json') + .expect(200) + .end(function (err, res) { + if (err) { + return done(err); + } + stub.args[0][0].should.have.property('user', undefined); + stub.args[0][0].should.have.property('scope', 'node'); + stub.args[0][0].should.have.property('id', '5678'); + stub.args[0][0].should.have.property('key', 'foo'); + stub.args[0][0].should.have.property('store', 'file'); + var body = res.body; + body.should.eql(nContext); + done(); + }); + }); + + it('should handle error which context.getValue causes', function (done) { + stub.returns(Promise.reject('error')); + request(app) + .get('/context/global') + .set('Accept', 'application/json') + .expect(400) + .end(function (err, res) { + if (err) { + return done(err); + } + res.body.should.has.a.property('code', 'unexpected_error'); + res.body.should.has.a.property('message', 'error'); + done(); + }); + }); }); + describe("delete", function () { + var stub = sinon.stub(); + + before(function () { + context.init({ + context: { + delete: stub + } + }); + }); + + afterEach(function () { + stub.reset(); + }); + + it('should call context.delete to delete a global context', function (done) { + stub.returns(Promise.resolve()); + request(app) + .delete('/context/global/abc?store=default') + .expect(204) + .end(function (err, res) { + if (err) { + return done(err); + } + stub.args[0][0].should.have.property('user', undefined); + stub.args[0][0].should.have.property('scope', 'global'); + stub.args[0][0].should.have.property('id', undefined); + stub.args[0][0].should.have.property('key', 'abc'); + stub.args[0][0].should.have.property('store', 'default'); + done(); + }); + }); + + it('should call context.delete to delete a flow context', function (done) { + stub.returns(Promise.resolve()); + request(app) + .delete('/context/flow/1234/abc?store=file') + .expect(204) + .end(function (err, res) { + if (err) { + return done(err); + } + stub.args[0][0].should.have.property('user', undefined); + stub.args[0][0].should.have.property('scope', 'flow'); + stub.args[0][0].should.have.property('id', '1234'); + stub.args[0][0].should.have.property('key', 'abc'); + stub.args[0][0].should.have.property('store', 'file'); + done(); + }); + }); + + it('should call context.delete to delete a node context', function (done) { + stub.returns(Promise.resolve()); + request(app) + .delete('/context/node/5678/foo?store=file') + .expect(204) + .end(function (err, res) { + if (err) { + return done(err); + } + stub.args[0][0].should.have.property('user', undefined); + stub.args[0][0].should.have.property('scope', 'node'); + stub.args[0][0].should.have.property('id', '5678'); + stub.args[0][0].should.have.property('key', 'foo'); + stub.args[0][0].should.have.property('store', 'file'); + done(); + }); + }); + + it('should handle error which context.delete causes', function (done) { + stub.returns(Promise.reject('error')); + request(app) + .delete('/context/global/abc?store=default') + .expect(400) + .end(function (err, res) { + if (err) { + return done(err); + } + res.body.should.has.a.property('code', 'unexpected_error'); + res.body.should.has.a.property('message', 'error'); + done(); + }); + }); + }); }); -*/ From f7c87e26db99a5a210895f766b8b5f7230afab95 Mon Sep 17 00:00:00 2001 From: Hiroki Uchikawa Date: Fri, 18 Jan 2019 19:53:15 +0900 Subject: [PATCH 004/213] Add test cases for context runtime API --- .../@node-red/runtime/lib/api/context_spec.js | 260 ++++++++++++++---- 1 file changed, 200 insertions(+), 60 deletions(-) diff --git a/test/unit/@node-red/runtime/lib/api/context_spec.js b/test/unit/@node-red/runtime/lib/api/context_spec.js index db604f2a9..bf23e9cc6 100644 --- a/test/unit/@node-red/runtime/lib/api/context_spec.js +++ b/test/unit/@node-red/runtime/lib/api/context_spec.js @@ -19,7 +19,7 @@ var should = require("should"); var sinon = require("sinon"); var NR_TEST_UTILS = require("nr-test-utils"); -var context = NR_TEST_UTILS.require("@node-red/runtime/lib/api/context") +var context = NR_TEST_UTILS.require("@node-red/runtime/lib/api/context"); var mockLog = () => ({ log: sinon.stub(), @@ -29,8 +29,8 @@ var mockLog = () => ({ info: sinon.stub(), metric: sinon.stub(), audit: sinon.stub(), - _: function() { return "abc"} -}) + _: function() { return "abc";} +}); var mockContext = function(contents) { return { @@ -41,51 +41,65 @@ var mockContext = function(contents) { callback(null,undefined); } }, - keys: function(store,callback) { + set: function (key, value, store, callback) { if (contents.hasOwnProperty(store)) { - callback(null,Object.keys(contents[store])); + if (!value) { + delete contents[store][key]; + callback(null); + } + } else { + callback("err store"); + } + }, + keys: function (store, callback) { + if (contents.hasOwnProperty(store)) { + callback(null, Object.keys(contents[store])); } else { callback("err store"); } } - } -} + }; +}; + describe("runtime-api/context", function() { - describe("getValue", function() { - var contexts = { - global: mockContext({ default: {abc:111}, file: {abc:222}}), - flow1: mockContext({ default: {abc:333}, file: {abc:444}}) - } - var nodeContext = mockContext({ default: {abc:555}, file: {abc:666}}) + var globalContext, flowContext, nodeContext, contexts; - beforeEach(function() { - context.init({ - nodes: { - listContextStores: function() { - return { default: 'default', stores: [ 'default', 'file' ] } - }, - getContext: function(id) { - return contexts[id] - }, - getNode: function(id) { - if (id === 'known') { - return { - context: function() { return nodeContext } - } - } else { - return null; - } - } + beforeEach(function() { + globalContext = { default: { abc: 111 }, file: { abc: 222 } }; + flowContext = { default: { abc: 333 }, file: { abc: 444 } }; + nodeContext = { default: { abc: 555 }, file: { abc: 666 } }; + contexts = { + global: mockContext(globalContext), + flow1: mockContext(flowContext) + }; + context.init({ + nodes: { + listContextStores: function() { + return { default: 'default', stores: [ 'default', 'file' ] }; }, - settings: { - functionGlobalContext: { - fgc:1234 - } + getContext: function(id) { + return contexts[id]; }, - log: mockLog() - }) + getNode: function(id) { + if (id === 'known') { + return { + context: function() { return mockContext(nodeContext); } + }; + } else { + return null; + } + } + }, + settings: { + functionGlobalContext: { + fgc:1234 + } + }, + log: mockLog() }); + }); + describe("getValue", function() { it('gets global value of default store', function() { return context.getValue({ scope: 'global', @@ -95,8 +109,9 @@ describe("runtime-api/context", function() { }).then(function(result) { result.should.have.property('msg','111'); result.should.have.property('format','number'); - }) - }) + }); + }); + it('gets global value of specified store', function() { return context.getValue({ scope: 'global', @@ -106,8 +121,9 @@ describe("runtime-api/context", function() { }).then(function(result) { result.should.have.property('msg','222'); result.should.have.property('format','number'); - }) - }) + }); + }); + it('gets flow value of default store', function() { return context.getValue({ scope: 'flow', @@ -117,8 +133,9 @@ describe("runtime-api/context", function() { }).then(function(result) { result.should.have.property('msg','333'); result.should.have.property('format','number'); - }) - }) + }); + }); + it('gets flow value of specified store', function() { return context.getValue({ scope: 'flow', @@ -128,8 +145,9 @@ describe("runtime-api/context", function() { }).then(function(result) { result.should.have.property('msg','444'); result.should.have.property('format','number'); - }) - }) + }); + }); + it('gets node value of default store', function() { return context.getValue({ scope: 'node', @@ -139,8 +157,9 @@ describe("runtime-api/context", function() { }).then(function(result) { result.should.have.property('msg','555'); result.should.have.property('format','number'); - }) - }) + }); + }); + it('gets node value of specified store', function() { return context.getValue({ scope: 'node', @@ -150,8 +169,8 @@ describe("runtime-api/context", function() { }).then(function(result) { result.should.have.property('msg','666'); result.should.have.property('format','number'); - }) - }) + }); + }); it('404s for unknown store', function(done) { context.getValue({ @@ -162,12 +181,11 @@ describe("runtime-api/context", function() { }).then(function(result) { done("getValue for unknown store should not resolve"); }).catch(function(err) { - err.should.have.property('code','not_found') + err.should.have.property('code','not_found'); err.should.have.property('status',404); done(); - }) - }) - + }); + }); it('gets all global value properties', function() { return context.getValue({ @@ -180,8 +198,9 @@ describe("runtime-api/context", function() { default: { abc: { msg: '111', format: 'number' } }, file: { abc: { msg: '222', format: 'number' } } }); - }) - }) + }); + }); + it('gets all flow value properties', function() { return context.getValue({ scope: 'flow', @@ -193,8 +212,9 @@ describe("runtime-api/context", function() { default: { abc: { msg: '333', format: 'number' } }, file: { abc: { msg: '444', format: 'number' } } }); - }) - }) + }); + }); + it('gets all node value properties', function() { return context.getValue({ scope: 'node', @@ -206,8 +226,128 @@ describe("runtime-api/context", function() { default: { abc: { msg: '555', format: 'number' } }, file: { abc: { msg: '666', format: 'number' } } }); - }) - }) + }); + }); - }) + it('gets empty object when specified context doesn\'t exist', function() { + return context.getValue({ + scope: 'node', + id: 'non-existent', + store: 'file', + key: 'abc' + }).then(function(result) { + result.should.be.an.Object(); + result.should.be.empty(); + }); + }); + }); + + describe("delete", function () { + it('deletes global value of default store', function () { + return context.delete({ + scope: 'global', + id: undefined, + store: undefined, // use default + key: 'abc' + }).then(function () { + globalContext.should.eql({ + default: {}, file: { abc: 222 } + }); + }); + }); + + it('deletes global value of specified store', function () { + return context.delete({ + scope: 'global', + id: undefined, + store: 'file', + key: 'abc' + }).then(function () { + globalContext.should.eql({ + default: { abc: 111 }, file: {} + }); + }); + }); + + it('deletes flow value of default store', function () { + return context.delete({ + scope: 'flow', + id: 'flow1', + store: undefined, // use default + key: 'abc' + }).then(function () { + flowContext.should.eql({ + default: {}, file: { abc: 444 } + }); + }); + }); + + it('deletes flow value of specified store', function () { + return context.delete({ + scope: 'flow', + id: 'flow1', + store: 'file', + key: 'abc' + }).then(function () { + flowContext.should.eql({ + default: { abc: 333 }, file: {} + }); + }); + }); + + it('deletes node value of default store', function () { + return context.delete({ + scope: 'node', + id: 'known', + store: undefined, // use default + key: 'abc' + }).then(function () { + nodeContext.should.eql({ + default: {}, file: { abc: 666 } + }); + }); + }); + + it('deletes node value of specified store', function () { + return context.delete({ + scope: 'node', + id: 'known', + store: 'file', + key: 'abc' + }).then(function () { + nodeContext.should.eql({ + default: { abc: 555 }, file: {} + }); + }); + }); + + it('does nothing when specified context doesn\'t exist', function() { + return context.delete({ + scope: 'node', + id: 'non-existent', + store: 'file', + key: 'abc' + }).then(function(result) { + should.not.exist(result); + nodeContext.should.eql({ + default: { abc: 555 }, file: { abc: 666 } + }); + }); + }); + + it('404s for unknown store', function (done) { + context.delete({ + scope: 'global', + id: undefined, + store: 'unknown', + key: 'abc' + }).then(function () { + done("delete for unknown store should not resolve"); + }).catch(function (err) { + err.should.have.property('code', 'not_found'); + err.should.have.property('status', 404); + done(); + }); + }); + }); }); From 0c13603185e69376a33ff4ef38fb5370d2a560e1 Mon Sep 17 00:00:00 2001 From: Dave Conway-Jones Date: Sun, 7 Apr 2019 16:23:17 +0100 Subject: [PATCH 005/213] let status be simple text if wanted --- packages/node_modules/@node-red/runtime/lib/nodes/Node.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/node_modules/@node-red/runtime/lib/nodes/Node.js b/packages/node_modules/@node-red/runtime/lib/nodes/Node.js index b7d44c7b5..b1003d717 100644 --- a/packages/node_modules/@node-red/runtime/lib/nodes/Node.js +++ b/packages/node_modules/@node-red/runtime/lib/nodes/Node.js @@ -306,8 +306,11 @@ Node.prototype.metric = function(eventname, msg, metricValue) { /** * status: { fill:"red|green", shape:"dot|ring", text:"blah" } + * or + * status: "simple text status" */ Node.prototype.status = function(status) { + if (typeof(status) === "string") { status = {text:status}; } this._flow.handleStatus(this,status); }; From 32630083790d22594dca8ebb8796b3b1308311d0 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Tue, 23 Apr 2019 14:23:17 +0100 Subject: [PATCH 006/213] Move library import/export to single dialog --- .../editor-client/locales/en-US/editor.json | 7 +- .../@node-red/editor-client/src/js/red.js | 2 +- .../editor-client/src/js/ui/clipboard.js | 251 ++++-- .../src/js/ui/common/treeList.js | 69 +- .../editor-client/src/js/ui/library.js | 782 ++++++++++-------- .../editor-client/src/sass/colors.scss | 4 + .../editor-client/src/sass/library.scss | 109 +++ .../editor-client/src/sass/mixins.scss | 26 +- .../editor-client/src/sass/tabs.scss | 2 +- .../src/sass/ui/common/treeList.scss | 21 +- 10 files changed, 842 insertions(+), 431 deletions(-) diff --git a/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json b/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json index 5b4985765..d178a8f1e 100755 --- a/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json +++ b/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json @@ -182,7 +182,11 @@ "all":"all flows", "compact":"compact", "formatted":"formatted", - "copy": "Export to clipboard" + "copy": "Copy to clipboard", + "export": "Export to library", + "exportAs": "Export as", + "overwrite": "Replace", + "exists": "

\"__file__\" already exists.

Do you want to replace it?

" }, "import": { "import": "Import to", @@ -354,6 +358,7 @@ "typeLibrary": "__type__ library", "unnamedType": "Unnamed __type__", "exportToLibrary": "Export nodes to library", + "exportedToLibrary": "Nodes exported to library", "dialogSaveOverwrite": "A __libraryType__ called __libraryName__ already exists. Overwrite?", "invalidFilename": "Invalid filename", "savedNodes": "Saved nodes", diff --git a/packages/node_modules/@node-red/editor-client/src/js/red.js b/packages/node_modules/@node-red/editor-client/src/js/red.js index 011b5b8d7..8fdb0de08 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/red.js +++ b/packages/node_modules/@node-red/editor-client/src/js/red.js @@ -468,7 +468,7 @@ var RED = (function() { ]}); menuOptions.push({id:"menu-item-export",label:RED._("menu.label.export"),options:[ {id:"menu-item-export-clipboard",label:RED._("menu.label.clipboard"),onselect:"core:show-export-dialog"}, - {id:"menu-item-export-library",label:RED._("menu.label.library"),disabled:true,onselect:"core:library-export"} + {id:"menu-item-export-library",label:RED._("menu.label.library"),onselect:"core:library-export"} ]}); menuOptions.push(null); menuOptions.push({id:"menu-item-search",label:RED._("menu.label.search"),onselect:"core:search"}); diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/clipboard.js b/packages/node_modules/@node-red/editor-client/src/js/ui/clipboard.js index 97addf5c5..09af09251 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/clipboard.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/clipboard.js @@ -24,6 +24,8 @@ RED.clipboard = (function() { var disabled = false; var popover; var currentPopoverError; + var activeExportTab; + var libraryBrowser; function setupDialogs() { dialog = $('
') @@ -31,7 +33,7 @@ RED.clipboard = (function() { .dialog({ modal: true, autoOpen: false, - width: 500, + width: 800, resizable: false, buttons: [ { @@ -41,14 +43,6 @@ RED.clipboard = (function() { $( this ).dialog( "close" ); } }, - { - id: "clipboard-dialog-close", - class: "primary", - text: RED._("common.label.close"), - click: function() { - $( this ).dialog( "close" ); - } - }, { id: "clipboard-dialog-download", class: "primary", @@ -65,15 +59,71 @@ RED.clipboard = (function() { } }, { - id: "clipboard-dialog-copy", + id: "clipboard-dialog-export", class: "primary", text: RED._("clipboard.export.copy"), click: function() { - $("#clipboard-export").select(); - document.execCommand("copy"); - document.getSelection().removeAllRanges(); - RED.notify(RED._("clipboard.nodesExported"),{id:"clipboard"}); - $( this ).dialog( "close" ); + if (activeExportTab === "clipboard-dialog-export-tab-clipboard") { + $("#clipboard-export").select(); + document.execCommand("copy"); + document.getSelection().removeAllRanges(); + RED.notify(RED._("clipboard.nodesExported"),{id:"clipboard"}); + $( this ).dialog( "close" ); + } else { + var flowToExport = $("#clipboard-export").val(); + var selectedPath = libraryBrowser.getPath(); + var filename = $("#clipboard-dialog-tab-library-name").val().trim(); + var saveFlow = function() { + $.ajax({ + url:'library/flows/'+selectedPath.path + filename, + type: "POST", + data: flowToExport, + contentType: "application/json; charset=utf-8" + }).done(function() { + // RED.library.loadFlowLibrary(); + $(dialog).dialog( "close" ); + RED.notify(RED._("library.exportedToLibrary"),"success"); + }).fail(function(xhr,textStatus,err) { + if (xhr.status === 401) { + RED.notify(RED._("library.saveFailed",{message:RED._("user.notAuthorized")}),"error"); + } else { + RED.notify(RED._("library.saveFailed",{message:xhr.responseText}),"error"); + } + }); + } + if (selectedPath.files) { + var exists = false; + selectedPath.files.forEach(function(f) { + if (f.label === filename) { + exists = true; + } + }); + if (exists) { + dialog.dialog("close"); + var notification = RED.notify(RED._("clipboard.export.exists",{file:RED.utils.sanitize(filename)}),{ + type: "warning", + fixed: true, + buttons: [{ + text: RED._("common.label.cancel"), + click: function() { + notification.hideNotification() + dialog.dialog( "open" ); + } + },{ + text: RED._("clipboard.export.overwrite"), + click: function() { + notification.hideNotification() + saveFlow(); + } + }] + }); + } else { + saveFlow(); + } + } else { + saveFlow(); + } + } } }, { @@ -101,22 +151,39 @@ RED.clipboard = (function() { exportNodesDialog = '
'+ - ''+ + ''+ ''+ ''+ ''+ ''+ ''+ '
'+ - '
'+ - ''+ - '
'+ - '
'+ - ''+ - ''+ - ''+ - ''+ - '
'; + '
'+ + '
'+ + '
    '+ + '
    '+ + '
    '+ + '
    '+ + '
    '+ + ''+ + '
    '+ + '
    '+ + ''+ + ''+ + ''+ + ''+ + '
    '+ + '
    '+ + '
    '+ + '
    '+ + '
    '+ + ''+ + '
    '+ + '
    '+ + '
    '+ + '
    ' + ; + importNodesDialog = '
    '+ @@ -135,6 +202,26 @@ RED.clipboard = (function() { '
    '; } + var validateExportFilenameTimeout + function validateExportFilename() { + if (validateExportFilenameTimeout) { + clearTimeout(validateExportFilenameTimeout); + } + validateExportFilenameTimeout = setTimeout(function() { + var filenameInput = $("#clipboard-dialog-tab-library-name"); + var filename = filenameInput.val().trim(); + var valid = filename.length > 0 && !/[\/\\]/.test(filename); + if (valid) { + filenameInput.removeClass("input-error"); + $("#clipboard-dialog-export").button("enable"); + } else { + filenameInput.addClass("input-error"); + $("#clipboard-dialog-export").button("disable"); + } + },100); + } + + var validateImportTimeout; function validateImport() { @@ -239,8 +326,7 @@ RED.clipboard = (function() { $("#clipboard-dialog-ok").show(); $("#clipboard-dialog-cancel").show(); - $("#clipboard-dialog-close").hide(); - $("#clipboard-dialog-copy").hide(); + $("#clipboard-dialog-export").hide(); $("#clipboard-dialog-download").hide(); $("#clipboard-dialog-ok").button("disable"); $("#clipboard-import").keyup(validateImport); @@ -277,13 +363,95 @@ RED.clipboard = (function() { }); } - function exportNodes() { + function exportNodes(mode) { if (disabled) { return; } + mode = mode || "clipboard"; + dialogContainer.empty(); dialogContainer.append($(exportNodesDialog)); + + var tabs = RED.tabs.create({ + id: "clipboard-dialog-export-tabs", + vertical: true, + onchange: function(tab) { + $("#clipboard-dialog-export-tabs-content").children().hide(); + $("#" + tab.id).show(); + activeExportTab = tab.id; + if (tab.id === "clipboard-dialog-export-tab-clipboard") { + $("#clipboard-dialog-export").button("option","label", RED._("clipboard.export.copy")) + $("#clipboard-dialog-download").show(); + } else { + $("#clipboard-dialog-export").button("option","label", RED._("clipboard.export.export")) + $("#clipboard-dialog-download").hide(); + } + + } + }); + tabs.addTab({ + id: "clipboard-dialog-export-tab-clipboard", + label: "Clipboard" + }); + tabs.addTab({ + id: "clipboard-dialog-export-tab-library", + label: "Library" + }); + + tabs.activateTab("clipboard-dialog-export-tab-"+mode); + + $("#clipboard-dialog-tab-library-name").keyup(validateExportFilename); + $("#clipboard-dialog-tab-library-name").on('paste',function() { setTimeout(validateExportFilename,10)}); + $("#clipboard-dialog-export").button("enable"); + + libraryBrowser = RED.library.createBrowser({ + container: $("#clipboard-dialog-export-tab-library-browser"), + addFolderButton: true, + onselect: function(file) { + if (file && file.label) { + $("#clipboard-dialog-tab-library-name").val(file.label); + } + } + }) + + function xformList(list,label,root) { + var result = { + icon: root===""?"fa fa-archive":'fa fa-folder', + label: label, + path: root + }; + + if (list.f) { + result.files = list.f.map(function(f) { + return { + icon: 'fa fa-file-o', + label: f, + path: root+f + } + }); + } + result.children = []; + if (list.d) { + for (var l in list.d) { + if (list.d.hasOwnProperty(l)) { + if (root+l !== "_examples_") { + result.children.push(xformList(list.d[l], l,root+l+"/")) + } + } + } + } + return result; + } + + $.getJSON("library/flows", function(data) { + var listing = [xformList(data,"Local","")]; + listing[0].expanded = true; + libraryBrowser.data(listing); + }); + $("#clipboard-dialog-tab-library-name").val("flows.json").select(); + + dialogContainer.i18n(); var format = RED.settings.flowFilePretty ? "export-format-full" : "export-format-mini"; @@ -307,6 +475,8 @@ RED.clipboard = (function() { flow = JSON.stringify(nodes); } $("#clipboard-export").val(flow); + setTimeout(function() { $("#clipboard-export").scrollTop(0); },50); + $("#clipboard-export").focus(); } }); @@ -314,7 +484,6 @@ RED.clipboard = (function() { $("#export-range-group > a").click(function(evt) { evt.preventDefault(); if ($(this).hasClass('disabled') || $(this).hasClass('selected')) { - $("#clipboard-export").focus(); return; } $(this).parent().children().removeClass('selected'); @@ -357,13 +526,13 @@ RED.clipboard = (function() { $("#export-copy").addClass('disabled'); } $("#clipboard-export").val(flow); + setTimeout(function() { $("#clipboard-export").scrollTop(0); },50); $("#clipboard-export").focus(); }) $("#clipboard-dialog-ok").hide(); $("#clipboard-dialog-cancel").hide(); - $("#clipboard-dialog-copy").hide(); - $("#clipboard-dialog-close").hide(); + $("#clipboard-dialog-export").hide(); var selection = RED.workspaces.selection(); if (selection.length > 0) { $("#export-range-selected").click(); @@ -381,25 +550,11 @@ RED.clipboard = (function() { } else { $("#export-format-mini").click(); } - $("#clipboard-export") - .focus(function() { - var textarea = $(this); - textarea.select(); - textarea.mouseup(function() { - textarea.unbind("mouseup"); - return false; - }) - }); dialog.dialog("option","title",RED._("clipboard.exportNodes")).dialog( "open" ); $("#clipboard-export").focus(); - if (!document.queryCommandSupported("copy")) { - $("#clipboard-dialog-cancel").hide(); - $("#clipboard-dialog-close").show(); - } else { - $("#clipboard-dialog-cancel").show(); - $("#clipboard-dialog-copy").show(); - } + $("#clipboard-dialog-cancel").show(); + $("#clipboard-dialog-export").show(); $("#clipboard-dialog-download").show(); } @@ -460,6 +615,7 @@ RED.clipboard = (function() { RED.actions.add("core:show-export-dialog",exportNodes); RED.actions.add("core:show-import-dialog",importNodes); + RED.actions.add("core:library-export",function() { exportNodes('library') }); RED.events.on("editor:open",function() { disabled = true; }); RED.events.on("editor:close",function() { disabled = false; }); @@ -468,7 +624,6 @@ RED.clipboard = (function() { RED.events.on("type-search:open",function() { disabled = true; }); RED.events.on("type-search:close",function() { disabled = false; }); - $('#chart').on("dragenter",function(event) { if ($.inArray("text/plain",event.originalEvent.dataTransfer.types) != -1 || $.inArray("Files",event.originalEvent.dataTransfer.types) != -1) { diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/common/treeList.js b/packages/node_modules/@node-red/editor-client/src/js/ui/common/treeList.js index 8e0249183..79371fee6 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/common/treeList.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/common/treeList.js @@ -32,7 +32,7 @@ * label: 'Local', // label for the item * icon: 'fa fa-rocket', // (optional) icon for the item * selected: true/false, // (optional) if present, display checkbox accordingly - * children: [] | function(done) // (optional) an array of child items, or a function + * children: [] | function(item,done) // (optional) an array of child items, or a function * // that will call the `done` callback with an array * // of child items * } @@ -66,7 +66,7 @@ scrollOnAdd: false, height: '100%', addItem: function(container,i,item) { - that._addSubtree(container,item,0); + that._addSubtree(that._topList,container,item,0); } }); if (this.options.data) { @@ -80,20 +80,27 @@ scrollOnAdd: false, height: 'auto', addItem: function(container,i,item) { - that._addSubtree(container,item,depth+1); + that._addSubtree(subtree,container,item,depth+1); } }); for (var i=0;i').appendTo(label); - // $('').appendTo(label); - label.click(function(e) { + item.treeList.addChild = function(newItem) { + item.treeList.childList.editableList('addItem',newItem) + item.children.push(newItem); + } + item.treeList.expand = function() { if (!container.hasClass("built") && typeof item.children === 'function') { container.addClass('built'); var childrenAdded = false; var spinner; - item.children(function(children) { + item.children(item,function(children) { childrenAdded = true; - that._addChildren(container,children,depth); + item.treeList.childList = that._addChildren(container,children,depth); if (spinner) { spinner.remove(); } + that._trigger("childrenloaded",null,item) }); if (!childrenAdded) { spinner = $('
    ').css({ @@ -125,7 +135,20 @@ } } - container.toggleClass("expanded"); + container.addClass("expanded"); + } + item.treeList.collapse = function() { + container.removeClass("expanded"); + } + + $('').appendTo(label); + // $('').appendTo(label); + label.click(function(e) { + if (container.hasClass("expanded")) { + item.treeList.collapse(); + } else { + item.treeList.expand(); + } }) } else { $('').appendTo(label); @@ -140,21 +163,27 @@ item.selected = this.checked; that._trigger("select",e,item); }) - } else if (!item.children) { + } else { label.click(function(e) { + that._topList.find(".selected").removeClass("selected"); + label.addClass("selected"); that._trigger("select",e,item) }) } if (item.icon) { $('').appendTo(label); } - $('').text(item.label).appendTo(label); + if (item.label) { + $('').text(item.label).appendTo(label); + } else if (item.element) { + $(item.element).appendTo(label); + } if (item.children) { if (Array.isArray(item.children)) { - that._addChildren(container,item.children,depth); + item.treeList.childList = that._addChildren(container,item.children,depth); } if (item.expanded) { - label.click(); + item.treeList.expand(); } } }, @@ -168,6 +197,8 @@ for (var i=0; i'+ + '
    '+ + '
    '+ + '
    '+ + '
    '+ + '
    '+ + '
    '+ + '
    '+ + '
    '+ + '
    '+ + ''+ + '
    ' - var _librarySaveConfirm = '
    '; - var _librarySave = '
    '; - var _libraryLookup = '
      '; + var _librarySave = '
      '+ + '
      '+ + '
      '+ + '
      '+ + '
      '+ + ''+ + '
      '+ + '
      '+ + '
      '+ + '
      ' + function saveToLibrary() { + var elementPrefix = activeLibrary.elementPrefix || "node-input-"; + var name = $("#"+elementPrefix+"name").val().trim(); + if (name === "") { + name = RED._("library.unnamedType",{type:activeLibrary.type}); + } + var filename = $("#node-dialog-library-save-filename").val().trim() + var selectedPath = saveLibraryBrowser.getPath(); + var queryArgs = []; + var data = {}; + for (var i=0; i 0 && !/[\/\\]/.test(filename); + if (valid) { + filenameInput.removeClass("input-error"); + $("#node-dialog-library-save-button").button("enable"); + } else { + filenameInput.addClass("input-error"); + $("#node-dialog-library-save-button").button("disable"); + } + },100); + } + function createUI(options) { var libraryData = {}; var selectedLibraryItem = null; - var libraryEditor = null; - elementPrefix = options.elementPrefix || "node-input-"; + var elementPrefix = options.elementPrefix || "node-input-"; // Orion editor has set/getText // ACE editor has set/getValue @@ -107,64 +247,7 @@ RED.library = (function() { options.editor.getValue = options.editor.getText; } - function buildFileListItem(item) { - var li = document.createElement("li"); - li.onmouseover = function(e) { $(this).addClass("list-hover"); }; - li.onmouseout = function(e) { $(this).removeClass("list-hover"); }; - return li; - } - - function buildFileList(root,data) { - var ul = document.createElement("ul"); - var li; - for (var i=0; i/ '); - $('').text(dirName).appendTo(bcli).click(function(e) { - $(this).parent().nextAll().remove(); - $.getJSON("library/"+options.url+root+dirName,function(data) { - $("#node-select-library").children().first().replaceWith(buildFileList(root+dirName+"/",data)); - }); - e.stopPropagation(); - }); - var bc = $("#node-dialog-library-breadcrumbs"); - $(".active",bc).removeClass("active"); - bc.append(bcli); - $.getJSON("library/"+options.url+root+dirName,function(data) { - $("#node-select-library").children().first().replaceWith(buildFileList(root+dirName+"/",data)); - }); - } - })(); - $('').appendTo(li); - $('').text(" "+v).appendTo(li); - ul.appendChild(li); - } else { - // file - li = buildFileListItem(v); - li.innerText = v.name; - li.onclick = (function() { - var item = v; - return function(e) { - $(".list-selected",ul).removeClass("list-selected"); - $(this).addClass("list-selected"); - $.get("library/"+options.url+root+item.fn, function(data) { - selectedLibraryItem = item; - libraryEditor.setValue(data,-1); - }); - } - })(); - ul.appendChild(li); - } - } - return ul; - } - + // Add the library button to the name in the edit dialog $('#'+elementPrefix+"name").css("width","calc(100% - 52px)").after( '
      '+ ' '+ @@ -175,331 +258,332 @@ RED.library = (function() { ); $('#node-input-'+options.type+'-menu-open-library').click(function(e) { - $("#node-select-library").children().remove(); - var bc = $("#node-dialog-library-breadcrumbs"); - bc.children().first().nextAll().remove(); - libraryEditor.setValue('',-1); - - $.getJSON("library/"+options.url,function(data) { - $("#node-select-library").append(buildFileList("/",data)); - $("#node-dialog-library-breadcrumbs a").click(function(e) { - $(this).parent().nextAll().remove(); - $("#node-select-library").children().first().replaceWith(buildFileList("/",data)); - e.stopPropagation(); - }); - $( "#node-dialog-library-lookup" ).dialog( "open" ); + activeLibrary = options; + loadLibraryFolder(options.url, "", function(files,items) { + var listing = [{ + icon: 'fa fa-archive', + label: "Local", + path: "", + expanded: true, + writable: false, + children: [{ + icon: 'fa fa-cube', + label: options.type, + path: options.type+"/", + expanded: true, + children: items, + files: files + }] + }] + loadLibraryBrowser.data(listing); }); + libraryEditor = ace.edit('node-dialog-library-load-preview-text'); + libraryEditor.setTheme("ace/theme/tomorrow"); + if (options.mode) { + libraryEditor.getSession().setMode(options.mode); + } + libraryEditor.setOptions({ + readOnly: true, + highlightActiveLine: false, + highlightGutterLine: false + }); + libraryEditor.renderer.$cursorLayer.element.style.opacity=0; + libraryEditor.$blockScrolling = Infinity; + + $( "#node-dialog-library-load" ).dialog("option","title",RED._("library.typeLibrary", {type:options.type})).dialog( "open" ); e.preventDefault(); }); $('#node-input-'+options.type+'-menu-save-library').click(function(e) { + activeLibrary = options; //var found = false; var name = $("#"+elementPrefix+"name").val().replace(/(^\s*)|(\s*$)/g,""); - - //var buildPathList = function(data,root) { - // var paths = []; - // if (data.d) { - // for (var i in data.d) { - // var dn = root+(root==""?"":"/")+i; - // var d = { - // label:dn, - // files:[] - // }; - // for (var f in data.d[i].f) { - // d.files.push(data.d[i].f[f].fn.split("/").slice(-1)[0]); - // } - // paths.push(d); - // paths = paths.concat(buildPathList(data.d[i],root+(root==""?"":"/")+i)); - // } - // } - // return paths; - //}; - $("#node-dialog-library-save-folder").attr("value",""); - var filename = name.replace(/[^\w-]/g,"-"); if (filename === "") { filename = "unnamed-"+options.type; } $("#node-dialog-library-save-filename").attr("value",filename+".js"); - //var paths = buildPathList(libraryData,""); - //$("#node-dialog-library-save-folder").autocomplete({ - // minLength: 0, - // source: paths, - // select: function( event, ui ) { - // $("#node-dialog-library-save-filename").autocomplete({ - // minLength: 0, - // source: ui.item.files - // }); - // } - //}); + loadLibraryFolder(options.url, "", function(files,items) { + var listing = [{ + icon: 'fa fa-archive', + label: "Local", + path: "", + expanded: true, + writable: false, + children: [{ + icon: 'fa fa-cube', + label: options.type, + path: options.type+"/", + expanded: true, + children: items, + files: files + }] + }] + saveLibraryBrowser.data(listing); + }); $( "#node-dialog-library-save" ).dialog( "open" ); e.preventDefault(); }); - libraryEditor = ace.edit('node-select-library-text'); - libraryEditor.setTheme("ace/theme/tomorrow"); - if (options.mode) { - libraryEditor.getSession().setMode(options.mode); - } - libraryEditor.setOptions({ - readOnly: true, - highlightActiveLine: false, - highlightGutterLine: false - }); - libraryEditor.renderer.$cursorLayer.element.style.opacity=0; - libraryEditor.$blockScrolling = Infinity; - - $( "#node-dialog-library-lookup" ).dialog({ - title: RED._("library.typeLibrary", {type:options.type}), - modal: true, - autoOpen: false, - width: 800, - height: 450, - buttons: [ - { - text: RED._("common.label.cancel"), - click: function() { - $( this ).dialog( "close" ); - } - }, - { - text: RED._("common.label.load"), - class: "primary", - click: function() { - if (selectedLibraryItem) { - for (var i=0; i
      ').appendTo(options.container); + var dirsPane = $('
      ').appendTo(panes); + var filesPane = $('
      ').appendTo(panes); + // + // '
      '+ + // '
      '+ + // '
      '+ + // '
      '+ + RED.panels.create({ + container:panes, + dir: "horizontal" + }); + var dirList = $("
      ").css({width: "100%", height: "100%"}).appendTo(dirsPane) + .treeList({}).on('treelistselect', function(event, item) { + fileList.treeList('data',item.files||[]); + if (addButton) { + if (item.writable === false) { + addButton.prop('disabled', true); + } else { + addButton.prop('disabled', false); + } + } + if (options.onpathselect) { + options.onpathselect(item); + } + }).on('treelistchildrenloaded', function(event) { + var selected = dirList.treeList('selected'); + fileList.treeList('data',selected.files||[]); + }); + var addButton; + if (options.addFolderButton) { + var tools = $("
      ").css({position: "absolute",bottom:"3px",left:"3px"}).appendTo(dirsPane) + addButton= $('').appendTo(tools).click(function(e) { + var selected = dirList.treeList('selected'); + + var defaultFolderName = "new-folder"; + var defaultFolderNameMatches = {}; + selected.children.forEach(function(c) { + if (/^new-folder/.test(c.label)) { + defaultFolderNameMatches[c.label] = true + } + }); + var folderIndex = 2; + while(defaultFolderNameMatches[defaultFolderName]) { + defaultFolderName = "new-folder-"+(folderIndex++) + } + + selected.treeList.expand(); + var input = $('').val(defaultFolderName); + var newItem = { + icon: "fa fa-folder-o", + children:[], + path: selected.path, + element: input + } + var confirmAdd = function() { + var val = input.val().trim(); + if (val === "") { + cancelAdd(); + return; + } else { + for (var i=0;i").css({width: "100%", height: "100%"}).appendTo(filesPane) + .treeList({}).on('treelistselect', function(event, item) { + if (options.onselect) { + options.onselect(item); + } + }) + return { + getFile: function() { + return fileList.treeList('selected'); + }, + getPath: function() { + return dirList.treeList('selected'); + }, + data: function(content) { + dirList.treeList('data',content); + setTimeout(function() { + dirList.treeList('select',content[0]); + },100); + } + } } return { init: function() { - $(_librarySave).appendTo(document.body); - $(_librarySaveConfirm).appendTo(document.body); $(_libraryLookup).appendTo(document.body); - RED.actions.add("core:library-export",exportFlow); - - RED.events.on("view:selection-changed",function(selection) { - if (!selection.nodes) { - RED.menu.setDisabled("menu-item-export-library",true); - } else { - RED.menu.setDisabled("menu-item-export-library",false); + $( "#node-dialog-library-save" ).dialog({ + title: RED._("library.saveToLibrary"), + modal: true, + autoOpen: false, + width: 800, + resizable: false, + buttons: [ + { + text: RED._("common.label.cancel"), + click: function() { + $( this ).dialog( "close" ); + } + }, + { + id: "node-dialog-library-save-button", + text: RED._("common.label.save"), + class: "primary", + click: function() { + saveToLibrary(false); + $( this ).dialog( "close" ); + } + } + ], + open: function(e) { + $(this).parent().find(".ui-dialog-titlebar-close").hide(); } }); + saveLibraryBrowser = RED.library.createBrowser({ + container: $("#node-dialog-library-save-browser"), + addFolderButton: true, + onpathselect: function(item) { + try { + if (item.writable === false) { + $("#node-dialog-library-save-button").button("disable"); + } else { + $("#node-dialog-library-save-button").button("enable"); + } + } catch(err) {} + }, + onselect: function(file) { + $("#node-dialog-library-save-filename").val(file.label); + } + }); + $("#node-dialog-library-save-filename").keyup(function() { validateExportFilename($(this))}); + $("#node-dialog-library-save-filename").on('paste',function() { var input = $(this); setTimeout(function() { validateExportFilename(input)},10)}); + + $( "#node-dialog-library-load" ).dialog({ + modal: true, + autoOpen: false, + width: 800, + resizable: false, + buttons: [ + { + text: RED._("common.label.cancel"), + click: function() { + $( this ).dialog( "close" ); + } + }, + { + text: RED._("common.label.load"), + class: "primary", + click: function() { + if (selectedLibraryItem) { + for (var i=0; iType').appendTo(table); + $(propRow.children()[1]).text(activeLibrary.type); + if (file.props.hasOwnProperty('name')) { + propRow = $('Name'+file.props.name+'').appendTo(table); + $(propRow.children()[1]).text(file.props.name); + } + for (var p in file.props) { + if (file.props.hasOwnProperty(p) && p !== 'name' && p !== 'fn') { + propRow = $('').appendTo(table); + $(propRow.children()[0]).text(p); + RED.utils.createObjectElement(file.props[p]).appendTo(propRow.children()[1]); + } + } + libraryEditor.setValue(data,-1); + }); + } else { + libraryEditor.setValue("",-1); + } + } + }); + + if (RED.settings.theme("menu.menu-item-import-library") !== false) { loadFlowLibrary(); } - - exportToLibraryDialog = $('
      ') - .appendTo("body") - .dialog({ - modal: true, - autoOpen: false, - width: 500, - resizable: false, - title: RED._("library.exportToLibrary"), - buttons: [ - { - id: "library-dialog-cancel", - text: RED._("common.label.cancel"), - click: function() { - $( this ).dialog( "close" ); - } - }, - { - id: "library-dialog-ok", - class: "primary", - text: RED._("common.label.export"), - click: function() { - //TODO: move this to RED.library - var flowName = $("#node-input-library-filename").val(); - flowName = flowName.trim(); - if(flowName === "" || flowName.endsWith("/")) { - RED.notify(RED._("library.invalidFilename"),"warning"); - } else { - $.ajax({ - url:'library/flows/'+flowName, - type: "POST", - data: $("#node-input-library-filename").attr('nodes'), - contentType: "application/json; charset=utf-8" - }).done(function() { - RED.library.loadFlowLibrary(); - RED.notify(RED._("library.savedNodes"),"success"); - }).fail(function(xhr,textStatus,err) { - if (xhr.status === 401) { - RED.notify(RED._("library.saveFailed",{message:RED._("user.notAuthorized")}),"error"); - } else { - RED.notify(RED._("library.saveFailed",{message:xhr.responseText}),"error"); - } - }); - } - $( this ).dialog( "close" ); - } - } - ], - open: function(e) { - $(this).parent().find(".ui-dialog-titlebar-close").hide(); - }, - close: function(e) { - } - }); - exportToLibraryDialog.children(".dialog-form").append($( - '
      '+ - ''+ - ''+ - ''+ // Second hidden input to prevent submit on Enter - '
      ' - )); }, create: createUI, loadFlowLibrary: loadFlowLibrary, - + createBrowser:createBrowser, export: exportFlow } })(); diff --git a/packages/node_modules/@node-red/editor-client/src/sass/colors.scss b/packages/node_modules/@node-red/editor-client/src/sass/colors.scss index 3066e247c..367a05a7c 100644 --- a/packages/node_modules/@node-red/editor-client/src/sass/colors.scss +++ b/packages/node_modules/@node-red/editor-client/src/sass/colors.scss @@ -22,6 +22,10 @@ $form-input-focus-color: rgba(85,150,230,0.8); $form-input-border-color: #ccc; $form-input-border-selected-color: #aaa; +$list-item-color: #666; +$list-item-background-hover: #f3f3f3; +$list-item-background-active: #efefef; +$list-item-background-selected: #eee; $node-selected-color: #ff7f0e; $port-selected-color: #ff7f0e; diff --git a/packages/node_modules/@node-red/editor-client/src/sass/library.scss b/packages/node_modules/@node-red/editor-client/src/sass/library.scss index cb0032cd0..26ef9fe09 100644 --- a/packages/node_modules/@node-red/editor-client/src/sass/library.scss +++ b/packages/node_modules/@node-red/editor-client/src/sass/library.scss @@ -48,3 +48,112 @@ } } } +#clipboard-dialog-export-tab-clipboard { + padding: 10px; + textarea { + resize: none; + width: 100%; + border-radius: 4px; + font-family: monospace !important; + font-size: 13px !important; + height: 430px; + line-height: 1.3em; + padding: 6px 10px; + background: #F3E7E7; + color: #533; + } +} + +#clipboard-dialog-export-tabs-content { + position: absolute; + top: 0; + left: 120px; + right: 0; + bottom: 0; + padding: 0; + background: white; + + &>div { + height: calc(100% - 20px) + } +} + +#clipboard-dialog-export-tab-library { + .form-row { + margin-left: 10px; + } +} + + +#clipboard-dialog-tab-library-name { + width: calc(100% - 120px); +} +#clipboard-dialog-export-tab-library-browser { + height: calc(100% - 40px); + margin-bottom: 10px; + border-bottom: 1px solid $primary-border-color; + box-sizing: border-box; +} + + + +.red-ui-library-browser { + height: 100%; + + .red-ui-treeList-container { + background: white; + border: none; + border-radius: 0; + li { + background: none; + } + label { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + } + .red-ui-editableList-border { + border-radius: 0; + } + + .red-ui-treeList-label input.red-ui-treeList-input { + border-radius: 2px; + margin-top: -6px; + margin-bottom: -6px; + } +} + +#node-dialog-library-save-browser { + height: calc(100% - 60px); + border: 1px solid $primary-border-color; + margin-bottom: 10px; +} +#node-dialog-library-load-browser { + height: 200px; + border: 1px solid $primary-border-color; + margin-bottom: 10px; +} +#node-dialog-library-load-preview { + height: 300px; +} + +#node-dialog-library-load-preview-text { + box-sizing: border-box; + display: inline-block; + height: 100%; + width: calc(50% - 5px); + border: 1px solid $primary-border-color; +} +#node-dialog-library-load-preview-details { + box-sizing: border-box; + display: inline-block; + height: 100%; + width: calc(50% - 5px); + margin-right: 10px; + border: 1px solid $primary-border-color; + + .node-info-node-row:first-child { + border-top: none; + } +} diff --git a/packages/node_modules/@node-red/editor-client/src/sass/mixins.scss b/packages/node_modules/@node-red/editor-client/src/sass/mixins.scss index 8dd0fc3ae..aacda031c 100644 --- a/packages/node_modules/@node-red/editor-client/src/sass/mixins.scss +++ b/packages/node_modules/@node-red/editor-client/src/sass/mixins.scss @@ -36,17 +36,10 @@ } -@mixin workspace-button { - @include disable-selection; - box-sizing: border-box; - display: inline-block; +@mixin reset-a-style { color: $workspace-button-color !important; background: $workspace-button-background; - border: 1px solid $form-input-border-color; - text-align: center; - margin:0; text-decoration: none; - cursor:pointer; &.disabled, &:disabled { cursor: default; @@ -67,6 +60,19 @@ background: $workspace-button-background-active; text-decoration: none; } +} + +@mixin workspace-button { + @include disable-selection; + @include reset-a-style; + + box-sizing: border-box; + display: inline-block; + border: 1px solid $form-input-border-color; + text-align: center; + margin:0; + cursor:pointer; + // &.selected:not(.disabled):not(:disabled) { // color: $workspace-button-color-selected !important; // background: $workspace-button-background-active; @@ -150,12 +156,12 @@ } &:not(.single) { color: $workspace-button-toggle-color !important; - background:$workspace-button-background-active; + background:$workspace-button-background; margin-bottom: 1px; &.selected:not(.disabled):not(:disabled) { color: $workspace-button-toggle-color-selected !important; - background: $workspace-button-background; + background: $workspace-button-background-active; border-bottom-width: 2px; border-bottom-color: $form-input-border-selected-color; margin-bottom: 0; diff --git a/packages/node_modules/@node-red/editor-client/src/sass/tabs.scss b/packages/node_modules/@node-red/editor-client/src/sass/tabs.scss index 489cce5f8..6770baf24 100644 --- a/packages/node_modules/@node-red/editor-client/src/sass/tabs.scss +++ b/packages/node_modules/@node-red/editor-client/src/sass/tabs.scss @@ -247,7 +247,7 @@ z-index: 2; &.red-ui-tab-link-button { &:not(.active) { - background: #eee; + // background: #eee; } } &.red-ui-tab-link-button-menu { diff --git a/packages/node_modules/@node-red/editor-client/src/sass/ui/common/treeList.scss b/packages/node_modules/@node-red/editor-client/src/sass/ui/common/treeList.scss index 2d090ae72..a6f0388e5 100644 --- a/packages/node_modules/@node-red/editor-client/src/sass/ui/common/treeList.scss +++ b/packages/node_modules/@node-red/editor-client/src/sass/ui/common/treeList.scss @@ -72,22 +72,29 @@ label.red-ui-treeList-label { @include disable-selection; padding: 6px 0; display: block; - color: $form-text-color; + color: $list-item-color; text-decoration: none; cursor: pointer; vertical-align: middle; margin: 0; - &:hover { - background: #f9f9f9; - color: $form-text-color; - text-decoration: none; - } + // &:hover { + // background: $list-item-background-hover; + // color: $list-item-color; + // text-decoration: none; + // } &:focus { + background: $list-item-background-hover; outline: none; - color: $form-text-color; + color: $list-item-color; text-decoration: none; } + &.selected { + background: $list-item-background-selected; + outline: none; + color: $list-item-color; + } + input { margin: 0; } From 6f37d5ca5c7db24a4f15a7a569c46ab347454dbd Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Tue, 23 Apr 2019 15:46:15 +0100 Subject: [PATCH 007/213] Move type-library dialogs to new treeList style --- .../@node-red/editor-client/src/js/red.js | 2 +- .../editor-client/src/js/ui/clipboard.js | 25 ++- .../editor-client/src/js/ui/common/menu.js | 1 - .../src/js/ui/common/treeList.js | 7 +- .../editor-client/src/js/ui/library.js | 212 +++++++----------- .../editor-client/src/sass/library.scss | 22 +- .../editor-client/src/sass/panels.scss | 4 +- 7 files changed, 108 insertions(+), 165 deletions(-) diff --git a/packages/node_modules/@node-red/editor-client/src/js/red.js b/packages/node_modules/@node-red/editor-client/src/js/red.js index 8fdb0de08..3cbaee458 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/red.js +++ b/packages/node_modules/@node-red/editor-client/src/js/red.js @@ -464,7 +464,7 @@ var RED = (function() { menuOptions.push(null); menuOptions.push({id:"menu-item-import",label:RED._("menu.label.import"),options:[ {id:"menu-item-import-clipboard",label:RED._("menu.label.clipboard"),onselect:"core:show-import-dialog"}, - {id:"menu-item-import-library",label:RED._("menu.label.library"),options:[]} + {id:"menu-item-import-library",label:RED._("menu.label.library"),onselect:"core:library-import"} ]}); menuOptions.push({id:"menu-item-export",label:RED._("menu.label.export"),options:[ {id:"menu-item-export-clipboard",label:RED._("menu.label.clipboard"),onselect:"core:show-export-dialog"}, diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/clipboard.js b/packages/node_modules/@node-red/editor-client/src/js/ui/clipboard.js index 09af09251..b84e98ed2 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/clipboard.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/clipboard.js @@ -71,7 +71,10 @@ RED.clipboard = (function() { $( this ).dialog( "close" ); } else { var flowToExport = $("#clipboard-export").val(); - var selectedPath = libraryBrowser.getPath(); + var selectedPath = libraryBrowser.getSelected(); + if (!selectedPath.children) { + selectedPath = selectedPath.parent; + } var filename = $("#clipboard-dialog-tab-library-name").val().trim(); var saveFlow = function() { $.ajax({ @@ -80,7 +83,6 @@ RED.clipboard = (function() { data: flowToExport, contentType: "application/json; charset=utf-8" }).done(function() { - // RED.library.loadFlowLibrary(); $(dialog).dialog( "close" ); RED.notify(RED._("library.exportedToLibrary"),"success"); }).fail(function(xhr,textStatus,err) { @@ -409,7 +411,7 @@ RED.clipboard = (function() { container: $("#clipboard-dialog-export-tab-library-browser"), addFolderButton: true, onselect: function(file) { - if (file && file.label) { + if (file && file.label && !file.children) { $("#clipboard-dialog-tab-library-name").val(file.label); } } @@ -421,17 +423,17 @@ RED.clipboard = (function() { label: label, path: root }; + result.children = []; if (list.f) { - result.files = list.f.map(function(f) { - return { + list.f.forEach(function(f) { + result.children.push({ icon: 'fa fa-file-o', label: f, path: root+f - } + }); }); } - result.children = []; if (list.d) { for (var l in list.d) { if (list.d.hasOwnProperty(l)) { @@ -441,6 +443,15 @@ RED.clipboard = (function() { } } } + result.children.sort(function(A,B){ + if (A.children && !B.children) { + return -1; + } else if (!A.children && B.children) { + return 1; + } else { + return A.label.localeCompare(B.label); + } + }); return result; } diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/common/menu.js b/packages/node_modules/@node-red/editor-client/src/js/ui/common/menu.js index 612bf4516..adac42463 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/common/menu.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/common/menu.js @@ -263,6 +263,5 @@ RED.menu = (function() { addItem: addItem, removeItem: removeItem, setAction: setAction - //TODO: add an api for replacing a submenu - see library.js:loadFlowLibrary } })(); diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/common/treeList.js b/packages/node_modules/@node-red/editor-client/src/js/ui/common/treeList.js index 79371fee6..70054c727 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/common/treeList.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/common/treeList.js @@ -73,7 +73,7 @@ this.data(this.options.data); } }, - _addChildren: function(container,children,depth) { + _addChildren: function(container,parent,children,depth) { var that = this; var subtree = $('
        ').appendTo(container).editableList({ addButton: false, @@ -84,6 +84,7 @@ } }); for (var i=0;i'+ '
        '+ - '
        '+ - '
        '+ - '
        '+ - '
        '+ + '
        '+ + '
        '+ + '
        '+ + '
        '+ + '
        '+ + '
        '+ + '
        '+ + '
        '+ + '
        '+ '
        '+ - '
        '+ '
        '+ '
        '+ ''+ @@ -54,7 +59,10 @@ RED.library = (function() { name = RED._("library.unnamedType",{type:activeLibrary.type}); } var filename = $("#node-dialog-library-save-filename").val().trim() - var selectedPath = saveLibraryBrowser.getPath(); + var selectedPath = saveLibraryBrowser.getSelected(); + if (!selectedPath.children) { + selectedPath = selectedPath.parent; + } var queryArgs = []; var data = {}; for (var i=0; i
        ').appendTo(options.container); - var dirsPane = $('
        ').appendTo(panes); - var filesPane = $('
        ').appendTo(panes); + // var dirsPane = $('
        ').appendTo(panes); + // var filesPane = $('
        ').appendTo(panes); // - // '
        '+ - // '
        '+ - // '
        '+ - // '
        '+ - RED.panels.create({ - container:panes, - dir: "horizontal" - }); - var dirList = $("
        ").css({width: "100%", height: "100%"}).appendTo(dirsPane) + // RED.panels.create({ + // container:panes, + // dir: "horizontal" + // }); + var dirList = $("
        ").css({width: "100%", height: "100%"}).appendTo(panes) .treeList({}).on('treelistselect', function(event, item) { - fileList.treeList('data',item.files||[]); if (addButton) { if (item.writable === false) { addButton.prop('disabled', true); @@ -358,21 +301,21 @@ RED.library = (function() { addButton.prop('disabled', false); } } - if (options.onpathselect) { - options.onpathselect(item); + if (options.onselect) { + options.onselect(item); } - }).on('treelistchildrenloaded', function(event) { - var selected = dirList.treeList('selected'); - fileList.treeList('data',selected.files||[]); }); var addButton; if (options.addFolderButton) { - var tools = $("
        ").css({position: "absolute",bottom:"3px",left:"3px"}).appendTo(dirsPane) + var tools = $("
        ").css({position: "absolute",bottom:"3px",left:"3px"}).appendTo(panes) addButton= $('').appendTo(tools).click(function(e) { - var selected = dirList.treeList('selected'); - var defaultFolderName = "new-folder"; var defaultFolderNameMatches = {}; + + var selected = dirList.treeList('selected'); + if (!selected.children) { + selected = selected.parent; + } selected.children.forEach(function(c) { if (/^new-folder/.test(c.label)) { defaultFolderNameMatches[c.label] = true @@ -435,17 +378,8 @@ RED.library = (function() { }); } - var fileList = $("
        ").css({width: "100%", height: "100%"}).appendTo(filesPane) - .treeList({}).on('treelistselect', function(event, item) { - if (options.onselect) { - options.onselect(item); - } - }) return { - getFile: function() { - return fileList.treeList('selected'); - }, - getPath: function() { + getSelected: function() { return dirList.treeList('selected'); }, data: function(content) { @@ -494,17 +428,18 @@ RED.library = (function() { saveLibraryBrowser = RED.library.createBrowser({ container: $("#node-dialog-library-save-browser"), addFolderButton: true, - onpathselect: function(item) { - try { + onselect: function(item) { + if (item.label) { + if (!item.children) { + $("#node-dialog-library-save-filename").val(item.label); + item = item.parent; + } if (item.writable === false) { $("#node-dialog-library-save-button").button("disable"); } else { $("#node-dialog-library-save-button").button("enable"); } - } catch(err) {} - }, - onselect: function(file) { - $("#node-dialog-library-save-filename").val(file.label); + } } }); $("#node-dialog-library-save-filename").keyup(function() { validateExportFilename($(this))}); @@ -527,11 +462,12 @@ RED.library = (function() { class: "primary", click: function() { if (selectedLibraryItem) { - for (var i=0; iType').appendTo(table); @@ -575,14 +511,16 @@ RED.library = (function() { } } }); - - - if (RED.settings.theme("menu.menu-item-import-library") !== false) { - loadFlowLibrary(); - } + RED.panels.create({ + container:$("#node-dialog-library-load-panes"), + dir: "horizontal" + }); + RED.panels.create({ + container:$("#node-dialog-library-load-preview"), + dir: "vertical" + }); }, create: createUI, - loadFlowLibrary: loadFlowLibrary, createBrowser:createBrowser, export: exportFlow } diff --git a/packages/node_modules/@node-red/editor-client/src/sass/library.scss b/packages/node_modules/@node-red/editor-client/src/sass/library.scss index 26ef9fe09..8fef4cadd 100644 --- a/packages/node_modules/@node-red/editor-client/src/sass/library.scss +++ b/packages/node_modules/@node-red/editor-client/src/sass/library.scss @@ -98,6 +98,7 @@ .red-ui-library-browser { + position: relative; height: 100%; .red-ui-treeList-container { @@ -130,29 +131,22 @@ margin-bottom: 10px; } #node-dialog-library-load-browser { - height: 200px; - border: 1px solid $primary-border-color; - margin-bottom: 10px; + // border: 1px solid $primary-border-color; } +#node-dialog-library-load-panes { + border: 1px solid $primary-border-color; +} + + #node-dialog-library-load-preview { - height: 300px; + height: 100%; } #node-dialog-library-load-preview-text { box-sizing: border-box; - display: inline-block; - height: 100%; - width: calc(50% - 5px); - border: 1px solid $primary-border-color; } #node-dialog-library-load-preview-details { box-sizing: border-box; - display: inline-block; - height: 100%; - width: calc(50% - 5px); - margin-right: 10px; - border: 1px solid $primary-border-color; - .node-info-node-row:first-child { border-top: none; } diff --git a/packages/node_modules/@node-red/editor-client/src/sass/panels.scss b/packages/node_modules/@node-red/editor-client/src/sass/panels.scss index 00fc771eb..b6b98e6ac 100644 --- a/packages/node_modules/@node-red/editor-client/src/sass/panels.scss +++ b/packages/node_modules/@node-red/editor-client/src/sass/panels.scss @@ -41,13 +41,13 @@ .red-ui-panels.red-ui-panels-horizontal { height: 100%; - .red-ui-panel { + &>.red-ui-panel { vertical-align: top; display: inline-block; height: 100%; width: calc(50% - 4px); } - .red-ui-panels-separator { + &>.red-ui-panels-separator { vertical-align: top; border-top: none; border-bottom: none; From 5e43a02cd3de4c8bb2510efe93213712ddef2a05 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Wed, 24 Apr 2019 11:50:24 +0100 Subject: [PATCH 008/213] Move remaining library dialogs to new style --- .../editor-client/locales/de/editor.json | 18 +- .../editor-client/locales/en-US/editor.json | 17 +- .../editor-client/locales/ja/editor.json | 16 +- .../editor-client/locales/ko/editor.json | 18 +- .../editor-client/locales/zh-CN/editor.json | 15 +- .../@node-red/editor-client/src/js/red.js | 10 +- .../editor-client/src/js/ui/clipboard.js | 404 +++++++++++------- .../src/js/ui/common/treeList.js | 25 +- .../editor-client/src/js/ui/library.js | 24 +- .../editor-client/src/sass/library.scss | 26 +- .../src/sass/ui/common/treeList.scss | 15 +- 11 files changed, 335 insertions(+), 253 deletions(-) diff --git a/packages/node_modules/@node-red/editor-client/locales/de/editor.json b/packages/node_modules/@node-red/editor-client/locales/de/editor.json index d477d3f0e..d052b5ef8 100755 --- a/packages/node_modules/@node-red/editor-client/locales/de/editor.json +++ b/packages/node_modules/@node-red/editor-client/locales/de/editor.json @@ -55,9 +55,6 @@ "export" : "Exportieren", "search" : "Flows durchsuchen", "searchInput" : "durchsuchen Sie Ihre Flows", - "clipboard" : "Zwischenablage", - "library" : "Bibliothek", - "examples" : "Beispiele", "subflows" : "Subflow", "createSubflow" : "Subflow erstellen", "selectionToSubflow" : "Auswahl für Subflow", @@ -136,8 +133,8 @@ } }, "clipboard" : { + "clipboard" : "Zwischenablage", "nodes" : "Knoten", - "selectNodes" : "Wählen Sie den Text oben aus, und kopieren Sie die Datei in die Zwischenablage.", "pasteNodes" : "Knoten hier einfügen", "importNodes" : "Knoten importieren", "exportNodes" : "Knoten in Zwischenablage exportieren", @@ -297,22 +294,19 @@ "managePalette" : "Palette verwalten" }, "library" : { + "library" : "Bibliothek", "openLibrary" : "Bibliothek öffnen ...", "saveToLibrary" : "In Bibliothek speichern ...", "typeLibrary" : "__type__, Bibliothek", "unnamedType" : "Unbenannt __type__", - "exportToLibrary" : "Knoten in Bibliothek exportieren", "dialogSaveOverwrite" : "Ein __libraryType__ mit dem Namen __libraryName__ ist bereits vorhanden. Überschreiben?", "invalidFilename" : "Ungültiger Dateiname", "savedNodes" : "Gespeicherte Knoten", "savedType" : "Gespeichert __type__", "saveFailed" : "Speichern fehlgeschlagen: __message__", - "filename" : "Name der Datei", - "folder" : "Ordner", - "filenamePlaceholder" : "Datei", - "fullFilenamePlaceholder" : "a/b/Datei", - "folderPlaceholder" : "a/b", - "breadcrumb" : "Bibliothek" + "types": { + "examples" : "Beispiele" + } }, "palette" : { "noInfo" : "Keine Informationen verfügbar", @@ -826,4 +820,4 @@ "code" : "code" } } -} \ No newline at end of file +} diff --git a/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json b/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json index d178a8f1e..c2eb9ad65 100755 --- a/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json +++ b/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json @@ -59,9 +59,6 @@ "export": "Export", "search": "Search flows", "searchInput": "search your flows", - "clipboard": "Clipboard", - "library": "Library", - "examples": "Examples", "subflows": "Subflows", "createSubflow": "Create Subflow", "selectionToSubflow": "Selection to Subflow", @@ -154,6 +151,7 @@ } }, "clipboard": { + "clipboard": "Clipboard", "nodes": "Nodes", "node": "__count__ node", "node_plural": "__count__ nodes", @@ -163,7 +161,6 @@ "flow_plural": "__count__ flows", "subflow": "__count__ subflow", "subflow_plural": "__count__ subflows", - "selectNodes": "Select the text above and copy to the clipboard.", "pasteNodes": "Paste flow json or", "selectFile": "select a file to import", "importNodes": "Import nodes", @@ -353,23 +350,21 @@ "managePalette": "Manage palette" }, "library": { + "library": "Library", "openLibrary": "Open Library...", "saveToLibrary": "Save to Library...", "typeLibrary": "__type__ library", "unnamedType": "Unnamed __type__", - "exportToLibrary": "Export nodes to library", "exportedToLibrary": "Nodes exported to library", "dialogSaveOverwrite": "A __libraryType__ called __libraryName__ already exists. Overwrite?", "invalidFilename": "Invalid filename", "savedNodes": "Saved nodes", "savedType": "Saved __type__", "saveFailed": "Save failed: __message__", - "filename": "Filename", - "folder": "Folder", - "filenamePlaceholder": "file", - "fullFilenamePlaceholder": "a/b/file", - "folderPlaceholder": "a/b", - "breadcrumb": "Library" + "types": { + "local": "Local", + "examples": "Examples" + } }, "palette": { "noInfo": "no information available", diff --git a/packages/node_modules/@node-red/editor-client/locales/ja/editor.json b/packages/node_modules/@node-red/editor-client/locales/ja/editor.json index 3672dde38..83a00a06e 100755 --- a/packages/node_modules/@node-red/editor-client/locales/ja/editor.json +++ b/packages/node_modules/@node-red/editor-client/locales/ja/editor.json @@ -59,9 +59,6 @@ "export": "書き出し", "search": "ノードを検索", "searchInput": "ノードを検索", - "clipboard": "クリップボード", - "library": "ライブラリ", - "examples": "サンプル", "subflows": "サブフロー", "createSubflow": "サブフローを作成", "selectionToSubflow": "選択部分をサブフロー化", @@ -154,6 +151,7 @@ } }, "clipboard": { + "clipboard": "クリップボード", "nodes": "ノード", "node": "__count__ 個のノード", "node_plural": "__count__ 個のノード", @@ -163,7 +161,6 @@ "flow_plural": "__count__ 個のフロー", "subflow": "__count__ 個のサブフロー", "subflow_plural": "__count__ 個のサブフロー", - "selectNodes": "上のテキストを選択し、クリップボードへコピーしてください", "pasteNodes": "JSON形式のフローデータを貼り付けてください", "selectFile": "読み込むファイルを選択してください", "importNodes": "フローをクリップボートから読み込み", @@ -349,22 +346,19 @@ "managePalette": "パレットの管理" }, "library": { + "library": "ライブラリ", "openLibrary": "ライブラリを開く", "saveToLibrary": "ライブラリへ保存", "typeLibrary": "__type__ ライブラリ", "unnamedType": "名前なし __type__", - "exportToLibrary": "ライブラリへフローを書き出す", "dialogSaveOverwrite": "__libraryName__ という __libraryType__ は既に存在しています 上書きしますか?", "invalidFilename": "不正なファイル名", "savedNodes": "フローを保存しました", "savedType": "__type__ を保存しました", "saveFailed": "保存に失敗しました: __message__", - "filename": "ファイル名", - "folder": "フォルダ", - "filenamePlaceholder": "ファイル", - "fullFilenamePlaceholder": "a/b/file", - "folderPlaceholder": "a/b", - "breadcrumb": "ライブラリ" + "types": { + "examples": "サンプル" + } }, "palette": { "noInfo": "情報がありません", diff --git a/packages/node_modules/@node-red/editor-client/locales/ko/editor.json b/packages/node_modules/@node-red/editor-client/locales/ko/editor.json index da7c69e64..3e7811f53 100755 --- a/packages/node_modules/@node-red/editor-client/locales/ko/editor.json +++ b/packages/node_modules/@node-red/editor-client/locales/ko/editor.json @@ -58,9 +58,6 @@ "export": "내보내기", "search": "플로우 겅색", "searchInput": "플로우 검색", - "clipboard": "클립보드", - "library": "라이브러리", - "examples": "예시", "subflows": "보조 플로우", "createSubflow": "보조 플로우 생성", "selectionToSubflow": "보조 플로우 선택", @@ -148,6 +145,7 @@ } }, "clipboard": { + "clipboard": "클립보드", "nodes": "노드", "node": "__count__ 개의 노드", "node_plural": "__count__ 개의 노드", @@ -157,7 +155,6 @@ "flow_plural": "__count__ 개의 플로우", "subflow": "__count__ 개의 서브 플로우", "subflow_plural": "__count__ 개의 서브 플로우", - "selectNodes": "텍스트를 선택하고 클립보드에 복사하세요", "pasteNodes": "여기에 노드를 붙여넣기 하세요", "selectFile": "불러올 파일을 선택하세요", "importNodes": "노드 불러오기", @@ -338,22 +335,19 @@ "managePalette": "팔렛트 관리" }, "library": { + "library": "라이브러리", "openLibrary": "라이브러리 열기...", "saveToLibrary": "라이브러리로 저장...", "typeLibrary": "__type__ 라이브러리", "unnamedType": "이름없는 __type__", - "exportToLibrary": "라이브러리로 노드 내보내기", "dialogSaveOverwrite": "__libraryType__이 __libraryName__으로 이미 등록되어있습니다. 덮어쓸까요?", "invalidFilename": "파일명이 올바르지 않습니다", "savedNodes": "저장된 노드", "savedType": "저장된 __type__", "saveFailed": "저장 실패 : __message__", - "filename": "파일명", - "folder": "폴더명", - "filenamePlaceholder": "파일", - "fullFilenamePlaceholder": "a/b/file", - "folderPlaceholder": "a/b", - "breadcrumb": "라이브러리" + "types": { + "examples": "예시" + } }, "palette": { "noInfo": "정보 없음", @@ -904,4 +898,4 @@ "description": "상세 내역", "appearance": "모양" } -} \ No newline at end of file +} diff --git a/packages/node_modules/@node-red/editor-client/locales/zh-CN/editor.json b/packages/node_modules/@node-red/editor-client/locales/zh-CN/editor.json index 975a20448..f65f428f6 100644 --- a/packages/node_modules/@node-red/editor-client/locales/zh-CN/editor.json +++ b/packages/node_modules/@node-red/editor-client/locales/zh-CN/editor.json @@ -50,9 +50,6 @@ "export": "导出", "search": "查找流程", "searchInput": "查找流程", - "clipboard": "剪贴板", - "library": "库", - "examples": "例子", "subflows": "子流程", "createSubflow": "新建子流程", "selectionToSubflow": "将选择部分更改为子流程", @@ -100,8 +97,8 @@ } }, "clipboard": { + "clipboard": "剪贴板", "nodes": "节点", - "selectNodes": "选择上面的文本并复制到剪贴板", "pasteNodes": "在这里粘贴节点", "importNodes": "导入节点", "exportNodes": "导出节点至剪贴板", @@ -237,6 +234,7 @@ "managePalette": "管理面板" }, "library": { + "library": "库", "openLibrary": "打开库...", "saveToLibrary": "保存到库...", "typeLibrary": "__type__类型库", @@ -247,12 +245,9 @@ "savedNodes": "保存的节点", "savedType": "已保存__type__", "saveFailed": "保存失败: __message__", - "filename": "文件名", - "folder": "文件夹", - "filenamePlaceholder": "文件", - "fullFilenamePlaceholder": "a/b/文件", - "folderPlaceholder": "a/b", - "breadcrumb": "库" + "types": { + "examples": "例子" + } }, "palette": { "noInfo": "无可用信息", diff --git a/packages/node_modules/@node-red/editor-client/src/js/red.js b/packages/node_modules/@node-red/editor-client/src/js/red.js index 3cbaee458..787166c00 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/red.js +++ b/packages/node_modules/@node-red/editor-client/src/js/red.js @@ -462,14 +462,8 @@ var RED = (function() { null ]}); menuOptions.push(null); - menuOptions.push({id:"menu-item-import",label:RED._("menu.label.import"),options:[ - {id:"menu-item-import-clipboard",label:RED._("menu.label.clipboard"),onselect:"core:show-import-dialog"}, - {id:"menu-item-import-library",label:RED._("menu.label.library"),onselect:"core:library-import"} - ]}); - menuOptions.push({id:"menu-item-export",label:RED._("menu.label.export"),options:[ - {id:"menu-item-export-clipboard",label:RED._("menu.label.clipboard"),onselect:"core:show-export-dialog"}, - {id:"menu-item-export-library",label:RED._("menu.label.library"),onselect:"core:library-export"} - ]}); + menuOptions.push({id:"menu-item-import",label:RED._("menu.label.import"),onselect:"core:show-import-dialog"}); + menuOptions.push({id:"menu-item-export",label:RED._("menu.label.export"),onselect:"core:show-export-dialog"}); menuOptions.push(null); menuOptions.push({id:"menu-item-search",label:RED._("menu.label.search"),onselect:"core:search"}); menuOptions.push(null); diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/clipboard.js b/packages/node_modules/@node-red/editor-client/src/js/ui/clipboard.js index b84e98ed2..fd339c5c3 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/clipboard.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/clipboard.js @@ -24,7 +24,7 @@ RED.clipboard = (function() { var disabled = false; var popover; var currentPopoverError; - var activeExportTab; + var activeTab; var libraryBrowser; function setupDialogs() { @@ -33,7 +33,7 @@ RED.clipboard = (function() { .dialog({ modal: true, autoOpen: false, - width: 800, + width: 700, resizable: false, buttons: [ { @@ -63,7 +63,7 @@ RED.clipboard = (function() { class: "primary", text: RED._("clipboard.export.copy"), click: function() { - if (activeExportTab === "clipboard-dialog-export-tab-clipboard") { + if (activeTab === "clipboard-dialog-export-tab-clipboard") { $("#clipboard-export").select(); document.execCommand("copy"); document.getSelection().removeAllRanges(); @@ -93,9 +93,9 @@ RED.clipboard = (function() { } }); } - if (selectedPath.files) { + if (selectedPath.children) { var exists = false; - selectedPath.files.forEach(function(f) { + selectedPath.children.forEach(function(f) { if (f.label === filename) { exists = true; } @@ -133,7 +133,17 @@ RED.clipboard = (function() { class: "primary", text: RED._("common.label.import"), click: function() { - RED.view.importNodes($("#clipboard-import").val(),$("#import-tab > a.selected").attr('id') === 'import-tab-new'); + var addNewFlow = ($("#import-tab > a.selected").attr('id') === 'import-tab-new'); + if (activeTab === "clipboard-dialog-import-tab-clipboard") { + RED.view.importNodes($("#clipboard-import").val(),addNewFlow); + } else { + var selectedPath = libraryBrowser.getSelected(); + if (selectedPath.path) { + $.get('library/flows/'+selectedPath.path, function(data) { + RED.view.importNodes(data,addNewFlow); + }); + } + } $( this ).dialog( "close" ); } } @@ -160,12 +170,12 @@ RED.clipboard = (function() { ''+ ''+ '
        '+ - '
        '+ + '
        '+ '
        '+ '
          '+ '
          '+ - '
          '+ - '
          '+ + '
          '+ + '
          '+ '
          '+ ''+ '
          '+ @@ -176,7 +186,7 @@ RED.clipboard = (function() { ''+ '
          '+ '
          '+ - '
          '+ + '
          '+ '
          '+ '
          '+ ''+ @@ -188,20 +198,33 @@ RED.clipboard = (function() { importNodesDialog = - '
          '+ - ' '+ - ''+ + '
          '+ + '
          '+ + '
            '+ + '
            '+ + '
            '+ + '
            '+ + '
            '+ + ' '+ + ''+ + '
            '+ + '
            '+ + ''+ + '
            '+ + '
            '+ + '
            '+ + '
            '+ + '
            '+ + '
            '+ '
            '+ '
            '+ - ''+ - '
            '+ - '
            '+ - ''+ - ''+ - ''+ - ''+ - ''+ + ''+ + ''+ + ''+ + ''+ + ''+ '
            '; + } var validateExportFilenameTimeout @@ -223,107 +246,167 @@ RED.clipboard = (function() { },100); } - var validateImportTimeout; - function validateImport() { - if (validateImportTimeout) { - clearTimeout(validateImportTimeout); - } - validateImportTimeout = setTimeout(function() { - var importInput = $("#clipboard-import"); - var v = importInput.val().trim(); - if (v === "") { - popover.close(true); - currentPopoverError = null; - importInput.removeClass("input-error"); - $("#clipboard-dialog-ok").button("disable"); - return; + if (activeTab === "clipboard-dialog-import-tab-clipboard") { + if (validateImportTimeout) { + clearTimeout(validateImportTimeout); } - try { - if (!/^\[[\s\S]*\]$/m.test(v)) { - throw new Error(RED._("clipboard.import.errors.notArray")); - } - var res = JSON.parse(v); - for (var i=0;i
            ').text(errString); - var errorPos; - // Chrome error messages - var m = /at position (\d+)/i.exec(errString); - if (m) { - errorPos = parseInt(m[1]); - } else { - // Firefox error messages - m = /at line (\d+) column (\d+)/i.exec(errString); - if (m) { - var line = parseInt(m[1])-1; - var col = parseInt(m[2])-1; - var lines = v.split("\n"); - errorPos = 0; - for (var i=0;i').appendTo(message); - var code = $('
            ').appendTo(parseError);
            -                            $('').text(v.substring(errorPos-12,errorPos)).appendTo(code)
            -                            $('').text(v.charAt(errorPos)).appendTo(code);
            -                            $('').text(v.substring(errorPos+1,errorPos+12)).appendTo(code);
            -                        }
            -                        popover.close(true).setContent(message).open();
            -                        currentPopoverError = errString;
            -                    }
            -                } else {
            +            validateImportTimeout = setTimeout(function() {
            +                var importInput = $("#clipboard-import");
            +                var v = importInput.val().trim();
            +                if (v === "") {
            +                    popover.close(true);
                                 currentPopoverError = null;
            +                    importInput.removeClass("input-error");
            +                    $("#clipboard-dialog-ok").button("disable");
            +                    return;
                             }
            +                try {
            +                    if (!/^\[[\s\S]*\]$/m.test(v)) {
            +                        throw new Error(RED._("clipboard.import.errors.notArray"));
            +                    }
            +                    var res = JSON.parse(v);
            +                    for (var i=0;i
            ').text(errString); + var errorPos; + // Chrome error messages + var m = /at position (\d+)/i.exec(errString); + if (m) { + errorPos = parseInt(m[1]); + } else { + // Firefox error messages + m = /at line (\d+) column (\d+)/i.exec(errString); + if (m) { + var line = parseInt(m[1])-1; + var col = parseInt(m[2])-1; + var lines = v.split("\n"); + errorPos = 0; + for (var i=0;i').appendTo(message); + var code = $('
            ').appendTo(parseError);
            +                                $('').text(v.substring(errorPos-12,errorPos)).appendTo(code)
            +                                $('').text(v.charAt(errorPos)).appendTo(code);
            +                                $('').text(v.substring(errorPos+1,errorPos+12)).appendTo(code);
            +                            }
            +                            popover.close(true).setContent(message).open();
            +                            currentPopoverError = errString;
            +                        }
            +                    } else {
            +                        currentPopoverError = null;
            +                    }
            +                    $("#clipboard-dialog-ok").button("disable");
            +                }
            +            },100);
            +        } else {
            +            var file = libraryBrowser.getSelected();
            +            if (file && file.label && !file.children) {
            +                $("#clipboard-dialog-ok").button("enable");
            +            } else {
                             $("#clipboard-dialog-ok").button("disable");
                         }
            -        },100);
            +        }
                 }
             
            -    function importNodes() {
            +    function importNodes(mode) {
                     if (disabled) {
                         return;
                     }
            +        mode = mode || "clipboard";
            +
                     dialogContainer.empty();
                     dialogContainer.append($(importNodesDialog));
            +
            +        var tabs = RED.tabs.create({
            +            id: "clipboard-dialog-import-tabs",
            +            vertical: true,
            +            onchange: function(tab) {
            +                $("#clipboard-dialog-import-tabs-content").children().hide();
            +                $("#" + tab.id).show();
            +                activeTab = tab.id;
            +                if (popover) {
            +                    popover.close(true);
            +                    currentPopoverError = null;
            +                }
            +                if (tab.id === "clipboard-dialog-import-tab-clipboard") {
            +                    $("#clipboard-import").focus();
            +                }
            +                validateImport();
            +            }
            +        });
            +        tabs.addTab({
            +            id: "clipboard-dialog-import-tab-clipboard",
            +            label: RED._("clipboard.clipboard")
            +        });
            +        tabs.addTab({
            +            id: "clipboard-dialog-import-tab-library",
            +            label: RED._("library.library")
            +        });
            +
            +        tabs.activateTab("clipboard-dialog-import-tab-"+mode);
            +        if (mode === 'clipboard') {
            +            setTimeout(function() {
            +                $("#clipboard-import").focus();
            +            },100)
            +        }
            +
            +
            +        $("#clipboard-dialog-tab-library-name").keyup(validateExportFilename);
            +        $("#clipboard-dialog-tab-library-name").on('paste',function() { setTimeout(validateExportFilename,10)});
            +        $("#clipboard-dialog-export").button("enable");
            +
            +        libraryBrowser = RED.library.createBrowser({
            +            container: $("#clipboard-dialog-import-tab-library-browser"),
            +            onselect: function(file) {
            +                if (file && file.label && !file.children) {
            +                    $("#clipboard-dialog-ok").button("enable");
            +                } else {
            +                    $("#clipboard-dialog-ok").button("disable");
            +                }
            +            }
            +        })
            +        loadFlowLibrary(libraryBrowser,true);
            +
                     dialogContainer.i18n();
             
                     $("#clipboard-dialog-ok").show();
            @@ -381,7 +464,7 @@ RED.clipboard = (function() {
                         onchange: function(tab) {
                             $("#clipboard-dialog-export-tabs-content").children().hide();
                             $("#" + tab.id).show();
            -                activeExportTab = tab.id;
            +                activeTab = tab.id;
                             if (tab.id === "clipboard-dialog-export-tab-clipboard") {
                                 $("#clipboard-dialog-export").button("option","label", RED._("clipboard.export.copy"))
                                 $("#clipboard-dialog-download").show();
            @@ -394,11 +477,11 @@ RED.clipboard = (function() {
                     });
                     tabs.addTab({
                         id: "clipboard-dialog-export-tab-clipboard",
            -            label: "Clipboard"
            +            label: RED._("clipboard.clipboard")
                     });
                     tabs.addTab({
                         id: "clipboard-dialog-export-tab-library",
            -            label: "Library"
            +            label: RED._("library.library")
                     });
             
                     tabs.activateTab("clipboard-dialog-export-tab-"+mode);
            @@ -416,53 +499,10 @@ RED.clipboard = (function() {
                             }
                         }
                     })
            +        loadFlowLibrary(libraryBrowser,false);
             
            -        function xformList(list,label,root) {
            -            var result = {
            -                icon: root===""?"fa fa-archive":'fa fa-folder',
            -                label: label,
            -                path: root
            -            };
            -            result.children = [];
            -
            -            if (list.f) {
            -                list.f.forEach(function(f) {
            -                    result.children.push({
            -                        icon: 'fa fa-file-o',
            -                        label: f,
            -                        path: root+f
            -                    });
            -                });
            -            }
            -            if (list.d) {
            -                for (var l in list.d) {
            -                    if (list.d.hasOwnProperty(l)) {
            -                        if (root+l !== "_examples_") {
            -                            result.children.push(xformList(list.d[l], l,root+l+"/"))
            -                        }
            -                    }
            -                }
            -            }
            -            result.children.sort(function(A,B){
            -                if (A.children && !B.children) {
            -                    return -1;
            -                } else if (!A.children && B.children) {
            -                    return 1;
            -                } else {
            -                    return A.label.localeCompare(B.label);
            -                }
            -            });
            -            return result;
            -        }
            -
            -        $.getJSON("library/flows", function(data) {
            -            var listing = [xformList(data,"Local","")];
            -            listing[0].expanded = true;
            -            libraryBrowser.data(listing);
            -        });
                     $("#clipboard-dialog-tab-library-name").val("flows.json").select();
             
            -
                     dialogContainer.i18n();
                     var format = RED.settings.flowFilePretty ? "export-format-full" : "export-format-mini";
             
            @@ -569,6 +609,60 @@ RED.clipboard = (function() {
                     $("#clipboard-dialog-download").show();
             
                 }
            +    function transformFlowList(list,label,root,includeExamples) {
            +        var result = {
            +            icon: root===""?"fa fa-archive":'fa fa-folder',
            +            label: label,
            +            path: root
            +        };
            +        result.children = [];
            +
            +        if (list.f) {
            +            list.f.forEach(function(f) {
            +                result.children.push({
            +                    icon: 'fa fa-file-o',
            +                    label: f,
            +                    path: root+f
            +                });
            +            });
            +        }
            +        if (list.d) {
            +            for (var l in list.d) {
            +                if (list.d.hasOwnProperty(l)) {
            +                    if (root+l !== "_examples_") {
            +                        result.children.push(transformFlowList(list.d[l], l,root+l+"/",includeExamples))
            +                    } else if (includeExamples) {
            +                        result._examples = transformFlowList(list.d[l], l,root+l+"/",includeExamples)
            +                    }
            +                }
            +            }
            +        }
            +        result.children.sort(function(A,B){
            +            if (A.children && !B.children) {
            +                return -1;
            +            } else if (!A.children && B.children) {
            +                return 1;
            +            } else {
            +                return A.label.localeCompare(B.label);
            +            }
            +        });
            +        return result;
            +    }
            +
            +    function loadFlowLibrary(browser,includeExamples) {
            +        $.getJSON("library/flows", function(data) {
            +            var listing = [transformFlowList(data,RED._("library.types.local"),"",includeExamples)];
            +            listing[0].expanded = true;
            +            if (includeExamples && listing[0]._examples) {
            +                var examples = listing[0]._examples;
            +                delete listing[0]._examples;
            +                examples.label = RED._("library.types.examples");
            +                examples.icon = "fa fa-archive";
            +                listing.unshift(examples)
            +            }
            +            browser.data(listing);
            +        });
            +    }
             
                 function hideDropTarget() {
                     $("#dropTarget").hide();
            diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/common/treeList.js b/packages/node_modules/@node-red/editor-client/src/js/ui/common/treeList.js
            index 70054c727..925acfba4 100644
            --- a/packages/node_modules/@node-red/editor-client/src/js/ui/common/treeList.js
            +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/common/treeList.js
            @@ -121,24 +121,41 @@
                                     container.addClass('built');
                                     var childrenAdded = false;
                                     var spinner;
            +                        var startTime = 0;
                                     item.children(item,function(children) {
                                         childrenAdded = true;
            -                            item.treeList.childList = that._addChildren(container,parent,children,depth);
            -                            if (spinner) {
            -                                spinner.remove();
            +                            item.treeList.childList = that._addChildren(container,item,children,depth).hide();
            +                            var delta = Date.now() - startTime;
            +                            if (delta < 400) {
            +                                setTimeout(function() {
            +                                    item.treeList.childList.slideDown('fast');
            +                                    if (spinner) {
            +                                        spinner.remove();
            +                                    }
            +                                },400-delta);
            +                            } else {
            +                                item.treeList.childList.slideDown('fast');
            +                                if (spinner) {
            +                                    spinner.remove();
            +                                }
                                         }
                                         that._trigger("childrenloaded",null,item)
                                     });
                                     if (!childrenAdded) {
            +                            startTime = Date.now();
                                         spinner = $('
            ').css({ "background-position": (35+depth*15)+'px 50%' }).appendTo(container); } + } else { + item.treeList.childList.slideDown('fast'); } container.addClass("expanded"); + } item.treeList.collapse = function() { + item.treeList.childList.slideUp('fast'); container.removeClass("expanded"); } @@ -181,7 +198,7 @@ } if (item.children) { if (Array.isArray(item.children)) { - item.treeList.childList = that._addChildren(container,item,item.children,depth); + item.treeList.childList = that._addChildren(container,item,item.children,depth).hide(); } if (item.expanded) { item.treeList.expand(); diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/library.js b/packages/node_modules/@node-red/editor-client/src/js/ui/library.js index 6b5af77fa..7c8b5d811 100755 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/library.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/library.js @@ -15,16 +15,14 @@ **/ RED.library = (function() { - var exportToLibraryDialog; var loadLibraryBrowser; var saveLibraryBrowser; var libraryEditor; var activeLibrary; - var _libraryLookup = '
            '+ '
            '+ - '
            '+ + '
            '+ '
            '+ '
            '+ '
            '+ @@ -60,6 +58,7 @@ RED.library = (function() { } var filename = $("#node-dialog-library-save-filename").val().trim() var selectedPath = saveLibraryBrowser.getSelected(); + console.log(selectedPath); if (!selectedPath.children) { selectedPath = selectedPath.parent; } @@ -90,9 +89,11 @@ RED.library = (function() { } }); } - if (selectedPath.files) { + console.log(filename); + console.log(selectedPath); + if (selectedPath.children) { var exists = false; - selectedPath.files.forEach(function(f) { + selectedPath.children.forEach(function(f) { if (f.label === filename) { exists = true; } @@ -211,7 +212,7 @@ RED.library = (function() { loadLibraryFolder(options.url, "", function(items) { var listing = [{ icon: 'fa fa-archive', - label: "Local", + label: RED._("library.types.local"), path: "", expanded: true, writable: false, @@ -257,7 +258,7 @@ RED.library = (function() { loadLibraryFolder(options.url, "", function(items) { var listing = [{ icon: 'fa fa-archive', - label: "Local", + label: RED._("library.types.local"), path: "", expanded: true, writable: false, @@ -285,13 +286,6 @@ RED.library = (function() { function createBrowser(options) { var panes = $('
            ').appendTo(options.container); - // var dirsPane = $('
            ').appendTo(panes); - // var filesPane = $('
            ').appendTo(panes); - // - // RED.panels.create({ - // container:panes, - // dir: "horizontal" - // }); var dirList = $("
            ").css({width: "100%", height: "100%"}).appendTo(panes) .treeList({}).on('treelistselect', function(event, item) { if (addButton) { @@ -307,7 +301,7 @@ RED.library = (function() { }); var addButton; if (options.addFolderButton) { - var tools = $("
            ").css({position: "absolute",bottom:"3px",left:"3px"}).appendTo(panes) + var tools = $("
            ").css({position: "absolute",bottom:"3px",right:"25px"}).appendTo(panes) addButton= $('').appendTo(tools).click(function(e) { var defaultFolderName = "new-folder"; var defaultFolderNameMatches = {}; diff --git a/packages/node_modules/@node-red/editor-client/src/sass/library.scss b/packages/node_modules/@node-red/editor-client/src/sass/library.scss index 8fef4cadd..5cbd9e24d 100644 --- a/packages/node_modules/@node-red/editor-client/src/sass/library.scss +++ b/packages/node_modules/@node-red/editor-client/src/sass/library.scss @@ -48,7 +48,7 @@ } } } -#clipboard-dialog-export-tab-clipboard { +.clipboard-dialog-tab-clipboard { padding: 10px; textarea { resize: none; @@ -56,7 +56,7 @@ border-radius: 4px; font-family: monospace !important; font-size: 13px !important; - height: 430px; + height: 300px; line-height: 1.3em; padding: 6px 10px; background: #F3E7E7; @@ -64,7 +64,7 @@ } } -#clipboard-dialog-export-tabs-content { +.clipboard-dialog-tabs-content { position: absolute; top: 0; left: 120px; @@ -72,19 +72,25 @@ bottom: 0; padding: 0; background: white; - &>div { height: calc(100% - 20px) } } -#clipboard-dialog-export-tab-library { +.clipboard-dialog-tab-library { .form-row { margin-left: 10px; } } - +#clipboard-dialog { + form { + margin-bottom: 0; + } + .form-row:last-child { + margin-bottom: 0; + } +} #clipboard-dialog-tab-library-name { width: calc(100% - 120px); } @@ -94,7 +100,13 @@ border-bottom: 1px solid $primary-border-color; box-sizing: border-box; } - +#clipboard-dialog-import-tab-library { + height: 100%; +} +#clipboard-dialog-import-tab-library-browser { + height: 100%; + box-sizing: border-box; +} .red-ui-library-browser { diff --git a/packages/node_modules/@node-red/editor-client/src/sass/ui/common/treeList.scss b/packages/node_modules/@node-red/editor-client/src/sass/ui/common/treeList.scss index a6f0388e5..17a304221 100644 --- a/packages/node_modules/@node-red/editor-client/src/sass/ui/common/treeList.scss +++ b/packages/node_modules/@node-red/editor-client/src/sass/ui/common/treeList.scss @@ -49,18 +49,18 @@ transition: transform 0.1s ease-in-out; } .red-ui-editableList { - display: none; + // display: none; } &.expanded { & > .red-ui-treeList-label .fa-angle-right { transform: rotate(90deg) } - & > .red-ui-editableList { - display: block - } - & > .red-ui-treeList-spinner { - display: block; - } + // & > .red-ui-editableList { + // display: block + // } + // & > .red-ui-treeList-spinner { + // display: block; + // } } } } @@ -108,7 +108,6 @@ label.red-ui-treeList-label { text-align: center; } .red-ui-treeList-spinner { - display: none; height: 32px; background: url(images/spin.svg) 50% 50% no-repeat; background-size: auto 20px; From b581e33611c65fe89331113ef86991d8738f8c56 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Thu, 25 Apr 2019 11:32:09 +0100 Subject: [PATCH 009/213] Update runtime apis to support multiple libraries --- .../@node-red/editor-api/lib/editor/index.js | 5 +- .../editor-api/lib/editor/library.js | 22 +- .../editor-client/src/js/ui/clipboard.js | 84 ++-- .../editor-client/src/js/ui/library.js | 28 +- .../@node-red/registry/lib/library.js | 18 +- .../@node-red/runtime/lib/api/library.js | 39 +- .../@node-red/runtime/lib/library/examples.js | 101 +++++ .../@node-red/runtime/lib/library/index.js | 86 ++-- .../@node-red/runtime/lib/library/local.js | 37 ++ .../editor-api/lib/editor/library_spec.js | 77 +--- .../@node-red/registry/lib/library_spec.js | 2 +- .../@node-red/runtime/lib/api/library_spec.js | 392 +----------------- .../runtime/lib/library/examples_spec.js | 138 ++++++ .../runtime/lib/library/index_spec.js | 144 +++---- .../runtime/lib/library/local_spec.js | 93 +++++ 15 files changed, 553 insertions(+), 713 deletions(-) create mode 100644 packages/node_modules/@node-red/runtime/lib/library/examples.js create mode 100644 packages/node_modules/@node-red/runtime/lib/library/local.js create mode 100644 test/unit/@node-red/runtime/lib/library/examples_spec.js create mode 100644 test/unit/@node-red/runtime/lib/library/local_spec.js diff --git a/packages/node_modules/@node-red/editor-api/lib/editor/index.js b/packages/node_modules/@node-red/editor-api/lib/editor/index.js index dbf4c18f2..d29ab0b1b 100644 --- a/packages/node_modules/@node-red/editor-api/lib/editor/index.js +++ b/packages/node_modules/@node-red/editor-api/lib/editor/index.js @@ -93,9 +93,8 @@ module.exports = { // Library var library = require("./library"); library.init(runtimeAPI); - editorApp.get("/library/flows",needsPermission("library.read"),library.getAll,apiUtil.errorHandler); - editorApp.get(/library\/([^\/]+)(?:$|\/(.*))/,needsPermission("library.read"),library.getEntry); - editorApp.post(/library\/([^\/]+)\/(.*)/,needsPermission("library.write"),library.saveEntry); + editorApp.get(/library\/([^\/]+)\/([^\/]+)(?:$|\/(.*))/,needsPermission("library.read"),library.getEntry); + editorApp.post(/library\/([^\/]+)\/([^\/]+)\/(.*)/,needsPermission("library.write"),library.saveEntry); // Credentials diff --git a/packages/node_modules/@node-red/editor-api/lib/editor/library.js b/packages/node_modules/@node-red/editor-api/lib/editor/library.js index e8b09424a..47a41bb7b 100644 --- a/packages/node_modules/@node-red/editor-api/lib/editor/library.js +++ b/packages/node_modules/@node-red/editor-api/lib/editor/library.js @@ -25,23 +25,12 @@ module.exports = { init: function(_runtimeAPI) { runtimeAPI = _runtimeAPI; }, - - getAll: function(req,res) { - var opts = { - user: req.user, - type: 'flows' - } - runtimeAPI.library.getEntries(opts).then(function(result) { - res.json(result); - }).catch(function(err) { - apiUtils.rejectHandler(req,res,err); - }); - }, getEntry: function(req,res) { var opts = { user: req.user, - type: req.params[0], - path: req.params[1]||"" + library: req.params[0], + type: req.params[1], + path: req.params[2]||"" } runtimeAPI.library.getEntry(opts).then(function(result) { if (typeof result === "string") { @@ -62,8 +51,9 @@ module.exports = { saveEntry: function(req,res) { var opts = { user: req.user, - type: req.params[0], - path: req.params[1]||"" + library: req.params[0], + type: req.params[1], + path: req.params[2]||"" } // TODO: horrible inconsistencies between flows and all other types if (opts.type === "flows") { diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/clipboard.js b/packages/node_modules/@node-red/editor-client/src/js/ui/clipboard.js index fd339c5c3..5ace86456 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/clipboard.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/clipboard.js @@ -78,7 +78,7 @@ RED.clipboard = (function() { var filename = $("#clipboard-dialog-tab-library-name").val().trim(); var saveFlow = function() { $.ajax({ - url:'library/flows/'+selectedPath.path + filename, + url:'library/'+selectedPath.library+'/'+selectedPath.type+'/'+selectedPath.path + filename, type: "POST", data: flowToExport, contentType: "application/json; charset=utf-8" @@ -139,7 +139,7 @@ RED.clipboard = (function() { } else { var selectedPath = libraryBrowser.getSelected(); if (selectedPath.path) { - $.get('library/flows/'+selectedPath.path, function(data) { + $.get('library/'+selectedPath.library+'/'+selectedPath.type+'/'+selectedPath.path, function(data) { RED.view.importNodes(data,addNewFlow); }); } @@ -609,59 +609,39 @@ RED.clipboard = (function() { $("#clipboard-dialog-download").show(); } - function transformFlowList(list,label,root,includeExamples) { - var result = { - icon: root===""?"fa fa-archive":'fa fa-folder', - label: label, - path: root - }; - result.children = []; - - if (list.f) { - list.f.forEach(function(f) { - result.children.push({ - icon: 'fa fa-file-o', - label: f, - path: root+f - }); - }); - } - if (list.d) { - for (var l in list.d) { - if (list.d.hasOwnProperty(l)) { - if (root+l !== "_examples_") { - result.children.push(transformFlowList(list.d[l], l,root+l+"/",includeExamples)) - } else if (includeExamples) { - result._examples = transformFlowList(list.d[l], l,root+l+"/",includeExamples) - } - } - } - } - result.children.sort(function(A,B){ - if (A.children && !B.children) { - return -1; - } else if (!A.children && B.children) { - return 1; - } else { - return A.label.localeCompare(B.label); - } - }); - return result; - } function loadFlowLibrary(browser,includeExamples) { - $.getJSON("library/flows", function(data) { - var listing = [transformFlowList(data,RED._("library.types.local"),"",includeExamples)]; - listing[0].expanded = true; - if (includeExamples && listing[0]._examples) { - var examples = listing[0]._examples; - delete listing[0]._examples; - examples.label = RED._("library.types.examples"); - examples.icon = "fa fa-archive"; - listing.unshift(examples) + var listing = []; + if (includeExamples) { + listing.push({ + library: "_examples_", + type: "flows", + icon: 'fa fa-hdd-o', + label: RED._("library.types.examples"), + path: "", + children: function(item,done) { + RED.library.loadLibraryFolder("_examples_","flows","",function(children) { + item.children = children; + done(children); + }) + } + }) + } + listing.push({ + library: "local", + type: "flows", + icon: 'fa fa-hdd-o', + label: RED._("library.types.local"), + path: "", + expanded: true, + children: function(item,done) { + RED.library.loadLibraryFolder("local","flows","",function(children) { + item.children = children; + done(children); + }) } - browser.data(listing); - }); + }) + browser.data(listing); } function hideDropTarget() { diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/library.js b/packages/node_modules/@node-red/editor-client/src/js/ui/library.js index 7c8b5d811..b78a31a8b 100755 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/library.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/library.js @@ -58,7 +58,6 @@ RED.library = (function() { } var filename = $("#node-dialog-library-save-filename").val().trim() var selectedPath = saveLibraryBrowser.getSelected(); - console.log(selectedPath); if (!selectedPath.children) { selectedPath = selectedPath.parent; } @@ -75,7 +74,7 @@ RED.library = (function() { data.text = activeLibrary.editor.getValue(); var saveFlow = function() { $.ajax({ - url:"library/"+activeLibrary.url+'/'+selectedPath.path + filename, + url:"library/"+selectedPath.library+'/'+selectedPath.type+'/'+selectedPath.path + filename, type: "POST", data: JSON.stringify(data), contentType: "application/json; charset=utf-8" @@ -89,8 +88,6 @@ RED.library = (function() { } }); } - console.log(filename); - console.log(selectedPath); if (selectedPath.children) { var exists = false; selectedPath.children.forEach(function(f) { @@ -125,16 +122,18 @@ RED.library = (function() { } } - function loadLibraryFolder(url,root,done) { - $.getJSON("library/"+url+"/"+root,function(data) { + function loadLibraryFolder(library,type,root,done) { + $.getJSON("library/"+library+"/"+type+"/"+root,function(data) { var items = data.map(function(d) { if (typeof d === "string") { return { + library: library, + type: type, icon: 'fa fa-folder', label: d, path: root+d+"/", children: function(item,done) { - loadLibraryFolder(url,root+d+"/", function(children) { + loadLibraryFolder(library,type,root+d+"/", function(children) { item.children = children; // TODO: should this be done by treeList for us done(children); }) @@ -142,6 +141,8 @@ RED.library = (function() { }; } else { return { + library: library, + type: type, icon: 'fa fa-file-o', label: d.fn, path: root+d.fn, @@ -209,9 +210,11 @@ RED.library = (function() { $('#node-input-'+options.type+'-menu-open-library').click(function(e) { activeLibrary = options; - loadLibraryFolder(options.url, "", function(items) { + loadLibraryFolder("local",options.url, "", function(items) { var listing = [{ - icon: 'fa fa-archive', + library: "local", + type: options.url, + icon: 'fa fa-hdd-o', label: RED._("library.types.local"), path: "", expanded: true, @@ -255,7 +258,7 @@ RED.library = (function() { } $("#node-dialog-library-save-filename").attr("value",filename+".js"); - loadLibraryFolder(options.url, "", function(items) { + loadLibraryFolder("local",options.url, "", function(items) { var listing = [{ icon: 'fa fa-archive', label: RED._("library.types.local"), @@ -483,7 +486,7 @@ RED.library = (function() { var table = $("#node-dialog-library-load-preview-details-table").empty(); selectedLibraryItem = file.props; if (file && file.label && !file.children) { - $.get("library/"+activeLibrary.url+"/"+file.path, function(data) { + $.get("library/"+file.library+"/"+file.type+"/"+file.path, function(data) { //TODO: nls + sanitize var propRow = $('Type').appendTo(table); $(propRow.children()[1]).text(activeLibrary.type); @@ -516,6 +519,7 @@ RED.library = (function() { }, create: createUI, createBrowser:createBrowser, - export: exportFlow + export: exportFlow, + loadLibraryFolder: loadLibraryFolder } })(); diff --git a/packages/node_modules/@node-red/registry/lib/library.js b/packages/node_modules/@node-red/registry/lib/library.js index 9f7c6e8b2..1eea6ed56 100644 --- a/packages/node_modules/@node-red/registry/lib/library.js +++ b/packages/node_modules/@node-red/registry/lib/library.js @@ -16,7 +16,6 @@ var fs = require('fs'); var fspath = require('path'); -var when = require('when'); var runtime; @@ -24,7 +23,7 @@ var exampleRoots = {}; var exampleFlows = null; function getFlowsFromPath(path) { - return when.promise(function(resolve,reject) { + return new Promise(function(resolve,reject) { var result = {}; fs.readdir(path,function(err,files) { var promises = []; @@ -37,11 +36,11 @@ function getFlowsFromPath(path) { promises.push(getFlowsFromPath(fullPath)); } else if (/\.json$/.test(file)){ validFiles.push(file); - promises.push(when.resolve(file.split(".")[0])) + promises.push(Promise.resolve(file.split(".")[0])) } }) var i=0; - when.all(promises).then(function(results) { + Promise.all(promises).then(function(results) { results.forEach(function(r) { if (typeof r === 'string') { result.f = result.f||[]; @@ -62,21 +61,20 @@ function getFlowsFromPath(path) { function addNodeExamplesDir(module,path) { exampleRoots[module] = path; return getFlowsFromPath(path).then(function(result) { - exampleFlows = exampleFlows||{d:{}}; - exampleFlows.d[module] = result; + exampleFlows = exampleFlows||{}; + exampleFlows[module] = result; }); } function removeNodeExamplesDir(module) { delete exampleRoots[module]; - if (exampleFlows && exampleFlows.d) { - delete exampleFlows.d[module]; + if (exampleFlows) { + delete exampleFlows[module]; } - if (exampleFlows && Object.keys(exampleFlows.d).length === 0) { + if (exampleFlows && Object.keys(exampleFlows).length === 0) { exampleFlows = null; } } - function init() { exampleRoots = {}; exampleFlows = null; diff --git a/packages/node_modules/@node-red/runtime/lib/api/library.js b/packages/node_modules/@node-red/runtime/lib/api/library.js index 6f6571463..31037858b 100644 --- a/packages/node_modules/@node-red/runtime/lib/api/library.js +++ b/packages/node_modules/@node-red/runtime/lib/api/library.js @@ -29,6 +29,7 @@ var api = module.exports = { * Gets an entry from the library. * @param {Object} opts * @param {User} opts.user - the user calling the api + * @param {String} opts.library - the library * @param {String} opts.type - the type of entry * @param {String} opts.path - the path of the entry * @return {Promise} - resolves when complete @@ -36,12 +37,12 @@ var api = module.exports = { */ getEntry: function(opts) { return new Promise(function(resolve,reject) { - runtime.library.getEntry(opts.type,opts.path).then(function(result) { - runtime.log.audit({event: "library.get",type:opts.type,path:opts.path}); + runtime.library.getEntry(opts.library,opts.type,opts.path).then(function(result) { + runtime.log.audit({event: "library.get",library:opts.library,type:opts.type,path:opts.path}); return resolve(result); }).catch(function(err) { if (err) { - runtime.log.warn(runtime.log._("api.library.error-load-entry",{path:opts.path,message:err.toString()})); + runtime.log.warn(runtime.log._("api.library.error-load-entry",{library:opts.library,type:opts.type,path:opts.path,message:err.toString()})); if (err.code === 'forbidden') { err.status = 403; return reject(err); @@ -50,10 +51,10 @@ var api = module.exports = { } else { err.status = 400; } - runtime.log.audit({event: "library.get",type:opts.type,path:opts.path,error:err.code}); + runtime.log.audit({event: "library.get",library:opts.library,type:opts.type,path:opts.path,error:err.code}); return reject(err); } - runtime.log.audit({event: "library.get",type:opts.type,error:"not_found"}); + runtime.log.audit({event: "library.get",library:opts.library,type:opts.type,error:"not_found"}); var error = new Error(); error.code = "not_found"; error.status = 404; @@ -66,6 +67,7 @@ var api = module.exports = { * Saves an entry to the library * @param {Object} opts * @param {User} opts.user - the user calling the api + * @param {String} opts.library - the library * @param {String} opts.type - the type of entry * @param {String} opts.path - the path of the entry * @param {Object} opts.meta - any meta data associated with the entry @@ -75,7 +77,7 @@ var api = module.exports = { */ saveEntry: function(opts) { return new Promise(function(resolve,reject) { - runtime.library.saveEntry(opts.type,opts.path,opts.meta,opts.body).then(function() { + runtime.library.saveEntry(opts.library,opts.type,opts.path,opts.meta,opts.body).then(function() { runtime.log.audit({event: "library.set",type:opts.type,path:opts.path}); return resolve(); }).catch(function(err) { @@ -91,30 +93,5 @@ var api = module.exports = { return reject(error); }); }) - }, - /** - * Returns a complete listing of all entries of a given type in the library. - * @param {Object} opts - * @param {User} opts.user - the user calling the api - * @param {String} opts.type - the type of entry - * @return {Promise} - the entry listing - * @memberof @node-red/runtime_library - */ - getEntries: function(opts) { - return new Promise(function(resolve,reject) { - if (opts.type !== 'flows') { - return reject(new Error("API only supports flows")); - - } - runtime.storage.getAllFlows().then(function(flows) { - runtime.log.audit({event: "library.get.all",type:"flow"}); - var examples = runtime.nodes.getNodeExampleFlows(); - if (examples) { - flows.d = flows.d||{}; - flows.d._examples_ = examples; - } - return resolve(flows); - }); - }) } } diff --git a/packages/node_modules/@node-red/runtime/lib/library/examples.js b/packages/node_modules/@node-red/runtime/lib/library/examples.js new file mode 100644 index 000000000..bde277a5f --- /dev/null +++ b/packages/node_modules/@node-red/runtime/lib/library/examples.js @@ -0,0 +1,101 @@ +/** + * 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. + **/ + +var fs = require('fs'); + +var runtime; + +function init(_runtime) { + runtime = _runtime; +} + +function getEntry(type,path) { + var examples = runtime.nodes.getNodeExampleFlows(); + var result = []; + if (path === "") { + return Promise.resolve(Object.keys(examples)); + } else { + path = path.replace(/\/$/,""); + var parts = path.split("/"); + var module = parts.shift(); + if (module[0] === "@") { + module = module+"/"+parts.shift(); + } + if (examples.hasOwnProperty(module)) { + examples = examples[module]; + examples = parts.reduce(function(ex,k) { + if (ex) { + if (ex.d && ex.d[k]) { + return ex.d[k] + } + if (ex.f && ex.f.indexOf(k) > -1) { + return runtime.nodes.getNodeExampleFlowPath(module,parts.join("/")); + } + } else { + return null; + } + },examples); + + if (!examples) { + return new Promise(function (resolve,reject) { + var error = new Error("not_found"); + error.code = "not_found"; + return reject(error); + }); + } else if (typeof examples === 'string') { + return new Promise(function(resolve,reject) { + try { + fs.readFile(examples,'utf8',function(err, data) { + runtime.log.audit({event: "library.get",library:"_examples",type:"flow",path:path}); + if (err) { + return reject(err); + } + return resolve(data); + }) + } catch(err) { + return reject(err); + } + }); + } else { + if (examples.d) { + for (var d in examples.d) { + if (examples.d.hasOwnProperty(d)) { + result.push(d); + } + } + } + if (examples.f) { + examples.f.forEach(function(f) { + result.push({fn:f}) + }) + } + return Promise.resolve(result); + } + } else { + return new Promise(function (resolve,reject) { + var error = new Error("not_found"); + error.code = "not_found"; + return reject(error); + }); + } + } +} + +module.exports = { + name: '_examples_', + init: init, + getEntry: getEntry +} diff --git a/packages/node_modules/@node-red/runtime/lib/library/index.js b/packages/node_modules/@node-red/runtime/lib/library/index.js index cc8cd1509..8a4e6518e 100644 --- a/packages/node_modules/@node-red/runtime/lib/library/index.js +++ b/packages/node_modules/@node-red/runtime/lib/library/index.js @@ -14,18 +14,22 @@ * limitations under the License. **/ -var fs = require('fs'); -var fspath = require('path'); -var runtime; var knownTypes = {}; -var storage; +var libraries = {}; + + +function init(runtime) { + knownTypes = { + 'flows': 'node-red' + }; + + libraries["_examples_"] = require("./examples"); + libraries["_examples_"].init(runtime); + libraries["local"] = require("./local"); + libraries["local"].init(runtime); -function init(_runtime) { - runtime = _runtime; - storage = runtime.storage; - knownTypes = {}; } function registerType(id,type) { @@ -37,66 +41,34 @@ function registerType(id,type) { knownTypes[type] = id; } -// function getAllEntries(type) { -// if (!knownTypes.hasOwnProperty(type)) { -// throw new Error(`Unknown library type '${type}'`); -// } -// } - -function getEntry(type,path) { - if (type !== 'flows') { - if (!knownTypes.hasOwnProperty(type)) { - throw new Error(`Unknown library type '${type}'`); - } - return storage.getLibraryEntry(type,path); +function getEntry(library,type,path) { + if (!knownTypes.hasOwnProperty(type)) { + throw new Error(`Unknown library type '${type}'`); + } + if (libraries.hasOwnProperty(library)) { + return libraries[library].getEntry(type,path); } else { - return new Promise(function(resolve,reject) { - if (path.indexOf("_examples_/") === 0) { - var m = /^_examples_\/(@.*?\/[^\/]+|[^\/]+)\/(.*)$/.exec(path); - if (m) { - var module = m[1]; - var entryPath = m[2]; - var fullPath = runtime.nodes.getNodeExampleFlowPath(module,entryPath); - if (fullPath) { - try { - fs.readFile(fullPath,'utf8',function(err, data) { - runtime.log.audit({event: "library.get",type:"flow",path:path}); - if (err) { - return reject(err); - } - return resolve(data); - }) - } catch(err) { - return reject(err); - } - return; - } - } - // IF we get here, we didn't find the file - var error = new Error("not_found"); - error.code = "not_found"; - return reject(error); - } else { - resolve(storage.getFlow(path)); - } - }); + throw new Error(`Unknown library '${library}'`); } } -function saveEntry(type,path,meta,body) { - if (type !== 'flows') { - if (!knownTypes.hasOwnProperty(type)) { - throw new Error(`Unknown library type '${type}'`); +function saveEntry(library,type,path,meta,body) { + if (!knownTypes.hasOwnProperty(type)) { + throw new Error(`Unknown library type '${type}'`); + } + if (libraries.hasOwnProperty(library)) { + if (libraries[library].hasOwnProperty("saveEntry")) { + return libraries[library].saveEntry(type,path,meta,body); + } else { + throw new Error(`Library '${library}' is read-only`); } - return storage.saveLibraryEntry(type,path,meta,body); } else { - return storage.saveFlow(path,body); + throw new Error(`Unknown library '${library}'`); } } module.exports = { init: init, register: registerType, - // getAllEntries: getAllEntries, getEntry: getEntry, saveEntry: saveEntry diff --git a/packages/node_modules/@node-red/runtime/lib/library/local.js b/packages/node_modules/@node-red/runtime/lib/library/local.js new file mode 100644 index 000000000..454e800ca --- /dev/null +++ b/packages/node_modules/@node-red/runtime/lib/library/local.js @@ -0,0 +1,37 @@ +/** + * 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. + **/ + +var runtime; +var storage; + +function init(_runtime) { + runtime = _runtime; + storage = runtime.storage; +} + +function getEntry(type,path) { + return storage.getLibraryEntry(type,path); +} +function saveEntry(type,path,meta,body) { + return storage.saveLibraryEntry(type,path,meta,body); +} + +module.exports = { + name: 'local', + init: init, + getEntry: getEntry, + saveEntry: saveEntry +} diff --git a/test/unit/@node-red/editor-api/lib/editor/library_spec.js b/test/unit/@node-red/editor-api/lib/editor/library_spec.js index 891305199..4a2c281b7 100644 --- a/test/unit/@node-red/editor-api/lib/editor/library_spec.js +++ b/test/unit/@node-red/editor-api/lib/editor/library_spec.js @@ -31,60 +31,11 @@ describe("api/editor/library", function() { before(function() { app = express(); app.use(bodyParser.json()); - - app.get("/library/flows",library.getAll); - app.post(/library\/([^\/]+)\/(.*)/,library.saveEntry); - app.get(/library\/([^\/]+)(?:$|\/(.*))/,library.getEntry); + app.get(/library\/([^\/]+)\/([^\/]+)(?:$|\/(.*))/,library.getEntry); + app.post(/library\/([^\/]+)\/([^\/]+)\/(.*)/,library.saveEntry); }); after(function() { }); - it('returns all flows', function(done) { - library.init({ - library: { - getEntries: function(opts) { - return Promise.resolve({a:1,b:2}); - } - } - }); - request(app) - .get('/library/flows') - .expect(200) - .end(function(err,res) { - if (err) { - return done(err); - } - res.body.should.have.property('a',1); - res.body.should.have.property('b',2); - done(); - }); - }) - it('returns an error on all flows', function(done) { - library.init({ - library: { - getEntries: function(opts) { - var err = new Error("message"); - err.code = "random_error"; - err.status = 400; - var p = Promise.reject(err); - p.catch(()=>{}); - return p; - } - } - }); - request(app) - .get('/library/flows') - .expect(400) - .end(function(err,res) { - if (err) { - return done(err); - } - res.body.should.have.property('code'); - res.body.code.should.be.equal("random_error"); - res.body.should.have.property('message'); - res.body.message.should.be.equal("message"); - done(); - }); - }); it('returns an individual entry - flow type', function(done) { var opts; @@ -97,7 +48,7 @@ describe("api/editor/library", function() { } }); request(app) - .get('/library/flows/abc') + .get('/library/local/flows/abc') .expect(200) .end(function(err,res) { if (err) { @@ -105,6 +56,7 @@ describe("api/editor/library", function() { } res.body.should.have.property('a',1); res.body.should.have.property('b',2); + opts.should.have.property('library','local'); opts.should.have.property('type','flows'); opts.should.have.property('path','abc'); done(); @@ -121,7 +73,7 @@ describe("api/editor/library", function() { } }); request(app) - .get('/library/flows/abc/def') + .get('/library/local/flows/abc/def') .expect(200) .end(function(err,res) { if (err) { @@ -129,6 +81,7 @@ describe("api/editor/library", function() { } res.body.should.have.property('a',1); res.body.should.have.property('b',2); + opts.should.have.property('library','local'); opts.should.have.property('type','flows'); opts.should.have.property('path','abc/def'); done(); @@ -145,12 +98,13 @@ describe("api/editor/library", function() { } }); request(app) - .get('/library/non-flow/abc') + .get('/library/local/non-flow/abc') .expect(200) .end(function(err,res) { if (err) { return done(err); } + opts.should.have.property('library','local'); opts.should.have.property('type','non-flow'); opts.should.have.property('path','abc'); res.text.should.eql('{"a":1,"b":2}'); @@ -168,7 +122,7 @@ describe("api/editor/library", function() { } }); request(app) - .get('/library/non-flow/abc/def') + .get('/library/local/non-flow/abc/def') .expect(200) .end(function(err,res) { if (err) { @@ -176,6 +130,7 @@ describe("api/editor/library", function() { } res.body.should.have.property('a',1); res.body.should.have.property('b',2); + opts.should.have.property('library','local'); opts.should.have.property('type','non-flow'); opts.should.have.property('path','abc/def'); done(); @@ -198,12 +153,13 @@ describe("api/editor/library", function() { } }); request(app) - .get('/library/flows/123') + .get('/library/local/flows/123') .expect(400) .end(function(err,res) { if (err) { return done(err); } + opts.should.have.property('library','local'); opts.should.have.property('type','flows'); opts.should.have.property('path','123'); @@ -227,13 +183,14 @@ describe("api/editor/library", function() { } }); request(app) - .post('/library/flows/abc/def') + .post('/library/local/flows/abc/def') .expect(204) .send({a:1,b:2,c:3}) .end(function(err,res) { if (err) { return done(err); } + opts.should.have.property('library','local'); opts.should.have.property('type','flows'); opts.should.have.property('path','abc/def'); opts.should.have.property('meta',{}); @@ -253,13 +210,14 @@ describe("api/editor/library", function() { } }); request(app) - .post('/library/non-flow/abc/def') + .post('/library/local/non-flow/abc/def') .expect(204) .send({a:1,b:2,text:"123"}) .end(function(err,res) { if (err) { return done(err); } + opts.should.have.property('library','local'); opts.should.have.property('type','non-flow'); opts.should.have.property('path','abc/def'); opts.should.have.property('meta',{a:1,b:2}); @@ -284,7 +242,7 @@ describe("api/editor/library", function() { } }); request(app) - .post('/library/non-flow/abc/def') + .post('/library/local/non-flow/abc/def') .send({a:1,b:2,text:"123"}) .expect(400) .end(function(err,res) { @@ -292,6 +250,7 @@ describe("api/editor/library", function() { return done(err); } opts.should.have.property('type','non-flow'); + opts.should.have.property('library','local'); opts.should.have.property('path','abc/def'); res.body.should.have.property('code'); diff --git a/test/unit/@node-red/registry/lib/library_spec.js b/test/unit/@node-red/registry/lib/library_spec.js index d8999c2db..2e0e7e99a 100644 --- a/test/unit/@node-red/registry/lib/library_spec.js +++ b/test/unit/@node-red/registry/lib/library_spec.js @@ -38,7 +38,7 @@ describe("library api", function() { library.addExamplesDir("test-module",path.resolve(__dirname+'/resources/examples')).then(function() { try { var flows = library.getExampleFlows(); - flows.should.deepEqual({"d":{"test-module":{"f":["one"]}}}); + flows.should.deepEqual({"test-module":{"f":["one"]}}); var examplePath = library.getExampleFlowPath('test-module','one'); examplePath.should.eql(path.resolve(__dirname+'/resources/examples/one.json')) diff --git a/test/unit/@node-red/runtime/lib/api/library_spec.js b/test/unit/@node-red/runtime/lib/api/library_spec.js index 8f99580b9..3fa5291d2 100644 --- a/test/unit/@node-red/runtime/lib/api/library_spec.js +++ b/test/unit/@node-red/runtime/lib/api/library_spec.js @@ -38,7 +38,7 @@ describe("runtime-api/library", function() { library.init({ log: mockLog, library: { - getEntry: function(type,path) { + getEntry: function(library, type,path) { if (type === "known") { return Promise.resolve("known"); } else if (type === "forbidden") { @@ -67,13 +67,13 @@ describe("runtime-api/library", function() { }) }) it("returns a known entry", function(done) { - library.getEntry({type: "known", path: "/abc"}).then(function(result) { + library.getEntry({library: "local",type: "known", path: "/abc"}).then(function(result) { result.should.eql("known") done(); }).catch(done) }) it("rejects a forbidden entry", function(done) { - library.getEntry({type: "forbidden", path: "/abc"}).then(function(result) { + library.getEntry({library: "local",type: "forbidden", path: "/abc"}).then(function(result) { done(new Error("did not reject")); }).catch(function(err) { err.should.have.property("code","forbidden"); @@ -82,7 +82,7 @@ describe("runtime-api/library", function() { }).catch(done) }) it("rejects an unknown entry", function(done) { - library.getEntry({type: "not_found", path: "/abc"}).then(function(result) { + library.getEntry({library: "local",type: "not_found", path: "/abc"}).then(function(result) { done(new Error("did not reject")); }).catch(function(err) { err.should.have.property("code","not_found"); @@ -91,7 +91,7 @@ describe("runtime-api/library", function() { }).catch(done) }) it("rejects a blank (unknown) entry", function(done) { - library.getEntry({type: "blank", path: "/abc"}).then(function(result) { + library.getEntry({library: "local",type: "blank", path: "/abc"}).then(function(result) { done(new Error("did not reject")); }).catch(function(err) { err.should.have.property("code","not_found"); @@ -100,7 +100,7 @@ describe("runtime-api/library", function() { }).catch(done) }) it("rejects unexpected error", function(done) { - library.getEntry({type: "error", path: "/abc"}).then(function(result) { + library.getEntry({library: "local",type: "error", path: "/abc"}).then(function(result) { done(new Error("did not reject")); }).catch(function(err) { err.should.have.property("status",400); @@ -114,7 +114,7 @@ describe("runtime-api/library", function() { library.init({ log: mockLog, library: { - saveEntry: function(type,path,meta,body) { + saveEntry: function(library,type,path,meta,body) { opts = {type,path,meta,body}; if (type === "known") { return Promise.resolve(); @@ -137,7 +137,7 @@ describe("runtime-api/library", function() { }) it("saves an entry", function(done) { - library.saveEntry({type: "known", path: "/abc", meta: {a:1}, body:"123"}).then(function() { + library.saveEntry({library: "local",type: "known", path: "/abc", meta: {a:1}, body:"123"}).then(function() { opts.should.have.property("type","known"); opts.should.have.property("path","/abc"); opts.should.have.property("meta",{a:1}); @@ -146,7 +146,7 @@ describe("runtime-api/library", function() { }).catch(done) }) it("rejects a forbidden entry", function(done) { - library.saveEntry({type: "forbidden", path: "/abc", meta: {a:1}, body:"123"}).then(function() { + library.saveEntry({library: "local",type: "forbidden", path: "/abc", meta: {a:1}, body:"123"}).then(function() { done(new Error("did not reject")); }).catch(function(err) { err.should.have.property("code","forbidden"); @@ -155,7 +155,7 @@ describe("runtime-api/library", function() { }).catch(done) }) it("rejects an unknown entry", function(done) { - library.saveEntry({type: "not_found", path: "/abc", meta: {a:1}, body:"123"}).then(function() { + library.saveEntry({library: "local",type: "not_found", path: "/abc", meta: {a:1}, body:"123"}).then(function() { done(new Error("did not reject")); }).catch(function(err) { err.should.have.property("status",400); @@ -163,377 +163,5 @@ describe("runtime-api/library", function() { }).catch(done) }) }) - describe("getEntries", function() { - var opts; - before(function() { - library.init({ - log: mockLog, - storage: { - getAllFlows: function() { - return Promise.resolve({a:1}); - } - }, - nodes: { - getNodeExampleFlows: function() { - return {b:2}; - } - } - }); - }); - it("returns all flows", function(done) { - library.getEntries({type:"flows"}).then(function(result) { - result.should.eql({a:1,d:{_examples_:{b:2}}}); - done(); - }).catch(done) - }); - it("fails for non-flows (currently)", function(done) { - library.getEntries({type:"functions"}).then(function(result) { - done(new Error("did not reject")); - }).catch(function(err) { - done(); - }).catch(done) - }) - }) - }); - - -/* - -var should = require("should"); -var sinon = require("sinon"); -var fs = require("fs"); -var fspath = require('path'); -var request = require('supertest'); -var express = require('express'); -var bodyParser = require('body-parser'); - -var when = require('when'); - -var app; -var library = require("../../../../red/api/editor/library"); -var auth = require("../../../../red/api/auth"); - -describe("api/editor/library", function() { - - function initLibrary(_flows,_libraryEntries,_examples,_exampleFlowPathFunction) { - var flows = _flows; - var libraryEntries = _libraryEntries; - library.init(app,{ - log:{audit:function(){},_:function(){},warn:function(){}}, - storage: { - init: function() { - return when.resolve(); - }, - getAllFlows: function() { - return when.resolve(flows); - }, - getFlow: function(fn) { - if (flows[fn]) { - return when.resolve(flows[fn]); - } else if (fn.indexOf("..")!==-1) { - var err = new Error(); - err.code = 'forbidden'; - return when.reject(err); - } else { - return when.reject(); - } - }, - saveFlow: function(fn,data) { - if (fn.indexOf("..")!==-1) { - var err = new Error(); - err.code = 'forbidden'; - return when.reject(err); - } - flows[fn] = data; - return when.resolve(); - }, - getLibraryEntry: function(type,path) { - if (path.indexOf("..")!==-1) { - var err = new Error(); - err.code = 'forbidden'; - return when.reject(err); - } - if (libraryEntries[type] && libraryEntries[type][path]) { - return when.resolve(libraryEntries[type][path]); - } else { - return when.reject(); - } - }, - saveLibraryEntry: function(type,path,meta,body) { - if (path.indexOf("..")!==-1) { - var err = new Error(); - err.code = 'forbidden'; - return when.reject(err); - } - libraryEntries[type][path] = body; - return when.resolve(); - } - }, - events: { - on: function(){}, - removeListener: function(){} - }, - nodes: { - getNodeExampleFlows: function() { - return _examples; - }, - getNodeExampleFlowPath: _exampleFlowPathFunction - } - }); - } - - describe("flows", function() { - before(function() { - app = express(); - app.use(bodyParser.json()); - app.get("/library/flows",library.getAll); - app.post(new RegExp("/library/flows\/(.*)"),library.post); - app.get(new RegExp("/library/flows\/(.*)"),library.get); - app.response.sendFile = function (path) { - app.response.json.call(this, {sendFile: path}); - }; - sinon.stub(fs,"statSync",function() { return true; }); - }); - after(function() { - fs.statSync.restore(); - }); - it('returns empty result', function(done) { - initLibrary({},{flows:{}}); - request(app) - .get('/library/flows') - .expect(200) - .end(function(err,res) { - if (err) { - throw err; - } - res.body.should.not.have.property('f'); - res.body.should.not.have.property('d'); - done(); - }); - }); - - it('returns 404 for non-existent entry', function(done) { - initLibrary({},{flows:{}}); - request(app) - .get('/library/flows/foo') - .expect(404) - .end(done); - }); - - - it('can store and retrieve item', function(done) { - initLibrary({},{flows:{}}); - var flow = '[]'; - request(app) - .post('/library/flows/foo') - .set('Content-Type', 'application/json') - .send(flow) - .expect(204).end(function (err, res) { - if (err) { - throw err; - } - request(app) - .get('/library/flows/foo') - .expect(200) - .end(function(err,res) { - if (err) { - throw err; - } - res.text.should.equal(flow); - done(); - }); - }); - }); - - it('lists a stored item', function(done) { - initLibrary({f:["bar"]}); - request(app) - .get('/library/flows') - .expect(200) - .end(function(err,res) { - if (err) { - throw err; - } - res.body.should.have.property('f'); - should.deepEqual(res.body.f,['bar']); - done(); - }); - }); - - it('returns 403 for malicious get attempt', function(done) { - initLibrary({}); - // without the userDir override the malicious url would be - // http://127.0.0.1:1880/library/flows/../../package to - // obtain package.json from the node-red root. - request(app) - .get('/library/flows/../../../../../package') - .expect(403) - .end(done); - }); - it('returns 403 for malicious post attempt', function(done) { - initLibrary({}); - // without the userDir override the malicious url would be - // http://127.0.0.1:1880/library/flows/../../package to - // obtain package.json from the node-red root. - request(app) - .post('/library/flows/../../../../../package') - .expect(403) - .end(done); - }); - it('includes examples flows if set', function(done) { - var examples = {"d":{"node-module":{"f":["example-one"]}}}; - initLibrary({},{},examples); - request(app) - .get('/library/flows') - .expect(200) - .end(function(err,res) { - if (err) { - throw err; - } - res.body.should.have.property('d'); - res.body.d.should.have.property('_examples_'); - should.deepEqual(res.body.d._examples_,examples); - done(); - }); - }); - - it('can retrieve an example flow', function(done) { - var examples = {"d":{"node-module":{"f":["example-one"]}}}; - initLibrary({},{},examples,function(module,path) { - return module + ':' + path - }); - request(app) - .get('/library/flows/_examples_/node-module/example-one') - .expect(200) - .end(function(err,res) { - if (err) { - throw err; - } - res.body.should.have.property('sendFile', - fspath.resolve('node-module') + ':example-one'); - done(); - }); - }); - - it('can retrieve an example flow in an org scoped package', function(done) { - var examples = {"d":{"@org_scope/node_package":{"f":["example-one"]}}}; - initLibrary({},{},examples,function(module,path) { - return module + ':' + path - }); - request(app) - .get('/library/flows/_examples_/@org_scope/node_package/example-one') - .expect(200) - .end(function(err,res) { - if (err) { - throw err; - } - res.body.should.have.property('sendFile', - fspath.resolve('@org_scope/node_package') + - ':example-one'); - done(); - }); - }); - }); - - describe("type", function() { - before(function() { - - app = express(); - app.use(bodyParser.json()); - initLibrary({},{}); - auth.init({settings:{}}); - library.register("test"); - }); - - it('returns empty result', function(done) { - initLibrary({},{'test':{"":[]}}); - request(app) - .get('/library/test') - .expect(200) - .end(function(err,res) { - if (err) { - throw err; - } - res.body.should.not.have.property('f'); - done(); - }); - }); - - it('returns 404 for non-existent entry', function(done) { - initLibrary({},{}); - request(app) - .get('/library/test/foo') - .expect(404) - .end(done); - }); - - it('can store and retrieve item', function(done) { - initLibrary({},{'test':{}}); - var flow = {text:"test content"}; - request(app) - .post('/library/test/foo') - .set('Content-Type', 'application/json') - .send(flow) - .expect(204).end(function (err, res) { - if (err) { - throw err; - } - request(app) - .get('/library/test/foo') - .expect(200) - .end(function(err,res) { - if (err) { - throw err; - } - res.text.should.equal(flow.text); - done(); - }); - }); - }); - - it('lists a stored item', function(done) { - initLibrary({},{'test':{'a':['abc','def']}}); - request(app) - .get('/library/test/a') - .expect(200) - .end(function(err,res) { - if (err) { - throw err; - } - // This response isn't strictly accurate - but it - // verifies the api returns what storage gave it - should.deepEqual(res.body,['abc','def']); - done(); - }); - }); - - - it('returns 403 for malicious access attempt', function(done) { - request(app) - .get('/library/test/../../../../../../../../../../etc/passwd') - .expect(403) - .end(done); - }); - - it('returns 403 for malicious access attempt', function(done) { - request(app) - .get('/library/test/..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\etc\\passwd') - .expect(403) - .end(done); - }); - - it('returns 403 for malicious access attempt', function(done) { - request(app) - .post('/library/test/../../../../../../../../../../etc/passwd') - .set('Content-Type', 'text/plain') - .send('root:x:0:0:root:/root:/usr/bin/tclsh') - .expect(403) - .end(done); - }); - - }); -}); - -*/ diff --git a/test/unit/@node-red/runtime/lib/library/examples_spec.js b/test/unit/@node-red/runtime/lib/library/examples_spec.js new file mode 100644 index 000000000..16d917c7b --- /dev/null +++ b/test/unit/@node-red/runtime/lib/library/examples_spec.js @@ -0,0 +1,138 @@ +/** + * 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. + **/ + +var should = require("should"); +var sinon = require("sinon"); +var fs = require("fs"); + +var NR_TEST_UTILS = require("nr-test-utils"); +var examplesLibrary = NR_TEST_UTILS.require("@node-red/runtime/lib/library/examples") + +var mockLog = { + log: sinon.stub(), + debug: sinon.stub(), + trace: sinon.stub(), + warn: sinon.stub(), + info: sinon.stub(), + metric: sinon.stub(), + audit: sinon.stub(), + _: function() { return "abc"} +} + +describe("runtime/library/examples", function() { + describe("getEntry", function() { + before(function() { + examplesLibrary.init({ + log: mockLog, + storage: { + getLibraryEntry: function(type,path) { + return Promise.resolve({type,path}); + }, + getFlow: function(path) { + return Promise.resolve({path}); + } + }, + nodes: { + getNodeExampleFlows: function() { + return { + "test-module": { + f: ["abc"] + }, + "@scope/test-module": { + f: ["abc","throw"] + } + + } + }, + getNodeExampleFlowPath: function(module,entryPath) { + if (module === "unknown") { + return null; + } + return "/tmp/"+module+"/"+entryPath; + } + } + }); + sinon.stub(fs,"readFile", function(path,opts,callback) { + if (path === "/tmp/test-module/abc") { + callback(null,"Example flow result"); + } else if (path === "/tmp/@scope/test-module/abc") { + callback(null,"Example scope flow result"); + } else if (path === "/tmp/test-module/throw") { + throw new Error("Instant error") + } else { + callback(new Error("Unexpected path:"+path)) + } + }) + }); + after(function() { + fs.readFile.restore(); + }) + + it ('returns a flow example entry', function(done) { + examplesLibrary.getEntry("flows","test-module/abc").then(function(result) { + result.should.eql("Example flow result"); + done(); + }).catch(done); + }); + + it ('returns a flow example listing - top level', function(done) { + examplesLibrary.getEntry("flows","").then(function(result) { + result.should.eql([ 'test-module', '@scope/test-module' ]) + done(); + }).catch(done); + }); + it ('returns a flow example listing - in module', function(done) { + examplesLibrary.getEntry("flows","test-module").then(function(result) { + result.should.eql([{ fn: 'abc' }]) + done(); + }).catch(done); + }); + it ('returns a flow example listing - in scoped module', function(done) { + examplesLibrary.getEntry("flows","@scope/test-module").then(function(result) { + result.should.eql([{ fn: 'abc' }, {fn: 'throw'}]) + done(); + }).catch(done); + }); + it ('returns a flow example entry from scoped module', function(done) { + examplesLibrary.getEntry("flows","@scope/test-module/abc").then(function(result) { + result.should.eql("Example scope flow result"); + done(); + }).catch(done); + }); + it ('returns an error for unknown flow example entry', function(done) { + examplesLibrary.getEntry("flows","unknown/abc").then(function(result) { + done(new Error("No error thrown")) + }).catch(function(err) { + err.should.have.property("code","not_found"); + done(); + }); + }); + it ('returns an error for file load error - async', function(done) { + examplesLibrary.getEntry("flows","test-module/unknown").then(function(result) { + done(new Error("No error thrown")) + }).catch(function(err) { + done(); + }); + }); + it ('returns an error for file load error - sync', function(done) { + examplesLibrary.getEntry("flows","test-module/throw").then(function(result) { + done(new Error("No error thrown")) + }).catch(function(err) { + done(); + }); + }); + }); +}); diff --git a/test/unit/@node-red/runtime/lib/library/index_spec.js b/test/unit/@node-red/runtime/lib/library/index_spec.js index bef42d824..ac2608a18 100644 --- a/test/unit/@node-red/runtime/lib/library/index_spec.js +++ b/test/unit/@node-red/runtime/lib/library/index_spec.js @@ -16,10 +16,11 @@ var should = require("should"); var sinon = require("sinon"); -var fs = require("fs"); var NR_TEST_UTILS = require("nr-test-utils"); var library = NR_TEST_UTILS.require("@node-red/runtime/lib/library/index") +var localLibrary = NR_TEST_UTILS.require("@node-red/runtime/lib/library/local") +var examplesLibrary = NR_TEST_UTILS.require("@node-red/runtime/lib/library/examples") var mockLog = { log: sinon.stub(), @@ -34,6 +35,36 @@ var mockLog = { describe("runtime/library", function() { + before(function() { + sinon.stub(localLibrary,"getEntry",function(type,path) { + return Promise.resolve({ + library: "local", + type:type, + path:path + }) + }); + sinon.stub(localLibrary,"saveEntry",function(type, path, meta, body) { + return Promise.resolve({ + library: "local", + type:type, + path:path, + meta:meta, + body:body + }) + }); + sinon.stub(examplesLibrary,"getEntry",function(type,path) { + return Promise.resolve({ + library: "_examples_", + type:type, + path:path + }) + }); + }); + after(function() { + localLibrary.getEntry.restore(); + localLibrary.saveEntry.restore(); + examplesLibrary.getEntry.restore(); + }) describe("register", function() { // it("throws error for duplicate type", function() { // library.init({}); @@ -43,47 +74,19 @@ describe("runtime/library", function() { }) describe("getEntry", function() { before(function() { - library.init({ - log: mockLog, - storage: { - getLibraryEntry: function(type,path) { - return Promise.resolve({type,path}); - }, - getFlow: function(path) { - return Promise.resolve({path}); - } - }, - nodes: { - getNodeExampleFlowPath: function(module,entryPath) { - if (module === "unknown") { - return null; - } - return "/tmp/"+module+"/"+entryPath; - } - } - }); - sinon.stub(fs,"readFile", function(path,opts,callback) { - if (path === "/tmp/test-module/abc") { - callback(null,"Example flow result"); - } else if (path === "/tmp/@scope/test-module/abc") { - callback(null,"Example scope flow result"); - } else if (path === "/tmp/test-module/throw") { - throw new Error("Instant error") - } else { - callback(new Error("Unexpected path:"+path)) - } - }) + library.init({}); }); - after(function() { - fs.readFile.restore(); - }) it('throws error for unregistered type', function() { - should(()=>{library.getEntry("unknown","/abc")} ).throw(); + should(()=>{library.getEntry("local","unknown","/abc")} ).throw(); + }); + it('throws error for unknown library', function() { + should(()=>{library.getEntry("unknown","unknown","/abc")} ).throw(); }); it('returns a registered non-flow entry', function(done) { library.register("test-module","test-type"); - library.getEntry("test-type","/abc").then(function(result) { + library.getEntry("local","test-type","/abc").then(function(result) { + result.should.have.property("library","local") result.should.have.property("type","test-type") result.should.have.property("path","/abc") done(); @@ -91,76 +94,37 @@ describe("runtime/library", function() { }); it ('returns a flow entry', function(done) { - library.getEntry("flows","/abc").then(function(result) { + library.getEntry("local","flows","/abc").then(function(result) { + result.should.have.property("library","local") result.should.have.property("path","/abc") done(); }).catch(done); }); it ('returns a flow example entry', function(done) { - library.getEntry("flows","_examples_/test-module/abc").then(function(result) { - result.should.eql("Example flow result"); + library.getEntry("_examples_","flows","/test-module/abc").then(function(result) { + result.should.have.property("library","_examples_") + result.should.have.property("path","/test-module/abc") done(); }).catch(done); }); - - it ('returns a flow example entry from scoped module', function(done) { - library.getEntry("flows","_examples_/@scope/test-module/abc").then(function(result) { - result.should.eql("Example scope flow result"); - done(); - }).catch(done); - }); - it ('returns an error for unknown flow example entry', function(done) { - library.getEntry("flows","_examples_/unknown/abc").then(function(result) { - done(new Error("No error thrown")) - }).catch(function(err) { - err.should.have.property("code","not_found"); - done(); - }); - }); - it ('returns an error for file load error - async', function(done) { - library.getEntry("flows","_examples_/test-module/unknown").then(function(result) { - done(new Error("No error thrown")) - }).catch(function(err) { - done(); - }); - }); - it ('returns an error for file load error - sync', function(done) { - library.getEntry("flows","_examples_/test-module/throw").then(function(result) { - done(new Error("No error thrown")) - }).catch(function(err) { - done(); - }); - }); }); describe("saveEntry", function() { before(function() { - library.init({ - log: mockLog, - storage: { - saveLibraryEntry: function(type, path, meta, body) { - return Promise.resolve({type,path,meta,body}) - }, - saveFlow: function(path,body) { - return Promise.resolve({path,body}); - } - }, - nodes: { - getNodeExampleFlowPath: function(module,entryPath) { - if (module === "unknown") { - return null; - } - return "/tmp/"+module+"/"+entryPath; - } - } - }); + library.init({}); + }); + it('throws error for unknown library', function() { + should(()=>{library.saveEntry("unknown","unknown","/abc",{id:"meta"},{id:"body"})} ).throw(); }); it('throws error for unregistered type', function() { - should(()=>{library.saveEntry("unknown","/abc",{id:"meta"},{id:"body"})} ).throw(); + should(()=>{library.saveEntry("local","unknown","/abc",{id:"meta"},{id:"body"})} ).throw(); + }); + it('throws error for save to readonly library', function() { + should(()=>{library.saveEntry("_examples_","unknown","/abc",{id:"meta"},{id:"body"})} ).throw(); }); it('saves a flow entry', function(done) { - library.saveEntry('flows','/abc',{id:"meta"},{id:"body"}).then(function(result) { + library.saveEntry('local','flows','/abc',{id:"meta"},{id:"body"}).then(function(result) { result.should.have.property("path","/abc"); result.should.have.property("body",{id:"body"}); done(); @@ -168,7 +132,7 @@ describe("runtime/library", function() { }) it('saves a non-flow entry', function(done) { library.register("test-module","test-type"); - library.saveEntry('test-type','/abc',{id:"meta"},{id:"body"}).then(function(result) { + library.saveEntry('local','test-type','/abc',{id:"meta"},{id:"body"}).then(function(result) { result.should.have.property("type","test-type"); result.should.have.property("path","/abc"); result.should.have.property("meta",{id:"meta"}); diff --git a/test/unit/@node-red/runtime/lib/library/local_spec.js b/test/unit/@node-red/runtime/lib/library/local_spec.js new file mode 100644 index 000000000..965e5c87a --- /dev/null +++ b/test/unit/@node-red/runtime/lib/library/local_spec.js @@ -0,0 +1,93 @@ +/** + * 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. + **/ + +var should = require("should"); +var sinon = require("sinon"); + +var NR_TEST_UTILS = require("nr-test-utils"); +var localLibrary = NR_TEST_UTILS.require("@node-red/runtime/lib/library/local") + +var mockLog = { + log: sinon.stub(), + debug: sinon.stub(), + trace: sinon.stub(), + warn: sinon.stub(), + info: sinon.stub(), + metric: sinon.stub(), + audit: sinon.stub(), + _: function() { return "abc"} +} + +describe("runtime/library/local", function() { + + describe("getEntry", function() { + before(function() { + localLibrary.init({ + log: mockLog, + storage: { + getLibraryEntry: function(type,path) { + return Promise.resolve({type,path}); + } + } + }); + }); + + it('returns a registered non-flow entry', function(done) { + localLibrary.getEntry("test-type","/abc").then(function(result) { + result.should.have.property("type","test-type") + result.should.have.property("path","/abc") + done(); + }).catch(done); + }); + + it ('returns a flow entry', function(done) { + localLibrary.getEntry("flows","/abc").then(function(result) { + result.should.have.property("path","/abc") + done(); + }).catch(done); + }); + }); + + describe("saveEntry", function() { + before(function() { + localLibrary.init({ + log: mockLog, + storage: { + saveLibraryEntry: function(type, path, meta, body) { + return Promise.resolve({type,path,meta,body}) + } + } + }); + }); + it('saves a flow entry', function(done) { + localLibrary.saveEntry('flows','/abc',{id:"meta"},{id:"body"}).then(function(result) { + result.should.have.property("path","/abc"); + result.should.have.property("body",{id:"body"}); + done(); + }).catch(done); + }) + it('saves a non-flow entry', function(done) { + localLibrary.saveEntry('test-type','/abc',{id:"meta"},{id:"body"}).then(function(result) { + result.should.have.property("type","test-type"); + result.should.have.property("path","/abc"); + result.should.have.property("meta",{id:"meta"}); + result.should.have.property("body",{id:"body"}); + done(); + }).catch(done); + }) + + }); +}); From 493687b5bb70a599823a3d3f9b93de8beb5478d4 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Thu, 25 Apr 2019 15:23:08 +0100 Subject: [PATCH 010/213] Allow editor language to be chosen in editor settings This gets stored in localStorage of the browser which is not ideal. This is because we load language catalogs before we load user preferences - so if this was stored in the runtime, the editor wouldn't know the user's preference until it was too late to apply it. This is likely good enough for now - may need to do something more convoluted later on. --- .../@node-red/editor-api/lib/editor/index.js | 4 +- .../editor-api/lib/editor/settings.js | 6 +- .../editor-client/locales/en-US/editor.json | 4 +- .../@node-red/editor-client/src/js/i18n.js | 9 ++- .../editor-client/src/js/ui/userSettings.js | 62 +++++++++++++++---- .../node_modules/@node-red/util/lib/i18n.js | 32 ++++++++-- 6 files changed, 95 insertions(+), 22 deletions(-) diff --git a/packages/node_modules/@node-red/editor-api/lib/editor/index.js b/packages/node_modules/@node-red/editor-api/lib/editor/index.js index dbf4c18f2..44b4ffa89 100644 --- a/packages/node_modules/@node-red/editor-api/lib/editor/index.js +++ b/packages/node_modules/@node-red/editor-api/lib/editor/index.js @@ -25,8 +25,8 @@ var auth = require("../auth"); var nodes = require("../admin/nodes"); // TODO: move /icons into here var needsPermission; var runtimeAPI; -var log = require("@node-red/util").log; // TODO: separate module -var i18n = require("@node-red/util").i18n; // TODO: separate module +var log = require("@node-red/util").log; +var i18n = require("@node-red/util").i18n; var apiUtil = require("../util"); diff --git a/packages/node_modules/@node-red/editor-api/lib/editor/settings.js b/packages/node_modules/@node-red/editor-api/lib/editor/settings.js index 944792fb7..9d9867f1b 100644 --- a/packages/node_modules/@node-red/editor-api/lib/editor/settings.js +++ b/packages/node_modules/@node-red/editor-api/lib/editor/settings.js @@ -19,6 +19,8 @@ var sshkeys = require("./sshkeys"); var theme = require("./theme"); var clone = require("clone"); +var i18n = require("@node-red/util").i18n + function extend(target, source) { var keys = Object.keys(source); var i = keys.length; @@ -53,12 +55,14 @@ module.exports = { user: req.user } runtimeAPI.settings.getRuntimeSettings(opts).then(function(result) { + result.editorTheme = result.editorTheme||{}; var themeSettings = theme.settings(); if (themeSettings) { // result.editorTheme may already exist with the palette // disabled. Need to merge that into the receive settings - result.editorTheme = extend(clone(themeSettings),result.editorTheme||{}); + result.editorTheme = extend(clone(themeSettings),result.editorTheme); } + result.editorTheme.languages = i18n.availableLanguages("editor"); res.json(result); }); }, diff --git a/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json b/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json index 5b4985765..22cb436b1 100755 --- a/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json +++ b/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json @@ -42,7 +42,9 @@ "defaultDir": "Default", "ltr": "Left-to-right", "rtl": "Right-to-left", - "auto": "Contextual" + "auto": "Contextual", + "language": "Language", + "browserDefault": "Browser default" }, "sidebar": { "show": "Show sidebar" diff --git a/packages/node_modules/@node-red/editor-client/src/js/i18n.js b/packages/node_modules/@node-red/editor-client/src/js/i18n.js index a2b962246..6efa12644 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/i18n.js +++ b/packages/node_modules/@node-red/editor-client/src/js/i18n.js @@ -21,7 +21,8 @@ RED.i18n = (function() { return { init: function(options, done) { apiRootUrl = options.apiRootUrl||""; - i18n.init({ + var preferredLanguage = localStorage.getItem("editor-language"); + var opts = { resGetPath: apiRootUrl+'locales/__ns__?lng=__lng__', dynamicLoad: false, load:'current', @@ -32,7 +33,11 @@ RED.i18n = (function() { fallbackLng: ['en-US'], useCookie: false, returnObjectTrees: true - },function() { + }; + if (preferredLanguage) { + opts.lng = preferredLanguage; + } + i18n.init(opts,function() { done(); }); RED["_"] = function() { diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/userSettings.js b/packages/node_modules/@node-red/editor-client/src/js/ui/userSettings.js index dfce5bf73..d50cd86f8 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/userSettings.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/userSettings.js @@ -104,6 +104,10 @@ RED.userSettings = (function() { var viewSettings = [ { + options: [ + {setting:"editor-language",local: true, label:"menu.label.view.language",options:function(done){ done([{val:'',text:RED._('menu.label.view.browserDefault')}].concat(RED.settings.theme("languages"))) }}, + ] + },{ title: "menu.label.view.grid", options: [ {setting:"view-show-grid",oldSetting:"menu-menu-item-view-show-grid",label:"menu.label.view.showGrid",toggle:true,onchange:"core:toggle-show-grid"}, @@ -136,14 +140,40 @@ RED.userSettings = (function() { currentEditorSettings.view = currentEditorSettings.view || {}; viewSettings.forEach(function(section) { - $('

            ').text(RED._(section.title)).appendTo(pane); + if (section.title) { + $('

            ').text(RED._(section.title)).appendTo(pane); + } section.options.forEach(function(opt) { - var initialState = currentEditorSettings.view[opt.setting]; + var initialState; + if (opt.local) { + initialState = localStorage.getItem(opt.setting); + } else { + initialState = currentEditorSettings.view[opt.setting]; + } var row = $('').appendTo(pane); var input; if (opt.toggle) { input = $('').appendTo(row).find("input"); input.prop('checked',initialState); + } else if (opt.options) { + $('').appendTo(row); + var select = $('').appendTo(row); + if (typeof opt.options === 'function') { + opt.options(function(options) { + options.forEach(function(opt) { + var val = opt; + var text = opt; + if (typeof opt !== 'string') { + val = opt.val; + text = opt.text; + } + $('