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") {
$('
'+
'