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 44b4ffa89..3628fa702 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/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 22cb436b1..a48af013d 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 @@ -61,9 +61,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", @@ -156,6 +153,7 @@ } }, "clipboard": { + "clipboard": "Clipboard", "nodes": "Nodes", "node": "__count__ node", "node_plural": "__count__ nodes", @@ -165,7 +163,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", @@ -184,7 +181,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", @@ -351,22 +352,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 011b5b8d7..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"),options:[]} - ]}); - 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"} - ]}); + 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 97addf5c5..ac14e2689 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 activeTab; + var libraryBrowser; function setupDialogs() { dialog = $('
') @@ -31,7 +33,7 @@ RED.clipboard = (function() { .dialog({ modal: true, autoOpen: false, - width: 500, + width: 700, 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,73 @@ 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 (activeTab === "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.getSelected(); + if (!selectedPath.children) { + selectedPath = selectedPath.parent; + } + var filename = $("#clipboard-dialog-tab-library-name").val().trim(); + var saveFlow = function() { + $.ajax({ + url:'library/'+selectedPath.library+'/'+selectedPath.type+'/'+selectedPath.path + filename, + type: "POST", + data: flowToExport, + contentType: "application/json; charset=utf-8" + }).done(function() { + $(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.children) { + var exists = false; + selectedPath.children.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(); + } + } } }, { @@ -81,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/'+selectedPath.library+'/'+selectedPath.type+'/'+selectedPath.path, function(data) { + RED.view.importNodes(data,addNewFlow); + }); + } + } $( this ).dialog( "close" ); } } @@ -101,146 +163,257 @@ RED.clipboard = (function() { exportNodesDialog = '
'+ - ''+ + ''+ ''+ ''+ ''+ ''+ ''+ '
'+ - '
'+ - ''+ + '
'+ + '
'+ + '
    '+ + '
    '+ + '
    '+ + '
    '+ + '
    '+ + ''+ + '
    '+ + '
    '+ + ''+ + ''+ + ''+ + ''+ + '
    '+ + '
    '+ + '
    '+ + '
    '+ + '
    '+ + ''+ + '
    '+ + '
    '+ + '
    '+ + '
    ' + ; + + + importNodesDialog = + '
    '+ + '
    '+ + '
      '+ + '
      '+ + '
      '+ + '
      '+ + '
      '+ + ' '+ + ''+ + '
      '+ + '
      '+ + ''+ + '
      '+ + '
      '+ + '
      '+ + '
      '+ + '
      '+ + '
      '+ '
      '+ - '
      '+ - ''+ - ''+ - ''+ + '
      '+ + ''+ + ''+ + ''+ + ''+ ''+ '
      '; - importNodesDialog = - '
      '+ - ' '+ - ''+ - '
      '+ - '
      '+ - ''+ - '
      '+ - '
      '+ - ''+ - ''+ - ''+ - ''+ - ''+ - '
      '; } - var validateImportTimeout; - - function validateImport() { - if (validateImportTimeout) { - clearTimeout(validateImportTimeout); + var validateExportFilenameTimeout + function validateExportFilename() { + if (validateExportFilenameTimeout) { + clearTimeout(validateExportFilenameTimeout); } - 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");
      +        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);
           }
       
      -    function importNodes() {
      +    var validateImportTimeout;
      +    function validateImport() {
      +        if (activeTab === "clipboard-dialog-import-tab-clipboard") {
      +            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;
      +                }
      +                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");
      +            }
      +        }
      +    }
      +
      +    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();
      +                } else {
      +                    libraryBrowser.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();
               $("#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 +450,62 @@ 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();
      +                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();
      +                } else {
      +                    $("#clipboard-dialog-export").button("option","label", RED._("clipboard.export.export"))
      +                    $("#clipboard-dialog-download").hide();
      +                    libraryBrowser.focus();
      +                }
      +
      +            }
      +        });
      +        tabs.addTab({
      +            id: "clipboard-dialog-export-tab-clipboard",
      +            label: RED._("clipboard.clipboard")
      +        });
      +        tabs.addTab({
      +            id: "clipboard-dialog-export-tab-library",
      +            label: RED._("library.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"),
      +            folderTools: true,
      +            onselect: function(file) {
      +                if (file && file.label && !file.children) {
      +                    $("#clipboard-dialog-tab-library-name").val(file.label);
      +                }
      +            }
      +        })
      +        loadFlowLibrary(libraryBrowser,false);
      +
      +        $("#clipboard-dialog-tab-library-name").val("flows.json").select();
      +
               dialogContainer.i18n();
               var format = RED.settings.flowFilePretty ? "export-format-full" : "export-format-mini";
       
      @@ -307,6 +529,8 @@ RED.clipboard = (function() {
                           flow = JSON.stringify(nodes);
                       }
                       $("#clipboard-export").val(flow);
      +                setTimeout(function() { $("#clipboard-export").scrollTop(0); },50);
      +
                       $("#clipboard-export").focus();
                   }
               });
      @@ -314,7 +538,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 +580,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,29 +604,49 @@ 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();
       
           }
       
      +    function loadFlowLibrary(browser,includeExamples) {
      +        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);
      +    }
      +
           function hideDropTarget() {
               $("#dropTarget").hide();
               RED.keyboard.remove("escape");
      @@ -460,6 +703,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 +712,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/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 8e0249183..0b21ca146 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
        *     }
      @@ -51,8 +51,55 @@
                   var that = this;
       
                   this.element.addClass('red-ui-treeList');
      +            this.element.attr("tabIndex",0);
                   var wrapper = $('
      ',{class:'red-ui-treeList-container'}).appendTo(this.element); - + this.element.on('keydown', function(evt) { + var selected = that._topList.find(".selected").parent().data('data'); + if (!selected && (evt.keyCode === 40 || evt.keyCode === 38)) { + that.select(that._data[0]); + return; + } + var target; + switch(evt.keyCode) { + case 37: // LEFT + if (selected.children&& selected.treeList.container.hasClass("expanded")) { + selected.treeList.collapse() + } else if (selected.parent) { + target = selected.parent; + } + break; + case 38: // UP + target = that._getPreviousSibling(selected); + if (target) { + target = that._getLastDescendant(target); + } + if (!target && selected.parent) { + target = selected.parent; + } + break; + case 39: // RIGHT + if (selected.children) { + if (!selected.treeList.container.hasClass("expanded")) { + selected.treeList.expand() + } + } + break + case 40: //DOWN + if (selected.children && Array.isArray(selected.children) && selected.children.length > 0 && selected.treeList.container.hasClass("expanded")) { + target = selected.children[0]; + } else { + target = that._getNextSibling(selected); + while (!target && selected.parent) { + selected = selected.parent; + target = that._getNextSibling(selected); + } + } + break + } + if (target) { + that.select(target); + } + }); this._data = []; this._topList = $('
        ').css({ @@ -66,34 +113,83 @@ 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) { this.data(this.options.data); } }, - _addChildren: function(container,children,depth) { + _getLastDescendant: function(item) { + // Gets the last visible descendant of the item + if (!item.children || !item.treeList.container.hasClass("expanded") || item.children.length === 0) { + return item; + } + return this._getLastDescendant(item.children[item.children.length-1]); + }, + _getPreviousSibling: function(item) { + var candidates; + if (!item.parent) { + candidates = this._data; + } else { + candidates = item.parent.children; + } + var index = candidates.indexOf(item); + if (index === 0) { + return null; + } else { + return candidates[index-1]; + } + }, + _getNextSibling: function(item) { + var candidates; + if (!item.parent) { + candidates = this._data; + } else { + candidates = item.parent.children; + } + var index = candidates.indexOf(item); + if (index === candidates.length - 1) { + return null; + } else { + return candidates[index+1]; + } + }, + _addChildren: function(container,parent,children,depth) { var that = this; var subtree = $('
          ').appendTo(container).editableList({ addButton: false, 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,select) { + item.treeList.childList.editableList('addItem',newItem) + newItem.parent = item; + item.children.push(newItem); + if (select) { + setTimeout(function() { + that.select(newItem) + },100); + } + } + item.treeList.expand = function(done) { + if (container.hasClass("expanded")) { + done && done(); + return; + } if (!container.hasClass("built") && typeof item.children === 'function') { container.addClass('built'); var childrenAdded = false; var spinner; - item.children(function(children) { + var startTime = 0; + item.children(item,function(children) { childrenAdded = true; - that._addChildren(container,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(); + } } + done && done(); + 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'); + done && done(); + } + container.addClass("expanded"); + } + item.treeList.collapse = function() { + item.treeList.childList.slideUp('fast'); + container.removeClass("expanded"); + } + + $('').appendTo(label); + // $('').appendTo(label); + label.click(function(e) { + if (container.hasClass("expanded")) { + if (item.hasOwnProperty('selected') || label.hasClass("selected")) { + item.treeList.collapse(); + } + } else { + item.treeList.expand(); } - container.toggleClass("expanded"); }) } else { $('').appendTo(label); @@ -140,21 +284,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,item.children,depth).hide(); } if (item.expanded) { - label.click(); + item.treeList.expand(); } } }, @@ -168,6 +318,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.getSelected(); + if (!selectedPath.children) { + selectedPath = selectedPath.parent; + } - function loadFlowLibrary() { - $.getJSON("library/flows",function(data) { - //console.log(data); - - var buildMenu = function(data,root) { - var i; - var li; - var a; - var ul = document.createElement("ul"); - if (root === "") { - ul.id = "menu-item-import-library-submenu"; - } - ul.className = "dropdown-menu"; - if (data.d) { - for (i in data.d) { - if (data.d.hasOwnProperty(i)) { - li = document.createElement("li"); - li.className = "dropdown-submenu pull-left"; - a = document.createElement("a"); - a.href="#"; - var label = i.replace(/^@.*\//,"").replace(/^node-red-contrib-/,"").replace(/^node-red-node-/,"").replace(/-/g," ").replace(/_/g," "); - a.innerText = label; - li.appendChild(a); - li.appendChild(buildMenu(data.d[i],root+(root!==""?"/":"")+i)); - ul.appendChild(li); - } - } - } - if (data.f) { - for (i in data.f) { - if (data.f.hasOwnProperty(i)) { - li = document.createElement("li"); - a = document.createElement("a"); - a.href="#"; - a.innerText = data.f[i]; - a.flowName = root+(root!==""?"/":"")+data.f[i]; - a.onclick = function() { - $.get('library/flows/'+this.flowName, function(data) { - RED.view.importNodes(data); - }); - }; - li.appendChild(a); - ul.appendChild(li); - } - } - } - return ul; - }; - var examples; - if (data.d && data.d._examples_) { - examples = data.d._examples_; - delete data.d._examples_; + 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 +199,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 +210,346 @@ 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("local",options.url, "", function(items) { + var listing = [{ + library: "local", + type: options.url, + icon: 'fa fa-hdd-o', + label: RED._("library.types.local"), + path: "", + expanded: true, + writable: false, + children: [{ + icon: 'fa fa-cube', + label: options.type, + path: options.type+"/", + expanded: true, + children: items + }] + }] + loadLibraryBrowser.data(listing); }); + libraryEditor = ace.edit('node-dialog-library-load-preview-text',{ + useWorker: false + }); + 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("local",options.url, "", function(items) { + var listing = [{ + icon: 'fa fa-archive', + label: RED._("library.types.local"), + path: "", + expanded: true, + writable: false, + children: [{ + icon: 'fa fa-cube', + label: options.type, + path: options.type+"/", + expanded: true, + children: items + }] + }] + 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 dirList = $("
            ").css({width: "100%", height: "100%"}).appendTo(panes) + .treeList({}).on('treelistselect', function(event, item) { + if (options.onselect) { + options.onselect(item); + } + }); + var itemTools = $("
            ").css({position: "absolute",bottom:"6px",right:"8px"}); + var menuButton = $('') + .click(function(evt) { + evt.preventDefault(); + evt.stopPropagation(); + var elementPos = menuButton.offset(); + + var menuOptionMenu = RED.menu.init({id:"red-ui-library-browser-menu", + options: [ + {id:"red-ui-library-browser-menu-addFolder",label:"New folder", onselect: function() { + var defaultFolderName = "new-folder"; + var defaultFolderNameMatches = {}; + + var selected = dirList.treeList('selected'); + if (!selected.children) { + selected = selected.parent; + } + var complete = function() { + 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
            ') - .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" ); - } + saveLibraryBrowser = RED.library.createBrowser({ + container: $("#node-dialog-library-save-browser"), + addFolderButton: true, + 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"); } - ], - 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 - '
            ' - )); + } + }); + $("#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) { + var elementPrefix = activeLibrary.elementPrefix || "node-input-"; + 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); + } + } + }); + 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, - - export: exportFlow + createBrowser:createBrowser, + export: exportFlow, + loadLibraryFolder: loadLibraryFolder } })(); 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..6b832f461 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,117 @@ } } } +.clipboard-dialog-tab-clipboard { + padding: 10px; + textarea { + resize: none; + width: 100%; + border-radius: 4px; + font-family: monospace !important; + font-size: 13px !important; + height: 300px; + line-height: 1.3em; + padding: 6px 10px; + background: #F3E7E7; + color: #533; + } +} + +.clipboard-dialog-tabs-content { + position: absolute; + top: 0; + left: 120px; + right: 0; + bottom: 0; + padding: 0; + background: white; + &>div { + height: calc(100% - 20px) + } +} + +.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); +} +#clipboard-dialog-export-tab-library-browser { + height: calc(100% - 40px); + margin-bottom: 10px; + 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 { + position: relative; + 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 { + // border: 1px solid $primary-border-color; +} +#node-dialog-library-load-panes { + border: 1px solid $primary-border-color; +} + + +#node-dialog-library-load-preview { + height: 100%; +} + +#node-dialog-library-load-preview-text { + box-sizing: border-box; +} +#node-dialog-library-load-preview-details { + box-sizing: border-box; + .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/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; 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..ea217cffe 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 @@ -15,7 +15,9 @@ **/ .red-ui-treeList { - + &:focus { + outline: none !important; + } } .red-ui-treeList-container { @@ -49,45 +51,49 @@ 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; + // } } } } -label.red-ui-treeList-label { - display: block; - width: auto; -} .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; + position: relative; - &: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; } @@ -101,7 +107,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; 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); + }) + + }); +});