@@ -1024,6 +1024,9 @@ RED.editor = (function() {
10241024 var removeInfoEditorOnClose = false ;
10251025 var skipInfoRefreshOnClose = false ;
10261026 var activeEditPanes = [ ] ;
1027+ var nodeModifiedExternally = false ;
1028+ var onNodeMultiplayerChange = null ;
1029+ var onRemoteChanges = null ;
10271030
10281031 editStack . push ( node ) ;
10291032 RED . view . state ( RED . state . EDITING ) ;
@@ -1114,95 +1117,112 @@ RED.editor = (function() {
11141117 text : RED . _ ( "common.label.done" ) ,
11151118 class : "primary" ,
11161119 click : function ( ) {
1117- var editState = {
1118- changes : { } ,
1119- changed : false ,
1120- outputMap : null
1121- }
1122- var wasDirty = RED . nodes . dirty ( ) ;
1123-
1124- handleEditSave ( editing_node , editState )
1125-
1126- activeEditPanes . forEach ( function ( pane ) {
1127- if ( pane . apply ) {
1128- pane . apply . call ( pane , editState ) ;
1120+ function doSave ( ) {
1121+ var editState = {
1122+ changes : { } ,
1123+ changed : false ,
1124+ outputMap : null
11291125 }
1130- } )
1126+ var wasDirty = RED . nodes . dirty ( ) ;
11311127
1132- var removedLinks = updateNodeProperties ( editing_node , editState . outputMap ) ;
1128+ handleEditSave ( editing_node , editState )
11331129
1134- if ( $ ( "#node-input-node-disabled" ) . prop ( 'checked' ) ) {
1135- if ( node . d !== true ) {
1136- editState . changes . d = node . d ;
1137- editState . changed = true ;
1138- node . d = true ;
1139- }
1140- } else {
1141- if ( node . d === true ) {
1142- editState . changes . d = node . d ;
1143- editState . changed = true ;
1144- delete node . d ;
1145- }
1146- }
1130+ activeEditPanes . forEach ( function ( pane ) {
1131+ if ( pane . apply ) {
1132+ pane . apply . call ( pane , editState ) ;
1133+ }
1134+ } )
11471135
1148- node . resize = true ;
1136+ var removedLinks = updateNodeProperties ( editing_node , editState . outputMap ) ;
11491137
1150- if ( editState . changed ) {
1151- var wasChanged = editing_node . changed ;
1152- editing_node . changed = true ;
1153- RED . nodes . dirty ( true ) ;
1138+ if ( $ ( "#node-input-node-disabled" ) . prop ( 'checked' ) ) {
1139+ if ( node . d !== true ) {
1140+ editState . changes . d = node . d ;
1141+ editState . changed = true ;
1142+ node . d = true ;
1143+ }
1144+ } else {
1145+ if ( node . d === true ) {
1146+ editState . changes . d = node . d ;
1147+ editState . changed = true ;
1148+ delete node . d ;
1149+ }
1150+ }
11541151
1155- var activeSubflow = RED . nodes . subflow ( RED . workspaces . active ( ) ) ;
1156- var subflowInstances = null ;
1157- if ( activeSubflow ) {
1158- subflowInstances = [ ] ;
1159- RED . nodes . eachNode ( function ( n ) {
1160- if ( n . type == "subflow:" + RED . workspaces . active ( ) ) {
1161- subflowInstances . push ( {
1162- id :n . id ,
1163- changed :n . changed
1164- } ) ;
1165- n . changed = true ;
1166- n . dirty = true ;
1167- updateNodeProperties ( n ) ;
1152+ node . resize = true ;
1153+
1154+ if ( editState . changed ) {
1155+ var wasChanged = editing_node . changed ;
1156+ editing_node . changed = true ;
1157+ RED . nodes . dirty ( true ) ;
1158+
1159+ var activeSubflow = RED . nodes . subflow ( RED . workspaces . active ( ) ) ;
1160+ var subflowInstances = null ;
1161+ if ( activeSubflow ) {
1162+ subflowInstances = [ ] ;
1163+ RED . nodes . eachNode ( function ( n ) {
1164+ if ( n . type == "subflow:" + RED . workspaces . active ( ) ) {
1165+ subflowInstances . push ( {
1166+ id :n . id ,
1167+ changed :n . changed
1168+ } ) ;
1169+ n . changed = true ;
1170+ n . dirty = true ;
1171+ updateNodeProperties ( n ) ;
1172+ }
1173+ } ) ;
1174+ }
1175+ let historyEvent = {
1176+ t :'edit' ,
1177+ node :editing_node ,
1178+ changes :editState . changes ,
1179+ links :removedLinks ,
1180+ dirty :wasDirty ,
1181+ changed :wasChanged
1182+ } ;
1183+ if ( editState . outputMap ) {
1184+ historyEvent . outputMap = editState . outputMap ;
1185+ }
1186+ if ( subflowInstances && subflowInstances . length ) {
1187+ historyEvent . subflow = {
1188+ instances :subflowInstances
11681189 }
1169- } ) ;
1170- }
1171- let historyEvent = {
1172- t :'edit' ,
1173- node :editing_node ,
1174- changes :editState . changes ,
1175- links :removedLinks ,
1176- dirty :wasDirty ,
1177- changed :wasChanged
1178- } ;
1179- if ( editState . outputMap ) {
1180- historyEvent . outputMap = editState . outputMap ;
1181- }
1182- if ( subflowInstances && subflowInstances . length ) {
1183- historyEvent . subflow = {
1184- instances :subflowInstances
11851190 }
1186- }
11871191
1188- if ( editState . history ) {
1189- historyEvent = {
1190- t : 'multi' ,
1191- events : [ historyEvent , ...editState . history ] ,
1192- dirty : wasDirty
1192+ if ( editState . history ) {
1193+ historyEvent = {
1194+ t : 'multi' ,
1195+ events : [ historyEvent , ...editState . history ] ,
1196+ dirty : wasDirty
1197+ }
11931198 }
1194- }
11951199
1196- RED . history . push ( historyEvent ) ;
1197- if ( RED . automerge && RED . automerge . isEnabled ( ) ) {
1198- RED . automerge . updateNode ( editing_node . id , RED . nodes . nodeToExportable ( editing_node ) ) ;
1200+ RED . history . push ( historyEvent ) ;
1201+ if ( RED . automerge && RED . automerge . isEnabled ( ) ) {
1202+ RED . automerge . updateNode ( editing_node . id , RED . nodes . nodeToExportable ( editing_node ) ) ;
1203+ }
11991204 }
1205+ editing_node . dirty = true ;
1206+ validateNode ( editing_node ) ;
1207+ RED . events . emit ( "editor:save" , editing_node ) ;
1208+ RED . events . emit ( "nodes:change" , editing_node ) ;
1209+ RED . tray . close ( ) ;
12001210 }
1201- editing_node . dirty = true ;
1202- validateNode ( editing_node ) ;
1203- RED . events . emit ( "editor:save" , editing_node ) ;
1204- RED . events . emit ( "nodes:change" , editing_node ) ;
1205- RED . tray . close ( ) ;
1211+ if ( nodeModifiedExternally ) {
1212+ var notification = RED . notify (
1213+ '<p><strong>This node was modified by another user while you were editing.</strong></p>' +
1214+ '<p>Saving will overwrite their changes.</p>' ,
1215+ {
1216+ modal : true , fixed : true , type : "warning" ,
1217+ buttons : [
1218+ { text : RED . _ ( "common.label.cancel" ) , click : function ( ) { notification . close ( ) ; } } ,
1219+ { text : "Save anyway" , class : "primary" , click : function ( ) { notification . close ( ) ; doSave ( ) ; } }
1220+ ]
1221+ }
1222+ ) ;
1223+ return ;
1224+ }
1225+ doSave ( ) ;
12061226 }
12071227 }
12081228 ] ,
@@ -1226,6 +1246,52 @@ RED.editor = (function() {
12261246 var trayBody = tray . find ( '.red-ui-tray-body' ) ;
12271247 trayBody . parent ( ) . css ( 'overflow' , 'hidden' ) ;
12281248
1249+ var multiplayerBanner = $ ( '<div class="red-ui-editor-multiplayer-banner" style="display:none">' +
1250+ '<i class="fa fa-users"></i> <span></span></div>' ) . prependTo ( trayBody . parent ( ) ) ;
1251+
1252+ nodeModifiedExternally = false ;
1253+
1254+ function updateMultiplayerBanner ( ) {
1255+ var users = editing_node . _multiplayer ?. users || [ ] ;
1256+ var mySessionId = RED . multiplayer . getActiveSessionId ( ) ;
1257+ var otherUsers = [ ] ;
1258+ for ( var i = 0 ; i < users . length ; i ++ ) {
1259+ if ( users [ i ] !== mySessionId ) {
1260+ var u = RED . multiplayer . getSessionUser ( users [ i ] ) ;
1261+ if ( u ) { otherUsers . push ( u . username || 'anonymous' ) ; }
1262+ }
1263+ }
1264+ if ( otherUsers . length > 0 || nodeModifiedExternally ) {
1265+ var msg = '' ;
1266+ if ( otherUsers . length === 1 ) {
1267+ msg = otherUsers [ 0 ] + ' is also editing this node' ;
1268+ } else if ( otherUsers . length > 1 ) {
1269+ msg = otherUsers [ 0 ] + ' and ' + ( otherUsers . length - 1 ) + ' other' + ( otherUsers . length - 1 > 1 ? 's are' : ' is' ) + ' also editing this node' ;
1270+ }
1271+ if ( nodeModifiedExternally ) {
1272+ if ( msg ) { msg += ' — ' ; }
1273+ msg += 'this node has been modified externally' ;
1274+ }
1275+ multiplayerBanner . find ( 'span' ) . text ( msg ) ;
1276+ multiplayerBanner . show ( ) ;
1277+ } else {
1278+ multiplayerBanner . hide ( ) ;
1279+ }
1280+ }
1281+
1282+ onNodeMultiplayerChange = function ( n ) {
1283+ if ( n . id === editing_node . id ) { updateMultiplayerBanner ( ) ; }
1284+ } ;
1285+ RED . events . on ( 'nodes:change' , onNodeMultiplayerChange ) ;
1286+
1287+ onRemoteChanges = function ( event ) {
1288+ if ( event . updated && event . updated . some ( function ( n ) { return n . id === editing_node . id ; } ) ) {
1289+ nodeModifiedExternally = true ;
1290+ updateMultiplayerBanner ( ) ;
1291+ }
1292+ } ;
1293+ RED . events . on ( 'automerge:remote-changes' , onRemoteChanges ) ;
1294+
12291295 var trayFooterLeft = $ ( '<div class="red-ui-tray-footer-left"></div>' ) . appendTo ( trayFooter )
12301296
12311297 var helpButton = $ ( '<button type="button" class="red-ui-button"><i class="fa fa-book"></button>' ) . on ( "click" , function ( evt ) {
@@ -1263,10 +1329,17 @@ RED.editor = (function() {
12631329 trayBody . i18n ( ) ;
12641330 trayFooter . i18n ( ) ;
12651331 buildingEditDialog = false ;
1332+ updateMultiplayerBanner ( ) ;
12661333 done ( ) ;
12671334 } ) ;
12681335 } ,
12691336 close : function ( ) {
1337+ if ( onNodeMultiplayerChange ) {
1338+ RED . events . off ( 'nodes:change' , onNodeMultiplayerChange ) ;
1339+ }
1340+ if ( onRemoteChanges ) {
1341+ RED . events . off ( 'automerge:remote-changes' , onRemoteChanges ) ;
1342+ }
12701343 if ( RED . view . state ( ) != RED . state . IMPORT_DRAGGING ) {
12711344 RED . view . state ( RED . state . DEFAULT ) ;
12721345 }
0 commit comments