feat(configmanager): add PipelineFormViewModel and ScheduleFormViewModel
Add form ViewModels for editing pipeline configurations in the ConfigManager. ScheduleFormViewModel wraps ScheduleModel for schedule editing. PipelineFormViewModel wraps PipelineModel with schedule sub-ViewModels.
This commit is contained in:
@@ -0,0 +1,172 @@
|
|||||||
|
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; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+287
@@ -0,0 +1,287 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void NullSchedules_AreInitializedToDefault()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var model = new PipelineModel
|
||||||
|
{
|
||||||
|
Schedules = new PipelineSchedules
|
||||||
|
{
|
||||||
|
Mass = null,
|
||||||
|
Daily = null,
|
||||||
|
Hourly = null
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var sut = new PipelineFormViewModel("Test", model, () => { });
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
sut.MassSchedule.ShouldNotBeNull();
|
||||||
|
sut.DailySchedule.ShouldNotBeNull();
|
||||||
|
sut.HourlySchedule.ShouldNotBeNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ExcludeFromUpdateText_JoinsAndSplits()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var model = new PipelineModel
|
||||||
|
{
|
||||||
|
Destination = new PipelineDestination
|
||||||
|
{
|
||||||
|
ExcludeFromUpdate = ["CreatedDate", "ModifiedDate"]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var sut = new PipelineFormViewModel("Test", model, () => { });
|
||||||
|
|
||||||
|
// Assert - verify getter joins correctly
|
||||||
|
sut.ExcludeFromUpdateText.ShouldBe("CreatedDate\nModifiedDate");
|
||||||
|
|
||||||
|
// Act - verify setter splits correctly
|
||||||
|
sut.ExcludeFromUpdateText = "Col1\nCol2";
|
||||||
|
model.Destination.ExcludeFromUpdate.Length.ShouldBe(2);
|
||||||
|
model.Destination.ExcludeFromUpdate[0].ShouldBe("Col1");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PostScriptsText_HandlesNullable()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var model = new PipelineModel { PostScripts = null };
|
||||||
|
var sut = new PipelineFormViewModel("Test", model, () => { });
|
||||||
|
|
||||||
|
// Assert - null should return empty string
|
||||||
|
sut.PostScriptsText.ShouldBe(string.Empty);
|
||||||
|
|
||||||
|
// Act - set some scripts
|
||||||
|
sut.PostScriptsText = "script1.sql\nscript2.sql";
|
||||||
|
model.PostScripts.ShouldNotBeNull();
|
||||||
|
model.PostScripts!.Length.ShouldBe(2);
|
||||||
|
|
||||||
|
// Act - clear scripts by setting empty
|
||||||
|
sut.PostScriptsText = "";
|
||||||
|
model.PostScripts.ShouldBeNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MassQuery_Property_ReadsAndWrites()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var model = new PipelineModel
|
||||||
|
{
|
||||||
|
Source = new PipelineSource
|
||||||
|
{
|
||||||
|
MassQuery = "SELECT * FROM Test WHERE All = 1"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var changedInvoked = false;
|
||||||
|
var sut = new PipelineFormViewModel("Test", model, () => changedInvoked = true);
|
||||||
|
|
||||||
|
// Assert - verify getter
|
||||||
|
sut.MassQuery.ShouldBe("SELECT * FROM Test WHERE All = 1");
|
||||||
|
|
||||||
|
// Act - verify setter
|
||||||
|
sut.MassQuery = "SELECT * FROM NewTable";
|
||||||
|
model.Source.MassQuery.ShouldBe("SELECT * FROM NewTable");
|
||||||
|
changedInvoked.ShouldBeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PropertyChange_RaisesPropertyChanged()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var model = new PipelineModel();
|
||||||
|
var sut = new PipelineFormViewModel("Test", model, () => { });
|
||||||
|
var propertyChangedRaised = false;
|
||||||
|
sut.PropertyChanged += (s, e) =>
|
||||||
|
{
|
||||||
|
if (e.PropertyName == nameof(PipelineFormViewModel.Query))
|
||||||
|
propertyChangedRaised = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
sut.Query = "SELECT * FROM NewQuery";
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
propertyChangedRaised.ShouldBeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ScheduleChange_InvokesOnChanged()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var model = new PipelineModel();
|
||||||
|
var changedInvoked = false;
|
||||||
|
var sut = new PipelineFormViewModel("Test", model, () => changedInvoked = true);
|
||||||
|
|
||||||
|
// Act - change schedule property (Enabled defaults to true, so set to false)
|
||||||
|
sut.MassSchedule.Enabled = false;
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
changedInvoked.ShouldBeTrue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ScheduleFormViewModelTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void Constructor_InitializesFromModel()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var model = new ScheduleModel
|
||||||
|
{
|
||||||
|
Enabled = true,
|
||||||
|
IntervalMinutes = 1440,
|
||||||
|
PrePurge = true,
|
||||||
|
ReIndex = false
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var sut = new ScheduleFormViewModel(model, () => { });
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
sut.Enabled.ShouldBeTrue();
|
||||||
|
sut.IntervalMinutes.ShouldBe(1440);
|
||||||
|
sut.PrePurge.ShouldBeTrue();
|
||||||
|
sut.ReIndex.ShouldBeFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PropertyChange_UpdatesModelAndInvokesOnChanged()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var model = new ScheduleModel();
|
||||||
|
var changedInvoked = false;
|
||||||
|
var sut = new ScheduleFormViewModel(model, () => changedInvoked = true);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
sut.IntervalMinutes = 120;
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
model.IntervalMinutes.ShouldBe(120);
|
||||||
|
changedInvoked.ShouldBeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PropertyChange_RaisesPropertyChanged()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var model = new ScheduleModel();
|
||||||
|
var sut = new ScheduleFormViewModel(model, () => { });
|
||||||
|
var propertyChangedRaised = false;
|
||||||
|
sut.PropertyChanged += (s, e) =>
|
||||||
|
{
|
||||||
|
if (e.PropertyName == nameof(ScheduleFormViewModel.PrePurge))
|
||||||
|
propertyChangedRaised = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
sut.PrePurge = true;
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
propertyChangedRaised.ShouldBeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SameValue_DoesNotInvokeOnChanged()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var model = new ScheduleModel { Enabled = true };
|
||||||
|
var changedInvoked = false;
|
||||||
|
var sut = new ScheduleFormViewModel(model, () => changedInvoked = true);
|
||||||
|
|
||||||
|
// Act - set same value
|
||||||
|
sut.Enabled = true;
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
changedInvoked.ShouldBeFalse();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user