Skip to content
Open
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
05e4210
Fix the Save/Save As menu items hardcoded IsEnabled="False" in XAML
edwin-vasquez-ucaldas Jul 28, 2026
302d8c9
Adding unit tests
edwin-vasquez-ucaldas Jul 29, 2026
9db01b0
Fix unit test
edwin-vasquez-ucaldas Jul 29, 2026
eb2fb46
call OnWorkspaceOpened/raise RequestEnableShortcutBarItems(true) fro…
edwin-vasquez-ucaldas Jul 29, 2026
acdb691
Sync Save/Save As enablement across menu, hotkey, and toolbar via sha…
edwin-vasquez-ucaldas Jul 30, 2026
91f0700
Correctly reverted — this file is now staged with exactly the inverse…
edwin-vasquez-ucaldas Jul 30, 2026
7d50b3e
Merge branch 'master' into DYN-10717_Fix_disabled_Save_and_Save_As_op…
RobertGlobant20 Jul 30, 2026
aed88c5
Fixing tests were failing. Add new test focus into the Guided Tour
edwin-vasquez-ucaldas Jul 31, 2026
b59c24b
Removed the duplicated isGuidedTourActive field and SetGuidedTourActi…
edwin-vasquez-ucaldas Aug 3, 2026
f93e6f3
Fixing unit tests. Stopped relying on ICommandSource's built-in coerc…
edwin-vasquez-ucaldas Aug 3, 2026
48702f4
Fix: Check for whether the model's position/size/any property actuall…
edwin-vasquez-ucaldas Aug 5, 2026
313ef50
Fix: passing each path segment separately, which resolves the analyze…
edwin-vasquez-ucaldas Aug 5, 2026
cd63a7b
Fix: raw stack depth conflated "position in undo history" with "conte…
edwin-vasquez-ucaldas Aug 5, 2026
f4ecaa5
Fix SonarQube analysis issues.
edwin-vasquez-ucaldas Aug 5, 2026
f886a97
Fix SonarQube analysis issues.
edwin-vasquez-ucaldas Aug 6, 2026
e2395b8
Apply minor fixes
edwin-vasquez-ucaldas Aug 6, 2026
936d742
Fix: UpdateHasUnsavedChangesFromSavedStateAffectingDepth() overwrites…
edwin-vasquez-ucaldas Aug 6, 2026
fc8f8d0
Apply all fixes for the independentDirtyFlag/MarkAsIndependentlyModif…
edwin-vasquez-ucaldas Aug 6, 2026
828a04a
Removing some comments
edwin-vasquez-ucaldas Aug 6, 2026
d648411
Merge remote-tracking branch 'origin/master' into DYN-10717_Fix_disab…
edwin-vasquez-ucaldas Aug 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/DynamoCore/Core/CustomNodeManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -984,7 +984,7 @@ private bool InitializeCustomNode(
newWorkspace.Category = workspaceInfo.Category;
// Mark the custom node workspace as having no changes - when we set the category on the above line
// this marks the workspace as changed.
newWorkspace.HasUnsavedChanges = false;
newWorkspace.MarkAsSaved();
}
}

Expand Down
57 changes: 55 additions & 2 deletions src/DynamoCore/Core/UndoRedoRecorder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,15 @@ internal interface IUndoRedoRecorderClient
/// Notifies the UI that the undo/redo state has changed so that undo/redo buttons can be enabled/disabled.
/// </summary>
void UpdateUndoRedoStack();

/// <summary>
/// UndoRedoRecorder calls this method right after recording a fresh user
/// modification (e.g. a node/note/group being moved or resized) so the
/// client can mark itself as having unsaved changes. Not called during
/// undo/redo replay itself, which goes through DeleteModel/CreateModel/
/// ReloadModel instead.
/// </summary>
void MarkAsModified();
}

internal class UndoRedoRecorder : LogSourceBase
Expand All @@ -75,6 +84,7 @@ public enum UserAction

