feat(configmanager): wire up form selection in MainWindowViewModel
- Add IFileSystem and IDialogService dependencies to constructor - Implement OnSelectedNodeChanged to create appropriate form ViewModels - Add LoadConfigForTesting helper for unit testing - Add MarkAsChanged helper to track unsaved changes - Update OpenFolderAsync to use IDialogService - Add comprehensive unit tests for form selection
This commit is contained in:
@@ -3,6 +3,7 @@ using System.Windows.Input;
|
||||
using Avalonia.Media;
|
||||
using JdeScoping.ConfigManager.Models;
|
||||
using JdeScoping.ConfigManager.Services;
|
||||
using JdeScoping.ConfigManager.ViewModels.Forms;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace JdeScoping.ConfigManager.ViewModels;
|
||||
@@ -12,10 +13,12 @@ namespace JdeScoping.ConfigManager.ViewModels;
|
||||
/// </summary>
|
||||
public class MainWindowViewModel : ViewModelBase
|
||||
{
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly IConfigFileService _configFileService;
|
||||
private readonly IValidationService _validationService;
|
||||
private readonly IBackupService _backupService;
|
||||
private readonly IAutoDiscoveryService _autoDiscoveryService;
|
||||
private readonly IDialogService? _dialogService;
|
||||
private readonly ILogger<MainWindowViewModel>? _logger;
|
||||
|
||||
private string _configFolderPath = "No folder selected";
|
||||
@@ -28,30 +31,45 @@ public class MainWindowViewModel : ViewModelBase
|
||||
private ConfigModel? _appSettings;
|
||||
private PipelinesConfigModel? _pipelines;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the currently loaded configuration folder path.
|
||||
/// </summary>
|
||||
public string ConfigFolderPath
|
||||
{
|
||||
get => _configFolderPath;
|
||||
set => SetProperty(ref _configFolderPath, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether there are unsaved configuration changes.
|
||||
/// </summary>
|
||||
public bool HasUnsavedChanges
|
||||
{
|
||||
get => _hasUnsavedChanges;
|
||||
set => SetProperty(ref _hasUnsavedChanges, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the validation status message displayed to the user.
|
||||
/// </summary>
|
||||
public string ValidationStatus
|
||||
{
|
||||
get => _validationStatus;
|
||||
set => SetProperty(ref _validationStatus, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the brush color for the validation status indicator.
|
||||
/// </summary>
|
||||
public IBrush ValidationStatusColor
|
||||
{
|
||||
get => _validationStatusColor;
|
||||
set => SetProperty(ref _validationStatusColor, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the currently selected tree node in the configuration tree view.
|
||||
/// </summary>
|
||||
public TreeNodeViewModel? SelectedNode
|
||||
{
|
||||
get => _selectedNode;
|
||||
@@ -62,33 +80,80 @@ public class MainWindowViewModel : ViewModelBase
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the view model for the form displayed when a node is selected.
|
||||
/// </summary>
|
||||
public object? SelectedFormViewModel
|
||||
{
|
||||
get => _selectedFormViewModel;
|
||||
set => SetProperty(ref _selectedFormViewModel, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the collection of tree nodes representing the configuration structure.
|
||||
/// </summary>
|
||||
public ObservableCollection<TreeNodeViewModel> TreeNodes { get; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets the command for opening a configuration folder.
|
||||
/// </summary>
|
||||
public ICommand OpenFolderCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the command for saving configuration changes.
|
||||
/// </summary>
|
||||
public ICommand SaveCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the command for exiting the application.
|
||||
/// </summary>
|
||||
public ICommand ExitCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the command for undoing the last configuration change.
|
||||
/// </summary>
|
||||
public ICommand UndoCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the command for redoing the last undone configuration change.
|
||||
/// </summary>
|
||||
public ICommand RedoCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the command for validating the current configuration.
|
||||
/// </summary>
|
||||
public ICommand ValidateCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the command for testing database connections.
|
||||
/// </summary>
|
||||
public ICommand TestConnectionCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MainWindowViewModel"/> class.
|
||||
/// </summary>
|
||||
/// <param name="fileSystem">File system abstraction for I/O operations.</param>
|
||||
/// <param name="configFileService">Service for loading and saving configuration files.</param>
|
||||
/// <param name="validationService">Service for validating configuration settings.</param>
|
||||
/// <param name="backupService">Service for creating configuration backups.</param>
|
||||
/// <param name="autoDiscoveryService">Service for discovering configuration folder locations.</param>
|
||||
/// <param name="dialogService">Service for showing platform dialogs.</param>
|
||||
/// <param name="logger">Optional logger for recording view model activities.</param>
|
||||
public MainWindowViewModel(
|
||||
IFileSystem fileSystem,
|
||||
IConfigFileService configFileService,
|
||||
IValidationService validationService,
|
||||
IBackupService backupService,
|
||||
IAutoDiscoveryService autoDiscoveryService,
|
||||
IDialogService? dialogService,
|
||||
ILogger<MainWindowViewModel>? logger)
|
||||
{
|
||||
_fileSystem = fileSystem;
|
||||
_configFileService = configFileService;
|
||||
_validationService = validationService;
|
||||
_backupService = backupService;
|
||||
_autoDiscoveryService = autoDiscoveryService;
|
||||
_dialogService = dialogService;
|
||||
_logger = logger;
|
||||
|
||||
OpenFolderCommand = new AsyncRelayCommand(OpenFolderAsync);
|
||||
@@ -106,10 +171,12 @@ public class MainWindowViewModel : ViewModelBase
|
||||
/// Design-time constructor for XAML previewer.
|
||||
/// </summary>
|
||||
public MainWindowViewModel() : this(
|
||||
new FileSystem(),
|
||||
new ConfigFileService(new FileSystem()),
|
||||
new ValidationService(),
|
||||
new BackupService(new FileSystem()),
|
||||
new AutoDiscoveryService(new FileSystem()),
|
||||
null,
|
||||
null)
|
||||
{
|
||||
}
|
||||
@@ -131,9 +198,17 @@ public class MainWindowViewModel : ViewModelBase
|
||||
/// </summary>
|
||||
private async Task OpenFolderAsync()
|
||||
{
|
||||
// TODO: Show folder picker dialog
|
||||
_logger?.LogInformation("Open folder requested");
|
||||
await Task.CompletedTask;
|
||||
if (_dialogService == null)
|
||||
{
|
||||
_logger?.LogWarning("Dialog service is not available");
|
||||
return;
|
||||
}
|
||||
|
||||
var folder = await _dialogService.ShowFolderPickerAsync("Select Configuration Folder");
|
||||
if (folder != null)
|
||||
{
|
||||
await LoadConfigAsync(folder);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -201,8 +276,48 @@ public class MainWindowViewModel : ViewModelBase
|
||||
/// </summary>
|
||||
private void OnSelectedNodeChanged()
|
||||
{
|
||||
// TODO: Load appropriate form ViewModel based on selected node
|
||||
SelectedFormViewModel = null;
|
||||
if (_selectedNode == null || _appSettings == null)
|
||||
{
|
||||
SelectedFormViewModel = null;
|
||||
return;
|
||||
}
|
||||
|
||||
SelectedFormViewModel = _selectedNode.SectionKey switch
|
||||
{
|
||||
"DataSync" => new DataSyncFormViewModel(_appSettings.DataSync, MarkAsChanged),
|
||||
"DataAccess" => new DataAccessFormViewModel(_appSettings.DataAccess, MarkAsChanged),
|
||||
"Auth" => new AuthFormViewModel(_appSettings.Auth, MarkAsChanged),
|
||||
"Ldap" => new LdapFormViewModel(_appSettings.Ldap, MarkAsChanged),
|
||||
"Search" => new SearchFormViewModel(_appSettings.Search, MarkAsChanged),
|
||||
"ExcelExport" => new ExcelExportFormViewModel(_appSettings.ExcelExport, MarkAsChanged),
|
||||
_ when _selectedNode.NodeType == TreeNodeType.Pipeline && _pipelines != null
|
||||
=> _pipelines.Pipelines.TryGetValue(_selectedNode.SectionKey!, out var pipeline)
|
||||
? new PipelineFormViewModel(_selectedNode.SectionKey!, pipeline, MarkAsChanged)
|
||||
: null,
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Marks the configuration as having unsaved changes.
|
||||
/// </summary>
|
||||
private void MarkAsChanged()
|
||||
{
|
||||
HasUnsavedChanges = true;
|
||||
if (_selectedNode != null)
|
||||
_selectedNode.IsModified = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads configuration for testing purposes.
|
||||
/// </summary>
|
||||
/// <param name="appSettings">The application settings configuration model.</param>
|
||||
/// <param name="pipelines">The pipelines configuration model.</param>
|
||||
public void LoadConfigForTesting(ConfigModel? appSettings, PipelinesConfigModel? pipelines)
|
||||
{
|
||||
_appSettings = appSettings;
|
||||
_pipelines = pipelines;
|
||||
BuildTreeNodes();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -0,0 +1,316 @@
|
||||
using JdeScoping.ConfigManager.Models;
|
||||
using JdeScoping.ConfigManager.Services;
|
||||
using JdeScoping.ConfigManager.ViewModels;
|
||||
using JdeScoping.ConfigManager.ViewModels.Forms;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NSubstitute;
|
||||
|
||||
namespace JdeScoping.ConfigManager.Tests.ViewModels;
|
||||
|
||||
public class MainWindowViewModelTests
|
||||
{
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly IConfigFileService _configFileService;
|
||||
private readonly IValidationService _validationService;
|
||||
private readonly IBackupService _backupService;
|
||||
private readonly IAutoDiscoveryService _autoDiscoveryService;
|
||||
private readonly IDialogService _dialogService;
|
||||
private readonly ILogger<MainWindowViewModel> _logger;
|
||||
|
||||
public MainWindowViewModelTests()
|
||||
{
|
||||
_fileSystem = Substitute.For<IFileSystem>();
|
||||
_configFileService = Substitute.For<IConfigFileService>();
|
||||
_validationService = Substitute.For<IValidationService>();
|
||||
_backupService = Substitute.For<IBackupService>();
|
||||
_autoDiscoveryService = Substitute.For<IAutoDiscoveryService>();
|
||||
_dialogService = Substitute.For<IDialogService>();
|
||||
_logger = Substitute.For<ILogger<MainWindowViewModel>>();
|
||||
|
||||
_validationService.ValidateAppSettings(Arg.Any<ConfigModel>())
|
||||
.Returns(new ValidationResult());
|
||||
_validationService.ValidatePipelines(Arg.Any<PipelinesConfigModel>())
|
||||
.Returns(new ValidationResult());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SelectingDataSyncNode_LoadsDataSyncFormViewModel()
|
||||
{
|
||||
// Arrange
|
||||
var config = new ConfigModel { DataSync = new DataSyncSection { MaxDegreeOfParallelism = 8 } };
|
||||
var sut = CreateViewModel();
|
||||
sut.LoadConfigForTesting(config, null);
|
||||
|
||||
var dataSyncNode = sut.TreeNodes
|
||||
.SelectMany(n => n.Children)
|
||||
.First(n => n.SectionKey == "DataSync");
|
||||
|
||||
// Act
|
||||
sut.SelectedNode = dataSyncNode;
|
||||
|
||||
// Assert
|
||||
sut.SelectedFormViewModel.ShouldBeOfType<DataSyncFormViewModel>();
|
||||
((DataSyncFormViewModel)sut.SelectedFormViewModel!).MaxDegreeOfParallelism.ShouldBe(8);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SelectingDataAccessNode_LoadsDataAccessFormViewModel()
|
||||
{
|
||||
// Arrange
|
||||
var config = new ConfigModel { DataAccess = new DataAccessSection { ProductionSchema = "custom" } };
|
||||
var sut = CreateViewModel();
|
||||
sut.LoadConfigForTesting(config, null);
|
||||
|
||||
var dataAccessNode = sut.TreeNodes
|
||||
.SelectMany(n => n.Children)
|
||||
.First(n => n.SectionKey == "DataAccess");
|
||||
|
||||
// Act
|
||||
sut.SelectedNode = dataAccessNode;
|
||||
|
||||
// Assert
|
||||
sut.SelectedFormViewModel.ShouldBeOfType<DataAccessFormViewModel>();
|
||||
((DataAccessFormViewModel)sut.SelectedFormViewModel!).ProductionSchema.ShouldBe("custom");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SelectingAuthNode_LoadsAuthFormViewModel()
|
||||
{
|
||||
// Arrange
|
||||
var config = new ConfigModel { Auth = new AuthSection { CookieName = "TestCookie" } };
|
||||
var sut = CreateViewModel();
|
||||
sut.LoadConfigForTesting(config, null);
|
||||
|
||||
var authNode = sut.TreeNodes
|
||||
.SelectMany(n => n.Children)
|
||||
.First(n => n.SectionKey == "Auth");
|
||||
|
||||
// Act
|
||||
sut.SelectedNode = authNode;
|
||||
|
||||
// Assert
|
||||
sut.SelectedFormViewModel.ShouldBeOfType<AuthFormViewModel>();
|
||||
((AuthFormViewModel)sut.SelectedFormViewModel!).CookieName.ShouldBe("TestCookie");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SelectingLdapNode_LoadsLdapFormViewModel()
|
||||
{
|
||||
// Arrange
|
||||
var config = new ConfigModel { Ldap = new LdapSection { GroupDn = "CN=TestGroup" } };
|
||||
var sut = CreateViewModel();
|
||||
sut.LoadConfigForTesting(config, null);
|
||||
|
||||
var ldapNode = sut.TreeNodes
|
||||
.SelectMany(n => n.Children)
|
||||
.First(n => n.SectionKey == "Ldap");
|
||||
|
||||
// Act
|
||||
sut.SelectedNode = ldapNode;
|
||||
|
||||
// Assert
|
||||
sut.SelectedFormViewModel.ShouldBeOfType<LdapFormViewModel>();
|
||||
((LdapFormViewModel)sut.SelectedFormViewModel!).GroupDn.ShouldBe("CN=TestGroup");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SelectingSearchNode_LoadsSearchFormViewModel()
|
||||
{
|
||||
// Arrange
|
||||
var config = new ConfigModel { Search = new SearchSection { MaxResultRows = 50000 } };
|
||||
var sut = CreateViewModel();
|
||||
sut.LoadConfigForTesting(config, null);
|
||||
|
||||
var searchNode = sut.TreeNodes
|
||||
.SelectMany(n => n.Children)
|
||||
.First(n => n.SectionKey == "Search");
|
||||
|
||||
// Act
|
||||
sut.SelectedNode = searchNode;
|
||||
|
||||
// Assert
|
||||
sut.SelectedFormViewModel.ShouldBeOfType<SearchFormViewModel>();
|
||||
((SearchFormViewModel)sut.SelectedFormViewModel!).MaxResultRows.ShouldBe(50000);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SelectingExcelExportNode_LoadsExcelExportFormViewModel()
|
||||
{
|
||||
// Arrange
|
||||
var config = new ConfigModel { ExcelExport = new ExcelExportSection { TimezoneId = "America/New_York" } };
|
||||
var sut = CreateViewModel();
|
||||
sut.LoadConfigForTesting(config, null);
|
||||
|
||||
var excelNode = sut.TreeNodes
|
||||
.SelectMany(n => n.Children)
|
||||
.First(n => n.SectionKey == "ExcelExport");
|
||||
|
||||
// Act
|
||||
sut.SelectedNode = excelNode;
|
||||
|
||||
// Assert
|
||||
sut.SelectedFormViewModel.ShouldBeOfType<ExcelExportFormViewModel>();
|
||||
((ExcelExportFormViewModel)sut.SelectedFormViewModel!).TimezoneId.ShouldBe("America/New_York");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SelectingPipelineNode_LoadsPipelineFormViewModel()
|
||||
{
|
||||
// Arrange
|
||||
var config = new ConfigModel();
|
||||
var pipelines = new PipelinesConfigModel
|
||||
{
|
||||
Pipelines = new Dictionary<string, PipelineModel>
|
||||
{
|
||||
["WorkOrders"] = new PipelineModel
|
||||
{
|
||||
Source = new PipelineSource { Connection = "jde", Query = "SELECT * FROM WO" },
|
||||
Destination = new PipelineDestination { Table = "WorkOrder_Curr" }
|
||||
}
|
||||
}
|
||||
};
|
||||
var sut = CreateViewModel();
|
||||
sut.LoadConfigForTesting(config, pipelines);
|
||||
|
||||
var pipelineNode = sut.TreeNodes
|
||||
.SelectMany(n => n.Children)
|
||||
.First(n => n.SectionKey == "WorkOrders");
|
||||
|
||||
// Act
|
||||
sut.SelectedNode = pipelineNode;
|
||||
|
||||
// Assert
|
||||
sut.SelectedFormViewModel.ShouldBeOfType<PipelineFormViewModel>();
|
||||
var pipelineForm = (PipelineFormViewModel)sut.SelectedFormViewModel!;
|
||||
pipelineForm.Name.ShouldBe("WorkOrders");
|
||||
pipelineForm.Connection.ShouldBe("jde");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ModifyingFormProperty_SetsHasUnsavedChanges()
|
||||
{
|
||||
// Arrange
|
||||
var config = new ConfigModel();
|
||||
var sut = CreateViewModel();
|
||||
sut.LoadConfigForTesting(config, null);
|
||||
|
||||
var dataSyncNode = sut.TreeNodes
|
||||
.SelectMany(n => n.Children)
|
||||
.First(n => n.SectionKey == "DataSync");
|
||||
sut.SelectedNode = dataSyncNode;
|
||||
|
||||
// Act
|
||||
((DataSyncFormViewModel)sut.SelectedFormViewModel!).BatchSize = 10000;
|
||||
|
||||
// Assert
|
||||
sut.HasUnsavedChanges.ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ModifyingFormProperty_MarksNodeAsModified()
|
||||
{
|
||||
// Arrange
|
||||
var config = new ConfigModel();
|
||||
var sut = CreateViewModel();
|
||||
sut.LoadConfigForTesting(config, null);
|
||||
|
||||
var dataSyncNode = sut.TreeNodes
|
||||
.SelectMany(n => n.Children)
|
||||
.First(n => n.SectionKey == "DataSync");
|
||||
sut.SelectedNode = dataSyncNode;
|
||||
|
||||
// Act
|
||||
((DataSyncFormViewModel)sut.SelectedFormViewModel!).MaxDegreeOfParallelism = 16;
|
||||
|
||||
// Assert
|
||||
dataSyncNode.IsModified.ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SelectingFolderNode_SetsSelectedFormViewModelToNull()
|
||||
{
|
||||
// Arrange
|
||||
var config = new ConfigModel();
|
||||
var sut = CreateViewModel();
|
||||
sut.LoadConfigForTesting(config, null);
|
||||
|
||||
var folderNode = sut.TreeNodes.First(); // Settings folder
|
||||
|
||||
// Act
|
||||
sut.SelectedNode = folderNode;
|
||||
|
||||
// Assert
|
||||
sut.SelectedFormViewModel.ShouldBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SelectingNull_SetsSelectedFormViewModelToNull()
|
||||
{
|
||||
// Arrange
|
||||
var config = new ConfigModel();
|
||||
var sut = CreateViewModel();
|
||||
sut.LoadConfigForTesting(config, null);
|
||||
|
||||
var dataSyncNode = sut.TreeNodes
|
||||
.SelectMany(n => n.Children)
|
||||
.First(n => n.SectionKey == "DataSync");
|
||||
sut.SelectedNode = dataSyncNode;
|
||||
|
||||
// Act
|
||||
sut.SelectedNode = null;
|
||||
|
||||
// Assert
|
||||
sut.SelectedFormViewModel.ShouldBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LoadConfigForTesting_BuildsTreeNodes()
|
||||
{
|
||||
// Arrange
|
||||
var config = new ConfigModel();
|
||||
var sut = CreateViewModel();
|
||||
|
||||
// Act
|
||||
sut.LoadConfigForTesting(config, null);
|
||||
|
||||
// Assert
|
||||
sut.TreeNodes.Count.ShouldBe(2); // Settings and Pipelines folders
|
||||
sut.TreeNodes[0].Name.ShouldBe("Settings");
|
||||
sut.TreeNodes[0].Children.Count.ShouldBe(6); // DataSync, DataAccess, Auth, Ldap, Search, ExcelExport
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LoadConfigForTesting_WithPipelines_BuildsPipelineNodes()
|
||||
{
|
||||
// Arrange
|
||||
var config = new ConfigModel();
|
||||
var pipelines = new PipelinesConfigModel
|
||||
{
|
||||
Pipelines = new Dictionary<string, PipelineModel>
|
||||
{
|
||||
["Pipeline1"] = new PipelineModel(),
|
||||
["Pipeline2"] = new PipelineModel()
|
||||
}
|
||||
};
|
||||
var sut = CreateViewModel();
|
||||
|
||||
// Act
|
||||
sut.LoadConfigForTesting(config, pipelines);
|
||||
|
||||
// Assert
|
||||
sut.TreeNodes[1].Name.ShouldBe("Pipelines");
|
||||
sut.TreeNodes[1].Children.Count.ShouldBe(2);
|
||||
}
|
||||
|
||||
private MainWindowViewModel CreateViewModel()
|
||||
{
|
||||
return new MainWindowViewModel(
|
||||
_fileSystem,
|
||||
_configFileService,
|
||||
_validationService,
|
||||
_backupService,
|
||||
_autoDiscoveryService,
|
||||
_dialogService,
|
||||
_logger);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user