Merge branch 'master' into dev

This commit is contained in:
Nick O'Leary
2025-03-17 16:07:46 +00:00
94 changed files with 2279 additions and 965 deletions

View File

@@ -58,7 +58,6 @@
"confirmDelete": "Confirm delete",
"delete": "Are you sure you want to delete '__label__'?",
"dropFlowHere": "Drop the flow here",
"dropImageHere": "Drop the image here",
"addFlow": "Add flow",
"addFlowToRight": "Add flow to the right",
"closeFlow": "Close flow",
@@ -375,7 +374,10 @@
"flowAdded": "flow added",
"moved": "moved",
"movedTo": "moved to __id__",
"movedFrom": "moved from __id__"
"movedFrom": "moved from __id__",
"none": "none",
"position": "position",
"wires": "wires"
},
"nodeCount": "__count__ node",
"nodeCount_plural": "__count__ nodes",
@@ -384,9 +386,14 @@
"reviewChanges": "Review Changes",
"noBinaryFileShowed": "Cannot show binary file contents",
"viewCommitDiff": "View Commit Changes",
"commit": "Commit",
"compareChanges": "Compare Changes",
"saveConflict": "Save conflict resolution",
"conflictHeader": "<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:"
@@ -554,7 +561,9 @@
"types": {
"local": "Local",
"examples": "Examples"
}
},
"type": "Type",
"name": "Name"
},
"palette": {
"noInfo": "no information available",
@@ -805,6 +814,7 @@
"branches": "Branches",
"noBranches": "No branches",
"deleteConfirm": "Are you sure you want to delete the local branch '__name__'? This cannot be undone.",
"deleteBranch": "Delete branch",
"unmergedConfirm": "The local branch '__name__' has unmerged changes that will be lost. Are you sure you want to delete it?",
"deleteUnmergedBranch": "Delete unmerged branch",
"gitRemotes": "Git remotes",

View File

@@ -27,7 +27,8 @@
"lock": "Bloquear",
"unlock": "Desbloquear",
"locked": "Bloqueado",
"unlocked": "Desbloqueado"
"unlocked": "Desbloqueado",
"format": "Formato"
},
"type": {
"string": "texto",
@@ -57,7 +58,6 @@
"confirmDelete": "Confirmar eliminación",
"delete": "¿Estás seguro de que quieres eliminar '__label__'?",
"dropFlowHere": "Suelta el flujo aquí",
"dropImageHere": "Suelta la imagen aquí",
"addFlow": "Añadir flujo",
"addFlowToRight": "Añadir flujo a la derecha",
"closeFlow": "Cerrar flujo",
@@ -303,7 +303,8 @@
"missingType": "La entrada no es un flujo válido - elemento __index__ falta la propiedad 'type'"
},
"conflictNotification1": "Algunos de los nodos que estás importando ya existen en tu espacio de trabajo.",
"conflictNotification2": "Selecciona qué nodos importar y si reemplazar los nodos existentes o importar una copia de los mismos."
"conflictNotification2": "Selecciona qué nodos importar y si reemplazar los nodos existentes o importar una copia de los mismos.",
"alreadyExists": "Este nodo ya existe"
},
"copyMessagePath": "Ruta copiada",
"copyMessageValue": "Valor copiado",
@@ -371,8 +372,12 @@
"deleted": "eliminado",
"flowDeleted": "flujo eliminado",
"flowAdded": "flujo añadido",
"moved": "movido",
"movedTo": "movido a __id__",
"movedFrom": "movido desde __id__"
"movedFrom": "movido desde __id__",
"none": "ninguno",
"position": "posición",
"wires": "conectores"
},
"nodeCount": "__count__ nodo",
"nodeCount_plural": "__count__ nodos",
@@ -381,9 +386,14 @@
"reviewChanges": "Revisar Cambios",
"noBinaryFileShowed": "No se puede mostrar el contenido del archivo binario",
"viewCommitDiff": "Ver cambios de commit",
"commit": "Commit",
"compareChanges": "Comparar Cambios",
"saveConflict": "Guardar resolución de conflictos",
"conflictHeader": "<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:"
@@ -551,7 +561,9 @@
"types": {
"local": "Local",
"examples": "Ejemplos"
}
},
"type": "Tipo",
"name": "Nombre"
},
"palette": {
"noInfo": "no hay información disponible",
@@ -613,6 +625,8 @@
},
"nodeCount": "__label__ nodo",
"nodeCount_plural": "__label__ nodos",
"pluginCount": "__count__ extensión",
"pluginCount_plural": "__count__ extensiones",
"moduleCount": "__count__ módulo disponible",
"moduleCount_plural": "__count__ módulos disponibles",
"inuse": "en uso",
@@ -640,6 +654,7 @@
"errors": {
"catalogLoadFailed": "<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>",
@@ -654,6 +669,9 @@
"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"
@@ -665,7 +683,8 @@
"review": "Abrir información del nodo",
"install": "Instalar",
"remove": "Eliminar",
"update": "Actualizar"
"update": "Actualizar",
"understood": "Entendido"
}
}
}
@@ -718,6 +737,7 @@
"nodeHelp": "Ayuda de nodo",
"showHelp": "Mostrar ayuda",
"showInOutline": "Mostrar en controno",
"hideTopics": "Esconder temas",
"showTopics": "Mostrar temas",
"noHelp": "No hay ningun tema de ayuda seleccionado",
"changeLog": "Registro de Cambios"
@@ -792,6 +812,7 @@
"branches": "Ramas",
"noBranches": "Sin ramas",
"deleteConfirm": "¿Estás seguro de que quieres eliminar la rama local '__name__'? Esta acción no puede deshacerse.",
"deleteBranch": "Eliminar rama",
"unmergedConfirm": "La rama local '__name__' tiene cambios no fusionados que se perderán. ¿Estás seguro de que quieres eliminarla?",
"deleteUnmergedBranch": "Eliminar rama no fusionada",
"gitRemotes": "Git remotes",
@@ -913,6 +934,8 @@
}
},
"typedInput": {
"selected": "__count__ seleccionado",
"selected_plural": "__count__ seleccionados",
"type": {
"str": "texto",
"num": "número",
@@ -923,7 +946,14 @@
"date": "marca tiempo",
"jsonata": "expresión",
"env": "variable de entorno",
"cred": "credencial"
"cred": "credencial",
"conf-types": "nodo configuración"
},
"date": {
"format": {
"timestamp": "milisegundos desde epoch",
"object": "Objeto de fecha de JavaScript"
}
}
},
"editableList": {
@@ -1205,6 +1235,18 @@
"diagnostics": {
"title": "Información Sistema"
},
"languages": {
"de": "Deutsch",
"en-US": "English",
"es-ES": "Español (España)",
"fr": "Français",
"ja": "日本語",
"ko": "Korean",
"pt-BR": "Português (Brasil)",
"ru": "Русский",
"zh-CN": "简体中文",
"zh-TW": "繁體中文"
},
"validator": {
"errors": {
"invalid-json": "Datos JSON inválidos: __error__",

View File

@@ -58,7 +58,6 @@
"confirmDelete": "Confirmer la suppression",
"delete": "Êtes-vous sûr de vouloir supprimer '__label__' ?",
"dropFlowHere": "Lâchez le flux ici",
"dropImageHere": "Lâchez l'image ici",
"addFlow": "Ajouter un flux",
"addFlowToRight": "Ajouter un flux à droite",
"closeFlow": "Fermer le flux",
@@ -375,7 +374,10 @@
"flowAdded": "flux ajouté",
"moved": "déplacé",
"movedTo": "déplacé vers __id__",
"movedFrom": "déplacé depuis __id__"
"movedFrom": "déplacé depuis __id__",
"none": "aucun",
"position": "position",
"wires": "câbles"
},
"nodeCount": "__count__ noeud",
"nodeCount_plural": "__count__ noeuds",
@@ -384,9 +386,14 @@
"reviewChanges": "Examiner les modifications",
"noBinaryFileShowed": "Impossible d'afficher le contenu du fichier binaire",
"viewCommitDiff": "Afficher les modifications de la validation",
"commit": "Validation",
"compareChanges": "Comparer les modifications",
"saveConflict": "Enregistrer la résolution des conflits",
"conflictHeader": "<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 :"
@@ -554,7 +561,9 @@
"types": {
"local": "Local",
"examples": "Exemples"
}
},
"type": "Type",
"name": "Nom"
},
"palette": {
"noInfo": "Pas d'information disponible",
@@ -803,6 +812,7 @@
"branches": "Branches",
"noBranches": "Pas de branche",
"deleteConfirm": "Êtes-vous sûr de vouloir supprimer la branche locale '__name__' ? Ça ne peut pas être annulé.",
"deleteBranch": "Supprimer la branche",
"unmergedConfirm": "La branche locale '__name__' contient des modifications non fusionnées qui seront perdues. Êtes-vous sûr de vouloir la supprimer?",
"deleteUnmergedBranch": "Supprimer la branche non fusionnée",
"gitRemotes": "Git distant",

View File

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

View File

@@ -453,10 +453,68 @@ RED.history = (function() {
RED.events.emit("nodes:change",newConfigNode);
}
});
} else if (i === "env" && ev.node.type.indexOf("subflow:") === 0) {
// Subflow can have config node in node.env
let nodeList = ev.node.env || [];
nodeList = nodeList.reduce((list, prop) => {
if (prop.type === "conf-type" && prop.value) {
list.push(prop.value);
}
return list;
}, []);
nodeList.forEach(function(id) {
const configNode = RED.nodes.node(id);
if (configNode) {
if (configNode.users.indexOf(ev.node) !== -1) {
configNode.users.splice(configNode.users.indexOf(ev.node), 1);
RED.events.emit("nodes:change", configNode);
}
}
});
nodeList = ev.changes.env || [];
nodeList = nodeList.reduce((list, prop) => {
if (prop.type === "conf-type" && prop.value) {
list.push(prop.value);
}
return list;
}, []);
nodeList.forEach(function(id) {
const configNode = RED.nodes.node(id);
if (configNode) {
if (configNode.users.indexOf(ev.node) === -1) {
configNode.users.push(ev.node);
RED.events.emit("nodes:change", configNode);
}
}
});
} else if (i === "color" && ev.node.type === "subflow") {
// Handle the Subflow definition color change
RED.utils.clearNodeColorCache();
const subflowDef = RED.nodes.getType("subflow:" + ev.node.id);
if (subflowDef) {
subflowDef.color = ev.changes[i] || "#DDAA99";
}
}
if (i === "credentials" && ev.changes[i]) {
// Reset - Only want to keep the changes
inverseEv.changes[i] = {};
for (const [key, value] of Object.entries(ev.changes[i])) {
// Edge case: node.credentials is cleared after a deploy, so we can't
// capture values for the inverse event when undoing past a deploy
if (ev.node.credentials) {
inverseEv.changes[i][key] = ev.node.credentials[key];
}
ev.node.credentials[key] = value;
}
} else {
ev.node[i] = ev.changes[i];
}
ev.node[i] = ev.changes[i];
}
}
ev.node.dirty = true;
ev.node.changed = ev.changed;
@@ -505,6 +563,10 @@ RED.history = (function() {
if (node) {
node.changed = n.changed;
node.dirty = true;
if (ev.changes && ev.changes.hasOwnProperty('color')) {
node._colorChanged = true;
}
}
});
}
@@ -536,6 +598,24 @@ RED.history = (function() {
RED.editor.updateNodeProperties(ev.node,outputMap);
RED.editor.validateNode(ev.node);
}
// If it's a Config Node, validate user nodes too.
// NOTE: The Config Node must be validated before validating users.
if (ev.node.users) {
const validatedNodes = new Set();
const userStack = ev.node.users.slice();
validatedNodes.add(ev.node.id);
while (userStack.length) {
const node = userStack.pop();
if (!validatedNodes.has(node.id)) {
validatedNodes.add(node.id);
if (node.users) {
userStack.push(...node.users);
}
RED.editor.validateNode(node);
}
}
}
if (ev.links) {
inverseEv.createdLinks = [];
for (i=0;i<ev.links.length;i++) {

View File

@@ -100,16 +100,36 @@ 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 () {
const location = getLocation()
if (location.workspace !== 0) {
log('send', 'multiplayer/location', location)
RED.comms.send('multiplayer/location', location)
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)
}
}
function revealUser(location, skipWorkspace) {
if (location.node) {
// Need to check if this is a known node, so we can fall back to revealing
@@ -271,7 +291,16 @@ RED.multiplayer = (function () {
function removeUserLocation (sessionId) {
updateUserLocation(sessionId, {})
removeUserCursor(sessionId)
}
function removeUserCursor (sessionId) {
// return
if (sessions[sessionId]?.cursor) {
sessions[sessionId].cursor.parentNode.removeChild(sessions[sessionId].cursor)
delete sessions[sessionId].cursor
}
}
function updateUserLocation (sessionId, location) {
let viewTouched = false
const oldLocation = sessions[sessionId].location
@@ -291,6 +320,28 @@ RED.multiplayer = (function () {
// console.log(`updateUserLocation sessionId:${sessionId} oldWS:${oldLocation?.workspace} newWS:${location.workspace}`)
if (location.workspace) {
getWorkspaceTray(location.workspace).addUser(sessionId)
if (location.cursor && location.workspace === RED.workspaces.active()) {
if (!sessions[sessionId].cursor) {
const user = sessions[sessionId].user
const cursorIcon = document.createElementNS("http://www.w3.org/2000/svg","g");
cursorIcon.setAttribute("class", "red-ui-multiplayer-annotation")
cursorIcon.appendChild(createAnnotationUser(user, true))
$(cursorIcon).css({
transform: `translate( ${location.cursor.x}px, ${location.cursor.y}px)`,
transition: 'transform 0.1s linear'
})
$("#red-ui-workspace-chart svg").append(cursorIcon)
sessions[sessionId].cursor = cursorIcon
} else {
const cursorIcon = sessions[sessionId].cursor
$(cursorIcon).css({
transform: `translate( ${location.cursor.x}px, ${location.cursor.y}px)`
})
}
} else if (sessions[sessionId].cursor) {
removeUserCursor(sessionId)
}
}
if (location.node) {
addUserToNode(sessionId, location.node)
@@ -309,67 +360,68 @@ RED.multiplayer = (function () {
// }
// }
function createAnnotationUser(user, pointer = false) {
const radius = 20
const halfRadius = radius/2
const group = document.createElementNS("http://www.w3.org/2000/svg","g");
const badge = document.createElementNS("http://www.w3.org/2000/svg","path");
let shapePath
if (!pointer) {
shapePath = `M 0 ${halfRadius} a ${halfRadius} ${halfRadius} 0 1 1 ${radius} 0 a ${halfRadius} ${halfRadius} 0 1 1 -${radius} 0 z`
} else {
shapePath = `M 0 0 h ${halfRadius} a ${halfRadius} ${halfRadius} 0 1 1 -${halfRadius} ${halfRadius} z`
}
badge.setAttribute('d', shapePath)
badge.setAttribute("class", "red-ui-multiplayer-annotation-background")
group.appendChild(badge)
if (user && user.profileColor !== undefined) {
badge.setAttribute("class", "red-ui-multiplayer-annotation-background red-ui-user-profile-color-" + user.profileColor)
}
if (user && user.image) {
const image = document.createElementNS("http://www.w3.org/2000/svg","image");
image.setAttribute("width", radius)
image.setAttribute("height", radius)
image.setAttribute("href", user.image)
image.setAttribute("clip-path", "circle("+Math.floor(radius/2)+")")
group.appendChild(image)
} else if (user && user.anonymous) {
const anonIconHead = document.createElementNS("http://www.w3.org/2000/svg","circle");
anonIconHead.setAttribute("cx", radius/2)
anonIconHead.setAttribute("cy", radius/2 - 2)
anonIconHead.setAttribute("r", 2.4)
anonIconHead.setAttribute("class","red-ui-multiplayer-annotation-anon-label");
group.appendChild(anonIconHead)
const anonIconBody = document.createElementNS("http://www.w3.org/2000/svg","path");
anonIconBody.setAttribute("class","red-ui-multiplayer-annotation-anon-label");
// anonIconBody.setAttribute("d",`M ${radius/2 - 4} ${radius/2 + 1} h 8 v4 h -8 z`);
anonIconBody.setAttribute("d",`M ${radius/2} ${radius/2 + 5} h -2.5 c -2 1 -2 -5 0.5 -4.5 c 2 1 2 1 4 0 c 2.5 -0.5 2.5 5.5 0 4.5 z`);
group.appendChild(anonIconBody)
} else {
const label = document.createElementNS("http://www.w3.org/2000/svg","text");
if (user.username || user.email) {
label.setAttribute("class","red-ui-multiplayer-annotation-label");
label.textContent = (user.username || user.email).substring(0,2)
} else {
label.setAttribute("class","red-ui-multiplayer-annotation-label red-ui-multiplayer-user-count")
label.textContent = 'nr'
}
label.setAttribute("text-anchor", "middle")
label.setAttribute("x",radius/2);
label.setAttribute("y",radius/2 + 3);
group.appendChild(label)
}
const border = document.createElementNS("http://www.w3.org/2000/svg","path");
border.setAttribute('d', shapePath)
border.setAttribute("class", "red-ui-multiplayer-annotation-border")
group.appendChild(border)
return group
}
return {
init: function () {
function createAnnotationUser(user) {
const group = document.createElementNS("http://www.w3.org/2000/svg","g");
const badge = document.createElementNS("http://www.w3.org/2000/svg","circle");
const radius = 20
badge.setAttribute("cx",radius/2);
badge.setAttribute("cy",radius/2);
badge.setAttribute("r",radius/2);
badge.setAttribute("class", "red-ui-multiplayer-annotation-background")
group.appendChild(badge)
if (user && user.profileColor !== undefined) {
badge.setAttribute("class", "red-ui-multiplayer-annotation-background red-ui-user-profile-color-" + user.profileColor)
}
if (user && user.image) {
const image = document.createElementNS("http://www.w3.org/2000/svg","image");
image.setAttribute("width", radius)
image.setAttribute("height", radius)
image.setAttribute("href", user.image)
image.setAttribute("clip-path", "circle("+Math.floor(radius/2)+")")
group.appendChild(image)
} else if (user && user.anonymous) {
const anonIconHead = document.createElementNS("http://www.w3.org/2000/svg","circle");
anonIconHead.setAttribute("cx", radius/2)
anonIconHead.setAttribute("cy", radius/2 - 2)
anonIconHead.setAttribute("r", 2.4)
anonIconHead.setAttribute("class","red-ui-multiplayer-annotation-anon-label");
group.appendChild(anonIconHead)
const anonIconBody = document.createElementNS("http://www.w3.org/2000/svg","path");
anonIconBody.setAttribute("class","red-ui-multiplayer-annotation-anon-label");
// anonIconBody.setAttribute("d",`M ${radius/2 - 4} ${radius/2 + 1} h 8 v4 h -8 z`);
anonIconBody.setAttribute("d",`M ${radius/2} ${radius/2 + 5} h -2.5 c -2 1 -2 -5 0.5 -4.5 c 2 1 2 1 4 0 c 2.5 -0.5 2.5 5.5 0 4.5 z`);
group.appendChild(anonIconBody)
} else {
const labelText = user.username ? user.username.substring(0,2) : user
const label = document.createElementNS("http://www.w3.org/2000/svg","text");
if (user.username) {
label.setAttribute("class","red-ui-multiplayer-annotation-label");
label.textContent = user.username.substring(0,2)
} else {
label.setAttribute("class","red-ui-multiplayer-annotation-label red-ui-multiplayer-user-count")
label.textContent = user
}
label.setAttribute("text-anchor", "middle")
label.setAttribute("x",radius/2);
label.setAttribute("y",radius/2 + 3);
group.appendChild(label)
}
const border = document.createElementNS("http://www.w3.org/2000/svg","circle");
border.setAttribute("cx",radius/2);
border.setAttribute("cy",radius/2);
border.setAttribute("r",radius/2);
border.setAttribute("class", "red-ui-multiplayer-annotation-border")
group.appendChild(border)
return group
}
RED.view.annotations.register("red-ui-multiplayer",{
type: 'badge',
@@ -479,6 +531,24 @@ RED.multiplayer = (function () {
RED.comms.send('multiplayer/disconnect', disconnectInfo)
RED.settings.removeLocal('multiplayer:sessionId')
})
const chart = $('#red-ui-workspace-chart')
chart.on('mousemove', function (evt) {
lastPosition[0] = evt.clientX
lastPosition[1] = evt.clientY
publishLocation()
})
chart.on('scroll', function (evt) {
publishLocation()
})
chart.on('mouseenter', function () {
isInWorkspace = true
publishLocation()
})
chart.on('mouseleave', function () {
isInWorkspace = false
publishLocation()
})
}
}

View File

@@ -73,7 +73,13 @@ RED.nodes = (function() {
var exports = {
setModulePendingUpdated: function(module,version) {
moduleList[module].pending_version = version;
if (!!RED.plugins.getModule(module)) {
// The module updated is a plugin
RED.plugins.getModule(module).pending_version = version;
} else {
moduleList[module].pending_version = version;
}
RED.events.emit("registry:module-updated",{module:module,version:version});
},
getModule: function(module) {
@@ -701,12 +707,15 @@ RED.nodes = (function() {
}
n["_"] = RED._;
}
// Both node and config node can use a config node
updateConfigNodeUsers(newNode, { action: "add" });
if (n._def.category == "config") {
configNodes[n.id] = n;
configNodes[n.id] = newNode;
} else {
if (n.wires && (n.wires.length > n.outputs)) { n.outputs = n.wires.length; }
n.dirty = true;
updateConfigNodeUsers(n);
if (n._def.category == "subflows" && typeof n.i === "undefined") {
var nextId = 0;
RED.nodes.eachNode(function(node) {
@@ -768,9 +777,11 @@ RED.nodes = (function() {
var removedLinks = [];
var removedNodes = [];
var node;
if (id in configNodes) {
node = configNodes[id];
delete configNodes[id];
updateConfigNodeUsers(node, { action: "remove" });
RED.events.emit('nodes:remove',node);
RED.workspaces.refresh();
} else if (allNodes.hasNode(id)) {
@@ -779,6 +790,9 @@ RED.nodes = (function() {
delete nodeLinks[id];
removedLinks = links.filter(function(l) { return (l.source === node) || (l.target === node); });
removedLinks.forEach(removeLink);
updateConfigNodeUsers(node, { action: "remove" });
// TODO: Legacy code for exclusive config node
var updatedConfigNode = false;
for (var d in node._def.defaults) {
if (node._def.defaults.hasOwnProperty(d)) {
@@ -792,10 +806,6 @@ RED.nodes = (function() {
if (configNode._def.exclusive) {
removeNode(node[d]);
removedNodes.push(configNode);
} else {
var users = configNode.users;
users.splice(users.indexOf(node),1);
RED.events.emit('nodes:change',configNode)
}
}
}
@@ -1032,23 +1042,34 @@ RED.nodes = (function() {
return {nodes:removedNodes,links:removedLinks, groups: removedGroups, junctions: removedJunctions};
}
/**
* Add a Subflow to the Workspace
*
* @param {object} sf The Subflow to add.
* @param {boolean|undefined} createNewIds Whether to update the name.
*/
function addSubflow(sf, createNewIds) {
if (createNewIds) {
var subflowNames = Object.keys(subflows).map(function(sfid) {
return subflows[sfid].name;
});
// Update the Subflow name to highlight that this is a copy
const subflowNames = Object.keys(subflows).map(function (sfid) {
return subflows[sfid].name || "";
})
subflowNames.sort()
subflowNames.sort();
var copyNumber = 1;
var subflowName = sf.name;
let copyNumber = 1;
let subflowName = sf.name;
subflowNames.forEach(function(name) {
if (subflowName == name) {
subflowName = sf.name + " (" + copyNumber + ")";
copyNumber++;
subflowName = sf.name+" ("+copyNumber+")";
}
});
sf.name = subflowName;
}
sf.instances = [];
subflows[sf.id] = sf;
allNodes.addTab(sf.id);
linkTabMap[sf.id] = [];
@@ -1101,7 +1122,7 @@ RED.nodes = (function() {
module: "node-red"
}
});
sf.instances = [];
sf._def = RED.nodes.getType("subflow:"+sf.id);
RED.events.emit("subflows:add",sf);
}
@@ -1743,7 +1764,8 @@ RED.nodes = (function() {
// Remove the old subflow definition - but leave the instances in place
var removalResult = RED.subflow.removeSubflow(n.id, true);
// Create the list of nodes for the new subflow def
var subflowNodes = [n].concat(zMap[n.id]);
// Need to sort the list in order to remove missing nodes
var subflowNodes = [n].concat(zMap[n.id]).filter((s) => !!s);
// Import the new subflow - no clashes should occur as we've removed
// the old version
var result = importNodes(subflowNodes);
@@ -1780,9 +1802,20 @@ RED.nodes = (function() {
// Replace config nodes
//
configNodeIds.forEach(function(id) {
removedNodes = removedNodes.concat(convertNode(getNode(id)));
const configNode = getNode(id);
const currentUserCount = configNode.users;
// Add a snapshot of the Config Node
removedNodes = removedNodes.concat(convertNode(configNode));
// Remove the Config Node instance
removeNode(id);
importNodes([newConfigNodes[id]])
// Import the new one
importNodes([newConfigNodes[id]]);
// Re-attributes the user count
getNode(id).users = currentUserCount;
});
return {
@@ -2023,6 +2056,8 @@ RED.nodes = (function() {
if (matchingSubflow) {
subflow_denylist[n.id] = matchingSubflow;
} else {
const oldId = n.id;
subflow_map[n.id] = n;
if (createNewIds || options.importMap[n.id] === "copy") {
nid = getID();
@@ -2050,7 +2085,7 @@ RED.nodes = (function() {
n.status.id = getID();
}
new_subflows.push(n);
addSubflow(n,createNewIds || options.importMap[n.id] === "copy");
addSubflow(n,createNewIds || options.importMap[oldId] === "copy");
}
}
}
@@ -2064,6 +2099,8 @@ RED.nodes = (function() {
activeWorkspace = RED.workspaces.active();
}
const pendingConfigNodes = []
const pendingConfigNodeIds = new Set()
// Find all config nodes and add them
for (i=0;i<newNodes.length;i++) {
n = newNodes[i];
@@ -2123,7 +2160,8 @@ RED.nodes = (function() {
type:n.type,
info: n.info,
users:[],
_config:{}
_config:{},
_configNodeReferences: new Set()
};
if (!n.z) {
delete configNode.z;
@@ -2138,6 +2176,9 @@ 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')) {
@@ -2154,11 +2195,55 @@ RED.nodes = (function() {
configNode.id = getID();
}
node_map[n.id] = configNode;
new_nodes.push(configNode);
pendingConfigNodes.push(configNode);
pendingConfigNodeIds.add(configNode.id)
}
}
}
// 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];
@@ -2170,7 +2255,7 @@ RED.nodes = (function() {
x:parseFloat(n.x || 0),
y:parseFloat(n.y || 0),
z:n.z,
type:0,
type: n.type,
info: n.info,
changed:false,
_config:{}
@@ -2231,7 +2316,6 @@ RED.nodes = (function() {
}
}
}
node.type = n.type;
node._def = def;
if (node.type === "group") {
node._def = RED.group.def;
@@ -2261,6 +2345,15 @@ 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;
@@ -2321,29 +2414,31 @@ RED.nodes = (function() {
node.type = "unknown";
}
if (node._def.category != "config") {
if (n.hasOwnProperty('inputs')) {
node.inputs = n.inputs;
if (n.hasOwnProperty('inputs') && node._def.defaults.hasOwnProperty("inputs")) {
node.inputs = parseInt(n.inputs, 10);
node._config.inputs = JSON.stringify(n.inputs);
} else {
node.inputs = node._def.inputs;
}
if (n.hasOwnProperty('outputs')) {
node.outputs = n.outputs;
if (n.hasOwnProperty('outputs') && node._def.defaults.hasOwnProperty("outputs")) {
node.outputs = parseInt(n.outputs, 10);
node._config.outputs = JSON.stringify(n.outputs);
} else {
node.outputs = node._def.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);
} else {
// The node declares outputs in its defaults, but has not got a valid value
// Defer to the length of the wires array
// The node declares outputs in its defaults, but has not got a valid value
// Defer to the length of the wires array
if (node.hasOwnProperty('wires')) {
if (isNaN(node.outputs)) {
node.outputs = node.wires.length;
} else if (node.wires.length > node.outputs) {
// If 'wires' is longer than outputs, clip wires
console.log("Warning: node.wires longer than node.outputs - trimming wires:", node.id, " wires:", node.wires.length, " outputs:", node.outputs);
node.wires = node.wires.slice(0, node.outputs);
}
}
for (d in node._def.defaults) {
if (node._def.defaults.hasOwnProperty(d) && d !== 'inputs' && d !== 'outputs') {
node[d] = n[d];
@@ -2406,11 +2501,28 @@ RED.nodes = (function() {
} else {
delete n.g
}
// If importing into a subflow, ensure an outbound-link doesn't get added
if (activeSubflow && /^link /.test(n.type) && n.links) {
// If importing a link node, ensure both ends of each link are either:
// - not in a subflow
// - both in the same subflow (not for link call node)
if (/^link /.test(n.type) && n.links) {
n.links = n.links.filter(function(id) {
const otherNode = node_map[id] || RED.nodes.node(id);
return (otherNode && otherNode.z === activeWorkspace);
if (!otherNode) {
// Cannot find other end - remove the link
return false
}
if (otherNode.z === n.z) {
// Both ends in the same flow/subflow
return true
} else if (n.type === "link call" && !getSubflow(otherNode.z)) {
// Link call node can call out of a subflow as long as otherNode is
// not in a subflow
return true
} else if (!!getSubflow(n.z) || !!getSubflow(otherNode.z)) {
// One end is in a subflow - remove the link
return false
}
return true
});
}
for (var d3 in n._def.defaults) {
@@ -2423,11 +2535,6 @@ RED.nodes = (function() {
nodeList = nodeList.map(function(id) {
var node = node_map[id];
if (node) {
if (node._def.category === 'config') {
if (node.users.indexOf(n) === -1) {
node.users.push(n);
}
}
return node.id;
}
return id;
@@ -2441,9 +2548,11 @@ RED.nodes = (function() {
n = new_subflows[i];
n.in.forEach(function(input) {
input.wires.forEach(function(wire) {
var link = {source:input, sourcePort:0, target:node_map[wire.id]};
addLink(link);
new_links.push(link);
if (node_map.hasOwnProperty(wire.id)) {
var link = {source:input, sourcePort:0, target:node_map[wire.id]};
addLink(link);
new_links.push(link);
}
});
delete input.wires;
});
@@ -2452,11 +2561,13 @@ RED.nodes = (function() {
var link;
if (subflow_map[wire.id] && subflow_map[wire.id].id == n.id) {
link = {source:n.in[wire.port], sourcePort:wire.port,target:output};
} else {
} else if (node_map.hasOwnProperty(wire.id) || subflow_map.hasOwnProperty(wire.id)) {
link = {source:node_map[wire.id]||subflow_map[wire.id], sourcePort:wire.port,target:output};
}
addLink(link);
new_links.push(link);
if (link) {
addLink(link);
new_links.push(link);
}
});
delete output.wires;
});
@@ -2465,11 +2576,13 @@ RED.nodes = (function() {
var link;
if (subflow_map[wire.id] && subflow_map[wire.id].id == n.id) {
link = {source:n.in[wire.port], sourcePort:wire.port,target:n.status};
} else {
} else if (node_map.hasOwnProperty(wire.id) || subflow_map.hasOwnProperty(wire.id)) {
link = {source:node_map[wire.id]||subflow_map[wire.id], sourcePort:wire.port,target:n.status};
}
addLink(link);
new_links.push(link);
if (link) {
addLink(link);
new_links.push(link);
}
});
delete n.status.wires;
}
@@ -2648,25 +2761,79 @@ RED.nodes = (function() {
return result;
}
// Update any config nodes referenced by the provided node to ensure their 'users' list is correct
function updateConfigNodeUsers(n) {
for (var d in n._def.defaults) {
if (n._def.defaults.hasOwnProperty(d)) {
var property = n._def.defaults[d];
/**
* Update any config nodes referenced by the provided node to ensure
* their 'users' list is correct.
*
* @param {object} node The node in which to check if it contains references
* @param {object} options Options to apply.
* @param {"add" | "remove"} [options.action] Add or remove the node from
* the Config Node users list. Default `add`.
* @param {boolean} [options.emitEvent] Emit the `nodes:changes` event.
* Default true.
*/
function updateConfigNodeUsers(node, options) {
const defaultOptions = { action: "add", emitEvent: true };
options = Object.assign({}, defaultOptions, options);
for (var d in node._def.defaults) {
if (node._def.defaults.hasOwnProperty(d)) {
var property = node._def.defaults[d];
if (property.type) {
var type = registry.getNodeType(property.type);
// Need to ensure the type is a config node to not treat links nodes
if (type && type.category == "config") {
var configNode = configNodes[n[d]];
var configNode = configNodes[node[d]];
if (configNode) {
if (configNode.users.indexOf(n) === -1) {
configNode.users.push(n);
RED.events.emit('nodes:change',configNode)
if (options.action === "add") {
if (configNode.users.indexOf(node) === -1) {
configNode.users.push(node);
if (options.emitEvent) {
RED.events.emit('nodes:change', configNode);
}
}
} else if (options.action === "remove") {
if (configNode.users.indexOf(node) !== -1) {
const users = configNode.users;
users.splice(users.indexOf(node), 1);
if (options.emitEvent) {
RED.events.emit('nodes:change', configNode);
}
}
}
}
}
}
}
}
// Subflows can have config node env
if (node.type.indexOf("subflow:") === 0) {
node.env?.forEach((prop) => {
if (prop.type === "conf-type" && prop.value) {
// Add the node to the config node users
const configNode = getNode(prop.value);
if (configNode) {
if (options.action === "add") {
if (configNode.users.indexOf(node) === -1) {
configNode.users.push(node);
if (options.emitEvent) {
RED.events.emit('nodes:change', configNode);
}
}
} else if (options.action === "remove") {
if (configNode.users.indexOf(node) !== -1) {
const users = configNode.users;
users.splice(users.indexOf(node), 1);
if (options.emitEvent) {
RED.events.emit('nodes:change', configNode);
}
}
}
}
}
});
}
}
function flowVersion(version) {

View File

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

View File

@@ -324,6 +324,30 @@ RED.clipboard = (function() {
},100);
}
/**
* Validates if the provided string looks like valid flow json
* @param {string} flowString the string to validate
* @returns If valid, returns the node array
*/
function validateFlowString(flowString) {
const res = JSON.parse(flowString)
if (!Array.isArray(res)) {
throw new Error(RED._("clipboard.import.errors.notArray"));
}
for (let i = 0; i < res.length; i++) {
if (typeof res[i] !== "object") {
throw new Error(RED._("clipboard.import.errors.itemNotObject",{index:i}));
}
if (!Object.hasOwn(res[i], 'id')) {
throw new Error(RED._("clipboard.import.errors.missingId",{index:i}));
}
if (!Object.hasOwn(res[i], 'type')) {
throw new Error(RED._("clipboard.import.errors.missingType",{index:i}));
}
}
return res
}
var validateImportTimeout;
function validateImport() {
if (activeTab === "red-ui-clipboard-dialog-import-tab-clipboard") {
@@ -341,21 +365,7 @@ RED.clipboard = (function() {
return;
}
try {
if (!/^\[[\s\S]*\]$/m.test(v)) {
throw new Error(RED._("clipboard.import.errors.notArray"));
}
var res = JSON.parse(v);
for (var i=0;i<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}));
}
}
validateFlowString(v)
currentPopoverError = null;
popover.close(true);
importInput.removeClass("input-error");
@@ -988,16 +998,16 @@ RED.clipboard = (function() {
}
function importNodes(nodesStr,addFlow) {
var newNodes = nodesStr;
let newNodes = nodesStr;
if (typeof nodesStr === 'string') {
try {
nodesStr = nodesStr.trim();
if (nodesStr.length === 0) {
return;
}
newNodes = JSON.parse(nodesStr);
newNodes = validateFlowString(nodesStr)
} catch(err) {
var e = new Error(RED._("clipboard.invalidFlow",{message:err.message}));
const e = new Error(RED._("clipboard.invalidFlow",{message:err.message}));
e.code = "NODE_RED";
throw e;
}
@@ -1332,6 +1342,7 @@ 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: 300,
width: Math.max(300, this.element.width()),
maxHeight: 200,
class: "red-ui-autoComplete-container",
options: completions,

View File

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

View File

@@ -54,21 +54,22 @@ RED.contextMenu = (function () {
}
}
const scale = RED.view.scale()
const offset = $("#red-ui-workspace-chart").offset()
let addX = options.x - offset.left + $("#red-ui-workspace-chart").scrollLeft()
let addY = options.y - offset.top + $("#red-ui-workspace-chart").scrollTop()
let addX = (options.x - offset.left + $("#red-ui-workspace-chart").scrollLeft()) / scale
let addY = (options.y - offset.top + $("#red-ui-workspace-chart").scrollTop()) / scale
if (RED.view.snapGrid) {
const gridSize = RED.view.gridSize()
addX = gridSize * Math.floor(addX / gridSize)
addY = gridSize * Math.floor(addY / gridSize)
addX = gridSize * Math.round(addX / gridSize)
addY = gridSize * Math.round(addY / gridSize)
}
menuItems.push(
{ onselect: 'core:show-action-list', label: RED._("contextMenu.showActionList"), onpostselect: function () { } }
)
if (RED.settings.theme("menu.menu-item-action-list", true)) {
menuItems.push(
{ onselect: 'core:show-action-list', label: RED._("contextMenu.showActionList"), onpostselect: function () { } }
)
}
const insertOptions = []
menuItems.push({ label: RED._("contextMenu.insert"), options: insertOptions })
insertOptions.push(
@@ -86,7 +87,9 @@ RED.contextMenu = (function () {
},
(hasLinks) ? { // has least 1 wire selected
label: RED._("contextMenu.junction"),
onselect: 'core:split-wires-with-junctions',
onselect: function () {
RED.actions.invoke('core:split-wires-with-junctions', { x: addX, y: addY })
},
disabled: !canEdit || !hasLinks
} : {
label: RED._("contextMenu.junction"),

View File

@@ -44,6 +44,7 @@ RED.deploy = (function() {
/**
* options:
* type: "default" - Button with drop-down options - no further customisation available
* label: the text to display - default: "Deploy"
* type: "simple" - Button without dropdown. Customisations:
* label: the text to display - default: "Deploy"
* icon : the icon to use. Null removes the icon. default: "red/images/deploy-full-o.svg"
@@ -51,13 +52,14 @@ RED.deploy = (function() {
function init(options) {
options = options || {};
var type = options.type || "default";
var label = options.label || RED._("deploy.deploy");
if (type == "default") {
$('<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>'+RED._("deploy.deploy")+'</span>'+
'<span>'+label+'</span>'+
'</span>'+
'<span class="red-ui-deploy-button-spinner hide">'+
'<img src="red/images/spin.svg"/>'+
@@ -78,7 +80,6 @@ RED.deploy = (function() {
mainMenuItems.push({id:"deploymenu-item-reload", icon:"red/images/deploy-reload.svg",label:RED._("deploy.restartFlows"),sublabel:RED._("deploy.restartFlowsDesc"),onselect:"core:restart-flows"})
RED.menu.init({id:"red-ui-header-button-deploy-options", options: mainMenuItems });
} else if (type == "simple") {
var label = options.label || RED._("deploy.deploy");
var icon = 'red/images/deploy-full-o.svg';
if (options.hasOwnProperty('icon')) {
icon = options.icon;
@@ -424,11 +425,15 @@ RED.deploy = (function() {
const unknownNodes = [];
const invalidNodes = [];
const isDisabled = function (node) {
return (node.d || RED.nodes.workspace(node.z)?.disabled);
};
RED.nodes.eachConfig(function (node) {
if (node.valid === undefined) {
RED.editor.validateNode(node);
}
if (!node.valid && !node.d) {
if (!node.valid && !isDisabled(node)) {
invalidNodes.push(getNodeInfo(node));
}
if (node.type === "unknown") {
@@ -438,7 +443,7 @@ RED.deploy = (function() {
}
});
RED.nodes.eachNode(function (node) {
if (!node.valid && !node.d) {
if (!node.valid && !isDisabled(node)) {
invalidNodes.push(getNodeInfo(node));
}
if (node.type === "unknown") {
@@ -452,7 +457,7 @@ RED.deploy = (function() {
const unusedConfigNodes = [];
RED.nodes.eachConfig(function (node) {
if ((node._def.hasUsers !== false) && (node.users.length === 0)) {
if ((node._def.hasUsers !== false) && (node.users.length === 0) && !isDisabled(node)) {
unusedConfigNodes.push(getNodeInfo(node));
hasUnusedConfig = true;
}
@@ -589,7 +594,9 @@ RED.deploy = (function() {
RED.notify('<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) {
@@ -642,6 +649,7 @@ RED.deploy = (function() {
delete confNode.credentials;
}
});
// Subflow cannot be locked
RED.nodes.eachSubflow(function (subflow) {
if (subflow.changed) {
subflow.changed = false;
@@ -650,12 +658,18 @@ RED.deploy = (function() {
});
RED.nodes.eachWorkspace(function (ws) {
if (ws.changed || ws.added) {
ensureUnlocked(ws.z)
// Ensure the Workspace is unlocked to modify its properties.
ensureUnlocked(ws.id);
ws.changed = false;
delete ws.added
if (flowsToLock.has(ws)) {
ws.locked = true;
flowsToLock.delete(ws);
}
RED.events.emit("flows:change", ws)
}
});
// Ensures all workspaces to be locked have been locked.
flowsToLock.forEach(flow => {
flow.locked = true
})

View File

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

View File

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

View File

@@ -46,8 +46,8 @@
initialised = selectedCodeEditor.init();
}
$('<div id="red-ui-image-drop-target"><div data-i18n="[append]workspace.dropImageHere"><i class="fa fa-download"></i><br></div></div>').appendTo('#red-ui-editor');
$("#red-ui-image-drop-target").hide();
$('<div id="red-ui-drop-target-markdown-editor"><div><i class="fa fa-download"></i><br></div></div>').appendTo('#red-ui-editor');
$("#red-ui-drop-target-markdown-editor").hide();
}
function create(options) {

View File

@@ -691,6 +691,7 @@ RED.editor.codeEditor.monaco = (function() {
2322, //Type 'unknown' is not assignable to type 'string'
2339, //property does not exist on
2345, //Argument of type xxx is not assignable to parameter of type 'DateTimeFormatOptions'
2538, //Ignore symbols as index property error.
7043, //i forget what this one is,
80001, //Convert to ES6 module
80004, //JSDoc types may be moved to TypeScript types.

View File

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

View File

@@ -27,6 +27,12 @@
reader.readAsDataURL(file);
}
function file2Text(file,cb) {
file.arrayBuffer().then(d => {
cb( new TextDecoder().decode(d) )
}).catch(ex => { cb(`error: ${ex}`) })
}
var initialized = false;
var currentEditor = null;
/**
@@ -35,16 +41,22 @@
function initImageDrag(elem, editor) {
$(elem).on("dragenter", function (ev) {
ev.preventDefault();
$("#red-ui-image-drop-target").css({display:'table'}).focus();
$("#red-ui-drop-target-markdown-editor").css({
display:'table',
top: $(elem).offset().top,
left: $(elem).offset().left,
width: $(elem).width(),
height: $(elem).height()
}).focus();
currentEditor = editor;
});
if (!initialized) {
initialized = true;
$("#red-ui-image-drop-target").on("dragover", function (ev) {
$("#red-ui-drop-target-markdown-editor").on("dragover", function (ev) {
ev.preventDefault();
}).on("dragleave", function (ev) {
$("#red-ui-image-drop-target").hide();
$("#red-ui-drop-target-markdown-editor").hide();
}).on("drop", function (ev) {
ev.preventDefault();
if ($.inArray("Files",ev.originalEvent.dataTransfer.types) != -1) {
@@ -52,20 +64,43 @@
if (files.length === 1) {
var file = files[0];
var name = file.name.toLowerCase();
var fileType = file.type.toLowerCase();
if (name.match(/\.(apng|avif|gif|jpeg|png|svg|webp)$/)) {
file2base64Image(file, function (image) {
var session = currentEditor.getSession();
var img = `<img src="${image}"/>\n`;
var pos = session.getCursorPosition();
session.insert(pos, img);
$("#red-ui-image-drop-target").hide();
$("#red-ui-drop-target-markdown-editor").hide();
});
return;
}
if ( fileType.startsWith("text/") ) {
file2Text(file, function (txt) {
var session = currentEditor.getSession();
var pos = session.getCursorPosition();
session.insert(pos, txt);
$("#red-ui-drop-target-markdown-editor").hide();
});
return;
}
}
} else if ($.inArray("text/plain", ev.originalEvent.dataTransfer.types) != -1) {
let item = Object.values(ev.originalEvent.dataTransfer.items).filter(d => d.type == "text/plain")[0]
if (item) {
item.getAsString(txt => {
var session = currentEditor.getSession();
var pos = session.getCursorPosition();
session.insert(pos, txt);
$("#red-ui-drop-target-markdown-editor").hide();
})
return
}
}
$("#red-ui-image-drop-target").hide();
$("#red-ui-drop-target-markdown-editor").hide();
});
}
}

View File

@@ -20,10 +20,31 @@
apply: function(editState) {
var old_env = node.env;
var new_env = [];
if (/^subflow:/.test(node.type)) {
// Get the list of environment variables from the node properties
new_env = RED.subflow.exportSubflowInstanceEnv(node);
}
if (old_env && old_env.length) {
old_env.forEach(function (prop) {
if (prop.type === "conf-type" && prop.value) {
const stillInUse = new_env?.some((p) => p.type === "conf-type" && p.name === prop.name && p.value === prop.value);
if (!stillInUse) {
// Remove the node from the config node users
// Only for empty value or modified
const configNode = RED.nodes.node(prop.value);
if (configNode) {
if (configNode.users.indexOf(node) !== -1) {
configNode.users.splice(configNode.users.indexOf(node), 1);
RED.events.emit('nodes:change', configNode)
}
}
}
}
});
}
// Get the values from the Properties table tab
var items = this.list.editableList('items');
items.each(function (i,el) {
@@ -41,7 +62,6 @@
}
});
if (new_env && new_env.length > 0) {
new_env.forEach(function(prop) {
if (prop.type === "cred") {
@@ -52,6 +72,15 @@
editState.changed = true;
}
delete prop.value;
} else if (prop.type === "conf-type" && prop.value) {
const configNode = RED.nodes.node(prop.value);
if (configNode) {
if (configNode.users.indexOf(node) === -1) {
// Add the node to the config node users
configNode.users.push(node);
RED.events.emit('nodes:change', configNode);
}
}
}
});
}

View File

@@ -26,6 +26,8 @@
if (node._def.category === "config" && nodeType !== "group") {
this.inputClass = "node-config-input";
formStyle = "node-config-dialog-edit-form";
} else if (node.type === "subflow") {
this.inputClass = "subflow-input";
}
RED.editor.buildEditForm(container,formStyle,nodeType,i18nNamespace,node);
},
@@ -44,6 +46,7 @@
apply: function(editState) {
var newValue;
var d;
// If the node is a subflow, the node's properties (exepts name) are saved by `envProperties`
if (node._def.defaults) {
for (d in node._def.defaults) {
if (node._def.defaults.hasOwnProperty(d)) {
@@ -131,9 +134,16 @@
}
}
if (node._def.credentials) {
var credDefinition = node._def.credentials;
var credsChanged = updateNodeCredentials(node,credDefinition,this.inputClass);
editState.changed = editState.changed || credsChanged;
const credDefinition = node._def.credentials;
const credChanges = updateNodeCredentials(node, credDefinition, this.inputClass);
if (Object.keys(credChanges).length) {
editState.changed = true;
editState.changes.credentials = {
...(editState.changes.credentials || {}),
...credChanges
};
}
}
}
}
@@ -161,10 +171,11 @@
* @param node - the node containing the credentials
* @param credDefinition - definition of the credentials
* @param prefix - prefix of the input fields
* @return {boolean} whether anything has changed
* @return {object} an object containing the modified properties
*/
function updateNodeCredentials(node, credDefinition, prefix) {
var changed = false;
const changes = {};
if (!node.credentials) {
node.credentials = {_:{}};
} else if (!node.credentials._) {
@@ -177,22 +188,33 @@
if (input.length > 0) {
var value = input.val();
if (credDefinition[cred].type == 'password') {
node.credentials['has_' + cred] = (value !== "");
if (value == '__PWRD__') {
continue;
if (value === '__PWRD__') {
// A cred value exists - no changes
} else if (value === '' && node.credentials['has_' + cred] === false) {
// Empty cred value exists - no changes
} else if (value === node.credentials[cred]) {
// A cred value exists locally in the editor - no changes
// Like the user sets a value, saves the config,
// reopens the config and save the config again
} else {
changes['has_' + cred] = node.credentials['has_' + cred];
changes[cred] = node.credentials[cred];
node.credentials[cred] = value;
}
changed = true;
}
node.credentials[cred] = value;
if (value != node.credentials._[cred]) {
changed = true;
node.credentials['has_' + cred] = (value !== '');
} else {
// Since these creds are loaded by the editor,
// values can be directly compared
if (value !== node.credentials[cred]) {
changes[cred] = node.credentials[cred];
node.credentials[cred] = value;
}
}
}
}
}
return changed;
return changes;
}
})();

View File

@@ -245,10 +245,15 @@ RED.library = (function() {
if (lib.types && lib.types.indexOf(options.url) === -1) {
return;
}
let icon = 'fa fa-hdd-o';
if (lib.icon) {
const fullIcon = RED.utils.separateIconPath(lib.icon);
icon = (fullIcon.module==="font-awesome"?"fa ":"")+fullIcon.file;
}
listing.push({
library: lib.id,
type: options.url,
icon: lib.icon || 'fa fa-hdd-o',
icon,
label: RED._(lib.label||lib.id),
path: "",
expanded: true,
@@ -303,10 +308,15 @@ RED.library = (function() {
if (lib.types && lib.types.indexOf(options.url) === -1) {
return;
}
let icon = 'fa fa-hdd-o';
if (lib.icon) {
const fullIcon = RED.utils.separateIconPath(lib.icon);
icon = (fullIcon.module==="font-awesome"?"fa ":"")+fullIcon.file;
}
listing.push({
library: lib.id,
type: options.url,
icon: lib.icon || 'fa fa-hdd-o',
icon,
label: RED._(lib.label||lib.id),
path: "",
expanded: true,
@@ -839,10 +849,10 @@ RED.library = (function() {
if (file && file.label && !file.children) {
$.get("library/"+file.library+"/"+file.type+"/"+file.path, function(data) {
//TODO: nls + sanitize
var propRow = $('<tr class="red-ui-help-info-row"><td>Type</td><td></td></tr>').appendTo(table);
var propRow = $('<tr class="red-ui-help-info-row"><td>'+RED._("library.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>Name</td><td>'+file.props.name+'</td></tr>').appendTo(table);
propRow = $('<tr class="red-ui-help-info-row"><td>'+RED._("library.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

@@ -562,7 +562,7 @@ RED.palette = (function() {
}
}
paletteNode.css("backgroundColor", sf.color);
paletteNode.css("backgroundColor", RED.utils.getNodeColor("subflow", sf._def));
}
function refreshFilter() {

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: 'Delete branch',
text: RED._("sidebar.project.projectSettings.deleteBranch"),
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()
item.value = input.val() === "_ADD_" ? "" : input.val();
item.type = "conf-type"
}
if (ui.type === "cred" || item.type !== data.parent.type || item.value !== data.parent.value) {

View File

@@ -52,11 +52,21 @@ RED.sidebar.config = (function() {
if (label) {
lockIcon = $('<span style="margin-right: 5px"><i class="fa fa-lock"/></span>').appendTo(header)
lockIcon.toggle(!!isLocked)
$('<span class="red-ui-sidebar-config-category-disabled-icon" style="margin-right: 5px"><i class="fa fa-ban"/></span>').appendTo(header)
$('<span class="red-ui-palette-node-config-label"/>').text(label).appendTo(header);
} 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");
@@ -150,9 +160,6 @@ 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);
@@ -181,15 +188,29 @@ 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) {
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))
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");
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> - ");
}
})
}
@@ -251,7 +272,13 @@ RED.sidebar.config = (function() {
if (!validList[id]) {
$(this).remove();
delete categories[id];
} else if (RED.nodes.workspace(id)) {
$(this).toggleClass("red-ui-sidebar-config-category-disabled", RED.nodes.workspace(id).disabled);
}
// Remove the `changed` badge from the category header
const categoryHeader = $(this).find(".red-ui-sidebar-config-tray-header.red-ui-palette-header");
categoryHeader.removeClass("red-ui-sidebar-config-changed");
})
var globalConfigNodes = [];
var configList = {};

View File

@@ -18,8 +18,6 @@ RED.sidebar.context = (function() {
var content;
var sections;
var localCache = {};
var flowAutoRefresh;
var nodeAutoRefresh;
var nodeSection;
@@ -27,6 +25,8 @@ RED.sidebar.context = (function() {
var flowSection;
var globalSection;
const expandedPaths = {}
var currentNode;
var currentFlow;
@@ -212,14 +212,41 @@ RED.sidebar.context = (function() {
var l = keys.length;
for (var i = 0; i < l; i++) {
sortedData[keys[i]].forEach(function(v) {
var k = keys[i];
var l2 = sortedData[k].length;
var propRow = $('<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]);
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]);
obj.text(k);
var tools = $('<span class="button-group"></span>');
const urlSafeK = encodeURIComponent(k)
var refreshItem = $('<button class="red-ui-button red-ui-button-small"><i class="fa fa-refresh"></i></button>').appendTo(tools).on("click", function(e) {
const 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) {
@@ -229,16 +256,14 @@ RED.sidebar.context = (function() {
tools.detach();
$(propRow.children()[1]).empty();
RED.utils.createObjectElement(RED.utils.decodeObject(payload,format), {
...objectElementOptions,
typeHint: data.format,
sourceId: id+"."+k,
tools: tools,
path: k
}).appendTo(propRow.children()[1]);
}
})
});
RED.popover.tooltip(refreshItem,RED._("sidebar.context.refrsh"));
var deleteItem = $('<button class="red-ui-button red-ui-button-small"><i class="fa fa-trash"></i></button>').appendTo(tools).on("click", function(e) {
const 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({
@@ -246,7 +271,7 @@ RED.sidebar.context = (function() {
target: propRow,
direction: "left",
content: function() {
var content = $('<div>');
const 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);
@@ -269,16 +294,15 @@ 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), {
typeHint: data.format,
sourceId: id+"."+k,
tools: tools,
path: k
...objectElementOptions,
typeHint: data.format
}).appendTo(propRow.children()[1]);
}
});
@@ -293,14 +317,7 @@ RED.sidebar.context = (function() {
});
RED.popover.tooltip(deleteItem,RED._("sidebar.context.delete"));
var payload = v.msg;
var format = v.format;
RED.utils.createObjectElement(RED.utils.decodeObject(payload,format), {
typeHint: v.format,
sourceId: id+"."+k,
tools: tools,
path: k
}).appendTo(propRow.children()[1]);
RED.utils.createObjectElement(RED.utils.decodeObject(payload,format), objectElementOptions).appendTo(propRow.children()[1]);
if (contextStores.length > 1) {
$("<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("Selection");
propertiesPanelHeaderLabel.text(RED._("sidebar.info.selection"));
propertiesPanelHeaderReveal.hide();
propertiesPanelHeaderHelp.hide();
propertiesPanelHeaderCopyLink.hide();

View File

@@ -279,6 +279,11 @@ RED.typeSearch = (function() {
if ($("#red-ui-main-container").height() - opts.y - 195 < 0) {
opts.y = opts.y - 275;
}
const dialogWidth = dialog.width() || 300 // default is 300 (defined in class .red-ui-search)
const workspaceWidth = $('#red-ui-workspace').width()
if (workspaceWidth > dialogWidth && workspaceWidth - opts.x - dialogWidth < 0) {
opts.x = opts.x - (dialogWidth - RED.view.node_width)
}
dialog.css({left:opts.x+"px",top:opts.y+"px"}).show();
searchResultsDiv.slideDown(300);
setTimeout(function() {
@@ -330,13 +335,25 @@ RED.typeSearch = (function() {
}
}
function applyFilter(filter,type,def) {
return !def || !filter ||
(
(!filter.spliceMultiple) &&
(!filter.type || type === filter.type) &&
(!filter.input || type === 'junction' || def.inputs > 0) &&
(!filter.output || type === 'junction' || def.outputs > 0)
)
if (!filter) {
// No filter; allow everything
return true
}
if (type === 'junction') {
// Only allow Junction is there's no specific type filter
return !filter.type
}
if (filter.type) {
// Handle explicit type filter
return filter.type === type
}
if (!def) {
// No node definition available - allow it
return true
}
// Check if the filter is for input/outputs and apply
return (!filter.input || def.inputs > 0) &&
(!filter.output || def.outputs > 0)
}
function refreshTypeList(opts) {
var i;
@@ -365,7 +382,7 @@ RED.typeSearch = (function() {
var items = [];
RED.nodes.registry.getNodeTypes().forEach(function(t) {
var def = RED.nodes.getType(t);
if (def.category !== 'config' && t !== 'unknown' && t !== 'tab') {
if (def.set?.enabled !== false && def.category !== 'config' && t !== 'unknown' && t !== 'tab') {
items.push({type:t,def: def, label:getTypeLabel(t,def)});
}
});

View File

@@ -121,7 +121,7 @@ RED.utils = (function() {
function renderMarkdown(txt) {
var rendered = _marked.parse(txt);
var cleaned = DOMPurify.sanitize(rendered, {SAFE_FOR_JQUERY: true})
const cleaned = DOMPurify.sanitize(rendered);
return cleaned;
}
@@ -230,7 +230,7 @@ RED.utils = (function() {
var pinnedPaths = {};
var formattedPaths = {};
function addMessageControls(obj,sourceId,key,msg,rootPath,strippedKey,extraTools) {
function addMessageControls(obj,sourceId,key,msg,rootPath,strippedKey,extraTools,enablePinning) {
if (!pinnedPaths.hasOwnProperty(sourceId)) {
pinnedPaths[sourceId] = {}
}
@@ -250,7 +250,7 @@ RED.utils = (function() {
RED.clipboard.copyText(msg,copyPayload,"clipboard.copyMessageValue");
})
RED.popover.tooltip(copyPayload,RED._("node-red:debug.sidebar.copyPayload"));
if (strippedKey !== undefined && strippedKey !== '') {
if (enablePinning && strippedKey !== undefined && strippedKey !== '') {
var isPinned = pinnedPaths[sourceId].hasOwnProperty(strippedKey);
var pinPath = $('<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,13 +281,16 @@ RED.utils = (function() {
}
}
}
function checkExpanded(strippedKey,expandPaths,minRange,maxRange) {
function checkExpanded(strippedKey, expandPaths, { minRange, maxRange, expandLeafNodes }) {
if (expandPaths && expandPaths.length > 0) {
if (strippedKey === '' && minRange === undefined) {
return true;
}
for (var i=0;i<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] === "[") {
@@ -394,6 +397,8 @@ 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;
@@ -416,11 +421,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);
addMessageControls(header,sourceId,path,obj,rootPath,strippedKey,tools, enablePinning);
}
if (!key) {
element.addClass("red-ui-debug-msg-top-level");
if (sourceId) {
if (sourceId && !expandPaths) {
var pinned = pinnedPaths[sourceId];
expandPaths = [];
if (pinned) {
@@ -476,7 +481,7 @@ RED.utils = (function() {
$('<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));
},function(state) {if (ontoggle) { ontoggle(path,state);}}, checkExpanded(strippedKey, expandPaths, { expandLeafNodes }));
}
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)) {
@@ -592,14 +597,16 @@ RED.utils = (function() {
typeHint: type==='buffer'?'hex':false,
hideKey: false,
path: path+"["+i+"]",
sourceId: sourceId,
rootPath: rootPath,
expandPaths: expandPaths,
ontoggle: ontoggle,
exposeApi: exposeApi,
sourceId,
rootPath,
expandPaths,
expandLeafNodes,
ontoggle,
exposeApi,
// tools: tools // Do not pass tools down as we
// keep them attached to the top-level header
nodeSelector: options.nodeSelector,
enablePinning
}
).appendTo(row);
}
@@ -623,21 +630,23 @@ RED.utils = (function() {
typeHint: type==='buffer'?'hex':false,
hideKey: false,
path: path+"["+i+"]",
sourceId: sourceId,
rootPath: rootPath,
expandPaths: expandPaths,
ontoggle: ontoggle,
exposeApi: exposeApi,
sourceId,
rootPath,
expandPaths,
expandLeafNodes,
ontoggle,
exposeApi,
// tools: tools // Do not pass tools down as we
// keep them attached to the top-level header
nodeSelector: options.nodeSelector,
enablePinning
}
).appendTo(row);
}
}
})(),
(function() { var path = path+"["+i+"]"; return function(state) {if (ontoggle) { ontoggle(path,state);}}})(),
checkExpanded(strippedKey,expandPaths,minRange,Math.min(fullLength-1,(minRange+9))));
checkExpanded(strippedKey,expandPaths,{ minRange, maxRange: Math.min(fullLength-1,(minRange+9)), expandLeafNodes}));
$('<span class="red-ui-debug-msg-object-key"></span>').html("["+minRange+" &hellip; "+Math.min(fullLength-1,(minRange+9))+"]").appendTo(header);
}
if (fullLength < originalLength) {
@@ -646,7 +655,7 @@ RED.utils = (function() {
}
},
function(state) {if (ontoggle) { ontoggle(path,state);}},
checkExpanded(strippedKey,expandPaths));
checkExpanded(strippedKey, expandPaths, { expandLeafNodes }));
}
} else if (typeof obj === 'object') {
element.addClass('collapsed');
@@ -680,14 +689,16 @@ RED.utils = (function() {
typeHint: false,
hideKey: false,
path: newPath,
sourceId: sourceId,
rootPath: rootPath,
expandPaths: expandPaths,
ontoggle: ontoggle,
exposeApi: exposeApi,
sourceId,
rootPath,
expandPaths,
expandLeafNodes,
ontoggle,
exposeApi,
// tools: tools // Do not pass tools down as we
// keep them attached to the top-level header
nodeSelector: options.nodeSelector,
enablePinning
}
).appendTo(row);
}
@@ -696,7 +707,7 @@ RED.utils = (function() {
}
},
function(state) {if (ontoggle) { ontoggle(path,state);}},
checkExpanded(strippedKey,expandPaths));
checkExpanded(strippedKey, expandPaths, { expandLeafNodes }));
}
if (key) {
$('<span class="red-ui-debug-msg-type-meta"></span>').text(type).appendTo(entryObj);
@@ -1274,7 +1285,6 @@ RED.utils = (function() {
payload = JSON.parse(payload);
} else if (/error/i.test(format)) {
payload = JSON.parse(payload);
payload = (payload.name?payload.name+": ":"")+payload.message;
} else if (format === 'null') {
payload = null;
} else if (format === 'undefined') {

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

View File

@@ -1102,18 +1102,27 @@ 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})
let maxNameNumber = 0;
existingNodes.forEach(n => {
let match = defaultNodeNameRE.exec(n.name)
const existingNodes = RED.nodes.filterNodes({ type: n.type });
const existingIds = existingNodes.reduce((ids, node) => {
let match = defaultNodeNameRE.exec(node.name);
if (match) {
let nodeNumber = parseInt(match[1])
if (nodeNumber > maxNameNumber) {
maxNameNumber = nodeNumber
const nodeNumber = parseInt(match[1], 10);
if (!ids.includes(nodeNumber)) {
ids.push(nodeNumber);
}
}
})
typeIndex[n.type] = maxNameNumber + 1
return ids;
}, []).sort((a, b) => a - b);
let availableNameNumber = 1;
for (let i = 0; i < existingIds.length; i++) {
if (existingIds[i] !== availableNameNumber) {
break;
}
availableNameNumber++;
}
typeIndex[n.type] = availableNameNumber;
}
if ((options.renameBlank && n.name === '') || (options.renameClash && defaultNodeNameRE.test(n.name))) {
if (generateHistory) {
@@ -1145,11 +1154,11 @@ RED.view.tools = (function() {
}
}
function addJunctionsToWires(wires) {
function addJunctionsToWires(options = {}) {
if (RED.workspaces.isLocked()) {
return
}
let wiresToSplit = wires || (RED.view.selection().links && RED.view.selection().links.filter(e => !e.link));
let wiresToSplit = options.wires || (RED.view.selection().links && RED.view.selection().links.filter(e => !e.link));
if (!wiresToSplit) {
return
}
@@ -1197,21 +1206,26 @@ RED.view.tools = (function() {
if (links.length === 0) {
return
}
let pointCount = 0
links.forEach(function(l) {
if (l._sliceLocation) {
junction.x += l._sliceLocation.x
junction.y += l._sliceLocation.y
delete l._sliceLocation
pointCount++
} else {
junction.x += l.source.x + l.source.w/2 + l.target.x - l.target.w/2
junction.y += l.source.y + l.target.y
pointCount += 2
}
})
junction.x = Math.round(junction.x/pointCount)
junction.y = Math.round(junction.y/pointCount)
if (addedJunctions.length === 0 && Object.hasOwn(options, 'x') && Object.hasOwn(options, 'y')) {
junction.x = options.x
junction.y = options.y
} else {
let pointCount = 0
links.forEach(function(l) {
if (l._sliceLocation) {
junction.x += l._sliceLocation.x
junction.y += l._sliceLocation.y
delete l._sliceLocation
pointCount++
} else {
junction.x += l.source.x + l.source.w/2 + l.target.x - l.target.w/2
junction.y += l.source.y + l.target.y
pointCount += 2
}
})
junction.x = Math.round(junction.x/pointCount)
junction.y = Math.round(junction.y/pointCount)
}
if (RED.view.snapGrid) {
let gridSize = RED.view.gridSize()
junction.x = (gridSize*Math.round(junction.x/gridSize));
@@ -1401,7 +1415,7 @@ RED.view.tools = (function() {
RED.actions.add("core:wire-multiple-to-node", function() { wireMultipleToNode() })
RED.actions.add("core:split-wire-with-link-nodes", function () { splitWiresWithLinkNodes() });
RED.actions.add("core:split-wires-with-junctions", function () { addJunctionsToWires() });
RED.actions.add("core:split-wires-with-junctions", function (options) { addJunctionsToWires(options) });
RED.actions.add("core:generate-node-names", generateNodeNames )

View File

@@ -288,7 +288,7 @@ RED.view = (function() {
}
selectedLinks.clearUnselected()
},
length: () => groups.length,
length: () => groups.size,
forEach: (func) => { groups.forEach(func) },
toArray: () => [...groups],
clear: function () {
@@ -321,8 +321,8 @@ RED.view = (function() {
evt.stopPropagation()
RED.contextMenu.show({
type: 'workspace',
x:evt.clientX-5,
y:evt.clientY-5
x: evt.clientX,
y: evt.clientY
})
return false
})
@@ -1209,7 +1209,10 @@ RED.view = (function() {
lasso = null;
}
if (d3.event.touches || d3.event.button === 0) {
if ((mouse_mode === 0 || mouse_mode === RED.state.QUICK_JOINING) && isControlPressed(d3.event) && !(d3.event.altKey || d3.event.shiftKey)) {
if (
(mouse_mode === 0 && isControlPressed(d3.event) && !(d3.event.altKey || d3.event.shiftKey)) ||
mouse_mode === RED.state.QUICK_JOINING
) {
// Trigger quick add dialog
d3.event.stopPropagation();
clearSelection();
@@ -1262,11 +1265,6 @@ RED.view = (function() {
var targetGroup = options.group;
var touchTrigger = options.touchTrigger;
if (targetGroup) {
selectedGroups.add(targetGroup,false);
RED.view.redraw();
}
// `point` is the place in the workspace the mouse has clicked.
// This takes into account scrolling and scaling of the workspace.
var ox = point[0];
@@ -1285,7 +1283,6 @@ RED.view = (function() {
}
var mainPos = $("#red-ui-main-container").position();
if (mouse_mode !== RED.state.QUICK_JOINING) {
mouse_mode = RED.state.QUICK_JOINING;
$(window).on('keyup',disableQuickJoinEventHandler);
@@ -1589,9 +1586,6 @@ RED.view = (function() {
// auto select dropped node - so info shows (if visible)
clearSelection();
nn.selected = true;
if (targetGroup) {
selectedGroups.add(targetGroup,false);
}
movingSet.add(nn);
updateActiveNodes();
updateSelection();
@@ -2176,19 +2170,24 @@ RED.view = (function() {
n.n.moved = true;
}
}
// Check to see if we need to splice a link
// If a node has moved and ends up being spliced into a link, keep
// track of which historyEvent to add the splice info to
let targetSpliceEvent = null
if (moveEvent.nodes.length > 0) {
historyEvent.events.push(moveEvent)
if (activeSpliceLink) {
var linkToSplice = d3.select(activeSpliceLink).data()[0];
spliceLink(linkToSplice, movingSet.get(0).n, moveEvent)
}
targetSpliceEvent = moveEvent
}
if (moveAndChangedGroupEvent.nodes.length > 0) {
historyEvent.events.push(moveAndChangedGroupEvent)
targetSpliceEvent = moveAndChangedGroupEvent
}
// activeSpliceLink will only be set if the movingSet has a single
// node that is able to splice.
if (targetSpliceEvent && activeSpliceLink) {
var linkToSplice = d3.select(activeSpliceLink).data()[0];
spliceLink(linkToSplice, movingSet.get(0).n, targetSpliceEvent)
}
// Only continue if something has moved
if (historyEvent.events.length > 0) {
RED.nodes.dirty(true);
@@ -2687,22 +2686,21 @@ RED.view = (function() {
addToRemovedLinks(reconnectResult.removedLinks)
}
var startDirty = RED.nodes.dirty();
var startChanged = false;
var selectedGroups = [];
const startDirty = RED.nodes.dirty();
let movingSelectedGroups = [];
if (movingSet.length() > 0) {
for (var i=0;i<movingSet.length();i++) {
node = movingSet.get(i).n;
if (node.type === "group") {
selectedGroups.push(node);
movingSelectedGroups.push(node);
}
}
// Make sure we have identified all groups about to be deleted
for (i=0;i<selectedGroups.length;i++) {
selectedGroups[i].nodes.forEach(function(n) {
if (n.type === "group" && selectedGroups.indexOf(n) === -1) {
selectedGroups.push(n);
for (i=0;i<movingSelectedGroups.length;i++) {
movingSelectedGroups[i].nodes.forEach(function(n) {
if (n.type === "group" && movingSelectedGroups.indexOf(n) === -1) {
movingSelectedGroups.push(n);
}
})
}
@@ -2719,7 +2717,7 @@ RED.view = (function() {
addToRemovedLinks(removedEntities.links);
if (node.g) {
var group = RED.nodes.group(node.g);
if (selectedGroups.indexOf(group) === -1) {
if (movingSelectedGroups.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);
@@ -2733,7 +2731,7 @@ RED.view = (function() {
removedLinks = removedLinks.concat(result.links);
if (node.g) {
var group = RED.nodes.group(node.g);
if (selectedGroups.indexOf(group) === -1) {
if (movingSelectedGroups.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);
@@ -2755,8 +2753,8 @@ RED.view = (function() {
// Groups must be removed in the right order - from inner-most
// to outermost.
for (i = selectedGroups.length-1; i>=0; i--) {
var g = selectedGroups[i];
for (i = movingSelectedGroups.length-1; i>=0; i--) {
var g = movingSelectedGroups[i];
removedGroups.push(g);
RED.nodes.removeGroup(g);
}
@@ -3057,8 +3055,8 @@ RED.view = (function() {
}
function disableQuickJoinEventHandler(evt) {
// Check for ctrl (all browsers), "Meta" (Chrome/FF), keyCode 91 (Safari)
if (evt.keyCode === 17 || evt.key === "Meta" || evt.keyCode === 91) {
// Check for ctrl (all browsers), "Meta" (Chrome/FF), keyCode 91 (Safari), or Escape
if (evt.keyCode === 17 || evt.key === "Meta" || evt.keyCode === 91 || evt.keyCode === 27) {
resetMouseVars();
hideDragLines();
redraw();
@@ -3189,27 +3187,59 @@ RED.view = (function() {
for (i=0;i<drag_lines.length;i++) {
if (portType != drag_lines[i].portType && mouseup_node !== drag_lines[i].node) {
var drag_line = drag_lines[i];
var src,dst,src_port;
let drag_line = drag_lines[i];
let src,dst,src_port;
let oldDst;
let oldSrc;
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 = $.extend(true,{},{v:src.links}).v
var oldDstLinks = $.extend(true,{},{v:dst.links}).v
var oldSrcLinks = [...src.links]
var oldDstLinks = [...dst.links]
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);
@@ -3237,6 +3267,7 @@ RED.view = (function() {
links:oldDstLinks
}
});
src.changed = true;
dst.changed = true;
}
@@ -5140,8 +5171,8 @@ RED.view = (function() {
var delta = Infinity;
for (var i = 0; i < lineLength; i++) {
var linePos = pathLine.getPointAtLength(i);
var posDeltaX = Math.abs(linePos.x-d3.event.offsetX)
var posDeltaY = Math.abs(linePos.y-d3.event.offsetY)
var posDeltaX = Math.abs(linePos.x-(d3.event.offsetX / scaleFactor))
var posDeltaY = Math.abs(linePos.y-(d3.event.offsetY / scaleFactor))
var posDelta = posDeltaX*posDeltaX + posDeltaY*posDeltaY
if (posDelta < delta) {
pos = linePos

View File

@@ -183,25 +183,29 @@ RED.workspaces = (function() {
},
null)
}
menuItems.push(
{
id:"red-ui-tabs-menu-option-add-flow",
label: RED._("workspace.addFlow"),
onselect: "core:add-flow"
}
)
if (isMenuButton || !!tab) {
if (RED.settings.theme("menu.menu-item-workspace-add", true)) {
menuItems.push(
{
id:"red-ui-tabs-menu-option-add-flow-right",
label: RED._("workspace.addFlowToRight"),
shortcut: RED.keyboard.getShortcut("core:add-flow-to-right"),
onselect: function() {
RED.actions.invoke("core:add-flow-to-right", tab)
}
},
null
id:"red-ui-tabs-menu-option-add-flow",
label: RED._("workspace.addFlow"),
onselect: "core:add-flow"
}
)
}
if (isMenuButton || !!tab) {
if (RED.settings.theme("menu.menu-item-workspace-add", true)) {
menuItems.push(
{
id:"red-ui-tabs-menu-option-add-flow-right",
label: RED._("workspace.addFlowToRight"),
shortcut: RED.keyboard.getShortcut("core:add-flow-to-right"),
onselect: function() {
RED.actions.invoke("core:add-flow-to-right", tab)
}
},
null
)
}
if (activeWorkspace && activeWorkspace.type === 'tab') {
menuItems.push(
isFlowDisabled ? {
@@ -255,7 +259,9 @@ RED.workspaces = (function() {
}
)
}
menuItems.push(null)
if (menuItems.length > 0) {
menuItems.push(null)
}
if (isMenuButton || !!tab) {
menuItems.push(
{
@@ -299,19 +305,24 @@ RED.workspaces = (function() {
}
)
if (tab) {
menuItems.push(null)
if (RED.settings.theme("menu.menu-item-workspace-delete", true)) {
menuItems.push(
{
label: RED._("common.label.delete"),
onselect: function() {
if (tab.type === 'tab') {
RED.workspaces.delete(tab)
} else if (tab.type === 'subflow') {
RED.subflow.delete(tab.id)
}
},
disabled: isCurrentLocked || (workspaceTabCount === 1)
}
)
}
menuItems.push(
null,
{
label: RED._("common.label.delete"),
onselect: function() {
if (tab.type === 'tab') {
RED.workspaces.delete(tab)
} else if (tab.type === 'subflow') {
RED.subflow.delete(tab.id)
}
},
disabled: isCurrentLocked || (workspaceTabCount === 1)
},
{
label: RED._("menu.label.export"),
shortcut: RED.keyboard.getShortcut("core:show-export-dialog"),
@@ -468,7 +479,7 @@ RED.workspaces = (function() {
},
minimumActiveTabWidth: 150,
scrollable: true,
addButton: "core:add-flow",
addButton: RED.settings.theme("menu.menu-item-workspace-add", true) ? "core:add-flow" : undefined,
addButtonCaption: RED._("workspace.addFlow"),
menu: function() { return getMenuItems(true) },
contextmenu: function(tab) { return getMenuItems(false, tab) }
@@ -525,19 +536,24 @@ RED.workspaces = (function() {
$(window).on("resize", function() {
workspace_tabs.resize();
});
RED.actions.add("core:add-flow",function(opts) { addWorkspace(undefined,undefined,opts?opts.index:undefined)});
RED.actions.add("core:add-flow-to-right",function(workspace) {
let index
if (workspace) {
index = workspace_tabs.getTabIndex(workspace.id)+1
} else {
index = workspace_tabs.activeIndex()+1
}
addWorkspace(undefined,undefined,index)
});
RED.actions.add("core:edit-flow",editWorkspace);
RED.actions.add("core:remove-flow",removeWorkspace);
if (RED.settings.theme("menu.menu-item-workspace-add", true)) {
RED.actions.add("core:add-flow",function(opts) { addWorkspace(undefined,undefined,opts?opts.index:undefined)});
RED.actions.add("core:add-flow-to-right",function(workspace) {
let index
if (workspace) {
index = workspace_tabs.getTabIndex(workspace.id)+1
} else {
index = workspace_tabs.activeIndex()+1
}
addWorkspace(undefined,undefined,index)
});
}
if (RED.settings.theme("menu.menu-item-workspace-edit", true)) {
RED.actions.add("core:edit-flow",editWorkspace);
}
if (RED.settings.theme("menu.menu-item-workspace-delete", true)) {
RED.actions.add("core:remove-flow",removeWorkspace);
}
RED.actions.add("core:enable-flow",enableWorkspace);
RED.actions.add("core:disable-flow",disableWorkspace);
RED.actions.add("core:lock-flow",lockWorkspace);
@@ -751,6 +767,7 @@ RED.workspaces = (function() {
RED.history.push(historyEvent);
RED.events.emit("flows:change",workspace);
RED.nodes.dirty(true);
RED.sidebar.config.refresh();
RED.nodes.filterNodes({z:workspace.id}).forEach(n => n.dirty = true)
RED.view.redraw(true);
}
@@ -904,6 +921,17 @@ RED.workspaces = (function() {
}
},
refresh: function() {
var workspace = RED.nodes.workspace(RED.workspaces.active());
if (workspace) {
document.title = `${documentTitle} : ${workspace.label}`;
} else {
var subflow = RED.nodes.subflow(RED.workspaces.active());
if (subflow) {
document.title = `${documentTitle} : ${subflow.name}`;
} else {
document.title = documentTitle
}
}
RED.nodes.eachWorkspace(function(ws) {
workspace_tabs.renameTab(ws.id,ws.label);
$("#red-ui-tab-"+(ws.id.replace(".","-"))).attr("flowname",ws.label)

View File

@@ -168,6 +168,37 @@ 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 ) {
@@ -320,10 +351,10 @@ RED.user = (function() {
userIcon.css({
backgroundImage: "url("+user.image+")",
})
} else if (user.anonymous) {
} else if (user.anonymous || (!user.username && !user.email)) {
$('<i class="fa fa-user"></i>').appendTo(userIcon);
} else {
$('<span>').text(user.username.substring(0,2)).appendTo(userIcon);
$('<span>').text((user.username || user.email).substring(0,2)).appendTo(userIcon);
}
if (user.profileColor !== undefined) {
userIcon.addClass('red-ui-user-profile-color-' + user.profileColor)

View File

@@ -38,12 +38,13 @@
}
}
#red-ui-image-drop-target {
#red-ui-drop-target-markdown-editor {
position: absolute;
top: 0; bottom: 0;
left: 0; right: 0;
background: var(--red-ui-dnd-background);
display:table;
border-radius: 3px;
width: 100%;
height: 100%;
display: none;

View File

@@ -14,6 +14,25 @@
* limitations under the License.
**/
.red-ui-sidebar-config-category-disabled-icon {
display: none;
}
.red-ui-sidebar-config-category-disabled {
.red-ui-sidebar-config-tray-header {
font-style: italic;
color: var(--red-ui-tab-text-color-disabled-inactive) !important;
.red-ui-sidebar-config-category-disabled-icon {
display: inline;
}
}
.red-ui-sidebar-node-config-list {
.red-ui-palette-node-config {
@extend .red-ui-palette-node-config-disabled;
}
}
}
.red-ui-sidebar-node-config {
position: relative;
background: var(--red-ui-secondary-background);
@@ -84,6 +103,11 @@ ul.red-ui-sidebar-node-config-list {
background: var(--red-ui-node-config-background);
color: var(--red-ui-primary-text-color);
cursor: pointer;
&.red-ui-palette-node-config-invalid.red-ui-palette-node-config-changed {
.red-ui-palette-node-annotations.red-ui-flow-node-error {
left: calc(100% - 28px);
}
}
}
ul.red-ui-sidebar-node-config-list li.red-ui-palette-node-config-type {
color: var(--red-ui-secondary-text-color);
@@ -115,6 +139,15 @@ ul.red-ui-sidebar-node-config-list li.red-ui-palette-node-config-type {
.red-ui-palette-node-config-invalid {
border-color: var(--red-ui-form-input-border-error-color)
}
.red-ui-sidebar-config-tray-header.red-ui-palette-header:not(.red-ui-sidebar-config-changed) .red-ui-flow-node-changed {
display: none;
}
.red-ui-sidebar-config-tray-header.red-ui-palette-header.red-ui-sidebar-config-changed .red-ui-flow-node-changed {
display: inline-block;
position: absolute;
top: 1px;
right: 1px;
}
.red-ui-palette-node-annotations {
position: absolute;
left: calc(100% - 15px);

View File

@@ -151,8 +151,9 @@
&.red-ui-tabs-add {
padding-right: 29px;
}
&.red-ui-tabs-add.red-ui-tabs-scrollable {
padding-right: 53px;
&.red-ui-tabs-add.red-ui-tabs-scrollable,
&.red-ui-tabs-menu.red-ui-tabs-scrollable {
padding-right: 53px;
}
&.red-ui-tabs-add.red-ui-tabs-menu.red-ui-tabs-scrollable,
&.red-ui-tabs-add.red-ui-tabs-search.red-ui-tabs-scrollable {
@@ -310,8 +311,9 @@
}
}
.red-ui-tabs.red-ui-tabs-add .red-ui-tab-scroll-right {
right: 32px;
.red-ui-tabs.red-ui-tabs-add .red-ui-tab-scroll-right,
.red-ui-tabs.red-ui-tabs-menu .red-ui-tab-scroll-right {
right: 32px;
}
.red-ui-tabs.red-ui-tabs-add.red-ui-tabs-menu .red-ui-tab-scroll-right,

View File

@@ -2,4 +2,15 @@
&.red-ui-popover-panel {
border-top: none;
}
}
.red-ui-autoComplete-completion {
font-family: var(--red-ui-monospace-font);
white-space: nowrap;
overflow: hidden;
flex-grow: 1;
text-overflow: ellipsis;
direction: rtl;
text-align: left;
}

View File

@@ -198,6 +198,7 @@ button.red-ui-typedInput-option-expand {
}
button.red-ui-typedInput-option-trigger {
border-left: 1px solid var(--red-ui-form-input-border-color);
border-top-left-radius: 0px;
border-bottom-left-radius: 0px;
border-top-right-radius: 4px;