private const string UserActionAttrib = "UserAction";
private const string ActionGroup = "ActionGroup";
private const string AffectsSavedStateAttrib = "AffectsSavedState";

private readonly IUndoRedoRecorderClient undoClient;
private readonly XmlDocument document = new XmlDocument();
Expand Down Expand Up @@ -201,6 +211,7 @@ public void RecordCreationForUndo(ModelBase model)
RecordActionInternal(currentActionGroup,
model, UserAction.Creation);

currentActionGroup.SetAttribute(AffectsSavedStateAttrib, bool.TrueString);
redoStack.Clear(); // Wipe out the redo-stack.
}

Expand All @@ -216,22 +227,35 @@ public void RecordDeletionForUndo(ModelBase model)
RecordActionInternal(currentActionGroup,
model, UserAction.Deletion);

currentActionGroup.SetAttribute(AffectsSavedStateAttrib, bool.TrueString);
redoStack.Clear(); // Wipe out the redo-stack.
}

/// <summary>
/// Record the given model right before it is modified. This results
/// in a modification action to be recorded under the current action
/// in a modification action to be recorded under the current action
/// group. Undoing this action will result in the model being reverted
/// to the states that it was in before the modification took place.
/// </summary>
/// <param name="model">The model to be recorded.</param>
public void RecordModificationForUndo(ModelBase model)
/// <param name="markAsModified">
/// Whether this recording represents a real content change that should mark the
/// workspace dirty (e.g. a node/note/group drag or resize). Pass false for recordings
/// that exist purely to make an incidental action (like a selection change) undoable,
/// since that state is never written to the saved file (DYN-10717).
/// </param>
public void RecordModificationForUndo(ModelBase model, bool markAsModified = true)
{
RecordActionInternal(currentActionGroup,
model, UserAction.Modification);

redoStack.Clear(); // Wipe out the redo-stack.

if (markAsModified)
{
currentActionGroup.SetAttribute(AffectsSavedStateAttrib, bool.TrueString);
undoClient.MarkAsModified();
}
}

/// <summary>
Expand Down Expand Up @@ -270,6 +294,22 @@ public XmlElement PopFromUndoGroup()
public bool CanUndo { get { return undoStack.Count > 0; } }
public bool CanRedo { get { return redoStack.Count > 0; } }

/// <summary>
/// The number of action groups currently on the undo stack that actually affect
/// what would be written to the saved file (i.e. were recorded via
/// RecordCreationForUndo/RecordDeletionForUndo, or RecordModificationForUndo with
/// markAsModified: true) -- action groups recorded purely to make an incidental,
/// non-persisted action undoable (e.g. a selection change) are excluded. Used by
/// the owning workspace to detect when Undo/Redo has returned to the exact content
/// position it was at when last saved, so it can correctly report having no unsaved
/// changes again (DYN-10717) instead of a one-way "dirty" flag that Undo never
/// clears, without being fooled by undoing/redoing a non-content action group.
/// </summary>
internal int SavedStateAffectingUndoDepth
{
get { return undoStack.Count(group => group.GetAttribute(AffectsSavedStateAttrib) == bool.TrueString); }
}

#endregion

#region Private Class Helper Methods
Expand Down Expand Up @@ -421,6 +461,13 @@ private void UndoActionGroup(XmlElement actionGroup)
}
}

// UndoActionGroup rebuilds a fresh XmlElement rather than reusing "actionGroup",
// so the AffectsSavedState tag must be explicitly carried over -- otherwise a
// later Redo of this same group would never be recognized as re-dirtying the
// workspace (DYN-10717).
if (actionGroup.GetAttribute(AffectsSavedStateAttrib) == bool.TrueString)
newGroup.SetAttribute(AffectsSavedStateAttrib, bool.TrueString);

redoStack.Push(newGroup); // Place the states on the redo-stack.
}

