Compare commits

..

No commits in common. "master" and "4.0.1" have entirely different histories.

96 changed files with 907 additions and 2261 deletions

1
.gitignore vendored
View File

@ -28,4 +28,3 @@ docs
.nyc_output
sync.ffs_db
package-lock.json
.editorconfig

4
.nodemonignore Normal file
View File

@ -0,0 +1,4 @@
/Gruntfile.js
/.git/*
*.backup
/public/*

View File

@ -1,149 +1,3 @@
#### 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
- Use a more subtle border on the header (#4818) @bonanitech
- Improve the editor's French translations (#4824) @GogoVega
- Clean up orphaned editors (#4821) @Steve-Mcl
- Fix node validation if the property is not required (#4812) @GogoVega
- Ensure mermaid.min.js is cached properly between loads of the editor (#4817) @knolleary
Runtime
- Allow auth cookie name to be customised (#4815) @knolleary
- Guard against undefined sessions in multiplayer (#4816) @knolleary
#### 4.0.1: Maintenance Release
Editor

View File

@ -1,7 +0,0 @@
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"

View File

@ -1,16 +0,0 @@
{
"ignoreRoot": [
".git",
".nyc_output",
".sass-cache",
"bower-components",
"coverage"
],
"ignore": [
"/Gruntfile.js",
"/.git/*",
"*.backup",
"/public/*"
]
}

View File

@ -1,6 +1,6 @@
{
"name": "node-red",
"version": "4.0.9",
"version": "4.0.1",
"description": "Low-code programming for event-driven applications",
"homepage": "https://nodered.org",
"license": "Apache-2.0",
@ -26,26 +26,26 @@
}
],
"dependencies": {
"acorn": "8.12.1",
"acorn-walk": "8.3.4",
"ajv": "8.17.1",
"acorn": "8.11.3",
"acorn-walk": "8.3.2",
"ajv": "8.14.0",
"async-mutex": "0.5.0",
"basic-auth": "2.0.1",
"bcryptjs": "2.4.3",
"body-parser": "1.20.3",
"body-parser": "1.20.2",
"cheerio": "1.0.0-rc.10",
"clone": "2.1.2",
"content-type": "1.0.5",
"cookie": "0.7.2",
"cookie-parser": "1.4.7",
"cookie": "0.6.0",
"cookie-parser": "1.4.6",
"cors": "2.8.5",
"cronosjs": "1.7.1",
"denque": "2.1.0",
"express": "4.21.2",
"express-session": "1.18.1",
"express": "4.19.2",
"express-session": "1.18.0",
"form-data": "4.0.0",
"fs-extra": "11.2.0",
"got": "12.6.1",
"got": "12.6.0",
"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.46",
"moment-timezone": "0.5.45",
"mqtt": "5.7.0",
"multer": "1.4.5-lts.1",
"mustache": "4.2.0",
"node-red-admin": "^4.0.1",
"node-red-admin": "^4.0.0",
"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": "3.0.0",
"raw-body": "2.5.2",
"rfdc": "^1.3.1",
"semver": "7.6.3",
"tar": "7.4.3",
"tough-cookie": "^5.0.0",
"semver": "7.5.4",
"tar": "7.2.0",
"tough-cookie": "4.1.4",
"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.5.7",
"dompurify": "2.4.1",
"grunt": "1.6.1",
"grunt-chmod": "~1.1.1",
"grunt-cli": "~1.5.0",
"grunt-cli": "~1.4.3",
"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": "3.0.0",
"grunt-jsonlint": "2.1.3",
"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": "11.3.0",
"mermaid": "^10.4.0",
"minami": "1.2.3",
"mocha": "9.2.2",
"node-red-node-test-helper": "^0.3.3",
"nodemon": "3.1.7",
"nodemon": "2.0.20",
"proxy": "^1.0.2",
"sass": "1.62.1",
"should": "13.2.3",

View File

@ -126,14 +126,6 @@ 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);
}
@ -190,10 +182,6 @@ function genericStrategy(adminApp,strategy) {
maxAge: null,
...settings.httpAdminCookieOptions
}
if (sessionOptions.cookie.name){
sessionOptions.name = sessionOptions.cookie.name
delete sessionOptions.cookie.name
}
}
adminApp.use(session(sessionOptions));
//TODO: all passport references ought to be in ./auth
@ -229,10 +217,10 @@ function genericStrategy(adminApp,strategy) {
adminApp.get('/auth/strategy',
passport.authenticate(strategy.name, {
session:false,
failWithError: true,
failureMessage: true
failureMessage: true,
failureRedirect: settings.httpAdminRoot + '?session_message=Login Failed'
}),
completeGenericStrategyAuth,
completeGenerateStrategyAuth,
handleStrategyError
);
@ -244,14 +232,14 @@ function genericStrategy(adminApp,strategy) {
passport.authenticate(strategy.name, {
session:false,
failureMessage: true,
failWithError: true
failureRedirect: settings.httpAdminRoot + '?session_message=Login Failed'
}),
completeGenericStrategyAuth,
completeGenerateStrategyAuth,
handleStrategyError
);
}
function completeGenericStrategyAuth(req,res) {
function completeGenerateStrategyAuth(req,res) {
var tokens = req.user.tokens;
delete req.user.tokens;
// Successful authentication, redirect home.
@ -261,8 +249,6 @@ function handleStrategyError(err, req, res, next) {
if (res.headersSent) {
return next(err)
}
// Remove the header that passport auto-adds as we don't need it
res.removeHeader('WWW-Authenticate')
log.audit({event: "auth.login.fail.oauth",error:err.toString()});
res.redirect(settings.httpAdminRoot + '?session_message='+err.toString());
}

View File

@ -185,12 +185,13 @@ 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 = theme.deployButton.type;
themeSettings.deployButton = {
type: "simple"
}
if (theme.deployButton.label) {
themeSettings.deployButton.label = theme.deployButton.label;
}
if (theme.deployButton.icon) {
url = serveFile(themeApp,"/deploy/",theme.deployButton.icon);
if (url) {
@ -205,26 +206,14 @@ module.exports = {
}
if (theme.login) {
let themeContextLogin = {}
let hasLoginTheme = false
if (theme.login.image) {
url = serveFile(themeApp,"/login/",theme.login.image);
if (url) {
themeContextLogin.image = url
hasLoginTheme = true
themeContext.login = {
image: url
}
}
}
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"});

View File

@ -1,6 +1,6 @@
{
"name": "@node-red/editor-api",
"version": "4.0.9",
"version": "4.0.1",
"license": "Apache-2.0",
"main": "./lib/index.js",
"repository": {
@ -16,14 +16,14 @@
}
],
"dependencies": {
"@node-red/util": "4.0.9",
"@node-red/editor-client": "4.0.9",
"@node-red/util": "4.0.1",
"@node-red/editor-client": "4.0.1",
"bcryptjs": "2.4.3",
"body-parser": "1.20.3",
"body-parser": "1.20.2",
"clone": "2.1.2",
"cors": "2.8.5",
"express-session": "1.18.1",
"express": "4.21.2",
"express-session": "1.18.0",
"express": "4.19.2",
"memorystore": "1.6.7",
"mime": "3.0.0",
"multer": "1.4.5-lts.1",

View File

@ -375,10 +375,7 @@
"flowAdded": "flow added",
"moved": "moved",
"movedTo": "moved to __id__",
"movedFrom": "moved from __id__",
"none": "none",
"position": "position",
"wires": "wires"
"movedFrom": "moved from __id__"
},
"nodeCount": "__count__ node",
"nodeCount_plural": "__count__ nodes",
@ -387,14 +384,9 @@
"reviewChanges": "Review Changes",
"noBinaryFileShowed": "Cannot show binary file contents",
"viewCommitDiff": "View Commit Changes",
"commit": "Commit",
"compareChanges": "Compare Changes",
"saveConflict": "Save conflict resolution",
"conflictHeader": "<span>__resolved__</span> of <span>__unresolved__</span> 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:"
@ -562,9 +554,7 @@
"types": {
"local": "Local",
"examples": "Examples"
},
"type": "Type",
"name": "Name"
}
},
"palette": {
"noInfo": "no information available",
@ -813,7 +803,6 @@
"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",

View File

@ -27,8 +27,7 @@
"lock": "Bloquear",
"unlock": "Desbloquear",
"locked": "Bloqueado",
"unlocked": "Desbloqueado",
"format": "Formato"
"unlocked": "Desbloqueado"
},
"type": {
"string": "texto",
@ -304,8 +303,7 @@
"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.",
"alreadyExists": "Este nodo ya existe"
"conflictNotification2": "Selecciona qué nodos importar y si reemplazar los nodos existentes o importar una copia de los mismos."
},
"copyMessagePath": "Ruta copiada",
"copyMessageValue": "Valor copiado",
@ -373,12 +371,8 @@
"deleted": "eliminado",
"flowDeleted": "flujo eliminado",
"flowAdded": "flujo añadido",
"moved": "movido",
"movedTo": "movido a __id__",
"movedFrom": "movido desde __id__",
"none": "ninguno",
"position": "posición",
"wires": "conectores"
"movedFrom": "movido desde __id__"
},
"nodeCount": "__count__ nodo",
"nodeCount_plural": "__count__ nodos",
@ -387,14 +381,9 @@
"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": "<span>__resolved__</span> de <span>__unresolved__</span> 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:"
@ -562,9 +551,7 @@
"types": {
"local": "Local",
"examples": "Ejemplos"
},
"type": "Tipo",
"name": "Nombre"
}
},
"palette": {
"noInfo": "no hay información disponible",
@ -626,8 +613,6 @@
},
"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",
@ -655,7 +640,6 @@
"errors": {
"catalogLoadFailed": "<p>La carga del catálogo de nodos ha fallado</p><p>Revise la consola del navegador para mas información</p>",
"installFailed": "<p>Fallo al instalar: __module__</p><p>__message__</p><p>Revise el log para mas información</p>",
"installTimeout": "<p>La instalación continúa en segundo plano.</p><p>Los nodos aparecerán en la paleta cuando finalice. Consulta el registro para obtener más información.</p>",
"removeFailed": "<p>Fallo al eliminar: __module__</p><p>__message__</p><p>Revise el log para mas información</p>",
"updateFailed": "<p>Fallo al actualizar: __module__</p><p>__message__</p><p>Revise el log para mas información</p>",
"enableFailed": "<p>Fallo al activar: __module__</p><p>__message__</p><p>Revise el log para mas información</p>",
@ -670,9 +654,6 @@
"body":"<p>Eliminando '__module__'</p><p>La eliminación del nodo lo desinstalará de Node-RED. Es posible que el nodo siga utilizando recursos hasta que Node-RED sea reiniciado.</p>",
"title": "Eliminar nodos"
},
"removePlugin": {
"body": "<p>Extensión __module__ eliminada. Vuelve a cargar el editor para borrar los elementos sobrantes.</p>"
},
"update": {
"body":"<p>Actualizando '__module__'</p><p>La actualización del nodo requerirá un reinicio manual de Node-RED para completarse. Debe ser reiniciado manualmente.</p>",
"title": "Actualizar nodos"
@ -684,8 +665,7 @@
"review": "Abrir información del nodo",
"install": "Instalar",
"remove": "Eliminar",
"update": "Actualizar",
"understood": "Entendido"
"update": "Actualizar"
}
}
}
@ -738,7 +718,6 @@
"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"
@ -813,7 +792,6 @@
"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",
@ -935,8 +913,6 @@
}
},
"typedInput": {
"selected": "__count__ seleccionado",
"selected_plural": "__count__ seleccionados",
"type": {
"str": "texto",
"num": "número",
@ -947,14 +923,7 @@
"date": "marca tiempo",
"jsonata": "expresión",
"env": "variable de entorno",
"cred": "credencial",
"conf-types": "nodo configuración"
},
"date": {
"format": {
"timestamp": "milisegundos desde epoch",
"object": "Objeto de fecha de JavaScript"
}
"cred": "credencial"
}
},
"editableList": {
@ -1236,18 +1205,6 @@
"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__",

View File

@ -27,8 +27,7 @@
"lock": "Verrouiller",
"unlock": "Déverrouiller",
"locked": "Verrouillé",
"unlocked": "Déverrouillé",
"format": "Format"
"unlocked": "Déverrouillé"
},
"type": {
"string": "chaîne de caractères",
@ -55,10 +54,10 @@
"workspace": {
"defaultName": "Flux __number__",
"editFlow": "Modifier le flux : __name__",
"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",
"confirmDelete": "Confirmation de la suppression",
"delete": "Etes-vous sûr de vouloir supprimer '__label__'?",
"dropFlowHere": "Déposer le flux ici",
"dropImageHere": "Déposer l'image ici",
"addFlow": "Ajouter un flux",
"addFlowToRight": "Ajouter un flux à droite",
"closeFlow": "Fermer le flux",
@ -75,7 +74,7 @@
"enabled": "Activé",
"disabled": "Désactivé",
"info": "Description",
"selectNodes": "Cliquer pour sélectionner",
"selectNodes": "Cliquer sur les noeuds pour sélectionner",
"enableFlow": "Activer le flux",
"disableFlow": "Désactiver le flux",
"lockFlow": "Verrouiller le flux",
@ -99,7 +98,7 @@
"rtl": "De droite à gauche",
"auto": "Contextuel",
"language": "Langue",
"browserDefault": "Par défaut du Navigateur"
"browserDefault": "Navigateur par défaut"
},
"sidebar": {
"show": "Afficher la barre latérale"
@ -135,7 +134,7 @@
"disableSelectedNodes": "Désactiver les noeuds sélectionnés",
"showSelectedNodeLabels": "Afficher les étiquettes des noeuds sélectionnés",
"hideSelectedNodeLabels": "Masquer les étiquettes des noeuds sélectionnés",
"showWelcomeTours": "Afficher les visites guidées des nouvelles versions",
"showWelcomeTours": "Afficher les visites guidées pour les nouvelles versions",
"help": "Site web de Node-RED",
"projects": "Projets",
"projects-new": "Nouveau projet",
@ -144,7 +143,7 @@
"showNodeLabelDefault": "Afficher l'étiquette des noeuds nouvellement ajoutés",
"codeEditor": "Éditeur de code",
"groups": "Groupes",
"groupSelection": "Grouper la sélection",
"groupSelection": "Grouper cette sélection",
"ungroupSelection": "Dégrouper la sélection",
"groupMergeSelection": "Fusionner la sélection",
"groupRemoveSelection": "Supprimer du groupe",
@ -156,7 +155,7 @@
"alignMiddle": "Aligner au milieu",
"alignBottom": "Aligner en bas",
"distributeHorizontally": "Répartir horizontalement",
"distributeVertically": "Répartir verticalement",
"distributeVertically": "Distribuer verticalement",
"moveToBack": "Déplacer vers l'arrière",
"moveToFront": "Déplacer vers l'avant",
"moveBackwards": "Reculer",
@ -164,21 +163,21 @@
}
},
"actions": {
"toggle-navigator": "Basculer l'affichage du navigateur",
"zoom-out": "Réduire",
"zoom-reset": "Réinitialiser",
"toggle-navigator": "Basculer de navigateur",
"zoom-out": "Dézoomer",
"zoom-reset": "Réinitialiser le zoom",
"zoom-in": "Agrandir",
"search-flows": "Rechercher le flux",
"search-prev": "Précédent",
"search-next": "Suivant",
"search-counter": "\"__term__\" __result__ sur __count__"
"search-counter": "\"__term__\" __result__ de __count__"
},
"user": {
"loggedInAs": "Connecté en tant que __name__",
"username": "Nom d'utilisateur",
"password": "Mot de passe",
"login": "Se connecter",
"loginFailed": "Échec de connexion",
"login": "Connexion",
"loginFailed": "Échec de la connexion",
"notAuthorized": "Pas autorisé",
"errors": {
"settings": "Vous devez être connecté pour accéder aux paramètres",
@ -194,16 +193,16 @@
"warning": "<strong>Attention</strong> : __message__",
"warnings": {
"undeployedChanges": "Le noeud a des modifications non déployées",
"nodeActionDisabled": "Les actions du noeud sont désactivées",
"nodeActionDisabledSubflow": "Les actions de noeud sont désactivées à l'intérieur du sous-flux",
"nodeActionDisabled": "Actions de noeud désactivées",
"nodeActionDisabledSubflow": "Actions de noeud désactivées dans le sous-flux",
"missing-types": "<p>Flux arrêtés en raison de types de noeuds manquants.</p>",
"missing-modules": "<p>Flux arrêtés en raison de modules manquants.</p>",
"safe-mode": "<p>Flux arrêtés en mode sans échec.</p><p>Vous pouvez modifier vos flux et déployer ensuite les changements afin de démarrer vos flux.</p>",
"safe-mode": "<p>Flux arrêtés en mode sans échec.</p><p>Vous pouvez modifier vos flux et déployer les changements pour redémarrer.</p>",
"restartRequired": "Node-RED doit être redémarré pour mettre à jour les modules",
"credentials_load_failed": "<p>Les flux se sont arrêtés car les informations d'identification n'ont pas pu être déchiffrées.</p><p>Le fichier d'informations d'identification du flux est chiffré mais la clé de chiffrement du projet est manquante ou invalide.</p>",
"credentials_load_failed_reset": "<p>Les informations d'identification n'ont pas pu être déchiffrées</p><p>Le fichier d'informations d'identification du flux est chiffré mais la clé de chiffrement du projet est manquante ou invalide.</p><p>Le fichier d'informations d'identification du flux sera réinitialisé lors du prochain déploiement. Toutes les informations d'identification des flux existants seront perdues.</p>",
"credentials_load_failed": "<p>Les flux se sont arrêtés car les informations d'identification n'ont pas pu être déchiffrées.</p><p>Le fichier d'informations d'identification du flux est chiffré, mais la clé de chiffrement du projet est manquante ou invalide.</p>",
"credentials_load_failed_reset": "<p>Les informations d'identification n'ont pas pu être déchiffrées</p><p>Le fichier d'informations d'identification du flux est chiffré, mais la clé de chiffrement du projet est manquante ou invalide.</p><p>Le fichier d'informations d'identification du flux sera réinitialisé lors du prochain déploiement. Toutes les informations d'identification de flux existantes seront perdues.</p>",
"missing_flow_file": "<p>Fichier contenant les flux introuvable.</p><p>Le projet n'est pas configuré avec un fichier de flux.</p>",
"missing_package_file": "<p>Fichier de paquetage du projet introuvable.</p><p>Il manque au projet le fichier <code>package.json</code>.</p>",
"missing_package_file": "<p>Fichier de paquetage du projet introuvable.</p><p>Il manque au projet un fichier package.json.</p>",
"project_empty": "<p>Le projet est vide.</p><p>Voulez-vous créer un ensemble de fichiers de projet par défaut ?<br/>Sinon, vous devrez ajouter manuellement des fichiers au projet (en dehors de l'éditeur).</p>",
"project_not_found": "<p>Le projet '__project__' est introuvable.</p>",
"git_merge_conflict": "<p>La fusion automatique des modifications a échoué.</p><p>Corriger les conflits non fusionnés, puis valider le résultat.</p>"
@ -220,7 +219,7 @@
},
"project": {
"change-branch": "Changer pour une branche locale '__project__'",
"merge-abort": "Fusion Git abandonnée",
"merge-abort": "Git fusion abandonnée",
"loaded": "Projet '__project__' chargé",
"updated": "Projet '__project__' mis à jour",
"pull": "Projet '__project__' rechargé",
@ -353,7 +352,7 @@
"backgroundUpdate": "Les flux sur le serveur ont été mis à jour.",
"conflictChecking": "Vérifier si les modifications peuvent être fusionnées automatiquement",
"conflictAutoMerge": "Les modifications n'incluent aucun conflit et peuvent être fusionnées automatiquement.",
"conflictManualMerge": "Les modifications incluent des conflits qui doivent être résolus avant de pouvoir être déployées.",
"conflictManualMerge": "Les changements incluent des conflits qui doivent être résolus avant de pouvoir être déployés.",
"plusNMore": "+ __count__ en plus"
}
},
@ -373,28 +372,19 @@
"deleted": "supprimé",
"flowDeleted": "flux supprimé",
"flowAdded": "flux ajouté",
"moved": "déplacé",
"movedTo": "déplacé vers __id__",
"movedFrom": "déplacé depuis __id__",
"none": "aucun",
"position": "position",
"wires": "câbles"
"movedFrom": "déplacé depuis __id__"
},
"nodeCount": "__count__ noeud",
"nodeCount_plural": "__count__ noeuds",
"local": "Changements locaux",
"remote": "Changements distants",
"remote": "Modifications à distance",
"reviewChanges": "Examiner les modifications",
"noBinaryFileShowed": "Impossible d'afficher le contenu du fichier binaire",
"viewCommitDiff": "Afficher les modifications de la validation",
"commit": "Validation",
"viewCommitDiff": "Afficher les modifications de validation",
"compareChanges": "Comparer les modifications",
"saveConflict": "Enregistrer la résolution des conflits",
"conflictHeader": "<span>__resolved__</span> sur <span>__unresolved__</span> 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 :"
@ -405,9 +395,9 @@
"edit": "Modifier le modèle du sous-flux",
"subflowInstances": "Il existe __count__ instance de ce modèle de sous-flux",
"subflowInstances_plural": "Il existe __count__ instances de ce modèle de sous-flux",
"editSubflowProperties": "Modifier les propriétés",
"input": "Entrées :",
"output": "Sorties :",
"editSubflowProperties": "modifier les propriétés",
"input": "Entrées:",
"output": "Sorties:",
"status": "Statut du noeud",
"deleteSubflow": "Supprimer le sous-flux",
"confirmDelete": "Voulez-vous vraiment supprimer ce sous-flux ?",
@ -421,7 +411,7 @@
"version": "Version",
"versionPlaceholder": "x.y.z",
"keys": "Mots clés",
"keysPlaceholder": "Mots clés séparés par une virgule",
"keysPlaceholder": "Mots clés séparés par des virgules",
"author": "Auteur",
"authorPlaceholder": "Votre nom <email@exemple.com>",
"desc": "Description",
@ -478,7 +468,7 @@
"select": "sélection",
"checkbox": "case à cocher",
"spinner": "valeurs à défiler",
"none": "aucun",
"none": "aucune",
"hidden": "masquer la propriété"
},
"types": {
@ -506,7 +496,7 @@
"max": "Maximum"
},
"errors": {
"scopeChange": "La modification de la portée rendra indisponible ce noeud de configuration aux noeuds d'autres flux qui l'utilisent",
"scopeChange": "La modification de la portée la rendra indisponible pour les noeuds d'autres flux qui l'utilisent",
"invalidProperties": "Propriétés invalides :",
"credentialLoadFailed": "Échec du chargement des identifiants du noeud"
}
@ -520,7 +510,7 @@
"unassigned": "Non attribué",
"global": "Global",
"workspace": "Espace de travail",
"editor": "Boîte d'édition",
"editor": "Boîte de dialogue d'édition",
"selectAll": "Tout sélectionner",
"selectNone": "Ne rien sélectionner",
"selectAllConnected": "Sélectionner tous les éléments connectés",
@ -551,7 +541,7 @@
"openLibrary": "Ouvrir la bibliothèque...",
"saveToLibrary": "Enregistrer dans la bibliothèque...",
"typeLibrary": "__type__ bibliothèque",
"unnamedType": "Sans nom __type__",
"unnamedType": "Innomé __type__",
"exportedToLibrary": "Noeuds exportés vers la bibliothèque",
"dialogSaveOverwrite": "Une __libraryType__ appelée __libraryName__ existe déjà. Écraser ?",
"invalidFilename": "Nom de fichier non valide",
@ -562,15 +552,13 @@
"types": {
"local": "Local",
"examples": "Exemples"
},
"type": "Type",
"name": "Nom"
}
},
"palette": {
"noInfo": "Pas d'information disponible",
"filter": "Rechercher le noeud",
"search": "Rechercher les modules",
"addCategory": "Ajouter une nouvelle...",
"addCategory": "Ajouter un nouveau...",
"label": {
"subflows": "Sous-flux",
"network": "Réseau",
@ -650,7 +638,7 @@
"sortAZ": "A-Z",
"sortRecent": "Récent",
"more": "+ __count__ en plus",
"upload": "Charger le fichier .tgz du module",
"upload": "Charger le fichier tgz du module",
"refresh": "Actualiser la liste des modules",
"errors": {
"catalogLoadFailed": "<p>Échec du chargement du catalogue de noeuds.</p><p>Vérifier la console du navigateur pour plus d'informations</p>",
@ -663,7 +651,7 @@
},
"confirm": {
"install": {
"body": "<p>Installation de '__module__'</p><p>Avant l'installation, veuillez lire la documentation du noeud. Certains noeuds ont des dépendances qui ne peuvent pas être résolues automatiquement et peuvent nécessiter un redémarrage de Node-RED.</p>",
"body": "<p>Installation de '__module__'</p><p>Avant l'installation, veuiller lire la documentation du noeud. Certains noeuds ont des dépendances qui ne peuvent pas être résolues automatiquement et peuvent nécessiter un redémarrage de Node-RED.</p>",
"title": "Installer les noeuds"
},
"remove": {
@ -678,7 +666,7 @@
"title": "Mettre à jour les noeuds"
},
"cannotUpdate": {
"body": "Une mise à jour pour ce noeud est disponible, mais il n'est pas installé dans un emplacement que le gestionnaire de palette peut mettre à jour.<br/><br/>Veuillez vous référer à la documentation pour savoir comment mettre à jour ce noeud."
"body": "Une mise à jour pour ce noeud est disponible, mais il n'est pas installé dans un emplacement que le gestionnaire de palette peut mettre à jour.<br/><br/>Veuiller vous référer à la documentation pour savoir comment mettre à jour ce noeud."
},
"button": {
"review": "Ouvrir la documentation",
@ -720,8 +708,8 @@
"nodeHelp": "Aide sur les noeuds",
"none": "Aucun",
"arrayItems": "__count__ éléments",
"showTips": "Vous pouvez afficher les astuces à partir du panneau des paramètres",
"outline": "Contour",
"showTips": "Vous pouvez ouvrir les astuces à partir du panneau des paramètres",
"outline": "Plan",
"empty": "Vide",
"globalConfig": "Noeuds de configuration globale",
"triggerAction": "Déclencher une action",
@ -734,7 +722,7 @@
"help": {
"name": "Aide",
"label": "Aide",
"search": "Rechercher l'aide",
"search": "Aide à la recherche",
"nodeHelp": "Aide sur les noeuds",
"showHelp": "Afficher l'aide",
"showInOutline": "Afficher dans les grandes lignes",
@ -813,8 +801,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?",
"unmergedConfirm": "La branche locale '__name__' contient des modifications non fusionnées qui seront perdues. Etes-vous sûr de vouloir la supprimer?",
"deleteUnmergedBranch": "Supprimer la branche non fusionnée",
"gitRemotes": "Git distant",
"addRemote": "Ajout distant",
@ -858,17 +845,17 @@
"deleteConfirm": "Êtes-vous sûr de vouloir supprimer la clé SSH __name__ ? Ça ne peut pas être annulé."
},
"versionControl": {
"unstagedChanges": "Changements non indexés",
"stagedChanges": "Changements indexés",
"unstageChange": "Annuler l'indexation des changements",
"stageChange": "Indexer les changements",
"unstageAllChange": "Annuler l'indexation de tous les changements",
"stageAllChange": "Indexer tous les changements",
"unstagedChanges": "Abandon des changements",
"stagedChanges": "Changement mis en place",
"unstageChange": "Ne pas mettre en place le changement",
"stageChange": "Mettre en place le changement",
"unstageAllChange": "Ne pas mettre en place tous les changements",
"stageAllChange": "Mettre en place tous les changements",
"commitChanges": "Valider les changements",
"resolveConflicts": "Résoudre les conflits",
"head": "En-tête",
"staged": "Indexé",
"unstaged": "Non indexé",
"staged": "Mis en place",
"unstaged": "Non mis en place",
"local": "Local",
"remote": "Distant",
"revert": "Voulez-vous vraiment annuler les modifications apportées à '__file__' ? Ça ne peut pas être annulé.",
@ -902,11 +889,11 @@
"pushFailed": "L'envoi a échoué car la branche a des validations plus récentes. Tirer et fusionner d'abord, puis envoyer à nouveau.",
"push": "Envoyer",
"pull": "Tirer",
"unablePull": "<p>Impossible d'extraire les modifications à distance; vos modifications locales non mises en place seraient écrasées.</p><p>Valider vos modifications et réessayer.</p>",
"showUnstagedChanges": "Afficher les modifications non indexées",
"unablePull": "<p>Impossible d'extraire les modifications à distance ; vos modifications locales non mises en place seraient écrasées.</p><p>Valider vos modifications et réessayer.</p>",
"showUnstagedChanges": "Afficher les modifications non mise en place",
"connectionFailed": "Impossible de se connecter au référentiel distant: ",
"pullUnrelatedHistory": "<p>Le réferentiel distant a un historique de validations sans rapport.</p><p>Êtes-vous sûr de vouloir extraire les modifications dans votre référentiel local ?</p>",
"pullChanges": "Tirer les changements distants",
"pullChanges": "Tirer les changements",
"history": "Historique",
"projectHistory": "Historique du projet",
"daysAgo": "il y a __count__ jour",
@ -987,7 +974,7 @@
"result": "Résultat",
"format": "Format",
"compatMode": "Mode de compatibilité activé",
"compatModeDesc": "<h3>Mode de compatibilité JSONata</h3><p> L'expression actuelle semble toujours faire référence à <code>msg</code> et sera donc évaluée en mode de compatibilité. Veuillez mettre à jour l'expression pour ne pas utiliser <code>msg</code> car ce mode sera supprimé à l'avenir.</p><p> Lorsque la prise en charge de JSONata a été ajoutée pour la première fois à Node-RED, il fallait que l'expression référencie l'objet <code>msg</code>. Par exemple, <code>msg.payload</code> serait utilisé pour accéder à la charge utile.</p><p> Cela n'est plus nécessaire car l'expression sera évaluée directement par rapport au message. Pour accéder à la charge utile, l'expression doit être simplement <code>charge utile</code>.</p>",
"compatModeDesc": "<h3>Mode de compatibilité JSONata</h3><p> L'expression actuelle semble toujours faire référence à <code>msg</code> et sera donc évaluée en mode de compatibilité. Veuiller mettre à jour l'expression pour ne pas utiliser <code>msg</code> car ce mode sera supprimé à l'avenir.</p><p> Lorsque la prise en charge de JSONata a été ajoutée pour la première fois à Node-RED, il fallait que l'expression référencie l'objet <code>msg</code>. Par exemple, <code>msg.payload</code> serait utilisé pour accéder à la charge utile.</p><p> Cela n'est plus nécessaire car l'expression sera évaluée directement par rapport au message. Pour accéder à la charge utile, l'expression doit être simplement <code>charge utile</code>.</p>",
"noMatch": "Aucun résultat correspondant",
"errors": {
"invalid-expr": "Expression JSONata non valide :\n __message__",
@ -1010,7 +997,7 @@
},
"jsonEditor": {
"title": "Éditeur JSON",
"format": "Formatter JSON",
"format": "Format JSON",
"rawMode": "Modifier JSON",
"uiMode": "Afficher l'éditeur",
"rawMode-readonly": "JSON",
@ -1029,7 +1016,7 @@
"markdownEditor": {
"title": "Éditeur Markdown",
"expand": "Développer",
"format": "Formatter avec Markdown",
"format": "Formaté avec Markdown",
"heading1": "Rubrique 1",
"heading2": "Rubrique 2",
"heading3": "Rubrique 3",
@ -1103,7 +1090,7 @@
"credential-key": "Clé de chiffrement des identifiants",
"cant-get-ssh-key": "Erreur! Impossible d'obtenir le chemin de la clé SSH sélectionnée.",
"already-exists2": "Existe déjà",
"git-error": "Erreur Git",
"git-error": "Erreur git",
"connection-failed": "La connexion a échoué",
"not-git-repo": "Ce n'est pas un dépôt Git",
"repo-not-found": "Référentiel introuvable"
@ -1117,7 +1104,7 @@
"credentials-file": "Fichier d'identifiants"
},
"encryption-config": {
"setup": "Configuration du chiffrement de votre fichier d'informations d'identification",
"setup": "Configuration du chiffrage de votre fichier d'informations d'identification",
"desc0": "Votre fichier d'informations d'identification de flux peut être chiffré pour sécuriser son contenu.",
"desc1": "Si vous souhaitez stocker ces identifiants dans un référentiel Git public, vous devez les chiffrer en fournissant une phrase clé secrète.",
"desc2": "Votre fichier d'identifiants de flux n'est actuellement pas chiffré.",
@ -1174,9 +1161,9 @@
"add-ssh-key": "Ajouter une clé ssh",
"credentials-encryption-key": "Clé de chiffrement des identifiants",
"already-exists-2": "Existe déjà",
"git-error": "Erreur Git",
"git-error": "Erreur git",
"con-failed": "La connexion a échoué",
"not-git": "Ce n'est pas un dépôt Git",
"not-git": "Ce n'est pas un dépôt git",
"no-resource": "Référentiel introuvable",
"cant-get-ssh-key-path": "Erreur! Impossible d'obtenir le chemin de la clé SSH sélectionnée.",
"unexpected_error": "Erreur inattendue",
@ -1214,7 +1201,7 @@
},
"errors": {
"no-username-email": "Votre client Git n'est pas configuré avec un nom d'utilisateur/e-mail.",
"unexpected": "Une erreur inattendue est survenue",
"unexpected": "Une erreur inattendue est apparue",
"code": "Code"
}
},
@ -1283,7 +1270,7 @@
"list-modified-nodes": "Afficher les flux modifiés",
"list-hidden-flows": "Afficher les flux cachés",
"list-flows": "Lister les flux",
"list-subflows": "Lister les sous-flux",
"list-subflows": "Liste les sous-flux",
"go-to-previous-location": "Aller à l'emplacement précédent",
"go-to-next-location": "Aller à l'emplacement suivant",
"copy-selection-to-internal-clipboard": "Copier la sélection dans le presse-papiers",
@ -1343,8 +1330,8 @@
"align-selection-to-bottom": "Aligner la sélection vers le bas",
"align-selection-to-middle": "Aligner la sélection au centre verticalement",
"align-selection-to-center": "Aligner la sélection au centre horizontalement",
"distribute-selection-horizontally": "Répartir la sélection horizontalement",
"distribute-selection-vertical": "Répartir la sélection verticalement",
"distribute-selection-horizontally": "Distribuer la sélection horizontalement",
"distribute-selection-vertical": "Distribuer la sélection verticalement",
"wire-series-of-nodes": "Connecter les noeuds en série",
"wire-node-to-multiple": "Connecter les noeuds à plusieurs",
"wire-multiple-to-node": "Connecter plusieurs au noeud",

View File

@ -375,10 +375,7 @@
"flowAdded": "追加されたフロー",
"moved": "移動",
"movedTo": "__id__ へ移動",
"movedFrom": "__id__ から移動",
"none": "なし",
"position": "位置",
"wires": "ワイヤー"
"movedFrom": "__id__ から移動"
},
"nodeCount": "__count__ 個のノード",
"nodeCount_plural": "__count__ 個のノード",
@ -387,14 +384,9 @@
"reviewChanges": "変更を表示",
"noBinaryFileShowed": "バイナリファイルの中身は表示することができません",
"viewCommitDiff": "コミットの内容を表示",
"commit": "コミット",
"compareChanges": "変更を比較",
"saveConflict": "解決して保存",
"conflictHeader": "<span>__unresolved__</span> 個中 <span>__resolved__</span> 個のコンフリクトを解決",
"localChanges": "ローカルの変更",
"remoteChanges": "リモートの変更",
"useLocalChanges": "ローカルの変更を使用",
"useRemoteChanges": "リモートの変更を使用",
"commonVersionError": "共通バージョンは正しいJSON形式ではありません:",
"oldVersionError": "古いバージョンは正しいJSON形式ではありません:",
"newVersionError": "新しいバージョンは正しいJSON形式ではありません:"
@ -562,9 +554,7 @@
"types": {
"local": "ローカル",
"examples": "サンプル"
},
"type": "型",
"name": "名前"
}
},
"palette": {
"noInfo": "情報がありません",
@ -813,7 +803,6 @@
"branches": "ブランチ",
"noBranches": "ブランチなし",
"deleteConfirm": "本当にローカルブランチ'__name__'を削除しますか?削除すると元に戻すことはできません。",
"deleteBranch": "ブランチを削除",
"unmergedConfirm": "ローカルブランチ'__name__'にはマージされていない変更があります。この変更は削除されます。本当に削除しますか?",
"deleteUnmergedBranch": "マージされていないブランチを削除",
"gitRemotes": "Gitリモート",

View File

@ -1,6 +1,6 @@
{
"name": "@node-red/editor-client",
"version": "4.0.9",
"version": "4.0.1",
"license": "Apache-2.0",
"repository": {
"type": "git",

View File

@ -453,61 +453,10 @@ 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;
@ -587,24 +536,6 @@ 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<ev.links.length;i++) {

View File

@ -100,36 +100,16 @@ RED.multiplayer = (function () {
break
}
}
if (isInWorkspace) {
const chart = $('#red-ui-workspace-chart')
const chartOffset = chart.offset()
const scaleFactor = RED.view.scale()
location.cursor = {
x: (lastPosition[0] - chartOffset.left + chart.scrollLeft()) / scaleFactor,
y: (lastPosition[1] - chartOffset.top + chart.scrollTop()) / scaleFactor
}
}
return location
}
let publishLocationTimeout
let lastPosition = [0,0]
let isInWorkspace = false
function publishLocation () {
if (!publishLocationTimeout) {
publishLocationTimeout = setTimeout(() => {
const location = getLocation()
if (location.workspace !== 0) {
log('send', 'multiplayer/location', location)
RED.comms.send('multiplayer/location', location)
}
publishLocationTimeout = null
}, 100)
const location = getLocation()
if (location.workspace !== 0) {
log('send', 'multiplayer/location', location)
RED.comms.send('multiplayer/location', location)
}
}
function revealUser(location, skipWorkspace) {
if (location.node) {
// Need to check if this is a known node, so we can fall back to revealing
@ -291,16 +271,7 @@ 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
@ -320,28 +291,6 @@ 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)
@ -360,68 +309,67 @@ 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',
@ -531,24 +479,6 @@ 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()
})
}
}

View File

@ -73,13 +73,7 @@ RED.nodes = (function() {
var exports = {
setModulePendingUpdated: function(module,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;
}
moduleList[module].pending_version = version;
RED.events.emit("registry:module-updated",{module:module,version:version});
},
getModule: function(module) {
@ -707,15 +701,12 @@ 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] = newNode;
configNodes[n.id] = n;
} 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) {
@ -777,11 +768,9 @@ 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)) {
@ -790,9 +779,6 @@ 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)) {
@ -806,6 +792,10 @@ 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)
}
}
}
@ -1042,34 +1032,23 @@ 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) {
// 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()
let copyNumber = 1;
let subflowName = sf.name;
subflowNames.forEach(function(name) {
if (subflowName == name) {
subflowName = sf.name + " (" + copyNumber + ")";
copyNumber++;
}
var subflowNames = Object.keys(subflows).map(function(sfid) {
return subflows[sfid].name;
});
subflowNames.sort();
var copyNumber = 1;
var subflowName = sf.name;
subflowNames.forEach(function(name) {
if (subflowName == name) {
copyNumber++;
subflowName = sf.name+" ("+copyNumber+")";
}
});
sf.name = subflowName;
}
sf.instances = [];
subflows[sf.id] = sf;
allNodes.addTab(sf.id);
linkTabMap[sf.id] = [];
@ -1122,7 +1101,7 @@ RED.nodes = (function() {
module: "node-red"
}
});
sf.instances = [];
sf._def = RED.nodes.getType("subflow:"+sf.id);
RED.events.emit("subflows:add",sf);
}
@ -1764,8 +1743,7 @@ 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
// Need to sort the list in order to remove missing nodes
var subflowNodes = [n].concat(zMap[n.id]).filter((s) => !!s);
var subflowNodes = [n].concat(zMap[n.id]);
// Import the new subflow - no clashes should occur as we've removed
// the old version
var result = importNodes(subflowNodes);
@ -1802,20 +1780,9 @@ RED.nodes = (function() {
// Replace config nodes
//
configNodeIds.forEach(function(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
removedNodes = removedNodes.concat(convertNode(getNode(id)));
removeNode(id);
// Import the new one
importNodes([newConfigNodes[id]]);
// Re-attributes the user count
getNode(id).users = currentUserCount;
importNodes([newConfigNodes[id]])
});
return {
@ -2056,8 +2023,6 @@ 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();
@ -2085,7 +2050,7 @@ RED.nodes = (function() {
n.status.id = getID();
}
new_subflows.push(n);
addSubflow(n,createNewIds || options.importMap[oldId] === "copy");
addSubflow(n,createNewIds || options.importMap[n.id] === "copy");
}
}
}
@ -2099,8 +2064,6 @@ RED.nodes = (function() {
activeWorkspace = RED.workspaces.active();
}
const pendingConfigNodes = []
const pendingConfigNodeIds = new Set()
// Find all config nodes and add them
for (i=0;i<newNodes.length;i++) {
n = newNodes[i];
@ -2160,8 +2123,7 @@ RED.nodes = (function() {
type:n.type,
info: n.info,
users:[],
_config:{},
_configNodeReferences: new Set()
_config:{}
};
if (!n.z) {
delete configNode.z;
@ -2176,9 +2138,6 @@ RED.nodes = (function() {
if (def.defaults.hasOwnProperty(d)) {
configNode[d] = n[d];
configNode._config[d] = JSON.stringify(n[d]);
if (def.defaults[d].type) {
configNode._configNodeReferences.add(n[d])
}
}
}
if (def.hasOwnProperty('credentials') && n.hasOwnProperty('credentials')) {
@ -2195,55 +2154,11 @@ RED.nodes = (function() {
configNode.id = getID();
}
node_map[n.id] = configNode;
pendingConfigNodes.push(configNode);
pendingConfigNodeIds.add(configNode.id)
new_nodes.push(configNode);
}
}
}
// We need to sort new_nodes (which only contains config nodes at this point)
// to ensure they get added in the right order. If NodeA depends on NodeB, then
// NodeB must be added first.
// Limit us to 5 full iterations of the list - this should be more than
// enough to process the list as config->config 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<newNodes.length;i++) {
n = newNodes[i];
@ -2255,7 +2170,7 @@ RED.nodes = (function() {
x:parseFloat(n.x || 0),
y:parseFloat(n.y || 0),
z:n.z,
type: n.type,
type:0,
info: n.info,
changed:false,
_config:{}
@ -2316,6 +2231,7 @@ RED.nodes = (function() {
}
}
}
node.type = n.type;
node._def = def;
if (node.type === "group") {
node._def = RED.group.def;
@ -2345,15 +2261,6 @@ RED.nodes = (function() {
outputs: n.outputs|| (n.wires && n.wires.length) || 0,
set: registry.getNodeSet("node-red/unknown")
}
var orig = {};
for (var p in n) {
if (n.hasOwnProperty(p) && p!="x" && p!="y" && p!="z" && p!="id" && p!="wires") {
orig[p] = n[p];
}
}
node._orig = orig;
node.name = n.type;
node.type = "unknown";
} else {
if (subflow_denylist[parentId] || createNewIds || options.importMap[n.id] === "copy") {
parentId = subflow.id;
@ -2414,31 +2321,29 @@ RED.nodes = (function() {
node.type = "unknown";
}
if (node._def.category != "config") {
if (n.hasOwnProperty('inputs') && node._def.defaults.hasOwnProperty("inputs")) {
node.inputs = parseInt(n.inputs, 10);
if (n.hasOwnProperty('inputs')) {
node.inputs = n.inputs;
node._config.inputs = JSON.stringify(n.inputs);
} else {
node.inputs = node._def.inputs;
}
if (n.hasOwnProperty('outputs') && node._def.defaults.hasOwnProperty("outputs")) {
node.outputs = parseInt(n.outputs, 10);
if (n.hasOwnProperty('outputs')) {
node.outputs = n.outputs;
node._config.outputs = JSON.stringify(n.outputs);
} else {
node.outputs = node._def.outputs;
}
// 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 (node.hasOwnProperty('wires') && node.wires.length > 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);
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
node.outputs = node.wires.length;
}
}
for (d in node._def.defaults) {
if (node._def.defaults.hasOwnProperty(d) && d !== 'inputs' && d !== 'outputs') {
node[d] = n[d];
@ -2501,28 +2406,11 @@ RED.nodes = (function() {
} else {
delete n.g
}
// 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) {
// If importing into a subflow, ensure an outbound-link doesn't get added
if (activeSubflow && /^link /.test(n.type) && n.links) {
n.links = n.links.filter(function(id) {
const otherNode = node_map[id] || RED.nodes.node(id);
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
return (otherNode && otherNode.z === activeWorkspace);
});
}
for (var d3 in n._def.defaults) {
@ -2535,6 +2423,11 @@ 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;
@ -2548,11 +2441,9 @@ RED.nodes = (function() {
n = new_subflows[i];
n.in.forEach(function(input) {
input.wires.forEach(function(wire) {
if (node_map.hasOwnProperty(wire.id)) {
var link = {source:input, sourcePort:0, target:node_map[wire.id]};
addLink(link);
new_links.push(link);
}
var link = {source:input, sourcePort:0, target:node_map[wire.id]};
addLink(link);
new_links.push(link);
});
delete input.wires;
});
@ -2561,13 +2452,11 @@ 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 if (node_map.hasOwnProperty(wire.id) || subflow_map.hasOwnProperty(wire.id)) {
} else {
link = {source:node_map[wire.id]||subflow_map[wire.id], sourcePort:wire.port,target:output};
}
if (link) {
addLink(link);
new_links.push(link);
}
addLink(link);
new_links.push(link);
});
delete output.wires;
});
@ -2576,13 +2465,11 @@ 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 if (node_map.hasOwnProperty(wire.id) || subflow_map.hasOwnProperty(wire.id)) {
} else {
link = {source:node_map[wire.id]||subflow_map[wire.id], sourcePort:wire.port,target:n.status};
}
if (link) {
addLink(link);
new_links.push(link);
}
addLink(link);
new_links.push(link);
});
delete n.status.wires;
}
@ -2761,79 +2648,25 @@ RED.nodes = (function() {
return result;
}
/**
* 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];
// 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];
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[node[d]];
var configNode = configNodes[n[d]];
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);
}
}
if (configNode.users.indexOf(n) === -1) {
configNode.users.push(n);
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) {

View File

@ -205,9 +205,7 @@ RED.actionList = (function() {
}
function init() {
if (RED.settings.theme("menu.menu-item-action-list", true)) {
RED.actions.add("core:show-action-list",show);
}
RED.actions.add("core:show-action-list",show);
RED.events.on("editor:open",function() { disabled = true; });
RED.events.on("editor:close",function() { disabled = false; });

View File

@ -334,30 +334,6 @@ 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") {
@ -375,7 +351,21 @@ RED.clipboard = (function() {
return;
}
try {
validateFlowString(v)
if (!/^\[[\s\S]*\]$/m.test(v)) {
throw new Error(RED._("clipboard.import.errors.notArray"));
}
var res = JSON.parse(v);
for (var i=0;i<res.length;i++) {
if (typeof res[i] !== "object") {
throw new Error(RED._("clipboard.import.errors.itemNotObject",{index:i}));
}
if (!res[i].hasOwnProperty('id')) {
throw new Error(RED._("clipboard.import.errors.missingId",{index:i}));
}
if (!res[i].hasOwnProperty('type')) {
throw new Error(RED._("clipboard.import.errors.missingType",{index:i}));
}
}
currentPopoverError = null;
popover.close(true);
importInput.removeClass("input-error");
@ -1008,16 +998,16 @@ RED.clipboard = (function() {
}
function importNodes(nodesStr,addFlow) {
let newNodes = nodesStr;
var newNodes = nodesStr;
if (typeof nodesStr === 'string') {
try {
nodesStr = nodesStr.trim();
if (nodesStr.length === 0) {
return;
}
newNodes = validateFlowString(nodesStr)
newNodes = JSON.parse(nodesStr);
} catch(err) {
const e = new Error(RED._("clipboard.invalidFlow",{message:err.message}));
var e = new Error(RED._("clipboard.invalidFlow",{message:err.message}));
e.code = "NODE_RED";
throw e;
}
@ -1352,7 +1342,6 @@ RED.clipboard = (function() {
}
}
} catch(err) {
console.warn('Import failed: ', err)
// Ensure any errors throw above doesn't stop the drop target from
// being hidden.
}

View File

@ -61,7 +61,7 @@
}
this.menu = RED.popover.menu({
tabSelect: true,
width: Math.max(300, this.element.width()),
width: 300,
maxHeight: 200,
class: "red-ui-autoComplete-container",
options: completions,

View File

@ -63,7 +63,6 @@
pre: value.substring(0,idx),
match: value.substring(idx,idx+len),
post: value.substring(idx+len),
exact: idx === 0 && value.length === searchValue.length
}
}
function generateSpans(match) {
@ -84,7 +83,7 @@
const srcMatch = getMatch(optSrc, val);
if (valMatch.found || srcMatch.found) {
const element = $('<div>',{style: "display: flex"});
const valEl = $('<div/>',{ class: "red-ui-autoComplete-completion" });
const valEl = $('<div/>',{style:"font-family: var(--red-ui-monospace-font); white-space:nowrap; overflow: hidden; flex-grow:1"});
valEl.append(generateSpans(valMatch));
valEl.appendTo(element);
if (optSrc) {
@ -160,7 +159,7 @@
if (valMatch.found) {
const optSrc = envVarsMap[v]
const element = $('<div>',{style: "display: flex"});
const valEl = $('<div/>',{ class: "red-ui-autoComplete-completion" });
const valEl = $('<div/>',{style:"font-family: var(--red-ui-monospace-font); white-space:nowrap; overflow: hidden; flex-grow:1"});
valEl.append(generateSpans(valMatch))
valEl.appendTo(element)
@ -202,7 +201,7 @@
const that = this
const getContextKeysFromRuntime = function(scope, store, searchKey, done) {
contextKnownKeys[scope] = contextKnownKeys[scope] || {}
contextKnownKeys[scope][store] = contextKnownKeys[scope][store] || new Map()
contextKnownKeys[scope][store] = contextKnownKeys[scope][store] || new Set()
if (searchKey.length > 0) {
try {
RED.utils.normalisePropertyExpression(searchKey)
@ -224,12 +223,11 @@
const result = data[store] || {}
const keys = result.keys || []
const keyPrefix = searchKey + (searchKey.length > 0 ? '.' : '')
keys.forEach(keyInfo => {
const key = keyInfo.key
keys.forEach(key => {
if (/^[a-zA-Z_$][0-9a-zA-Z_$]*$/.test(key)) {
contextKnownKeys[scope][store].set(keyPrefix + key, keyInfo)
contextKnownKeys[scope][store].add(keyPrefix + key)
} else {
contextKnownKeys[scope][store].set(searchKey + "[\""+key.replace(/"/,"\\\"")+"\"]", keyInfo)
contextKnownKeys[scope][store].add(searchKey + "[\""+key.replace(/"/,"\\\"")+"\"]")
}
})
done()
@ -244,14 +242,14 @@
// Get the flow id of the node we're editing
const editStack = RED.editor.getEditStack()
if (editStack.length === 0) {
done(new Map())
done([])
return
}
const editingNode = editStack.pop()
if (editingNode.z) {
scope = `${scope}/${editingNode.z}`
} else {
done(new Map())
done([])
return
}
}
@ -271,29 +269,17 @@
return function(val, done) {
getContextKeys(val, function (keys) {
const matches = []
keys.forEach((keyInfo, v) => {
keys.forEach(v => {
let optVal = v
let valMatch = getMatch(optVal, val);
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 && 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) {
const element = $('<div>',{style: "display: flex"});
const valEl = $('<div/>',{ 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 += '['
}
const valEl = $('<div/>',{style:"font-family: var(--red-ui-monospace-font); white-space:nowrap; overflow: hidden; flex-grow:1"});
valEl.append(generateSpans(valMatch))
valEl.appendTo(element)
matches.push({
@ -1581,8 +1567,7 @@
if (tooltip) {
tooltip.setContent(valid);
} else {
const target = this.typeMap[type]?.options ? this.optionSelectLabel : this.elementDiv;
tooltip = RED.popover.tooltip(target, valid);
tooltip = RED.popover.tooltip(this.elementDiv, valid);
this.element.data("tooltip", tooltip);
}
}

View File

@ -32,44 +32,39 @@ RED.contextMenu = (function () {
const canRemoveFromGroup = hasSelection && !!selection.nodes[0].g
let hasGroup, isAllGroups = true, hasDisabledNode, hasEnabledNode, hasLabeledNode, hasUnlabeledNode;
if (hasSelection) {
const nodes = selection.nodes.slice();
while (nodes.length) {
const n = nodes.shift();
selection.nodes.forEach(n => {
if (n.type === 'group') {
hasGroup = true;
nodes.push(...n.nodes);
} else {
isAllGroups = false;
if (n.d) {
hasDisabledNode = true;
} else {
hasEnabledNode = true;
}
}
if (n.d) {
hasDisabledNode = true;
} else {
hasEnabledNode = true;
}
if (n.l === undefined || n.l) {
hasLabeledNode = true;
} else {
hasUnlabeledNode = true;
}
}
});
}
const scale = RED.view.scale()
const offset = $("#red-ui-workspace-chart").offset()
let addX = (options.x - offset.left + $("#red-ui-workspace-chart").scrollLeft()) / scale
let addY = (options.y - offset.top + $("#red-ui-workspace-chart").scrollTop()) / scale
let addX = options.x - offset.left + $("#red-ui-workspace-chart").scrollLeft()
let addY = options.y - offset.top + $("#red-ui-workspace-chart").scrollTop()
if (RED.view.snapGrid) {
const gridSize = RED.view.gridSize()
addX = gridSize * Math.round(addX / gridSize)
addY = gridSize * Math.round(addY / gridSize)
addX = gridSize * Math.floor(addX / gridSize)
addY = gridSize * Math.floor(addY / gridSize)
}
if (RED.settings.theme("menu.menu-item-action-list", true)) {
menuItems.push(
{ onselect: 'core:show-action-list', label: RED._("contextMenu.showActionList"), onpostselect: function () { } }
)
}
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(
@ -87,9 +82,7 @@ RED.contextMenu = (function () {
},
(hasLinks) ? { // has least 1 wire selected
label: RED._("contextMenu.junction"),
onselect: function () {
RED.actions.invoke('core:split-wires-with-junctions', { x: addX, y: addY })
},
onselect: 'core:split-wires-with-junctions',
disabled: !canEdit || !hasLinks
} : {
label: RED._("contextMenu.junction"),

View File

@ -44,7 +44,6 @@ 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"
@ -52,14 +51,13 @@ RED.deploy = (function() {
function init(options) {
options = options || {};
var type = options.type || "default";
var label = options.label || RED._("deploy.deploy");
if (type == "default") {
$('<li><span class="red-ui-deploy-button-group button-group">'+
'<a id="red-ui-header-button-deploy" class="red-ui-deploy-button disabled" href="#">'+
'<span class="red-ui-deploy-button-content">'+
'<img id="red-ui-header-button-deploy-icon" src="red/images/deploy-full-o.svg"> '+
'<span>'+label+'</span>'+
'<span>'+RED._("deploy.deploy")+'</span>'+
'</span>'+
'<span class="red-ui-deploy-button-spinner hide">'+
'<img src="red/images/spin.svg"/>'+
@ -80,6 +78,7 @@ 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;
@ -425,15 +424,11 @@ 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 && !isDisabled(node)) {
if (!node.valid && !node.d) {
invalidNodes.push(getNodeInfo(node));
}
if (node.type === "unknown") {
@ -443,7 +438,7 @@ RED.deploy = (function() {
}
});
RED.nodes.eachNode(function (node) {
if (!node.valid && !isDisabled(node)) {
if (!node.valid && !node.d) {
invalidNodes.push(getNodeInfo(node));
}
if (node.type === "unknown") {
@ -457,7 +452,7 @@ RED.deploy = (function() {
const unusedConfigNodes = [];
RED.nodes.eachConfig(function (node) {
if ((node._def.hasUsers !== false) && (node.users.length === 0) && !isDisabled(node)) {
if ((node._def.hasUsers !== false) && (node.users.length === 0)) {
unusedConfigNodes.push(getNodeInfo(node));
hasUnusedConfig = true;
}
@ -594,9 +589,7 @@ RED.deploy = (function() {
RED.notify('<p>' + RED._("deploy.successfulDeploy") + '</p>', "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) {
@ -649,7 +642,6 @@ RED.deploy = (function() {
delete confNode.credentials;
}
});
// Subflow cannot be locked
RED.nodes.eachSubflow(function (subflow) {
if (subflow.changed) {
subflow.changed = false;
@ -658,18 +650,12 @@ RED.deploy = (function() {
});
RED.nodes.eachWorkspace(function (ws) {
if (ws.changed || ws.added) {
// Ensure the Workspace is unlocked to modify its properties.
ensureUnlocked(ws.id);
ensureUnlocked(ws.z)
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
})

View File

@ -497,7 +497,7 @@ RED.diff = (function() {
}
})
if (c === 0) {
result.text(RED._("diff.type.none"));
result.text("none");
} else {
list.appendTo(result);
}
@ -821,7 +821,7 @@ RED.diff = (function() {
conflict = true;
}
row = $("<tr>").appendTo(nodePropertiesTableBody);
$("<td>",{class:"red-ui-diff-list-cell-label"}).text(RED._("diff.type.position")).appendTo(row);
$("<td>",{class:"red-ui-diff-list-cell-label"}).text("position").appendTo(row);
localCell = $("<td>",{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 = $("<tr>").appendTo(nodePropertiesTableBody);
$("<td>",{class:"red-ui-diff-list-cell-label"}).text(RED._("diff.type.wires")).appendTo(row);
$("<td>",{class:"red-ui-diff-list-cell-label"}).text("wires").appendTo(row);
localCell = $("<td>",{class:"red-ui-diff-list-cell red-ui-diff-list-node-local"}).appendTo(row);
if (localNode) {
if (!conflict) {
@ -2029,14 +2029,15 @@ RED.diff = (function() {
if (!isSeparator) {
var isOurs = /^..<<<<<<</.test(lineText);
if (isOurs) {
$('<span>').text("<<<<<<< " + RED._("diff.localChanges")).appendTo(line);
$('<span>').text("<<<<<<< Local Changes").appendTo(line);
hunk.localChangeStart = actualLineNumber;
} else {
hunk.remoteChangeEnd = actualLineNumber;
$('<span>').text(">>>>>>> " + RED._("diff.remoteChanges")).appendTo(line);
$('<span>').text(">>>>>>> Remote Changes").appendTo(line);
}
diffRow.addClass("mergeHeader-"+(isOurs?"ours":"theirs"));
$('<button class="red-ui-button red-ui-button-small" style="float: right; margin-right: 20px;"><i class="fa fa-angle-double-'+(isOurs?"down":"up")+'"></i> '+RED._(isOurs?"diff.useLocalChanges":"diff.useRemoteChanges")+'</button>')
$('<button class="red-ui-button red-ui-button-small" style="float: right; margin-right: 20px;"><i class="fa fa-angle-double-'+(isOurs?"down":"up")+'"></i> use '+(isOurs?"local":"remote")+' changes</button>')
.appendTo(line)
.on("click", function(evt) {
evt.preventDefault();
@ -2118,7 +2119,7 @@ RED.diff = (function() {
$("<h3>").text(commit.title).appendTo(content);
$('<div class="commit-body"></div>').text(commit.comment).appendTo(content);
var summary = $('<div class="commit-summary"></div>').appendTo(content);
$('<div style="float: right">').text(RED._('diff.commit')+" "+commit.sha).appendTo(summary);
$('<div style="float: right">').text("Commit "+commit.sha).appendTo(summary);
$('<div>').text((commit.authorName||commit.author)+" - "+options.date).appendTo(summary);
if (commit.files) {

View File

@ -157,12 +157,6 @@ RED.editor = (function() {
}
}
if (valid && "validate" in definition[property]) {
if (definition[property].hasOwnProperty("required") &&
definition[property].required === false) {
if (value === "") {
return true;
}
}
try {
var opt = {};
if (label) {
@ -189,11 +183,6 @@ RED.editor = (function() {
});
}
} else if (valid) {
if (definition[property].hasOwnProperty("required") && definition[property].required === false) {
if (value === "") {
return true;
}
}
// If the validator is not provided in node property => Check if the input has a validator
if ("category" in node._def) {
const isConfig = node._def.category === "config";
@ -808,20 +797,6 @@ 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) {
@ -853,25 +828,6 @@ 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;
}
}
}
}
}
}
@ -932,7 +888,7 @@ RED.editor = (function() {
const labelText = RED.editor.envVarList.lookupLabel(labels, labels["en-US"] || tenv.name, locale)
const config = {
env: tenv,
id: '${' + tenv.name + '}',
id: '${' + parentEnv[0].name + '}',
type: type,
label: labelText,
__label__: `[env] ${labelText}`
@ -1514,181 +1470,134 @@ 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() {
// TODO: Already defined
const configProperty = name;
const configType = type;
const configTypeDef = RED.nodes.getType(configType);
const wasChanged = editing_config_node.changed;
const editState = {
var editState = {
changes: {},
changed: false,
outputMap: null
};
// Call `oneditsave` and search for changes
handleEditSave(editing_config_node, editState);
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;
// Search for changes in the edit box (panes)
activeEditPanes.forEach(function (pane) {
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) {
if (pane.apply) {
pane.apply.call(pane, editState);
}
});
})
// TODO: Why?
editing_config_node.label = configTypeDef.label
editing_config_node.label = configTypeDef.label;
var scope = $("#red-ui-editor-config-scope").val();
editing_config_node.z = scope;
// 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) {
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] = "";
}
}
// 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);
}
}
}
// Add the node modified to the history
if (nodeModified) {
historyEvents.push(nodeModified);
}
// 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;
return keep;
});
// 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;
}
}
if (editState.changed) {
// Set the congig node as changed
editing_config_node.changed = true;
if (configAdding) {
RED.nodes.add(editing_config_node);
}
// Now, validate the config node
validateNode(editing_config_node);
var validatedNodes = {};
validatedNodes[editing_config_node.id] = true;
// 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);
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);
}
validateNode(node);
validateNode(user);
}
}
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
};
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 (!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
@ -1836,18 +1745,8 @@ RED.editor = (function() {
}
});
}
let envToRemove = new Set()
if (!isSameObj(old_env, new_env)) {
// Get a list of env properties that have been removed
// by comparing old_env and new_env
if (old_env) {
old_env.forEach(env => { envToRemove.add(env.name) })
}
if (new_env) {
new_env.forEach(env => {
envToRemove.delete(env.name)
})
}
editState.changes.env = editing_node.env;
editing_node.env = new_env;
editState.changed = true;
@ -1856,11 +1755,10 @@ RED.editor = (function() {
if (editState.changed) {
let wasChanged = editing_node.changed;
var wasChanged = editing_node.changed;
editing_node.changed = true;
validateNode(editing_node);
let subflowInstances = [];
let instanceHistoryEvents = []
var subflowInstances = [];
RED.nodes.eachNode(function(n) {
if (n.type == "subflow:"+editing_node.id) {
subflowInstances.push({
@ -1870,35 +1768,13 @@ RED.editor = (function() {
n._def.color = editing_node.color;
n.changed = true;
n.dirty = 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);
let historyEvent = {
var historyEvent = {
t:'edit',
node:editing_node,
changes:editState.changes,
@ -1908,13 +1784,7 @@ RED.editor = (function() {
instances:subflowInstances
}
};
if (instanceHistoryEvents.length > 0) {
historyEvent = {
t: 'multi',
events: [ historyEvent, ...instanceHistoryEvents ],
dirty: wasDirty
}
}
RED.history.push(historyEvent);
}
editing_node.dirty = true;

View File

@ -165,13 +165,7 @@ RED.editor.codeEditor.monaco = (function() {
//Handles orphaned models
//ensure loaded models that are not explicitly destroyed by a call to .destroy() are disposed
RED.events.on("editor:close",function() {
if (!window.monaco) { return; }
const editors = window.monaco.editor.getEditors()
const orphanEditors = editors.filter(editor => editor && !document.body.contains(editor.getDomNode()))
orphanEditors.forEach(editor => {
editor.dispose();
});
let models = monaco.editor.getModels()
let models = window.monaco ? monaco.editor.getModels() : null;
if(models && models.length) {
console.warn("Cleaning up monaco models left behind. Any node that calls createEditor() should call .destroy().")
for (let index = 0; index < models.length; index++) {
@ -691,7 +685,6 @@ 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.
@ -1131,7 +1124,6 @@ RED.editor.codeEditor.monaco = (function() {
$(el).remove();
$(toolbarRow).remove();
ed.dispose();
}
ed.resize = function resize() {

View File

@ -131,7 +131,7 @@ RED.editor.envVarList = (function() {
nameField.trigger('change');
}
},
sortable: true,
sortable: ".red-ui-editableList-item-handle",
removable: false
});
var parentEnv = {};

View File

@ -27,12 +27,6 @@
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;
/**
@ -58,8 +52,7 @@
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();
@ -70,30 +63,7 @@
});
return;
}
if ( fileType.startsWith("text/") ) {
file2Text(file, function (txt) {
var session = currentEditor.getSession();
var pos = session.getCursorPosition();
session.insert(pos, txt);
$("#red-ui-image-drop-target").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-image-drop-target").hide();
})
return
}
}
$("#red-ui-image-drop-target").hide();
});

View File

@ -11,22 +11,9 @@ RED.editor.mermaid = (function () {
if (!initializing) {
initializing = true
// Find the cache-buster:
let cacheBuster
$('script').each(function (i, el) {
if (!cacheBuster) {
const src = el.getAttribute('src')
const m = /\?v=(.+)$/.exec(src)
if (m) {
cacheBuster = m[1]
}
}
})
$.ajax({
url: `vendor/mermaid/mermaid.min.js?v=${cacheBuster}`,
dataType: "script",
cache: true,
success: function (data, stat, jqxhr) {
$.getScript(
'vendor/mermaid/mermaid.min.js',
function (data, stat, jqxhr) {
mermaid.initialize({
startOnLoad: false,
theme: RED.settings.get('mermaid', {}).theme
@ -37,7 +24,7 @@ RED.editor.mermaid = (function () {
render(pending)
}
}
});
)
}
} else {
const nodes = document.querySelectorAll(selector)

View File

@ -20,31 +20,10 @@
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) {
@ -62,6 +41,7 @@
}
});
if (new_env && new_env.length > 0) {
new_env.forEach(function(prop) {
if (prop.type === "cred") {
@ -72,15 +52,6 @@
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);
}
}
}
});
}

View File

@ -44,7 +44,6 @@
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)) {
@ -132,16 +131,9 @@
}
}
if (node._def.credentials) {
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
};
}
var credDefinition = node._def.credentials;
var credsChanged = updateNodeCredentials(node,credDefinition,this.inputClass);
editState.changed = editState.changed || credsChanged;
}
}
}
@ -169,11 +161,10 @@
* @param node - the node containing the credentials
* @param credDefinition - definition of the credentials
* @param prefix - prefix of the input fields
* @return {object} an object containing the modified properties
* @return {boolean} whether anything has changed
*/
function updateNodeCredentials(node, credDefinition, prefix) {
const changes = {};
var changed = false;
if (!node.credentials) {
node.credentials = {_:{}};
} else if (!node.credentials._) {
@ -186,33 +177,22 @@
if (input.length > 0) {
var value = input.val();
if (credDefinition[cred].type == 'password') {
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;
node.credentials['has_' + cred] = (value !== "");
if (value == '__PWRD__') {
continue;
}
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;
}
}
node.credentials[cred] = value;
if (value != node.credentials._[cred]) {
changed = true;
}
}
}
}
return changes;
return changed;
}
})();

