Compare commits

..

40 Commits

Author SHA1 Message Date
Nick O'Leary
3389c8160b Handle group w/h properties as move rather than change 2024-05-14 13:51:52 +01:00
Nick O'Leary
c214710f8e Improve diff view display of nodes that have only moved 2024-05-14 13:35:05 +01:00
Nick O'Leary
4e33e785fb Handle multiple background deploys 2024-05-14 10:27:55 +01:00
Nick O'Leary
a745ddc164 Fix linting 2024-05-13 14:33:35 +01:00
Nick O'Leary
18d0fa2259 Improve background conflict handling 2024-05-13 14:19:24 +01:00
Nick O'Leary
0e0bba25c1 Merge pull request #4679 from node-red/use-rfdc
Use rfdc for cloning pure JSON values
2024-05-08 16:18:13 +01:00
Nick O'Leary
af701d65ac Merge pull request #4681 from GogoVega/addfrench-v4beta.2
Add French translations for 4.0.0-beta.2
2024-05-08 16:17:57 +01:00
GogoVega
b02f69b77a Add translations for 4.0.0-beta.2 2024-05-05 15:00:42 +02:00
Nick O'Leary
598b0c84ab Merge pull request #4657 from node-red/tab-close-middle-click
Hide workspace tab on middle mouse click
2024-05-03 16:59:28 +01:00
Nick O'Leary
22cc8da088 Apply suggestions from code review 2024-05-03 16:59:17 +01:00
Nick O'Leary
a70618cdef Merge pull request #4666 from node-red/multiplayer-2
[multiplayer] Add user presence indicators
2024-05-03 16:52:50 +01:00
Nick O'Leary
faf142cf66 Merge pull request #4676 from kazuhitoyokoi/dev-fixpulldependencies
Enable updating dependency node of package.json in project feature
2024-05-03 16:52:33 +01:00
Nick O'Leary
1a3cc06935 Use rfdc module for cloning when we know its pure JSON 2024-05-03 16:45:50 +01:00
Nick O'Leary
a712a9363b Merge pull request #4674 from kazuhitoyokoi/dev-addjpn
Add Japanese translations for 4.0.0-beta.2
2024-05-03 16:18:50 +01:00
Steve-Mcl
67e716466f handle middle click hide-tab in onclick handler 2024-04-29 20:14:45 +01:00
Steve-Mcl
3fae03da98 Merge branch 'dev' into tab-close-middle-click 2024-04-29 19:04:47 +01:00
Kazuhito Yokoi
361391ceb8 Load the latest project files when retrieving project information 2024-04-29 16:02:56 +09:00
Kazuhito Yokoi
bf0ca38350 Enable updating dependencies of package.json in project feature 2024-04-29 02:12:08 +09:00
Kazuhito Yokoi
437c28e2b8 Fix typos in welcome tour for 4.0.0-beta.2 2024-04-28 21:27:25 +09:00
Kazuhito Yokoi
c05d18ada1 Add Japanese translations for 4.0.0-beta.2 2024-04-28 21:22:15 +09:00
Nick O'Leary
cfb300ec06 Merge pull request #4672 from node-red/allow-blank-env-var-substitutions
Allow blank strings to be used for env var property substitutions
2024-04-24 23:37:23 +02:00
Nick O'Leary
236e668201 Allow blank strings to be used for env var property substitutions
Fixes #4663
2024-04-24 22:58:11 +02:00
Nick O'Leary
c9b902c2b4 Merge pull request #4658 from node-red/fix-subflow-conf-type
Fix saving of conf-type properties in module packaged subflows
2024-04-23 23:45:29 +02:00
Nick O'Leary
ac8b1e19b7 Merge pull request #4662 from hardillb/timeout-npm-install
Add npm install timeout notification
2024-04-23 23:44:51 +02:00
Nick O'Leary
595933d046 Fix linting 2024-04-23 09:40:01 +02:00
Nick O'Leary
789426f80e Add user presence indication to tabs and nodes 2024-04-23 09:27:35 +02:00
Ben Hardill
148e64c3da Update packages/node_modules/@node-red/editor-client/locales/en-US/editor.json
Co-authored-by: Nick O'Leary <nick.oleary@gmail.com>
2024-04-18 14:22:50 +01:00
Ben Hardill
c6289ebb2c Merge branch 'dev' into timeout-npm-install 2024-04-18 12:12:53 +01:00
Ben Hardill
5f4ece6813 Move translation 2024-04-18 11:47:49 +01:00
Ben Hardill
c990ec39d6 revert DELETE change 2024-04-18 11:35:51 +01:00
Ben Hardill
1fdc600ecd Add npm install timeout notification
part of https://github.com/node-red/node-red/issues/4622
2024-04-18 11:27:32 +01:00
Nick O'Leary
e354d2ce29 Fix saving of conf-type properties in module packaged subflows 2024-04-12 14:08:07 +01:00
Nick O'Leary
d218af8619 Merge branch 'master' into dev 2024-04-12 13:04:54 +01:00
Steve-Mcl
d938e5fb6b close tab on middle mouse click 2024-04-12 11:42:55 +01:00
Nick O'Leary
29ed5b2792 Merge pull request #4655 from node-red/rel319
Bump for 3.1.9 release
2024-04-11 19:22:24 +01:00
Nick O'Leary
e39216e65a Bump for 3.1.9 release 2024-04-11 19:15:46 +01:00
Nick O'Leary
7ac7f9b4c8 Merge pull request #4654 from node-red/fix-subflow-recursion-check
Prevent subflow being added to itself
2024-04-11 19:12:43 +01:00
Stephen McLaughlin
4709eb9d49 Merge pull request #4652 from node-red/fix-windows-spawn
Fix use of spawn on windows with cmd files
2024-04-11 17:51:13 +01:00
Nick O'Leary
c13b8266dd Prevent subflow being added to itself 2024-04-11 17:05:10 +01:00
Nick O'Leary
bd58431603 Fix use of spawn on windows with cmd files 2024-04-11 14:40:29 +01:00
39 changed files with 1154 additions and 451 deletions

View File

@@ -50,6 +50,13 @@ Nodes
- Let debug node status msg length be settable via settings (#4402) @dceejay - Let debug node status msg length be settable via settings (#4402) @dceejay
- Feat: Add ability to set headers for WebSocket client (#4436) @marcus-j-davies - Feat: Add ability to set headers for WebSocket client (#4436) @marcus-j-davies
#### 3.1.9: Maintenance Release
- Prevent subflow being added to itself (#4654) @knolleary
- Fix use of spawn on windows with cmd files (#4652) @knolleary
- Guard refresh of unknown subflow (#4640) @knolleary
- Fix subflow module sending messages to debug sidebar (#4642) @knolleary
#### 3.1.8: Maintenance Release #### 3.1.8: Maintenance Release
- Add validation and error handling on subflow instance properties (#4632) @knolleary - Add validation and error handling on subflow instance properties (#4632) @knolleary

View File

@@ -73,8 +73,9 @@
"passport-http-bearer": "1.0.1", "passport-http-bearer": "1.0.1",
"passport-oauth2-client-password": "0.1.2", "passport-oauth2-client-password": "0.1.2",
"raw-body": "2.5.2", "raw-body": "2.5.2",
"rfdc": "^1.3.1",
"semver": "7.5.4", "semver": "7.5.4",
"tar": "6.1.13", "tar": "6.2.1",
"tough-cookie": "4.1.3", "tough-cookie": "4.1.3",
"uglify-js": "3.17.4", "uglify-js": "3.17.4",
"uuid": "9.0.0", "uuid": "9.0.0",

View File

@@ -372,6 +372,7 @@
"deleted": "deleted", "deleted": "deleted",
"flowDeleted": "flow deleted", "flowDeleted": "flow deleted",
"flowAdded": "flow added", "flowAdded": "flow added",
"moved": "moved",
"movedTo": "moved to __id__", "movedTo": "moved to __id__",
"movedFrom": "moved from __id__" "movedFrom": "moved from __id__"
}, },
@@ -643,6 +644,7 @@
"errors": { "errors": {
"catalogLoadFailed": "<p>Failed to load node catalogue.</p><p>Check the browser console for more information</p>", "catalogLoadFailed": "<p>Failed to load node catalogue.</p><p>Check the browser console for more information</p>",
"installFailed": "<p>Failed to install: __module__</p><p>__message__</p><p>Check the log for more information</p>", "installFailed": "<p>Failed to install: __module__</p><p>__message__</p><p>Check the log for more information</p>",
"installTimeout": "<p>Install continuing the background.</p><p>Nodes will appear in palette when complete. Check the log for more information.</p>",
"removeFailed": "<p>Failed to remove: __module__</p><p>__message__</p><p>Check the log for more information</p>", "removeFailed": "<p>Failed to remove: __module__</p><p>__message__</p><p>Check the log for more information</p>",
"updateFailed": "<p>Failed to update: __module__</p><p>__message__</p><p>Check the log for more information</p>", "updateFailed": "<p>Failed to update: __module__</p><p>__message__</p><p>Check the log for more information</p>",
"enableFailed": "<p>Failed to enable: __module__</p><p>__message__</p><p>Check the log for more information</p>", "enableFailed": "<p>Failed to enable: __module__</p><p>__message__</p><p>Check the log for more information</p>",
@@ -657,6 +659,9 @@
"body": "<p>Removing '__module__'</p><p>Removing the node will uninstall it from Node-RED. The node may continue to use resources until Node-RED is restarted.</p>", "body": "<p>Removing '__module__'</p><p>Removing the node will uninstall it from Node-RED. The node may continue to use resources until Node-RED is restarted.</p>",
"title": "Remove nodes" "title": "Remove nodes"
}, },
"removePlugin": {
"body": "<p>Removed plugin __module__. Please reload the editor to clear left-overs.</p>"
},
"update": { "update": {
"body": "<p>Updating '__module__'</p><p>Updating the node will require a restart of Node-RED to complete the update. This must be done manually.</p>", "body": "<p>Updating '__module__'</p><p>Updating the node will require a restart of Node-RED to complete the update. This must be done manually.</p>",
"title": "Update nodes" "title": "Update nodes"
@@ -668,7 +673,8 @@
"review": "Open node information", "review": "Open node information",
"install": "Install", "install": "Install",
"remove": "Remove", "remove": "Remove",
"update": "Update" "update": "Update",
"understood": "Understood"
} }
} }
} }

View File

@@ -614,6 +614,8 @@
}, },
"nodeCount": "__label__ noeud", "nodeCount": "__label__ noeud",
"nodeCount_plural": "__label__ noeuds", "nodeCount_plural": "__label__ noeuds",
"pluginCount": "__count__ plugin",
"pluginCount_plural": "__count__ plugins",
"moduleCount": "__count__ module disponible", "moduleCount": "__count__ module disponible",
"moduleCount_plural": "__count__ modules disponibles", "moduleCount_plural": "__count__ modules disponibles",
"inuse": "En cours d'utilisation", "inuse": "En cours d'utilisation",
@@ -641,6 +643,7 @@
"errors": { "errors": {
"catalogLoadFailed": "<p>Échec du chargement du catalogue de noeuds.</p><p>Vérifier la console du navigateur pour plus d'informations</p>", "catalogLoadFailed": "<p>Échec du chargement du catalogue de noeuds.</p><p>Vérifier la console du navigateur pour plus d'informations</p>",
"installFailed": "<p>Échec lors de l'installation : __module__</p><p>__message__</p><p>Consulter le journal pour plus d'informations</p>", "installFailed": "<p>Échec lors de l'installation : __module__</p><p>__message__</p><p>Consulter le journal pour plus d'informations</p>",
"installTimeout": "<p>L'installation continue en arrière-plan.</p><p>Les noeuds apparaîtront dans la palette une fois l'installation terminée. Consulter le journal pour plus d'informations.</p>",
"removeFailed": "<p>Échec lors de la suppression : __module__</p><p>__message__</p><p>Consulter le journal pour plus d'informations</p>", "removeFailed": "<p>Échec lors de la suppression : __module__</p><p>__message__</p><p>Consulter le journal pour plus d'informations</p>",
"updateFailed": "<p>Échec lors de la mise à jour : __module__</p><p>__message__</p><p>Consulter le journal pour plus d'informations</p>", "updateFailed": "<p>Échec lors de la mise à jour : __module__</p><p>__message__</p><p>Consulter le journal pour plus d'informations</p>",
"enableFailed": "<p>Échec lors de l'activation : __module__</p><p>__message__</p><p>Consulter le journal pour plus d'informations</p>", "enableFailed": "<p>Échec lors de l'activation : __module__</p><p>__message__</p><p>Consulter le journal pour plus d'informations</p>",
@@ -652,9 +655,12 @@
"title": "Installer les noeuds" "title": "Installer les noeuds"
}, },
"remove": { "remove": {
"body": "<p>Suppression de '__module__'</p><p>La suppression du noeud le désinstallera de Node-RED. Le noeud peut continuer à utiliser des ressources jusqu'au redémarrage de Node-RED.</p>", "body": "<p>Suppression de '__module__'</p><p>La suppression du noeud le désinstallera de Node-RED. Le noeud peut continuer à utiliser ses ressources jusqu'au redémarrage de Node-RED.</p>",
"title": "Supprimer les noeuds" "title": "Supprimer les noeuds"
}, },
"removePlugin": {
"body": "<p>Suppression du plugin '__module__'. Veuillez recharger l'éditeur afin d'appliquer les changements.</p>"
},
"update": { "update": {
"body": "<p>Mise à jour de '__module__'</p><p>La mise à jour du noeud nécessitera un redémarrage de Node-RED pour terminer la mise à jour. Cela doit être fait manuellement.</p>", "body": "<p>Mise à jour de '__module__'</p><p>La mise à jour du noeud nécessitera un redémarrage de Node-RED pour terminer la mise à jour. Cela doit être fait manuellement.</p>",
"title": "Mettre à jour les noeuds" "title": "Mettre à jour les noeuds"
@@ -666,7 +672,8 @@
"review": "Ouvrir la documentation", "review": "Ouvrir la documentation",
"install": "Installer", "install": "Installer",
"remove": "Supprimer", "remove": "Supprimer",
"update": "Mettre à jour" "update": "Mettre à jour",
"understood": "Compris"
} }
} }
} }

View File

@@ -614,6 +614,8 @@
}, },
"nodeCount": "__label__ 個のノード", "nodeCount": "__label__ 個のノード",
"nodeCount_plural": "__label__ 個のノード", "nodeCount_plural": "__label__ 個のノード",
"pluginCount": "__count__ 個のプラグイン",
"pluginCount_plural": "__count__ 個のプラグイン",
"moduleCount": "__count__ 個のモジュール", "moduleCount": "__count__ 個のモジュール",
"moduleCount_plural": "__count__ 個のモジュール", "moduleCount_plural": "__count__ 個のモジュール",
"inuse": "使用中", "inuse": "使用中",
@@ -641,6 +643,7 @@
"errors": { "errors": {
"catalogLoadFailed": "<p>ノードのカタログの読み込みに失敗しました。</p><p>詳細はブラウザのコンソールを確認してください。</p>", "catalogLoadFailed": "<p>ノードのカタログの読み込みに失敗しました。</p><p>詳細はブラウザのコンソールを確認してください。</p>",
"installFailed": "<p>追加処理が失敗しました: __module__</p><p>__message__</p><p>詳細はログを確認してください。</p>", "installFailed": "<p>追加処理が失敗しました: __module__</p><p>__message__</p><p>詳細はログを確認してください。</p>",
"installTimeout": "<p>バックグラウンドでインストールが継続されます。</p><p>完了した時にノードが表示されます。詳細はログを確認してください。</p>",
"removeFailed": "<p>削除処理が失敗しました: __module__</p><p>__message__</p><p>詳細はログを確認してください。</p>", "removeFailed": "<p>削除処理が失敗しました: __module__</p><p>__message__</p><p>詳細はログを確認してください。</p>",
"updateFailed": "<p>更新処理が失敗しました: __module__</p><p>__message__</p><p>詳細はログを確認してください。</p>", "updateFailed": "<p>更新処理が失敗しました: __module__</p><p>__message__</p><p>詳細はログを確認してください。</p>",
"enableFailed": "<p>有効化処理が失敗しました: __module__</p><p>__message__</p><p>詳細はログを確認してください。</p>", "enableFailed": "<p>有効化処理が失敗しました: __module__</p><p>__message__</p><p>詳細はログを確認してください。</p>",
@@ -655,6 +658,9 @@
"body": "<p>__module__ を削除します。</p><p>Node-REDからードを削除します。ードはNode-REDが再起動されるまで、リソースを使い続ける可能性があります。</p>", "body": "<p>__module__ を削除します。</p><p>Node-REDからードを削除します。ードはNode-REDが再起動されるまで、リソースを使い続ける可能性があります。</p>",
"title": "ノードを削除" "title": "ノードを削除"
}, },
"removePlugin": {
"body": "<p>プラグイン __module__ を削除しました。ブラウザを再読み込みして残った表示を消してください。</p>"
},
"update": { "update": {
"body": "<p>__module__ を更新します。</p><p>更新を完了するには手動でNode-REDを再起動する必要があります。</p>", "body": "<p>__module__ を更新します。</p><p>更新を完了するには手動でNode-REDを再起動する必要があります。</p>",
"title": "ノードの更新" "title": "ノードの更新"
@@ -666,7 +672,8 @@
"review": "ノードの情報を参照", "review": "ノードの情報を参照",
"install": "追加", "install": "追加",
"remove": "削除", "remove": "削除",
"update": "更新" "update": "更新",
"understood": "了解"
} }
} }
} }

