Merge branch 'dev' into fix_html_tags

This commit is contained in:
Nick O'Leary 2024-09-11 15:27:45 +01:00 committed by GitHub
commit db51307e7f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
37 changed files with 557 additions and 204 deletions

View File

@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
node-version: [18, 20, 22] node-version: [18, 20, 22.4.x]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Use Node.js ${{ matrix.node-version }} - name: Use Node.js ${{ matrix.node-version }}

View File

@ -1,3 +1,38 @@
#### 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
- Ensure subflow instance credential property values are extracted (#4802) @knolleary
- Use `_ADD_` value for both `add new...` and `none` options (#4800) @GogoVega
- Fix the config node select value assignment (#4788) @GogoVega
- Add tooltip for number of subflow instance on info tab (#4786) @kazuhitoyokoi
- Add Japanese translations for v4.0.0 (#4785) @kazuhitoyokoi
Runtime
- Ensure group nodes are properly exported in /flow api (#4803) @knolleary
Nodes
- Joins: make using msg.parts optional in join node (#4796) @dceejay
- HTTP Request: UI proxy should setup agents for both http_proxy and https_proxy (#4794) @Steve-Mcl
- HTTP Request: Remove default user agent (#4791) @Steve-Mcl
#### 4.0.0: Milestone Release #### 4.0.0: Milestone Release
This marks the next major release of Node-RED. The following changes represent This marks the next major release of Node-RED. The following changes represent

View File

@ -1,6 +1,6 @@
{ {
"name": "node-red", "name": "node-red",
"version": "4.0.0", "version": "4.1.0-beta.0",
"description": "Low-code programming for event-driven applications", "description": "Low-code programming for event-driven applications",
"homepage": "https://nodered.org", "homepage": "https://nodered.org",
"license": "Apache-2.0", "license": "Apache-2.0",

View File

@ -182,6 +182,10 @@ function genericStrategy(adminApp,strategy) {
maxAge: null, maxAge: null,
...settings.httpAdminCookieOptions ...settings.httpAdminCookieOptions
} }
if (sessionOptions.cookie.name){
sessionOptions.name = sessionOptions.cookie.name
delete sessionOptions.cookie.name
}
} }
adminApp.use(session(sessionOptions)); adminApp.use(session(sessionOptions));
//TODO: all passport references ought to be in ./auth //TODO: all passport references ought to be in ./auth
@ -217,10 +221,10 @@ function genericStrategy(adminApp,strategy) {
adminApp.get('/auth/strategy', adminApp.get('/auth/strategy',
passport.authenticate(strategy.name, { passport.authenticate(strategy.name, {
session:false, session:false,
failureMessage: true, failWithError: true,
failureRedirect: settings.httpAdminRoot + '?session_message=Login Failed' failureMessage: true
}), }),
completeGenerateStrategyAuth, completeGenericStrategyAuth,
handleStrategyError handleStrategyError
); );
@ -232,14 +236,14 @@ function genericStrategy(adminApp,strategy) {
passport.authenticate(strategy.name, { passport.authenticate(strategy.name, {
session:false, session:false,
failureMessage: true, failureMessage: true,
failureRedirect: settings.httpAdminRoot + '?session_message=Login Failed' failWithError: true
}), }),
completeGenerateStrategyAuth, completeGenericStrategyAuth,
handleStrategyError handleStrategyError
); );
} }
function completeGenerateStrategyAuth(req,res) { function completeGenericStrategyAuth(req,res) {
var tokens = req.user.tokens; var tokens = req.user.tokens;
delete req.user.tokens; delete req.user.tokens;
// Successful authentication, redirect home. // Successful authentication, redirect home.
@ -249,6 +253,8 @@ function handleStrategyError(err, req, res, next) {
if (res.headersSent) { if (res.headersSent) {
return next(err) 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()}); log.audit({event: "auth.login.fail.oauth",error:err.toString()});
res.redirect(settings.httpAdminRoot + '?session_message='+err.toString()); res.redirect(settings.httpAdminRoot + '?session_message='+err.toString());
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "@node-red/editor-api", "name": "@node-red/editor-api",
"version": "4.0.0", "version": "4.1.0-beta.0",
"license": "Apache-2.0", "license": "Apache-2.0",
"main": "./lib/index.js", "main": "./lib/index.js",
"repository": { "repository": {
@ -16,8 +16,8 @@
} }
], ],
"dependencies": { "dependencies": {
"@node-red/util": "4.0.0", "@node-red/util": "4.1.0-beta.0",
"@node-red/editor-client": "4.0.0", "@node-red/editor-client": "4.1.0-beta.0",
"bcryptjs": "2.4.3", "bcryptjs": "2.4.3",
"body-parser": "1.20.2", "body-parser": "1.20.2",
"clone": "2.1.2", "clone": "2.1.2",

View File

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

View File

@ -27,7 +27,8 @@
"lock": "固定", "lock": "固定",
"unlock": "固定を解除", "unlock": "固定を解除",
"locked": "固定済み", "locked": "固定済み",
"unlocked": "固定なし" "unlocked": "固定なし",
"format": "形式"
}, },
"type": { "type": {
"string": "文字列", "string": "文字列",
@ -281,8 +282,8 @@
"selected": "選択したフロー", "selected": "選択したフロー",
"current": "現在のタブ", "current": "現在のタブ",
"all": "全てのタブ", "all": "全てのタブ",
"compact": "インデントのないJSONフォーマット", "compact": "インデントなし",
"formatted": "インデント付きのJSONフォーマット", "formatted": "インデント付き",
"copy": "書き出し", "copy": "書き出し",
"export": "ライブラリに書き出し", "export": "ライブラリに書き出し",
"exportAs": "書き出し先", "exportAs": "書き出し先",
@ -923,6 +924,8 @@
} }
}, },
"typedInput": { "typedInput": {
"selected": "__count__個を選択",
"selected_plural": "__count__個を選択",
"type": { "type": {
"str": "文字列", "str": "文字列",
"num": "数値", "num": "数値",

View File

@ -1,6 +1,6 @@
{ {
"name": "@node-red/editor-client", "name": "@node-red/editor-client",
"version": "4.0.0", "version": "4.1.0-beta.0",
"license": "Apache-2.0", "license": "Apache-2.0",
"repository": { "repository": {
"type": "git", "type": "git",

View File

@ -32,24 +32,28 @@ RED.contextMenu = (function () {
const canRemoveFromGroup = hasSelection && !!selection.nodes[0].g const canRemoveFromGroup = hasSelection && !!selection.nodes[0].g
let hasGroup, isAllGroups = true, hasDisabledNode, hasEnabledNode, hasLabeledNode, hasUnlabeledNode; let hasGroup, isAllGroups = true, hasDisabledNode, hasEnabledNode, hasLabeledNode, hasUnlabeledNode;
if (hasSelection) { if (hasSelection) {
selection.nodes.forEach(n => { const nodes = selection.nodes.slice();
while (nodes.length) {
const n = nodes.shift();
if (n.type === 'group') { if (n.type === 'group') {
hasGroup = true; hasGroup = true;
nodes.push(...n.nodes);
} else { } else {
isAllGroups = false; isAllGroups = false;
} if (n.d) {
if (n.d) { hasDisabledNode = true;
hasDisabledNode = true; } else {
} else { hasEnabledNode = true;
hasEnabledNode = true; }
} }
if (n.l === undefined || n.l) { if (n.l === undefined || n.l) {
hasLabeledNode = true; hasLabeledNode = true;
} else { } else {
hasUnlabeledNode = true; hasUnlabeledNode = true;
} }
}); }
} }
const offset = $("#red-ui-workspace-chart").offset() const offset = $("#red-ui-workspace-chart").offset()
let addX = options.x - offset.left + $("#red-ui-workspace-chart").scrollLeft() let addX = options.x - offset.left + $("#red-ui-workspace-chart").scrollLeft()

View File

@ -157,6 +157,12 @@ RED.editor = (function() {
} }
} }
if (valid && "validate" in definition[property]) { if (valid && "validate" in definition[property]) {
if (definition[property].hasOwnProperty("required") &&
definition[property].required === false) {
if (value === "") {
return true;
}
}
try { try {
var opt = {}; var opt = {};
if (label) { if (label) {
@ -183,6 +189,11 @@ RED.editor = (function() {
}); });
} }
} else if (valid) { } 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 the validator is not provided in node property => Check if the input has a validator
if ("category" in node._def) { if ("category" in node._def) {
const isConfig = node._def.category === "config"; const isConfig = node._def.category === "config";
@ -413,11 +424,8 @@ RED.editor = (function() {
if (selectedOpt?.data('env')) { if (selectedOpt?.data('env')) {
disableButton(addButton, true); disableButton(addButton, true);
disableButton(editButton, true); disableButton(editButton, true);
// disable the edit button if no options available // disable the edit button if no options available or 'none' selected
} else if (optionsLength === 1 && selectedOpt.val() === "_ADD_") { } else if (optionsLength === 1 || selectedOpt.val() === "_ADD_") {
disableButton(addButton, false);
disableButton(editButton, true);
} else if (selectedOpt.val() === "") {
disableButton(addButton, false); disableButton(addButton, false);
disableButton(editButton, true); disableButton(editButton, true);
} else { } else {
@ -426,14 +434,9 @@ RED.editor = (function() {
} }
}); });
var label = ""; // If the value is "", 'add new...' option if no config node available or 'none' option
var configNode = RED.nodes.node(nodeValue); // Otherwise, it's a config node
select.val(nodeValue || '_ADD_');
if (configNode) {
label = RED.utils.getNodeLabel(configNode, configNode.id);
}
input.val(label);
} }
/** /**
@ -934,9 +937,11 @@ RED.editor = (function() {
} }
if (!configNodes.length) { if (!configNodes.length) {
// Add 'add new...' option
select.append('<option value="_ADD_" selected>' + RED._("editor.addNewType", { type: label }) + '</option>'); select.append('<option value="_ADD_" selected>' + RED._("editor.addNewType", { type: label }) + '</option>');
} else { } else {
select.append('<option value="">' + RED._("editor.inputs.none") + '</option>'); // Add 'none' option
select.append('<option value="_ADD_">' + RED._("editor.inputs.none") + '</option>');
} }
window.setTimeout(function() { select.trigger("change");},50); window.setTimeout(function() { select.trigger("change");},50);

View File

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

View File

@ -11,9 +11,22 @@ RED.editor.mermaid = (function () {
if (!initializing) { if (!initializing) {
initializing = true initializing = true
$.getScript( // Find the cache-buster:
'vendor/mermaid/mermaid.min.js', let cacheBuster
function (data, stat, jqxhr) { $('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) {
mermaid.initialize({ mermaid.initialize({
startOnLoad: false, startOnLoad: false,
theme: RED.settings.get('mermaid', {}).theme theme: RED.settings.get('mermaid', {}).theme
@ -24,7 +37,7 @@ RED.editor.mermaid = (function () {
render(pending) render(pending)
} }
} }
) });
} }
} else { } else {
const nodes = document.querySelectorAll(selector) const nodes = document.querySelectorAll(selector)

View File

@ -1100,7 +1100,7 @@ RED.subflow = (function() {
input.val(val.value); input.val(val.value);
break; break;
case "cred": case "cred":
input = $('<input type="password">').css('width','70%').appendTo(row); input = $('<input type="password">').css('width','70%').attr('id', elId).appendTo(row);
if (node.credentials) { if (node.credentials) {
if (node.credentials[tenv.name]) { if (node.credentials[tenv.name]) {
input.val(node.credentials[tenv.name]); input.val(node.credentials[tenv.name]);
@ -1346,7 +1346,7 @@ RED.subflow = (function() {
} }
break; break;
case "cred": case "cred":
item.value = input.val(); item.value = input.typedInput('value');
item.type = 'cred'; item.type = 'cred';
break; break;
case "spinner": case "spinner":

View File

@ -103,7 +103,7 @@ RED.sidebar.info.outliner = (function() {
evt.stopPropagation(); evt.stopPropagation();
RED.search.show("type:subflow:"+n.id); RED.search.show("type:subflow:"+n.id);
}) })
// RED.popover.tooltip(userCountBadge,function() { return RED._('editor.nodesUse',{count:n.users.length})}); RED.popover.tooltip(subflowInstanceBadge,function() { return RED._('subflow.subflowInstances',{count:n.instances.length})});
} }
if (n._def.category === "config" && n.type !== "group") { if (n._def.category === "config" && n.type !== "group") {
var userCountBadge = $('<button type="button" class="red-ui-info-outline-item-control-users red-ui-button red-ui-button-small"><i class="fa fa-toggle-right"></i></button>').text(n.users.length).appendTo(controls).on("click",function(evt) { var userCountBadge = $('<button type="button" class="red-ui-info-outline-item-control-users red-ui-button red-ui-button-small"><i class="fa fa-toggle-right"></i></button>').text(n.users.length).appendTo(controls).on("click",function(evt) {

View File

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

View File

@ -108,12 +108,13 @@ in your Node-RED user directory (${RED.settings.userDir}).
if (n.proxy && proxyConfig) { if (n.proxy && proxyConfig) {
proxyOptions.env = { proxyOptions.env = {
no_proxy: (proxyConfig.noproxy || []).join(','), no_proxy: (proxyConfig.noproxy || []).join(','),
http_proxy: (proxyConfig.url) http_proxy: (proxyConfig.url),
https_proxy: (proxyConfig.url)
} }
} }
return getProxyForUrl(url, proxyOptions) return getProxyForUrl(url, proxyOptions)
} }
let prox = getProxy(nodeUrl || '') let prox = nodeUrl ? getProxy(nodeUrl) : null
let timingLog = false; let timingLog = false;
if (RED.settings.hasOwnProperty("httpRequestTimingLog")) { if (RED.settings.hasOwnProperty("httpRequestTimingLog")) {
@ -534,9 +535,7 @@ in your Node-RED user directory (${RED.settings.userDir}).
opts.headers[clSet] = opts.headers['content-length']; opts.headers[clSet] = opts.headers['content-length'];
delete opts.headers['content-length']; delete opts.headers['content-length'];
} }
if (!opts.headers.hasOwnProperty('user-agent')) {
opts.headers['user-agent'] = 'Mozilla/5.0 (Node-RED)';
}
if (proxyUrl) { if (proxyUrl) {
const match = proxyUrl.match(/^(https?:\/\/)?(.+)?:([0-9]+)?/i); const match = proxyUrl.match(/^(https?:\/\/)?(.+)?:([0-9]+)?/i);
if (match) { if (match) {
@ -566,7 +565,7 @@ in your Node-RED user directory (${RED.settings.userDir}).
//need both incase of http -> https redirect //need both incase of http -> https redirect
opts.agent = { opts.agent = {
http: new HttpProxyAgent(proxyOptions), http: new HttpProxyAgent(proxyOptions),
https: new HttpProxyAgent(proxyOptions) https: new HttpsProxyAgent(proxyOptions)
}; };
} else { } else {

View File

@ -17,7 +17,11 @@
<script type="text/html" data-template-name="split"> <script type="text/html" data-template-name="split">
<!-- <div class="form-row"><span data-i18n="[html]split.intro"></span></div> --> <!-- <div class="form-row"><span data-i18n="[html]split.intro"></span></div> -->
<div class="form-row"> <div class="form-row">
<label for="node-input-property"><i class="fa fa-forward"></i> <span data-i18n="split.split"></span></label> <label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="node-red:common.label.name"></span></label>
<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>
<input type="text" id="node-input-property" style="width:70%;"> <input type="text" id="node-input-property" style="width:70%;">
</div> </div>
<div class="form-row"><span data-i18n="[html]split.strBuff"></span></div> <div class="form-row"><span data-i18n="[html]split.strBuff"></span></div>
@ -43,10 +47,6 @@
<label for="node-input-addname-cb" style="width:auto;" data-i18n="split.addname"></label> <label for="node-input-addname-cb" style="width:auto;" data-i18n="split.addname"></label>
<input type="text" id="node-input-addname" style="width:70%"> <input type="text" id="node-input-addname" style="width:70%">
</div> </div>
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="node-red:common.label.name"></span></label>
<input type="text" id="node-input-name" data-i18n="[placeholder]node-red:common.label.name">
</div>
</script> </script>
<script type="text/javascript"> <script type="text/javascript">
@ -122,6 +122,10 @@
<script type="text/html" data-template-name="join"> <script type="text/html" data-template-name="join">
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="common.label.name"></span></label>
<input type="text" id="node-input-name" data-i18n="[placeholder]common.label.name">
</div>
<div class="form-row"> <div class="form-row">
<label data-i18n="join.mode.mode"></label> <label data-i18n="join.mode.mode"></label>
<select id="node-input-mode" style="width:200px;"> <select id="node-input-mode" style="width:200px;">
@ -157,6 +161,12 @@
<input type="text" id="node-input-joiner" style="width:70%"> <input type="text" id="node-input-joiner" style="width:70%">
<input type="hidden" id="node-input-joinerType"> <input type="hidden" id="node-input-joinerType">
</div> </div>
<div class="form-row">
<input type="checkbox" id="node-input-useparts" style="margin-left:8px; margin-right:8px; vertical-align:baseline; width:auto;">
<label for="node-input-useparts" style="width:auto;" data-i18n="join.useparts"></label>
</div>
<div class="form-row node-row-trigger" id="trigger-row"> <div class="form-row node-row-trigger" id="trigger-row">
<label style="width:auto;" data-i18n="join.send"></label> <label style="width:auto;" data-i18n="join.send"></label>
<ul> <ul>
@ -195,10 +205,6 @@
<label for="node-input-reduceRight" data-i18n="join.reduce.right" style="width:70%; margin-left:10px;"></label> <label for="node-input-reduceRight" data-i18n="join.reduce.right" style="width:70%; margin-left:10px;"></label>
</div> </div>
</div> </div>
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="common.label.name"></span></label>
<input type="text" id="node-input-name" data-i18n="[placeholder]common.label.name">
</div>
<div class="form-tips form-tips-auto hide" data-i18n="[html]join.tip"></div> <div class="form-tips form-tips-auto hide" data-i18n="[html]join.tip"></div>
</script> </script>
@ -234,6 +240,7 @@
}, },
joiner: { value:"\\n"}, joiner: { value:"\\n"},
joinerType: { value:"str"}, joinerType: { value:"str"},
useparts: { value:false },
accumulate: { value:"false" }, accumulate: { value:"false" },
timeout: {value:""}, timeout: {value:""},
count: {value:""}, count: {value:""},
@ -259,6 +266,12 @@
}, },
oneditprepare: function() { oneditprepare: function() {
var node = this; var node = this;
$("#node-input-useparts").on("change", function(e) {
if (node.useparts === undefined) {
node.useparts = true;
$("#node-input-useparts").attr('checked', true);
}
});
$("#node-input-mode").on("change", function(e) { $("#node-input-mode").on("change", function(e) {
var val = $(this).val(); var val = $(this).val();

View File

@ -444,6 +444,8 @@ module.exports = function(RED) {
this.count = Number(n.count || 0); this.count = Number(n.count || 0);
this.joiner = n.joiner||""; this.joiner = n.joiner||"";
this.joinerType = n.joinerType||"str"; this.joinerType = n.joinerType||"str";
if (n.useparts === undefined) { this.useparts = true; }
else { this.useparts = n.useparts || false; }
this.reduce = (this.mode === "reduce"); this.reduce = (this.mode === "reduce");
if (this.reduce) { if (this.reduce) {
@ -611,7 +613,7 @@ module.exports = function(RED) {
return; return;
} }
if (node.mode === 'custom' && msg.hasOwnProperty('parts')) { if (node.mode === 'custom' && msg.hasOwnProperty('parts') && node.useparts === false ) {
if (msg.parts.hasOwnProperty('parts')) { if (msg.parts.hasOwnProperty('parts')) {
msg.parts = { parts: msg.parts.parts }; msg.parts = { parts: msg.parts.parts };
} }

View File

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

View File

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

View File

@ -20,12 +20,26 @@
<dt class="optional">delay <span class="property-type">number</span></dt> <dt class="optional">delay <span class="property-type">number</span></dt>
<dd>Legt die Verzögerung in Millisekunden fest, die auf die Nachricht angewendet werden soll. <dd>Legt die Verzögerung in Millisekunden fest, die auf die Nachricht angewendet werden soll.
Zur Nutzung dieser Option muss <i>Verzög. mit msg.delay überschreibbar</i> aktiviert sein.</dd> Zur Nutzung dieser Option muss <i>Verzög. mit msg.delay überschreibbar</i> aktiviert sein.</dd>
<dt class="optional">rate <span class="property-type">number</span></dt>
<dd>Setzt die Verzögerung in Millisekunden zwischen den Nachrichten. Diese Node überschreibt die
bestehende Verzögerung die in der Node konfiguration, wenn die empfangende Nachricht <code>msg.rate</code>
in Millisekunden enthält. Dies trifft nur zu, wenn in der Node konfiguriert ist, das empfangene
Nachrichten den konfigurierten Wert überschreiben können.</dd>
<dt class="optional">reset</dt> <dt class="optional">reset</dt>
<dd>Wenn bei der empfangenen Nachricht diese Eigenschaft auf einen beliebigen Wert gesetzt ist, <dd>Wenn bei der empfangenen Nachricht diese Eigenschaft auf einen beliebigen Wert gesetzt ist,
werden alle im Node gepufferten Nachrichten gelöscht.</dd> werden alle im Node gepufferten Nachrichten gelöscht.</dd>
<dt class="optional">flush</dt> <dt class="optional">flush</dt>
<dd>Wenn bei der empfangenen Nachricht diese Eigenschaft auf einen beliebigen Wert gesetzt ist, <dd>Wenn bei der empfangenen Nachricht diese Eigenschaft auf einen beliebigen Wert gesetzt ist,
werden alle im Node gepufferten Nachrichten sofort gesendet.</dd> werden alle im Node gepufferten Nachrichten sofort gesendet.</dd>
<dt class="optional">flush</dt>
<dd>Wenn bei der empfangenen Nachricht diese Eigenschaft auf einen numerischen Wert gesetzt ist,
wird diese Anzahl an Nachrichten sofort gesendet. Wenn ein anderer Typ gesetzt ist (z.B. Boolean),
werden alle in der Node gepufferten Nachrichten gesendet.</dd>
<dt class="optional">toFront</dt>
<dd>Wenn diese Eigenschaft im Ratenbegrenzungsmodus für die empfangene Nachricht auf den booleschen Wert
<code>true</code> gesetzt ist, Anschließend wird die Nachricht an den Anfang der Warteschlange verschoben
und als nächstes freigegeben. Dies kann in Kombination mit <code>msg.flush=1</code> verwendet werden, um sofort erneut zu senden.
</dd>
</dl> </dl>
<h3>Details</h3> <h3>Details</h3>
<p>Wenn Verzögerung als Nachrichtenaktion eingestellt ist, kann die Verzögerungszeit ein fixer Wert, <p>Wenn Verzögerung als Nachrichtenaktion eingestellt ist, kann die Verzögerungszeit ein fixer Wert,

View File

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

View File

@ -1011,12 +1011,13 @@
"tip": "Tip: The filename should be an absolute path, otherwise it will be relative to the working directory of the Node-RED process." "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": "split",
"intro": "Split <code>msg.payload</code> based on type:", "intro": "Split <code>msg.payload</code> based on type:",
"object": "<b>Object</b>", "object": "<b>Object</b>",
"objectSend": "Send a message for each key/value pair", "objectSend": "Send a message for each key/value pair",
"strBuff": "<b>String</b> / <b>Buffer</b>", "strBuff": "<b>String</b> / <b>Buffer</b>",
"array": "<b>Array</b>", "array": "<b>Array</b>",
"splitThe": "Split the",
"splitUsing": "Split using", "splitUsing": "Split using",
"splitLength": "Fixed length of", "splitLength": "Fixed length of",
"stream": "Handle as a stream of messages", "stream": "Handle as a stream of messages",
@ -1046,6 +1047,7 @@
"joinedUsing": "joined using", "joinedUsing": "joined using",
"send": "Send the message:", "send": "Send the message:",
"afterCount": "After a number of message parts", "afterCount": "After a number of message parts",
"useparts": "Use existing msg.parts property",
"count": "count", "count": "count",
"subsequent": "and every subsequent message.", "subsequent": "and every subsequent message.",
"afterTimeout": "After a timeout following the first message", "afterTimeout": "After a timeout following the first message",
@ -1112,6 +1114,7 @@
"too-many": "too many pending messages in batch node", "too-many": "too many pending messages in batch node",
"unexpected": "unexpected mode", "unexpected": "unexpected mode",
"no-parts": "no parts property in message", "no-parts": "no parts property in message",
"honourParts": "Allow msg.parts to also complete batch operation.",
"error": { "error": {
"invalid-count": "Invalid count", "invalid-count": "Invalid count",
"invalid-overlap": "Invalid overlap", "invalid-overlap": "Invalid overlap",

View File

@ -1017,6 +1017,7 @@
"objectSend": "各key/valueペアのメッセージを送信", "objectSend": "各key/valueペアのメッセージを送信",
"strBuff": "<b>文字列</b> / <b>バッファ</b>", "strBuff": "<b>文字列</b> / <b>バッファ</b>",
"array": "<b>配列</b>", "array": "<b>配列</b>",
"splitThe": "に基づく",
"splitUsing": "分割", "splitUsing": "分割",
"splitLength": "固定長", "splitLength": "固定長",
"stream": "メッセージのストリームとして処理", "stream": "メッセージのストリームとして処理",

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
{ {
"name": "@node-red/nodes", "name": "@node-red/nodes",
"version": "4.0.0", "version": "4.1.0-beta.0",
"license": "Apache-2.0", "license": "Apache-2.0",
"repository": { "repository": {
"type": "git", "type": "git",

View File

@ -1,6 +1,6 @@
{ {
"name": "@node-red/registry", "name": "@node-red/registry",
"version": "4.0.0", "version": "4.1.0-beta.0",
"license": "Apache-2.0", "license": "Apache-2.0",
"main": "./lib/index.js", "main": "./lib/index.js",
"repository": { "repository": {
@ -16,7 +16,7 @@
} }
], ],
"dependencies": { "dependencies": {
"@node-red/util": "4.0.0", "@node-red/util": "4.1.0-beta.0",
"clone": "2.1.2", "clone": "2.1.2",
"fs-extra": "11.2.0", "fs-extra": "11.2.0",
"semver": "7.5.4", "semver": "7.5.4",

View File

@ -645,16 +645,27 @@ function getFlow(id) {
if (id !== 'global') { if (id !== 'global') {
result.nodes = []; result.nodes = [];
} }
if (flow.groups) {
var nodeIds = Object.keys(flow.groups);
if (nodeIds.length > 0) {
nodeIds.forEach(function(nodeId) {
var node = jsonClone(flow.groups[nodeId]);
delete node.credentials;
result.nodes.push(node)
})
}
}
if (flow.nodes) { if (flow.nodes) {
var nodeIds = Object.keys(flow.nodes); var nodeIds = Object.keys(flow.nodes);
if (nodeIds.length > 0) { if (nodeIds.length > 0) {
result.nodes = nodeIds.map(function(nodeId) { nodeIds.forEach(function(nodeId) {
var node = jsonClone(flow.nodes[nodeId]); var node = jsonClone(flow.nodes[nodeId]);
if (node.type === 'link out') { if (node.type === 'link out') {
delete node.wires; delete node.wires;
} }
delete node.credentials; delete node.credentials;
return node; result.nodes.push(node)
}) })
} }
} }
@ -680,6 +691,17 @@ function getFlow(id) {
delete node.credentials delete node.credentials
return node return node
}); });
if (subflow.groups) {
var nodeIds = Object.keys(subflow.groups);
if (nodeIds.length > 0) {
nodeIds.forEach(function(nodeId) {
var node = jsonClone(subflow.groups[nodeId]);
delete node.credentials;
subflow.nodes.push(node)
})
}
delete subflow.groups
}
if (subflow.configs) { if (subflow.configs) {
var configIds = Object.keys(subflow.configs); var configIds = Object.keys(subflow.configs);
subflow.configs = configIds.map(function(id) { subflow.configs = configIds.map(function(id) {

View File

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

View File

@ -1,6 +1,6 @@
{ {
"name": "@node-red/runtime", "name": "@node-red/runtime",
"version": "4.0.0", "version": "4.1.0-beta.0",
"license": "Apache-2.0", "license": "Apache-2.0",
"main": "./lib/index.js", "main": "./lib/index.js",
"repository": { "repository": {
@ -16,8 +16,8 @@
} }
], ],
"dependencies": { "dependencies": {
"@node-red/registry": "4.0.0", "@node-red/registry": "4.1.0-beta.0",
"@node-red/util": "4.0.0", "@node-red/util": "4.1.0-beta.0",
"async-mutex": "0.5.0", "async-mutex": "0.5.0",
"clone": "2.1.2", "clone": "2.1.2",
"express": "4.19.2", "express": "4.19.2",

View File

@ -1,6 +1,6 @@
{ {
"name": "@node-red/util", "name": "@node-red/util",
"version": "4.0.0", "version": "4.1.0-beta.0",
"license": "Apache-2.0", "license": "Apache-2.0",
"repository": { "repository": {
"type": "git", "type": "git",

View File

@ -1,6 +1,6 @@
{ {
"name": "node-red", "name": "node-red",
"version": "4.0.0", "version": "4.1.0-beta.0",
"description": "Low-code programming for event-driven applications", "description": "Low-code programming for event-driven applications",
"homepage": "https://nodered.org", "homepage": "https://nodered.org",
"license": "Apache-2.0", "license": "Apache-2.0",
@ -31,10 +31,10 @@
"flow" "flow"
], ],
"dependencies": { "dependencies": {
"@node-red/editor-api": "4.0.0", "@node-red/editor-api": "4.1.0-beta.0",
"@node-red/runtime": "4.0.0", "@node-red/runtime": "4.1.0-beta.0",
"@node-red/util": "4.0.0", "@node-red/util": "4.1.0-beta.0",
"@node-red/nodes": "4.0.0", "@node-red/nodes": "4.1.0-beta.0",
"basic-auth": "2.0.1", "basic-auth": "2.0.1",
"bcryptjs": "2.4.3", "bcryptjs": "2.4.3",
"cors": "2.8.5", "cors": "2.8.5",

View File

@ -17,6 +17,8 @@
var http = require("http"); var http = require("http");
var https = require("https"); var https = require("https");
var should = require("should"); var should = require("should");
var sinon = require("sinon");
var httpProxyHelper = require("nr-test-utils").require("@node-red/nodes/core/network/lib/proxyHelper.js");
var express = require("express"); var express = require("express");
var bodyParser = require('body-parser'); var bodyParser = require('body-parser');
var stoppable = require('stoppable'); var stoppable = require('stoppable');
@ -493,6 +495,7 @@ describe('HTTP Request Node', function() {
}); });
afterEach(function() { afterEach(function() {
sinon.restore();
process.env.http_proxy = preEnvHttpProxyLowerCase; process.env.http_proxy = preEnvHttpProxyLowerCase;
process.env.HTTP_PROXY = preEnvHttpProxyUpperCase; process.env.HTTP_PROXY = preEnvHttpProxyUpperCase;
// On Windows, if environment variable of NO_PROXY that includes lower cases // On Windows, if environment variable of NO_PROXY that includes lower cases
@ -1799,27 +1802,80 @@ describe('HTTP Request Node', function() {
}) })
}); });
//Removing HTTP Proxy testcases as GOT + Proxy_Agent doesn't work with mock'd proxy it('should use env var http_proxy', function(done) {
/* */ const url = getTestURL('/postInspect')
it('should use http_proxy', function(done) { const proxyUrl = "http://localhost:" + testProxyPort
var flow = [{id:"n1",type:"http request",wires:[["n2"]],method:"POST",ret:"obj",url:getTestURL('/postInspect')},
{id:"n2", type:"helper"}]; const flow = [
{ id: "n1", type: "http request", wires: [["n2"]], method: "POST", ret: "obj", url: url },
{ id: "n2", type: "helper" },
];
const proxySpy = sinon.spy(httpProxyHelper, 'getProxyForUrl')
const testNode = [httpRequestNode, httpProxyNode];
deleteProxySetting(); deleteProxySetting();
process.env.http_proxy = "http://localhost:" + testProxyPort; process.env.http_proxy = proxyUrl
helper.load(httpRequestNode, flow, function() { helper.load(testNode, flow, function (msg) {
var n1 = helper.getNode("n1"); try {
var n2 = helper.getNode("n2"); // static URL set in the nodes configuration and the proxy will be setup upon initialisation
n2.on("input", function(msg) { proxySpy.calledOnce.should.be.true()
try { proxySpy.calledWith(url, { }).should.be.true()
msg.should.have.property('statusCode',200); proxySpy.returnValues[0].should.be.equal(proxyUrl)
msg.payload.should.have.property('headers'); done()
//msg.payload.headers.should.have.property('x-testproxy-header','foobar'); } catch (err) {
done(); done(err);
} catch(err) { }
done(err); });
} });
});
n1.receive({payload:"foo"}); it('should use env var https_proxy', function(done) {
const url = getSslTestURL('/postInspect')
const proxyUrl = "http://localhost:" + testProxyPort
const flow = [
{ id: "n1", type: "http request", wires: [["n2"]], method: "POST", ret: "obj", url: url },
{ id: "n2", type: "helper" },
];
const proxySpy = sinon.spy(httpProxyHelper, 'getProxyForUrl')
const testNode = [httpRequestNode, httpProxyNode];
deleteProxySetting();
process.env.https_proxy = proxyUrl
helper.load(testNode, flow, function (msg) {
try {
// static URL set in the nodes configuration and the proxy will be setup upon initialisation
proxySpy.calledOnce.should.be.true()
proxySpy.calledWith(url, { }).should.be.true()
proxySpy.returnValues[0].should.be.equal(proxyUrl)
done()
} catch (err) {
done(err);
}
});
});
it('should not use env var http*_proxy when no_proxy is set', function(done) {
const url = getSslTestURL('/postInspect')
const proxyUrl = "http://localhost:" + testProxyPort
const flow = [
{ id: "n1", type: "http request", wires: [["n2"]], method: "POST", ret: "obj", url: url },
{ id: "n2", type: "helper" },
];
const proxySpy = sinon.spy(httpProxyHelper, 'getProxyForUrl')
const testNode = [httpRequestNode, httpProxyNode];
deleteProxySetting();
process.env.http_proxy = proxyUrl
process.env.https_proxy = proxyUrl
process.env.no_proxy = "localhost"
helper.load(testNode, flow, function (msg) {
try {
// static URL set in the nodes configuration and the proxy will be setup upon initialisation
proxySpy.calledOnce.should.be.true()
proxySpy.calledWith(url, { }).should.be.true()
proxySpy.returnValues[0].should.be.equal('')
done()
} catch (err) {
done(err);
}
}); });
}); });
@ -1997,6 +2053,135 @@ describe('HTTP Request Node', function() {
}); });
}); });
it('should use UI proxy for statically configured URL', function (done) {
const url = getTestURL('/postInspect')
const proxyUrl = "http://localhost:" + testProxyPort
const flow = [
{ id: "n1", type: "http request", wires: [["n2"]], method: "POST", ret: "obj", url: url, proxy: "n3" },
{ id: "n2", type: "helper" },
{ id: "n3", type: "http proxy", url: proxyUrl, noproxy: ["foo"] }
];
const proxySpy = sinon.spy(httpProxyHelper, 'getProxyForUrl')
const testNode = [httpRequestNode, httpProxyNode];
deleteProxySetting();
// static URL set in the nodes configuration will cause the proxy setup to be called
// no no need to send a message to the node
helper.load(testNode, flow, function () {
try {
// ensure getProxyForUrl was called and returned the correct proxy URL
proxySpy.calledOnce.should.be.true()
proxySpy.calledWith(url, { env: { no_proxy: "foo", http_proxy: proxyUrl, https_proxy: proxyUrl } }).should.be.true()
proxySpy.returnValues[0].should.be.equal(proxyUrl)
done();
} catch (err) {
done(err);
}
});
});
it('should use UI proxy for HTTP URL passed in via msg', function (done) {
const url = getTestURL('/postInspect')
const proxyUrl = "http://localhost:" + testProxyPort
const flow = [
{ id: "n1", type: "http request", wires: [["n2"]], method: "POST", ret: "obj", url: "", proxy: "n3" },
{ id: "n2", type: "helper" },
{ id: "n3", type: "http proxy", url: proxyUrl, noproxy: ["foo,bar"] }
];
const proxySpy = sinon.spy(httpProxyHelper, 'getProxyForUrl')
const testNode = [httpRequestNode, httpProxyNode];
deleteProxySetting();
helper.load(testNode, flow, function () {
const n1 = helper.getNode("n1");
const n2 = helper.getNode("n2");
try {
proxySpy.calledOnce.should.be.false() // proxy setup should not be called when there is no URL to check needs proxying
} catch (err) {
done(err);
return
}
n2.on("input", function (msg) {
try {
// ensure getProxyForUrl was called and returned the correct proxy URL
proxySpy.calledOnce.should.be.true()
proxySpy.calledWith(url, { env: { no_proxy: "foo,bar", http_proxy: proxyUrl, https_proxy: proxyUrl } }).should.be.true()
proxySpy.returnValues[0].should.be.equal(proxyUrl)
done();
} catch (err) {
done(err);
}
});
n1.receive({ url: url });
});
});
it('should use UI proxy for HTTPS URL passed in via msg', function (done) {
const url = getSslTestURL('/postInspect')
const proxyUrl = "http://localhost:" + testProxyPort
const flow = [
{ id: "n1", type: "http request", wires: [["n2"]], method: "POST", ret: "obj", url: "", proxy: "n3" },
{ id: "n2", type: "helper" },
{ id: "n3", type: "http proxy", url: proxyUrl, noproxy: ["foo,bar,baz"] }
];
const proxySpy = sinon.spy(httpProxyHelper, 'getProxyForUrl')
const testNode = [httpRequestNode, httpProxyNode];
deleteProxySetting();
helper.load(testNode, flow, function () {
const n1 = helper.getNode("n1");
const n2 = helper.getNode("n2");
try {
proxySpy.calledOnce.should.be.false() // proxy setup should not be called when there is no URL to check needs proxying
} catch (err) {
done(err);
return
}
n2.on("input", function (msg) {
try {
// ensure getProxyForUrl was called and returned the correct proxy URL
proxySpy.calledOnce.should.be.true()
proxySpy.calledWith(url, { env: { no_proxy: "foo,bar,baz", http_proxy: proxyUrl, https_proxy: proxyUrl } }).should.be.true()
proxySpy.returnValues[0].should.be.equal(proxyUrl)
done();
} catch (err) {
done(err);
}
});
n1.receive({ url: url });
});
});
it('should not use UI proxy if noproxy excludes it', function (done) {
const url = getSslTestURL('/postInspect')
const proxyUrl = "http://localhost:" + testProxyPort
const flow = [
{ id: "n1", type: "http request", wires: [["n2"]], method: "POST", ret: "obj", url: "", proxy: "n3" },
{ id: "n2", type: "helper" },
{ id: "n3", type: "http proxy", url: proxyUrl, noproxy: ["foo,localhost,baz"] }
];
const proxySpy = sinon.spy(httpProxyHelper, 'getProxyForUrl')
const testNode = [httpRequestNode, httpProxyNode];
deleteProxySetting();
helper.load(testNode, flow, function () {
const n1 = helper.getNode("n1");
const n2 = helper.getNode("n2");
try {
proxySpy.calledOnce.should.be.false() // proxy setup should not be called when there is no URL to check needs proxying
} catch (err) {
done(err);
return
}
n2.on("input", function (msg) {
try {
// ensure getProxyForUrl was called and returned no proxy
proxySpy.calledOnce.should.be.true()
proxySpy.calledWith(url, { env: { no_proxy: "foo,localhost,baz", http_proxy: proxyUrl, https_proxy: proxyUrl } }).should.be.true()
proxySpy.returnValues[0].should.be.equal('')
done();
} catch (err) {
done(err);
}
});
n1.receive({ url: url });
});
});
}); });
describe('authentication', function() { describe('authentication', function() {

View File

@ -98,7 +98,7 @@ describe('BATCH node', function() {
var n2 = helper.getNode("n2"); var n2 = helper.getNode("n2");
check_data(n1, n2, results, done); check_data(n1, n2, results, done);
for(var i = 0; i < 6; i++) { for(var i = 0; i < 6; i++) {
n1.receive({payload: i}); n1.receive({payload: i, parts: { count:6, index:i }});
} }
}); });
} }
@ -168,6 +168,25 @@ describe('BATCH node', function() {
check_count(flow, results, done); 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) { 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"]]}, 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"}]; {id:"n2", type:"helper"}];
@ -455,7 +474,7 @@ describe('BATCH node', function() {
function mapiDoneTestHelper(done, mode, count, overlap, interval, allowEmptySequence, msgAndTimings) { 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 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 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:[[]]}, allowEmptySequence, topics: [{topic: "TA"}], wires:[[]]},
{id:"completeNode1",type:"complete",scope: ["batchNode1"],uncaught:false,wires:[["helperNode1"]]}, {id:"completeNode1",type:"complete",scope: ["batchNode1"],uncaught:false,wires:[["helperNode1"]]},
{id:"catchNode1", type:"catch",scope: ["batchNode1"],uncaught:false,wires:[["helperNode1"]]}, {id:"catchNode1", type:"catch",scope: ["batchNode1"],uncaught:false,wires:[["helperNode1"]]},
@ -482,13 +501,13 @@ describe('BATCH node', function() {
} }
it('should call done() when message is sent (mode: count)', function(done) { 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: 0}, delay: 0, avr: 0, var: 100},
{ msg: {payload: 1}, 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) { 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: 0}, delay: 0, avr: 200, var: 100},
{ msg: {payload: 1, reset:true}, delay: 200, avr: 200, var: 100} { msg: {payload: 1, reset:true}, delay: 200, avr: 200, var: 100}
]); ]);