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/.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 f705e0752..d2fb401b6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-red", - "version": "4.0.2", + "version": "4.0.9", "description": "Low-code programming for event-driven applications", "homepage": "https://nodered.org", "license": "Apache-2.0", @@ -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 792b5b67f..62298a003 100644 --- a/packages/node_modules/@node-red/editor-api/package.json +++ b/packages/node_modules/@node-red/editor-api/package.json @@ -1,6 +1,6 @@ { "name": "@node-red/editor-api", - "version": "4.0.2", + "version": "4.0.9", "license": "Apache-2.0", "main": "./lib/index.js", "repository": { @@ -16,14 +16,14 @@ } ], "dependencies": { - "@node-red/util": "4.0.2", - "@node-red/editor-client": "4.0.2", + "@node-red/util": "4.0.9", + "@node-red/editor-client": "4.0.9", "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 3f6617752..9bdd55155 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 @@ -562,7 +562,9 @@ "types": { "local": "Local", "examples": "Examples" - } + }, + "type": "Type", + "name": "Name" }, "palette": { "noInfo": "no information available", 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..eb2ef79d2 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", @@ -303,7 +304,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 +373,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 +387,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 +562,9 @@ "types": { "local": "Local", "examples": "Ejemplos" - } + }, + "type": "Tipo", + "name": "Nombre" }, "palette": { "noInfo": "no hay información disponible", @@ -613,6 +626,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 +655,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 +670,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 +684,8 @@ "review": "Abrir información del nodo", "install": "Instalar", "remove": "Eliminar", - "update": "Actualizar" + "update": "Actualizar", + "understood": "Entendido" } } } @@ -718,6 +738,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 +813,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 +935,8 @@ } }, "typedInput": { + "selected": "__count__ seleccionado", + "selected_plural": "__count__ seleccionados", "type": { "str": "texto", "num": "número", @@ -923,7 +947,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 +1236,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/ja/editor.json b/packages/node_modules/@node-red/editor-client/locales/ja/editor.json index 5b11e24c4..c0296f457 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 @@ -562,7 +562,9 @@ "types": { "local": "ローカル", "examples": "サンプル" - } + }, + "type": "型", + "name": "名前" }, "palette": { "noInfo": "情報がありません", diff --git a/packages/node_modules/@node-red/editor-client/package.json b/packages/node_modules/@node-red/editor-client/package.json index 932afb7ac..ef2600a54 100644 --- a/packages/node_modules/@node-red/editor-client/package.json +++ b/packages/node_modules/@node-red/editor-client/package.json @@ -1,6 +1,6 @@ { "name": "@node-red/editor-client", - "version": "4.0.2", + "version": "4.0.9", "license": "Apache-2.0", "repository": { "type": "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..2b95b35d3 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,61 @@ 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); + } + } + }); + } + 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; @@ -536,6 +587,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