View File

@@ -29,7 +29,14 @@ RED.history = (function() {
} }
return RED.nodes.junction(id); return RED.nodes.junction(id);
} }
function ensureUnlocked(id, flowsToLock) {
const flow = id && (RED.nodes.workspace(id) || RED.nodes.subflow(id) || null);
const isLocked = flow ? flow.locked : false;
if (flow && isLocked) {
flow.locked = false;
flowsToLock.add(flow)
}
}
function undoEvent(ev) { function undoEvent(ev) {
var i; var i;
var len; var len;
@@ -59,18 +66,46 @@ RED.history = (function() {
t: 'replace', t: 'replace',
config: RED.nodes.createCompleteNodeSet(), config: RED.nodes.createCompleteNodeSet(),
changed: {}, changed: {},
rev: RED.nodes.version() moved: {},
complete: true,
rev: RED.nodes.version(),
dirty: RED.nodes.dirty()
}; };
var selectedTab = RED.workspaces.active();
inverseEv.config.forEach(n => {
const node = RED.nodes.node(n.id)
if (node) {
inverseEv.changed[n.id] = node.changed
inverseEv.moved[n.id] = node.moved
}
})
RED.nodes.clear(); RED.nodes.clear();
var imported = RED.nodes.import(ev.config); var imported = RED.nodes.import(ev.config);
// Clear all change flags from the import
RED.nodes.dirty(false);
const flowsToLock = new Set()
imported.nodes.forEach(function(n) { imported.nodes.forEach(function(n) {
if (ev.changed[n.id]) { if (ev.changed[n.id]) {
ensureUnlocked(n.z, flowsToLock)
n.changed = true; n.changed = true;
inverseEv.changed[n.id] = true;
} }
if (ev.moved[n.id]) {
ensureUnlocked(n.z, flowsToLock)
n.moved = true;
}
})
flowsToLock.forEach(flow => {
flow.locked = true
}) })
RED.nodes.version(ev.rev); RED.nodes.version(ev.rev);
RED.view.redraw(true);
RED.palette.refresh();
RED.workspaces.refresh();
RED.workspaces.show(selectedTab, true);
RED.sidebar.config.refresh();
} else { } else {
var importMap = {}; var importMap = {};
ev.config.forEach(function(n) { ev.config.forEach(function(n) {

View File

@@ -1,114 +1,92 @@
RED.multiplayer = (function () { RED.multiplayer = (function () {
// sessionId - used to identify sessions across websocket reconnects // activeSessionId - used to identify sessions across websocket reconnects
let sessionId let activeSessionId
let headerWidget let headerWidget
// Map of session id to { session:'', user:{}, location:{}} // Map of session id to { session:'', user:{}, location:{}}
let connections = {} let sessions = {}
// Map of username to { user:{}, connections:[] } // Map of username to { user:{}, sessions:[] }
let users = {} let users = {}
function addUserConnection (connection) { function addUserSession (session) {
if (connections[connection.session]) { if (sessions[session.session]) {
// This is an existing connection that has been authenticated // This is an existing connection that has been authenticated
const existingConnection = connections[connection.session] const existingSession = sessions[session.session]
if (existingConnection.user.username !== connection.user.username) { if (existingSession.user.username !== session.user.username) {
removeUserButton(users[existingConnection.user.username]) removeUserHeaderButton(users[existingSession.user.username])
} }
} }
connections[connection.session] = connection sessions[session.session] = session
const user = users[connection.user.username] = users[connection.user.username] || { const user = users[session.user.username] = users[session.user.username] || {
user: connection.user, user: session.user,
connections: [] sessions: []
} }
connection.location = connection.location || {} if (session.user.profileColor === undefined) {
user.connections.push(connection) session.user.profileColor = (1 + Math.floor(Math.random() * 5))
}
session.location = session.location || {}
user.sessions.push(session)
if (connection.user.username === RED.settings.user?.username || if (session.session === activeSessionId) {
connection.session === sessionId // This is the current user session - do not add a extra button for them
) {
// This is the current user - do not add a extra button for them
} else { } else {
if (user.connections.length === 1) { if (user.sessions.length === 1) {
if (user.button) { if (user.button) {
clearTimeout(user.inactiveTimeout) clearTimeout(user.inactiveTimeout)
clearTimeout(user.removeTimeout) clearTimeout(user.removeTimeout)
user.button.removeClass('inactive') user.button.removeClass('inactive')
} else { } else {
addUserButton(user) addUserHeaderButton(user)
} }
} }
sessions[session.session].location = session.location
updateUserLocation(session.session)
} }
} }
function removeUserConnection (session, isDisconnected) { function removeUserSession (sessionId, isDisconnected) {
const connection = connections[session] removeUserLocation(sessionId)
delete connections[session] const session = sessions[sessionId]
const user = users[connection.user.username] delete sessions[sessionId]
const i = user.connections.indexOf(connection) const user = users[session.user.username]
user.connections.splice(i, 1) const i = user.sessions.indexOf(session)
user.sessions.splice(i, 1)
if (isDisconnected) { if (isDisconnected) {
removeUserButton(user) removeUserHeaderButton(user)
} else { } else {
if (user.connections.length === 0) { if (user.sessions.length === 0) {
// Give the user 5s to reconnect before marking inactive // Give the user 5s to reconnect before marking inactive
user.inactiveTimeout = setTimeout(() => { user.inactiveTimeout = setTimeout(() => {
user.button.addClass('inactive') user.button.addClass('inactive')
// Give the user further 20 seconds to reconnect before removing them // Give the user further 20 seconds to reconnect before removing them
// from the user toolbar entirely // from the user toolbar entirely
user.removeTimeout = setTimeout(() => { user.removeTimeout = setTimeout(() => {
removeUserButton(user) removeUserHeaderButton(user)
}, 20000) }, 20000)
}, 5000) }, 5000)
} }
} }
} }
function addUserButton (user) { function addUserHeaderButton (user) {
user.button = $('<li class="red-ui-multiplayer-user"><button type="button" class="red-ui-multiplayer-user-icon" href="#"></button></li>') user.button = $('<li class="red-ui-multiplayer-user"><button type="button" class="red-ui-multiplayer-user-icon"></button></li>')
.attr('data-username', user.user.username) .attr('data-username', user.user.username)
.prependTo("#red-ui-multiplayer-user-list"); .prependTo("#red-ui-multiplayer-user-list");
var button = user.button.find("button") var button = user.button.find("button")
RED.popover.tooltip(button, user.user.username)
button.on('click', function () { button.on('click', function () {
RED.popover.create({ const location = user.sessions[0].location
target:button, revealUser(location)
trigger: 'modal',
interactive: true,
width: "250px",
direction: 'bottom',
content: () => {
const content = $('<div>')
$('<div style="text-align: center">').text(user.user.username).appendTo(content)
const location = user.connections[0].location
if (location.workspace) {
const ws = RED.nodes.workspace(location.workspace) || RED.nodes.subflow(location.workspace)
if (ws) {
$('<div>').text(`${ws.type}: ${ws.label||ws.name||ws.id}`).appendTo(content)
} else {
$('<div>').text(`tab: unknown`).appendTo(content)
}
}
if (location.node) {
const node = RED.nodes.node(location.node)
if (node) {
$('<div>').text(`node: ${node.id}`).appendTo(content)
} else {
$('<div>').text(`node: unknown`).appendTo(content)
}
}
return content
},
}).open()
}) })
if (!user.user.image) {
$('<i class="fa fa-user"></i>').appendTo(button); const userProfile = RED.user.generateUserIcon(user.user)
} else { userProfile.appendTo(button)
$('<span class="user-profile"></span>').css({ }
backgroundImage: "url("+user.user.image+")",
}).appendTo(button); function removeUserHeaderButton (user) {
} user.button.remove()
delete user.button
} }
function getLocation () { function getLocation () {
@@ -124,7 +102,7 @@ RED.multiplayer = (function () {
} }
return location return location
} }
function updateLocation () { function publishLocation () {
const location = getLocation() const location = getLocation()
if (location.workspace !== 0) { if (location.workspace !== 0) {
log('send', 'multiplayer/location', location) log('send', 'multiplayer/location', location)
@@ -132,31 +110,314 @@ RED.multiplayer = (function () {
} }
} }
function removeUserButton (user) { function revealUser(location, skipWorkspace) {
user.button.remove() if (location.node) {
delete user.button // Need to check if this is a known node, so we can fall back to revealing
// the workspace instead
const node = RED.nodes.node(location.node)
if (node) {
RED.view.reveal(location.node)
} else if (!skipWorkspace && location.workspace) {
RED.view.reveal(location.workspace)
}
} else if (!skipWorkspace && location.workspace) {
RED.view.reveal(location.workspace)
}
} }
function updateUserLocation (data) { const workspaceTrays = {}
connections[data.session].location = data function getWorkspaceTray(workspaceId) {
delete data.session // console.log('get tray for',workspaceId)
if (!workspaceTrays[workspaceId]) {
const tray = $('<div class="red-ui-multiplayer-users-tray"></div>')
const users = []
const userIcons = {}
const userCountIcon = $(`<div class="red-ui-multiplayer-user-location"><span class="red-ui-user-profile red-ui-multiplayer-user-count"><span></span></span></div>`)
const userCountSpan = userCountIcon.find('span span')
userCountIcon.hide()
userCountSpan.text('')
userCountIcon.appendTo(tray)
const userCountTooltip = RED.popover.tooltip(userCountIcon, function () {
const content = $('<div>')
users.forEach(sessionId => {
$('<div>').append($('<a href="#">').text(sessions[sessionId].user.username).on('click', function (evt) {
evt.preventDefault()
revealUser(sessions[sessionId].location, true)
userCountTooltip.close()
})).appendTo(content)
})
return content
},
null,
true
)
const updateUserCount = function () {
const maxShown = 2
const children = tray.children()
children.each(function (index, element) {
const i = users.length - index
if (i > maxShown) {
$(this).hide()
} else if (i >= 0) {
$(this).show()
}
})
if (users.length < maxShown + 1) {
userCountIcon.hide()
} else {
userCountSpan.text('+'+(users.length - maxShown))
userCountIcon.show()
}
}
workspaceTrays[workspaceId] = {
attached: false,
tray,
users,
userIcons,
addUser: function (sessionId) {
if (users.indexOf(sessionId) === -1) {
// console.log(`addUser ws:${workspaceId} session:${sessionId}`)
users.push(sessionId)
const userLocationId = `red-ui-multiplayer-user-location-${sessionId}`
const userLocationIcon = $(`<div class="red-ui-multiplayer-user-location" id="${userLocationId}"></div>`)
RED.user.generateUserIcon(sessions[sessionId].user).appendTo(userLocationIcon)
userLocationIcon.prependTo(tray)
RED.popover.tooltip(userLocationIcon, sessions[sessionId].user.username)
userIcons[sessionId] = userLocationIcon
updateUserCount()
}
},
removeUser: function (sessionId) {
// console.log(`removeUser ws:${workspaceId} session:${sessionId}`)
const userLocationId = `red-ui-multiplayer-user-location-${sessionId}`
const index = users.indexOf(sessionId)
if (index > -1) {
users.splice(index, 1)
userIcons[sessionId].remove()
delete userIcons[sessionId]
}
updateUserCount()
},
updateUserCount
}
}
const trayDef = workspaceTrays[workspaceId]
if (!trayDef.attached) {
const workspaceTab = $(`#red-ui-tab-${workspaceId}`)
if (workspaceTab.length > 0) {
trayDef.attached = true
trayDef.tray.appendTo(workspaceTab)
trayDef.users.forEach(sessionId => {
trayDef.userIcons[sessionId].on('click', function (evt) {
revealUser(sessions[sessionId].location, true)
})
})
}
}
return workspaceTrays[workspaceId]
} }
function attachWorkspaceTrays () {
let viewTouched = false
for (let sessionId of Object.keys(sessions)) {
const location = sessions[sessionId].location
if (location) {
if (location.workspace) {
getWorkspaceTray(location.workspace).updateUserCount()
}
if (location.node) {
addUserToNode(sessionId, location.node)
viewTouched = true
}
}
}
if (viewTouched) {
RED.view.redraw()
}
}
function addUserToNode(sessionId, nodeId) {
const node = RED.nodes.node(nodeId)
if (node) {
if (!node._multiplayer) {
node._multiplayer = {
users: [sessionId]
}
node._multiplayer_refresh = true
} else {
if (node._multiplayer.users.indexOf(sessionId) === -1) {
node._multiplayer.users.push(sessionId)
node._multiplayer_refresh = true
}
}
}
}
function removeUserFromNode(sessionId, nodeId) {
const node = RED.nodes.node(nodeId)
if (node && node._multiplayer) {
const i = node._multiplayer.users.indexOf(sessionId)
if (i > -1) {
node._multiplayer.users.splice(i, 1)
}
if (node._multiplayer.users.length === 0) {
delete node._multiplayer
} else {
node._multiplayer_refresh = true
}
}
}
function removeUserLocation (sessionId) {
updateUserLocation(sessionId, {})
}
function updateUserLocation (sessionId, location) {
let viewTouched = false
const oldLocation = sessions[sessionId].location
if (location) {
if (oldLocation.workspace !== location.workspace) {
// console.log('removing', sessionId, oldLocation.workspace)
workspaceTrays[oldLocation.workspace]?.removeUser(sessionId)
}
if (oldLocation.node !== location.node) {
removeUserFromNode(sessionId, oldLocation.node)
viewTouched = true
}
sessions[sessionId].location = location
} else {
location = sessions[sessionId].location
}
// console.log(`updateUserLocation sessionId:${sessionId} oldWS:${oldLocation?.workspace} newWS:${location.workspace}`)
if (location.workspace) {
getWorkspaceTray(location.workspace).addUser(sessionId)
}
if (location.node) {
addUserToNode(sessionId, location.node)
viewTouched = true
}
if (viewTouched) {
RED.view.redraw()
}
}
// function refreshUserLocations () {
// for (const session of Object.keys(sessions)) {
// if (session !== activeSessionId) {
// updateUserLocation(session)
// }
// }
// }
return { return {
init: function () { init: function () {
sessionId = RED.settings.getLocal('multiplayer:sessionId') function createAnnotationUser(user) {
if (!sessionId) {
sessionId = RED.nodes.id() const group = document.createElementNS("http://www.w3.org/2000/svg","g");
RED.settings.setLocal('multiplayer:sessionId', sessionId) 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',
align: 'left',
class: "red-ui-multiplayer-annotation",
show: "_multiplayer",
refresh: "_multiplayer_refresh",
element: function(node) {
const containerGroup = document.createElementNS("http://www.w3.org/2000/svg","g");
containerGroup.setAttribute("transform","translate(0,-4)")
if (node._multiplayer) {
let y = 0
for (let i = Math.min(1, node._multiplayer.users.length - 1); i >= 0; i--) {
const user = sessions[node._multiplayer.users[i]].user
const group = createAnnotationUser(user)
group.setAttribute("transform","translate("+y+",0)")
y += 15
containerGroup.appendChild(group)
}
if (node._multiplayer.users.length > 2) {
const group = createAnnotationUser('+'+(node._multiplayer.users.length - 2))
group.setAttribute("transform","translate("+y+",0)")
y += 12
containerGroup.appendChild(group)
}
}
return containerGroup;
},
tooltip: node => { return node._multiplayer.users.map(u => sessions[u].user.username).join('\n') }
});
// activeSessionId = RED.settings.getLocal('multiplayer:sessionId')
// if (!activeSessionId) {
activeSessionId = RED.nodes.id()
// RED.settings.setLocal('multiplayer:sessionId', activeSessionId)
// log('Session ID (new)', activeSessionId)
// } else {
log('Session ID', activeSessionId)
// }
headerWidget = $('<li><ul id="red-ui-multiplayer-user-list"></ul></li>').prependTo('.red-ui-header-toolbar') headerWidget = $('<li><ul id="red-ui-multiplayer-user-list"></ul></li>').prependTo('.red-ui-header-toolbar')
RED.comms.on('connect', () => { RED.comms.on('connect', () => {
const location = getLocation() const location = getLocation()
const connectInfo = { const connectInfo = {
session: sessionId session: activeSessionId
} }
if (location.workspace !== 0) { if (location.workspace !== 0) {
connectInfo.location = location connectInfo.location = location
@@ -168,40 +429,52 @@ RED.multiplayer = (function () {
if (topic === 'multiplayer/init') { if (topic === 'multiplayer/init') {
// We have just reconnected, runtime has sent state to // We have just reconnected, runtime has sent state to
// initialise the world // initialise the world
connections = {} sessions = {}
users = {} users = {}
$('#red-ui-multiplayer-user-list').empty() $('#red-ui-multiplayer-user-list').empty()
msg.forEach(connection => { msg.sessions.forEach(session => {
addUserConnection(connection) addUserSession(session)
}) })
} else if (topic === 'multiplayer/connection-added') { } else if (topic === 'multiplayer/connection-added') {
addUserConnection(msg) addUserSession(msg)
} else if (topic === 'multiplayer/connection-removed') { } else if (topic === 'multiplayer/connection-removed') {
removeUserConnection(msg.session, msg.disconnected) removeUserSession(msg.session, msg.disconnected)
} else if (topic === 'multiplayer/location') { } else if (topic === 'multiplayer/location') {
updateUserLocation(msg) const session = msg.session
delete msg.session
updateUserLocation(session, msg)
} }
}) })
RED.events.on('workspace:change', (event) => { RED.events.on('workspace:change', (event) => {
updateLocation() getWorkspaceTray(event.workspace)
publishLocation()
}) })
RED.events.on('editor:open', () => { RED.events.on('editor:open', () => {
updateLocation() publishLocation()
}) })
RED.events.on('editor:close', () => { RED.events.on('editor:close', () => {
updateLocation() publishLocation()
}) })
RED.events.on('editor:change', () => { RED.events.on('editor:change', () => {
updateLocation() publishLocation()
}) })
RED.events.on('login', () => { RED.events.on('login', () => {
updateLocation() publishLocation()
})
RED.events.on('flows:loaded', () => {
attachWorkspaceTrays()
})
RED.events.on('workspace:close', (event) => {
// A subflow tab has been closed. Need to mark its tray as detached
if (workspaceTrays[event.workspace]) {
workspaceTrays[event.workspace].attached = false
}
}) })
RED.events.on('logout', () => { RED.events.on('logout', () => {
const disconnectInfo = { const disconnectInfo = {
session: sessionId session: activeSessionId
} }
RED.comms.send('multiplayer/disconnect', disconnectInfo) RED.comms.send('multiplayer/disconnect', disconnectInfo)
RED.settings.removeLocal('multiplayer:sessionId') RED.settings.removeLocal('multiplayer:sessionId')

View File

@@ -298,6 +298,7 @@ var RED = (function() {
RED.workspaces.show(workspaces[0]); RED.workspaces.show(workspaces[0]);
} }
} }
RED.events.emit('flows:loaded')
} catch(err) { } catch(err) {
console.warn(err); console.warn(err);
RED.notify( RED.notify(

View File

@@ -445,9 +445,12 @@ RED.popover = (function() {
return { return {
create: createPopover, create: createPopover,
tooltip: function(target,content, action) { tooltip: function(target,content, action, interactive) {
var label = function() { var label = function() {
var label = content; var label = content;
if (typeof content === 'function') {
label = content()
}
if (action) { if (action) {
var shortcut = RED.keyboard.getShortcut(action); var shortcut = RED.keyboard.getShortcut(action);
if (shortcut && shortcut.key) { if (shortcut && shortcut.key) {
@@ -463,6 +466,7 @@ RED.popover = (function() {
size: "small", size: "small",
direction: "bottom", direction: "bottom",
content: label, content: label,
interactive,
delay: { show: 750, hide: 50 } delay: { show: 750, hide: 50 }
}); });
popover.setContent = function(newContent) { popover.setContent = function(newContent) {

View File

@@ -365,7 +365,10 @@ RED.tabs = (function() {
var thisTabA = thisTab.find("a"); var thisTabA = thisTab.find("a");
if (options.onclick) { if (options.onclick) {
options.onclick(tabs[thisTabA.attr('href').slice(1)]); options.onclick(tabs[thisTabA.attr('href').slice(1)], evt);
if (evt.isDefaultPrevented() && evt.isPropagationStopped()) {
return false
}
} }
activateTab(thisTabA); activateTab(thisTabA);
if (fireSelectionChanged) { if (fireSelectionChanged) {
@@ -548,6 +551,8 @@ RED.tabs = (function() {
ul.find("li.red-ui-tab a") ul.find("li.red-ui-tab a")
.on("mousedown", function(evt) { mousedownTab = evt.currentTarget }) .on("mousedown", function(evt) { mousedownTab = evt.currentTarget })
.on("mouseup",onTabClick) .on("mouseup",onTabClick)
// prevent browser-default middle-click behaviour
.on("auxclick", function(evt) { evt.preventDefault() })
.on("click", function(evt) {evt.preventDefault(); }) .on("click", function(evt) {evt.preventDefault(); })
.on("dblclick", function(evt) {evt.stopPropagation(); evt.preventDefault(); }) .on("dblclick", function(evt) {evt.stopPropagation(); evt.preventDefault(); })
@@ -816,6 +821,8 @@ RED.tabs = (function() {
} }
link.on("mousedown", function(evt) { mousedownTab = evt.currentTarget }) link.on("mousedown", function(evt) { mousedownTab = evt.currentTarget })
link.on("mouseup",onTabClick); link.on("mouseup",onTabClick);
// prevent browser-default middle-click behaviour
link.on("auxclick", function(evt) { evt.preventDefault() })
link.on("click", function(evt) { evt.preventDefault(); }) link.on("click", function(evt) { evt.preventDefault(); })
link.on("dblclick", function(evt) { evt.stopPropagation(); evt.preventDefault(); }) link.on("dblclick", function(evt) { evt.stopPropagation(); evt.preventDefault(); })

View File

@@ -34,6 +34,8 @@ RED.deploy = (function() {
var currentDiff = null; var currentDiff = null;
var activeBackgroundDeployNotification;
function changeDeploymentType(type) { function changeDeploymentType(type) {
deploymentType = type; deploymentType = type;
$("#red-ui-header-button-deploy-icon").attr("src",deploymentTypes[type].img); $("#red-ui-header-button-deploy-icon").attr("src",deploymentTypes[type].img);
@@ -112,51 +114,59 @@ RED.deploy = (function() {
RED.actions.add("core:set-deploy-type-to-modified-nodes",function() { RED.menu.setSelected("deploymenu-item-node",true); }); RED.actions.add("core:set-deploy-type-to-modified-nodes",function() { RED.menu.setSelected("deploymenu-item-node",true); });
} }
window.addEventListener('beforeunload', function (event) {
if (RED.nodes.dirty()) {
event.preventDefault();
event.stopImmediatePropagation()
event.returnValue = RED._("deploy.confirm.undeployedChanges");
return
}
})
RED.events.on('workspace:dirty',function(state) { RED.events.on('workspace:dirty',function(state) {
if (state.dirty) { if (state.dirty) {
window.onbeforeunload = function() { // window.onbeforeunload = function() {
return RED._("deploy.confirm.undeployedChanges"); // return
} // }
$("#red-ui-header-button-deploy").removeClass("disabled"); $("#red-ui-header-button-deploy").removeClass("disabled");
} else { } else {
window.onbeforeunload = null; // window.onbeforeunload = null;
$("#red-ui-header-button-deploy").addClass("disabled"); $("#red-ui-header-button-deploy").addClass("disabled");
} }
}); });
var activeNotifyMessage;
RED.comms.subscribe("notification/runtime-deploy",function(topic,msg) { RED.comms.subscribe("notification/runtime-deploy",function(topic,msg) {
if (!activeNotifyMessage) { var currentRev = RED.nodes.version();
var currentRev = RED.nodes.version(); if (currentRev === null || deployInflight || currentRev === msg.revision) {
if (currentRev === null || deployInflight || currentRev === msg.revision) { return;
return; }
} if (activeBackgroundDeployNotification?.hidden && !activeBackgroundDeployNotification?.closed) {
var message = $('<p>').text(RED._('deploy.confirm.backgroundUpdate')); activeBackgroundDeployNotification.showNotification()
activeNotifyMessage = RED.notify(message,{ return
modal: true, }
fixed: true, const message = $('<p>').text(RED._('deploy.confirm.backgroundUpdate'));
buttons: [ const options = {
{ id: 'background-update',
text: RED._('deploy.confirm.button.ignore'), type: 'compact',
click: function() { modal: false,
activeNotifyMessage.close(); fixed: true,
activeNotifyMessage = null; timeout: 10000,
} buttons: [
}, {
{ text: RED._('deploy.confirm.button.review'),
text: RED._('deploy.confirm.button.review'), class: "primary",
class: "primary", click: function() {
click: function() { activeBackgroundDeployNotification.hideNotification();
activeNotifyMessage.close(); var nns = RED.nodes.createCompleteNodeSet();
var nns = RED.nodes.createCompleteNodeSet(); resolveConflict(nns,false);
resolveConflict(nns,false);
activeNotifyMessage = null;
}
} }
] }
}); ]
}
if (!activeBackgroundDeployNotification || activeBackgroundDeployNotification.closed) {
activeBackgroundDeployNotification = RED.notify(message, options)
} else {
activeBackgroundDeployNotification.update(message, options)
} }
}); });
} }
@@ -213,7 +223,11 @@ RED.deploy = (function() {
class: "primary disabled", class: "primary disabled",
click: function() { click: function() {
if (!$("#red-ui-deploy-dialog-confirm-deploy-review").hasClass('disabled')) { if (!$("#red-ui-deploy-dialog-confirm-deploy-review").hasClass('disabled')) {
RED.diff.showRemoteDiff(); RED.diff.showRemoteDiff(null, {
onmerge: function () {
activeBackgroundDeployNotification.close()
}
});
conflictNotification.close(); conflictNotification.close();
} }
} }
@@ -226,6 +240,7 @@ RED.deploy = (function() {
if (!$("#red-ui-deploy-dialog-confirm-deploy-merge").hasClass('disabled')) { if (!$("#red-ui-deploy-dialog-confirm-deploy-merge").hasClass('disabled')) {
RED.diff.mergeDiff(currentDiff); RED.diff.mergeDiff(currentDiff);
conflictNotification.close(); conflictNotification.close();
activeBackgroundDeployNotification.close()
} }
} }
} }
@@ -238,6 +253,7 @@ RED.deploy = (function() {
click: function() { click: function() {
save(true,activeDeploy); save(true,activeDeploy);
conflictNotification.close(); conflictNotification.close();
activeBackgroundDeployNotification.close()
} }
}) })
} }
@@ -248,21 +264,17 @@ RED.deploy = (function() {
buttons: buttons buttons: buttons
}); });
var now = Date.now();
RED.diff.getRemoteDiff(function(diff) { RED.diff.getRemoteDiff(function(diff) {
var ellapsed = Math.max(1000 - (Date.now()-now), 0);
currentDiff = diff; currentDiff = diff;
setTimeout(function() { conflictCheck.hide();
conflictCheck.hide(); var d = Object.keys(diff.conflicts);
var d = Object.keys(diff.conflicts); if (d.length === 0) {
if (d.length === 0) { conflictAutoMerge.show();
conflictAutoMerge.show(); $("#red-ui-deploy-dialog-confirm-deploy-merge").removeClass('disabled')
$("#red-ui-deploy-dialog-confirm-deploy-merge").removeClass('disabled') } else {
} else { conflictManualMerge.show();
conflictManualMerge.show(); }
} $("#red-ui-deploy-dialog-confirm-deploy-review").removeClass('disabled')
$("#red-ui-deploy-dialog-confirm-deploy-review").removeClass('disabled')
},ellapsed);
}) })
} }
function cropList(list) { function cropList(list) {

View File

@@ -1,5 +1,4 @@
RED.diff = (function() { RED.diff = (function() {
var currentDiff = {}; var currentDiff = {};
var diffVisible = false; var diffVisible = false;
var diffList; var diffList;
@@ -62,12 +61,14 @@ RED.diff = (function() {
addedCount:0, addedCount:0,
deletedCount:0, deletedCount:0,
changedCount:0, changedCount:0,
movedCount:0,
unchangedCount: 0 unchangedCount: 0
}, },
remote: { remote: {
addedCount:0, addedCount:0,
deletedCount:0, deletedCount:0,
changedCount:0, changedCount:0,
movedCount:0,
unchangedCount: 0 unchangedCount: 0
}, },
conflicts: 0 conflicts: 0
@@ -138,7 +139,7 @@ RED.diff = (function() {
$(this).parent().toggleClass('collapsed'); $(this).parent().toggleClass('collapsed');
}); });
createNodePropertiesTable(def,tab,localTabNode,remoteTabNode,conflicts).appendTo(div); createNodePropertiesTable(def,tab,localTabNode,remoteTabNode).appendTo(div);
selectState = ""; selectState = "";
if (conflicts[tab.id]) { if (conflicts[tab.id]) {
flowStats.conflicts++; flowStats.conflicts++;
@@ -208,19 +209,26 @@ RED.diff = (function() {
var localStats = $('<span>',{class:"red-ui-diff-list-flow-stats"}).appendTo(localCell); var localStats = $('<span>',{class:"red-ui-diff-list-flow-stats"}).appendTo(localCell);
$('<span class="red-ui-diff-status"></span>').text(RED._('diff.nodeCount',{count:localNodeCount})).appendTo(localStats); $('<span class="red-ui-diff-status"></span>').text(RED._('diff.nodeCount',{count:localNodeCount})).appendTo(localStats);
if (flowStats.conflicts + flowStats.local.addedCount + flowStats.local.changedCount + flowStats.local.deletedCount > 0) { if (flowStats.conflicts + flowStats.local.addedCount + flowStats.local.changedCount + flowStats.local.movedCount + flowStats.local.deletedCount > 0) {
$('<span class="red-ui-diff-status"> [ </span>').appendTo(localStats); $('<span class="red-ui-diff-status"> [ </span>').appendTo(localStats);
if (flowStats.conflicts > 0) { if (flowStats.conflicts > 0) {
$('<span class="red-ui-diff-status-conflict"><span class="red-ui-diff-status"><i class="fa fa-exclamation"></i> '+flowStats.conflicts+'</span></span>').appendTo(localStats); $('<span class="red-ui-diff-status-conflict"><span class="red-ui-diff-status"><i class="fa fa-exclamation"></i> '+flowStats.conflicts+'</span></span>').appendTo(localStats);
} }
if (flowStats.local.addedCount > 0) { if (flowStats.local.addedCount > 0) {
$('<span class="red-ui-diff-status-added"><span class="red-ui-diff-status"><i class="fa fa-plus-square"></i> '+flowStats.local.addedCount+'</span></span>').appendTo(localStats); const cell = $('<span class="red-ui-diff-status-added"><span class="red-ui-diff-status"><i class="fa fa-plus-square"></i> '+flowStats.local.addedCount+'</span></span>').appendTo(localStats);
RED.popover.tooltip(cell, RED._('diff.type.added'))
} }
if (flowStats.local.changedCount > 0) { if (flowStats.local.changedCount > 0) {
$('<span class="red-ui-diff-status-changed"><span class="red-ui-diff-status"><i class="fa fa-square"></i> '+flowStats.local.changedCount+'</span></span>').appendTo(localStats); const cell = $('<span class="red-ui-diff-status-changed"><span class="red-ui-diff-status"><i class="fa fa-square"></i> '+flowStats.local.changedCount+'</span></span>').appendTo(localStats);
RED.popover.tooltip(cell, RED._('diff.type.changed'))
}
if (flowStats.local.movedCount > 0) {
const cell = $('<span class="red-ui-diff-status-moved"><span class="red-ui-diff-status"><i class="fa fa-square"></i> '+flowStats.local.movedCount+'</span></span>').appendTo(localStats);
RED.popover.tooltip(cell, RED._('diff.type.moved'))
} }
if (flowStats.local.deletedCount > 0) { if (flowStats.local.deletedCount > 0) {
$('<span class="red-ui-diff-status-deleted"><span class="red-ui-diff-status"><i class="fa fa-minus-square"></i> '+flowStats.local.deletedCount+'</span></span>').appendTo(localStats); const cell = $('<span class="red-ui-diff-status-deleted"><span class="red-ui-diff-status"><i class="fa fa-minus-square"></i> '+flowStats.local.deletedCount+'</span></span>').appendTo(localStats);
RED.popover.tooltip(cell, RED._('diff.type.deleted'))
} }
$('<span class="red-ui-diff-status"> ] </span>').appendTo(localStats); $('<span class="red-ui-diff-status"> ] </span>').appendTo(localStats);
} }
@@ -246,19 +254,26 @@ RED.diff = (function() {
} }
var remoteStats = $('<span>',{class:"red-ui-diff-list-flow-stats"}).appendTo(remoteCell); var remoteStats = $('<span>',{class:"red-ui-diff-list-flow-stats"}).appendTo(remoteCell);
$('<span class="red-ui-diff-status"></span>').text(RED._('diff.nodeCount',{count:remoteNodeCount})).appendTo(remoteStats); $('<span class="red-ui-diff-status"></span>').text(RED._('diff.nodeCount',{count:remoteNodeCount})).appendTo(remoteStats);
if (flowStats.conflicts + flowStats.remote.addedCount + flowStats.remote.changedCount + flowStats.remote.deletedCount > 0) { if (flowStats.conflicts + flowStats.remote.addedCount + flowStats.remote.changedCount + flowStats.remote.movedCount + flowStats.remote.deletedCount > 0) {
$('<span class="red-ui-diff-status"> [ </span>').appendTo(remoteStats); $('<span class="red-ui-diff-status"> [ </span>').appendTo(remoteStats);
if (flowStats.conflicts > 0) { if (flowStats.conflicts > 0) {
$('<span class="red-ui-diff-status-conflict"><span class="red-ui-diff-status"><i class="fa fa-exclamation"></i> '+flowStats.conflicts+'</span></span>').appendTo(remoteStats); $('<span class="red-ui-diff-status-conflict"><span class="red-ui-diff-status"><i class="fa fa-exclamation"></i> '+flowStats.conflicts+'</span></span>').appendTo(remoteStats);
} }
if (flowStats.remote.addedCount > 0) { if (flowStats.remote.addedCount > 0) {
$('<span class="red-ui-diff-status-added"><span class="red-ui-diff-status"><i class="fa fa-plus-square"></i> '+flowStats.remote.addedCount+'</span></span>').appendTo(remoteStats); const cell = $('<span class="red-ui-diff-status-added"><span class="red-ui-diff-status"><i class="fa fa-plus-square"></i> '+flowStats.remote.addedCount+'</span></span>').appendTo(remoteStats);
RED.popover.tooltip(cell, RED._('diff.type.added'))
} }
if (flowStats.remote.changedCount > 0) { if (flowStats.remote.changedCount > 0) {
$('<span class="red-ui-diff-status-changed"><span class="red-ui-diff-status"><i class="fa fa-square"></i> '+flowStats.remote.changedCount+'</span></span>').appendTo(remoteStats); const cell = $('<span class="red-ui-diff-status-changed"><span class="red-ui-diff-status"><i class="fa fa-square"></i> '+flowStats.remote.changedCount+'</span></span>').appendTo(remoteStats);
RED.popover.tooltip(cell, RED._('diff.type.changed'))
}
if (flowStats.remote.movedCount > 0) {
const cell = $('<span class="red-ui-diff-status-moved"><span class="red-ui-diff-status"><i class="fa fa-square"></i> '+flowStats.remote.movedCount+'</span></span>').appendTo(remoteStats);
RED.popover.tooltip(cell, RED._('diff.type.moved'))
} }
if (flowStats.remote.deletedCount > 0) { if (flowStats.remote.deletedCount > 0) {
$('<span class="red-ui-diff-status-deleted"><span class="red-ui-diff-status"><i class="fa fa-minus-square"></i> '+flowStats.remote.deletedCount+'</span></span>').appendTo(remoteStats); const cell = $('<span class="red-ui-diff-status-deleted"><span class="red-ui-diff-status"><i class="fa fa-minus-square"></i> '+flowStats.remote.deletedCount+'</span></span>').appendTo(remoteStats);
RED.popover.tooltip(cell, RED._('diff.type.deleted'))
} }
$('<span class="red-ui-diff-status"> ] </span>').appendTo(remoteStats); $('<span class="red-ui-diff-status"> ] </span>').appendTo(remoteStats);
} }
@@ -293,7 +308,7 @@ RED.diff = (function() {
if (options.mode === "merge") { if (options.mode === "merge") {
diffPanel.addClass("red-ui-diff-panel-merge"); diffPanel.addClass("red-ui-diff-panel-merge");
} }
var diffList = createDiffTable(diffPanel, diff); var diffList = createDiffTable(diffPanel, diff, options);
var localDiff = diff.localDiff; var localDiff = diff.localDiff;
var remoteDiff = diff.remoteDiff; var remoteDiff = diff.remoteDiff;
@@ -516,7 +531,6 @@ RED.diff = (function() {
var hasChanges = false; // exists in original and local/remote but with changes var hasChanges = false; // exists in original and local/remote but with changes
var unChanged = true; // existing in original,local,remote unchanged var unChanged = true; // existing in original,local,remote unchanged
var localChanged = false;
if (localDiff.added[node.id]) { if (localDiff.added[node.id]) {
stats.local.addedCount++; stats.local.addedCount++;
@@ -535,12 +549,20 @@ RED.diff = (function() {
unChanged = false; unChanged = false;
} }
if (localDiff.changed[node.id]) { if (localDiff.changed[node.id]) {
stats.local.changedCount++; if (localDiff.positionChanged[node.id]) {
stats.local.movedCount++
} else {
stats.local.changedCount++;
}
hasChanges = true; hasChanges = true;
unChanged = false; unChanged = false;
} }
if (remoteDiff && remoteDiff.changed[node.id]) { if (remoteDiff && remoteDiff.changed[node.id]) {
stats.remote.changedCount++; if (remoteDiff.positionChanged[node.id]) {
stats.remote.movedCount++
} else {
stats.remote.changedCount++;
}
hasChanges = true; hasChanges = true;
unChanged = false; unChanged = false;
} }
@@ -605,27 +627,32 @@ RED.diff = (function() {
localNodeDiv.addClass("red-ui-diff-status-moved"); localNodeDiv.addClass("red-ui-diff-status-moved");
var localMovedMessage = ""; var localMovedMessage = "";
if (node.z === localN.z) { if (node.z === localN.z) {
localMovedMessage = RED._("diff.type.movedFrom",{id:(localDiff.currentConfig.all[node.id].z||'global')}); const movedFromNodeTab = localDiff.currentConfig.all[localDiff.currentConfig.all[node.id].z]
const movedFromLabel = `'${movedFromNodeTab ? (movedFromNodeTab.label || movedFromNodeTab.id) : 'global'}'`
localMovedMessage = RED._("diff.type.movedFrom",{id: movedFromLabel});
} else { } else {
localMovedMessage = RED._("diff.type.movedTo",{id:(localN.z||'global')}); const movedToNodeTab = localDiff.newConfig.all[localN.z]
const movedToLabel = `'${movedToNodeTab ? (movedToNodeTab.label || movedToNodeTab.id) : 'global'}'`
localMovedMessage = RED._("diff.type.movedTo",{id:movedToLabel});
} }
$('<span class="red-ui-diff-status"><i class="fa fa-caret-square-o-right"></i> '+localMovedMessage+'</span>').appendTo(localNodeDiv); $('<span class="red-ui-diff-status"><i class="fa fa-caret-square-o-right"></i> '+localMovedMessage+'</span>').appendTo(localNodeDiv);
} }
localChanged = true;
} else if (localDiff.deleted[node.z]) { } else if (localDiff.deleted[node.z]) {
localNodeDiv.addClass("red-ui-diff-empty"); localNodeDiv.addClass("red-ui-diff-empty");
localChanged = true;
} else if (localDiff.deleted[node.id]) { } else if (localDiff.deleted[node.id]) {
localNodeDiv.addClass("red-ui-diff-status-deleted"); localNodeDiv.addClass("red-ui-diff-status-deleted");
$('<span class="red-ui-diff-status"><i class="fa fa-minus-square"></i> <span data-i18n="diff.type.deleted"></span></span>').appendTo(localNodeDiv); $('<span class="red-ui-diff-status"><i class="fa fa-minus-square"></i> <span data-i18n="diff.type.deleted"></span></span>').appendTo(localNodeDiv);
localChanged = true;
} else if (localDiff.changed[node.id]) { } else if (localDiff.changed[node.id]) {
if (localDiff.newConfig.all[node.id].z !== node.z) { if (localDiff.newConfig.all[node.id].z !== node.z) {
localNodeDiv.addClass("red-ui-diff-empty"); localNodeDiv.addClass("red-ui-diff-empty");
} else { } else {
localNodeDiv.addClass("red-ui-diff-status-changed"); if (localDiff.positionChanged[node.id]) {
$('<span class="red-ui-diff-status"><i class="fa fa-square"></i> <span data-i18n="diff.type.changed"></span></span>').appendTo(localNodeDiv); localNodeDiv.addClass("red-ui-diff-status-moved");
localChanged = true; $('<span class="red-ui-diff-status"><i class="fa fa-square"></i> <span data-i18n="diff.type.moved"></span></span>').appendTo(localNodeDiv);
} else {
localNodeDiv.addClass("red-ui-diff-status-changed");
$('<span class="red-ui-diff-status"><i class="fa fa-square"></i> <span data-i18n="diff.type.changed"></span></span>').appendTo(localNodeDiv);
}
} }
} else { } else {
if (localDiff.newConfig.all[node.id].z !== node.z) { if (localDiff.newConfig.all[node.id].z !== node.z) {
@@ -646,9 +673,13 @@ RED.diff = (function() {
remoteNodeDiv.addClass("red-ui-diff-status-moved"); remoteNodeDiv.addClass("red-ui-diff-status-moved");
var remoteMovedMessage = ""; var remoteMovedMessage = "";
if (node.z === remoteN.z) { if (node.z === remoteN.z) {
remoteMovedMessage = RED._("diff.type.movedFrom",{id:(remoteDiff.currentConfig.all[node.id].z||'global')}); const movedFromNodeTab = remoteDiff.currentConfig.all[remoteDiff.currentConfig.all[node.id].z]
const movedFromLabel = `'${movedFromNodeTab ? (movedFromNodeTab.label || movedFromNodeTab.id) : 'global'}'`
remoteMovedMessage = RED._("diff.type.movedFrom",{id:movedFromLabel});
} else { } else {
remoteMovedMessage = RED._("diff.type.movedTo",{id:(remoteN.z||'global')}); const movedToNodeTab = remoteDiff.newConfig.all[remoteN.z]
const movedToLabel = `'${movedToNodeTab ? (movedToNodeTab.label || movedToNodeTab.id) : 'global'}'`
remoteMovedMessage = RED._("diff.type.movedTo",{id:movedToLabel});
} }
$('<span class="red-ui-diff-status"><i class="fa fa-caret-square-o-right"></i> '+remoteMovedMessage+'</span>').appendTo(remoteNodeDiv); $('<span class="red-ui-diff-status"><i class="fa fa-caret-square-o-right"></i> '+remoteMovedMessage+'</span>').appendTo(remoteNodeDiv);
} }
@@ -661,8 +692,13 @@ RED.diff = (function() {
if (remoteDiff.newConfig.all[node.id].z !== node.z) { if (remoteDiff.newConfig.all[node.id].z !== node.z) {
remoteNodeDiv.addClass("red-ui-diff-empty"); remoteNodeDiv.addClass("red-ui-diff-empty");
} else { } else {
remoteNodeDiv.addClass("red-ui-diff-status-changed"); if (remoteDiff.positionChanged[node.id]) {
$('<span class="red-ui-diff-status"><i class="fa fa-square"></i> <span data-i18n="diff.type.changed"></span></span>').appendTo(remoteNodeDiv); remoteNodeDiv.addClass("red-ui-diff-status-moved");
$('<span class="red-ui-diff-status"><i class="fa fa-square"></i> <span data-i18n="diff.type.moved"></span></span>').appendTo(remoteNodeDiv);
} else {
remoteNodeDiv.addClass("red-ui-diff-status-changed");
$('<span class="red-ui-diff-status"><i class="fa fa-square"></i> <span data-i18n="diff.type.changed"></span></span>').appendTo(remoteNodeDiv);
}
} }
} else { } else {
if (remoteDiff.newConfig.all[node.id].z !== node.z) { if (remoteDiff.newConfig.all[node.id].z !== node.z) {
@@ -788,7 +824,7 @@ RED.diff = (function() {
$("<td>",{class:"red-ui-diff-list-cell-label"}).text("position").appendTo(row); $("<td>",{class:"red-ui-diff-list-cell-label"}).text("position").appendTo(row);
localCell = $("<td>",{class:"red-ui-diff-list-cell red-ui-diff-list-node-local"}).appendTo(row); localCell = $("<td>",{class:"red-ui-diff-list-cell red-ui-diff-list-node-local"}).appendTo(row);
if (localNode) { if (localNode) {
localCell.addClass("red-ui-diff-status-"+(localChanged?"changed":"unchanged")); localCell.addClass("red-ui-diff-status-"+(localChanged?"moved":"unchanged"));
$('<span class="red-ui-diff-status">'+(localChanged?'<i class="fa fa-square"></i>':'')+'</span>').appendTo(localCell); $('<span class="red-ui-diff-status">'+(localChanged?'<i class="fa fa-square"></i>':'')+'</span>').appendTo(localCell);
element = $('<span class="red-ui-diff-list-element"></span>').appendTo(localCell); element = $('<span class="red-ui-diff-list-element"></span>').appendTo(localCell);
var localPosition = {x:localNode.x,y:localNode.y}; var localPosition = {x:localNode.x,y:localNode.y};
@@ -813,7 +849,7 @@ RED.diff = (function() {
if (remoteNode !== undefined) { if (remoteNode !== undefined) {
remoteCell = $("<td>",{class:"red-ui-diff-list-cell red-ui-diff-list-node-remote"}).appendTo(row); remoteCell = $("<td>",{class:"red-ui-diff-list-cell red-ui-diff-list-node-remote"}).appendTo(row);
remoteCell.addClass("red-ui-diff-status-"+(remoteChanged?"changed":"unchanged")); remoteCell.addClass("red-ui-diff-status-"+(remoteChanged?"moved":"unchanged"));
if (remoteNode) { if (remoteNode) {
$('<span class="red-ui-diff-status">'+(remoteChanged?'<i class="fa fa-square"></i>':'')+'</span>').appendTo(remoteCell); $('<span class="red-ui-diff-status">'+(remoteChanged?'<i class="fa fa-square"></i>':'')+'</span>').appendTo(remoteCell);
element = $('<span class="red-ui-diff-list-element"></span>').appendTo(remoteCell); element = $('<span class="red-ui-diff-list-element"></span>').appendTo(remoteCell);
@@ -1099,11 +1135,11 @@ RED.diff = (function() {
// var diff = generateDiff(originalFlow,nns); // var diff = generateDiff(originalFlow,nns);
// showDiff(diff); // showDiff(diff);
// } // }
function showRemoteDiff(diff) { function showRemoteDiff(diff, options = {}) {
if (diff === undefined) { if (!diff) {
getRemoteDiff(showRemoteDiff); getRemoteDiff((remoteDiff) => showRemoteDiff(remoteDiff, options));
} else { } else {
showDiff(diff,{mode:'merge'}); showDiff(diff,{...options, mode:'merge'});
} }
} }
function parseNodes(nodeList) { function parseNodes(nodeList) {
@@ -1144,23 +1180,53 @@ RED.diff = (function() {
} }
} }
function generateDiff(currentNodes,newNodes) { function generateDiff(currentNodes,newNodes) {
var currentConfig = parseNodes(currentNodes); const currentConfig = parseNodes(currentNodes);
var newConfig = parseNodes(newNodes); const newConfig = parseNodes(newNodes);
var added = {}; const added = {};
var deleted = {}; const deleted = {};
var changed = {}; const changed = {};
var moved = {}; const positionChanged = {};
const moved = {};
Object.keys(currentConfig.all).forEach(function(id) { Object.keys(currentConfig.all).forEach(function(id) {
var node = RED.nodes.workspace(id)||RED.nodes.subflow(id)||RED.nodes.node(id); const node = RED.nodes.workspace(id)||RED.nodes.subflow(id)||RED.nodes.node(id);
if (!newConfig.all.hasOwnProperty(id)) { if (!newConfig.all.hasOwnProperty(id)) {
deleted[id] = true; deleted[id] = true;
} else if (JSON.stringify(currentConfig.all[id]) !== JSON.stringify(newConfig.all[id])) { return
}
const currentConfigJSON = JSON.stringify(currentConfig.all[id])
const newConfigJSON = JSON.stringify(newConfig.all[id])
if (currentConfigJSON !== newConfigJSON) {
changed[id] = true; changed[id] = true;
if (currentConfig.all[id].z !== newConfig.all[id].z) { if (currentConfig.all[id].z !== newConfig.all[id].z) {
moved[id] = true; moved[id] = true;
} else if (
currentConfig.all[id].x !== newConfig.all[id].x ||
currentConfig.all[id].y !== newConfig.all[id].y ||
currentConfig.all[id].w !== newConfig.all[id].w ||
currentConfig.all[id].h !== newConfig.all[id].h
) {
// This node's position on its parent has changed. We want to
// check if this is the *only* change for this given node
const currentNodeClone = JSON.parse(currentConfigJSON)
const newNodeClone = JSON.parse(newConfigJSON)
delete currentNodeClone.x
delete currentNodeClone.y
delete currentNodeClone.w
delete currentNodeClone.h
delete newNodeClone.x
delete newNodeClone.y
delete newNodeClone.w
delete newNodeClone.h
if (JSON.stringify(currentNodeClone) === JSON.stringify(newNodeClone)) {
// Only the position has changed - everything else is the same
positionChanged[id] = true
}
} }
} }
}); });
Object.keys(newConfig.all).forEach(function(id) { Object.keys(newConfig.all).forEach(function(id) {
@@ -1169,13 +1235,14 @@ RED.diff = (function() {
} }
}); });
var diff = { const diff = {
currentConfig: currentConfig, currentConfig,
newConfig: newConfig, newConfig,
added: added, added,
deleted: deleted, deleted,
changed: changed, changed,
moved: moved positionChanged,
moved
}; };
return diff; return diff;
} }
@@ -1240,12 +1307,14 @@ RED.diff = (function() {
return diff; return diff;
} }
function showDiff(diff,options) { function showDiff(diff, options) {
if (diffVisible) { if (diffVisible) {
return; return;
} }
options = options || {}; options = options || {};
var mode = options.mode || 'merge'; var mode = options.mode || 'merge';
options.hidePositionChanges = true
var localDiff = diff.localDiff; var localDiff = diff.localDiff;
var remoteDiff = diff.remoteDiff; var remoteDiff = diff.remoteDiff;
@@ -1315,6 +1384,9 @@ RED.diff = (function() {
if (!$("#red-ui-diff-view-diff-merge").hasClass('disabled')) { if (!$("#red-ui-diff-view-diff-merge").hasClass('disabled')) {
refreshConflictHeader(diff); refreshConflictHeader(diff);
mergeDiff(diff); mergeDiff(diff);
if (options.onmerge) {
options.onmerge()
}
RED.tray.close(); RED.tray.close();
} }
} }
@@ -1345,6 +1417,7 @@ RED.diff = (function() {
var newConfig = []; var newConfig = [];
var node; var node;
var nodeChangedStates = {}; var nodeChangedStates = {};
var nodeMovedStates = {};
var localChangedStates = {}; var localChangedStates = {};
for (id in localDiff.newConfig.all) { for (id in localDiff.newConfig.all) {
if (localDiff.newConfig.all.hasOwnProperty(id)) { if (localDiff.newConfig.all.hasOwnProperty(id)) {
@@ -1352,12 +1425,14 @@ RED.diff = (function() {
if (resolutions[id] === 'local') { if (resolutions[id] === 'local') {
if (node) { if (node) {
nodeChangedStates[id] = node.changed; nodeChangedStates[id] = node.changed;
nodeMovedStates[id] = node.moved;
} }
newConfig.push(localDiff.newConfig.all[id]); newConfig.push(localDiff.newConfig.all[id]);
} else if (resolutions[id] === 'remote') { } else if (resolutions[id] === 'remote') {
if (!remoteDiff.deleted[id] && remoteDiff.newConfig.all.hasOwnProperty(id)) { if (!remoteDiff.deleted[id] && remoteDiff.newConfig.all.hasOwnProperty(id)) {
if (node) { if (node) {
nodeChangedStates[id] = node.changed; nodeChangedStates[id] = node.changed;
nodeMovedStates[id] = node.moved;
} }
localChangedStates[id] = 1; localChangedStates[id] = 1;
newConfig.push(remoteDiff.newConfig.all[id]); newConfig.push(remoteDiff.newConfig.all[id]);
@@ -1381,8 +1456,9 @@ RED.diff = (function() {
} }
return { return {
config: newConfig, config: newConfig,
nodeChangedStates: nodeChangedStates, nodeChangedStates,
localChangedStates: localChangedStates nodeMovedStates,
localChangedStates
} }
} }
@@ -1393,6 +1469,7 @@ RED.diff = (function() {
var newConfig = appliedDiff.config; var newConfig = appliedDiff.config;
var nodeChangedStates = appliedDiff.nodeChangedStates; var nodeChangedStates = appliedDiff.nodeChangedStates;
var nodeMovedStates = appliedDiff.nodeMovedStates;
var localChangedStates = appliedDiff.localChangedStates; var localChangedStates = appliedDiff.localChangedStates;
var isDirty = RED.nodes.dirty(); var isDirty = RED.nodes.dirty();
@@ -1401,33 +1478,56 @@ RED.diff = (function() {
t:"replace", t:"replace",
config: RED.nodes.createCompleteNodeSet(), config: RED.nodes.createCompleteNodeSet(),
changed: nodeChangedStates, changed: nodeChangedStates,
moved: nodeMovedStates,
complete: true,
dirty: isDirty, dirty: isDirty,
rev: RED.nodes.version() rev: RED.nodes.version()
} }
RED.history.push(historyEvent); RED.history.push(historyEvent);
var originalFlow = RED.nodes.originalFlow(); // var originalFlow = RED.nodes.originalFlow();
// originalFlow is what the editor things it loaded // // originalFlow is what the editor thinks it loaded
// - add any newly added nodes from remote diff as they are now part of the record // // - add any newly added nodes from remote diff as they are now part of the record
for (var id in diff.remoteDiff.added) { // for (var id in diff.remoteDiff.added) {
if (diff.remoteDiff.added.hasOwnProperty(id)) { // if (diff.remoteDiff.added.hasOwnProperty(id)) {
if (diff.remoteDiff.newConfig.all.hasOwnProperty(id)) { // if (diff.remoteDiff.newConfig.all.hasOwnProperty(id)) {
originalFlow.push(JSON.parse(JSON.stringify(diff.remoteDiff.newConfig.all[id]))); // originalFlow.push(JSON.parse(JSON.stringify(diff.remoteDiff.newConfig.all[id])));
} // }
} // }
} // }
RED.nodes.clear(); RED.nodes.clear();
var imported = RED.nodes.import(newConfig); var imported = RED.nodes.import(newConfig);
// Restore the original flow so subsequent merge resolutions can properly // // Restore the original flow so subsequent merge resolutions can properly
// identify new-vs-old // // identify new-vs-old
RED.nodes.originalFlow(originalFlow); // RED.nodes.originalFlow(originalFlow);
// Clear all change flags from the import
RED.nodes.dirty(false);
const flowsToLock = new Set()
function ensureUnlocked(id) {
const flow = id && (RED.nodes.workspace(id) || RED.nodes.subflow(id) || null);
const isLocked = flow ? flow.locked : false;
if (flow && isLocked) {
flow.locked = false;
flowsToLock.add(flow)
}
}
imported.nodes.forEach(function(n) { imported.nodes.forEach(function(n) {
if (nodeChangedStates[n.id] || localChangedStates[n.id]) { if (nodeChangedStates[n.id]) {
ensureUnlocked(n.z)
n.changed = true; n.changed = true;
} }
if (nodeMovedStates[n.id]) {
ensureUnlocked(n.z)
n.moved = true;
}
})
flowsToLock.forEach(flow => {
flow.locked = true
}) })
RED.nodes.version(diff.remoteDiff.rev); RED.nodes.version(diff.remoteDiff.rev);

View File

@@ -221,12 +221,12 @@ RED.notifications = (function() {
if (newType) { if (newType) {
n.className = "red-ui-notification red-ui-notification-"+newType; n.className = "red-ui-notification red-ui-notification-"+newType;
} }
newTimeout = newOptions.hasOwnProperty('timeout')?newOptions.timeout:timeout
if (!fixed || newOptions.fixed === false) { if (!fixed || newOptions.fixed === false) {
newTimeout = (newOptions.hasOwnProperty('timeout')?newOptions.timeout:timeout)||5000; newTimeout = newTimeout || 5000
} }
if (newOptions.buttons) { if (newOptions.buttons) {
var buttonSet = $('<div style="margin-top: 20px;" class="ui-dialog-buttonset"></div>').appendTo(nn) var buttonSet = $('<div class="ui-dialog-buttonset"></div>').appendTo(nn)
newOptions.buttons.forEach(function(buttonDef) { newOptions.buttons.forEach(function(buttonDef) {
var b = $('<button>').text(buttonDef.text).on("click", buttonDef.click).appendTo(buttonSet); var b = $('<button>').text(buttonDef.text).on("click", buttonDef.click).appendTo(buttonSet);
if (buttonDef.id) { if (buttonDef.id) {
@@ -272,6 +272,15 @@ RED.notifications = (function() {
}; };
})()); })());
n.timeoutid = window.setTimeout(n.close,timeout||5000); n.timeoutid = window.setTimeout(n.close,timeout||5000);
} else if (timeout) {
$(n).on("click.red-ui-notification-close", (function() {
var nn = n;
return function() {
nn.hideNotification();
window.clearTimeout(nn.timeoutid);
};
})());
n.timeoutid = window.setTimeout(n.hideNotification,timeout||5000);
} }
currentNotifications.push(n); currentNotifications.push(n);
if (options.id) { if (options.id) {

View File

@@ -133,7 +133,7 @@ RED.palette.editor = (function() {
}).done(function(data,textStatus,xhr) { }).done(function(data,textStatus,xhr) {
callback(); callback();
}).fail(function(xhr,textStatus,err) { }).fail(function(xhr,textStatus,err) {
callback(xhr); callback(xhr,textStatus,err);
}); });
} }
function removeNodeModule(id,callback) { function removeNodeModule(id,callback) {
@@ -1346,13 +1346,13 @@ RED.palette.editor = (function() {
}); });
if (!found_onremove) { if (!found_onremove) {
let removeNotify = RED.notify("Removed plugin " + entry.name + ". Please reload the editor to clear left-overs.",{ let removeNotify = RED.notify(RED._("palette.editor.confirm.removePlugin.body",{module:entry.name}),{
modal: true, modal: true,
fixed: true, fixed: true,
type: 'warning', type: 'warning',
buttons: [ buttons: [
{ {
text: "Understood", text: RED._("palette.editor.confirm.button.understood"),
class:"primary", class:"primary",
click: function(e) { click: function(e) {
removeNotify.close(); removeNotify.close();
@@ -1405,9 +1405,28 @@ RED.palette.editor = (function() {
RED.actions.invoke("core:show-event-log"); RED.actions.invoke("core:show-event-log");
}); });
RED.eventLog.startEvent(RED._("palette.editor.confirm.button.install")+" : "+entry.id+" "+entry.version); RED.eventLog.startEvent(RED._("palette.editor.confirm.button.install")+" : "+entry.id+" "+entry.version);
installNodeModule(entry.id,entry.version,entry.pkg_url,function(xhr) { installNodeModule(entry.id,entry.version,entry.pkg_url,function(xhr, textStatus,err) {
spinner.remove(); spinner.remove();
if (xhr) { if (err && xhr.status === 504) {
var notification = RED.notify(RED._("palette.editor.errors.installTimeout"), {
modal: true,
fixed: true,
buttons: [
{
text: RED._("common.label.close"),
click: function() {
notification.close();
}
},{
text: RED._("eventLog.view"),
click: function() {
notification.close();
RED.actions.invoke("core:show-event-log");
}
}
]
})
} else if (xhr) {
if (xhr.responseJSON) { if (xhr.responseJSON) {
var notification = RED.notify(RED._('palette.editor.errors.installFailed',{module: entry.id,message:xhr.responseJSON.message}),{ var notification = RED.notify(RED._('palette.editor.errors.installFailed',{module: entry.id,message:xhr.responseJSON.message}),{
type: 'error', type: 'error',

View File

@@ -287,7 +287,7 @@ RED.projects.settings = (function() {
var notInstalledCount = 0; var notInstalledCount = 0;
for (var m in modulesInUse) { for (var m in modulesInUse) {
if (modulesInUse.hasOwnProperty(m)) { if (modulesInUse.hasOwnProperty(m) && !activeProject.dependencies.hasOwnProperty(m)) {
depsList.editableList('addItem',{ depsList.editableList('addItem',{
id: modulesInUse[m].module, id: modulesInUse[m].module,
version: modulesInUse[m].version, version: modulesInUse[m].version,
@@ -307,8 +307,8 @@ RED.projects.settings = (function() {
if (activeProject.dependencies) { if (activeProject.dependencies) {
for (var m in activeProject.dependencies) { for (var m in activeProject.dependencies) {
if (activeProject.dependencies.hasOwnProperty(m) && !modulesInUse.hasOwnProperty(m)) { if (activeProject.dependencies.hasOwnProperty(m)) {
var installed = !!RED.nodes.registry.getModule(m); var installed = !!RED.nodes.registry.getModule(m) && activeProject.dependencies[m] === modulesInUse[m].version;
depsList.editableList('addItem',{ depsList.editableList('addItem',{
id: m, id: m,
version: activeProject.dependencies[m], //RED.nodes.registry.getModule(module).version, version: activeProject.dependencies[m], //RED.nodes.registry.getModule(module).version,

View File

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

View File

@@ -9,14 +9,27 @@ RED.view.annotations = (function() {
addAnnotation(evt.node.__pendingAnnotation__,evt); addAnnotation(evt.node.__pendingAnnotation__,evt);
delete evt.node.__pendingAnnotation__; delete evt.node.__pendingAnnotation__;
} }
var badgeDX = 0; let badgeRDX = 0;
var controlDX = 0; let badgeLDX = 0;
for (var i=0,l=evt.el.__annotations__.length;i<l;i++) {
var annotation = evt.el.__annotations__[i]; for (let i=0,l=evt.el.__annotations__.length;i<l;i++) {
const annotation = evt.el.__annotations__[i];
if (annotations.hasOwnProperty(annotation.id)) { if (annotations.hasOwnProperty(annotation.id)) {
var opts = annotations[annotation.id]; const opts = annotations[annotation.id];
var showAnnotation = true; let showAnnotation = true;
var isBadge = opts.type === 'badge'; const isBadge = opts.type === 'badge';
if (opts.refresh !== undefined) {
let refreshAnnotation = false
if (typeof opts.refresh === "string") {
refreshAnnotation = !!evt.node[opts.refresh]
delete evt.node[opts.refresh]
} else if (typeof opts.refresh === "function") {
refreshAnnotation = opts.refresh(evnt.node)
}
if (refreshAnnotation) {
refreshAnnotationElement(annotation.id, annotation.node, annotation.element)
}
}
if (opts.show !== undefined) { if (opts.show !== undefined) {
if (typeof opts.show === "string") { if (typeof opts.show === "string") {
showAnnotation = !!evt.node[opts.show] showAnnotation = !!evt.node[opts.show]
@@ -29,17 +42,24 @@ RED.view.annotations = (function() {
} }
if (isBadge) { if (isBadge) {
if (showAnnotation) { if (showAnnotation) {
var rect = annotation.element.getBoundingClientRect(); const rect = annotation.element.getBoundingClientRect();
badgeDX += rect.width; let annotationX
annotation.element.setAttribute("transform", "translate("+(evt.node.w-3-badgeDX)+", -8)"); if (!opts.align || opts.align === 'right') {
badgeDX += 4; annotationX = evt.node.w - 3 - badgeRDX - rect.width
} badgeRDX += rect.width + 4;
} else {
if (showAnnotation) { } else if (opts.align === 'left') {
var rect = annotation.element.getBoundingClientRect(); annotationX = 3 + badgeLDX
annotation.element.setAttribute("transform", "translate("+(3+controlDX)+", -12)"); badgeLDX += rect.width + 4;
controlDX += rect.width + 4; }
annotation.element.setAttribute("transform", "translate("+annotationX+", -8)");
} }
// } else {
// if (showAnnotation) {
// var rect = annotation.element.getBoundingClientRect();
// annotation.element.setAttribute("transform", "translate("+(3+controlDX)+", -12)");
// controlDX += rect.width + 4;
// }
} }
} else { } else {
annotation.element.parentNode.removeChild(annotation.element); annotation.element.parentNode.removeChild(annotation.element);
@@ -95,15 +115,25 @@ RED.view.annotations = (function() {
annotationGroup.setAttribute("class",opts.class || ""); annotationGroup.setAttribute("class",opts.class || "");
evt.el.__annotations__.push({ evt.el.__annotations__.push({
id:id, id:id,
node: evt.node,
element: annotationGroup element: annotationGroup
}); });
var annotation = opts.element(evt.node); refreshAnnotationElement(id, evt.node, annotationGroup)
evt.el.appendChild(annotationGroup);
}
function refreshAnnotationElement(id, node, annotationGroup) {
const opts = annotations[id];
const annotation = opts.element(node);
if (opts.tooltip) { if (opts.tooltip) {
annotation.addEventListener("mouseenter", getAnnotationMouseEnter(annotation,evt.node,opts.tooltip)); annotation.addEventListener("mouseenter", getAnnotationMouseEnter(annotation, node, opts.tooltip));
annotation.addEventListener("mouseleave", annotationMouseLeave); annotation.addEventListener("mouseleave", annotationMouseLeave);
} }
if (annotationGroup.hasChildNodes()) {
annotationGroup.removeChild(annotationGroup.firstChild)
}
annotationGroup.appendChild(annotation); annotationGroup.appendChild(annotation);
evt.el.appendChild(annotationGroup);
} }

View File

@@ -646,120 +646,128 @@ RED.view = (function() {
} }
d3.event = event; d3.event = event;
var selected_tool = $(ui.draggable[0]).attr("data-palette-type"); var selected_tool = $(ui.draggable[0]).attr("data-palette-type");
var result = createNode(selected_tool);
if (!result) {
return;
}
var historyEvent = result.historyEvent;
var nn = RED.nodes.add(result.node);
var showLabel = RED.utils.getMessageProperty(RED.settings.get('editor'),"view.view-node-show-label");
if (showLabel !== undefined && (nn._def.hasOwnProperty("showLabel")?nn._def.showLabel:true) && !nn._def.defaults.hasOwnProperty("l")) {
nn.l = showLabel;
}
var helperOffset = d3.touches(ui.helper.get(0))[0]||d3.mouse(ui.helper.get(0));
var helperWidth = ui.helper.width();
var helperHeight = ui.helper.height();
var mousePos = d3.touches(this)[0]||d3.mouse(this);
try { try {
var isLink = (nn.type === "link in" || nn.type === "link out") var result = createNode(selected_tool);
var hideLabel = nn.hasOwnProperty('l')?!nn.l : isLink; if (!result) {
return;
var label = RED.utils.getNodeLabel(nn, nn.type);
var labelParts = getLabelParts(label, "red-ui-flow-node-label");
if (hideLabel) {
nn.w = node_height;
nn.h = Math.max(node_height,(nn.outputs || 0) * 15);
} else {
nn.w = Math.max(node_width,20*(Math.ceil((labelParts.width+50+(nn._def.inputs>0?7:0))/20)) );
nn.h = Math.max(6+24*labelParts.lines.length,(nn.outputs || 0) * 15, 30);
} }
} catch(err) { var historyEvent = result.historyEvent;
} var nn = RED.nodes.add(result.node);
mousePos[1] += this.scrollTop + ((helperHeight/2)-helperOffset[1]); var showLabel = RED.utils.getMessageProperty(RED.settings.get('editor'),"view.view-node-show-label");
mousePos[0] += this.scrollLeft + ((helperWidth/2)-helperOffset[0]); if (showLabel !== undefined && (nn._def.hasOwnProperty("showLabel")?nn._def.showLabel:true) && !nn._def.defaults.hasOwnProperty("l")) {
mousePos[1] /= scaleFactor; nn.l = showLabel;
mousePos[0] /= scaleFactor; }
nn.x = mousePos[0]; var helperOffset = d3.touches(ui.helper.get(0))[0]||d3.mouse(ui.helper.get(0));
nn.y = mousePos[1]; var helperWidth = ui.helper.width();
var helperHeight = ui.helper.height();
var mousePos = d3.touches(this)[0]||d3.mouse(this);
var minX = nn.w/2 -5; try {
if (nn.x < minX) { var isLink = (nn.type === "link in" || nn.type === "link out")
nn.x = minX; var hideLabel = nn.hasOwnProperty('l')?!nn.l : isLink;
}
var minY = nn.h/2 -5;
if (nn.y < minY) {
nn.y = minY;
}
var maxX = space_width -nn.w/2 +5;
if (nn.x > maxX) {
nn.x = maxX;
}
var maxY = space_height -nn.h +5;
if (nn.y > maxY) {
nn.y = maxY;
}
if (snapGrid) { var label = RED.utils.getNodeLabel(nn, nn.type);
var gridOffset = RED.view.tools.calculateGridSnapOffsets(nn); var labelParts = getLabelParts(label, "red-ui-flow-node-label");
nn.x -= gridOffset.x; if (hideLabel) {
nn.y -= gridOffset.y; nn.w = node_height;
} nn.h = Math.max(node_height,(nn.outputs || 0) * 15);
} else {
nn.w = Math.max(node_width,20*(Math.ceil((labelParts.width+50+(nn._def.inputs>0?7:0))/20)) );
nn.h = Math.max(6+24*labelParts.lines.length,(nn.outputs || 0) * 15, 30);
}
} catch(err) {
}
var linkToSplice = $(ui.helper).data("splice"); mousePos[1] += this.scrollTop + ((helperHeight/2)-helperOffset[1]);
if (linkToSplice) { mousePos[0] += this.scrollLeft + ((helperWidth/2)-helperOffset[0]);
spliceLink(linkToSplice, nn, historyEvent) mousePos[1] /= scaleFactor;
} mousePos[0] /= scaleFactor;
nn.x = mousePos[0];
nn.y = mousePos[1];
var minX = nn.w/2 -5;
if (nn.x < minX) {
nn.x = minX;
}
var minY = nn.h/2 -5;
if (nn.y < minY) {
nn.y = minY;
}
var maxX = space_width -nn.w/2 +5;
if (nn.x > maxX) {
nn.x = maxX;
}
var maxY = space_height -nn.h +5;
if (nn.y > maxY) {
nn.y = maxY;
}
if (snapGrid) {
var gridOffset = RED.view.tools.calculateGridSnapOffsets(nn);
nn.x -= gridOffset.x;
nn.y -= gridOffset.y;
}
var linkToSplice = $(ui.helper).data("splice");
if (linkToSplice) {
spliceLink(linkToSplice, nn, historyEvent)
}
var group = $(ui.helper).data("group");
if (group) {
var oldX = group.x;
var oldY = group.y;
RED.group.addToGroup(group, nn);
var moveEvent = null;
if ((group.x !== oldX) ||
(group.y !== oldY)) {
moveEvent = {
t: "move",
nodes: [{n: group,
ox: oldX, oy: oldY,
dx: group.x -oldX,
dy: group.y -oldY}],
dirty: true
};
}
historyEvent = {
t: 'multi',
events: [historyEvent],
var group = $(ui.helper).data("group");
if (group) {
var oldX = group.x;
var oldY = group.y;
RED.group.addToGroup(group, nn);
var moveEvent = null;
if ((group.x !== oldX) ||
(group.y !== oldY)) {
moveEvent = {
t: "move",
nodes: [{n: group,
ox: oldX, oy: oldY,
dx: group.x -oldX,
dy: group.y -oldY}],
dirty: true
}; };
if (moveEvent) {
historyEvent.events.push(moveEvent)
}
historyEvent.events.push({
t: "addToGroup",
group: group,
nodes: nn
})
} }
historyEvent = {
t: 'multi',
events: [historyEvent],
}; RED.history.push(historyEvent);
if (moveEvent) { RED.editor.validateNode(nn);
historyEvent.events.push(moveEvent) RED.nodes.dirty(true);
// auto select dropped node - so info shows (if visible)
clearSelection();
nn.selected = true;
movingSet.add(nn);
updateActiveNodes();
updateSelection();
redraw();
if (nn._def.autoedit) {
RED.editor.edit(nn);
}
} catch (error) {
if (error.code != "NODE_RED") {
RED.notify(RED._("notification.error",{message:error.toString()}),"error");
} else {
RED.notify(RED._("notification.error",{message:error.message}),"error");
} }
historyEvent.events.push({
t: "addToGroup",
group: group,
nodes: nn
})
}
RED.history.push(historyEvent);
RED.editor.validateNode(nn);
RED.nodes.dirty(true);
// auto select dropped node - so info shows (if visible)
clearSelection();
nn.selected = true;
movingSet.add(nn);
updateActiveNodes();
updateSelection();
redraw();
if (nn._def.autoedit) {
RED.editor.edit(nn);
} }
} }
}); });
@@ -6063,14 +6071,19 @@ RED.view = (function() {
function createNode(type, x, y, z) { function createNode(type, x, y, z) {
const wasDirty = RED.nodes.dirty() const wasDirty = RED.nodes.dirty()
var m = /^subflow:(.+)$/.exec(type); var m = /^subflow:(.+)$/.exec(type);
var activeSubflow = z ? RED.nodes.subflow(z) : null; var activeSubflow = (z || RED.workspaces.active()) ? RED.nodes.subflow(z || RED.workspaces.active()) : null;
if (activeSubflow && m) { if (activeSubflow && m) {
var subflowId = m[1]; var subflowId = m[1];
let err
if (subflowId === activeSubflow.id) { if (subflowId === activeSubflow.id) {
throw new Error(RED._("notification.error", { message: RED._("notification.errors.cannotAddSubflowToItself") })) err = new Error(RED._("notification.errors.cannotAddSubflowToItself"))
} else if (RED.nodes.subflowContains(m[1], activeSubflow.id)) {
err = new Error(RED._("notification.errors.cannotAddCircularReference"))
} }
if (RED.nodes.subflowContains(m[1], activeSubflow.id)) { if (err) {
throw new Error(RED._("notification.error", { message: RED._("notification.errors.cannotAddCircularReference") })) err.code = 'NODE_RED'
throw err
} }
} }

View File

@@ -359,11 +359,17 @@ RED.workspaces = (function() {
RED.sidebar.config.refresh(); RED.sidebar.config.refresh();
RED.view.focus(); RED.view.focus();
}, },
onclick: function(tab) { onclick: function(tab, evt) {
if (tab.id !== activeWorkspace) { if(evt.which === 2) {
addToViewStack(activeWorkspace); evt.preventDefault();
evt.stopPropagation();
RED.actions.invoke("core:hide-flow", tab)
} else {
if (tab.id !== activeWorkspace) {
addToViewStack(activeWorkspace);
}
RED.view.focus();
} }
RED.view.focus();
}, },
ondblclick: function(tab) { ondblclick: function(tab) {
if (tab.type != "subflow") { if (tab.type != "subflow") {
@@ -401,6 +407,7 @@ RED.workspaces = (function() {
if (tab.type === "tab") { if (tab.type === "tab") {
workspaceTabCount--; workspaceTabCount--;
} else { } else {
RED.events.emit("workspace:close",{workspace: tab.id})
hideStack.push(tab.id); hideStack.push(tab.id);
} }
RED.menu.setDisabled("menu-item-workspace-delete",activeWorkspace === 0 || workspaceTabCount <= 1); RED.menu.setDisabled("menu-item-workspace-delete",activeWorkspace === 0 || workspaceTabCount <= 1);

View File

@@ -212,6 +212,8 @@ RED.user = (function() {
function updateUserMenu() { function updateUserMenu() {
$("#red-ui-header-button-user-submenu li").remove(); $("#red-ui-header-button-user-submenu li").remove();
const userMenu = $("#red-ui-header-button-user")
userMenu.empty()
if (RED.settings.user.anonymous) { if (RED.settings.user.anonymous) {
RED.menu.addItem("red-ui-header-button-user",{ RED.menu.addItem("red-ui-header-button-user",{
id:"usermenu-item-login", id:"usermenu-item-login",
@@ -226,7 +228,6 @@ RED.user = (function() {
}); });
} }
}); });
$('<i class="fa fa-user"></i>').appendTo("#red-ui-header-button-user");
} else { } else {
RED.menu.addItem("red-ui-header-button-user",{ RED.menu.addItem("red-ui-header-button-user",{
id:"usermenu-item-username", id:"usermenu-item-username",
@@ -239,17 +240,9 @@ RED.user = (function() {
RED.user.logout(); RED.user.logout();
} }
}); });
const userMenu = $("#red-ui-header-button-user")
userMenu.empty()
if (RED.settings.user.image) {
$('<span class="user-profile"></span>').css({
backgroundImage: "url("+RED.settings.user.image+")",
}).appendTo(userMenu);
} else {
$('<i class="fa fa-user"></i>').appendTo(userMenu);
}
} }
const userIcon = generateUserIcon(RED.settings.user)
userIcon.appendTo(userMenu);
} }
function init() { function init() {
@@ -320,12 +313,30 @@ RED.user = (function() {
return false; return false;
} }
function generateUserIcon(user) {
const userIcon = $('<span class="red-ui-user-profile"></span>')
if (user.image) {
userIcon.addClass('has_profile_image')
userIcon.css({
backgroundImage: "url("+user.image+")",
})
} else if (user.anonymous) {
$('<i class="fa fa-user"></i>').appendTo(userIcon);
} else {
$('<span>').text(user.username.substring(0,2)).appendTo(userIcon);
}
if (user.profileColor !== undefined) {
userIcon.addClass('red-ui-user-profile-color-' + user.profileColor)
}
return userIcon
}
return { return {
init: init, init: init,
login: login, login: login,
logout: logout, logout: logout,
hasPermission: hasPermission hasPermission: hasPermission,
generateUserIcon
} }
})(); })();

View File

@@ -314,6 +314,16 @@ $spinner-color: #999;
$tab-icon-color: #dedede; $tab-icon-color: #dedede;
// Anonymous User Colors
$user-profile-colors: (
1: #822e81,
2: #955e42,
3: #9c914f,
4: #748e54,
5: #06bcc1
);
// Deprecated // Deprecated
$text-color-green: $text-color-success; $text-color-green: $text-color-success;
$info-text-code-color: $text-color-code; $info-text-code-color: $text-color-code;

View File

@@ -274,18 +274,44 @@
#usermenu-item-username > .red-ui-menu-label { #usermenu-item-username > .red-ui-menu-label {
color: var(--red-ui-header-menu-heading-color); color: var(--red-ui-header-menu-heading-color);
} }
}
.user-profile {
background-position: center center; .red-ui-user-profile {
background-repeat: no-repeat; background-color: var(--red-ui-header-background);
background-size: contain; border: 2px solid var(--red-ui-header-menu-color);
display: inline-block; border-radius: 30px;
width: 30px; overflow: hidden;
height: 30px;
vertical-align: middle; background-position: center center;
background-repeat: no-repeat;
background-size: contain;
display: inline-flex;
justify-content: center;
align-items: center;
vertical-align: middle;
width: 30px;
height: 30px;
font-size: 20px;
&.red-ui-user-profile-color-1 {
background-color: var(--red-ui-user-profile-colors-1);
}
&.red-ui-user-profile-color-2 {
background-color: var(--red-ui-user-profile-colors-2);
}
&.red-ui-user-profile-color-3 {
background-color: var(--red-ui-user-profile-colors-3);
}
&.red-ui-user-profile-color-4 {
background-color: var(--red-ui-user-profile-colors-4);
}
&.red-ui-user-profile-color-5 {
background-color: var(--red-ui-user-profile-colors-5);
} }
} }
@media only screen and (max-width: 450px) { @media only screen and (max-width: 450px) {
span.red-ui-header-logo > span { span.red-ui-header-logo > span {
display: none; display: none;

View File

@@ -5,23 +5,18 @@
li { li {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
width: 30px;
margin: 0 2px; margin: 0 2px;
} }
} }
.red-ui-multiplayer-user-icon { .red-ui-multiplayer-user-icon {
background: var(--red-ui-header-background); background: none;
border: 2px solid var(--red-ui-header-menu-color); border: none;
border-radius: 30px;
display: inline-flex; display: inline-flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
width: 28px;
height: 28px;
text-align: center; text-align: center;
overflow: hidden;
box-sizing: border-box; box-sizing: border-box;
text-decoration: none; text-decoration: none;
color: var(--red-ui-header-menu-color); color: var(--red-ui-header-menu-color);
@@ -36,13 +31,86 @@
.red-ui-multiplayer-user.inactive & { .red-ui-multiplayer-user.inactive & {
opacity: 0.5; opacity: 0.5;
} }
.user-profile { .red-ui-user-profile {
background-position: center center; width: 20px;
background-repeat: no-repeat; border-radius: 20px;
background-size: contain; height: 20px;
display: inline-block; font-size: 12px
vertical-align: middle; }
width: 28px; }
height: 28px; .red-ui-multiplayer-users-tray {
position: absolute;
top: 5px;
right: 20px;
line-height: normal;
cursor: pointer;
// &:hover {
// .red-ui-multiplayer-user-location {
// margin-left: 1px;
// }
// }
}
$multiplayer-user-icon-background: var(--red-ui-primary-background);
$multiplayer-user-icon-border: var(--red-ui-view-background);
$multiplayer-user-icon-text-color: var(--red-ui-header-menu-color);
$multiplayer-user-icon-count-text-color: var(--red-ui-primary-color);
$multiplayer-user-icon-shadow: 0px 0px 4px var(--red-ui-shadow);
.red-ui-multiplayer-user-location {
display: inline-block;
margin-left: -6px;
transition: margin-left 0.2s;
.red-ui-user-profile {
border: 1px solid $multiplayer-user-icon-border;
color: $multiplayer-user-icon-text-color;
width: 18px;
height: 18px;
border-radius: 18px;
font-size: 10px;
font-weight: normal;
box-shadow: $multiplayer-user-icon-shadow;
&.red-ui-multiplayer-user-count {
color: $multiplayer-user-icon-count-text-color;
background-color: $multiplayer-user-icon-background;
}
}
}
.red-ui-multiplayer-annotation {
.red-ui-multiplayer-annotation-background {
filter: drop-shadow($multiplayer-user-icon-shadow);
fill: $multiplayer-user-icon-background;
&.red-ui-user-profile-color-1 {
fill: var(--red-ui-user-profile-colors-1);
}
&.red-ui-user-profile-color-2 {
fill: var(--red-ui-user-profile-colors-2);
}
&.red-ui-user-profile-color-3 {
fill: var(--red-ui-user-profile-colors-3);
}
&.red-ui-user-profile-color-4 {
fill: var(--red-ui-user-profile-colors-4);
}
&.red-ui-user-profile-color-5 {
fill: var(--red-ui-user-profile-colors-5);
}
}
.red-ui-multiplayer-annotation-border {
stroke: $multiplayer-user-icon-border;
stroke-width: 1px;
fill: none;
}
.red-ui-multiplayer-annotation-anon-label {
fill: $multiplayer-user-icon-text-color;
stroke: none;
}
text {
user-select: none;
fill: $multiplayer-user-icon-text-color;
stroke: none;
font-size: 10px;
&.red-ui-multiplayer-user-count {
fill: $multiplayer-user-icon-count-text-color;
}
} }
} }

View File

@@ -299,4 +299,7 @@
--red-ui-tab-icon-color: #{$tab-icon-color}; --red-ui-tab-icon-color: #{$tab-icon-color};
@each $current-color in 1 2 3 4 5 {
--red-ui-user-profile-colors-#{"" + $current-color}: #{map-get($user-profile-colors, $current-color)};
}
} }

View File

@@ -16,7 +16,9 @@ export default {
}, },
{ {
title: { title: {
"en-US": "Multiplayer Mode" "en-US": "Multiplayer Mode",
"ja": "複数ユーザ同時利用モード",
"fr": "Mode Multi-utilisateur"
}, },
image: 'images/nr4-multiplayer.png', image: 'images/nr4-multiplayer.png',
description: { description: {
@@ -24,46 +26,77 @@ export default {
to work with when you have multiple people editing flows at the same time.</p> to work with when you have multiple people editing flows at the same time.</p>
<p>When this feature is enabled, you will now see who else has the editor open and some <p>When this feature is enabled, you will now see who else has the editor open and some
basic information on where they are in the editor.</p> basic information on where they are in the editor.</p>
<p>Check the release post for details on how to enable this feature in your settings file.</p>` <p>Check the release post for details on how to enable this feature in your settings file.</p>`,
"ja": `<p>本リリースには、複数ユーザが同時にフローを編集する時に、Node-REDをより使いやすくするのための最初の微修正が入っています。</p>
<p>本機能を有効にすると、誰がエディタを開いているか、その人がエディタ上のどこにいるかの基本的な情報が表示されます。</p>
<p>設定ファイルで本機能を有効化する方法の詳細は、リリースの投稿を確認してください。</p>`,
"fr": `<p>Cette version inclut les premières étapes visant à rendre Node-RED plus facile à utiliser
lorsque plusieurs personnes modifient des flux en même temps.</p>
<p>Lorsque cette fonctionnalité est activée, vous pourrez désormais voir si dautres utilisateurs ont
ouvert l'éditeur. Vous pourrez également savoir où ces utilisateurs se trouvent dans l'éditeur.</p>
<p>Consultez la note de publication pour plus de détails sur la façon d'activer cette fonctionnalité
dans votre fichier de paramètres.</p>`
} }
}, },
{ {
title: { title: {
"en-US": "Better Configuration Node UX" "en-US": "Better Configuration Node UX",
"ja": "設定ードのUXが向上",
"fr": "Meilleure expérience utilisateur du noeud de configuration"
}, },
image: 'images/nr4-config-select.png', image: 'images/nr4-config-select.png',
description: { description: {
"en-US": `<p>The Configuration node selection UI has had a small update to have a dedicated 'add' button "en-US": `<p>The Configuration node selection UI has had a small update to have a dedicated 'add' button
next to the select box.</p> next to the select box.</p>
<p>It's a small change, but should make it easier to work with your config nodes.</p>` <p>It's a small change, but should make it easier to work with your config nodes.</p>`,
"ja": `<p>設定ードを選択するUIが修正され、選択ボックスの隣に専用の「追加」ボタンが追加されました。</p>
<p>微修正ですが設定ノードの操作が容易になります。</p>`,
"fr": `<p>L'interface utilisateur de la sélection du noeud de configuration a fait l'objet d'une petite
mise à jour afin de disposer d'un bouton « Ajouter » à côté de la zone de sélection.</p>
<p>C'est un petit changement, mais cela devrait faciliter le travail avec vos noeuds de configuration.</p>`
} }
}, },
{ {
title: { title: {
"en-US": "Remembering palette state" "en-US": "Remembering palette state",
"ja": "パレットの状態を維持",
"fr": "Mémorisation de l'état de la palette"
}, },
description: { description: {
"en-US": `<p>The palette now remembers what categories you have hidden between reloads - as well as any "en-US": `<p>The palette now remembers what categories you have hidden between reloads - as well as any
filter you have applied.</p>` filter you have applied.</p>`,
"ja": `<p>パレット上で非表示にしたカテゴリや適用したフィルタが、リロードしても記憶されるようになりました。</p>`,
"fr": `<p>La palette se souvient désormais des catégories que vous avez masquées entre les rechargements,
ainsi que le filtre que vous avez appliqué.</p>`
} }
}, },
{ {
title: { title: {
"en-US": "Plugins shown in the Palette Manager" "en-US": "Plugins shown in the Palette Manager",
"ja": "パレット管理にプラグインを表示",
"fr": "Affichage des Plugins dans le gestionnaire de palettes"
}, },
image: 'images/nr4-plugins.png', image: 'images/nr4-plugins.png',
description: { description: {
"en-US": `<p>The palette manager now shows any plugin modules you have installed, such as "en-US": `<p>The palette manager now shows any plugin modules you have installed, such as
<code>node-red-debugger</code>. Previously they would only be shown if they plugin include <code>node-red-debugger</code>. Previously they would only be shown if the plugins include
nodes for the palette.</p>` nodes for the palette.</p>`,
"ja": `<p>パレットの管理に <code>node-red-debugger</code> の様なインストールしたプラグインが表示されます。以前はプラグインにパレット向けのノードが含まれている時のみ表示されていました。</p>`,
"fr": `<p>Le gestionnaire de palettes affiche désormais tous les plugins que vous avez installés,
tels que <code>node-red-debugger</code>. Auparavant, ils n'étaient affichés que s'ils contenaient
des noeuds pour la palette.</p>`
} }
}, },
{ {
title: { title: {
"en-US": "That's if for Beta 2!" "en-US": "That's it for Beta 2!",
"ja": "ベータ2については以上です!",
"fr": "C'est tout pour la bêta 2 !"
}, },
description: { description: {
"en-US": `<p>Keep clicking through to see what was added in Beta 1</p>` "en-US": `<p>Keep clicking through to see what was added in Beta 1</p>`,
"ja": `<p>クリックを続けてベータ1で追加された内容を確認してください。</p>`,
"fr": `<p>Continuez à cliquer pour voir ce qui a été ajouté dans la version bêta 1</p>`
} }
}, },
{ {

View File

@@ -20,6 +20,7 @@ module.exports = function(RED) {
var exec = require('child_process').exec; var exec = require('child_process').exec;
var fs = require('fs'); var fs = require('fs');
var isUtf8 = require('is-utf8'); var isUtf8 = require('is-utf8');
const isWindows = process.platform === 'win32'
function ExecNode(n) { function ExecNode(n) {
RED.nodes.createNode(this,n); RED.nodes.createNode(this,n);
@@ -85,9 +86,12 @@ module.exports = function(RED) {
} }
}); });
var cmd = arg.shift(); var cmd = arg.shift();
// Since 18.20.2/20.12.2, it is invalid to call spawn on Windows with a .bat/.cmd file
// without using shell: true.
const opts = isWindows ? { ...node.spawnOpt, shell: true } : node.spawnOpt
/* istanbul ignore else */ /* istanbul ignore else */
node.debug(cmd+" ["+arg+"]"); node.debug(cmd+" ["+arg+"]");
child = spawn(cmd,arg,node.spawnOpt); child = spawn(cmd,arg,opts);
node.status({fill:"blue",shape:"dot",text:"pid:"+child.pid}); node.status({fill:"blue",shape:"dot",text:"pid:"+child.pid});
var unknownCommand = (child.pid === undefined); var unknownCommand = (child.pid === undefined);
if (node.timer !== 0) { if (node.timer !== 0) {

View File

@@ -273,7 +273,7 @@ async function installModule(moduleDetails) {
let extraArgs = triggerPayload.args || []; let extraArgs = triggerPayload.args || [];
let args = ['install', ...extraArgs, installSpec] let args = ['install', ...extraArgs, installSpec]
log.trace(NPM_COMMAND + JSON.stringify(args)); log.trace(NPM_COMMAND + JSON.stringify(args));
return exec.run(NPM_COMMAND, args, { cwd: installDir },true) return exec.run(NPM_COMMAND, args, { cwd: installDir, shell: true },true)
} else { } else {
log.trace("skipping npm install"); log.trace("skipping npm install");
} }

View File

@@ -25,14 +25,17 @@ const registryUtil = require("./util");
const library = require("./library"); const library = require("./library");
const {exec,log,events,hooks} = require("@node-red/util"); const {exec,log,events,hooks} = require("@node-red/util");
const child_process = require('child_process'); const child_process = require('child_process');
const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm';
let installerEnabled = false;
const plugins = require("./plugins"); const plugins = require("./plugins");
const isWindows = process.platform === 'win32'
const npmCommand = isWindows ? 'npm.cmd' : 'npm';
let installerEnabled = false;
let settings; let settings;
const moduleRe = /^(@[^/@]+?[/])?[^/@]+?$/; const moduleRe = /^(@[^/@]+?[/])?[^/@]+?$/;
const slashRe = process.platform === "win32" ? /\\|[/]/ : /[/]/; const slashRe = isWindows ? /\\|[/]/ : /[/]/;
const pkgurlRe = /^(https?|git(|\+https?|\+ssh|\+file)):\/\//; const pkgurlRe = /^(https?|git(|\+https?|\+ssh|\+file)):\/\//;
const localtgzRe = /^([a-zA-Z]:|\/).+tgz$/; const localtgzRe = /^([a-zA-Z]:|\/).+tgz$/;
@@ -227,7 +230,7 @@ async function installModule(module,version,url) {
let extraArgs = triggerPayload.args || []; let extraArgs = triggerPayload.args || [];
let args = ['install', ...extraArgs, installName] let args = ['install', ...extraArgs, installName]
log.trace(npmCommand + JSON.stringify(args)); log.trace(npmCommand + JSON.stringify(args));
return exec.run(npmCommand,args,{ cwd: installDir}, true) return exec.run(npmCommand,args,{ cwd: installDir, shell: true }, true)
} else { } else {
log.trace("skipping npm install"); log.trace("skipping npm install");
} }
@@ -262,7 +265,7 @@ async function installModule(module,version,url) {
log.warn("------------------------------------------"); log.warn("------------------------------------------");
e = new Error(log._("server.install.install-failed")+": "+err.toString()); e = new Error(log._("server.install.install-failed")+": "+err.toString());
if (err.hook === "postInstall") { if (err.hook === "postInstall") {
return exec.run(npmCommand,["remove",module],{ cwd: installDir}, false).finally(() => { return exec.run(npmCommand,["remove",module],{ cwd: installDir, shell: true }, false).finally(() => {
throw e; throw e;
}) })
} }
@@ -366,7 +369,7 @@ async function getModuleVersionFromNPM(module, version) {
} }
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
child_process.execFile(npmCommand,['info','--json',installName],function(err,stdout,stderr) { child_process.execFile(npmCommand,['info','--json',installName],{ shell: true },function(err,stdout,stderr) {
try { try {
if (!stdout) { if (!stdout) {
log.warn(log._("server.install.install-failed-not-found",{name:module})); log.warn(log._("server.install.install-failed-not-found",{name:module}));
@@ -525,7 +528,7 @@ function uninstallModule(module) {
let extraArgs = triggerPayload.args || []; let extraArgs = triggerPayload.args || [];
let args = ['remove', ...extraArgs, module] let args = ['remove', ...extraArgs, module]
log.trace(npmCommand + JSON.stringify(args)); log.trace(npmCommand + JSON.stringify(args));
return exec.run(npmCommand,args,{ cwd: installDir}, true) return exec.run(npmCommand,args,{ cwd: installDir, shell: true }, true)
} else { } else {
log.trace("skipping npm uninstall"); log.trace("skipping npm uninstall");
} }
@@ -592,7 +595,7 @@ async function checkPrereq() {
installerEnabled = false; installerEnabled = false;
} else { } else {
return new Promise(resolve => { return new Promise(resolve => {
child_process.execFile(npmCommand,['-v'],function(err,stdout) { child_process.execFile(npmCommand,['-v'],{ shell: true },function(err,stdout) {
if (err) { if (err) {
log.info(log._("server.palette-editor.npm-not-found")); log.info(log._("server.palette-editor.npm-not-found"));
installerEnabled = false; installerEnabled = false;

View File

@@ -88,7 +88,7 @@ function generateSubflowConfig(subflow) {
this.credentials['has_' + prop.name] = (this.credentials[prop.name] !== ""); this.credentials['has_' + prop.name] = (this.credentials[prop.name] !== "");
} else { } else {
switch(prop.type) { switch(prop.type) {
case "str": this[prop.name] = prop.value||""; break; case "str": case "conf-type": this[prop.name] = prop.value||""; break;
case "bool": this[prop.name] = (typeof prop.value === 'boolean')?prop.value:prop.value === "true" ; break; case "bool": this[prop.name] = (typeof prop.value === 'boolean')?prop.value:prop.value === "true" ; break;
case "num": this[prop.name] = (typeof prop.value === 'number')?prop.value:Number(prop.value); break; case "num": this[prop.name] = (typeof prop.value === 'number')?prop.value:Number(prop.value); break;
default: default:

View File

@@ -20,7 +20,7 @@
"clone": "2.1.2", "clone": "2.1.2",
"fs-extra": "11.1.1", "fs-extra": "11.1.1",
"semver": "7.5.4", "semver": "7.5.4",
"tar": "6.1.13", "tar": "6.2.1",
"uglify-js": "3.17.4" "uglify-js": "3.17.4"
} }
} }

View File

@@ -15,6 +15,7 @@
**/ **/
const clone = require("clone"); const clone = require("clone");
const jsonClone = require("rfdc")();
const Flow = require('./Flow').Flow; const Flow = require('./Flow').Flow;
const context = require('../nodes/context'); const context = require('../nodes/context');
const util = require("util"); const util = require("util");
@@ -108,7 +109,7 @@ class Subflow extends Flow {
} }
} }
subflowInternalFlowConfig.subflows = clone(subflowDef.subflows || {}); subflowInternalFlowConfig.subflows = jsonClone(subflowDef.subflows || {});
remapSubflowNodes(subflowInternalFlowConfig.configs,node_map); remapSubflowNodes(subflowInternalFlowConfig.configs,node_map);
remapSubflowNodes(subflowInternalFlowConfig.nodes,node_map); remapSubflowNodes(subflowInternalFlowConfig.nodes,node_map);
@@ -220,7 +221,7 @@ class Subflow extends Flow {
} }
if (this.subflowDef.in) { if (this.subflowDef.in) {
subflowInstanceConfig.wires = this.subflowDef.in.map(function(n) { return n.wires.map(function(w) { return self.node_map[w.id].id;})}) subflowInstanceConfig.wires = this.subflowDef.in.map(function(n) { return n.wires.map(function(w) { return self.node_map[w.id].id;})})
subflowInstanceConfig._originalWires = clone(subflowInstanceConfig.wires); subflowInstanceConfig._originalWires = jsonClone(subflowInstanceConfig.wires);
} }
this.node = new Node(subflowInstanceConfig); this.node = new Node(subflowInstanceConfig);
@@ -244,14 +245,14 @@ class Subflow extends Flow {
if (self.subflowDef.out) { if (self.subflowDef.out) {
var node,wires,i,j; var node,wires,i,j;
// Restore the original wiring to the internal nodes // Restore the original wiring to the internal nodes
subflowInstanceConfig.wires = clone(subflowInstanceConfig._originalWires); subflowInstanceConfig.wires = jsonClone(subflowInstanceConfig._originalWires);
for (i=0;i<self.subflowDef.out.length;i++) { for (i=0;i<self.subflowDef.out.length;i++) {
wires = self.subflowDef.out[i].wires; wires = self.subflowDef.out[i].wires;
for (j=0;j<wires.length;j++) { for (j=0;j<wires.length;j++) {
if (wires[j].id != self.subflowDef.id) { if (wires[j].id != self.subflowDef.id) {
node = self.node_map[wires[j].id]; node = self.node_map[wires[j].id];
if (node && node._originalWires) { if (node && node._originalWires) {
node.wires = clone(node._originalWires); node.wires = jsonClone(node._originalWires);
} }
} }
} }
@@ -300,7 +301,7 @@ class Subflow extends Flow {
var node = self.node_map[wires[j].id]; var node = self.node_map[wires[j].id];
if (node) { if (node) {
if (!node._originalWires) { if (!node._originalWires) {
node._originalWires = clone(node.wires); node._originalWires = jsonClone(node.wires);
} }
node.wires[wires[j].port] = (node.wires[wires[j].port]||[]).concat(this.subflowInstance.wires[i]); node.wires[wires[j].port] = (node.wires[wires[j].port]||[]).concat(this.subflowInstance.wires[i]);
} else { } else {
@@ -323,7 +324,7 @@ class Subflow extends Flow {
var node = self.node_map[wires[j].id]; var node = self.node_map[wires[j].id];
if (node) { if (node) {
if (!node._originalWires) { if (!node._originalWires) {
node._originalWires = clone(node.wires); node._originalWires = jsonClone(node.wires);
} }
node.wires[wires[j].port] = (node.wires[wires[j].port]||[]); node.wires[wires[j].port] = (node.wires[wires[j].port]||[]);
node.wires[wires[j].port].push(subflowStatusId); node.wires[wires[j].port].push(subflowStatusId);
@@ -463,7 +464,7 @@ class Subflow extends Flow {
* @return {[type]} [description] * @return {[type]} [description]
*/ */
function createNodeInSubflow(subflowInstanceId, def) { function createNodeInSubflow(subflowInstanceId, def) {
let node = clone(def); let node = jsonClone(def);
let nid = `${subflowInstanceId}-${node.id}` //redUtil.generateId(); let nid = `${subflowInstanceId}-${node.id}` //redUtil.generateId();
// console.log("Create Node In subflow",node._alias, "--->",nid, "(",node.type,")") // console.log("Create Node In subflow",node._alias, "--->",nid, "(",node.type,")")
// node_map[node.id] = node; // node_map[node.id] = node;

View File

@@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
**/ **/
var clone = require("clone"); const jsonClone = require("rfdc")();
var Flow = require('./Flow'); var Flow = require('./Flow');
@@ -140,16 +140,16 @@ function setFlows(_config,_credentials,type,muteLog,forceStart,user) {
if (type === "load") { if (type === "load") {
isLoad = true; isLoad = true;
configSavePromise = loadFlows().then(function(_config) { configSavePromise = loadFlows().then(function(_config) {
config = clone(_config.flows); config = jsonClone(_config.flows);
newFlowConfig = flowUtil.parseConfig(clone(config)); newFlowConfig = flowUtil.parseConfig(jsonClone(config));
type = "full"; type = "full";
return _config.rev; return _config.rev;
}); });
} else { } else {
// Clone the provided config so it can be manipulated // Clone the provided config so it can be manipulated
config = clone(_config); config = jsonClone(_config);
// Parse the configuration // Parse the configuration
newFlowConfig = flowUtil.parseConfig(clone(config)); newFlowConfig = flowUtil.parseConfig(jsonClone(config));
// Generate a diff to identify what has changed // Generate a diff to identify what has changed
diff = flowUtil.diffConfigs(activeFlowConfig,newFlowConfig); diff = flowUtil.diffConfigs(activeFlowConfig,newFlowConfig);
@@ -609,7 +609,7 @@ async function addFlow(flow, user) {
nodes.push(node); nodes.push(node);
} }
} }
var newConfig = clone(activeConfig.flows); var newConfig = jsonClone(activeConfig.flows);
newConfig = newConfig.concat(nodes); newConfig = newConfig.concat(nodes);
return setFlows(newConfig, null, 'flows', true, null, user).then(function() { return setFlows(newConfig, null, 'flows', true, null, user).then(function() {
@@ -650,7 +650,7 @@ function getFlow(id) {
var nodeIds = Object.keys(flow.nodes); var nodeIds = Object.keys(flow.nodes);
if (nodeIds.length > 0) { if (nodeIds.length > 0) {
result.nodes = nodeIds.map(function(nodeId) { result.nodes = nodeIds.map(function(nodeId) {
var node = clone(flow.nodes[nodeId]); var node = jsonClone(flow.nodes[nodeId]);
if (node.type === 'link out') { if (node.type === 'link out') {
delete node.wires; delete node.wires;
} }
@@ -662,7 +662,7 @@ function getFlow(id) {
if (flow.configs) { if (flow.configs) {
var configIds = Object.keys(flow.configs); var configIds = Object.keys(flow.configs);
result.configs = configIds.map(function(configId) { result.configs = configIds.map(function(configId) {
const node = clone(flow.configs[configId]); const node = jsonClone(flow.configs[configId]);
delete node.credentials; delete node.credentials;
return node return node
@@ -674,17 +674,17 @@ function getFlow(id) {
if (flow.subflows) { if (flow.subflows) {
var subflowIds = Object.keys(flow.subflows); var subflowIds = Object.keys(flow.subflows);
result.subflows = subflowIds.map(function(subflowId) { result.subflows = subflowIds.map(function(subflowId) {
var subflow = clone(flow.subflows[subflowId]); var subflow = jsonClone(flow.subflows[subflowId]);
var nodeIds = Object.keys(subflow.nodes); var nodeIds = Object.keys(subflow.nodes);
subflow.nodes = nodeIds.map(function(id) { subflow.nodes = nodeIds.map(function(id) {
const node = clone(subflow.nodes[id]) const node = jsonClone(subflow.nodes[id])
delete node.credentials delete node.credentials
return node return node
}); });
if (subflow.configs) { if (subflow.configs) {
var configIds = Object.keys(subflow.configs); var configIds = Object.keys(subflow.configs);
subflow.configs = configIds.map(function(id) { subflow.configs = configIds.map(function(id) {
const node = clone(subflow.configs[id]) const node = jsonClone(subflow.configs[id])
delete node.credentials delete node.credentials
return node return node
}) })
@@ -709,7 +709,7 @@ async function updateFlow(id,newFlow, user) {
} }
label = activeFlowConfig.flows[id].label; label = activeFlowConfig.flows[id].label;
} }
var newConfig = clone(activeConfig.flows); var newConfig = jsonClone(activeConfig.flows);
var nodes; var nodes;
if (id === 'global') { if (id === 'global') {
@@ -779,7 +779,7 @@ async function removeFlow(id, user) {
throw e; throw e;
} }
var newConfig = clone(activeConfig.flows); var newConfig = jsonClone(activeConfig.flows);
newConfig = newConfig.filter(function(node) { newConfig = newConfig.filter(function(node) {
return node.z !== id && node.id !== id; return node.z !== id && node.id !== id;
}); });

View File

@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
**/ **/
const clone = require("clone"); const jsonClone = require("rfdc")();
const redUtil = require("@node-red/util").util; const redUtil = require("@node-red/util").util;
const Log = require("@node-red/util").log; const Log = require("@node-red/util").log;
const typeRegistry = require("@node-red/registry"); const typeRegistry = require("@node-red/registry");
@@ -68,7 +68,7 @@ function mapEnvVarProperties(obj,prop,flow,config) {
if (obj[prop][0] === "$" && (EnvVarPropertyRE_old.test(v) || EnvVarPropertyRE.test(v)) ) { if (obj[prop][0] === "$" && (EnvVarPropertyRE_old.test(v) || EnvVarPropertyRE.test(v)) ) {
const envVar = v.substring(2,v.length-1); const envVar = v.substring(2,v.length-1);
const r = redUtil.getSetting(config, envVar, flow); const r = redUtil.getSetting(config, envVar, flow);
if (r !== undefined && r !== '') { if (r !== undefined) {
obj[prop] = r obj[prop] = r
} }
} }
@@ -175,7 +175,7 @@ async function createNode(flow,config) {
try { try {
var nodeTypeConstructor = typeRegistry.get(type); var nodeTypeConstructor = typeRegistry.get(type);
if (typeof nodeTypeConstructor === "function") { if (typeof nodeTypeConstructor === "function") {
var conf = clone(config); var conf = jsonClone(config);
delete conf.credentials; delete conf.credentials;
try { try {
Object.defineProperty(conf,'_module', {value: typeRegistry.getNodeInfo(type), enumerable: false, writable: true }) Object.defineProperty(conf,'_module', {value: typeRegistry.getNodeInfo(type), enumerable: false, writable: true })
@@ -202,8 +202,8 @@ async function createNode(flow,config) {
var subflowInstanceConfig = subflowConfig.subflows[nodeTypeConstructor.subflow.id]; var subflowInstanceConfig = subflowConfig.subflows[nodeTypeConstructor.subflow.id];
delete subflowConfig.subflows[nodeTypeConstructor.subflow.id]; delete subflowConfig.subflows[nodeTypeConstructor.subflow.id];
subflowInstanceConfig.subflows = subflowConfig.subflows; subflowInstanceConfig.subflows = subflowConfig.subflows;
var instanceConfig = clone(config); var instanceConfig = jsonClone(config);
instanceConfig.env = clone(nodeTypeConstructor.subflow.env); instanceConfig.env = jsonClone(nodeTypeConstructor.subflow.env);
instanceConfig.env = nodeTypeConstructor.subflow.env.map(nodeProp => { instanceConfig.env = nodeTypeConstructor.subflow.env.map(nodeProp => {
var nodePropType; var nodePropType;
@@ -256,7 +256,7 @@ function parseConfig(config) {
flow.missingTypes = []; flow.missingTypes = [];
config.forEach(function (n) { config.forEach(function (n) {
flow.allNodes[n.id] = clone(n); flow.allNodes[n.id] = jsonClone(n);
if (n.type === 'tab') { if (n.type === 'tab') {
flow.flows[n.id] = n; flow.flows[n.id] = n;
flow.flows[n.id].subflows = {}; flow.flows[n.id].subflows = {};

View File

@@ -69,7 +69,7 @@ module.exports = {
// Send init info to new connection // Send init info to new connection
const initPacket = { const initPacket = {
topic: "multiplayer/init", topic: "multiplayer/init",
data: getSessionsList(), data: { sessions: getSessionsList() },
session: opts.session session: opts.session
} }
// console.log('<<', initPacket) // console.log('<<', initPacket)

View File

@@ -14,9 +14,8 @@
* limitations under the License. * limitations under the License.
**/ **/
var path = require("path");
var fs = require("fs"); const jsonClone = require("rfdc")();
var clone = require("clone");
var util = require("util"); var util = require("util");
var registry = require("@node-red/registry"); var registry = require("@node-red/registry");
@@ -98,7 +97,7 @@ function createNode(node,def) {
} }
var creds = credentials.get(id); var creds = credentials.get(id);
if (creds) { if (creds) {
creds = clone(creds); creds = jsonClone(creds);
//console.log("Attaching credentials to ",node.id); //console.log("Attaching credentials to ",node.id);
// allow $(foo) syntax to substitute env variables for credentials also... // allow $(foo) syntax to substitute env variables for credentials also...
for (var p in creds) { for (var p in creds) {

View File

@@ -242,7 +242,9 @@ function loadProject(name) {
function getProject(user, name) { function getProject(user, name) {
checkActiveProject(name); checkActiveProject(name);
return Promise.resolve(activeProject.export()); return loadProject(name).then(function () {
return Promise.resolve(activeProject.export());
});
} }
function deleteProject(user, name) { function deleteProject(user, name) {

View File

@@ -20,10 +20,12 @@
"errors-help": "Exécuter avec -v pour plus de détails", "errors-help": "Exécuter avec -v pour plus de détails",
"missing-modules": "Modules de noeud manquants :", "missing-modules": "Modules de noeud manquants :",
"node-version-mismatch": "Le module de noeud ne peut pas être chargé sur cette version. Nécessite : __version__ ", "node-version-mismatch": "Le module de noeud ne peut pas être chargé sur cette version. Nécessite : __version__ ",
"set-has-no-types": "L'ensemble n'a aucun type. Nom : '__name__', module : '__module__', fichier : '__file__'",
"type-already-registered": "'__type__' déjà enregistré par le module __module__", "type-already-registered": "'__type__' déjà enregistré par le module __module__",
"removing-modules": "Suppression de modules de la configuration", "removing-modules": "Suppression de modules de la configuration",
"added-types": "Types de noeuds ajoutés :", "added-types": "Types de noeuds ajoutés :",
"removed-types": "Types de noeuds supprimés :", "removed-types": "Types de noeuds supprimés :",
"removed-plugins": "Plugins supprimés :",
"install": { "install": {
"invalid": "Nom de module invalide", "invalid": "Nom de module invalide",
"installing": "Installation du module : __name__, version : __version__", "installing": "Installation du module : __name__, version : __version__",
@@ -134,7 +136,8 @@
"flow": { "flow": {
"unknown-type": "Type inconnu : __type__", "unknown-type": "Type inconnu : __type__",
"missing-types": "Types manquants", "missing-types": "Types manquants",
"error-loop": "Le message a dépassé le nombre maximum de captures (catches)" "error-loop": "Le message a dépassé le nombre maximum de captures (catches)",
"non-message-returned": "Le noeud a tenté d'envoyer un message du type __type__"
}, },
"index": { "index": {
"unrecognised-id": "Identifiant non reconnu : __id__", "unrecognised-id": "Identifiant non reconnu : __id__",

View File

@@ -25,6 +25,7 @@
"removing-modules": "設定からモジュールを削除します", "removing-modules": "設定からモジュールを削除します",
"added-types": "追加したノード:", "added-types": "追加したノード:",
"removed-types": "削除したノード:", "removed-types": "削除したノード:",
"removed-plugins": "削除したプラグイン:",
"install": { "install": {
"invalid": "不正なモジュール名", "invalid": "不正なモジュール名",
"installing": "モジュール __name__, バージョン: __version__ をインストールします", "installing": "モジュール __name__, バージョン: __version__ をインストールします",

View File

@@ -22,6 +22,7 @@
"clone": "2.1.2", "clone": "2.1.2",
"express": "4.19.2", "express": "4.19.2",
"fs-extra": "11.1.1", "fs-extra": "11.1.1",
"json-stringify-safe": "5.0.1" "json-stringify-safe": "5.0.1",
"rfdc": "^1.3.1"
} }
} }