View File

@ -245,15 +245,10 @@ 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,
icon: lib.icon || 'fa fa-hdd-o',
label: RED._(lib.label||lib.id),
path: "",
expanded: true,
@ -308,15 +303,10 @@ 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,
icon: lib.icon || 'fa fa-hdd-o',
label: RED._(lib.label||lib.id),
path: "",
expanded: true,
@ -849,10 +839,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 = $('<tr class="red-ui-help-info-row"><td>'+RED._("library.type")+'</td><td></td></tr>').appendTo(table);
var propRow = $('<tr class="red-ui-help-info-row"><td>Type</td><td></td></tr>').appendTo(table);
$(propRow.children()[1]).text(activeLibrary.type);
if (file.props.hasOwnProperty('name')) {
propRow = $('<tr class="red-ui-help-info-row"><td>'+RED._("library.name")+'</td><td>'+file.props.name+'</td></tr>').appendTo(table);
propRow = $('<tr class="red-ui-help-info-row"><td>Name</td><td>'+file.props.name+'</td></tr>').appendTo(table);
$(propRow.children()[1]).text(file.props.name);
}
for (var p in file.props) {

View File

@ -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: RED._("sidebar.project.projectSettings.deleteBranch"),
text: 'Delete branch',
click: function() {
notification.close();
var url = "projects/"+activeProject.name+"/branches/"+entry.name;

View File

@ -1362,7 +1362,7 @@ RED.subflow = (function() {
item.value = ""+input.prop("checked");
break;
case "conf-types":
item.value = input.val() === "_ADD_" ? "" : input.val();
item.value = input.val()
item.type = "conf-type"
}
if (ui.type === "cred" || item.type !== data.parent.type || item.value !== data.parent.value) {

View File

@ -56,16 +56,7 @@ RED.sidebar.config = (function() {
} else {
$('<span class="red-ui-palette-node-config-label" data-i18n="sidebar.config.'+name+'">').appendTo(header);
}
$('<span class="red-ui-sidebar-node-config-filter-info"></span>').appendTo(header);
const changeBadgeContainer = $('<svg class="red-ui-sidebar-config-category-changed red-ui-flow-node-changed" width="10" height="10" viewBox="-1 -1 12 12"></svg>').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 = $('<ul class="red-ui-palette-content red-ui-sidebar-node-config-list"></ul>').appendTo(container);
category.on("click", function(e) {
$(content).find(".red-ui-palette-node").removeClass("selected");
@ -159,6 +150,9 @@ RED.sidebar.config = (function() {
$('<li class="red-ui-palette-node-config-type">'+node.type+'</li>').appendTo(list);
currentType = node.type;
}
if (node.changed) {
labelText += "!!"
}
var entry = $('<li class="red-ui-palette-node_id_'+node.id.replace(/\./g,"-")+'"></li>').appendTo(list);
var nodeDiv = $('<div class="red-ui-palette-node-config red-ui-palette-node"></div>').appendTo(entry);
entry.data('node',node.id);
@ -187,29 +181,15 @@ RED.sidebar.config = (function() {
}
}
if (node.changed) {
const nodeDivAnnotations = $('<svg class="red-ui-palette-node-annotations red-ui-flow-node-changed" width="10" height="10" viewBox="-1 -1 12 12"></svg>').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) {
const nodeDivAnnotations = $('<svg class="red-ui-palette-node-annotations red-ui-flow-node-error" width="10" height="10"></svg>').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");
nodeDiv.addClass("red-ui-palette-node-config-invalid")
const nodeDivAnnotations = $('<svg class="red-ui-palette-node-annotations red-ui-flow-node-error" width="10" height="10"></svg>').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))
RED.popover.tooltip(nodeDivAnnotations, function () {
if (node.validationErrors && node.validationErrors.length > 0) {
return RED._("editor.errors.invalidProperties") + "<br> - " + node.validationErrors.join("<br> - ");
return RED._("editor.errors.invalidProperties")+"<br> - "+node.validationErrors.join("<br> - ")
}
})
}
@ -272,10 +252,6 @@ RED.sidebar.config = (function() {
$(this).remove();
delete categories[id];
}
// 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 = {};

View File

@ -18,6 +18,8 @@ RED.sidebar.context = (function() {
var content;
var sections;
var localCache = {};
var flowAutoRefresh;
var nodeAutoRefresh;
var nodeSection;
@ -25,8 +27,6 @@ RED.sidebar.context = (function() {
var flowSection;
var globalSection;
const expandedPaths = {}
var currentNode;
var currentFlow;
@ -212,41 +212,14 @@ RED.sidebar.context = (function() {
var l = keys.length;
for (var i = 0; i < l; i++) {
sortedData[keys[i]].forEach(function(v) {
const k = keys[i];
let payload = v.msg;
let format = v.format;
const tools = $('<span class="button-group"></span>');
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 = $('<tr class="red-ui-help-info-row"><td class="red-ui-sidebar-context-property"></td><td></td></tr>').appendTo(container);
const obj = $(propRow.children()[0]);
var k = keys[i];
var l2 = sortedData[k].length;
var propRow = $('<tr class="red-ui-help-info-row"><td class="red-ui-sidebar-context-property"></td><td></td></tr>').appendTo(container);
var obj = $(propRow.children()[0]);
obj.text(k);
var tools = $('<span class="button-group"></span>');
const urlSafeK = encodeURIComponent(k)
const refreshItem = $('<button class="red-ui-button red-ui-button-small"><i class="fa fa-refresh"></i></button>').appendTo(tools).on("click", function(e) {
var refreshItem = $('<button class="red-ui-button red-ui-button-small"><i class="fa fa-refresh"></i></button>').appendTo(tools).on("click", function(e) {
e.preventDefault();
e.stopPropagation();
$.getJSON(baseUrl+"/"+urlSafeK+"?store="+v.store, function(data) {
@ -256,14 +229,16 @@ 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"));
const deleteItem = $('<button class="red-ui-button red-ui-button-small"><i class="fa fa-trash"></i></button>').appendTo(tools).on("click", function(e) {
var deleteItem = $('<button class="red-ui-button red-ui-button-small"><i class="fa fa-trash"></i></button>').appendTo(tools).on("click", function(e) {
e.preventDefault();
e.stopPropagation();
var popover = RED.popover.create({
@ -271,7 +246,7 @@ RED.sidebar.context = (function() {
target: propRow,
direction: "left",
content: function() {
const content = $('<div>');
var content = $('<div>');
$('<p data-i18n="sidebar.context.deleteConfirm"></p>').appendTo(content);
var row = $('<p>').appendTo(content);
var bg = $('<span class="button-group"></span>').appendTo(row);
@ -294,15 +269,16 @@ RED.sidebar.context = (function() {
if (container.children().length === 0) {
$('<tr class="red-ui-help-info-row red-ui-search-empty blank" colspan="2"><td data-i18n="sidebar.context.empty"></td></tr>').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), {
...objectElementOptions,
typeHint: data.format
typeHint: data.format,
sourceId: id+"."+k,
tools: tools,
path: k
}).appendTo(propRow.children()[1]);
}
});
@ -317,7 +293,14 @@ RED.sidebar.context = (function() {
});
RED.popover.tooltip(deleteItem,RED._("sidebar.context.delete"));
RED.utils.createObjectElement(RED.utils.decodeObject(payload,format), objectElementOptions).appendTo(propRow.children()[1]);
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]);
if (contextStores.length > 1) {
$("<span>",{class:"red-ui-sidebar-context-property-storename"}).text(v.store).appendTo($(propRow.children()[0]))
}

View File

@ -204,7 +204,7 @@ RED.sidebar.info = (function() {
propertiesPanelHeaderIcon.empty();
RED.utils.createNodeIcon({type:"_selection_"}).appendTo(propertiesPanelHeaderIcon);
propertiesPanelHeaderLabel.text(RED._("sidebar.info.selection"));
propertiesPanelHeaderLabel.text("Selection");
propertiesPanelHeaderReveal.hide();
propertiesPanelHeaderHelp.hide();
propertiesPanelHeaderCopyLink.hide();

View File

@ -279,11 +279,6 @@ 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() {
@ -335,25 +330,13 @@ RED.typeSearch = (function() {
}
}
function applyFilter(filter,type,def) {
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)
return !def || !filter ||
(
(!filter.spliceMultiple) &&
(!filter.type || type === filter.type) &&
(!filter.input || type === 'junction' || def.inputs > 0) &&
(!filter.output || type === 'junction' || def.outputs > 0)
)
}
function refreshTypeList(opts) {
var i;
@ -382,7 +365,7 @@ RED.typeSearch = (function() {
var items = [];
RED.nodes.registry.getNodeTypes().forEach(function(t) {
var def = RED.nodes.getType(t);
if (def.set?.enabled !== false && def.category !== 'config' && t !== 'unknown' && t !== 'tab') {
if (def.category !== 'config' && t !== 'unknown' && t !== 'tab') {
items.push({type:t,def: def, label:getTypeLabel(t,def)});
}
});

View File

@ -230,7 +230,7 @@ RED.utils = (function() {
var pinnedPaths = {};
var formattedPaths = {};
function addMessageControls(obj,sourceId,key,msg,rootPath,strippedKey,extraTools,enablePinning) {
function addMessageControls(obj,sourceId,key,msg,rootPath,strippedKey,extraTools) {
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 (enablePinning && strippedKey !== undefined && strippedKey !== '') {
if (strippedKey !== undefined && strippedKey !== '') {
var isPinned = pinnedPaths[sourceId].hasOwnProperty(strippedKey);
var pinPath = $('<button class="red-ui-button red-ui-button-small red-ui-debug-msg-tools-pin"><i class="fa fa-map-pin"></i></button>').appendTo(tools).on("click", function(e) {
@ -281,16 +281,13 @@ RED.utils = (function() {
}
}
}
function checkExpanded(strippedKey, expandPaths, { minRange, maxRange, expandLeafNodes }) {
function checkExpanded(strippedKey,expandPaths,minRange,maxRange) {
if (expandPaths && expandPaths.length > 0) {
if (strippedKey === '' && minRange === undefined) {
return true;
}
for (var i=0;i<expandPaths.length;i++) {
var p = expandPaths[i];
if (expandLeafNodes && p === strippedKey) {
return true
}
if (p.indexOf(strippedKey) === 0 && (p[strippedKey.length] === "." || p[strippedKey.length] === "[") ) {
if (minRange !== undefined && p[strippedKey.length] === "[") {
@ -397,8 +394,6 @@ RED.utils = (function() {
var sourceId = options.sourceId;
var rootPath = options.rootPath;
var expandPaths = options.expandPaths;
const enablePinning = options.enablePinning
const expandLeafNodes = options.expandLeafNodes;
var ontoggle = options.ontoggle;
var exposeApi = options.exposeApi;
var tools = options.tools;
@ -421,11 +416,11 @@ RED.utils = (function() {
}
header = $('<span class="red-ui-debug-msg-row"></span>').appendTo(element);
if (sourceId) {
addMessageControls(header,sourceId,path,obj,rootPath,strippedKey,tools, enablePinning);
addMessageControls(header,sourceId,path,obj,rootPath,strippedKey,tools);
}
if (!key) {
element.addClass("red-ui-debug-msg-top-level");
if (sourceId && !expandPaths) {
if (sourceId) {
var pinned = pinnedPaths[sourceId];
expandPaths = [];
if (pinned) {
@ -481,7 +476,7 @@ RED.utils = (function() {
$('<span class="red-ui-debug-msg-type-meta red-ui-debug-msg-object-type-header"></span>').text(typeHint||'string').appendTo(header);
var row = $('<div class="red-ui-debug-msg-object-entry collapsed"></div>').appendTo(element);
$('<pre class="red-ui-debug-msg-type-string"></pre>').text(obj).appendTo(row);
},function(state) {if (ontoggle) { ontoggle(path,state);}}, checkExpanded(strippedKey, expandPaths, { expandLeafNodes }));
},function(state) {if (ontoggle) { ontoggle(path,state);}}, checkExpanded(strippedKey,expandPaths));
}
e = $('<span class="red-ui-debug-msg-type-string red-ui-debug-msg-object-header"></span>').html('"'+formatString(sanitize(obj))+'"').appendTo(entryObj);
if (/^#[0-9a-f]{6}$/i.test(obj)) {
@ -597,16 +592,14 @@ RED.utils = (function() {
typeHint: type==='buffer'?'hex':false,
hideKey: false,
path: path+"["+i+"]",
sourceId,
rootPath,
expandPaths,
expandLeafNodes,
ontoggle,
exposeApi,
sourceId: sourceId,
rootPath: rootPath,
expandPaths: expandPaths,
ontoggle: ontoggle,
exposeApi: exposeApi,
// tools: tools // Do not pass tools down as we
// keep them attached to the top-level header
nodeSelector: options.nodeSelector,
enablePinning
}
).appendTo(row);
}
@ -630,23 +623,21 @@ RED.utils = (function() {
typeHint: type==='buffer'?'hex':false,
hideKey: false,
path: path+"["+i+"]",
sourceId,
rootPath,
expandPaths,
expandLeafNodes,
ontoggle,
exposeApi,
sourceId: sourceId,
rootPath: rootPath,
expandPaths: expandPaths,
ontoggle: ontoggle,
exposeApi: 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, maxRange: Math.min(fullLength-1,(minRange+9)), expandLeafNodes}));
checkExpanded(strippedKey,expandPaths,minRange,Math.min(fullLength-1,(minRange+9))));
$('<span class="red-ui-debug-msg-object-key"></span>').html("["+minRange+" &hellip; "+Math.min(fullLength-1,(minRange+9))+"]").appendTo(header);
}
if (fullLength < originalLength) {
@ -655,7 +646,7 @@ RED.utils = (function() {
}
},
function(state) {if (ontoggle) { ontoggle(path,state);}},
checkExpanded(strippedKey, expandPaths, { expandLeafNodes }));
checkExpanded(strippedKey,expandPaths));
}
} else if (typeof obj === 'object') {
element.addClass('collapsed');
@ -689,16 +680,14 @@ RED.utils = (function() {
typeHint: false,
hideKey: false,
path: newPath,
sourceId,
rootPath,
expandPaths,
expandLeafNodes,
ontoggle,
exposeApi,
sourceId: sourceId,
rootPath: rootPath,
expandPaths: expandPaths,
ontoggle: ontoggle,
exposeApi: exposeApi,
// tools: tools // Do not pass tools down as we
// keep them attached to the top-level header
nodeSelector: options.nodeSelector,
enablePinning
}
).appendTo(row);
}
@ -707,7 +696,7 @@ RED.utils = (function() {
}
},
function(state) {if (ontoggle) { ontoggle(path,state);}},
checkExpanded(strippedKey, expandPaths, { expandLeafNodes }));
checkExpanded(strippedKey,expandPaths));
}
if (key) {
$('<span class="red-ui-debug-msg-type-meta"></span>').text(type).appendTo(entryObj);

View File

@ -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<l;i++) {
const annotation = evt.el.__annotations__[i];
if (annotations.hasOwnProperty(annotation.id)) {
@ -42,17 +42,15 @@ RED.view.annotations = (function() {
}
if (isBadge) {
if (showAnnotation) {
// getBoundingClientRect is in real-world scale so needs to be adjusted according to
// the current scale factor
const rectWidth = annotation.element.getBoundingClientRect().width / scale;
const rect = annotation.element.getBoundingClientRect();
let annotationX
if (!opts.align || opts.align === 'right') {
annotationX = evt.node.w - 3 - badgeRDX - rectWidth
badgeRDX += rectWidth + 4;
annotationX = evt.node.w - 3 - badgeRDX - rect.width
badgeRDX += rect.width + 4;
} else if (opts.align === 'left') {
annotationX = 3 + badgeLDX
badgeLDX += rectWidth + 4;
badgeLDX += rect.width + 4;
}
annotation.element.setAttribute("transform", "translate("+annotationX+", -8)");
}

View File

@ -1102,27 +1102,18 @@ RED.view.tools = (function() {
const paletteLabel = RED.utils.getPaletteLabel(n.type, nodeDef)
const defaultNodeNameRE = new RegExp('^'+paletteLabel.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')+' (\\d+)$')
if (!typeIndex.hasOwnProperty(n.type)) {
const existingNodes = RED.nodes.filterNodes({ type: n.type });
const existingIds = existingNodes.reduce((ids, node) => {
let match = defaultNodeNameRE.exec(node.name);
const existingNodes = RED.nodes.filterNodes({type: n.type})
let maxNameNumber = 0;
existingNodes.forEach(n => {
let match = defaultNodeNameRE.exec(n.name)
if (match) {
const nodeNumber = parseInt(match[1], 10);
if (!ids.includes(nodeNumber)) {
ids.push(nodeNumber);
let nodeNumber = parseInt(match[1])
if (nodeNumber > maxNameNumber) {
maxNameNumber = nodeNumber
}
}
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;
})
typeIndex[n.type] = maxNameNumber + 1
}
if ((options.renameBlank && n.name === '') || (options.renameClash && defaultNodeNameRE.test(n.name))) {
if (generateHistory) {
@ -1154,11 +1145,11 @@ RED.view.tools = (function() {
}
}
function addJunctionsToWires(options = {}) {
function addJunctionsToWires(wires) {
if (RED.workspaces.isLocked()) {
return
}
let wiresToSplit = options.wires || (RED.view.selection().links && RED.view.selection().links.filter(e => !e.link));
let wiresToSplit = wires || (RED.view.selection().links && RED.view.selection().links.filter(e => !e.link));
if (!wiresToSplit) {
return
}
@ -1206,26 +1197,21 @@ RED.view.tools = (function() {
if (links.length === 0) {
return
}
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)
}
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));
@ -1415,7 +1401,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 (options) { addJunctionsToWires(options) });
RED.actions.add("core:split-wires-with-junctions", function () { addJunctionsToWires() });
RED.actions.add("core:generate-node-names", generateNodeNames )

View File

@ -288,7 +288,7 @@ RED.view = (function() {
}
selectedLinks.clearUnselected()
},
length: () => groups.size,
length: () => groups.length,
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,
y: evt.clientY
x:evt.clientX-5,
y:evt.clientY-5
})
return false
})
@ -1209,10 +1209,7 @@ RED.view = (function() {
lasso = null;
}
if (d3.event.touches || d3.event.button === 0) {
if (
(mouse_mode === 0 && isControlPressed(d3.event) && !(d3.event.altKey || d3.event.shiftKey)) ||
mouse_mode === RED.state.QUICK_JOINING
) {
if ((mouse_mode === 0 || mouse_mode === RED.state.QUICK_JOINING) && isControlPressed(d3.event) && !(d3.event.altKey || d3.event.shiftKey)) {
// Trigger quick add dialog
d3.event.stopPropagation();
clearSelection();
@ -1265,6 +1262,11 @@ 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];
@ -1283,6 +1285,7 @@ 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);
@ -1586,6 +1589,9 @@ 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();
@ -2170,24 +2176,19 @@ RED.view = (function() {
n.n.moved = true;
}
}
// 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
// Check to see if we need to splice a link
if (moveEvent.nodes.length > 0) {
historyEvent.events.push(moveEvent)
targetSpliceEvent = moveEvent
if (activeSpliceLink) {
var linkToSplice = d3.select(activeSpliceLink).data()[0];
spliceLink(linkToSplice, movingSet.get(0).n, 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);
@ -2686,21 +2687,22 @@ RED.view = (function() {
addToRemovedLinks(reconnectResult.removedLinks)
}
const startDirty = RED.nodes.dirty();
let movingSelectedGroups = [];
var startDirty = RED.nodes.dirty();
var startChanged = false;
var selectedGroups = [];
if (movingSet.length() > 0) {
for (var i=0;i<movingSet.length();i++) {
node = movingSet.get(i).n;
if (node.type === "group") {
movingSelectedGroups.push(node);
selectedGroups.push(node);
}
}
// Make sure we have identified all groups about to be deleted
for (i=0;i<movingSelectedGroups.length;i++) {
movingSelectedGroups[i].nodes.forEach(function(n) {
if (n.type === "group" && movingSelectedGroups.indexOf(n) === -1) {
movingSelectedGroups.push(n);
for (i=0;i<selectedGroups.length;i++) {
selectedGroups[i].nodes.forEach(function(n) {
if (n.type === "group" && selectedGroups.indexOf(n) === -1) {
selectedGroups.push(n);
}
})
}
@ -2717,7 +2719,7 @@ RED.view = (function() {
addToRemovedLinks(removedEntities.links);
if (node.g) {
var group = RED.nodes.group(node.g);
if (movingSelectedGroups.indexOf(group) === -1) {
if (selectedGroups.indexOf(group) === -1) {
// Don't use RED.group.removeFromGroup as that emits
// a change event on the node - but we're deleting it
var index = group.nodes.indexOf(node);
@ -2731,7 +2733,7 @@ RED.view = (function() {
removedLinks = removedLinks.concat(result.links);
if (node.g) {
var group = RED.nodes.group(node.g);
if (movingSelectedGroups.indexOf(group) === -1) {
if (selectedGroups.indexOf(group) === -1) {
// Don't use RED.group.removeFromGroup as that emits
// a change event on the node - but we're deleting it
var index = group.nodes.indexOf(node);
@ -2753,8 +2755,8 @@ RED.view = (function() {
// Groups must be removed in the right order - from inner-most
// to outermost.
for (i = movingSelectedGroups.length-1; i>=0; i--) {
var g = movingSelectedGroups[i];
for (i = selectedGroups.length-1; i>=0; i--) {
var g = selectedGroups[i];
removedGroups.push(g);
RED.nodes.removeGroup(g);
}
@ -3055,8 +3057,8 @@ RED.view = (function() {
}
function disableQuickJoinEventHandler(evt) {
// 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) {
// Check for ctrl (all browsers), "Meta" (Chrome/FF), keyCode 91 (Safari)
if (evt.keyCode === 17 || evt.key === "Meta" || evt.keyCode === 91) {
resetMouseVars();
hideDragLines();
redraw();
@ -3187,59 +3189,27 @@ RED.view = (function() {
for (i=0;i<drag_lines.length;i++) {
if (portType != drag_lines[i].portType && mouseup_node !== drag_lines[i].node) {
let drag_line = drag_lines[i];
let src,dst,src_port;
let oldDst;
let oldSrc;
var drag_line = drag_lines[i];
var src,dst,src_port;
if (drag_line.portType === PORT_TYPE_OUTPUT) {
src = drag_line.node;
src_port = drag_line.port;
dst = mouseup_node;
oldSrc = src;
if (drag_line.link) {
oldDst = drag_line.link.target;
}
} else if (drag_line.portType === PORT_TYPE_INPUT) {
src = mouseup_node;
dst = drag_line.node;
src_port = portIndex || 0;
oldSrc = dst;
if (drag_line.link) {
oldDst = drag_line.link.source
}
}
var link = {source: src, sourcePort:src_port, target: dst};
if (drag_line.virtualLink) {
if (/^link (in|out)$/.test(src.type) && /^link (in|out)$/.test(dst.type) && src.type !== dst.type) {
if (src.links.indexOf(dst.id) === -1 && dst.links.indexOf(src.id) === -1) {
var oldSrcLinks = [...src.links]
var oldDstLinks = [...dst.links]
var oldSrcLinks = $.extend(true,{},{v:src.links}).v
var oldDstLinks = $.extend(true,{},{v:dst.links}).v
src.links.push(dst.id);
dst.links.push(src.id);
if (oldDst) {
src.links = src.links.filter(id => 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);
@ -3267,7 +3237,6 @@ RED.view = (function() {
links:oldDstLinks
}
});
src.changed = true;
dst.changed = true;
}
@ -5171,8 +5140,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 / scaleFactor))
var posDeltaY = Math.abs(linePos.y-(d3.event.offsetY / scaleFactor))
var posDeltaX = Math.abs(linePos.x-d3.event.offsetX)
var posDeltaY = Math.abs(linePos.y-d3.event.offsetY)
var posDelta = posDeltaX*posDeltaX + posDeltaY*posDeltaY
if (posDelta < delta) {
pos = linePos

View File

@ -183,29 +183,25 @@ RED.workspaces = (function() {
},
null)
}
if (RED.settings.theme("menu.menu-item-workspace-add", true)) {
menuItems.push(
{
id:"red-ui-tabs-menu-option-add-flow",
label: RED._("workspace.addFlow"),
onselect: "core:add-flow"
}
)
if (isMenuButton || !!tab) {
menuItems.push(
{
id:"red-ui-tabs-menu-option-add-flow",
label: RED._("workspace.addFlow"),
onselect: "core:add-flow"
}
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 (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 ? {
@ -259,9 +255,7 @@ RED.workspaces = (function() {
}
)
}
if (menuItems.length > 0) {
menuItems.push(null)
}
menuItems.push(null)
if (isMenuButton || !!tab) {
menuItems.push(
{
@ -305,24 +299,19 @@ 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"),
@ -479,7 +468,7 @@ RED.workspaces = (function() {
},
minimumActiveTabWidth: 150,
scrollable: true,
addButton: RED.settings.theme("menu.menu-item-workspace-add", true) ? "core:add-flow" : undefined,
addButton: "core:add-flow",
addButtonCaption: RED._("workspace.addFlow"),
menu: function() { return getMenuItems(true) },
contextmenu: function(tab) { return getMenuItems(false, tab) }
@ -536,24 +525,19 @@ RED.workspaces = (function() {
$(window).on("resize", function() {
workspace_tabs.resize();
});
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: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);
RED.actions.add("core:enable-flow",enableWorkspace);
RED.actions.add("core:disable-flow",disableWorkspace);
RED.actions.add("core:lock-flow",lockWorkspace);
@ -920,17 +904,6 @@ 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)

View File

@ -168,37 +168,6 @@ RED.user = (function() {
}
} else {
if (data.prompts) {
if (data.loginMessage) {
const sessionMessages = $("<div/>",{class:"form-row",style:"text-align: center"}).appendTo("#node-dialog-login-fields");
$('<div>').text(data.loginMessage).appendTo(sessionMessages);
}
i = 0;
for (;i<data.prompts.length;i++) {
var field = data.prompts[i];
var row = $("<div/>",{class:"form-row",style:"text-align: center"}).appendTo("#node-dialog-login-fields");
var loginButton = $('<a href="#" class="red-ui-button"></a>',{style: "padding: 10px"}).appendTo(row).on("click", function() {
document.location = field.url;
});
if (field.image) {
$("<img>",{src:field.image}).appendTo(loginButton);
} else if (field.label) {
var label = $('<span></span>').text(field.label);
if (field.icon) {
$('<i></i>',{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 ) {
@ -351,10 +320,10 @@ RED.user = (function() {
userIcon.css({
backgroundImage: "url("+user.image+")",
})
} else if (user.anonymous || (!user.username && !user.email)) {
} else if (user.anonymous) {
$('<i class="fa fa-user"></i>').appendTo(userIcon);
} else {
$('<span>').text((user.username || user.email).substring(0,2)).appendTo(userIcon);
$('<span>').text(user.username.substring(0,2)).appendTo(userIcon);
}
if (user.profileColor !== undefined) {
userIcon.addClass('red-ui-user-profile-color-' + user.profileColor)

View File

@ -259,7 +259,7 @@ $deploy-button-background-disabled-hover: #555;
$header-background: #000;
$header-button-background-active: #121212;
$header-accent: #C02020;
$header-accent: #d41313;
$header-menu-color: #eee;
$header-menu-color-disabled: #666;
$header-menu-heading-color: #fff;

View File

@ -84,11 +84,6 @@ 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);
@ -120,15 +115,6 @@ 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);

View File

@ -151,9 +151,8 @@
&.red-ui-tabs-add {
padding-right: 29px;
}
&.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-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 {
@ -311,9 +310,8 @@
}
}
.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-tab-scroll-right {
right: 32px;
}
.red-ui-tabs.red-ui-tabs-add.red-ui-tabs-menu .red-ui-tab-scroll-right,

View File

@ -2,15 +2,4 @@
&.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;
}

View File

@ -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' && output?.fill && output?.shape && output?.text) {
if (typeof output === 'object' && hasOwnProperty.call(output, "fill") && hasOwnProperty.call(output, "shape") && hasOwnProperty.call(output, "text")) {
fill = output.fill;
shape = output.shape;
st = output.text;

View File

@ -511,10 +511,9 @@ 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);

View File

@ -111,6 +111,8 @@ 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;"+
@ -164,13 +166,7 @@ module.exports = function(RED) {
Buffer:Buffer,
Date: Date,
RED: {
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);
}
}
util: RED.util
},
__node__: {
id: node.id,

View File

@ -352,9 +352,7 @@ module.exports = function(RED) {
if (msgs.length === 0) {
done()
} else {
setImmediate(() => {
drainMessageGroup(msgs,count,done);
})
drainMessageGroup(msgs,count,done);
}
}
})
@ -507,9 +505,7 @@ module.exports = function(RED) {
if (err) {
node.error(err,nextMsg);
}
setImmediate(() => {
processMessageQueue()
})
processMessageQueue()
});
}

View File

@ -253,13 +253,7 @@ module.exports = function(RED) {
if (node.allowrate && m.hasOwnProperty("rate") && !isNaN(parseFloat(m.rate))) {
node.rate = m.rate;
}
if (msg.hasOwnProperty("reset")) {
if (msg.hasOwnProperty("flush")) {
node.buffer.push({msg: m, send: send, done: done});
}
}
else { send(m); }
send(m);
node.reportDepth();
node.intervalID = setInterval(sendMsgFromBuffer, node.rate);
done();
@ -291,23 +285,42 @@ module.exports = function(RED) {
}
}
else if (!msg.hasOwnProperty("reset")) {
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 (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])
}
}
done();
}

View File

@ -24,14 +24,6 @@ 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";
@ -201,7 +193,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" || node.op2type === "date") {
if (node.op2type === "flow" || node.op2type === "global") {
promise = new Promise((resolve,reject) => {
RED.util.evaluateNodeProperty(node.op2,node.op2type,node,msg,(err,value) => {
if (err) {
@ -221,6 +213,7 @@ 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); }
}

View File

@ -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;
}

View File

@ -158,16 +158,9 @@ 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;
@ -680,8 +673,6 @@ 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;
@ -700,21 +691,6 @@ 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) {
@ -749,7 +725,6 @@ 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);

View File

@ -367,21 +367,20 @@ module.exports = function(RED) {
const sendHeadersAlways = node.hdrout === "all"
const sendHeaders = !dontSendHeaders && (sendHeadersOnce || sendHeadersAlways)
const quoteables = [node.sep, node.quo, "\n", "\r"]
const templateQuoteables = [node.sep, node.quo, "\n", "\r"]
const templateQuoteablesStrict = [',', '"', "\n", "\r"]
const templateQuoteables = [',', '"', "\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.data?.length === 1) { node.goodtmpl = true } else { node.goodtmpl = false }
return node.goodtmpl ? parsed.data[0] : null
if (parsed.headers.length > 0) { node.goodtmpl = true } else { node.goodtmpl = false }
return parsed.headers.length ? parsed.headers : null
}
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 })
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 })
return keepEmptyColumns
? parsed.headers.map(e => addQuotes(e || '', { separator, quoteables: quotables })).join(separator)
? parsed.headers.map(e => addQuotes(e || '', { separator: ',', quoteables: templateQuoteables}))
: parsed.header // exclues empty columns
// TODO: resolve inconsistency between CSV->JSON and JSON->CSV
// CSV->JSON: empty columns are excluded
@ -448,7 +447,7 @@ module.exports = function(RED) {
template = Object.keys(inputData[0]) || ['']
}
}
stringBuilder.push(templateArrayToColumnString(template, true, node.sep, templateQuoteables)) // use user set separator for output data.
stringBuilder.push(templateArrayToColumnString(template, true))
if (sendHeadersOnce) { node.hdrSent = true }
}
@ -484,7 +483,6 @@ 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)) {
@ -520,7 +518,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) // always strict commas + double quotes for
msg.columns = templateArrayToColumnString(template)
if (msg.payload !== '') { send(msg) }
done()
}
@ -617,15 +615,16 @@ module.exports = function(RED) {
}
if (msg.parts.index + 1 === msg.parts.count) {
msg.payload = node.store
// msg.columns = csvParseResult.header
msg.columns = templateArrayToColumnString(csvParseResult.headers) // always strict commas + double quotes for msg.columns
msg.columns = csvParseResult.header
// msg._mode = 'RFC4180 mode'
delete msg.parts
send(msg)
node.store = []
}
}
else {
msg.columns = templateArrayToColumnString(csvParseResult.headers) // always strict commas + double quotes for msg.columns
msg.columns = csvParseResult.header
// msg._mode = 'RFC4180 mode'
msg.payload = data
send(msg); // finally send the array
}
@ -634,8 +633,7 @@ 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 = templateArrayToColumnString(csvParseResult.headers) // always strict commas + double quotes for msg.columns
newMessage.columns = csvParseResult.header
newMessage.payload = data[row]
if (!has_parts) {
newMessage.parts = {

View File

@ -21,7 +21,7 @@
<input type="text" id="node-input-name" data-i18n="[placeholder]node-red:common.label.name">
</div>
<div class="form-row">
<label for="node-input-property"><i class="fa fa-forward"></i> <span data-i18n="split.splitThe"></span></label>
<label for="node-input-property"><i class="fa fa-forward"></i> <span data-i18n="split.split"></span></label>
<input type="text" id="node-input-property" style="width:70%;"/>
</div>
<div class="form-row"><span data-i18n="[html]split.strBuff"></span></div>

View File

@ -36,10 +36,6 @@
<label style="margin-left: 10px; width: 175px;" for="node-input-overlap" data-i18n="batch.count.overlap"></label>
<input type="text" id="node-input-overlap" style="width: 50px;">
</div>
<div class="form-row">
<input type="checkbox" id="node-input-honourParts" style="margin-left: 10px; margin-right:10px; vertical-align:top; width:auto;">
<label for="node-input-honourParts" style="width:auto;" data-i18n="batch.honourParts"></label>
</div>
</div>
<div class="node-row-msg-interval">
@ -49,7 +45,7 @@
<span data-i18n="batch.interval.seconds"></span>
</div>
<div class="form-row">
<input type="checkbox" id="node-input-allowEmptySequence" style="margin-left:20px; margin-right:10px; vertical-align:top; width:auto;">
<input type="checkbox" id="node-input-allowEmptySequence" style="margin-left:20px; margin-right: 10px; vertical-align:top; width:auto;">
<label for="node-input-allowEmptySequence" style="width:auto;" data-i18n="batch.interval.empty"></label>
</div>
</div>
@ -105,7 +101,6 @@
}
},
allowEmptySequence: {value:false},
honourParts: {value:false},
topics: {value:[{topic:""}]}
},
inputs:1,

View File

@ -181,8 +181,6 @@ module.exports = function(RED) {
RED.nodes.createNode(this,n);
var node = this;
var mode = n.mode || "count";
var eof = false;
node.honourParts = n.honourParts || false;
node.pending_count = 0;
if (mode === "count") {
@ -203,12 +201,9 @@ module.exports = function(RED) {
return;
}
var queue = node.pending;
if (node.honourParts && msg.hasOwnProperty("parts")) {
if (msg.parts.index + 1 === msg.parts.count) { eof = true; }
}
queue.push({msg, send, done});
node.pending_count++;
if (queue.length === count || eof === true) {
if (queue.length === count) {
send_msgs(node, queue, is_overlap);
for (let i = 0; i < queue.length-overlap; i++) {
queue[i].done();
@ -216,7 +211,6 @@ module.exports = function(RED) {
node.pending =
(overlap === 0) ? [] : queue.slice(-overlap);
node.pending_count = 0;
eof = false;
}
var max_msgs = max_kept_msgs_count(node);
if ((max_msgs > 0) && (node.pending_count > max_msgs)) {

View File

@ -339,7 +339,7 @@ module.exports = function(RED) {
}
else {
msg.filename = filename;
const bufferArray = [];
var lines = Buffer.from([]);
var spare = "";
var count = 0;
var type = "buffer";
@ -397,7 +397,7 @@ module.exports = function(RED) {
}
}
else {
bufferArray.push(chunk);
lines = Buffer.concat([lines,chunk]);
}
}
})
@ -413,11 +413,10 @@ module.exports = function(RED) {
})
.on('end', function() {
if (node.chunk === false) {
const buffer = Buffer.concat(bufferArray);
if (node.format === "utf8") {
msg.payload = decode(buffer, node.encoding);
msg.payload = decode(lines, node.encoding);
}
else { msg.payload = buffer; }
else { msg.payload = lines; }
nodeSend(msg);
}
else if (node.format === "lines") {

View File

@ -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,"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":[]}]
[{"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":[]}]

View File

@ -912,7 +912,6 @@
"objectSend": "Sende eine Nachricht für jedes Schlüssel/Wert-Paar",
"strBuff": "<b>string</b> / <b>buffer</b>",
"array": "<b>array</b>",
"splitThe": "Split",
"splitUsing": "Aufteilung",
"splitLength": "feste Längen von",
"stream": "Als Nachrichtenstrom behandeln (Streaming-Modus)",

View File

@ -39,36 +39,10 @@
<dd><b>MQTTv5</b>: Ablaufzeit der Nachricht in Sekunden.</dd>
</dl>
<h3>Details</h3>
<p>Das abonnierte Topic darf MQTT-Platzhalterzeichen (wildcards) enthalten (+ für eine Ebene und # für mehrere Ebenen).</p>
<p>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.</p>
<p>Das abonnierte Topic darf MQTT-Platzhalterzeichen (wildcards) enthalten (+ für eine Ebene und # für mehrere Ebenen).</p>
<p>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.</p>
<p>Mehrere MQTT-Nodes (in oder out) können bei Bedarf dieselbe Broker-Verbindung nutzen.</p>
<h4>Dynamische Steuerung</h4>
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.
<h4>Eingangsdaten</h4>
<p>Nur Verfügbar, wenn die Node für dynamische Abonnements konfiguriert wurde.</p>
<dl class="message-properties">
<dt>action <span class="property-type">string</span></dt>
<dd>Der Name der Aktion, die die MQTT-Node ausführen soll. Verfügbare Aktionen sind: <code>"connect"</code>, <code>"disconnect"</code>, <code>"getSubscriptions"</code>, <code>"subscribe"</code> und <code>"unsubscribe"</code>.</dd>
<dt class="optional">topic <span class="property-type">string|object|array</span></dt>
<dd>Bei den Aktionen <code>"subscribe"</code> und <code>"unsubscribe"</code> gibt diese Eigenschaft die MQTT-Topic an. Dabei kann es sich um Folgendes handeln:
<ul>
<li>eine Zeichenfolge, die den Topic-Filter enthält</li>
<li>ein Objekt mit den Eigenschaften <code>topic</code> und <code>qos</code></li>
<li>ein Array aus Zeichenfolgen oder Objekten, um mehrere Topics gleichzeitig zu verwalten</li>
</ul>
</dd>
<dt class="optional">broker <span class="property-type">broker</span> </dt>
<dd>Für die Aktion <code>"connect"</code> kann diese Eigenschaft jede der einzelnen Broker-Konfigurationseinstellungen überschreiben, einschließlich: <ul>
<li><code>broker</code></li>
<li><code>port</code></li>
<li><code>url</code> - überschreibt Broker/Port, um eine vollständige Verbindungs-URL bereitzustellen</li>
<li><code>username</code></li>
<li><code>password</code></li>
</ul>
<p>Wenn diese Eigenschaft gesetzt ist und der Broker bereits verbunden ist, wird ein Fehler protokolliert, es sei denn, die Eigenschaft <code>force</code> gesetzt - in diesem Fall wird die Verbindung zum Broker getrennt, die neuen Einstellungen angewendet und erneut verbunden.</p>
</dd>
</dl>
</script>
<script type="text/html" data-help-name="mqtt out">

View File

@ -456,7 +456,7 @@
"staticTopic": "Subscribe to single topic",
"dynamicTopic": "Dynamic subscription",
"auto-connect": "Connect automatically",
"auto-mode-depreciated": "This option is deprecated. Please use the new auto-detect mode.",
"auto-mode-depreciated": "This option is depreciated. Please use the new auto-detect mode.",
"none": "none",
"other": "other"
},
@ -1011,13 +1011,12 @@
"tip": "Tip: The filename should be an absolute path, otherwise it will be relative to the working directory of the Node-RED process."
},
"split": {
"split": "split",
"split": "Split",
"intro": "Split <code>msg.payload</code> based on type:",
"object": "<b>Object</b>",
"objectSend": "Send a message for each key/value pair",
"strBuff": "<b>String</b> / <b>Buffer</b>",
"array": "<b>Array</b>",
"splitThe": "Split the",
"splitUsing": "Split using",
"splitLength": "Fixed length of",
"stream": "Handle as a stream of messages",
@ -1114,7 +1113,6 @@
"too-many": "too many pending messages in batch node",
"unexpected": "unexpected mode",
"no-parts": "no parts property in message",
"honourParts": "Allow msg.parts to also complete batch operation.",
"error": {
"invalid-count": "Invalid count",
"invalid-overlap": "Invalid overlap",

View File

@ -48,8 +48,7 @@
<dl class="message-properties">
<dt>action <span class="property-type">string</span></dt>
<dd>the name of the action the node should perform. Available actions are: <code>"connect"</code>,
<code>"disconnect"</code>, <code>"getSubscriptions"</code>, <code>"subscribe"</code> and
<code>"unsubscribe"</code>.</dd>
<code>"disconnect"</code>, <code>"subscribe"</code> and <code>"unsubscribe"</code>.</dd>
<dt class="optional">topic <span class="property-type">string|object|array</span></dt>
<dd>For the <code>"subscribe"</code> and <code>"unsubscribe"</code> actions, this property
provides the topic. It can be set as either:<ul>

View File

@ -513,15 +513,15 @@
"method": "Método",
"url": "URL",
"doc": "Docs",
"return": "Devolver",
"upload": "¿Aceptar cargas de archivos?",
"status": "Código de estado",
"headers": "Encabezados",
"return": "Return",
"upload": "Accept file uploads?",
"status": "Status code",
"headers": "Headers",
"other": "otro",
"paytoqs": {
"ignore": "Ignorar",
"query": "Agregar a los parámetros de la cadena de consulta",
"body": "Enviar como cuerpo de la solicitud"
"ignore": "Ignore",
"query": "Append to query-string parameters",
"body": "Send as request body"
},
"utf8String": "texto UTF8",
"binaryBuffer": "buffer binario",
@ -529,45 +529,45 @@
"authType": "Tipo",
"bearerToken": "Token"
},
"setby": "- establecido por msg.method -",
"basicauth": "Usar autenticación",
"use-tls": "Habilitar conexión segura (SSL/TLS)",
"tls-config": "Configuración TLS",
"basic": "autenticación básica",
"digest": "autenticación digest",
"bearer": "autenticación bearer",
"use-proxy": "Usar proxy",
"persist": "Habilitar conexión activa (keep-alive)",
"proxy-config": "Configuración Proxy",
"use-proxyauth": "Usar autenticación de proxy",
"noproxy-hosts": "Ignorar hosts",
"senderr": "Enviar solo respuestas que no sean 2xx al nodo Catch",
"utf8": "una cadena UTF-8",
"binary": "un búfer binario",
"json": "un objeto JSON analizado",
"setby": "- set by msg.method -",
"basicauth": "Use authentication",
"use-tls": "Enable secure (SSL/TLS) connection",
"tls-config": "TLS Configuration",
"basic": "basic authentication",
"digest": "digest authentication",
"bearer": "bearer authentication",
"use-proxy": "Use proxy",
"persist": "Enable connection keep-alive",
"proxy-config": "Proxy Configuration",
"use-proxyauth": "Use proxy authentication",
"noproxy-hosts": "Ignore hosts",
"senderr": "Only send non-2xx responses to Catch node",
"utf8": "a UTF-8 string",
"binary": "a binary buffer",
"json": "a parsed JSON object",
"tip": {
"in": "La URL será relativa a ",
"res": "Los mensajes enviados a este nodo <b>deben</b> originarse desde un nodo de <i>http input</i>",
"in": "The url will be relative to ",
"res": "The messages sent to this node <b>must</b> originate from an <i>http input</i> node",
"req": "Tip: If the JSON parse fails the fetched string is returned as-is."
},
"httpreq": "solicitud http",
"httpreq": "http request",
"errors": {
"not-created": "No se puede crear el nodo http-in cuando httpNodeRoot está establecido en falso",
"missing-path": "falta la ruta",
"no-response": "No hay objeto de respuesta",
"json-error": "Error de análisis en JSON",
"no-url": "No se especificó ninguna URL",
"deprecated-call": "Llamada obsoleta a __method__",
"invalid-transport": "protocolo no-http solicitado",
"timeout-isnan": "El valor de tiempo de espera no es un número válido, se ignora",
"timeout-isnegative": "El valor de tiempo de espera es negativo, se ignora",
"invalid-payload": "payload Invalido",
"invalid-url": "URL Inválida"
"not-created": "Cannot create http-in node when httpNodeRoot set to false",
"missing-path": "missing path",
"no-response": "No response object",
"json-error": "JSON parse error",
"no-url": "No url specified",
"deprecated-call": "Deprecated call to __method__",
"invalid-transport": "non-http transport requested",
"timeout-isnan": "Timeout value is not a valid number, ignoring",
"timeout-isnegative": "Timeout value is negative, ignoring",
"invalid-payload": "Invalid payload",
"invalid-url": "Invalid url"
},
"status": {
"requesting": "solicitando"
"requesting": "requesting"
},
"insecureHTTPParser": "Deshabilitar el análisis estricto de HTTP"
"insecureHTTPParser": "Disable strict HTTP parsing"
},
"websocket": {
"label": {
@ -576,42 +576,41 @@
"url": "URL",
"subprotocol": "Subprotocolo"
},
"listenon": "Escuchar",
"connectto": "Conectar a",
"sendrec": "Enviar/Recibir",
"listenon": "Listen on",
"connectto": "Connect to",
"sendrec": "Send/Receive",
"payload": "payload",
"message": "mensaje completo",
"sendheartbeat": "Enviar latido",
"message": "entire message",
"sendheartbeat": "Send heartbeat",
"tip": {
"path1": "De manera predeterminada, <code>payload</code> contendrá los datos que se enviarán o recibirán de un websocket. El receptor puede configurarse para enviar o recibir el objeto de mensaje completo como una cadena en formato JSON.",
"path2": "Esta ruta será relativa a <code>__path__</code>.",
"url1": "La URL debe usar el esquema ws:&#47;&#47; o wss:&#47;&#47; y apuntar a un receptor de websocket existente.",
"url2": "De manera predeterminada, <code>payload</code> contendrá los datos que se enviarán o recibirán de un websocket. El cliente puede configurarse para enviar o recibir el objeto de mensaje completo como una cadena en formato JSON",
"headers": "Los encabezados solo se envían durante el mecanismo de actualización del protocolo, de HTTP al protocolo WS/WSS."
"path1": "By default, <code>payload</code> will contain the data to be sent over, or received from a websocket. The listener can be configured to send or receive the entire message object as a JSON formatted string.",
"path2": "This path will be relative to <code>__path__</code>.",
"url1": "URL should use ws:&#47;&#47; or wss:&#47;&#47; scheme and point to an existing websocket listener.",
"url2": "By default, <code>payload</code> will contain the data to be sent over, or received from a websocket. The client can be configured to send or receive the entire message object as a JSON formatted string."
},
"status": {
"connected": "__count__ conectado",
"connected_plural": "__count__ conectados"
"connected": "connected __count__",
"connected_plural": "connected __count__"
},
"errors": {
"connect-error": "Se produjo un error en la conexión ws:",
"send-error": "Se produjo un error al enviar: ",
"missing-conf": "Falta la configuración del servidor",
"duplicate-path": "No se pueden tener dos escuchas de WebSocket en la misma ruta: __path__",
"missing-server": "Falta la configuración del servidor",
"missing-client": "Falta la configuración del cliente"
"connect-error": "An error occurred on the ws connection: ",
"send-error": "An error occurred while sending: ",
"missing-conf": "Missing server configuration",
"duplicate-path": "Cannot have two WebSocket listeners on the same path: __path__",
"missing-server": "Missing server configuration",
"missing-client": "Missing client configuration"
}
},
"watch": {
"watch": "observar",
"watch": "watch",
"label": {
"files": "Fichero(s)",
"recursive": "Observar subdirectorios recursivamente"
"files": "File(s)",
"recursive": "Watch sub-directories recursively"
},
"placeholder": {
"files": "Lista de archivos y/o directorios separados por comas"
"files": "Comma-separated list of files and/or directories"
},
"tip": "En Windows, debes utilizar barras invertidas dobles \\\\ en cualquier nombre de directorio."
"tip": "On Windows you must use double back-slashes \\\\ in any directory names."
},
"tcpin": {
"label": {
@ -850,13 +849,7 @@
"newline": "Nueva línea",
"usestrings": "analizar valores numéricos",
"include_empty_strings": "incluir cadenas vacías",
"include_null_values": "incluir valores nulos",
"spec": "Analizador"
},
"spec": {
"rfc": "RFC4180",
"legacy": "Legado",
"legacy_warning": "El modo legado se eliminará en una versión futura."
"include_null_values": "incluir valores nulos"
},
"placeholder": {
"columns": "nombres de columnas separados por comas"
@ -885,7 +878,6 @@
"once": "enviar encabezados una vez, hasta msg.reset"
},
"errors": {
"bad_template": "Plantilla de columnas mal formada.",
"csv_js": "Este nodo solo maneja cadenas CSV u objetos JS.",
"obj_csv": "No se ha especificado ninguna plantilla de columnas para el objeto -> CSV.",
"bad_csv": "Datos CSV con formato incorrecto: la salida probablemente esté corrupta."
@ -895,14 +887,12 @@
"label": {
"select": "Selector",
"output": "Salida",
"in": "en",
"prefix": "Nombre de la propiedad para el contenido HTML"
"in": "en"
},
"output": {
"html": "el contenido HTML de los elementos",
"text": "sólo el contenido textual de los elementos",
"attr": "un objeto de cualquier atributo de los elementos",
"compl": "un objeto de cualquier atributo de los elementos y contenidos html"
"attr": "un objeto de cualquier atributo de los elementos"
},
"format": {
"single": "como un mensaje único que contiene una matriz",
@ -1017,7 +1007,6 @@
"objectSend": "Enviar un mensaje para cada par clave/valor",
"strBuff": "<b>Texto</b> / <b>Buffer</b>",
"array": "<b>Array</b>",
"splitThe": "Dividir el",
"splitUsing": "Dividir usando",
"splitLength": "Longitud fija de",
"stream": "Manejar como un flujo de mensajes",
@ -1047,7 +1036,6 @@
"joinedUsing": "se unió usando",
"send": "Enviar el mensaje:",
"afterCount": "Después de varias partes del mensaje",
"useparts": "Usar la propiedad msg.parts existente",
"count": "contar",
"subsequent": "y cada mensaje posterior.",
"afterTimeout": "Después de un tiempo de espera trás el primer mensaje",
@ -1114,7 +1102,6 @@
"too-many": "demasiados mensajes pendientes en el nodo de lotes",
"unexpected": "modo inesperado",
"no-parts": "ninguna propiedad 'parte' en el mensaje",
"honourParts": "Permitir que msg.parts también complete la operación por lotes.",
"error": {
"invalid-count": "Recuento no válido",
"invalid-overlap": "Solapamiento no válido",

View File

@ -24,14 +24,12 @@
<p>Solo se envía el <code>msg.payload</code>.</p>
<p>Si <code>msg.payload</code> es una cadena que contiene una codificación Base64 de datos binarios, la opción de decodificación Base64 hará que se vuelva a convertir a binario antes de enviarse.</p>
<p>Si <code>msg._session</code> no está presente, la carga se envía a <b>todos</b> los clientes conectados.</p>
<p>En el modo Responder a, configurar <code>msg.reset = true</code> restablecerá la conexión especificada por _session.id, o todas las conexiones si no se especifica _session.id.</p>
<p><b>Nota: </b>En algunos sistemas, es posible que necesites acceso raíz o de administrador para acceder a los puertos inferiores a 1024.</p>
</script>
<script type="text/html" data-help-name="tcp request">
<p>Un nodo de solicitud TCP simple: envía el <code>msg.payload</code> a un puerto tcp del servidor y espera una respuesta.</p>
<p>Se conecta, envía la "solicitud" y lee la "respuesta". Puede contar una cantidad de caracteres devueltos en un búfer fijo, hacer coincidir un carácter específico antes de regresar, esperar un tiempo de espera fijo desde la primera respuesta y luego regresar, esperar datos, o enviar y luego cerrar la conexión inmediatamente, sin esperar una respuesta.</p>
<p>Si está en modo sentado y esperando (permanecer conectado), puede enviar <code>msg.reset = true</code> o <code>msg.reset = "host:port"</code> para forzar una interrupción en la conexión y una reconexión automática.</p>
<p>La respuesta se generará en <code>msg.payload</code> como un búfer, por lo que es posible que quieras utilizar .toString().</p>
<p>Si dejas el host TCP o el puerto en blanco, debes configurarlos utilizando las propiedades <code>msg.host</code> y <code>msg.port</code> en cada mensaje enviado al nodo.</p>
</script>

View File

@ -35,9 +35,7 @@
</dd>
</dl>
<h3>Detalles</h3>
<p>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.
<p>Cuando se selecciona el analizador RFC, la plantilla de columna debe ser compatible con RFC4180.</p>
</p>
<p>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.</p>
<p>Al convertir a CSV, la plantilla de columnas se utiliza para identificar qué propiedades extraer del objeto y en qué orden.</p>
<p>Si la plantilla de columnas está en blanco, puede utilizar una lista simple de propiedades separadas por comas proporcionada en <code>msg.columns</code> 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.</p>
<p>Si la entrada es una matriz, entonces la plantilla de columnas solo se usa para generar opcionalmente una fila de títulos de columnas.</p>
@ -48,5 +46,4 @@
<p>Si genera varios mensajes, tendrán su propiedad <code>parts</code> configurada y formarán una secuencia de mensajes completa.</p>
<p>Si el nodo está configurado para enviar encabezados de columna solo una vez, si se configura <code>msg.reset</code> en cualquier valor hará que el nodo reenvíe los encabezados.</p>
<p><b>Nota:</b> la plantilla de columna debe estar separada por comas, incluso si se elige un separador diferente para los datos.</p>
<p><b>Nota:</b> en el modo RFC, se generarán errores detectables para encabezados CSV mal formados y datos de carga útil de entrada no válidos</p>
</script>

View File

@ -1017,7 +1017,6 @@
"objectSend": "Envoie un message pour chaque paire clé/valeur",
"strBuff": "<b>Chaîne</b> / <b>Tampon</b>",
"array": "<b>Tableau</b>",
"splitThe": "Diviser le",
"splitUsing": "Diviser en utilisant",
"splitLength": "Longueur fixe de",
"stream": "Gérer comme un flux de messages",
@ -1047,7 +1046,6 @@
"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",
@ -1114,7 +1112,6 @@
"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",

View File

@ -1017,7 +1017,6 @@
"objectSend": "各key/valueペアのメッセージを送信",
"strBuff": "<b>文字列</b> / <b>バッファ</b>",
"array": "<b>配列</b>",
"splitThe": "に基づく",
"splitUsing": "分割",
"splitLength": "固定長",
"stream": "メッセージのストリームとして処理",
@ -1047,7 +1046,6 @@
"joinedUsing": "連結文字",
"send": "メッセージ送信:",
"afterCount": "指定数のメッセージパーツを受信後",
"useparts": "既存のmsg.partsプロパティを使用",
"count": "合計値",
"subsequent": "後続のメッセージ毎",
"afterTimeout": "最初のメッセージ受信からのタイムアウト後",
@ -1114,7 +1112,6 @@
"too-many": "batchード内で保持しているメッセージが多すぎます",
"unexpected": "想定外のモード",
"no-parts": "メッセージにpartsプロパティがありません",
"honourParts": "msg.partsを用いたbatch操作を許可",
"error": {
"invalid-count": "メッセージ数が不正",
"invalid-overlap": "オーバラップが不正",

View File

@ -44,7 +44,7 @@
"global": "contexto global",
"str": "Cadeia de caracteres",
"num": "número",
"bool": "booliano",
"bool": "booliano",
"json": "objeto",
"bin": "Armazenamento temporário",
"date": "Carimbo de data/hora",
@ -352,8 +352,8 @@
}
},
"trigger": {
"send": "Enviar",
"then": "então",
"send": "Enviar",
"then": "então",
"then-send": "então enviem",
"output": {
"string": "a cadeia de caracteres",
@ -446,7 +446,7 @@
"staticTopic": "Assinar um tópico único",
"dynamicTopic": "Assinatura dinâmica",
"auto-connect": "Conectar automaticamente",
"auto-mode-depreciated": "Esta opção está deprecada. Favor utilizar o novo modo de auto-detecção."
"auto-mode-depreciated": "Esta opção está deprecada. Favor utilizar o novo modo de auto-detecção."
},
"sections-label": {
"birth-message": "Mensagem enviada na conexão (mensagem de nascimento)",
@ -466,8 +466,8 @@
"close-topic": "Deixe em branco para desativar a mensagem de fechamento"
},
"state": {
"connected": "Conectado ao negociante: _ broker _",
"disconnected": "Desconectado do negociante: _ broker _",
"connected": "Conectado ao negociante: _ broker _",
"disconnected": "Desconectado do negociante: _ broker _",
"connect-failed": "Falha na conexão com o negociante: __broker__",
"broker-disconnected": "Cliente de negociante __broker__ desconectado: __reasonCode__ __reasonString__"
},
@ -898,7 +898,7 @@
"o2j": "Objeto para opções JSON",
"pretty": "Formatar cadeia de caracteres JSON",
"action": "Ação",
"property": "Propriedade",
"property": "Propriedade",
"actions": {
"toggle": "Converter entre cadeia de caracteres JSON e Objeto",
"str": "Sempre converter em cadeia de caracteres JSON",
@ -929,7 +929,7 @@
"write": "escrever arquivo",
"read": "ler arquivo",
"filename": "Nome do arquivo",
"path": "caminho",
"path": "caminho",
"action": "Ação",
"addnewline": "Adicionar nova linha (\\n) a cada carga útil?",
"createdir": "Criar diretório se não existir?",
@ -994,7 +994,6 @@
"objectSend": "Envia uma mensagem para cada par chave/valor",
"strBuff": "<b>Cadeia de caracteres</b> / <b>Armazenamento Temporário</b>",
"array": "<b>Matriz</b>",
"splitThe": "Dividir",
"splitUsing": "Dividir usando",
"splitLength": "Comprimento fixo de",
"stream": "Tratar como uma transmissão de mensagens",
@ -1067,9 +1066,9 @@
"batch" : {
"batch": "lote",
"mode": {
"label": "Modo",
"num-msgs": "Agrupar por número de mensagens",
"interval": "Agrupar por intervalo de tempo",
"label": "Modo",
"num-msgs": "Agrupar por número de mensagens",
"interval": "Agrupar por intervalo de tempo",
"concat": "Concatenar sequências"
},
"count": {

View File

@ -874,7 +874,6 @@
"objectSend":"Отправлять сообщение для каждой пары ключ/значение",
"strBuff":"<b>Строка</b> / <b>Буфер</b>",
"array":"<b>Массив</b>",
"splitThe": "Pазделить",
"splitUsing":"С помощью",
"splitLength":"Фикс. длина",
"stream":"Обрабатывать как поток сообщений",

View File

@ -997,7 +997,6 @@
"objectSend": "每个键值对作为单个消息发送",
"strBuff": "<b>字符串</b> / <b>Buffer</b>",
"array": "<b>数组</b>",
"splitThe": "Split",
"splitUsing": "拆分使用",
"splitLength": "固定长度",
"stream": "作为消息流处理",

View File

@ -866,7 +866,6 @@
"objectSend": "每個鍵值對作為單個消息發送",
"strBuff": "<b>字串</b> / <b>Buffer</b>",
"array": "<b>陣列</b>",
"splitThe": "Split",
"splitUsing": "拆分使用",
"splitLength": "固定長度",
"stream": "作為消息流處理",

View File

@ -1,6 +1,6 @@
{
"name": "@node-red/nodes",
"version": "4.0.9",
"version": "4.0.1",
"license": "Apache-2.0",
"repository": {
"type": "git",
@ -15,20 +15,20 @@
}
],
"dependencies": {
"acorn": "8.12.1",
"acorn-walk": "8.3.4",
"ajv": "8.17.1",
"body-parser": "1.20.3",
"acorn": "8.11.3",
"acorn-walk": "8.3.2",
"ajv": "8.14.0",
"body-parser": "1.20.2",
"cheerio": "1.0.0-rc.10",
"content-type": "1.0.5",
"cookie-parser": "1.4.7",
"cookie": "0.7.2",
"cookie-parser": "1.4.6",
"cookie": "0.6.0",
"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.1",
"got": "12.6.0",
"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": "3.0.0",
"tough-cookie": "^5.0.0",
"raw-body": "2.5.2",
"tough-cookie": "4.1.4",
"uuid": "9.0.1",
"ws": "7.5.10",
"xml2js": "0.6.2",

View File

@ -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 = localtgzRe.test(url) && slashRe.test(url) ? `"${url}"` : url;
installName = url;
isRegistryPackage = false;
} else {
log.warn(log._("server.install.install-failed-url",{name:module,url:url}));

View File

@ -1,6 +1,6 @@
{
"name": "@node-red/registry",
"version": "4.0.9",
"version": "4.0.1",
"license": "Apache-2.0",
"main": "./lib/index.js",
"repository": {
@ -16,11 +16,11 @@
}
],
"dependencies": {
"@node-red/util": "4.0.9",
"@node-red/util": "4.0.1",
"clone": "2.1.2",
"fs-extra": "11.2.0",
"semver": "7.6.3",
"tar": "7.4.3",
"semver": "7.5.4",
"tar": "7.2.0",
"uglify-js": "3.17.4"
}
}

View File

@ -96,11 +96,7 @@ var api = module.exports = {
} else if (scope === 'node') {
var node = runtime.nodes.getNode(id);
if (node) {
if (/^subflow:/.test(node.type)) {
ctx = runtime.nodes.getContext(node.id);
} else {
ctx = node.context();
}
ctx = node.context();
}
}
if (ctx) {
@ -108,25 +104,13 @@ var api = module.exports = {
store = store || availableStores.default;
ctx.get(key,store,function(err, v) {
if (opts.keysOnly) {
const result = {}
if (Array.isArray(v)) {
result.format = `array[${v.length}]`
resolve({ [store]: { format: `array[${v.length}]`}})
} else if (typeof v === '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'
resolve({ [store]: { keys: Object.keys(v), format: 'Object' } })
} else {
result.keys = []
resolve({ [store]: { keys: [] }})
}
resolve({ [store]: result })
return
}
var encoded = util.encodeObject({msg:v});
if (store !== availableStores.default) {
@ -163,7 +147,7 @@ var api = module.exports = {
}
return
}
result[store] = { keys: keys.map(key => { return { key }}) }
result[store] = { keys }
c--;
if (c === 0) {
if (!errorReported) {

View File

@ -719,14 +719,6 @@ 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);

View File

@ -49,14 +49,6 @@ class Group {
}
return this.parent.getSetting(key);
}
error(msg) {
this.parent.error(msg);
}
getContext(scope) {
return this.parent.getContext(scope);
}
}
module.exports = {

View File

@ -100,24 +100,7 @@ async function evaluateEnvProperties(flow, env, credentials) {
}
} else if (type ==='jsonata') {
pendingEvaluations.push(new Promise((resolve, _) => {
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) => {
redUtil.evaluateNodeProperty(value, 'jsonata', {_flow: flow}, null, (err, result) => {
if (!err) {
if (typeof result === 'object') {
result = { value: result, __clone__: true}
@ -130,10 +113,6 @@ 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);

View File

@ -23,16 +23,14 @@ module.exports = {
if (existingSessionId) {
connections.delete(opts.session)
const session = sessions.get(existingSessionId)
if (session) {
session.active = false
session.idleTimeout = setTimeout(() => {
sessions.delete(existingSessionId)
}, 30000)
runtime.events.emit('comms', {
topic: "multiplayer/connection-removed",
data: { session: existingSessionId }
})
}
session.active = false
session.idleTimeout = setTimeout(() => {
sessions.delete(existingSessionId)
}, 30000)
runtime.events.emit('comms', {
topic: "multiplayer/connection-removed",
data: { session: existingSessionId }
})
}
})
runtime.events.on('comms:message:multiplayer/connect', (opts) => {
@ -93,32 +91,29 @@ module.exports = {
const sessionId = connections.get(opts.session)
const session = sessions.get(sessionId)
if (session) {
if (opts.user) {
if (session.user.anonymous !== opts.user.anonymous) {
session.user = opts.user
runtime.events.emit('comms', {
topic: 'multiplayer/connection-added',
excludeSession: opts.session,
data: session
})
}
if (opts.user) {
if (session.user.anonymous !== opts.user.anonymous) {
session.user = opts.user
runtime.events.emit('comms', {
topic: 'multiplayer/connection-added',
excludeSession: opts.session,
data: session
})
}
session.location = opts.data
const payload = {
session: sessionId,
workspace: opts.data.workspace,
node: opts.data.node,
cursor: opts.data.cursor
}
runtime.events.emit('comms', {
topic: 'multiplayer/location',
data: payload,
excludeSession: opts.session
})
}
session.location = opts.data
const payload = {
session: sessionId,
workspace: opts.data.workspace,
node: opts.data.node
}
runtime.events.emit('comms', {
topic: 'multiplayer/location',
data: payload,
excludeSession: opts.session
})
})
}
}

View File

@ -25,7 +25,6 @@
"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__",

View File

@ -1,6 +1,6 @@
{
"name": "@node-red/runtime",
"version": "4.0.9",
"version": "4.0.1",
"license": "Apache-2.0",
"main": "./lib/index.js",
"repository": {
@ -16,11 +16,11 @@
}
],
"dependencies": {
"@node-red/registry": "4.0.9",
"@node-red/util": "4.0.9",
"@node-red/registry": "4.0.1",
"@node-red/util": "4.0.1",
"async-mutex": "0.5.0",
"clone": "2.1.2",
"express": "4.21.2",
"express": "4.19.2",
"fs-extra": "11.2.0",
"json-stringify-safe": "5.0.1",
"rfdc": "^1.3.1"

View File

@ -75,28 +75,12 @@ 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) {
utilLog("["+levelNames[msg.level]+"] "+JSON.stringify(msg));
util.log("["+levelNames[msg.level]+"] "+JSON.stringify(msg));
} else {
if (verbose && msg.msg && msg.msg.stack) {
utilLog("["+levelNames[msg.level]+"] "+(msg.type?"["+msg.type+":"+(msg.name||msg.id)+"] ":"")+msg.msg.stack);
util.log("["+levelNames[msg.level]+"] "+(msg.type?"["+msg.type+":"+(msg.name||msg.id)+"] ":"")+msg.msg.stack);
} else {
var message = msg.msg;
try {
@ -107,7 +91,7 @@ var consoleLogger = function(msg) {
message = 'Exception trying to log: '+util.inspect(message);
}
utilLog("["+levelNames[msg.level]+"] "+(msg.type?"["+msg.type+":"+(msg.name||msg.id)+"] ":"")+message);
util.log("["+levelNames[msg.level]+"] "+(msg.type?"["+msg.type+":"+(msg.name||msg.id)+"] ":"")+message);
}
}
}

View File

@ -1,6 +1,6 @@
{
"name": "@node-red/util",
"version": "4.0.9",
"version": "4.0.1",
"license": "Apache-2.0",
"repository": {
"type": "git",
@ -21,6 +21,6 @@
"jsonata": "2.0.5",
"lodash.clonedeep": "^4.5.0",
"moment": "2.30.1",
"moment-timezone": "0.5.46"
"moment-timezone": "0.5.45"
}
}

View File

@ -1,6 +1,6 @@
{
"name": "node-red",
"version": "4.0.9",
"version": "4.0.1",
"description": "Low-code programming for event-driven applications",
"homepage": "https://nodered.org",
"license": "Apache-2.0",
@ -31,18 +31,18 @@
"flow"
],
"dependencies": {
"@node-red/editor-api": "4.0.9",
"@node-red/runtime": "4.0.9",
"@node-red/util": "4.0.9",
"@node-red/nodes": "4.0.9",
"@node-red/editor-api": "4.0.1",
"@node-red/runtime": "4.0.1",
"@node-red/util": "4.0.1",
"@node-red/nodes": "4.0.1",
"basic-auth": "2.0.1",
"bcryptjs": "2.4.3",
"cors": "2.8.5",
"express": "4.21.2",
"express": "4.19.2",
"fs-extra": "11.2.0",
"node-red-admin": "^4.0.1",
"node-red-admin": "^4.0.0",
"nopt": "5.0.0",
"semver": "7.6.3"
"semver": "7.5.4"
},
"optionalDependencies": {
"@node-rs/bcrypt": "1.10.4"

View File

@ -36,12 +36,10 @@ function generateScript() {
packages.forEach(name => {
const tarName = name.replace(/@/,"").replace(/\//,"-")
lines.push(`npm publish ${tarName}-${version}.tgz ${tagArg}\n`);
})
if (updateNextToLatest) {
packages.forEach(name => {
if (updateNextToLatest) {
lines.push(`npm dist-tag add ${name}@${version} next\n`);
})
}
}
})
resolve(lines.join(""))
});
}

View File

@ -1009,29 +1009,6 @@ 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");

View File

@ -111,15 +111,7 @@ describe('trigger node', function() {
try {
if (rval) {
msg.should.have.property("payload");
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);
}
should.deepEqual(msg.payload, rval);
}
else {
msg.should.have.property("payload", val);
@ -134,7 +126,6 @@ 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;
@ -151,15 +142,7 @@ describe('trigger node', function() {
else {
if (rval) {
msg.should.have.property("payload");
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);
}
should.deepEqual(msg.payload, rval);
}
else {
msg.should.have.property("payload", val);
@ -183,9 +166,6 @@ 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"]] },

View File

@ -2067,27 +2067,6 @@ 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);
@ -2107,7 +2086,6 @@ 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);
@ -2128,7 +2106,6 @@ 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);
@ -2194,7 +2171,6 @@ 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); }
@ -2213,7 +2189,6 @@ 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); }
@ -2233,7 +2208,6 @@ 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); }
@ -2353,7 +2327,6 @@ 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); }

View File

@ -98,7 +98,7 @@ describe('BATCH node', function() {
var n2 = helper.getNode("n2");
check_data(n1, n2, results, done);
for(var i = 0; i < 6; i++) {
n1.receive({payload: i, parts: { count:6, index:i }});
n1.receive({payload: i});
}
});
}
@ -168,25 +168,6 @@ describe('BATCH node', function() {
check_count(flow, results, done);
});
it('should create seq. with count (more sent than count)', function(done) {
var flow = [{id:"n1", type:"batch", name: "BatchNode", mode: "count", count: 4, overlap: 0, interval: 10, allowEmptySequence: false, topics: [], wires:[["n2"]]},
{id:"n2", type:"helper"}];
var results = [
[0, 1, 2, 3]
];
check_count(flow, results, done);
});
it('should create seq. with count and terminate early if parts honoured', function(done) {
var flow = [{id:"n1", type:"batch", name: "BatchNode", mode: "count", count: 4, overlap: 0, interval: 10, allowEmptySequence:false, honourParts:true, topics: [], wires:[["n2"]]},
{id:"n2", type:"helper"}];
var results = [
[0, 1, 2, 3],
[4, 5]
];
check_count(flow, results, done);
});
it('should create seq. with count and overlap', function(done) {
var flow = [{id:"n1", type:"batch", name: "BatchNode", mode: "count", count: 3, overlap: 2, interval: 10, allowEmptySequence: false, topics: [], wires:[["n2"]]},
{id:"n2", type:"helper"}];
@ -474,7 +455,7 @@ describe('BATCH node', function() {
function mapiDoneTestHelper(done, mode, count, overlap, interval, allowEmptySequence, msgAndTimings) {
const completeNode = require("nr-test-utils").require("@node-red/nodes/core/common/24-complete.js");
const catchNode = require("nr-test-utils").require("@node-red/nodes/core/common/25-catch.js");
const flow = [{id:"batchNode1", type:"batch", name: "BatchNode", mode, count, overlap, interval,
const flow = [{id:"batchNode1", type:"batch", name: "BatchNode", mode, count, overlap, interval,
allowEmptySequence, topics: [{topic: "TA"}], wires:[[]]},
{id:"completeNode1",type:"complete",scope: ["batchNode1"],uncaught:false,wires:[["helperNode1"]]},
{id:"catchNode1", type:"catch",scope: ["batchNode1"],uncaught:false,wires:[["helperNode1"]]},
@ -501,13 +482,13 @@ describe('BATCH node', function() {
}
it('should call done() when message is sent (mode: count)', function(done) {
mapiDoneTestHelper(done, "count", 2, 0, 2, false, [
mapiDoneTestHelper(done, "count", 2, 0, 2, false, [
{ msg: {payload: 0}, delay: 0, avr: 0, var: 100},
{ msg: {payload: 1}, delay: 0, avr: 0, var: 100}
]);
});
it('should call done() when reset (mode: count)', function(done) {
mapiDoneTestHelper(done, "count", 2, 0, 2, false, [
mapiDoneTestHelper(done, "count", 2, 0, 2, false, [
{ msg: {payload: 0}, delay: 0, avr: 200, var: 100},
{ msg: {payload: 1, reset:true}, delay: 200, avr: 200, var: 100}
]);

View File

@ -258,29 +258,6 @@ 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; })

View File

@ -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(console, 'log').callsFake(function(arg){});
var spy = sinon.stub(util, 'log').callsFake(function(arg){});
var settings = {logging: { console: { level: 'metric', metrics: true } } };
log.init(settings);
});
afterEach(function() {
console.log.restore();
util.log.restore();
});
it('it can raise an error', function() {
var ret = log.error("This is an error");
sinon.assert.calledWithMatch(console.log,"[error] This is an error");
sinon.assert.calledWithMatch(util.log,"[error] This is an error");
});
it('it can raise a trace', function() {
var ret = log.trace("This is a trace");
sinon.assert.calledWithMatch(console.log,"[trace] This is a trace");
sinon.assert.calledWithMatch(util.log,"[trace] This is a trace");
});
it('it can raise a debug', function() {
var ret = log.debug("This is a debug");
sinon.assert.calledWithMatch(console.log,"[debug] This is a debug");
sinon.assert.calledWithMatch(util.log,"[debug] This is a debug");
});
it('it can raise a info', function() {
var ret = log.info("This is an info");
sinon.assert.calledWithMatch(console.log,"[info] This is an info");
sinon.assert.calledWithMatch(util.log,"[info] This is an info");
});
it('it can raise a warn', function() {
var ret = log.warn("This is a warn");
sinon.assert.calledWithMatch(console.log,"[warn] This is a warn");
sinon.assert.calledWithMatch(util.log,"[warn] This is a warn");
});
it('it can raise a metric', function() {
@ -66,10 +66,9 @@ describe("@node-red/util/log", function() {
metrics.msgid = "12345";
metrics.value = "the metric payload";
var ret = log.log(metrics);
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])
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));
body.should.have.a.property("nodeid","testid");
body.should.have.a.property("event","node.test.testevent");
body.should.have.a.property("msgid","12345");
@ -87,13 +86,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"});
console.log.calledOnce.should.be.true();
console.log.firstCall.args[0].indexOf("[nodeType:nodeName]").should.not.equal(-1);
util.log.calledOnce.should.be.true();
util.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"});
console.log.calledOnce.should.be.true();
console.log.firstCall.args[0].indexOf("[nodeType:nodeId]").should.not.equal(-1);
util.log.calledOnce.should.be.true();
util.log.firstCall.args[0].indexOf("[nodeType:nodeId]").should.not.equal(-1);
});
it('ignores lower level messages and metrics', function() {
@ -105,12 +104,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(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] ");
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] ");
});
it('ignores lower level messages but accepts metrics', function() {
var settings = {logging: { console: { level: 'log', metrics: true } } };
@ -121,12 +120,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(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] ");
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] ");
});
it('default settings set to INFO and metrics off', function() {
@ -137,12 +136,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(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] ");
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] ");
});
it('no logger used if custom logger handler does not exist', function() {
var settings = {logging: { customLogger: { level: 'trace', metrics: true } } };
@ -153,12 +152,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(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] ");
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] ");
});
it('add a custom log handler directly', function() {
@ -245,7 +244,7 @@ describe("@node-red/util/log", function() {
},
};
var ret = log.info(msg.msg);
sinon.assert.calledWithMatch(console.log,"my special message");
sinon.assert.calledWithMatch(util.log,"my special message");
});