Add comprehensive XML documentation (param/returns tags) across 132 source files to improve IntelliSense and API discoverability. Include ConfigManager design documents and implementation plans for phases 1-9.
82 KiB
ConfigManager Implementation Plan: Phases 7-9
For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
Goal: Complete the ConfigManager UI by implementing form ViewModels, dialogs, and polish features.
Architecture: Each settings section gets a dedicated FormViewModel that wraps the model and provides two-way binding with change tracking. Dialogs follow the AvaloniaDialogService pattern from SecureStoreManager.
Tech Stack: Avalonia 11.2, MVVM, TDD with xUnit/NSubstitute/Shouldly
Prerequisites: Phases 1-6 complete (13 tasks, all services and basic UI shell implemented)
Phase 7: Form ViewModels
Task 14: Create DataSyncFormViewModel
Files:
- Create:
NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/Forms/DataSyncFormViewModel.cs - Create:
NEW/tests/JdeScoping.ConfigManager.Tests/ViewModels/Forms/DataSyncFormViewModelTests.cs
Step 1: Write the failing test
using JdeScoping.ConfigManager.Models;
using JdeScoping.ConfigManager.ViewModels.Forms;
namespace JdeScoping.ConfigManager.Tests.ViewModels.Forms;
public class DataSyncFormViewModelTests
{
[Fact]
public void Constructor_InitializesFromModel()
{
// Arrange
var model = new DataSyncSection
{
Enabled = true,
MaxDegreeOfParallelism = 8,
BatchSize = 25000
};
// Act
var sut = new DataSyncFormViewModel(model, () => { });
// Assert
sut.Enabled.ShouldBeTrue();
sut.MaxDegreeOfParallelism.ShouldBe(8);
sut.BatchSize.ShouldBe(25000);
}
[Fact]
public void PropertyChange_UpdatesModel()
{
// Arrange
var model = new DataSyncSection { MaxDegreeOfParallelism = 4 };
var sut = new DataSyncFormViewModel(model, () => { });
// Act
sut.MaxDegreeOfParallelism = 16;
// Assert
model.MaxDegreeOfParallelism.ShouldBe(16);
}
[Fact]
public void PropertyChange_InvokesOnChanged()
{
// Arrange
var model = new DataSyncSection();
var changedInvoked = false;
var sut = new DataSyncFormViewModel(model, () => changedInvoked = true);
// Act
sut.BatchSize = 10000;
// Assert
changedInvoked.ShouldBeTrue();
}
[Fact]
public void PropertyChange_RaisesPropertyChanged()
{
// Arrange
var model = new DataSyncSection();
var sut = new DataSyncFormViewModel(model, () => { });
var propertyChangedRaised = false;
sut.PropertyChanged += (s, e) =>
{
if (e.PropertyName == nameof(DataSyncFormViewModel.LookbackMultiplier))
propertyChangedRaised = true;
};
// Act
sut.LookbackMultiplier = 2.5;
// Assert
propertyChangedRaised.ShouldBeTrue();
}
}
Step 2: Run test to verify it fails
Run: dotnet test NEW/tests/JdeScoping.ConfigManager.Tests/ --filter "DataSyncFormViewModelTests"
Expected: FAIL with "DataSyncFormViewModel not found"
Step 3: Create DataSyncFormViewModel
using JdeScoping.ConfigManager.Models;
namespace JdeScoping.ConfigManager.ViewModels.Forms;
/// <summary>
/// ViewModel for editing DataSync configuration section.
/// </summary>
public class DataSyncFormViewModel : ViewModelBase
{
private readonly DataSyncSection _model;
private readonly Action _onChanged;
public DataSyncFormViewModel(DataSyncSection model, Action onChanged)
{
_model = model ?? throw new ArgumentNullException(nameof(model));
_onChanged = onChanged ?? throw new ArgumentNullException(nameof(onChanged));
}
/// <summary>
/// Gets or sets whether data synchronization is enabled.
/// </summary>
public bool Enabled
{
get => _model.Enabled;
set
{
if (_model.Enabled != value)
{
_model.Enabled = value;
OnPropertyChanged();
_onChanged();
}
}
}
/// <summary>
/// Gets or sets the check interval in minutes.
/// </summary>
public int CheckIntervalMinutes
{
get => (int)_model.CheckInterval.TotalMinutes;
set
{
var newValue = TimeSpan.FromMinutes(value);
if (_model.CheckInterval != newValue)
{
_model.CheckInterval = newValue;
OnPropertyChanged();
_onChanged();
}
}
}
/// <summary>
/// Gets or sets the maximum degree of parallelism.
/// </summary>
public int MaxDegreeOfParallelism
{
get => _model.MaxDegreeOfParallelism;
set
{
if (_model.MaxDegreeOfParallelism != value)
{
_model.MaxDegreeOfParallelism = value;
OnPropertyChanged();
_onChanged();
}
}
}
/// <summary>
/// Gets or sets the batch size for sync operations.
/// </summary>
public int BatchSize
{
get => _model.BatchSize;
set
{
if (_model.BatchSize != value)
{
_model.BatchSize = value;
OnPropertyChanged();
_onChanged();
}
}
}
/// <summary>
/// Gets or sets the bulk copy batch size.
/// </summary>
public int BulkCopyBatchSize
{
get => _model.BulkCopyBatchSize;
set
{
if (_model.BulkCopyBatchSize != value)
{
_model.BulkCopyBatchSize = value;
OnPropertyChanged();
_onChanged();
}
}
}
/// <summary>
/// Gets or sets the lookback multiplier.
/// </summary>
public double LookbackMultiplier
{
get => _model.LookbackMultiplier;
set
{
if (Math.Abs(_model.LookbackMultiplier - value) > 0.001)
{
_model.LookbackMultiplier = value;
OnPropertyChanged();
_onChanged();
}
}
}
/// <summary>
/// Gets or sets the purge retention days.
/// </summary>
public int PurgeRetentionDays
{
get => _model.PurgeRetentionDays;
set
{
if (_model.PurgeRetentionDays != value)
{
_model.PurgeRetentionDays = value;
OnPropertyChanged();
_onChanged();
}
}
}
/// <summary>
/// Gets or sets the sync timeout in seconds.
/// </summary>
public int SyncTimeoutSeconds
{
get => _model.SyncTimeoutSeconds;
set
{
if (_model.SyncTimeoutSeconds != value)
{
_model.SyncTimeoutSeconds = value;
OnPropertyChanged();
_onChanged();
}
}
}
}
Step 4: Run test to verify it passes
Run: dotnet test NEW/tests/JdeScoping.ConfigManager.Tests/ --filter "DataSyncFormViewModelTests"
Expected: PASS
Step 5: Commit
git add NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/Forms/
git add NEW/tests/JdeScoping.ConfigManager.Tests/ViewModels/Forms/
git commit -m "feat(configmanager): add DataSyncFormViewModel"
Task 15: Create DataAccessFormViewModel
Files:
- Create:
NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/Forms/DataAccessFormViewModel.cs - Create:
NEW/tests/JdeScoping.ConfigManager.Tests/ViewModels/Forms/DataAccessFormViewModelTests.cs
Step 1: Write the failing test
using JdeScoping.ConfigManager.Models;
using JdeScoping.ConfigManager.ViewModels.Forms;
namespace JdeScoping.ConfigManager.Tests.ViewModels.Forms;
public class DataAccessFormViewModelTests
{
[Fact]
public void Constructor_InitializesFromModel()
{
// Arrange
var model = new DataAccessSection
{
DefaultTimeoutSeconds = 60,
ProductionSchema = "dbo",
EnableDetailedLogging = true
};
// Act
var sut = new DataAccessFormViewModel(model, () => { });
// Assert
sut.DefaultTimeoutSeconds.ShouldBe(60);
sut.ProductionSchema.ShouldBe("dbo");
sut.EnableDetailedLogging.ShouldBeTrue();
}
[Fact]
public void PropertyChange_UpdatesModelAndInvokesOnChanged()
{
// Arrange
var model = new DataAccessSection();
var changedInvoked = false;
var sut = new DataAccessFormViewModel(model, () => changedInvoked = true);
// Act
sut.ArchiveSchema = "hist";
// Assert
model.ArchiveSchema.ShouldBe("hist");
changedInvoked.ShouldBeTrue();
}
}
Step 2: Run test to verify it fails
Run: dotnet test NEW/tests/JdeScoping.ConfigManager.Tests/ --filter "DataAccessFormViewModelTests"
Expected: FAIL with "DataAccessFormViewModel not found"
Step 3: Create DataAccessFormViewModel
using JdeScoping.ConfigManager.Models;
namespace JdeScoping.ConfigManager.ViewModels.Forms;
/// <summary>
/// ViewModel for editing DataAccess configuration section.
/// </summary>
public class DataAccessFormViewModel : ViewModelBase
{
private readonly DataAccessSection _model;
private readonly Action _onChanged;
public DataAccessFormViewModel(DataAccessSection model, Action onChanged)
{
_model = model ?? throw new ArgumentNullException(nameof(model));
_onChanged = onChanged ?? throw new ArgumentNullException(nameof(onChanged));
}
/// <summary>
/// Gets or sets the default query timeout in seconds.
/// </summary>
public int DefaultTimeoutSeconds
{
get => _model.DefaultTimeoutSeconds;
set
{
if (_model.DefaultTimeoutSeconds != value)
{
_model.DefaultTimeoutSeconds = value;
OnPropertyChanged();
_onChanged();
}
}
}
/// <summary>
/// Gets or sets the lot usage query timeout in seconds.
/// </summary>
public int LotUsageTimeoutSeconds
{
get => _model.LotUsageTimeoutSeconds;
set
{
if (_model.LotUsageTimeoutSeconds != value)
{
_model.LotUsageTimeoutSeconds = value;
OnPropertyChanged();
_onChanged();
}
}
}
/// <summary>
/// Gets or sets the MIS data query timeout in seconds.
/// </summary>
public int MisDataTimeoutSeconds
{
get => _model.MisDataTimeoutSeconds;
set
{
if (_model.MisDataTimeoutSeconds != value)
{
_model.MisDataTimeoutSeconds = value;
OnPropertyChanged();
_onChanged();
}
}
}
/// <summary>
/// Gets or sets the production schema name.
/// </summary>
public string ProductionSchema
{
get => _model.ProductionSchema;
set
{
if (_model.ProductionSchema != value)
{
_model.ProductionSchema = value;
OnPropertyChanged();
_onChanged();
}
}
}
/// <summary>
/// Gets or sets the archive schema name.
/// </summary>
public string ArchiveSchema
{
get => _model.ArchiveSchema;
set
{
if (_model.ArchiveSchema != value)
{
_model.ArchiveSchema = value;
OnPropertyChanged();
_onChanged();
}
}
}
/// <summary>
/// Gets or sets the stage schema name.
/// </summary>
public string StageSchema
{
get => _model.StageSchema;
set
{
if (_model.StageSchema != value)
{
_model.StageSchema = value;
OnPropertyChanged();
_onChanged();
}
}
}
/// <summary>
/// Gets or sets whether detailed logging is enabled.
/// </summary>
public bool EnableDetailedLogging
{
get => _model.EnableDetailedLogging;
set
{
if (_model.EnableDetailedLogging != value)
{
_model.EnableDetailedLogging = value;
OnPropertyChanged();
_onChanged();
}
}
}
}
Step 4: Run test to verify it passes
Run: dotnet test NEW/tests/JdeScoping.ConfigManager.Tests/ --filter "DataAccessFormViewModelTests"
Expected: PASS
Step 5: Commit
git add NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/Forms/
git add NEW/tests/JdeScoping.ConfigManager.Tests/ViewModels/Forms/
git commit -m "feat(configmanager): add DataAccessFormViewModel"
Task 16: Create AuthFormViewModel
Files:
- Create:
NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/Forms/AuthFormViewModel.cs - Create:
NEW/tests/JdeScoping.ConfigManager.Tests/ViewModels/Forms/AuthFormViewModelTests.cs
Step 1: Write the failing test
using JdeScoping.ConfigManager.Models;
using JdeScoping.ConfigManager.ViewModels.Forms;
namespace JdeScoping.ConfigManager.Tests.ViewModels.Forms;
public class AuthFormViewModelTests
{
[Fact]
public void Constructor_InitializesFromModel()
{
// Arrange
var model = new AuthSection
{
CookieName = ".TestAuth",
CookieExpirationMinutes = 120
};
// Act
var sut = new AuthFormViewModel(model, () => { });
// Assert
sut.CookieName.ShouldBe(".TestAuth");
sut.CookieExpirationMinutes.ShouldBe(120);
}
[Fact]
public void PropertyChange_UpdatesModelAndInvokesOnChanged()
{
// Arrange
var model = new AuthSection();
var changedInvoked = false;
var sut = new AuthFormViewModel(model, () => changedInvoked = true);
// Act
sut.CookieExpirationMinutes = 240;
// Assert
model.CookieExpirationMinutes.ShouldBe(240);
changedInvoked.ShouldBeTrue();
}
}
Step 2: Run test to verify it fails
Run: dotnet test NEW/tests/JdeScoping.ConfigManager.Tests/ --filter "AuthFormViewModelTests"
Expected: FAIL with "AuthFormViewModel not found"
Step 3: Create AuthFormViewModel
using JdeScoping.ConfigManager.Models;
namespace JdeScoping.ConfigManager.ViewModels.Forms;
/// <summary>
/// ViewModel for editing Auth configuration section.
/// </summary>
public class AuthFormViewModel : ViewModelBase
{
private readonly AuthSection _model;
private readonly Action _onChanged;
public AuthFormViewModel(AuthSection model, Action onChanged)
{
_model = model ?? throw new ArgumentNullException(nameof(model));
_onChanged = onChanged ?? throw new ArgumentNullException(nameof(onChanged));
}
/// <summary>
/// Gets or sets the authentication cookie name.
/// </summary>
public string CookieName
{
get => _model.CookieName;
set
{
if (_model.CookieName != value)
{
_model.CookieName = value;
OnPropertyChanged();
_onChanged();
}
}
}
/// <summary>
/// Gets or sets the cookie expiration time in minutes.
/// </summary>
public int CookieExpirationMinutes
{
get => _model.CookieExpirationMinutes;
set
{
if (_model.CookieExpirationMinutes != value)
{
_model.CookieExpirationMinutes = value;
OnPropertyChanged();
_onChanged();
}
}
}
}
Step 4: Run test to verify it passes
Run: dotnet test NEW/tests/JdeScoping.ConfigManager.Tests/ --filter "AuthFormViewModelTests"
Expected: PASS
Step 5: Commit
git add NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/Forms/
git add NEW/tests/JdeScoping.ConfigManager.Tests/ViewModels/Forms/
git commit -m "feat(configmanager): add AuthFormViewModel"
Task 17: Create LdapFormViewModel
Files:
- Create:
NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/Forms/LdapFormViewModel.cs - Create:
NEW/tests/JdeScoping.ConfigManager.Tests/ViewModels/Forms/LdapFormViewModelTests.cs
Step 1: Write the failing test
using JdeScoping.ConfigManager.Models;
using JdeScoping.ConfigManager.ViewModels.Forms;
namespace JdeScoping.ConfigManager.Tests.ViewModels.Forms;
public class LdapFormViewModelTests
{
[Fact]
public void Constructor_InitializesFromModel()
{
// Arrange
var model = new LdapSection
{
ServerUrls = ["ldap://server1.local", "ldap://server2.local"],
GroupDn = "CN=Admins,DC=corp",
SearchBase = "DC=corp,DC=local",
UseFakeAuth = true
};
// Act
var sut = new LdapFormViewModel(model, () => { });
// Assert
sut.ServerUrlsText.ShouldBe("ldap://server1.local\nldap://server2.local");
sut.GroupDn.ShouldBe("CN=Admins,DC=corp");
sut.SearchBase.ShouldBe("DC=corp,DC=local");
sut.UseFakeAuth.ShouldBeTrue();
}
[Fact]
public void ServerUrlsText_SplitsIntoArray()
{
// Arrange
var model = new LdapSection();
var sut = new LdapFormViewModel(model, () => { });
// Act
sut.ServerUrlsText = "ldap://a.local\nldap://b.local\nldap://c.local";
// Assert
model.ServerUrls.Length.ShouldBe(3);
model.ServerUrls[0].ShouldBe("ldap://a.local");
model.ServerUrls[2].ShouldBe("ldap://c.local");
}
[Fact]
public void AdminBypassUsersText_SplitsIntoArray()
{
// Arrange
var model = new LdapSection();
var sut = new LdapFormViewModel(model, () => { });
// Act
sut.AdminBypassUsersText = "admin\nservice_account";
// Assert
model.AdminBypassUsers.Length.ShouldBe(2);
model.AdminBypassUsers[0].ShouldBe("admin");
}
}
Step 2: Run test to verify it fails
Run: dotnet test NEW/tests/JdeScoping.ConfigManager.Tests/ --filter "LdapFormViewModelTests"
Expected: FAIL with "LdapFormViewModel not found"
Step 3: Create LdapFormViewModel
using JdeScoping.ConfigManager.Models;
namespace JdeScoping.ConfigManager.ViewModels.Forms;
/// <summary>
/// ViewModel for editing LDAP configuration section.
/// </summary>
public class LdapFormViewModel : ViewModelBase
{
private readonly LdapSection _model;
private readonly Action _onChanged;
public LdapFormViewModel(LdapSection model, Action onChanged)
{
_model = model ?? throw new ArgumentNullException(nameof(model));
_onChanged = onChanged ?? throw new ArgumentNullException(nameof(onChanged));
}
/// <summary>
/// Gets or sets the server URLs as newline-separated text.
/// </summary>
public string ServerUrlsText
{
get => string.Join("\n", _model.ServerUrls);
set
{
var urls = value.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
if (!_model.ServerUrls.SequenceEqual(urls))
{
_model.ServerUrls = urls;
OnPropertyChanged();
_onChanged();
}
}
}
/// <summary>
/// Gets or sets the group distinguished name.
/// </summary>
public string GroupDn
{
get => _model.GroupDn;
set
{
if (_model.GroupDn != value)
{
_model.GroupDn = value;
OnPropertyChanged();
_onChanged();
}
}
}
/// <summary>
/// Gets or sets the search base distinguished name.
/// </summary>
public string SearchBase
{
get => _model.SearchBase;
set
{
if (_model.SearchBase != value)
{
_model.SearchBase = value;
OnPropertyChanged();
_onChanged();
}
}
}
/// <summary>
/// Gets or sets the connection timeout in seconds.
/// </summary>
public int ConnectionTimeoutSeconds
{
get => _model.ConnectionTimeoutSeconds;
set
{
if (_model.ConnectionTimeoutSeconds != value)
{
_model.ConnectionTimeoutSeconds = value;
OnPropertyChanged();
_onChanged();
}
}
}
/// <summary>
/// Gets or sets whether to use fake authentication.
/// </summary>
public bool UseFakeAuth
{
get => _model.UseFakeAuth;
set
{
if (_model.UseFakeAuth != value)
{
_model.UseFakeAuth = value;
OnPropertyChanged();
_onChanged();
}
}
}
/// <summary>
/// Gets or sets the admin bypass users as newline-separated text.
/// </summary>
public string AdminBypassUsersText
{
get => string.Join("\n", _model.AdminBypassUsers);
set
{
var users = value.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
if (!_model.AdminBypassUsers.SequenceEqual(users))
{
_model.AdminBypassUsers = users;
OnPropertyChanged();
_onChanged();
}
}
}
}
Step 4: Run test to verify it passes
Run: dotnet test NEW/tests/JdeScoping.ConfigManager.Tests/ --filter "LdapFormViewModelTests"
Expected: PASS
Step 5: Commit
git add NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/Forms/
git add NEW/tests/JdeScoping.ConfigManager.Tests/ViewModels/Forms/
git commit -m "feat(configmanager): add LdapFormViewModel"
Task 18: Create SearchFormViewModel
Files:
- Create:
NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/Forms/SearchFormViewModel.cs - Create:
NEW/tests/JdeScoping.ConfigManager.Tests/ViewModels/Forms/SearchFormViewModelTests.cs
Step 1: Write the failing test
using JdeScoping.ConfigManager.Models;
using JdeScoping.ConfigManager.ViewModels.Forms;
namespace JdeScoping.ConfigManager.Tests.ViewModels.Forms;
public class SearchFormViewModelTests
{
[Fact]
public void Constructor_InitializesFromModel()
{
// Arrange
var model = new SearchSection
{
MaxResultRows = 50000,
TimeoutSeconds = 600,
MaxConcurrentSearches = 10
};
// Act
var sut = new SearchFormViewModel(model, () => { });
// Assert
sut.MaxResultRows.ShouldBe(50000);
sut.TimeoutSeconds.ShouldBe(600);
sut.MaxConcurrentSearches.ShouldBe(10);
}
[Fact]
public void PropertyChange_UpdatesModelAndInvokesOnChanged()
{
// Arrange
var model = new SearchSection();
var changedInvoked = false;
var sut = new SearchFormViewModel(model, () => changedInvoked = true);
// Act
sut.MaxResultRows = 25000;
// Assert
model.MaxResultRows.ShouldBe(25000);
changedInvoked.ShouldBeTrue();
}
}
Step 2: Run test to verify it fails
Run: dotnet test NEW/tests/JdeScoping.ConfigManager.Tests/ --filter "SearchFormViewModelTests"
Expected: FAIL with "SearchFormViewModel not found"
Step 3: Create SearchFormViewModel
using JdeScoping.ConfigManager.Models;
namespace JdeScoping.ConfigManager.ViewModels.Forms;
/// <summary>
/// ViewModel for editing Search configuration section.
/// </summary>
public class SearchFormViewModel : ViewModelBase
{
private readonly SearchSection _model;
private readonly Action _onChanged;
public SearchFormViewModel(SearchSection model, Action onChanged)
{
_model = model ?? throw new ArgumentNullException(nameof(model));
_onChanged = onChanged ?? throw new ArgumentNullException(nameof(onChanged));
}
/// <summary>
/// Gets or sets the maximum number of result rows.
/// </summary>
public int MaxResultRows
{
get => _model.MaxResultRows;
set
{
if (_model.MaxResultRows != value)
{
_model.MaxResultRows = value;
OnPropertyChanged();
_onChanged();
}
}
}
/// <summary>
/// Gets or sets the search timeout in seconds.
/// </summary>
public int TimeoutSeconds
{
get => _model.TimeoutSeconds;
set
{
if (_model.TimeoutSeconds != value)
{
_model.TimeoutSeconds = value;
OnPropertyChanged();
_onChanged();
}
}
}
/// <summary>
/// Gets or sets the maximum number of concurrent searches.
/// </summary>
public int MaxConcurrentSearches
{
get => _model.MaxConcurrentSearches;
set
{
if (_model.MaxConcurrentSearches != value)
{
_model.MaxConcurrentSearches = value;
OnPropertyChanged();
_onChanged();
}
}
}
}
Step 4: Run test to verify it passes
Run: dotnet test NEW/tests/JdeScoping.ConfigManager.Tests/ --filter "SearchFormViewModelTests"
Expected: PASS
Step 5: Commit
git add NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/Forms/
git add NEW/tests/JdeScoping.ConfigManager.Tests/ViewModels/Forms/
git commit -m "feat(configmanager): add SearchFormViewModel"
Task 19: Create ExcelExportFormViewModel
Files:
- Create:
NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/Forms/ExcelExportFormViewModel.cs - Create:
NEW/tests/JdeScoping.ConfigManager.Tests/ViewModels/Forms/ExcelExportFormViewModelTests.cs
Step 1: Write the failing test
using JdeScoping.ConfigManager.Models;
using JdeScoping.ConfigManager.ViewModels.Forms;
namespace JdeScoping.ConfigManager.Tests.ViewModels.Forms;
public class ExcelExportFormViewModelTests
{
[Fact]
public void Constructor_InitializesFromModel()
{
// Arrange
var model = new ExcelExportSection
{
MaxRowsPerSheet = 500000,
DefaultDateFormat = "MM/dd/yyyy",
TimezoneId = "America/New_York",
DebugWriteToFile = true
};
// Act
var sut = new ExcelExportFormViewModel(model, () => { });
// Assert
sut.MaxRowsPerSheet.ShouldBe(500000);
sut.DefaultDateFormat.ShouldBe("MM/dd/yyyy");
sut.TimezoneId.ShouldBe("America/New_York");
sut.DebugWriteToFile.ShouldBeTrue();
}
[Fact]
public void PropertyChange_UpdatesModelAndInvokesOnChanged()
{
// Arrange
var model = new ExcelExportSection();
var changedInvoked = false;
var sut = new ExcelExportFormViewModel(model, () => changedInvoked = true);
// Act
sut.TimezoneAbbreviation = "ET";
// Assert
model.TimezoneAbbreviation.ShouldBe("ET");
changedInvoked.ShouldBeTrue();
}
}
Step 2: Run test to verify it fails
Run: dotnet test NEW/tests/JdeScoping.ConfigManager.Tests/ --filter "ExcelExportFormViewModelTests"
Expected: FAIL with "ExcelExportFormViewModel not found"
Step 3: Create ExcelExportFormViewModel
using JdeScoping.ConfigManager.Models;
namespace JdeScoping.ConfigManager.ViewModels.Forms;
/// <summary>
/// ViewModel for editing ExcelExport configuration section.
/// </summary>
public class ExcelExportFormViewModel : ViewModelBase
{
private readonly ExcelExportSection _model;
private readonly Action _onChanged;
public ExcelExportFormViewModel(ExcelExportSection model, Action onChanged)
{
_model = model ?? throw new ArgumentNullException(nameof(model));
_onChanged = onChanged ?? throw new ArgumentNullException(nameof(onChanged));
}
/// <summary>
/// Gets or sets the criteria sheet password.
/// </summary>
public string CriteriaSheetPassword
{
get => _model.CriteriaSheetPassword;
set
{
if (_model.CriteriaSheetPassword != value)
{
_model.CriteriaSheetPassword = value;
OnPropertyChanged();
_onChanged();
}
}
}
/// <summary>
/// Gets or sets the data sheet password.
/// </summary>
public string DataSheetPassword
{
get => _model.DataSheetPassword;
set
{
if (_model.DataSheetPassword != value)
{
_model.DataSheetPassword = value;
OnPropertyChanged();
_onChanged();
}
}
}
/// <summary>
/// Gets or sets the maximum rows per sheet.
/// </summary>
public int MaxRowsPerSheet
{
get => _model.MaxRowsPerSheet;
set
{
if (_model.MaxRowsPerSheet != value)
{
_model.MaxRowsPerSheet = value;
OnPropertyChanged();
_onChanged();
}
}
}
/// <summary>
/// Gets or sets the default date format.
/// </summary>
public string DefaultDateFormat
{
get => _model.DefaultDateFormat;
set
{
if (_model.DefaultDateFormat != value)
{
_model.DefaultDateFormat = value;
OnPropertyChanged();
_onChanged();
}
}
}
/// <summary>
/// Gets or sets whether to write debug output to file.
/// </summary>
public bool DebugWriteToFile
{
get => _model.DebugWriteToFile;
set
{
if (_model.DebugWriteToFile != value)
{
_model.DebugWriteToFile = value;
OnPropertyChanged();
_onChanged();
}
}
}
/// <summary>
/// Gets or sets the debug output directory.
/// </summary>
public string DebugOutputDirectory
{
get => _model.DebugOutputDirectory;
set
{
if (_model.DebugOutputDirectory != value)
{
_model.DebugOutputDirectory = value;
OnPropertyChanged();
_onChanged();
}
}
}
/// <summary>
/// Gets or sets the timezone identifier.
/// </summary>
public string TimezoneId
{
get => _model.TimezoneId;
set
{
if (_model.TimezoneId != value)
{
_model.TimezoneId = value;
OnPropertyChanged();
_onChanged();
}
}
}
/// <summary>
/// Gets or sets the timezone abbreviation.
/// </summary>
public string TimezoneAbbreviation
{
get => _model.TimezoneAbbreviation;
set
{
if (_model.TimezoneAbbreviation != value)
{
_model.TimezoneAbbreviation = value;
OnPropertyChanged();
_onChanged();
}
}
}
}
Step 4: Run test to verify it passes
Run: dotnet test NEW/tests/JdeScoping.ConfigManager.Tests/ --filter "ExcelExportFormViewModelTests"
Expected: PASS
Step 5: Commit
git add NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/Forms/
git add NEW/tests/JdeScoping.ConfigManager.Tests/ViewModels/Forms/
git commit -m "feat(configmanager): add ExcelExportFormViewModel"
Task 20: Create PipelineFormViewModel
Files:
- Create:
NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/Forms/PipelineFormViewModel.cs - Create:
NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/Forms/ScheduleFormViewModel.cs - Create:
NEW/tests/JdeScoping.ConfigManager.Tests/ViewModels/Forms/PipelineFormViewModelTests.cs
Step 1: Write the failing test
using JdeScoping.ConfigManager.Models;
using JdeScoping.ConfigManager.ViewModels.Forms;
namespace JdeScoping.ConfigManager.Tests.ViewModels.Forms;
public class PipelineFormViewModelTests
{
[Fact]
public void Constructor_InitializesFromModel()
{
// Arrange
var model = new PipelineModel
{
Source = new PipelineSource
{
Connection = "jde",
Query = "SELECT * FROM Test"
},
Destination = new PipelineDestination
{
Table = "TestTable",
MatchColumns = ["Id", "Name"]
}
};
// Act
var sut = new PipelineFormViewModel("TestPipeline", model, () => { });
// Assert
sut.Name.ShouldBe("TestPipeline");
sut.Connection.ShouldBe("jde");
sut.Query.ShouldBe("SELECT * FROM Test");
sut.DestinationTable.ShouldBe("TestTable");
sut.MatchColumnsText.ShouldBe("Id\nName");
}
[Fact]
public void PropertyChange_UpdatesModelAndInvokesOnChanged()
{
// Arrange
var model = new PipelineModel();
var changedInvoked = false;
var sut = new PipelineFormViewModel("Test", model, () => changedInvoked = true);
// Act
sut.Connection = "cms";
// Assert
model.Source.Connection.ShouldBe("cms");
changedInvoked.ShouldBeTrue();
}
[Fact]
public void MatchColumnsText_SplitsIntoArray()
{
// Arrange
var model = new PipelineModel();
var sut = new PipelineFormViewModel("Test", model, () => { });
// Act
sut.MatchColumnsText = "Col1\nCol2\nCol3";
// Assert
model.Destination.MatchColumns.Length.ShouldBe(3);
model.Destination.MatchColumns[0].ShouldBe("Col1");
}
[Fact]
public void Schedules_AreInitialized()
{
// Arrange
var model = new PipelineModel
{
Schedules = new PipelineSchedules
{
Mass = new ScheduleModel { Enabled = true, IntervalMinutes = 10080 }
}
};
// Act
var sut = new PipelineFormViewModel("Test", model, () => { });
// Assert
sut.MassSchedule.ShouldNotBeNull();
sut.MassSchedule.Enabled.ShouldBeTrue();
sut.MassSchedule.IntervalMinutes.ShouldBe(10080);
}
}
Step 2: Run test to verify it fails
Run: dotnet test NEW/tests/JdeScoping.ConfigManager.Tests/ --filter "PipelineFormViewModelTests"
Expected: FAIL with "PipelineFormViewModel not found"
Step 3: Create ScheduleFormViewModel
using JdeScoping.ConfigManager.Models;
namespace JdeScoping.ConfigManager.ViewModels.Forms;
/// <summary>
/// ViewModel for editing a schedule configuration.
/// </summary>
public class ScheduleFormViewModel : ViewModelBase
{
private readonly ScheduleModel _model;
private readonly Action _onChanged;
public ScheduleFormViewModel(ScheduleModel model, Action onChanged)
{
_model = model ?? throw new ArgumentNullException(nameof(model));
_onChanged = onChanged ?? throw new ArgumentNullException(nameof(onChanged));
}
/// <summary>
/// Gets or sets whether this schedule is enabled.
/// </summary>
public bool Enabled
{
get => _model.Enabled;
set
{
if (_model.Enabled != value)
{
_model.Enabled = value;
OnPropertyChanged();
_onChanged();
}
}
}
/// <summary>
/// Gets or sets the interval in minutes.
/// </summary>
public int IntervalMinutes
{
get => _model.IntervalMinutes;
set
{
if (_model.IntervalMinutes != value)
{
_model.IntervalMinutes = value;
OnPropertyChanged();
_onChanged();
}
}
}
/// <summary>
/// Gets or sets whether to purge before sync.
/// </summary>
public bool PrePurge
{
get => _model.PrePurge;
set
{
if (_model.PrePurge != value)
{
_model.PrePurge = value;
OnPropertyChanged();
_onChanged();
}
}
}
/// <summary>
/// Gets or sets whether to reindex after sync.
/// </summary>
public bool ReIndex
{
get => _model.ReIndex;
set
{
if (_model.ReIndex != value)
{
_model.ReIndex = value;
OnPropertyChanged();
_onChanged();
}
}
}
}
Step 4: Create PipelineFormViewModel
using JdeScoping.ConfigManager.Models;
namespace JdeScoping.ConfigManager.ViewModels.Forms;
/// <summary>
/// ViewModel for editing a pipeline configuration.
/// </summary>
public class PipelineFormViewModel : ViewModelBase
{
private readonly PipelineModel _model;
private readonly Action _onChanged;
public PipelineFormViewModel(string name, PipelineModel model, Action onChanged)
{
Name = name ?? throw new ArgumentNullException(nameof(name));
_model = model ?? throw new ArgumentNullException(nameof(model));
_onChanged = onChanged ?? throw new ArgumentNullException(nameof(onChanged));
// Initialize schedule view models
_model.Schedules.Mass ??= new ScheduleModel();
_model.Schedules.Daily ??= new ScheduleModel();
_model.Schedules.Hourly ??= new ScheduleModel();
MassSchedule = new ScheduleFormViewModel(_model.Schedules.Mass, _onChanged);
DailySchedule = new ScheduleFormViewModel(_model.Schedules.Daily, _onChanged);
HourlySchedule = new ScheduleFormViewModel(_model.Schedules.Hourly, _onChanged);
}
/// <summary>
/// Gets the pipeline name.
/// </summary>
public string Name { get; }
/// <summary>
/// Gets or sets the source connection name.
/// </summary>
public string Connection
{
get => _model.Source.Connection;
set
{
if (_model.Source.Connection != value)
{
_model.Source.Connection = value;
OnPropertyChanged();
_onChanged();
}
}
}
/// <summary>
/// Gets or sets the source query.
/// </summary>
public string Query
{
get => _model.Source.Query;
set
{
if (_model.Source.Query != value)
{
_model.Source.Query = value;
OnPropertyChanged();
_onChanged();
}
}
}
/// <summary>
/// Gets or sets the optional mass query.
/// </summary>
public string? MassQuery
{
get => _model.Source.MassQuery;
set
{
if (_model.Source.MassQuery != value)
{
_model.Source.MassQuery = value;
OnPropertyChanged();
_onChanged();
}
}
}
/// <summary>
/// Gets or sets the destination table name.
/// </summary>
public string DestinationTable
{
get => _model.Destination.Table;
set
{
if (_model.Destination.Table != value)
{
_model.Destination.Table = value;
OnPropertyChanged();
_onChanged();
}
}
}
/// <summary>
/// Gets or sets the match columns as newline-separated text.
/// </summary>
public string MatchColumnsText
{
get => string.Join("\n", _model.Destination.MatchColumns);
set
{
var columns = value.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
if (!_model.Destination.MatchColumns.SequenceEqual(columns))
{
_model.Destination.MatchColumns = columns;
OnPropertyChanged();
_onChanged();
}
}
}
/// <summary>
/// Gets or sets the exclude from update columns as newline-separated text.
/// </summary>
public string ExcludeFromUpdateText
{
get => string.Join("\n", _model.Destination.ExcludeFromUpdate);
set
{
var columns = value.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
if (!_model.Destination.ExcludeFromUpdate.SequenceEqual(columns))
{
_model.Destination.ExcludeFromUpdate = columns;
OnPropertyChanged();
_onChanged();
}
}
}
/// <summary>
/// Gets or sets the post scripts as newline-separated text.
/// </summary>
public string PostScriptsText
{
get => _model.PostScripts != null ? string.Join("\n", _model.PostScripts) : string.Empty;
set
{
var scripts = string.IsNullOrWhiteSpace(value)
? null
: value.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
if (_model.PostScripts?.SequenceEqual(scripts ?? []) != true)
{
_model.PostScripts = scripts;
OnPropertyChanged();
_onChanged();
}
}
}
/// <summary>
/// Gets the mass schedule view model.
/// </summary>
public ScheduleFormViewModel MassSchedule { get; }
/// <summary>
/// Gets the daily schedule view model.
/// </summary>
public ScheduleFormViewModel DailySchedule { get; }
/// <summary>
/// Gets the hourly schedule view model.
/// </summary>
public ScheduleFormViewModel HourlySchedule { get; }
}
Step 5: Run test to verify it passes
Run: dotnet test NEW/tests/JdeScoping.ConfigManager.Tests/ --filter "PipelineFormViewModelTests"
Expected: PASS
Step 6: Commit
git add NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/Forms/
git add NEW/tests/JdeScoping.ConfigManager.Tests/ViewModels/Forms/
git commit -m "feat(configmanager): add PipelineFormViewModel and ScheduleFormViewModel"
Phase 8: Dialogs
Task 21: Create Platform Dialog Service
Files:
- Create:
NEW/src/Utils/JdeScoping.ConfigManager/Services/IDialogService.cs - Create:
NEW/src/Utils/JdeScoping.ConfigManager/Services/AvaloniaDialogService.cs
Step 1: Create IDialogService interface
namespace JdeScoping.ConfigManager.Services;
/// <summary>
/// Service for showing platform dialogs.
/// </summary>
public interface IDialogService
{
Task<string?> ShowFolderPickerAsync(string? title = null);
Task ShowMessageAsync(string title, string message);
Task<bool> ShowConfirmationAsync(string title, string message);
Task<bool> ShowDiffPreviewAsync(string title, DiffResult diff);
Task ShowValidationResultsAsync(ValidationResult appSettingsResult, ValidationResult pipelinesResult);
}
Step 2: Create AvaloniaDialogService
using Avalonia.Controls;
using Avalonia.Platform.Storage;
namespace JdeScoping.ConfigManager.Services;
/// <summary>
/// Avalonia implementation of the dialog service.
/// </summary>
public class AvaloniaDialogService : IDialogService
{
private readonly Func<Window?> _getMainWindow;
public AvaloniaDialogService(Func<Window?> getMainWindow)
{
_getMainWindow = getMainWindow ?? throw new ArgumentNullException(nameof(getMainWindow));
}
public async Task<string?> ShowFolderPickerAsync(string? title = null)
{
var window = _getMainWindow();
if (window == null) return null;
var result = await window.StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions
{
Title = title ?? "Select Configuration Folder",
AllowMultiple = false
});
return result.FirstOrDefault()?.Path.LocalPath;
}
public async Task ShowMessageAsync(string title, string message)
{
var window = _getMainWindow();
if (window == null) return;
var dialog = new Window
{
Title = title,
Width = 400,
Height = 200,
WindowStartupLocation = WindowStartupLocation.CenterOwner,
Content = new StackPanel
{
Margin = new Avalonia.Thickness(24),
Children =
{
new TextBlock { Text = message, TextWrapping = Avalonia.Media.TextWrapping.Wrap },
new Button { Content = "OK", HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Right, Margin = new Avalonia.Thickness(0, 16, 0, 0) }
}
}
};
await dialog.ShowDialog(window);
}
public async Task<bool> ShowConfirmationAsync(string title, string message)
{
var window = _getMainWindow();
if (window == null) return false;
var result = false;
var dialog = new Window
{
Title = title,
Width = 400,
Height = 200,
WindowStartupLocation = WindowStartupLocation.CenterOwner
};
var okButton = new Button { Content = "OK" };
var cancelButton = new Button { Content = "Cancel" };
okButton.Click += (s, e) => { result = true; dialog.Close(); };
cancelButton.Click += (s, e) => { result = false; dialog.Close(); };
dialog.Content = new StackPanel
{
Margin = new Avalonia.Thickness(24),
Children =
{
new TextBlock { Text = message, TextWrapping = Avalonia.Media.TextWrapping.Wrap },
new StackPanel
{
Orientation = Avalonia.Layout.Orientation.Horizontal,
HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Right,
Margin = new Avalonia.Thickness(0, 16, 0, 0),
Spacing = 8,
Children = { cancelButton, okButton }
}
}
};
await dialog.ShowDialog(window);
return result;
}
public async Task<bool> ShowDiffPreviewAsync(string title, DiffResult diff)
{
var window = _getMainWindow();
if (window == null) return false;
// TODO: Implement rich diff preview dialog
return await ShowConfirmationAsync(title, $"Changes detected: {diff.Insertions} insertions, {diff.Deletions} deletions. Save changes?");
}
public async Task ShowValidationResultsAsync(ValidationResult appSettingsResult, ValidationResult pipelinesResult)
{
var window = _getMainWindow();
if (window == null) return;
var allErrors = appSettingsResult.Errors.Concat(pipelinesResult.Errors).ToList();
var allWarnings = appSettingsResult.Warnings.Concat(pipelinesResult.Warnings).ToList();
var message = $"Errors: {allErrors.Count}\nWarnings: {allWarnings.Count}\n\n";
if (allErrors.Count > 0)
message += "Errors:\n" + string.Join("\n", allErrors.Select(e => "• " + e)) + "\n\n";
if (allWarnings.Count > 0)
message += "Warnings:\n" + string.Join("\n", allWarnings.Select(w => "• " + w));
await ShowMessageAsync("Validation Results", message);
}
}
Step 3: Verify build
Run: dotnet build NEW/src/Utils/JdeScoping.ConfigManager/
Expected: Build succeeded
Step 4: Commit
git add NEW/src/Utils/JdeScoping.ConfigManager/Services/
git commit -m "feat(configmanager): add IDialogService and AvaloniaDialogService"
Task 22: Create DiffPreviewDialog
Files:
- Create:
NEW/src/Utils/JdeScoping.ConfigManager/Views/Dialogs/DiffPreviewDialog.axaml - Create:
NEW/src/Utils/JdeScoping.ConfigManager/Views/Dialogs/DiffPreviewDialog.axaml.cs - Create:
NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/Dialogs/DiffPreviewDialogViewModel.cs
Step 1: Create DiffPreviewDialogViewModel
using System.Collections.ObjectModel;
using System.Windows.Input;
using JdeScoping.ConfigManager.Services;
namespace JdeScoping.ConfigManager.ViewModels.Dialogs;
/// <summary>
/// ViewModel for the diff preview dialog.
/// </summary>
public class DiffPreviewDialogViewModel : ViewModelBase
{
private bool _result;
public DiffPreviewDialogViewModel(DiffResult diff)
{
ArgumentNullException.ThrowIfNull(diff);
Lines = new ObservableCollection<DiffLineViewModel>(
diff.Lines.Select(l => new DiffLineViewModel(l)));
Insertions = diff.Insertions;
Deletions = diff.Deletions;
HasChanges = diff.HasChanges;
SaveCommand = new RelayCommand(() => { Result = true; RequestClose?.Invoke(); });
CancelCommand = new RelayCommand(() => { Result = false; RequestClose?.Invoke(); });
}
public ObservableCollection<DiffLineViewModel> Lines { get; }
public int Insertions { get; }
public int Deletions { get; }
public bool HasChanges { get; }
public bool Result
{
get => _result;
private set => SetProperty(ref _result, value);
}
public ICommand SaveCommand { get; }
public ICommand CancelCommand { get; }
public Action? RequestClose { get; set; }
}
/// <summary>
/// ViewModel for a single diff line.
/// </summary>
public class DiffLineViewModel
{
public DiffLineViewModel(DiffLine line)
{
OldLineNumber = line.OldLineNumber?.ToString() ?? "";
NewLineNumber = line.NewLineNumber?.ToString() ?? "";
Text = line.Text;
Type = line.Type;
Background = line.Type switch
{
DiffLineType.Added => "#1A3DD68C",
DiffLineType.Removed => "#1AFF6B6B",
_ => "Transparent"
};
BorderColor = line.Type switch
{
DiffLineType.Added => "#3DD68C",
DiffLineType.Removed => "#FF6B6B",
_ => "Transparent"
};
}
public string OldLineNumber { get; }
public string NewLineNumber { get; }
public string Text { get; }
public DiffLineType Type { get; }
public string Background { get; }
public string BorderColor { get; }
}
Step 2: Create DiffPreviewDialog.axaml
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:JdeScoping.ConfigManager.ViewModels.Dialogs"
x:Class="JdeScoping.ConfigManager.Views.Dialogs.DiffPreviewDialog"
x:DataType="vm:DiffPreviewDialogViewModel"
Title="Preview Changes"
Width="800" Height="600"
MinWidth="600" MinHeight="400"
Background="#151920"
WindowStartupLocation="CenterOwner">
<DockPanel>
<!-- Header -->
<Border DockPanel.Dock="Top" Background="#1C2128" Padding="24,16"
BorderBrush="#2D3540" BorderThickness="0,0,0,1">
<TextBlock Text="Preview Changes"
Foreground="#E6EDF5" FontSize="18" FontWeight="SemiBold"/>
</Border>
<!-- Footer -->
<Border DockPanel.Dock="Bottom" Background="#1C2128" Padding="24,16"
BorderBrush="#2D3540" BorderThickness="0,1,0,0">
<Grid>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Left" VerticalAlignment="Center">
<TextBlock Foreground="#9BA8B8" FontFamily="JetBrains Mono" FontSize="12">
<Run Text="{Binding Insertions}"/> insertions, <Run Text="{Binding Deletions}"/> deletions
</TextBlock>
</StackPanel>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Spacing="8">
<Button Content="Cancel" Command="{Binding CancelCommand}"
Background="Transparent" BorderBrush="#3D4550"
Foreground="#9BA8B8" Padding="16,8"/>
<Button Content="💾 Save Changes" Command="{Binding SaveCommand}"
Background="#5C9AFF" Foreground="#0D0F12"
Padding="16,8" FontWeight="Medium"/>
</StackPanel>
</Grid>
</Border>
<!-- Diff Content -->
<ScrollViewer Background="#0D0F12" Padding="0">
<ItemsControl ItemsSource="{Binding Lines}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border Background="{Binding Background}"
BorderBrush="{Binding BorderColor}"
BorderThickness="3,0,0,0"
Padding="0,2">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="50"/>
<ColumnDefinition Width="50"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="{Binding OldLineNumber}"
Foreground="#5C6A7A" FontFamily="JetBrains Mono" FontSize="12"
HorizontalAlignment="Right" Padding="0,0,8,0"/>
<TextBlock Grid.Column="1" Text="{Binding NewLineNumber}"
Foreground="#5C6A7A" FontFamily="JetBrains Mono" FontSize="12"
HorizontalAlignment="Right" Padding="0,0,8,0"/>
<Border Grid.Column="1" Width="1" Background="#2D3540" HorizontalAlignment="Right"/>
<TextBlock Grid.Column="2" Text="{Binding Text}"
Foreground="#E6EDF5" FontFamily="JetBrains Mono" FontSize="12"
Padding="12,0,0,0"/>
</Grid>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</DockPanel>
</Window>
Step 3: Create DiffPreviewDialog.axaml.cs
using Avalonia.Controls;
using JdeScoping.ConfigManager.ViewModels.Dialogs;
namespace JdeScoping.ConfigManager.Views.Dialogs;
public partial class DiffPreviewDialog : Window
{
public DiffPreviewDialog()
{
InitializeComponent();
}
public DiffPreviewDialog(DiffPreviewDialogViewModel viewModel) : this()
{
DataContext = viewModel;
viewModel.RequestClose = Close;
}
}
Step 4: Verify build
Run: dotnet build NEW/src/Utils/JdeScoping.ConfigManager/
Expected: Build succeeded
Step 5: Commit
git add NEW/src/Utils/JdeScoping.ConfigManager/Views/Dialogs/
git add NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/Dialogs/
git commit -m "feat(configmanager): add DiffPreviewDialog"
Task 23: Create ValidationResultsDialog
Files:
- Create:
NEW/src/Utils/JdeScoping.ConfigManager/Views/Dialogs/ValidationResultsDialog.axaml - Create:
NEW/src/Utils/JdeScoping.ConfigManager/Views/Dialogs/ValidationResultsDialog.axaml.cs - Create:
NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/Dialogs/ValidationResultsDialogViewModel.cs
Step 1: Create ValidationResultsDialogViewModel
using System.Collections.ObjectModel;
using System.Windows.Input;
using JdeScoping.ConfigManager.Services;
namespace JdeScoping.ConfigManager.ViewModels.Dialogs;
/// <summary>
/// ViewModel for the validation results dialog.
/// </summary>
public class ValidationResultsDialogViewModel : ViewModelBase
{
public ValidationResultsDialogViewModel(ValidationResult appSettingsResult, ValidationResult pipelinesResult)
{
var items = new List<ValidationItemViewModel>();
foreach (var error in appSettingsResult.Errors)
items.Add(new ValidationItemViewModel(error, "appsettings.json", ValidationItemType.Error));
foreach (var warning in appSettingsResult.Warnings)
items.Add(new ValidationItemViewModel(warning, "appsettings.json", ValidationItemType.Warning));
foreach (var error in pipelinesResult.Errors)
items.Add(new ValidationItemViewModel(error, "pipelines.json", ValidationItemType.Error));
foreach (var warning in pipelinesResult.Warnings)
items.Add(new ValidationItemViewModel(warning, "pipelines.json", ValidationItemType.Warning));
Items = new ObservableCollection<ValidationItemViewModel>(items);
ErrorCount = appSettingsResult.Errors.Count + pipelinesResult.Errors.Count;
WarningCount = appSettingsResult.Warnings.Count + pipelinesResult.Warnings.Count;
IsValid = ErrorCount == 0 && WarningCount == 0;
CloseCommand = new RelayCommand(() => RequestClose?.Invoke());
}
public ObservableCollection<ValidationItemViewModel> Items { get; }
public int ErrorCount { get; }
public int WarningCount { get; }
public bool IsValid { get; }
public ICommand CloseCommand { get; }
public Action? RequestClose { get; set; }
}
public enum ValidationItemType { Error, Warning }
/// <summary>
/// ViewModel for a single validation item.
/// </summary>
public class ValidationItemViewModel
{
public ValidationItemViewModel(string message, string source, ValidationItemType type)
{
Message = message;
Source = source;
Type = type;
Icon = type == ValidationItemType.Error ? "✗" : "⚠";
IconColor = type == ValidationItemType.Error ? "#FF6B6B" : "#FFB84D";
Background = type == ValidationItemType.Error ? "#1AFF6B6B" : "#1AFFB84D";
BorderColor = type == ValidationItemType.Error ? "#FF6B6B" : "#FFB84D";
}
public string Message { get; }
public string Source { get; }
public ValidationItemType Type { get; }
public string Icon { get; }
public string IconColor { get; }
public string Background { get; }
public string BorderColor { get; }
}
Step 2: Create ValidationResultsDialog.axaml
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:JdeScoping.ConfigManager.ViewModels.Dialogs"
x:Class="JdeScoping.ConfigManager.Views.Dialogs.ValidationResultsDialog"
x:DataType="vm:ValidationResultsDialogViewModel"
Title="Validation Results"
Width="600" Height="500"
MinWidth="400" MinHeight="300"
Background="#151920"
WindowStartupLocation="CenterOwner">
<DockPanel>
<!-- Header -->
<Border DockPanel.Dock="Top" Background="#1C2128" Padding="24,16"
BorderBrush="#2D3540" BorderThickness="0,0,0,1">
<StackPanel>
<TextBlock Text="Validation Results"
Foreground="#E6EDF5" FontSize="18" FontWeight="SemiBold"/>
<StackPanel Orientation="Horizontal" Margin="0,8,0,0" Spacing="16">
<TextBlock Foreground="#FF6B6B" FontSize="12">
<Run Text="{Binding ErrorCount}"/> errors
</TextBlock>
<TextBlock Foreground="#FFB84D" FontSize="12">
<Run Text="{Binding WarningCount}"/> warnings
</TextBlock>
</StackPanel>
</StackPanel>
</Border>
<!-- Footer -->
<Border DockPanel.Dock="Bottom" Background="#1C2128" Padding="24,16"
BorderBrush="#2D3540" BorderThickness="0,1,0,0">
<Button Content="Close" Command="{Binding CloseCommand}"
HorizontalAlignment="Right"
Background="#5C9AFF" Foreground="#0D0F12"
Padding="16,8" FontWeight="Medium"/>
</Border>
<!-- Content -->
<ScrollViewer Background="#0D0F12" Padding="16">
<ItemsControl ItemsSource="{Binding Items}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border Background="{Binding Background}"
BorderBrush="{Binding BorderColor}"
BorderThickness="3,0,0,0"
Margin="0,0,0,8"
Padding="12">
<StackPanel>
<StackPanel Orientation="Horizontal" Spacing="8">
<TextBlock Text="{Binding Icon}"
Foreground="{Binding IconColor}"
FontSize="14"/>
<TextBlock Text="{Binding Source}"
Foreground="#5C6A7A"
FontFamily="JetBrains Mono" FontSize="11"/>
</StackPanel>
<TextBlock Text="{Binding Message}"
Foreground="#E6EDF5"
TextWrapping="Wrap"
Margin="22,4,0,0"/>
</StackPanel>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</DockPanel>
</Window>
Step 3: Create ValidationResultsDialog.axaml.cs
using Avalonia.Controls;
using JdeScoping.ConfigManager.ViewModels.Dialogs;
namespace JdeScoping.ConfigManager.Views.Dialogs;
public partial class ValidationResultsDialog : Window
{
public ValidationResultsDialog()
{
InitializeComponent();
}
public ValidationResultsDialog(ValidationResultsDialogViewModel viewModel) : this()
{
DataContext = viewModel;
viewModel.RequestClose = Close;
}
}
Step 4: Verify build
Run: dotnet build NEW/src/Utils/JdeScoping.ConfigManager/
Expected: Build succeeded
Step 5: Commit
git add NEW/src/Utils/JdeScoping.ConfigManager/Views/Dialogs/
git add NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/Dialogs/
git commit -m "feat(configmanager): add ValidationResultsDialog"
Phase 9: Polish & Integration
Task 24: Wire Up Form Selection in MainWindowViewModel
Files:
- Modify:
NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/MainWindowViewModel.cs - Create:
NEW/tests/JdeScoping.ConfigManager.Tests/ViewModels/MainWindowViewModelTests.cs
Step 1: Write the failing test
using JdeScoping.ConfigManager.Models;
using JdeScoping.ConfigManager.Services;
using JdeScoping.ConfigManager.ViewModels;
using JdeScoping.ConfigManager.ViewModels.Forms;
using Microsoft.Extensions.Logging;
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 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();
}
private MainWindowViewModel CreateViewModel()
{
return new MainWindowViewModel(
_fileSystem,
_configFileService,
_validationService,
_backupService,
_autoDiscoveryService,
_dialogService,
_logger);
}
}
Step 2: Run test to verify it fails
Run: dotnet test NEW/tests/JdeScoping.ConfigManager.Tests/ --filter "MainWindowViewModelTests"
Expected: FAIL - methods not implemented
Step 3: Update MainWindowViewModel with form selection logic
Add to MainWindowViewModel.cs:
// Add field
private readonly IDialogService _dialogService;
private readonly IFileSystem _fileSystem;
// Update constructor to include new services
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;
// ... existing command initialization
}
// Add test helper method
public void LoadConfigForTesting(ConfigModel? appSettings, PipelinesConfigModel? pipelines)
{
_appSettings = appSettings;
_pipelines = pipelines;
BuildTreeNodes();
}
// Update OnSelectedNodeChanged
private void OnSelectedNodeChanged()
{
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
};
}
private void MarkAsChanged()
{
HasUnsavedChanges = true;
if (_selectedNode != null)
_selectedNode.IsModified = true;
}
// Update OpenFolderAsync
private async Task OpenFolderAsync()
{
var folder = await _dialogService.ShowFolderPickerAsync("Select Configuration Folder");
if (folder != null)
{
await LoadConfigAsync(folder);
}
}
Step 4: Run test to verify it passes
Run: dotnet test NEW/tests/JdeScoping.ConfigManager.Tests/ --filter "MainWindowViewModelTests"
Expected: PASS
Step 5: Commit
git add NEW/src/Utils/JdeScoping.ConfigManager/
git add NEW/tests/JdeScoping.ConfigManager.Tests/
git commit -m "feat(configmanager): wire up form selection in MainWindowViewModel"
Task 25: Create Form Views (XAML)
Files:
- Create:
NEW/src/Utils/JdeScoping.ConfigManager/Views/Forms/DataSyncFormView.axaml - Create:
NEW/src/Utils/JdeScoping.ConfigManager/Views/Forms/DataSyncFormView.axaml.cs - Create:
NEW/src/Utils/JdeScoping.ConfigManager/Views/Forms/DataAccessFormView.axaml - Create:
NEW/src/Utils/JdeScoping.ConfigManager/Views/Forms/DataAccessFormView.axaml.cs - Create:
NEW/src/Utils/JdeScoping.ConfigManager/Views/Forms/AuthFormView.axaml - Create:
NEW/src/Utils/JdeScoping.ConfigManager/Views/Forms/AuthFormView.axaml.cs - Create:
NEW/src/Utils/JdeScoping.ConfigManager/Views/Forms/LdapFormView.axaml - Create:
NEW/src/Utils/JdeScoping.ConfigManager/Views/Forms/LdapFormView.axaml.cs - Create:
NEW/src/Utils/JdeScoping.ConfigManager/Views/Forms/SearchFormView.axaml - Create:
NEW/src/Utils/JdeScoping.ConfigManager/Views/Forms/SearchFormView.axaml.cs - Create:
NEW/src/Utils/JdeScoping.ConfigManager/Views/Forms/ExcelExportFormView.axaml - Create:
NEW/src/Utils/JdeScoping.ConfigManager/Views/Forms/ExcelExportFormView.axaml.cs - Create:
NEW/src/Utils/JdeScoping.ConfigManager/Views/Forms/PipelineFormView.axaml - Create:
NEW/src/Utils/JdeScoping.ConfigManager/Views/Forms/PipelineFormView.axaml.cs
Step 1: Create DataSyncFormView.axaml
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:JdeScoping.ConfigManager.ViewModels.Forms"
x:Class="JdeScoping.ConfigManager.Views.Forms.DataSyncFormView"
x:DataType="vm:DataSyncFormViewModel">
<StackPanel Spacing="16" MaxWidth="600">
<!-- Header -->
<TextBlock Text="Data Sync Options" FontSize="18" FontWeight="SemiBold"
Foreground="#E6EDF5" Margin="0,0,0,8"/>
<Border Height="1" Background="#2D3540" Margin="0,0,0,16"/>
<!-- Enabled Toggle -->
<StackPanel>
<CheckBox IsChecked="{Binding Enabled}" Content="Enable Data Synchronization"
Foreground="#E6EDF5"/>
</StackPanel>
<!-- Timing Section -->
<Border Background="#0D0F12" CornerRadius="6" Padding="16" BorderBrush="#2D3540" BorderThickness="1">
<StackPanel Spacing="12">
<TextBlock Text="Timing" FontWeight="SemiBold" Foreground="#E6EDF5"/>
<Grid ColumnDefinitions="*,*" RowDefinitions="Auto,Auto" RowSpacing="12" ColumnSpacing="16">
<StackPanel Grid.Row="0" Grid.Column="0">
<TextBlock Text="Check Interval (minutes)" Foreground="#9BA8B8" FontSize="12" Margin="0,0,0,4"/>
<NumericUpDown Value="{Binding CheckIntervalMinutes}" Minimum="1" Maximum="60"
Background="#232A35" Foreground="#E6EDF5"/>
</StackPanel>
<StackPanel Grid.Row="0" Grid.Column="1">
<TextBlock Text="Sync Timeout (seconds)" Foreground="#9BA8B8" FontSize="12" Margin="0,0,0,4"/>
<NumericUpDown Value="{Binding SyncTimeoutSeconds}" Minimum="60" Maximum="86400"
Background="#232A35" Foreground="#E6EDF5"/>
</StackPanel>
</Grid>
</StackPanel>
</Border>
<!-- Performance Section -->
<Border Background="#0D0F12" CornerRadius="6" Padding="16" BorderBrush="#2D3540" BorderThickness="1">
<StackPanel Spacing="12">
<TextBlock Text="Performance" FontWeight="SemiBold" Foreground="#E6EDF5"/>
<Grid ColumnDefinitions="*,*" RowDefinitions="Auto,Auto" RowSpacing="12" ColumnSpacing="16">
<StackPanel Grid.Row="0" Grid.Column="0">
<TextBlock Text="Max Parallelism" Foreground="#9BA8B8" FontSize="12" Margin="0,0,0,4"/>
<NumericUpDown Value="{Binding MaxDegreeOfParallelism}" Minimum="1" Maximum="32"
Background="#232A35" Foreground="#E6EDF5"/>
<TextBlock Text="1-32" Foreground="#5C6A7A" FontSize="11" Margin="0,4,0,0"/>
</StackPanel>
<StackPanel Grid.Row="0" Grid.Column="1">
<TextBlock Text="Batch Size" Foreground="#9BA8B8" FontSize="12" Margin="0,0,0,4"/>
<NumericUpDown Value="{Binding BatchSize}" Minimum="1000" Maximum="10000000"
Background="#232A35" Foreground="#E6EDF5"/>
</StackPanel>
<StackPanel Grid.Row="1" Grid.Column="0">
<TextBlock Text="Bulk Copy Batch Size" Foreground="#9BA8B8" FontSize="12" Margin="0,0,0,4"/>
<NumericUpDown Value="{Binding BulkCopyBatchSize}" Minimum="100" Maximum="100000"
Background="#232A35" Foreground="#E6EDF5"/>
</StackPanel>
</Grid>
</StackPanel>
</Border>
<!-- Data Retention Section -->
<Border Background="#0D0F12" CornerRadius="6" Padding="16" BorderBrush="#2D3540" BorderThickness="1">
<StackPanel Spacing="12">
<TextBlock Text="Data Retention" FontWeight="SemiBold" Foreground="#E6EDF5"/>
<Grid ColumnDefinitions="*,*" RowDefinitions="Auto" ColumnSpacing="16">
<StackPanel Grid.Column="0">
<TextBlock Text="Lookback Multiplier" Foreground="#9BA8B8" FontSize="12" Margin="0,0,0,4"/>
<NumericUpDown Value="{Binding LookbackMultiplier}" Minimum="1" Maximum="10" Increment="0.1"
Background="#232A35" Foreground="#E6EDF5"/>
</StackPanel>
<StackPanel Grid.Column="1">
<TextBlock Text="Purge Retention (days)" Foreground="#9BA8B8" FontSize="12" Margin="0,0,0,4"/>
<NumericUpDown Value="{Binding PurgeRetentionDays}" Minimum="1" Maximum="365"
Background="#232A35" Foreground="#E6EDF5"/>
</StackPanel>
</Grid>
</StackPanel>
</Border>
</StackPanel>
</UserControl>
Step 2: Create DataSyncFormView.axaml.cs
using Avalonia.Controls;
namespace JdeScoping.ConfigManager.Views.Forms;
public partial class DataSyncFormView : UserControl
{
public DataSyncFormView()
{
InitializeComponent();
}
}
Step 3: Create remaining form views following the same pattern
(Repeat for DataAccessFormView, AuthFormView, LdapFormView, SearchFormView, ExcelExportFormView, PipelineFormView - each with appropriate fields from the corresponding FormViewModel)
Step 4: Verify build
Run: dotnet build NEW/src/Utils/JdeScoping.ConfigManager/
Expected: Build succeeded
Step 5: Commit
git add NEW/src/Utils/JdeScoping.ConfigManager/Views/Forms/
git commit -m "feat(configmanager): add form views for all configuration sections"
Task 26: Add DataTemplates for Form Selection
Files:
- Modify:
NEW/src/Utils/JdeScoping.ConfigManager/Views/MainWindow.axaml
Step 1: Add DataTemplates to MainWindow.axaml
Add inside the Window before DockPanel:
<Window.DataTemplates>
<DataTemplate DataType="{x:Type forms:DataSyncFormViewModel}">
<views:DataSyncFormView/>
</DataTemplate>
<DataTemplate DataType="{x:Type forms:DataAccessFormViewModel}">
<views:DataAccessFormView/>
</DataTemplate>
<DataTemplate DataType="{x:Type forms:AuthFormViewModel}">
<views:AuthFormView/>
</DataTemplate>
<DataTemplate DataType="{x:Type forms:LdapFormViewModel}">
<views:LdapFormView/>
</DataTemplate>
<DataTemplate DataType="{x:Type forms:SearchFormViewModel}">
<views:SearchFormView/>
</DataTemplate>
<DataTemplate DataType="{x:Type forms:ExcelExportFormViewModel}">
<views:ExcelExportFormView/>
</DataTemplate>
<DataTemplate DataType="{x:Type forms:PipelineFormViewModel}">
<views:PipelineFormView/>
</DataTemplate>
</Window.DataTemplates>
Add namespaces:
xmlns:forms="using:JdeScoping.ConfigManager.ViewModels.Forms"
xmlns:views="using:JdeScoping.ConfigManager.Views.Forms"
Step 2: Verify build
Run: dotnet build NEW/src/Utils/JdeScoping.ConfigManager/
Expected: Build succeeded
Step 3: Commit
git add NEW/src/Utils/JdeScoping.ConfigManager/Views/MainWindow.axaml
git commit -m "feat(configmanager): add DataTemplates for form view selection"
Task 27: Update App.axaml.cs with All Service Registrations
Files:
- Modify:
NEW/src/Utils/JdeScoping.ConfigManager/App.axaml.cs
Step 1: Update ConfigureServices
private void ConfigureServices(IServiceCollection services)
{
// Logging
services.AddLogging(builder => builder
.AddConsole()
.SetMinimumLevel(LogLevel.Debug));
// Services
services.AddSingleton<IFileSystem, FileSystem>();
services.AddSingleton<IAutoDiscoveryService, AutoDiscoveryService>();
services.AddSingleton<IBackupService, BackupService>();
services.AddSingleton<IDiffService, DiffService>();
services.AddSingleton<IValidationService, ValidationService>();
services.AddScoped<IConfigFileService, ConfigFileService>();
// Platform Services
services.AddSingleton<IDialogService>(sp =>
new AvaloniaDialogService(GetMainWindow));
// ViewModels
services.AddTransient<MainWindowViewModel>();
}
private Window? GetMainWindow()
{
return (ApplicationLifetime as IClassicDesktopStyleApplicationLifetime)?.MainWindow;
}
Step 2: Update MainWindow creation
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
desktop.MainWindow = new Views.MainWindow
{
DataContext = Services.GetRequiredService<MainWindowViewModel>()
};
}
Step 3: Verify build
Run: dotnet build NEW/src/Utils/JdeScoping.ConfigManager/
Expected: Build succeeded
Step 4: Run all tests
Run: dotnet test NEW/tests/JdeScoping.ConfigManager.Tests/
Expected: All tests pass
Step 5: Commit
git add NEW/src/Utils/JdeScoping.ConfigManager/App.axaml.cs
git commit -m "feat(configmanager): complete service registrations in App.axaml.cs"
Task 28: Final Integration Test
Step 1: Run the application
Run: dotnet run --project NEW/src/Utils/JdeScoping.ConfigManager/
Expected: Application launches with dark theme UI
Step 2: Verify tree view appears
Expected: Settings and Pipelines folders visible with section nodes
Step 3: Verify form selection works
Click on DataSync node Expected: DataSync form appears with numeric inputs
Step 4: Run full test suite
Run: dotnet test NEW/tests/JdeScoping.ConfigManager.Tests/ -v normal
Expected: All tests pass
Step 5: Final commit
git add -A
git commit -m "feat(configmanager): complete Phases 7-9 implementation"
Summary
Phase 7 Tasks (14-20): 7 form ViewModels with TDD
- DataSyncFormViewModel
- DataAccessFormViewModel
- AuthFormViewModel
- LdapFormViewModel
- SearchFormViewModel
- ExcelExportFormViewModel
- PipelineFormViewModel + ScheduleFormViewModel
Phase 8 Tasks (21-23): 3 dialog components
- IDialogService + AvaloniaDialogService
- DiffPreviewDialog
- ValidationResultsDialog
Phase 9 Tasks (24-28): 5 integration tasks
- Wire up form selection in MainWindowViewModel
- Create XAML form views
- Add DataTemplates
- Update App.axaml.cs service registrations
- Final integration test
Total Tasks: 15 Estimated Commits: 15+
Key Patterns:
- TDD for all ViewModels
- Two-way binding with change tracking
- Action callback pattern for marking changes
- DataTemplate-based view selection
- Dialog service abstraction