diff --git a/NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/MainWindowViewModel.cs b/NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/MainWindowViewModel.cs index 8365dba..0c941a8 100644 --- a/NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/MainWindowViewModel.cs +++ b/NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/MainWindowViewModel.cs @@ -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; /// 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? _logger; private string _configFolderPath = "No folder selected"; @@ -28,30 +31,45 @@ public class MainWindowViewModel : ViewModelBase private ConfigModel? _appSettings; private PipelinesConfigModel? _pipelines; + /// + /// Gets or sets the currently loaded configuration folder path. + /// public string ConfigFolderPath { get => _configFolderPath; set => SetProperty(ref _configFolderPath, value); } + /// + /// Gets or sets a value indicating whether there are unsaved configuration changes. + /// public bool HasUnsavedChanges { get => _hasUnsavedChanges; set => SetProperty(ref _hasUnsavedChanges, value); } + /// + /// Gets or sets the validation status message displayed to the user. + /// public string ValidationStatus { get => _validationStatus; set => SetProperty(ref _validationStatus, value); } + /// + /// Gets or sets the brush color for the validation status indicator. + /// public IBrush ValidationStatusColor { get => _validationStatusColor; set => SetProperty(ref _validationStatusColor, value); } + /// + /// Gets or sets the currently selected tree node in the configuration tree view. + /// public TreeNodeViewModel? SelectedNode { get => _selectedNode; @@ -62,33 +80,80 @@ public class MainWindowViewModel : ViewModelBase } } + /// + /// Gets or sets the view model for the form displayed when a node is selected. + /// public object? SelectedFormViewModel { get => _selectedFormViewModel; set => SetProperty(ref _selectedFormViewModel, value); } + /// + /// Gets the collection of tree nodes representing the configuration structure. + /// public ObservableCollection TreeNodes { get; } = []; + /// + /// Gets the command for opening a configuration folder. + /// public ICommand OpenFolderCommand { get; } + + /// + /// Gets the command for saving configuration changes. + /// public ICommand SaveCommand { get; } + + /// + /// Gets the command for exiting the application. + /// public ICommand ExitCommand { get; } + + /// + /// Gets the command for undoing the last configuration change. + /// public ICommand UndoCommand { get; } + + /// + /// Gets the command for redoing the last undone configuration change. + /// public ICommand RedoCommand { get; } + + /// + /// Gets the command for validating the current configuration. + /// public ICommand ValidateCommand { get; } + + /// + /// Gets the command for testing database connections. + /// public ICommand TestConnectionCommand { get; } + /// + /// Initializes a new instance of the class. + /// + /// File system abstraction for I/O operations. + /// Service for loading and saving configuration files. + /// Service for validating configuration settings. + /// Service for creating configuration backups. + /// Service for discovering configuration folder locations. + /// Service for showing platform dialogs. + /// Optional logger for recording view model activities. public MainWindowViewModel( + IFileSystem fileSystem, IConfigFileService configFileService, IValidationService validationService, IBackupService backupService, IAutoDiscoveryService autoDiscoveryService, + IDialogService? dialogService, ILogger? 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. /// 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 /// 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); + } } /// @@ -201,8 +276,48 @@ public class MainWindowViewModel : ViewModelBase /// 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 + }; + } + + /// + /// Marks the configuration as having unsaved changes. + /// + private void MarkAsChanged() + { + HasUnsavedChanges = true; + if (_selectedNode != null) + _selectedNode.IsModified = true; + } + + /// + /// Loads configuration for testing purposes. + /// + /// The application settings configuration model. + /// The pipelines configuration model. + public void LoadConfigForTesting(ConfigModel? appSettings, PipelinesConfigModel? pipelines) + { + _appSettings = appSettings; + _pipelines = pipelines; + BuildTreeNodes(); } /// diff --git a/NEW/tests/JdeScoping.ConfigManager.Tests/ViewModels/MainWindowViewModelTests.cs b/NEW/tests/JdeScoping.ConfigManager.Tests/ViewModels/MainWindowViewModelTests.cs new file mode 100644 index 0000000..7afc6c0 --- /dev/null +++ b/NEW/tests/JdeScoping.ConfigManager.Tests/ViewModels/MainWindowViewModelTests.cs @@ -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 _logger; + + public MainWindowViewModelTests() + { + _fileSystem = Substitute.For(); + _configFileService = Substitute.For(); + _validationService = Substitute.For(); + _backupService = Substitute.For(); + _autoDiscoveryService = Substitute.For(); + _dialogService = Substitute.For(); + _logger = Substitute.For>(); + + _validationService.ValidateAppSettings(Arg.Any()) + .Returns(new ValidationResult()); + _validationService.ValidatePipelines(Arg.Any()) + .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)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)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)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)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)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)sut.SelectedFormViewModel!).TimezoneId.ShouldBe("America/New_York"); + } + + [Fact] + public void SelectingPipelineNode_LoadsPipelineFormViewModel() + { + // Arrange + var config = new ConfigModel(); + var pipelines = new PipelinesConfigModel + { + Pipelines = new Dictionary + { + ["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(); + 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 + { + ["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); + } +}