From fd9da9da7bf1cc5a0e56785ee5d565c6e55321eb Mon Sep 17 00:00:00 2001 From: Erfan Amiri Date: Fri, 22 May 2026 00:07:37 -0400 Subject: [PATCH 1/9] feat: begin implementation for #10515 From d94bb385c40d68fbdc897abd57122750b97a66bd Mon Sep 17 00:00:00 2001 From: Erfan Amiri Date: Fri, 22 May 2026 00:10:09 -0400 Subject: [PATCH 2/9] feat: begin implementation for #10515 From f208941e256a340cc894ea1c68dcc7e79174f6f4 Mon Sep 17 00:00:00 2001 From: Erfan Amiri Date: Fri, 22 May 2026 09:27:13 -0400 Subject: [PATCH 3/9] feat: begin implementation for #10515 From 8a72d1df02a6cdcbf4aea74455f23f78f624bfed Mon Sep 17 00:00:00 2001 From: Erfan Amiri Date: Fri, 22 May 2026 09:29:28 -0400 Subject: [PATCH 4/9] feat: add SaveContext.AutoSave enum value (DYN-10515) --- src/DynamoCore/Graph/ModelBase.cs | 2 +- src/DynamoCore/PublicAPI.Unshipped.txt | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/DynamoCore/Graph/ModelBase.cs b/src/DynamoCore/Graph/ModelBase.cs index eec9bb7202a..c8f21e943b8 100644 --- a/src/DynamoCore/Graph/ModelBase.cs +++ b/src/DynamoCore/Graph/ModelBase.cs @@ -13,7 +13,7 @@ namespace Dynamo.Graph /// /// SaveContext represents several contexts, in which node can be serialized/deserialized. /// - public enum SaveContext { [Obsolete("Use Save or SaveAs, instead of File")] File, Copy, Undo, Preset, None, Save, SaveAs }; + public enum SaveContext { [Obsolete("Use Save or SaveAs, instead of File")] File, Copy, Undo, Preset, None, Save, SaveAs, AutoSave }; /// /// This class encapsulates the input parameters that need to be passed into nodes diff --git a/src/DynamoCore/PublicAPI.Unshipped.txt b/src/DynamoCore/PublicAPI.Unshipped.txt index fd19c94b62b..e1243d5986c 100644 --- a/src/DynamoCore/PublicAPI.Unshipped.txt +++ b/src/DynamoCore/PublicAPI.Unshipped.txt @@ -4,3 +4,4 @@ Dynamo.Graph.Nodes.IValueSchemaProvider.ValueTypeId.get -> string Dynamo.Models.DynamoModel.DefaultStartConfiguration.EnableUnTrustedLocationsNotifications.get -> bool Dynamo.Models.DynamoModel.DefaultStartConfiguration.EnableUnTrustedLocationsNotifications.set -> void Dynamo.Models.DynamoModel.IStartConfiguration.EnableUnTrustedLocationsNotifications.get -> bool +Dynamo.Graph.SaveContext.AutoSave = 7 -> Dynamo.Graph.SaveContext From d0f5f140a164fb10d2dd6191de855c83702101e5 Mon Sep 17 00:00:00 2001 From: Erfan Amiri Date: Fri, 22 May 2026 09:31:07 -0400 Subject: [PATCH 5/9] feat: add EnableAutoSave preference setting (DYN-10515) Add EnableAutoSave property to PreferenceSettings with default false, backing field, and PublicAPI entries. Co-Authored-By: Claude Opus 4.7 --- .../Configuration/PreferenceSettings.cs | 19 +++++++++++++++++++ src/DynamoCore/PublicAPI.Unshipped.txt | 2 ++ 2 files changed, 21 insertions(+) diff --git a/src/DynamoCore/Configuration/PreferenceSettings.cs b/src/DynamoCore/Configuration/PreferenceSettings.cs index 08fe38e99c2..e9cd35e5334 100644 --- a/src/DynamoCore/Configuration/PreferenceSettings.cs +++ b/src/DynamoCore/Configuration/PreferenceSettings.cs @@ -73,6 +73,7 @@ private readonly static Lazy private double defaultScaleFactor; private bool disableTrustWarnings = false; private bool isNotificationCenterEnabled; + private bool isAutoSaveEnabled; private bool isEnablePersistExtensionsEnabled; private bool isAutoSyncDocumentBrowser = true; private bool isStaticSplashScreenEnabled; @@ -778,6 +779,23 @@ public bool EnableNotificationCenter } } + /// + /// This defines if user wants to enable AutoSave: automatically save edited graphs + /// to their original file on disk after a period of inactivity. The default value is false. + /// + public bool EnableAutoSave + { + get + { + return isAutoSaveEnabled; + } + set + { + isAutoSaveEnabled = value; + RaisePropertyChanged(nameof(EnableAutoSave)); + } + } + /// /// This defines if user wants the Extensions settings to persist across sessions. /// @@ -1099,6 +1117,7 @@ public PreferenceSettings() MLRecommendationNumberOfResults = 10; HideAutocompleteMethodOptions = false; EnableNotificationCenter = true; + EnableAutoSave = false; isStaticSplashScreenEnabled = true; isTimeStampIncludedInExportFilePath = true; DefaultPythonEngine = string.Empty; diff --git a/src/DynamoCore/PublicAPI.Unshipped.txt b/src/DynamoCore/PublicAPI.Unshipped.txt index e1243d5986c..cb39c80054c 100644 --- a/src/DynamoCore/PublicAPI.Unshipped.txt +++ b/src/DynamoCore/PublicAPI.Unshipped.txt @@ -5,3 +5,5 @@ Dynamo.Models.DynamoModel.DefaultStartConfiguration.EnableUnTrustedLocationsNoti Dynamo.Models.DynamoModel.DefaultStartConfiguration.EnableUnTrustedLocationsNotifications.set -> void Dynamo.Models.DynamoModel.IStartConfiguration.EnableUnTrustedLocationsNotifications.get -> bool Dynamo.Graph.SaveContext.AutoSave = 7 -> Dynamo.Graph.SaveContext +Dynamo.Configuration.PreferenceSettings.EnableAutoSave.get -> bool +Dynamo.Configuration.PreferenceSettings.EnableAutoSave.set -> void From ecf30db37ca3b8cf1672004967721046a56899e2 Mon Sep 17 00:00:00 2001 From: Erfan Amiri Date: Fri, 22 May 2026 09:36:53 -0400 Subject: [PATCH 6/9] feat: implement AutoSave debouncer in DynamoViewModel (DYN-10515) Subscribe to each workspace's PropertyChanged event and trigger a debounced save to the original file when HasUnsavedChanges becomes true, gated on PreferenceSettings.EnableAutoSave and a non-empty FileName. Per-workspace ActionDebouncer coalesces rapid edits into a single save after 30s of inactivity; saves route through the existing SaveAs(Guid, ...) entry point with SaveContext.AutoSave so extension listeners can distinguish AutoSave events. Debouncers are created on workspace add (and for the initial workspace in the constructor), cancelled/disposed on workspace remove, and all remaining debouncers are disposed during shutdown. --- .../ViewModels/Core/DynamoViewModel.cs | 118 ++++++++++++++++++ 1 file changed, 118 insertions(+) diff --git a/src/DynamoCoreWpf/ViewModels/Core/DynamoViewModel.cs b/src/DynamoCoreWpf/ViewModels/Core/DynamoViewModel.cs index 2fbe44f1901..3920dc10b57 100644 --- a/src/DynamoCoreWpf/ViewModels/Core/DynamoViewModel.cs +++ b/src/DynamoCoreWpf/ViewModels/Core/DynamoViewModel.cs @@ -75,6 +75,10 @@ public partial class DynamoViewModel : ViewModelBase, IDynamoViewModel private string dynamoMLDataPath = string.Empty; private const string dynamoMLDataFileName = "DynamoMLDataPipeline.json"; + private const int AutoSaveDebounceMs = 30_000; + private readonly Dictionary autoSaveDebouncers = new Dictionary(); + private readonly Dictionary autoSaveHandlers = new Dictionary(); + private bool onlineAccess = true; //2px tolerance range for node filtering during Home and End key press private readonly int tolerance = 2; @@ -878,6 +882,8 @@ protected DynamoViewModel(StartConfiguration startConfiguration) workspaces.Add(homespaceViewModel); currentWorkspaceViewModel = homespaceViewModel; + SubscribeAutoSaveForWorkspace(model.CurrentWorkspace); + model.WorkspaceAdded += WorkspaceAdded; model.WorkspaceRemoved += WorkspaceRemoved; if (model.LinterManager != null) @@ -1882,6 +1888,8 @@ internal void AddGroupToGroup(object hostGroupGuid) private void WorkspaceAdded(WorkspaceModel item) { + SubscribeAutoSaveForWorkspace(item); + if (item is HomeWorkspaceModel) { var newVm = new HomeWorkspaceViewModel(item as HomeWorkspaceModel, this); @@ -1915,6 +1923,8 @@ private void WorkspaceAdded(WorkspaceModel item) private void WorkspaceRemoved(WorkspaceModel item) { + UnsubscribeAutoSaveForWorkspace(item); + var viewModel = workspaces.First(x => x.Model == item); if (currentWorkspaceViewModel == viewModel) if(currentWorkspaceViewModel != null) @@ -1925,6 +1935,106 @@ private void WorkspaceRemoved(WorkspaceModel item) workspaces.Remove(viewModel); } + /// + /// Subscribes to the workspace's event so that + /// AutoSave can be triggered when HasUnsavedChanges becomes true. A per-workspace + /// coalesces rapid edits into a single save after a period of inactivity. + /// + private void SubscribeAutoSaveForWorkspace(WorkspaceModel workspace) + { + if (workspace == null || autoSaveHandlers.ContainsKey(workspace.Guid)) + { + return; + } + + var debouncer = new ActionDebouncer(Model.Logger); + autoSaveDebouncers[workspace.Guid] = debouncer; + + var workspaceGuid = workspace.Guid; + PropertyChangedEventHandler handler = (sender, e) => + { + if (e.PropertyName != nameof(WorkspaceModel.HasUnsavedChanges)) + { + return; + } + + if (sender is not WorkspaceModel ws) + { + return; + } + + if (!ws.HasUnsavedChanges) + { + return; + } + + if (string.IsNullOrEmpty(ws.FileName)) + { + return; + } + + if (!PreferenceSettings.EnableAutoSave) + { + return; + } + + debouncer.Debounce(AutoSaveDebounceMs, () => TriggerAutoSave(workspaceGuid)); + }; + + autoSaveHandlers[workspace.Guid] = handler; + workspace.PropertyChanged += handler; + } + + /// + /// Cancels and disposes the per-workspace AutoSave debouncer and detaches the property-changed handler. + /// Called when a workspace is removed so that no stale save fires after close. + /// + private void UnsubscribeAutoSaveForWorkspace(WorkspaceModel workspace) + { + if (workspace == null) + { + return; + } + + if (autoSaveHandlers.TryGetValue(workspace.Guid, out var handler)) + { + workspace.PropertyChanged -= handler; + autoSaveHandlers.Remove(workspace.Guid); + } + + if (autoSaveDebouncers.TryGetValue(workspace.Guid, out var debouncer)) + { + debouncer.Dispose(); + autoSaveDebouncers.Remove(workspace.Guid); + } + } + + /// + /// Resolves the workspace by GUID and writes it to its current + /// using . Re-reads FileName at trigger time so that a Save As + /// performed during the debounce window writes to the new path. + /// + private void TriggerAutoSave(Guid workspaceGuid) + { + var workspace = Model.Workspaces.FirstOrDefault(w => w.Guid == workspaceGuid); + if (workspace == null) + { + return; + } + + if (!PreferenceSettings.EnableAutoSave) + { + return; + } + + if (string.IsNullOrEmpty(workspace.FileName)) + { + return; + } + + SaveAs(workspaceGuid, workspace.FileName, isBackup: false, SaveContext.AutoSave); + } + private void OnRuleEvaluationResultsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) { RaisePropertyChanged(nameof(LinterIssuesCount)); @@ -4662,6 +4772,14 @@ public bool PerformShutdownSequence(ShutdownParams shutdownParams) { wsvm.Dispose(); } + + foreach (var debouncer in autoSaveDebouncers.Values) + { + debouncer.Dispose(); + } + autoSaveDebouncers.Clear(); + autoSaveHandlers.Clear(); + ToastManager?.CloseRealTimeInfoWindow(); model.ShutDown(shutdownParams.ShutdownHost); From 9624466029b0f41ba4b3929377b779e5d0dd18bc Mon Sep 17 00:00:00 2001 From: Erfan Amiri Date: Fri, 22 May 2026 09:42:14 -0400 Subject: [PATCH 7/9] feat: add AutoSave toggle to Preferences UI (DYN-10515) Wire EnableAutoSave preference to a ToggleButton in the Preferences > Backup expander via a new AutoSaveIsChecked property on PreferencesViewModel. Include the user-facing resource strings in the primary and en-US resx files so the build compiles; other locales follow in TASK 5. Co-Authored-By: Claude Opus 4.7 --- .../Properties/Resources.en-US.resx | 8 ++++++++ src/DynamoCoreWpf/Properties/Resources.resx | 8 ++++++++ src/DynamoCoreWpf/PublicAPI.Unshipped.txt | 2 ++ .../ViewModels/Menu/PreferencesViewModel.cs | 17 +++++++++++++++++ .../Views/Menu/PreferencesView.xaml | 17 +++++++++++++++++ 5 files changed, 52 insertions(+) diff --git a/src/DynamoCoreWpf/Properties/Resources.en-US.resx b/src/DynamoCoreWpf/Properties/Resources.en-US.resx index 5f45b505dd9..51e85eba9ce 100644 --- a/src/DynamoCoreWpf/Properties/Resources.en-US.resx +++ b/src/DynamoCoreWpf/Properties/Resources.en-US.resx @@ -3585,6 +3585,14 @@ You can manage this in Preferences -> Security. Receive notification Preferences | Features | Notification Center | Receive notification + + Enable AutoSave + Preferences | General | Backup | Enable AutoSave toggle label + + + Automatically save your graph to its original file after a period of inactivity. Only applies to graphs that have already been saved to disk. + Preferences | General | Backup | Enable AutoSave toggle tooltip + Notification Center Preferences | Features | Notification Center diff --git a/src/DynamoCoreWpf/Properties/Resources.resx b/src/DynamoCoreWpf/Properties/Resources.resx index 29ddf57ce0e..d2163f45f77 100644 --- a/src/DynamoCoreWpf/Properties/Resources.resx +++ b/src/DynamoCoreWpf/Properties/Resources.resx @@ -3579,6 +3579,14 @@ You can manage this in Preferences -> Security. Receive notification Preferences | Features | Notification Center | Receive notification + + Enable AutoSave + Preferences | General | Backup | Enable AutoSave toggle label + + + Automatically save your graph to its original file after a period of inactivity. Only applies to graphs that have already been saved to disk. + Preferences | General | Backup | Enable AutoSave toggle tooltip + Notification Center Preferences | Features | Notification Center diff --git a/src/DynamoCoreWpf/PublicAPI.Unshipped.txt b/src/DynamoCoreWpf/PublicAPI.Unshipped.txt index f4ba45d8860..452703db520 100644 --- a/src/DynamoCoreWpf/PublicAPI.Unshipped.txt +++ b/src/DynamoCoreWpf/PublicAPI.Unshipped.txt @@ -26,6 +26,8 @@ Dynamo.ViewModels.DynamoViewModel.PythonEngineUpgradeToastRequested -> System.Ac Dynamo.ViewModels.DynamoViewModel.ToastManager.get -> Dynamo.Wpf.UI.ToastManager Dynamo.ViewModels.DynamoViewModel.ToastManager.set -> void Dynamo.ViewModels.DynamoViewModel.ShowPythonEngineUpgradeCanvasToast(string message, bool stayOpen = true, string filePath = null) -> void +Dynamo.ViewModels.PreferencesViewModel.AutoSaveIsChecked.get -> bool +Dynamo.ViewModels.PreferencesViewModel.AutoSaveIsChecked.set -> void Dynamo.ViewModels.PreferencesViewModel.AutoSyncDocumentBrowserIsChecked.get -> bool Dynamo.ViewModels.PreferencesViewModel.AutoSyncDocumentBrowserIsChecked.set -> void Dynamo.ViewModels.PreferencesViewModel.ShowPythonAutoMigrationNotificationIsChecked.get -> bool diff --git a/src/DynamoCoreWpf/ViewModels/Menu/PreferencesViewModel.cs b/src/DynamoCoreWpf/ViewModels/Menu/PreferencesViewModel.cs index d5f50cac3b0..ff7656e42dc 100644 --- a/src/DynamoCoreWpf/ViewModels/Menu/PreferencesViewModel.cs +++ b/src/DynamoCoreWpf/ViewModels/Menu/PreferencesViewModel.cs @@ -1116,6 +1116,23 @@ public bool NotificationCenterIsChecked } } + /// + /// Controls the IsChecked property in the "Enable AutoSave" toggle button + /// in the Preferences > Backup section. + /// + public bool AutoSaveIsChecked + { + get + { + return preferenceSettings.EnableAutoSave; + } + set + { + preferenceSettings.EnableAutoSave = value; + RaisePropertyChanged(nameof(AutoSaveIsChecked)); + } + } + /// /// Controls the IsChecked property in the "Extensions" toggle button, to enable persisted extensions, that will remember /// extensions setting as per the last session. diff --git a/src/DynamoCoreWpf/Views/Menu/PreferencesView.xaml b/src/DynamoCoreWpf/Views/Menu/PreferencesView.xaml index 246c0329346..9096e166a7c 100644 --- a/src/DynamoCoreWpf/Views/Menu/PreferencesView.xaml +++ b/src/DynamoCoreWpf/Views/Menu/PreferencesView.xaml @@ -579,6 +579,23 @@ IsExpanded="{Binding PreferencesTabs[General].ExpanderActive, Converter={StaticResource ExpandersBindingConverter}, ConverterParameter=BackupSettingsExpander}" Header="{x:Static p:Resources.PreferencesViewGeneralSettingsBackup}"> + + + + + + + + From f9dba7bb39cf63cd1fd512e3eb2ee4f909c523c7 Mon Sep 17 00:00:00 2001 From: Erfan Amiri Date: Fri, 22 May 2026 09:45:49 -0400 Subject: [PATCH 8/9] feat: add AutoSave localization placeholders (DYN-10515) Add PreferencesViewEnableAutoSave and PreferencesViewAutoSaveTooltip entries to all 13 localized resx siblings as English placeholders. Translation team will localize in a follow-up pass. --- src/DynamoCoreWpf/Properties/Resources.cs-CZ.resx | 6 ++++++ src/DynamoCoreWpf/Properties/Resources.de-DE.resx | 6 ++++++ src/DynamoCoreWpf/Properties/Resources.en-GB.resx | 6 ++++++ src/DynamoCoreWpf/Properties/Resources.es-ES.resx | 6 ++++++ src/DynamoCoreWpf/Properties/Resources.fr-FR.resx | 6 ++++++ src/DynamoCoreWpf/Properties/Resources.it-IT.resx | 6 ++++++ src/DynamoCoreWpf/Properties/Resources.ja-JP.resx | 6 ++++++ src/DynamoCoreWpf/Properties/Resources.ko-KR.resx | 6 ++++++ src/DynamoCoreWpf/Properties/Resources.pl-PL.resx | 6 ++++++ src/DynamoCoreWpf/Properties/Resources.pt-BR.resx | 6 ++++++ src/DynamoCoreWpf/Properties/Resources.ru-RU.resx | 6 ++++++ src/DynamoCoreWpf/Properties/Resources.zh-CN.resx | 6 ++++++ src/DynamoCoreWpf/Properties/Resources.zh-TW.resx | 6 ++++++ 13 files changed, 78 insertions(+) diff --git a/src/DynamoCoreWpf/Properties/Resources.cs-CZ.resx b/src/DynamoCoreWpf/Properties/Resources.cs-CZ.resx index d252315885a..172ecbede75 100644 --- a/src/DynamoCoreWpf/Properties/Resources.cs-CZ.resx +++ b/src/DynamoCoreWpf/Properties/Resources.cs-CZ.resx @@ -3235,6 +3235,12 @@ Tato umístění můžete spravovat v části Předvolby -> Zabezpečení. Přijímat oznámení + + Enable AutoSave + + + Automatically save your graph to its original file after a period of inactivity. Only applies to graphs that have already been saved to disk. + Centrum upozornění diff --git a/src/DynamoCoreWpf/Properties/Resources.de-DE.resx b/src/DynamoCoreWpf/Properties/Resources.de-DE.resx index 55b2b9be18f..222e1679b03 100644 --- a/src/DynamoCoreWpf/Properties/Resources.de-DE.resx +++ b/src/DynamoCoreWpf/Properties/Resources.de-DE.resx @@ -3234,6 +3234,12 @@ Sie können dies unter Voreinstellungen -> Sicherheit verwalten. Benachrichtigungen erhalten + + Enable AutoSave + + + Automatically save your graph to its original file after a period of inactivity. Only applies to graphs that have already been saved to disk. + Benachrichtigungscenter diff --git a/src/DynamoCoreWpf/Properties/Resources.en-GB.resx b/src/DynamoCoreWpf/Properties/Resources.en-GB.resx index f7dc64a70df..187231fcd1f 100644 --- a/src/DynamoCoreWpf/Properties/Resources.en-GB.resx +++ b/src/DynamoCoreWpf/Properties/Resources.en-GB.resx @@ -3236,6 +3236,12 @@ You can manage this in Preferences -> Security. Receive notification + + Enable AutoSave + + + Automatically save your graph to its original file after a period of inactivity. Only applies to graphs that have already been saved to disk. + Notification Center diff --git a/src/DynamoCoreWpf/Properties/Resources.es-ES.resx b/src/DynamoCoreWpf/Properties/Resources.es-ES.resx index 3a2e87bf8e2..14d0222bf71 100644 --- a/src/DynamoCoreWpf/Properties/Resources.es-ES.resx +++ b/src/DynamoCoreWpf/Properties/Resources.es-ES.resx @@ -3236,6 +3236,12 @@ Puede administrar esto en Preferencias - > Seguridad. Recibir notificación + + Enable AutoSave + + + Automatically save your graph to its original file after a period of inactivity. Only applies to graphs that have already been saved to disk. + Centro de notificaciones diff --git a/src/DynamoCoreWpf/Properties/Resources.fr-FR.resx b/src/DynamoCoreWpf/Properties/Resources.fr-FR.resx index 312ca03fa51..4994c18e6e5 100644 --- a/src/DynamoCoreWpf/Properties/Resources.fr-FR.resx +++ b/src/DynamoCoreWpf/Properties/Resources.fr-FR.resx @@ -3234,6 +3234,12 @@ Vous pouvez gérer ce paramètre dans Préférences -> Sécurité. Recevoir une notification + + Enable AutoSave + + + Automatically save your graph to its original file after a period of inactivity. Only applies to graphs that have already been saved to disk. + Centre de notification diff --git a/src/DynamoCoreWpf/Properties/Resources.it-IT.resx b/src/DynamoCoreWpf/Properties/Resources.it-IT.resx index cecf314a0c4..ff55907f74d 100644 --- a/src/DynamoCoreWpf/Properties/Resources.it-IT.resx +++ b/src/DynamoCoreWpf/Properties/Resources.it-IT.resx @@ -3218,6 +3218,12 @@ Provare a posizionare il nodo **ByOrigin** evidenziato. Ricevi notifica + + Enable AutoSave + + + Automatically save your graph to its original file after a period of inactivity. Only applies to graphs that have already been saved to disk. + Centro notifiche diff --git a/src/DynamoCoreWpf/Properties/Resources.ja-JP.resx b/src/DynamoCoreWpf/Properties/Resources.ja-JP.resx index 8196203d8cb..860acb69ff4 100644 --- a/src/DynamoCoreWpf/Properties/Resources.ja-JP.resx +++ b/src/DynamoCoreWpf/Properties/Resources.ja-JP.resx @@ -3236,6 +3236,12 @@ Dynamo を再起動してアンインストールを完了します。 通知を受信 + + Enable AutoSave + + + Automatically save your graph to its original file after a period of inactivity. Only applies to graphs that have already been saved to disk. + 通知センター diff --git a/src/DynamoCoreWpf/Properties/Resources.ko-KR.resx b/src/DynamoCoreWpf/Properties/Resources.ko-KR.resx index ace48776029..86a6cf36bae 100644 --- a/src/DynamoCoreWpf/Properties/Resources.ko-KR.resx +++ b/src/DynamoCoreWpf/Properties/Resources.ko-KR.resx @@ -3234,6 +3234,12 @@ 알림 수신 + + Enable AutoSave + + + Automatically save your graph to its original file after a period of inactivity. Only applies to graphs that have already been saved to disk. + 알림 센터 diff --git a/src/DynamoCoreWpf/Properties/Resources.pl-PL.resx b/src/DynamoCoreWpf/Properties/Resources.pl-PL.resx index 8013bc75664..da6fdde747f 100644 --- a/src/DynamoCoreWpf/Properties/Resources.pl-PL.resx +++ b/src/DynamoCoreWpf/Properties/Resources.pl-PL.resx @@ -3236,6 +3236,12 @@ Można tym zarządzać w obszarze Preferencje -> Zabezpieczenia. Odbierz powiadomienie + + Enable AutoSave + + + Automatically save your graph to its original file after a period of inactivity. Only applies to graphs that have already been saved to disk. + Centrum powiadomień diff --git a/src/DynamoCoreWpf/Properties/Resources.pt-BR.resx b/src/DynamoCoreWpf/Properties/Resources.pt-BR.resx index b53c3479607..c3391126128 100644 --- a/src/DynamoCoreWpf/Properties/Resources.pt-BR.resx +++ b/src/DynamoCoreWpf/Properties/Resources.pt-BR.resx @@ -3236,6 +3236,12 @@ Tente colocar o nó **ByOrigin** realçado. Receber notificações + + Enable AutoSave + + + Automatically save your graph to its original file after a period of inactivity. Only applies to graphs that have already been saved to disk. + Centro de notificações diff --git a/src/DynamoCoreWpf/Properties/Resources.ru-RU.resx b/src/DynamoCoreWpf/Properties/Resources.ru-RU.resx index 10175173a7c..e35bb27ac5a 100644 --- a/src/DynamoCoreWpf/Properties/Resources.ru-RU.resx +++ b/src/DynamoCoreWpf/Properties/Resources.ru-RU.resx @@ -3236,6 +3236,12 @@ Уведомление о получении + + Enable AutoSave + + + Automatically save your graph to its original file after a period of inactivity. Only applies to graphs that have already been saved to disk. + Центр уведомлений diff --git a/src/DynamoCoreWpf/Properties/Resources.zh-CN.resx b/src/DynamoCoreWpf/Properties/Resources.zh-CN.resx index abf5e3c849f..5f59cd43539 100644 --- a/src/DynamoCoreWpf/Properties/Resources.zh-CN.resx +++ b/src/DynamoCoreWpf/Properties/Resources.zh-CN.resx @@ -3234,6 +3234,12 @@ 接收通知 + + Enable AutoSave + + + Automatically save your graph to its original file after a period of inactivity. Only applies to graphs that have already been saved to disk. + 通知中心 diff --git a/src/DynamoCoreWpf/Properties/Resources.zh-TW.resx b/src/DynamoCoreWpf/Properties/Resources.zh-TW.resx index 8a24cbcc6bb..1daacdaa63d 100644 --- a/src/DynamoCoreWpf/Properties/Resources.zh-TW.resx +++ b/src/DynamoCoreWpf/Properties/Resources.zh-TW.resx @@ -3235,6 +3235,12 @@ 接收通知 + + Enable AutoSave + + + Automatically save your graph to its original file after a period of inactivity. Only applies to graphs that have already been saved to disk. + 通知中心 From fb81b6064092e48759849dacf43ca0f20607cd20 Mon Sep 17 00:00:00 2001 From: Erfan Amiri Date: Fri, 22 May 2026 09:53:33 -0400 Subject: [PATCH 9/9] test: add AutoSave tests for PreferenceSettings and WorkspaceSaving (DYN-10515) Adds NUnit coverage for: - EnableAutoSave default value (false) and XML round-trip - AutoSave fires for dirty saved workspaces with SaveContext.AutoSave - AutoSave is a no-op for untitled workspaces and when disabled - Debouncer is disposed when a workspace is removed Exposes the AutoSave debouncer dictionaries and TriggerAutoSave as internal to keep tests deterministic without the 30s debounce delay. Co-Authored-By: Claude Opus 4.7 --- .../ViewModels/Core/DynamoViewModel.cs | 8 +- .../Configuration/PreferenceSettingsTests.cs | 23 ++++ test/DynamoCoreWpf3Tests/WorkspaceSaving.cs | 130 ++++++++++++++++++ test/settings/DynamoSettings-NewSettings.xml | 1 + 4 files changed, 158 insertions(+), 4 deletions(-) diff --git a/src/DynamoCoreWpf/ViewModels/Core/DynamoViewModel.cs b/src/DynamoCoreWpf/ViewModels/Core/DynamoViewModel.cs index 3920dc10b57..35420f270c5 100644 --- a/src/DynamoCoreWpf/ViewModels/Core/DynamoViewModel.cs +++ b/src/DynamoCoreWpf/ViewModels/Core/DynamoViewModel.cs @@ -75,9 +75,9 @@ public partial class DynamoViewModel : ViewModelBase, IDynamoViewModel private string dynamoMLDataPath = string.Empty; private const string dynamoMLDataFileName = "DynamoMLDataPipeline.json"; - private const int AutoSaveDebounceMs = 30_000; - private readonly Dictionary autoSaveDebouncers = new Dictionary(); - private readonly Dictionary autoSaveHandlers = new Dictionary(); + internal const int AutoSaveDebounceMs = 30_000; + internal readonly Dictionary autoSaveDebouncers = new Dictionary(); + internal readonly Dictionary autoSaveHandlers = new Dictionary(); private bool onlineAccess = true; //2px tolerance range for node filtering during Home and End key press @@ -2014,7 +2014,7 @@ private void UnsubscribeAutoSaveForWorkspace(WorkspaceModel workspace) /// using . Re-reads FileName at trigger time so that a Save As /// performed during the debounce window writes to the new path. /// - private void TriggerAutoSave(Guid workspaceGuid) + internal void TriggerAutoSave(Guid workspaceGuid) { var workspace = Model.Workspaces.FirstOrDefault(w => w.Guid == workspaceGuid); if (workspace == null) diff --git a/test/DynamoCoreTests/Configuration/PreferenceSettingsTests.cs b/test/DynamoCoreTests/Configuration/PreferenceSettingsTests.cs index ab659c59255..b367e99df50 100644 --- a/test/DynamoCoreTests/Configuration/PreferenceSettingsTests.cs +++ b/test/DynamoCoreTests/Configuration/PreferenceSettingsTests.cs @@ -185,6 +185,29 @@ public void TestSettingsSerialization() Assert.AreEqual(settings.Locale, "zh-CN"); } + [Test] + [Category("UnitTests")] + public void WhenDefaultSettingsThenAutoSaveIsDisabled() + { + var settings = new PreferenceSettings(); + + Assert.IsFalse(settings.EnableAutoSave); + } + + [Test] + [Category("UnitTests")] + public void WhenSettingsSavedAndLoadedThenAutoSaveRoundTrips() + { + var tempPath = GetNewFileNameOnTempPath(".xml"); + + var settings = new PreferenceSettings { EnableAutoSave = true }; + settings.Save(tempPath); + + var loaded = PreferenceSettings.Load(tempPath); + + Assert.IsTrue(loaded.EnableAutoSave); + } + [Test] [Category("UnitTests")] public void TestMigrateStdLibTokenToBuiltInToken() diff --git a/test/DynamoCoreWpf3Tests/WorkspaceSaving.cs b/test/DynamoCoreWpf3Tests/WorkspaceSaving.cs index b40c45328bb..c086f72d6c0 100644 --- a/test/DynamoCoreWpf3Tests/WorkspaceSaving.cs +++ b/test/DynamoCoreWpf3Tests/WorkspaceSaving.cs @@ -1740,5 +1740,135 @@ public void WorkapceChecksumTest() Assert.AreEqual("65b395b9874b9d82e088093f30234c496704006030ecf35471404f62b62a6442", checksumString); } #endregion + + #region AutoSave + + [Test] + [Category("UnitTests")] + public void WhenAutoSaveEnabledAndWorkspaceIsDirtyAndHasFileNameThenSaveIsCalledAfterDebounce() + { + ViewModel.Model.PreferenceSettings.EnableAutoSave = true; + + var workspace = ViewModel.Model.CurrentWorkspace; + var savePath = GetNewFileNameOnTempPath("dyn"); + workspace.Save(savePath); + Assert.IsTrue(File.Exists(savePath)); + + var originalTimestamp = File.GetLastWriteTimeUtc(savePath); + // Make the on-disk timestamp distinct from the autosave write. + File.SetLastWriteTimeUtc(savePath, originalTimestamp.AddSeconds(-5)); + + workspace.HasUnsavedChanges = true; + Assert.IsTrue(workspace.HasUnsavedChanges); + + ViewModel.TriggerAutoSave(workspace.Guid); + + Assert.IsTrue(File.Exists(savePath)); + Assert.IsFalse(workspace.HasUnsavedChanges); + Assert.Greater(File.GetLastWriteTimeUtc(savePath), originalTimestamp.AddSeconds(-5)); + } + + [Test] + [Category("UnitTests")] + public void WhenAutoSaveEnabledAndWorkspaceHasNoFileNameThenSaveIsNotCalled() + { + ViewModel.Model.PreferenceSettings.EnableAutoSave = true; + + var workspace = ViewModel.Model.CurrentWorkspace; + Assert.IsTrue(string.IsNullOrEmpty(workspace.FileName)); + + var savingFired = false; + Action handler = _ => savingFired = true; + workspace.WorkspaceSaving += handler; + try + { + workspace.HasUnsavedChanges = true; + ViewModel.TriggerAutoSave(workspace.Guid); + + Assert.IsFalse(savingFired); + } + finally + { + workspace.WorkspaceSaving -= handler; + } + } + + [Test] + [Category("UnitTests")] + public void WhenAutoSaveEnabledAndSaveFiredThenSaveContextIsAutoSave() + { + ViewModel.Model.PreferenceSettings.EnableAutoSave = true; + + var workspace = ViewModel.Model.CurrentWorkspace; + var savePath = GetNewFileNameOnTempPath("dyn"); + workspace.Save(savePath); + + SaveContext? capturedContext = null; + Action handler = ctx => capturedContext = ctx; + workspace.WorkspaceSaving += handler; + try + { + workspace.HasUnsavedChanges = true; + ViewModel.TriggerAutoSave(workspace.Guid); + + Assert.IsTrue(capturedContext.HasValue); + Assert.AreEqual(SaveContext.AutoSave, capturedContext.Value); + } + finally + { + workspace.WorkspaceSaving -= handler; + } + } + + [Test] + [Category("UnitTests")] + public void WhenAutoSaveDisabledAndWorkspaceIsDirtyThenSaveIsNotCalled() + { + ViewModel.Model.PreferenceSettings.EnableAutoSave = false; + + var workspace = ViewModel.Model.CurrentWorkspace; + var savePath = GetNewFileNameOnTempPath("dyn"); + workspace.Save(savePath); + + workspace.HasUnsavedChanges = true; + + var savingFired = false; + Action handler = _ => savingFired = true; + workspace.WorkspaceSaving += handler; + try + { + ViewModel.TriggerAutoSave(workspace.Guid); + + Assert.IsFalse(savingFired); + Assert.IsTrue(workspace.HasUnsavedChanges); + } + finally + { + workspace.WorkspaceSaving -= handler; + } + } + + [Test] + [Category("UnitTests")] + public void WhenWorkspaceRemovedThenAutoSaveDebouncerIsDisposed() + { + var funcguid = GuidUtility.Create(GuidUtility.UrlNamespace, "AutoSaveDebouncerDisposedTest"); + ViewModel.ExecuteCommand(new DynamoModel.CreateCustomNodeCommand(funcguid, "AutoSaveNode", "Custom Nodes", "", true)); + ViewModel.FocusCustomNodeWorkspace(funcguid); + + var customNodeWorkspace = ViewModel.CurrentSpace; + Assert.IsTrue(customNodeWorkspace is CustomNodeWorkspaceModel); + var workspaceGuid = customNodeWorkspace.Guid; + + Assert.IsTrue(ViewModel.autoSaveDebouncers.ContainsKey(workspaceGuid)); + Assert.IsTrue(ViewModel.autoSaveHandlers.ContainsKey(workspaceGuid)); + + ViewModel.Model.RemoveWorkspace(customNodeWorkspace); + + Assert.IsFalse(ViewModel.autoSaveDebouncers.ContainsKey(workspaceGuid)); + Assert.IsFalse(ViewModel.autoSaveHandlers.ContainsKey(workspaceGuid)); + } + + #endregion } } diff --git a/test/settings/DynamoSettings-NewSettings.xml b/test/settings/DynamoSettings-NewSettings.xml index 6d0b1938a2e..32403650f4b 100644 --- a/test/settings/DynamoSettings-NewSettings.xml +++ b/test/settings/DynamoSettings-NewSettings.xml @@ -86,6 +86,7 @@ true false false + true false false false