Files
jdescopingtool/NEW/src/Utils/JdeScoping.ConfigManager.Ui/ViewModels/PipelineSteps/TransformerStepViewModels.cs
T
Joseph Doherty 1fc7792cd1 refactor(configmanager): rename UI project and split test projects
Rename ConfigManager to ConfigManager.Ui to match the Core/CLI/UI project
structure, and split the monolithic test project into Core.Tests,
Cli.Tests, and Ui.Tests to align with the source project organization.
2026-01-28 10:24:36 -05:00

787 lines
26 KiB
C#

using JdeScoping.DataSync.Configuration;
using System.Collections.ObjectModel;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Windows.Input;
namespace JdeScoping.ConfigManager.Ui.ViewModels.PipelineSteps;
/// <summary>
/// Base class for transformer step view models.
/// </summary>
public abstract class TransformerStepViewModelBase : PipelineStepViewModelBase
{
protected static readonly JsonSerializerOptions JsonOptions = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
/// <summary>
/// Initializes a new instance of the <see cref="TransformerStepViewModelBase"/> class.
/// </summary>
/// <param name="onChanged">Callback invoked when the transformer changes.</param>
protected TransformerStepViewModelBase(Action onChanged) : base(onChanged)
{
}
/// <inheritdoc />
public override PipelineStepType StepType => PipelineStepType.Transformer;
/// <inheritdoc />
public override string Icon => "󰁖"; // mdi-cog-transfer
/// <summary>
/// Gets the transformer type name.
/// </summary>
public abstract string TransformerType { get; }
/// <summary>
/// Converts this view model back to a TransformElement.
/// </summary>
public abstract TransformElement ToModel();
/// <summary>
/// Helper to create a JsonElement from an object.
/// </summary>
/// <param name="config">The object to serialize into a JsonElement.</param>
/// <returns>A JsonElement containing the serialized configuration.</returns>
protected static JsonElement CreateConfigElement(object config)
{
var json = JsonSerializer.Serialize(config, JsonOptions);
using var doc = JsonDocument.Parse(json);
return doc.RootElement.Clone();
}
}
/// <summary>
/// View model for ColumnDrop transformer.
/// </summary>
public class ColumnDropTransformerViewModel : TransformerStepViewModelBase
{
private string _columnsText;
/// <summary>
/// Initializes a new instance of the <see cref="ColumnDropTransformerViewModel"/> class with an existing configuration.
/// </summary>
/// <param name="element">The transform element containing the column configuration.</param>
/// <param name="onChanged">Callback invoked when the configuration changes.</param>
public ColumnDropTransformerViewModel(TransformElement element, Action onChanged) : base(onChanged)
{
_columnsText = string.Empty;
if (element.Config.HasValue)
{
if (element.Config.Value.TryGetProperty("columns", out var columnsProp) &&
columnsProp.ValueKind == JsonValueKind.Array)
{
var columns = columnsProp.EnumerateArray().Select(c => c.GetString() ?? "").Where(c => !string.IsNullOrEmpty(c));
_columnsText = string.Join("\n", columns);
}
}
}
/// <summary>
/// Initializes a new instance of the <see cref="ColumnDropTransformerViewModel"/> class with default values.
/// </summary>
/// <param name="onChanged">Callback invoked when the configuration changes.</param>
public ColumnDropTransformerViewModel(Action onChanged) : base(onChanged)
{
_columnsText = string.Empty;
}
/// <inheritdoc />
public override string TransformerType => "ColumnDrop";
/// <inheritdoc />
public override string DisplayName => "Column Drop";
/// <inheritdoc />
public override string Summary => GetColumnCount() > 0 ? $"Drop {GetColumnCount()} columns" : "No columns";
/// <summary>
/// Gets or sets the columns to drop as newline-separated text.
/// </summary>
public string ColumnsText
{
get => _columnsText;
set
{
if (SetProperty(ref _columnsText, value ?? string.Empty))
{
OnPropertyChanged(nameof(Summary));
NotifyChanged();
}
}
}
/// <summary>
/// Gets the columns as a list.
/// </summary>
public List<string> GetColumns()
{
return _columnsText.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).ToList();
}
private int GetColumnCount()
{
return _columnsText.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).Length;
}
/// <inheritdoc />
public override TransformElement ToModel() => new()
{
TransformType = TransformerType,
Config = CreateConfigElement(new { columns = GetColumns() })
};
}
/// <summary>
/// View model for ColumnRename transformer.
/// </summary>
public class ColumnRenameTransformerViewModel : TransformerStepViewModelBase
{
/// <summary>
/// Initializes a new instance of the <see cref="ColumnRenameTransformerViewModel"/> class with an existing configuration.
/// </summary>
/// <param name="element">The transform element containing the mapping configuration.</param>
/// <param name="onChanged">Callback invoked when the configuration changes.</param>
public ColumnRenameTransformerViewModel(TransformElement element, Action onChanged) : base(onChanged)
{
Mappings = [];
if (element.Config.HasValue)
{
if (element.Config.Value.TryGetProperty("mappings", out var mappingsProp) &&
mappingsProp.ValueKind == JsonValueKind.Object)
{
foreach (var prop in mappingsProp.EnumerateObject())
{
var newName = prop.Value.GetString() ?? "";
Mappings.Add(new ColumnMappingViewModel(prop.Name, newName, () =>
{
OnPropertyChanged(nameof(Summary));
NotifyChanged();
}));
}
}
}
AddMappingCommand = new RelayCommand(AddMapping);
}
/// <summary>
/// Initializes a new instance of the <see cref="ColumnRenameTransformerViewModel"/> class with default values.
/// </summary>
/// <param name="onChanged">Callback invoked when the configuration changes.</param>
public ColumnRenameTransformerViewModel(Action onChanged) : base(onChanged)
{
Mappings = [];
AddMappingCommand = new RelayCommand(AddMapping);
}
/// <inheritdoc />
public override string TransformerType => "ColumnRename";
/// <inheritdoc />
public override string DisplayName => "Column Rename";
/// <inheritdoc />
public override string Summary => Mappings.Count > 0 ? $"Rename {Mappings.Count} columns" : "No mappings";
/// <summary>
/// Gets the collection of column mappings (old name -> new name).
/// </summary>
public ObservableCollection<ColumnMappingViewModel> Mappings { get; }
/// <summary>
/// Gets the command to add a new mapping.
/// </summary>
public ICommand AddMappingCommand { get; }
/// <summary>
/// Adds a new mapping.
/// </summary>
public void AddMapping()
{
Mappings.Add(new ColumnMappingViewModel("", "", () =>
{
OnPropertyChanged(nameof(Summary));
NotifyChanged();
}));
OnPropertyChanged(nameof(Summary));
NotifyChanged();
}
/// <summary>
/// Removes a mapping.
/// </summary>
/// <param name="mapping">The column mapping to remove.</param>
public void RemoveMapping(ColumnMappingViewModel mapping)
{
if (Mappings.Remove(mapping))
{
OnPropertyChanged(nameof(Summary));
NotifyChanged();
}
}
/// <inheritdoc />
public override TransformElement ToModel() => new()
{
TransformType = TransformerType,
Config = CreateConfigElement(new { mappings = Mappings.ToDictionary(m => m.OldName, m => m.NewName) })
};
}
/// <summary>
/// View model for a column rename mapping.
/// </summary>
public class ColumnMappingViewModel : ViewModelBase
{
private string _oldName;
private string _newName;
private readonly Action _onChanged;
/// <summary>
/// Initializes a new instance of the <see cref="ColumnMappingViewModel"/> class.
/// </summary>
/// <param name="oldName">The original column name.</param>
/// <param name="newName">The new column name.</param>
/// <param name="onChanged">Callback invoked when the mapping changes.</param>
public ColumnMappingViewModel(string oldName, string newName, Action onChanged)
{
_oldName = oldName;
_newName = newName;
_onChanged = onChanged;
}
/// <summary>
/// Gets or sets the original column name.
/// </summary>
public string OldName
{
get => _oldName;
set
{
if (SetProperty(ref _oldName, value ?? string.Empty))
_onChanged();
}
}
/// <summary>
/// Gets or sets the new column name.
/// </summary>
public string NewName
{
get => _newName;
set
{
if (SetProperty(ref _newName, value ?? string.Empty))
_onChanged();
}
}
}
/// <summary>
/// View model for JdeDate transformer (converts JDE Julian date/time to DateTime).
/// </summary>
public class JdeDateTransformerViewModel : TransformerStepViewModelBase
{
private string? _dateColumn;
private string? _timeColumn;
private string? _outputColumn;
/// <summary>
/// Initializes a new instance of the <see cref="JdeDateTransformerViewModel"/> class from a TransformElement.
/// </summary>
/// <param name="element">The transform element containing date/time column configuration.</param>
/// <param name="onChanged">Callback invoked when the configuration changes.</param>
public JdeDateTransformerViewModel(TransformElement element, Action onChanged) : base(onChanged)
{
_dateColumn = null;
_timeColumn = null;
_outputColumn = null;
if (element.Config.HasValue)
{
if (element.Config.Value.TryGetProperty("dateColumn", out var dateProp))
_dateColumn = dateProp.GetString();
if (element.Config.Value.TryGetProperty("timeColumn", out var timeProp))
_timeColumn = timeProp.GetString();
if (element.Config.Value.TryGetProperty("outputColumn", out var outputProp))
_outputColumn = outputProp.GetString();
}
}
/// <summary>
/// Initializes a new instance of the <see cref="JdeDateTransformerViewModel"/> class with default values.
/// </summary>
/// <param name="onChanged">Callback invoked when the configuration changes.</param>
public JdeDateTransformerViewModel(Action onChanged) : base(onChanged)
{
_dateColumn = null;
_timeColumn = null;
_outputColumn = null;
}
/// <inheritdoc />
public override string TransformerType => "JdeDate";
/// <inheritdoc />
public override string DisplayName => "JDE Date Convert";
/// <inheritdoc />
public override string Icon => "󰃭"; // mdi-calendar
/// <inheritdoc />
public override string Summary => !string.IsNullOrEmpty(_outputColumn) ? $"→ {_outputColumn}" : "Configure...";
/// <summary>
/// Gets or sets the date column name.
/// </summary>
public string? DateColumn
{
get => _dateColumn;
set
{
if (SetProperty(ref _dateColumn, value))
{
OnPropertyChanged(nameof(Summary));
NotifyChanged();
}
}
}
/// <summary>
/// Gets or sets the time column name.
/// </summary>
public string? TimeColumn
{
get => _timeColumn;
set
{
if (SetProperty(ref _timeColumn, value))
NotifyChanged();
}
}
/// <summary>
/// Gets or sets the output column name.
/// </summary>
public string? OutputColumn
{
get => _outputColumn;
set
{
if (SetProperty(ref _outputColumn, value))
{
OnPropertyChanged(nameof(Summary));
NotifyChanged();
}
}
}
/// <inheritdoc />
public override TransformElement ToModel() => new()
{
TransformType = TransformerType,
Config = CreateConfigElement(new { dateColumn = _dateColumn, timeColumn = _timeColumn, outputColumn = _outputColumn })
};
}
/// <summary>
/// Specifies behavior when a regex pattern does not match the input value.
/// </summary>
public enum NonMatchBehavior
{
/// <summary>Keep the original value unchanged.</summary>
KeepOriginal,
/// <summary>Return null/DBNull.</summary>
ReturnNull,
/// <summary>Return an empty string.</summary>
ReturnEmpty
}
/// <summary>
/// View model for Regex transformer.
/// </summary>
public class RegexTransformerViewModel : TransformerStepViewModelBase
{
private string _columnName = string.Empty;
private string _pattern = string.Empty;
private string? _replacement = string.Empty;
private bool _isFindReplaceMode = true;
private bool _ignoreCase;
private NonMatchBehavior _nonMatchBehavior = NonMatchBehavior.KeepOriginal;
// Test feature fields
private string _testInput = string.Empty;
private string _testResultValue = string.Empty;
private string _testResultLabel = string.Empty;
private string _testResultIcon = string.Empty;
private string _testResultBackground = string.Empty;
private bool _hasTestResult;
private bool _hasTestError;
private string _testErrorMessage = string.Empty;
/// <summary>
/// Initializes a new instance of the <see cref="RegexTransformerViewModel"/> class from a TransformElement.
/// </summary>
/// <param name="element">The transform element containing regex configuration.</param>
/// <param name="onChanged">Callback invoked when the configuration changes.</param>
public RegexTransformerViewModel(TransformElement element, Action onChanged) : base(onChanged)
{
_columnName = string.Empty;
_pattern = string.Empty;
_replacement = null;
_isFindReplaceMode = true;
_ignoreCase = false;
_nonMatchBehavior = NonMatchBehavior.KeepOriginal;
if (element.Config.HasValue)
{
if (element.Config.Value.TryGetProperty("columnName", out var colProp))
_columnName = colProp.GetString() ?? string.Empty;
if (element.Config.Value.TryGetProperty("pattern", out var patternProp))
_pattern = patternProp.GetString() ?? string.Empty;
if (element.Config.Value.TryGetProperty("replacement", out var replaceProp))
{
_replacement = replaceProp.ValueKind == JsonValueKind.Null ? null : replaceProp.GetString();
_isFindReplaceMode = _replacement != null;
}
else
{
// No replacement property means Match & Extract mode
_replacement = null;
_isFindReplaceMode = false;
}
if (element.Config.Value.TryGetProperty("ignoreCase", out var ignoreProp))
_ignoreCase = ignoreProp.GetBoolean();
if (element.Config.Value.TryGetProperty("nonMatchBehavior", out var behaviorProp))
{
var behaviorStr = behaviorProp.GetString();
if (Enum.TryParse<NonMatchBehavior>(behaviorStr, true, out var behavior))
_nonMatchBehavior = behavior;
}
}
TestPatternCommand = new RelayCommand(ExecuteTestPattern);
}
/// <summary>
/// Initializes a new instance of the <see cref="RegexTransformerViewModel"/> class with default values.
/// </summary>
/// <param name="onChanged">Callback invoked when the configuration changes.</param>
public RegexTransformerViewModel(Action onChanged) : base(onChanged)
{
TestPatternCommand = new RelayCommand(ExecuteTestPattern);
}
/// <inheritdoc />
public override string TransformerType => "Regex";
/// <inheritdoc />
public override string DisplayName => "Regex Transform";
/// <inheritdoc />
public override string Icon => "󰑑"; // mdi-regex
/// <inheritdoc />
public override string Summary => !string.IsNullOrEmpty(_columnName)
? $"{_columnName}: {(_isFindReplaceMode ? "Replace" : "Extract")}"
: "Configure...";
/// <summary>Gets or sets the column name to transform.</summary>
public string ColumnName
{
get => _columnName;
set
{
if (SetProperty(ref _columnName, value ?? string.Empty))
{
OnPropertyChanged(nameof(Summary));
NotifyChanged();
}
}
}
/// <summary>Gets or sets the regex pattern.</summary>
public string Pattern
{
get => _pattern;
set
{
if (SetProperty(ref _pattern, value ?? string.Empty))
{
ClearTestResult();
NotifyChanged();
}
}
}
/// <summary>Gets or sets the replacement string (Find &amp; Replace mode).</summary>
public string? Replacement
{
get => _replacement;
set
{
if (SetProperty(ref _replacement, value))
{
ClearTestResult();
NotifyChanged();
}
}
}
/// <summary>Gets or sets whether Find &amp; Replace mode is active.</summary>
public bool IsFindReplaceMode
{
get => _isFindReplaceMode;
set
{
if (SetProperty(ref _isFindReplaceMode, value))
{
OnPropertyChanged(nameof(IsMatchExtractMode));
OnPropertyChanged(nameof(PatternHelpText));
OnPropertyChanged(nameof(Summary));
ClearTestResult();
NotifyChanged();
}
}
}
/// <summary>Gets or sets whether Match &amp; Extract mode is active.</summary>
public bool IsMatchExtractMode
{
get => !_isFindReplaceMode;
set => IsFindReplaceMode = !value;
}
/// <summary>Gets the help text for the pattern field based on current mode.</summary>
public string PatternHelpText => _isFindReplaceMode
? "Pattern to search for in the column value"
: "Pattern with capture group - first group (parentheses) will be extracted";
/// <summary>Gets or sets whether matching is case-insensitive.</summary>
public bool IgnoreCase
{
get => _ignoreCase;
set
{
if (SetProperty(ref _ignoreCase, value))
{
ClearTestResult();
NotifyChanged();
}
}
}
/// <summary>Gets or sets the behavior when pattern doesn't match.</summary>
public NonMatchBehavior NonMatchBehavior
{
get => _nonMatchBehavior;
set
{
if (SetProperty(ref _nonMatchBehavior, value))
{
ClearTestResult();
NotifyChanged();
}
}
}
/// <summary>Gets the available non-match behavior options for binding.</summary>
public static IReadOnlyList<NonMatchBehavior> NonMatchBehaviorOptions { get; } =
[NonMatchBehavior.KeepOriginal, NonMatchBehavior.ReturnNull, NonMatchBehavior.ReturnEmpty];
// Test feature properties
/// <summary>
/// Gets or sets the test input value for pattern testing.
/// </summary>
public string TestInput
{
get => _testInput;
set => SetProperty(ref _testInput, value ?? string.Empty);
}
/// <summary>
/// Gets or sets the result value from pattern testing.
/// </summary>
public string TestResultValue
{
get => _testResultValue;
set => SetProperty(ref _testResultValue, value);
}
/// <summary>
/// Gets or sets the label describing the test result (e.g., "Output" or "No Match").
/// </summary>
public string TestResultLabel
{
get => _testResultLabel;
set => SetProperty(ref _testResultLabel, value);
}
/// <summary>
/// Gets or sets the icon for the test result.
/// </summary>
public string TestResultIcon
{
get => _testResultIcon;
set => SetProperty(ref _testResultIcon, value);
}
/// <summary>
/// Gets or sets the background color for the test result display.
/// </summary>
public string TestResultBackground
{
get => _testResultBackground;
set => SetProperty(ref _testResultBackground, value);
}
/// <summary>
/// Gets or sets a value indicating whether a test result is available.
/// </summary>
public bool HasTestResult
{
get => _hasTestResult;
set => SetProperty(ref _hasTestResult, value);
}
/// <summary>
/// Gets or sets a value indicating whether a test error occurred.
/// </summary>
public bool HasTestError
{
get => _hasTestError;
set => SetProperty(ref _hasTestError, value);
}
/// <summary>
/// Gets or sets the error message from test execution.
/// </summary>
public string TestErrorMessage
{
get => _testErrorMessage;
set => SetProperty(ref _testErrorMessage, value);
}
/// <summary>
/// Gets the command to execute pattern testing.
/// </summary>
public ICommand TestPatternCommand { get; }
private void ExecuteTestPattern()
{
ClearTestResult();
if (string.IsNullOrEmpty(_pattern))
{
HasTestError = true;
TestErrorMessage = "Pattern is required";
return;
}
try
{
var options = _ignoreCase ? RegexOptions.IgnoreCase : RegexOptions.None;
var regex = new Regex(_pattern, options);
string result;
bool matched;
if (_isFindReplaceMode)
{
result = regex.Replace(_testInput, _replacement ?? string.Empty);
matched = regex.IsMatch(_testInput);
}
else
{
var match = regex.Match(_testInput);
if (match.Success && match.Groups.Count > 1)
{
result = match.Groups[1].Value;
matched = true;
}
else
{
matched = false;
result = _nonMatchBehavior switch
{
NonMatchBehavior.ReturnNull => "(null)",
NonMatchBehavior.ReturnEmpty => "(empty)",
_ => _testInput
};
}
}
HasTestResult = true;
TestResultValue = result;
TestResultLabel = matched ? "Output" : "No Match";
TestResultIcon = matched ? "✓" : "—";
TestResultBackground = matched ? "#22C55E" : "#F59E0B";
}
catch (RegexParseException ex)
{
HasTestError = true;
TestErrorMessage = ex.Message;
}
}
private void ClearTestResult()
{
HasTestResult = false;
HasTestError = false;
TestResultValue = string.Empty;
TestResultLabel = string.Empty;
TestErrorMessage = string.Empty;
}
/// <inheritdoc />
public override TransformElement ToModel() => new()
{
TransformType = TransformerType,
Config = CreateConfigElement(new
{
columnName = _columnName,
pattern = _pattern,
replacement = _isFindReplaceMode ? _replacement : null,
ignoreCase = _ignoreCase,
nonMatchBehavior = _nonMatchBehavior.ToString()
})
};
}
/// <summary>
/// Factory methods for creating transformer view models.
/// </summary>
public static class TransformerFactory
{
/// <summary>
/// Creates a transformer view model from a TransformElement.
/// </summary>
/// <param name="element">The transform element containing configuration.</param>
/// <param name="onChanged">Callback invoked when the view model changes.</param>
/// <returns>A transformer view model of the appropriate type, or null if the transformer type is unknown.</returns>
public static TransformerStepViewModelBase? Create(TransformElement element, Action onChanged)
{
return element.TransformType?.ToLowerInvariant() switch
{
"columndrop" => new ColumnDropTransformerViewModel(element, onChanged),
"columnrename" => new ColumnRenameTransformerViewModel(element, onChanged),
"jdedate" => new JdeDateTransformerViewModel(element, onChanged),
"regex" => new RegexTransformerViewModel(element, onChanged),
_ => null // Unknown transformer type
};
}
/// <summary>
/// Creates a new transformer view model by type name.
/// </summary>
/// <param name="typeName">The type name of the transformer to create.</param>
/// <param name="onChanged">Callback invoked when the view model changes.</param>
/// <returns>A new transformer view model of the specified type, or null if the type name is unknown.</returns>
public static TransformerStepViewModelBase? CreateNew(string typeName, Action onChanged)
{
return typeName?.ToLowerInvariant() switch
{
"columndrop" => new ColumnDropTransformerViewModel(onChanged),
"columnrename" => new ColumnRenameTransformerViewModel(onChanged),
"jdedate" => new JdeDateTransformerViewModel(onChanged),
"regex" => new RegexTransformerViewModel(onChanged),
_ => null
};
}
/// <summary>
/// Gets the list of available transformer type names.
/// </summary>
public static IReadOnlyList<string> AvailableTypes => ["ColumnDrop", "ColumnRename", "JdeDate", "Regex"];
}