diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index cf871716a..70d36deb1 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -16,7 +16,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node-version: [18, 20, 22.4.x] + node-version: [18, 20, 22] steps: - uses: actions/checkout@v4 - name: Use Node.js ${{ matrix.node-version }} diff --git a/.gitignore b/.gitignore index 6a2ebfaa1..6b5311152 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,4 @@ docs .nyc_output sync.ffs_db package-lock.json +.editorconfig diff --git a/.nodemonignore b/.nodemonignore deleted file mode 100644 index 612a1e15b..000000000 --- a/.nodemonignore +++ /dev/null @@ -1,4 +0,0 @@ -/Gruntfile.js -/.git/* -*.backup -/public/* diff --git a/CHANGELOG.md b/CHANGELOG.md index e4ebd3087..4e77657c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,134 @@ +#### 4.0.9: Maintenance Release + + Editor + + - Add details for the dynamic subscription to match the English docs (#5050) @aikitori + - Fix tooltip snapping based on `typedInput` type (#5051) @GogoVega + - Prevent symbol usage warning in monaco (#5049) @Steve-Mcl + - Show subflow flow context under node section of sidebar (#5025) @knolleary + - feat: Add custom label for default deploy button in settings.editorTheme (#5030) @matiseni51 + - Handle long auto-complete suggests (#5042) @knolleary + - Handle undefined username when generating user icon (#5043) @knolleary + - Handle dragging node into group and splicing link at same time (#5027) @knolleary + - Remember context sidebar tree state when refreshing (#5021) @knolleary + - Update sf instance env vars when removed from template (#5023) @knolleary + - Do not select group when triggering quick-add within it (#5022) @knolleary + - Fix library icon handling within library browser component (#5017) @knolleary + +Runtime + - Allow env var access to context (#5016) @knolleary + - fix debug status reporting if null (#5018) @dceejay + - Fix grunt dev via better ndoemon ignore rules (#5015) @knolleary + - Fix typo in CHANGELOG (4.0.7-->4.0.8) (#5007) @natcl + +Nodes + - Switch: Avoid exceeding call stack when draining message group in Switch (#5014) @knolleary + +#### 4.0.8: Maintenance Release + +Editor + + - Fix config node sort order when importing (#5000) @knolleary + +#### 4.0.7: Maintenance Release + +Editor + + - Fix def can be undefined if the type is missing (#4997) @GogoVega + - Fix the user list of nested config node (#4995) @GogoVega + - Support custom login message and button (#4993) @knolleary + +#### 4.0.6: Maintenance Release + +Editor + + - Roll up various fixes on config node change history (#4975) @knolleary + - Add quotes when installing local tgz to fix spacing in the file path (#4949) @AGhorab-upland + - Validate json dropped into editor to avoid unhelpful error messages (#4964) @knolleary + - Fix junction insert position via context menu (#4974) @knolleary + - Apply zoom scale when calculating annotation positions (#4981) @knolleary + - Handle the import of an incomplete Subflow (#4811) @GogoVega + - Fix updating the Subflow name during a copy (#4809) @GogoVega + - Rename variable to avoid confusion in view.js (#4963) @knolleary + - Change groups.length to groups.size (#4959) @hungtcs + - Remove disabled node types from QuickAddDialog list (#4946) @GogoVega + - Fix `setModulePendingUpdated` with plugins (#4939) @GogoVega + - Missing getSubscriptions in the docs while its implemented (#4934) @ersinpw + - Apply `envVarExcludes` setting to `util.getSetting` into the function node (#4925) @GogoVega + - Fix `envVar` editable list should be sortable (#4932) @GogoVega + - Improve the node name auto-generated with the first available number (#4912) @GogoVega + +Runtime + + - Get the env config node from the parent subflow (#4960) @GogoVega + - Update dependencies (#4987) @knolleary + +Nodes + + - Performance : make reading single buffer / string file faster by not re-allocating and handling huge buffers (#4980) @Fadoli + - Make delay node rate limit reset consistent - not send on reset. (#4940) @dceejay + - Fix trigger node date handling for latest time type input (#4915) @dceejay + - Fix delay node not dropping when nodeMessageBufferMaxLength is set (#4973) + - Ensure node.sep is honoured when generating CSV (#4982) @knolleary + +#### 4.0.5: Maintenance Release + +Editor + + - Refix link call node can call out of a subflow (#4908) @GogoVega + +#### 4.0.4: Maintenance Release + +Editor + + - Fix `link call` node can call out of a subflow (#4892) @GogoVega + - Fix wrong unlock state when event is triggered after deployment (#4889) @GogoVega + - i18n(App) update with latest language file changes (#4903) @joebordes + - fix typo: depreciated (#4895) @dxdc + +Runtime + + - Update dev dependencies (#4893) @knolleary + +Nodes + + - MQTT: Allow msg.userProperties to have number values (#4900) @hardillb + +#### 4.0.3: Maintenance Release + +Editor + + - Refresh page title after changing tab name (#4850) @kazuhitoyokoi + - Add Japanese translations for v4.0.2 (again) (#4853) @kazuhitoyokoi + - Stay in quick-add mode following context menu insert (#4883) @knolleary + - Do not include Junction type in quick-add for virtual links (#4879) @knolleary + - Multiplayer cursor tracking (#4845) @knolleary + - Hide add-flow options when disabled via editorTheme (#4869) @knolleary + - Fix env-var config select when multiple defined (#4872) @knolleary + - Fix subflow outbound-link filter (#4857) @GogoVega + - Add French translations for v4.0.2 (#4856) @GogoVega + - Fix moving link wires (#4851) @knolleary + - Adjust type search dialog position to prevent x-overflow (#4844) @Steve-Mcl + - fix: modulesInUse might be undefined (#4838) @lorenz-maurer + - Add Japanese translations for v4.0.2 (#4849) @kazuhitoyokoi + - Fix menu to enable/disable selection when it's a group (#4828) @GogoVega + +Runtime + + - Update dependencies (#4874) @knolleary + - GitHub: Add citation file to enable "Cite this repository" feature (#4861) @lobis + - Remove use of util.log (#4875) @knolleary + +Nodes + + - Fix invalid property error in range node example (#4855) + - Fix typo in flow example name (#4854) @kazuhitoyokoi + - Move SNI, ALPN and Verify Server cert out of check (#4882) @hardillb + - Set status of mqtt nodes to "disconnected" when deregistered from broker (#4878) @Steve-Mcl + - MQTT: Ensure will payload is a string (#4873) @knolleary + - Let batch node terminate "early" if msg.parts set to end of sequence (#4829) @dceejay + - Fix unintentional Capitalisation in Split node name (#4835) @dceejay + #### 4.0.2: Maintenance Release Editor diff --git a/CITATION.cff b/CITATION.cff new file mode 100644 index 000000000..9372ad005 --- /dev/null +++ b/CITATION.cff @@ -0,0 +1,7 @@ +cff-version: 1.2.0 +message: "If you use this software, please cite it as below." +title: "Node-RED" +authors: + - family-names: "OpenJS Foundation" + - family-names: "Contributors" +url: "https://nodered.org" diff --git a/nodemon.json b/nodemon.json new file mode 100644 index 000000000..98d660626 --- /dev/null +++ b/nodemon.json @@ -0,0 +1,16 @@ +{ + "ignoreRoot": [ + ".git", + ".nyc_output", + ".sass-cache", + "bower-components", + "coverage" + ], + "ignore": [ + "/Gruntfile.js", + "/.git/*", + "*.backup", + "/public/*" + ] +} + diff --git a/package.json b/package.json index 6015c0c9e..29b70f6a2 100644 --- a/package.json +++ b/package.json @@ -26,26 +26,26 @@ } ], "dependencies": { - "acorn": "8.11.3", - "acorn-walk": "8.3.2", - "ajv": "8.14.0", + "acorn": "8.12.1", + "acorn-walk": "8.3.4", + "ajv": "8.17.1", "async-mutex": "0.5.0", "basic-auth": "2.0.1", "bcryptjs": "2.4.3", - "body-parser": "1.20.2", + "body-parser": "1.20.3", "cheerio": "1.0.0-rc.10", "clone": "2.1.2", "content-type": "1.0.5", - "cookie": "0.6.0", - "cookie-parser": "1.4.6", + "cookie": "0.7.2", + "cookie-parser": "1.4.7", "cors": "2.8.5", "cronosjs": "1.7.1", "denque": "2.1.0", - "express": "4.19.2", - "express-session": "1.18.0", + "express": "4.21.2", + "express-session": "1.18.1", "form-data": "4.0.0", "fs-extra": "11.2.0", - "got": "12.6.0", + "got": "12.6.1", "hash-sum": "2.0.0", "hpagent": "1.2.0", "https-proxy-agent": "5.0.1", @@ -60,11 +60,11 @@ "memorystore": "1.6.7", "mime": "3.0.0", "moment": "2.30.1", - "moment-timezone": "0.5.45", + "moment-timezone": "0.5.46", "mqtt": "5.7.0", "multer": "1.4.5-lts.1", "mustache": "4.2.0", - "node-red-admin": "^4.0.0", + "node-red-admin": "^4.0.1", "node-watch": "0.7.4", "nopt": "5.0.0", "oauth2orize": "1.12.0", @@ -72,11 +72,11 @@ "passport": "0.7.0", "passport-http-bearer": "1.0.1", "passport-oauth2-client-password": "0.1.2", - "raw-body": "2.5.2", + "raw-body": "3.0.0", "rfdc": "^1.3.1", - "semver": "7.5.4", - "tar": "7.2.0", - "tough-cookie": "4.1.4", + "semver": "7.6.3", + "tar": "7.4.3", + "tough-cookie": "^5.0.0", "uglify-js": "3.17.4", "uuid": "9.0.1", "ws": "7.5.10", @@ -86,10 +86,10 @@ "@node-rs/bcrypt": "1.10.4" }, "devDependencies": { - "dompurify": "2.4.1", + "dompurify": "2.5.7", "grunt": "1.6.1", "grunt-chmod": "~1.1.1", - "grunt-cli": "~1.4.3", + "grunt-cli": "~1.5.0", "grunt-concurrent": "3.0.0", "grunt-contrib-clean": "2.0.1", "grunt-contrib-compress": "2.0.0", @@ -100,7 +100,7 @@ "grunt-contrib-watch": "1.1.0", "grunt-jsdoc": "2.4.1", "grunt-jsdoc-to-markdown": "6.0.0", - "grunt-jsonlint": "2.1.3", + "grunt-jsonlint": "3.0.0", "grunt-mkdir": "~1.1.0", "grunt-npm-command": "~0.1.2", "grunt-sass": "~3.1.0", @@ -110,11 +110,11 @@ "jquery-i18next": "1.2.1", "jsdoc-nr-template": "github:node-red/jsdoc-nr-template", "marked": "4.3.0", - "mermaid": "^10.4.0", + "mermaid": "11.3.0", "minami": "1.2.3", "mocha": "9.2.2", "node-red-node-test-helper": "^0.3.3", - "nodemon": "2.0.20", + "nodemon": "3.1.7", "proxy": "^1.0.2", "sass": "1.62.1", "should": "13.2.3", diff --git a/packages/node_modules/@node-red/editor-api/lib/auth/index.js b/packages/node_modules/@node-red/editor-api/lib/auth/index.js index 30ff06756..9581983cb 100644 --- a/packages/node_modules/@node-red/editor-api/lib/auth/index.js +++ b/packages/node_modules/@node-red/editor-api/lib/auth/index.js @@ -126,6 +126,14 @@ async function login(req,res) { if (themeContext.login && themeContext.login.image) { response.image = themeContext.login.image; } + if (themeContext.login?.message) { + response.loginMessage = themeContext.login?.message + } + if (themeContext.login?.button) { + response.prompts = [ + { type: "button", ...themeContext.login.button } + ] + } } res.json(response); } diff --git a/packages/node_modules/@node-red/editor-api/lib/editor/theme.js b/packages/node_modules/@node-red/editor-api/lib/editor/theme.js index 2bbcbcd97..1917b55fd 100644 --- a/packages/node_modules/@node-red/editor-api/lib/editor/theme.js +++ b/packages/node_modules/@node-red/editor-api/lib/editor/theme.js @@ -185,13 +185,12 @@ module.exports = { } if (theme.deployButton) { + themeSettings.deployButton = {}; + if (theme.deployButton.label) { + themeSettings.deployButton.label = theme.deployButton.label; + } if (theme.deployButton.type == "simple") { - themeSettings.deployButton = { - type: "simple" - } - if (theme.deployButton.label) { - themeSettings.deployButton.label = theme.deployButton.label; - } + themeSettings.deployButton.type = theme.deployButton.type; if (theme.deployButton.icon) { url = serveFile(themeApp,"/deploy/",theme.deployButton.icon); if (url) { @@ -206,14 +205,26 @@ module.exports = { } if (theme.login) { + let themeContextLogin = {} + let hasLoginTheme = false if (theme.login.image) { url = serveFile(themeApp,"/login/",theme.login.image); if (url) { - themeContext.login = { - image: url - } + themeContextLogin.image = url + hasLoginTheme = true } } + if (theme.login.message) { + themeContextLogin.message = theme.login.message + hasLoginTheme = true + } + if (theme.login.button) { + themeContextLogin.button = theme.login.button + hasLoginTheme = true + } + if (hasLoginTheme) { + themeContext.login = themeContextLogin + } } themeApp.get("/", async function(req,res) { const themePluginList = await runtimeAPI.plugins.getPluginsByType({type:"node-red-theme"}); diff --git a/packages/node_modules/@node-red/editor-api/package.json b/packages/node_modules/@node-red/editor-api/package.json index 2c9d571bf..809580243 100644 --- a/packages/node_modules/@node-red/editor-api/package.json +++ b/packages/node_modules/@node-red/editor-api/package.json @@ -19,11 +19,11 @@ "@node-red/util": "4.1.0-beta.0", "@node-red/editor-client": "4.1.0-beta.0", "bcryptjs": "2.4.3", - "body-parser": "1.20.2", + "body-parser": "1.20.3", "clone": "2.1.2", "cors": "2.8.5", - "express-session": "1.18.0", - "express": "4.19.2", + "express-session": "1.18.1", + "express": "4.21.2", "memorystore": "1.6.7", "mime": "3.0.0", "multer": "1.4.5-lts.1", 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 9cc557efd..5a35135ee 100644 --- 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 @@ -58,7 +58,6 @@ "confirmDelete": "Confirm delete", "delete": "Are you sure you want to delete '__label__'?", "dropFlowHere": "Drop the flow here", - "dropImageHere": "Drop the image here", "addFlow": "Add flow", "addFlowToRight": "Add flow to the right", "closeFlow": "Close flow", @@ -375,7 +374,10 @@ "flowAdded": "flow added", "moved": "moved", "movedTo": "moved to __id__", - "movedFrom": "moved from __id__" + "movedFrom": "moved from __id__", + "none": "none", + "position": "position", + "wires": "wires" }, "nodeCount": "__count__ node", "nodeCount_plural": "__count__ nodes", @@ -384,9 +386,14 @@ "reviewChanges": "Review Changes", "noBinaryFileShowed": "Cannot show binary file contents", "viewCommitDiff": "View Commit Changes", + "commit": "Commit", "compareChanges": "Compare Changes", "saveConflict": "Save conflict resolution", "conflictHeader": "__resolved__ of __unresolved__ conflicts resolved", + "localChanges": "Local Changes", + "remoteChanges": "Remote Changes", + "useLocalChanges": "use local changes", + "useRemoteChanges": "use remote changes", "commonVersionError": "Common Version doesn't contain valid JSON:", "oldVersionError": "Old Version doesn't contain valid JSON:", "newVersionError": "New Version doesn't contain valid JSON:" @@ -554,7 +561,9 @@ "types": { "local": "Local", "examples": "Examples" - } + }, + "type": "Type", + "name": "Name" }, "palette": { "noInfo": "no information available", @@ -805,6 +814,7 @@ "branches": "Branches", "noBranches": "No branches", "deleteConfirm": "Are you sure you want to delete the local branch '__name__'? This cannot be undone.", + "deleteBranch": "Delete branch", "unmergedConfirm": "The local branch '__name__' has unmerged changes that will be lost. Are you sure you want to delete it?", "deleteUnmergedBranch": "Delete unmerged branch", "gitRemotes": "Git remotes", diff --git a/packages/node_modules/@node-red/editor-client/locales/es-ES/editor.json b/packages/node_modules/@node-red/editor-client/locales/es-ES/editor.json index 2655dfe27..015b3d32e 100644 --- a/packages/node_modules/@node-red/editor-client/locales/es-ES/editor.json +++ b/packages/node_modules/@node-red/editor-client/locales/es-ES/editor.json @@ -27,7 +27,8 @@ "lock": "Bloquear", "unlock": "Desbloquear", "locked": "Bloqueado", - "unlocked": "Desbloqueado" + "unlocked": "Desbloqueado", + "format": "Formato" }, "type": { "string": "texto", @@ -57,7 +58,6 @@ "confirmDelete": "Confirmar eliminación", "delete": "¿Estás seguro de que quieres eliminar '__label__'?", "dropFlowHere": "Suelta el flujo aquí", - "dropImageHere": "Suelta la imagen aquí", "addFlow": "Añadir flujo", "addFlowToRight": "Añadir flujo a la derecha", "closeFlow": "Cerrar flujo", @@ -303,7 +303,8 @@ "missingType": "La entrada no es un flujo válido - elemento __index__ falta la propiedad 'type'" }, "conflictNotification1": "Algunos de los nodos que estás importando ya existen en tu espacio de trabajo.", - "conflictNotification2": "Selecciona qué nodos importar y si reemplazar los nodos existentes o importar una copia de los mismos." + "conflictNotification2": "Selecciona qué nodos importar y si reemplazar los nodos existentes o importar una copia de los mismos.", + "alreadyExists": "Este nodo ya existe" }, "copyMessagePath": "Ruta copiada", "copyMessageValue": "Valor copiado", @@ -371,8 +372,12 @@ "deleted": "eliminado", "flowDeleted": "flujo eliminado", "flowAdded": "flujo añadido", + "moved": "movido", "movedTo": "movido a __id__", - "movedFrom": "movido desde __id__" + "movedFrom": "movido desde __id__", + "none": "ninguno", + "position": "posición", + "wires": "conectores" }, "nodeCount": "__count__ nodo", "nodeCount_plural": "__count__ nodos", @@ -381,9 +386,14 @@ "reviewChanges": "Revisar Cambios", "noBinaryFileShowed": "No se puede mostrar el contenido del archivo binario", "viewCommitDiff": "Ver cambios de commit", + "commit": "Commit", "compareChanges": "Comparar Cambios", "saveConflict": "Guardar resolución de conflictos", "conflictHeader": "__resolved__ de __unresolved__ conflictos resueltos", + "localChanges": "Cambios Locales", + "remoteChanges": "Cambios Remotos", + "useLocalChanges": "utilizar cambios locales", + "useRemoteChanges": "utilizar cambios remotos", "commonVersionError": "La versión común no contiene JSON válido:", "oldVersionError": "La versión anterior no contiene JSON válido:", "newVersionError": "La versión nueva no contiene JSON válido:" @@ -551,7 +561,9 @@ "types": { "local": "Local", "examples": "Ejemplos" - } + }, + "type": "Tipo", + "name": "Nombre" }, "palette": { "noInfo": "no hay información disponible", @@ -613,6 +625,8 @@ }, "nodeCount": "__label__ nodo", "nodeCount_plural": "__label__ nodos", + "pluginCount": "__count__ extensión", + "pluginCount_plural": "__count__ extensiones", "moduleCount": "__count__ módulo disponible", "moduleCount_plural": "__count__ módulos disponibles", "inuse": "en uso", @@ -640,6 +654,7 @@ "errors": { "catalogLoadFailed": "

La carga del catálogo de nodos ha fallado

Revise la consola del navegador para mas información

", "installFailed": "

Fallo al instalar: __module__

__message__

Revise el log para mas información

", + "installTimeout": "

La instalación continúa en segundo plano.

Los nodos aparecerán en la paleta cuando finalice. Consulta el registro para obtener más información.

", "removeFailed": "

Fallo al eliminar: __module__

__message__

Revise el log para mas información

", "updateFailed": "

Fallo al actualizar: __module__

__message__

Revise el log para mas información

", "enableFailed": "

Fallo al activar: __module__

__message__

Revise el log para mas información

", @@ -654,6 +669,9 @@ "body":"

Eliminando '__module__'

La eliminación del nodo lo desinstalará de Node-RED. Es posible que el nodo siga utilizando recursos hasta que Node-RED sea reiniciado.

", "title": "Eliminar nodos" }, + "removePlugin": { + "body": "

Extensión __module__ eliminada. Vuelve a cargar el editor para borrar los elementos sobrantes.

" + }, "update": { "body":"

Actualizando '__module__'

La actualización del nodo requerirá un reinicio manual de Node-RED para completarse. Debe ser reiniciado manualmente.

", "title": "Actualizar nodos" @@ -665,7 +683,8 @@ "review": "Abrir información del nodo", "install": "Instalar", "remove": "Eliminar", - "update": "Actualizar" + "update": "Actualizar", + "understood": "Entendido" } } } @@ -718,6 +737,7 @@ "nodeHelp": "Ayuda de nodo", "showHelp": "Mostrar ayuda", "showInOutline": "Mostrar en controno", + "hideTopics": "Esconder temas", "showTopics": "Mostrar temas", "noHelp": "No hay ningun tema de ayuda seleccionado", "changeLog": "Registro de Cambios" @@ -792,6 +812,7 @@ "branches": "Ramas", "noBranches": "Sin ramas", "deleteConfirm": "¿Estás seguro de que quieres eliminar la rama local '__name__'? Esta acción no puede deshacerse.", + "deleteBranch": "Eliminar rama", "unmergedConfirm": "La rama local '__name__' tiene cambios no fusionados que se perderán. ¿Estás seguro de que quieres eliminarla?", "deleteUnmergedBranch": "Eliminar rama no fusionada", "gitRemotes": "Git remotes", @@ -913,6 +934,8 @@ } }, "typedInput": { + "selected": "__count__ seleccionado", + "selected_plural": "__count__ seleccionados", "type": { "str": "texto", "num": "número", @@ -923,7 +946,14 @@ "date": "marca tiempo", "jsonata": "expresión", "env": "variable de entorno", - "cred": "credencial" + "cred": "credencial", + "conf-types": "nodo configuración" + }, + "date": { + "format": { + "timestamp": "milisegundos desde epoch", + "object": "Objeto de fecha de JavaScript" + } } }, "editableList": { @@ -1205,6 +1235,18 @@ "diagnostics": { "title": "Información Sistema" }, + "languages": { + "de": "Deutsch", + "en-US": "English", + "es-ES": "Español (España)", + "fr": "Français", + "ja": "日本語", + "ko": "Korean", + "pt-BR": "Português (Brasil)", + "ru": "Русский", + "zh-CN": "简体中文", + "zh-TW": "繁體中文" + }, "validator": { "errors": { "invalid-json": "Datos JSON inválidos: __error__", diff --git a/packages/node_modules/@node-red/editor-client/locales/fr/editor.json b/packages/node_modules/@node-red/editor-client/locales/fr/editor.json index 950266007..ddc464650 100644 --- a/packages/node_modules/@node-red/editor-client/locales/fr/editor.json +++ b/packages/node_modules/@node-red/editor-client/locales/fr/editor.json @@ -58,7 +58,6 @@ "confirmDelete": "Confirmer la suppression", "delete": "Êtes-vous sûr de vouloir supprimer '__label__' ?", "dropFlowHere": "Lâchez le flux ici", - "dropImageHere": "Lâchez l'image ici", "addFlow": "Ajouter un flux", "addFlowToRight": "Ajouter un flux à droite", "closeFlow": "Fermer le flux", @@ -375,7 +374,10 @@ "flowAdded": "flux ajouté", "moved": "déplacé", "movedTo": "déplacé vers __id__", - "movedFrom": "déplacé depuis __id__" + "movedFrom": "déplacé depuis __id__", + "none": "aucun", + "position": "position", + "wires": "câbles" }, "nodeCount": "__count__ noeud", "nodeCount_plural": "__count__ noeuds", @@ -384,9 +386,14 @@ "reviewChanges": "Examiner les modifications", "noBinaryFileShowed": "Impossible d'afficher le contenu du fichier binaire", "viewCommitDiff": "Afficher les modifications de la validation", + "commit": "Validation", "compareChanges": "Comparer les modifications", "saveConflict": "Enregistrer la résolution des conflits", "conflictHeader": "__resolved__ sur __unresolved__ conflit(s) résolu(s)", + "localChanges": "Modifications locales", + "remoteChanges": "Modifications distantes", + "useLocalChanges": "utiliser les modifications locales", + "useRemoteChanges": "utiliser les modifications distantes", "commonVersionError": "La version commune ne contient pas de JSON valide :", "oldVersionError": "L'ancienne version ne contient pas de JSON valide :", "newVersionError": "La nouvelle version ne contient pas de JSON valide :" @@ -554,7 +561,9 @@ "types": { "local": "Local", "examples": "Exemples" - } + }, + "type": "Type", + "name": "Nom" }, "palette": { "noInfo": "Pas d'information disponible", @@ -803,6 +812,7 @@ "branches": "Branches", "noBranches": "Pas de branche", "deleteConfirm": "Êtes-vous sûr de vouloir supprimer la branche locale '__name__' ? Ça ne peut pas être annulé.", + "deleteBranch": "Supprimer la branche", "unmergedConfirm": "La branche locale '__name__' contient des modifications non fusionnées qui seront perdues. Êtes-vous sûr de vouloir la supprimer?", "deleteUnmergedBranch": "Supprimer la branche non fusionnée", "gitRemotes": "Git distant", 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 ab19d459e..e6c02590b 100644 --- a/packages/node_modules/@node-red/editor-client/locales/ja/editor.json +++ b/packages/node_modules/@node-red/editor-client/locales/ja/editor.json @@ -58,7 +58,6 @@ "confirmDelete": "削除の確認", "delete": "本当に '__label__' を削除しますか?", "dropFlowHere": "ここにフローをドロップしてください", - "dropImageHere": "ここに画像ファイルをドロップしてください", "addFlow": "フローの追加", "addFlowToRight": "右側にフローを追加", "closeFlow": "フローを閉じる", @@ -375,7 +374,10 @@ "flowAdded": "追加されたフロー", "moved": "移動", "movedTo": "__id__ へ移動", - "movedFrom": "__id__ から移動" + "movedFrom": "__id__ から移動", + "none": "なし", + "position": "位置", + "wires": "ワイヤー" }, "nodeCount": "__count__ 個のノード", "nodeCount_plural": "__count__ 個のノード", @@ -384,9 +386,14 @@ "reviewChanges": "変更を表示", "noBinaryFileShowed": "バイナリファイルの中身は表示することができません", "viewCommitDiff": "コミットの内容を表示", + "commit": "コミット", "compareChanges": "変更を比較", "saveConflict": "解決して保存", "conflictHeader": "__unresolved__ 個中 __resolved__ 個のコンフリクトを解決", + "localChanges": "ローカルの変更", + "remoteChanges": "リモートの変更", + "useLocalChanges": "ローカルの変更を使用", + "useRemoteChanges": "リモートの変更を使用", "commonVersionError": "共通バージョンは正しいJSON形式ではありません:", "oldVersionError": "古いバージョンは正しいJSON形式ではありません:", "newVersionError": "新しいバージョンは正しいJSON形式ではありません:" @@ -554,7 +561,9 @@ "types": { "local": "ローカル", "examples": "サンプル" - } + }, + "type": "型", + "name": "名前" }, "palette": { "noInfo": "情報がありません", @@ -803,6 +812,7 @@ "branches": "ブランチ", "noBranches": "ブランチなし", "deleteConfirm": "本当にローカルブランチ'__name__'を削除しますか?削除すると元に戻すことはできません。", + "deleteBranch": "ブランチを削除", "unmergedConfirm": "ローカルブランチ'__name__'にはマージされていない変更があります。この変更は削除されます。本当に削除しますか?", "deleteUnmergedBranch": "マージされていないブランチを削除", "gitRemotes": "Gitリモート", diff --git a/packages/node_modules/@node-red/editor-client/src/js/history.js b/packages/node_modules/@node-red/editor-client/src/js/history.js index 2fa4e4427..8b9601fa6 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/history.js +++ b/packages/node_modules/@node-red/editor-client/src/js/history.js @@ -453,10 +453,68 @@ RED.history = (function() { RED.events.emit("nodes:change",newConfigNode); } }); + } else if (i === "env" && ev.node.type.indexOf("subflow:") === 0) { + // Subflow can have config node in node.env + let nodeList = ev.node.env || []; + nodeList = nodeList.reduce((list, prop) => { + if (prop.type === "conf-type" && prop.value) { + list.push(prop.value); + } + return list; + }, []); + + nodeList.forEach(function(id) { + const configNode = RED.nodes.node(id); + if (configNode) { + if (configNode.users.indexOf(ev.node) !== -1) { + configNode.users.splice(configNode.users.indexOf(ev.node), 1); + RED.events.emit("nodes:change", configNode); + } + } + }); + + nodeList = ev.changes.env || []; + nodeList = nodeList.reduce((list, prop) => { + if (prop.type === "conf-type" && prop.value) { + list.push(prop.value); + } + return list; + }, []); + + nodeList.forEach(function(id) { + const configNode = RED.nodes.node(id); + if (configNode) { + if (configNode.users.indexOf(ev.node) === -1) { + configNode.users.push(ev.node); + RED.events.emit("nodes:change", configNode); + } + } + }); + } else if (i === "color" && ev.node.type === "subflow") { + // Handle the Subflow definition color change + RED.utils.clearNodeColorCache(); + const subflowDef = RED.nodes.getType("subflow:" + ev.node.id); + if (subflowDef) { + subflowDef.color = ev.changes[i] || "#DDAA99"; + } + } + if (i === "credentials" && ev.changes[i]) { + // Reset - Only want to keep the changes + inverseEv.changes[i] = {}; + for (const [key, value] of Object.entries(ev.changes[i])) { + // Edge case: node.credentials is cleared after a deploy, so we can't + // capture values for the inverse event when undoing past a deploy + if (ev.node.credentials) { + inverseEv.changes[i][key] = ev.node.credentials[key]; + } + ev.node.credentials[key] = value; + } + } else { + ev.node[i] = ev.changes[i]; } - ev.node[i] = ev.changes[i]; } } + ev.node.dirty = true; ev.node.changed = ev.changed; @@ -505,6 +563,10 @@ RED.history = (function() { if (node) { node.changed = n.changed; node.dirty = true; + + if (ev.changes && ev.changes.hasOwnProperty('color')) { + node._colorChanged = true; + } } }); } @@ -536,6 +598,24 @@ RED.history = (function() { RED.editor.updateNodeProperties(ev.node,outputMap); RED.editor.validateNode(ev.node); } + // If it's a Config Node, validate user nodes too. + // NOTE: The Config Node must be validated before validating users. + if (ev.node.users) { + const validatedNodes = new Set(); + const userStack = ev.node.users.slice(); + + validatedNodes.add(ev.node.id); + while (userStack.length) { + const node = userStack.pop(); + if (!validatedNodes.has(node.id)) { + validatedNodes.add(node.id); + if (node.users) { + userStack.push(...node.users); + } + RED.editor.validateNode(node); + } + } + } if (ev.links) { inverseEv.createdLinks = []; for (i=0;i { + const location = getLocation() + if (location.workspace !== 0) { + log('send', 'multiplayer/location', location) + RED.comms.send('multiplayer/location', location) + } + publishLocationTimeout = null + }, 100) } } + function revealUser(location, skipWorkspace) { if (location.node) { // Need to check if this is a known node, so we can fall back to revealing @@ -271,7 +291,16 @@ RED.multiplayer = (function () { function removeUserLocation (sessionId) { updateUserLocation(sessionId, {}) + removeUserCursor(sessionId) } + function removeUserCursor (sessionId) { + // return + if (sessions[sessionId]?.cursor) { + sessions[sessionId].cursor.parentNode.removeChild(sessions[sessionId].cursor) + delete sessions[sessionId].cursor + } + } + function updateUserLocation (sessionId, location) { let viewTouched = false const oldLocation = sessions[sessionId].location @@ -291,6 +320,28 @@ RED.multiplayer = (function () { // console.log(`updateUserLocation sessionId:${sessionId} oldWS:${oldLocation?.workspace} newWS:${location.workspace}`) if (location.workspace) { getWorkspaceTray(location.workspace).addUser(sessionId) + if (location.cursor && location.workspace === RED.workspaces.active()) { + if (!sessions[sessionId].cursor) { + const user = sessions[sessionId].user + const cursorIcon = document.createElementNS("http://www.w3.org/2000/svg","g"); + cursorIcon.setAttribute("class", "red-ui-multiplayer-annotation") + cursorIcon.appendChild(createAnnotationUser(user, true)) + $(cursorIcon).css({ + transform: `translate( ${location.cursor.x}px, ${location.cursor.y}px)`, + transition: 'transform 0.1s linear' + }) + $("#red-ui-workspace-chart svg").append(cursorIcon) + sessions[sessionId].cursor = cursorIcon + } else { + const cursorIcon = sessions[sessionId].cursor + $(cursorIcon).css({ + transform: `translate( ${location.cursor.x}px, ${location.cursor.y}px)` + }) + + } + } else if (sessions[sessionId].cursor) { + removeUserCursor(sessionId) + } } if (location.node) { addUserToNode(sessionId, location.node) @@ -309,67 +360,68 @@ RED.multiplayer = (function () { // } // } + + function createAnnotationUser(user, pointer = false) { + const radius = 20 + const halfRadius = radius/2 + const group = document.createElementNS("http://www.w3.org/2000/svg","g"); + const badge = document.createElementNS("http://www.w3.org/2000/svg","path"); + let shapePath + if (!pointer) { + shapePath = `M 0 ${halfRadius} a ${halfRadius} ${halfRadius} 0 1 1 ${radius} 0 a ${halfRadius} ${halfRadius} 0 1 1 -${radius} 0 z` + } else { + shapePath = `M 0 0 h ${halfRadius} a ${halfRadius} ${halfRadius} 0 1 1 -${halfRadius} ${halfRadius} z` + } + badge.setAttribute('d', shapePath) + badge.setAttribute("class", "red-ui-multiplayer-annotation-background") + group.appendChild(badge) + if (user && user.profileColor !== undefined) { + badge.setAttribute("class", "red-ui-multiplayer-annotation-background red-ui-user-profile-color-" + user.profileColor) + } + if (user && user.image) { + const image = document.createElementNS("http://www.w3.org/2000/svg","image"); + image.setAttribute("width", radius) + image.setAttribute("height", radius) + image.setAttribute("href", user.image) + image.setAttribute("clip-path", "circle("+Math.floor(radius/2)+")") + group.appendChild(image) + } else if (user && user.anonymous) { + const anonIconHead = document.createElementNS("http://www.w3.org/2000/svg","circle"); + anonIconHead.setAttribute("cx", radius/2) + anonIconHead.setAttribute("cy", radius/2 - 2) + anonIconHead.setAttribute("r", 2.4) + anonIconHead.setAttribute("class","red-ui-multiplayer-annotation-anon-label"); + group.appendChild(anonIconHead) + const anonIconBody = document.createElementNS("http://www.w3.org/2000/svg","path"); + anonIconBody.setAttribute("class","red-ui-multiplayer-annotation-anon-label"); + // anonIconBody.setAttribute("d",`M ${radius/2 - 4} ${radius/2 + 1} h 8 v4 h -8 z`); + anonIconBody.setAttribute("d",`M ${radius/2} ${radius/2 + 5} h -2.5 c -2 1 -2 -5 0.5 -4.5 c 2 1 2 1 4 0 c 2.5 -0.5 2.5 5.5 0 4.5 z`); + group.appendChild(anonIconBody) + } else { + const label = document.createElementNS("http://www.w3.org/2000/svg","text"); + if (user.username || user.email) { + label.setAttribute("class","red-ui-multiplayer-annotation-label"); + label.textContent = (user.username || user.email).substring(0,2) + } else { + label.setAttribute("class","red-ui-multiplayer-annotation-label red-ui-multiplayer-user-count") + label.textContent = 'nr' + } + label.setAttribute("text-anchor", "middle") + label.setAttribute("x",radius/2); + label.setAttribute("y",radius/2 + 3); + group.appendChild(label) + } + const border = document.createElementNS("http://www.w3.org/2000/svg","path"); + border.setAttribute('d', shapePath) + border.setAttribute("class", "red-ui-multiplayer-annotation-border") + group.appendChild(border) + return group + } + return { init: function () { - function createAnnotationUser(user) { - - const group = document.createElementNS("http://www.w3.org/2000/svg","g"); - const badge = document.createElementNS("http://www.w3.org/2000/svg","circle"); - const radius = 20 - badge.setAttribute("cx",radius/2); - badge.setAttribute("cy",radius/2); - badge.setAttribute("r",radius/2); - badge.setAttribute("class", "red-ui-multiplayer-annotation-background") - group.appendChild(badge) - if (user && user.profileColor !== undefined) { - badge.setAttribute("class", "red-ui-multiplayer-annotation-background red-ui-user-profile-color-" + user.profileColor) - } - if (user && user.image) { - const image = document.createElementNS("http://www.w3.org/2000/svg","image"); - image.setAttribute("width", radius) - image.setAttribute("height", radius) - image.setAttribute("href", user.image) - image.setAttribute("clip-path", "circle("+Math.floor(radius/2)+")") - group.appendChild(image) - } else if (user && user.anonymous) { - const anonIconHead = document.createElementNS("http://www.w3.org/2000/svg","circle"); - anonIconHead.setAttribute("cx", radius/2) - anonIconHead.setAttribute("cy", radius/2 - 2) - anonIconHead.setAttribute("r", 2.4) - anonIconHead.setAttribute("class","red-ui-multiplayer-annotation-anon-label"); - group.appendChild(anonIconHead) - const anonIconBody = document.createElementNS("http://www.w3.org/2000/svg","path"); - anonIconBody.setAttribute("class","red-ui-multiplayer-annotation-anon-label"); - // anonIconBody.setAttribute("d",`M ${radius/2 - 4} ${radius/2 + 1} h 8 v4 h -8 z`); - anonIconBody.setAttribute("d",`M ${radius/2} ${radius/2 + 5} h -2.5 c -2 1 -2 -5 0.5 -4.5 c 2 1 2 1 4 0 c 2.5 -0.5 2.5 5.5 0 4.5 z`); - group.appendChild(anonIconBody) - } else { - const labelText = user.username ? user.username.substring(0,2) : user - const label = document.createElementNS("http://www.w3.org/2000/svg","text"); - if (user.username) { - label.setAttribute("class","red-ui-multiplayer-annotation-label"); - label.textContent = user.username.substring(0,2) - } else { - label.setAttribute("class","red-ui-multiplayer-annotation-label red-ui-multiplayer-user-count") - label.textContent = user - } - label.setAttribute("text-anchor", "middle") - label.setAttribute("x",radius/2); - label.setAttribute("y",radius/2 + 3); - group.appendChild(label) - } - const border = document.createElementNS("http://www.w3.org/2000/svg","circle"); - border.setAttribute("cx",radius/2); - border.setAttribute("cy",radius/2); - border.setAttribute("r",radius/2); - border.setAttribute("class", "red-ui-multiplayer-annotation-border") - group.appendChild(border) - - - - return group - } + RED.view.annotations.register("red-ui-multiplayer",{ type: 'badge', @@ -479,6 +531,24 @@ RED.multiplayer = (function () { RED.comms.send('multiplayer/disconnect', disconnectInfo) RED.settings.removeLocal('multiplayer:sessionId') }) + + const chart = $('#red-ui-workspace-chart') + chart.on('mousemove', function (evt) { + lastPosition[0] = evt.clientX + lastPosition[1] = evt.clientY + publishLocation() + }) + chart.on('scroll', function (evt) { + publishLocation() + }) + chart.on('mouseenter', function () { + isInWorkspace = true + publishLocation() + }) + chart.on('mouseleave', function () { + isInWorkspace = false + publishLocation() + }) } } diff --git a/packages/node_modules/@node-red/editor-client/src/js/nodes.js b/packages/node_modules/@node-red/editor-client/src/js/nodes.js index f569b6d5a..258f14569 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/nodes.js +++ b/packages/node_modules/@node-red/editor-client/src/js/nodes.js @@ -73,7 +73,13 @@ RED.nodes = (function() { var exports = { setModulePendingUpdated: function(module,version) { - moduleList[module].pending_version = version; + if (!!RED.plugins.getModule(module)) { + // The module updated is a plugin + RED.plugins.getModule(module).pending_version = version; + } else { + moduleList[module].pending_version = version; + } + RED.events.emit("registry:module-updated",{module:module,version:version}); }, getModule: function(module) { @@ -701,12 +707,15 @@ RED.nodes = (function() { } n["_"] = RED._; } + + // Both node and config node can use a config node + updateConfigNodeUsers(newNode, { action: "add" }); + if (n._def.category == "config") { - configNodes[n.id] = n; + configNodes[n.id] = newNode; } else { if (n.wires && (n.wires.length > n.outputs)) { n.outputs = n.wires.length; } n.dirty = true; - updateConfigNodeUsers(n); if (n._def.category == "subflows" && typeof n.i === "undefined") { var nextId = 0; RED.nodes.eachNode(function(node) { @@ -768,9 +777,11 @@ RED.nodes = (function() { var removedLinks = []; var removedNodes = []; var node; + if (id in configNodes) { node = configNodes[id]; delete configNodes[id]; + updateConfigNodeUsers(node, { action: "remove" }); RED.events.emit('nodes:remove',node); RED.workspaces.refresh(); } else if (allNodes.hasNode(id)) { @@ -779,6 +790,9 @@ RED.nodes = (function() { delete nodeLinks[id]; removedLinks = links.filter(function(l) { return (l.source === node) || (l.target === node); }); removedLinks.forEach(removeLink); + updateConfigNodeUsers(node, { action: "remove" }); + + // TODO: Legacy code for exclusive config node var updatedConfigNode = false; for (var d in node._def.defaults) { if (node._def.defaults.hasOwnProperty(d)) { @@ -792,10 +806,6 @@ RED.nodes = (function() { if (configNode._def.exclusive) { removeNode(node[d]); removedNodes.push(configNode); - } else { - var users = configNode.users; - users.splice(users.indexOf(node),1); - RED.events.emit('nodes:change',configNode) } } } @@ -1032,23 +1042,34 @@ RED.nodes = (function() { return {nodes:removedNodes,links:removedLinks, groups: removedGroups, junctions: removedJunctions}; } + /** + * Add a Subflow to the Workspace + * + * @param {object} sf The Subflow to add. + * @param {boolean|undefined} createNewIds Whether to update the name. + */ function addSubflow(sf, createNewIds) { if (createNewIds) { - var subflowNames = Object.keys(subflows).map(function(sfid) { - return subflows[sfid].name; - }); + // Update the Subflow name to highlight that this is a copy + const subflowNames = Object.keys(subflows).map(function (sfid) { + return subflows[sfid].name || ""; + }) + subflowNames.sort() - subflowNames.sort(); - var copyNumber = 1; - var subflowName = sf.name; + let copyNumber = 1; + let subflowName = sf.name; subflowNames.forEach(function(name) { if (subflowName == name) { + subflowName = sf.name + " (" + copyNumber + ")"; copyNumber++; - subflowName = sf.name+" ("+copyNumber+")"; } }); + sf.name = subflowName; } + + sf.instances = []; + subflows[sf.id] = sf; allNodes.addTab(sf.id); linkTabMap[sf.id] = []; @@ -1101,7 +1122,7 @@ RED.nodes = (function() { module: "node-red" } }); - sf.instances = []; + sf._def = RED.nodes.getType("subflow:"+sf.id); RED.events.emit("subflows:add",sf); } @@ -1743,7 +1764,8 @@ RED.nodes = (function() { // Remove the old subflow definition - but leave the instances in place var removalResult = RED.subflow.removeSubflow(n.id, true); // Create the list of nodes for the new subflow def - var subflowNodes = [n].concat(zMap[n.id]); + // Need to sort the list in order to remove missing nodes + var subflowNodes = [n].concat(zMap[n.id]).filter((s) => !!s); // Import the new subflow - no clashes should occur as we've removed // the old version var result = importNodes(subflowNodes); @@ -1780,9 +1802,20 @@ RED.nodes = (function() { // Replace config nodes // configNodeIds.forEach(function(id) { - removedNodes = removedNodes.concat(convertNode(getNode(id))); + const configNode = getNode(id); + const currentUserCount = configNode.users; + + // Add a snapshot of the Config Node + removedNodes = removedNodes.concat(convertNode(configNode)); + + // Remove the Config Node instance removeNode(id); - importNodes([newConfigNodes[id]]) + + // Import the new one + importNodes([newConfigNodes[id]]); + + // Re-attributes the user count + getNode(id).users = currentUserCount; }); return { @@ -2023,6 +2056,8 @@ RED.nodes = (function() { if (matchingSubflow) { subflow_denylist[n.id] = matchingSubflow; } else { + const oldId = n.id; + subflow_map[n.id] = n; if (createNewIds || options.importMap[n.id] === "copy") { nid = getID(); @@ -2050,7 +2085,7 @@ RED.nodes = (function() { n.status.id = getID(); } new_subflows.push(n); - addSubflow(n,createNewIds || options.importMap[n.id] === "copy"); + addSubflow(n,createNewIds || options.importMap[oldId] === "copy"); } } } @@ -2064,6 +2099,8 @@ RED.nodes = (function() { activeWorkspace = RED.workspaces.active(); } + const pendingConfigNodes = [] + const pendingConfigNodeIds = new Set() // Find all config nodes and add them for (i=0;iconfig node relationships are + // not very common + let iterationLimit = pendingConfigNodes.length * 5 + const handledConfigNodes = new Set() + while (pendingConfigNodes.length > 0 && iterationLimit > 0) { + const node = pendingConfigNodes.shift() + let hasPending = false + // Loop through the nodes referenced by this node to see if anything + // is pending + node._configNodeReferences.forEach(id => { + if (pendingConfigNodeIds.has(id) && !handledConfigNodes.has(id)) { + // This reference is for a node we know is in this import, but + // it isn't added yet - flag as pending + hasPending = true + } + }) + if (!hasPending) { + // This node has no pending config node references - safe to add + delete node._configNodeReferences + new_nodes.push(node) + handledConfigNodes.add(node.id) + } else { + // This node has pending config node references + // Put to the back of the queue + pendingConfigNodes.push(node) + } + iterationLimit-- + } + if (pendingConfigNodes.length > 0) { + // We exceeded the iteration count. Could be due to reference loops + // between the config nodes. At this point, just add the remaining + // nodes as-is + pendingConfigNodes.forEach(node => { + delete node._configNodeReferences + new_nodes.push(node) + }) + } + // Find regular flow nodes and subflow instances for (i=0;i node.outputs) { - if (!node._def.defaults.hasOwnProperty("outputs") || !isNaN(parseInt(n.outputs))) { - // If 'wires' is longer than outputs, clip wires - console.log("Warning: node.wires longer than node.outputs - trimming wires:",node.id," wires:",node.wires.length," outputs:",node.outputs); - node.wires = node.wires.slice(0,node.outputs); - } else { - // The node declares outputs in its defaults, but has not got a valid value - // Defer to the length of the wires array + + // The node declares outputs in its defaults, but has not got a valid value + // Defer to the length of the wires array + if (node.hasOwnProperty('wires')) { + if (isNaN(node.outputs)) { node.outputs = node.wires.length; + } else if (node.wires.length > node.outputs) { + // If 'wires' is longer than outputs, clip wires + console.log("Warning: node.wires longer than node.outputs - trimming wires:", node.id, " wires:", node.wires.length, " outputs:", node.outputs); + node.wires = node.wires.slice(0, node.outputs); } } + for (d in node._def.defaults) { if (node._def.defaults.hasOwnProperty(d) && d !== 'inputs' && d !== 'outputs') { node[d] = n[d]; @@ -2406,11 +2501,28 @@ RED.nodes = (function() { } else { delete n.g } - // If importing into a subflow, ensure an outbound-link doesn't get added - if (activeSubflow && /^link /.test(n.type) && n.links) { + // If importing a link node, ensure both ends of each link are either: + // - not in a subflow + // - both in the same subflow (not for link call node) + if (/^link /.test(n.type) && n.links) { n.links = n.links.filter(function(id) { const otherNode = node_map[id] || RED.nodes.node(id); - return (otherNode && otherNode.z === activeWorkspace); + if (!otherNode) { + // Cannot find other end - remove the link + return false + } + if (otherNode.z === n.z) { + // Both ends in the same flow/subflow + return true + } else if (n.type === "link call" && !getSubflow(otherNode.z)) { + // Link call node can call out of a subflow as long as otherNode is + // not in a subflow + return true + } else if (!!getSubflow(n.z) || !!getSubflow(otherNode.z)) { + // One end is in a subflow - remove the link + return false + } + return true }); } for (var d3 in n._def.defaults) { @@ -2423,11 +2535,6 @@ RED.nodes = (function() { nodeList = nodeList.map(function(id) { var node = node_map[id]; if (node) { - if (node._def.category === 'config') { - if (node.users.indexOf(n) === -1) { - node.users.push(n); - } - } return node.id; } return id; @@ -2441,9 +2548,11 @@ RED.nodes = (function() { n = new_subflows[i]; n.in.forEach(function(input) { input.wires.forEach(function(wire) { - var link = {source:input, sourcePort:0, target:node_map[wire.id]}; - addLink(link); - new_links.push(link); + if (node_map.hasOwnProperty(wire.id)) { + var link = {source:input, sourcePort:0, target:node_map[wire.id]}; + addLink(link); + new_links.push(link); + } }); delete input.wires; }); @@ -2452,11 +2561,13 @@ RED.nodes = (function() { var link; if (subflow_map[wire.id] && subflow_map[wire.id].id == n.id) { link = {source:n.in[wire.port], sourcePort:wire.port,target:output}; - } else { + } else if (node_map.hasOwnProperty(wire.id) || subflow_map.hasOwnProperty(wire.id)) { link = {source:node_map[wire.id]||subflow_map[wire.id], sourcePort:wire.port,target:output}; } - addLink(link); - new_links.push(link); + if (link) { + addLink(link); + new_links.push(link); + } }); delete output.wires; }); @@ -2465,11 +2576,13 @@ RED.nodes = (function() { var link; if (subflow_map[wire.id] && subflow_map[wire.id].id == n.id) { link = {source:n.in[wire.port], sourcePort:wire.port,target:n.status}; - } else { + } else if (node_map.hasOwnProperty(wire.id) || subflow_map.hasOwnProperty(wire.id)) { link = {source:node_map[wire.id]||subflow_map[wire.id], sourcePort:wire.port,target:n.status}; } - addLink(link); - new_links.push(link); + if (link) { + addLink(link); + new_links.push(link); + } }); delete n.status.wires; } @@ -2648,25 +2761,79 @@ RED.nodes = (function() { return result; } - // Update any config nodes referenced by the provided node to ensure their 'users' list is correct - function updateConfigNodeUsers(n) { - for (var d in n._def.defaults) { - if (n._def.defaults.hasOwnProperty(d)) { - var property = n._def.defaults[d]; + /** + * Update any config nodes referenced by the provided node to ensure + * their 'users' list is correct. + * + * @param {object} node The node in which to check if it contains references + * @param {object} options Options to apply. + * @param {"add" | "remove"} [options.action] Add or remove the node from + * the Config Node users list. Default `add`. + * @param {boolean} [options.emitEvent] Emit the `nodes:changes` event. + * Default true. + */ + function updateConfigNodeUsers(node, options) { + const defaultOptions = { action: "add", emitEvent: true }; + options = Object.assign({}, defaultOptions, options); + + for (var d in node._def.defaults) { + if (node._def.defaults.hasOwnProperty(d)) { + var property = node._def.defaults[d]; if (property.type) { var type = registry.getNodeType(property.type); + // Need to ensure the type is a config node to not treat links nodes if (type && type.category == "config") { - var configNode = configNodes[n[d]]; + var configNode = configNodes[node[d]]; if (configNode) { - if (configNode.users.indexOf(n) === -1) { - configNode.users.push(n); - RED.events.emit('nodes:change',configNode) + if (options.action === "add") { + if (configNode.users.indexOf(node) === -1) { + configNode.users.push(node); + if (options.emitEvent) { + RED.events.emit('nodes:change', configNode); + } + } + } else if (options.action === "remove") { + if (configNode.users.indexOf(node) !== -1) { + const users = configNode.users; + users.splice(users.indexOf(node), 1); + if (options.emitEvent) { + RED.events.emit('nodes:change', configNode); + } + } } } } } } } + + // Subflows can have config node env + if (node.type.indexOf("subflow:") === 0) { + node.env?.forEach((prop) => { + if (prop.type === "conf-type" && prop.value) { + // Add the node to the config node users + const configNode = getNode(prop.value); + if (configNode) { + if (options.action === "add") { + if (configNode.users.indexOf(node) === -1) { + configNode.users.push(node); + if (options.emitEvent) { + RED.events.emit('nodes:change', configNode); + } + } + } else if (options.action === "remove") { + if (configNode.users.indexOf(node) !== -1) { + const users = configNode.users; + users.splice(users.indexOf(node), 1); + if (options.emitEvent) { + RED.events.emit('nodes:change', configNode); + } + } + } + } + } + }); + } } function flowVersion(version) { diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/actionList.js b/packages/node_modules/@node-red/editor-client/src/js/ui/actionList.js index d949899ca..d47a20f5d 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/actionList.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/actionList.js @@ -205,7 +205,9 @@ RED.actionList = (function() { } function init() { - RED.actions.add("core:show-action-list",show); + if (RED.settings.theme("menu.menu-item-action-list", true)) { + RED.actions.add("core:show-action-list",show); + } RED.events.on("editor:open",function() { disabled = true; }); RED.events.on("editor:close",function() { disabled = false; }); 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 4089e392e..148af989f 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 @@ -324,6 +324,30 @@ RED.clipboard = (function() { },100); } + /** + * Validates if the provided string looks like valid flow json + * @param {string} flowString the string to validate + * @returns If valid, returns the node array + */ + function validateFlowString(flowString) { + const res = JSON.parse(flowString) + if (!Array.isArray(res)) { + throw new Error(RED._("clipboard.import.errors.notArray")); + } + for (let i = 0; i < res.length; i++) { + if (typeof res[i] !== "object") { + throw new Error(RED._("clipboard.import.errors.itemNotObject",{index:i})); + } + if (!Object.hasOwn(res[i], 'id')) { + throw new Error(RED._("clipboard.import.errors.missingId",{index:i})); + } + if (!Object.hasOwn(res[i], 'type')) { + throw new Error(RED._("clipboard.import.errors.missingType",{index:i})); + } + } + return res + } + var validateImportTimeout; function validateImport() { if (activeTab === "red-ui-clipboard-dialog-import-tab-clipboard") { @@ -341,21 +365,7 @@ RED.clipboard = (function() { 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',{style: "display: flex"}); - const valEl = $('
',{style:"font-family: var(--red-ui-monospace-font); white-space:nowrap; overflow: hidden; flex-grow:1"}); + const valEl = $('
',{ class: "red-ui-autoComplete-completion" }); valEl.append(generateSpans(valMatch)); valEl.appendTo(element); if (optSrc) { @@ -159,7 +160,7 @@ if (valMatch.found) { const optSrc = envVarsMap[v] const element = $('
',{style: "display: flex"}); - const valEl = $('
',{style:"font-family: var(--red-ui-monospace-font); white-space:nowrap; overflow: hidden; flex-grow:1"}); + const valEl = $('
',{ class: "red-ui-autoComplete-completion" }); valEl.append(generateSpans(valMatch)) valEl.appendTo(element) @@ -201,7 +202,7 @@ const that = this const getContextKeysFromRuntime = function(scope, store, searchKey, done) { contextKnownKeys[scope] = contextKnownKeys[scope] || {} - contextKnownKeys[scope][store] = contextKnownKeys[scope][store] || new Set() + contextKnownKeys[scope][store] = contextKnownKeys[scope][store] || new Map() if (searchKey.length > 0) { try { RED.utils.normalisePropertyExpression(searchKey) @@ -223,11 +224,12 @@ const result = data[store] || {} const keys = result.keys || [] const keyPrefix = searchKey + (searchKey.length > 0 ? '.' : '') - keys.forEach(key => { + keys.forEach(keyInfo => { + const key = keyInfo.key if (/^[a-zA-Z_$][0-9a-zA-Z_$]*$/.test(key)) { - contextKnownKeys[scope][store].add(keyPrefix + key) + contextKnownKeys[scope][store].set(keyPrefix + key, keyInfo) } else { - contextKnownKeys[scope][store].add(searchKey + "[\""+key.replace(/"/,"\\\"")+"\"]") + contextKnownKeys[scope][store].set(searchKey + "[\""+key.replace(/"/,"\\\"")+"\"]", keyInfo) } }) done() @@ -242,14 +244,14 @@ // Get the flow id of the node we're editing const editStack = RED.editor.getEditStack() if (editStack.length === 0) { - done([]) + done(new Map()) return } const editingNode = editStack.pop() if (editingNode.z) { scope = `${scope}/${editingNode.z}` } else { - done([]) + done(new Map()) return } } @@ -269,17 +271,29 @@ return function(val, done) { getContextKeys(val, function (keys) { const matches = [] - keys.forEach(v => { + keys.forEach((keyInfo, v) => { let optVal = v let valMatch = getMatch(optVal, val); - if (!valMatch.found && val.length > 0 && val.endsWith('.')) { - // Search key ends in '.' - but doesn't match. Check again - // with [" at the end instead so we match bracket notation - valMatch = getMatch(optVal, val.substring(0, val.length - 1) + '["') + if (!valMatch.found && val.length > 0) { + if (val.endsWith('.')) { + // Search key ends in '.' - but doesn't match. Check again + // with [" at the end instead so we match bracket notation + valMatch = getMatch(optVal, val.substring(0, val.length - 1) + '["') + // } else if (val.endsWith('[') && /^array/.test(keyInfo.format)) { + // console.log('this case') + } } if (valMatch.found) { const element = $('
',{style: "display: flex"}); - const valEl = $('
',{style:"font-family: var(--red-ui-monospace-font); white-space:nowrap; overflow: hidden; flex-grow:1"}); + const valEl = $('
',{ class: "red-ui-autoComplete-completion" }); + // if (keyInfo.format) { + // valMatch.post += ' ' + keyInfo.format + // } + if (valMatch.exact && /^array/.test(keyInfo.format)) { + valMatch.post += `[0-${keyInfo.length}]` + optVal += '[' + + } valEl.append(generateSpans(valMatch)) valEl.appendTo(element) matches.push({ @@ -1567,7 +1581,8 @@ if (tooltip) { tooltip.setContent(valid); } else { - tooltip = RED.popover.tooltip(this.elementDiv, valid); + const target = this.typeMap[type]?.options ? this.optionSelectLabel : this.elementDiv; + tooltip = RED.popover.tooltip(target, valid); this.element.data("tooltip", tooltip); } } diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/contextMenu.js b/packages/node_modules/@node-red/editor-client/src/js/ui/contextMenu.js index 60615671e..53ebe5c4b 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/contextMenu.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/contextMenu.js @@ -54,21 +54,22 @@ RED.contextMenu = (function () { } } + const scale = RED.view.scale() const offset = $("#red-ui-workspace-chart").offset() - - let addX = options.x - offset.left + $("#red-ui-workspace-chart").scrollLeft() - let addY = options.y - offset.top + $("#red-ui-workspace-chart").scrollTop() + let addX = (options.x - offset.left + $("#red-ui-workspace-chart").scrollLeft()) / scale + let addY = (options.y - offset.top + $("#red-ui-workspace-chart").scrollTop()) / scale if (RED.view.snapGrid) { const gridSize = RED.view.gridSize() - addX = gridSize * Math.floor(addX / gridSize) - addY = gridSize * Math.floor(addY / gridSize) + addX = gridSize * Math.round(addX / gridSize) + addY = gridSize * Math.round(addY / gridSize) } - menuItems.push( - { onselect: 'core:show-action-list', label: RED._("contextMenu.showActionList"), onpostselect: function () { } } - ) - + if (RED.settings.theme("menu.menu-item-action-list", true)) { + menuItems.push( + { onselect: 'core:show-action-list', label: RED._("contextMenu.showActionList"), onpostselect: function () { } } + ) + } const insertOptions = [] menuItems.push({ label: RED._("contextMenu.insert"), options: insertOptions }) insertOptions.push( @@ -86,7 +87,9 @@ RED.contextMenu = (function () { }, (hasLinks) ? { // has least 1 wire selected label: RED._("contextMenu.junction"), - onselect: 'core:split-wires-with-junctions', + onselect: function () { + RED.actions.invoke('core:split-wires-with-junctions', { x: addX, y: addY }) + }, disabled: !canEdit || !hasLinks } : { label: RED._("contextMenu.junction"), diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/deploy.js b/packages/node_modules/@node-red/editor-client/src/js/ui/deploy.js index 25a67907c..d318f476c 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/deploy.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/deploy.js @@ -44,6 +44,7 @@ RED.deploy = (function() { /** * options: * type: "default" - Button with drop-down options - no further customisation available + * label: the text to display - default: "Deploy" * type: "simple" - Button without dropdown. Customisations: * label: the text to display - default: "Deploy" * icon : the icon to use. Null removes the icon. default: "red/images/deploy-full-o.svg" @@ -51,13 +52,14 @@ RED.deploy = (function() { function init(options) { options = options || {}; var type = options.type || "default"; + var label = options.label || RED._("deploy.deploy"); if (type == "default") { $('
  • '+ ''+ ''+ ' '+ - ''+RED._("deploy.deploy")+''+ + ''+label+''+ ''+ ''+ ''+ @@ -78,7 +80,6 @@ RED.deploy = (function() { mainMenuItems.push({id:"deploymenu-item-reload", icon:"red/images/deploy-reload.svg",label:RED._("deploy.restartFlows"),sublabel:RED._("deploy.restartFlowsDesc"),onselect:"core:restart-flows"}) RED.menu.init({id:"red-ui-header-button-deploy-options", options: mainMenuItems }); } else if (type == "simple") { - var label = options.label || RED._("deploy.deploy"); var icon = 'red/images/deploy-full-o.svg'; if (options.hasOwnProperty('icon')) { icon = options.icon; @@ -424,11 +425,15 @@ RED.deploy = (function() { const unknownNodes = []; const invalidNodes = []; + const isDisabled = function (node) { + return (node.d || RED.nodes.workspace(node.z)?.disabled); + }; + RED.nodes.eachConfig(function (node) { if (node.valid === undefined) { RED.editor.validateNode(node); } - if (!node.valid && !node.d) { + if (!node.valid && !isDisabled(node)) { invalidNodes.push(getNodeInfo(node)); } if (node.type === "unknown") { @@ -438,7 +443,7 @@ RED.deploy = (function() { } }); RED.nodes.eachNode(function (node) { - if (!node.valid && !node.d) { + if (!node.valid && !isDisabled(node)) { invalidNodes.push(getNodeInfo(node)); } if (node.type === "unknown") { @@ -452,7 +457,7 @@ RED.deploy = (function() { const unusedConfigNodes = []; RED.nodes.eachConfig(function (node) { - if ((node._def.hasUsers !== false) && (node.users.length === 0)) { + if ((node._def.hasUsers !== false) && (node.users.length === 0) && !isDisabled(node)) { unusedConfigNodes.push(getNodeInfo(node)); hasUnusedConfig = true; } @@ -589,7 +594,9 @@ RED.deploy = (function() { RED.notify('

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

    ', "success"); } const flowsToLock = new Set() + // Node's properties cannot be modified if its workspace is locked. function ensureUnlocked(id) { + // TODO: `RED.nodes.subflow` is useless const flow = id && (RED.nodes.workspace(id) || RED.nodes.subflow(id) || null); const isLocked = flow ? flow.locked : false; if (flow && isLocked) { @@ -642,6 +649,7 @@ RED.deploy = (function() { delete confNode.credentials; } }); + // Subflow cannot be locked RED.nodes.eachSubflow(function (subflow) { if (subflow.changed) { subflow.changed = false; @@ -650,12 +658,18 @@ RED.deploy = (function() { }); RED.nodes.eachWorkspace(function (ws) { if (ws.changed || ws.added) { - ensureUnlocked(ws.z) + // Ensure the Workspace is unlocked to modify its properties. + ensureUnlocked(ws.id); ws.changed = false; delete ws.added + if (flowsToLock.has(ws)) { + ws.locked = true; + flowsToLock.delete(ws); + } RED.events.emit("flows:change", ws) } }); + // Ensures all workspaces to be locked have been locked. flowsToLock.forEach(flow => { flow.locked = true }) diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/diff.js b/packages/node_modules/@node-red/editor-client/src/js/ui/diff.js index ebdf683e3..26cdceb7e 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/diff.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/diff.js @@ -497,7 +497,7 @@ RED.diff = (function() { } }) if (c === 0) { - result.text("none"); + result.text(RED._("diff.type.none")); } else { list.appendTo(result); } @@ -821,7 +821,7 @@ RED.diff = (function() { conflict = true; } row = $("").appendTo(nodePropertiesTableBody); - $("",{class:"red-ui-diff-list-cell-label"}).text("position").appendTo(row); + $("",{class:"red-ui-diff-list-cell-label"}).text(RED._("diff.type.position")).appendTo(row); localCell = $("",{class:"red-ui-diff-list-cell red-ui-diff-list-node-local"}).appendTo(row); if (localNode) { localCell.addClass("red-ui-diff-status-"+(localChanged?"moved":"unchanged")); @@ -899,7 +899,7 @@ RED.diff = (function() { conflict = true; } row = $("").appendTo(nodePropertiesTableBody); - $("",{class:"red-ui-diff-list-cell-label"}).text("wires").appendTo(row); + $("",{class:"red-ui-diff-list-cell-label"}).text(RED._("diff.type.wires")).appendTo(row); localCell = $("",{class:"red-ui-diff-list-cell red-ui-diff-list-node-local"}).appendTo(row); if (localNode) { if (!conflict) { @@ -2029,15 +2029,14 @@ RED.diff = (function() { if (!isSeparator) { var isOurs = /^..<<<<<<').text("<<<<<<< Local Changes").appendTo(line); + $('').text("<<<<<<< " + RED._("diff.localChanges")).appendTo(line); hunk.localChangeStart = actualLineNumber; } else { hunk.remoteChangeEnd = actualLineNumber; - $('').text(">>>>>>> Remote Changes").appendTo(line); - + $('').text(">>>>>>> " + RED._("diff.remoteChanges")).appendTo(line); } diffRow.addClass("mergeHeader-"+(isOurs?"ours":"theirs")); - $('') + $('') .appendTo(line) .on("click", function(evt) { evt.preventDefault(); @@ -2119,7 +2118,7 @@ RED.diff = (function() { $("

    ").text(commit.title).appendTo(content); $('
    ').text(commit.comment).appendTo(content); var summary = $('
    ').appendTo(content); - $('
    ').text("Commit "+commit.sha).appendTo(summary); + $('
    ').text(RED._('diff.commit')+" "+commit.sha).appendTo(summary); $('
    ').text((commit.authorName||commit.author)+" - "+options.date).appendTo(summary); if (commit.files) { diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/editor.js b/packages/node_modules/@node-red/editor-client/src/js/ui/editor.js index 30c329c41..1de42a69a 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/editor.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/editor.js @@ -295,8 +295,8 @@ RED.editor = (function() { * Called when the node's properties have changed. * Marks the node as dirty and needing a size check. * Removes any links to non-existant outputs. - * @param node - the node that has been updated - * @param outputMap - (optional) a map of old->new port numbers if wires should be moved + * @param {object} node - the node that has been updated + * @param {object} [outputMap] - (optional) a map of old->new port numbers if wires should be moved * @returns {array} the links that were removed due to this update */ function updateNodeProperties(node, outputMap) { @@ -808,6 +808,20 @@ RED.editor = (function() { } } + const oldCreds = {}; + if (editing_node._def.credentials) { + for (const prop in editing_node._def.credentials) { + if (Object.prototype.hasOwnProperty.call(editing_node._def.credentials, prop)) { + if (editing_node._def.credentials[prop].type === 'password') { + oldCreds['has_' + prop] = editing_node.credentials['has_' + prop]; + } + if (prop in editing_node.credentials) { + oldCreds[prop] = editing_node.credentials[prop]; + } + } + } + } + try { const rc = editing_node._def.oneditsave.call(editing_node); if (rc === true) { @@ -839,6 +853,25 @@ RED.editor = (function() { } } } + + if (editing_node._def.credentials) { + for (const prop in editing_node._def.credentials) { + if (Object.prototype.hasOwnProperty.call(editing_node._def.credentials, prop)) { + if (oldCreds[prop] !== editing_node.credentials[prop]) { + if (editing_node.credentials[prop] === '__PWRD__') { + // The password may not exist in oldCreds + // The value '__PWRD__' means the password exists, + // so ignore this change + continue; + } + editState.changes.credentials = editState.changes.credentials || {}; + editState.changes.credentials['has_' + prop] = oldCreds['has_' + prop]; + editState.changes.credentials[prop] = oldCreds[prop]; + editState.changed = true; + } + } + } + } } } @@ -899,7 +932,7 @@ RED.editor = (function() { const labelText = RED.editor.envVarList.lookupLabel(labels, labels["en-US"] || tenv.name, locale) const config = { env: tenv, - id: '${' + parentEnv[0].name + '}', + id: '${' + tenv.name + '}', type: type, label: labelText, __label__: `[env] ${labelText}` @@ -1481,134 +1514,181 @@ RED.editor = (function() { }, { id: "node-config-dialog-ok", - text: adding?RED._("editor.configAdd"):RED._("editor.configUpdate"), + text: adding ? RED._("editor.configAdd") : RED._("editor.configUpdate"), class: "primary", click: function() { - var editState = { + // TODO: Already defined + const configProperty = name; + const configType = type; + const configTypeDef = RED.nodes.getType(configType); + + const wasChanged = editing_config_node.changed; + const editState = { changes: {}, changed: false, outputMap: null }; - var configProperty = name; - var configId = editing_config_node.id; - var configType = type; - var configAdding = adding; - var configTypeDef = RED.nodes.getType(configType); - var d; - var input; + + // Call `oneditsave` and search for changes + handleEditSave(editing_config_node, editState); - if (configTypeDef.oneditsave) { - try { - configTypeDef.oneditsave.call(editing_config_node); - } catch(err) { - console.warn("oneditsave",editing_config_node.id,editing_config_node.type,err.toString()); - } - } - - for (d in configTypeDef.defaults) { - if (configTypeDef.defaults.hasOwnProperty(d)) { - var newValue; - input = $("#node-config-input-"+d); - if (input.attr('type') === "checkbox") { - newValue = input.prop('checked'); - } else if ("format" in configTypeDef.defaults[d] && configTypeDef.defaults[d].format !== "" && input[0].nodeName === "DIV") { - newValue = input.text(); - } else { - newValue = input.val(); - } - if (newValue != null && newValue !== editing_config_node[d]) { - if (editing_config_node._def.defaults[d].type) { - if (newValue == "_ADD_") { - newValue = ""; - } - // Change to a related config node - var configNode = RED.nodes.node(editing_config_node[d]); - if (configNode) { - var users = configNode.users; - users.splice(users.indexOf(editing_config_node),1); - RED.events.emit("nodes:change",configNode); - } - configNode = RED.nodes.node(newValue); - if (configNode) { - configNode.users.push(editing_config_node); - RED.events.emit("nodes:change",configNode); - } - } - editing_config_node[d] = newValue; - } - } - } - - activeEditPanes.forEach(function(pane) { + // Search for changes in the edit box (panes) + activeEditPanes.forEach(function (pane) { if (pane.apply) { pane.apply.call(pane, editState); } - }) + }); - editing_config_node.label = configTypeDef.label; - - var scope = $("#red-ui-editor-config-scope").val(); - editing_config_node.z = scope; + // TODO: Why? + editing_config_node.label = configTypeDef.label + // Check if disabled has changed if ($("#node-config-input-node-disabled").prop('checked')) { if (editing_config_node.d !== true) { + editState.changes.d = editing_config_node.d; + editState.changed = true; editing_config_node.d = true; } } else { if (editing_config_node.d === true) { + editState.changes.d = editing_config_node.d; + editState.changed = true; delete editing_config_node.d; } } + // NOTE: must be undefined if no scope used + const scope = $("#red-ui-editor-config-scope").val() || undefined; + + // Check if the scope has changed + if (editing_config_node.z !== scope) { + editState.changes.z = editing_config_node.z; + editState.changed = true; + editing_config_node.z = scope; + } + + // Search for nodes that use this config node that are no longer + // in scope, so must be removed + const historyEvents = []; if (scope) { - // Search for nodes that use this one that are no longer - // in scope, so must be removed - editing_config_node.users = editing_config_node.users.filter(function(n) { - var keep = true; - for (var d in n._def.defaults) { - if (n._def.defaults.hasOwnProperty(d)) { - if (n._def.defaults[d].type === editing_config_node.type && - n[d] === editing_config_node.id && - n.z !== scope) { - keep = false; - // Remove the reference to this node - // and revalidate - n[d] = null; - n.dirty = true; - n.changed = true; - validateNode(n); + const newUsers = editing_config_node.users.filter(function (node) { + let keepNode = false; + let nodeModified = null; + + for (const d in node._def.defaults) { + if (node._def.defaults.hasOwnProperty(d)) { + if (node._def.defaults[d].type === editing_config_node.type) { + if (node[d] === editing_config_node.id) { + if (node.z === editing_config_node.z) { + // The node is kept only if at least one property uses + // this config node in the correct scope. + keepNode = true; + } else { + if (!nodeModified) { + nodeModified = { + t: "edit", + node: node, + changes: { [d]: node[d] }, + changed: node.changed, + dirty: node.dirty + }; + } else { + nodeModified.changes[d] = node[d]; + } + + // Remove the reference to the config node + node[d] = ""; + } + } } } } - return keep; - }); - } - if (configAdding) { - RED.nodes.add(editing_config_node); - } - - validateNode(editing_config_node); - var validatedNodes = {}; - validatedNodes[editing_config_node.id] = true; - - var userStack = editing_config_node.users.slice(); - while(userStack.length > 0) { - var user = userStack.pop(); - if (!validatedNodes[user.id]) { - validatedNodes[user.id] = true; - if (user.users) { - userStack = userStack.concat(user.users); + // Add the node modified to the history + if (nodeModified) { + historyEvents.push(nodeModified); } - validateNode(user); + + // Mark as changed and revalidate this node + if (!keepNode) { + node.changed = true; + node.dirty = true; + validateNode(node); + RED.events.emit("nodes:change", node); + } + + return keepNode; + }); + + // Check if users are changed + if (editing_config_node.users.length !== newUsers.length) { + editState.changes.users = editing_config_node.users; + editState.changed = true; + editing_config_node.users = newUsers; } } - RED.nodes.dirty(true); - RED.view.redraw(true); - if (!configAdding) { - RED.events.emit("editor:save",editing_config_node); - RED.events.emit("nodes:change",editing_config_node); + + if (editState.changed) { + // Set the congig node as changed + editing_config_node.changed = true; } + + // Now, validate the config node + validateNode(editing_config_node); + + // And validate nodes using this config node too + const validatedNodes = new Set(); + const userStack = editing_config_node.users.slice(); + + validatedNodes.add(editing_config_node.id); + while (userStack.length) { + const node = userStack.pop(); + if (!validatedNodes.has(node.id)) { + validatedNodes.add(node.id); + if (node.users) { + userStack.push(...node.users); + } + validateNode(node); + } + } + + let historyEvent = { + t: "edit", + node: editing_config_node, + changes: editState.changes, + changed: wasChanged, + dirty: RED.nodes.dirty() + }; + + if (historyEvents.length) { + // Need a multi events + historyEvent = { + t: "multi", + events: [historyEvent].concat(historyEvents), + dirty: historyEvent.dirty + }; + } + + if (!adding) { + // This event is triggered when the edit box is saved, + // regardless of whether there are any modifications. + RED.events.emit("editor:save", editing_config_node); + } + + if (editState.changed) { + if (adding) { + RED.history.push({ t: "add", nodes: [editing_config_node.id], dirty: RED.nodes.dirty() }); + // Add the new config node and trigger the `nodes:add` event + RED.nodes.add(editing_config_node); + } else { + RED.history.push(historyEvent); + RED.events.emit("nodes:change", editing_config_node); + } + + RED.nodes.dirty(true); + RED.view.redraw(true); + } + RED.tray.close(function() { var filter = null; // when editing a config via subflow edit panel, the `configProperty` will not @@ -1698,17 +1778,19 @@ RED.editor = (function() { function showEditSubflowDialog(subflow, defaultTab) { if (buildingEditDialog) { return } buildingEditDialog = true; - var editing_node = subflow; - var activeEditPanes = []; + editStack.push(subflow); RED.view.state(RED.state.EDITING); - var trayOptions = { + + let editingNode = subflow; + let activeEditPanes = []; + const trayOptions = { title: getEditStackTitle(), buttons: [ { id: "node-dialog-cancel", text: RED._("common.label.cancel"), - click: function() { + click: function () { RED.tray.close(); } }, @@ -1716,39 +1798,32 @@ RED.editor = (function() { id: "node-dialog-ok", class: "primary", text: RED._("common.label.done"), - click: function() { - var i; - var editState = { + click: function () { + const wasDirty = RED.nodes.dirty(); + const editState = { changes: {}, changed: false, outputMap: null - } - var wasDirty = RED.nodes.dirty(); + }; - activeEditPanes.forEach(function(pane) { + // Search for changes in edit boxes (panes) + // NOTE: no `oneditsave` for Subflow def + activeEditPanes.forEach(function (pane) { if (pane.apply) { pane.apply.call(pane, editState); } - }) + }); - var newName = $("#subflow-input-name").val(); + // Search for env changes (not handled in properties pane) + const oldEnv = editingNode.env; + const newEnv = RED.subflow.exportSubflowTemplateEnv($("#node-input-env-container").editableList("items")); - if (newName != editing_node.name) { - editState.changes['name'] = editing_node.name; - editing_node.name = newName; - editState.changed = true; - } - - - var old_env = editing_node.env; - var new_env = RED.subflow.exportSubflowTemplateEnv($("#node-input-env-container").editableList("items")); - - if (new_env && new_env.length > 0) { - new_env.forEach(function(prop) { + if (newEnv && newEnv.length > 0) { + newEnv.forEach(function (prop) { if (prop.type === "cred") { - editing_node.credentials = editing_node.credentials || {_:{}}; - editing_node.credentials[prop.name] = prop.value; - editing_node.credentials['has_'+prop.name] = (prop.value !== ""); + editingNode.credentials = editingNode.credentials || { _: {} }; + editingNode.credentials[prop.name] = prop.value; + editingNode.credentials['has_' + prop.name] = (prop.value !== ""); if (prop.value !== '__PWRD__') { editState.changed = true; } @@ -1757,111 +1832,162 @@ RED.editor = (function() { }); } - if (!isSameObj(old_env, new_env)) { - editState.changes.env = editing_node.env; - editing_node.env = new_env; + const envToRemove = new Set(); + if (!isSameObj(oldEnv, newEnv)) { + // Get a list of env properties that have been removed + // by comparing oldEnv and newEnv + if (oldEnv) { + oldEnv.forEach((env) => { envToRemove.add(env.name) }); + } + if (newEnv) { + newEnv.forEach((env) => { + envToRemove.delete(env.name) + }); + } + editState.changes.env = oldEnv; + editingNode.env = newEnv; editState.changed = true; } - - if (editState.changed) { - var wasChanged = editing_node.changed; - editing_node.changed = true; - validateNode(editing_node); - var subflowInstances = []; - RED.nodes.eachNode(function(n) { - if (n.type == "subflow:"+editing_node.id) { + const wasChanged = editingNode.changed; + const subflowInstances = []; + const instanceHistoryEvents = []; + + // Marks the Subflow has changed and validate it + editingNode.changed = true; + validateNode(editingNode); + + // Update each Subflow instances + RED.nodes.eachNode(function (n) { + if (n.type == "subflow:" + editingNode.id) { subflowInstances.push({ - id:n.id, - changed:n.changed - }) - n._def.color = editing_node.color; + id: n.id, + changed: n.changed + }); + n.changed = true; n.dirty = true; + if (editState.changes.hasOwnProperty("color")) { + // Redraw the node color + n._colorChanged = true; + } + + if (n.env) { + const oldEnv = n.env; + const newEnv = []; + let envChanged = false; + n.env.forEach((env, index) => { + if (envToRemove.has(env.name)) { + envChanged = true; + } else { + newEnv.push(env); + } + }); + if (envChanged) { + instanceHistoryEvents.push({ + t: 'edit', + node: n, + changes: { env: oldEnv }, + dirty: n.dirty, + changed: n.changed + }); + n.env = newEnv; + } + } + updateNodeProperties(n); validateNode(n); } }); - RED.events.emit("subflows:change",editing_node); - RED.nodes.dirty(true); - var historyEvent = { - t:'edit', - node:editing_node, - changes:editState.changes, - dirty:wasDirty, - changed:wasChanged, + + let historyEvent = { + t: 'edit', + node: editingNode, + changes: editState.changes, + dirty: wasDirty, + changed: wasChanged, subflow: { - instances:subflowInstances + instances: subflowInstances } }; + if (instanceHistoryEvents.length > 0) { + historyEvent = { + t: 'multi', + events: [ historyEvent, ...instanceHistoryEvents ], + dirty: wasDirty + }; + } + + RED.events.emit("subflows:change", editingNode); RED.history.push(historyEvent); + RED.nodes.dirty(true); } - editing_node.dirty = true; + + editingNode.dirty = true; RED.tray.close(); } } ], - resize: function(dimensions) { + resize: function (dimensions) { $(".red-ui-tray-content").height(dimensions.height - 50); - var form = $(".red-ui-tray-content form").height(dimensions.height - 50 - 40); - var size = {width:form.width(),height:form.height()}; - activeEditPanes.forEach(function(pane) { + const form = $(".red-ui-tray-content form").height(dimensions.height - 50 - 40); + const size = { width: form.width(), height: form.height() }; + activeEditPanes.forEach(function (pane) { if (pane.resize) { pane.resize.call(pane, size); } - }) + }); }, - open: function(tray, done) { - var trayFooter = tray.find(".red-ui-tray-footer"); - var trayFooterLeft = $("
    ", { - class: "red-ui-tray-footer-left" - }).appendTo(trayFooter) - var trayBody = tray.find('.red-ui-tray-body'); - trayBody.parent().css('overflow','hidden'); + open: function (tray, done) { + const trayBody = tray.find('.red-ui-tray-body'); + const trayFooter = tray.find(".red-ui-tray-footer"); + trayBody.parent().css('overflow', 'hidden'); + + const trayFooterLeft = $("
    ", { class: "red-ui-tray-footer-left" }).appendTo(trayFooter); $(' ').appendTo(trayFooterLeft); - if (editing_node) { - RED.sidebar.info.refresh(editing_node); + if (editingNode) { + RED.sidebar.info.refresh(editingNode); } - var nodeEditPanes = [ + const nodeEditPanes = [ 'editor-tab-properties', 'editor-tab-subflow-module', 'editor-tab-description', 'editor-tab-appearance' ]; - - prepareEditDialog(trayBody, nodeEditPanes, subflow, subflow._def, "node-input", defaultTab, function(_activeEditPanes) { + prepareEditDialog(trayBody, nodeEditPanes, subflow, subflow._def, "subflow-input", defaultTab, function (_activeEditPanes) { activeEditPanes = _activeEditPanes; - $("#subflow-input-name").val(subflow.name); - RED.text.bidi.prepareInput($("#subflow-input-name")); trayBody.i18n(); trayFooter.i18n(); buildingEditDialog = false; done(); }); }, - close: function() { + close: function () { if (RED.view.state() != RED.state.IMPORT_DRAGGING) { RED.view.state(RED.state.DEFAULT); } - RED.sidebar.info.refresh(editing_node); + + RED.sidebar.info.refresh(editingNode); RED.workspaces.refresh(); - activeEditPanes.forEach(function(pane) { + activeEditPanes.forEach(function (pane) { if (pane.close) { pane.close.call(pane); } - }) + }); + editStack.pop(); - editing_node = null; + // TODO: useless? + editingNode = null; }, - show: function() { - } + show: function () {} } + RED.tray.show(trayOptions); } diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/editors/code-editor.js b/packages/node_modules/@node-red/editor-client/src/js/ui/editors/code-editor.js index b92881764..2b2e90a43 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/editors/code-editor.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/editors/code-editor.js @@ -46,8 +46,8 @@ initialised = selectedCodeEditor.init(); } - $('

    ').appendTo('#red-ui-editor'); - $("#red-ui-image-drop-target").hide(); + $('

    ').appendTo('#red-ui-editor'); + $("#red-ui-drop-target-markdown-editor").hide(); } function create(options) { diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/editors/code-editors/monaco.js b/packages/node_modules/@node-red/editor-client/src/js/ui/editors/code-editors/monaco.js index b9f586944..b35e60d50 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/editors/code-editors/monaco.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/editors/code-editors/monaco.js @@ -691,6 +691,7 @@ RED.editor.codeEditor.monaco = (function() { 2322, //Type 'unknown' is not assignable to type 'string' 2339, //property does not exist on 2345, //Argument of type xxx is not assignable to parameter of type 'DateTimeFormatOptions' + 2538, //Ignore symbols as index property error. 7043, //i forget what this one is, 80001, //Convert to ES6 module 80004, //JSDoc types may be moved to TypeScript types. diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/editors/envVarList.js b/packages/node_modules/@node-red/editor-client/src/js/ui/editors/envVarList.js index dda5d1660..b974fbc50 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/editors/envVarList.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/editors/envVarList.js @@ -131,7 +131,7 @@ RED.editor.envVarList = (function() { nameField.trigger('change'); } }, - sortable: ".red-ui-editableList-item-handle", + sortable: true, removable: false }); var parentEnv = {}; diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/editors/markdown.js b/packages/node_modules/@node-red/editor-client/src/js/ui/editors/markdown.js index c4d7bf26d..217257cad 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/editors/markdown.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/editors/markdown.js @@ -27,6 +27,12 @@ reader.readAsDataURL(file); } + function file2Text(file,cb) { + file.arrayBuffer().then(d => { + cb( new TextDecoder().decode(d) ) + }).catch(ex => { cb(`error: ${ex}`) }) + } + var initialized = false; var currentEditor = null; /** @@ -35,16 +41,22 @@ function initImageDrag(elem, editor) { $(elem).on("dragenter", function (ev) { ev.preventDefault(); - $("#red-ui-image-drop-target").css({display:'table'}).focus(); + $("#red-ui-drop-target-markdown-editor").css({ + display:'table', + top: $(elem).offset().top, + left: $(elem).offset().left, + width: $(elem).width(), + height: $(elem).height() + }).focus(); currentEditor = editor; }); if (!initialized) { initialized = true; - $("#red-ui-image-drop-target").on("dragover", function (ev) { + $("#red-ui-drop-target-markdown-editor").on("dragover", function (ev) { ev.preventDefault(); }).on("dragleave", function (ev) { - $("#red-ui-image-drop-target").hide(); + $("#red-ui-drop-target-markdown-editor").hide(); }).on("drop", function (ev) { ev.preventDefault(); if ($.inArray("Files",ev.originalEvent.dataTransfer.types) != -1) { @@ -52,20 +64,43 @@ if (files.length === 1) { var file = files[0]; var name = file.name.toLowerCase(); - + var fileType = file.type.toLowerCase(); + if (name.match(/\.(apng|avif|gif|jpeg|png|svg|webp)$/)) { file2base64Image(file, function (image) { var session = currentEditor.getSession(); var img = `\n`; var pos = session.getCursorPosition(); session.insert(pos, img); - $("#red-ui-image-drop-target").hide(); + $("#red-ui-drop-target-markdown-editor").hide(); }); return; } + + if ( fileType.startsWith("text/") ) { + file2Text(file, function (txt) { + var session = currentEditor.getSession(); + var pos = session.getCursorPosition(); + session.insert(pos, txt); + $("#red-ui-drop-target-markdown-editor").hide(); + }); + return; + } } + } else if ($.inArray("text/plain", ev.originalEvent.dataTransfer.types) != -1) { + let item = Object.values(ev.originalEvent.dataTransfer.items).filter(d => d.type == "text/plain")[0] + + if (item) { + item.getAsString(txt => { + var session = currentEditor.getSession(); + var pos = session.getCursorPosition(); + session.insert(pos, txt); + $("#red-ui-drop-target-markdown-editor").hide(); + }) + return + } } - $("#red-ui-image-drop-target").hide(); + $("#red-ui-drop-target-markdown-editor").hide(); }); } } diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/editors/panes/envVarProperties.js b/packages/node_modules/@node-red/editor-client/src/js/ui/editors/panes/envVarProperties.js index b004662be..0c7694126 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/editors/panes/envVarProperties.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/editors/panes/envVarProperties.js @@ -20,10 +20,31 @@ apply: function(editState) { var old_env = node.env; var new_env = []; + if (/^subflow:/.test(node.type)) { + // Get the list of environment variables from the node properties new_env = RED.subflow.exportSubflowInstanceEnv(node); } + if (old_env && old_env.length) { + old_env.forEach(function (prop) { + if (prop.type === "conf-type" && prop.value) { + const stillInUse = new_env?.some((p) => p.type === "conf-type" && p.name === prop.name && p.value === prop.value); + if (!stillInUse) { + // Remove the node from the config node users + // Only for empty value or modified + const configNode = RED.nodes.node(prop.value); + if (configNode) { + if (configNode.users.indexOf(node) !== -1) { + configNode.users.splice(configNode.users.indexOf(node), 1); + RED.events.emit('nodes:change', configNode) + } + } + } + } + }); + } + // Get the values from the Properties table tab var items = this.list.editableList('items'); items.each(function (i,el) { @@ -41,7 +62,6 @@ } }); - if (new_env && new_env.length > 0) { new_env.forEach(function(prop) { if (prop.type === "cred") { @@ -52,6 +72,15 @@ editState.changed = true; } delete prop.value; + } else if (prop.type === "conf-type" && prop.value) { + const configNode = RED.nodes.node(prop.value); + if (configNode) { + if (configNode.users.indexOf(node) === -1) { + // Add the node to the config node users + configNode.users.push(node); + RED.events.emit('nodes:change', configNode); + } + } } }); } diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/editors/panes/properties.js b/packages/node_modules/@node-red/editor-client/src/js/ui/editors/panes/properties.js index cfa72be10..5a08a68dc 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/editors/panes/properties.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/editors/panes/properties.js @@ -26,6 +26,8 @@ if (node._def.category === "config" && nodeType !== "group") { this.inputClass = "node-config-input"; formStyle = "node-config-dialog-edit-form"; + } else if (node.type === "subflow") { + this.inputClass = "subflow-input"; } RED.editor.buildEditForm(container,formStyle,nodeType,i18nNamespace,node); }, @@ -44,6 +46,7 @@ apply: function(editState) { var newValue; var d; + // If the node is a subflow, the node's properties (exepts name) are saved by `envProperties` if (node._def.defaults) { for (d in node._def.defaults) { if (node._def.defaults.hasOwnProperty(d)) { @@ -131,9 +134,16 @@ } } if (node._def.credentials) { - var credDefinition = node._def.credentials; - var credsChanged = updateNodeCredentials(node,credDefinition,this.inputClass); - editState.changed = editState.changed || credsChanged; + const credDefinition = node._def.credentials; + const credChanges = updateNodeCredentials(node, credDefinition, this.inputClass); + + if (Object.keys(credChanges).length) { + editState.changed = true; + editState.changes.credentials = { + ...(editState.changes.credentials || {}), + ...credChanges + }; + } } } } @@ -161,10 +171,11 @@ * @param node - the node containing the credentials * @param credDefinition - definition of the credentials * @param prefix - prefix of the input fields - * @return {boolean} whether anything has changed + * @return {object} an object containing the modified properties */ function updateNodeCredentials(node, credDefinition, prefix) { - var changed = false; + const changes = {}; + if (!node.credentials) { node.credentials = {_:{}}; } else if (!node.credentials._) { @@ -177,22 +188,33 @@ if (input.length > 0) { var value = input.val(); if (credDefinition[cred].type == 'password') { - node.credentials['has_' + cred] = (value !== ""); - if (value == '__PWRD__') { - continue; + if (value === '__PWRD__') { + // A cred value exists - no changes + } else if (value === '' && node.credentials['has_' + cred] === false) { + // Empty cred value exists - no changes + } else if (value === node.credentials[cred]) { + // A cred value exists locally in the editor - no changes + // Like the user sets a value, saves the config, + // reopens the config and save the config again + } else { + changes['has_' + cred] = node.credentials['has_' + cred]; + changes[cred] = node.credentials[cred]; + node.credentials[cred] = value; } - changed = true; - } - node.credentials[cred] = value; - if (value != node.credentials._[cred]) { - changed = true; + node.credentials['has_' + cred] = (value !== ''); + } else { + // Since these creds are loaded by the editor, + // values can be directly compared + if (value !== node.credentials[cred]) { + changes[cred] = node.credentials[cred]; + node.credentials[cred] = value; + } } } } } - return changed; + + return changes; } - - })(); diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/library.js b/packages/node_modules/@node-red/editor-client/src/js/ui/library.js index d276b1572..64b1f1547 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/library.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/library.js @@ -245,10 +245,15 @@ RED.library = (function() { if (lib.types && lib.types.indexOf(options.url) === -1) { return; } + let icon = 'fa fa-hdd-o'; + if (lib.icon) { + const fullIcon = RED.utils.separateIconPath(lib.icon); + icon = (fullIcon.module==="font-awesome"?"fa ":"")+fullIcon.file; + } listing.push({ library: lib.id, type: options.url, - icon: lib.icon || 'fa fa-hdd-o', + icon, label: RED._(lib.label||lib.id), path: "", expanded: true, @@ -303,10 +308,15 @@ RED.library = (function() { if (lib.types && lib.types.indexOf(options.url) === -1) { return; } + let icon = 'fa fa-hdd-o'; + if (lib.icon) { + const fullIcon = RED.utils.separateIconPath(lib.icon); + icon = (fullIcon.module==="font-awesome"?"fa ":"")+fullIcon.file; + } listing.push({ library: lib.id, type: options.url, - icon: lib.icon || 'fa fa-hdd-o', + icon, label: RED._(lib.label||lib.id), path: "", expanded: true, @@ -839,10 +849,10 @@ RED.library = (function() { if (file && file.label && !file.children) { $.get("library/"+file.library+"/"+file.type+"/"+file.path, function(data) { //TODO: nls + sanitize - var propRow = $('Type').appendTo(table); + var propRow = $(''+RED._("library.type")+'').appendTo(table); $(propRow.children()[1]).text(activeLibrary.type); if (file.props.hasOwnProperty('name')) { - propRow = $('Name'+file.props.name+'').appendTo(table); + propRow = $(''+RED._("library.name")+''+file.props.name+'').appendTo(table); $(propRow.children()[1]).text(file.props.name); } for (var p in file.props) { diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/palette.js b/packages/node_modules/@node-red/editor-client/src/js/ui/palette.js index 4437f6c40..be17776fa 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/palette.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/palette.js @@ -562,7 +562,7 @@ RED.palette = (function() { } } - paletteNode.css("backgroundColor", sf.color); + paletteNode.css("backgroundColor", RED.utils.getNodeColor("subflow", sf._def)); } function refreshFilter() { diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/projects/projectSettings.js b/packages/node_modules/@node-red/editor-client/src/js/ui/projects/projectSettings.js index c5dbfbdbf..b682a5f60 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/projects/projectSettings.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/projects/projectSettings.js @@ -308,7 +308,7 @@ RED.projects.settings = (function() { if (activeProject.dependencies) { for (var m in activeProject.dependencies) { if (activeProject.dependencies.hasOwnProperty(m)) { - var installed = !!RED.nodes.registry.getModule(m) && activeProject.dependencies[m] === modulesInUse[m].version; + var installed = !!RED.nodes.registry.getModule(m) && activeProject.dependencies[m] === modulesInUse[m]?.version; depsList.editableList('addItem',{ id: m, version: activeProject.dependencies[m], //RED.nodes.registry.getModule(module).version, @@ -1256,7 +1256,7 @@ RED.projects.settings = (function() { notification.close(); } },{ - text: 'Delete branch', + text: RED._("sidebar.project.projectSettings.deleteBranch"), click: function() { notification.close(); var url = "projects/"+activeProject.name+"/branches/"+entry.name; diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/subflow.js b/packages/node_modules/@node-red/editor-client/src/js/ui/subflow.js index 751bf9cfb..3e1b9a410 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/subflow.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/subflow.js @@ -1362,7 +1362,7 @@ RED.subflow = (function() { item.value = ""+input.prop("checked"); break; case "conf-types": - item.value = input.val() + item.value = input.val() === "_ADD_" ? "" : input.val(); item.type = "conf-type" } if (ui.type === "cred" || item.type !== data.parent.type || item.value !== data.parent.value) { diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/tab-config.js b/packages/node_modules/@node-red/editor-client/src/js/ui/tab-config.js index b8e3aa0ba..9af711e09 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/tab-config.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/tab-config.js @@ -52,11 +52,21 @@ RED.sidebar.config = (function() { if (label) { lockIcon = $('').appendTo(header) lockIcon.toggle(!!isLocked) + $('').appendTo(header) $('').text(label).appendTo(header); } else { $('').appendTo(header); } + $('').appendTo(header); + + const changeBadgeContainer = $('').appendTo(header); + const changeBadge = document.createElementNS("http://www.w3.org/2000/svg", "circle"); + changeBadge.setAttribute("cx", "5"); + changeBadge.setAttribute("cy", "5"); + changeBadge.setAttribute("r", "5"); + changeBadgeContainer.append(changeBadge); + category = $('
      ').appendTo(container); category.on("click", function(e) { $(content).find(".red-ui-palette-node").removeClass("selected"); @@ -150,9 +160,6 @@ RED.sidebar.config = (function() { $('
    • '+node.type+'
    • ').appendTo(list); currentType = node.type; } - if (node.changed) { - labelText += "!!" - } var entry = $('
    • ').appendTo(list); var nodeDiv = $('
      ').appendTo(entry); entry.data('node',node.id); @@ -181,15 +188,29 @@ RED.sidebar.config = (function() { } } + if (node.changed) { + const nodeDivAnnotations = $('').appendTo(nodeDiv); + const changeBadge = document.createElementNS("http://www.w3.org/2000/svg", "circle"); + changeBadge.setAttribute("cx", "5"); + changeBadge.setAttribute("cy", "5"); + changeBadge.setAttribute("r", "5"); + nodeDivAnnotations.append($(changeBadge)); + + const categoryHeader = list.parent().find(".red-ui-sidebar-config-tray-header.red-ui-palette-header"); + categoryHeader.addClass("red-ui-sidebar-config-changed"); + nodeDiv.addClass("red-ui-palette-node-config-changed"); + } + if (!node.valid) { - nodeDiv.addClass("red-ui-palette-node-config-invalid") - const nodeDivAnnotations = $('').appendTo(nodeDiv) - const errorBadge = document.createElementNS("http://www.w3.org/2000/svg","path"); - errorBadge.setAttribute("d","M 0,9 l 10,0 -5,-8 z"); - nodeDivAnnotations.append($(errorBadge)) + const nodeDivAnnotations = $('').appendTo(nodeDiv); + const errorBadge = document.createElementNS("http://www.w3.org/2000/svg", "path"); + errorBadge.setAttribute("d", "M 0,9 l 10,0 -5,-8 z"); + nodeDivAnnotations.append($(errorBadge)); + + nodeDiv.addClass("red-ui-palette-node-config-invalid"); RED.popover.tooltip(nodeDivAnnotations, function () { if (node.validationErrors && node.validationErrors.length > 0) { - return RED._("editor.errors.invalidProperties")+"
      - "+node.validationErrors.join("
      - ") + return RED._("editor.errors.invalidProperties") + "
      - " + node.validationErrors.join("
      - "); } }) } @@ -251,7 +272,13 @@ RED.sidebar.config = (function() { if (!validList[id]) { $(this).remove(); delete categories[id]; + } else if (RED.nodes.workspace(id)) { + $(this).toggleClass("red-ui-sidebar-config-category-disabled", RED.nodes.workspace(id).disabled); } + + // Remove the `changed` badge from the category header + const categoryHeader = $(this).find(".red-ui-sidebar-config-tray-header.red-ui-palette-header"); + categoryHeader.removeClass("red-ui-sidebar-config-changed"); }) var globalConfigNodes = []; var configList = {}; diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/tab-context.js b/packages/node_modules/@node-red/editor-client/src/js/ui/tab-context.js index 6cb034ccc..2c56786c8 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/tab-context.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/tab-context.js @@ -18,8 +18,6 @@ RED.sidebar.context = (function() { var content; var sections; - var localCache = {}; - var flowAutoRefresh; var nodeAutoRefresh; var nodeSection; @@ -27,6 +25,8 @@ RED.sidebar.context = (function() { var flowSection; var globalSection; + const expandedPaths = {} + var currentNode; var currentFlow; @@ -212,14 +212,41 @@ RED.sidebar.context = (function() { var l = keys.length; for (var i = 0; i < l; i++) { sortedData[keys[i]].forEach(function(v) { - var k = keys[i]; - var l2 = sortedData[k].length; - var propRow = $('').appendTo(container); - var obj = $(propRow.children()[0]); + const k = keys[i]; + let payload = v.msg; + let format = v.format; + const tools = $(''); + expandedPaths[id + "." + k] = expandedPaths[id + "." + k] || new Set() + const objectElementOptions = { + typeHint: format, + sourceId: id + "." + k, + tools, + path: k, + rootPath: k, + exposeApi: true, + ontoggle: function(path,state) { + path = path.substring(k.length+1) + if (state) { + expandedPaths[id+"."+k].add(path) + } else { + // if 'a' has been collapsed, we want to remove 'a.b' and 'a[0]...' from the set + // of collapsed paths + for (let expandedPath of expandedPaths[id+"."+k]) { + if (expandedPath.startsWith(path+".") || expandedPath.startsWith(path+"[")) { + expandedPaths[id+"."+k].delete(expandedPath) + } + } + expandedPaths[id+"."+k].delete(path) + } + }, + expandPaths: [ ...expandedPaths[id+"."+k] ].sort(), + expandLeafNodes: true + } + const propRow = $('').appendTo(container); + const obj = $(propRow.children()[0]); obj.text(k); - var tools = $(''); const urlSafeK = encodeURIComponent(k) - var refreshItem = $('').appendTo(tools).on("click", function(e) { + const refreshItem = $('').appendTo(tools).on("click", function(e) { e.preventDefault(); e.stopPropagation(); $.getJSON(baseUrl+"/"+urlSafeK+"?store="+v.store, function(data) { @@ -229,16 +256,14 @@ RED.sidebar.context = (function() { tools.detach(); $(propRow.children()[1]).empty(); RED.utils.createObjectElement(RED.utils.decodeObject(payload,format), { + ...objectElementOptions, typeHint: data.format, - sourceId: id+"."+k, - tools: tools, - path: k }).appendTo(propRow.children()[1]); } }) }); RED.popover.tooltip(refreshItem,RED._("sidebar.context.refrsh")); - var deleteItem = $('').appendTo(tools).on("click", function(e) { + const deleteItem = $('').appendTo(tools).on("click", function(e) { e.preventDefault(); e.stopPropagation(); var popover = RED.popover.create({ @@ -246,7 +271,7 @@ RED.sidebar.context = (function() { target: propRow, direction: "left", content: function() { - var content = $('
      '); + const content = $('
      '); $('

      ').appendTo(content); var row = $('

      ').appendTo(content); var bg = $('').appendTo(row); @@ -269,16 +294,15 @@ RED.sidebar.context = (function() { if (container.children().length === 0) { $('').appendTo(container).i18n(); } + delete expandedPaths[id + "." + k] } else { payload = data.msg; format = data.format; tools.detach(); $(propRow.children()[1]).empty(); RED.utils.createObjectElement(RED.utils.decodeObject(payload,format), { - typeHint: data.format, - sourceId: id+"."+k, - tools: tools, - path: k + ...objectElementOptions, + typeHint: data.format }).appendTo(propRow.children()[1]); } }); @@ -293,14 +317,7 @@ RED.sidebar.context = (function() { }); RED.popover.tooltip(deleteItem,RED._("sidebar.context.delete")); - var payload = v.msg; - var format = v.format; - RED.utils.createObjectElement(RED.utils.decodeObject(payload,format), { - typeHint: v.format, - sourceId: id+"."+k, - tools: tools, - path: k - }).appendTo(propRow.children()[1]); + RED.utils.createObjectElement(RED.utils.decodeObject(payload,format), objectElementOptions).appendTo(propRow.children()[1]); if (contextStores.length > 1) { $("",{class:"red-ui-sidebar-context-property-storename"}).text(v.store).appendTo($(propRow.children()[0])) } diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/tab-info.js b/packages/node_modules/@node-red/editor-client/src/js/ui/tab-info.js index f72a7b3f2..fa9b98322 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/tab-info.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/tab-info.js @@ -204,7 +204,7 @@ RED.sidebar.info = (function() { propertiesPanelHeaderIcon.empty(); RED.utils.createNodeIcon({type:"_selection_"}).appendTo(propertiesPanelHeaderIcon); - propertiesPanelHeaderLabel.text("Selection"); + propertiesPanelHeaderLabel.text(RED._("sidebar.info.selection")); propertiesPanelHeaderReveal.hide(); propertiesPanelHeaderHelp.hide(); propertiesPanelHeaderCopyLink.hide(); diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/typeSearch.js b/packages/node_modules/@node-red/editor-client/src/js/ui/typeSearch.js index 3d05fd1c8..f284f2464 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/typeSearch.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/typeSearch.js @@ -279,6 +279,11 @@ RED.typeSearch = (function() { if ($("#red-ui-main-container").height() - opts.y - 195 < 0) { opts.y = opts.y - 275; } + const dialogWidth = dialog.width() || 300 // default is 300 (defined in class .red-ui-search) + const workspaceWidth = $('#red-ui-workspace').width() + if (workspaceWidth > dialogWidth && workspaceWidth - opts.x - dialogWidth < 0) { + opts.x = opts.x - (dialogWidth - RED.view.node_width) + } dialog.css({left:opts.x+"px",top:opts.y+"px"}).show(); searchResultsDiv.slideDown(300); setTimeout(function() { @@ -330,13 +335,25 @@ RED.typeSearch = (function() { } } function applyFilter(filter,type,def) { - return !def || !filter || - ( - (!filter.spliceMultiple) && - (!filter.type || type === filter.type) && - (!filter.input || type === 'junction' || def.inputs > 0) && - (!filter.output || type === 'junction' || def.outputs > 0) - ) + if (!filter) { + // No filter; allow everything + return true + } + if (type === 'junction') { + // Only allow Junction is there's no specific type filter + return !filter.type + } + if (filter.type) { + // Handle explicit type filter + return filter.type === type + } + if (!def) { + // No node definition available - allow it + return true + } + // Check if the filter is for input/outputs and apply + return (!filter.input || def.inputs > 0) && + (!filter.output || def.outputs > 0) } function refreshTypeList(opts) { var i; @@ -365,7 +382,7 @@ RED.typeSearch = (function() { var items = []; RED.nodes.registry.getNodeTypes().forEach(function(t) { var def = RED.nodes.getType(t); - if (def.category !== 'config' && t !== 'unknown' && t !== 'tab') { + if (def.set?.enabled !== false && def.category !== 'config' && t !== 'unknown' && t !== 'tab') { items.push({type:t,def: def, label:getTypeLabel(t,def)}); } }); diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/utils.js b/packages/node_modules/@node-red/editor-client/src/js/ui/utils.js index cfc7f65de..4529a6923 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/utils.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/utils.js @@ -121,7 +121,7 @@ RED.utils = (function() { function renderMarkdown(txt) { var rendered = _marked.parse(txt); - var cleaned = DOMPurify.sanitize(rendered, {SAFE_FOR_JQUERY: true}) + const cleaned = DOMPurify.sanitize(rendered); return cleaned; } @@ -230,7 +230,7 @@ RED.utils = (function() { var pinnedPaths = {}; var formattedPaths = {}; - function addMessageControls(obj,sourceId,key,msg,rootPath,strippedKey,extraTools) { + function addMessageControls(obj,sourceId,key,msg,rootPath,strippedKey,extraTools,enablePinning) { if (!pinnedPaths.hasOwnProperty(sourceId)) { pinnedPaths[sourceId] = {} } @@ -250,7 +250,7 @@ RED.utils = (function() { RED.clipboard.copyText(msg,copyPayload,"clipboard.copyMessageValue"); }) RED.popover.tooltip(copyPayload,RED._("node-red:debug.sidebar.copyPayload")); - if (strippedKey !== undefined && strippedKey !== '') { + if (enablePinning && strippedKey !== undefined && strippedKey !== '') { var isPinned = pinnedPaths[sourceId].hasOwnProperty(strippedKey); var pinPath = $('').appendTo(tools).on("click", function(e) { @@ -281,13 +281,16 @@ RED.utils = (function() { } } } - function checkExpanded(strippedKey,expandPaths,minRange,maxRange) { + function checkExpanded(strippedKey, expandPaths, { minRange, maxRange, expandLeafNodes }) { if (expandPaths && expandPaths.length > 0) { if (strippedKey === '' && minRange === undefined) { return true; } for (var i=0;i').appendTo(element); if (sourceId) { - addMessageControls(header,sourceId,path,obj,rootPath,strippedKey,tools); + addMessageControls(header,sourceId,path,obj,rootPath,strippedKey,tools, enablePinning); } if (!key) { element.addClass("red-ui-debug-msg-top-level"); - if (sourceId) { + if (sourceId && !expandPaths) { var pinned = pinnedPaths[sourceId]; expandPaths = []; if (pinned) { @@ -476,7 +481,7 @@ RED.utils = (function() { $('').text(typeHint||'string').appendTo(header); var row = $('

      ').appendTo(element); $('
      ').text(obj).appendTo(row);
      -                },function(state) {if (ontoggle) { ontoggle(path,state);}}, checkExpanded(strippedKey,expandPaths));
      +                },function(state) {if (ontoggle) { ontoggle(path,state);}}, checkExpanded(strippedKey, expandPaths, { expandLeafNodes }));
                   }
                   e = $('').html('"'+formatString(sanitize(obj))+'"').appendTo(entryObj);
                   if (/^#[0-9a-f]{6}$/i.test(obj)) {
      @@ -592,14 +597,16 @@ RED.utils = (function() {
                                           typeHint: type==='buffer'?'hex':false,
                                           hideKey: false,
                                           path: path+"["+i+"]",
      -                                    sourceId: sourceId,
      -                                    rootPath: rootPath,
      -                                    expandPaths: expandPaths,
      -                                    ontoggle: ontoggle,
      -                                    exposeApi: exposeApi,
      +                                    sourceId,
      +                                    rootPath,
      +                                    expandPaths,
      +                                    expandLeafNodes,
      +                                    ontoggle,
      +                                    exposeApi,
                                           // tools: tools // Do not pass tools down as we
                                                           // keep them attached to the top-level header
                                           nodeSelector: options.nodeSelector,
      +                                    enablePinning
                                       }
                                   ).appendTo(row);
                               }
      @@ -623,21 +630,23 @@ RED.utils = (function() {
                                                       typeHint: type==='buffer'?'hex':false,
                                                       hideKey: false,
                                                       path: path+"["+i+"]",
      -                                                sourceId: sourceId,
      -                                                rootPath: rootPath,
      -                                                expandPaths: expandPaths,
      -                                                ontoggle: ontoggle,
      -                                                exposeApi: exposeApi,
      +                                                sourceId,
      +                                                rootPath,
      +                                                expandPaths,
      +                                                expandLeafNodes,
      +                                                ontoggle,
      +                                                exposeApi,
                                                       // tools: tools // Do not pass tools down as we
                                                                       // keep them attached to the top-level header
                                                       nodeSelector: options.nodeSelector,
      +                                                enablePinning
                                                   }
                                               ).appendTo(row);
                                           }
                                       }
                                   })(),
                                   (function() { var path = path+"["+i+"]"; return function(state) {if (ontoggle) { ontoggle(path,state);}}})(),
      -                            checkExpanded(strippedKey,expandPaths,minRange,Math.min(fullLength-1,(minRange+9))));
      +                            checkExpanded(strippedKey,expandPaths,{ minRange, maxRange: Math.min(fullLength-1,(minRange+9)), expandLeafNodes}));
                                   $('').html("["+minRange+" … "+Math.min(fullLength-1,(minRange+9))+"]").appendTo(header);
                               }
                               if (fullLength < originalLength) {
      @@ -646,7 +655,7 @@ RED.utils = (function() {
                           }
                       },
                       function(state) {if (ontoggle) { ontoggle(path,state);}},
      -                checkExpanded(strippedKey,expandPaths));
      +                checkExpanded(strippedKey, expandPaths, { expandLeafNodes }));
                   }
               } else if (typeof obj === 'object') {
                   element.addClass('collapsed');
      @@ -680,14 +689,16 @@ RED.utils = (function() {
                                       typeHint: false,
                                       hideKey: false,
                                       path: newPath,
      -                                sourceId: sourceId,
      -                                rootPath: rootPath,
      -                                expandPaths: expandPaths,
      -                                ontoggle: ontoggle,
      -                                exposeApi: exposeApi,
      +                                sourceId,
      +                                rootPath,
      +                                expandPaths,
      +                                expandLeafNodes,
      +                                ontoggle,
      +                                exposeApi,
                                       // tools: tools // Do not pass tools down as we
                                                       // keep them attached to the top-level header
                                       nodeSelector: options.nodeSelector,
      +                                enablePinning
                                   }
                               ).appendTo(row);
                           }
      @@ -696,7 +707,7 @@ RED.utils = (function() {
                           }
                       },
                       function(state) {if (ontoggle) { ontoggle(path,state);}},
      -                checkExpanded(strippedKey,expandPaths));
      +                checkExpanded(strippedKey, expandPaths, { expandLeafNodes }));
                   }
                   if (key) {
                       $('').text(type).appendTo(entryObj);
      @@ -1274,7 +1285,6 @@ RED.utils = (function() {
                   payload = JSON.parse(payload);
               } else if (/error/i.test(format)) {
                   payload = JSON.parse(payload);
      -            payload = (payload.name?payload.name+": ":"")+payload.message;
               } else if (format === 'null') {
                   payload = null;
               } else if (format === 'undefined') {
      diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/view-annotations.js b/packages/node_modules/@node-red/editor-client/src/js/ui/view-annotations.js
      index b08df80ae..f4cda8d2c 100644
      --- a/packages/node_modules/@node-red/editor-client/src/js/ui/view-annotations.js
      +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/view-annotations.js
      @@ -11,7 +11,7 @@ RED.view.annotations = (function() {
                       }
                       let badgeRDX = 0;
                       let badgeLDX = 0;
      -                
      +                const scale = RED.view.scale()
                       for (let i=0,l=evt.el.__annotations__.length;i {
      -                            let match = defaultNodeNameRE.exec(n.name)
      +                        const existingNodes = RED.nodes.filterNodes({ type: n.type });
      +                        const existingIds = existingNodes.reduce((ids, node) => {
      +                            let match = defaultNodeNameRE.exec(node.name);
                                   if (match) {
      -                                let nodeNumber = parseInt(match[1])
      -                                if (nodeNumber > maxNameNumber) {
      -                                    maxNameNumber = nodeNumber
      +                                const nodeNumber = parseInt(match[1], 10);
      +                                if (!ids.includes(nodeNumber)) {
      +                                    ids.push(nodeNumber);
                                       }
                                   }
      -                        })
      -                        typeIndex[n.type] = maxNameNumber + 1
      +                            return ids;
      +                        }, []).sort((a, b) => a - b);
      +
      +                        let availableNameNumber = 1;
      +                        for (let i = 0; i < existingIds.length; i++) {
      +                            if (existingIds[i] !== availableNameNumber) {
      +                                break;
      +                            }
      +                            availableNameNumber++;
      +                        }
      +
      +                        typeIndex[n.type] = availableNameNumber;
                           }
                           if ((options.renameBlank && n.name === '') || (options.renameClash && defaultNodeNameRE.test(n.name))) {
                               if (generateHistory) {
      @@ -1145,11 +1154,11 @@ RED.view.tools = (function() {
               }
           }
       
      -    function addJunctionsToWires(wires) {
      +    function addJunctionsToWires(options = {}) {
               if (RED.workspaces.isLocked()) {
                   return
               }
      -        let wiresToSplit = wires || (RED.view.selection().links && RED.view.selection().links.filter(e => !e.link));
      +        let wiresToSplit = options.wires || (RED.view.selection().links && RED.view.selection().links.filter(e => !e.link));
               if (!wiresToSplit) {
                   return
               }
      @@ -1197,21 +1206,26 @@ RED.view.tools = (function() {
                   if (links.length === 0) {
                       return
                   }
      -            let pointCount = 0
      -            links.forEach(function(l) {
      -                if (l._sliceLocation) {
      -                    junction.x += l._sliceLocation.x
      -                    junction.y += l._sliceLocation.y
      -                    delete l._sliceLocation
      -                    pointCount++
      -                } else {
      -                    junction.x += l.source.x + l.source.w/2 + l.target.x - l.target.w/2
      -                    junction.y += l.source.y + l.target.y
      -                    pointCount += 2
      -                }
      -            })
      -            junction.x = Math.round(junction.x/pointCount)
      -            junction.y = Math.round(junction.y/pointCount)
      +            if (addedJunctions.length === 0 && Object.hasOwn(options, 'x') && Object.hasOwn(options, 'y')) {
      +                junction.x = options.x
      +                junction.y = options.y
      +            } else {
      +                let pointCount = 0
      +                links.forEach(function(l) {
      +                    if (l._sliceLocation) {
      +                        junction.x += l._sliceLocation.x
      +                        junction.y += l._sliceLocation.y
      +                        delete l._sliceLocation
      +                        pointCount++
      +                    } else {
      +                        junction.x += l.source.x + l.source.w/2 + l.target.x - l.target.w/2
      +                        junction.y += l.source.y + l.target.y
      +                        pointCount += 2
      +                    }
      +                })
      +                junction.x = Math.round(junction.x/pointCount)
      +                junction.y = Math.round(junction.y/pointCount)
      +            }
                   if (RED.view.snapGrid) {
                       let gridSize = RED.view.gridSize()
                       junction.x = (gridSize*Math.round(junction.x/gridSize));
      @@ -1401,7 +1415,7 @@ RED.view.tools = (function() {
                   RED.actions.add("core:wire-multiple-to-node", function() { wireMultipleToNode() })
       
                   RED.actions.add("core:split-wire-with-link-nodes", function () { splitWiresWithLinkNodes() });
      -            RED.actions.add("core:split-wires-with-junctions", function () { addJunctionsToWires() });
      +            RED.actions.add("core:split-wires-with-junctions", function (options) { addJunctionsToWires(options) });
       
                   RED.actions.add("core:generate-node-names", generateNodeNames )
       
      diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/view.js b/packages/node_modules/@node-red/editor-client/src/js/ui/view.js
      index e97e2e36e..198af0850 100644
      --- a/packages/node_modules/@node-red/editor-client/src/js/ui/view.js
      +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/view.js
      @@ -288,7 +288,7 @@ RED.view = (function() {
                       }
                       selectedLinks.clearUnselected()
                   },
      -            length: () => groups.length,
      +            length: () => groups.size,
                   forEach: (func) => { groups.forEach(func) },
                   toArray: () => [...groups],
                   clear: function () {
      @@ -321,8 +321,8 @@ RED.view = (function() {
                   evt.stopPropagation()
                   RED.contextMenu.show({
                       type: 'workspace',
      -                x:evt.clientX-5,
      -                y:evt.clientY-5
      +                x: evt.clientX,
      +                y: evt.clientY
                   })
                   return false
               })
      @@ -1209,7 +1209,10 @@ RED.view = (function() {
                   lasso = null;
               }
               if (d3.event.touches || d3.event.button === 0) {
      -            if ((mouse_mode === 0 || mouse_mode === RED.state.QUICK_JOINING) && isControlPressed(d3.event) && !(d3.event.altKey || d3.event.shiftKey)) {
      +            if (
      +                (mouse_mode === 0 && isControlPressed(d3.event) && !(d3.event.altKey || d3.event.shiftKey)) ||
      +                mouse_mode === RED.state.QUICK_JOINING
      +            ) {
                       // Trigger quick add dialog
                       d3.event.stopPropagation();
                       clearSelection();
      @@ -1262,11 +1265,6 @@ RED.view = (function() {
               var targetGroup = options.group;
               var touchTrigger = options.touchTrigger;
       
      -        if (targetGroup) {
      -            selectedGroups.add(targetGroup,false);
      -            RED.view.redraw();
      -        }
      -
               // `point` is the place in the workspace the mouse has clicked.
               //  This takes into account scrolling and scaling of the workspace.
               var ox = point[0];
      @@ -1285,7 +1283,6 @@ RED.view = (function() {
               }
       
               var mainPos = $("#red-ui-main-container").position();
      -
               if (mouse_mode !== RED.state.QUICK_JOINING) {
                   mouse_mode = RED.state.QUICK_JOINING;
                   $(window).on('keyup',disableQuickJoinEventHandler);
      @@ -1589,9 +1586,6 @@ RED.view = (function() {
                       // auto select dropped node - so info shows (if visible)
                       clearSelection();
                       nn.selected = true;
      -                if (targetGroup) {
      -                    selectedGroups.add(targetGroup,false);
      -                }
                       movingSet.add(nn);
                       updateActiveNodes();
                       updateSelection();
      @@ -2176,19 +2170,24 @@ RED.view = (function() {
                               n.n.moved = true;
                           }
                       }
      -
      -                // Check to see if we need to splice a link
      +                // If a node has moved and ends up being spliced into a link, keep
      +                // track of which historyEvent to add the splice info to
      +                let targetSpliceEvent = null
                       if (moveEvent.nodes.length > 0) {
                           historyEvent.events.push(moveEvent)
      -                    if (activeSpliceLink) {
      -                        var linkToSplice = d3.select(activeSpliceLink).data()[0];
      -                        spliceLink(linkToSplice, movingSet.get(0).n, moveEvent)
      -                    }
      +                    targetSpliceEvent = moveEvent
                       }
                       if (moveAndChangedGroupEvent.nodes.length > 0) {
                           historyEvent.events.push(moveAndChangedGroupEvent)
      +                    targetSpliceEvent = moveAndChangedGroupEvent
                       }
      -                
      +                // activeSpliceLink will only be set if the movingSet has a single
      +                // node that is able to splice.
      +                if (targetSpliceEvent && activeSpliceLink) {
      +                    var linkToSplice = d3.select(activeSpliceLink).data()[0];
      +                    spliceLink(linkToSplice, movingSet.get(0).n, targetSpliceEvent)
      +                }
      +
                       // Only continue if something has moved
                       if (historyEvent.events.length > 0) {
                           RED.nodes.dirty(true);
      @@ -2687,22 +2686,21 @@ RED.view = (function() {
                       addToRemovedLinks(reconnectResult.removedLinks)
                   }
       
      -            var startDirty = RED.nodes.dirty();
      -            var startChanged = false;
      -            var selectedGroups = [];
      +            const startDirty = RED.nodes.dirty();
      +            let movingSelectedGroups = [];
                   if (movingSet.length() > 0) {
       
                       for (var i=0;i=0; i--) {
      -                    var g = selectedGroups[i];
      +                for (i = movingSelectedGroups.length-1; i>=0; i--) {
      +                    var g = movingSelectedGroups[i];
                           removedGroups.push(g);
                           RED.nodes.removeGroup(g);
                       }
      @@ -3057,8 +3055,8 @@ RED.view = (function() {
           }
       
           function disableQuickJoinEventHandler(evt) {
      -        // Check for ctrl (all browsers), "Meta" (Chrome/FF), keyCode 91 (Safari)
      -        if (evt.keyCode === 17 || evt.key === "Meta" || evt.keyCode === 91) {
      +        // Check for ctrl (all browsers), "Meta" (Chrome/FF), keyCode 91 (Safari), or Escape
      +        if (evt.keyCode === 17 || evt.key === "Meta" || evt.keyCode === 91 || evt.keyCode === 27) {
                   resetMouseVars();
                   hideDragLines();
                   redraw();
      @@ -3189,27 +3187,59 @@ RED.view = (function() {
       
                   for (i=0;i id !== oldDst.id)
      +                                    dst.links = dst.links.filter(id => id !== oldDst.id)
      +                                    var oldOldDstLinks = [...oldDst.links]
      +                                    oldDst.links = oldDst.links.filter(id => id !== oldSrc.id)
      +                                    oldDst.dirty = true;
      +                                    modifiedNodes.push(oldDst);
      +                                    linkEditEvents.push({
      +                                        t:'edit',
      +                                        node: oldDst,
      +                                        dirty: RED.nodes.dirty(),
      +                                        changed: oldDst.changed,
      +                                        changes: {
      +                                            links:oldOldDstLinks
      +                                        }
      +                                    });
      +                                    oldDst.changed = true;
      +                                }
      +
                                       src.dirty = true;
                                       dst.dirty = true;
      +
                                       modifiedNodes.push(src);
                                       modifiedNodes.push(dst);
       
      @@ -3237,6 +3267,7 @@ RED.view = (function() {
                                               links:oldDstLinks
                                           }
                                       });
      +                               
                                       src.changed = true;
                                       dst.changed = true;
                                   }
      @@ -5140,8 +5171,8 @@ RED.view = (function() {
                                       var delta = Infinity;
                                       for (var i = 0; i < lineLength; i++) {
                                           var linePos = pathLine.getPointAtLength(i);
      -                                    var posDeltaX = Math.abs(linePos.x-d3.event.offsetX)
      -                                    var posDeltaY = Math.abs(linePos.y-d3.event.offsetY)
      +                                    var posDeltaX = Math.abs(linePos.x-(d3.event.offsetX / scaleFactor))
      +                                    var posDeltaY = Math.abs(linePos.y-(d3.event.offsetY / scaleFactor))
                                           var posDelta = posDeltaX*posDeltaX + posDeltaY*posDeltaY
                                           if (posDelta < delta) {
                                               pos = linePos
      diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/workspaces.js b/packages/node_modules/@node-red/editor-client/src/js/ui/workspaces.js
      index 0d7b1da54..78e1399cd 100644
      --- a/packages/node_modules/@node-red/editor-client/src/js/ui/workspaces.js
      +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/workspaces.js
      @@ -183,25 +183,29 @@ RED.workspaces = (function() {
                   },
                   null)
               }
      -        menuItems.push(
      -            {
      -                id:"red-ui-tabs-menu-option-add-flow",
      -                label: RED._("workspace.addFlow"),
      -                onselect: "core:add-flow"
      -            }
      -        )
      -        if (isMenuButton || !!tab) {
      +        if (RED.settings.theme("menu.menu-item-workspace-add", true)) {
                   menuItems.push(
                       {
      -                    id:"red-ui-tabs-menu-option-add-flow-right",
      -                    label: RED._("workspace.addFlowToRight"),
      -                    shortcut: RED.keyboard.getShortcut("core:add-flow-to-right"),
      -                    onselect: function() {
      -                        RED.actions.invoke("core:add-flow-to-right", tab)
      -                    }
      -                },
      -                null
      +                    id:"red-ui-tabs-menu-option-add-flow",
      +                    label: RED._("workspace.addFlow"),
      +                    onselect: "core:add-flow"
      +                }
                   )
      +        }
      +        if (isMenuButton || !!tab) {
      +            if (RED.settings.theme("menu.menu-item-workspace-add", true)) {
      +                menuItems.push(
      +                    {
      +                        id:"red-ui-tabs-menu-option-add-flow-right",
      +                        label: RED._("workspace.addFlowToRight"),
      +                        shortcut: RED.keyboard.getShortcut("core:add-flow-to-right"),
      +                        onselect: function() {
      +                            RED.actions.invoke("core:add-flow-to-right", tab)
      +                        }
      +                    },
      +                    null
      +                )
      +            }
                   if (activeWorkspace && activeWorkspace.type === 'tab') {
                       menuItems.push(
                           isFlowDisabled ? {
      @@ -255,7 +259,9 @@ RED.workspaces = (function() {
                       }
                   )
               }
      -        menuItems.push(null)
      +        if (menuItems.length > 0) {
      +            menuItems.push(null)
      +        }
               if (isMenuButton || !!tab) {
                   menuItems.push(
                       {
      @@ -299,19 +305,24 @@ RED.workspaces = (function() {
                   }
               )
               if (tab) {
      +            menuItems.push(null)
      +
      +            if (RED.settings.theme("menu.menu-item-workspace-delete", true)) {
      +                menuItems.push(
      +                    {
      +                        label: RED._("common.label.delete"),
      +                        onselect: function() {
      +                            if (tab.type === 'tab') {
      +                                RED.workspaces.delete(tab)
      +                            } else if (tab.type === 'subflow') {
      +                                RED.subflow.delete(tab.id)
      +                            }
      +                        },
      +                        disabled: isCurrentLocked || (workspaceTabCount === 1)
      +                    }
      +                )
      +            }
                   menuItems.push(
      -                null,
      -                {
      -                    label: RED._("common.label.delete"),
      -                    onselect: function() {
      -                        if (tab.type === 'tab') {
      -                            RED.workspaces.delete(tab)
      -                        } else if (tab.type === 'subflow') {
      -                            RED.subflow.delete(tab.id)
      -                        }
      -                    },
      -                    disabled: isCurrentLocked || (workspaceTabCount === 1)
      -                },
                       {
                           label: RED._("menu.label.export"),
                           shortcut: RED.keyboard.getShortcut("core:show-export-dialog"),
      @@ -468,7 +479,7 @@ RED.workspaces = (function() {
                   },
                   minimumActiveTabWidth: 150,
                   scrollable: true,
      -            addButton: "core:add-flow",
      +            addButton: RED.settings.theme("menu.menu-item-workspace-add", true) ? "core:add-flow" : undefined,
                   addButtonCaption: RED._("workspace.addFlow"),
                   menu: function() { return getMenuItems(true) },
                   contextmenu: function(tab) { return getMenuItems(false, tab) }
      @@ -525,19 +536,24 @@ RED.workspaces = (function() {
               $(window).on("resize", function() {
                   workspace_tabs.resize();
               });
      -
      -        RED.actions.add("core:add-flow",function(opts) { addWorkspace(undefined,undefined,opts?opts.index:undefined)});
      -        RED.actions.add("core:add-flow-to-right",function(workspace) {
      -            let index
      -            if (workspace) {
      -                index = workspace_tabs.getTabIndex(workspace.id)+1
      -            } else {
      -                index = workspace_tabs.activeIndex()+1
      -            }
      -            addWorkspace(undefined,undefined,index)
      -        });
      -        RED.actions.add("core:edit-flow",editWorkspace);
      -        RED.actions.add("core:remove-flow",removeWorkspace);
      +        if (RED.settings.theme("menu.menu-item-workspace-add", true)) {
      +            RED.actions.add("core:add-flow",function(opts) { addWorkspace(undefined,undefined,opts?opts.index:undefined)});
      +            RED.actions.add("core:add-flow-to-right",function(workspace) {
      +                let index
      +                if (workspace) {
      +                    index = workspace_tabs.getTabIndex(workspace.id)+1
      +                } else {
      +                    index = workspace_tabs.activeIndex()+1
      +                }
      +                addWorkspace(undefined,undefined,index)
      +            });
      +        }
      +        if (RED.settings.theme("menu.menu-item-workspace-edit", true)) {
      +            RED.actions.add("core:edit-flow",editWorkspace);
      +        }
      +        if (RED.settings.theme("menu.menu-item-workspace-delete", true)) {
      +            RED.actions.add("core:remove-flow",removeWorkspace);
      +        }
               RED.actions.add("core:enable-flow",enableWorkspace);
               RED.actions.add("core:disable-flow",disableWorkspace);
               RED.actions.add("core:lock-flow",lockWorkspace);
      @@ -751,6 +767,7 @@ RED.workspaces = (function() {
                   RED.history.push(historyEvent);
                   RED.events.emit("flows:change",workspace);
                   RED.nodes.dirty(true);
      +            RED.sidebar.config.refresh();
                   RED.nodes.filterNodes({z:workspace.id}).forEach(n => n.dirty = true)
                   RED.view.redraw(true);
               }
      @@ -904,6 +921,17 @@ RED.workspaces = (function() {
                   }
               },
               refresh: function() {
      +            var workspace = RED.nodes.workspace(RED.workspaces.active());
      +            if (workspace) {
      +                document.title = `${documentTitle} : ${workspace.label}`;
      +            } else {
      +                var subflow = RED.nodes.subflow(RED.workspaces.active());
      +                if (subflow) {
      +                    document.title = `${documentTitle} : ${subflow.name}`;
      +                } else {
      +                    document.title = documentTitle
      +                }
      +            }
                   RED.nodes.eachWorkspace(function(ws) {
                       workspace_tabs.renameTab(ws.id,ws.label);
                       $("#red-ui-tab-"+(ws.id.replace(".","-"))).attr("flowname",ws.label)
      diff --git a/packages/node_modules/@node-red/editor-client/src/js/user.js b/packages/node_modules/@node-red/editor-client/src/js/user.js
      index 2deda8970..e626f4ec0 100644
      --- a/packages/node_modules/@node-red/editor-client/src/js/user.js
      +++ b/packages/node_modules/@node-red/editor-client/src/js/user.js
      @@ -168,6 +168,37 @@ RED.user = (function() {
                           }
       
       
      +                } else {
      +                    if (data.prompts) {
      +                        if (data.loginMessage) {
      +                            const sessionMessages = $("
      ",{class:"form-row",style:"text-align: center"}).appendTo("#node-dialog-login-fields"); + $('
      ').text(data.loginMessage).appendTo(sessionMessages); + } + + i = 0; + for (;i",{class:"form-row",style:"text-align: center"}).appendTo("#node-dialog-login-fields"); + var loginButton = $('',{style: "padding: 10px"}).appendTo(row).on("click", function() { + document.location = field.url; + }); + if (field.image) { + $("",{src:field.image}).appendTo(loginButton); + } else if (field.label) { + var label = $('').text(field.label); + if (field.icon) { + $('',{class: "fa fa-2x "+field.icon, style:"vertical-align: middle"}).appendTo(loginButton); + label.css({ + "verticalAlign":"middle", + "marginLeft":"8px" + }); + + } + label.appendTo(loginButton); + } + loginButton.button(); + } + } } if (opts.cancelable) { $("#node-dialog-login-cancel").button().on("click", function( event ) { @@ -320,10 +351,10 @@ RED.user = (function() { userIcon.css({ backgroundImage: "url("+user.image+")", }) - } else if (user.anonymous) { + } else if (user.anonymous || (!user.username && !user.email)) { $('').appendTo(userIcon); } else { - $('').text(user.username.substring(0,2)).appendTo(userIcon); + $('').text((user.username || user.email).substring(0,2)).appendTo(userIcon); } if (user.profileColor !== undefined) { userIcon.addClass('red-ui-user-profile-color-' + user.profileColor) diff --git a/packages/node_modules/@node-red/editor-client/src/sass/dragdrop.scss b/packages/node_modules/@node-red/editor-client/src/sass/dragdrop.scss index 3d4b2253a..08402e3c5 100644 --- a/packages/node_modules/@node-red/editor-client/src/sass/dragdrop.scss +++ b/packages/node_modules/@node-red/editor-client/src/sass/dragdrop.scss @@ -38,12 +38,13 @@ } } -#red-ui-image-drop-target { +#red-ui-drop-target-markdown-editor { position: absolute; top: 0; bottom: 0; left: 0; right: 0; background: var(--red-ui-dnd-background); display:table; + border-radius: 3px; width: 100%; height: 100%; display: none; diff --git a/packages/node_modules/@node-red/editor-client/src/sass/tab-config.scss b/packages/node_modules/@node-red/editor-client/src/sass/tab-config.scss index aed01240a..f0074b1dc 100644 --- a/packages/node_modules/@node-red/editor-client/src/sass/tab-config.scss +++ b/packages/node_modules/@node-red/editor-client/src/sass/tab-config.scss @@ -14,6 +14,25 @@ * limitations under the License. **/ +.red-ui-sidebar-config-category-disabled-icon { + display: none; +} + +.red-ui-sidebar-config-category-disabled { + .red-ui-sidebar-config-tray-header { + font-style: italic; + color: var(--red-ui-tab-text-color-disabled-inactive) !important; + .red-ui-sidebar-config-category-disabled-icon { + display: inline; + } + } + .red-ui-sidebar-node-config-list { + .red-ui-palette-node-config { + @extend .red-ui-palette-node-config-disabled; + } + } +} + .red-ui-sidebar-node-config { position: relative; background: var(--red-ui-secondary-background); @@ -84,6 +103,11 @@ ul.red-ui-sidebar-node-config-list { background: var(--red-ui-node-config-background); color: var(--red-ui-primary-text-color); cursor: pointer; + &.red-ui-palette-node-config-invalid.red-ui-palette-node-config-changed { + .red-ui-palette-node-annotations.red-ui-flow-node-error { + left: calc(100% - 28px); + } + } } ul.red-ui-sidebar-node-config-list li.red-ui-palette-node-config-type { color: var(--red-ui-secondary-text-color); @@ -115,6 +139,15 @@ ul.red-ui-sidebar-node-config-list li.red-ui-palette-node-config-type { .red-ui-palette-node-config-invalid { border-color: var(--red-ui-form-input-border-error-color) } +.red-ui-sidebar-config-tray-header.red-ui-palette-header:not(.red-ui-sidebar-config-changed) .red-ui-flow-node-changed { + display: none; +} +.red-ui-sidebar-config-tray-header.red-ui-palette-header.red-ui-sidebar-config-changed .red-ui-flow-node-changed { + display: inline-block; + position: absolute; + top: 1px; + right: 1px; +} .red-ui-palette-node-annotations { position: absolute; left: calc(100% - 15px); 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 595423888..f2cb882b7 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 @@ -151,8 +151,9 @@ &.red-ui-tabs-add { padding-right: 29px; } - &.red-ui-tabs-add.red-ui-tabs-scrollable { - padding-right: 53px; + &.red-ui-tabs-add.red-ui-tabs-scrollable, + &.red-ui-tabs-menu.red-ui-tabs-scrollable { + padding-right: 53px; } &.red-ui-tabs-add.red-ui-tabs-menu.red-ui-tabs-scrollable, &.red-ui-tabs-add.red-ui-tabs-search.red-ui-tabs-scrollable { @@ -310,8 +311,9 @@ } } -.red-ui-tabs.red-ui-tabs-add .red-ui-tab-scroll-right { - right: 32px; +.red-ui-tabs.red-ui-tabs-add .red-ui-tab-scroll-right, +.red-ui-tabs.red-ui-tabs-menu .red-ui-tab-scroll-right { + right: 32px; } .red-ui-tabs.red-ui-tabs-add.red-ui-tabs-menu .red-ui-tab-scroll-right, diff --git a/packages/node_modules/@node-red/editor-client/src/sass/ui/common/autoComplete.scss b/packages/node_modules/@node-red/editor-client/src/sass/ui/common/autoComplete.scss index 0501bb6a2..85cb4f1db 100644 --- a/packages/node_modules/@node-red/editor-client/src/sass/ui/common/autoComplete.scss +++ b/packages/node_modules/@node-red/editor-client/src/sass/ui/common/autoComplete.scss @@ -2,4 +2,15 @@ &.red-ui-popover-panel { border-top: none; } + + +} +.red-ui-autoComplete-completion { + font-family: var(--red-ui-monospace-font); + white-space: nowrap; + overflow: hidden; + flex-grow: 1; + text-overflow: ellipsis; + direction: rtl; + text-align: left; } diff --git a/packages/node_modules/@node-red/editor-client/src/sass/ui/common/typedInput.scss b/packages/node_modules/@node-red/editor-client/src/sass/ui/common/typedInput.scss index 1a421fac5..12b0731d1 100644 --- a/packages/node_modules/@node-red/editor-client/src/sass/ui/common/typedInput.scss +++ b/packages/node_modules/@node-red/editor-client/src/sass/ui/common/typedInput.scss @@ -198,6 +198,7 @@ button.red-ui-typedInput-option-expand { } button.red-ui-typedInput-option-trigger { + border-left: 1px solid var(--red-ui-form-input-border-color); border-top-left-radius: 0px; border-bottom-left-radius: 0px; border-top-right-radius: 4px; diff --git a/packages/node_modules/@node-red/nodes/core/common/21-debug.js b/packages/node_modules/@node-red/nodes/core/common/21-debug.js index fe9827fce..45deb5ab3 100644 --- a/packages/node_modules/@node-red/nodes/core/common/21-debug.js +++ b/packages/node_modules/@node-red/nodes/core/common/21-debug.js @@ -148,7 +148,7 @@ module.exports = function(RED) { var st = (typeof output === 'string') ? output : util.inspect(output); var fill = "grey"; var shape = "dot"; - if (typeof output === 'object' && hasOwnProperty.call(output, "fill") && hasOwnProperty.call(output, "shape") && hasOwnProperty.call(output, "text")) { + if (typeof output === 'object' && output?.fill && output?.shape && output?.text) { fill = output.fill; shape = output.shape; st = output.text; diff --git a/packages/node_modules/@node-red/nodes/core/common/lib/debug/debug-utils.js b/packages/node_modules/@node-red/nodes/core/common/lib/debug/debug-utils.js index b244273dc..0a84b24c7 100644 --- a/packages/node_modules/@node-red/nodes/core/common/lib/debug/debug-utils.js +++ b/packages/node_modules/@node-red/nodes/core/common/lib/debug/debug-utils.js @@ -511,9 +511,10 @@ RED.debug = (function() { typeHint: format, hideKey: false, path: path, - sourceId: sourceNode&&sourceNode.id, + sourceId: sourceNode && sourceNode.id, rootPath: path, nodeSelector: config.messageSourceClick, + enablePinning: true }); // Do this in a separate step so the element functions aren't stripped debugMessage.appendTo(el); diff --git a/packages/node_modules/@node-red/nodes/core/function/10-function.html b/packages/node_modules/@node-red/nodes/core/function/10-function.html index 446ef918d..4b63373a1 100644 --- a/packages/node_modules/@node-red/nodes/core/function/10-function.html +++ b/packages/node_modules/@node-red/nodes/core/function/10-function.html @@ -197,14 +197,6 @@ // object that maps from library name to its descriptor var allLibs = []; - function moduleName(module) { - var match = /^([^@]+)@(.+)/.exec(module); - if (match) { - return [match[1], match[2]]; - } - return [module, undefined]; - } - function getAllUsedModules() { var moduleSet = new Set(); for (var id in knownFunctionNodes) { @@ -302,7 +294,7 @@ if (val === "_custom_") { val = $(this).val(); } - var varName = val.trim().replace(/^@/,"").replace(/@.*$/,"").replace(/[-_/\.].?/g, function(v) { return v[1]?v[1].toUpperCase():"" }); + var varName = val.trim().replace(/^node:/,"").replace(/^@/,"").replace(/@.*$/,"").replace(/[-_/\.].?/g, function(v) { return v[1]?v[1].toUpperCase():"" }); fvar.val(varName); fvar.trigger("change"); diff --git a/packages/node_modules/@node-red/nodes/core/function/10-function.js b/packages/node_modules/@node-red/nodes/core/function/10-function.js index 0120d8c92..71f6e15d6 100644 --- a/packages/node_modules/@node-red/nodes/core/function/10-function.js +++ b/packages/node_modules/@node-red/nodes/core/function/10-function.js @@ -111,8 +111,6 @@ module.exports = function(RED) { throw new Error(RED._("function.error.externalModuleNotAllowed")); } - - var functionText = "var results = null;"+ "results = (async function(msg,__send__,__done__){ "+ "var __msgid__ = msg._msgid;"+ @@ -166,7 +164,13 @@ module.exports = function(RED) { Buffer:Buffer, Date: Date, RED: { - util: RED.util + util: { + ...RED.util, + getSetting: function (_node, name, _flow) { + // Ensure `node` argument is the Function node and do not allow flow to be overridden. + return RED.util.getSetting(node, name); + } + } }, __node__: { id: node.id, diff --git a/packages/node_modules/@node-red/nodes/core/function/10-switch.js b/packages/node_modules/@node-red/nodes/core/function/10-switch.js index fdc345f47..9d68f6b0b 100644 --- a/packages/node_modules/@node-red/nodes/core/function/10-switch.js +++ b/packages/node_modules/@node-red/nodes/core/function/10-switch.js @@ -352,7 +352,9 @@ module.exports = function(RED) { if (msgs.length === 0) { done() } else { - drainMessageGroup(msgs,count,done); + setImmediate(() => { + drainMessageGroup(msgs,count,done); + }) } } }) @@ -505,7 +507,9 @@ module.exports = function(RED) { if (err) { node.error(err,nextMsg); } - processMessageQueue() + setImmediate(() => { + processMessageQueue() + }) }); } diff --git a/packages/node_modules/@node-red/nodes/core/function/89-delay.js b/packages/node_modules/@node-red/nodes/core/function/89-delay.js index b0fc2ed86..17cbd2f4f 100644 --- a/packages/node_modules/@node-red/nodes/core/function/89-delay.js +++ b/packages/node_modules/@node-red/nodes/core/function/89-delay.js @@ -253,7 +253,13 @@ module.exports = function(RED) { if (node.allowrate && m.hasOwnProperty("rate") && !isNaN(parseFloat(m.rate))) { node.rate = m.rate; } - send(m); + if (msg.hasOwnProperty("reset")) { + if (msg.hasOwnProperty("flush")) { + node.buffer.push({msg: m, send: send, done: done}); + } + } + else { send(m); } + node.reportDepth(); node.intervalID = setInterval(sendMsgFromBuffer, node.rate); done(); @@ -285,42 +291,23 @@ module.exports = function(RED) { } } else if (!msg.hasOwnProperty("reset")) { - if (maxKeptMsgsCount(node) > 0) { - if (node.intervalID === -1) { - node.send(msg); - node.intervalID = setInterval(sendMsgFromBuffer, node.rate); - } else { - if (node.allowrate && msg.hasOwnProperty("rate") && !isNaN(parseFloat(msg.rate)) && node.rate !== msg.rate) { - node.rate = msg.rate; - clearInterval(node.intervalID); - node.intervalID = setInterval(sendMsgFromBuffer, node.rate); - } - if (node.buffer.length < _maxKeptMsgsCount) { - var m = RED.util.cloneMessage(msg); - node.buffer.push({msg: m, send: send, done: done}); - } else { - node.trace("dropped due to buffer overflow. msg._msgid = " + msg._msgid); - node.droppedMsgs++; - } - } - } else { - if (node.allowrate && msg.hasOwnProperty("rate") && !isNaN(parseFloat(msg.rate))) { - node.rate = msg.rate; - } - var timeSinceLast; - if (node.lastSent) { - timeSinceLast = process.hrtime(node.lastSent); - } - if (!node.lastSent) { // ensuring that we always send the first message - node.lastSent = process.hrtime(); - send(msg); - } - else if ( ( (timeSinceLast[0] * SECONDS_TO_NANOS) + timeSinceLast[1] ) > (node.rate * MILLIS_TO_NANOS) ) { - node.lastSent = process.hrtime(); - send(msg); - } else if (node.outputs === 2) { - send([null,msg]) - } + if (node.allowrate && msg.hasOwnProperty("rate") && !isNaN(parseFloat(msg.rate))) { + node.rate = msg.rate; + } + var timeSinceLast; + if (node.lastSent) { + timeSinceLast = process.hrtime(node.lastSent); + } + if (!node.lastSent) { // ensuring that we always send the first message + node.lastSent = process.hrtime(); + send(msg); + } + else if ( ( (timeSinceLast[0] * SECONDS_TO_NANOS) + timeSinceLast[1] ) > (node.rate * MILLIS_TO_NANOS) ) { + node.lastSent = process.hrtime(); + send(msg); + } + else if (node.outputs === 2) { + send([null,msg]) } done(); } diff --git a/packages/node_modules/@node-red/nodes/core/function/89-trigger.js b/packages/node_modules/@node-red/nodes/core/function/89-trigger.js index 16a00e99d..8b3625f2d 100644 --- a/packages/node_modules/@node-red/nodes/core/function/89-trigger.js +++ b/packages/node_modules/@node-red/nodes/core/function/89-trigger.js @@ -24,6 +24,14 @@ module.exports = function(RED) { this.op2 = n.op2 || "0"; this.op1type = n.op1type || "str"; this.op2type = n.op2type || "str"; + // If the op1/2type is 'date', then we need to leave op1/2 alone so that + // evaluateNodeProperty works as expected. + if (this.op1type === 'date' && this.op1 === '1') { + this.op1 = '' + } + if (this.op2type === 'date' && this.op2 === '0') { + this.op2 = '' + } this.second = (n.outputs == 2) ? true : false; this.topic = n.topic || "topic"; @@ -193,7 +201,7 @@ module.exports = function(RED) { if (node.op2type !== "nul") { var promise = Promise.resolve(); msg2 = RED.util.cloneMessage(msg); - if (node.op2type === "flow" || node.op2type === "global") { + if (node.op2type === "flow" || node.op2type === "global" || node.op2type === "date") { promise = new Promise((resolve,reject) => { RED.util.evaluateNodeProperty(node.op2,node.op2type,node,msg,(err,value) => { if (err) { @@ -213,7 +221,6 @@ module.exports = function(RED) { } else { msg2.payload = node.topics[topic].m2; - if (node.op2type === "date") { msg2.payload = Date.now(); } if (node.second === true) { msgInfo.send([null,msg2]); } else { msgInfo.send(msg2); } } diff --git a/packages/node_modules/@node-red/nodes/core/network/05-tls.js b/packages/node_modules/@node-red/nodes/core/network/05-tls.js index 888d749fd..f47067729 100644 --- a/packages/node_modules/@node-red/nodes/core/network/05-tls.js +++ b/packages/node_modules/@node-red/nodes/core/network/05-tls.js @@ -104,14 +104,14 @@ module.exports = function(RED) { if (this.credentials && this.credentials.passphrase) { opts.passphrase = this.credentials.passphrase; } - if (this.servername) { - opts.servername = this.servername; - } - if (this.alpnprotocol) { - opts.ALPNProtocols = [this.alpnprotocol]; - } - opts.rejectUnauthorized = this.verifyservercert; } + if (this.servername) { + opts.servername = this.servername; + } + if (this.alpnprotocol) { + opts.ALPNProtocols = [this.alpnprotocol]; + } + opts.rejectUnauthorized = this.verifyservercert; return opts; } diff --git a/packages/node_modules/@node-red/nodes/core/network/10-mqtt.js b/packages/node_modules/@node-red/nodes/core/network/10-mqtt.js index 7be0263d6..afa0066f4 100644 --- a/packages/node_modules/@node-red/nodes/core/network/10-mqtt.js +++ b/packages/node_modules/@node-red/nodes/core/network/10-mqtt.js @@ -158,9 +158,16 @@ module.exports = function(RED) { if(!keys || !keys.length) return null; keys.forEach(key => { let val = srcUserProperties[key]; - if(typeof val == "string") { + if(typeof val === "string") { count++; _clone[key] = val; + } else if (val !== undefined && val !== null) { + try { + _clone[key] = JSON.stringify(val) + count++; + } catch (err) { + // Silently drop property + } } }); if(count) properties.userProperties = _clone; @@ -673,6 +680,8 @@ module.exports = function(RED) { delete node.options.protocolId; //V4+ default delete node.options.protocolVersion; //V4 default delete node.options.properties;//V5 only + + if (node.compatmode == "true" || node.compatmode === true || node.protocolVersion == 3) { node.options.protocolId = 'MQIsdp';//V3 compat only node.options.protocolVersion = 3; @@ -691,6 +700,21 @@ module.exports = function(RED) { setIntProp(node,node.options.properties,"sessionExpiryInterval"); } } + // Ensure will payload, if set, is a string + if (node.options.will && Object.hasOwn(node.options.will, 'payload')) { + let payload = node.options.will.payload + if (payload === null || typeof payload === 'undefined') { + payload = ""; + } else if (!Buffer.isBuffer(payload)) { + if (typeof payload === "object") { + payload = JSON.stringify(payload); + } else if (typeof payload !== "string") { + payload = "" + payload; + } + } + node.options.will.payload = payload + } + if (node.usetls && n.tls) { var tlsNode = RED.nodes.getNode(n.tls); if (tlsNode) { @@ -725,6 +749,7 @@ module.exports = function(RED) { }; node.deregister = function(mqttNode, done, autoDisconnect) { + setStatusDisconnected(mqttNode, false); delete node.users[mqttNode.id]; if (autoDisconnect && !node.closing && node.connected && Object.keys(node.users).length === 0) { node.disconnect(done); diff --git a/packages/node_modules/@node-red/nodes/core/parsers/70-CSV.js b/packages/node_modules/@node-red/nodes/core/parsers/70-CSV.js index c6a91d61a..26fdb636f 100644 --- a/packages/node_modules/@node-red/nodes/core/parsers/70-CSV.js +++ b/packages/node_modules/@node-red/nodes/core/parsers/70-CSV.js @@ -367,20 +367,21 @@ module.exports = function(RED) { const sendHeadersAlways = node.hdrout === "all" const sendHeaders = !dontSendHeaders && (sendHeadersOnce || sendHeadersAlways) const quoteables = [node.sep, node.quo, "\n", "\r"] - const templateQuoteables = [',', '"', "\n", "\r"] + const templateQuoteables = [node.sep, node.quo, "\n", "\r"] + const templateQuoteablesStrict = [',', '"', "\n", "\r"] let badTemplateWarnOnce = true const columnStringToTemplateArray = function (col, sep) { // NOTE: enforce strict column template parsing in RFC4180 mode const parsed = csv.parse(col, { separator: sep, quote: node.quo, outputStyle: 'array', strict: true }) - if (parsed.headers.length > 0) { node.goodtmpl = true } else { node.goodtmpl = false } - return parsed.headers.length ? parsed.headers : null + if (parsed.data?.length === 1) { node.goodtmpl = true } else { node.goodtmpl = false } + return node.goodtmpl ? parsed.data[0] : null } - const templateArrayToColumnString = function (template, keepEmptyColumns) { - // NOTE: enforce strict column template parsing in RFC4180 mode - const parsed = csv.parse('', {headers: template, headersOnly:true, separator: ',', quote: node.quo, outputStyle: 'array', strict: true }) + const templateArrayToColumnString = function (template, keepEmptyColumns, separator = ',', quotables = templateQuoteablesStrict) { + // NOTE: defaults to strict column template parsing (commas and double quotes) + const parsed = csv.parse('', {headers: template, headersOnly:true, separator, quote: node.quo, outputStyle: 'array', strict: true }) return keepEmptyColumns - ? parsed.headers.map(e => addQuotes(e || '', { separator: ',', quoteables: templateQuoteables})) + ? parsed.headers.map(e => addQuotes(e || '', { separator, quoteables: quotables })).join(separator) : parsed.header // exclues empty columns // TODO: resolve inconsistency between CSV->JSON and JSON->CSV // CSV->JSON: empty columns are excluded @@ -447,7 +448,7 @@ module.exports = function(RED) { template = Object.keys(inputData[0]) || [''] } } - stringBuilder.push(templateArrayToColumnString(template, true)) + stringBuilder.push(templateArrayToColumnString(template, true, node.sep, templateQuoteables)) // use user set separator for output data. if (sendHeadersOnce) { node.hdrSent = true } } @@ -483,6 +484,7 @@ module.exports = function(RED) { node.warn(RED._("csv.errors.obj_csv")) badTemplateWarnOnce = false } + template = Object.keys(row) || [''] const rowData = [] for (let header in inputData[0]) { if (row.hasOwnProperty(header)) { @@ -518,7 +520,7 @@ module.exports = function(RED) { // join lines, don't forget to add the last new line msg.payload = stringBuilder.join(node.ret) + node.ret - msg.columns = templateArrayToColumnString(template) + msg.columns = templateArrayToColumnString(template) // always strict commas + double quotes for if (msg.payload !== '') { send(msg) } done() } @@ -615,16 +617,15 @@ module.exports = function(RED) { } if (msg.parts.index + 1 === msg.parts.count) { msg.payload = node.store - msg.columns = csvParseResult.header - // msg._mode = 'RFC4180 mode' + // msg.columns = csvParseResult.header + msg.columns = templateArrayToColumnString(csvParseResult.headers) // always strict commas + double quotes for msg.columns delete msg.parts send(msg) node.store = [] } } else { - msg.columns = csvParseResult.header - // msg._mode = 'RFC4180 mode' + msg.columns = templateArrayToColumnString(csvParseResult.headers) // always strict commas + double quotes for msg.columns msg.payload = data send(msg); // finally send the array } @@ -633,7 +634,8 @@ module.exports = function(RED) { const len = data.length for (let row = 0; row < len; row++) { const newMessage = RED.util.cloneMessage(msg) - newMessage.columns = csvParseResult.header + // newMessage.columns = csvParseResult.header + newMessage.columns = templateArrayToColumnString(csvParseResult.headers) // always strict commas + double quotes for msg.columns newMessage.payload = data[row] if (!has_parts) { newMessage.parts = { diff --git a/packages/node_modules/@node-red/nodes/core/storage/10-file.js b/packages/node_modules/@node-red/nodes/core/storage/10-file.js index fea8c490e..5cff2b631 100644 --- a/packages/node_modules/@node-red/nodes/core/storage/10-file.js +++ b/packages/node_modules/@node-red/nodes/core/storage/10-file.js @@ -339,7 +339,7 @@ module.exports = function(RED) { } else { msg.filename = filename; - var lines = Buffer.from([]); + const bufferArray = []; var spare = ""; var count = 0; var type = "buffer"; @@ -397,7 +397,7 @@ module.exports = function(RED) { } } else { - lines = Buffer.concat([lines,chunk]); + bufferArray.push(chunk); } } }) @@ -413,10 +413,11 @@ module.exports = function(RED) { }) .on('end', function() { if (node.chunk === false) { + const buffer = Buffer.concat(bufferArray); if (node.format === "utf8") { - msg.payload = decode(lines, node.encoding); + msg.payload = decode(buffer, node.encoding); } - else { msg.payload = lines; } + else { msg.payload = buffer; } nodeSend(msg); } else if (node.format === "lines") { diff --git a/packages/node_modules/@node-red/nodes/examples/function/range/01 - Scale input value.json b/packages/node_modules/@node-red/nodes/examples/function/range/01 - Scale input value.json index 39bc40c4a..851634d0c 100644 --- a/packages/node_modules/@node-red/nodes/examples/function/range/01 - Scale input value.json +++ b/packages/node_modules/@node-red/nodes/examples/function/range/01 - Scale input value.json @@ -1 +1 @@ -[{"id":"827a48c0.912d88","type":"comment","z":"ff17dfa9.8fa6d","name":"Map property between different numeric ranges","info":"Range node can scale a number from one numeric range to another.\n\nSee Node-RED cookbook [item](https://cookbook.nodered.org/basic/map-between-different-number-ranges).","x":240,"y":60,"wires":[]},{"id":"bb23bd77.ce725","type":"inject","z":"ff17dfa9.8fa6d","name":"","repeat":"","crontab":"","once":false,"topic":"","payload":"0","payloadType":"num","x":170,"y":120,"wires":[["42ed281c.790b38"]]},{"id":"42ed281c.790b38","type":"range","z":"ff17dfa9.8fa6d","minin":"0","maxin":"1023","minout":"0","maxout":"5","action":"clamp","round":false,"name":"","x":390,"y":160,"wires":[["56e6dd0f.436c24"]]},{"id":"54659d5c.0283e4","type":"inject","z":"ff17dfa9.8fa6d","name":"","repeat":"","crontab":"","once":false,"topic":"","payload":"512","payloadType":"num","x":170,"y":160,"wires":[["42ed281c.790b38"]]},{"id":"85ce0127.07b06","type":"inject","z":"ff17dfa9.8fa6d","name":"","repeat":"","crontab":"","once":false,"topic":"","payload":"1023","payloadType":"num","x":170,"y":200,"wires":[["42ed281c.790b38"]]},{"id":"56e6dd0f.436c24","type":"debug","z":"ff17dfa9.8fa6d","name":"","active":true,"console":"false","complete":"false","x":590,"y":160,"wires":[]}] \ No newline at end of file +[{"id":"827a48c0.912d88","type":"comment","z":"ff17dfa9.8fa6d","name":"Map property between different numeric ranges","info":"Range node can scale a number from one numeric range to another.\n\nSee Node-RED cookbook [item](https://cookbook.nodered.org/basic/map-between-different-number-ranges).","x":240,"y":60,"wires":[]},{"id":"bb23bd77.ce725","type":"inject","z":"ff17dfa9.8fa6d","name":"","repeat":"","crontab":"","once":false,"topic":"","payload":"0","payloadType":"num","x":170,"y":120,"wires":[["42ed281c.790b38"]]},{"id":"42ed281c.790b38","type":"range","z":"ff17dfa9.8fa6d","minin":"0","maxin":"1023","minout":"0","maxout":"5","action":"clamp","round":false,"property":"payload","name":"","x":390,"y":160,"wires":[["56e6dd0f.436c24"]]},{"id":"54659d5c.0283e4","type":"inject","z":"ff17dfa9.8fa6d","name":"","repeat":"","crontab":"","once":false,"topic":"","payload":"512","payloadType":"num","x":170,"y":160,"wires":[["42ed281c.790b38"]]},{"id":"85ce0127.07b06","type":"inject","z":"ff17dfa9.8fa6d","name":"","repeat":"","crontab":"","once":false,"topic":"","payload":"1023","payloadType":"num","x":170,"y":200,"wires":[["42ed281c.790b38"]]},{"id":"56e6dd0f.436c24","type":"debug","z":"ff17dfa9.8fa6d","name":"","active":true,"console":"false","complete":"false","x":590,"y":160,"wires":[]}] \ No newline at end of file diff --git a/packages/node_modules/@node-red/nodes/examples/parser/html/04 - Join extracedt sequence of HTML element using join node.json b/packages/node_modules/@node-red/nodes/examples/parser/html/04 - Join extracted sequence of HTML element using join node.json similarity index 100% rename from packages/node_modules/@node-red/nodes/examples/parser/html/04 - Join extracedt sequence of HTML element using join node.json rename to packages/node_modules/@node-red/nodes/examples/parser/html/04 - Join extracted sequence of HTML element using join node.json diff --git a/packages/node_modules/@node-red/nodes/locales/de/network/10-mqtt.html b/packages/node_modules/@node-red/nodes/locales/de/network/10-mqtt.html index 2aa31c081..e5c7b5fb5 100644 --- a/packages/node_modules/@node-red/nodes/locales/de/network/10-mqtt.html +++ b/packages/node_modules/@node-red/nodes/locales/de/network/10-mqtt.html @@ -39,10 +39,36 @@
      MQTTv5: Ablaufzeit der Nachricht in Sekunden.

      Details

      -

      Das abonnierte Topic darf MQTT-Platzhalterzeichen (wildcards) enthalten (+ für eine Ebene und # für mehrere Ebenen).

      -

      Dieser Node erfordert eine Verbindung zu einem MQTT-Broker, der über die Auswahlliste selektiert werden kann. - Eine neue Verbindung wird durch Klicken auf das Stiftsymbol erstellt.

      +

      Das abonnierte Topic darf MQTT-Platzhalterzeichen (wildcards) enthalten (+ für eine Ebene und # für mehrere Ebenen).

      +

      Diese Node erfordert eine Verbindung zu einem MQTT-Broker, der über die Auswahlliste selektiert werden kann. Eine neue Verbindung wird durch Klicken auf das Stiftsymbol erstellt.

      Mehrere MQTT-Nodes (in oder out) können bei Bedarf dieselbe Broker-Verbindung nutzen.

      +

      Dynamische Steuerung

      + Die von der Node genutzte Verbindung kann dynamisch gesteuert werden, wenn die MQTT-Node eine der folgenden Nachrichten erhält. Die Payload dieser Nachrichten werden nicht veröffentlicht. +

      Eingangsdaten

      +

      Nur Verfügbar, wenn die Node für dynamische Abonnements konfiguriert wurde.

      +
      +
      action string
      +
      Der Name der Aktion, die die MQTT-Node ausführen soll. Verfügbare Aktionen sind: "connect", "disconnect", "getSubscriptions", "subscribe" und "unsubscribe".
      +
      topic string|object|array
      +
      Bei den Aktionen "subscribe" und "unsubscribe" gibt diese Eigenschaft die MQTT-Topic an. Dabei kann es sich um Folgendes handeln: +
        +
      • eine Zeichenfolge, die den Topic-Filter enthält
      • +
      • ein Objekt mit den Eigenschaften topic und qos
      • +
      • ein Array aus Zeichenfolgen oder Objekten, um mehrere Topics gleichzeitig zu verwalten
      • +
      +
      +
      broker broker
      +
      Für die Aktion "connect" kann diese Eigenschaft jede der einzelnen Broker-Konfigurationseinstellungen überschreiben, einschließlich:
        +
      • broker
      • +
      • port
      • +
      • url - überschreibt Broker/Port, um eine vollständige Verbindungs-URL bereitzustellen
      • +
      • username
      • +
      • password
      • +
      +

      Wenn diese Eigenschaft gesetzt ist und der Broker bereits verbunden ist, wird ein Fehler protokolliert, es sei denn, die Eigenschaft force gesetzt - in diesem Fall wird die Verbindung zum Broker getrennt, die neuen Einstellungen angewendet und erneut verbunden.

      +
      +
      + diff --git a/packages/node_modules/@node-red/nodes/locales/es-ES/parsers/70-CSV.html b/packages/node_modules/@node-red/nodes/locales/es-ES/parsers/70-CSV.html index 5dbbb88f0..940c65dfb 100644 --- a/packages/node_modules/@node-red/nodes/locales/es-ES/parsers/70-CSV.html +++ b/packages/node_modules/@node-red/nodes/locales/es-ES/parsers/70-CSV.html @@ -35,7 +35,9 @@

      Detalles

      -

      La plantilla de columnas puede contener una lista ordenada de nombres de columnas. Al convertir CSV en un objeto, los nombres de las columnas se utilizarán como nombres de propiedades. Alternativamente, los nombres de las columnas se pueden tomar de la primera fila del CSV.

      +

      La plantilla de columnas puede contener una lista ordenada de nombres de columnas. Al convertir CSV en un objeto, los nombres de las columnas se utilizarán como nombres de propiedades. Alternativamente, los nombres de las columnas se pueden tomar de la primera fila del CSV. +

      Cuando se selecciona el analizador RFC, la plantilla de columna debe ser compatible con RFC4180.

      +

      Al convertir a CSV, la plantilla de columnas se utiliza para identificar qué propiedades extraer del objeto y en qué orden.

      Si la plantilla de columnas está en blanco, puede utilizar una lista simple de propiedades separadas por comas proporcionada en msg.columns para determinar qué extraer y en qué orden. Si ninguno de los dos está presente, todas las propiedades del objeto se muestran en el orden en que se encuentran en la primera fila.

      Si la entrada es una matriz, entonces la plantilla de columnas solo se usa para generar opcionalmente una fila de títulos de columnas.

      @@ -46,4 +48,5 @@

      Si genera varios mensajes, tendrán su propiedad parts configurada y formarán una secuencia de mensajes completa.

      Si el nodo está configurado para enviar encabezados de columna solo una vez, si se configura msg.reset en cualquier valor hará que el nodo reenvíe los encabezados.

      Nota: la plantilla de columna debe estar separada por comas, incluso si se elige un separador diferente para los datos.

      +

      Nota: en el modo RFC, se generarán errores detectables para encabezados CSV mal formados y datos de carga útil de entrada no válidos

      diff --git a/packages/node_modules/@node-red/nodes/locales/fr/messages.json b/packages/node_modules/@node-red/nodes/locales/fr/messages.json index 99002f48f..238763e3b 100644 --- a/packages/node_modules/@node-red/nodes/locales/fr/messages.json +++ b/packages/node_modules/@node-red/nodes/locales/fr/messages.json @@ -1017,6 +1017,7 @@ "objectSend": "Envoie un message pour chaque paire clé/valeur", "strBuff": "Chaîne / Tampon", "array": "Tableau", + "splitThe": "Diviser le", "splitUsing": "Diviser en utilisant", "splitLength": "Longueur fixe de", "stream": "Gérer comme un flux de messages", @@ -1046,6 +1047,7 @@ "joinedUsing": "joint en utilisant", "send": "Envoyer le message :", "afterCount": "Après un nombre de parties du message", + "useparts": "Utiliser la propriété msg.parts existante", "count": "nombre", "subsequent": "Et tous les messages suivants.", "afterTimeout": "Après un délai d'attente après le premier message", @@ -1112,6 +1114,7 @@ "too-many": "Trop de messages en attente dans le noeud batch", "unexpected": "Mode inattendu", "no-parts": "Aucune propriété de pièces dans le message", + "honourParts": "Autoriser msg.parts à compléter les opération par lots", "error": { "invalid-count": "Compte invalide", "invalid-overlap": "Recouvrement invalide", diff --git a/packages/node_modules/@node-red/nodes/locales/ja/messages.json b/packages/node_modules/@node-red/nodes/locales/ja/messages.json index 8d38ac077..1693f879e 100644 --- a/packages/node_modules/@node-red/nodes/locales/ja/messages.json +++ b/packages/node_modules/@node-red/nodes/locales/ja/messages.json @@ -1047,6 +1047,7 @@ "joinedUsing": "連結文字", "send": "メッセージ送信:", "afterCount": "指定数のメッセージパーツを受信後", + "useparts": "既存のmsg.partsプロパティを使用", "count": "合計値", "subsequent": "後続のメッセージ毎", "afterTimeout": "最初のメッセージ受信からのタイムアウト後", @@ -1113,6 +1114,7 @@ "too-many": "batchノード内で保持しているメッセージが多すぎます", "unexpected": "想定外のモード", "no-parts": "メッセージにpartsプロパティがありません", + "honourParts": "msg.partsを用いたbatch操作を許可", "error": { "invalid-count": "メッセージ数が不正", "invalid-overlap": "オーバラップが不正", diff --git a/packages/node_modules/@node-red/nodes/package.json b/packages/node_modules/@node-red/nodes/package.json index ff26f1e6f..af8d93500 100644 --- a/packages/node_modules/@node-red/nodes/package.json +++ b/packages/node_modules/@node-red/nodes/package.json @@ -15,20 +15,20 @@ } ], "dependencies": { - "acorn": "8.11.3", - "acorn-walk": "8.3.2", - "ajv": "8.14.0", - "body-parser": "1.20.2", + "acorn": "8.12.1", + "acorn-walk": "8.3.4", + "ajv": "8.17.1", + "body-parser": "1.20.3", "cheerio": "1.0.0-rc.10", "content-type": "1.0.5", - "cookie-parser": "1.4.6", - "cookie": "0.6.0", + "cookie-parser": "1.4.7", + "cookie": "0.7.2", "cors": "2.8.5", "cronosjs": "1.7.1", "denque": "2.1.0", "form-data": "4.0.0", "fs-extra": "11.2.0", - "got": "12.6.0", + "got": "12.6.1", "hash-sum": "2.0.0", "hpagent": "1.2.0", "https-proxy-agent": "5.0.1", @@ -40,8 +40,8 @@ "mustache": "4.2.0", "node-watch": "0.7.4", "on-headers": "1.0.2", - "raw-body": "2.5.2", - "tough-cookie": "4.1.4", + "raw-body": "3.0.0", + "tough-cookie": "^5.0.0", "uuid": "9.0.1", "ws": "7.5.10", "xml2js": "0.6.2", diff --git a/packages/node_modules/@node-red/registry/lib/externalModules.js b/packages/node_modules/@node-red/registry/lib/externalModules.js index a7ce04f63..b3877c75c 100644 --- a/packages/node_modules/@node-red/registry/lib/externalModules.js +++ b/packages/node_modules/@node-red/registry/lib/externalModules.js @@ -92,7 +92,7 @@ function requireModule(module) { const parsedModule = parseModuleName(module); - if (BUILTIN_MODULES.indexOf(parsedModule.module) !== -1) { + if (parsedModule.builtin) { return require(parsedModule.module + parsedModule.subpath); } if (!knownExternalModules[parsedModule.module]) { @@ -113,7 +113,7 @@ function importModule(module) { const parsedModule = parseModuleName(module); - if (BUILTIN_MODULES.indexOf(parsedModule.module) !== -1) { + if (parsedModule.builtin) { return import(parsedModule.module + parsedModule.subpath); } if (!knownExternalModules[parsedModule.module]) { @@ -135,15 +135,22 @@ function importModule(module) { } function parseModuleName(module) { - var match = /((?:@[^/]+\/)?[^/@]+)(\/[^/@]+)?(?:@([\s\S]+))?/.exec(module); + const match = /((?:@[^/]+\/)?[^/@]+)(\/[^/@]+)?(?:@([\s\S]+))?/.exec(module); if (match) { + const moduleName = match[1] + let isBuiltIn = false + if (/^node:/.test(moduleName)) { + isBuiltIn = BUILTIN_MODULES.includes(moduleName.substring(5)) + } else { + isBuiltIn = BUILTIN_MODULES.includes(moduleName) + } return { spec: module, - module: match[1], + module: moduleName, subpath: match[2] || '', version: match[3], - builtin: BUILTIN_MODULES.indexOf(match[1]) !== -1, - known: !!knownExternalModules[match[1]] + builtin: isBuiltIn, + known: !!knownExternalModules[moduleName] } } return null; diff --git a/packages/node_modules/@node-red/registry/lib/installer.js b/packages/node_modules/@node-red/registry/lib/installer.js index 495a956af..3ffd65513 100644 --- a/packages/node_modules/@node-red/registry/lib/installer.js +++ b/packages/node_modules/@node-red/registry/lib/installer.js @@ -144,7 +144,7 @@ async function installModule(module,version,url) { if (url) { if (pkgurlRe.test(url) || localtgzRe.test(url)) { // Git remote url or Tarball url - check the valid package url - installName = url; + installName = localtgzRe.test(url) && slashRe.test(url) ? `"${url}"` : url; isRegistryPackage = false; } else { log.warn(log._("server.install.install-failed-url",{name:module,url:url})); diff --git a/packages/node_modules/@node-red/registry/package.json b/packages/node_modules/@node-red/registry/package.json index 7f7a0a694..d4d3b03a1 100644 --- a/packages/node_modules/@node-red/registry/package.json +++ b/packages/node_modules/@node-red/registry/package.json @@ -19,8 +19,8 @@ "@node-red/util": "4.1.0-beta.0", "clone": "2.1.2", "fs-extra": "11.2.0", - "semver": "7.5.4", - "tar": "7.2.0", + "semver": "7.6.3", + "tar": "7.4.3", "uglify-js": "3.17.4" } } diff --git a/packages/node_modules/@node-red/runtime/lib/api/context.js b/packages/node_modules/@node-red/runtime/lib/api/context.js index f27075577..5ba9835bf 100644 --- a/packages/node_modules/@node-red/runtime/lib/api/context.js +++ b/packages/node_modules/@node-red/runtime/lib/api/context.js @@ -96,7 +96,11 @@ var api = module.exports = { } else if (scope === 'node') { var node = runtime.nodes.getNode(id); if (node) { - ctx = node.context(); + if (/^subflow:/.test(node.type)) { + ctx = runtime.nodes.getContext(node.id); + } else { + ctx = node.context(); + } } } if (ctx) { @@ -104,13 +108,25 @@ var api = module.exports = { store = store || availableStores.default; ctx.get(key,store,function(err, v) { if (opts.keysOnly) { + const result = {} if (Array.isArray(v)) { - resolve({ [store]: { format: `array[${v.length}]`}}) + result.format = `array[${v.length}]` } else if (typeof v === 'object') { - resolve({ [store]: { keys: Object.keys(v), format: 'Object' } }) + result.keys = Object.keys(v).map(k => { + if (Array.isArray(v[k])) { + return { key: k, format: `array[${v[k].length}]`, length: v[k].length } + } else if (typeof v[k] === 'object') { + return { key: k, format: 'object' } + } else { + return { key: k } + } + }) + result.format = 'object' } else { - resolve({ [store]: { keys: [] }}) + result.keys = [] } + resolve({ [store]: result }) + return } var encoded = util.encodeObject({msg:v}); if (store !== availableStores.default) { @@ -147,7 +163,7 @@ var api = module.exports = { } return } - result[store] = { keys } + result[store] = { keys: keys.map(key => { return { key }}) } c--; if (c === 0) { if (!errorReported) { @@ -225,7 +241,11 @@ var api = module.exports = { } else if (scope === 'node') { var node = runtime.nodes.getNode(id); if (node) { - ctx = node.context(); + if (/^subflow:/.test(node.type)) { + ctx = runtime.nodes.getContext(node.id); + } else { + ctx = node.context(); + } } } if (ctx) { diff --git a/packages/node_modules/@node-red/runtime/lib/flows/Flow.js b/packages/node_modules/@node-red/runtime/lib/flows/Flow.js index c4f4e39a2..0b6045326 100644 --- a/packages/node_modules/@node-red/runtime/lib/flows/Flow.js +++ b/packages/node_modules/@node-red/runtime/lib/flows/Flow.js @@ -719,6 +719,14 @@ class Flow { }); } + getContext(scope) { + if (scope === 'flow') { + return this.context + } else if (scope === 'global') { + return context.get('global') + } + } + dump() { console.log("==================") console.log(this.TYPE, this.id); diff --git a/packages/node_modules/@node-red/runtime/lib/flows/Group.js b/packages/node_modules/@node-red/runtime/lib/flows/Group.js index 589cdf115..d95b4e553 100644 --- a/packages/node_modules/@node-red/runtime/lib/flows/Group.js +++ b/packages/node_modules/@node-red/runtime/lib/flows/Group.js @@ -49,6 +49,14 @@ class Group { } return this.parent.getSetting(key); } + + error(msg) { + this.parent.error(msg); + } + + getContext(scope) { + return this.parent.getContext(scope); + } } module.exports = { diff --git a/packages/node_modules/@node-red/runtime/lib/flows/util.js b/packages/node_modules/@node-red/runtime/lib/flows/util.js index fa25a26d0..d50825212 100644 --- a/packages/node_modules/@node-red/runtime/lib/flows/util.js +++ b/packages/node_modules/@node-red/runtime/lib/flows/util.js @@ -100,7 +100,24 @@ async function evaluateEnvProperties(flow, env, credentials) { } } else if (type ==='jsonata') { pendingEvaluations.push(new Promise((resolve, _) => { - redUtil.evaluateNodeProperty(value, 'jsonata', {_flow: flow}, null, (err, result) => { + redUtil.evaluateNodeProperty(value, 'jsonata',{ + // Fake a node object to provide access to _flow and context + _flow: flow, + context: () => { + return { + flow: { + get: (value, store, callback) => { + return flow.getContext('flow').get(value, store, callback) + } + }, + global: { + get: (value, store, callback) => { + return flow.getContext('global').get(value, store, callback) + } + } + } + } + }, null, (err, result) => { if (!err) { if (typeof result === 'object') { result = { value: result, __clone__: true} @@ -113,6 +130,10 @@ async function evaluateEnvProperties(flow, env, credentials) { resolve() }); })) + } else if (type === "conf-type" && /^\${[^}]+}$/.test(value)) { + // Get the config node from the parent subflow + const name = value.substring(2, value.length - 1); + value = flow.getSetting(name); } else { try { value = redUtil.evaluateNodeProperty(value, type, {_flow: flow}, null, null); diff --git a/packages/node_modules/@node-red/runtime/lib/multiplayer/index.js b/packages/node_modules/@node-red/runtime/lib/multiplayer/index.js index 08cb0d5a1..85a90e977 100644 --- a/packages/node_modules/@node-red/runtime/lib/multiplayer/index.js +++ b/packages/node_modules/@node-red/runtime/lib/multiplayer/index.js @@ -110,7 +110,8 @@ module.exports = { const payload = { session: sessionId, workspace: opts.data.workspace, - node: opts.data.node + node: opts.data.node, + cursor: opts.data.cursor } runtime.events.emit('comms', { topic: 'multiplayer/location', diff --git a/packages/node_modules/@node-red/runtime/locales/es-ES/runtime.json b/packages/node_modules/@node-red/runtime/locales/es-ES/runtime.json index 9fba64ad3..d1c2c44a9 100644 --- a/packages/node_modules/@node-red/runtime/locales/es-ES/runtime.json +++ b/packages/node_modules/@node-red/runtime/locales/es-ES/runtime.json @@ -25,6 +25,7 @@ "removing-modules": "Eliminando módulos de la configuración", "added-types": "Tipos de nodos añadidos:", "removed-types": "Tipos de nodos eliminados:", + "removed-plugins": "Extensiones eliminadas:", "install": { "invalid": "Nombre de módulo no válido", "installing": "Instalando módulo: __name__, versión: __version__", diff --git a/packages/node_modules/@node-red/runtime/package.json b/packages/node_modules/@node-red/runtime/package.json index 76cfac8a0..e3d373673 100644 --- a/packages/node_modules/@node-red/runtime/package.json +++ b/packages/node_modules/@node-red/runtime/package.json @@ -20,7 +20,7 @@ "@node-red/util": "4.1.0-beta.0", "async-mutex": "0.5.0", "clone": "2.1.2", - "express": "4.19.2", + "express": "4.21.2", "fs-extra": "11.2.0", "json-stringify-safe": "5.0.1", "rfdc": "^1.3.1" diff --git a/packages/node_modules/@node-red/util/lib/log.js b/packages/node_modules/@node-red/util/lib/log.js index 341019080..7b7e9b2dc 100644 --- a/packages/node_modules/@node-red/util/lib/log.js +++ b/packages/node_modules/@node-red/util/lib/log.js @@ -75,12 +75,28 @@ LogHandler.prototype.shouldReportMessage = function(msglevel) { msglevel <= this.logLevel; } + +// Older versions of Node-RED used the deprecated util.log function. +// With Node.js 22, use of that function causes warnings. So here we +// are replicating the same format output to ensure we don't break any +// log parsing that happens in the real world. +const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; +const utilLog = function (msg) { + const d = new Date(); + const time = [ + d.getHours().toString().padStart(2, '0'), + d.getMinutes().toString().padStart(2, '0'), + d.getSeconds().toString().padStart(2, '0') + ].join(':'); + console.log(`${d.getDate()} ${months[d.getMonth()]} ${time} - ${msg}`) +} + var consoleLogger = function(msg) { if (msg.level == log.METRIC || msg.level == log.AUDIT) { - util.log("["+levelNames[msg.level]+"] "+JSON.stringify(msg)); + utilLog("["+levelNames[msg.level]+"] "+JSON.stringify(msg)); } else { if (verbose && msg.msg && msg.msg.stack) { - util.log("["+levelNames[msg.level]+"] "+(msg.type?"["+msg.type+":"+(msg.name||msg.id)+"] ":"")+msg.msg.stack); + utilLog("["+levelNames[msg.level]+"] "+(msg.type?"["+msg.type+":"+(msg.name||msg.id)+"] ":"")+msg.msg.stack); } else { var message = msg.msg; try { @@ -91,7 +107,7 @@ var consoleLogger = function(msg) { message = 'Exception trying to log: '+util.inspect(message); } - util.log("["+levelNames[msg.level]+"] "+(msg.type?"["+msg.type+":"+(msg.name||msg.id)+"] ":"")+message); + utilLog("["+levelNames[msg.level]+"] "+(msg.type?"["+msg.type+":"+(msg.name||msg.id)+"] ":"")+message); } } } diff --git a/packages/node_modules/@node-red/util/lib/util.js b/packages/node_modules/@node-red/util/lib/util.js index 9ebb46bc0..d205700a5 100644 --- a/packages/node_modules/@node-red/util/lib/util.js +++ b/packages/node_modules/@node-red/util/lib/util.js @@ -828,18 +828,25 @@ function encodeObject(msg,opts) { debuglength = opts.maxLength; } var msgType = typeof msg.msg; - if (msg.msg instanceof Error) { + if (msg.msg instanceof Error || /Error/.test(msg.msg?.__proto__?.name)) { msg.format = "error"; - var errorMsg = {}; - if (msg.msg.name) { - errorMsg.name = msg.msg.name; + + const cause = msg.msg.cause + const value = { + __enc__: true, + type: 'error', + data: { + name: msg.msg.name, + message: hasOwnProperty.call(msg.msg, 'message') ? msg.msg.message : msg.msg.toString(), + cause: cause + "", + stack: msg.msg.stack, + } } - if (hasOwnProperty.call(msg.msg, 'message')) { - errorMsg.message = msg.msg.message; - } else { - errorMsg.message = msg.msg.toString(); + // Remove cause if not defined + if (!cause) { + delete value.data.cause } - msg.msg = JSON.stringify(errorMsg); + msg.msg = JSON.stringify(value); } else if (msg.msg instanceof Buffer) { msg.format = "buffer["+msg.msg.length+"]"; msg.msg = msg.msg.toString('hex'); @@ -857,6 +864,7 @@ function encodeObject(msg,opts) { msg.format = "Object"; } if (/error/i.test(msg.format)) { + // TODO: check if this is needed msg.msg = JSON.stringify({ name: msg.msg.name, message: msg.msg.message @@ -904,8 +912,22 @@ function encodeObject(msg,opts) { __enc__: true, type: "internal" } - } else if (value instanceof Error) { - value = value.toString() + } else if (value instanceof Error || /Error/.test(value?.__proto__?.name)) { + const cause = value.cause + value = { + __enc__: true, + type: 'error', + data: { + name: value.name, + message: hasOwnProperty.call(value, 'message') ? value.message : value.toString(), + cause: cause + "", + stack: value.stack, + } + } + // Remove cause if not defined + if (!cause) { + delete value.data.cause + } } else if (Array.isArray(value) && value.length > debuglength) { value = { __enc__: true, @@ -977,8 +999,19 @@ function encodeObject(msg,opts) { return value; }); } else { - try { msg.msg = msg.msg.toString(); } - catch(e) { msg.msg = "[Type not printable]" + util.inspect(msg.msg); } + try { + msg.msg = msg.msg.toString(); + } catch(e) { + msg.msg.format = 'error' + msg.msg = JSON.stringify({ + __enc__: true, + type: 'error', + data: { + message: "[Type not serializable]", + stack: e.stack + } + }) + } } } } else if (msgType === "function") { @@ -1009,17 +1042,14 @@ function encodeObject(msg,opts) { return msg; } catch(e) { msg.format = "error"; - var errorMsg = {}; - if (e.name) { - errorMsg.name = e.name; - } - if (hasOwnProperty.call(e, 'message')) { - errorMsg.message = 'encodeObject Error: ['+e.message + '] Value: '+util.inspect(msg.msg); - } else { - errorMsg.message = 'encodeObject Error: ['+e.toString() + '] Value: '+util.inspect(msg.msg); - } - if (errorMsg.message.length > debuglength) { - errorMsg.message = errorMsg.message.substring(0,debuglength); + const errorMsg = { + __enc__: true, + type: 'error', + data: { + name: e.name, + message: 'encodeObject Error: ' + (hasOwnProperty.call(e, 'message') ? e.message : e.toString()), + stack: e.stack, + } } msg.msg = JSON.stringify(errorMsg); return msg; diff --git a/packages/node_modules/@node-red/util/package.json b/packages/node_modules/@node-red/util/package.json index d952e5989..514f12863 100644 --- a/packages/node_modules/@node-red/util/package.json +++ b/packages/node_modules/@node-red/util/package.json @@ -21,6 +21,6 @@ "jsonata": "2.0.5", "lodash.clonedeep": "^4.5.0", "moment": "2.30.1", - "moment-timezone": "0.5.45" + "moment-timezone": "0.5.46" } } diff --git a/packages/node_modules/node-red/package.json b/packages/node_modules/node-red/package.json index 3c33133ec..52302a53a 100644 --- a/packages/node_modules/node-red/package.json +++ b/packages/node_modules/node-red/package.json @@ -38,11 +38,11 @@ "basic-auth": "2.0.1", "bcryptjs": "2.4.3", "cors": "2.8.5", - "express": "4.19.2", + "express": "4.21.2", "fs-extra": "11.2.0", - "node-red-admin": "^4.0.0", + "node-red-admin": "^4.0.1", "nopt": "5.0.0", - "semver": "7.5.4" + "semver": "7.6.3" }, "optionalDependencies": { "@node-rs/bcrypt": "1.10.4" diff --git a/scripts/generate-publish-script.js b/scripts/generate-publish-script.js index 0b2a75cd0..c31b2dd9e 100644 --- a/scripts/generate-publish-script.js +++ b/scripts/generate-publish-script.js @@ -36,10 +36,12 @@ function generateScript() { packages.forEach(name => { const tarName = name.replace(/@/,"").replace(/\//,"-") lines.push(`npm publish ${tarName}-${version}.tgz ${tagArg}\n`); - if (updateNextToLatest) { - lines.push(`npm dist-tag add ${name}@${version} next\n`); - } }) + if (updateNextToLatest) { + packages.forEach(name => { + lines.push(`npm dist-tag add ${name}@${version} next\n`); + }) + } resolve(lines.join("")) }); } diff --git a/test/nodes/core/common/21-debug_spec.js b/test/nodes/core/common/21-debug_spec.js index 2931ea1e4..a772608ec 100644 --- a/test/nodes/core/common/21-debug_spec.js +++ b/test/nodes/core/common/21-debug_spec.js @@ -173,9 +173,19 @@ describe('debug node', function() { websocket_test(function() { n1.emit("input", {payload: new Error("oops")}); }, function(msg) { - JSON.parse(msg).should.eql([{ - topic:"debug",data:{id:"n1",msg:'{"name":"Error","message":"oops"}',property:"payload",format:"error",path:"global"} - }]); + const fullMsg = JSON.parse(msg) + fullMsg[0].should.have.property('topic', 'debug') + fullMsg[0].should.have.property('data') + fullMsg[0].data.should.have.property('id', 'n1') + fullMsg[0].data.should.have.property('property', 'payload') + fullMsg[0].data.should.have.property('format', 'error') + fullMsg[0].data.should.have.property('path', 'global') + fullMsg[0].data.should.have.property('msg') + const msgData = JSON.parse(fullMsg[0].data.msg) + msgData.should.have.property('__enc__', true) + msgData.should.have.property('type', 'error') + msgData.data.should.have.property('name', 'Error') + msgData.data.should.have.property('message', 'oops') }, done); }); }); diff --git a/test/nodes/core/function/89-delay_spec.js b/test/nodes/core/function/89-delay_spec.js index 46b0037bc..4fbf3df54 100644 --- a/test/nodes/core/function/89-delay_spec.js +++ b/test/nodes/core/function/89-delay_spec.js @@ -1009,6 +1009,29 @@ describe('delay Node', function() { }); }); + it('sending a msg with reset to empty queue doesnt send anything', function(done) { + this.timeout(2000); + var flow = [{"id":"delayNode1","type":"delay","name":"delayNode","pauseType":"rate","timeout":1,"timeoutUnits":"seconds","rate":2,"rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"wires":[["helperNode1"]]}, + {id:"helperNode1", type:"helper", wires:[]}]; + helper.load(delayNode, flow, function() { + var delayNode1 = helper.getNode("delayNode1"); + var helperNode1 = helper.getNode("helperNode1"); + var t = Date.now(); + var c = 0; + helperNode1.on("input", function(msg) { + console.log("Shold not get here") + done(e); + }); + + setTimeout( function() { + if (c === 0) { done(); } + }, 250); + + // send test messages + delayNode1.receive({payload:1,topic:"foo",reset:true}); // send something with blank topic + }); + }); + /* Messaging API support */ function mapiDoneTestHelper(done, pauseType, drop, msgAndTimings) { const completeNode = require("nr-test-utils").require("@node-red/nodes/core/common/24-complete.js"); diff --git a/test/nodes/core/function/89-trigger_spec.js b/test/nodes/core/function/89-trigger_spec.js index 33401e540..913298901 100644 --- a/test/nodes/core/function/89-trigger_spec.js +++ b/test/nodes/core/function/89-trigger_spec.js @@ -111,7 +111,15 @@ describe('trigger node', function() { try { if (rval) { msg.should.have.property("payload"); - should.deepEqual(msg.payload, rval); + if (type == "date" && val == "1") { + should.deepEqual(Math.round(msg.payload/1000000), Math.round(Date.now()/1000000)); + } + else if (type == "date" && val == "iso") { + should.deepEqual(msg.payload.substr(0,11), rval.substr(0,11)); + } + else { + should.deepEqual(msg.payload, rval); + } } else { msg.should.have.property("payload", val); @@ -126,6 +134,7 @@ describe('trigger node', function() { }); it('should output 2st value when triggered ('+type+')', function(done) { + if (type == "date" && val == "1") { val = "0"; } var flow = [{"id":"n1", "type":"trigger", "name":"triggerNode", op1:"foo", op1type:"str", op2:val, op2type:type, duration:"20", wires:[["n2"]] }, {id:"n2", type:"helper"} ]; process.env[val] = rval; @@ -142,7 +151,15 @@ describe('trigger node', function() { else { if (rval) { msg.should.have.property("payload"); - should.deepEqual(msg.payload, rval); + if (type == "date" && val == "0") { + ;(Math.round(msg.payload/1000000)).should.be.approximately(parseInt(Date.now()/1000000), 1); + } + else if (type == "date" && val == "iso") { + should.deepEqual(msg.payload.substr(0,11), rval.substr(0,11)); + } + else { + should.deepEqual(msg.payload, rval); + } } else { msg.should.have.property("payload", val); @@ -166,6 +183,9 @@ describe('trigger node', function() { var val_buf = "[1,2,3,4,5]"; basicTest("bin", val_buf, Buffer.from(JSON.parse(val_buf))); basicTest("env", "NR-TEST", "env-val"); + basicTest("date", "1", Date.now()); + basicTest("date", "iso", (new Date()).toISOString()); + // basicTest("date", "object", Date.now()); it('should output 1 then 0 when triggered (default)', function(done) { var flow = [{"id":"n1", "type":"trigger", "name":"triggerNode", duration:"20", wires:[["n2"]] }, diff --git a/test/nodes/core/parsers/70-CSV_spec.js b/test/nodes/core/parsers/70-CSV_spec.js index f362ecdbf..9f6749b9f 100644 --- a/test/nodes/core/parsers/70-CSV_spec.js +++ b/test/nodes/core/parsers/70-CSV_spec.js @@ -2067,6 +2067,27 @@ describe('CSV node (RFC Mode)', function () { n2.on("input", function (msg) { try { msg.should.have.property('payload', '1\tfoo\t"ba""r"\tdi,ng\n'); + msg.should.have.property('columns', 'd,b,c,a'); // Strict RFC columns + done(); + } catch (e) { + done(e); + } + }); + const testJson = { d: 1, b: "foo", c: "ba\"r", a: "di,ng" }; + n1.emit("input", { payload: testJson }); + }); + }); + + it('should convert a simple object back to a tsv with headers using a tab as a separator', function (done) { + const flow = [{ id: "n1", type: "csv", spec: "rfc", temp: "", sep: "\t", ret: '\n', hdrout: "all", wires: [["n2"]] }, // RFC-vs-Legacy difference - use line separator \n to satisfy original test + { id: "n2", type: "helper" }]; + helper.load(csvNode, flow, function () { + const n1 = helper.getNode("n1"); + const n2 = helper.getNode("n2"); + n2.on("input", function (msg) { + try { + msg.should.have.property('payload', 'd\tb\tc\ta\n1\tfoo\t"ba""r"\tdi,ng\n'); + msg.should.have.property('columns', 'd,b,c,a'); // Strict RFC columns done(); } catch (e) { done(e); @@ -2086,6 +2107,7 @@ describe('CSV node (RFC Mode)', function () { n2.on("input", function (msg) { try { msg.should.have.property('payload', '4,foo,true,,0\n'); + msg.should.have.property('columns', 'a,b o,c p,e'); // Strict RFC columns done(); } catch (e) { done(e); @@ -2106,6 +2128,7 @@ describe('CSV node (RFC Mode)', function () { try { // 'payload', 'a"a,b\'b\nA1,B1\nA2,B2\n'); // Legacy msg.should.have.property('payload', '"a""a",b\'b\nA1,B1\nA2,B2\n'); // RFC-vs-Legacy difference - RFC4180 Section 2.6, 2.7 quote handling + msg.should.have.property('columns', '"a""a",b\'b'); // RCF compliant column names done(); } catch (e) { done(e); @@ -2171,6 +2194,7 @@ describe('CSV node (RFC Mode)', function () { n2.on("input", function (msg) { try { msg.should.have.property('payload', '1,3,2,4\n4,2,3,1\n'); + msg.should.have.property('columns', 'd,b,c,a'); // Strict RFC columns done(); } catch (e) { done(e); } @@ -2189,6 +2213,7 @@ describe('CSV node (RFC Mode)', function () { n2.on("input", function (msg) { try { msg.should.have.property('payload', 'd,b,c,a\n1,3,2,4\n4,"f\ng",3,1\n'); + msg.should.have.property('columns', 'd,b,c,a'); // Strict RFC columns done(); } catch (e) { done(e); } @@ -2208,6 +2233,7 @@ describe('CSV node (RFC Mode)', function () { try { // 'payload', ',0,1,foo,"ba""r","di,ng","fa\nba"\n'); msg.should.have.property('payload', ',0,1,foo\n'); // RFC-vs-Legacy difference - respect that user has specified a template with 4 columns + msg.should.have.property('columns', 'a,b,c,d'); done(); } catch (e) { done(e); } @@ -2327,6 +2353,7 @@ describe('CSV node (RFC Mode)', function () { n2.on("input", function (msg) { try { msg.should.have.property('payload', '{},"text,with,commas","This ""is"" a banana","{""sub"":""object""}"\n'); + msg.should.have.property('columns', 'a,b,c,d'); done(); } catch (e) { done(e); } diff --git a/test/unit/@node-red/registry/lib/installer_spec.js b/test/unit/@node-red/registry/lib/installer_spec.js index 7f514e99d..f7ca223e8 100644 --- a/test/unit/@node-red/registry/lib/installer_spec.js +++ b/test/unit/@node-red/registry/lib/installer_spec.js @@ -258,6 +258,29 @@ describe('nodes/registry/installer', function() { }).catch(done); }); + it("succeeds when file path is valid node-red module", function(done) { + var nodeInfo = {nodes:{module:"foo",types:["a"]}}; + + var res = { + code: 0, + stdout:"", + stderr:"" + } + var p = Promise.resolve(res); + p.catch((err)=>{}); + execResponse = p; + + var addModule = sinon.stub(registry,"addModule").callsFake(function(md) { + return Promise.resolve(nodeInfo); + }); + + installer.installModule("foo",null,"/example path/foo-0.1.1.tgz").then(function(info) { + exec.run.lastCall.args[1].should.eql([ 'install', '--no-audit', '--no-update-notifier', '--no-fund', '--save', '--save-prefix=~', '--omit=dev', '--engine-strict', '"/example path/foo-0.1.1.tgz"' ]); + info.should.eql(nodeInfo); + done(); + }).catch(done); + }); + it("triggers preInstall and postInstall hooks", function(done) { let receivedPreEvent,receivedPostEvent; hooks.add("preInstall", function(event) { event.args = ["a"]; receivedPreEvent = event; }) diff --git a/test/unit/@node-red/util/lib/log_spec.js b/test/unit/@node-red/util/lib/log_spec.js index 056f37672..8fe09b3bf 100644 --- a/test/unit/@node-red/util/lib/log_spec.js +++ b/test/unit/@node-red/util/lib/log_spec.js @@ -24,38 +24,38 @@ var log = NR_TEST_UTILS.require("@node-red/util").log; describe("@node-red/util/log", function() { beforeEach(function () { - var spy = sinon.stub(util, 'log').callsFake(function(arg){}); + var spy = sinon.stub(console, 'log').callsFake(function(arg){}); var settings = {logging: { console: { level: 'metric', metrics: true } } }; log.init(settings); }); afterEach(function() { - util.log.restore(); + console.log.restore(); }); it('it can raise an error', function() { var ret = log.error("This is an error"); - sinon.assert.calledWithMatch(util.log,"[error] This is an error"); + sinon.assert.calledWithMatch(console.log,"[error] This is an error"); }); it('it can raise a trace', function() { var ret = log.trace("This is a trace"); - sinon.assert.calledWithMatch(util.log,"[trace] This is a trace"); + sinon.assert.calledWithMatch(console.log,"[trace] This is a trace"); }); it('it can raise a debug', function() { var ret = log.debug("This is a debug"); - sinon.assert.calledWithMatch(util.log,"[debug] This is a debug"); + sinon.assert.calledWithMatch(console.log,"[debug] This is a debug"); }); it('it can raise a info', function() { var ret = log.info("This is an info"); - sinon.assert.calledWithMatch(util.log,"[info] This is an info"); + sinon.assert.calledWithMatch(console.log,"[info] This is an info"); }); it('it can raise a warn', function() { var ret = log.warn("This is a warn"); - sinon.assert.calledWithMatch(util.log,"[warn] This is a warn"); + sinon.assert.calledWithMatch(console.log,"[warn] This is a warn"); }); it('it can raise a metric', function() { @@ -66,9 +66,10 @@ describe("@node-red/util/log", function() { metrics.msgid = "12345"; metrics.value = "the metric payload"; var ret = log.log(metrics); - util.log.calledOnce.should.be.true(); - util.log.firstCall.args[0].indexOf("[metric] ").should.equal(0); - var body = JSON.parse(util.log.firstCall.args[0].substring(9)); + console.log.calledOnce.should.be.true(); + console.log.firstCall.args[0].indexOf("[metric]").should.not.equal(-1); + const parts = console.log.firstCall.args[0].split("[metric] ") + var body = JSON.parse(parts[1]) body.should.have.a.property("nodeid","testid"); body.should.have.a.property("event","node.test.testevent"); body.should.have.a.property("msgid","12345"); @@ -86,13 +87,13 @@ describe("@node-red/util/log", function() { it('it logs node type and name if provided',function() { log.log({level:log.INFO,type:"nodeType",msg:"test",name:"nodeName",id:"nodeId"}); - util.log.calledOnce.should.be.true(); - util.log.firstCall.args[0].indexOf("[nodeType:nodeName]").should.not.equal(-1); + console.log.calledOnce.should.be.true(); + console.log.firstCall.args[0].indexOf("[nodeType:nodeName]").should.not.equal(-1); }); it('it logs node type and id if no name provided',function() { log.log({level:log.INFO,type:"nodeType",msg:"test",id:"nodeId"}); - util.log.calledOnce.should.be.true(); - util.log.firstCall.args[0].indexOf("[nodeType:nodeId]").should.not.equal(-1); + console.log.calledOnce.should.be.true(); + console.log.firstCall.args[0].indexOf("[nodeType:nodeId]").should.not.equal(-1); }); it('ignores lower level messages and metrics', function() { @@ -104,12 +105,12 @@ describe("@node-red/util/log", function() { log.debug("This is a debug"); log.trace("This is a trace"); log.log({level:log.METRIC,msg:"testMetric"}); - sinon.assert.calledWithMatch(util.log,"[error] This is an error"); - sinon.assert.calledWithMatch(util.log,"[warn] This is a warn"); - sinon.assert.neverCalledWithMatch(util.log,"[info] This is an info"); - sinon.assert.neverCalledWithMatch(util.log,"[debug] This is a debug"); - sinon.assert.neverCalledWithMatch(util.log,"[trace] This is a trace"); - sinon.assert.neverCalledWithMatch(util.log,"[metric] "); + sinon.assert.calledWithMatch(console.log,"[error] This is an error"); + sinon.assert.calledWithMatch(console.log,"[warn] This is a warn"); + sinon.assert.neverCalledWithMatch(console.log,"[info] This is an info"); + sinon.assert.neverCalledWithMatch(console.log,"[debug] This is a debug"); + sinon.assert.neverCalledWithMatch(console.log,"[trace] This is a trace"); + sinon.assert.neverCalledWithMatch(console.log,"[metric] "); }); it('ignores lower level messages but accepts metrics', function() { var settings = {logging: { console: { level: 'log', metrics: true } } }; @@ -120,12 +121,12 @@ describe("@node-red/util/log", function() { log.debug("This is a debug"); log.trace("This is a trace"); log.log({level:log.METRIC,msg:"testMetric"}); - sinon.assert.calledWithMatch(util.log,"[error] This is an error"); - sinon.assert.calledWithMatch(util.log,"[warn] This is a warn"); - sinon.assert.calledWithMatch(util.log,"[info] This is an info"); - sinon.assert.neverCalledWithMatch(util.log,"[debug] This is a debug"); - sinon.assert.neverCalledWithMatch(util.log,"[trace] This is a trace"); - sinon.assert.calledWithMatch(util.log,"[metric] "); + sinon.assert.calledWithMatch(console.log,"[error] This is an error"); + sinon.assert.calledWithMatch(console.log,"[warn] This is a warn"); + sinon.assert.calledWithMatch(console.log,"[info] This is an info"); + sinon.assert.neverCalledWithMatch(console.log,"[debug] This is a debug"); + sinon.assert.neverCalledWithMatch(console.log,"[trace] This is a trace"); + sinon.assert.calledWithMatch(console.log,"[metric] "); }); it('default settings set to INFO and metrics off', function() { @@ -136,12 +137,12 @@ describe("@node-red/util/log", function() { log.debug("This is a debug"); log.trace("This is a trace"); log.log({level:log.METRIC,msg:"testMetric"}); - sinon.assert.calledWithMatch(util.log,"[error] This is an error"); - sinon.assert.calledWithMatch(util.log,"[warn] This is a warn"); - sinon.assert.calledWithMatch(util.log,"[info] This is an info"); - sinon.assert.neverCalledWithMatch(util.log,"[debug] This is a debug"); - sinon.assert.neverCalledWithMatch(util.log,"[trace] This is a trace"); - sinon.assert.neverCalledWithMatch(util.log,"[metric] "); + sinon.assert.calledWithMatch(console.log,"[error] This is an error"); + sinon.assert.calledWithMatch(console.log,"[warn] This is a warn"); + sinon.assert.calledWithMatch(console.log,"[info] This is an info"); + sinon.assert.neverCalledWithMatch(console.log,"[debug] This is a debug"); + sinon.assert.neverCalledWithMatch(console.log,"[trace] This is a trace"); + sinon.assert.neverCalledWithMatch(console.log,"[metric] "); }); it('no logger used if custom logger handler does not exist', function() { var settings = {logging: { customLogger: { level: 'trace', metrics: true } } }; @@ -152,12 +153,12 @@ describe("@node-red/util/log", function() { log.debug("This is a debug"); log.trace("This is a trace"); log.log({level:log.METRIC,msg:"testMetric"}); - sinon.assert.neverCalledWithMatch(util.log,"[error] This is an error"); - sinon.assert.neverCalledWithMatch(util.log,"[warn] This is a warn"); - sinon.assert.neverCalledWithMatch(util.log,"[info] This is an info"); - sinon.assert.neverCalledWithMatch(util.log,"[debug] This is a debug"); - sinon.assert.neverCalledWithMatch(util.log,"[trace] This is a trace"); - sinon.assert.neverCalledWithMatch(util.log,"[metric] "); + sinon.assert.neverCalledWithMatch(console.log,"[error] This is an error"); + sinon.assert.neverCalledWithMatch(console.log,"[warn] This is a warn"); + sinon.assert.neverCalledWithMatch(console.log,"[info] This is an info"); + sinon.assert.neverCalledWithMatch(console.log,"[debug] This is a debug"); + sinon.assert.neverCalledWithMatch(console.log,"[trace] This is a trace"); + sinon.assert.neverCalledWithMatch(console.log,"[metric] "); }); it('add a custom log handler directly', function() { @@ -244,7 +245,7 @@ describe("@node-red/util/log", function() { }, }; var ret = log.info(msg.msg); - sinon.assert.calledWithMatch(util.log,"my special message"); + sinon.assert.calledWithMatch(console.log,"my special message"); }); diff --git a/test/unit/@node-red/util/lib/util_spec.js b/test/unit/@node-red/util/lib/util_spec.js index e48e9fc84..3a52939f8 100644 --- a/test/unit/@node-red/util/lib/util_spec.js +++ b/test/unit/@node-red/util/lib/util_spec.js @@ -518,8 +518,8 @@ describe("@node-red/util/util", function() { } function testToString(input,msg,expected) { var result = util.normalisePropertyExpression(input,msg,true); - console.log("+",input); - console.log(result); + // console.log("+",input); + // console.log(result); result.should.eql(expected); } it('pass a.b.c',function() { testABC('a.b.c',['a','b','c']); }) @@ -784,9 +784,14 @@ describe("@node-red/util/util", function() { var result = util.encodeObject(msg); result.format.should.eql("error"); var resultJson = JSON.parse(result.msg); - resultJson.name.should.eql('encodeError'); - resultJson.message.should.eql('encode error'); + resultJson.should.have.property("__enc__",true); + resultJson.should.have.property("type","error"); + resultJson.should.have.property("data"); + resultJson.data.should.have.property("name","encodeError") + resultJson.data.should.have.property("message","encode error") + resultJson.data.should.have.property("stack") }); + it('encodes Error without message', function() { var err = new Error(); err.name = 'encodeError'; @@ -795,8 +800,12 @@ describe("@node-red/util/util", function() { var result = util.encodeObject(msg); result.format.should.eql("error"); var resultJson = JSON.parse(result.msg); - resultJson.name.should.eql('encodeError'); - resultJson.message.should.eql('error message'); + resultJson.should.have.property("__enc__",true); + resultJson.should.have.property("type","error"); + resultJson.should.have.property("data"); + resultJson.data.should.have.property("name","encodeError") + resultJson.data.should.have.property("message","error message") + resultJson.data.should.have.property("stack") }); it('encodes Buffer', function() { var msg = {msg:Buffer.from("abc")}; @@ -988,7 +997,13 @@ describe("@node-red/util/util", function() { var result = util.encodeObject(msg); result.format.should.eql("array[1]"); var resultJson = JSON.parse(result.msg); - resultJson[0].should.eql('Error: encode error'); + resultJson[0].should.have.property("__enc__",true); + resultJson[0].should.have.property("type","error"); + resultJson[0].should.have.property("data"); + resultJson[0].data.should.have.property("name","Error") + resultJson[0].data.should.have.property("message","encode error") + resultJson[0].data.should.have.property("stack") + }); it('long array in msg', function() { var msg = {msg:{array:[1,2,3,4]}}; @@ -1074,7 +1089,7 @@ describe("@node-red/util/util", function() { var resultJson = JSON.parse(result.msg); resultJson.socket.should.eql('[internal]'); }); - it('object which fails to serialise', function(done) { + it('object which fails to serialise', function() { var msg = { msg: { obj:{ @@ -1093,13 +1108,13 @@ describe("@node-red/util/util", function() { }; var result = util.encodeObject(msg); result.format.should.eql("error"); - var success = (result.msg.indexOf('cantserialise') > 0); - success &= (result.msg.indexOf('this exception should have been caught') > 0); - success &= (result.msg.indexOf('canserialise') > 0); - success.should.eql(1); - done(); + const resultJson = JSON.parse(result.msg); + resultJson.should.have.property("__enc__",true); + resultJson.should.have.property("type","error"); + resultJson.should.have.property("data"); + resultJson.data.should.have.property("message","encodeObject Error: this exception should have been caught") }); - it('object which fails to serialise - different error type', function(done) { + it('object which fails to serialise - different error type', function() { var msg = { msg: { obj:{ @@ -1116,45 +1131,15 @@ describe("@node-red/util/util", function() { }, } }; - var result = util.encodeObject(msg); + const result = util.encodeObject(msg); result.format.should.eql("error"); - var success = (result.msg.indexOf('cantserialise') > 0); - success &= (result.msg.indexOf('this exception should have been caught') > 0); - success &= (result.msg.indexOf('canserialise') > 0); - success.should.eql(1); - done(); + const resultJson = JSON.parse(result.msg); + resultJson.should.have.property("__enc__",true); + resultJson.should.have.property("type","error"); + resultJson.should.have.property("data"); + resultJson.data.should.have.property("message","encodeObject Error: this exception should have been caught") }); - it('very large object which fails to serialise should be truncated', function(done) { - var msg = { - msg: { - obj:{ - big:"", - cantserialise:{ - message:'this will not be displayed', - toJSON: function(val) { - throw new Error('this exception should have been caught'); - return 'should not display because we threw first'; - }, - }, - canserialise:{ - message:'this should be displayed', - } - }, - } - }; - - for (var i = 0; i < 1000; i++) { - msg.msg.obj.big += 'some more string '; - } - - var result = util.encodeObject(msg); - result.format.should.eql("error"); - var resultJson = JSON.parse(result.msg); - var success = (resultJson.message.length <= 1000); - success.should.eql(true); - done(); - }); - it('test bad toString', function(done) { + it('test bad toString', function() { var msg = { msg: { mystrangeobj:"hello", @@ -1166,25 +1151,12 @@ describe("@node-red/util/util", function() { msg.msg.constructor = { name: "strangeobj" }; var result = util.encodeObject(msg); - var success = (result.msg.indexOf('[Type not printable]') >= 0); - success.should.eql(true); - done(); + const resultJson = JSON.parse(result.msg); + resultJson.should.have.property("__enc__",true); + resultJson.should.have.property("type","error"); + resultJson.should.have.property("data"); + resultJson.data.should.have.property("message","[Type not serializable]") }); - it('test bad object constructor', function(done) { - var msg = { - msg: { - mystrangeobj:"hello", - constructor: { - get name(){ - throw new Error('Exception in constructor name'); - } - } - }, - }; - var result = util.encodeObject(msg); - done(); - }); - }); });