d49330e697
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.
2707 lines
82 KiB
Markdown
2707 lines
82 KiB
Markdown
# 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**
|
|
|
|
```csharp
|
|
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**
|
|
|
|
```csharp
|
|
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**
|
|
|
|
```bash
|
|
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**
|
|
|
|
```csharp
|
|
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**
|
|
|
|
```csharp
|
|
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**
|
|
|
|
```bash
|
|
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**
|
|
|
|
```csharp
|
|
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**
|
|
|
|
```csharp
|
|
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**
|
|
|
|
```bash
|
|
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**
|
|
|
|
```csharp
|
|
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**
|
|
|
|
```csharp
|
|
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**
|
|
|
|
```bash
|
|
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**
|
|
|
|
```csharp
|
|
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**
|
|
|
|
```csharp
|
|
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**
|
|
|
|
```bash
|
|
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**
|
|
|
|
```csharp
|
|
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**
|
|
|
|
```csharp
|
|
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**
|
|
|
|
```bash
|
|
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**
|
|
|
|
```csharp
|
|
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**
|
|
|
|
```csharp
|
|
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**
|
|
|
|
```csharp
|
|
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**
|
|
|
|
```bash
|
|
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**
|
|
|
|
```csharp
|
|
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**
|
|
|
|
```csharp
|
|
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**
|
|
|
|
```bash
|
|
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**
|
|
|
|
```csharp
|
|
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**
|
|
|
|
```xml
|
|
<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**
|
|
|
|
```csharp
|
|
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**
|
|
|
|
```bash
|
|
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**
|
|
|
|
```csharp
|
|
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**
|
|
|
|
```xml
|
|
<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**
|
|
|
|
```csharp
|
|
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**
|
|
|
|
```bash
|
|
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**
|
|
|
|
```csharp
|
|
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:
|
|
|
|
```csharp
|
|
// 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**
|
|
|
|
```bash
|
|
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**
|
|
|
|
```xml
|
|
<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**
|
|
|
|
```csharp
|
|
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**
|
|
|
|
```bash
|
|
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:
|
|
|
|
```xml
|
|
<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:
|
|
|
|
```xml
|
|
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**
|
|
|
|
```bash
|
|
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**
|
|
|
|
```csharp
|
|
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**
|
|
|
|
```csharp
|
|
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**
|
|
|
|
```bash
|
|
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**
|
|
|
|
```bash
|
|
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
|