Expand Down Expand Up @@ -467,6 +514,12 @@ private void RedoActionGroup(XmlElement actionGroup)
}
}

// See the matching comment in UndoActionGroup -- the tag must be carried over
// for a subsequent Undo of this redone group to correctly clear the dirty
// flag again (DYN-10717).
if (actionGroup.GetAttribute(AffectsSavedStateAttrib) == bool.TrueString)
newGroup.SetAttribute(AffectsSavedStateAttrib, bool.TrueString);

undoStack.Push(newGroup);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ public CustomNodeWorkspaceModel(
{
Debug.WriteLine("Creating a custom node workspace...");

HasUnsavedChanges = false;
MarkAsSaved();

CustomNodeId = Guid.Parse(info.ID);
Category = info.Category;
Expand Down
63 changes: 61 additions & 2 deletions src/DynamoCore/Graph/Workspaces/HomeWorkspaceModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -512,6 +512,65 @@
base.DisposeNode(node);
}

/// <summary>
/// Computes the external file references by inspecting evaluated output values from
/// this workspace's running engine (only possible for a home workspace, not e.g. a
/// custom node workspace, hence this override).
/// </summary>
private protected override List<INodeLibraryDependencyInfo> ComputeExternalFileReferences()

Check failure on line 520 in src/DynamoCore/Graph/Workspaces/HomeWorkspaceModel.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this method to reduce its Cognitive Complexity from 31 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=DynamoDS_Dynamo&issues=AZ_T2uCh5aQ-hGRORkZ9&open=AZ_T2uCh5aQ-hGRORkZ9&pullRequest=17255
{
var externalFiles = new Dictionary<object, DependencyInfo>();

// If an execution is in progress we'll have to wait for it to be done before we can gather the
// external file references as this implementation relies on the output values of each node.
// instead just bail to avoid blocking the UI.
if (RunSettings.RunEnabled && !RunSettings.ForceBlockRun)
{
foreach (var node in Nodes)
{
externalFilesDictionary.TryGetValue(node.GUID, out var serializedDependencyInfo);

// Check for the file path string value at each of the output ports of all nodes in the workspace.
foreach (var port in node.OutPorts)
{
var id = node.GetAstIdentifierForOutputIndex(port.Index)?.Name;
var mirror = EngineController.GetMirror(id);
var data = mirror?.GetData().Data;

if (data is string dataString && dataString.Contains(@"\"))
{
// Check if the value exists on disk
DynamoUtilities.PathHelper.FileInfoAtPath(dataString, out bool fileExists, out string fileSize);
if (fileExists)
{
var externalFilePath = System.IO.Path.GetFullPath(dataString);
var externalFileName = System.IO.Path.GetFileName(dataString);

if (!externalFiles.ContainsKey(externalFilePath))
{
externalFiles[externalFilePath] = new DependencyInfo(externalFileName, dataString, ReferenceType.External);
}

externalFiles[externalFilePath].AddDependent(node.GUID);
externalFiles[externalFilePath].Size = fileSize;
}
// Read the serialized value for that node.
else if (serializedDependencyInfo != null && dataString.Contains(serializedDependencyInfo.Name))
{
if (!externalFiles.ContainsKey(serializedDependencyInfo.Name))
{
externalFiles[serializedDependencyInfo.Name] = new DependencyInfo(serializedDependencyInfo.Name, ReferenceType.External);
}
externalFiles[serializedDependencyInfo.Name].AddDependent(node.GUID);
}
}
}
Comment thread
edwin-vasquez-ucaldas marked this conversation as resolved.
Fixed
}
}

return externalFiles.Values.ToList<INodeLibraryDependencyInfo>();
}

/// <summary>
/// Called when the RequestSilenceNodeModifiedEvents event is emitted from a Node
/// </summary>
Expand Down Expand Up @@ -925,10 +984,10 @@
cbn.ProcessCodeDirect(cbn.RecompileCodeBlockAST);
}
// This method is intended to be called only during opening of an existing workspace
// and therefore if the workspace is set as dirty on account of CBN precompilation,
// and therefore if the workspace is set as dirty on account of CBN precompilation,
// the workspace should be reverted to a clean state as it's undesirable to have unsaved
// changes for a workspace that is newly opened.
HasUnsavedChanges = false;
MarkAsSaved();
}

internal bool TryGetMatchingWorkspaceData(string uniqueId, out Dictionary<string, string> data)
Expand Down
39 changes: 37 additions & 2 deletions src/DynamoCore/Graph/Workspaces/UndoRedo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ internal void Undo()
if (null != undoRecorder)
{
undoRecorder.Undo();
UpdateHasUnsavedChangesFromSavedStateAffectingDepth();

// http://adsk-oss.myjetbrains.com/youtrack/issue/MAGN-7883
// Request run for every undo action
Expand All @@ -72,6 +73,7 @@ internal void Redo()
if (null != undoRecorder)
{
undoRecorder.Redo();
UpdateHasUnsavedChangesFromSavedStateAffectingDepth();

// http://adsk-oss.myjetbrains.com/youtrack/issue/MAGN-7883
// Request run for every redo action
Expand All @@ -83,6 +85,25 @@ internal void ClearUndoRecorder()
{
if (null != undoRecorder)
undoRecorder.Clear();

savedUndoDepth = 0;
}

/// <summary>
/// Marks this workspace as having no unsaved changes, and remembers the current
/// position in the undo history so that Undo/Redo back to this exact point can
/// correctly report the workspace as clean again, rather than leaving Undo unable
/// to ever clear the one-way dirty flag (DYN-10717).
/// </summary>
internal void MarkAsSaved()
{
HasUnsavedChanges = false;
savedUndoDepth = undoRecorder?.SavedStateAffectingUndoDepth ?? 0;
}

private void UpdateHasUnsavedChangesFromSavedStateAffectingDepth()
{
HasUnsavedChanges = undoRecorder.SavedStateAffectingUndoDepth != savedUndoDepth;
}
Comment thread
edwin-vasquez-ucaldas marked this conversation as resolved.

// See RecordModelsForModification below for more details.
Expand All @@ -106,7 +127,12 @@ internal static void RecordModelForModification(ModelBase model, UndoRedoRecorde
/// </summary>
/// <param name="models">The models to be recorded for undo.</param>
/// <param name="recorder"></param>
internal static void RecordModelsForModification(List<ModelBase> models, UndoRedoRecorder recorder)
/// <param name="markAsModified">
/// Whether this recording represents a real content change that should mark the
/// workspace dirty. Pass false for recordings that exist purely to make an incidental
/// action (like a selection change) undoable (DYN-10717).
/// </param>
internal static void RecordModelsForModification(List<ModelBase> models, UndoRedoRecorder recorder, bool markAsModified = true)
{
if (null == recorder)
return;
Expand All @@ -116,7 +142,7 @@ internal static void RecordModelsForModification(List<ModelBase> models, UndoRed
using (recorder.BeginActionGroup())
{
foreach (var model in models)
recorder.RecordModificationForUndo(model);
recorder.RecordModificationForUndo(model, markAsModified);
}
}

Expand Down Expand Up @@ -852,6 +878,15 @@ public void UpdateUndoRedoStack()
{
RaisePropertyChanged("CanUndoRedoCommand");
}

// Explicit implementation (rather than matching the public/implicit style of
// the other IUndoRedoRecorderClient members above) so this stays an internal
// implementation detail of the undo/dirty-flag wiring, not new public API.
void IUndoRedoRecorderClient.MarkAsModified()
{
HasUnsavedChanges = true;
}

/// <summary>
/// Returns model by GUID
/// </summary>
Expand Down
Loading
Loading