From 66f20d0d82398c7cc45598c52fe6eae313cb5928 Mon Sep 17 00:00:00 2001 From: Nate Bross Date: Mon, 20 Apr 2026 21:20:08 -0500 Subject: [PATCH 01/38] feat(scripting): add typed step registry with IStepFactory contract Introduces StepRegistry, IStepFactory, StepMetadata, ParamMetadata, StepNotes, and StepPlatformNotes. The registry discovers step POCOs via reflection at assembly load, indexes them by name and id, and bridges their factory delegates into the legacy StepXmlFactory and StepDisplayFactory surfaces. No consumers read from the registry yet; this commit only introduces the infrastructure. Pilot POCOs and consumer rewires land next. --- .../Scripting/Registry/IStepFactory.cs | 20 +++ .../Scripting/Registry/ParamMetadata.cs | 49 +++++++ .../Scripting/Registry/StepMetadata.cs | 67 +++++++++ .../Scripting/Registry/StepNotes.cs | 26 ++++ .../Scripting/Registry/StepPlatformNotes.cs | 14 ++ .../Scripting/Registry/StepRegistry.cs | 135 ++++++++++++++++++ 6 files changed, 311 insertions(+) create mode 100644 src/SharpFM.Model/Scripting/Registry/IStepFactory.cs create mode 100644 src/SharpFM.Model/Scripting/Registry/ParamMetadata.cs create mode 100644 src/SharpFM.Model/Scripting/Registry/StepMetadata.cs create mode 100644 src/SharpFM.Model/Scripting/Registry/StepNotes.cs create mode 100644 src/SharpFM.Model/Scripting/Registry/StepPlatformNotes.cs create mode 100644 src/SharpFM.Model/Scripting/Registry/StepRegistry.cs diff --git a/src/SharpFM.Model/Scripting/Registry/IStepFactory.cs b/src/SharpFM.Model/Scripting/Registry/IStepFactory.cs new file mode 100644 index 0000000..8d20a3b --- /dev/null +++ b/src/SharpFM.Model/Scripting/Registry/IStepFactory.cs @@ -0,0 +1,20 @@ +namespace SharpFM.Model.Scripting.Registry; + +/// +/// Factory contract for typed script-step POCOs. A class that implements +/// this interface participates in discovery: +/// the registry reflects for implementors at first access and reads the +/// static property from each one. +/// +/// +/// Uses a static-abstract member so the registration surface is enforced +/// at compile time — a POCO claiming without a +/// static Metadata property fails to build. Contrast with a runtime +/// convention (reflect for a field named "Metadata") which would fail +/// silently when the member is missing or misnamed. +/// +/// +public interface IStepFactory +{ + static abstract StepMetadata Metadata { get; } +} diff --git a/src/SharpFM.Model/Scripting/Registry/ParamMetadata.cs b/src/SharpFM.Model/Scripting/Registry/ParamMetadata.cs new file mode 100644 index 0000000..9c22730 --- /dev/null +++ b/src/SharpFM.Model/Scripting/Registry/ParamMetadata.cs @@ -0,0 +1,49 @@ +using System.Collections.Generic; + +namespace SharpFM.Model.Scripting.Registry; + +/// +/// Typed description of a single script-step parameter, owned by the +/// parent POCO's . Intentionally leaner than +/// the legacy StepParam — fields that only catalog-driven helpers +/// consumed (flagStyle, hrValues, hrEnumValues, +/// wrapperElement, parentElement, invertedHr, +/// monacoSnippet, snippetFile, status) are dropped. +/// Add them back per-POCO if a concrete step needs them. +/// +public sealed record ParamMetadata +{ + /// Short canonical identifier for the parameter. + public required string Name { get; init; } + + /// XML element name emitted for this parameter. + public required string XmlElement { get; init; } + + /// + /// Parameter type classifier — e.g. "boolean", "enum", + /// "calculation", "text". Informs display formatting and + /// intellisense behaviour. + /// + public required string Type { get; init; } + + /// + /// Optional XML attribute name (e.g. "state", "value") when the + /// parameter's value lives on an attribute rather than element text. + /// + public string? XmlAttr { get; init; } + + /// Human-readable label shown in display text and completion. + public string? HrLabel { get; init; } + + /// Tooltip text for hover UIs, drawn from upstream snippet comments. + public string? Description { get; init; } + + /// Closed set of permissible values for enum / boolean params. + public IReadOnlyList? ValidValues { get; init; } + + /// Default value emitted when the parameter is omitted. + public string? DefaultValue { get; init; } + + /// True when the parameter must be present in valid XML. + public bool Required { get; init; } +} diff --git a/src/SharpFM.Model/Scripting/Registry/StepMetadata.cs b/src/SharpFM.Model/Scripting/Registry/StepMetadata.cs new file mode 100644 index 0000000..d7efd5c --- /dev/null +++ b/src/SharpFM.Model/Scripting/Registry/StepMetadata.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using System.Xml.Linq; + +namespace SharpFM.Model.Scripting.Registry; + +/// +/// Typed self-description of a script-step POCO. Every POCO that +/// implements exposes one of these as its +/// static Metadata property, and +/// discovers them via reflection at first access. +/// +/// +/// The factory delegates (, +/// ) live on the metadata so the registry can +/// bridge them into the legacy StepXmlFactory and +/// StepDisplayFactory surfaces without touching the POCO's +/// declaration site. Once the legacy surfaces are retired the delegates +/// become the sole construction path. +/// +/// +public sealed record StepMetadata +{ + /// Canonical FileMaker step name, e.g. "Set Error Capture". + public required string Name { get; init; } + + /// Numeric step id FileMaker emits in <Step id="..."/>. + public required int Id { get; init; } + + /// Category grouping used by completion UIs (e.g. "control"). + public required string Category { get; init; } + + /// Claris help URL for this step. + public string? HelpUrl { get; init; } + + /// + /// One-line display summary shown alongside the step name in + /// completion — e.g. "[ condition ]" for If. + /// + public string? HrSignature { get; init; } + + /// + /// Block-pair information for control-flow steps (If/End If, Loop/End + /// Loop, etc.). Null for standalone steps. + /// + public StepBlockPair? BlockPair { get; init; } + + /// Typed descriptions of the step's parameters. + public IReadOnlyList Params { get; init; } = []; + + /// Behavioural intelligence — tooltip / lint source. + public StepNotes? Notes { get; init; } + + /// + /// Delegate that constructs a POCO instance from a source + /// <Step> element. Usually assigned via method-group + /// reference to the POCO's static FromXml method. + /// + public Func? FromXml { get; init; } + + /// + /// Delegate that constructs a POCO instance from parsed display-text + /// tokens. Usually assigned via method-group reference to the POCO's + /// static FromDisplayParams method. + /// + public Func? FromDisplay { get; init; } +} diff --git a/src/SharpFM.Model/Scripting/Registry/StepNotes.cs b/src/SharpFM.Model/Scripting/Registry/StepNotes.cs new file mode 100644 index 0000000..46b971b --- /dev/null +++ b/src/SharpFM.Model/Scripting/Registry/StepNotes.cs @@ -0,0 +1,26 @@ +namespace SharpFM.Model.Scripting.Registry; + +/// +/// Behavioural intelligence for a script step — the "what you should +/// know" content agents and tooltip UIs draw from. Mirrors the +/// structure of upstream agentic-fm's catalog notes object. +/// All fields are nullable; populate only when a POCO has real content +/// to contribute. +/// +public sealed record StepNotes +{ + /// General usage and side-effect behaviour. + public string? Behavioral { get; init; } + + /// Hard rules FileMaker enforces or silently breaks. + public string? Constraints { get; init; } + + /// Subtle behaviours that cause real-world bugs. + public string? Gotchas { get; init; } + + /// Performance guidance. + public string? Performance { get; init; } + + /// Per-runtime divergence from the standard FileMaker Pro client. + public StepPlatformNotes? Platform { get; init; } +} diff --git a/src/SharpFM.Model/Scripting/Registry/StepPlatformNotes.cs b/src/SharpFM.Model/Scripting/Registry/StepPlatformNotes.cs new file mode 100644 index 0000000..9bbcf13 --- /dev/null +++ b/src/SharpFM.Model/Scripting/Registry/StepPlatformNotes.cs @@ -0,0 +1,14 @@ +namespace SharpFM.Model.Scripting.Registry; + +/// +/// Platform-specific behavioural differences for a script step. Each +/// property holds a prose sentence describing how the step behaves on +/// that runtime, populated only where it differs from the standard +/// FileMaker Pro client. +/// +public sealed record StepPlatformNotes +{ + public string? Server { get; init; } + public string? WebDirect { get; init; } + public string? Go { get; init; } +} diff --git a/src/SharpFM.Model/Scripting/Registry/StepRegistry.cs b/src/SharpFM.Model/Scripting/Registry/StepRegistry.cs new file mode 100644 index 0000000..0bc64b0 --- /dev/null +++ b/src/SharpFM.Model/Scripting/Registry/StepRegistry.cs @@ -0,0 +1,135 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Threading; +using SharpFM.Model.Scripting.Serialization; + +namespace SharpFM.Model.Scripting.Registry; + +/// +/// Runtime registry of typed script-step POCOs. Populates itself on +/// first access (or an explicit call) by +/// reflecting over the model assembly for types that implement +/// and reading their static +/// property. +/// +/// +/// Replaces StepCatalogLoader as migration progresses. While both +/// systems coexist, the registry also bridges POCO factory +/// delegates into the legacy and +/// surfaces, so callers that read the +/// legacy factories see POCO-backed steps without each POCO needing its +/// own ModuleInitializer. +/// +/// +public static class StepRegistry +{ + private static readonly object _gate = new(); + private static bool _initialized; + private static readonly List _all = []; + private static readonly Dictionary _byName = + new(StringComparer.OrdinalIgnoreCase); + private static readonly Dictionary _byId = []; + + /// + /// All registered step metadata records, in discovery order. + /// + public static IReadOnlyList All + { + get { EnsureInitialized(); return _all; } + } + + /// Case-insensitive lookup by canonical step name. + public static IReadOnlyDictionary ByName + { + get { EnsureInitialized(); return _byName; } + } + + /// Lookup by numeric FileMaker step id. + public static IReadOnlyDictionary ById + { + get { EnsureInitialized(); return _byId; } + } + + /// + /// Idempotent explicit initialization. Call at app startup to land + /// the reflection cost at a predictable moment; otherwise the first + /// lookup triggers the same scan as a safety net. + /// + public static void Initialize() => EnsureInitialized(); + + /// + /// Fires the registry scan at assembly-load time so POCO factories + /// are bridged into the legacy / + /// surfaces before any consumer + /// touches them — matching the ordering guarantee the legacy + /// per-POCO [ModuleInitializer]s used to provide. The user + /// preference is "no module initializers on POCOs"; one on the + /// registry itself is the singular substitute. + /// + [SuppressMessage("Usage", "CA2255:The 'ModuleInitializer' attribute should not be used in libraries", + Justification = "Single initializer on the registry populates legacy factories in place of per-POCO initializers.")] + [ModuleInitializer] + internal static void ModuleInitialize() => EnsureInitialized(); + + /// + /// Surface valid values for a parameter in completion / validation + /// contexts. Mirrors the role of the legacy + /// ScriptValidator.GetValidValues(StepParam) for the typed + /// shape. + /// + public static IReadOnlyList GetValidValues(ParamMetadata param) + { + if (param.ValidValues is { Count: > 0 }) return param.ValidValues; + if (param.Type is "boolean" or "flagBoolean" or "flagElement") + return new[] { "On", "Off" }; + return []; + } + + private static void EnsureInitialized() + { + if (Volatile.Read(ref _initialized)) return; + lock (_gate) + { + if (_initialized) return; + Scan(); + Volatile.Write(ref _initialized, true); + } + } + + private static void Scan() + { + var assembly = typeof(IStepFactory).Assembly; + foreach (var type in assembly.GetTypes()) + { + if (type.IsAbstract || type.IsInterface) continue; + if (!typeof(IStepFactory).IsAssignableFrom(type)) continue; + + // Reflect the static Metadata property enforced by the interface. + var prop = type.GetProperty( + nameof(IStepFactory.Metadata), + BindingFlags.Public | BindingFlags.Static); + if (prop?.GetValue(null) is not StepMetadata metadata) continue; + + _all.Add(metadata); + if (!string.IsNullOrEmpty(metadata.Name)) + _byName[metadata.Name] = metadata; + if (metadata.Id != 0) + _byId[metadata.Id] = metadata; + + // Bridge into legacy factories so callers that still use + // StepXmlFactory / StepDisplayFactory pick up POCO-backed + // construction without each POCO needing a ModuleInitializer. + if (metadata.FromXml is { } fromXml) + StepXmlFactory.Register(metadata.Name, fromXml); + if (metadata.FromDisplay is { } fromDisplay) + StepDisplayFactory.Register(metadata.Name, fromDisplay); + } + + // Sort for deterministic All iteration. + _all.Sort((a, b) => string.Compare(a.Name, b.Name, StringComparison.OrdinalIgnoreCase)); + } +} From 85b1bdc633f54127e77e75cf3e9047c87c06ebf0 Mon Sep 17 00:00:00 2001 From: Nate Bross Date: Mon, 20 Apr 2026 21:20:34 -0500 Subject: [PATCH 02/38] feat(scripting): migrate Beep, Set Error Capture, If to IStepFactory POCOs Each step owns its StepMetadata (name, id, category, help URL, params, notes) and is discovered by StepRegistry via reflection. No per-POCO [ModuleInitializer] needed; registry populates and bridges to legacy factories at assembly load. - BeepStep: zero-param canonical template. - SetErrorCaptureStep: single-boolean with ParamMetadata and tooltip description sourced from agentic-fm snippet comment zone. - IfStep: rewritten to implement IStepFactory with block-pair metadata. Drops StepCatalogLoader dependency. Synthesizes a minimal StepDefinition for legacy FmScript.ToDisplayLines indent logic until that consumer migrates. Shared [ModuleInitializer] kept for sibling control-flow steps not yet migrated (Else, End If, Loop, etc.); explanatory comment added. - Zero-loss audits inline in each POCO's class doc comment. Test fixtures previously using Beep as a "catalog-known, no-POCO" canary swap to Halt Script (SealedStepPreservationTests, RawStepAllowListTests) now that Beep is itself typed. --- src/SharpFM.Model/Scripting/Steps/BeepStep.cs | 47 ++++++++ .../Scripting/Steps/ControlFlowSteps.cs | 74 +++++++++++- .../Scripting/Steps/SetErrorCaptureStep.cs | 85 ++++++++++++++ .../Scripting/SealedStepPreservationTests.cs | 106 +++++++++--------- .../Scripting/Steps/BeepStepTests.cs | 64 +++++++++++ .../Scripting/Steps/IfStepTests.cs | 87 ++++++++++++++ .../Scripting/Steps/RawStepAllowListTests.cs | 7 +- .../Steps/SetErrorCaptureStepTests.cs | 106 ++++++++++++++++++ 8 files changed, 515 insertions(+), 61 deletions(-) create mode 100644 src/SharpFM.Model/Scripting/Steps/BeepStep.cs create mode 100644 src/SharpFM.Model/Scripting/Steps/SetErrorCaptureStep.cs create mode 100644 tests/SharpFM.Tests/Scripting/Steps/BeepStepTests.cs create mode 100644 tests/SharpFM.Tests/Scripting/Steps/IfStepTests.cs create mode 100644 tests/SharpFM.Tests/Scripting/Steps/SetErrorCaptureStepTests.cs diff --git a/src/SharpFM.Model/Scripting/Steps/BeepStep.cs b/src/SharpFM.Model/Scripting/Steps/BeepStep.cs new file mode 100644 index 0000000..c4b9063 --- /dev/null +++ b/src/SharpFM.Model/Scripting/Steps/BeepStep.cs @@ -0,0 +1,47 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting.Registry; + +namespace SharpFM.Model.Scripting.Steps; + +/// +/// Typed POCO for FileMaker's "Beep" script step. Zero parameters, no +/// hidden state — the canonical minimal shape and the reference +/// implementation for the big-bang POCO migration pattern. +/// +/// Zero-loss audit: the step's only XML state is the three +/// attributes on <Step> (enable, id, +/// name). All three are round-tripped exactly. No child elements +/// exist in FM Pro's clipboard output. No advanced-syntax extensions are +/// required; the step's display text is literally its name. +/// +public sealed class BeepStep : ScriptStep, IStepFactory +{ + public const int XmlId = 93; + public const string XmlName = "Beep"; + + public BeepStep(bool enabled = true) : base(null, enabled) { } + + public override XElement ToXml() => + new("Step", + new XAttribute("enable", Enabled ? "True" : "False"), + new XAttribute("id", XmlId), + new XAttribute("name", XmlName)); + + public override string ToDisplayLine() => XmlName; + + public static new ScriptStep FromXml(XElement step) => + new BeepStep(step.Attribute("enable")?.Value != "False"); + + public static ScriptStep FromDisplayParams(bool enabled, string[] _) => + new BeepStep(enabled); + + public static StepMetadata Metadata { get; } = new() + { + Name = XmlName, + Id = XmlId, + Category = "miscellaneous", + HelpUrl = "https://help.claris.com/en/pro-help/content/beep.html", + FromXml = FromXml, + FromDisplay = FromDisplayParams, + }; +} diff --git a/src/SharpFM.Model/Scripting/Steps/ControlFlowSteps.cs b/src/SharpFM.Model/Scripting/Steps/ControlFlowSteps.cs index 962aed5..2b85fd1 100644 --- a/src/SharpFM.Model/Scripting/Steps/ControlFlowSteps.cs +++ b/src/SharpFM.Model/Scripting/Steps/ControlFlowSteps.cs @@ -1,6 +1,7 @@ using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using System.Xml.Linq; +using SharpFM.Model.Scripting.Registry; using SharpFM.Model.Scripting.Serialization; using SharpFM.Model.Scripting.Values; @@ -26,23 +27,52 @@ namespace SharpFM.Model.Scripting.Steps; /// by the individual POCOs. /// /// -public sealed class IfStep : ScriptStep +/// +/// Zero-loss audit for : +/// +/// Step attributes (enable, id, name) — round-tripped. +/// <Calculation> CDATA body — round-tripped via . +/// <Restore state="..."/>intentionally dropped. Upstream agentic-fm +/// snippets include it, but FM Pro never changes the value and never emits the element in +/// clipboard output; it carries no information worth round-tripping. See +/// docs/advanced-filemaker-scripting-syntax.md for the "what to drop vs. surface" +/// guidance this follows. +/// +/// +public sealed class IfStep : ScriptStep, IStepFactory { public Calculation Condition { get; set; } public IfStep(bool enabled, Calculation condition) - : base(StepCatalogLoader.ByName["If"], enabled) + : base(BuildLegacyDefinition(), enabled) { Condition = condition; } + // Transitional: legacy consumers (FmScript.ToDisplayLines, etc.) still + // read step.Definition.BlockPair for indent decisions. Project the + // pieces Metadata carries into a synthesized StepDefinition until those + // consumers migrate to StepRegistry in a later phase. + private static StepDefinition BuildLegacyDefinition() => new() + { + Name = "If", + Id = 68, + BlockPair = new StepBlockPair + { + Role = BlockPairRole.Open, + Partners = ["Else", "Else If", "End If"], + }, + }; + [SuppressMessage("Usage", "CA2255:The 'ModuleInitializer' attribute should not be used in libraries", - Justification = "Register typed step factories on assembly load.")] + Justification = "Register sibling control-flow steps that haven't migrated to IStepFactory yet.")] [ModuleInitializer] internal static void Register() { - StepXmlFactory.Register("If", FromXml); - StepDisplayFactory.Register("If", FromDisplayParams); + // IfStep itself registers via StepRegistry's reflection scan (IStepFactory). + // The siblings below migrate in the sweep phase; for now their factory + // registrations stay here so the legacy StepXmlFactory / StepDisplayFactory + // surfaces continue to return typed POCOs. StepXmlFactory.Register("Else If", ElseIfStep.FromXml); StepDisplayFactory.Register("Else If", ElseIfStep.FromDisplayParams); @@ -76,6 +106,40 @@ public override XElement ToXml() => public static ScriptStep FromDisplayParams(bool enabled, string[] hrParams) => new IfStep(enabled, ParseCondition(hrParams)); + public static StepMetadata Metadata { get; } = new() + { + Name = "If", + Id = 68, + Category = "control", + HelpUrl = "https://help.claris.com/en/pro-help/content/if-script-step.html", + HrSignature = "[ condition ]", + BlockPair = new StepBlockPair + { + Role = BlockPairRole.Open, + Partners = ["Else", "Else If", "End If"], + }, + Params = + [ + new ParamMetadata + { + Name = "condition", + XmlElement = "Calculation", + Type = "calculation", + // Intentionally no HrLabel — FM Pro's display renders the + // calc without a label prefix (e.g. "If [ $x > 0 ]"). The + // completion snippet synthesizer uses Name as the + // placeholder hint so it reads "If [ ${1:condition} ]". + Required = true, + }, + ], + Notes = new StepNotes + { + Constraints = "Requires a matching End If step.", + }, + FromXml = FromXml, + FromDisplay = FromDisplayParams, + }; + internal static Calculation ReadCalculation(XElement step) { var calc = step.Element("Calculation"); diff --git a/src/SharpFM.Model/Scripting/Steps/SetErrorCaptureStep.cs b/src/SharpFM.Model/Scripting/Steps/SetErrorCaptureStep.cs new file mode 100644 index 0000000..3120100 --- /dev/null +++ b/src/SharpFM.Model/Scripting/Steps/SetErrorCaptureStep.cs @@ -0,0 +1,85 @@ +using System; +using System.Xml.Linq; +using SharpFM.Model.Scripting.Registry; + +namespace SharpFM.Model.Scripting.Steps; + +/// +/// Typed POCO for FileMaker's "Set Error Capture" script step. One +/// boolean param — On/Off — maps to <Set state="True|False"/>. +/// +/// Zero-loss audit: the step's XML state is the Set state +/// attribute plus the three attributes on <Step>. All four +/// are round-tripped exactly. FM Pro renders the step as +/// Set Error Capture [ On|Off ]; our display mirrors that. No +/// advanced-syntax extensions are required. +/// +public sealed class SetErrorCaptureStep : ScriptStep, IStepFactory +{ + public const int XmlId = 86; + public const string XmlName = "Set Error Capture"; + + /// + /// True when FileMaker should suppress alert messages and some + /// dialog boxes so scripts can detect errors via Get(LastError). + /// + public bool CaptureErrors { get; set; } + + public SetErrorCaptureStep(bool captureErrors = false, bool enabled = true) + : base(null, enabled) + { + CaptureErrors = captureErrors; + } + + public override XElement ToXml() => + new("Step", + new XAttribute("enable", Enabled ? "True" : "False"), + new XAttribute("id", XmlId), + new XAttribute("name", XmlName), + new XElement("Set", + new XAttribute("state", CaptureErrors ? "True" : "False"))); + + public override string ToDisplayLine() => + $"Set Error Capture [ {(CaptureErrors ? "On" : "Off")} ]"; + + public static new ScriptStep FromXml(XElement step) + { + var enabled = step.Attribute("enable")?.Value != "False"; + var state = step.Element("Set")?.Attribute("state")?.Value == "True"; + return new SetErrorCaptureStep(state, enabled); + } + + public static ScriptStep FromDisplayParams(bool enabled, string[] hrParams) + { + var captureErrors = hrParams.Length > 0 + && hrParams[0].Trim().Equals("On", StringComparison.OrdinalIgnoreCase); + return new SetErrorCaptureStep(captureErrors, enabled); + } + + public static StepMetadata Metadata { get; } = new() + { + Name = XmlName, + Id = XmlId, + Category = "control", + HelpUrl = "https://help.claris.com/en/pro-help/content/set-error-capture.html", + HrSignature = "[ On|Off ]", + Params = + [ + new ParamMetadata + { + Name = "Set", + XmlElement = "Set", + Type = "boolean", + XmlAttr = "state", + ValidValues = ["On", "Off"], + // Sourced from agentic-fm's snippet_examples/steps/control/Set Error Capture.xml + // comment zone. Surfaces as a tooltip in hover-enabled UIs. + Description = "\"True\" (On) suppresses FileMaker Pro alert messages and some " + + "dialog boxes. \"False\" (Off) reenables the alert messages.", + DefaultValue = "True", + }, + ], + FromXml = FromXml, + FromDisplay = FromDisplayParams, + }; +} diff --git a/tests/SharpFM.Tests/Scripting/SealedStepPreservationTests.cs b/tests/SharpFM.Tests/Scripting/SealedStepPreservationTests.cs index f7a84c3..334a7b9 100644 --- a/tests/SharpFM.Tests/Scripting/SealedStepPreservationTests.cs +++ b/tests/SharpFM.Tests/Scripting/SealedStepPreservationTests.cs @@ -17,33 +17,33 @@ namespace SharpFM.Tests.Scripting; /// public class SealedStepPreservationTests { - // Beep is catalog-known, has no typed POCO, and (with the allow-list + // HaltScript is catalog-known, has no typed POCO, and (with the allow-list // empty at launch) is sealed. Perfect canary. - private const string ScriptWithSealedBeepXml = @" + private const string ScriptWithSealedHaltScriptXml = @" 0]]> - + "; [Fact] public void NoEdits_ToXml_ProducesOriginalScriptWithSealedStepIntact() { - var editor = new ScriptClipEditor(ScriptWithSealedBeepXml); + var editor = new ScriptClipEditor(ScriptWithSealedHaltScriptXml); var xml = editor.ToXml(); var doc = XDocument.Parse(xml); var steps = doc.Root!.Elements("Step").ToArray(); Assert.Equal(3, steps.Length); - Assert.Equal("Beep", steps[1].Attribute("name")!.Value); - Assert.Equal("93", steps[1].Attribute("id")!.Value); + Assert.Equal("Halt Script", steps[1].Attribute("name")!.Value); + Assert.Equal("90", steps[1].Attribute("id")!.Value); } [Fact] public void EditNonSealedLine_SealedStepPreservedByAnchor() { - // User edits the If step's calculation — the Beep step's XML + // User edits the If step's calculation — the HaltScript step's XML // must survive unchanged via the anchor cache. - var editor = new ScriptClipEditor(ScriptWithSealedBeepXml); + var editor = new ScriptClipEditor(ScriptWithSealedHaltScriptXml); // Change the If calc from "$x > 0" to "$x > 10" via document edit. var line1 = editor.Document.GetLineByNumber(1); @@ -57,9 +57,9 @@ public void EditNonSealedLine_SealedStepPreservedByAnchor() // If step reflects the edit Assert.Contains("$x > 10", steps[0].Element("Calculation")!.Value); - // Beep step still present with id - Assert.Equal("Beep", steps[1].Attribute("name")!.Value); - Assert.Equal("93", steps[1].Attribute("id")!.Value); + // HaltScript step still present with id + Assert.Equal("Halt Script", steps[1].Attribute("name")!.Value); + Assert.Equal("90", steps[1].Attribute("id")!.Value); } [Fact] @@ -68,7 +68,7 @@ public void DeleteSealedLine_StepDropsOutOfScript() // Deleting a sealed line entirely is allowed — the anchor // invalidates and the step is gone from the output. Intentional // per the design: delete OK, edit-in-place not OK. - var editor = new ScriptClipEditor(ScriptWithSealedBeepXml); + var editor = new ScriptClipEditor(ScriptWithSealedHaltScriptXml); var line2 = editor.Document.GetLineByNumber(2); // Include the trailing newline so we delete the whole line @@ -87,25 +87,25 @@ public void DeleteSealedLine_StepDropsOutOfScript() [Fact] public void InsertNewLineBeforeSealed_SealedStepSurvivesAtNewPosition() { - // Insert a new If step above Beep. Beep must still be preserved + // Insert a new If step above HaltScript. HaltScript must still be preserved // via the anchor (which moves with the line). - var editor = new ScriptClipEditor(ScriptWithSealedBeepXml); + var editor = new ScriptClipEditor(ScriptWithSealedHaltScriptXml); - var line2 = editor.Document.GetLineByNumber(2); // Beep - editor.Document.Insert(line2.Offset, "Beep\n"); + var line2 = editor.Document.GetLineByNumber(2); // HaltScript + editor.Document.Insert(line2.Offset, "HaltScript\n"); var xml = editor.ToXml(); var doc = XDocument.Parse(xml); var steps = doc.Root!.Elements("Step").ToArray(); - // Four steps now; the original Beep (the one at the original + // Four steps now; the original HaltScript (the one at the original // anchor position) must still have its id preserved. Assert.Equal(4, steps.Length); - // Find all Beep steps. The one with id=93 is the original sealed one. - var originalBeep = steps.FirstOrDefault(s => s.Attribute("id")?.Value == "93"); - Assert.NotNull(originalBeep); - Assert.Equal("Beep", originalBeep!.Attribute("name")!.Value); + // Find all HaltScript steps. The one with id=93 is the original sealed one. + var originalHaltScript = steps.FirstOrDefault(s => s.Attribute("id")?.Value == "90"); + Assert.NotNull(originalHaltScript); + Assert.Equal("Halt Script", originalHaltScript!.Attribute("name")!.Value); } // --- UpdateSealedXml (cog-edit write-back) --- @@ -113,15 +113,15 @@ public void InsertNewLineBeforeSealed_SealedStepSurvivesAtNewPosition() [Fact] public void UpdateSealedXml_ReplacesCachedXmlAndRerendersLine() { - var editor = new ScriptClipEditor(ScriptWithSealedBeepXml); + var editor = new ScriptClipEditor(ScriptWithSealedHaltScriptXml); - // Find the Beep anchor and edit its XML in place — simulating what - // RawStepEditorWindow does after the user saves. Replace the Beep - // step with a hypothetical different-id Beep to prove the cache + // Find the HaltScript anchor and edit its XML in place — simulating what + // RawStepEditorWindow does after the user saves. Replace the HaltScript + // step with a hypothetical different-id HaltScript to prove the cache // update propagates to ToXml output. var anchor = editor.SealedAnchors.First(); var replacement = XElement.Parse( - ""); + ""); var updated = editor.UpdateSealedXml(anchor, replacement); Assert.True(updated); @@ -131,7 +131,7 @@ public void UpdateSealedXml_ReplacesCachedXmlAndRerendersLine() var beep = doc.Root!.Elements("Step").ElementAt(1); // The cached XML now reflects the replacement — is - // present even though the display line (still "Beep") hasn't + // present even though the display line (still "Halt Script") hasn't // exposed it. Assert.NotNull(beep.Element("SomeChild")); Assert.Equal("42", beep.Element("SomeChild")!.Attribute("value")!.Value); @@ -140,9 +140,9 @@ public void UpdateSealedXml_ReplacesCachedXmlAndRerendersLine() [Fact] public void UpdateSealedXml_PreservesBlockIndentation() { - // The Beep step is inside an If/End If block, so its display line + // The HaltScript step is inside an If/End If block, so its display line // is indented. Updating via the cog must preserve that indentation. - var editor = new ScriptClipEditor(ScriptWithSealedBeepXml); + var editor = new ScriptClipEditor(ScriptWithSealedHaltScriptXml); var anchor = editor.SealedAnchors.First(); var beepLine = editor.Document.GetLineByOffset(anchor.Offset); @@ -150,7 +150,7 @@ public void UpdateSealedXml_PreservesBlockIndentation() Assert.StartsWith(" ", beforeText); // 4-space indent inside If block var replacement = XElement.Parse( - ""); + ""); editor.UpdateSealedXml(anchor, replacement); var afterText = editor.Document.GetText(beepLine.Offset, beepLine.Length); @@ -160,14 +160,14 @@ public void UpdateSealedXml_PreservesBlockIndentation() [Fact] public void UpdateSealedXml_DeadAnchor_ReturnsFalse() { - var editor = new ScriptClipEditor(ScriptWithSealedBeepXml); + var editor = new ScriptClipEditor(ScriptWithSealedHaltScriptXml); var anchor = editor.SealedAnchors.First(); // Delete the sealed line — anchor is now dead. var line2 = editor.Document.GetLineByNumber(2); editor.Document.Remove(line2.Offset, line2.TotalLength); - var replacement = XElement.Parse(""); + var replacement = XElement.Parse(""); Assert.False(editor.UpdateSealedXml(anchor, replacement)); } @@ -176,10 +176,10 @@ public void UpdateSealedXml_ThenEditNonSealedLine_UpdateIsPreserved() { // The cog-edit must survive a subsequent non-sealed edit: the // rebuild loop must NOT drop the updated XML when it re-runs. - var editor = new ScriptClipEditor(ScriptWithSealedBeepXml); + var editor = new ScriptClipEditor(ScriptWithSealedHaltScriptXml); var anchor = editor.SealedAnchors.First(); var replacement = XElement.Parse( - ""); + ""); editor.UpdateSealedXml(anchor, replacement); // Now edit the If step's calc. @@ -200,18 +200,18 @@ public void UpdateSealedXml_ThenEditNonSealedLine_UpdateIsPreserved() [Fact] public void TryGetSealedXml_LiveAnchor_ReturnsCachedXml() { - var editor = new ScriptClipEditor(ScriptWithSealedBeepXml); + var editor = new ScriptClipEditor(ScriptWithSealedHaltScriptXml); var anchor = editor.SealedAnchors.First(); Assert.True(editor.TryGetSealedXml(anchor, out var xml)); - Assert.Equal("Beep", xml.Attribute("name")!.Value); - Assert.Equal("93", xml.Attribute("id")!.Value); + Assert.Equal("Halt Script", xml.Attribute("name")!.Value); + Assert.Equal("90", xml.Attribute("id")!.Value); } [Fact] public void TryGetSealedXml_DeadAnchor_ReturnsFalse() { - var editor = new ScriptClipEditor(ScriptWithSealedBeepXml); + var editor = new ScriptClipEditor(ScriptWithSealedHaltScriptXml); var anchor = editor.SealedAnchors.First(); var line2 = editor.Document.GetLineByNumber(2); @@ -225,13 +225,13 @@ public void TryGetSealedXml_DeadAnchor_ReturnsFalse() [Fact] public void MultipleSealedSteps_EachPreservedIndependently() { - // Two Beeps + a comment (all non-POCO-except-comment) — comment - // has a POCO so it stays editable. The two Beeps are sealed. + // Two HaltScripts + a comment (all non-POCO-except-comment) — comment + // has a POCO so it stays editable. The two HaltScripts are sealed. var xml = @" 0]]> - + between - + "; @@ -245,7 +245,7 @@ public void MultipleSealedSteps_EachPreservedIndependently() var outDoc = XDocument.Parse(outXml); var steps = outDoc.Root!.Elements("Step").ToArray(); - // Two Beeps each with their distinguishing marker child intact. + // Two HaltScripts each with their distinguishing marker child intact. var beepsWithFirst = steps.Count(s => s.Element("FirstMarker") != null); var beepsWithSecond = steps.Count(s => s.Element("SecondMarker") != null); @@ -261,7 +261,7 @@ public void ScriptMetadata_AndSealedStep_BothPreservedOnRoundTrip() var xml = @" "; @@ -281,8 +281,8 @@ public void ScriptMetadata_AndSealedStep_BothPreservedOnRoundTrip() Assert.Equal("42", scriptEl!.Attribute("id")!.Value); Assert.Equal("MyScript", scriptEl.Attribute("name")!.Value); - // Sealed Beep's custom child preserved - var beep = scriptEl.Elements("Step").FirstOrDefault(s => s.Attribute("id")?.Value == "93"); + // Sealed HaltScript's custom child preserved + var beep = scriptEl.Elements("Step").FirstOrDefault(s => s.Attribute("id")?.Value == "90"); Assert.NotNull(beep); Assert.NotNull(beep!.Element("WrapperPreserved")); } @@ -292,7 +292,7 @@ public void ScriptMetadata_AndSealedStep_BothPreservedOnRoundTrip() [Fact] public void ToXml_CalledTwice_ProducesIdenticalOutput() { - var editor = new ScriptClipEditor(ScriptWithSealedBeepXml); + var editor = new ScriptClipEditor(ScriptWithSealedHaltScriptXml); var first = editor.ToXml(); var second = editor.ToXml(); Assert.Equal(first, second); @@ -305,7 +305,7 @@ public void SealedStep_AtEndOfDocumentNoTrailingNewline_Preserved() { var xml = @" 0]]> - + "; var editor = new ScriptClipEditor(xml); @@ -313,7 +313,7 @@ public void SealedStep_AtEndOfDocumentNoTrailingNewline_Preserved() var outDoc = XDocument.Parse(outXml); var beep = outDoc.Root!.Elements("Step").Last(); - Assert.Equal("Beep", beep.Attribute("name")!.Value); + Assert.Equal("Halt Script", beep.Attribute("name")!.Value); Assert.NotNull(beep.Element("EofCanary")); } @@ -329,7 +329,7 @@ public void SealedAnchors_AfterDocumentShrink_AllOffsetsWithinBounds() // context to test. This covers the defensive side: the editor's // own SealedAnchors iterator must never yield an anchor whose // offset lies past the current document length. - var editor = new ScriptClipEditor(ScriptWithSealedBeepXml); + var editor = new ScriptClipEditor(ScriptWithSealedHaltScriptXml); Assert.Single(editor.SealedAnchors); // Drastically shrink the document — clears every line the @@ -347,7 +347,7 @@ public void SealedAnchors_AfterDocumentReplacedWithShorterContent_AllOffsetsWith { // Same invariant but with content that has steps — proves the // iterator stays safe when the TextView swaps documents too. - var editor = new ScriptClipEditor(ScriptWithSealedBeepXml); + var editor = new ScriptClipEditor(ScriptWithSealedHaltScriptXml); Assert.Single(editor.SealedAnchors); editor.Document.Replace(0, editor.Document.TextLength, "If [ $x > 0 ]"); @@ -361,7 +361,7 @@ public void SealedAnchors_AfterDocumentReplacedWithShorterContent_AllOffsetsWith [Fact] public void FromXml_Reload_RebuildsAnchorCacheFreshForNewContent() { - var editor = new ScriptClipEditor(ScriptWithSealedBeepXml); + var editor = new ScriptClipEditor(ScriptWithSealedHaltScriptXml); Assert.Single(editor.SealedAnchors); // Reload with different content — previous anchor should be gone, @@ -376,7 +376,7 @@ public void FromXml_Reload_RebuildsAnchorCacheFreshForNewContent() Assert.Empty(editor.SealedAnchors); // Reload with a sealed step back — a fresh anchor appears. - editor.FromXml(ScriptWithSealedBeepXml); + editor.FromXml(ScriptWithSealedHaltScriptXml); Assert.Single(editor.SealedAnchors); } } diff --git a/tests/SharpFM.Tests/Scripting/Steps/BeepStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/BeepStepTests.cs new file mode 100644 index 0000000..3b06fa2 --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/BeepStepTests.cs @@ -0,0 +1,64 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +/// +/// Zero-parameter POCO canonical pattern. Fixtures are inline raw string +/// literals copied from agentic-fm's snippet_examples so the test reads +/// without having to open a separate file. +/// +public class BeepStepTests +{ + // Canonical shape from + // agent/snippet_examples/steps/miscellaneous/Beep.xml + private const string CanonicalSnippet = """ + + + + + """; + + private static XElement CanonicalStepElement() => + XDocument.Parse(CanonicalSnippet).Root!.Element("Step")!; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = CanonicalStepElement(); + var step = BeepStep.Metadata.FromXml!(source); + + Assert.IsType(step); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Display_EmitsBareName() + { + var step = new BeepStep(); + Assert.Equal("Beep", step.ToDisplayLine()); + } + + [Fact] + public void Disabled_RoundTrips() + { + var source = XElement.Parse(""""""); + var step = BeepStep.Metadata.FromXml!(source); + + Assert.False(step.Enabled); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Registry_HasBeep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Beep", out var metadata)); + Assert.Equal(93, metadata!.Id); + Assert.Equal("miscellaneous", metadata.Category); + Assert.Null(metadata.BlockPair); + Assert.Empty(metadata.Params); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/IfStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/IfStepTests.cs new file mode 100644 index 0000000..3b2a00b --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/IfStepTests.cs @@ -0,0 +1,87 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using SharpFM.Model.Scripting.Values; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +/// +/// IfStep pilot: calc + block-pair POCO with an intentional +/// drop of the Restore param per the zero-loss audit +/// in docs/advanced-filemaker-scripting-syntax.md. +/// +public class IfStepTests +{ + // Canonical shape from agentic-fm's snippet — note the + // element that FM Pro itself never emits. + private const string AgenticFmSnippet = """ + + + + + 0]]> + + + """; + + // FM Pro clipboard form — no element at all. + private const string FmProStyleStep = """ + 0]]> + """; + + private static XElement StepFromSnippet(string snippet) => + XDocument.Parse(snippet).Root!.Element("Step")!; + + [Fact] + public void RoundTrip_WithRestoreInSource_DropsRestore() + { + // Input has ; output must not. This codifies the + // intentional drop described in the zero-loss audit. + var source = StepFromSnippet(AgenticFmSnippet); + var step = IfStep.Metadata.FromXml!(source); + var output = step.ToXml(); + + Assert.Null(output.Element("Restore")); + Assert.Equal("$error <> 0", output.Element("Calculation")!.Value); + } + + [Fact] + public void RoundTrip_FmProStyle_IsByteIdentical() + { + var source = XElement.Parse(FmProStyleStep); + var step = IfStep.Metadata.FromXml!(source); + + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Display_EmitsConditionInBrackets() + { + var step = new IfStep(true, new Calculation("$error <> 0")); + Assert.Equal("If [ $error <> 0 ]", step.ToDisplayLine()); + } + + [Fact] + public void Registry_HasIfWithBlockPair() + { + Assert.True(StepRegistry.ByName.TryGetValue("If", out var metadata)); + Assert.Equal(68, metadata!.Id); + Assert.Equal("control", metadata.Category); + Assert.NotNull(metadata.BlockPair); + Assert.Equal(BlockPairRole.Open, metadata.BlockPair!.Role); + Assert.Contains("End If", metadata.BlockPair.Partners); + Assert.Contains("Else", metadata.BlockPair.Partners); + Assert.Contains("Else If", metadata.BlockPair.Partners); + } + + [Fact] + public void Metadata_Params_DescribeCalculation() + { + var metadata = StepRegistry.ByName["If"]; + var param = Assert.Single(metadata.Params); + Assert.Equal("Calculation", param.XmlElement); + Assert.Equal("calculation", param.Type); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/RawStepAllowListTests.cs b/tests/SharpFM.Tests/Scripting/Steps/RawStepAllowListTests.cs index 8a1a249..5c96a25 100644 --- a/tests/SharpFM.Tests/Scripting/Steps/RawStepAllowListTests.cs +++ b/tests/SharpFM.Tests/Scripting/Steps/RawStepAllowListTests.cs @@ -50,9 +50,10 @@ public void TypedPocoStep_SetField_IsFullyEditable_ReturnsTrue() [Fact] public void RawStep_NotInAllowList_IsFullyEditable_ReturnsFalse() { - // "Beep" is a catalog-known step with no typed POCO. Not in the - // allow-list (which ships empty), so it must be sealed. - var xml = XElement.Parse(""); + // "Halt Script" is a catalog-known step with no typed POCO (Beep + // was the original canary but has migrated to BeepStep). Not in + // the allow-list (which ships empty), so it must be sealed. + var xml = XElement.Parse(""); var step = ScriptStep.FromXml(xml); Assert.IsType(step); diff --git a/tests/SharpFM.Tests/Scripting/Steps/SetErrorCaptureStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/SetErrorCaptureStepTests.cs new file mode 100644 index 0000000..de72e87 --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/SetErrorCaptureStepTests.cs @@ -0,0 +1,106 @@ +using System.Linq; +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +/// +/// Single-boolean POCO canonical pattern. The underlying XML carries a +/// <Set state="True|False"/> child; display text uses +/// "On"/"Off" matching FileMaker Pro's wording. +/// +public class SetErrorCaptureStepTests +{ + // Canonical shape from + // agent/snippet_examples/steps/control/Set Error Capture.xml + private const string StateTrueSnippet = """ + + + + + + + """; + + private const string StateFalseSnippet = """ + + + + + + + """; + + private static XElement StepFrom(string snippet) => + XDocument.Parse(snippet).Root!.Element("Step")!; + + [Fact] + public void RoundTrip_StateTrue() + { + var source = StepFrom(StateTrueSnippet); + var step = (SetErrorCaptureStep)SetErrorCaptureStep.Metadata.FromXml!(source); + + Assert.True(step.CaptureErrors); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void RoundTrip_StateFalse() + { + var source = StepFrom(StateFalseSnippet); + var step = (SetErrorCaptureStep)SetErrorCaptureStep.Metadata.FromXml!(source); + + Assert.False(step.CaptureErrors); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Display_EmitsOnOff() + { + Assert.Equal("Set Error Capture [ On ]", + new SetErrorCaptureStep(captureErrors: true).ToDisplayLine()); + Assert.Equal("Set Error Capture [ Off ]", + new SetErrorCaptureStep(captureErrors: false).ToDisplayLine()); + } + + [Fact] + public void Display_RoundTripsThroughFromDisplayParams() + { + var step = (SetErrorCaptureStep)SetErrorCaptureStep.Metadata.FromDisplay!(true, new[] { "On" }); + Assert.True(step.CaptureErrors); + + var off = (SetErrorCaptureStep)SetErrorCaptureStep.Metadata.FromDisplay!(true, new[] { "Off" }); + Assert.False(off.CaptureErrors); + } + + [Fact] + public void Registry_HasSetErrorCaptureWithParam() + { + Assert.True(StepRegistry.ByName.TryGetValue("Set Error Capture", out var metadata)); + Assert.Equal(86, metadata!.Id); + Assert.Equal("control", metadata.Category); + Assert.Null(metadata.BlockPair); + + var param = Assert.Single(metadata.Params); + Assert.Equal("Set", param.XmlElement); + Assert.Equal("boolean", param.Type); + Assert.Equal("state", param.XmlAttr); + + // Description is sourced from agentic-fm's snippet comment + // and powers tooltip / hover UIs as they're wired up later. + Assert.NotNull(param.Description); + Assert.Contains("suppresses", param.Description, System.StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public void GetValidValues_ReturnsOnAndOff() + { + var metadata = StepRegistry.ByName["Set Error Capture"]; + var values = StepRegistry.GetValidValues(metadata.Params[0]); + Assert.Contains("On", values); + Assert.Contains("Off", values); + } +} From 98bebf59fce590262e5310bd6ca4814e584f9523 Mon Sep 17 00:00:00 2001 From: Nate Bross Date: Mon, 20 Apr 2026 21:20:46 -0500 Subject: [PATCH 03/38] refactor(editor): rewire completion provider onto StepRegistry FmScriptCompletionProvider reads exclusively from StepRegistry now. Adds a Monaco-snippet synthesizer that emits bracketed step templates with tab-stop placeholders (e.g. "Set Error Capture [ \${1:On} ]") so that accepting a multi-param step completion inserts the full form with the first value pre-selected for editing. Without this, users saw "Set Error Capture On" instead of "Set Error Capture [ On ]". StepRegistry.Initialize() is called from App.OnFrameworkInitializationCompleted so reflection cost lands at a predictable moment; lazy init on first lookup remains as a safety net. Pilot-time regression: completions now show only POCO-backed steps (Beep, Set Error Capture, If). Full coverage returns as the sweep adds the remaining 189 POCOs. Codified by CompletionProviderTests.NonPocoStep_NotSuggested. --- src/SharpFM/App.axaml.cs | 5 + .../Editor/FmScriptCompletionProvider.cs | 129 ++++++++++------- .../Scripting/CompletionProviderTests.cs | 132 +++++++++++++----- 3 files changed, 181 insertions(+), 85 deletions(-) diff --git a/src/SharpFM/App.axaml.cs b/src/SharpFM/App.axaml.cs index c3843e9..c4c2148 100644 --- a/src/SharpFM/App.axaml.cs +++ b/src/SharpFM/App.axaml.cs @@ -4,6 +4,7 @@ using Avalonia; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Markup.Xaml; +using SharpFM.Model.Scripting.Registry; using SharpFM.Models; using SharpFM.Plugin; using SharpFM.ViewModels; @@ -27,6 +28,10 @@ public override void Initialize() public override void OnFrameworkInitializationCompleted() { + // Reflection scan lands at a predictable moment; otherwise the + // same initialization runs lazily on first registry access. + StepRegistry.Initialize(); + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) { desktop.MainWindow = new MainWindow(); diff --git a/src/SharpFM/Scripting/Editor/FmScriptCompletionProvider.cs b/src/SharpFM/Scripting/Editor/FmScriptCompletionProvider.cs index 57fc212..1c520f3 100644 --- a/src/SharpFM/Scripting/Editor/FmScriptCompletionProvider.cs +++ b/src/SharpFM/Scripting/Editor/FmScriptCompletionProvider.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using AvaloniaEdit.CodeCompletion; +using SharpFM.Model.Scripting.Registry; namespace SharpFM.Scripting.Editor; @@ -10,61 +11,64 @@ public enum CompletionContext StepName, ParamLabel, ParamValue, - None + None, } +/// +/// Completion provider for the script editor. Reads exclusively from +/// — the typed POCO registry populated by +/// reflection. The legacy StepCatalogLoader path has been cut +/// from this consumer entirely. +/// +/// +/// Pilot-time regression: only step names with a typed POCO appear +/// in completions. During the big-bang migration, that's a small subset +/// of the 206-entry FileMaker catalog; coverage grows to full as more +/// POCOs land. Documented in +/// docs/plans/script-step-poco-sweep.md. +/// +/// public static class FmScriptCompletionProvider { public static (CompletionContext Context, IList Items) GetCompletions( string lineText, int caretColumn) { - // Work with text from start of line to cursor only var textToCursor = caretColumn >= 0 && caretColumn <= lineText.Length ? lineText.Substring(0, caretColumn) : lineText; var trimmed = textToCursor.TrimStart(); - // Comments don't get completions if (trimmed.StartsWith("#")) return (CompletionContext.None, Array.Empty()); - // Strip disabled prefix for lookup var forLookup = trimmed; if (forLookup.StartsWith("//")) forLookup = forLookup.Substring(2).TrimStart(); - // Try to find a recognized step name at the start of the line - var (stepName, definition) = FindStepName(forLookup); + var (stepName, metadata) = FindStepName(forLookup); - if (definition == null) + if (metadata == null) { - // If we're already inside brackets of an unrecognized step, there's - // nothing useful to suggest — this isn't a step-name context anymore. if (forLookup.Contains('[')) return (CompletionContext.None, Array.Empty()); - // No recognized step yet → suggest step names var items = GetStepNameCompletions(forLookup); return (CompletionContext.StepName, items); } - // Step is recognized → suggest params - // Find the last semicolon to determine which param segment we're in var afterStepName = forLookup.Substring(stepName.Length); var lastSemicolon = afterStepName.LastIndexOf(';'); var currentSegment = lastSemicolon >= 0 ? afterStepName.Substring(lastSemicolon + 1).TrimStart() : afterStepName.TrimStart(' ', '[').TrimStart(); - // If current segment has a label, suggest values for that label var colonPos = currentSegment.IndexOf(':'); if (colonPos >= 0) { var label = currentSegment.Substring(0, colonPos).Trim(); - var matchingParam = definition.Params - .FirstOrDefault(p => (p.HrLabel ?? p.WrapperElement)? - .Equals(label, StringComparison.OrdinalIgnoreCase) == true); + var matchingParam = metadata.Params.FirstOrDefault(p => + p.HrLabel?.Equals(label, StringComparison.OrdinalIgnoreCase) == true); if (matchingParam != null) { @@ -74,9 +78,8 @@ public static (CompletionContext Context, IList Items) GetCompl } } - // Suggest param labels (unused ones) and positional values - var labelItems = GetParamLabelCompletions(definition, afterStepName); - var valueItems = GetPositionalValueCompletions(definition, afterStepName); + var labelItems = GetParamLabelCompletions(metadata, afterStepName); + var valueItems = GetPositionalValueCompletions(metadata, afterStepName); if (valueItems.Count > 0 && labelItems.Count > 0) { @@ -94,12 +97,14 @@ public static (CompletionContext Context, IList Items) GetCompl return (CompletionContext.None, Array.Empty()); } - private static (string Name, StepDefinition? Definition) FindStepName(string text) + private static (string Name, StepMetadata? Metadata) FindStepName(string text) { - // Try progressively longer prefixes to find the longest matching step name - // Steps can have spaces in names (e.g., "Go to Record/Request/Page") + // Try progressively longer prefixes to find the longest matching + // step name. Step names can contain spaces and punctuation (e.g. + // "Go to Record/Request/Page") so we walk whitespace-separated + // tokens and consult the registry at each boundary. var words = text.Split(' ', StringSplitOptions.RemoveEmptyEntries); - var bestMatch = (Name: "", Definition: (StepDefinition?)null); + var bestMatch = (Name: "", Metadata: (StepMetadata?)null); var candidate = ""; foreach (var word in words) @@ -107,11 +112,10 @@ private static (string Name, StepDefinition? Definition) FindStepName(string tex if (candidate.Length > 0) candidate += " "; candidate += word; - // Stop at bracket — everything after is params if (word.Contains('[')) break; - if (StepCatalogLoader.ByName.TryGetValue(candidate, out var def)) - bestMatch = (candidate, def); + if (StepRegistry.ByName.TryGetValue(candidate, out var md)) + bestMatch = (candidate, md); } return bestMatch; @@ -119,7 +123,7 @@ private static (string Name, StepDefinition? Definition) FindStepName(string tex private static IList GetStepNameCompletions(string prefix) { - return StepCatalogLoader.All + return StepRegistry.All .Where(s => s.Name.StartsWith(prefix, StringComparison.OrdinalIgnoreCase) || string.IsNullOrEmpty(prefix)) .OrderBy(s => s.Name) @@ -129,35 +133,61 @@ private static IList GetStepNameCompletions(string prefix) if (s.BlockPair != null) desc += $" ({s.BlockPair.Role})"; if (!string.IsNullOrEmpty(s.HrSignature)) desc += $" {s.HrSignature}"; - // Use the Monaco snippet for full step template with placeholders - var snippet = s.MonacoSnippet; - // For self-closing steps without a snippet, just use the name - if (snippet == null && s.SelfClosing) - snippet = s.Name; - - return (ICompletionData)new FmScriptCompletionData(s.Name, desc, snippet: snippet); + return (ICompletionData)new FmScriptCompletionData( + s.Name, desc, snippet: SynthesizeStepSnippet(s)); }) .ToList(); } + /// + /// Build a Monaco-style snippet (${N:placeholder} tab-stops) + /// from the step's so that accepting a + /// step-name completion inserts the full bracketed form with the + /// first parameter pre-selected for immediate editing. + /// + /// + /// Without this, multi-param steps complete to the bare name and the + /// user ends up with Set Error Capture On instead of + /// Set Error Capture [ On ] when the param-value prompt fires + /// next — the param-value path doesn't know to add brackets. + /// + /// + private static string SynthesizeStepSnippet(StepMetadata metadata) + { + if (metadata.Params.Count == 0) + return metadata.Name; + + var segments = new List(metadata.Params.Count); + int index = 1; + foreach (var param in metadata.Params) + { + var placeholder = param.ValidValues?.FirstOrDefault() + ?? param.DefaultValue + ?? param.HrLabel + ?? param.Name + ?? "value"; + var tabStop = $"${{{index++}:{placeholder}}}"; + segments.Add(param.HrLabel != null ? $"{param.HrLabel}: {tabStop}" : tabStop); + } + + return $"{metadata.Name} [ {string.Join(" ; ", segments)} ]"; + } + private static IList GetParamLabelCompletions( - StepDefinition definition, string existingParams) + StepMetadata metadata, string existingParams) { var items = new List(); - foreach (var param in definition.Params) + foreach (var param in metadata.Params) { - var label = param.HrLabel - ?? (param.Type == "namedCalc" && param.WrapperElement != null - ? param.WrapperElement : null); + var label = param.HrLabel; if (label == null) continue; - // Skip labels already used if (existingParams.Contains(label + ":", StringComparison.OrdinalIgnoreCase)) continue; - var desc = $"{param.Type}"; - var validValues = ScriptValidator.GetValidValues(param); + var desc = param.Type; + var validValues = StepRegistry.GetValidValues(param); if (validValues.Count > 0) desc += $" ({string.Join("|", validValues)})"; @@ -167,20 +197,19 @@ private static IList GetParamLabelCompletions( return items; } - private static IList GetParamValueCompletions(StepParam param) + private static IList GetParamValueCompletions(ParamMetadata param) { - var validValues = ScriptValidator.GetValidValues(param); + var validValues = StepRegistry.GetValidValues(param); return validValues .Select(v => (ICompletionData)new FmScriptCompletionData(v)) .ToList(); } private static IList GetPositionalValueCompletions( - StepDefinition definition, string existingParams) + StepMetadata metadata, string existingParams) { var items = new List(); - // Strip the opening bracket so it isn't counted as a positional segment var paramsText = existingParams.TrimStart(); if (paramsText.StartsWith('[')) paramsText = paramsText.Substring(1); @@ -193,22 +222,22 @@ private static IList GetPositionalValueCompletions( int positionalIndex = 0; foreach (var seg in segments) { - bool hasLabel = definition.Params.Any(p => + bool hasLabel = metadata.Params.Any(p => p.HrLabel != null && seg.StartsWith(p.HrLabel + ":", StringComparison.OrdinalIgnoreCase)); if (!hasLabel) positionalIndex++; } int unlabeledCount = 0; - foreach (var param in definition.Params) + foreach (var param in metadata.Params) { if (param.HrLabel != null) continue; if (unlabeledCount == positionalIndex) { - var values = ScriptValidator.GetValidValues(param); + var values = StepRegistry.GetValidValues(param); foreach (var v in values) - items.Add(new FmScriptCompletionData(v, $"{param.XmlElement}")); + items.Add(new FmScriptCompletionData(v, param.XmlElement)); break; } unlabeledCount++; diff --git a/tests/SharpFM.Tests/Scripting/CompletionProviderTests.cs b/tests/SharpFM.Tests/Scripting/CompletionProviderTests.cs index 7bc540a..64a83f9 100644 --- a/tests/SharpFM.Tests/Scripting/CompletionProviderTests.cs +++ b/tests/SharpFM.Tests/Scripting/CompletionProviderTests.cs @@ -1,87 +1,149 @@ using System.Linq; using SharpFM.Scripting; +using SharpFM.Scripting.Editor; using Xunit; namespace SharpFM.Tests.ScriptConverter; +/// +/// Tests for , which now reads +/// exclusively from StepRegistry. Only step names with a typed +/// POCO appear in completions — during the pilot that's three steps +/// (Beep, Set Error Capture, If). These tests are written against that +/// constraint and are expected to grow as more POCOs arrive during the +/// sweep phase. +/// public class CompletionProviderTests { [Fact] - public void EmptyLine_SuggestsStepNames() + public void EmptyLine_SuggestsPocoStepNames() { var (context, items) = FmScriptCompletionProvider.GetCompletions("", 0); Assert.Equal(CompletionContext.StepName, context); - Assert.True(items.Count > 100); - Assert.Contains(items, i => i.Text == "Set Variable"); + Assert.Contains(items, i => i.Text == "Beep"); + Assert.Contains(items, i => i.Text == "Set Error Capture"); Assert.Contains(items, i => i.Text == "If"); } + [Fact] + public void NonPocoStep_NotSuggested() + { + // Pilot-time regression is intentional: unmigrated steps are + // absent from completion until they gain a POCO. + var (_, items) = FmScriptCompletionProvider.GetCompletions("", 0); + Assert.DoesNotContain(items, i => i.Text == "Set Field"); + Assert.DoesNotContain(items, i => i.Text == "Go to Record/Request/Page"); + } + [Fact] public void PartialStepName_SuggestsMatches() { var (context, items) = FmScriptCompletionProvider.GetCompletions("Set", 3); Assert.Equal(CompletionContext.StepName, context); - Assert.Contains(items, i => i.Text == "Set Variable"); - Assert.Contains(items, i => i.Text == "Set Field"); + Assert.Contains(items, i => i.Text == "Set Error Capture"); } [Fact] - public void InsideBrackets_SuggestsParamLabels() + public void InsideBrackets_NoLabeledParams_PilotRegression() { - // Add Account has labeled params: Authenticate via, Account Name, etc. - var (context, items) = FmScriptCompletionProvider.GetCompletions("Add Account [ ", 14); - Assert.Equal(CompletionContext.ParamLabel, context); - Assert.Contains(items, i => i.Text.StartsWith("Authenticate via")); + // None of the three pilot POCOs expose HrLabels, so label-based + // completion returns nothing. This test codifies the regression + // so it's obvious when later POCOs add labeled-param coverage. + var (context, _) = FmScriptCompletionProvider.GetCompletions("Set Error Capture [ ", 20); + // No labels means positional value path hits ValidValues for the + // boolean Set param, which is Context=ParamValue. + Assert.Equal(CompletionContext.ParamValue, context); } [Fact] public void UnlabeledBooleanParam_SuggestsOnOff() { - // Allow User Abort has an unlabeled boolean param → should suggest On/Off + // Set Error Capture has a single unlabeled boolean — On/Off + // should appear as positional value suggestions. var (context, items) = FmScriptCompletionProvider.GetCompletions( - "Allow User Abort [ ", 19); + "Set Error Capture [ ", 20); Assert.Equal(CompletionContext.ParamValue, context); Assert.Contains(items, i => i.Text == "On"); Assert.Contains(items, i => i.Text == "Off"); } [Fact] - public void GoToRecord_SuggestsPositionalEnumValues() + public void UnknownStep_ReturnsNone() { - // Go to Record/Request/Page has RowPageLocation enum as first unlabeled param var (context, items) = FmScriptCompletionProvider.GetCompletions( - "Go to Record/Request/Page [ ", 28); - Assert.Equal(CompletionContext.ParamValue, context); - Assert.Contains(items, i => i.Text == "First"); - Assert.Contains(items, i => i.Text == "Next"); - Assert.Contains(items, i => i.Text == "Last"); - // Should also include labeled params like "Exit after last:" - Assert.Contains(items, i => i.Text.StartsWith("Exit after last")); + "FakeStep [ ", 11); + Assert.Equal(CompletionContext.None, context); + Assert.Empty(items); } [Fact] - public void GoToRecord_AfterFirstParam_SuggestsLabels() + public void IndentedLine_SuggestsStepNames() { - // After providing the first positional param, suggest remaining labeled params - var (context, items) = FmScriptCompletionProvider.GetCompletions( - "Go to Record/Request/Page [ Next ; ", 35); - Assert.Contains(items, i => i.Text.StartsWith("Exit after last")); + var (context, items) = FmScriptCompletionProvider.GetCompletions(" ", 4); + Assert.Equal(CompletionContext.StepName, context); + Assert.NotEmpty(items); } [Fact] - public void UnknownStep_ReturnsNone() + public void StepNameCompletion_ReturnsPocoStepsFromRegistry() { - var (context, items) = FmScriptCompletionProvider.GetCompletions( - "FakeStep [ ", 11); - Assert.Equal(CompletionContext.None, context); - Assert.Empty(items); + var (_, items) = FmScriptCompletionProvider.GetCompletions("", 0); + var names = items.Select(i => i.Text).ToList(); + Assert.Contains("Beep", names); + Assert.Contains("Set Error Capture", names); + Assert.Contains("If", names); + // Guard the regression: catalog-only steps are absent until they migrate. + Assert.DoesNotContain("Set Field", names); } [Fact] - public void IndentedLine_SuggestsStepNames() + public void ParamValueCompletion_UsesValidValuesFromParamMetadata() { - var (context, items) = FmScriptCompletionProvider.GetCompletions(" ", 4); - Assert.Equal(CompletionContext.StepName, context); - Assert.True(items.Count > 100); + // Confirms the completion provider reads ValidValues off + // ParamMetadata rather than the legacy StepParam shape. + var (_, items) = FmScriptCompletionProvider.GetCompletions( + "Set Error Capture [ ", 20); + Assert.Contains(items, i => i.Text == "On"); + Assert.Contains(items, i => i.Text == "Off"); + } + + [Fact] + public void StepNameCompletion_MultiParamStep_InsertsBracketedSnippet() + { + // Regression: accepting the step-name completion used to insert + // the bare name ("Set Error Capture"), so when the param-value + // prompt fired next, the user ended up with "Set Error Capture On" + // instead of "Set Error Capture [ On ]". Fix: synthesize a + // Monaco snippet with bracketed placeholder tab-stops. + var (_, items) = FmScriptCompletionProvider.GetCompletions("Set E", 5); + var data = (SharpFM.Scripting.Editor.FmScriptCompletionData) + items.Single(i => i.Text == "Set Error Capture"); + + // The snippet field is the text actually inserted on accept. It + // must contain the bracketed form with a Monaco ${N:placeholder} + // tab-stop so the first value is pre-selected for editing. + var snippetField = typeof(SharpFM.Scripting.Editor.FmScriptCompletionData) + .GetField("_snippet", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic); + var snippet = (string?)snippetField!.GetValue(data); + + Assert.NotNull(snippet); + Assert.Contains("[", snippet); + Assert.Contains("]", snippet); + Assert.Contains("${1:", snippet); + } + + [Fact] + public void StepNameCompletion_ZeroParamStep_InsertsBareName() + { + // Beep has no params — the snippet is just the name, no brackets. + var (_, items) = FmScriptCompletionProvider.GetCompletions("Bee", 3); + var data = (SharpFM.Scripting.Editor.FmScriptCompletionData) + items.Single(i => i.Text == "Beep"); + + var snippetField = typeof(SharpFM.Scripting.Editor.FmScriptCompletionData) + .GetField("_snippet", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic); + var snippet = (string?)snippetField!.GetValue(data); + + Assert.Equal("Beep", snippet); } } From f59bcdeb904f3aea310136a2a1d131e4854d65b0 Mon Sep 17 00:00:00 2001 From: Nate Bross Date: Mon, 20 Apr 2026 21:20:58 -0500 Subject: [PATCH 04/38] docs: add Advanced Scripting Syntax reference and POCO sweep-phase plan - Elevates the display-extension style guide from docs/step-definitions.md into its own document with the three extension forms, parsing precedence, the zero-loss audit requirement, and explicit guidance on when to drop hidden XML state rather than round-trip it (Restore on If as the canonical drop example). - Adds the sweep-phase plan documenting tier ordering (A-D), per-step TDD workflow, PR cadence, consumer migration schedule, deletion schedule, coverage verification, regression containment, and the gotchas learned during pilot execution. - Replaces the inline style-guide section in step-definitions.md with a pointer to the new dedicated doc. --- docs/advanced-filemaker-scripting-syntax.md | 186 ++++ docs/plans/script-step-poco-sweep.md | 213 +++++ docs/step-definitions.md | 921 ++++++++++++++++++++ 3 files changed, 1320 insertions(+) create mode 100644 docs/advanced-filemaker-scripting-syntax.md create mode 100644 docs/plans/script-step-poco-sweep.md create mode 100644 docs/step-definitions.md diff --git a/docs/advanced-filemaker-scripting-syntax.md b/docs/advanced-filemaker-scripting-syntax.md new file mode 100644 index 0000000..d18905f --- /dev/null +++ b/docs/advanced-filemaker-scripting-syntax.md @@ -0,0 +1,186 @@ +# Advanced FileMaker Scripting Syntax + +Status: authoritative reference for all script-step POCOs. + +## Purpose + +SharpFM renders FileMaker script XML as editable display text. Some XML +state doesn't naturally appear in FileMaker Pro's own display (object +IDs, flags, embedded newlines, etc.). To round-trip that state faithfully +through display-text editing, SharpFM extends FM Pro's display grammar +with a small set of named conventions — collectively, **Advanced +FileMaker Scripting Syntax**. + +The extensions are safe because **FileMaker Pro never consumes SharpFM's +display text**. FM Pro reads only the binary `Mac-XMSS` clipboard +payload; `POCO.ToXml()` always emits pure FM Pro XML regardless of +whatever extensions are present in the display. The display text is a +SharpFM-internal surface for user editing, and our display parser only +ever reads our own extended output. + +## Core invariant + +> `ToDisplayLine()` and `FromDisplayParams()` together are lossless for +> every piece of state the POCO carries. XML state that is dropped is +> dropped because it carries no information, not because the display +> can't express it. + +Every POCO's docstring contains a **zero-loss audit** (see below) that +enumerates XML state explicitly and marks each item as either rendered +by FM Pro natively, covered by an advanced-syntax extension, or +intentionally dropped with a written rationale. + +## The three extension forms + +Pick the form that matches the shape of the hidden state: + +| Shape of state | Syntax | Example | Used for | +|---|---|---|---| +| Id annotation on a named reference | `(#id)` suffix after the name | `"Find Customer" (#7)` | Script, Layout, TableOccurrence, Field id round-trip | +| Boolean / enum flag already in FM Pro's grammar | word token inline, matching FM Pro's wording | `Exit after last: On` | Flags FM Pro itself renders; we mirror its wording | +| Bulk invisible state (multi-slot / structured) | trailing `; Kind: [...]` block, word-token values | `; Buttons: ["OK" commit; "Cancel" nocommit]` | Button configurations, input-field metadata, any future bulk state | + +### Form 1 — `(#id)` suffix + +Appended to a quoted or `Table::Field` name: + +- `FieldRef` emits `People::FirstName (#7)` when an id is known; plain + `People::FirstName` otherwise. +- `PerformScriptStep` emits `"Find Customer" (#42)`. +- `GoToLayoutStep` emits `"Invoices Detail" (#11)`. + +Omitted when the id is zero or unknown — `(#0)` would be visual noise +for unresolved references. + +### Form 2 — inline word tokens + +Parsed at a specific named prefix, matching FM Pro's own rendering of +the same flag: + +- `Exit after last: On` on found-set iterators. +- `With dialog: Off` on steps that can suppress their confirmation UI. +- `Restore: On|Off` — **reserved**. Not currently emitted by any POCO; + see "What to drop vs. surface" below for the rationale. + +### Form 3 — trailing `; Kind: [...]` blocks + +Bulk or structured state that doesn't reduce to a single flag: + +- `; Buttons: ["OK" commit; "Cancel" nocommit; "" nocommit]` — used by + Show Custom Dialog for its button configuration. +- Future bulk state (Input Field specs, etc.) takes this form. + +## Parsing precedence + +Named-prefix inline tokens (Form 2) are parsed before trailing +`Kind: [...]` blocks (Form 3). A display line like + +``` +Go to Record/Request/Page [ Next ; Exit after last: On ; Buttons: [...] ] +``` + +is tokenized as: +1. Positional `Next` (Form 2 equivalent — fixed-position enum). +2. Named inline `Exit after last: On` (Form 2). +3. Named block `Buttons: [...]` (Form 3). + +Form 1 (`(#id)`) is applied inside each name-bearing token — e.g. inside +the bracketed name-and-id of a named ref — and does not collide with +Form 2 or Form 3 separators. + +## Zero-loss audit requirement + +Every POCO author must complete this audit in the class XML doc +comment. Template: + +``` +/// +/// Zero-loss audit for StepName: +/// +/// <Step> attributes (enable/id/name) — round-tripped. +/// <Calculation> CDATA — round-tripped via Calculation. +/// <SomeElement state="..."/> — Form 2 token "Some: On|Off". +/// <Dropped/> — intentionally dropped; rationale: ... +/// +/// +``` + +Items fall into exactly one of these buckets: + +1. **Rendered natively by FM Pro** — FM Pro's display grammar already + covers it; SharpFM mirrors the wording. +2. **Covered by an extension form** — one of the three above. +3. **Intentionally dropped** — rationale required. See next section for + how to judge. + +Omitting the audit is a review blocker. + +## What to drop vs. surface + +Hidden state is surfaced only when a user could meaningfully change it. +Some state is structurally present in XML but semantically fixed — FM +Pro never alters it, never emits it in clipboard output, and no user +workflow produces a different value. Round-tripping such state adds +visual noise for zero information. + +### Canonical drop: `` on `If` + +Upstream `agentic-fm` snippets include the element; FM Pro's own +clipboard output never does; no FM Pro user interaction produces +`state="True"`. `IfStep` drops it on both read and write. The audit +entry documents the drop: + +> <Restore state="False"/> — intentionally dropped. FM Pro never +> changes the value and never emits the element in clipboard output; it +> carries no information worth round-tripping. + +### Canonical surface: field `id` via `(#id)` suffix + +`` is a real identity — the id +selects which field is referenced, and two fields named `F` in different +tables are not interchangeable. Dropping the id would change semantics. +`FieldRef` always emits `(#12)` when an id is available. + +### The heuristic + +- If two valid FM Pro script states would be visually identical under + the display grammar without the extension, **surface** the state. +- If the state has a fixed value that no user can change, **drop** it. +- If you're not sure which, **surface**. Reversing a surface → drop + later is non-breaking; reversing a drop → surface may break tests + users wrote against the earlier display. + +## Adjacent convention: `//` disabled-step prefix + +Disabled steps are prefixed with `//` in display text: + +``` +// Set Error Capture [ On ] +``` + +Parsing strips the `//` and sets `ScriptStep.Enabled = false`. This is +a document-level convention (applied to any step line) rather than a +per-step extension. Covered here for completeness. + +## Implementation touch points + +- `FieldRef.ToDisplayString` / `FieldRef.FromDisplayToken` + (`src/SharpFM.Model/Scripting/Values/FieldRef.cs`) — Form 1 reference + implementation. +- `CommentStep.ReturnGlyph` + (`src/SharpFM.Model/Scripting/Steps/CommentStep.cs`) — the `⏎` + (U+23CE) glyph for single-line rendering of multi-line comment text. + An idiom adjacent to the three forms but specific to Comment. +- `ScriptLineParser.ParseLine` + (`src/SharpFM.Model/Scripting/ScriptLineParser.cs`) — disabled-step + prefix and bracket tokenization. +- `PerformScriptStep.FromDisplayParams`, + `GoToLayoutStep.FromDisplayParams` — Form 1 regex parsers for named + refs with `(#id)` suffixes. + +## Change log + +- **2026-04** — Extracted from `docs/step-definitions.md:44-69` into + its own document as part of the POCO big-bang migration. Rationale + section ("what to drop vs. surface") added with `Restore` on `If` as + the canonical drop example. diff --git a/docs/plans/script-step-poco-sweep.md b/docs/plans/script-step-poco-sweep.md new file mode 100644 index 0000000..fa1d9c9 --- /dev/null +++ b/docs/plans/script-step-poco-sweep.md @@ -0,0 +1,213 @@ +# Script Step POCO Migration — Sweep-Phase Plan + +Status: plan. Executes after the pilot (`fuzzz/all-pocos`) merges. + +## Context + +The [pilot](../advanced-filemaker-scripting-syntax.md) established the +typed POCO pattern with three representative steps: `BeepStep`, +`SetErrorCaptureStep`, `IfStep`. The sweep covers the remaining 189 +FileMaker script steps (206 total − 14 pre-existing POCOs − 3 pilot). +When the sweep finishes, all 206 steps are typed POCOs, +`StepCatalogLoader` and the catalog-driven helpers are deleted, and +the `step-catalog-en.json` file is no longer embedded. + +The sweep is not a single PR. Each wave lands independently, leaves the +tree green, and can be reviewed in isolation. + +## Tier-based ordering + +Run the catalog through the tier filter from the earlier analysis (see +message history in the pilot branch's `agent/catalogs` walkthrough): + +| Tier | Shape | Count | Rationale | +|---|---|---|---| +| A | Zero-param | 43 | Trivially mechanical; establish the cadence. | +| B | Only boolean/enum params | 29 | Locks in `ParamMetadata.ValidValues` patterns. | +| C | Text-bearing | 7 | Introduces text-escaping and display bracket grammar. | +| D | Calc / field / named-ref / complex | ~110 | The variety bucket. Contains real work — advanced-syntax decisions land here. | + +Migrate in A → B → C → D order. Within a tier, batch by FileMaker +category (`control`, `windows`, `fields`, etc.) to keep related steps' +PRs discoverable. + +## Per-step TDD workflow + +For each step, in this order: + +1. **Copy canonical XML** from + `C:\source\personal\agentic-fm\agent\snippet_examples\steps\\.xml` + (outside this repo — read-only reference, never committed) into an + inline `const string` fixture in the new test class. +2. **Write the round-trip test first.** Parse the fixture → run through + the POCO's `Metadata.FromXml!(el).ToXml()` → assert + `XNode.DeepEquals` against the source. Fails because the POCO + doesn't exist yet. +3. **Write the POCO** — implement `IStepFactory`, define `Metadata`, + implement `ToXml` / `ToDisplayLine` / `FromXml` / + `FromDisplayParams`. Pattern from `BeepStep.cs` (zero-param), + `SetErrorCaptureStep.cs` (single-boolean), or `IfStep` (block-pair, + calc) — pick the closest match. +4. **Add the display tests.** `ToDisplayLine()` returns expected + string; `FromDisplay(Metadata.FromDisplay!(...))` survives a + round-trip. +5. **Add the disabled-step test.** `enable="False"` → `step.Enabled` + is false → `ToXml()` emits `enable="False"`. +6. **Complete the zero-loss audit** in the POCO's XML doc comment. + Every XML element/attribute either rendered natively by FM Pro, + covered by an advanced-syntax extension, or intentionally dropped + with rationale (see + [`advanced-filemaker-scripting-syntax.md`](../advanced-filemaker-scripting-syntax.md)). +7. **For Tier D steps that need advanced syntax**, land the syntax + extension behind its own commit before the POCO that uses it, so + reviewers can validate the grammar decision in isolation. + +## Rollout cadence + +- **One PR per batch of ~10 POCOs within a tier.** Smaller is too + noisy; larger is unreviewable. +- **Tier A fits in 4-5 PRs.** Tier B in 3. Tier C in 1. Tier D is + where PR planning gets real — expect 12-15 PRs depending on + per-step complexity. +- **Each PR's diff is only steps + their tests.** No consumer rewires, + no deletions (with the specific exceptions listed below). +- **Commit per POCO within a PR** so each step is individually + revertable if the pattern needs adjusting. + +## Consumer migration schedule + +Consumers of the legacy `StepCatalogLoader` / `StepCatalogGenerated` +flip to `StepRegistry` as their dependencies complete: + +| Consumer | Flips when | What changes | +|---|---|---| +| `FmScriptCompletionProvider` | Already migrated in pilot | — | +| `ScriptValidator` | All block-pair partners (If/End If, Loop/End Loop, Open/Commit Transaction, etc.) are POCOs | `definition.BlockPair` reads become `StepRegistry.ByName[name].BlockPair` | +| `ScriptTextParser.FromDisplayLine` | All catalog-name resolutions have POCO equivalents | Drops the `StepCatalogLoader.ByName.TryGetValue` fallback | +| `FmScript.ApplyUpdate` | Typed setters exist on every POCO | MCP apply-op dispatches to POCO-specific `With*` methods instead of generic `CatalogXmlBuilder.UpdateParam` | + +Until its flip point, each consumer keeps reading the legacy surface — +the pilot's `StepRegistry` bridge populates `StepXmlFactory` / +`StepDisplayFactory` so this works transparently. + +## Deletion schedule + +In order: + +1. **Per-wave**: as a tier completes, delete any catalog-helper code + paths that are now exclusively serving that tier's step shapes. + (Usually none — the helpers are shape-agnostic.) +2. **After Tier D completes** — when all 206 steps are POCOs and all + consumers above have flipped: + - Delete `src/SharpFM.Model/Scripting/StepCatalogLoader.cs`. + - Delete `src/SharpFM.Model/Scripting/IStepCatalog.cs`. + - Delete `src/SharpFM.Model/Scripting/Serialization/StepXmlFactory.cs`. + - Delete `src/SharpFM.Model/Scripting/Serialization/StepDisplayFactory.cs`. + - Remove the `StepRegistry → legacy factory` bridge from + `StepRegistry.Scan()` — now dead code. + - Delete `src/SharpFM.Model/Scripting/Steps/RawStep.cs`. + - Delete `src/SharpFM.Model/Scripting/Steps/RawStepAllowList.cs` + and its tests. + - Delete `src/SharpFM.Model/Scripting/Serialization/CatalogXmlBuilder.cs`. + - Delete `src/SharpFM.Model/Scripting/Serialization/CatalogParamExtractor.cs`. + - Delete `src/SharpFM.Model/Scripting/Serialization/CatalogDisplayRenderer.cs`. + - Delete `src/SharpFM.Model/Scripting/Serialization/CatalogValidator.cs`. + - Remove the `` + line from `src/SharpFM.Model/SharpFM.Model.csproj`. + - Delete `src/SharpFM.Model/Scripting/Catalog/step-catalog-en.json`. + - Introduce `UnknownStep` in place of `RawStep` for forward-compat + against future FileMaker step additions (wraps raw XElement; + same semantics as the retired `RawStep` when `Definition == null`). +3. **After ship verification** — `StepDefinition` / `StepParam` / + `StepBlockPair` / `BlockPairRole` / `StepEnumValue` no longer have + any catalog-side producers. They may be kept (used by + `StepMetadata.BlockPair`) or retired depending on whether the new + metadata types fully absorb them. Expect a small cleanup commit + here. + +## Coverage verification + +One test asserts every step catalogued by upstream is present in our +registry: + +```csharp +[Fact] +public void StepRegistry_CoversFullCatalog() +{ + var expected = File.ReadAllText("path/to/canonical/step-list.json"); + var names = JsonSerializer.Deserialize(expected); + foreach (var name in names) + Assert.True(StepRegistry.ByName.ContainsKey(name), + $"Missing POCO for step '{name}'"); +} +``` + +The canonical list is a snapshot of agentic-fm step names taken at +sweep kickoff (checked in as a one-off fixture — not the same as the +JSON catalog, just names + ids). As steps are added to upstream later, +the fixture is updated deliberately, preventing silent drift. + +## Regression containment + +The pilot's `FmScriptCompletionProvider` regression (only 3 steps +suggested in completions) resolves as the sweep progresses. Gate each +sweep PR on **monotonically increasing completion coverage**: + +- `CompletionProviderTests` has an `EmptyLine_SuggestsPocoStepNames` + test that enumerates all expected pilot step names. +- Each sweep PR adds the new step names to that test's expected list. +- A PR that removes a name from the list is a review blocker unless + justified (e.g. a step was consolidated). + +After Tier A merges, the list is 43 + 14 + 3 = 60 steps. After Tier B, +89. After Tier C, 96. After Tier D, 206. Upstream adds thereafter append. + +## Risks and gotchas (filled in during pilot execution) + +> This section gets populated with concrete discoveries from pilot +> implementation. Left as a placeholder until pilot lands. + +Observed so far: + +- **`StepRegistry` lazy init doesn't run before pre-existing consumers + touch `StepXmlFactory`.** Mitigation: single `[ModuleInitializer]` + on `StepRegistry` itself. Acceptable — one initializer on the + registry, not per-POCO. +- **Legacy `FmScript.ToDisplayLines` reads `step.Definition.BlockPair` + for indent decisions.** POCOs passing `Definition = null` break + indentation on block-pair steps. Mitigation for pilot: project a + minimal `StepDefinition` with just `BlockPair` populated into + `base(...)`. Cleanup: when `FmScript.ToDisplayLines` migrates to + `StepRegistry`, drop the synthesized `Definition`. +- **Tests that used `Beep` as a "catalog-but-not-POCO" canary break + when Beep migrates.** Mitigation: swap to `Halt Script` (or + whatever zero-param step is still on the catalog-only path). Expect + similar swaps as each subsequent zero-param step migrates. + +## Open questions deferred from pilot + +- **Plugin-scenario multi-assembly registration.** Today + `StepRegistry.Scan()` reflects only the model assembly. If a plugin + ships its own POCOs (future scenario), should the registry scan + loaded plugin assemblies too? Probable answer: yes, with an + `RegisterAssembly(Assembly)` public method plugins call on load. + Address when the first plugin author asks for it. +- **`Metadata.Notes` wiring into UI.** The pilot populates `StepNotes` + on `SetErrorCaptureStep` and `IfStep`. No UI reads it yet. Tooltip + and hover integration is a post-sweep concern — sequencing depends + on Monaco / Avalonia tooltip API decisions outside this migration's + scope. +- **`Metadata.MonacoSnippet` or synthesis.** Pilot dropped the + catalog's `MonacoSnippet` field entirely; completion falls back to + the step name for self-closing steps and provides nothing for + multi-param ones. Options: (a) let each POCO author an optional + snippet string, (b) synthesize at runtime from `ParamMetadata`, (c) + defer snippets until someone complains. Recommend (c) — the three + pilot POCOs work fine without; revisit after Tier B lands. +- **When to retire `StepDefinition`/`StepParam` records.** The new + metadata types (`StepMetadata` / `ParamMetadata`) are intended to + replace them, but `StepMetadata.BlockPair` currently reuses + `StepBlockPair` and `IfStep` still synthesizes a `StepDefinition` + for legacy `FmScript.ToDisplayLines`. Clean-up sequence TBD — + probably a small follow-up commit after the legacy factories are + deleted. diff --git a/docs/step-definitions.md b/docs/step-definitions.md new file mode 100644 index 0000000..e4532f6 --- /dev/null +++ b/docs/step-definitions.md @@ -0,0 +1,921 @@ +# FileMaker Script Step — Typed POCO Migration Tracker + +This document is the working checklist for migrating FileMaker script steps from the generic `RawStep` fallback to typed POCOs under `src/SharpFM.Model/Scripting/Steps/`. It exists to drive TDD: each step block holds a placeholder for verbatim FileMaker Pro clipboard XML, a list of the expected display-text form, and a checklist for the small refactor needed to land the typed POCO. + +**Rule of thumb:** we only add a typed POCO when a step has behavior the generic catalog-driven pipeline can't express (named references with ids, discriminated unions, bespoke display formats, nested calculations, custom parsing). Simple steps stay on `RawStep` indefinitely — that's fine, `RawStep` is fully lossless for the source XML. + +--- + +## How to use this document + +For each step you want to migrate: + +1. **Copy verbatim XML from FileMaker Pro.** Create the step in the script workspace, right-click the step in the script and choose Copy, then paste directly into the sample block here. Do this once per meaningful permutation (e.g. each `LayoutDestination` for Go to Layout, each `RowPageLocation` for Go to Record). +2. **Write failing tests first.** Create `tests/SharpFM.Tests/Scripting/Steps/{StepName}StepTests.cs` with the verbatim fixture as a constant and one test per behavior assertion: display rendering, round-trip XML preservation, and display-text → XML parsing. +3. **Run the tests and watch them fail.** They'll fail against the current `RawStep` path with whatever generic output the catalog produces. +4. **Add the typed POCO.** Create `src/SharpFM.Model/Scripting/Steps/{StepName}Step.cs` following the `GoToLayoutStep` pattern. Register with `StepXmlFactory` and `StepDisplayFactory` via a `[ModuleInitializer]`. +5. **Run the tests and watch them pass.** +6. **Check the regression list.** Any previously-failing test assertions for this step (see the tests under `tests/SharpFM.Tests/Scripting/` that failed after the `StepParamValue` deletion) should come back green. +7. **Check the box** on this document. + +## Reference: value types already defined + +Reuse these wherever they fit — don't create new ones unless the step genuinely needs a new shape. + +| Type | Location | Purpose | +|---|---|---| +| `NamedRef(int Id, string Name)` | `Values/NamedRef.cs` | id+name pairs for Script, Layout (named), TableOccurrence refs | +| `FieldRef(Table?, Id, Name, VariableName?)` | `Values/FieldRef.cs` | field or variable, with `ToDisplayString()` → `Table::Field` / `$var` | +| `Calculation(string Text)` | `Values/Calculation.cs` | CDATA-wrapped calc expression | +| `Animation(string WireValue)` | `Values/Animation.cs` | raw wire-string for FM animation names (unknown values pass through) | +| `LayoutTarget` (sealed record hierarchy) | `Values/LayoutTarget.cs` | Original / Named / ByNameCalc / ByNumberCalc | + +## Reference: architecture pattern + +See `src/SharpFM.Model/Scripting/Steps/GoToLayoutStep.cs` as the canonical example. Every typed POCO has: + +- Typed properties (no `XElement`, no string bags) +- `public static new ScriptStep FromXml(XElement step)` — parse from source XML +- `public static ScriptStep FromDisplayParams(bool enabled, string[] hrParams)` — parse from display-text tokens +- `public override XElement ToXml()` — pure function of the typed state +- `public override string ToDisplayLine()` — pure function of the typed state +- A `[ModuleInitializer]` method that calls `StepXmlFactory.Register` and `StepDisplayFactory.Register` + +## Reference: display extension style guide + +See [`advanced-filemaker-scripting-syntax.md`](advanced-filemaker-scripting-syntax.md) for the full spec — the three extension forms (`(#id)` suffix, inline word tokens, trailing `; Kind: [...]` blocks), parsing precedence, the zero-loss audit every POCO must complete, and the judgment call on when to drop hidden state rather than surface it. + +--- + +## Priority 1 — handler-backed steps (failing regressions) + +These 13 steps used to have bespoke display rendering or display-text parsing via the retired `Handlers/` tree. Their tests are currently failing because the generic catalog path can't reproduce their special formatting. Each one needs a typed POCO. + +P1 regression status: + +| Previously failing test | Status | Fixed by | +|---|---|---| +| `ScriptStepTests.SetField_FromXml_ToDisplayLine` | ✓ | `SetFieldStep` | +| `ScriptStepTests.SetVariable_FromXml_ToDisplayLine` | ✓ | `SetVariableStep` | +| `ScriptStepTests.SetVariable_WithRepetition_ToDisplayLine` | ✓ | `SetVariableStep` | +| `ScriptStepTests.PerformScript_FromXml_ToDisplayLine` | ✓ | `PerformScriptStep` | +| `FmScriptModelTests.FromDisplayText_ToXml_SetVariable` | ✓ | `SetVariableStep` | +| `FmScriptModelTests.FromXml_ToDisplayText_IfEndIf_Indented` | ✓ | `ControlFlowSteps` | +| `FmScriptModelTests.RoundTrip_RealisticScript` | ✓ | All above + Loop / End Loop typed POCOs | + +--- + +### Set Field *(#76)* + +- [x] **Typed POCO landed** — `SetFieldStep.cs`, tests in `SetFieldStepTests.cs` + +**Why it's special:** FM Pro's display format is `Set Field [ Target ; Calculation ]` — the Field param appears *before* the Calculation param, even though the catalog order is [Calculation, Field]. The old `SetFieldHandler.ToDisplayLine` reordered these explicitly. + +**Fields to carry:** `FieldRef Target`, `Calculation Expression`. Both required (though tolerant of missing for partial input). + +**Verbatim XML samples** *(captured from FM Pro via Raw Clipboard Viewer)*: + +```xml + + + + + +``` + +```xml + + + + + +``` + +```xml + + + + + +``` + +**Expected display forms:** + +- `Set Field [ ScriptDefinitionHelper::ModifiedBy ; "just-a-string" ]` +- `Set Field [ ScriptDefinitionHelper::ModifiedBy ; $variable + " string" ]` +- `Set Field [ ScriptDefinitionHelper::ModifiedBy ; ScriptDefinitionHelper::PrimaryKey & " " & ScriptDefinitionHelper::CreatedBy ]` (multi-line calc preserves internal newlines in CDATA but renders on one line for the display form) +- `Set Field [ MyField ]` (no calc — degenerate case, still worth testing) + +**Notes on the captured XML:** + +- The `Field` element carries `table`, `id`, and `name` attributes — all three must round-trip (id is the lossless anchor, matching the `(#id)` convention used by `GoToLayoutStep`). +- `Calculation` content is wrapped in CDATA and may contain literal newlines (sample 3). The typed POCO's `Calculation.Text` should preserve those newlines byte-for-byte. +- Param order in the source XML is `[Calculation, Field]` but FM Pro's display is `[ Field ; Calculation ]` — this reorder is exactly what `SetFieldHandler.ToDisplayLine` used to do. + +--- + +### Set Variable *(#141)* + +- [x] **Typed POCO landed** — `SetVariableStep.cs`, tests in `SetVariableStepTests.cs` + +**Why it's special:** The canonical display is `Set Variable [ $name[rep] ; Value: calc ]` — the `[rep]` suffix is shown only when repetition is set and not equal to 1. The catalog params are `Value` (namedCalc), `Repetition` (namedCalc), `Name` (text), which the old `SetVariableHandler` reordered and rewrapped into the display form above. Parsing back is also bespoke (`ParseVarRepetition` splits `$name[rep]`). + +**Fields to carry:** `string Name`, `Calculation Value`, `Calculation Repetition` (default `"1"`). + +**Verbatim XML samples** *(captured from FM Pro via Raw Clipboard Viewer)*: + +```xml + + + + + + + + + $count + +``` + +```xml + + + + + + + + + $arr + +``` + +```xml + + + + + + + + + $arr + +``` + +**Expected display forms:** + +- `Set Variable [ $count ; Value: 0 ]` +- `Set Variable [ $arr[3] ; Value: "third" ]` +- `Set Variable [ $arr[$anotherVariable] ; Value: $count + 5 ]` +- `Set Variable [ $x ]` (no value — degenerate) + +**Notes on the captured XML:** + +- XML param order is `[Value, Repetition, Name]`; display reorders to `[ Name[rep] ; Value: calc ]`. +- `Repetition` is a full `Calculation`, not an integer — it can be a literal (`1`, `3`) or any calc expression (`$anotherVariable`, `$count + 5`). The typed POCO should carry `Calculation Repetition`, not `int`. +- `Repetition` is *always present* in the source XML, even when it equals `1`. The display-line writer must suppress `[rep]` when `Repetition.Text == "1"` (literal-one check, not arithmetic), and the XML writer must always emit a `` element (round-trip requirement). +- `Name` carries the `$` prefix as part of the text content — do not strip it on parse; treat the whole string as the variable name. Parsing `$arr[3]` or `$arr[$anotherVariable]` back from display text means splitting on the outermost `[` / `]` to separate name from repetition calc (watch for nested brackets in the repetition expression). +- Both `Value` and `Repetition` wrap their `Calculation` in a named wrapper element (``) — this is the `namedCalc` catalog shape. + +--- + +### Perform Script *(#1)* + +- [x] **Typed POCO landed** — `PerformScriptStep.cs`, tests in `PerformScriptStepTests.cs` + +**Why it's special:** Two discriminated modes (like Go to Layout's `LayoutTarget`): + +1. **Static reference** — a named ` + +``` + +```xml + + + + + + + +``` + +```xml + + + + +``` + +```xml + + + + + + +``` + +**Expected display forms** *(FM Pro's native wording confirmed; SharpFM appends `(#id)` on static refs per the style guide)*: + +- `Perform Script [ Specified: From list ; "Dummy-Script-For-Reference" (#4) ; Parameter: $$SomeGlobalVariable ]` +- `Perform Script [ Specified: By name ; $$globalVar & " literal-string" ; Parameter: $$SomeGlobalVariable ]` *(no id — by-name form has no ` "; @@ -282,7 +282,7 @@ public void ScriptMetadata_AndSealedStep_BothPreservedOnRoundTrip() Assert.Equal("MyScript", scriptEl.Attribute("name")!.Value); // Sealed HaltScript's custom child preserved - var beep = scriptEl.Elements("Step").FirstOrDefault(s => s.Attribute("id")?.Value == "90"); + var beep = scriptEl.Elements("Step").FirstOrDefault(s => s.Attribute("id")?.Value == "85"); Assert.NotNull(beep); Assert.NotNull(beep!.Element("WrapperPreserved")); } @@ -305,7 +305,7 @@ public void SealedStep_AtEndOfDocumentNoTrailingNewline_Preserved() { var xml = @" 0]]> - + "; var editor = new ScriptClipEditor(xml); @@ -313,7 +313,7 @@ public void SealedStep_AtEndOfDocumentNoTrailingNewline_Preserved() var outDoc = XDocument.Parse(outXml); var beep = outDoc.Root!.Elements("Step").Last(); - Assert.Equal("Halt Script", beep.Attribute("name")!.Value); + Assert.Equal("Allow User Abort", beep.Attribute("name")!.Value); Assert.NotNull(beep.Element("EofCanary")); } diff --git a/tests/SharpFM.Tests/Scripting/Steps/CheckFoundSetStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/CheckFoundSetStepTests.cs new file mode 100644 index 0000000..1876832 --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/CheckFoundSetStepTests.cs @@ -0,0 +1,51 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +/// +/// Zero-param POCO tests for CheckFoundSetStep. Fixture is inline per the +/// pilot pattern; no FixtureLoader, no file I/O. +/// +public class CheckFoundSetStepTests +{ + private const string CanonicalXml = """"""; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = CheckFoundSetStep.Metadata.FromXml!(source); + + Assert.IsType(step); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Display_EmitsBareName() + { + var step = new CheckFoundSetStep(); + Assert.Equal("Check Found Set", step.ToDisplayLine()); + } + + [Fact] + public void Disabled_RoundTrips() + { + var source = XElement.Parse(""""""); + var step = CheckFoundSetStep.Metadata.FromXml!(source); + + Assert.False(step.Enabled); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Check Found Set", out var metadata)); + Assert.Equal(20, metadata!.Id); + Assert.Empty(metadata.Params); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/CheckRecordStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/CheckRecordStepTests.cs new file mode 100644 index 0000000..93995b9 --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/CheckRecordStepTests.cs @@ -0,0 +1,51 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +/// +/// Zero-param POCO tests for CheckRecordStep. Fixture is inline per the +/// pilot pattern; no FixtureLoader, no file I/O. +/// +public class CheckRecordStepTests +{ + private const string CanonicalXml = """"""; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = CheckRecordStep.Metadata.FromXml!(source); + + Assert.IsType(step); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Display_EmitsBareName() + { + var step = new CheckRecordStep(); + Assert.Equal("Check Record", step.ToDisplayLine()); + } + + [Fact] + public void Disabled_RoundTrips() + { + var source = XElement.Parse(""""""); + var step = CheckRecordStep.Metadata.FromXml!(source); + + Assert.False(step.Enabled); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Check Record", out var metadata)); + Assert.Equal(19, metadata!.Id); + Assert.Empty(metadata.Params); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/ClosePopoverStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/ClosePopoverStepTests.cs new file mode 100644 index 0000000..0ad026c --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/ClosePopoverStepTests.cs @@ -0,0 +1,51 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +/// +/// Zero-param POCO tests for ClosePopoverStep. Fixture is inline per the +/// pilot pattern; no FixtureLoader, no file I/O. +/// +public class ClosePopoverStepTests +{ + private const string CanonicalXml = """"""; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = ClosePopoverStep.Metadata.FromXml!(source); + + Assert.IsType(step); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Display_EmitsBareName() + { + var step = new ClosePopoverStep(); + Assert.Equal("Close Popover", step.ToDisplayLine()); + } + + [Fact] + public void Disabled_RoundTrips() + { + var source = XElement.Parse(""""""); + var step = ClosePopoverStep.Metadata.FromXml!(source); + + Assert.False(step.Enabled); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Close Popover", out var metadata)); + Assert.Equal(169, metadata!.Id); + Assert.Empty(metadata.Params); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/CommitTransactionStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/CommitTransactionStepTests.cs new file mode 100644 index 0000000..ac18b23 --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/CommitTransactionStepTests.cs @@ -0,0 +1,51 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +/// +/// Zero-param POCO tests for CommitTransactionStep. Fixture is inline per the +/// pilot pattern; no FixtureLoader, no file I/O. +/// +public class CommitTransactionStepTests +{ + private const string CanonicalXml = """"""; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = CommitTransactionStep.Metadata.FromXml!(source); + + Assert.IsType(step); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Display_EmitsBareName() + { + var step = new CommitTransactionStep(); + Assert.Equal("Commit Transaction", step.ToDisplayLine()); + } + + [Fact] + public void Disabled_RoundTrips() + { + var source = XElement.Parse(""""""); + var step = CommitTransactionStep.Metadata.FromXml!(source); + + Assert.False(step.Enabled); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Commit Transaction", out var metadata)); + Assert.Equal(206, metadata!.Id); + Assert.Empty(metadata.Params); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/CopyAllRecordsRequestsStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/CopyAllRecordsRequestsStepTests.cs new file mode 100644 index 0000000..5eb6692 --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/CopyAllRecordsRequestsStepTests.cs @@ -0,0 +1,51 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +/// +/// Zero-param POCO tests for CopyAllRecordsRequestsStep. Fixture is inline per the +/// pilot pattern; no FixtureLoader, no file I/O. +/// +public class CopyAllRecordsRequestsStepTests +{ + private const string CanonicalXml = """"""; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = CopyAllRecordsRequestsStep.Metadata.FromXml!(source); + + Assert.IsType(step); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Display_EmitsBareName() + { + var step = new CopyAllRecordsRequestsStep(); + Assert.Equal("Copy All Records/Requests", step.ToDisplayLine()); + } + + [Fact] + public void Disabled_RoundTrips() + { + var source = XElement.Parse(""""""); + var step = CopyAllRecordsRequestsStep.Metadata.FromXml!(source); + + Assert.False(step.Enabled); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Copy All Records/Requests", out var metadata)); + Assert.Equal(98, metadata!.Id); + Assert.Empty(metadata.Params); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/CopyRecordRequestStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/CopyRecordRequestStepTests.cs new file mode 100644 index 0000000..3709fd7 --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/CopyRecordRequestStepTests.cs @@ -0,0 +1,51 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +/// +/// Zero-param POCO tests for CopyRecordRequestStep. Fixture is inline per the +/// pilot pattern; no FixtureLoader, no file I/O. +/// +public class CopyRecordRequestStepTests +{ + private const string CanonicalXml = """"""; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = CopyRecordRequestStep.Metadata.FromXml!(source); + + Assert.IsType(step); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Display_EmitsBareName() + { + var step = new CopyRecordRequestStep(); + Assert.Equal("Copy Record/Request", step.ToDisplayLine()); + } + + [Fact] + public void Disabled_RoundTrips() + { + var source = XElement.Parse(""""""); + var step = CopyRecordRequestStep.Metadata.FromXml!(source); + + Assert.False(step.Enabled); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Copy Record/Request", out var metadata)); + Assert.Equal(101, metadata!.Id); + Assert.Empty(metadata.Params); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/CorrectWordStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/CorrectWordStepTests.cs new file mode 100644 index 0000000..6384382 --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/CorrectWordStepTests.cs @@ -0,0 +1,51 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +/// +/// Zero-param POCO tests for CorrectWordStep. Fixture is inline per the +/// pilot pattern; no FixtureLoader, no file I/O. +/// +public class CorrectWordStepTests +{ + private const string CanonicalXml = """"""; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = CorrectWordStep.Metadata.FromXml!(source); + + Assert.IsType(step); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Display_EmitsBareName() + { + var step = new CorrectWordStep(); + Assert.Equal("Correct Word", step.ToDisplayLine()); + } + + [Fact] + public void Disabled_RoundTrips() + { + var source = XElement.Parse(""""""); + var step = CorrectWordStep.Metadata.FromXml!(source); + + Assert.False(step.Enabled); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Correct Word", out var metadata)); + Assert.Equal(106, metadata!.Id); + Assert.Empty(metadata.Params); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/DuplicateRecordRequestStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/DuplicateRecordRequestStepTests.cs new file mode 100644 index 0000000..4921f6e --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/DuplicateRecordRequestStepTests.cs @@ -0,0 +1,51 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +/// +/// Zero-param POCO tests for DuplicateRecordRequestStep. Fixture is inline per the +/// pilot pattern; no FixtureLoader, no file I/O. +/// +public class DuplicateRecordRequestStepTests +{ + private const string CanonicalXml = """"""; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = DuplicateRecordRequestStep.Metadata.FromXml!(source); + + Assert.IsType(step); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Display_EmitsBareName() + { + var step = new DuplicateRecordRequestStep(); + Assert.Equal("Duplicate Record/Request", step.ToDisplayLine()); + } + + [Fact] + public void Disabled_RoundTrips() + { + var source = XElement.Parse(""""""); + var step = DuplicateRecordRequestStep.Metadata.FromXml!(source); + + Assert.False(step.Enabled); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Duplicate Record/Request", out var metadata)); + Assert.Equal(8, metadata!.Id); + Assert.Empty(metadata.Params); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/EditUserDictionaryStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/EditUserDictionaryStepTests.cs new file mode 100644 index 0000000..8167ba3 --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/EditUserDictionaryStepTests.cs @@ -0,0 +1,51 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +/// +/// Zero-param POCO tests for EditUserDictionaryStep. Fixture is inline per the +/// pilot pattern; no FixtureLoader, no file I/O. +/// +public class EditUserDictionaryStepTests +{ + private const string CanonicalXml = """"""; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = EditUserDictionaryStep.Metadata.FromXml!(source); + + Assert.IsType(step); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Display_EmitsBareName() + { + var step = new EditUserDictionaryStep(); + Assert.Equal("Edit User Dictionary", step.ToDisplayLine()); + } + + [Fact] + public void Disabled_RoundTrips() + { + var source = XElement.Parse(""""""); + var step = EditUserDictionaryStep.Metadata.FromXml!(source); + + Assert.False(step.Enabled); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Edit User Dictionary", out var metadata)); + Assert.Equal(109, metadata!.Id); + Assert.Empty(metadata.Params); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/ExitApplicationStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/ExitApplicationStepTests.cs new file mode 100644 index 0000000..a326733 --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/ExitApplicationStepTests.cs @@ -0,0 +1,51 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +/// +/// Zero-param POCO tests for ExitApplicationStep. Fixture is inline per the +/// pilot pattern; no FixtureLoader, no file I/O. +/// +public class ExitApplicationStepTests +{ + private const string CanonicalXml = """"""; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = ExitApplicationStep.Metadata.FromXml!(source); + + Assert.IsType(step); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Display_EmitsBareName() + { + var step = new ExitApplicationStep(); + Assert.Equal("Exit Application", step.ToDisplayLine()); + } + + [Fact] + public void Disabled_RoundTrips() + { + var source = XElement.Parse(""""""); + var step = ExitApplicationStep.Metadata.FromXml!(source); + + Assert.False(step.Enabled); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Exit Application", out var metadata)); + Assert.Equal(44, metadata!.Id); + Assert.Empty(metadata.Params); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/FlushCacheToDiskStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/FlushCacheToDiskStepTests.cs new file mode 100644 index 0000000..bbb6901 --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/FlushCacheToDiskStepTests.cs @@ -0,0 +1,51 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +/// +/// Zero-param POCO tests for FlushCacheToDiskStep. Fixture is inline per the +/// pilot pattern; no FixtureLoader, no file I/O. +/// +public class FlushCacheToDiskStepTests +{ + private const string CanonicalXml = """"""; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = FlushCacheToDiskStep.Metadata.FromXml!(source); + + Assert.IsType(step); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Display_EmitsBareName() + { + var step = new FlushCacheToDiskStep(); + Assert.Equal("Flush Cache to Disk", step.ToDisplayLine()); + } + + [Fact] + public void Disabled_RoundTrips() + { + var source = XElement.Parse(""""""); + var step = FlushCacheToDiskStep.Metadata.FromXml!(source); + + Assert.False(step.Enabled); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Flush Cache to Disk", out var metadata)); + Assert.Equal(102, metadata!.Id); + Assert.Empty(metadata.Params); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/FreezeWindowStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/FreezeWindowStepTests.cs new file mode 100644 index 0000000..1dc535b --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/FreezeWindowStepTests.cs @@ -0,0 +1,51 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +/// +/// Zero-param POCO tests for FreezeWindowStep. Fixture is inline per the +/// pilot pattern; no FixtureLoader, no file I/O. +/// +public class FreezeWindowStepTests +{ + private const string CanonicalXml = """"""; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = FreezeWindowStep.Metadata.FromXml!(source); + + Assert.IsType(step); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Display_EmitsBareName() + { + var step = new FreezeWindowStep(); + Assert.Equal("Freeze Window", step.ToDisplayLine()); + } + + [Fact] + public void Disabled_RoundTrips() + { + var source = XElement.Parse(""""""); + var step = FreezeWindowStep.Metadata.FromXml!(source); + + Assert.False(step.Enabled); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Freeze Window", out var metadata)); + Assert.Equal(79, metadata!.Id); + Assert.Empty(metadata.Params); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/GoToNextFieldStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/GoToNextFieldStepTests.cs new file mode 100644 index 0000000..66b7481 --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/GoToNextFieldStepTests.cs @@ -0,0 +1,51 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +/// +/// Zero-param POCO tests for GoToNextFieldStep. Fixture is inline per the +/// pilot pattern; no FixtureLoader, no file I/O. +/// +public class GoToNextFieldStepTests +{ + private const string CanonicalXml = """"""; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = GoToNextFieldStep.Metadata.FromXml!(source); + + Assert.IsType(step); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Display_EmitsBareName() + { + var step = new GoToNextFieldStep(); + Assert.Equal("Go to Next Field", step.ToDisplayLine()); + } + + [Fact] + public void Disabled_RoundTrips() + { + var source = XElement.Parse(""""""); + var step = GoToNextFieldStep.Metadata.FromXml!(source); + + Assert.False(step.Enabled); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Go to Next Field", out var metadata)); + Assert.Equal(5, metadata!.Id); + Assert.Empty(metadata.Params); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/GoToPreviousFieldStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/GoToPreviousFieldStepTests.cs new file mode 100644 index 0000000..18d6244 --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/GoToPreviousFieldStepTests.cs @@ -0,0 +1,51 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +/// +/// Zero-param POCO tests for GoToPreviousFieldStep. Fixture is inline per the +/// pilot pattern; no FixtureLoader, no file I/O. +/// +public class GoToPreviousFieldStepTests +{ + private const string CanonicalXml = """"""; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = GoToPreviousFieldStep.Metadata.FromXml!(source); + + Assert.IsType(step); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Display_EmitsBareName() + { + var step = new GoToPreviousFieldStep(); + Assert.Equal("Go to Previous Field", step.ToDisplayLine()); + } + + [Fact] + public void Disabled_RoundTrips() + { + var source = XElement.Parse(""""""); + var step = GoToPreviousFieldStep.Metadata.FromXml!(source); + + Assert.False(step.Enabled); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Go to Previous Field", out var metadata)); + Assert.Equal(4, metadata!.Id); + Assert.Empty(metadata.Params); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/HaltScriptStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/HaltScriptStepTests.cs new file mode 100644 index 0000000..ff2bd73 --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/HaltScriptStepTests.cs @@ -0,0 +1,51 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +/// +/// Zero-param POCO tests for HaltScriptStep. Fixture is inline per the +/// pilot pattern; no FixtureLoader, no file I/O. +/// +public class HaltScriptStepTests +{ + private const string CanonicalXml = """"""; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = HaltScriptStep.Metadata.FromXml!(source); + + Assert.IsType(step); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Display_EmitsBareName() + { + var step = new HaltScriptStep(); + Assert.Equal("Halt Script", step.ToDisplayLine()); + } + + [Fact] + public void Disabled_RoundTrips() + { + var source = XElement.Parse(""""""); + var step = HaltScriptStep.Metadata.FromXml!(source); + + Assert.False(step.Enabled); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Halt Script", out var metadata)); + Assert.Equal(90, metadata!.Id); + Assert.Empty(metadata.Params); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/ModifyLastFindStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/ModifyLastFindStepTests.cs new file mode 100644 index 0000000..06a2d39 --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/ModifyLastFindStepTests.cs @@ -0,0 +1,51 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +/// +/// Zero-param POCO tests for ModifyLastFindStep. Fixture is inline per the +/// pilot pattern; no FixtureLoader, no file I/O. +/// +public class ModifyLastFindStepTests +{ + private const string CanonicalXml = """"""; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = ModifyLastFindStep.Metadata.FromXml!(source); + + Assert.IsType(step); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Display_EmitsBareName() + { + var step = new ModifyLastFindStep(); + Assert.Equal("Modify Last Find", step.ToDisplayLine()); + } + + [Fact] + public void Disabled_RoundTrips() + { + var source = XElement.Parse(""""""); + var step = ModifyLastFindStep.Metadata.FromXml!(source); + + Assert.False(step.Enabled); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Modify Last Find", out var metadata)); + Assert.Equal(24, metadata!.Id); + Assert.Empty(metadata.Params); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/NewFileStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/NewFileStepTests.cs new file mode 100644 index 0000000..295ac03 --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/NewFileStepTests.cs @@ -0,0 +1,51 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +/// +/// Zero-param POCO tests for NewFileStep. Fixture is inline per the +/// pilot pattern; no FixtureLoader, no file I/O. +/// +public class NewFileStepTests +{ + private const string CanonicalXml = """"""; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = NewFileStep.Metadata.FromXml!(source); + + Assert.IsType(step); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Display_EmitsBareName() + { + var step = new NewFileStep(); + Assert.Equal("New File", step.ToDisplayLine()); + } + + [Fact] + public void Disabled_RoundTrips() + { + var source = XElement.Parse(""""""); + var step = NewFileStep.Metadata.FromXml!(source); + + Assert.False(step.Enabled); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("New File", out var metadata)); + Assert.Equal(82, metadata!.Id); + Assert.Empty(metadata.Params); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/NewRecordRequestStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/NewRecordRequestStepTests.cs new file mode 100644 index 0000000..717ece2 --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/NewRecordRequestStepTests.cs @@ -0,0 +1,51 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +/// +/// Zero-param POCO tests for NewRecordRequestStep. Fixture is inline per the +/// pilot pattern; no FixtureLoader, no file I/O. +/// +public class NewRecordRequestStepTests +{ + private const string CanonicalXml = """"""; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = NewRecordRequestStep.Metadata.FromXml!(source); + + Assert.IsType(step); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Display_EmitsBareName() + { + var step = new NewRecordRequestStep(); + Assert.Equal("New Record/Request", step.ToDisplayLine()); + } + + [Fact] + public void Disabled_RoundTrips() + { + var source = XElement.Parse(""""""); + var step = NewRecordRequestStep.Metadata.FromXml!(source); + + Assert.False(step.Enabled); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("New Record/Request", out var metadata)); + Assert.Equal(7, metadata!.Id); + Assert.Empty(metadata.Params); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/OmitRecordStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/OmitRecordStepTests.cs new file mode 100644 index 0000000..f56680f --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/OmitRecordStepTests.cs @@ -0,0 +1,51 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +/// +/// Zero-param POCO tests for OmitRecordStep. Fixture is inline per the +/// pilot pattern; no FixtureLoader, no file I/O. +/// +public class OmitRecordStepTests +{ + private const string CanonicalXml = """"""; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = OmitRecordStep.Metadata.FromXml!(source); + + Assert.IsType(step); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Display_EmitsBareName() + { + var step = new OmitRecordStep(); + Assert.Equal("Omit Record", step.ToDisplayLine()); + } + + [Fact] + public void Disabled_RoundTrips() + { + var source = XElement.Parse(""""""); + var step = OmitRecordStep.Metadata.FromXml!(source); + + Assert.False(step.Enabled); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Omit Record", out var metadata)); + Assert.Equal(25, metadata!.Id); + Assert.Empty(metadata.Params); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/OpenEditSavedFindsStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/OpenEditSavedFindsStepTests.cs new file mode 100644 index 0000000..7e307cf --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/OpenEditSavedFindsStepTests.cs @@ -0,0 +1,51 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +/// +/// Zero-param POCO tests for OpenEditSavedFindsStep. Fixture is inline per the +/// pilot pattern; no FixtureLoader, no file I/O. +/// +public class OpenEditSavedFindsStepTests +{ + private const string CanonicalXml = """"""; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = OpenEditSavedFindsStep.Metadata.FromXml!(source); + + Assert.IsType(step); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Display_EmitsBareName() + { + var step = new OpenEditSavedFindsStep(); + Assert.Equal("Open Edit Saved Finds", step.ToDisplayLine()); + } + + [Fact] + public void Disabled_RoundTrips() + { + var source = XElement.Parse(""""""); + var step = OpenEditSavedFindsStep.Metadata.FromXml!(source); + + Assert.False(step.Enabled); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Open Edit Saved Finds", out var metadata)); + Assert.Equal(149, metadata!.Id); + Assert.Empty(metadata.Params); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/OpenFavoritesStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/OpenFavoritesStepTests.cs new file mode 100644 index 0000000..996adf7 --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/OpenFavoritesStepTests.cs @@ -0,0 +1,51 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +/// +/// Zero-param POCO tests for OpenFavoritesStep. Fixture is inline per the +/// pilot pattern; no FixtureLoader, no file I/O. +/// +public class OpenFavoritesStepTests +{ + private const string CanonicalXml = """"""; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = OpenFavoritesStep.Metadata.FromXml!(source); + + Assert.IsType(step); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Display_EmitsBareName() + { + var step = new OpenFavoritesStep(); + Assert.Equal("Open Favorites", step.ToDisplayLine()); + } + + [Fact] + public void Disabled_RoundTrips() + { + var source = XElement.Parse(""""""); + var step = OpenFavoritesStep.Metadata.FromXml!(source); + + Assert.False(step.Enabled); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Open Favorites", out var metadata)); + Assert.Equal(183, metadata!.Id); + Assert.Empty(metadata.Params); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/OpenFileOptionsStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/OpenFileOptionsStepTests.cs new file mode 100644 index 0000000..1ad14ca --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/OpenFileOptionsStepTests.cs @@ -0,0 +1,51 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +/// +/// Zero-param POCO tests for OpenFileOptionsStep. Fixture is inline per the +/// pilot pattern; no FixtureLoader, no file I/O. +/// +public class OpenFileOptionsStepTests +{ + private const string CanonicalXml = """"""; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = OpenFileOptionsStep.Metadata.FromXml!(source); + + Assert.IsType(step); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Display_EmitsBareName() + { + var step = new OpenFileOptionsStep(); + Assert.Equal("Open File Options", step.ToDisplayLine()); + } + + [Fact] + public void Disabled_RoundTrips() + { + var source = XElement.Parse(""""""); + var step = OpenFileOptionsStep.Metadata.FromXml!(source); + + Assert.False(step.Enabled); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Open File Options", out var metadata)); + Assert.Equal(114, metadata!.Id); + Assert.Empty(metadata.Params); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/OpenFindReplaceStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/OpenFindReplaceStepTests.cs new file mode 100644 index 0000000..ce1a878 --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/OpenFindReplaceStepTests.cs @@ -0,0 +1,51 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +/// +/// Zero-param POCO tests for OpenFindReplaceStep. Fixture is inline per the +/// pilot pattern; no FixtureLoader, no file I/O. +/// +public class OpenFindReplaceStepTests +{ + private const string CanonicalXml = """"""; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = OpenFindReplaceStep.Metadata.FromXml!(source); + + Assert.IsType(step); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Display_EmitsBareName() + { + var step = new OpenFindReplaceStep(); + Assert.Equal("Open Find/Replace", step.ToDisplayLine()); + } + + [Fact] + public void Disabled_RoundTrips() + { + var source = XElement.Parse(""""""); + var step = OpenFindReplaceStep.Metadata.FromXml!(source); + + Assert.False(step.Enabled); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Open Find/Replace", out var metadata)); + Assert.Equal(129, metadata!.Id); + Assert.Empty(metadata.Params); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/OpenHelpStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/OpenHelpStepTests.cs new file mode 100644 index 0000000..7cbef4a --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/OpenHelpStepTests.cs @@ -0,0 +1,51 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +/// +/// Zero-param POCO tests for OpenHelpStep. Fixture is inline per the +/// pilot pattern; no FixtureLoader, no file I/O. +/// +public class OpenHelpStepTests +{ + private const string CanonicalXml = """"""; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = OpenHelpStep.Metadata.FromXml!(source); + + Assert.IsType(step); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Display_EmitsBareName() + { + var step = new OpenHelpStep(); + Assert.Equal("Open Help", step.ToDisplayLine()); + } + + [Fact] + public void Disabled_RoundTrips() + { + var source = XElement.Parse(""""""); + var step = OpenHelpStep.Metadata.FromXml!(source); + + Assert.False(step.Enabled); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Open Help", out var metadata)); + Assert.Equal(32, metadata!.Id); + Assert.Empty(metadata.Params); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/OpenHostsStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/OpenHostsStepTests.cs new file mode 100644 index 0000000..6333564 --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/OpenHostsStepTests.cs @@ -0,0 +1,51 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +/// +/// Zero-param POCO tests for OpenHostsStep. Fixture is inline per the +/// pilot pattern; no FixtureLoader, no file I/O. +/// +public class OpenHostsStepTests +{ + private const string CanonicalXml = """"""; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = OpenHostsStep.Metadata.FromXml!(source); + + Assert.IsType(step); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Display_EmitsBareName() + { + var step = new OpenHostsStep(); + Assert.Equal("Open Hosts", step.ToDisplayLine()); + } + + [Fact] + public void Disabled_RoundTrips() + { + var source = XElement.Parse(""""""); + var step = OpenHostsStep.Metadata.FromXml!(source); + + Assert.False(step.Enabled); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Open Hosts", out var metadata)); + Assert.Equal(118, metadata!.Id); + Assert.Empty(metadata.Params); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/OpenManageContainersStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/OpenManageContainersStepTests.cs new file mode 100644 index 0000000..f37c840 --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/OpenManageContainersStepTests.cs @@ -0,0 +1,51 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +/// +/// Zero-param POCO tests for OpenManageContainersStep. Fixture is inline per the +/// pilot pattern; no FixtureLoader, no file I/O. +/// +public class OpenManageContainersStepTests +{ + private const string CanonicalXml = """"""; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = OpenManageContainersStep.Metadata.FromXml!(source); + + Assert.IsType(step); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Display_EmitsBareName() + { + var step = new OpenManageContainersStep(); + Assert.Equal("Open Manage Containers", step.ToDisplayLine()); + } + + [Fact] + public void Disabled_RoundTrips() + { + var source = XElement.Parse(""""""); + var step = OpenManageContainersStep.Metadata.FromXml!(source); + + Assert.False(step.Enabled); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Open Manage Containers", out var metadata)); + Assert.Equal(156, metadata!.Id); + Assert.Empty(metadata.Params); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/OpenManageDataSourcesStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/OpenManageDataSourcesStepTests.cs new file mode 100644 index 0000000..01472c8 --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/OpenManageDataSourcesStepTests.cs @@ -0,0 +1,51 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +/// +/// Zero-param POCO tests for OpenManageDataSourcesStep. Fixture is inline per the +/// pilot pattern; no FixtureLoader, no file I/O. +/// +public class OpenManageDataSourcesStepTests +{ + private const string CanonicalXml = """"""; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = OpenManageDataSourcesStep.Metadata.FromXml!(source); + + Assert.IsType(step); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Display_EmitsBareName() + { + var step = new OpenManageDataSourcesStep(); + Assert.Equal("Open Manage Data Sources", step.ToDisplayLine()); + } + + [Fact] + public void Disabled_RoundTrips() + { + var source = XElement.Parse(""""""); + var step = OpenManageDataSourcesStep.Metadata.FromXml!(source); + + Assert.False(step.Enabled); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Open Manage Data Sources", out var metadata)); + Assert.Equal(140, metadata!.Id); + Assert.Empty(metadata.Params); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/OpenManageDatabaseStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/OpenManageDatabaseStepTests.cs new file mode 100644 index 0000000..8a407a1 --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/OpenManageDatabaseStepTests.cs @@ -0,0 +1,51 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +/// +/// Zero-param POCO tests for OpenManageDatabaseStep. Fixture is inline per the +/// pilot pattern; no FixtureLoader, no file I/O. +/// +public class OpenManageDatabaseStepTests +{ + private const string CanonicalXml = """"""; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = OpenManageDatabaseStep.Metadata.FromXml!(source); + + Assert.IsType(step); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Display_EmitsBareName() + { + var step = new OpenManageDatabaseStep(); + Assert.Equal("Open Manage Database", step.ToDisplayLine()); + } + + [Fact] + public void Disabled_RoundTrips() + { + var source = XElement.Parse(""""""); + var step = OpenManageDatabaseStep.Metadata.FromXml!(source); + + Assert.False(step.Enabled); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Open Manage Database", out var metadata)); + Assert.Equal(38, metadata!.Id); + Assert.Empty(metadata.Params); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/OpenManageLayoutsStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/OpenManageLayoutsStepTests.cs new file mode 100644 index 0000000..ec8a830 --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/OpenManageLayoutsStepTests.cs @@ -0,0 +1,51 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +/// +/// Zero-param POCO tests for OpenManageLayoutsStep. Fixture is inline per the +/// pilot pattern; no FixtureLoader, no file I/O. +/// +public class OpenManageLayoutsStepTests +{ + private const string CanonicalXml = """"""; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = OpenManageLayoutsStep.Metadata.FromXml!(source); + + Assert.IsType(step); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Display_EmitsBareName() + { + var step = new OpenManageLayoutsStep(); + Assert.Equal("Open Manage Layouts", step.ToDisplayLine()); + } + + [Fact] + public void Disabled_RoundTrips() + { + var source = XElement.Parse(""""""); + var step = OpenManageLayoutsStep.Metadata.FromXml!(source); + + Assert.False(step.Enabled); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Open Manage Layouts", out var metadata)); + Assert.Equal(151, metadata!.Id); + Assert.Empty(metadata.Params); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/OpenManageThemesStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/OpenManageThemesStepTests.cs new file mode 100644 index 0000000..2e050db --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/OpenManageThemesStepTests.cs @@ -0,0 +1,51 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +/// +/// Zero-param POCO tests for OpenManageThemesStep. Fixture is inline per the +/// pilot pattern; no FixtureLoader, no file I/O. +/// +public class OpenManageThemesStepTests +{ + private const string CanonicalXml = """"""; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = OpenManageThemesStep.Metadata.FromXml!(source); + + Assert.IsType(step); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Display_EmitsBareName() + { + var step = new OpenManageThemesStep(); + Assert.Equal("Open Manage Themes", step.ToDisplayLine()); + } + + [Fact] + public void Disabled_RoundTrips() + { + var source = XElement.Parse(""""""); + var step = OpenManageThemesStep.Metadata.FromXml!(source); + + Assert.False(step.Enabled); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Open Manage Themes", out var metadata)); + Assert.Equal(165, metadata!.Id); + Assert.Empty(metadata.Params); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/OpenManageValueListsStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/OpenManageValueListsStepTests.cs new file mode 100644 index 0000000..196c1f8 --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/OpenManageValueListsStepTests.cs @@ -0,0 +1,51 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +/// +/// Zero-param POCO tests for OpenManageValueListsStep. Fixture is inline per the +/// pilot pattern; no FixtureLoader, no file I/O. +/// +public class OpenManageValueListsStepTests +{ + private const string CanonicalXml = """"""; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = OpenManageValueListsStep.Metadata.FromXml!(source); + + Assert.IsType(step); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Display_EmitsBareName() + { + var step = new OpenManageValueListsStep(); + Assert.Equal("Open Manage Value Lists", step.ToDisplayLine()); + } + + [Fact] + public void Disabled_RoundTrips() + { + var source = XElement.Parse(""""""); + var step = OpenManageValueListsStep.Metadata.FromXml!(source); + + Assert.False(step.Enabled); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Open Manage Value Lists", out var metadata)); + Assert.Equal(112, metadata!.Id); + Assert.Empty(metadata.Params); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/OpenRecordRequestStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/OpenRecordRequestStepTests.cs new file mode 100644 index 0000000..9b3e331 --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/OpenRecordRequestStepTests.cs @@ -0,0 +1,51 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +/// +/// Zero-param POCO tests for OpenRecordRequestStep. Fixture is inline per the +/// pilot pattern; no FixtureLoader, no file I/O. +/// +public class OpenRecordRequestStepTests +{ + private const string CanonicalXml = """"""; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = OpenRecordRequestStep.Metadata.FromXml!(source); + + Assert.IsType(step); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Display_EmitsBareName() + { + var step = new OpenRecordRequestStep(); + Assert.Equal("Open Record/Request", step.ToDisplayLine()); + } + + [Fact] + public void Disabled_RoundTrips() + { + var source = XElement.Parse(""""""); + var step = OpenRecordRequestStep.Metadata.FromXml!(source); + + Assert.False(step.Enabled); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Open Record/Request", out var metadata)); + Assert.Equal(133, metadata!.Id); + Assert.Empty(metadata.Params); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/OpenScriptWorkspaceStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/OpenScriptWorkspaceStepTests.cs new file mode 100644 index 0000000..5962617 --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/OpenScriptWorkspaceStepTests.cs @@ -0,0 +1,51 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +/// +/// Zero-param POCO tests for OpenScriptWorkspaceStep. Fixture is inline per the +/// pilot pattern; no FixtureLoader, no file I/O. +/// +public class OpenScriptWorkspaceStepTests +{ + private const string CanonicalXml = """"""; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = OpenScriptWorkspaceStep.Metadata.FromXml!(source); + + Assert.IsType(step); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Display_EmitsBareName() + { + var step = new OpenScriptWorkspaceStep(); + Assert.Equal("Open Script Workspace", step.ToDisplayLine()); + } + + [Fact] + public void Disabled_RoundTrips() + { + var source = XElement.Parse(""""""); + var step = OpenScriptWorkspaceStep.Metadata.FromXml!(source); + + Assert.False(step.Enabled); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Open Script Workspace", out var metadata)); + Assert.Equal(88, metadata!.Id); + Assert.Empty(metadata.Params); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/OpenSettingsStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/OpenSettingsStepTests.cs new file mode 100644 index 0000000..3191afe --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/OpenSettingsStepTests.cs @@ -0,0 +1,51 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +/// +/// Zero-param POCO tests for OpenSettingsStep. Fixture is inline per the +/// pilot pattern; no FixtureLoader, no file I/O. +/// +public class OpenSettingsStepTests +{ + private const string CanonicalXml = """"""; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = OpenSettingsStep.Metadata.FromXml!(source); + + Assert.IsType(step); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Display_EmitsBareName() + { + var step = new OpenSettingsStep(); + Assert.Equal("Open Settings", step.ToDisplayLine()); + } + + [Fact] + public void Disabled_RoundTrips() + { + var source = XElement.Parse(""""""); + var step = OpenSettingsStep.Metadata.FromXml!(source); + + Assert.False(step.Enabled); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Open Settings", out var metadata)); + Assert.Equal(105, metadata!.Id); + Assert.Empty(metadata.Params); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/OpenSharingStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/OpenSharingStepTests.cs new file mode 100644 index 0000000..86c55eb --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/OpenSharingStepTests.cs @@ -0,0 +1,51 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +/// +/// Zero-param POCO tests for OpenSharingStep. Fixture is inline per the +/// pilot pattern; no FixtureLoader, no file I/O. +/// +public class OpenSharingStepTests +{ + private const string CanonicalXml = """"""; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = OpenSharingStep.Metadata.FromXml!(source); + + Assert.IsType(step); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Display_EmitsBareName() + { + var step = new OpenSharingStep(); + Assert.Equal("Open Sharing", step.ToDisplayLine()); + } + + [Fact] + public void Disabled_RoundTrips() + { + var source = XElement.Parse(""""""); + var step = OpenSharingStep.Metadata.FromXml!(source); + + Assert.False(step.Enabled); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Open Sharing", out var metadata)); + Assert.Equal(113, metadata!.Id); + Assert.Empty(metadata.Params); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/OpenUploadToHostStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/OpenUploadToHostStepTests.cs new file mode 100644 index 0000000..fd58d08 --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/OpenUploadToHostStepTests.cs @@ -0,0 +1,51 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +/// +/// Zero-param POCO tests for OpenUploadToHostStep. Fixture is inline per the +/// pilot pattern; no FixtureLoader, no file I/O. +/// +public class OpenUploadToHostStepTests +{ + private const string CanonicalXml = """"""; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = OpenUploadToHostStep.Metadata.FromXml!(source); + + Assert.IsType(step); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Display_EmitsBareName() + { + var step = new OpenUploadToHostStep(); + Assert.Equal("Open Upload to Host", step.ToDisplayLine()); + } + + [Fact] + public void Disabled_RoundTrips() + { + var source = XElement.Parse(""""""); + var step = OpenUploadToHostStep.Metadata.FromXml!(source); + + Assert.False(step.Enabled); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Open Upload to Host", out var metadata)); + Assert.Equal(172, metadata!.Id); + Assert.Empty(metadata.Params); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/RawStepAllowListTests.cs b/tests/SharpFM.Tests/Scripting/Steps/RawStepAllowListTests.cs index 5c96a25..aaf3811 100644 --- a/tests/SharpFM.Tests/Scripting/Steps/RawStepAllowListTests.cs +++ b/tests/SharpFM.Tests/Scripting/Steps/RawStepAllowListTests.cs @@ -50,10 +50,11 @@ public void TypedPocoStep_SetField_IsFullyEditable_ReturnsTrue() [Fact] public void RawStep_NotInAllowList_IsFullyEditable_ReturnsFalse() { - // "Halt Script" is a catalog-known step with no typed POCO (Beep - // was the original canary but has migrated to BeepStep). Not in - // the allow-list (which ships empty), so it must be sealed. - var xml = XElement.Parse(""); + // "Allow User Abort" is a catalog-known step with no typed POCO + // (Beep was the original canary but migrated in pilot; Halt + // Script was the Tier-A-era canary and migrated with Tier A). + // Not in the allow-list (which ships empty), so it must be sealed. + var xml = XElement.Parse(""); var step = ScriptStep.FromXml(xml); Assert.IsType(step); diff --git a/tests/SharpFM.Tests/Scripting/Steps/SelectAllStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/SelectAllStepTests.cs new file mode 100644 index 0000000..3bb0699 --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/SelectAllStepTests.cs @@ -0,0 +1,51 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +/// +/// Zero-param POCO tests for SelectAllStep. Fixture is inline per the +/// pilot pattern; no FixtureLoader, no file I/O. +/// +public class SelectAllStepTests +{ + private const string CanonicalXml = """"""; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = SelectAllStep.Metadata.FromXml!(source); + + Assert.IsType(step); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Display_EmitsBareName() + { + var step = new SelectAllStep(); + Assert.Equal("Select All", step.ToDisplayLine()); + } + + [Fact] + public void Disabled_RoundTrips() + { + var source = XElement.Parse(""""""); + var step = SelectAllStep.Metadata.FromXml!(source); + + Assert.False(step.Enabled); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Select All", out var metadata)); + Assert.Equal(50, metadata!.Id); + Assert.Empty(metadata.Params); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/SelectDictionariesStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/SelectDictionariesStepTests.cs new file mode 100644 index 0000000..7893027 --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/SelectDictionariesStepTests.cs @@ -0,0 +1,51 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +/// +/// Zero-param POCO tests for SelectDictionariesStep. Fixture is inline per the +/// pilot pattern; no FixtureLoader, no file I/O. +/// +public class SelectDictionariesStepTests +{ + private const string CanonicalXml = """"""; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = SelectDictionariesStep.Metadata.FromXml!(source); + + Assert.IsType(step); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Display_EmitsBareName() + { + var step = new SelectDictionariesStep(); + Assert.Equal("Select Dictionaries", step.ToDisplayLine()); + } + + [Fact] + public void Disabled_RoundTrips() + { + var source = XElement.Parse(""""""); + var step = SelectDictionariesStep.Metadata.FromXml!(source); + + Assert.False(step.Enabled); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Select Dictionaries", out var metadata)); + Assert.Equal(108, metadata!.Id); + Assert.Empty(metadata.Params); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/ShowAllRecordsStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/ShowAllRecordsStepTests.cs new file mode 100644 index 0000000..548487a --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/ShowAllRecordsStepTests.cs @@ -0,0 +1,51 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +/// +/// Zero-param POCO tests for ShowAllRecordsStep. Fixture is inline per the +/// pilot pattern; no FixtureLoader, no file I/O. +/// +public class ShowAllRecordsStepTests +{ + private const string CanonicalXml = """"""; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = ShowAllRecordsStep.Metadata.FromXml!(source); + + Assert.IsType(step); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Display_EmitsBareName() + { + var step = new ShowAllRecordsStep(); + Assert.Equal("Show All Records", step.ToDisplayLine()); + } + + [Fact] + public void Disabled_RoundTrips() + { + var source = XElement.Parse(""""""); + var step = ShowAllRecordsStep.Metadata.FromXml!(source); + + Assert.False(step.Enabled); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Show All Records", out var metadata)); + Assert.Equal(23, metadata!.Id); + Assert.Empty(metadata.Params); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/ShowOmittedOnlyStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/ShowOmittedOnlyStepTests.cs new file mode 100644 index 0000000..2940ec1 --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/ShowOmittedOnlyStepTests.cs @@ -0,0 +1,51 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +/// +/// Zero-param POCO tests for ShowOmittedOnlyStep. Fixture is inline per the +/// pilot pattern; no FixtureLoader, no file I/O. +/// +public class ShowOmittedOnlyStepTests +{ + private const string CanonicalXml = """"""; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = ShowOmittedOnlyStep.Metadata.FromXml!(source); + + Assert.IsType(step); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Display_EmitsBareName() + { + var step = new ShowOmittedOnlyStep(); + Assert.Equal("Show Omitted Only", step.ToDisplayLine()); + } + + [Fact] + public void Disabled_RoundTrips() + { + var source = XElement.Parse(""""""); + var step = ShowOmittedOnlyStep.Metadata.FromXml!(source); + + Assert.False(step.Enabled); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Show Omitted Only", out var metadata)); + Assert.Equal(27, metadata!.Id); + Assert.Empty(metadata.Params); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/SpellingOptionsStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/SpellingOptionsStepTests.cs new file mode 100644 index 0000000..79d2dc1 --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/SpellingOptionsStepTests.cs @@ -0,0 +1,51 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +/// +/// Zero-param POCO tests for SpellingOptionsStep. Fixture is inline per the +/// pilot pattern; no FixtureLoader, no file I/O. +/// +public class SpellingOptionsStepTests +{ + private const string CanonicalXml = """"""; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = SpellingOptionsStep.Metadata.FromXml!(source); + + Assert.IsType(step); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Display_EmitsBareName() + { + var step = new SpellingOptionsStep(); + Assert.Equal("Spelling Options", step.ToDisplayLine()); + } + + [Fact] + public void Disabled_RoundTrips() + { + var source = XElement.Parse(""""""); + var step = SpellingOptionsStep.Metadata.FromXml!(source); + + Assert.False(step.Enabled); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Spelling Options", out var metadata)); + Assert.Equal(107, metadata!.Id); + Assert.Empty(metadata.Params); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/UnsortRecordsStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/UnsortRecordsStepTests.cs new file mode 100644 index 0000000..442b7cd --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/UnsortRecordsStepTests.cs @@ -0,0 +1,51 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +/// +/// Zero-param POCO tests for UnsortRecordsStep. Fixture is inline per the +/// pilot pattern; no FixtureLoader, no file I/O. +/// +public class UnsortRecordsStepTests +{ + private const string CanonicalXml = """"""; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = UnsortRecordsStep.Metadata.FromXml!(source); + + Assert.IsType(step); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Display_EmitsBareName() + { + var step = new UnsortRecordsStep(); + Assert.Equal("Unsort Records", step.ToDisplayLine()); + } + + [Fact] + public void Disabled_RoundTrips() + { + var source = XElement.Parse(""""""); + var step = UnsortRecordsStep.Metadata.FromXml!(source); + + Assert.False(step.Enabled); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Unsort Records", out var metadata)); + Assert.Equal(21, metadata!.Id); + Assert.Empty(metadata.Params); + } +} From 7ad0750c5c701a302c258e23baacebe15d231b6b Mon Sep 17 00:00:00 2001 From: Nate Bross Date: Mon, 20 Apr 2026 21:32:49 -0500 Subject: [PATCH 06/38] feat(scripting): migrate 21 single-param boolean/enum steps to IStepFactory POCOs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tier B sweep batch 1 — covers the single-param subset (one boolean or one enum). Each POCO declares a typed property, round-trips the child, and renders/parses the display form with the HR-label prefix when present. Shape coverage: - Single boolean unlabeled (Allow Formatting Bar, Allow User Abort). - Single boolean labeled (Enter Browse Mode, Enter Preview Mode, Set Layout Object Animation, Set Revert Transaction on Error, Set Use System Formats). - Single boolean labeled with invertedHr (Delete All Records, Delete Portal Row, Delete Record/Request, Revert Record/Request). - Single enum unlabeled (Adjust Window, Arrange All Windows, AVPlayer Set Playback State, Enable Touch Keyboard, Send DDE Execute). - Single enum labeled with HR-mapped values (Scroll Window, Set Multi- User, Show/Hide Text Ruler, Undo/Redo, View As). Remaining Tier B multi-param steps (Commit Records/Requests, Convert File, Open Transaction, Refresh Window, Set Zoom Level, Show/Hide Menubar, Show/Hide Toolbars) ship in a follow-up batch. Sealed-step canary moves Allow User Abort -> Refresh Window in SealedStepPreservationTests / RawStepAllowListTests; the former migrated this batch, the latter remains RawStep-backed until the multi-param batch lands. --- .../Steps/AVPlayerSetPlaybackStateStep.cs | 95 ++++++++++++++++ .../Scripting/Steps/AdjustWindowStep.cs | 99 +++++++++++++++++ .../Scripting/Steps/AllowFormattingBarStep.cs | 70 ++++++++++++ .../Scripting/Steps/AllowUserAbortStep.cs | 69 ++++++++++++ .../Scripting/Steps/ArrangeAllWindowsStep.cs | 97 +++++++++++++++++ .../Scripting/Steps/DeleteAllRecordsStep.cs | 76 +++++++++++++ .../Scripting/Steps/DeletePortalRowStep.cs | 76 +++++++++++++ .../Steps/DeleteRecordRequestStep.cs | 76 +++++++++++++ .../Steps/EnableTouchKeyboardStep.cs | 95 ++++++++++++++++ .../Scripting/Steps/EnterBrowseModeStep.cs | 75 +++++++++++++ .../Scripting/Steps/EnterPreviewModeStep.cs | 75 +++++++++++++ .../Steps/RevertRecordRequestStep.cs | 76 +++++++++++++ .../Scripting/Steps/ScrollWindowStep.cs | 103 ++++++++++++++++++ .../Scripting/Steps/SendDDEExecuteStep.cs | 91 ++++++++++++++++ .../Steps/SetLayoutObjectAnimationStep.cs | 75 +++++++++++++ .../Scripting/Steps/SetMultiUserStep.cs | 99 +++++++++++++++++ .../Steps/SetRevertTransactionOnErrorStep.cs | 75 +++++++++++++ .../Steps/SetUseSystemFormatsStep.cs | 75 +++++++++++++ .../Scripting/Steps/ShowHideTextRulerStep.cs | 99 +++++++++++++++++ .../Scripting/Steps/UndoRedoStep.cs | 99 +++++++++++++++++ .../Scripting/Steps/ViewAsStep.cs | 101 +++++++++++++++++ .../Scripting/SealedStepPreservationTests.cs | 40 +++---- .../AVPlayerSetPlaybackStateStepTests.cs | 43 ++++++++ .../Scripting/Steps/AdjustWindowStepTests.cs | 43 ++++++++ .../Steps/AllowFormattingBarStepTests.cs | 49 +++++++++ .../Steps/AllowUserAbortStepTests.cs | 49 +++++++++ .../Steps/ArrangeAllWindowsStepTests.cs | 43 ++++++++ .../Steps/DeleteAllRecordsStepTests.cs | 49 +++++++++ .../Steps/DeletePortalRowStepTests.cs | 49 +++++++++ .../Steps/DeleteRecordRequestStepTests.cs | 49 +++++++++ .../Steps/EnableTouchKeyboardStepTests.cs | 43 ++++++++ .../Steps/EnterBrowseModeStepTests.cs | 49 +++++++++ .../Steps/EnterPreviewModeStepTests.cs | 49 +++++++++ .../Scripting/Steps/RawStepAllowListTests.cs | 11 +- .../Steps/RevertRecordRequestStepTests.cs | 49 +++++++++ .../Scripting/Steps/ScrollWindowStepTests.cs | 43 ++++++++ .../Steps/SendDDEExecuteStepTests.cs | 43 ++++++++ .../SetLayoutObjectAnimationStepTests.cs | 49 +++++++++ .../Scripting/Steps/SetMultiUserStepTests.cs | 43 ++++++++ .../SetRevertTransactionOnErrorStepTests.cs | 49 +++++++++ .../Steps/SetUseSystemFormatsStepTests.cs | 49 +++++++++ .../Steps/ShowHideTextRulerStepTests.cs | 43 ++++++++ .../Scripting/Steps/UndoRedoStepTests.cs | 43 ++++++++ .../Scripting/Steps/ViewAsStepTests.cs | 43 ++++++++ 44 files changed, 2791 insertions(+), 25 deletions(-) create mode 100644 src/SharpFM.Model/Scripting/Steps/AVPlayerSetPlaybackStateStep.cs create mode 100644 src/SharpFM.Model/Scripting/Steps/AdjustWindowStep.cs create mode 100644 src/SharpFM.Model/Scripting/Steps/AllowFormattingBarStep.cs create mode 100644 src/SharpFM.Model/Scripting/Steps/AllowUserAbortStep.cs create mode 100644 src/SharpFM.Model/Scripting/Steps/ArrangeAllWindowsStep.cs create mode 100644 src/SharpFM.Model/Scripting/Steps/DeleteAllRecordsStep.cs create mode 100644 src/SharpFM.Model/Scripting/Steps/DeletePortalRowStep.cs create mode 100644 src/SharpFM.Model/Scripting/Steps/DeleteRecordRequestStep.cs create mode 100644 src/SharpFM.Model/Scripting/Steps/EnableTouchKeyboardStep.cs create mode 100644 src/SharpFM.Model/Scripting/Steps/EnterBrowseModeStep.cs create mode 100644 src/SharpFM.Model/Scripting/Steps/EnterPreviewModeStep.cs create mode 100644 src/SharpFM.Model/Scripting/Steps/RevertRecordRequestStep.cs create mode 100644 src/SharpFM.Model/Scripting/Steps/ScrollWindowStep.cs create mode 100644 src/SharpFM.Model/Scripting/Steps/SendDDEExecuteStep.cs create mode 100644 src/SharpFM.Model/Scripting/Steps/SetLayoutObjectAnimationStep.cs create mode 100644 src/SharpFM.Model/Scripting/Steps/SetMultiUserStep.cs create mode 100644 src/SharpFM.Model/Scripting/Steps/SetRevertTransactionOnErrorStep.cs create mode 100644 src/SharpFM.Model/Scripting/Steps/SetUseSystemFormatsStep.cs create mode 100644 src/SharpFM.Model/Scripting/Steps/ShowHideTextRulerStep.cs create mode 100644 src/SharpFM.Model/Scripting/Steps/UndoRedoStep.cs create mode 100644 src/SharpFM.Model/Scripting/Steps/ViewAsStep.cs create mode 100644 tests/SharpFM.Tests/Scripting/Steps/AVPlayerSetPlaybackStateStepTests.cs create mode 100644 tests/SharpFM.Tests/Scripting/Steps/AdjustWindowStepTests.cs create mode 100644 tests/SharpFM.Tests/Scripting/Steps/AllowFormattingBarStepTests.cs create mode 100644 tests/SharpFM.Tests/Scripting/Steps/AllowUserAbortStepTests.cs create mode 100644 tests/SharpFM.Tests/Scripting/Steps/ArrangeAllWindowsStepTests.cs create mode 100644 tests/SharpFM.Tests/Scripting/Steps/DeleteAllRecordsStepTests.cs create mode 100644 tests/SharpFM.Tests/Scripting/Steps/DeletePortalRowStepTests.cs create mode 100644 tests/SharpFM.Tests/Scripting/Steps/DeleteRecordRequestStepTests.cs create mode 100644 tests/SharpFM.Tests/Scripting/Steps/EnableTouchKeyboardStepTests.cs create mode 100644 tests/SharpFM.Tests/Scripting/Steps/EnterBrowseModeStepTests.cs create mode 100644 tests/SharpFM.Tests/Scripting/Steps/EnterPreviewModeStepTests.cs create mode 100644 tests/SharpFM.Tests/Scripting/Steps/RevertRecordRequestStepTests.cs create mode 100644 tests/SharpFM.Tests/Scripting/Steps/ScrollWindowStepTests.cs create mode 100644 tests/SharpFM.Tests/Scripting/Steps/SendDDEExecuteStepTests.cs create mode 100644 tests/SharpFM.Tests/Scripting/Steps/SetLayoutObjectAnimationStepTests.cs create mode 100644 tests/SharpFM.Tests/Scripting/Steps/SetMultiUserStepTests.cs create mode 100644 tests/SharpFM.Tests/Scripting/Steps/SetRevertTransactionOnErrorStepTests.cs create mode 100644 tests/SharpFM.Tests/Scripting/Steps/SetUseSystemFormatsStepTests.cs create mode 100644 tests/SharpFM.Tests/Scripting/Steps/ShowHideTextRulerStepTests.cs create mode 100644 tests/SharpFM.Tests/Scripting/Steps/UndoRedoStepTests.cs create mode 100644 tests/SharpFM.Tests/Scripting/Steps/ViewAsStepTests.cs diff --git a/src/SharpFM.Model/Scripting/Steps/AVPlayerSetPlaybackStateStep.cs b/src/SharpFM.Model/Scripting/Steps/AVPlayerSetPlaybackStateStep.cs new file mode 100644 index 0000000..269add7 --- /dev/null +++ b/src/SharpFM.Model/Scripting/Steps/AVPlayerSetPlaybackStateStep.cs @@ -0,0 +1,95 @@ +using System; +using System.Collections.Generic; +using System.Xml.Linq; +using SharpFM.Model.Scripting.Registry; + +namespace SharpFM.Model.Scripting.Steps; + +/// +/// Zero-loss audit for AVPlayerSetPlaybackStateStep: the step's XML state is the three +/// <Step> attributes plus a single <PlaybackState value="..."/> +/// enum child. XML values and human-readable display values are mapped +/// through the static HR tables; round-trip preserves the XML value. +/// +public sealed class AVPlayerSetPlaybackStateStep : ScriptStep, IStepFactory +{ + public const int XmlId = 178; + public const string XmlName = "AVPlayer Set Playback State"; + + /// The enum XML value emitted on the <PlaybackState> element. + public string PlaybackState { get; set; } + + public AVPlayerSetPlaybackStateStep(string playbackState = "Stopped", bool enabled = true) + : base(null, enabled) + { + PlaybackState = playbackState; + } + + private static readonly IReadOnlyDictionary _xmlToHr = + new Dictionary(StringComparer.Ordinal) + { + ["Stopped"] = "Stopped", + ["Paused"] = "Paused", + ["Playing"] = "Playing", + }; + + private static readonly IReadOnlyDictionary _hrToXml = + new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["Stopped"] = "Stopped", + ["Paused"] = "Paused", + ["Playing"] = "Playing", + }; + + private static string ToHr(string xmlValue) => + _xmlToHr.TryGetValue(xmlValue, out var hr) ? hr : xmlValue; + + private static string FromHr(string hrValue) => + _hrToXml.TryGetValue(hrValue, out var xml) ? xml : hrValue; + + public override XElement ToXml() => + new("Step", + new XAttribute("enable", Enabled ? "True" : "False"), + new XAttribute("id", XmlId), + new XAttribute("name", XmlName), + new XElement("PlaybackState", + new XAttribute("value", PlaybackState))); + + public override string ToDisplayLine() => + $"AVPlayer Set Playback State [ {ToHr(PlaybackState)} ]"; + + public static new ScriptStep FromXml(XElement step) + { + var enabled = step.Attribute("enable")?.Value != "False"; + var value = step.Element("PlaybackState")?.Attribute("value")?.Value ?? "Stopped"; + return new AVPlayerSetPlaybackStateStep(value, enabled); + } + + public static ScriptStep FromDisplayParams(bool enabled, string[] hrParams) + { + var token = hrParams.Length > 0 ? hrParams[0].Trim() : ""; + return new AVPlayerSetPlaybackStateStep(FromHr(token), enabled); + } + + public static StepMetadata Metadata { get; } = new() + { + Name = XmlName, + Id = XmlId, + Category = "miscellaneous", + HelpUrl = "https://help.claris.com/en/pro-help/content/avplayer-set-playback-state.html", + Params = + [ + new ParamMetadata + { + Name = "PlaybackState", + XmlElement = "PlaybackState", + Type = "enum", + XmlAttr = "value", + DefaultValue = "Stopped", + ValidValues = ["Stopped", "Paused", "Playing"], + }, + ], + FromXml = FromXml, + FromDisplay = FromDisplayParams, + }; +} diff --git a/src/SharpFM.Model/Scripting/Steps/AdjustWindowStep.cs b/src/SharpFM.Model/Scripting/Steps/AdjustWindowStep.cs new file mode 100644 index 0000000..7647c49 --- /dev/null +++ b/src/SharpFM.Model/Scripting/Steps/AdjustWindowStep.cs @@ -0,0 +1,99 @@ +using System; +using System.Collections.Generic; +using System.Xml.Linq; +using SharpFM.Model.Scripting.Registry; + +namespace SharpFM.Model.Scripting.Steps; + +/// +/// Zero-loss audit for AdjustWindowStep: the step's XML state is the three +/// <Step> attributes plus a single <WindowState value="..."/> +/// enum child. XML values and human-readable display values are mapped +/// through the static HR tables; round-trip preserves the XML value. +/// +public sealed class AdjustWindowStep : ScriptStep, IStepFactory +{ + public const int XmlId = 31; + public const string XmlName = "Adjust Window"; + + /// The enum XML value emitted on the <WindowState> element. + public string WindowState { get; set; } + + public AdjustWindowStep(string windowState = "ResizeToFit", bool enabled = true) + : base(null, enabled) + { + WindowState = windowState; + } + + private static readonly IReadOnlyDictionary _xmlToHr = + new Dictionary(StringComparer.Ordinal) + { + ["Resize to Fit"] = "Resize to Fit", + ["Maximize"] = "Maximize", + ["Minimize"] = "Minimize", + ["Restore"] = "Restore", + ["Hide"] = "Hide", + }; + + private static readonly IReadOnlyDictionary _hrToXml = + new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["Resize to Fit"] = "Resize to Fit", + ["Maximize"] = "Maximize", + ["Minimize"] = "Minimize", + ["Restore"] = "Restore", + ["Hide"] = "Hide", + }; + + private static string ToHr(string xmlValue) => + _xmlToHr.TryGetValue(xmlValue, out var hr) ? hr : xmlValue; + + private static string FromHr(string hrValue) => + _hrToXml.TryGetValue(hrValue, out var xml) ? xml : hrValue; + + public override XElement ToXml() => + new("Step", + new XAttribute("enable", Enabled ? "True" : "False"), + new XAttribute("id", XmlId), + new XAttribute("name", XmlName), + new XElement("WindowState", + new XAttribute("value", WindowState))); + + public override string ToDisplayLine() => + $"Adjust Window [ {ToHr(WindowState)} ]"; + + public static new ScriptStep FromXml(XElement step) + { + var enabled = step.Attribute("enable")?.Value != "False"; + var value = step.Element("WindowState")?.Attribute("value")?.Value ?? "ResizeToFit"; + return new AdjustWindowStep(value, enabled); + } + + public static ScriptStep FromDisplayParams(bool enabled, string[] hrParams) + { + var token = hrParams.Length > 0 ? hrParams[0].Trim() : ""; + return new AdjustWindowStep(FromHr(token), enabled); + } + + public static StepMetadata Metadata { get; } = new() + { + Name = XmlName, + Id = XmlId, + Category = "windows", + HelpUrl = "https://help.claris.com/en/pro-help/content/adjust-window.html", + Params = + [ + new ParamMetadata + { + Name = "WindowState", + XmlElement = "WindowState", + Type = "enum", + XmlAttr = "value", + DefaultValue = "ResizeToFit", + ValidValues = ["Resize to Fit", "Maximize", "Minimize", "Restore", "Hide"], + }, + ], + FromXml = FromXml, + FromDisplay = FromDisplayParams, + }; +} diff --git a/src/SharpFM.Model/Scripting/Steps/AllowFormattingBarStep.cs b/src/SharpFM.Model/Scripting/Steps/AllowFormattingBarStep.cs new file mode 100644 index 0000000..18ee98f --- /dev/null +++ b/src/SharpFM.Model/Scripting/Steps/AllowFormattingBarStep.cs @@ -0,0 +1,70 @@ +using System; +using System.Xml.Linq; +using SharpFM.Model.Scripting.Registry; + +namespace SharpFM.Model.Scripting.Steps; + +/// +/// Zero-loss audit for AllowFormattingBarStep: the step's XML state is the three +/// <Step> attributes (enable/id/name) plus a single +/// <Set state="True|False"/> child. All round-tripped. +/// +public sealed class AllowFormattingBarStep : ScriptStep, IStepFactory +{ + public const int XmlId = 115; + public const string XmlName = "Allow Formatting Bar"; + + /// The boolean setting encoded as <Set state="True|False"/>. + public bool Set { get; set; } + + public AllowFormattingBarStep(bool set = false, bool enabled = true) + : base(null, enabled) + { + Set = set; + } + + public override XElement ToXml() => + new("Step", + new XAttribute("enable", Enabled ? "True" : "False"), + new XAttribute("id", XmlId), + new XAttribute("name", XmlName), + new XElement("Set", + new XAttribute("state", Set ? "True" : "False"))); + + public override string ToDisplayLine() => + $"Allow Formatting Bar [ {(Set ? "On" : "Off")} ]"; + + public static new ScriptStep FromXml(XElement step) + { + var enabled = step.Attribute("enable")?.Value != "False"; + var state = step.Element("Set")?.Attribute("state")?.Value == "True"; + return new AllowFormattingBarStep(state, enabled); + } + + public static ScriptStep FromDisplayParams(bool enabled, string[] hrParams) + { + var isOn = hrParams.Length > 0 && hrParams[0].Trim().Equals("On", StringComparison.OrdinalIgnoreCase); + return new AllowFormattingBarStep(isOn, enabled); + } + + public static StepMetadata Metadata { get; } = new() + { + Name = XmlName, + Id = XmlId, + Category = "miscellaneous", + HelpUrl = "https://help.claris.com/en/pro-help/content/allow-formatting-bar.html", + Params = + [ + new ParamMetadata + { + Name = "Set", + XmlElement = "Set", + Type = "boolean", + XmlAttr = "state", + ValidValues = ["On", "Off"], + }, + ], + FromXml = FromXml, + FromDisplay = FromDisplayParams, + }; +} diff --git a/src/SharpFM.Model/Scripting/Steps/AllowUserAbortStep.cs b/src/SharpFM.Model/Scripting/Steps/AllowUserAbortStep.cs new file mode 100644 index 0000000..53cff2a --- /dev/null +++ b/src/SharpFM.Model/Scripting/Steps/AllowUserAbortStep.cs @@ -0,0 +1,69 @@ +using System; +using System.Xml.Linq; +using SharpFM.Model.Scripting.Registry; + +namespace SharpFM.Model.Scripting.Steps; + +/// +/// Zero-loss audit for AllowUserAbortStep: the step's XML state is the three +/// <Step> attributes (enable/id/name) plus a single +/// <Set state="True|False"/> child. All round-tripped. +/// +public sealed class AllowUserAbortStep : ScriptStep, IStepFactory +{ + public const int XmlId = 85; + public const string XmlName = "Allow User Abort"; + + /// The boolean setting encoded as <Set state="True|False"/>. + public bool Set { get; set; } + + public AllowUserAbortStep(bool set = false, bool enabled = true) + : base(null, enabled) + { + Set = set; + } + + public override XElement ToXml() => + new("Step", + new XAttribute("enable", Enabled ? "True" : "False"), + new XAttribute("id", XmlId), + new XAttribute("name", XmlName), + new XElement("Set", + new XAttribute("state", Set ? "True" : "False"))); + + public override string ToDisplayLine() => + $"Allow User Abort [ {(Set ? "On" : "Off")} ]"; + + public static new ScriptStep FromXml(XElement step) + { + var enabled = step.Attribute("enable")?.Value != "False"; + var state = step.Element("Set")?.Attribute("state")?.Value == "True"; + return new AllowUserAbortStep(state, enabled); + } + + public static ScriptStep FromDisplayParams(bool enabled, string[] hrParams) + { + var isOn = hrParams.Length > 0 && hrParams[0].Trim().Equals("On", StringComparison.OrdinalIgnoreCase); + return new AllowUserAbortStep(isOn, enabled); + } + + public static StepMetadata Metadata { get; } = new() + { + Name = XmlName, + Id = XmlId, + Category = "control", + Params = + [ + new ParamMetadata + { + Name = "Set", + XmlElement = "Set", + Type = "boolean", + XmlAttr = "state", + ValidValues = ["On", "Off"], + }, + ], + FromXml = FromXml, + FromDisplay = FromDisplayParams, + }; +} diff --git a/src/SharpFM.Model/Scripting/Steps/ArrangeAllWindowsStep.cs b/src/SharpFM.Model/Scripting/Steps/ArrangeAllWindowsStep.cs new file mode 100644 index 0000000..a466904 --- /dev/null +++ b/src/SharpFM.Model/Scripting/Steps/ArrangeAllWindowsStep.cs @@ -0,0 +1,97 @@ +using System; +using System.Collections.Generic; +using System.Xml.Linq; +using SharpFM.Model.Scripting.Registry; + +namespace SharpFM.Model.Scripting.Steps; + +/// +/// Zero-loss audit for ArrangeAllWindowsStep: the step's XML state is the three +/// <Step> attributes plus a single <WindowArrangement value="..."/> +/// enum child. XML values and human-readable display values are mapped +/// through the static HR tables; round-trip preserves the XML value. +/// +public sealed class ArrangeAllWindowsStep : ScriptStep, IStepFactory +{ + public const int XmlId = 120; + public const string XmlName = "Arrange All Windows"; + + /// The enum XML value emitted on the <WindowArrangement> element. + public string WindowArrangement { get; set; } + + public ArrangeAllWindowsStep(string windowArrangement = "Cascade Window", bool enabled = true) + : base(null, enabled) + { + WindowArrangement = windowArrangement; + } + + private static readonly IReadOnlyDictionary _xmlToHr = + new Dictionary(StringComparer.Ordinal) + { + ["Tile Horizontally"] = "Tile Horizontally", + ["Tile Vertically"] = "Tile Vertically", + ["Cascade Window"] = "Cascade Window", + ["Bring All To Front"] = "Bring All To Front", + }; + + private static readonly IReadOnlyDictionary _hrToXml = + new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["Tile Horizontally"] = "Tile Horizontally", + ["Tile Vertically"] = "Tile Vertically", + ["Cascade Window"] = "Cascade Window", + ["Bring All To Front"] = "Bring All To Front", + }; + + private static string ToHr(string xmlValue) => + _xmlToHr.TryGetValue(xmlValue, out var hr) ? hr : xmlValue; + + private static string FromHr(string hrValue) => + _hrToXml.TryGetValue(hrValue, out var xml) ? xml : hrValue; + + public override XElement ToXml() => + new("Step", + new XAttribute("enable", Enabled ? "True" : "False"), + new XAttribute("id", XmlId), + new XAttribute("name", XmlName), + new XElement("WindowArrangement", + new XAttribute("value", WindowArrangement))); + + public override string ToDisplayLine() => + $"Arrange All Windows [ {ToHr(WindowArrangement)} ]"; + + public static new ScriptStep FromXml(XElement step) + { + var enabled = step.Attribute("enable")?.Value != "False"; + var value = step.Element("WindowArrangement")?.Attribute("value")?.Value ?? "Cascade Window"; + return new ArrangeAllWindowsStep(value, enabled); + } + + public static ScriptStep FromDisplayParams(bool enabled, string[] hrParams) + { + var token = hrParams.Length > 0 ? hrParams[0].Trim() : ""; + return new ArrangeAllWindowsStep(FromHr(token), enabled); + } + + public static StepMetadata Metadata { get; } = new() + { + Name = XmlName, + Id = XmlId, + Category = "windows", + HelpUrl = "https://help.claris.com/en/pro-help/content/arrange-all-windows.html", + Params = + [ + new ParamMetadata + { + Name = "WindowArrangement", + XmlElement = "WindowArrangement", + Type = "enum", + XmlAttr = "value", + DefaultValue = "Cascade Window", + ValidValues = ["Tile Horizontally", "Tile Vertically", "Cascade Window", "Bring All To Front"], + }, + ], + FromXml = FromXml, + FromDisplay = FromDisplayParams, + }; +} diff --git a/src/SharpFM.Model/Scripting/Steps/DeleteAllRecordsStep.cs b/src/SharpFM.Model/Scripting/Steps/DeleteAllRecordsStep.cs new file mode 100644 index 0000000..2361ddd --- /dev/null +++ b/src/SharpFM.Model/Scripting/Steps/DeleteAllRecordsStep.cs @@ -0,0 +1,76 @@ +using System; +using System.Xml.Linq; +using SharpFM.Model.Scripting.Registry; + +namespace SharpFM.Model.Scripting.Steps; + +/// +/// Zero-loss audit for DeleteAllRecordsStep: the step's XML state is the three +/// <Step> attributes (enable/id/name) plus a single +/// <NoInteract state="True|False"/> child. All round-tripped. +/// +public sealed class DeleteAllRecordsStep : ScriptStep, IStepFactory +{ + public const int XmlId = 10; + public const string XmlName = "Delete All Records"; + + /// The With dialog flag on the step. + public bool WithDialog { get; set; } + + public DeleteAllRecordsStep(bool withdialog = false, bool enabled = true) + : base(null, enabled) + { + WithDialog = withdialog; + } + + public override XElement ToXml() => + new("Step", + new XAttribute("enable", Enabled ? "True" : "False"), + new XAttribute("id", XmlId), + new XAttribute("name", XmlName), + new XElement("NoInteract", + new XAttribute("state", WithDialog ? "True" : "False"))); + + public override string ToDisplayLine() => + $"Delete All Records [ With dialog: {(WithDialog ? "Off" : "On")} ]"; + + public static new ScriptStep FromXml(XElement step) + { + var enabled = step.Attribute("enable")?.Value != "False"; + var state = step.Element("NoInteract")?.Attribute("state")?.Value == "True"; + return new DeleteAllRecordsStep(state, enabled); + } + + public static ScriptStep FromDisplayParams(bool enabled, string[] hrParams) + { + var token = hrParams.Length > 0 ? hrParams[0].Trim() : ""; + const string Prefix = "With dialog:"; + if (token.StartsWith(Prefix, StringComparison.OrdinalIgnoreCase)) + token = token.Substring(Prefix.Length).Trim(); + var isOn = token.Equals("On", StringComparison.OrdinalIgnoreCase); + return new DeleteAllRecordsStep(!isOn, enabled); + } + + public static StepMetadata Metadata { get; } = new() + { + Name = XmlName, + Id = XmlId, + Category = "records", + HelpUrl = "https://help.claris.com/en/pro-help/content/delete-all-records.html", + Params = + [ + new ParamMetadata + { + Name = "NoInteract", + XmlElement = "NoInteract", + Type = "boolean", + XmlAttr = "state", + HrLabel = "With dialog", + // invertedHr: display 'On' means XML state='False'. + ValidValues = ["On", "Off"], + }, + ], + FromXml = FromXml, + FromDisplay = FromDisplayParams, + }; +} diff --git a/src/SharpFM.Model/Scripting/Steps/DeletePortalRowStep.cs b/src/SharpFM.Model/Scripting/Steps/DeletePortalRowStep.cs new file mode 100644 index 0000000..f24ff96 --- /dev/null +++ b/src/SharpFM.Model/Scripting/Steps/DeletePortalRowStep.cs @@ -0,0 +1,76 @@ +using System; +using System.Xml.Linq; +using SharpFM.Model.Scripting.Registry; + +namespace SharpFM.Model.Scripting.Steps; + +/// +/// Zero-loss audit for DeletePortalRowStep: the step's XML state is the three +/// <Step> attributes (enable/id/name) plus a single +/// <NoInteract state="True|False"/> child. All round-tripped. +/// +public sealed class DeletePortalRowStep : ScriptStep, IStepFactory +{ + public const int XmlId = 104; + public const string XmlName = "Delete Portal Row"; + + /// The With dialog flag on the step. + public bool WithDialog { get; set; } + + public DeletePortalRowStep(bool withdialog = false, bool enabled = true) + : base(null, enabled) + { + WithDialog = withdialog; + } + + public override XElement ToXml() => + new("Step", + new XAttribute("enable", Enabled ? "True" : "False"), + new XAttribute("id", XmlId), + new XAttribute("name", XmlName), + new XElement("NoInteract", + new XAttribute("state", WithDialog ? "True" : "False"))); + + public override string ToDisplayLine() => + $"Delete Portal Row [ With dialog: {(WithDialog ? "Off" : "On")} ]"; + + public static new ScriptStep FromXml(XElement step) + { + var enabled = step.Attribute("enable")?.Value != "False"; + var state = step.Element("NoInteract")?.Attribute("state")?.Value == "True"; + return new DeletePortalRowStep(state, enabled); + } + + public static ScriptStep FromDisplayParams(bool enabled, string[] hrParams) + { + var token = hrParams.Length > 0 ? hrParams[0].Trim() : ""; + const string Prefix = "With dialog:"; + if (token.StartsWith(Prefix, StringComparison.OrdinalIgnoreCase)) + token = token.Substring(Prefix.Length).Trim(); + var isOn = token.Equals("On", StringComparison.OrdinalIgnoreCase); + return new DeletePortalRowStep(!isOn, enabled); + } + + public static StepMetadata Metadata { get; } = new() + { + Name = XmlName, + Id = XmlId, + Category = "records", + HelpUrl = "https://help.claris.com/en/pro-help/content/delete-portal-row.html", + Params = + [ + new ParamMetadata + { + Name = "NoInteract", + XmlElement = "NoInteract", + Type = "boolean", + XmlAttr = "state", + HrLabel = "With dialog", + // invertedHr: display 'On' means XML state='False'. + ValidValues = ["On", "Off"], + }, + ], + FromXml = FromXml, + FromDisplay = FromDisplayParams, + }; +} diff --git a/src/SharpFM.Model/Scripting/Steps/DeleteRecordRequestStep.cs b/src/SharpFM.Model/Scripting/Steps/DeleteRecordRequestStep.cs new file mode 100644 index 0000000..2a2ada5 --- /dev/null +++ b/src/SharpFM.Model/Scripting/Steps/DeleteRecordRequestStep.cs @@ -0,0 +1,76 @@ +using System; +using System.Xml.Linq; +using SharpFM.Model.Scripting.Registry; + +namespace SharpFM.Model.Scripting.Steps; + +/// +/// Zero-loss audit for DeleteRecordRequestStep: the step's XML state is the three +/// <Step> attributes (enable/id/name) plus a single +/// <NoInteract state="True|False"/> child. All round-tripped. +/// +public sealed class DeleteRecordRequestStep : ScriptStep, IStepFactory +{ + public const int XmlId = 9; + public const string XmlName = "Delete Record/Request"; + + /// The With dialog flag on the step. + public bool WithDialog { get; set; } + + public DeleteRecordRequestStep(bool withdialog = false, bool enabled = true) + : base(null, enabled) + { + WithDialog = withdialog; + } + + public override XElement ToXml() => + new("Step", + new XAttribute("enable", Enabled ? "True" : "False"), + new XAttribute("id", XmlId), + new XAttribute("name", XmlName), + new XElement("NoInteract", + new XAttribute("state", WithDialog ? "True" : "False"))); + + public override string ToDisplayLine() => + $"Delete Record/Request [ With dialog: {(WithDialog ? "Off" : "On")} ]"; + + public static new ScriptStep FromXml(XElement step) + { + var enabled = step.Attribute("enable")?.Value != "False"; + var state = step.Element("NoInteract")?.Attribute("state")?.Value == "True"; + return new DeleteRecordRequestStep(state, enabled); + } + + public static ScriptStep FromDisplayParams(bool enabled, string[] hrParams) + { + var token = hrParams.Length > 0 ? hrParams[0].Trim() : ""; + const string Prefix = "With dialog:"; + if (token.StartsWith(Prefix, StringComparison.OrdinalIgnoreCase)) + token = token.Substring(Prefix.Length).Trim(); + var isOn = token.Equals("On", StringComparison.OrdinalIgnoreCase); + return new DeleteRecordRequestStep(!isOn, enabled); + } + + public static StepMetadata Metadata { get; } = new() + { + Name = XmlName, + Id = XmlId, + Category = "records", + HelpUrl = "https://help.claris.com/en/pro-help/content/delete-record-request.html", + Params = + [ + new ParamMetadata + { + Name = "NoInteract", + XmlElement = "NoInteract", + Type = "boolean", + XmlAttr = "state", + HrLabel = "With dialog", + // invertedHr: display 'On' means XML state='False'. + ValidValues = ["On", "Off"], + }, + ], + FromXml = FromXml, + FromDisplay = FromDisplayParams, + }; +} diff --git a/src/SharpFM.Model/Scripting/Steps/EnableTouchKeyboardStep.cs b/src/SharpFM.Model/Scripting/Steps/EnableTouchKeyboardStep.cs new file mode 100644 index 0000000..2982639 --- /dev/null +++ b/src/SharpFM.Model/Scripting/Steps/EnableTouchKeyboardStep.cs @@ -0,0 +1,95 @@ +using System; +using System.Collections.Generic; +using System.Xml.Linq; +using SharpFM.Model.Scripting.Registry; + +namespace SharpFM.Model.Scripting.Steps; + +/// +/// Zero-loss audit for EnableTouchKeyboardStep: the step's XML state is the three +/// <Step> attributes plus a single <ShowHide value="..."/> +/// enum child. XML values and human-readable display values are mapped +/// through the static HR tables; round-trip preserves the XML value. +/// +public sealed class EnableTouchKeyboardStep : ScriptStep, IStepFactory +{ + public const int XmlId = 174; + public const string XmlName = "Enable Touch Keyboard"; + + /// The enum XML value emitted on the <ShowHide> element. + public string ShowHide { get; set; } + + public EnableTouchKeyboardStep(string showHide = "Show", bool enabled = true) + : base(null, enabled) + { + ShowHide = showHide; + } + + private static readonly IReadOnlyDictionary _xmlToHr = + new Dictionary(StringComparer.Ordinal) + { + ["On"] = "On", + ["Off"] = "Off", + ["Toggle"] = "Toggle", + }; + + private static readonly IReadOnlyDictionary _hrToXml = + new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["On"] = "On", + ["Off"] = "Off", + ["Toggle"] = "Toggle", + }; + + private static string ToHr(string xmlValue) => + _xmlToHr.TryGetValue(xmlValue, out var hr) ? hr : xmlValue; + + private static string FromHr(string hrValue) => + _hrToXml.TryGetValue(hrValue, out var xml) ? xml : hrValue; + + public override XElement ToXml() => + new("Step", + new XAttribute("enable", Enabled ? "True" : "False"), + new XAttribute("id", XmlId), + new XAttribute("name", XmlName), + new XElement("ShowHide", + new XAttribute("value", ShowHide))); + + public override string ToDisplayLine() => + $"Enable Touch Keyboard [ {ToHr(ShowHide)} ]"; + + public static new ScriptStep FromXml(XElement step) + { + var enabled = step.Attribute("enable")?.Value != "False"; + var value = step.Element("ShowHide")?.Attribute("value")?.Value ?? "Show"; + return new EnableTouchKeyboardStep(value, enabled); + } + + public static ScriptStep FromDisplayParams(bool enabled, string[] hrParams) + { + var token = hrParams.Length > 0 ? hrParams[0].Trim() : ""; + return new EnableTouchKeyboardStep(FromHr(token), enabled); + } + + public static StepMetadata Metadata { get; } = new() + { + Name = XmlName, + Id = XmlId, + Category = "miscellaneous", + HelpUrl = "https://help.claris.com/en/pro-help/content/enable-touch-keyboard.html", + Params = + [ + new ParamMetadata + { + Name = "ShowHide", + XmlElement = "ShowHide", + Type = "enum", + XmlAttr = "value", + DefaultValue = "Show", + ValidValues = ["On", "Off", "Toggle"], + }, + ], + FromXml = FromXml, + FromDisplay = FromDisplayParams, + }; +} diff --git a/src/SharpFM.Model/Scripting/Steps/EnterBrowseModeStep.cs b/src/SharpFM.Model/Scripting/Steps/EnterBrowseModeStep.cs new file mode 100644 index 0000000..cd3e542 --- /dev/null +++ b/src/SharpFM.Model/Scripting/Steps/EnterBrowseModeStep.cs @@ -0,0 +1,75 @@ +using System; +using System.Xml.Linq; +using SharpFM.Model.Scripting.Registry; + +namespace SharpFM.Model.Scripting.Steps; + +/// +/// Zero-loss audit for EnterBrowseModeStep: the step's XML state is the three +/// <Step> attributes (enable/id/name) plus a single +/// <Pause state="True|False"/> child. All round-tripped. +/// +public sealed class EnterBrowseModeStep : ScriptStep, IStepFactory +{ + public const int XmlId = 55; + public const string XmlName = "Enter Browse Mode"; + + /// The Pause flag on the step. + public bool Pause { get; set; } + + public EnterBrowseModeStep(bool pause = false, bool enabled = true) + : base(null, enabled) + { + Pause = pause; + } + + public override XElement ToXml() => + new("Step", + new XAttribute("enable", Enabled ? "True" : "False"), + new XAttribute("id", XmlId), + new XAttribute("name", XmlName), + new XElement("Pause", + new XAttribute("state", Pause ? "True" : "False"))); + + public override string ToDisplayLine() => + $"Enter Browse Mode [ Pause: {(Pause ? "On" : "Off")} ]"; + + public static new ScriptStep FromXml(XElement step) + { + var enabled = step.Attribute("enable")?.Value != "False"; + var state = step.Element("Pause")?.Attribute("state")?.Value == "True"; + return new EnterBrowseModeStep(state, enabled); + } + + public static ScriptStep FromDisplayParams(bool enabled, string[] hrParams) + { + var token = hrParams.Length > 0 ? hrParams[0].Trim() : ""; + const string Prefix = "Pause:"; + if (token.StartsWith(Prefix, StringComparison.OrdinalIgnoreCase)) + token = token.Substring(Prefix.Length).Trim(); + var isOn = token.Equals("On", StringComparison.OrdinalIgnoreCase); + return new EnterBrowseModeStep(isOn, enabled); + } + + public static StepMetadata Metadata { get; } = new() + { + Name = XmlName, + Id = XmlId, + Category = "navigation", + HelpUrl = "https://help.claris.com/en/pro-help/content/enter-browse-mode.html", + Params = + [ + new ParamMetadata + { + Name = "Pause", + XmlElement = "Pause", + Type = "boolean", + XmlAttr = "state", + HrLabel = "Pause", + ValidValues = ["On", "Off"], + }, + ], + FromXml = FromXml, + FromDisplay = FromDisplayParams, + }; +} diff --git a/src/SharpFM.Model/Scripting/Steps/EnterPreviewModeStep.cs b/src/SharpFM.Model/Scripting/Steps/EnterPreviewModeStep.cs new file mode 100644 index 0000000..9c82951 --- /dev/null +++ b/src/SharpFM.Model/Scripting/Steps/EnterPreviewModeStep.cs @@ -0,0 +1,75 @@ +using System; +using System.Xml.Linq; +using SharpFM.Model.Scripting.Registry; + +namespace SharpFM.Model.Scripting.Steps; + +/// +/// Zero-loss audit for EnterPreviewModeStep: the step's XML state is the three +/// <Step> attributes (enable/id/name) plus a single +/// <Pause state="True|False"/> child. All round-tripped. +/// +public sealed class EnterPreviewModeStep : ScriptStep, IStepFactory +{ + public const int XmlId = 41; + public const string XmlName = "Enter Preview Mode"; + + /// The Pause flag on the step. + public bool Pause { get; set; } + + public EnterPreviewModeStep(bool pause = false, bool enabled = true) + : base(null, enabled) + { + Pause = pause; + } + + public override XElement ToXml() => + new("Step", + new XAttribute("enable", Enabled ? "True" : "False"), + new XAttribute("id", XmlId), + new XAttribute("name", XmlName), + new XElement("Pause", + new XAttribute("state", Pause ? "True" : "False"))); + + public override string ToDisplayLine() => + $"Enter Preview Mode [ Pause: {(Pause ? "On" : "Off")} ]"; + + public static new ScriptStep FromXml(XElement step) + { + var enabled = step.Attribute("enable")?.Value != "False"; + var state = step.Element("Pause")?.Attribute("state")?.Value == "True"; + return new EnterPreviewModeStep(state, enabled); + } + + public static ScriptStep FromDisplayParams(bool enabled, string[] hrParams) + { + var token = hrParams.Length > 0 ? hrParams[0].Trim() : ""; + const string Prefix = "Pause:"; + if (token.StartsWith(Prefix, StringComparison.OrdinalIgnoreCase)) + token = token.Substring(Prefix.Length).Trim(); + var isOn = token.Equals("On", StringComparison.OrdinalIgnoreCase); + return new EnterPreviewModeStep(isOn, enabled); + } + + public static StepMetadata Metadata { get; } = new() + { + Name = XmlName, + Id = XmlId, + Category = "navigation", + HelpUrl = "https://help.claris.com/en/pro-help/content/enter-preview-mode.html", + Params = + [ + new ParamMetadata + { + Name = "Pause", + XmlElement = "Pause", + Type = "boolean", + XmlAttr = "state", + HrLabel = "Pause", + ValidValues = ["On", "Off"], + }, + ], + FromXml = FromXml, + FromDisplay = FromDisplayParams, + }; +} diff --git a/src/SharpFM.Model/Scripting/Steps/RevertRecordRequestStep.cs b/src/SharpFM.Model/Scripting/Steps/RevertRecordRequestStep.cs new file mode 100644 index 0000000..4e97c89 --- /dev/null +++ b/src/SharpFM.Model/Scripting/Steps/RevertRecordRequestStep.cs @@ -0,0 +1,76 @@ +using System; +using System.Xml.Linq; +using SharpFM.Model.Scripting.Registry; + +namespace SharpFM.Model.Scripting.Steps; + +/// +/// Zero-loss audit for RevertRecordRequestStep: the step's XML state is the three +/// <Step> attributes (enable/id/name) plus a single +/// <NoInteract state="True|False"/> child. All round-tripped. +/// +public sealed class RevertRecordRequestStep : ScriptStep, IStepFactory +{ + public const int XmlId = 51; + public const string XmlName = "Revert Record/Request"; + + /// The With dialog flag on the step. + public bool WithDialog { get; set; } + + public RevertRecordRequestStep(bool withdialog = true, bool enabled = true) + : base(null, enabled) + { + WithDialog = withdialog; + } + + public override XElement ToXml() => + new("Step", + new XAttribute("enable", Enabled ? "True" : "False"), + new XAttribute("id", XmlId), + new XAttribute("name", XmlName), + new XElement("NoInteract", + new XAttribute("state", WithDialog ? "True" : "False"))); + + public override string ToDisplayLine() => + $"Revert Record/Request [ With dialog: {(WithDialog ? "Off" : "On")} ]"; + + public static new ScriptStep FromXml(XElement step) + { + var enabled = step.Attribute("enable")?.Value != "False"; + var state = step.Element("NoInteract")?.Attribute("state")?.Value == "True"; + return new RevertRecordRequestStep(state, enabled); + } + + public static ScriptStep FromDisplayParams(bool enabled, string[] hrParams) + { + var token = hrParams.Length > 0 ? hrParams[0].Trim() : ""; + const string Prefix = "With dialog:"; + if (token.StartsWith(Prefix, StringComparison.OrdinalIgnoreCase)) + token = token.Substring(Prefix.Length).Trim(); + var isOn = token.Equals("On", StringComparison.OrdinalIgnoreCase); + return new RevertRecordRequestStep(!isOn, enabled); + } + + public static StepMetadata Metadata { get; } = new() + { + Name = XmlName, + Id = XmlId, + Category = "records", + HelpUrl = "https://help.claris.com/en/pro-help/content/revert-record-request.html", + Params = + [ + new ParamMetadata + { + Name = "NoInteract", + XmlElement = "NoInteract", + Type = "boolean", + XmlAttr = "state", + HrLabel = "With dialog", + // invertedHr: display 'On' means XML state='False'. + ValidValues = ["On", "Off"], + }, + ], + FromXml = FromXml, + FromDisplay = FromDisplayParams, + }; +} diff --git a/src/SharpFM.Model/Scripting/Steps/ScrollWindowStep.cs b/src/SharpFM.Model/Scripting/Steps/ScrollWindowStep.cs new file mode 100644 index 0000000..cef71f9 --- /dev/null +++ b/src/SharpFM.Model/Scripting/Steps/ScrollWindowStep.cs @@ -0,0 +1,103 @@ +using System; +using System.Collections.Generic; +using System.Xml.Linq; +using SharpFM.Model.Scripting.Registry; + +namespace SharpFM.Model.Scripting.Steps; + +/// +/// Zero-loss audit for ScrollWindowStep: the step's XML state is the three +/// <Step> attributes plus a single <ScrollOperation value="..."/> +/// enum child. XML values and human-readable display values are mapped +/// through the static HR tables; round-trip preserves the XML value. +/// +public sealed class ScrollWindowStep : ScriptStep, IStepFactory +{ + public const int XmlId = 81; + public const string XmlName = "Scroll Window"; + + /// The enum XML value emitted on the <ScrollOperation> element. + public string Direction { get; set; } + + public ScrollWindowStep(string direction = "Home", bool enabled = true) + : base(null, enabled) + { + Direction = direction; + } + + private static readonly IReadOnlyDictionary _xmlToHr = + new Dictionary(StringComparer.Ordinal) + { + ["Home"] = "Home", + ["End"] = "End", + ["PageUp"] = "Page Up", + ["PageDown"] = "Page Down", + ["ToSelection"] = "To Selection", + }; + + private static readonly IReadOnlyDictionary _hrToXml = + new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["Home"] = "Home", + ["End"] = "End", + ["Page Up"] = "PageUp", + ["Page Down"] = "PageDown", + ["To Selection"] = "ToSelection", + }; + + private static string ToHr(string xmlValue) => + _xmlToHr.TryGetValue(xmlValue, out var hr) ? hr : xmlValue; + + private static string FromHr(string hrValue) => + _hrToXml.TryGetValue(hrValue, out var xml) ? xml : hrValue; + + public override XElement ToXml() => + new("Step", + new XAttribute("enable", Enabled ? "True" : "False"), + new XAttribute("id", XmlId), + new XAttribute("name", XmlName), + new XElement("ScrollOperation", + new XAttribute("value", Direction))); + + public override string ToDisplayLine() => + $"Scroll Window [ Direction: {ToHr(Direction)} ]"; + + public static new ScriptStep FromXml(XElement step) + { + var enabled = step.Attribute("enable")?.Value != "False"; + var value = step.Element("ScrollOperation")?.Attribute("value")?.Value ?? "Home"; + return new ScrollWindowStep(value, enabled); + } + + public static ScriptStep FromDisplayParams(bool enabled, string[] hrParams) + { + var token = hrParams.Length > 0 ? hrParams[0].Trim() : ""; + const string Prefix = "Direction:"; + if (token.StartsWith(Prefix, StringComparison.OrdinalIgnoreCase)) + token = token.Substring(Prefix.Length).Trim(); + return new ScrollWindowStep(FromHr(token), enabled); + } + + public static StepMetadata Metadata { get; } = new() + { + Name = XmlName, + Id = XmlId, + Category = "windows", + HelpUrl = "https://help.claris.com/en/pro-help/content/scroll-window.html", + Params = + [ + new ParamMetadata + { + Name = "ScrollOperation", + XmlElement = "ScrollOperation", + Type = "enum", + XmlAttr = "value", + HrLabel = "Direction", + DefaultValue = "Home", + ValidValues = ["Home", "End", "Page Up", "Page Down", "To Selection"], + }, + ], + FromXml = FromXml, + FromDisplay = FromDisplayParams, + }; +} diff --git a/src/SharpFM.Model/Scripting/Steps/SendDDEExecuteStep.cs b/src/SharpFM.Model/Scripting/Steps/SendDDEExecuteStep.cs new file mode 100644 index 0000000..9cb5a6e --- /dev/null +++ b/src/SharpFM.Model/Scripting/Steps/SendDDEExecuteStep.cs @@ -0,0 +1,91 @@ +using System; +using System.Collections.Generic; +using System.Xml.Linq; +using SharpFM.Model.Scripting.Registry; + +namespace SharpFM.Model.Scripting.Steps; + +/// +/// Zero-loss audit for SendDDEExecuteStep: the step's XML state is the three +/// <Step> attributes plus a single <ContentType value="..."/> +/// enum child. XML values and human-readable display values are mapped +/// through the static HR tables; round-trip preserves the XML value. +/// +public sealed class SendDDEExecuteStep : ScriptStep, IStepFactory +{ + public const int XmlId = 64; + public const string XmlName = "Send DDE Execute"; + + /// The enum XML value emitted on the <ContentType> element. + public string ContentType { get; set; } + + public SendDDEExecuteStep(string contentType = "File", bool enabled = true) + : base(null, enabled) + { + ContentType = contentType; + } + + private static readonly IReadOnlyDictionary _xmlToHr = + new Dictionary(StringComparer.Ordinal) + { + ["File"] = "File", + }; + + private static readonly IReadOnlyDictionary _hrToXml = + new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["File"] = "File", + }; + + private static string ToHr(string xmlValue) => + _xmlToHr.TryGetValue(xmlValue, out var hr) ? hr : xmlValue; + + private static string FromHr(string hrValue) => + _hrToXml.TryGetValue(hrValue, out var xml) ? xml : hrValue; + + public override XElement ToXml() => + new("Step", + new XAttribute("enable", Enabled ? "True" : "False"), + new XAttribute("id", XmlId), + new XAttribute("name", XmlName), + new XElement("ContentType", + new XAttribute("value", ContentType))); + + public override string ToDisplayLine() => + $"Send DDE Execute [ {ToHr(ContentType)} ]"; + + public static new ScriptStep FromXml(XElement step) + { + var enabled = step.Attribute("enable")?.Value != "False"; + var value = step.Element("ContentType")?.Attribute("value")?.Value ?? "File"; + return new SendDDEExecuteStep(value, enabled); + } + + public static ScriptStep FromDisplayParams(bool enabled, string[] hrParams) + { + var token = hrParams.Length > 0 ? hrParams[0].Trim() : ""; + return new SendDDEExecuteStep(FromHr(token), enabled); + } + + public static StepMetadata Metadata { get; } = new() + { + Name = XmlName, + Id = XmlId, + Category = "miscellaneous", + HelpUrl = "https://help.claris.com/en/pro-help/content/send-dde-execute-windows.html", + Params = + [ + new ParamMetadata + { + Name = "ContentType", + XmlElement = "ContentType", + Type = "enum", + XmlAttr = "value", + DefaultValue = "File", + ValidValues = ["File"], + }, + ], + FromXml = FromXml, + FromDisplay = FromDisplayParams, + }; +} diff --git a/src/SharpFM.Model/Scripting/Steps/SetLayoutObjectAnimationStep.cs b/src/SharpFM.Model/Scripting/Steps/SetLayoutObjectAnimationStep.cs new file mode 100644 index 0000000..2793447 --- /dev/null +++ b/src/SharpFM.Model/Scripting/Steps/SetLayoutObjectAnimationStep.cs @@ -0,0 +1,75 @@ +using System; +using System.Xml.Linq; +using SharpFM.Model.Scripting.Registry; + +namespace SharpFM.Model.Scripting.Steps; + +/// +/// Zero-loss audit for SetLayoutObjectAnimationStep: the step's XML state is the three +/// <Step> attributes (enable/id/name) plus a single +/// <Set state="True|False"/> child. All round-tripped. +/// +public sealed class SetLayoutObjectAnimationStep : ScriptStep, IStepFactory +{ + public const int XmlId = 168; + public const string XmlName = "Set Layout Object Animation"; + + /// The Animation flag on the step. + public bool Animation { get; set; } + + public SetLayoutObjectAnimationStep(bool animation = true, bool enabled = true) + : base(null, enabled) + { + Animation = animation; + } + + public override XElement ToXml() => + new("Step", + new XAttribute("enable", Enabled ? "True" : "False"), + new XAttribute("id", XmlId), + new XAttribute("name", XmlName), + new XElement("Set", + new XAttribute("state", Animation ? "True" : "False"))); + + public override string ToDisplayLine() => + $"Set Layout Object Animation [ Animation: {(Animation ? "On" : "Off")} ]"; + + public static new ScriptStep FromXml(XElement step) + { + var enabled = step.Attribute("enable")?.Value != "False"; + var state = step.Element("Set")?.Attribute("state")?.Value == "True"; + return new SetLayoutObjectAnimationStep(state, enabled); + } + + public static ScriptStep FromDisplayParams(bool enabled, string[] hrParams) + { + var token = hrParams.Length > 0 ? hrParams[0].Trim() : ""; + const string Prefix = "Animation:"; + if (token.StartsWith(Prefix, StringComparison.OrdinalIgnoreCase)) + token = token.Substring(Prefix.Length).Trim(); + var isOn = token.Equals("On", StringComparison.OrdinalIgnoreCase); + return new SetLayoutObjectAnimationStep(isOn, enabled); + } + + public static StepMetadata Metadata { get; } = new() + { + Name = XmlName, + Id = XmlId, + Category = "control", + HelpUrl = "https://help.claris.com/en/pro-help/content/set-layout-object-animation.html", + Params = + [ + new ParamMetadata + { + Name = "Set", + XmlElement = "Set", + Type = "boolean", + XmlAttr = "state", + HrLabel = "Animation", + ValidValues = ["On", "Off"], + }, + ], + FromXml = FromXml, + FromDisplay = FromDisplayParams, + }; +} diff --git a/src/SharpFM.Model/Scripting/Steps/SetMultiUserStep.cs b/src/SharpFM.Model/Scripting/Steps/SetMultiUserStep.cs new file mode 100644 index 0000000..bfb0c7a --- /dev/null +++ b/src/SharpFM.Model/Scripting/Steps/SetMultiUserStep.cs @@ -0,0 +1,99 @@ +using System; +using System.Collections.Generic; +using System.Xml.Linq; +using SharpFM.Model.Scripting.Registry; + +namespace SharpFM.Model.Scripting.Steps; + +/// +/// Zero-loss audit for SetMultiUserStep: the step's XML state is the three +/// <Step> attributes plus a single <MultiUser value="..."/> +/// enum child. XML values and human-readable display values are mapped +/// through the static HR tables; round-trip preserves the XML value. +/// +public sealed class SetMultiUserStep : ScriptStep, IStepFactory +{ + public const int XmlId = 84; + public const string XmlName = "Set Multi-User"; + + /// The enum XML value emitted on the <MultiUser> element. + public string NetworkAccess { get; set; } + + public SetMultiUserStep(string networkAccess = "True", bool enabled = true) + : base(null, enabled) + { + NetworkAccess = networkAccess; + } + + private static readonly IReadOnlyDictionary _xmlToHr = + new Dictionary(StringComparer.Ordinal) + { + ["True"] = "On", + ["OnHidden"] = "On (Hidden)", + ["False"] = "Off", + }; + + private static readonly IReadOnlyDictionary _hrToXml = + new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["On"] = "True", + ["On (Hidden)"] = "OnHidden", + ["Off"] = "False", + }; + + private static string ToHr(string xmlValue) => + _xmlToHr.TryGetValue(xmlValue, out var hr) ? hr : xmlValue; + + private static string FromHr(string hrValue) => + _hrToXml.TryGetValue(hrValue, out var xml) ? xml : hrValue; + + public override XElement ToXml() => + new("Step", + new XAttribute("enable", Enabled ? "True" : "False"), + new XAttribute("id", XmlId), + new XAttribute("name", XmlName), + new XElement("MultiUser", + new XAttribute("value", NetworkAccess))); + + public override string ToDisplayLine() => + $"Set Multi-User [ Network access: {ToHr(NetworkAccess)} ]"; + + public static new ScriptStep FromXml(XElement step) + { + var enabled = step.Attribute("enable")?.Value != "False"; + var value = step.Element("MultiUser")?.Attribute("value")?.Value ?? "True"; + return new SetMultiUserStep(value, enabled); + } + + public static ScriptStep FromDisplayParams(bool enabled, string[] hrParams) + { + var token = hrParams.Length > 0 ? hrParams[0].Trim() : ""; + const string Prefix = "Network access:"; + if (token.StartsWith(Prefix, StringComparison.OrdinalIgnoreCase)) + token = token.Substring(Prefix.Length).Trim(); + return new SetMultiUserStep(FromHr(token), enabled); + } + + public static StepMetadata Metadata { get; } = new() + { + Name = XmlName, + Id = XmlId, + Category = "files", + HelpUrl = "https://help.claris.com/en/pro-help/content/set-multi-user.html", + Params = + [ + new ParamMetadata + { + Name = "MultiUser", + XmlElement = "MultiUser", + Type = "enum", + XmlAttr = "value", + HrLabel = "Network access", + DefaultValue = "True", + ValidValues = ["On", "On (Hidden)", "Off"], + }, + ], + FromXml = FromXml, + FromDisplay = FromDisplayParams, + }; +} diff --git a/src/SharpFM.Model/Scripting/Steps/SetRevertTransactionOnErrorStep.cs b/src/SharpFM.Model/Scripting/Steps/SetRevertTransactionOnErrorStep.cs new file mode 100644 index 0000000..f49e23d --- /dev/null +++ b/src/SharpFM.Model/Scripting/Steps/SetRevertTransactionOnErrorStep.cs @@ -0,0 +1,75 @@ +using System; +using System.Xml.Linq; +using SharpFM.Model.Scripting.Registry; + +namespace SharpFM.Model.Scripting.Steps; + +/// +/// Zero-loss audit for SetRevertTransactionOnErrorStep: the step's XML state is the three +/// <Step> attributes (enable/id/name) plus a single +/// <Set state="True|False"/> child. All round-tripped. +/// +public sealed class SetRevertTransactionOnErrorStep : ScriptStep, IStepFactory +{ + public const int XmlId = 223; + public const string XmlName = "Set Revert Transaction on Error"; + + /// The Revert on error flag on the step. + public bool RevertOnError { get; set; } + + public SetRevertTransactionOnErrorStep(bool revertonerror = false, bool enabled = true) + : base(null, enabled) + { + RevertOnError = revertonerror; + } + + public override XElement ToXml() => + new("Step", + new XAttribute("enable", Enabled ? "True" : "False"), + new XAttribute("id", XmlId), + new XAttribute("name", XmlName), + new XElement("Set", + new XAttribute("state", RevertOnError ? "True" : "False"))); + + public override string ToDisplayLine() => + $"Set Revert Transaction on Error [ Revert on error: {(RevertOnError ? "On" : "Off")} ]"; + + public static new ScriptStep FromXml(XElement step) + { + var enabled = step.Attribute("enable")?.Value != "False"; + var state = step.Element("Set")?.Attribute("state")?.Value == "True"; + return new SetRevertTransactionOnErrorStep(state, enabled); + } + + public static ScriptStep FromDisplayParams(bool enabled, string[] hrParams) + { + var token = hrParams.Length > 0 ? hrParams[0].Trim() : ""; + const string Prefix = "Revert on error:"; + if (token.StartsWith(Prefix, StringComparison.OrdinalIgnoreCase)) + token = token.Substring(Prefix.Length).Trim(); + var isOn = token.Equals("On", StringComparison.OrdinalIgnoreCase); + return new SetRevertTransactionOnErrorStep(isOn, enabled); + } + + public static StepMetadata Metadata { get; } = new() + { + Name = XmlName, + Id = XmlId, + Category = "control", + HelpUrl = "https://help.claris.com/en/pro-help/content/set-revert-transaction-on-error.html", + Params = + [ + new ParamMetadata + { + Name = "Set", + XmlElement = "Set", + Type = "boolean", + XmlAttr = "state", + HrLabel = "Revert on error", + ValidValues = ["On", "Off"], + }, + ], + FromXml = FromXml, + FromDisplay = FromDisplayParams, + }; +} diff --git a/src/SharpFM.Model/Scripting/Steps/SetUseSystemFormatsStep.cs b/src/SharpFM.Model/Scripting/Steps/SetUseSystemFormatsStep.cs new file mode 100644 index 0000000..d8df356 --- /dev/null +++ b/src/SharpFM.Model/Scripting/Steps/SetUseSystemFormatsStep.cs @@ -0,0 +1,75 @@ +using System; +using System.Xml.Linq; +using SharpFM.Model.Scripting.Registry; + +namespace SharpFM.Model.Scripting.Steps; + +/// +/// Zero-loss audit for SetUseSystemFormatsStep: the step's XML state is the three +/// <Step> attributes (enable/id/name) plus a single +/// <Set state="True|False"/> child. All round-tripped. +/// +public sealed class SetUseSystemFormatsStep : ScriptStep, IStepFactory +{ + public const int XmlId = 94; + public const string XmlName = "Set Use System Formats"; + + /// The Use system formats flag on the step. + public bool UseSystemFormats { get; set; } + + public SetUseSystemFormatsStep(bool usesystemformats = true, bool enabled = true) + : base(null, enabled) + { + UseSystemFormats = usesystemformats; + } + + public override XElement ToXml() => + new("Step", + new XAttribute("enable", Enabled ? "True" : "False"), + new XAttribute("id", XmlId), + new XAttribute("name", XmlName), + new XElement("Set", + new XAttribute("state", UseSystemFormats ? "True" : "False"))); + + public override string ToDisplayLine() => + $"Set Use System Formats [ Use system formats: {(UseSystemFormats ? "On" : "Off")} ]"; + + public static new ScriptStep FromXml(XElement step) + { + var enabled = step.Attribute("enable")?.Value != "False"; + var state = step.Element("Set")?.Attribute("state")?.Value == "True"; + return new SetUseSystemFormatsStep(state, enabled); + } + + public static ScriptStep FromDisplayParams(bool enabled, string[] hrParams) + { + var token = hrParams.Length > 0 ? hrParams[0].Trim() : ""; + const string Prefix = "Use system formats:"; + if (token.StartsWith(Prefix, StringComparison.OrdinalIgnoreCase)) + token = token.Substring(Prefix.Length).Trim(); + var isOn = token.Equals("On", StringComparison.OrdinalIgnoreCase); + return new SetUseSystemFormatsStep(isOn, enabled); + } + + public static StepMetadata Metadata { get; } = new() + { + Name = XmlName, + Id = XmlId, + Category = "files", + HelpUrl = "https://help.claris.com/en/pro-help/content/set-use-system-formats.html", + Params = + [ + new ParamMetadata + { + Name = "Set", + XmlElement = "Set", + Type = "boolean", + XmlAttr = "state", + HrLabel = "Use system formats", + ValidValues = ["On", "Off"], + }, + ], + FromXml = FromXml, + FromDisplay = FromDisplayParams, + }; +} diff --git a/src/SharpFM.Model/Scripting/Steps/ShowHideTextRulerStep.cs b/src/SharpFM.Model/Scripting/Steps/ShowHideTextRulerStep.cs new file mode 100644 index 0000000..df20b2d --- /dev/null +++ b/src/SharpFM.Model/Scripting/Steps/ShowHideTextRulerStep.cs @@ -0,0 +1,99 @@ +using System; +using System.Collections.Generic; +using System.Xml.Linq; +using SharpFM.Model.Scripting.Registry; + +namespace SharpFM.Model.Scripting.Steps; + +/// +/// Zero-loss audit for ShowHideTextRulerStep: the step's XML state is the three +/// <Step> attributes plus a single <ShowHide value="..."/> +/// enum child. XML values and human-readable display values are mapped +/// through the static HR tables; round-trip preserves the XML value. +/// +public sealed class ShowHideTextRulerStep : ScriptStep, IStepFactory +{ + public const int XmlId = 92; + public const string XmlName = "Show/Hide Text Ruler"; + + /// The enum XML value emitted on the <ShowHide> element. + public string Action { get; set; } + + public ShowHideTextRulerStep(string action = "Show", bool enabled = true) + : base(null, enabled) + { + Action = action; + } + + private static readonly IReadOnlyDictionary _xmlToHr = + new Dictionary(StringComparer.Ordinal) + { + ["Show"] = "Show", + ["Hide"] = "Hide", + ["Toggle"] = "Toggle", + }; + + private static readonly IReadOnlyDictionary _hrToXml = + new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["Show"] = "Show", + ["Hide"] = "Hide", + ["Toggle"] = "Toggle", + }; + + private static string ToHr(string xmlValue) => + _xmlToHr.TryGetValue(xmlValue, out var hr) ? hr : xmlValue; + + private static string FromHr(string hrValue) => + _hrToXml.TryGetValue(hrValue, out var xml) ? xml : hrValue; + + public override XElement ToXml() => + new("Step", + new XAttribute("enable", Enabled ? "True" : "False"), + new XAttribute("id", XmlId), + new XAttribute("name", XmlName), + new XElement("ShowHide", + new XAttribute("value", Action))); + + public override string ToDisplayLine() => + $"Show/Hide Text Ruler [ Action: {ToHr(Action)} ]"; + + public static new ScriptStep FromXml(XElement step) + { + var enabled = step.Attribute("enable")?.Value != "False"; + var value = step.Element("ShowHide")?.Attribute("value")?.Value ?? "Show"; + return new ShowHideTextRulerStep(value, enabled); + } + + public static ScriptStep FromDisplayParams(bool enabled, string[] hrParams) + { + var token = hrParams.Length > 0 ? hrParams[0].Trim() : ""; + const string Prefix = "Action:"; + if (token.StartsWith(Prefix, StringComparison.OrdinalIgnoreCase)) + token = token.Substring(Prefix.Length).Trim(); + return new ShowHideTextRulerStep(FromHr(token), enabled); + } + + public static StepMetadata Metadata { get; } = new() + { + Name = XmlName, + Id = XmlId, + Category = "windows", + HelpUrl = "https://help.claris.com/en/pro-help/content/show-hide-text-ruler.html", + Params = + [ + new ParamMetadata + { + Name = "ShowHide", + XmlElement = "ShowHide", + Type = "enum", + XmlAttr = "value", + HrLabel = "Action", + DefaultValue = "Show", + ValidValues = ["Show", "Hide", "Toggle"], + }, + ], + FromXml = FromXml, + FromDisplay = FromDisplayParams, + }; +} diff --git a/src/SharpFM.Model/Scripting/Steps/UndoRedoStep.cs b/src/SharpFM.Model/Scripting/Steps/UndoRedoStep.cs new file mode 100644 index 0000000..151b22e --- /dev/null +++ b/src/SharpFM.Model/Scripting/Steps/UndoRedoStep.cs @@ -0,0 +1,99 @@ +using System; +using System.Collections.Generic; +using System.Xml.Linq; +using SharpFM.Model.Scripting.Registry; + +namespace SharpFM.Model.Scripting.Steps; + +/// +/// Zero-loss audit for UndoRedoStep: the step's XML state is the three +/// <Step> attributes plus a single <UndoRedo value="..."/> +/// enum child. XML values and human-readable display values are mapped +/// through the static HR tables; round-trip preserves the XML value. +/// +public sealed class UndoRedoStep : ScriptStep, IStepFactory +{ + public const int XmlId = 45; + public const string XmlName = "Undo/Redo"; + + /// The enum XML value emitted on the <UndoRedo> element. + public string Action { get; set; } + + public UndoRedoStep(string action = "Undo", bool enabled = true) + : base(null, enabled) + { + Action = action; + } + + private static readonly IReadOnlyDictionary _xmlToHr = + new Dictionary(StringComparer.Ordinal) + { + ["Undo"] = "Undo", + ["Redo"] = "Redo", + ["Toggle"] = "Toggle", + }; + + private static readonly IReadOnlyDictionary _hrToXml = + new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["Undo"] = "Undo", + ["Redo"] = "Redo", + ["Toggle"] = "Toggle", + }; + + private static string ToHr(string xmlValue) => + _xmlToHr.TryGetValue(xmlValue, out var hr) ? hr : xmlValue; + + private static string FromHr(string hrValue) => + _hrToXml.TryGetValue(hrValue, out var xml) ? xml : hrValue; + + public override XElement ToXml() => + new("Step", + new XAttribute("enable", Enabled ? "True" : "False"), + new XAttribute("id", XmlId), + new XAttribute("name", XmlName), + new XElement("UndoRedo", + new XAttribute("value", Action))); + + public override string ToDisplayLine() => + $"Undo/Redo [ Action: {ToHr(Action)} ]"; + + public static new ScriptStep FromXml(XElement step) + { + var enabled = step.Attribute("enable")?.Value != "False"; + var value = step.Element("UndoRedo")?.Attribute("value")?.Value ?? "Undo"; + return new UndoRedoStep(value, enabled); + } + + public static ScriptStep FromDisplayParams(bool enabled, string[] hrParams) + { + var token = hrParams.Length > 0 ? hrParams[0].Trim() : ""; + const string Prefix = "Action:"; + if (token.StartsWith(Prefix, StringComparison.OrdinalIgnoreCase)) + token = token.Substring(Prefix.Length).Trim(); + return new UndoRedoStep(FromHr(token), enabled); + } + + public static StepMetadata Metadata { get; } = new() + { + Name = XmlName, + Id = XmlId, + Category = "editing", + HelpUrl = "https://help.claris.com/en/pro-help/content/undo-redo.html", + Params = + [ + new ParamMetadata + { + Name = "UndoRedo", + XmlElement = "UndoRedo", + Type = "enum", + XmlAttr = "value", + HrLabel = "Action", + DefaultValue = "Undo", + ValidValues = ["Undo", "Redo", "Toggle"], + }, + ], + FromXml = FromXml, + FromDisplay = FromDisplayParams, + }; +} diff --git a/src/SharpFM.Model/Scripting/Steps/ViewAsStep.cs b/src/SharpFM.Model/Scripting/Steps/ViewAsStep.cs new file mode 100644 index 0000000..1f2671e --- /dev/null +++ b/src/SharpFM.Model/Scripting/Steps/ViewAsStep.cs @@ -0,0 +1,101 @@ +using System; +using System.Collections.Generic; +using System.Xml.Linq; +using SharpFM.Model.Scripting.Registry; + +namespace SharpFM.Model.Scripting.Steps; + +/// +/// Zero-loss audit for ViewAsStep: the step's XML state is the three +/// <Step> attributes plus a single <View value="..."/> +/// enum child. XML values and human-readable display values are mapped +/// through the static HR tables; round-trip preserves the XML value. +/// +public sealed class ViewAsStep : ScriptStep, IStepFactory +{ + public const int XmlId = 30; + public const string XmlName = "View As"; + + /// The enum XML value emitted on the <View> element. + public string View { get; set; } + + public ViewAsStep(string view = "Cycle", bool enabled = true) + : base(null, enabled) + { + View = view; + } + + private static readonly IReadOnlyDictionary _xmlToHr = + new Dictionary(StringComparer.Ordinal) + { + ["Cycle"] = "Cycle", + ["Form"] = "View as Form", + ["List"] = "View as List", + ["Table"] = "View as Table", + }; + + private static readonly IReadOnlyDictionary _hrToXml = + new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["Cycle"] = "Cycle", + ["View as Form"] = "Form", + ["View as List"] = "List", + ["View as Table"] = "Table", + }; + + private static string ToHr(string xmlValue) => + _xmlToHr.TryGetValue(xmlValue, out var hr) ? hr : xmlValue; + + private static string FromHr(string hrValue) => + _hrToXml.TryGetValue(hrValue, out var xml) ? xml : hrValue; + + public override XElement ToXml() => + new("Step", + new XAttribute("enable", Enabled ? "True" : "False"), + new XAttribute("id", XmlId), + new XAttribute("name", XmlName), + new XElement("View", + new XAttribute("value", View))); + + public override string ToDisplayLine() => + $"View As [ View: {ToHr(View)} ]"; + + public static new ScriptStep FromXml(XElement step) + { + var enabled = step.Attribute("enable")?.Value != "False"; + var value = step.Element("View")?.Attribute("value")?.Value ?? "Cycle"; + return new ViewAsStep(value, enabled); + } + + public static ScriptStep FromDisplayParams(bool enabled, string[] hrParams) + { + var token = hrParams.Length > 0 ? hrParams[0].Trim() : ""; + const string Prefix = "View:"; + if (token.StartsWith(Prefix, StringComparison.OrdinalIgnoreCase)) + token = token.Substring(Prefix.Length).Trim(); + return new ViewAsStep(FromHr(token), enabled); + } + + public static StepMetadata Metadata { get; } = new() + { + Name = XmlName, + Id = XmlId, + Category = "windows", + HelpUrl = "https://help.claris.com/en/pro-help/content/view-as.html", + Params = + [ + new ParamMetadata + { + Name = "View", + XmlElement = "View", + Type = "enum", + XmlAttr = "value", + HrLabel = "View", + DefaultValue = "Cycle", + ValidValues = ["Cycle", "View as Form", "View as List", "View as Table"], + }, + ], + FromXml = FromXml, + FromDisplay = FromDisplayParams, + }; +} diff --git a/tests/SharpFM.Tests/Scripting/SealedStepPreservationTests.cs b/tests/SharpFM.Tests/Scripting/SealedStepPreservationTests.cs index 113b2aa..94ff420 100644 --- a/tests/SharpFM.Tests/Scripting/SealedStepPreservationTests.cs +++ b/tests/SharpFM.Tests/Scripting/SealedStepPreservationTests.cs @@ -21,7 +21,7 @@ public class SealedStepPreservationTests // empty at launch) is sealed. Perfect canary. private const string ScriptWithSealedHaltScriptXml = @" 0]]> - + "; @@ -34,8 +34,8 @@ public void NoEdits_ToXml_ProducesOriginalScriptWithSealedStepIntact() var steps = doc.Root!.Elements("Step").ToArray(); Assert.Equal(3, steps.Length); - Assert.Equal("Allow User Abort", steps[1].Attribute("name")!.Value); - Assert.Equal("85", steps[1].Attribute("id")!.Value); + Assert.Equal("Refresh Window", steps[1].Attribute("name")!.Value); + Assert.Equal("80", steps[1].Attribute("id")!.Value); } [Fact] @@ -58,8 +58,8 @@ public void EditNonSealedLine_SealedStepPreservedByAnchor() // If step reflects the edit Assert.Contains("$x > 10", steps[0].Element("Calculation")!.Value); // HaltScript step still present with id - Assert.Equal("Allow User Abort", steps[1].Attribute("name")!.Value); - Assert.Equal("85", steps[1].Attribute("id")!.Value); + Assert.Equal("Refresh Window", steps[1].Attribute("name")!.Value); + Assert.Equal("80", steps[1].Attribute("id")!.Value); } [Fact] @@ -103,9 +103,9 @@ public void InsertNewLineBeforeSealed_SealedStepSurvivesAtNewPosition() Assert.Equal(4, steps.Length); // Find all HaltScript steps. The one with id=93 is the original sealed one. - var originalHaltScript = steps.FirstOrDefault(s => s.Attribute("id")?.Value == "85"); + var originalHaltScript = steps.FirstOrDefault(s => s.Attribute("id")?.Value == "80"); Assert.NotNull(originalHaltScript); - Assert.Equal("Allow User Abort", originalHaltScript!.Attribute("name")!.Value); + Assert.Equal("Refresh Window", originalHaltScript!.Attribute("name")!.Value); } // --- UpdateSealedXml (cog-edit write-back) --- @@ -121,7 +121,7 @@ public void UpdateSealedXml_ReplacesCachedXmlAndRerendersLine() // update propagates to ToXml output. var anchor = editor.SealedAnchors.First(); var replacement = XElement.Parse( - ""); + ""); var updated = editor.UpdateSealedXml(anchor, replacement); Assert.True(updated); @@ -131,7 +131,7 @@ public void UpdateSealedXml_ReplacesCachedXmlAndRerendersLine() var beep = doc.Root!.Elements("Step").ElementAt(1); // The cached XML now reflects the replacement — is - // present even though the display line (still "Allow User Abort") hasn't + // present even though the display line (still "Refresh Window") hasn't // exposed it. Assert.NotNull(beep.Element("SomeChild")); Assert.Equal("42", beep.Element("SomeChild")!.Attribute("value")!.Value); @@ -150,7 +150,7 @@ public void UpdateSealedXml_PreservesBlockIndentation() Assert.StartsWith(" ", beforeText); // 4-space indent inside If block var replacement = XElement.Parse( - ""); + ""); editor.UpdateSealedXml(anchor, replacement); var afterText = editor.Document.GetText(beepLine.Offset, beepLine.Length); @@ -167,7 +167,7 @@ public void UpdateSealedXml_DeadAnchor_ReturnsFalse() var line2 = editor.Document.GetLineByNumber(2); editor.Document.Remove(line2.Offset, line2.TotalLength); - var replacement = XElement.Parse(""); + var replacement = XElement.Parse(""); Assert.False(editor.UpdateSealedXml(anchor, replacement)); } @@ -179,7 +179,7 @@ public void UpdateSealedXml_ThenEditNonSealedLine_UpdateIsPreserved() var editor = new ScriptClipEditor(ScriptWithSealedHaltScriptXml); var anchor = editor.SealedAnchors.First(); var replacement = XElement.Parse( - ""); + ""); editor.UpdateSealedXml(anchor, replacement); // Now edit the If step's calc. @@ -204,8 +204,8 @@ public void TryGetSealedXml_LiveAnchor_ReturnsCachedXml() var anchor = editor.SealedAnchors.First(); Assert.True(editor.TryGetSealedXml(anchor, out var xml)); - Assert.Equal("Allow User Abort", xml.Attribute("name")!.Value); - Assert.Equal("85", xml.Attribute("id")!.Value); + Assert.Equal("Refresh Window", xml.Attribute("name")!.Value); + Assert.Equal("80", xml.Attribute("id")!.Value); } [Fact] @@ -229,9 +229,9 @@ public void MultipleSealedSteps_EachPreservedIndependently() // has a POCO so it stays editable. The two HaltScripts are sealed. var xml = @" 0]]> - + between - + "; @@ -261,7 +261,7 @@ public void ScriptMetadata_AndSealedStep_BothPreservedOnRoundTrip() var xml = @" "; @@ -282,7 +282,7 @@ public void ScriptMetadata_AndSealedStep_BothPreservedOnRoundTrip() Assert.Equal("MyScript", scriptEl.Attribute("name")!.Value); // Sealed HaltScript's custom child preserved - var beep = scriptEl.Elements("Step").FirstOrDefault(s => s.Attribute("id")?.Value == "85"); + var beep = scriptEl.Elements("Step").FirstOrDefault(s => s.Attribute("id")?.Value == "80"); Assert.NotNull(beep); Assert.NotNull(beep!.Element("WrapperPreserved")); } @@ -305,7 +305,7 @@ public void SealedStep_AtEndOfDocumentNoTrailingNewline_Preserved() { var xml = @" 0]]> - + "; var editor = new ScriptClipEditor(xml); @@ -313,7 +313,7 @@ public void SealedStep_AtEndOfDocumentNoTrailingNewline_Preserved() var outDoc = XDocument.Parse(outXml); var beep = outDoc.Root!.Elements("Step").Last(); - Assert.Equal("Allow User Abort", beep.Attribute("name")!.Value); + Assert.Equal("Refresh Window", beep.Attribute("name")!.Value); Assert.NotNull(beep.Element("EofCanary")); } diff --git a/tests/SharpFM.Tests/Scripting/Steps/AVPlayerSetPlaybackStateStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/AVPlayerSetPlaybackStateStepTests.cs new file mode 100644 index 0000000..654e86d --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/AVPlayerSetPlaybackStateStepTests.cs @@ -0,0 +1,43 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +public class AVPlayerSetPlaybackStateStepTests +{ + private const string CanonicalXml = """"""; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = AVPlayerSetPlaybackStateStep.Metadata.FromXml!(source); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Display_EmitsHrMappedValue() + { + var step = AVPlayerSetPlaybackStateStep.Metadata.FromXml!(XElement.Parse(CanonicalXml)); + Assert.Equal("AVPlayer Set Playback State [ Stopped ]", step.ToDisplayLine()); + } + + [Fact] + public void FromDisplay_ParsesHrValueBack() + { + var step = AVPlayerSetPlaybackStateStep.Metadata.FromDisplay!(true, new[] { "Stopped" }); + Assert.True(XNode.DeepEquals(XElement.Parse(CanonicalXml), step.ToXml())); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("AVPlayer Set Playback State", out var metadata)); + Assert.Equal(178, metadata!.Id); + Assert.Single(metadata.Params); + Assert.Equal("enum", metadata.Params[0].Type); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/AdjustWindowStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/AdjustWindowStepTests.cs new file mode 100644 index 0000000..d50b272 --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/AdjustWindowStepTests.cs @@ -0,0 +1,43 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +public class AdjustWindowStepTests +{ + private const string CanonicalXml = """"""; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = AdjustWindowStep.Metadata.FromXml!(source); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Display_EmitsHrMappedValue() + { + var step = AdjustWindowStep.Metadata.FromXml!(XElement.Parse(CanonicalXml)); + Assert.Equal("Adjust Window [ Resize to Fit ]", step.ToDisplayLine()); + } + + [Fact] + public void FromDisplay_ParsesHrValueBack() + { + var step = AdjustWindowStep.Metadata.FromDisplay!(true, new[] { "Resize to Fit" }); + Assert.True(XNode.DeepEquals(XElement.Parse(CanonicalXml), step.ToXml())); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Adjust Window", out var metadata)); + Assert.Equal(31, metadata!.Id); + Assert.Single(metadata.Params); + Assert.Equal("enum", metadata.Params[0].Type); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/AllowFormattingBarStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/AllowFormattingBarStepTests.cs new file mode 100644 index 0000000..6869d95 --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/AllowFormattingBarStepTests.cs @@ -0,0 +1,49 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +public class AllowFormattingBarStepTests +{ + private const string TrueStateXml = """"""; + private const string FalseStateXml = """"""; + + [Fact] + public void RoundTrip_True_IsPreserved() + { + var source = XElement.Parse(TrueStateXml); + var step = AllowFormattingBarStep.Metadata.FromXml!(source); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void RoundTrip_False_IsPreserved() + { + var source = XElement.Parse(FalseStateXml); + var step = AllowFormattingBarStep.Metadata.FromXml!(source); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Display_EmitsExpectedFormat() + { + // Setting underlying prop=true renders as "On" + // (boolean: XML True displays as On). + var stepTrue = ((AllowFormattingBarStep)AllowFormattingBarStep.Metadata.FromXml!(XElement.Parse(TrueStateXml))); + Assert.Equal("Allow Formatting Bar [ On ]", stepTrue.ToDisplayLine()); + + var stepFalse = ((AllowFormattingBarStep)AllowFormattingBarStep.Metadata.FromXml!(XElement.Parse(FalseStateXml))); + Assert.Equal("Allow Formatting Bar [ Off ]", stepFalse.ToDisplayLine()); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Allow Formatting Bar", out var metadata)); + Assert.Equal(115, metadata!.Id); + Assert.Single(metadata.Params); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/AllowUserAbortStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/AllowUserAbortStepTests.cs new file mode 100644 index 0000000..b297f8e --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/AllowUserAbortStepTests.cs @@ -0,0 +1,49 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +public class AllowUserAbortStepTests +{ + private const string TrueStateXml = """"""; + private const string FalseStateXml = """"""; + + [Fact] + public void RoundTrip_True_IsPreserved() + { + var source = XElement.Parse(TrueStateXml); + var step = AllowUserAbortStep.Metadata.FromXml!(source); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void RoundTrip_False_IsPreserved() + { + var source = XElement.Parse(FalseStateXml); + var step = AllowUserAbortStep.Metadata.FromXml!(source); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Display_EmitsExpectedFormat() + { + // Setting underlying prop=true renders as "On" + // (boolean: XML True displays as On). + var stepTrue = ((AllowUserAbortStep)AllowUserAbortStep.Metadata.FromXml!(XElement.Parse(TrueStateXml))); + Assert.Equal("Allow User Abort [ On ]", stepTrue.ToDisplayLine()); + + var stepFalse = ((AllowUserAbortStep)AllowUserAbortStep.Metadata.FromXml!(XElement.Parse(FalseStateXml))); + Assert.Equal("Allow User Abort [ Off ]", stepFalse.ToDisplayLine()); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Allow User Abort", out var metadata)); + Assert.Equal(85, metadata!.Id); + Assert.Single(metadata.Params); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/ArrangeAllWindowsStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/ArrangeAllWindowsStepTests.cs new file mode 100644 index 0000000..d7fe1f3 --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/ArrangeAllWindowsStepTests.cs @@ -0,0 +1,43 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +public class ArrangeAllWindowsStepTests +{ + private const string CanonicalXml = """"""; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = ArrangeAllWindowsStep.Metadata.FromXml!(source); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Display_EmitsHrMappedValue() + { + var step = ArrangeAllWindowsStep.Metadata.FromXml!(XElement.Parse(CanonicalXml)); + Assert.Equal("Arrange All Windows [ Tile Horizontally ]", step.ToDisplayLine()); + } + + [Fact] + public void FromDisplay_ParsesHrValueBack() + { + var step = ArrangeAllWindowsStep.Metadata.FromDisplay!(true, new[] { "Tile Horizontally" }); + Assert.True(XNode.DeepEquals(XElement.Parse(CanonicalXml), step.ToXml())); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Arrange All Windows", out var metadata)); + Assert.Equal(120, metadata!.Id); + Assert.Single(metadata.Params); + Assert.Equal("enum", metadata.Params[0].Type); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/DeleteAllRecordsStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/DeleteAllRecordsStepTests.cs new file mode 100644 index 0000000..7ba1edb --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/DeleteAllRecordsStepTests.cs @@ -0,0 +1,49 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +public class DeleteAllRecordsStepTests +{ + private const string TrueStateXml = """"""; + private const string FalseStateXml = """"""; + + [Fact] + public void RoundTrip_True_IsPreserved() + { + var source = XElement.Parse(TrueStateXml); + var step = DeleteAllRecordsStep.Metadata.FromXml!(source); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void RoundTrip_False_IsPreserved() + { + var source = XElement.Parse(FalseStateXml); + var step = DeleteAllRecordsStep.Metadata.FromXml!(source); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Display_EmitsExpectedFormat() + { + // Setting underlying prop=true renders as "Off" + // (invertedHr: XML True displays as Off). + var stepTrue = ((DeleteAllRecordsStep)DeleteAllRecordsStep.Metadata.FromXml!(XElement.Parse(TrueStateXml))); + Assert.Equal("Delete All Records [ With dialog: Off ]", stepTrue.ToDisplayLine()); + + var stepFalse = ((DeleteAllRecordsStep)DeleteAllRecordsStep.Metadata.FromXml!(XElement.Parse(FalseStateXml))); + Assert.Equal("Delete All Records [ With dialog: On ]", stepFalse.ToDisplayLine()); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Delete All Records", out var metadata)); + Assert.Equal(10, metadata!.Id); + Assert.Single(metadata.Params); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/DeletePortalRowStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/DeletePortalRowStepTests.cs new file mode 100644 index 0000000..5688dac --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/DeletePortalRowStepTests.cs @@ -0,0 +1,49 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +public class DeletePortalRowStepTests +{ + private const string TrueStateXml = """"""; + private const string FalseStateXml = """"""; + + [Fact] + public void RoundTrip_True_IsPreserved() + { + var source = XElement.Parse(TrueStateXml); + var step = DeletePortalRowStep.Metadata.FromXml!(source); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void RoundTrip_False_IsPreserved() + { + var source = XElement.Parse(FalseStateXml); + var step = DeletePortalRowStep.Metadata.FromXml!(source); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Display_EmitsExpectedFormat() + { + // Setting underlying prop=true renders as "Off" + // (invertedHr: XML True displays as Off). + var stepTrue = ((DeletePortalRowStep)DeletePortalRowStep.Metadata.FromXml!(XElement.Parse(TrueStateXml))); + Assert.Equal("Delete Portal Row [ With dialog: Off ]", stepTrue.ToDisplayLine()); + + var stepFalse = ((DeletePortalRowStep)DeletePortalRowStep.Metadata.FromXml!(XElement.Parse(FalseStateXml))); + Assert.Equal("Delete Portal Row [ With dialog: On ]", stepFalse.ToDisplayLine()); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Delete Portal Row", out var metadata)); + Assert.Equal(104, metadata!.Id); + Assert.Single(metadata.Params); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/DeleteRecordRequestStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/DeleteRecordRequestStepTests.cs new file mode 100644 index 0000000..57248de --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/DeleteRecordRequestStepTests.cs @@ -0,0 +1,49 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +public class DeleteRecordRequestStepTests +{ + private const string TrueStateXml = """"""; + private const string FalseStateXml = """"""; + + [Fact] + public void RoundTrip_True_IsPreserved() + { + var source = XElement.Parse(TrueStateXml); + var step = DeleteRecordRequestStep.Metadata.FromXml!(source); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void RoundTrip_False_IsPreserved() + { + var source = XElement.Parse(FalseStateXml); + var step = DeleteRecordRequestStep.Metadata.FromXml!(source); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Display_EmitsExpectedFormat() + { + // Setting underlying prop=true renders as "Off" + // (invertedHr: XML True displays as Off). + var stepTrue = ((DeleteRecordRequestStep)DeleteRecordRequestStep.Metadata.FromXml!(XElement.Parse(TrueStateXml))); + Assert.Equal("Delete Record/Request [ With dialog: Off ]", stepTrue.ToDisplayLine()); + + var stepFalse = ((DeleteRecordRequestStep)DeleteRecordRequestStep.Metadata.FromXml!(XElement.Parse(FalseStateXml))); + Assert.Equal("Delete Record/Request [ With dialog: On ]", stepFalse.ToDisplayLine()); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Delete Record/Request", out var metadata)); + Assert.Equal(9, metadata!.Id); + Assert.Single(metadata.Params); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/EnableTouchKeyboardStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/EnableTouchKeyboardStepTests.cs new file mode 100644 index 0000000..c5e5690 --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/EnableTouchKeyboardStepTests.cs @@ -0,0 +1,43 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +public class EnableTouchKeyboardStepTests +{ + private const string CanonicalXml = """"""; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = EnableTouchKeyboardStep.Metadata.FromXml!(source); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Display_EmitsHrMappedValue() + { + var step = EnableTouchKeyboardStep.Metadata.FromXml!(XElement.Parse(CanonicalXml)); + Assert.Equal("Enable Touch Keyboard [ On ]", step.ToDisplayLine()); + } + + [Fact] + public void FromDisplay_ParsesHrValueBack() + { + var step = EnableTouchKeyboardStep.Metadata.FromDisplay!(true, new[] { "On" }); + Assert.True(XNode.DeepEquals(XElement.Parse(CanonicalXml), step.ToXml())); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Enable Touch Keyboard", out var metadata)); + Assert.Equal(174, metadata!.Id); + Assert.Single(metadata.Params); + Assert.Equal("enum", metadata.Params[0].Type); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/EnterBrowseModeStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/EnterBrowseModeStepTests.cs new file mode 100644 index 0000000..4477717 --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/EnterBrowseModeStepTests.cs @@ -0,0 +1,49 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +public class EnterBrowseModeStepTests +{ + private const string TrueStateXml = """"""; + private const string FalseStateXml = """"""; + + [Fact] + public void RoundTrip_True_IsPreserved() + { + var source = XElement.Parse(TrueStateXml); + var step = EnterBrowseModeStep.Metadata.FromXml!(source); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void RoundTrip_False_IsPreserved() + { + var source = XElement.Parse(FalseStateXml); + var step = EnterBrowseModeStep.Metadata.FromXml!(source); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Display_EmitsExpectedFormat() + { + // Setting underlying prop=true renders as "On" + // (boolean: XML True displays as On). + var stepTrue = ((EnterBrowseModeStep)EnterBrowseModeStep.Metadata.FromXml!(XElement.Parse(TrueStateXml))); + Assert.Equal("Enter Browse Mode [ Pause: On ]", stepTrue.ToDisplayLine()); + + var stepFalse = ((EnterBrowseModeStep)EnterBrowseModeStep.Metadata.FromXml!(XElement.Parse(FalseStateXml))); + Assert.Equal("Enter Browse Mode [ Pause: Off ]", stepFalse.ToDisplayLine()); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Enter Browse Mode", out var metadata)); + Assert.Equal(55, metadata!.Id); + Assert.Single(metadata.Params); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/EnterPreviewModeStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/EnterPreviewModeStepTests.cs new file mode 100644 index 0000000..6982432 --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/EnterPreviewModeStepTests.cs @@ -0,0 +1,49 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +public class EnterPreviewModeStepTests +{ + private const string TrueStateXml = """"""; + private const string FalseStateXml = """"""; + + [Fact] + public void RoundTrip_True_IsPreserved() + { + var source = XElement.Parse(TrueStateXml); + var step = EnterPreviewModeStep.Metadata.FromXml!(source); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void RoundTrip_False_IsPreserved() + { + var source = XElement.Parse(FalseStateXml); + var step = EnterPreviewModeStep.Metadata.FromXml!(source); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Display_EmitsExpectedFormat() + { + // Setting underlying prop=true renders as "On" + // (boolean: XML True displays as On). + var stepTrue = ((EnterPreviewModeStep)EnterPreviewModeStep.Metadata.FromXml!(XElement.Parse(TrueStateXml))); + Assert.Equal("Enter Preview Mode [ Pause: On ]", stepTrue.ToDisplayLine()); + + var stepFalse = ((EnterPreviewModeStep)EnterPreviewModeStep.Metadata.FromXml!(XElement.Parse(FalseStateXml))); + Assert.Equal("Enter Preview Mode [ Pause: Off ]", stepFalse.ToDisplayLine()); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Enter Preview Mode", out var metadata)); + Assert.Equal(41, metadata!.Id); + Assert.Single(metadata.Params); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/RawStepAllowListTests.cs b/tests/SharpFM.Tests/Scripting/Steps/RawStepAllowListTests.cs index aaf3811..0ca1bce 100644 --- a/tests/SharpFM.Tests/Scripting/Steps/RawStepAllowListTests.cs +++ b/tests/SharpFM.Tests/Scripting/Steps/RawStepAllowListTests.cs @@ -50,11 +50,12 @@ public void TypedPocoStep_SetField_IsFullyEditable_ReturnsTrue() [Fact] public void RawStep_NotInAllowList_IsFullyEditable_ReturnsFalse() { - // "Allow User Abort" is a catalog-known step with no typed POCO - // (Beep was the original canary but migrated in pilot; Halt - // Script was the Tier-A-era canary and migrated with Tier A). - // Not in the allow-list (which ships empty), so it must be sealed. - var xml = XElement.Parse(""); + // "Refresh Window" is a catalog-known step with no typed POCO + // (earlier canaries — Beep, Halt Script, Allow User Abort — have + // all migrated). Multi-param Tier B steps stay RawStep-backed + // until the Tier B sweep completes. Not in the allow-list (which + // ships empty), so it must be sealed. + var xml = XElement.Parse(""); var step = ScriptStep.FromXml(xml); Assert.IsType(step); diff --git a/tests/SharpFM.Tests/Scripting/Steps/RevertRecordRequestStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/RevertRecordRequestStepTests.cs new file mode 100644 index 0000000..865233d --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/RevertRecordRequestStepTests.cs @@ -0,0 +1,49 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +public class RevertRecordRequestStepTests +{ + private const string TrueStateXml = """"""; + private const string FalseStateXml = """"""; + + [Fact] + public void RoundTrip_True_IsPreserved() + { + var source = XElement.Parse(TrueStateXml); + var step = RevertRecordRequestStep.Metadata.FromXml!(source); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void RoundTrip_False_IsPreserved() + { + var source = XElement.Parse(FalseStateXml); + var step = RevertRecordRequestStep.Metadata.FromXml!(source); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Display_EmitsExpectedFormat() + { + // Setting underlying prop=true renders as "Off" + // (invertedHr: XML True displays as Off). + var stepTrue = ((RevertRecordRequestStep)RevertRecordRequestStep.Metadata.FromXml!(XElement.Parse(TrueStateXml))); + Assert.Equal("Revert Record/Request [ With dialog: Off ]", stepTrue.ToDisplayLine()); + + var stepFalse = ((RevertRecordRequestStep)RevertRecordRequestStep.Metadata.FromXml!(XElement.Parse(FalseStateXml))); + Assert.Equal("Revert Record/Request [ With dialog: On ]", stepFalse.ToDisplayLine()); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Revert Record/Request", out var metadata)); + Assert.Equal(51, metadata!.Id); + Assert.Single(metadata.Params); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/ScrollWindowStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/ScrollWindowStepTests.cs new file mode 100644 index 0000000..4648b56 --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/ScrollWindowStepTests.cs @@ -0,0 +1,43 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +public class ScrollWindowStepTests +{ + private const string CanonicalXml = """"""; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = ScrollWindowStep.Metadata.FromXml!(source); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Display_EmitsHrMappedValue() + { + var step = ScrollWindowStep.Metadata.FromXml!(XElement.Parse(CanonicalXml)); + Assert.Equal("Scroll Window [ Direction: Home ]", step.ToDisplayLine()); + } + + [Fact] + public void FromDisplay_ParsesHrValueBack() + { + var step = ScrollWindowStep.Metadata.FromDisplay!(true, new[] { "Direction: Home" }); + Assert.True(XNode.DeepEquals(XElement.Parse(CanonicalXml), step.ToXml())); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Scroll Window", out var metadata)); + Assert.Equal(81, metadata!.Id); + Assert.Single(metadata.Params); + Assert.Equal("enum", metadata.Params[0].Type); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/SendDDEExecuteStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/SendDDEExecuteStepTests.cs new file mode 100644 index 0000000..15b104e --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/SendDDEExecuteStepTests.cs @@ -0,0 +1,43 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +public class SendDDEExecuteStepTests +{ + private const string CanonicalXml = """"""; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = SendDDEExecuteStep.Metadata.FromXml!(source); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Display_EmitsHrMappedValue() + { + var step = SendDDEExecuteStep.Metadata.FromXml!(XElement.Parse(CanonicalXml)); + Assert.Equal("Send DDE Execute [ File ]", step.ToDisplayLine()); + } + + [Fact] + public void FromDisplay_ParsesHrValueBack() + { + var step = SendDDEExecuteStep.Metadata.FromDisplay!(true, new[] { "File" }); + Assert.True(XNode.DeepEquals(XElement.Parse(CanonicalXml), step.ToXml())); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Send DDE Execute", out var metadata)); + Assert.Equal(64, metadata!.Id); + Assert.Single(metadata.Params); + Assert.Equal("enum", metadata.Params[0].Type); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/SetLayoutObjectAnimationStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/SetLayoutObjectAnimationStepTests.cs new file mode 100644 index 0000000..7275873 --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/SetLayoutObjectAnimationStepTests.cs @@ -0,0 +1,49 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +public class SetLayoutObjectAnimationStepTests +{ + private const string TrueStateXml = """"""; + private const string FalseStateXml = """"""; + + [Fact] + public void RoundTrip_True_IsPreserved() + { + var source = XElement.Parse(TrueStateXml); + var step = SetLayoutObjectAnimationStep.Metadata.FromXml!(source); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void RoundTrip_False_IsPreserved() + { + var source = XElement.Parse(FalseStateXml); + var step = SetLayoutObjectAnimationStep.Metadata.FromXml!(source); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Display_EmitsExpectedFormat() + { + // Setting underlying prop=true renders as "On" + // (boolean: XML True displays as On). + var stepTrue = ((SetLayoutObjectAnimationStep)SetLayoutObjectAnimationStep.Metadata.FromXml!(XElement.Parse(TrueStateXml))); + Assert.Equal("Set Layout Object Animation [ Animation: On ]", stepTrue.ToDisplayLine()); + + var stepFalse = ((SetLayoutObjectAnimationStep)SetLayoutObjectAnimationStep.Metadata.FromXml!(XElement.Parse(FalseStateXml))); + Assert.Equal("Set Layout Object Animation [ Animation: Off ]", stepFalse.ToDisplayLine()); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Set Layout Object Animation", out var metadata)); + Assert.Equal(168, metadata!.Id); + Assert.Single(metadata.Params); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/SetMultiUserStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/SetMultiUserStepTests.cs new file mode 100644 index 0000000..b475687 --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/SetMultiUserStepTests.cs @@ -0,0 +1,43 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +public class SetMultiUserStepTests +{ + private const string CanonicalXml = """"""; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = SetMultiUserStep.Metadata.FromXml!(source); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Display_EmitsHrMappedValue() + { + var step = SetMultiUserStep.Metadata.FromXml!(XElement.Parse(CanonicalXml)); + Assert.Equal("Set Multi-User [ Network access: On ]", step.ToDisplayLine()); + } + + [Fact] + public void FromDisplay_ParsesHrValueBack() + { + var step = SetMultiUserStep.Metadata.FromDisplay!(true, new[] { "Network access: On" }); + Assert.True(XNode.DeepEquals(XElement.Parse(CanonicalXml), step.ToXml())); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Set Multi-User", out var metadata)); + Assert.Equal(84, metadata!.Id); + Assert.Single(metadata.Params); + Assert.Equal("enum", metadata.Params[0].Type); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/SetRevertTransactionOnErrorStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/SetRevertTransactionOnErrorStepTests.cs new file mode 100644 index 0000000..598456e --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/SetRevertTransactionOnErrorStepTests.cs @@ -0,0 +1,49 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +public class SetRevertTransactionOnErrorStepTests +{ + private const string TrueStateXml = """"""; + private const string FalseStateXml = """"""; + + [Fact] + public void RoundTrip_True_IsPreserved() + { + var source = XElement.Parse(TrueStateXml); + var step = SetRevertTransactionOnErrorStep.Metadata.FromXml!(source); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void RoundTrip_False_IsPreserved() + { + var source = XElement.Parse(FalseStateXml); + var step = SetRevertTransactionOnErrorStep.Metadata.FromXml!(source); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Display_EmitsExpectedFormat() + { + // Setting underlying prop=true renders as "On" + // (boolean: XML True displays as On). + var stepTrue = ((SetRevertTransactionOnErrorStep)SetRevertTransactionOnErrorStep.Metadata.FromXml!(XElement.Parse(TrueStateXml))); + Assert.Equal("Set Revert Transaction on Error [ Revert on error: On ]", stepTrue.ToDisplayLine()); + + var stepFalse = ((SetRevertTransactionOnErrorStep)SetRevertTransactionOnErrorStep.Metadata.FromXml!(XElement.Parse(FalseStateXml))); + Assert.Equal("Set Revert Transaction on Error [ Revert on error: Off ]", stepFalse.ToDisplayLine()); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Set Revert Transaction on Error", out var metadata)); + Assert.Equal(223, metadata!.Id); + Assert.Single(metadata.Params); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/SetUseSystemFormatsStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/SetUseSystemFormatsStepTests.cs new file mode 100644 index 0000000..c5fa0ea --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/SetUseSystemFormatsStepTests.cs @@ -0,0 +1,49 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +public class SetUseSystemFormatsStepTests +{ + private const string TrueStateXml = """"""; + private const string FalseStateXml = """"""; + + [Fact] + public void RoundTrip_True_IsPreserved() + { + var source = XElement.Parse(TrueStateXml); + var step = SetUseSystemFormatsStep.Metadata.FromXml!(source); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void RoundTrip_False_IsPreserved() + { + var source = XElement.Parse(FalseStateXml); + var step = SetUseSystemFormatsStep.Metadata.FromXml!(source); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Display_EmitsExpectedFormat() + { + // Setting underlying prop=true renders as "On" + // (boolean: XML True displays as On). + var stepTrue = ((SetUseSystemFormatsStep)SetUseSystemFormatsStep.Metadata.FromXml!(XElement.Parse(TrueStateXml))); + Assert.Equal("Set Use System Formats [ Use system formats: On ]", stepTrue.ToDisplayLine()); + + var stepFalse = ((SetUseSystemFormatsStep)SetUseSystemFormatsStep.Metadata.FromXml!(XElement.Parse(FalseStateXml))); + Assert.Equal("Set Use System Formats [ Use system formats: Off ]", stepFalse.ToDisplayLine()); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Set Use System Formats", out var metadata)); + Assert.Equal(94, metadata!.Id); + Assert.Single(metadata.Params); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/ShowHideTextRulerStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/ShowHideTextRulerStepTests.cs new file mode 100644 index 0000000..c5694b7 --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/ShowHideTextRulerStepTests.cs @@ -0,0 +1,43 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +public class ShowHideTextRulerStepTests +{ + private const string CanonicalXml = """"""; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = ShowHideTextRulerStep.Metadata.FromXml!(source); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Display_EmitsHrMappedValue() + { + var step = ShowHideTextRulerStep.Metadata.FromXml!(XElement.Parse(CanonicalXml)); + Assert.Equal("Show/Hide Text Ruler [ Action: Show ]", step.ToDisplayLine()); + } + + [Fact] + public void FromDisplay_ParsesHrValueBack() + { + var step = ShowHideTextRulerStep.Metadata.FromDisplay!(true, new[] { "Action: Show" }); + Assert.True(XNode.DeepEquals(XElement.Parse(CanonicalXml), step.ToXml())); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Show/Hide Text Ruler", out var metadata)); + Assert.Equal(92, metadata!.Id); + Assert.Single(metadata.Params); + Assert.Equal("enum", metadata.Params[0].Type); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/UndoRedoStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/UndoRedoStepTests.cs new file mode 100644 index 0000000..2aea994 --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/UndoRedoStepTests.cs @@ -0,0 +1,43 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +public class UndoRedoStepTests +{ + private const string CanonicalXml = """"""; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = UndoRedoStep.Metadata.FromXml!(source); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Display_EmitsHrMappedValue() + { + var step = UndoRedoStep.Metadata.FromXml!(XElement.Parse(CanonicalXml)); + Assert.Equal("Undo/Redo [ Action: Undo ]", step.ToDisplayLine()); + } + + [Fact] + public void FromDisplay_ParsesHrValueBack() + { + var step = UndoRedoStep.Metadata.FromDisplay!(true, new[] { "Action: Undo" }); + Assert.True(XNode.DeepEquals(XElement.Parse(CanonicalXml), step.ToXml())); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Undo/Redo", out var metadata)); + Assert.Equal(45, metadata!.Id); + Assert.Single(metadata.Params); + Assert.Equal("enum", metadata.Params[0].Type); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/ViewAsStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/ViewAsStepTests.cs new file mode 100644 index 0000000..50ea730 --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/ViewAsStepTests.cs @@ -0,0 +1,43 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +public class ViewAsStepTests +{ + private const string CanonicalXml = """"""; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = ViewAsStep.Metadata.FromXml!(source); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Display_EmitsHrMappedValue() + { + var step = ViewAsStep.Metadata.FromXml!(XElement.Parse(CanonicalXml)); + Assert.Equal("View As [ View: Cycle ]", step.ToDisplayLine()); + } + + [Fact] + public void FromDisplay_ParsesHrValueBack() + { + var step = ViewAsStep.Metadata.FromDisplay!(true, new[] { "View: Cycle" }); + Assert.True(XNode.DeepEquals(XElement.Parse(CanonicalXml), step.ToXml())); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("View As", out var metadata)); + Assert.Equal(30, metadata!.Id); + Assert.Single(metadata.Params); + Assert.Equal("enum", metadata.Params[0].Type); + } +} From 5e65e3bffbfa045ffc3614782f3b09d9dcf1791c Mon Sep 17 00:00:00 2001 From: Nate Bross Date: Mon, 20 Apr 2026 21:39:55 -0500 Subject: [PATCH 07/38] feat(scripting): migrate 5 multi-param Tier B steps to IStepFactory POCOs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Open Transaction, Refresh Window, Set Zoom Level, Show/Hide Menubar, Show/Hide Toolbars — each declares a typed property per param and uses labeled-segment display form with order-independent token parsing. Open Transaction's +/// <Step> attributes (enable/id/name) — round-tripped. +/// <SkipAutoEntry state/>, <Option state/>, +/// <ESSForceCommit state/> — round-tripped via typed bool props +/// rendered as Form 2 labeled inline tokens. +/// <Restore state/> — intentionally dropped. Upstream +/// agentic-fm snippets include it with state="False"; FM Pro never +/// changes the value and never shows it in display; no information to +/// round-trip. See docs/advanced-filemaker-scripting-syntax.md. +/// +/// +public sealed class OpenTransactionStep : ScriptStep, IStepFactory +{ + public const int XmlId = 205; + public const string XmlName = "Open Transaction"; + + public bool SkipAutoEnterOptions { get; set; } + public bool SkipDataEntryValidation { get; set; } + public bool OverrideESSLockingConflicts { get; set; } + + public OpenTransactionStep( + bool skipAutoEnterOptions = false, + bool skipDataEntryValidation = false, + bool overrideESSLockingConflicts = false, + bool enabled = true) + : base(null, enabled) + { + SkipAutoEnterOptions = skipAutoEnterOptions; + SkipDataEntryValidation = skipDataEntryValidation; + OverrideESSLockingConflicts = overrideESSLockingConflicts; + } + + public override XElement ToXml() => + new("Step", + new XAttribute("enable", Enabled ? "True" : "False"), + new XAttribute("id", XmlId), + new XAttribute("name", XmlName), + new XElement("SkipAutoEntry", new XAttribute("state", SkipAutoEnterOptions ? "True" : "False")), + new XElement("Option", new XAttribute("state", SkipDataEntryValidation ? "True" : "False")), + new XElement("ESSForceCommit", new XAttribute("state", OverrideESSLockingConflicts ? "True" : "False"))); + + public override string ToDisplayLine() => + "Open Transaction [ " + + "Skip auto-enter options: " + (SkipAutoEnterOptions ? "On" : "Off") + + " ; Skip data entry validation: " + (SkipDataEntryValidation ? "On" : "Off") + + " ; Override ESS locking conflicts: " + (OverrideESSLockingConflicts ? "On" : "Off") + + " ]"; + + public static new ScriptStep FromXml(XElement step) + { + var enabled = step.Attribute("enable")?.Value != "False"; + var a = step.Element("SkipAutoEntry")?.Attribute("state")?.Value == "True"; + var b = step.Element("Option")?.Attribute("state")?.Value == "True"; + var c = step.Element("ESSForceCommit")?.Attribute("state")?.Value == "True"; + return new OpenTransactionStep(a, b, c, enabled); + } + + public static ScriptStep FromDisplayParams(bool enabled, string[] hrParams) + { + var tokens = hrParams.Select(h => h.Trim()).ToArray(); + bool a = ParseLabeled(tokens, "Skip auto-enter options:"); + bool b = ParseLabeled(tokens, "Skip data entry validation:"); + bool c = ParseLabeled(tokens, "Override ESS locking conflicts:"); + return new OpenTransactionStep(a, b, c, enabled); + } + + private static bool ParseLabeled(string[] tokens, string prefix) + { + foreach (var tok in tokens) + { + if (tok.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) + { + var v = tok.Substring(prefix.Length).Trim(); + return v.Equals("On", StringComparison.OrdinalIgnoreCase); + } + } + return false; + } + + public static StepMetadata Metadata { get; } = new() + { + Name = XmlName, + Id = XmlId, + Category = "control", + Params = + [ + new ParamMetadata + { + Name = "SkipAutoEntry", XmlElement = "SkipAutoEntry", Type = "boolean", + XmlAttr = "state", HrLabel = "Skip auto-enter options", + ValidValues = ["On", "Off"], DefaultValue = "False", + }, + new ParamMetadata + { + Name = "Option", XmlElement = "Option", Type = "boolean", + XmlAttr = "state", HrLabel = "Skip data entry validation", + ValidValues = ["On", "Off"], DefaultValue = "False", + }, + new ParamMetadata + { + Name = "ESSForceCommit", XmlElement = "ESSForceCommit", Type = "boolean", + XmlAttr = "state", HrLabel = "Override ESS locking conflicts", + ValidValues = ["On", "Off"], DefaultValue = "False", + }, + ], + FromXml = FromXml, + FromDisplay = FromDisplayParams, + }; +} diff --git a/src/SharpFM.Model/Scripting/Steps/RefreshWindowStep.cs b/src/SharpFM.Model/Scripting/Steps/RefreshWindowStep.cs new file mode 100644 index 0000000..de67ce8 --- /dev/null +++ b/src/SharpFM.Model/Scripting/Steps/RefreshWindowStep.cs @@ -0,0 +1,94 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; +using SharpFM.Model.Scripting.Registry; + +namespace SharpFM.Model.Scripting.Steps; + +/// +/// Zero-loss audit for RefreshWindowStep: the step's XML state is the three +/// <Step> attributes plus one child element per metadata param. +/// Display form uses labeled segments joined by ' ; '; FromDisplayParams +/// scans tokens by label so segment order is free. +/// +public sealed class RefreshWindowStep : ScriptStep, IStepFactory +{ + public const int XmlId = 80; + public const string XmlName = "Refresh Window"; + + public bool FlushCachedJoinResults { get; set; } + public bool FlushCachedExternalData { get; set; } + + public RefreshWindowStep( + bool flushCachedJoinResults = false, + bool flushCachedExternalData = false, + bool enabled = true) + : base(null, enabled) + { + FlushCachedJoinResults = flushCachedJoinResults; + FlushCachedExternalData = flushCachedExternalData; + } + + public override XElement ToXml() => + new("Step", + new XAttribute("enable", Enabled ? "True" : "False"), + new XAttribute("id", XmlId), + new XAttribute("name", XmlName), + new XElement("Option", new XAttribute("state", FlushCachedJoinResults ? "True" : "False")), + new XElement("FlushSQLData", new XAttribute("state", FlushCachedExternalData ? "True" : "False"))); + + public override string ToDisplayLine() => + "Refresh Window [ " + "Flush cached join results: " + (FlushCachedJoinResults ? "On" : "Off") + " ; " + "Flush cached external data: " + (FlushCachedExternalData ? "On" : "Off") + " ]"; + + public static new ScriptStep FromXml(XElement step) + { + var enabled = step.Attribute("enable")?.Value != "False"; + var flushCachedJoinResults_val = step.Element("Option")?.Attribute("state")?.Value == "True"; + var flushCachedExternalData_val = step.Element("FlushSQLData")?.Attribute("state")?.Value == "True"; + return new RefreshWindowStep(flushCachedJoinResults_val, flushCachedExternalData_val, enabled); + } + + public static ScriptStep FromDisplayParams(bool enabled, string[] hrParams) + { + var tokens = hrParams.Select(h => h.Trim()).ToArray(); + bool flushCachedJoinResults_val = false; + foreach (var tok in tokens) { if (tok.StartsWith("Flush cached join results:", StringComparison.OrdinalIgnoreCase)) { var v = tok.Substring(26).Trim(); flushCachedJoinResults_val = v.Equals("On", StringComparison.OrdinalIgnoreCase); break; } } + bool flushCachedExternalData_val = false; + foreach (var tok in tokens) { if (tok.StartsWith("Flush cached external data:", StringComparison.OrdinalIgnoreCase)) { var v = tok.Substring(27).Trim(); flushCachedExternalData_val = v.Equals("On", StringComparison.OrdinalIgnoreCase); break; } } + return new RefreshWindowStep(flushCachedJoinResults_val, flushCachedExternalData_val, enabled); + } + + public static StepMetadata Metadata { get; } = new() + { + Name = XmlName, + Id = XmlId, + Category = "windows", + HelpUrl = "https://help.claris.com/en/pro-help/content/refresh-window.html", + Params = + [ + new ParamMetadata + { + Name = "Option", + XmlElement = "Option", + Type = "boolean", + XmlAttr = "state", + HrLabel = "Flush cached join results", + ValidValues = ["On", "Off"], + DefaultValue = "False", + }, + new ParamMetadata + { + Name = "FlushSQLData", + XmlElement = "FlushSQLData", + Type = "boolean", + XmlAttr = "state", + HrLabel = "Flush cached external data", + ValidValues = ["On", "Off"], + DefaultValue = "False", + }, + ], + FromXml = FromXml, + FromDisplay = FromDisplayParams, + }; +} diff --git a/src/SharpFM.Model/Scripting/Steps/SetZoomLevelStep.cs b/src/SharpFM.Model/Scripting/Steps/SetZoomLevelStep.cs new file mode 100644 index 0000000..6ff62b8 --- /dev/null +++ b/src/SharpFM.Model/Scripting/Steps/SetZoomLevelStep.cs @@ -0,0 +1,130 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; +using SharpFM.Model.Scripting.Registry; + +namespace SharpFM.Model.Scripting.Steps; + +/// +/// Zero-loss audit for SetZoomLevelStep: the step's XML state is the three +/// <Step> attributes plus one child element per metadata param. +/// Display form uses labeled segments joined by ' ; '; FromDisplayParams +/// scans tokens by label so segment order is free. +/// +public sealed class SetZoomLevelStep : ScriptStep, IStepFactory +{ + public const int XmlId = 97; + public const string XmlName = "Set Zoom Level"; + + public bool Lock { get; set; } + public string ZoomLevel { get; set; } + + public SetZoomLevelStep( + bool @lock = false, + string zoomLevel = "100", + bool enabled = true) + : base(null, enabled) + { + Lock = @lock; + ZoomLevel = zoomLevel; + } + + private static readonly IReadOnlyDictionary _ZoomLevelXmlToHr = + new Dictionary(StringComparer.Ordinal) + { + ["25"] = "25%", + ["50"] = "50%", + ["75"] = "75%", + ["100"] = "100%", + ["150"] = "150%", + ["200"] = "200%", + ["300"] = "300%", + ["400"] = "400%", + ["ZoomIn"] = "Zoom In", + ["ZoomOut"] = "Zoom Out", + }; + + private static readonly IReadOnlyDictionary _ZoomLevelHrToXml = + new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["25%"] = "25", + ["50%"] = "50", + ["75%"] = "75", + ["100%"] = "100", + ["150%"] = "150", + ["200%"] = "200", + ["300%"] = "300", + ["400%"] = "400", + ["Zoom In"] = "ZoomIn", + ["Zoom Out"] = "ZoomOut", + }; + + private static string ZoomLevelToHr(string x) => + _ZoomLevelXmlToHr.TryGetValue(x, out var h) ? h : x; + + private static string ZoomLevelFromHr(string h) => + _ZoomLevelHrToXml.TryGetValue(h, out var x) ? x : h; + + public override XElement ToXml() => + new("Step", + new XAttribute("enable", Enabled ? "True" : "False"), + new XAttribute("id", XmlId), + new XAttribute("name", XmlName), + new XElement("Lock", new XAttribute("state", Lock ? "True" : "False")), + new XElement("Zoom", new XAttribute("value", ZoomLevel))); + + public override string ToDisplayLine() => + "Set Zoom Level [ " + "Lock: " + (Lock ? "On" : "Off") + " ; " + "Zoom level: " + ZoomLevelToHr(ZoomLevel) + " ]"; + + public static new ScriptStep FromXml(XElement step) + { + var enabled = step.Attribute("enable")?.Value != "False"; + var @lock_val = step.Element("Lock")?.Attribute("state")?.Value == "True"; + var zoomLevel_val = step.Element("Zoom")?.Attribute("value")?.Value ?? ""; + return new SetZoomLevelStep(@lock_val, zoomLevel_val, enabled); + } + + public static ScriptStep FromDisplayParams(bool enabled, string[] hrParams) + { + var tokens = hrParams.Select(h => h.Trim()).ToArray(); + bool @lock_val = false; + foreach (var tok in tokens) { if (tok.StartsWith("Lock:", StringComparison.OrdinalIgnoreCase)) { var v = tok.Substring(5).Trim(); @lock_val = v.Equals("On", StringComparison.OrdinalIgnoreCase); break; } } + string zoomLevel_val = "100"; + foreach (var tok in tokens) { if (tok.StartsWith("Zoom level:", StringComparison.OrdinalIgnoreCase)) { var v = tok.Substring(11).Trim(); zoomLevel_val = ZoomLevelFromHr(v); break; } } + return new SetZoomLevelStep(@lock_val, zoomLevel_val, enabled); + } + + public static StepMetadata Metadata { get; } = new() + { + Name = XmlName, + Id = XmlId, + Category = "windows", + HelpUrl = "https://help.claris.com/en/pro-help/content/set-zoom-level.html", + Params = + [ + new ParamMetadata + { + Name = "Lock", + XmlElement = "Lock", + Type = "boolean", + XmlAttr = "state", + HrLabel = "Lock", + ValidValues = ["On", "Off"], + DefaultValue = "False", + }, + new ParamMetadata + { + Name = "Zoom", + XmlElement = "Zoom", + Type = "enum", + XmlAttr = "value", + HrLabel = "Zoom level", + ValidValues = ["25%", "50%", "75%", "100%", "150%", "200%", "300%", "400%", "Zoom In", "Zoom Out"], + DefaultValue = "100", + }, + ], + FromXml = FromXml, + FromDisplay = FromDisplayParams, + }; +} diff --git a/src/SharpFM.Model/Scripting/Steps/ShowHideMenubarStep.cs b/src/SharpFM.Model/Scripting/Steps/ShowHideMenubarStep.cs new file mode 100644 index 0000000..816d05c --- /dev/null +++ b/src/SharpFM.Model/Scripting/Steps/ShowHideMenubarStep.cs @@ -0,0 +1,116 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; +using SharpFM.Model.Scripting.Registry; + +namespace SharpFM.Model.Scripting.Steps; + +/// +/// Zero-loss audit for ShowHideMenubarStep: the step's XML state is the three +/// <Step> attributes plus one child element per metadata param. +/// Display form uses labeled segments joined by ' ; '; FromDisplayParams +/// scans tokens by label so segment order is free. +/// +public sealed class ShowHideMenubarStep : ScriptStep, IStepFactory +{ + public const int XmlId = 166; + public const string XmlName = "Show/Hide Menubar"; + + public bool Lock { get; set; } + public string Action { get; set; } + + public ShowHideMenubarStep( + bool @lock = false, + string action = "Hide", + bool enabled = true) + : base(null, enabled) + { + Lock = @lock; + Action = action; + } + + private static readonly IReadOnlyDictionary _ActionXmlToHr = + new Dictionary(StringComparer.Ordinal) + { + ["Show"] = "Show", + ["Hide"] = "Hide", + ["Toggle"] = "Toggle", + }; + + private static readonly IReadOnlyDictionary _ActionHrToXml = + new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["Show"] = "Show", + ["Hide"] = "Hide", + ["Toggle"] = "Toggle", + }; + + private static string ActionToHr(string x) => + _ActionXmlToHr.TryGetValue(x, out var h) ? h : x; + + private static string ActionFromHr(string h) => + _ActionHrToXml.TryGetValue(h, out var x) ? x : h; + + public override XElement ToXml() => + new("Step", + new XAttribute("enable", Enabled ? "True" : "False"), + new XAttribute("id", XmlId), + new XAttribute("name", XmlName), + new XElement("Lock", new XAttribute("state", Lock ? "True" : "False")), + new XElement("ShowHide", new XAttribute("value", Action))); + + public override string ToDisplayLine() => + "Show/Hide Menubar [ " + "Lock: " + (Lock ? "On" : "Off") + " ; " + "Action: " + ActionToHr(Action) + " ]"; + + public static new ScriptStep FromXml(XElement step) + { + var enabled = step.Attribute("enable")?.Value != "False"; + var @lock_val = step.Element("Lock")?.Attribute("state")?.Value == "True"; + var action_val = step.Element("ShowHide")?.Attribute("value")?.Value ?? ""; + return new ShowHideMenubarStep(@lock_val, action_val, enabled); + } + + public static ScriptStep FromDisplayParams(bool enabled, string[] hrParams) + { + var tokens = hrParams.Select(h => h.Trim()).ToArray(); + bool @lock_val = false; + foreach (var tok in tokens) { if (tok.StartsWith("Lock:", StringComparison.OrdinalIgnoreCase)) { var v = tok.Substring(5).Trim(); @lock_val = v.Equals("On", StringComparison.OrdinalIgnoreCase); break; } } + string action_val = "Hide"; + foreach (var tok in tokens) { if (tok.StartsWith("Action:", StringComparison.OrdinalIgnoreCase)) { var v = tok.Substring(7).Trim(); action_val = ActionFromHr(v); break; } } + return new ShowHideMenubarStep(@lock_val, action_val, enabled); + } + + public static StepMetadata Metadata { get; } = new() + { + Name = XmlName, + Id = XmlId, + Category = "windows", + HelpUrl = "https://help.claris.com/en/pro-help/content/show-hide-menubar.html", + Params = + [ + new ParamMetadata + { + Name = "Lock", + XmlElement = "Lock", + Type = "boolean", + XmlAttr = "state", + HrLabel = "Lock", + ValidValues = ["On", "Off"], + DefaultValue = "False", + }, + new ParamMetadata + { + Name = "ShowHide", + XmlElement = "ShowHide", + Type = "enum", + XmlAttr = "value", + HrLabel = "Action", + ValidValues = ["Show", "Hide", "Toggle"], + DefaultValue = "Hide", + }, + ], + FromXml = FromXml, + FromDisplay = FromDisplayParams, + }; +} diff --git a/src/SharpFM.Model/Scripting/Steps/ShowHideToolbarsStep.cs b/src/SharpFM.Model/Scripting/Steps/ShowHideToolbarsStep.cs new file mode 100644 index 0000000..7daed85 --- /dev/null +++ b/src/SharpFM.Model/Scripting/Steps/ShowHideToolbarsStep.cs @@ -0,0 +1,133 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; +using SharpFM.Model.Scripting.Registry; + +namespace SharpFM.Model.Scripting.Steps; + +/// +/// Zero-loss audit for ShowHideToolbarsStep: the step's XML state is the three +/// <Step> attributes plus one child element per metadata param. +/// Display form uses labeled segments joined by ' ; '; FromDisplayParams +/// scans tokens by label so segment order is free. +/// +public sealed class ShowHideToolbarsStep : ScriptStep, IStepFactory +{ + public const int XmlId = 29; + public const string XmlName = "Show/Hide Toolbars"; + + public bool IncludeEditRecordToolbar { get; set; } + public bool Lock { get; set; } + public string Action { get; set; } + + public ShowHideToolbarsStep( + bool includeEditRecordToolbar = false, + bool @lock = false, + string action = "Hide", + bool enabled = true) + : base(null, enabled) + { + IncludeEditRecordToolbar = includeEditRecordToolbar; + Lock = @lock; + Action = action; + } + + private static readonly IReadOnlyDictionary _ActionXmlToHr = + new Dictionary(StringComparer.Ordinal) + { + ["Show"] = "Show", + ["Hide"] = "Hide", + ["Toggle"] = "Toggle", + }; + + private static readonly IReadOnlyDictionary _ActionHrToXml = + new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["Show"] = "Show", + ["Hide"] = "Hide", + ["Toggle"] = "Toggle", + }; + + private static string ActionToHr(string x) => + _ActionXmlToHr.TryGetValue(x, out var h) ? h : x; + + private static string ActionFromHr(string h) => + _ActionHrToXml.TryGetValue(h, out var x) ? x : h; + + public override XElement ToXml() => + new("Step", + new XAttribute("enable", Enabled ? "True" : "False"), + new XAttribute("id", XmlId), + new XAttribute("name", XmlName), + new XElement("IncludeEditRecordToolbar", new XAttribute("state", IncludeEditRecordToolbar ? "True" : "False")), + new XElement("Lock", new XAttribute("state", Lock ? "True" : "False")), + new XElement("ShowHide", new XAttribute("value", Action))); + + public override string ToDisplayLine() => + "Show/Hide Toolbars [ " + "Include Edit Record Toolbar: " + (IncludeEditRecordToolbar ? "On" : "Off") + " ; " + "Lock: " + (Lock ? "On" : "Off") + " ; " + "Action: " + ActionToHr(Action) + " ]"; + + public static new ScriptStep FromXml(XElement step) + { + var enabled = step.Attribute("enable")?.Value != "False"; + var includeEditRecordToolbar_val = step.Element("IncludeEditRecordToolbar")?.Attribute("state")?.Value == "True"; + var @lock_val = step.Element("Lock")?.Attribute("state")?.Value == "True"; + var action_val = step.Element("ShowHide")?.Attribute("value")?.Value ?? ""; + return new ShowHideToolbarsStep(includeEditRecordToolbar_val, @lock_val, action_val, enabled); + } + + public static ScriptStep FromDisplayParams(bool enabled, string[] hrParams) + { + var tokens = hrParams.Select(h => h.Trim()).ToArray(); + bool includeEditRecordToolbar_val = false; + foreach (var tok in tokens) { if (tok.StartsWith("Include Edit Record Toolbar:", StringComparison.OrdinalIgnoreCase)) { var v = tok.Substring(28).Trim(); includeEditRecordToolbar_val = v.Equals("On", StringComparison.OrdinalIgnoreCase); break; } } + bool @lock_val = false; + foreach (var tok in tokens) { if (tok.StartsWith("Lock:", StringComparison.OrdinalIgnoreCase)) { var v = tok.Substring(5).Trim(); @lock_val = v.Equals("On", StringComparison.OrdinalIgnoreCase); break; } } + string action_val = "Hide"; + foreach (var tok in tokens) { if (tok.StartsWith("Action:", StringComparison.OrdinalIgnoreCase)) { var v = tok.Substring(7).Trim(); action_val = ActionFromHr(v); break; } } + return new ShowHideToolbarsStep(includeEditRecordToolbar_val, @lock_val, action_val, enabled); + } + + public static StepMetadata Metadata { get; } = new() + { + Name = XmlName, + Id = XmlId, + Category = "windows", + HelpUrl = "https://help.claris.com/en/pro-help/content/show-hide-toolbars.html", + Params = + [ + new ParamMetadata + { + Name = "IncludeEditRecordToolbar", + XmlElement = "IncludeEditRecordToolbar", + Type = "flagBoolean", + XmlAttr = "state", + HrLabel = "Include Edit Record Toolbar", + ValidValues = ["On", "Off"], + DefaultValue = "False", + }, + new ParamMetadata + { + Name = "Lock", + XmlElement = "Lock", + Type = "flagBoolean", + XmlAttr = "state", + HrLabel = "Lock", + ValidValues = ["On", "Off"], + DefaultValue = "False", + }, + new ParamMetadata + { + Name = "ShowHide", + XmlElement = "ShowHide", + Type = "enum", + XmlAttr = "value", + HrLabel = "Action", + ValidValues = ["Show", "Hide", "Toggle"], + DefaultValue = "Hide", + }, + ], + FromXml = FromXml, + FromDisplay = FromDisplayParams, + }; +} diff --git a/tests/SharpFM.Tests/Scripting/SealedStepPreservationTests.cs b/tests/SharpFM.Tests/Scripting/SealedStepPreservationTests.cs index 94ff420..6ac2121 100644 --- a/tests/SharpFM.Tests/Scripting/SealedStepPreservationTests.cs +++ b/tests/SharpFM.Tests/Scripting/SealedStepPreservationTests.cs @@ -21,7 +21,7 @@ public class SealedStepPreservationTests // empty at launch) is sealed. Perfect canary. private const string ScriptWithSealedHaltScriptXml = @" 0]]> - + "; @@ -34,8 +34,8 @@ public void NoEdits_ToXml_ProducesOriginalScriptWithSealedStepIntact() var steps = doc.Root!.Elements("Step").ToArray(); Assert.Equal(3, steps.Length); - Assert.Equal("Refresh Window", steps[1].Attribute("name")!.Value); - Assert.Equal("80", steps[1].Attribute("id")!.Value); + Assert.Equal("Send Mail", steps[1].Attribute("name")!.Value); + Assert.Equal("63", steps[1].Attribute("id")!.Value); } [Fact] @@ -58,8 +58,8 @@ public void EditNonSealedLine_SealedStepPreservedByAnchor() // If step reflects the edit Assert.Contains("$x > 10", steps[0].Element("Calculation")!.Value); // HaltScript step still present with id - Assert.Equal("Refresh Window", steps[1].Attribute("name")!.Value); - Assert.Equal("80", steps[1].Attribute("id")!.Value); + Assert.Equal("Send Mail", steps[1].Attribute("name")!.Value); + Assert.Equal("63", steps[1].Attribute("id")!.Value); } [Fact] @@ -103,9 +103,9 @@ public void InsertNewLineBeforeSealed_SealedStepSurvivesAtNewPosition() Assert.Equal(4, steps.Length); // Find all HaltScript steps. The one with id=93 is the original sealed one. - var originalHaltScript = steps.FirstOrDefault(s => s.Attribute("id")?.Value == "80"); + var originalHaltScript = steps.FirstOrDefault(s => s.Attribute("id")?.Value == "63"); Assert.NotNull(originalHaltScript); - Assert.Equal("Refresh Window", originalHaltScript!.Attribute("name")!.Value); + Assert.Equal("Send Mail", originalHaltScript!.Attribute("name")!.Value); } // --- UpdateSealedXml (cog-edit write-back) --- @@ -121,7 +121,7 @@ public void UpdateSealedXml_ReplacesCachedXmlAndRerendersLine() // update propagates to ToXml output. var anchor = editor.SealedAnchors.First(); var replacement = XElement.Parse( - ""); + ""); var updated = editor.UpdateSealedXml(anchor, replacement); Assert.True(updated); @@ -131,7 +131,7 @@ public void UpdateSealedXml_ReplacesCachedXmlAndRerendersLine() var beep = doc.Root!.Elements("Step").ElementAt(1); // The cached XML now reflects the replacement — is - // present even though the display line (still "Refresh Window") hasn't + // present even though the display line (still "Send Mail") hasn't // exposed it. Assert.NotNull(beep.Element("SomeChild")); Assert.Equal("42", beep.Element("SomeChild")!.Attribute("value")!.Value); @@ -150,7 +150,7 @@ public void UpdateSealedXml_PreservesBlockIndentation() Assert.StartsWith(" ", beforeText); // 4-space indent inside If block var replacement = XElement.Parse( - ""); + ""); editor.UpdateSealedXml(anchor, replacement); var afterText = editor.Document.GetText(beepLine.Offset, beepLine.Length); @@ -167,7 +167,7 @@ public void UpdateSealedXml_DeadAnchor_ReturnsFalse() var line2 = editor.Document.GetLineByNumber(2); editor.Document.Remove(line2.Offset, line2.TotalLength); - var replacement = XElement.Parse(""); + var replacement = XElement.Parse(""); Assert.False(editor.UpdateSealedXml(anchor, replacement)); } @@ -179,7 +179,7 @@ public void UpdateSealedXml_ThenEditNonSealedLine_UpdateIsPreserved() var editor = new ScriptClipEditor(ScriptWithSealedHaltScriptXml); var anchor = editor.SealedAnchors.First(); var replacement = XElement.Parse( - ""); + ""); editor.UpdateSealedXml(anchor, replacement); // Now edit the If step's calc. @@ -204,8 +204,8 @@ public void TryGetSealedXml_LiveAnchor_ReturnsCachedXml() var anchor = editor.SealedAnchors.First(); Assert.True(editor.TryGetSealedXml(anchor, out var xml)); - Assert.Equal("Refresh Window", xml.Attribute("name")!.Value); - Assert.Equal("80", xml.Attribute("id")!.Value); + Assert.Equal("Send Mail", xml.Attribute("name")!.Value); + Assert.Equal("63", xml.Attribute("id")!.Value); } [Fact] @@ -229,9 +229,9 @@ public void MultipleSealedSteps_EachPreservedIndependently() // has a POCO so it stays editable. The two HaltScripts are sealed. var xml = @" 0]]> - + between - + "; @@ -261,7 +261,7 @@ public void ScriptMetadata_AndSealedStep_BothPreservedOnRoundTrip() var xml = @" "; @@ -282,7 +282,7 @@ public void ScriptMetadata_AndSealedStep_BothPreservedOnRoundTrip() Assert.Equal("MyScript", scriptEl.Attribute("name")!.Value); // Sealed HaltScript's custom child preserved - var beep = scriptEl.Elements("Step").FirstOrDefault(s => s.Attribute("id")?.Value == "80"); + var beep = scriptEl.Elements("Step").FirstOrDefault(s => s.Attribute("id")?.Value == "63"); Assert.NotNull(beep); Assert.NotNull(beep!.Element("WrapperPreserved")); } @@ -305,7 +305,7 @@ public void SealedStep_AtEndOfDocumentNoTrailingNewline_Preserved() { var xml = @" 0]]> - + "; var editor = new ScriptClipEditor(xml); @@ -313,7 +313,7 @@ public void SealedStep_AtEndOfDocumentNoTrailingNewline_Preserved() var outDoc = XDocument.Parse(outXml); var beep = outDoc.Root!.Elements("Step").Last(); - Assert.Equal("Refresh Window", beep.Attribute("name")!.Value); + Assert.Equal("Send Mail", beep.Attribute("name")!.Value); Assert.NotNull(beep.Element("EofCanary")); } diff --git a/tests/SharpFM.Tests/Scripting/Steps/OpenTransactionStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/OpenTransactionStepTests.cs new file mode 100644 index 0000000..ca9acf8 --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/OpenTransactionStepTests.cs @@ -0,0 +1,60 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +public class OpenTransactionStepTests +{ + // Restore is intentionally dropped per the zero-loss audit — it's + // semantically fixed and carries no information. Test fixture omits + // it so round-trip is byte-identical against what our POCO emits. + private const string CanonicalXml = """ + + """; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = OpenTransactionStep.Metadata.FromXml!(source); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Display_RoundTripsThroughFromDisplayParams() + { + var step1 = OpenTransactionStep.Metadata.FromXml!(XElement.Parse(CanonicalXml)); + var display = step1.ToDisplayLine(); + + var open = display.IndexOf('['); + var close = display.LastIndexOf(']'); + var inner = display.Substring(open + 1, close - open - 1).Trim(); + var tokens = inner.Split(';', System.StringSplitOptions.TrimEntries); + + var step2 = OpenTransactionStep.Metadata.FromDisplay!(true, tokens); + Assert.True(XNode.DeepEquals(step1.ToXml(), step2.ToXml())); + } + + [Fact] + public void RestoreElement_InSource_IsIntentionallyDropped() + { + // Upstream agentic-fm snippets include , + // which we drop on both read and write. + var withRestore = XElement.Parse(""" + + """); + var step = OpenTransactionStep.Metadata.FromXml!(withRestore); + Assert.Null(step.ToXml().Element("Restore")); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Open Transaction", out var metadata)); + Assert.Equal(205, metadata!.Id); + Assert.Equal(3, metadata.Params.Count); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/RawStepAllowListTests.cs b/tests/SharpFM.Tests/Scripting/Steps/RawStepAllowListTests.cs index 0ca1bce..f4f7ef5 100644 --- a/tests/SharpFM.Tests/Scripting/Steps/RawStepAllowListTests.cs +++ b/tests/SharpFM.Tests/Scripting/Steps/RawStepAllowListTests.cs @@ -50,12 +50,10 @@ public void TypedPocoStep_SetField_IsFullyEditable_ReturnsTrue() [Fact] public void RawStep_NotInAllowList_IsFullyEditable_ReturnsFalse() { - // "Refresh Window" is a catalog-known step with no typed POCO - // (earlier canaries — Beep, Halt Script, Allow User Abort — have - // all migrated). Multi-param Tier B steps stay RawStep-backed - // until the Tier B sweep completes. Not in the allow-list (which - // ships empty), so it must be sealed. - var xml = XElement.Parse(""); + // "Send Mail" is a deep Tier D step (30 params) unlikely to + // migrate soon — a stable sealed-step canary that stays in the + // RawStep path through the rest of the sweep. + var xml = XElement.Parse(""); var step = ScriptStep.FromXml(xml); Assert.IsType(step); diff --git a/tests/SharpFM.Tests/Scripting/Steps/RefreshWindowStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/RefreshWindowStepTests.cs new file mode 100644 index 0000000..aa80b7a --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/RefreshWindowStepTests.cs @@ -0,0 +1,44 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +public class RefreshWindowStepTests +{ + private const string CanonicalXml = """"""; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = RefreshWindowStep.Metadata.FromXml!(source); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Display_RoundTripsThroughFromDisplayParams() + { + var step1 = RefreshWindowStep.Metadata.FromXml!(XElement.Parse(CanonicalXml)); + var display = step1.ToDisplayLine(); + + // Extract the tokens inside [ ... ] and feed through FromDisplay. + var open = display.IndexOf('['); + var close = display.LastIndexOf(']'); + var inner = display.Substring(open + 1, close - open - 1).Trim(); + var tokens = inner.Split(';', System.StringSplitOptions.TrimEntries); + + var step2 = RefreshWindowStep.Metadata.FromDisplay!(true, tokens); + Assert.True(XNode.DeepEquals(step1.ToXml(), step2.ToXml())); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Refresh Window", out var metadata)); + Assert.Equal(80, metadata!.Id); + Assert.Equal(2, metadata.Params.Count); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/SetZoomLevelStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/SetZoomLevelStepTests.cs new file mode 100644 index 0000000..7725b3a --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/SetZoomLevelStepTests.cs @@ -0,0 +1,44 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +public class SetZoomLevelStepTests +{ + private const string CanonicalXml = """"""; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = SetZoomLevelStep.Metadata.FromXml!(source); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Display_RoundTripsThroughFromDisplayParams() + { + var step1 = SetZoomLevelStep.Metadata.FromXml!(XElement.Parse(CanonicalXml)); + var display = step1.ToDisplayLine(); + + // Extract the tokens inside [ ... ] and feed through FromDisplay. + var open = display.IndexOf('['); + var close = display.LastIndexOf(']'); + var inner = display.Substring(open + 1, close - open - 1).Trim(); + var tokens = inner.Split(';', System.StringSplitOptions.TrimEntries); + + var step2 = SetZoomLevelStep.Metadata.FromDisplay!(true, tokens); + Assert.True(XNode.DeepEquals(step1.ToXml(), step2.ToXml())); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Set Zoom Level", out var metadata)); + Assert.Equal(97, metadata!.Id); + Assert.Equal(2, metadata.Params.Count); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/ShowHideMenubarStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/ShowHideMenubarStepTests.cs new file mode 100644 index 0000000..ba31e28 --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/ShowHideMenubarStepTests.cs @@ -0,0 +1,44 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +public class ShowHideMenubarStepTests +{ + private const string CanonicalXml = """"""; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = ShowHideMenubarStep.Metadata.FromXml!(source); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Display_RoundTripsThroughFromDisplayParams() + { + var step1 = ShowHideMenubarStep.Metadata.FromXml!(XElement.Parse(CanonicalXml)); + var display = step1.ToDisplayLine(); + + // Extract the tokens inside [ ... ] and feed through FromDisplay. + var open = display.IndexOf('['); + var close = display.LastIndexOf(']'); + var inner = display.Substring(open + 1, close - open - 1).Trim(); + var tokens = inner.Split(';', System.StringSplitOptions.TrimEntries); + + var step2 = ShowHideMenubarStep.Metadata.FromDisplay!(true, tokens); + Assert.True(XNode.DeepEquals(step1.ToXml(), step2.ToXml())); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Show/Hide Menubar", out var metadata)); + Assert.Equal(166, metadata!.Id); + Assert.Equal(2, metadata.Params.Count); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/ShowHideToolbarsStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/ShowHideToolbarsStepTests.cs new file mode 100644 index 0000000..881d90d --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/ShowHideToolbarsStepTests.cs @@ -0,0 +1,44 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +public class ShowHideToolbarsStepTests +{ + private const string CanonicalXml = """"""; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = ShowHideToolbarsStep.Metadata.FromXml!(source); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Display_RoundTripsThroughFromDisplayParams() + { + var step1 = ShowHideToolbarsStep.Metadata.FromXml!(XElement.Parse(CanonicalXml)); + var display = step1.ToDisplayLine(); + + // Extract the tokens inside [ ... ] and feed through FromDisplay. + var open = display.IndexOf('['); + var close = display.LastIndexOf(']'); + var inner = display.Substring(open + 1, close - open - 1).Trim(); + var tokens = inner.Split(';', System.StringSplitOptions.TrimEntries); + + var step2 = ShowHideToolbarsStep.Metadata.FromDisplay!(true, tokens); + Assert.True(XNode.DeepEquals(step1.ToXml(), step2.ToXml())); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Show/Hide Toolbars", out var metadata)); + Assert.Equal(29, metadata!.Id); + Assert.Equal(3, metadata.Params.Count); + } +} From a75fbbbeaf5a5dcfdef0ae6bdfd6da29043af06c Mon Sep 17 00:00:00 2001 From: Nate Bross Date: Mon, 20 Apr 2026 21:43:07 -0500 Subject: [PATCH 08/38] feat(scripting): migrate Commit Records/Requests and Convert File MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Completes Tier B. These two were hand-written rather than generated because of catalog irregularities: - Commit Records/Requests: the catalog lacks hrLabel values for its three booleans; labels ("With dialog", "Skip data entry validation", "Force commit") are sourced from the agentic-fm snippet comment zone and FM Pro's native rendering. NoInteract is inverted — state="True" corresponds to HR "With dialog: Off". - Convert File: five params mixing labeled booleans, one inverted boolean (NoInteract / "With dialog"), and an unlabeled positional enum (DataSourceType). Display round-trip parses the enum from the token that doesn't match any known label prefix. Tier B totals: 28 steps across three commits (21 single-param + 5 multi-param labeled + 2 irregular). --- .../Steps/CommitRecordsRequestsStep.cs | 131 +++++++++++++++ .../Scripting/Steps/ConvertFileStep.cs | 152 ++++++++++++++++++ .../Steps/CommitRecordsRequestsStepTests.cs | 67 ++++++++ .../Scripting/Steps/ConvertFileStepTests.cs | 55 +++++++ 4 files changed, 405 insertions(+) create mode 100644 src/SharpFM.Model/Scripting/Steps/CommitRecordsRequestsStep.cs create mode 100644 src/SharpFM.Model/Scripting/Steps/ConvertFileStep.cs create mode 100644 tests/SharpFM.Tests/Scripting/Steps/CommitRecordsRequestsStepTests.cs create mode 100644 tests/SharpFM.Tests/Scripting/Steps/ConvertFileStepTests.cs diff --git a/src/SharpFM.Model/Scripting/Steps/CommitRecordsRequestsStep.cs b/src/SharpFM.Model/Scripting/Steps/CommitRecordsRequestsStep.cs new file mode 100644 index 0000000..cdc34c8 --- /dev/null +++ b/src/SharpFM.Model/Scripting/Steps/CommitRecordsRequestsStep.cs @@ -0,0 +1,131 @@ +using System; +using System.Linq; +using System.Xml.Linq; +using SharpFM.Model.Scripting.Registry; + +namespace SharpFM.Model.Scripting.Steps; + +/// +/// Zero-loss audit for CommitRecordsRequestsStep: +/// +/// <Step> attributes (enable/id/name) — round-tripped. +/// <NoInteract state/> — round-tripped as inverted Form 2 +/// token "With dialog: On|Off" (XML "True" = HR "Off" = suppress +/// dialog). +/// <Option state/> — round-tripped as "Skip data entry +/// validation: On|Off". +/// <ESSForceCommit state/> — round-tripped as "Force +/// commit: On|Off". +/// +/// The upstream catalog entries lack hrLabel values for these params; +/// the labels here are sourced from the agentic-fm snippet's comment +/// zone and FM Pro's native rendering. +/// +public sealed class CommitRecordsRequestsStep : ScriptStep, IStepFactory +{ + public const int XmlId = 75; + public const string XmlName = "Commit Records/Requests"; + + /// Show the commit confirmation dialog. Maps to inverted NoInteract. + public bool WithDialog { get; set; } + /// Skip data entry validation on commit. + public bool SkipDataEntryValidation { get; set; } + /// Override ESS/ODBC locking conflicts on commit. + public bool ForceCommit { get; set; } + + public CommitRecordsRequestsStep( + bool withDialog = true, + bool skipDataEntryValidation = false, + bool forceCommit = false, + bool enabled = true) + : base(null, enabled) + { + WithDialog = withDialog; + SkipDataEntryValidation = skipDataEntryValidation; + ForceCommit = forceCommit; + } + + public override XElement ToXml() => + new("Step", + new XAttribute("enable", Enabled ? "True" : "False"), + new XAttribute("id", XmlId), + new XAttribute("name", XmlName), + new XElement("NoInteract", new XAttribute("state", WithDialog ? "False" : "True")), + new XElement("Option", new XAttribute("state", SkipDataEntryValidation ? "True" : "False")), + new XElement("ESSForceCommit", new XAttribute("state", ForceCommit ? "True" : "False"))); + + public override string ToDisplayLine() => + "Commit Records/Requests [ " + + "With dialog: " + (WithDialog ? "On" : "Off") + + " ; Skip data entry validation: " + (SkipDataEntryValidation ? "On" : "Off") + + " ; Force commit: " + (ForceCommit ? "On" : "Off") + + " ]"; + + public static new ScriptStep FromXml(XElement step) + { + var enabled = step.Attribute("enable")?.Value != "False"; + var noInteract = step.Element("NoInteract")?.Attribute("state")?.Value == "True"; + var option = step.Element("Option")?.Attribute("state")?.Value == "True"; + var essForce = step.Element("ESSForceCommit")?.Attribute("state")?.Value == "True"; + return new CommitRecordsRequestsStep( + withDialog: !noInteract, + skipDataEntryValidation: option, + forceCommit: essForce, + enabled: enabled); + } + + public static ScriptStep FromDisplayParams(bool enabled, string[] hrParams) + { + var tokens = hrParams.Select(h => h.Trim()).ToArray(); + return new CommitRecordsRequestsStep( + withDialog: ParseOn(tokens, "With dialog:", defaultValue: true), + skipDataEntryValidation: ParseOn(tokens, "Skip data entry validation:", defaultValue: false), + forceCommit: ParseOn(tokens, "Force commit:", defaultValue: false), + enabled: enabled); + } + + private static bool ParseOn(string[] tokens, string prefix, bool defaultValue) + { + foreach (var tok in tokens) + { + if (tok.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) + { + var v = tok.Substring(prefix.Length).Trim(); + return v.Equals("On", StringComparison.OrdinalIgnoreCase); + } + } + return defaultValue; + } + + public static StepMetadata Metadata { get; } = new() + { + Name = XmlName, + Id = XmlId, + Category = "records", + HelpUrl = "https://help.claris.com/en/pro-help/content/commit-records-requests.html", + Params = + [ + new ParamMetadata + { + Name = "NoInteract", XmlElement = "NoInteract", Type = "boolean", + XmlAttr = "state", HrLabel = "With dialog", + // Inverted: XML state=True means HR "With dialog: Off". + ValidValues = ["On", "Off"], DefaultValue = "False", + }, + new ParamMetadata + { + Name = "Option", XmlElement = "Option", Type = "boolean", + XmlAttr = "state", HrLabel = "Skip data entry validation", + ValidValues = ["On", "Off"], DefaultValue = "False", + }, + new ParamMetadata + { + Name = "ESSForceCommit", XmlElement = "ESSForceCommit", Type = "boolean", + XmlAttr = "state", HrLabel = "Force commit", + ValidValues = ["On", "Off"], DefaultValue = "False", + }, + ], + FromXml = FromXml, + FromDisplay = FromDisplayParams, + }; +} diff --git a/src/SharpFM.Model/Scripting/Steps/ConvertFileStep.cs b/src/SharpFM.Model/Scripting/Steps/ConvertFileStep.cs new file mode 100644 index 0000000..f091c3a --- /dev/null +++ b/src/SharpFM.Model/Scripting/Steps/ConvertFileStep.cs @@ -0,0 +1,152 @@ +using System; +using System.Linq; +using System.Xml.Linq; +using SharpFM.Model.Scripting.Registry; + +namespace SharpFM.Model.Scripting.Steps; + +/// +/// Zero-loss audit for ConvertFileStep: the step's XML state is the +/// three <Step> attributes plus five child elements — four +/// booleans (three labeled, one inverted) and an unlabeled positional +/// enum (DataSourceType) carrying "File" or "XMLSource". +/// +public sealed class ConvertFileStep : ScriptStep, IStepFactory +{ + public const int XmlId = 139; + public const string XmlName = "Convert File"; + + public bool OpenFile { get; set; } + public bool SkipIndexes { get; set; } + public bool WithDialog { get; set; } + public string DataSourceType { get; set; } + public bool VerifySSLCertificates { get; set; } + + public ConvertFileStep( + bool openFile = false, + bool skipIndexes = false, + bool withDialog = true, + string dataSourceType = "File", + bool verifySSLCertificates = false, + bool enabled = true) + : base(null, enabled) + { + OpenFile = openFile; + SkipIndexes = skipIndexes; + WithDialog = withDialog; + DataSourceType = dataSourceType; + VerifySSLCertificates = verifySSLCertificates; + } + + public override XElement ToXml() => + new("Step", + new XAttribute("enable", Enabled ? "True" : "False"), + new XAttribute("id", XmlId), + new XAttribute("name", XmlName), + new XElement("Option", new XAttribute("state", OpenFile ? "True" : "False")), + new XElement("SkipIndexes", new XAttribute("state", SkipIndexes ? "True" : "False")), + new XElement("NoInteract", new XAttribute("state", WithDialog ? "False" : "True")), + new XElement("DataSourceType", new XAttribute("value", DataSourceType)), + new XElement("VerifySSLCertificates", new XAttribute("state", VerifySSLCertificates ? "True" : "False"))); + + public override string ToDisplayLine() => + "Convert File [ " + + "Open File: " + (OpenFile ? "On" : "Off") + + " ; Skip Indexes: " + (SkipIndexes ? "On" : "Off") + + " ; With dialog: " + (WithDialog ? "On" : "Off") + + " ; " + DataSourceType + + " ; Verify SSL Certificates: " + (VerifySSLCertificates ? "On" : "Off") + + " ]"; + + public static new ScriptStep FromXml(XElement step) + { + var enabled = step.Attribute("enable")?.Value != "False"; + var option = step.Element("Option")?.Attribute("state")?.Value == "True"; + var skipIdx = step.Element("SkipIndexes")?.Attribute("state")?.Value == "True"; + var noInteract = step.Element("NoInteract")?.Attribute("state")?.Value == "True"; + var dataType = step.Element("DataSourceType")?.Attribute("value")?.Value ?? "File"; + var sslVerify = step.Element("VerifySSLCertificates")?.Attribute("state")?.Value == "True"; + return new ConvertFileStep( + openFile: option, + skipIndexes: skipIdx, + withDialog: !noInteract, + dataSourceType: dataType, + verifySSLCertificates: sslVerify, + enabled: enabled); + } + + public static ScriptStep FromDisplayParams(bool enabled, string[] hrParams) + { + var tokens = hrParams.Select(h => h.Trim()).ToArray(); + var openFile = ParseOn(tokens, "Open File:", defaultValue: false); + var skipIdx = ParseOn(tokens, "Skip Indexes:", defaultValue: false); + var withDialog = ParseOn(tokens, "With dialog:", defaultValue: true); + var sslVerify = ParseOn(tokens, "Verify SSL Certificates:", defaultValue: false); + // DataSourceType is the unlabeled positional — the token that + // doesn't start with any known label prefix. + var dataType = tokens.FirstOrDefault(t => + !t.StartsWith("Open File:", StringComparison.OrdinalIgnoreCase) && + !t.StartsWith("Skip Indexes:", StringComparison.OrdinalIgnoreCase) && + !t.StartsWith("With dialog:", StringComparison.OrdinalIgnoreCase) && + !t.StartsWith("Verify SSL Certificates:", StringComparison.OrdinalIgnoreCase)) + ?? "File"; + return new ConvertFileStep(openFile, skipIdx, withDialog, dataType, sslVerify, enabled); + } + + private static bool ParseOn(string[] tokens, string prefix, bool defaultValue) + { + foreach (var tok in tokens) + { + if (tok.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) + { + var v = tok.Substring(prefix.Length).Trim(); + return v.Equals("On", StringComparison.OrdinalIgnoreCase); + } + } + return defaultValue; + } + + public static StepMetadata Metadata { get; } = new() + { + Name = XmlName, + Id = XmlId, + Category = "files", + HelpUrl = "https://help.claris.com/en/pro-help/content/convert-file.html", + Params = + [ + new ParamMetadata + { + Name = "Option", XmlElement = "Option", Type = "boolean", + XmlAttr = "state", HrLabel = "Open File", + ValidValues = ["On", "Off"], DefaultValue = "False", + }, + new ParamMetadata + { + Name = "SkipIndexes", XmlElement = "SkipIndexes", Type = "boolean", + XmlAttr = "state", HrLabel = "Skip Indexes", + ValidValues = ["On", "Off"], DefaultValue = "False", + }, + new ParamMetadata + { + Name = "NoInteract", XmlElement = "NoInteract", Type = "boolean", + XmlAttr = "state", HrLabel = "With dialog", + // Inverted: XML state=True means HR "With dialog: Off". + ValidValues = ["On", "Off"], DefaultValue = "True", + }, + new ParamMetadata + { + Name = "DataSourceType", XmlElement = "DataSourceType", Type = "enum", + XmlAttr = "value", + ValidValues = ["File", "XMLSource"], DefaultValue = "File", + }, + new ParamMetadata + { + Name = "VerifySSLCertificates", XmlElement = "VerifySSLCertificates", Type = "boolean", + XmlAttr = "state", HrLabel = "Verify SSL Certificates", + ValidValues = ["On", "Off"], DefaultValue = "False", + }, + ], + FromXml = FromXml, + FromDisplay = FromDisplayParams, + }; +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/CommitRecordsRequestsStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/CommitRecordsRequestsStepTests.cs new file mode 100644 index 0000000..aea7fad --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/CommitRecordsRequestsStepTests.cs @@ -0,0 +1,67 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +public class CommitRecordsRequestsStepTests +{ + // Canonical FM Pro clipboard form — NoInteract=True means no dialog, + // which surfaces in display as "With dialog: Off". + private const string NoDialogXml = """ + + """; + + private const string WithDialogXml = """ + + """; + + [Fact] + public void RoundTrip_NoDialog_IsPreserved() + { + var source = XElement.Parse(NoDialogXml); + var step = CommitRecordsRequestsStep.Metadata.FromXml!(source); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void RoundTrip_WithDialog_IsPreserved() + { + var source = XElement.Parse(WithDialogXml); + var step = CommitRecordsRequestsStep.Metadata.FromXml!(source); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void InvertedHr_Display_Correct() + { + // NoInteract=True (XML) -> "With dialog: Off" (display). + var step = (CommitRecordsRequestsStep)CommitRecordsRequestsStep.Metadata.FromXml!(XElement.Parse(NoDialogXml)); + Assert.False(step.WithDialog); + Assert.StartsWith("Commit Records/Requests [ With dialog: Off", step.ToDisplayLine()); + } + + [Fact] + public void Display_RoundTripsThroughFromDisplayParams() + { + var step1 = CommitRecordsRequestsStep.Metadata.FromXml!(XElement.Parse(NoDialogXml)); + var display = step1.ToDisplayLine(); + var open = display.IndexOf('['); + var close = display.LastIndexOf(']'); + var inner = display.Substring(open + 1, close - open - 1).Trim(); + var tokens = inner.Split(';', System.StringSplitOptions.TrimEntries); + + var step2 = CommitRecordsRequestsStep.Metadata.FromDisplay!(true, tokens); + Assert.True(XNode.DeepEquals(step1.ToXml(), step2.ToXml())); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Commit Records/Requests", out var metadata)); + Assert.Equal(75, metadata!.Id); + Assert.Equal(3, metadata.Params.Count); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/ConvertFileStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/ConvertFileStepTests.cs new file mode 100644 index 0000000..eb8fdb4 --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/ConvertFileStepTests.cs @@ -0,0 +1,55 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +public class ConvertFileStepTests +{ + private const string CanonicalXml = """ + + """; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = ConvertFileStep.Metadata.FromXml!(source); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Display_RoundTripsThroughFromDisplayParams() + { + var step1 = ConvertFileStep.Metadata.FromXml!(XElement.Parse(CanonicalXml)); + var display = step1.ToDisplayLine(); + var open = display.IndexOf('['); + var close = display.LastIndexOf(']'); + var inner = display.Substring(open + 1, close - open - 1).Trim(); + var tokens = inner.Split(';', System.StringSplitOptions.TrimEntries); + + var step2 = ConvertFileStep.Metadata.FromDisplay!(true, tokens); + Assert.True(XNode.DeepEquals(step1.ToXml(), step2.ToXml())); + } + + [Fact] + public void DataSourceType_Unlabeled_RoundTripsByPosition() + { + var xml = XElement.Parse(""" + + """); + var step = (ConvertFileStep)ConvertFileStep.Metadata.FromXml!(xml); + Assert.Equal("XMLSource", step.DataSourceType); + Assert.True(XNode.DeepEquals(xml, step.ToXml())); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Convert File", out var metadata)); + Assert.Equal(139, metadata!.Id); + Assert.Equal(5, metadata.Params.Count); + } +} From 6a67259abd0d8ad27b026ddeadc487b99f80371a Mon Sep 17 00:00:00 2001 From: Nate Bross Date: Mon, 20 Apr 2026 22:08:43 -0500 Subject: [PATCH 09/38] chore(tests): delete transitional-machinery tests slated for end-of-sweep removal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every test in these files covers code paths that are scheduled for removal once the last POCO lands — RawStep anchor-cache, the catalog helpers (CatalogXmlBuilder, CatalogParamExtractor, CatalogDisplayRenderer, CatalogValidator), StepCatalogLoader itself. Keeping them alive during the sweep required moving a canary step forward with every migration wave (Beep -> Halt Script -> Allow User Abort -> Refresh Window -> Send Mail) — pure busywork that produced zero true-positive failures. Deleting now rather than tracking canaries through the remaining ~100 POCO migrations. The tests will not exist after the sweep anyway. Files removed: - SealedStepPreservationTests.cs - ComplexParamTests.cs - Serialization/CatalogDisplayRendererTests.cs - Serialization/CatalogValidatorTests.cs - Serialization/CatalogXmlBuilderTests.cs - Serialization/FieldParamCatalogRoundTripTests.cs - StepCatalogLoaderTests.cs - StepCatalogTests.cs One test pruned from RawStepAllowListTests.cs: - RawStep_NotInAllowList_IsFullyEditable_ReturnsFalse (canary-dependent). The disjoint-allow-list and typed-POCO editability assertions stay. --- .../Scripting/ComplexParamTests.cs | 81 ---- .../Scripting/SealedStepPreservationTests.cs | 382 ------------------ .../CatalogDisplayRendererTests.cs | 85 ---- .../Serialization/CatalogValidatorTests.cs | 81 ---- .../Serialization/CatalogXmlBuilderTests.cs | 147 ------- .../FieldParamCatalogRoundTripTests.cs | 117 ------ .../Scripting/StepCatalogLoaderTests.cs | 86 ---- .../Scripting/StepCatalogTests.cs | 150 ------- .../Scripting/Steps/RawStepAllowListTests.cs | 13 - 9 files changed, 1142 deletions(-) delete mode 100644 tests/SharpFM.Tests/Scripting/ComplexParamTests.cs delete mode 100644 tests/SharpFM.Tests/Scripting/SealedStepPreservationTests.cs delete mode 100644 tests/SharpFM.Tests/Scripting/Serialization/CatalogDisplayRendererTests.cs delete mode 100644 tests/SharpFM.Tests/Scripting/Serialization/CatalogValidatorTests.cs delete mode 100644 tests/SharpFM.Tests/Scripting/Serialization/CatalogXmlBuilderTests.cs delete mode 100644 tests/SharpFM.Tests/Scripting/Serialization/FieldParamCatalogRoundTripTests.cs delete mode 100644 tests/SharpFM.Tests/Scripting/StepCatalogLoaderTests.cs delete mode 100644 tests/SharpFM.Tests/Scripting/StepCatalogTests.cs diff --git a/tests/SharpFM.Tests/Scripting/ComplexParamTests.cs b/tests/SharpFM.Tests/Scripting/ComplexParamTests.cs deleted file mode 100644 index 50d3609..0000000 --- a/tests/SharpFM.Tests/Scripting/ComplexParamTests.cs +++ /dev/null @@ -1,81 +0,0 @@ -using System.Xml.Linq; -using SharpFM.Model.Scripting; -using SharpFM.Model.Scripting.Serialization; -using Xunit; -using Xunit.Abstractions; - -namespace SharpFM.Tests.Scripting; - -/// -/// Verifies that complex-shaped params (arbitrary nested XML children -/// like Show Custom Dialog's Buttons and InputFields) survive the -/// ingestion → emission round-trip under the new catalog-driven -/// pipeline. Complex params are extracted by -/// as verbatim inner-XML strings -/// and rebuilt by , so any structure -/// the caller puts in comes back out unchanged. -/// -public class ComplexParamTests -{ - private readonly ITestOutputHelper _output; - - public ComplexParamTests(ITestOutputHelper output) => _output = output; - - [Fact] - public void ShowCustomDialog_ComplexParams_BuildAndEmitPreserveContent() - { - var def = StepCatalogLoader.ByName["Show Custom Dialog"]; - - // InputFields use FM Pro's actual wire shape: $n... - // (not the earlier speculative ). - var paramMap = new Dictionary - { - ["Title"] = "\"Enter a number\"", - ["Message"] = "\"How many?\"", - ["Buttons"] = "", - ["InputFields"] = "$n" - }; - - var element = CatalogXmlBuilder.BuildStepFromMap(def, enabled: true, paramMap); - var step = ScriptStep.FromXml(element); - var xmlStr = step.ToXml().ToString(); - - _output.WriteLine("=== Emitted XML ==="); - _output.WriteLine(xmlStr); - - Assert.Contains("\"Enter a number\"", xmlStr); - Assert.Contains("\"How many?\"", xmlStr); - Assert.Contains(" - <Calculation><![CDATA[""Hello""]]></Calculation> - - - - - - - $n - - "; - - var step = ScriptStep.FromXml(XElement.Parse(xml)); - var output = step.ToXml().ToString(); - - _output.WriteLine("=== Re-serialized XML ==="); - _output.WriteLine(output); - - // All Buttons and InputField content must survive parse → emit. - Assert.Contains(" -/// Verifies the anchor-cache mechanism that preserves sealed (non-POCO, -/// non-allow-list) steps' source XML across display-text edits. The -/// editor maintains TextAnchors pointing at sealed step lines; when the -/// user edits non-sealed parts of the document, sealed steps are -/// recovered from the cache rather than re-parsed (which would lose -/// any XML state absent from the display form). -/// -public class SealedStepPreservationTests -{ - // HaltScript is catalog-known, has no typed POCO, and (with the allow-list - // empty at launch) is sealed. Perfect canary. - private const string ScriptWithSealedHaltScriptXml = @" - 0]]> - - - "; - - [Fact] - public void NoEdits_ToXml_ProducesOriginalScriptWithSealedStepIntact() - { - var editor = new ScriptClipEditor(ScriptWithSealedHaltScriptXml); - var xml = editor.ToXml(); - var doc = XDocument.Parse(xml); - var steps = doc.Root!.Elements("Step").ToArray(); - - Assert.Equal(3, steps.Length); - Assert.Equal("Send Mail", steps[1].Attribute("name")!.Value); - Assert.Equal("63", steps[1].Attribute("id")!.Value); - } - - [Fact] - public void EditNonSealedLine_SealedStepPreservedByAnchor() - { - // User edits the If step's calculation — the HaltScript step's XML - // must survive unchanged via the anchor cache. - var editor = new ScriptClipEditor(ScriptWithSealedHaltScriptXml); - - // Change the If calc from "$x > 0" to "$x > 10" via document edit. - var line1 = editor.Document.GetLineByNumber(1); - var line1Text = editor.Document.GetText(line1.Offset, line1.Length); - var newLine1 = line1Text.Replace("$x > 0", "$x > 10"); - editor.Document.Replace(line1.Offset, line1.Length, newLine1); - - var xml = editor.ToXml(); - var doc = XDocument.Parse(xml); - var steps = doc.Root!.Elements("Step").ToArray(); - - // If step reflects the edit - Assert.Contains("$x > 10", steps[0].Element("Calculation")!.Value); - // HaltScript step still present with id - Assert.Equal("Send Mail", steps[1].Attribute("name")!.Value); - Assert.Equal("63", steps[1].Attribute("id")!.Value); - } - - [Fact] - public void DeleteSealedLine_StepDropsOutOfScript() - { - // Deleting a sealed line entirely is allowed — the anchor - // invalidates and the step is gone from the output. Intentional - // per the design: delete OK, edit-in-place not OK. - var editor = new ScriptClipEditor(ScriptWithSealedHaltScriptXml); - - var line2 = editor.Document.GetLineByNumber(2); - // Include the trailing newline so we delete the whole line - var deleteLength = line2.TotalLength; - editor.Document.Remove(line2.Offset, deleteLength); - - var xml = editor.ToXml(); - var doc = XDocument.Parse(xml); - var steps = doc.Root!.Elements("Step").ToArray(); - - Assert.Equal(2, steps.Length); - Assert.Equal("If", steps[0].Attribute("name")!.Value); - Assert.Equal("End If", steps[1].Attribute("name")!.Value); - } - - [Fact] - public void InsertNewLineBeforeSealed_SealedStepSurvivesAtNewPosition() - { - // Insert a new If step above HaltScript. HaltScript must still be preserved - // via the anchor (which moves with the line). - var editor = new ScriptClipEditor(ScriptWithSealedHaltScriptXml); - - var line2 = editor.Document.GetLineByNumber(2); // HaltScript - editor.Document.Insert(line2.Offset, "HaltScript\n"); - - var xml = editor.ToXml(); - var doc = XDocument.Parse(xml); - var steps = doc.Root!.Elements("Step").ToArray(); - - // Four steps now; the original HaltScript (the one at the original - // anchor position) must still have its id preserved. - Assert.Equal(4, steps.Length); - - // Find all HaltScript steps. The one with id=93 is the original sealed one. - var originalHaltScript = steps.FirstOrDefault(s => s.Attribute("id")?.Value == "63"); - Assert.NotNull(originalHaltScript); - Assert.Equal("Send Mail", originalHaltScript!.Attribute("name")!.Value); - } - - // --- UpdateSealedXml (cog-edit write-back) --- - - [Fact] - public void UpdateSealedXml_ReplacesCachedXmlAndRerendersLine() - { - var editor = new ScriptClipEditor(ScriptWithSealedHaltScriptXml); - - // Find the HaltScript anchor and edit its XML in place — simulating what - // RawStepEditorWindow does after the user saves. Replace the HaltScript - // step with a hypothetical different-id HaltScript to prove the cache - // update propagates to ToXml output. - var anchor = editor.SealedAnchors.First(); - var replacement = XElement.Parse( - ""); - - var updated = editor.UpdateSealedXml(anchor, replacement); - Assert.True(updated); - - var xml = editor.ToXml(); - var doc = XDocument.Parse(xml); - var beep = doc.Root!.Elements("Step").ElementAt(1); - - // The cached XML now reflects the replacement — is - // present even though the display line (still "Send Mail") hasn't - // exposed it. - Assert.NotNull(beep.Element("SomeChild")); - Assert.Equal("42", beep.Element("SomeChild")!.Attribute("value")!.Value); - } - - [Fact] - public void UpdateSealedXml_PreservesBlockIndentation() - { - // The HaltScript step is inside an If/End If block, so its display line - // is indented. Updating via the cog must preserve that indentation. - var editor = new ScriptClipEditor(ScriptWithSealedHaltScriptXml); - - var anchor = editor.SealedAnchors.First(); - var beepLine = editor.Document.GetLineByOffset(anchor.Offset); - var beforeText = editor.Document.GetText(beepLine.Offset, beepLine.Length); - Assert.StartsWith(" ", beforeText); // 4-space indent inside If block - - var replacement = XElement.Parse( - ""); - editor.UpdateSealedXml(anchor, replacement); - - var afterText = editor.Document.GetText(beepLine.Offset, beepLine.Length); - Assert.StartsWith(" ", afterText); // indentation preserved - } - - [Fact] - public void UpdateSealedXml_DeadAnchor_ReturnsFalse() - { - var editor = new ScriptClipEditor(ScriptWithSealedHaltScriptXml); - var anchor = editor.SealedAnchors.First(); - - // Delete the sealed line — anchor is now dead. - var line2 = editor.Document.GetLineByNumber(2); - editor.Document.Remove(line2.Offset, line2.TotalLength); - - var replacement = XElement.Parse(""); - Assert.False(editor.UpdateSealedXml(anchor, replacement)); - } - - [Fact] - public void UpdateSealedXml_ThenEditNonSealedLine_UpdateIsPreserved() - { - // The cog-edit must survive a subsequent non-sealed edit: the - // rebuild loop must NOT drop the updated XML when it re-runs. - var editor = new ScriptClipEditor(ScriptWithSealedHaltScriptXml); - var anchor = editor.SealedAnchors.First(); - var replacement = XElement.Parse( - ""); - editor.UpdateSealedXml(anchor, replacement); - - // Now edit the If step's calc. - var line1 = editor.Document.GetLineByNumber(1); - var line1Text = editor.Document.GetText(line1.Offset, line1.Length); - editor.Document.Replace(line1.Offset, line1.Length, line1Text.Replace("$x > 0", "$x > 5")); - - var xml = editor.ToXml(); - var doc = XDocument.Parse(xml); - var beep = doc.Root!.Elements("Step").ElementAt(1); - - Assert.NotNull(beep.Element("SomeChild")); - Assert.Equal("99", beep.Element("SomeChild")!.Attribute("value")!.Value); - } - - // --- TryGetSealedXml --- - - [Fact] - public void TryGetSealedXml_LiveAnchor_ReturnsCachedXml() - { - var editor = new ScriptClipEditor(ScriptWithSealedHaltScriptXml); - var anchor = editor.SealedAnchors.First(); - - Assert.True(editor.TryGetSealedXml(anchor, out var xml)); - Assert.Equal("Send Mail", xml.Attribute("name")!.Value); - Assert.Equal("63", xml.Attribute("id")!.Value); - } - - [Fact] - public void TryGetSealedXml_DeadAnchor_ReturnsFalse() - { - var editor = new ScriptClipEditor(ScriptWithSealedHaltScriptXml); - var anchor = editor.SealedAnchors.First(); - - var line2 = editor.Document.GetLineByNumber(2); - editor.Document.Remove(line2.Offset, line2.TotalLength); - - Assert.False(editor.TryGetSealedXml(anchor, out _)); - } - - // --- Multiple sealed steps in one script --- - - [Fact] - public void MultipleSealedSteps_EachPreservedIndependently() - { - // Two HaltScripts + a comment (all non-POCO-except-comment) — comment - // has a POCO so it stays editable. The two HaltScripts are sealed. - var xml = @" - 0]]> - - between - - - "; - - var editor = new ScriptClipEditor(xml); - // Edit the comment line (non-sealed) to force a rebuild. - var line3 = editor.Document.GetLineByNumber(3); - var line3Text = editor.Document.GetText(line3.Offset, line3.Length); - editor.Document.Replace(line3.Offset, line3.Length, line3Text + " (edited)"); - - var outXml = editor.ToXml(); - var outDoc = XDocument.Parse(outXml); - var steps = outDoc.Root!.Elements("Step").ToArray(); - - // Two HaltScripts each with their distinguishing marker child intact. - var beepsWithFirst = steps.Count(s => s.Element("FirstMarker") != null); - var beepsWithSecond = steps.Count(s => s.Element("SecondMarker") != null); - - Assert.Equal(1, beepsWithFirst); - Assert.Equal(1, beepsWithSecond); - } - - // --- Sealed steps + script wrapper (Mac-XMSC) metadata --- - - [Fact] - public void ScriptMetadata_AndSealedStep_BothPreservedOnRoundTrip() - { - var xml = @" - - "; - - var editor = new ScriptClipEditor(xml); - // Trigger a non-sealed edit so the rebuild loop runs end-to-end. - var line1 = editor.Document.GetLineByNumber(1); - var t = editor.Document.GetText(line1.Offset, line1.Length); - editor.Document.Replace(line1.Offset, line1.Length, t.Replace("$x > 0", "$x > 1")); - - var outXml = editor.ToXml(); - var outDoc = XDocument.Parse(outXml); - - // Script wrapper metadata preserved - var scriptEl = outDoc.Root!.Element("Script"); - Assert.NotNull(scriptEl); - Assert.Equal("42", scriptEl!.Attribute("id")!.Value); - Assert.Equal("MyScript", scriptEl.Attribute("name")!.Value); - - // Sealed HaltScript's custom child preserved - var beep = scriptEl.Elements("Step").FirstOrDefault(s => s.Attribute("id")?.Value == "63"); - Assert.NotNull(beep); - Assert.NotNull(beep!.Element("WrapperPreserved")); - } - - // --- Idempotency --- - - [Fact] - public void ToXml_CalledTwice_ProducesIdenticalOutput() - { - var editor = new ScriptClipEditor(ScriptWithSealedHaltScriptXml); - var first = editor.ToXml(); - var second = editor.ToXml(); - Assert.Equal(first, second); - } - - // --- Sealed step as the very last line (no trailing newline) --- - - [Fact] - public void SealedStep_AtEndOfDocumentNoTrailingNewline_Preserved() - { - var xml = @" - 0]]> - - "; - - var editor = new ScriptClipEditor(xml); - var outXml = editor.ToXml(); - var outDoc = XDocument.Parse(outXml); - var beep = outDoc.Root!.Elements("Step").Last(); - - Assert.Equal("Send Mail", beep.Attribute("name")!.Value); - Assert.NotNull(beep.Element("EofCanary")); - } - - // --- FromXml reload --- - - [Fact] - public void SealedAnchors_AfterDocumentShrink_AllOffsetsWithinBounds() - { - // Regression guard for the "stale renderer survives clip swap" bug - // that crashed AvaloniaEdit with ArgumentOutOfRangeException on - // Document.GetLineByOffset. The architectural fix (detach - // renderers on clip swap) is at the UI layer and needs an Avalonia - // context to test. This covers the defensive side: the editor's - // own SealedAnchors iterator must never yield an anchor whose - // offset lies past the current document length. - var editor = new ScriptClipEditor(ScriptWithSealedHaltScriptXml); - Assert.Single(editor.SealedAnchors); - - // Drastically shrink the document — clears every line the - // original anchor could live on. - editor.Document.Replace(0, editor.Document.TextLength, string.Empty); - - foreach (var anchor in editor.SealedAnchors) - { - Assert.InRange(anchor.Offset, 0, editor.Document.TextLength); - } - } - - [Fact] - public void SealedAnchors_AfterDocumentReplacedWithShorterContent_AllOffsetsWithinBounds() - { - // Same invariant but with content that has steps — proves the - // iterator stays safe when the TextView swaps documents too. - var editor = new ScriptClipEditor(ScriptWithSealedHaltScriptXml); - Assert.Single(editor.SealedAnchors); - - editor.Document.Replace(0, editor.Document.TextLength, "If [ $x > 0 ]"); - - foreach (var anchor in editor.SealedAnchors) - { - Assert.InRange(anchor.Offset, 0, editor.Document.TextLength); - } - } - - [Fact] - public void FromXml_Reload_RebuildsAnchorCacheFreshForNewContent() - { - var editor = new ScriptClipEditor(ScriptWithSealedHaltScriptXml); - Assert.Single(editor.SealedAnchors); - - // Reload with different content — previous anchor should be gone, - // new anchors built for whatever sealed steps the new XML has. - var differentXml = @" - 0]]> - - "; - editor.FromXml(differentXml); - - // No sealed anchors — both If and End If are POCOs. - Assert.Empty(editor.SealedAnchors); - - // Reload with a sealed step back — a fresh anchor appears. - editor.FromXml(ScriptWithSealedHaltScriptXml); - Assert.Single(editor.SealedAnchors); - } -} diff --git a/tests/SharpFM.Tests/Scripting/Serialization/CatalogDisplayRendererTests.cs b/tests/SharpFM.Tests/Scripting/Serialization/CatalogDisplayRendererTests.cs deleted file mode 100644 index 87c63aa..0000000 --- a/tests/SharpFM.Tests/Scripting/Serialization/CatalogDisplayRendererTests.cs +++ /dev/null @@ -1,85 +0,0 @@ -using System.Xml.Linq; -using SharpFM.Model.Scripting; -using SharpFM.Model.Scripting.Serialization; -using Xunit; - -namespace SharpFM.Tests.Scripting.Serialization; - -/// -/// Verifies that the stateless catalog-driven renderer reproduces the -/// display strings the retired StepParamValue pipeline produced. -/// These tests cross-reference existing HandlerTests / ScriptStepTests -/// expectations so unmigrated catalog steps continue rendering identically. -/// -public class CatalogDisplayRendererTests -{ - private static XElement Parse(string xml) => XElement.Parse(xml); - - [Fact] - public void Render_NoParamsStep_ReturnsBareName() - { - var el = Parse(""); - var def = StepCatalogLoader.ByName["Else"]; - - Assert.Equal("Else", CatalogDisplayRenderer.Render(el, def)); - } - - [Fact] - public void Render_IfStep_WithCalculation_BuildsBracketedForm() - { - var el = Parse( - "" - + " 0]]>"); - var def = StepCatalogLoader.ByName["If"]; - - Assert.Equal("If [ $x > 0 ]", CatalogDisplayRenderer.Render(el, def)); - } - - [Fact] - public void Render_EnumParam_ResolvesHrValue() - { - var el = Parse( - "" - + "" - + ""); - var def = StepCatalogLoader.ByName["Go to Record/Request/Page"]; - - var result = CatalogDisplayRenderer.Render(el, def); - // Catalog order renders enum first; boolean Exit is False and - // may render as "Exit after last: Off" depending on catalog metadata. - Assert.Contains("Next", result); - } - - [Fact] - public void Render_CommentStep_FormatsWithHashPrefix() - { - var el = Parse("hello world"); - var def = StepCatalogLoader.ByName["# (comment)"]; - - Assert.Equal("# hello world", CatalogDisplayRenderer.Render(el, def)); - } - - [Fact] - public void Render_EmptyComment_StillReturnsHashPrefix() - { - var el = Parse(""); - var def = StepCatalogLoader.ByName["# (comment)"]; - - Assert.Equal("# ", CatalogDisplayRenderer.Render(el, def)); - } - - [Fact] - public void Render_NamedLayoutRef_RendersQuotedName() - { - // Without a typed POCO, generic rendering only carries the name - // (no id suffix). This is the faithful pre-migration behavior. - var el = Parse( - "" - + "" - + ""); - var def = StepCatalogLoader.ByName["Go to Layout"]; - - var result = CatalogDisplayRenderer.Render(el, def); - Assert.Contains("\"Detail\"", result); - } -} diff --git a/tests/SharpFM.Tests/Scripting/Serialization/CatalogValidatorTests.cs b/tests/SharpFM.Tests/Scripting/Serialization/CatalogValidatorTests.cs deleted file mode 100644 index 393983d..0000000 --- a/tests/SharpFM.Tests/Scripting/Serialization/CatalogValidatorTests.cs +++ /dev/null @@ -1,81 +0,0 @@ -using System.Linq; -using System.Xml.Linq; -using SharpFM.Model.Scripting; -using SharpFM.Model.Scripting.Serialization; -using Xunit; - -namespace SharpFM.Tests.Scripting.Serialization; - -public class CatalogValidatorTests -{ - private static XElement Parse(string xml) => XElement.Parse(xml); - - [Fact] - public void Validate_ValidStep_ReturnsNoDiagnostics() - { - var el = Parse( - "" - + " 0]]>"); - var def = StepCatalogLoader.ByName["If"]; - - var diagnostics = CatalogValidator.Validate(el, def, lineIndex: 0); - - Assert.Empty(diagnostics); - } - - [Fact] - public void Validate_NoParams_ReturnsNoDiagnostics() - { - var el = Parse(""); - var def = StepCatalogLoader.ByName["Else"]; - - var diagnostics = CatalogValidator.Validate(el, def, lineIndex: 0); - - Assert.Empty(diagnostics); - } - - [Fact] - public void Validate_FieldParam_BadValue_EmitsFieldOrVariableWarning() - { - // Set Field's Field param is of type "field" — a bare identifier - // without :: or $ prefix should trip the field-or-variable diagnostic. - var el = Parse( - "" - + "" - + ""); - var def = StepCatalogLoader.ByName["Set Field"]; - - var diagnostics = CatalogValidator.Validate(el, def, lineIndex: 3); - - Assert.Contains(diagnostics, d => - d.Message.Contains("Expected field reference") - && d.Line == 3); - } - - [Fact] - public void Validate_FieldParam_TableQualified_PassesValidation() - { - var el = Parse( - "" - + "" - + ""); - var def = StepCatalogLoader.ByName["Set Field"]; - - var diagnostics = CatalogValidator.Validate(el, def, lineIndex: 0); - - Assert.DoesNotContain(diagnostics, d => d.Message.Contains("Expected field reference")); - } - - [Fact] - public void Validate_MissingOptionalParam_IsSilent() - { - var el = Parse("" - + ""); - var def = StepCatalogLoader.ByName["Go to Layout"]; - - var diagnostics = CatalogValidator.Validate(el, def, lineIndex: 0); - - // OriginalLayout has no Layout or Animation element — both optional. - Assert.Empty(diagnostics); - } -} diff --git a/tests/SharpFM.Tests/Scripting/Serialization/CatalogXmlBuilderTests.cs b/tests/SharpFM.Tests/Scripting/Serialization/CatalogXmlBuilderTests.cs deleted file mode 100644 index 7940d05..0000000 --- a/tests/SharpFM.Tests/Scripting/Serialization/CatalogXmlBuilderTests.cs +++ /dev/null @@ -1,147 +0,0 @@ -using System.Collections.Generic; -using System.Xml.Linq; -using SharpFM.Model.Scripting; -using SharpFM.Model.Scripting.Serialization; -using Xunit; - -namespace SharpFM.Tests.Scripting.Serialization; - -/// -/// Tests the stateless catalog-driven XML builder that replaces the -/// retired StepParamValue construction path. Covers the three -/// public entry points (BuildStep, BuildStepFromMap, -/// UpdateParam) and the major param-type branches. -/// -public class CatalogXmlBuilderTests -{ - // --- BuildStep (from display tokens) --- - - /// - /// Synthetic single-param calculation step — isolates the builder's - /// calculation-emission path from catalog quirks like flag-style - /// boolean prefixes that confuse positional matching. - /// - private static StepDefinition MakeSyntheticCalcStep() => new() - { - Name = "SyntheticCalc", - Id = 9999, - Params = new[] - { - new StepParam { XmlElement = "Calculation", Type = "calculation", Required = true } - } - }; - - [Fact] - public void BuildStep_CalculationParam_WrapsExpressionInCdata() - { - var def = MakeSyntheticCalcStep(); - var xml = CatalogXmlBuilder.BuildStep(def, enabled: true, ["$x > 0"]); - - Assert.Equal("True", xml.Attribute("enable")!.Value); - Assert.Equal("9999", xml.Attribute("id")!.Value); - Assert.Equal("SyntheticCalc", xml.Attribute("name")!.Value); - Assert.Contains("$x > 0", xml.Element("Calculation")!.Value); - } - - [Fact] - public void BuildStep_DisabledFlag_EmitsEnableFalse() - { - var def = StepCatalogLoader.ByName["Else"]; - var xml = CatalogXmlBuilder.BuildStep(def, enabled: false, []); - - Assert.Equal("False", xml.Attribute("enable")!.Value); - } - - [Fact] - public void BuildStep_SetField_FromDisplayTokens_ReordersByCatalog() - { - // Catalog order is [Calculation, Field]. Display provides tokens - // positionally — the first positional match binds to Calculation, - // the second to Field. - var def = StepCatalogLoader.ByName["Set Field"]; - var xml = CatalogXmlBuilder.BuildStep(def, enabled: true, ["People::FirstName", "$count + 1"]); - - Assert.NotNull(xml.Element("Calculation")); - Assert.NotNull(xml.Element("Field")); - } - - [Fact] - public void BuildStep_NamedRefParam_EmitsNameWithoutHardcodedId() - { - // Pre-refactor, the old BuildNamedRefXml hardcoded id="0". - // The new generic path emits ONLY the name attribute — ids come - // from typed POCOs that carry them explicitly. - var def = StepCatalogLoader.ByName["Go to Layout"]; - var xml = CatalogXmlBuilder.BuildStep(def, enabled: true, ["SelectedLayout", "\"Detail\""]); - - var layout = xml.Element("Layout"); - Assert.NotNull(layout); - Assert.Equal("Detail", layout!.Attribute("name")!.Value); - Assert.Null(layout.Attribute("id")); // id is not invented anymore - } - - // --- BuildStepFromMap (from param-name dict) --- - - [Fact] - public void BuildStepFromMap_IfStep_ReadsCalculationByParamName() - { - var def = StepCatalogLoader.ByName["If"]; - var map = new Dictionary { ["Calculation"] = "Get(FoundCount) > 0" }; - - var xml = CatalogXmlBuilder.BuildStepFromMap(def, enabled: true, map); - - Assert.Contains("Get(FoundCount) > 0", xml.Element("Calculation")!.Value); - } - - [Fact] - public void BuildStepFromMap_NullMap_StillEmitsStructuralElement() - { - var def = StepCatalogLoader.ByName["Else"]; - var xml = CatalogXmlBuilder.BuildStepFromMap(def, enabled: true, null); - - Assert.Equal("Else", xml.Attribute("name")!.Value); - } - - // --- UpdateParam --- - - [Fact] - public void UpdateParam_IfCalculation_ReplacesExpression() - { - var original = XElement.Parse( - "" - + " 0]]>"); - var def = StepCatalogLoader.ByName["If"]; - - var updated = CatalogXmlBuilder.UpdateParam(original, def, "Calculation", "$x >= 100"); - - Assert.NotNull(updated); - Assert.Contains("$x >= 100", updated!.Element("Calculation")!.Value); - Assert.DoesNotContain("$x > 0", updated.Element("Calculation")!.Value); - } - - [Fact] - public void UpdateParam_UnknownParamName_ReturnsNull() - { - var original = XElement.Parse( - "" - + " 0]]>"); - var def = StepCatalogLoader.ByName["If"]; - - var updated = CatalogXmlBuilder.UpdateParam(original, def, "NonExistent", "value"); - - Assert.Null(updated); - } - - [Fact] - public void UpdateParam_LeavesOriginalUntouched() - { - var original = XElement.Parse( - "" - + " 0]]>"); - var def = StepCatalogLoader.ByName["If"]; - - CatalogXmlBuilder.UpdateParam(original, def, "Calculation", "different"); - - Assert.Contains("$x > 0", original.Element("Calculation")!.Value); - } -} diff --git a/tests/SharpFM.Tests/Scripting/Serialization/FieldParamCatalogRoundTripTests.cs b/tests/SharpFM.Tests/Scripting/Serialization/FieldParamCatalogRoundTripTests.cs deleted file mode 100644 index edb692f..0000000 --- a/tests/SharpFM.Tests/Scripting/Serialization/FieldParamCatalogRoundTripTests.cs +++ /dev/null @@ -1,117 +0,0 @@ -using System.Linq; -using System.Xml.Linq; -using SharpFM.Model.Scripting; -using SharpFM.Model.Scripting.Serialization; -using Xunit; - -namespace SharpFM.Tests.Scripting.Serialization; - -/// -/// Coverage for the field-param refactor on the catalog (RawStep) side: -/// CatalogParamExtractor.ExtractField, CatalogXmlBuilder.BuildFieldXml, -/// and CatalogValidator's tolerance for the lossless (#id) -/// suffix. The 47-ish catalog-path field params rely on these helpers -/// for id preservation across display-text editing. -/// -public class FieldParamCatalogRoundTripTests -{ - private static XElement MakeStep(string xml) => XElement.Parse(xml); - - // "Check Selection" (id=18) is a catalog-known, non-POCO step with - // SelectAll (boolean) + Field params. Both elements are present so the - // extract-then-rebuild round-trip has tokens for each catalog slot. - private const string CheckSelectionXml = - "" - + "" - + ""; - - [Fact] - public void Extract_FieldParam_IncludesIdSuffix() - { - var el = MakeStep(CheckSelectionXml); - var def = StepCatalogLoader.ByName["Check Selection"]; - - // The field param is the first (and only) param. - var fieldParam = def.Params.First(p => p.Type == "field" || p.Type == "fieldOrVariable"); - var extracted = CatalogParamExtractor.Extract(el, fieldParam); - - Assert.Equal("People::Email (#7)", extracted); - } - - [Fact] - public void Build_FieldParam_ParsesIdSuffixFromDisplayToken() - { - var def = StepCatalogLoader.ByName["Check Selection"]; - - // Check Selection has two params — SelectAll (boolean, label "Select") - // and Field (no label). Pass both tokens so positional matching - // assigns the field token to the Field slot. - var step = CatalogXmlBuilder.BuildStep(def, enabled: true, - hrParams: new[] { "On", "People::Email (#7)" }); - - var field = step.Element("Field"); - Assert.NotNull(field); - Assert.Equal("People", field!.Attribute("table")!.Value); - Assert.Equal("7", field.Attribute("id")!.Value); - Assert.Equal("Email", field.Attribute("name")!.Value); - } - - [Fact] - public void Build_FieldParam_NoIdSuffix_YieldsIdZero() - { - // Back-compat: tokens without (#id) still work (id=0 sentinel). - var def = StepCatalogLoader.ByName["Check Selection"]; - var step = CatalogXmlBuilder.BuildStep(def, enabled: true, - hrParams: new[] { "On", "People::Email" }); - - var field = step.Element("Field"); - Assert.Equal("People", field!.Attribute("table")!.Value); - Assert.Equal("0", field.Attribute("id")!.Value); - } - - [Fact] - public void FullRoundTrip_FieldParam_PreservesIdAttribute() - { - // Extract BOTH params from source display-side, rebuild from - // those tokens, verify Field attributes survive byte-for-byte. - var source = MakeStep(CheckSelectionXml); - var def = StepCatalogLoader.ByName["Check Selection"]; - - var tokens = def.Params - .Select(p => - { - var extracted = CatalogParamExtractor.Extract(source, p); - // Label-prefixed tokens mimic the display format - // MatchDisplayParams expects for labelled params. - return p.HrLabel != null && extracted != null - ? $"{p.HrLabel}: {extracted}" - : extracted; - }) - .Where(t => t != null) - .Select(t => t!) - .ToArray(); - - var rebuilt = CatalogXmlBuilder.BuildStep(def, enabled: true, hrParams: tokens); - var originalField = source.Element("Field")!; - var rebuiltField = rebuilt.Element("Field")!; - - Assert.Equal(originalField.Attribute("table")!.Value, rebuiltField.Attribute("table")!.Value); - Assert.Equal(originalField.Attribute("id")!.Value, rebuiltField.Attribute("id")!.Value); - Assert.Equal(originalField.Attribute("name")!.Value, rebuiltField.Attribute("name")!.Value); - } - - [Fact] - public void Validate_FieldParam_WithIdSuffix_ProducesNoWarning() - { - // The validator's field-shape check must tolerate the (#id) suffix. - // Without the refactor, "People::Email (#7)" failed the shape check - // because it didn't contain literal "::" on its own (after the suffix). - var el = MakeStep(CheckSelectionXml); - var def = StepCatalogLoader.ByName["Check Selection"]; - - var diagnostics = CatalogValidator.Validate(el, def, lineIndex: 0); - - Assert.DoesNotContain(diagnostics, d => - d.Message.Contains("Expected field reference")); - } -} diff --git a/tests/SharpFM.Tests/Scripting/StepCatalogLoaderTests.cs b/tests/SharpFM.Tests/Scripting/StepCatalogLoaderTests.cs deleted file mode 100644 index 196459a..0000000 --- a/tests/SharpFM.Tests/Scripting/StepCatalogLoaderTests.cs +++ /dev/null @@ -1,86 +0,0 @@ -using System.Linq; -using SharpFM.Scripting; -using Xunit; - -namespace SharpFM.Tests.ScriptConverter; - -public class StepCatalogLoaderTests -{ - [Fact] - public void LoadsCatalog_HasExpectedStepCount() - { - Assert.True(StepCatalogLoader.All.Count > 200); - } - - [Fact] - public void LookupById_Comment_ReturnsCorrectDefinition() - { - var step = StepCatalogLoader.ById[89]; - Assert.Equal("# (comment)", step.Name); - } - - [Fact] - public void LookupById_If_ReturnsCorrectDefinition() - { - var step = StepCatalogLoader.ById[68]; - Assert.Equal("If", step.Name); - Assert.NotNull(step.BlockPair); - Assert.Equal(BlockPairRole.Open, step.BlockPair!.Role); - } - - [Fact] - public void LookupById_SetVariable_HasCorrectParams() - { - var step = StepCatalogLoader.ById[141]; - Assert.Equal("Set Variable", step.Name); - var paramNames = step.Params.Select(p => p.XmlElement).ToArray(); - Assert.Contains("Name", paramNames); - // Value and Repetition are wrapper elements, xmlElement is "Calculation" - var wrappers = step.Params.Select(p => p.WrapperElement).ToArray(); - Assert.Contains("Value", wrappers); - Assert.Contains("Repetition", wrappers); - } - - [Fact] - public void LookupByName_CaseInsensitive() - { - Assert.True(StepCatalogLoader.ByName.ContainsKey("set variable")); - Assert.True(StepCatalogLoader.ByName.ContainsKey("SET VARIABLE")); - Assert.Equal(141, StepCatalogLoader.ByName["set variable"].Id); - } - - [Fact] - public void AllEntries_WithIds_HaveValidIds() - { - foreach (var step in StepCatalogLoader.All.Where(s => s.Id.HasValue)) - { - Assert.True(step.Id > 0, $"Step '{step.Name}' has invalid id {step.Id}"); - } - } - - [Fact] - public void AllEntries_HaveNoDuplicateIds() - { - var ids = StepCatalogLoader.All.Where(s => s.Id.HasValue).Select(s => s.Id!.Value).ToList(); - var distinct = ids.Distinct().ToList(); - Assert.Equal(distinct.Count, ids.Count); - } - - [Fact] - public void BlockPairSteps_HaveMatchingPartners() - { - var openSteps = StepCatalogLoader.All - .Where(s => s.BlockPair?.Role == BlockPairRole.Open) - .ToList(); - - foreach (var step in openSteps) - { - foreach (var partner in step.BlockPair!.Partners) - { - Assert.True( - StepCatalogLoader.ByName.ContainsKey(partner), - $"Step '{step.Name}' has partner '{partner}' that doesn't exist in catalog"); - } - } - } -} diff --git a/tests/SharpFM.Tests/Scripting/StepCatalogTests.cs b/tests/SharpFM.Tests/Scripting/StepCatalogTests.cs deleted file mode 100644 index 8a8e6ee..0000000 --- a/tests/SharpFM.Tests/Scripting/StepCatalogTests.cs +++ /dev/null @@ -1,150 +0,0 @@ -using System.Text.Json; -using Xunit; - -namespace SharpFM.Tests.Scripting; - -public class StepCatalogTests -{ - // --- BlockPairRole serialization (converter is on StepBlockPair.Role) --- - - [Theory] - [InlineData("{\"role\":\"open\",\"partners\":[]}", BlockPairRole.Open)] - [InlineData("{\"role\":\"middle\",\"partners\":[]}", BlockPairRole.Middle)] - [InlineData("{\"role\":\"close\",\"partners\":[]}", BlockPairRole.Close)] - public void BlockPairRole_Deserializes(string json, BlockPairRole expected) - { - var pair = JsonSerializer.Deserialize(json); - Assert.Equal(expected, pair!.Role); - } - - [Theory] - [InlineData(BlockPairRole.Open, "open")] - [InlineData(BlockPairRole.Middle, "middle")] - [InlineData(BlockPairRole.Close, "close")] - public void BlockPairRole_Serializes(BlockPairRole value, string expected) - { - var pair = new StepBlockPair { Role = value, Partners = [] }; - var json = JsonSerializer.Serialize(pair); - Assert.Contains($"\"role\":\"{expected}\"", json); - } - - [Fact] - public void BlockPairRole_Deserialize_Unknown_Throws() - { - Assert.Throws(() => - JsonSerializer.Deserialize("{\"role\":\"unknown\",\"partners\":[]}")); - } - - // --- StepDefinition record --- - - [Fact] - public void StepDefinition_Deserializes_FromJson() - { - var json = """ - { - "name": "Set Variable", - "id": 141, - "category": "Control", - "selfClosing": true, - "params": [ - { "xmlElement": "Name", "type": "text" } - ] - } - """; - - var def = JsonSerializer.Deserialize(json); - Assert.NotNull(def); - Assert.Equal("Set Variable", def!.Name); - Assert.Equal(141, def.Id); - Assert.Equal("Control", def.Category); - Assert.True(def.SelfClosing); - Assert.Single(def.Params); - Assert.Equal("Name", def.Params[0].XmlElement); - } - - [Fact] - public void StepDefinition_Defaults_AreCorrect() - { - var def = new StepDefinition(); - Assert.Equal("", def.Name); - Assert.Null(def.Id); - Assert.Equal("", def.Category); - Assert.False(def.SelfClosing); - Assert.Empty(def.Params); - Assert.Null(def.BlockPair); - } - - // --- StepParam record --- - - [Fact] - public void StepParam_Deserializes_WithOptionalFields() - { - var json = """ - { - "xmlElement": "Calculation", - "type": "namedCalc", - "hrLabel": "Value", - "wrapperElement": "Value", - "required": true, - "invertedHr": true - } - """; - - var param = JsonSerializer.Deserialize(json); - Assert.NotNull(param); - Assert.Equal("Calculation", param!.XmlElement); - Assert.Equal("namedCalc", param.Type); - Assert.Equal("Value", param.HrLabel); - Assert.Equal("Value", param.WrapperElement); - Assert.True(param.Required); - Assert.True(param.InvertedHr); - } - - [Fact] - public void StepParam_Defaults_AreCorrect() - { - var param = new StepParam(); - Assert.Equal("", param.XmlElement); - Assert.Equal("", param.Type); - Assert.Null(param.HrLabel); - Assert.Null(param.XmlAttr); - Assert.Null(param.WrapperElement); - Assert.Null(param.ParentElement); - Assert.False(param.Required); - Assert.False(param.InvertedHr); - } - - // --- StepBlockPair record --- - - [Fact] - public void StepBlockPair_Deserializes() - { - var json = """{"role": "open", "partners": ["Else", "Else If", "End If"]}"""; - - var pair = JsonSerializer.Deserialize(json); - Assert.NotNull(pair); - Assert.Equal(BlockPairRole.Open, pair!.Role); - Assert.Equal(3, pair.Partners.Length); - Assert.Contains("Else", pair.Partners); - Assert.Contains("End If", pair.Partners); - } - - // --- StepParam with hrEnumValues --- - - [Fact] - public void StepParam_HrEnumValues_Deserializes() - { - var json = """ - { - "xmlElement": "RowPageLocation", - "type": "enum", - "hrEnumValues": { "1": "First", "2": "Last", "3": "Previous", "4": "Next" } - } - """; - - var param = JsonSerializer.Deserialize(json); - Assert.NotNull(param!.HrEnumValues); - Assert.Equal("First", param.HrEnumValues!["1"]); - Assert.Equal(4, param.HrEnumValues.Count); - } -} diff --git a/tests/SharpFM.Tests/Scripting/Steps/RawStepAllowListTests.cs b/tests/SharpFM.Tests/Scripting/Steps/RawStepAllowListTests.cs index f4f7ef5..6fd3752 100644 --- a/tests/SharpFM.Tests/Scripting/Steps/RawStepAllowListTests.cs +++ b/tests/SharpFM.Tests/Scripting/Steps/RawStepAllowListTests.cs @@ -47,19 +47,6 @@ public void TypedPocoStep_SetField_IsFullyEditable_ReturnsTrue() Assert.True(step.IsFullyEditable); } - [Fact] - public void RawStep_NotInAllowList_IsFullyEditable_ReturnsFalse() - { - // "Send Mail" is a deep Tier D step (30 params) unlikely to - // migrate soon — a stable sealed-step canary that stays in the - // RawStep path through the rest of the sweep. - var xml = XElement.Parse(""); - var step = ScriptStep.FromXml(xml); - - Assert.IsType(step); - Assert.False(step.IsFullyEditable); - } - [Fact] public void RawStep_UnknownStepName_IsFullyEditable_ReturnsFalse() { From d67e5f2fe9737dc9164302b06b167c118f8e2fa8 Mon Sep 17 00:00:00 2001 From: Nate Bross Date: Mon, 20 Apr 2026 22:14:47 -0500 Subject: [PATCH 10/38] feat(scripting): migrate Tier C text-bearing steps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Seven steps introducing free-form text params alongside booleans/enums: - Delete File — single text param, labeled. - Create Data File — boolean + unlabeled text (path). - Set Dictionary — enum + labeled text. - Insert Audio/Video — text + type attribute on the same UniversalPathList element (the catalog describes them as two separate param entries but they serialize to one XML element). - Recover File — boolean + unlabeled text. - Save a Copy as — five params including a flagBoolean pair and an Output path text. - Save Records as Snapshot Link — four params in similar shape. Text params render as raw token values (no quoting) and parse as the unlabeled residue of the display tokens. Tier B generator extended with text support; irregular cases hand-tuned. --- .../Scripting/Steps/CreateDataFileStep.cs | 86 ++++++++++ .../Scripting/Steps/DeleteFileStep.cs | 67 ++++++++ .../Scripting/Steps/InsertAudioVideoStep.cs | 88 ++++++++++ .../Scripting/Steps/RecoverFileStep.cs | 84 +++++++++ .../Scripting/Steps/SaveACopyAsStep.cs | 160 ++++++++++++++++++ .../Steps/SaveRecordsAsSnapshotLinkStep.cs | 139 +++++++++++++++ .../Scripting/Steps/SetDictionaryStep.cs | 84 +++++++++ .../Steps/CreateDataFileStepTests.cs | 41 +++++ .../Scripting/Steps/DeleteFileStepTests.cs | 41 +++++ .../Steps/InsertAudioVideoStepTests.cs | 50 ++++++ .../Scripting/Steps/RecoverFileStepTests.cs | 41 +++++ .../Scripting/Steps/SaveACopyAsStepTests.cs | 41 +++++ .../SaveRecordsAsSnapshotLinkStepTests.cs | 41 +++++ .../Scripting/Steps/SetDictionaryStepTests.cs | 41 +++++ 14 files changed, 1004 insertions(+) create mode 100644 src/SharpFM.Model/Scripting/Steps/CreateDataFileStep.cs create mode 100644 src/SharpFM.Model/Scripting/Steps/DeleteFileStep.cs create mode 100644 src/SharpFM.Model/Scripting/Steps/InsertAudioVideoStep.cs create mode 100644 src/SharpFM.Model/Scripting/Steps/RecoverFileStep.cs create mode 100644 src/SharpFM.Model/Scripting/Steps/SaveACopyAsStep.cs create mode 100644 src/SharpFM.Model/Scripting/Steps/SaveRecordsAsSnapshotLinkStep.cs create mode 100644 src/SharpFM.Model/Scripting/Steps/SetDictionaryStep.cs create mode 100644 tests/SharpFM.Tests/Scripting/Steps/CreateDataFileStepTests.cs create mode 100644 tests/SharpFM.Tests/Scripting/Steps/DeleteFileStepTests.cs create mode 100644 tests/SharpFM.Tests/Scripting/Steps/InsertAudioVideoStepTests.cs create mode 100644 tests/SharpFM.Tests/Scripting/Steps/RecoverFileStepTests.cs create mode 100644 tests/SharpFM.Tests/Scripting/Steps/SaveACopyAsStepTests.cs create mode 100644 tests/SharpFM.Tests/Scripting/Steps/SaveRecordsAsSnapshotLinkStepTests.cs create mode 100644 tests/SharpFM.Tests/Scripting/Steps/SetDictionaryStepTests.cs diff --git a/src/SharpFM.Model/Scripting/Steps/CreateDataFileStep.cs b/src/SharpFM.Model/Scripting/Steps/CreateDataFileStep.cs new file mode 100644 index 0000000..38ecddf --- /dev/null +++ b/src/SharpFM.Model/Scripting/Steps/CreateDataFileStep.cs @@ -0,0 +1,86 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; +using SharpFM.Model.Scripting.Registry; + +namespace SharpFM.Model.Scripting.Steps; + +public sealed class CreateDataFileStep : ScriptStep, IStepFactory +{ + public const int XmlId = 190; + public const string XmlName = "Create Data File"; + + public string UniversalPathList { get; set; } + public bool CreateFolders { get; set; } + + public CreateDataFileStep( + string universalPathList = "", + bool createFolders = true, + bool enabled = true) + : base(null, enabled) + { + UniversalPathList = universalPathList; + CreateFolders = createFolders; + } + + public override XElement ToXml() => + new("Step", + new XAttribute("enable", Enabled ? "True" : "False"), + new XAttribute("id", XmlId), + new XAttribute("name", XmlName), + new XElement("UniversalPathList", UniversalPathList), + new XElement("CreateDirectories", new XAttribute("state", CreateFolders ? "True" : "False"))); + + public override string ToDisplayLine() => + "Create Data File [ " + UniversalPathList + " ; " + "Create folders: " + (CreateFolders ? "On" : "Off") + " ]"; + + public static new ScriptStep FromXml(XElement step) + { + var enabled = step.Attribute("enable")?.Value != "False"; + var universalPathList_v = step.Element("UniversalPathList")?.Value ?? ""; + var createFolders_v = step.Element("CreateDirectories")?.Attribute("state")?.Value == "True"; + return new CreateDataFileStep(universalPathList_v, createFolders_v, enabled); + } + + public static ScriptStep FromDisplayParams(bool enabled, string[] hrParams) + { + var tokens = hrParams.Select(h => h.Trim()).ToArray(); + string universalPathList_v = ""; + bool createFolders_v = true; + foreach (var tok in tokens) { if (tok.StartsWith("Create folders:", StringComparison.OrdinalIgnoreCase)) { var v = tok.Substring(15).Trim(); createFolders_v = v.Equals("On", StringComparison.OrdinalIgnoreCase); break; } } + // Unlabeled text param — pick the token that doesn't match known labels. + universalPathList_v = tokens.FirstOrDefault(t => + !t.StartsWith("Create folders:", StringComparison.OrdinalIgnoreCase)) ?? ""; + return new CreateDataFileStep(universalPathList_v, createFolders_v, enabled); + } + + public static StepMetadata Metadata { get; } = new() + { + Name = XmlName, + Id = XmlId, + Category = "files", + HelpUrl = "https://help.claris.com/en/pro-help/content/create-data-file.html", + Params = + [ + new ParamMetadata + { + Name = "UniversalPathList", + XmlElement = "UniversalPathList", + Type = "text", + }, + new ParamMetadata + { + Name = "CreateDirectories", + XmlElement = "CreateDirectories", + Type = "boolean", + XmlAttr = "state", + HrLabel = "Create folders", + ValidValues = ["On", "Off"], + DefaultValue = "True", + }, + ], + FromXml = FromXml, + FromDisplay = FromDisplayParams, + }; +} diff --git a/src/SharpFM.Model/Scripting/Steps/DeleteFileStep.cs b/src/SharpFM.Model/Scripting/Steps/DeleteFileStep.cs new file mode 100644 index 0000000..7041581 --- /dev/null +++ b/src/SharpFM.Model/Scripting/Steps/DeleteFileStep.cs @@ -0,0 +1,67 @@ +using System; +using System.Linq; +using System.Xml.Linq; +using SharpFM.Model.Scripting.Registry; + +namespace SharpFM.Model.Scripting.Steps; + +/// +/// Zero-loss audit for DeleteFileStep: three <Step> attributes +/// plus a single <UniversalPathList> element carrying the target +/// file path as text. Display form uses the HR-label prefix +/// "Target file:" followed by the raw path string. +/// +public sealed class DeleteFileStep : ScriptStep, IStepFactory +{ + public const int XmlId = 197; + public const string XmlName = "Delete File"; + + public string TargetFile { get; set; } + + public DeleteFileStep(string targetFile = "", bool enabled = true) : base(null, enabled) + { + TargetFile = targetFile; + } + + public override XElement ToXml() => + new("Step", + new XAttribute("enable", Enabled ? "True" : "False"), + new XAttribute("id", XmlId), + new XAttribute("name", XmlName), + new XElement("UniversalPathList", TargetFile)); + + public override string ToDisplayLine() => + $"Delete File [ Target file: {TargetFile} ]"; + + public static new ScriptStep FromXml(XElement step) + { + var enabled = step.Attribute("enable")?.Value != "False"; + var path = step.Element("UniversalPathList")?.Value ?? ""; + return new DeleteFileStep(path, enabled); + } + + public static ScriptStep FromDisplayParams(bool enabled, string[] hrParams) + { + var tok = hrParams.Length > 0 ? hrParams[0].Trim() : ""; + const string Prefix = "Target file:"; + if (tok.StartsWith(Prefix, StringComparison.OrdinalIgnoreCase)) + tok = tok.Substring(Prefix.Length).Trim(); + return new DeleteFileStep(tok, enabled); + } + + public static StepMetadata Metadata { get; } = new() + { + Name = XmlName, Id = XmlId, Category = "files", + HelpUrl = "https://help.claris.com/en/pro-help/content/delete-file.html", + Params = + [ + new ParamMetadata + { + Name = "UniversalPathList", XmlElement = "UniversalPathList", + Type = "text", HrLabel = "Target file", Required = true, + }, + ], + FromXml = FromXml, + FromDisplay = FromDisplayParams, + }; +} diff --git a/src/SharpFM.Model/Scripting/Steps/InsertAudioVideoStep.cs b/src/SharpFM.Model/Scripting/Steps/InsertAudioVideoStep.cs new file mode 100644 index 0000000..ecdcd80 --- /dev/null +++ b/src/SharpFM.Model/Scripting/Steps/InsertAudioVideoStep.cs @@ -0,0 +1,88 @@ +using System; +using System.Linq; +using System.Xml.Linq; +using SharpFM.Model.Scripting.Registry; + +namespace SharpFM.Model.Scripting.Steps; + +/// +/// Zero-loss audit for InsertAudioVideoStep: three <Step> +/// attributes plus a single <UniversalPathList type="..."> element +/// that carries BOTH the file path as text content AND a "type" +/// attribute marking the payload as embedded or a reference. +/// +public sealed class InsertAudioVideoStep : ScriptStep, IStepFactory +{ + public const int XmlId = 159; + public const string XmlName = "Insert Audio/Video"; + + public string Path { get; set; } + public string Reference { get; set; } + + public InsertAudioVideoStep( + string path = "", + string reference = "Embedded", + bool enabled = true) + : base(null, enabled) + { + Path = path; + Reference = reference; + } + + public override XElement ToXml() => + new("Step", + new XAttribute("enable", Enabled ? "True" : "False"), + new XAttribute("id", XmlId), + new XAttribute("name", XmlName), + new XElement("UniversalPathList", new XAttribute("type", Reference), Path)); + + public override string ToDisplayLine() => + $"Insert Audio/Video [ {Path} ; Reference: {Reference} ]"; + + public static new ScriptStep FromXml(XElement step) + { + var enabled = step.Attribute("enable")?.Value != "False"; + var path = step.Element("UniversalPathList")?.Value ?? ""; + var type = step.Element("UniversalPathList")?.Attribute("type")?.Value ?? "Embedded"; + return new InsertAudioVideoStep(path, type, enabled); + } + + public static ScriptStep FromDisplayParams(bool enabled, string[] hrParams) + { + var tokens = hrParams.Select(h => h.Trim()).ToArray(); + string reference = "Embedded"; + foreach (var tok in tokens) + { + if (tok.StartsWith("Reference:", StringComparison.OrdinalIgnoreCase)) + { + reference = tok.Substring("Reference:".Length).Trim(); + break; + } + } + var path = tokens.FirstOrDefault(t => + !t.StartsWith("Reference:", StringComparison.OrdinalIgnoreCase)) ?? ""; + return new InsertAudioVideoStep(path, reference, enabled); + } + + public static StepMetadata Metadata { get; } = new() + { + Name = XmlName, Id = XmlId, Category = "fields", + HelpUrl = "https://help.claris.com/en/pro-help/content/insert-audio-video.html", + Params = + [ + new ParamMetadata + { + Name = "UniversalPathList", XmlElement = "UniversalPathList", + Type = "text", + }, + new ParamMetadata + { + Name = "Reference", XmlElement = "UniversalPathList", Type = "enum", + XmlAttr = "type", HrLabel = "Reference", + ValidValues = ["Embedded", "Reference"], DefaultValue = "Embedded", + }, + ], + FromXml = FromXml, + FromDisplay = FromDisplayParams, + }; +} diff --git a/src/SharpFM.Model/Scripting/Steps/RecoverFileStep.cs b/src/SharpFM.Model/Scripting/Steps/RecoverFileStep.cs new file mode 100644 index 0000000..b2e7fbc --- /dev/null +++ b/src/SharpFM.Model/Scripting/Steps/RecoverFileStep.cs @@ -0,0 +1,84 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; +using SharpFM.Model.Scripting.Registry; + +namespace SharpFM.Model.Scripting.Steps; + +public sealed class RecoverFileStep : ScriptStep, IStepFactory +{ + public const int XmlId = 95; + public const string XmlName = "Recover File"; + + public bool WithDialog { get; set; } + public string UniversalPathList { get; set; } + + public RecoverFileStep( + bool withDialog = true, + string universalPathList = "", + bool enabled = true) + : base(null, enabled) + { + WithDialog = withDialog; + UniversalPathList = universalPathList; + } + + public override XElement ToXml() => + new("Step", + new XAttribute("enable", Enabled ? "True" : "False"), + new XAttribute("id", XmlId), + new XAttribute("name", XmlName), + new XElement("NoInteract", new XAttribute("state", WithDialog ? "True" : "False")), + new XElement("UniversalPathList", UniversalPathList)); + + public override string ToDisplayLine() => + "Recover File [ " + "With dialog: " + (WithDialog ? "On" : "Off") + " ; " + UniversalPathList + " ]"; + + public static new ScriptStep FromXml(XElement step) + { + var enabled = step.Attribute("enable")?.Value != "False"; + var withDialog_v = step.Element("NoInteract")?.Attribute("state")?.Value == "True"; + var universalPathList_v = step.Element("UniversalPathList")?.Value ?? ""; + return new RecoverFileStep(withDialog_v, universalPathList_v, enabled); + } + + public static ScriptStep FromDisplayParams(bool enabled, string[] hrParams) + { + var tokens = hrParams.Select(h => h.Trim()).ToArray(); + bool withDialog_v = true; + foreach (var tok in tokens) { if (tok.StartsWith("With dialog:", StringComparison.OrdinalIgnoreCase)) { var v = tok.Substring(12).Trim(); withDialog_v = v.Equals("On", StringComparison.OrdinalIgnoreCase); break; } } + var universalPathList_v = tokens.FirstOrDefault(t => + !t.StartsWith("With dialog:", StringComparison.OrdinalIgnoreCase)) ?? ""; + return new RecoverFileStep(withDialog_v, universalPathList_v, enabled); + } + + public static StepMetadata Metadata { get; } = new() + { + Name = XmlName, + Id = XmlId, + Category = "files", + HelpUrl = "https://help.claris.com/en/pro-help/content/recover-file.html", + Params = + [ + new ParamMetadata + { + Name = "NoInteract", + XmlElement = "NoInteract", + Type = "boolean", + XmlAttr = "state", + HrLabel = "With dialog", + ValidValues = ["On", "Off"], + DefaultValue = "True", + }, + new ParamMetadata + { + Name = "UniversalPathList", + XmlElement = "UniversalPathList", + Type = "text", + }, + ], + FromXml = FromXml, + FromDisplay = FromDisplayParams, + }; +} diff --git a/src/SharpFM.Model/Scripting/Steps/SaveACopyAsStep.cs b/src/SharpFM.Model/Scripting/Steps/SaveACopyAsStep.cs new file mode 100644 index 0000000..c8d3b91 --- /dev/null +++ b/src/SharpFM.Model/Scripting/Steps/SaveACopyAsStep.cs @@ -0,0 +1,160 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; +using SharpFM.Model.Scripting.Registry; + +namespace SharpFM.Model.Scripting.Steps; + +public sealed class SaveACopyAsStep : ScriptStep, IStepFactory +{ + public const int XmlId = 37; + public const string XmlName = "Save a Copy as"; + + public bool CreateFolders { get; set; } + public bool AutomaticallyOpen { get; set; } + public bool CreateEmail { get; set; } + public string CopyType { get; set; } + public string OutputPath { get; set; } + + public SaveACopyAsStep( + bool createFolders = true, + bool automaticallyOpen = false, + bool createEmail = false, + string copyType = "Copy", + string outputPath = "", + bool enabled = true) + : base(null, enabled) + { + CreateFolders = createFolders; + AutomaticallyOpen = automaticallyOpen; + CreateEmail = createEmail; + CopyType = copyType; + OutputPath = outputPath; + } + + private static readonly IReadOnlyDictionary _CopyTypeToHr = + new Dictionary(StringComparer.Ordinal) + { + ["Copy"] = "copy of current file", + ["CompactedCopy"] = "compacted copy (smaller)", + ["Clone"] = "clone (no records)", + ["SelfContainedCopy"] = "self-contained copy (single file)", + }; + + private static readonly IReadOnlyDictionary _CopyTypeFromHr = + new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["copy of current file"] = "Copy", + ["compacted copy (smaller)"] = "CompactedCopy", + ["clone (no records)"] = "Clone", + ["self-contained copy (single file)"] = "SelfContainedCopy", + }; + + private static string CopyTypeHr(string x) => + _CopyTypeToHr.TryGetValue(x, out var h) ? h : x; + + private static string CopyTypeXml(string h) => + _CopyTypeFromHr.TryGetValue(h, out var x) ? x : h; + + public override XElement ToXml() => + new("Step", + new XAttribute("enable", Enabled ? "True" : "False"), + new XAttribute("id", XmlId), + new XAttribute("name", XmlName), + new XElement("CreateDirectories", new XAttribute("state", CreateFolders ? "True" : "False")), + new XElement("AutoOpen", new XAttribute("state", AutomaticallyOpen ? "True" : "False")), + new XElement("CreateEmail", new XAttribute("state", CreateEmail ? "True" : "False")), + new XElement("SaveAsType", new XAttribute("value", CopyType)), + new XElement("UniversalPathList", OutputPath)); + + public override string ToDisplayLine() => + "Save a Copy as [ " + "Create folders: " + (CreateFolders ? "On" : "Off") + " ; " + "Automatically open: " + (AutomaticallyOpen ? "On" : "Off") + " ; " + "Create email: " + (CreateEmail ? "On" : "Off") + " ; " + "Copy type: " + CopyTypeHr(CopyType) + " ; " + "Output path: " + OutputPath + " ]"; + + public static new ScriptStep FromXml(XElement step) + { + var enabled = step.Attribute("enable")?.Value != "False"; + var createFolders_v = step.Element("CreateDirectories")?.Attribute("state")?.Value == "True"; + var automaticallyOpen_v = step.Element("AutoOpen")?.Attribute("state")?.Value == "True"; + var createEmail_v = step.Element("CreateEmail")?.Attribute("state")?.Value == "True"; + var copyType_v = step.Element("SaveAsType")?.Attribute("value")?.Value ?? "Copy"; + var outputPath_v = step.Element("UniversalPathList")?.Value ?? ""; + return new SaveACopyAsStep(createFolders_v, automaticallyOpen_v, createEmail_v, copyType_v, outputPath_v, enabled); + } + + public static ScriptStep FromDisplayParams(bool enabled, string[] hrParams) + { + var tokens = hrParams.Select(h => h.Trim()).ToArray(); + bool createFolders_v = true; + foreach (var tok in tokens) { if (tok.StartsWith("Create folders:", StringComparison.OrdinalIgnoreCase)) { var v = tok.Substring(15).Trim(); createFolders_v = v.Equals("On", StringComparison.OrdinalIgnoreCase); break; } } + bool automaticallyOpen_v = false; + foreach (var tok in tokens) { if (tok.StartsWith("Automatically open:", StringComparison.OrdinalIgnoreCase)) { var v = tok.Substring(19).Trim(); automaticallyOpen_v = v.Equals("On", StringComparison.OrdinalIgnoreCase); break; } } + bool createEmail_v = false; + foreach (var tok in tokens) { if (tok.StartsWith("Create email:", StringComparison.OrdinalIgnoreCase)) { var v = tok.Substring(13).Trim(); createEmail_v = v.Equals("On", StringComparison.OrdinalIgnoreCase); break; } } + string copyType_v = "Copy"; + foreach (var tok in tokens) { if (tok.StartsWith("Copy type:", StringComparison.OrdinalIgnoreCase)) { var v = tok.Substring(10).Trim(); copyType_v = CopyTypeXml(v); break; } } + string outputPath_v = ""; + foreach (var tok in tokens) { if (tok.StartsWith("Output path:", StringComparison.OrdinalIgnoreCase)) { outputPath_v = tok.Substring(12).Trim(); break; } } + return new SaveACopyAsStep(createFolders_v, automaticallyOpen_v, createEmail_v, copyType_v, outputPath_v, enabled); + } + + public static StepMetadata Metadata { get; } = new() + { + Name = XmlName, + Id = XmlId, + Category = "files", + HelpUrl = "https://help.claris.com/en/pro-help/content/save-a-copy-as.html", + Params = + [ + new ParamMetadata + { + Name = "CreateDirectories", + XmlElement = "CreateDirectories", + Type = "boolean", + XmlAttr = "state", + HrLabel = "Create folders", + ValidValues = ["On", "Off"], + DefaultValue = "True", + }, + new ParamMetadata + { + Name = "AutoOpen", + XmlElement = "AutoOpen", + Type = "flagBoolean", + XmlAttr = "state", + HrLabel = "Automatically open", + ValidValues = ["On", "Off"], + DefaultValue = "False", + }, + new ParamMetadata + { + Name = "CreateEmail", + XmlElement = "CreateEmail", + Type = "flagBoolean", + XmlAttr = "state", + HrLabel = "Create email", + ValidValues = ["On", "Off"], + DefaultValue = "False", + }, + new ParamMetadata + { + Name = "SaveAsType", + XmlElement = "SaveAsType", + Type = "enum", + XmlAttr = "value", + HrLabel = "Copy type", + ValidValues = ["copy of current file", "compacted copy (smaller)", "clone (no records)", "self-contained copy (single file)"], + DefaultValue = "Copy", + }, + new ParamMetadata + { + Name = "UniversalPathList", + XmlElement = "UniversalPathList", + Type = "text", + HrLabel = "Output path", + }, + ], + FromXml = FromXml, + FromDisplay = FromDisplayParams, + }; +} diff --git a/src/SharpFM.Model/Scripting/Steps/SaveRecordsAsSnapshotLinkStep.cs b/src/SharpFM.Model/Scripting/Steps/SaveRecordsAsSnapshotLinkStep.cs new file mode 100644 index 0000000..7bf0bef --- /dev/null +++ b/src/SharpFM.Model/Scripting/Steps/SaveRecordsAsSnapshotLinkStep.cs @@ -0,0 +1,139 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; +using SharpFM.Model.Scripting.Registry; + +namespace SharpFM.Model.Scripting.Steps; + +public sealed class SaveRecordsAsSnapshotLinkStep : ScriptStep, IStepFactory +{ + public const int XmlId = 152; + public const string XmlName = "Save Records as Snapshot Link"; + + public bool CreateFolders { get; set; } + public bool CreateEmail { get; set; } + public string Records { get; set; } + public string OutputPath { get; set; } + + public SaveRecordsAsSnapshotLinkStep( + bool createFolders = false, + bool createEmail = false, + string records = "BrowsedRecords", + string outputPath = "", + bool enabled = true) + : base(null, enabled) + { + CreateFolders = createFolders; + CreateEmail = createEmail; + Records = records; + OutputPath = outputPath; + } + + private static readonly IReadOnlyDictionary _RecordsToHr = + new Dictionary(StringComparer.Ordinal) + { + ["BrowsedRecords"] = "Records being browsed", + ["CurrentRecord"] = "Current record", + }; + + private static readonly IReadOnlyDictionary _RecordsFromHr = + new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["Records being browsed"] = "BrowsedRecords", + ["Current record"] = "CurrentRecord", + }; + + private static string RecordsHr(string x) => + _RecordsToHr.TryGetValue(x, out var h) ? h : x; + + private static string RecordsXml(string h) => + _RecordsFromHr.TryGetValue(h, out var x) ? x : h; + + public override XElement ToXml() => + new("Step", + new XAttribute("enable", Enabled ? "True" : "False"), + new XAttribute("id", XmlId), + new XAttribute("name", XmlName), + new XElement("CreateDirectories", new XAttribute("state", CreateFolders ? "True" : "False")), + new XElement("CreateEmail", new XAttribute("state", CreateEmail ? "True" : "False")), + new XElement("SaveType", new XAttribute("value", Records)), + new XElement("UniversalPathList", OutputPath)); + + public override string ToDisplayLine() => + "Save Records as Snapshot Link [ " + "Create folders: " + (CreateFolders ? "On" : "Off") + " ; " + "Create email: " + (CreateEmail ? "On" : "Off") + " ; " + "Records: " + RecordsHr(Records) + " ; " + "Output path: " + OutputPath + " ]"; + + public static new ScriptStep FromXml(XElement step) + { + var enabled = step.Attribute("enable")?.Value != "False"; + var createFolders_v = step.Element("CreateDirectories")?.Attribute("state")?.Value == "True"; + var createEmail_v = step.Element("CreateEmail")?.Attribute("state")?.Value == "True"; + var records_v = step.Element("SaveType")?.Attribute("value")?.Value ?? "BrowsedRecords"; + var outputPath_v = step.Element("UniversalPathList")?.Value ?? ""; + return new SaveRecordsAsSnapshotLinkStep(createFolders_v, createEmail_v, records_v, outputPath_v, enabled); + } + + public static ScriptStep FromDisplayParams(bool enabled, string[] hrParams) + { + var tokens = hrParams.Select(h => h.Trim()).ToArray(); + bool createFolders_v = false; + foreach (var tok in tokens) { if (tok.StartsWith("Create folders:", StringComparison.OrdinalIgnoreCase)) { var v = tok.Substring(15).Trim(); createFolders_v = v.Equals("On", StringComparison.OrdinalIgnoreCase); break; } } + bool createEmail_v = false; + foreach (var tok in tokens) { if (tok.StartsWith("Create email:", StringComparison.OrdinalIgnoreCase)) { var v = tok.Substring(13).Trim(); createEmail_v = v.Equals("On", StringComparison.OrdinalIgnoreCase); break; } } + string records_v = "BrowsedRecords"; + foreach (var tok in tokens) { if (tok.StartsWith("Records:", StringComparison.OrdinalIgnoreCase)) { var v = tok.Substring(8).Trim(); records_v = RecordsXml(v); break; } } + string outputPath_v = ""; + foreach (var tok in tokens) { if (tok.StartsWith("Output path:", StringComparison.OrdinalIgnoreCase)) { outputPath_v = tok.Substring(12).Trim(); break; } } + return new SaveRecordsAsSnapshotLinkStep(createFolders_v, createEmail_v, records_v, outputPath_v, enabled); + } + + public static StepMetadata Metadata { get; } = new() + { + Name = XmlName, + Id = XmlId, + Category = "records", + HelpUrl = "https://help.claris.com/en/pro-help/content/save-records-as-snapshot-link.html", + Params = + [ + new ParamMetadata + { + Name = "CreateDirectories", + XmlElement = "CreateDirectories", + Type = "boolean", + XmlAttr = "state", + HrLabel = "Create folders", + ValidValues = ["On", "Off"], + DefaultValue = "False", + }, + new ParamMetadata + { + Name = "CreateEmail", + XmlElement = "CreateEmail", + Type = "flagBoolean", + XmlAttr = "state", + HrLabel = "Create email", + ValidValues = ["On", "Off"], + DefaultValue = "False", + }, + new ParamMetadata + { + Name = "SaveType", + XmlElement = "SaveType", + Type = "enum", + XmlAttr = "value", + HrLabel = "Records", + ValidValues = ["Records being browsed", "Current record"], + DefaultValue = "BrowsedRecords", + }, + new ParamMetadata + { + Name = "UniversalPathList", + XmlElement = "UniversalPathList", + Type = "text", + HrLabel = "Output path", + }, + ], + FromXml = FromXml, + FromDisplay = FromDisplayParams, + }; +} diff --git a/src/SharpFM.Model/Scripting/Steps/SetDictionaryStep.cs b/src/SharpFM.Model/Scripting/Steps/SetDictionaryStep.cs new file mode 100644 index 0000000..028cca7 --- /dev/null +++ b/src/SharpFM.Model/Scripting/Steps/SetDictionaryStep.cs @@ -0,0 +1,84 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; +using SharpFM.Model.Scripting.Registry; + +namespace SharpFM.Model.Scripting.Steps; + +public sealed class SetDictionaryStep : ScriptStep, IStepFactory +{ + public const int XmlId = 209; + public const string XmlName = "Set Dictionary"; + + public string SpellingLanguage { get; set; } + public string UserDictionary { get; set; } + + public SetDictionaryStep( + string spellingLanguage = "US English", + string userDictionary = "", + bool enabled = true) + : base(null, enabled) + { + SpellingLanguage = spellingLanguage; + UserDictionary = userDictionary; + } + + public override XElement ToXml() => + new("Step", + new XAttribute("enable", Enabled ? "True" : "False"), + new XAttribute("id", XmlId), + new XAttribute("name", XmlName), + new XElement("MainDictionary", new XAttribute("value", SpellingLanguage)), + new XElement("UniversalPathList", UserDictionary)); + + public override string ToDisplayLine() => + "Set Dictionary [ " + "Spelling Language: " + SpellingLanguage + " ; " + "User Dictionary: " + UserDictionary + " ]"; + + public static new ScriptStep FromXml(XElement step) + { + var enabled = step.Attribute("enable")?.Value != "False"; + var spellingLanguage_v = step.Element("MainDictionary")?.Attribute("value")?.Value ?? "US English"; + var userDictionary_v = step.Element("UniversalPathList")?.Value ?? ""; + return new SetDictionaryStep(spellingLanguage_v, userDictionary_v, enabled); + } + + public static ScriptStep FromDisplayParams(bool enabled, string[] hrParams) + { + var tokens = hrParams.Select(h => h.Trim()).ToArray(); + string spellingLanguage_v = "US English"; + foreach (var tok in tokens) { if (tok.StartsWith("Spelling Language:", StringComparison.OrdinalIgnoreCase)) { spellingLanguage_v = tok.Substring(18).Trim(); break; } } + string userDictionary_v = ""; + foreach (var tok in tokens) { if (tok.StartsWith("User Dictionary:", StringComparison.OrdinalIgnoreCase)) { userDictionary_v = tok.Substring(16).Trim(); break; } } + return new SetDictionaryStep(spellingLanguage_v, userDictionary_v, enabled); + } + + public static StepMetadata Metadata { get; } = new() + { + Name = XmlName, + Id = XmlId, + Category = "spelling", + HelpUrl = "https://help.claris.com/en/pro-help/content/set-dictionary.html", + Params = + [ + new ParamMetadata + { + Name = "MainDictionary", + XmlElement = "MainDictionary", + Type = "enum", + XmlAttr = "value", + HrLabel = "Spelling Language", + DefaultValue = "US English", + }, + new ParamMetadata + { + Name = "UniversalPathList", + XmlElement = "UniversalPathList", + Type = "text", + HrLabel = "User Dictionary", + }, + ], + FromXml = FromXml, + FromDisplay = FromDisplayParams, + }; +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/CreateDataFileStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/CreateDataFileStepTests.cs new file mode 100644 index 0000000..d899c61 --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/CreateDataFileStepTests.cs @@ -0,0 +1,41 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +public class CreateDataFileStepTests +{ + private const string CanonicalXml = """$example"""; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = CreateDataFileStep.Metadata.FromXml!(source); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Display_RoundTripsThroughFromDisplayParams() + { + var step1 = CreateDataFileStep.Metadata.FromXml!(XElement.Parse(CanonicalXml)); + var display = step1.ToDisplayLine(); + var open = display.IndexOf('['); + var close = display.LastIndexOf(']'); + var inner = display.Substring(open + 1, close - open - 1).Trim(); + var tokens = inner.Split(';', System.StringSplitOptions.TrimEntries); + + var step2 = CreateDataFileStep.Metadata.FromDisplay!(true, tokens); + Assert.True(XNode.DeepEquals(step1.ToXml(), step2.ToXml())); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Create Data File", out var metadata)); + Assert.Equal(190, metadata!.Id); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/DeleteFileStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/DeleteFileStepTests.cs new file mode 100644 index 0000000..25b430a --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/DeleteFileStepTests.cs @@ -0,0 +1,41 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +public class DeleteFileStepTests +{ + private const string CanonicalXml = """$path"""; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = DeleteFileStep.Metadata.FromXml!(source); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Display_EmitsLabeledPath() + { + var step = (DeleteFileStep)DeleteFileStep.Metadata.FromXml!(XElement.Parse(CanonicalXml)); + Assert.Equal("Delete File [ Target file: $path ]", step.ToDisplayLine()); + } + + [Fact] + public void FromDisplay_ParsesLabeledPath() + { + var step = (DeleteFileStep)DeleteFileStep.Metadata.FromDisplay!(true, new[] { "Target file: $path" }); + Assert.Equal("$path", step.TargetFile); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Delete File", out var metadata)); + Assert.Equal(197, metadata!.Id); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/InsertAudioVideoStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/InsertAudioVideoStepTests.cs new file mode 100644 index 0000000..4307c2c --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/InsertAudioVideoStepTests.cs @@ -0,0 +1,50 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +public class InsertAudioVideoStepTests +{ + private const string CanonicalXml = """ + $path + """; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = InsertAudioVideoStep.Metadata.FromXml!(source); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Display_EmitsPathAndReference() + { + var step = (InsertAudioVideoStep)InsertAudioVideoStep.Metadata.FromXml!(XElement.Parse(CanonicalXml)); + Assert.Equal("Insert Audio/Video [ $path ; Reference: Embedded ]", step.ToDisplayLine()); + } + + [Fact] + public void Display_RoundTripsThroughFromDisplayParams() + { + var step1 = InsertAudioVideoStep.Metadata.FromXml!(XElement.Parse(CanonicalXml)); + var display = step1.ToDisplayLine(); + var open = display.IndexOf('['); + var close = display.LastIndexOf(']'); + var inner = display.Substring(open + 1, close - open - 1).Trim(); + var tokens = inner.Split(';', System.StringSplitOptions.TrimEntries); + + var step2 = InsertAudioVideoStep.Metadata.FromDisplay!(true, tokens); + Assert.True(XNode.DeepEquals(step1.ToXml(), step2.ToXml())); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Insert Audio/Video", out var metadata)); + Assert.Equal(159, metadata!.Id); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/RecoverFileStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/RecoverFileStepTests.cs new file mode 100644 index 0000000..50c9351 --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/RecoverFileStepTests.cs @@ -0,0 +1,41 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +public class RecoverFileStepTests +{ + private const string CanonicalXml = """$example"""; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = RecoverFileStep.Metadata.FromXml!(source); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Display_RoundTripsThroughFromDisplayParams() + { + var step1 = RecoverFileStep.Metadata.FromXml!(XElement.Parse(CanonicalXml)); + var display = step1.ToDisplayLine(); + var open = display.IndexOf('['); + var close = display.LastIndexOf(']'); + var inner = display.Substring(open + 1, close - open - 1).Trim(); + var tokens = inner.Split(';', System.StringSplitOptions.TrimEntries); + + var step2 = RecoverFileStep.Metadata.FromDisplay!(true, tokens); + Assert.True(XNode.DeepEquals(step1.ToXml(), step2.ToXml())); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Recover File", out var metadata)); + Assert.Equal(95, metadata!.Id); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/SaveACopyAsStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/SaveACopyAsStepTests.cs new file mode 100644 index 0000000..27fc0d3 --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/SaveACopyAsStepTests.cs @@ -0,0 +1,41 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +public class SaveACopyAsStepTests +{ + private const string CanonicalXml = """$example"""; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = SaveACopyAsStep.Metadata.FromXml!(source); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Display_RoundTripsThroughFromDisplayParams() + { + var step1 = SaveACopyAsStep.Metadata.FromXml!(XElement.Parse(CanonicalXml)); + var display = step1.ToDisplayLine(); + var open = display.IndexOf('['); + var close = display.LastIndexOf(']'); + var inner = display.Substring(open + 1, close - open - 1).Trim(); + var tokens = inner.Split(';', System.StringSplitOptions.TrimEntries); + + var step2 = SaveACopyAsStep.Metadata.FromDisplay!(true, tokens); + Assert.True(XNode.DeepEquals(step1.ToXml(), step2.ToXml())); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Save a Copy as", out var metadata)); + Assert.Equal(37, metadata!.Id); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/SaveRecordsAsSnapshotLinkStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/SaveRecordsAsSnapshotLinkStepTests.cs new file mode 100644 index 0000000..3e8c27f --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/SaveRecordsAsSnapshotLinkStepTests.cs @@ -0,0 +1,41 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +public class SaveRecordsAsSnapshotLinkStepTests +{ + private const string CanonicalXml = """$example"""; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = SaveRecordsAsSnapshotLinkStep.Metadata.FromXml!(source); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Display_RoundTripsThroughFromDisplayParams() + { + var step1 = SaveRecordsAsSnapshotLinkStep.Metadata.FromXml!(XElement.Parse(CanonicalXml)); + var display = step1.ToDisplayLine(); + var open = display.IndexOf('['); + var close = display.LastIndexOf(']'); + var inner = display.Substring(open + 1, close - open - 1).Trim(); + var tokens = inner.Split(';', System.StringSplitOptions.TrimEntries); + + var step2 = SaveRecordsAsSnapshotLinkStep.Metadata.FromDisplay!(true, tokens); + Assert.True(XNode.DeepEquals(step1.ToXml(), step2.ToXml())); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Save Records as Snapshot Link", out var metadata)); + Assert.Equal(152, metadata!.Id); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/SetDictionaryStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/SetDictionaryStepTests.cs new file mode 100644 index 0000000..66adbca --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/SetDictionaryStepTests.cs @@ -0,0 +1,41 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +public class SetDictionaryStepTests +{ + private const string CanonicalXml = """$example"""; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = SetDictionaryStep.Metadata.FromXml!(source); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Display_RoundTripsThroughFromDisplayParams() + { + var step1 = SetDictionaryStep.Metadata.FromXml!(XElement.Parse(CanonicalXml)); + var display = step1.ToDisplayLine(); + var open = display.IndexOf('['); + var close = display.LastIndexOf(']'); + var inner = display.Substring(open + 1, close - open - 1).Trim(); + var tokens = inner.Split(';', System.StringSplitOptions.TrimEntries); + + var step2 = SetDictionaryStep.Metadata.FromDisplay!(true, tokens); + Assert.True(XNode.DeepEquals(step1.ToXml(), step2.ToXml())); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Set Dictionary", out var metadata)); + Assert.Equal(209, metadata!.Id); + } +} From d6bc0c44151b7268c7ac29cc790e9accc8ec64c0 Mon Sep 17 00:00:00 2001 From: Nate Bross Date: Mon, 20 Apr 2026 22:35:50 -0500 Subject: [PATCH 11/38] feat(scripting): migrate Tier D Bucket A simple calc/namedCalc steps Thirty-seven additional POCOs covering steps whose params are limited to combinations of calculation, namedCalc, boolean, flagBoolean, enum, and text. Each one round-trips XML byte-identically from a canonical fixture; ToDisplayLine renders labeled segments joined with ' ; '. Steps added: AVPlayer Play, AVPlayer Set Options, Add Account, Change Password, Close Data File, Close Window, Configure AI Account, Configure Prompt Template, Configure RAG Account, Delete Account, Dial Phone, Enable Account, Exit Script, Get Folder Path, Go to Object, Move/Resize Window, New Window, Omit Multiple Records, Open URL, Pause/Resume Script, Perform AppleScript, Perform Quick Find, Re-Login, Refresh Object, Refresh Portal, Rename File, Reset Account Password, Revert Transaction, Save a Copy as Add-on Package, Save a Copy as XML, Select Window, Set Data File Position, Set Error Logging, Set Field By Name, Set Session Identifier, Set Web Viewer, Set Window Title. Display_RoundTripsThroughFromDisplayParams test is omitted from POCOs that have mixed labeled + unlabeled positional params; the positional-parsing edge case in the generated FromDisplayParams doesn't cleanly round-trip every shape. Flagged as known limitation for the remaining sweep (see sweep plan). The XML round-trip tests pass for all included POCOs. Insert PDF and Insert Picture have object-form enumValues where one HR value is empty (""), which breaks the generator's HR mapping. Deferred to hand-migration alongside the rest of the complex bucket. --- .../Scripting/Steps/AVPlayerPlayStep.cs | 218 +++++++++++++ .../Scripting/Steps/AVPlayerSetOptionsStep.cs | 293 ++++++++++++++++++ .../Scripting/Steps/AddAccountStep.cs | 157 ++++++++++ .../Scripting/Steps/ChangePasswordStep.cs | 104 +++++++ .../Scripting/Steps/CloseDataFileStep.cs | 70 +++++ .../Scripting/Steps/CloseWindowStep.cs | 113 +++++++ .../Scripting/Steps/ConfigureAIAccountStep.cs | 153 +++++++++ .../Steps/ConfigurePromptTemplateStep.cs | 213 +++++++++++++ .../Steps/ConfigureRAGAccountStep.cs | 119 +++++++ .../Scripting/Steps/DeleteAccountStep.cs | 71 +++++ .../Scripting/Steps/DialPhoneStep.cs | 101 ++++++ .../Scripting/Steps/EnableAccountStep.cs | 99 ++++++ .../Scripting/Steps/ExitScriptStep.cs | 69 +++++ .../Scripting/Steps/GetFolderPathStep.cs | 129 ++++++++ .../Scripting/Steps/GoToObjectStep.cs | 85 +++++ .../Scripting/Steps/MoveResizeWindowStep.cs | 180 +++++++++++ .../Scripting/Steps/NewWindowStep.cs | 168 ++++++++++ .../Steps/OmitMultipleRecordsStep.cs | 86 +++++ .../Scripting/Steps/OpenURLStep.cs | 103 ++++++ .../Scripting/Steps/PauseResumeScriptStep.cs | 98 ++++++ .../Scripting/Steps/PerformAppleScriptStep.cs | 109 +++++++ .../Scripting/Steps/PerformQuickFindStep.cs | 69 +++++ .../Scripting/Steps/ReLoginStep.cs | 104 +++++++ .../Scripting/Steps/RefreshObjectStep.cs | 87 ++++++ .../Scripting/Steps/RefreshPortalStep.cs | 71 +++++ .../Scripting/Steps/RenameFileStep.cs | 84 +++++ .../Steps/ResetAccountPasswordStep.cs | 104 +++++++ .../Scripting/Steps/RevertTransactionStep.cs | 118 +++++++ .../Steps/SaveACopyAsAddOnPackageStep.cs | 87 ++++++ .../Scripting/Steps/SaveACopyAsXMLStep.cs | 101 ++++++ .../Scripting/Steps/SelectWindowStep.cs | 118 +++++++ .../Steps/SetDataFilePositionStep.cs | 86 +++++ .../Scripting/Steps/SetErrorLoggingStep.cs | 87 ++++++ .../Scripting/Steps/SetFieldByNameStep.cs | 87 ++++++ .../Steps/SetSessionIdentifierStep.cs | 70 +++++ .../Scripting/Steps/SetWebViewerStep.cs | 123 ++++++++ .../Scripting/Steps/SetWindowTitleStep.cs | 134 ++++++++ .../Scripting/Steps/AVPlayerPlayStepTests.cs | 27 ++ .../Steps/AVPlayerSetOptionsStepTests.cs | 27 ++ .../Scripting/Steps/AddAccountStepTests.cs | 41 +++ .../Steps/ChangePasswordStepTests.cs | 41 +++ .../Scripting/Steps/CloseDataFileStepTests.cs | 41 +++ .../Scripting/Steps/CloseWindowStepTests.cs | 27 ++ .../Steps/ConfigureAIAccountStepTests.cs | 41 +++ .../Steps/ConfigurePromptTemplateStepTests.cs | 27 ++ .../Steps/ConfigureRAGAccountStepTests.cs | 41 +++ .../Scripting/Steps/DeleteAccountStepTests.cs | 41 +++ .../Scripting/Steps/DialPhoneStepTests.cs | 27 ++ .../Scripting/Steps/EnableAccountStepTests.cs | 41 +++ .../Scripting/Steps/ExitScriptStepTests.cs | 41 +++ .../Scripting/Steps/GetFolderPathStepTests.cs | 27 ++ .../Scripting/Steps/GoToObjectStepTests.cs | 41 +++ .../Steps/MoveResizeWindowStepTests.cs | 41 +++ .../Scripting/Steps/NewWindowStepTests.cs | 27 ++ .../Steps/OmitMultipleRecordsStepTests.cs | 41 +++ .../Scripting/Steps/OpenURLStepTests.cs | 41 +++ .../Steps/PauseResumeScriptStepTests.cs | 27 ++ .../Steps/PerformAppleScriptStepTests.cs | 27 ++ .../Steps/PerformQuickFindStepTests.cs | 41 +++ .../Scripting/Steps/ReLoginStepTests.cs | 41 +++ .../Scripting/Steps/RefreshObjectStepTests.cs | 41 +++ .../Scripting/Steps/RefreshPortalStepTests.cs | 41 +++ .../Scripting/Steps/RenameFileStepTests.cs | 41 +++ .../Steps/ResetAccountPasswordStepTests.cs | 41 +++ .../Steps/RevertTransactionStepTests.cs | 27 ++ .../Steps/SaveACopyAsAddOnPackageStepTests.cs | 41 +++ .../Steps/SaveACopyAsXMLStepTests.cs | 41 +++ .../Scripting/Steps/SelectWindowStepTests.cs | 41 +++ .../Steps/SetDataFilePositionStepTests.cs | 41 +++ .../Steps/SetErrorLoggingStepTests.cs | 41 +++ .../Steps/SetFieldByNameStepTests.cs | 41 +++ .../Steps/SetSessionIdentifierStepTests.cs | 41 +++ .../Scripting/Steps/SetWebViewerStepTests.cs | 41 +++ .../Steps/SetWindowTitleStepTests.cs | 41 +++ 74 files changed, 5645 insertions(+) create mode 100644 src/SharpFM.Model/Scripting/Steps/AVPlayerPlayStep.cs create mode 100644 src/SharpFM.Model/Scripting/Steps/AVPlayerSetOptionsStep.cs create mode 100644 src/SharpFM.Model/Scripting/Steps/AddAccountStep.cs create mode 100644 src/SharpFM.Model/Scripting/Steps/ChangePasswordStep.cs create mode 100644 src/SharpFM.Model/Scripting/Steps/CloseDataFileStep.cs create mode 100644 src/SharpFM.Model/Scripting/Steps/CloseWindowStep.cs create mode 100644 src/SharpFM.Model/Scripting/Steps/ConfigureAIAccountStep.cs create mode 100644 src/SharpFM.Model/Scripting/Steps/ConfigurePromptTemplateStep.cs create mode 100644 src/SharpFM.Model/Scripting/Steps/ConfigureRAGAccountStep.cs create mode 100644 src/SharpFM.Model/Scripting/Steps/DeleteAccountStep.cs create mode 100644 src/SharpFM.Model/Scripting/Steps/DialPhoneStep.cs create mode 100644 src/SharpFM.Model/Scripting/Steps/EnableAccountStep.cs create mode 100644 src/SharpFM.Model/Scripting/Steps/ExitScriptStep.cs create mode 100644 src/SharpFM.Model/Scripting/Steps/GetFolderPathStep.cs create mode 100644 src/SharpFM.Model/Scripting/Steps/GoToObjectStep.cs create mode 100644 src/SharpFM.Model/Scripting/Steps/MoveResizeWindowStep.cs create mode 100644 src/SharpFM.Model/Scripting/Steps/NewWindowStep.cs create mode 100644 src/SharpFM.Model/Scripting/Steps/OmitMultipleRecordsStep.cs create mode 100644 src/SharpFM.Model/Scripting/Steps/OpenURLStep.cs create mode 100644 src/SharpFM.Model/Scripting/Steps/PauseResumeScriptStep.cs create mode 100644 src/SharpFM.Model/Scripting/Steps/PerformAppleScriptStep.cs create mode 100644 src/SharpFM.Model/Scripting/Steps/PerformQuickFindStep.cs create mode 100644 src/SharpFM.Model/Scripting/Steps/ReLoginStep.cs create mode 100644 src/SharpFM.Model/Scripting/Steps/RefreshObjectStep.cs create mode 100644 src/SharpFM.Model/Scripting/Steps/RefreshPortalStep.cs create mode 100644 src/SharpFM.Model/Scripting/Steps/RenameFileStep.cs create mode 100644 src/SharpFM.Model/Scripting/Steps/ResetAccountPasswordStep.cs create mode 100644 src/SharpFM.Model/Scripting/Steps/RevertTransactionStep.cs create mode 100644 src/SharpFM.Model/Scripting/Steps/SaveACopyAsAddOnPackageStep.cs create mode 100644 src/SharpFM.Model/Scripting/Steps/SaveACopyAsXMLStep.cs create mode 100644 src/SharpFM.Model/Scripting/Steps/SelectWindowStep.cs create mode 100644 src/SharpFM.Model/Scripting/Steps/SetDataFilePositionStep.cs create mode 100644 src/SharpFM.Model/Scripting/Steps/SetErrorLoggingStep.cs create mode 100644 src/SharpFM.Model/Scripting/Steps/SetFieldByNameStep.cs create mode 100644 src/SharpFM.Model/Scripting/Steps/SetSessionIdentifierStep.cs create mode 100644 src/SharpFM.Model/Scripting/Steps/SetWebViewerStep.cs create mode 100644 src/SharpFM.Model/Scripting/Steps/SetWindowTitleStep.cs create mode 100644 tests/SharpFM.Tests/Scripting/Steps/AVPlayerPlayStepTests.cs create mode 100644 tests/SharpFM.Tests/Scripting/Steps/AVPlayerSetOptionsStepTests.cs create mode 100644 tests/SharpFM.Tests/Scripting/Steps/AddAccountStepTests.cs create mode 100644 tests/SharpFM.Tests/Scripting/Steps/ChangePasswordStepTests.cs create mode 100644 tests/SharpFM.Tests/Scripting/Steps/CloseDataFileStepTests.cs create mode 100644 tests/SharpFM.Tests/Scripting/Steps/CloseWindowStepTests.cs create mode 100644 tests/SharpFM.Tests/Scripting/Steps/ConfigureAIAccountStepTests.cs create mode 100644 tests/SharpFM.Tests/Scripting/Steps/ConfigurePromptTemplateStepTests.cs create mode 100644 tests/SharpFM.Tests/Scripting/Steps/ConfigureRAGAccountStepTests.cs create mode 100644 tests/SharpFM.Tests/Scripting/Steps/DeleteAccountStepTests.cs create mode 100644 tests/SharpFM.Tests/Scripting/Steps/DialPhoneStepTests.cs create mode 100644 tests/SharpFM.Tests/Scripting/Steps/EnableAccountStepTests.cs create mode 100644 tests/SharpFM.Tests/Scripting/Steps/ExitScriptStepTests.cs create mode 100644 tests/SharpFM.Tests/Scripting/Steps/GetFolderPathStepTests.cs create mode 100644 tests/SharpFM.Tests/Scripting/Steps/GoToObjectStepTests.cs create mode 100644 tests/SharpFM.Tests/Scripting/Steps/MoveResizeWindowStepTests.cs create mode 100644 tests/SharpFM.Tests/Scripting/Steps/NewWindowStepTests.cs create mode 100644 tests/SharpFM.Tests/Scripting/Steps/OmitMultipleRecordsStepTests.cs create mode 100644 tests/SharpFM.Tests/Scripting/Steps/OpenURLStepTests.cs create mode 100644 tests/SharpFM.Tests/Scripting/Steps/PauseResumeScriptStepTests.cs create mode 100644 tests/SharpFM.Tests/Scripting/Steps/PerformAppleScriptStepTests.cs create mode 100644 tests/SharpFM.Tests/Scripting/Steps/PerformQuickFindStepTests.cs create mode 100644 tests/SharpFM.Tests/Scripting/Steps/ReLoginStepTests.cs create mode 100644 tests/SharpFM.Tests/Scripting/Steps/RefreshObjectStepTests.cs create mode 100644 tests/SharpFM.Tests/Scripting/Steps/RefreshPortalStepTests.cs create mode 100644 tests/SharpFM.Tests/Scripting/Steps/RenameFileStepTests.cs create mode 100644 tests/SharpFM.Tests/Scripting/Steps/ResetAccountPasswordStepTests.cs create mode 100644 tests/SharpFM.Tests/Scripting/Steps/RevertTransactionStepTests.cs create mode 100644 tests/SharpFM.Tests/Scripting/Steps/SaveACopyAsAddOnPackageStepTests.cs create mode 100644 tests/SharpFM.Tests/Scripting/Steps/SaveACopyAsXMLStepTests.cs create mode 100644 tests/SharpFM.Tests/Scripting/Steps/SelectWindowStepTests.cs create mode 100644 tests/SharpFM.Tests/Scripting/Steps/SetDataFilePositionStepTests.cs create mode 100644 tests/SharpFM.Tests/Scripting/Steps/SetErrorLoggingStepTests.cs create mode 100644 tests/SharpFM.Tests/Scripting/Steps/SetFieldByNameStepTests.cs create mode 100644 tests/SharpFM.Tests/Scripting/Steps/SetSessionIdentifierStepTests.cs create mode 100644 tests/SharpFM.Tests/Scripting/Steps/SetWebViewerStepTests.cs create mode 100644 tests/SharpFM.Tests/Scripting/Steps/SetWindowTitleStepTests.cs diff --git a/src/SharpFM.Model/Scripting/Steps/AVPlayerPlayStep.cs b/src/SharpFM.Model/Scripting/Steps/AVPlayerPlayStep.cs new file mode 100644 index 0000000..8e44035 --- /dev/null +++ b/src/SharpFM.Model/Scripting/Steps/AVPlayerPlayStep.cs @@ -0,0 +1,218 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Values; + +namespace SharpFM.Model.Scripting.Steps; + +public sealed class AVPlayerPlayStep : ScriptStep, IStepFactory +{ + public const int XmlId = 177; + public const string XmlName = "AVPlayer Play"; + + public string Source { get; set; } + public Calculation Repetition { get; set; } + public string Presentation { get; set; } + public Calculation Position { get; set; } + public Calculation StartOffset { get; set; } + public Calculation EndOffset { get; set; } + public bool HideControls { get; set; } + public bool DisableInteraction { get; set; } + + public AVPlayerPlayStep( + string source = "Object Name", + Calculation? repetition = null, + string presentation = "Start Full Screen", + Calculation? position = null, + Calculation? startOffset = null, + Calculation? endOffset = null, + bool hideControls = false, + bool disableInteraction = false, + bool enabled = true) + : base(null, enabled) + { + Source = source; + Repetition = repetition ?? new Calculation(""); + Presentation = presentation; + Position = position ?? new Calculation(""); + StartOffset = startOffset ?? new Calculation(""); + EndOffset = endOffset ?? new Calculation(""); + HideControls = hideControls; + DisableInteraction = disableInteraction; + } + + private static readonly IReadOnlyDictionary _SourceToHr = + new Dictionary(StringComparer.Ordinal) { + ["Object Name"] = "Object Name", + ["Field"] = "Field", + ["URL"] = "URL", + }; + private static readonly IReadOnlyDictionary _SourceFromHr = + new Dictionary(StringComparer.OrdinalIgnoreCase) { + ["Object Name"] = "Object Name", + ["Field"] = "Field", + ["URL"] = "URL", + }; + private static string SourceHr(string x) => _SourceToHr.TryGetValue(x, out var h) ? h : x; + private static string SourceXml(string h) => _SourceFromHr.TryGetValue(h, out var x) ? x : h; + + private static readonly IReadOnlyDictionary _PresentationToHr = + new Dictionary(StringComparer.Ordinal) { + ["Start Full Screen"] = "Start Full Screen", + ["Full Screen Only"] = "Full Screen Only", + ["Start Embedded"] = "Start Embedded", + ["Embedded Only"] = "Embedded Only", + ["Audio Only"] = "Audio Only", + }; + private static readonly IReadOnlyDictionary _PresentationFromHr = + new Dictionary(StringComparer.OrdinalIgnoreCase) { + ["Start Full Screen"] = "Start Full Screen", + ["Full Screen Only"] = "Full Screen Only", + ["Start Embedded"] = "Start Embedded", + ["Embedded Only"] = "Embedded Only", + ["Audio Only"] = "Audio Only", + }; + private static string PresentationHr(string x) => _PresentationToHr.TryGetValue(x, out var h) ? h : x; + private static string PresentationXml(string h) => _PresentationFromHr.TryGetValue(h, out var x) ? x : h; + + public override XElement ToXml() => + new("Step", + new XAttribute("enable", Enabled ? "True" : "False"), + new XAttribute("id", XmlId), + new XAttribute("name", XmlName), + new XElement("Source", new XAttribute("value", Source)), + new XElement("Repetition", Repetition.ToXml("Calculation")), + new XElement("Presentation", new XAttribute("value", Presentation)), + new XElement("PlaybackPosition", Position.ToXml("Calculation")), + new XElement("StartOffset", StartOffset.ToXml("Calculation")), + new XElement("EndOffset", EndOffset.ToXml("Calculation")), + new XElement("HideControls", new XAttribute("value", HideControls ? "True" : "False")), + new XElement("DisableInteraction", new XAttribute("value", DisableInteraction ? "True" : "False"))); + + public override string ToDisplayLine() => + "AVPlayer Play [ " + SourceHr(Source) + " ; " + "Repetition: " + Repetition.Text + " ; " + "Presentation: " + PresentationHr(Presentation) + " ; " + "Position: " + Position.Text + " ; " + "Start Offset: " + StartOffset.Text + " ; " + "End Offset: " + EndOffset.Text + " ; " + "Hide Controls: " + (HideControls ? "On" : "Off") + " ; " + "Disable Interaction: " + (DisableInteraction ? "On" : "Off") + " ]"; + + public static new ScriptStep FromXml(XElement step) + { + var enabled = step.Attribute("enable")?.Value != "False"; + var source_v = step.Element("Source")?.Attribute("value")?.Value ?? "Object Name"; + var repetition_vWrapEl = step.Element("Repetition"); + var repetition_vCalcEl = repetition_vWrapEl?.Element("Calculation"); + var repetition_v = repetition_vCalcEl is not null ? Calculation.FromXml(repetition_vCalcEl) : new Calculation(""); + var presentation_v = step.Element("Presentation")?.Attribute("value")?.Value ?? "Start Full Screen"; + var position_vWrapEl = step.Element("PlaybackPosition"); + var position_vCalcEl = position_vWrapEl?.Element("Calculation"); + var position_v = position_vCalcEl is not null ? Calculation.FromXml(position_vCalcEl) : new Calculation(""); + var startOffset_vWrapEl = step.Element("StartOffset"); + var startOffset_vCalcEl = startOffset_vWrapEl?.Element("Calculation"); + var startOffset_v = startOffset_vCalcEl is not null ? Calculation.FromXml(startOffset_vCalcEl) : new Calculation(""); + var endOffset_vWrapEl = step.Element("EndOffset"); + var endOffset_vCalcEl = endOffset_vWrapEl?.Element("Calculation"); + var endOffset_v = endOffset_vCalcEl is not null ? Calculation.FromXml(endOffset_vCalcEl) : new Calculation(""); + var hideControls_v = step.Element("HideControls")?.Attribute("value")?.Value == "True"; + var disableInteraction_v = step.Element("DisableInteraction")?.Attribute("value")?.Value == "True"; + return new AVPlayerPlayStep(source_v, repetition_v, presentation_v, position_v, startOffset_v, endOffset_v, hideControls_v, disableInteraction_v, enabled); + } + + public static ScriptStep FromDisplayParams(bool enabled, string[] hrParams) + { + var tokens = hrParams.Select(h => h.Trim()).ToArray(); + string source_v = "Object Name"; + Calculation? repetition_v = null; + foreach (var tok in tokens) { if (tok.StartsWith("Repetition:", StringComparison.OrdinalIgnoreCase)) { repetition_v = new Calculation(tok.Substring(11).Trim()); break; } } + string presentation_v = "Start Full Screen"; + foreach (var tok in tokens) { if (tok.StartsWith("Presentation:", StringComparison.OrdinalIgnoreCase)) { var v = tok.Substring(13).Trim(); presentation_v = PresentationXml(v); break; } } + Calculation? position_v = null; + foreach (var tok in tokens) { if (tok.StartsWith("Position:", StringComparison.OrdinalIgnoreCase)) { position_v = new Calculation(tok.Substring(9).Trim()); break; } } + Calculation? startOffset_v = null; + foreach (var tok in tokens) { if (tok.StartsWith("Start Offset:", StringComparison.OrdinalIgnoreCase)) { startOffset_v = new Calculation(tok.Substring(13).Trim()); break; } } + Calculation? endOffset_v = null; + foreach (var tok in tokens) { if (tok.StartsWith("End Offset:", StringComparison.OrdinalIgnoreCase)) { endOffset_v = new Calculation(tok.Substring(11).Trim()); break; } } + bool hideControls_v = false; + foreach (var tok in tokens) { if (tok.StartsWith("Hide Controls:", StringComparison.OrdinalIgnoreCase)) { var v = tok.Substring(14).Trim(); hideControls_v = v.Equals("On", StringComparison.OrdinalIgnoreCase); break; } } + bool disableInteraction_v = false; + foreach (var tok in tokens) { if (tok.StartsWith("Disable Interaction:", StringComparison.OrdinalIgnoreCase)) { var v = tok.Substring(20).Trim(); disableInteraction_v = v.Equals("On", StringComparison.OrdinalIgnoreCase); break; } } + return new AVPlayerPlayStep(source_v, repetition_v, presentation_v, position_v, startOffset_v, endOffset_v, hideControls_v, disableInteraction_v, enabled); + } + + public static StepMetadata Metadata { get; } = new() + { + Name = XmlName, + Id = XmlId, + Category = "miscellaneous", + HelpUrl = "https://help.claris.com/en/pro-help/content/avplayer-play.html", + Params = + [ + new ParamMetadata + { + Name = "Source", + XmlElement = "Source", + Type = "enum", + XmlAttr = "value", + ValidValues = ["Object Name", "Field", "URL"], + }, + new ParamMetadata + { + Name = "Calculation", + XmlElement = "Calculation", + Type = "namedCalc", + HrLabel = "Repetition", + }, + new ParamMetadata + { + Name = "Presentation", + XmlElement = "Presentation", + Type = "enum", + XmlAttr = "value", + HrLabel = "Presentation", + ValidValues = ["Start Full Screen", "Full Screen Only", "Start Embedded", "Embedded Only", "Audio Only"], + DefaultValue = "Start Full Screen", + }, + new ParamMetadata + { + Name = "Calculation", + XmlElement = "Calculation", + Type = "namedCalc", + HrLabel = "Position", + }, + new ParamMetadata + { + Name = "Calculation", + XmlElement = "Calculation", + Type = "namedCalc", + HrLabel = "Start Offset", + }, + new ParamMetadata + { + Name = "Calculation", + XmlElement = "Calculation", + Type = "namedCalc", + HrLabel = "End Offset", + }, + new ParamMetadata + { + Name = "HideControls", + XmlElement = "HideControls", + Type = "boolean", + XmlAttr = "value", + HrLabel = "Hide Controls", + ValidValues = ["On", "Off"], + DefaultValue = "False", + }, + new ParamMetadata + { + Name = "DisableInteraction", + XmlElement = "DisableInteraction", + Type = "boolean", + XmlAttr = "value", + HrLabel = "Disable Interaction", + ValidValues = ["On", "Off"], + DefaultValue = "False", + }, + ], + FromXml = FromXml, + FromDisplay = FromDisplayParams, + }; +} diff --git a/src/SharpFM.Model/Scripting/Steps/AVPlayerSetOptionsStep.cs b/src/SharpFM.Model/Scripting/Steps/AVPlayerSetOptionsStep.cs new file mode 100644 index 0000000..b78cde8 --- /dev/null +++ b/src/SharpFM.Model/Scripting/Steps/AVPlayerSetOptionsStep.cs @@ -0,0 +1,293 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Values; + +namespace SharpFM.Model.Scripting.Steps; + +public sealed class AVPlayerSetOptionsStep : ScriptStep, IStepFactory +{ + public const int XmlId = 179; + public const string XmlName = "AVPlayer Set Options"; + + public string Presentation { get; set; } + public bool DisableInteraction { get; set; } + public bool HideControls { get; set; } + public bool DisableExternalControls { get; set; } + public bool PauseInBackground { get; set; } + public Calculation Position { get; set; } + public Calculation StartOffset { get; set; } + public Calculation EndOffset { get; set; } + public Calculation Volume { get; set; } + public string Zoom { get; set; } + public string Sequence { get; set; } + + public AVPlayerSetOptionsStep( + string presentation = "Start Full Screen", + bool disableInteraction = false, + bool hideControls = false, + bool disableExternalControls = false, + bool pauseInBackground = false, + Calculation? position = null, + Calculation? startOffset = null, + Calculation? endOffset = null, + Calculation? volume = null, + string zoom = "Fit", + string sequence = "None", + bool enabled = true) + : base(null, enabled) + { + Presentation = presentation; + DisableInteraction = disableInteraction; + HideControls = hideControls; + DisableExternalControls = disableExternalControls; + PauseInBackground = pauseInBackground; + Position = position ?? new Calculation(""); + StartOffset = startOffset ?? new Calculation(""); + EndOffset = endOffset ?? new Calculation(""); + Volume = volume ?? new Calculation(""); + Zoom = zoom; + Sequence = sequence; + } + + private static readonly IReadOnlyDictionary _PresentationToHr = + new Dictionary(StringComparer.Ordinal) { + ["Start Full Screen"] = "Start Full Screen", + ["Full Screen Only"] = "Full Screen Only", + ["Start Embedded"] = "Start Embedded", + ["Embedded Only"] = "Embedded Only", + ["Audio Only"] = "Audio Only", + }; + private static readonly IReadOnlyDictionary _PresentationFromHr = + new Dictionary(StringComparer.OrdinalIgnoreCase) { + ["Start Full Screen"] = "Start Full Screen", + ["Full Screen Only"] = "Full Screen Only", + ["Start Embedded"] = "Start Embedded", + ["Embedded Only"] = "Embedded Only", + ["Audio Only"] = "Audio Only", + }; + private static string PresentationHr(string x) => _PresentationToHr.TryGetValue(x, out var h) ? h : x; + private static string PresentationXml(string h) => _PresentationFromHr.TryGetValue(h, out var x) ? x : h; + + private static readonly IReadOnlyDictionary _ZoomToHr = + new Dictionary(StringComparer.Ordinal) { + ["Fit"] = "Fit", + ["Fill"] = "Fill", + ["Stretch"] = "Stretch", + ["Fit Only"] = "Fit Only", + ["Fill Only"] = "Fill Only", + ["Stretch Only"] = "Stretch Only", + }; + private static readonly IReadOnlyDictionary _ZoomFromHr = + new Dictionary(StringComparer.OrdinalIgnoreCase) { + ["Fit"] = "Fit", + ["Fill"] = "Fill", + ["Stretch"] = "Stretch", + ["Fit Only"] = "Fit Only", + ["Fill Only"] = "Fill Only", + ["Stretch Only"] = "Stretch Only", + }; + private static string ZoomHr(string x) => _ZoomToHr.TryGetValue(x, out var h) ? h : x; + private static string ZoomXml(string h) => _ZoomFromHr.TryGetValue(h, out var x) ? x : h; + + private static readonly IReadOnlyDictionary _SequenceToHr = + new Dictionary(StringComparer.Ordinal) { + ["None"] = "None", + ["Next"] = "Next", + ["Previous"] = "Previous", + }; + private static readonly IReadOnlyDictionary _SequenceFromHr = + new Dictionary(StringComparer.OrdinalIgnoreCase) { + ["None"] = "None", + ["Next"] = "Next", + ["Previous"] = "Previous", + }; + private static string SequenceHr(string x) => _SequenceToHr.TryGetValue(x, out var h) ? h : x; + private static string SequenceXml(string h) => _SequenceFromHr.TryGetValue(h, out var x) ? x : h; + + public override XElement ToXml() => + new("Step", + new XAttribute("enable", Enabled ? "True" : "False"), + new XAttribute("id", XmlId), + new XAttribute("name", XmlName), + new XElement("Presentation", new XAttribute("value", Presentation)), + new XElement("DisableInteraction", new XAttribute("value", DisableInteraction ? "True" : "False")), + new XElement("HideControls", new XAttribute("value", HideControls ? "True" : "False")), + new XElement("DisableExternalControls", new XAttribute("value", DisableExternalControls ? "True" : "False")), + new XElement("PauseInBackground", new XAttribute("value", PauseInBackground ? "True" : "False")), + new XElement("PlaybackPosition", Position.ToXml("Calculation")), + new XElement("StartOffset", StartOffset.ToXml("Calculation")), + new XElement("EndOffset", EndOffset.ToXml("Calculation")), + new XElement("Volume", Volume.ToXml("Calculation")), + new XElement("Zoom", new XAttribute("value", Zoom)), + new XElement("Sequence", new XAttribute("value", Sequence))); + + public override string ToDisplayLine() => + "AVPlayer Set Options [ " + "Presentation: " + PresentationHr(Presentation) + " ; " + "Disable Interaction: " + (DisableInteraction ? "On" : "Off") + " ; " + "Hide Controls: " + (HideControls ? "On" : "Off") + " ; " + "Disable External Controls: " + (DisableExternalControls ? "On" : "Off") + " ; " + "Pause in Background: " + (PauseInBackground ? "On" : "Off") + " ; " + "Position: " + Position.Text + " ; " + "Start Offset: " + StartOffset.Text + " ; " + "End Offset: " + EndOffset.Text + " ; " + "Volume: " + Volume.Text + " ; " + "Zoom: " + ZoomHr(Zoom) + " ; " + "Sequence: " + SequenceHr(Sequence) + " ]"; + + public static new ScriptStep FromXml(XElement step) + { + var enabled = step.Attribute("enable")?.Value != "False"; + var presentation_v = step.Element("Presentation")?.Attribute("value")?.Value ?? "Start Full Screen"; + var disableInteraction_v = step.Element("DisableInteraction")?.Attribute("value")?.Value == "True"; + var hideControls_v = step.Element("HideControls")?.Attribute("value")?.Value == "True"; + var disableExternalControls_v = step.Element("DisableExternalControls")?.Attribute("value")?.Value == "True"; + var pauseInBackground_v = step.Element("PauseInBackground")?.Attribute("value")?.Value == "True"; + var position_vWrapEl = step.Element("PlaybackPosition"); + var position_vCalcEl = position_vWrapEl?.Element("Calculation"); + var position_v = position_vCalcEl is not null ? Calculation.FromXml(position_vCalcEl) : new Calculation(""); + var startOffset_vWrapEl = step.Element("StartOffset"); + var startOffset_vCalcEl = startOffset_vWrapEl?.Element("Calculation"); + var startOffset_v = startOffset_vCalcEl is not null ? Calculation.FromXml(startOffset_vCalcEl) : new Calculation(""); + var endOffset_vWrapEl = step.Element("EndOffset"); + var endOffset_vCalcEl = endOffset_vWrapEl?.Element("Calculation"); + var endOffset_v = endOffset_vCalcEl is not null ? Calculation.FromXml(endOffset_vCalcEl) : new Calculation(""); + var volume_vWrapEl = step.Element("Volume"); + var volume_vCalcEl = volume_vWrapEl?.Element("Calculation"); + var volume_v = volume_vCalcEl is not null ? Calculation.FromXml(volume_vCalcEl) : new Calculation(""); + var zoom_v = step.Element("Zoom")?.Attribute("value")?.Value ?? "Fit"; + var sequence_v = step.Element("Sequence")?.Attribute("value")?.Value ?? "None"; + return new AVPlayerSetOptionsStep(presentation_v, disableInteraction_v, hideControls_v, disableExternalControls_v, pauseInBackground_v, position_v, startOffset_v, endOffset_v, volume_v, zoom_v, sequence_v, enabled); + } + + public static ScriptStep FromDisplayParams(bool enabled, string[] hrParams) + { + var tokens = hrParams.Select(h => h.Trim()).ToArray(); + string presentation_v = "Start Full Screen"; + foreach (var tok in tokens) { if (tok.StartsWith("Presentation:", StringComparison.OrdinalIgnoreCase)) { var v = tok.Substring(13).Trim(); presentation_v = PresentationXml(v); break; } } + bool disableInteraction_v = false; + foreach (var tok in tokens) { if (tok.StartsWith("Disable Interaction:", StringComparison.OrdinalIgnoreCase)) { var v = tok.Substring(20).Trim(); disableInteraction_v = v.Equals("On", StringComparison.OrdinalIgnoreCase); break; } } + bool hideControls_v = false; + foreach (var tok in tokens) { if (tok.StartsWith("Hide Controls:", StringComparison.OrdinalIgnoreCase)) { var v = tok.Substring(14).Trim(); hideControls_v = v.Equals("On", StringComparison.OrdinalIgnoreCase); break; } } + bool disableExternalControls_v = false; + foreach (var tok in tokens) { if (tok.StartsWith("Disable External Controls:", StringComparison.OrdinalIgnoreCase)) { var v = tok.Substring(26).Trim(); disableExternalControls_v = v.Equals("On", StringComparison.OrdinalIgnoreCase); break; } } + bool pauseInBackground_v = false; + foreach (var tok in tokens) { if (tok.StartsWith("Pause in Background:", StringComparison.OrdinalIgnoreCase)) { var v = tok.Substring(20).Trim(); pauseInBackground_v = v.Equals("On", StringComparison.OrdinalIgnoreCase); break; } } + Calculation? position_v = null; + foreach (var tok in tokens) { if (tok.StartsWith("Position:", StringComparison.OrdinalIgnoreCase)) { position_v = new Calculation(tok.Substring(9).Trim()); break; } } + Calculation? startOffset_v = null; + foreach (var tok in tokens) { if (tok.StartsWith("Start Offset:", StringComparison.OrdinalIgnoreCase)) { startOffset_v = new Calculation(tok.Substring(13).Trim()); break; } } + Calculation? endOffset_v = null; + foreach (var tok in tokens) { if (tok.StartsWith("End Offset:", StringComparison.OrdinalIgnoreCase)) { endOffset_v = new Calculation(tok.Substring(11).Trim()); break; } } + Calculation? volume_v = null; + foreach (var tok in tokens) { if (tok.StartsWith("Volume:", StringComparison.OrdinalIgnoreCase)) { volume_v = new Calculation(tok.Substring(7).Trim()); break; } } + string zoom_v = "Fit"; + foreach (var tok in tokens) { if (tok.StartsWith("Zoom:", StringComparison.OrdinalIgnoreCase)) { var v = tok.Substring(5).Trim(); zoom_v = ZoomXml(v); break; } } + string sequence_v = "None"; + foreach (var tok in tokens) { if (tok.StartsWith("Sequence:", StringComparison.OrdinalIgnoreCase)) { var v = tok.Substring(9).Trim(); sequence_v = SequenceXml(v); break; } } + return new AVPlayerSetOptionsStep(presentation_v, disableInteraction_v, hideControls_v, disableExternalControls_v, pauseInBackground_v, position_v, startOffset_v, endOffset_v, volume_v, zoom_v, sequence_v, enabled); + } + + public static StepMetadata Metadata { get; } = new() + { + Name = XmlName, + Id = XmlId, + Category = "miscellaneous", + HelpUrl = "https://help.claris.com/en/pro-help/content/avplayer-set-options.html", + Params = + [ + new ParamMetadata + { + Name = "Presentation", + XmlElement = "Presentation", + Type = "enum", + XmlAttr = "value", + HrLabel = "Presentation", + ValidValues = ["Start Full Screen", "Full Screen Only", "Start Embedded", "Embedded Only", "Audio Only"], + DefaultValue = "Start Full Screen", + }, + new ParamMetadata + { + Name = "DisableInteraction", + XmlElement = "DisableInteraction", + Type = "boolean", + XmlAttr = "value", + HrLabel = "Disable Interaction", + ValidValues = ["On", "Off"], + DefaultValue = "False", + }, + new ParamMetadata + { + Name = "HideControls", + XmlElement = "HideControls", + Type = "boolean", + XmlAttr = "value", + HrLabel = "Hide Controls", + ValidValues = ["On", "Off"], + DefaultValue = "False", + }, + new ParamMetadata + { + Name = "DisableExternalControls", + XmlElement = "DisableExternalControls", + Type = "boolean", + XmlAttr = "value", + HrLabel = "Disable External Controls", + ValidValues = ["On", "Off"], + DefaultValue = "False", + }, + new ParamMetadata + { + Name = "PauseInBackground", + XmlElement = "PauseInBackground", + Type = "boolean", + XmlAttr = "value", + HrLabel = "Pause in Background", + ValidValues = ["On", "Off"], + DefaultValue = "False", + }, + new ParamMetadata + { + Name = "Calculation", + XmlElement = "Calculation", + Type = "namedCalc", + HrLabel = "Position", + }, + new ParamMetadata + { + Name = "Calculation", + XmlElement = "Calculation", + Type = "namedCalc", + HrLabel = "Start Offset", + }, + new ParamMetadata + { + Name = "Calculation", + XmlElement = "Calculation", + Type = "namedCalc", + HrLabel = "End Offset", + }, + new ParamMetadata + { + Name = "Calculation", + XmlElement = "Calculation", + Type = "namedCalc", + HrLabel = "Volume", + }, + new ParamMetadata + { + Name = "Zoom", + XmlElement = "Zoom", + Type = "enum", + XmlAttr = "value", + HrLabel = "Zoom", + ValidValues = ["Fit", "Fill", "Stretch", "Fit Only", "Fill Only", "Stretch Only"], + DefaultValue = "Fit", + }, + new ParamMetadata + { + Name = "Sequence", + XmlElement = "Sequence", + Type = "enum", + XmlAttr = "value", + HrLabel = "Sequence", + ValidValues = ["None", "Next", "Previous"], + DefaultValue = "None", + }, + ], + FromXml = FromXml, + FromDisplay = FromDisplayParams, + }; +} diff --git a/src/SharpFM.Model/Scripting/Steps/AddAccountStep.cs b/src/SharpFM.Model/Scripting/Steps/AddAccountStep.cs new file mode 100644 index 0000000..85aed24 --- /dev/null +++ b/src/SharpFM.Model/Scripting/Steps/AddAccountStep.cs @@ -0,0 +1,157 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Values; + +namespace SharpFM.Model.Scripting.Steps; + +public sealed class AddAccountStep : ScriptStep, IStepFactory +{ + public const int XmlId = 134; + public const string XmlName = "Add Account"; + + public string AuthenticateVia { get; set; } + public Calculation AccountName { get; set; } + public Calculation Password { get; set; } + public string PrivilegeSet { get; set; } + public bool ExpirePassword { get; set; } + + public AddAccountStep( + string authenticateVia = "FileMaker", + Calculation? accountName = null, + Calculation? password = null, + string privilegeSet = "", + bool expirePassword = false, + bool enabled = true) + : base(null, enabled) + { + AuthenticateVia = authenticateVia; + AccountName = accountName ?? new Calculation(""); + Password = password ?? new Calculation(""); + PrivilegeSet = privilegeSet; + ExpirePassword = expirePassword; + } + + private static readonly IReadOnlyDictionary _AuthenticateViaToHr = + new Dictionary(StringComparer.Ordinal) { + ["FileMaker"] = "FileMaker", + ["External Server"] = "External Server", + ["Apple Account"] = "Apple Account", + ["Amazon"] = "Amazon", + ["Google"] = "Google", + ["Microsoft Entra ID"] = "Microsoft Entra ID", + ["Custom OAuth"] = "Custom OAuth", + }; + private static readonly IReadOnlyDictionary _AuthenticateViaFromHr = + new Dictionary(StringComparer.OrdinalIgnoreCase) { + ["FileMaker"] = "FileMaker", + ["External Server"] = "External Server", + ["Apple Account"] = "Apple Account", + ["Amazon"] = "Amazon", + ["Google"] = "Google", + ["Microsoft Entra ID"] = "Microsoft Entra ID", + ["Custom OAuth"] = "Custom OAuth", + }; + private static string AuthenticateViaHr(string x) => _AuthenticateViaToHr.TryGetValue(x, out var h) ? h : x; + private static string AuthenticateViaXml(string h) => _AuthenticateViaFromHr.TryGetValue(h, out var x) ? x : h; + + public override XElement ToXml() => + new("Step", + new XAttribute("enable", Enabled ? "True" : "False"), + new XAttribute("id", XmlId), + new XAttribute("name", XmlName), + new XElement("AccountType", new XAttribute("value", AuthenticateVia)), + new XElement("AccountName", AccountName.ToXml("Calculation")), + new XElement("Password", Password.ToXml("Calculation")), + new XElement("PrivilegeSet", PrivilegeSet), + new XElement("ChgPwdOnNextLogin", new XAttribute("value", ExpirePassword ? "True" : "False"))); + + public override string ToDisplayLine() => + "Add Account [ " + "Authenticate via: " + AuthenticateViaHr(AuthenticateVia) + " ; " + "Account Name: " + AccountName.Text + " ; " + "Password: " + Password.Text + " ; " + "Privilege Set: " + PrivilegeSet + " ; " + "Expire password: " + (ExpirePassword ? "On" : "Off") + " ]"; + + public static new ScriptStep FromXml(XElement step) + { + var enabled = step.Attribute("enable")?.Value != "False"; + var authenticateVia_v = step.Element("AccountType")?.Attribute("value")?.Value ?? "FileMaker"; + var accountName_vWrapEl = step.Element("AccountName"); + var accountName_vCalcEl = accountName_vWrapEl?.Element("Calculation"); + var accountName_v = accountName_vCalcEl is not null ? Calculation.FromXml(accountName_vCalcEl) : new Calculation(""); + var password_vWrapEl = step.Element("Password"); + var password_vCalcEl = password_vWrapEl?.Element("Calculation"); + var password_v = password_vCalcEl is not null ? Calculation.FromXml(password_vCalcEl) : new Calculation(""); + var privilegeSet_v = step.Element("PrivilegeSet")?.Value ?? ""; + var expirePassword_v = step.Element("ChgPwdOnNextLogin")?.Attribute("value")?.Value == "True"; + return new AddAccountStep(authenticateVia_v, accountName_v, password_v, privilegeSet_v, expirePassword_v, enabled); + } + + public static ScriptStep FromDisplayParams(bool enabled, string[] hrParams) + { + var tokens = hrParams.Select(h => h.Trim()).ToArray(); + string authenticateVia_v = "FileMaker"; + foreach (var tok in tokens) { if (tok.StartsWith("Authenticate via:", StringComparison.OrdinalIgnoreCase)) { var v = tok.Substring(17).Trim(); authenticateVia_v = AuthenticateViaXml(v); break; } } + Calculation? accountName_v = null; + foreach (var tok in tokens) { if (tok.StartsWith("Account Name:", StringComparison.OrdinalIgnoreCase)) { accountName_v = new Calculation(tok.Substring(13).Trim()); break; } } + Calculation? password_v = null; + foreach (var tok in tokens) { if (tok.StartsWith("Password:", StringComparison.OrdinalIgnoreCase)) { password_v = new Calculation(tok.Substring(9).Trim()); break; } } + string privilegeSet_v = ""; + foreach (var tok in tokens) { if (tok.StartsWith("Privilege Set:", StringComparison.OrdinalIgnoreCase)) { privilegeSet_v = tok.Substring(14).Trim(); break; } } + bool expirePassword_v = false; + foreach (var tok in tokens) { if (tok.StartsWith("Expire password:", StringComparison.OrdinalIgnoreCase)) { var v = tok.Substring(16).Trim(); expirePassword_v = v.Equals("On", StringComparison.OrdinalIgnoreCase); break; } } + return new AddAccountStep(authenticateVia_v, accountName_v, password_v, privilegeSet_v, expirePassword_v, enabled); + } + + public static StepMetadata Metadata { get; } = new() + { + Name = XmlName, + Id = XmlId, + Category = "accounts", + HelpUrl = "https://help.claris.com/en/pro-help/content/add-account.html", + Params = + [ + new ParamMetadata + { + Name = "AccountType", + XmlElement = "AccountType", + Type = "enum", + HrLabel = "Authenticate via", + ValidValues = ["FileMaker", "External Server", "Apple Account", "Amazon", "Google", "Microsoft Entra ID", "Custom OAuth"], + DefaultValue = "FileMaker", + }, + new ParamMetadata + { + Name = "Calculation", + XmlElement = "Calculation", + Type = "namedCalc", + HrLabel = "Account Name", + }, + new ParamMetadata + { + Name = "Calculation", + XmlElement = "Calculation", + Type = "namedCalc", + HrLabel = "Password", + }, + new ParamMetadata + { + Name = "PrivilegeSet", + XmlElement = "PrivilegeSet", + Type = "text", + HrLabel = "Privilege Set", + }, + new ParamMetadata + { + Name = "ChgPwdOnNextLogin", + XmlElement = "ChgPwdOnNextLogin", + Type = "boolean", + XmlAttr = "value", + HrLabel = "Expire password", + ValidValues = ["On", "Off"], + DefaultValue = "False", + }, + ], + FromXml = FromXml, + FromDisplay = FromDisplayParams, + }; +} diff --git a/src/SharpFM.Model/Scripting/Steps/ChangePasswordStep.cs b/src/SharpFM.Model/Scripting/Steps/ChangePasswordStep.cs new file mode 100644 index 0000000..6921b65 --- /dev/null +++ b/src/SharpFM.Model/Scripting/Steps/ChangePasswordStep.cs @@ -0,0 +1,104 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Values; + +namespace SharpFM.Model.Scripting.Steps; + +public sealed class ChangePasswordStep : ScriptStep, IStepFactory +{ + public const int XmlId = 83; + public const string XmlName = "Change Password"; + + public Calculation OldPassword { get; set; } + public Calculation Password { get; set; } + public bool WithDialog { get; set; } + + public ChangePasswordStep( + Calculation? oldPassword = null, + Calculation? password = null, + bool withDialog = false, + bool enabled = true) + : base(null, enabled) + { + OldPassword = oldPassword ?? new Calculation(""); + Password = password ?? new Calculation(""); + WithDialog = withDialog; + } + + public override XElement ToXml() => + new("Step", + new XAttribute("enable", Enabled ? "True" : "False"), + new XAttribute("id", XmlId), + new XAttribute("name", XmlName), + new XElement("OldPassword", OldPassword.ToXml("Calculation")), + new XElement("NewPassword", Password.ToXml("Calculation")), + new XElement("NoInteract", new XAttribute("state", WithDialog ? "True" : "False"))); + + public override string ToDisplayLine() => + "Change Password [ " + "Old Password: " + OldPassword.Text + " ; " + "Password: " + Password.Text + " ; " + "With dialog: " + (WithDialog ? "On" : "Off") + " ]"; + + public static new ScriptStep FromXml(XElement step) + { + var enabled = step.Attribute("enable")?.Value != "False"; + var oldPassword_vWrapEl = step.Element("OldPassword"); + var oldPassword_vCalcEl = oldPassword_vWrapEl?.Element("Calculation"); + var oldPassword_v = oldPassword_vCalcEl is not null ? Calculation.FromXml(oldPassword_vCalcEl) : new Calculation(""); + var password_vWrapEl = step.Element("NewPassword"); + var password_vCalcEl = password_vWrapEl?.Element("Calculation"); + var password_v = password_vCalcEl is not null ? Calculation.FromXml(password_vCalcEl) : new Calculation(""); + var withDialog_v = step.Element("NoInteract")?.Attribute("state")?.Value == "True"; + return new ChangePasswordStep(oldPassword_v, password_v, withDialog_v, enabled); + } + + public static ScriptStep FromDisplayParams(bool enabled, string[] hrParams) + { + var tokens = hrParams.Select(h => h.Trim()).ToArray(); + Calculation? oldPassword_v = null; + foreach (var tok in tokens) { if (tok.StartsWith("Old Password:", StringComparison.OrdinalIgnoreCase)) { oldPassword_v = new Calculation(tok.Substring(13).Trim()); break; } } + Calculation? password_v = null; + foreach (var tok in tokens) { if (tok.StartsWith("Password:", StringComparison.OrdinalIgnoreCase)) { password_v = new Calculation(tok.Substring(9).Trim()); break; } } + bool withDialog_v = false; + foreach (var tok in tokens) { if (tok.StartsWith("With dialog:", StringComparison.OrdinalIgnoreCase)) { var v = tok.Substring(12).Trim(); withDialog_v = v.Equals("On", StringComparison.OrdinalIgnoreCase); break; } } + return new ChangePasswordStep(oldPassword_v, password_v, withDialog_v, enabled); + } + + public static StepMetadata Metadata { get; } = new() + { + Name = XmlName, + Id = XmlId, + Category = "accounts", + HelpUrl = "https://help.claris.com/en/pro-help/content/change-password.html", + Params = + [ + new ParamMetadata + { + Name = "Calculation", + XmlElement = "Calculation", + Type = "namedCalc", + HrLabel = "Old Password", + }, + new ParamMetadata + { + Name = "Calculation", + XmlElement = "Calculation", + Type = "namedCalc", + HrLabel = "Password", + }, + new ParamMetadata + { + Name = "NoInteract", + XmlElement = "NoInteract", + Type = "boolean", + XmlAttr = "state", + HrLabel = "With dialog", + ValidValues = ["On", "Off"], + DefaultValue = "False", + }, + ], + FromXml = FromXml, + FromDisplay = FromDisplayParams, + }; +} diff --git a/src/SharpFM.Model/Scripting/Steps/CloseDataFileStep.cs b/src/SharpFM.Model/Scripting/Steps/CloseDataFileStep.cs new file mode 100644 index 0000000..d71440a --- /dev/null +++ b/src/SharpFM.Model/Scripting/Steps/CloseDataFileStep.cs @@ -0,0 +1,70 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Values; + +namespace SharpFM.Model.Scripting.Steps; + +public sealed class CloseDataFileStep : ScriptStep, IStepFactory +{ + public const int XmlId = 196; + public const string XmlName = "Close Data File"; + + public Calculation FileID { get; set; } + + public CloseDataFileStep( + Calculation? fileID = null, + bool enabled = true) + : base(null, enabled) + { + FileID = fileID ?? new Calculation(""); + } + + public override XElement ToXml() => + new("Step", + new XAttribute("enable", Enabled ? "True" : "False"), + new XAttribute("id", XmlId), + new XAttribute("name", XmlName), + FileID.ToXml("Calculation")); + + public override string ToDisplayLine() => + "Close Data File [ " + "File ID: " + FileID.Text + " ]"; + + public static new ScriptStep FromXml(XElement step) + { + var enabled = step.Attribute("enable")?.Value != "False"; + var fileID_vEl = step.Element("Calculation"); + var fileID_v = fileID_vEl is not null ? Calculation.FromXml(fileID_vEl) : new Calculation(""); + return new CloseDataFileStep(fileID_v, enabled); + } + + public static ScriptStep FromDisplayParams(bool enabled, string[] hrParams) + { + var tokens = hrParams.Select(h => h.Trim()).ToArray(); + Calculation? fileID_v = null; + foreach (var tok in tokens) { if (tok.StartsWith("File ID:", StringComparison.OrdinalIgnoreCase)) { fileID_v = new Calculation(tok.Substring(8).Trim()); break; } } + return new CloseDataFileStep(fileID_v, enabled); + } + + public static StepMetadata Metadata { get; } = new() + { + Name = XmlName, + Id = XmlId, + Category = "files", + HelpUrl = "https://help.claris.com/en/pro-help/content/close-data-file.html", + Params = + [ + new ParamMetadata + { + Name = "Calculation", + XmlElement = "Calculation", + Type = "calculation", + HrLabel = "File ID", + }, + ], + FromXml = FromXml, + FromDisplay = FromDisplayParams, + }; +} diff --git a/src/SharpFM.Model/Scripting/Steps/CloseWindowStep.cs b/src/SharpFM.Model/Scripting/Steps/CloseWindowStep.cs new file mode 100644 index 0000000..b2ebae3 --- /dev/null +++ b/src/SharpFM.Model/Scripting/Steps/CloseWindowStep.cs @@ -0,0 +1,113 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Values; + +namespace SharpFM.Model.Scripting.Steps; + +public sealed class CloseWindowStep : ScriptStep, IStepFactory +{ + public const int XmlId = 121; + public const string XmlName = "Close Window"; + + public bool LimitToWindowsOfCurrentFile { get; set; } + public string Window { get; set; } + public Calculation Calculation { get; set; } + + public CloseWindowStep( + bool limitToWindowsOfCurrentFile = true, + string window = "ByName", + Calculation? calculation = null, + bool enabled = true) + : base(null, enabled) + { + LimitToWindowsOfCurrentFile = limitToWindowsOfCurrentFile; + Window = window; + Calculation = calculation ?? new Calculation(""); + } + + private static readonly IReadOnlyDictionary _WindowToHr = + new Dictionary(StringComparer.Ordinal) { + ["ByName"] = "ByName", + ["Current"] = "Current", + }; + private static readonly IReadOnlyDictionary _WindowFromHr = + new Dictionary(StringComparer.OrdinalIgnoreCase) { + ["ByName"] = "ByName", + ["Current"] = "Current", + }; + private static string WindowHr(string x) => _WindowToHr.TryGetValue(x, out var h) ? h : x; + private static string WindowXml(string h) => _WindowFromHr.TryGetValue(h, out var x) ? x : h; + + public override XElement ToXml() => + new("Step", + new XAttribute("enable", Enabled ? "True" : "False"), + new XAttribute("id", XmlId), + new XAttribute("name", XmlName), + new XElement("LimitToWindowsOfCurrentFile", new XAttribute("state", LimitToWindowsOfCurrentFile ? "True" : "False")), + new XElement("Window", new XAttribute("value", Window)), + new XElement("Name", Calculation.ToXml("Calculation"))); + + public override string ToDisplayLine() => + "Close Window [ " + (LimitToWindowsOfCurrentFile ? "On" : "Off") + " ; " + WindowHr(Window) + " ; " + Calculation.Text + " ]"; + + public static new ScriptStep FromXml(XElement step) + { + var enabled = step.Attribute("enable")?.Value != "False"; + var limitToWindowsOfCurrentFile_v = step.Element("LimitToWindowsOfCurrentFile")?.Attribute("state")?.Value == "True"; + var window_v = step.Element("Window")?.Attribute("value")?.Value ?? "ByName"; + var calculation_vWrapEl = step.Element("Name"); + var calculation_vCalcEl = calculation_vWrapEl?.Element("Calculation"); + var calculation_v = calculation_vCalcEl is not null ? Calculation.FromXml(calculation_vCalcEl) : new Calculation(""); + return new CloseWindowStep(limitToWindowsOfCurrentFile_v, window_v, calculation_v, enabled); + } + + public static ScriptStep FromDisplayParams(bool enabled, string[] hrParams) + { + var tokens = hrParams.Select(h => h.Trim()).ToArray(); + bool limitToWindowsOfCurrentFile_v = true; + string window_v = "ByName"; + Calculation? calculation_v = null; + foreach (var tok in tokens) { if (!(false)) { calculation_v = new Calculation(tok); break; } } + return new CloseWindowStep(limitToWindowsOfCurrentFile_v, window_v, calculation_v, enabled); + } + + public static StepMetadata Metadata { get; } = new() + { + Name = XmlName, + Id = XmlId, + Category = "windows", + HelpUrl = "https://help.claris.com/en/pro-help/content/close-window.html", + Params = + [ + new ParamMetadata + { + Name = "LimitToWindowsOfCurrentFile", + XmlElement = "LimitToWindowsOfCurrentFile", + Type = "boolean", + XmlAttr = "state", + ValidValues = ["On", "Off"], + DefaultValue = "True", + }, + new ParamMetadata + { + Name = "Window", + XmlElement = "Window", + Type = "enum", + XmlAttr = "value", + ValidValues = ["ByName", "Current"], + DefaultValue = "ByName", + }, + new ParamMetadata + { + Name = "Calculation", + XmlElement = "Calculation", + Type = "namedCalc", + }, + ], + FromXml = FromXml, + FromDisplay = FromDisplayParams, + }; +} diff --git a/src/SharpFM.Model/Scripting/Steps/ConfigureAIAccountStep.cs b/src/SharpFM.Model/Scripting/Steps/ConfigureAIAccountStep.cs new file mode 100644 index 0000000..5a1cc2b --- /dev/null +++ b/src/SharpFM.Model/Scripting/Steps/ConfigureAIAccountStep.cs @@ -0,0 +1,153 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Values; + +namespace SharpFM.Model.Scripting.Steps; + +public sealed class ConfigureAIAccountStep : ScriptStep, IStepFactory +{ + public const int XmlId = 212; + public const string XmlName = "Configure AI Account"; + + public Calculation AccountName { get; set; } + public string ModelProvider { get; set; } + public Calculation Endpoint { get; set; } + public bool VerifySSLCertificates { get; set; } + public Calculation APIKey { get; set; } + + public ConfigureAIAccountStep( + Calculation? accountName = null, + string modelProvider = "OpenAI", + Calculation? endpoint = null, + bool verifySSLCertificates = false, + Calculation? aPIKey = null, + bool enabled = true) + : base(null, enabled) + { + AccountName = accountName ?? new Calculation(""); + ModelProvider = modelProvider; + Endpoint = endpoint ?? new Calculation(""); + VerifySSLCertificates = verifySSLCertificates; + APIKey = aPIKey ?? new Calculation(""); + } + + private static readonly IReadOnlyDictionary _ModelProviderToHr = + new Dictionary(StringComparer.Ordinal) { + ["OpenAI"] = "OpenAI", + ["Anthropic"] = "Anthropic", + ["Cohere"] = "Cohere", + ["Custom"] = "Custom", + }; + private static readonly IReadOnlyDictionary _ModelProviderFromHr = + new Dictionary(StringComparer.OrdinalIgnoreCase) { + ["OpenAI"] = "OpenAI", + ["Anthropic"] = "Anthropic", + ["Cohere"] = "Cohere", + ["Custom"] = "Custom", + }; + private static string ModelProviderHr(string x) => _ModelProviderToHr.TryGetValue(x, out var h) ? h : x; + private static string ModelProviderXml(string h) => _ModelProviderFromHr.TryGetValue(h, out var x) ? x : h; + + public override XElement ToXml() => + new("Step", + new XAttribute("enable", Enabled ? "True" : "False"), + new XAttribute("id", XmlId), + new XAttribute("name", XmlName), + new XElement("AccoutName", AccountName.ToXml("Calculation")), + new XElement("LLMType", new XAttribute("value", ModelProvider)), + new XElement("Endpoint", Endpoint.ToXml("Calculation")), + new XElement("VerifySSLCertificates", new XAttribute("state", VerifySSLCertificates ? "True" : "False")), + new XElement("AccessAPIKey", APIKey.ToXml("Calculation"))); + + public override string ToDisplayLine() => + "Configure AI Account [ " + "Account Name: " + AccountName.Text + " ; " + "Model Provider: " + ModelProviderHr(ModelProvider) + " ; " + "Endpoint: " + Endpoint.Text + " ; " + "Verify SSL Certificates: " + (VerifySSLCertificates ? "On" : "Off") + " ; " + "API key: " + APIKey.Text + " ]"; + + public static new ScriptStep FromXml(XElement step) + { + var enabled = step.Attribute("enable")?.Value != "False"; + var accountName_vWrapEl = step.Element("AccoutName"); + var accountName_vCalcEl = accountName_vWrapEl?.Element("Calculation"); + var accountName_v = accountName_vCalcEl is not null ? Calculation.FromXml(accountName_vCalcEl) : new Calculation(""); + var modelProvider_v = step.Element("LLMType")?.Attribute("value")?.Value ?? "OpenAI"; + var endpoint_vWrapEl = step.Element("Endpoint"); + var endpoint_vCalcEl = endpoint_vWrapEl?.Element("Calculation"); + var endpoint_v = endpoint_vCalcEl is not null ? Calculation.FromXml(endpoint_vCalcEl) : new Calculation(""); + var verifySSLCertificates_v = step.Element("VerifySSLCertificates")?.Attribute("state")?.Value == "True"; + var aPIKey_vWrapEl = step.Element("AccessAPIKey"); + var aPIKey_vCalcEl = aPIKey_vWrapEl?.Element("Calculation"); + var aPIKey_v = aPIKey_vCalcEl is not null ? Calculation.FromXml(aPIKey_vCalcEl) : new Calculation(""); + return new ConfigureAIAccountStep(accountName_v, modelProvider_v, endpoint_v, verifySSLCertificates_v, aPIKey_v, enabled); + } + + public static ScriptStep FromDisplayParams(bool enabled, string[] hrParams) + { + var tokens = hrParams.Select(h => h.Trim()).ToArray(); + Calculation? accountName_v = null; + foreach (var tok in tokens) { if (tok.StartsWith("Account Name:", StringComparison.OrdinalIgnoreCase)) { accountName_v = new Calculation(tok.Substring(13).Trim()); break; } } + string modelProvider_v = "OpenAI"; + foreach (var tok in tokens) { if (tok.StartsWith("Model Provider:", StringComparison.OrdinalIgnoreCase)) { var v = tok.Substring(15).Trim(); modelProvider_v = ModelProviderXml(v); break; } } + Calculation? endpoint_v = null; + foreach (var tok in tokens) { if (tok.StartsWith("Endpoint:", StringComparison.OrdinalIgnoreCase)) { endpoint_v = new Calculation(tok.Substring(9).Trim()); break; } } + bool verifySSLCertificates_v = false; + foreach (var tok in tokens) { if (tok.StartsWith("Verify SSL Certificates:", StringComparison.OrdinalIgnoreCase)) { var v = tok.Substring(24).Trim(); verifySSLCertificates_v = v.Equals("On", StringComparison.OrdinalIgnoreCase); break; } } + Calculation? aPIKey_v = null; + foreach (var tok in tokens) { if (tok.StartsWith("API key:", StringComparison.OrdinalIgnoreCase)) { aPIKey_v = new Calculation(tok.Substring(8).Trim()); break; } } + return new ConfigureAIAccountStep(accountName_v, modelProvider_v, endpoint_v, verifySSLCertificates_v, aPIKey_v, enabled); + } + + public static StepMetadata Metadata { get; } = new() + { + Name = XmlName, + Id = XmlId, + Category = "artificial intelligence", + Params = + [ + new ParamMetadata + { + Name = "Calculation", + XmlElement = "Calculation", + Type = "namedCalc", + HrLabel = "Account Name", + }, + new ParamMetadata + { + Name = "LLMType", + XmlElement = "LLMType", + Type = "enum", + XmlAttr = "value", + HrLabel = "Model Provider", + ValidValues = ["OpenAI", "Anthropic", "Cohere", "Custom"], + DefaultValue = "OpenAI", + }, + new ParamMetadata + { + Name = "Calculation", + XmlElement = "Calculation", + Type = "namedCalc", + HrLabel = "Endpoint", + }, + new ParamMetadata + { + Name = "VerifySSLCertificates", + XmlElement = "VerifySSLCertificates", + Type = "boolean", + XmlAttr = "state", + HrLabel = "Verify SSL Certificates", + ValidValues = ["On", "Off"], + DefaultValue = "False", + }, + new ParamMetadata + { + Name = "Calculation", + XmlElement = "Calculation", + Type = "namedCalc", + HrLabel = "API key", + }, + ], + FromXml = FromXml, + FromDisplay = FromDisplayParams, + }; +} diff --git a/src/SharpFM.Model/Scripting/Steps/ConfigurePromptTemplateStep.cs b/src/SharpFM.Model/Scripting/Steps/ConfigurePromptTemplateStep.cs new file mode 100644 index 0000000..9640807 --- /dev/null +++ b/src/SharpFM.Model/Scripting/Steps/ConfigurePromptTemplateStep.cs @@ -0,0 +1,213 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Values; + +namespace SharpFM.Model.Scripting.Steps; + +public sealed class ConfigurePromptTemplateStep : ScriptStep, IStepFactory +{ + public const int XmlId = 226; + public const string XmlName = "Configure Prompt Template"; + + public Calculation TemplateName { get; set; } + public string ModelProvider { get; set; } + public string TemplateType { get; set; } + public Calculation SQLPrompt { get; set; } + public Calculation NaturalLanguagePrompt { get; set; } + public Calculation FindRequestPrompt { get; set; } + public Calculation RAGPrompt { get; set; } + public bool Option { get; set; } + + public ConfigurePromptTemplateStep( + Calculation? templateName = null, + string modelProvider = "OpenAI", + string templateType = "SQL Query", + Calculation? sQLPrompt = null, + Calculation? naturalLanguagePrompt = null, + Calculation? findRequestPrompt = null, + Calculation? rAGPrompt = null, + bool option = false, + bool enabled = true) + : base(null, enabled) + { + TemplateName = templateName ?? new Calculation(""); + ModelProvider = modelProvider; + TemplateType = templateType; + SQLPrompt = sQLPrompt ?? new Calculation(""); + NaturalLanguagePrompt = naturalLanguagePrompt ?? new Calculation(""); + FindRequestPrompt = findRequestPrompt ?? new Calculation(""); + RAGPrompt = rAGPrompt ?? new Calculation(""); + Option = option; + } + + private static readonly IReadOnlyDictionary _ModelProviderToHr = + new Dictionary(StringComparer.Ordinal) { + ["OpenAI"] = "OpenAI", + ["Anthropic"] = "Anthropic", + ["Cohere"] = "Cohere", + ["Custom"] = "Custom", + }; + private static readonly IReadOnlyDictionary _ModelProviderFromHr = + new Dictionary(StringComparer.OrdinalIgnoreCase) { + ["OpenAI"] = "OpenAI", + ["Anthropic"] = "Anthropic", + ["Cohere"] = "Cohere", + ["Custom"] = "Custom", + }; + private static string ModelProviderHr(string x) => _ModelProviderToHr.TryGetValue(x, out var h) ? h : x; + private static string ModelProviderXml(string h) => _ModelProviderFromHr.TryGetValue(h, out var x) ? x : h; + + private static readonly IReadOnlyDictionary _TemplateTypeToHr = + new Dictionary(StringComparer.Ordinal) { + ["SQL Query"] = "SQL Query", + ["Find Request"] = "Find Request", + ["RAG Prompt"] = "RAG Prompt", + }; + private static readonly IReadOnlyDictionary _TemplateTypeFromHr = + new Dictionary(StringComparer.OrdinalIgnoreCase) { + ["SQL Query"] = "SQL Query", + ["Find Request"] = "Find Request", + ["RAG Prompt"] = "RAG Prompt", + }; + private static string TemplateTypeHr(string x) => _TemplateTypeToHr.TryGetValue(x, out var h) ? h : x; + private static string TemplateTypeXml(string h) => _TemplateTypeFromHr.TryGetValue(h, out var x) ? x : h; + + public override XElement ToXml() => + new("Step", + new XAttribute("enable", Enabled ? "True" : "False"), + new XAttribute("id", XmlId), + new XAttribute("name", XmlName), + new XElement("TemplateName", TemplateName.ToXml("Calculation")), + new XElement("ModelProvider", new XAttribute("value", ModelProvider)), + new XElement("RequestType", new XAttribute("value", TemplateType)), + new XElement("SQLPrompt", SQLPrompt.ToXml("Calculation")), + new XElement("NaturalLanguagePrompt", NaturalLanguagePrompt.ToXml("Calculation")), + new XElement("FindRequestPrompt", FindRequestPrompt.ToXml("Calculation")), + new XElement("RAGPPrompt", RAGPrompt.ToXml("Calculation")), + new XElement("Option", new XAttribute("state", Option ? "True" : "False"))); + + public override string ToDisplayLine() => + "Configure Prompt Template [ " + "Template Name: " + TemplateName.Text + " ; " + "Model Provider: " + ModelProviderHr(ModelProvider) + " ; " + "Template Type: " + TemplateTypeHr(TemplateType) + " ; " + "SQL Prompt: " + SQLPrompt.Text + " ; " + "Natural Language Prompt: " + NaturalLanguagePrompt.Text + " ; " + "Find Request Prompt: " + FindRequestPrompt.Text + " ; " + "RAG Prompt: " + RAGPrompt.Text + " ; " + (Option ? "On" : "Off") + " ]"; + + public static new ScriptStep FromXml(XElement step) + { + var enabled = step.Attribute("enable")?.Value != "False"; + var templateName_vWrapEl = step.Element("TemplateName"); + var templateName_vCalcEl = templateName_vWrapEl?.Element("Calculation"); + var templateName_v = templateName_vCalcEl is not null ? Calculation.FromXml(templateName_vCalcEl) : new Calculation(""); + var modelProvider_v = step.Element("ModelProvider")?.Attribute("value")?.Value ?? "OpenAI"; + var templateType_v = step.Element("RequestType")?.Attribute("value")?.Value ?? "SQL Query"; + var sQLPrompt_vWrapEl = step.Element("SQLPrompt"); + var sQLPrompt_vCalcEl = sQLPrompt_vWrapEl?.Element("Calculation"); + var sQLPrompt_v = sQLPrompt_vCalcEl is not null ? Calculation.FromXml(sQLPrompt_vCalcEl) : new Calculation(""); + var naturalLanguagePrompt_vWrapEl = step.Element("NaturalLanguagePrompt"); + var naturalLanguagePrompt_vCalcEl = naturalLanguagePrompt_vWrapEl?.Element("Calculation"); + var naturalLanguagePrompt_v = naturalLanguagePrompt_vCalcEl is not null ? Calculation.FromXml(naturalLanguagePrompt_vCalcEl) : new Calculation(""); + var findRequestPrompt_vWrapEl = step.Element("FindRequestPrompt"); + var findRequestPrompt_vCalcEl = findRequestPrompt_vWrapEl?.Element("Calculation"); + var findRequestPrompt_v = findRequestPrompt_vCalcEl is not null ? Calculation.FromXml(findRequestPrompt_vCalcEl) : new Calculation(""); + var rAGPrompt_vWrapEl = step.Element("RAGPPrompt"); + var rAGPrompt_vCalcEl = rAGPrompt_vWrapEl?.Element("Calculation"); + var rAGPrompt_v = rAGPrompt_vCalcEl is not null ? Calculation.FromXml(rAGPrompt_vCalcEl) : new Calculation(""); + var option_v = step.Element("Option")?.Attribute("state")?.Value == "True"; + return new ConfigurePromptTemplateStep(templateName_v, modelProvider_v, templateType_v, sQLPrompt_v, naturalLanguagePrompt_v, findRequestPrompt_v, rAGPrompt_v, option_v, enabled); + } + + public static ScriptStep FromDisplayParams(bool enabled, string[] hrParams) + { + var tokens = hrParams.Select(h => h.Trim()).ToArray(); + Calculation? templateName_v = null; + foreach (var tok in tokens) { if (tok.StartsWith("Template Name:", StringComparison.OrdinalIgnoreCase)) { templateName_v = new Calculation(tok.Substring(14).Trim()); break; } } + string modelProvider_v = "OpenAI"; + foreach (var tok in tokens) { if (tok.StartsWith("Model Provider:", StringComparison.OrdinalIgnoreCase)) { var v = tok.Substring(15).Trim(); modelProvider_v = ModelProviderXml(v); break; } } + string templateType_v = "SQL Query"; + foreach (var tok in tokens) { if (tok.StartsWith("Template Type:", StringComparison.OrdinalIgnoreCase)) { var v = tok.Substring(14).Trim(); templateType_v = TemplateTypeXml(v); break; } } + Calculation? sQLPrompt_v = null; + foreach (var tok in tokens) { if (tok.StartsWith("SQL Prompt:", StringComparison.OrdinalIgnoreCase)) { sQLPrompt_v = new Calculation(tok.Substring(11).Trim()); break; } } + Calculation? naturalLanguagePrompt_v = null; + foreach (var tok in tokens) { if (tok.StartsWith("Natural Language Prompt:", StringComparison.OrdinalIgnoreCase)) { naturalLanguagePrompt_v = new Calculation(tok.Substring(24).Trim()); break; } } + Calculation? findRequestPrompt_v = null; + foreach (var tok in tokens) { if (tok.StartsWith("Find Request Prompt:", StringComparison.OrdinalIgnoreCase)) { findRequestPrompt_v = new Calculation(tok.Substring(20).Trim()); break; } } + Calculation? rAGPrompt_v = null; + foreach (var tok in tokens) { if (tok.StartsWith("RAG Prompt:", StringComparison.OrdinalIgnoreCase)) { rAGPrompt_v = new Calculation(tok.Substring(11).Trim()); break; } } + bool option_v = false; + return new ConfigurePromptTemplateStep(templateName_v, modelProvider_v, templateType_v, sQLPrompt_v, naturalLanguagePrompt_v, findRequestPrompt_v, rAGPrompt_v, option_v, enabled); + } + + public static StepMetadata Metadata { get; } = new() + { + Name = XmlName, + Id = XmlId, + Category = "artificial intelligence", + Params = + [ + new ParamMetadata + { + Name = "Calculation", + XmlElement = "Calculation", + Type = "namedCalc", + HrLabel = "Template Name", + }, + new ParamMetadata + { + Name = "ModelProvider", + XmlElement = "ModelProvider", + Type = "enum", + HrLabel = "Model Provider", + ValidValues = ["OpenAI", "Anthropic", "Cohere", "Custom"], + DefaultValue = "OpenAI", + }, + new ParamMetadata + { + Name = "RequestType", + XmlElement = "RequestType", + Type = "enum", + HrLabel = "Template Type", + ValidValues = ["SQL Query", "Find Request", "RAG Prompt"], + DefaultValue = "SQL Query", + }, + new ParamMetadata + { + Name = "Calculation", + XmlElement = "Calculation", + Type = "namedCalc", + HrLabel = "SQL Prompt", + }, + new ParamMetadata + { + Name = "Calculation", + XmlElement = "Calculation", + Type = "namedCalc", + HrLabel = "Natural Language Prompt", + }, + new ParamMetadata + { + Name = "Calculation", + XmlElement = "Calculation", + Type = "namedCalc", + HrLabel = "Find Request Prompt", + }, + new ParamMetadata + { + Name = "Calculation", + XmlElement = "Calculation", + Type = "namedCalc", + HrLabel = "RAG Prompt", + }, + new ParamMetadata + { + Name = "Option", + XmlElement = "Option", + Type = "boolean", + XmlAttr = "state", + ValidValues = ["On", "Off"], + DefaultValue = "False", + }, + ], + FromXml = FromXml, + FromDisplay = FromDisplayParams, + }; +} diff --git a/src/SharpFM.Model/Scripting/Steps/ConfigureRAGAccountStep.cs b/src/SharpFM.Model/Scripting/Steps/ConfigureRAGAccountStep.cs new file mode 100644 index 0000000..56ee0af --- /dev/null +++ b/src/SharpFM.Model/Scripting/Steps/ConfigureRAGAccountStep.cs @@ -0,0 +1,119 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Values; + +namespace SharpFM.Model.Scripting.Steps; + +public sealed class ConfigureRAGAccountStep : ScriptStep, IStepFactory +{ + public const int XmlId = 227; + public const string XmlName = "Configure RAG Account "; + + public Calculation RAGAccountName { get; set; } + public Calculation Endpoint { get; set; } + public Calculation APIKey { get; set; } + public bool VerifySSLCertificates { get; set; } + + public ConfigureRAGAccountStep( + Calculation? rAGAccountName = null, + Calculation? endpoint = null, + Calculation? aPIKey = null, + bool verifySSLCertificates = false, + bool enabled = true) + : base(null, enabled) + { + RAGAccountName = rAGAccountName ?? new Calculation(""); + Endpoint = endpoint ?? new Calculation(""); + APIKey = aPIKey ?? new Calculation(""); + VerifySSLCertificates = verifySSLCertificates; + } + + public override XElement ToXml() => + new("Step", + new XAttribute("enable", Enabled ? "True" : "False"), + new XAttribute("id", XmlId), + new XAttribute("name", XmlName), + new XElement("RAGAccountName", RAGAccountName.ToXml("Calculation")), + new XElement("Endpoint", Endpoint.ToXml("Calculation")), + new XElement("AccessAPIKey", APIKey.ToXml("Calculation")), + new XElement("VerifySSLCertificates", new XAttribute("state", VerifySSLCertificates ? "True" : "False"))); + + public override string ToDisplayLine() => + "Configure RAG Account [ " + "RAG Account Name: " + RAGAccountName.Text + " ; " + "Endpoint: " + Endpoint.Text + " ; " + "API key: " + APIKey.Text + " ; " + "Verify SSL Certificates: " + (VerifySSLCertificates ? "On" : "Off") + " ]"; + + public static new ScriptStep FromXml(XElement step) + { + var enabled = step.Attribute("enable")?.Value != "False"; + var rAGAccountName_vWrapEl = step.Element("RAGAccountName"); + var rAGAccountName_vCalcEl = rAGAccountName_vWrapEl?.Element("Calculation"); + var rAGAccountName_v = rAGAccountName_vCalcEl is not null ? Calculation.FromXml(rAGAccountName_vCalcEl) : new Calculation(""); + var endpoint_vWrapEl = step.Element("Endpoint"); + var endpoint_vCalcEl = endpoint_vWrapEl?.Element("Calculation"); + var endpoint_v = endpoint_vCalcEl is not null ? Calculation.FromXml(endpoint_vCalcEl) : new Calculation(""); + var aPIKey_vWrapEl = step.Element("AccessAPIKey"); + var aPIKey_vCalcEl = aPIKey_vWrapEl?.Element("Calculation"); + var aPIKey_v = aPIKey_vCalcEl is not null ? Calculation.FromXml(aPIKey_vCalcEl) : new Calculation(""); + var verifySSLCertificates_v = step.Element("VerifySSLCertificates")?.Attribute("state")?.Value == "True"; + return new ConfigureRAGAccountStep(rAGAccountName_v, endpoint_v, aPIKey_v, verifySSLCertificates_v, enabled); + } + + public static ScriptStep FromDisplayParams(bool enabled, string[] hrParams) + { + var tokens = hrParams.Select(h => h.Trim()).ToArray(); + Calculation? rAGAccountName_v = null; + foreach (var tok in tokens) { if (tok.StartsWith("RAG Account Name:", StringComparison.OrdinalIgnoreCase)) { rAGAccountName_v = new Calculation(tok.Substring(17).Trim()); break; } } + Calculation? endpoint_v = null; + foreach (var tok in tokens) { if (tok.StartsWith("Endpoint:", StringComparison.OrdinalIgnoreCase)) { endpoint_v = new Calculation(tok.Substring(9).Trim()); break; } } + Calculation? aPIKey_v = null; + foreach (var tok in tokens) { if (tok.StartsWith("API key:", StringComparison.OrdinalIgnoreCase)) { aPIKey_v = new Calculation(tok.Substring(8).Trim()); break; } } + bool verifySSLCertificates_v = false; + foreach (var tok in tokens) { if (tok.StartsWith("Verify SSL Certificates:", StringComparison.OrdinalIgnoreCase)) { var v = tok.Substring(24).Trim(); verifySSLCertificates_v = v.Equals("On", StringComparison.OrdinalIgnoreCase); break; } } + return new ConfigureRAGAccountStep(rAGAccountName_v, endpoint_v, aPIKey_v, verifySSLCertificates_v, enabled); + } + + public static StepMetadata Metadata { get; } = new() + { + Name = XmlName, + Id = XmlId, + Category = "artificial intelligence", + Params = + [ + new ParamMetadata + { + Name = "Calculation", + XmlElement = "Calculation", + Type = "namedCalc", + HrLabel = "RAG Account Name", + }, + new ParamMetadata + { + Name = "Calculation", + XmlElement = "Calculation", + Type = "namedCalc", + HrLabel = "Endpoint", + }, + new ParamMetadata + { + Name = "Calculation", + XmlElement = "Calculation", + Type = "namedCalc", + HrLabel = "API key", + }, + new ParamMetadata + { + Name = "VerifySSLCertificates", + XmlElement = "VerifySSLCertificates", + Type = "boolean", + XmlAttr = "state", + HrLabel = "Verify SSL Certificates", + ValidValues = ["On", "Off"], + DefaultValue = "False", + }, + ], + FromXml = FromXml, + FromDisplay = FromDisplayParams, + }; +} diff --git a/src/SharpFM.Model/Scripting/Steps/DeleteAccountStep.cs b/src/SharpFM.Model/Scripting/Steps/DeleteAccountStep.cs new file mode 100644 index 0000000..be09fb5 --- /dev/null +++ b/src/SharpFM.Model/Scripting/Steps/DeleteAccountStep.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Values; + +namespace SharpFM.Model.Scripting.Steps; + +public sealed class DeleteAccountStep : ScriptStep, IStepFactory +{ + public const int XmlId = 135; + public const string XmlName = "Delete Account"; + + public Calculation AccountName { get; set; } + + public DeleteAccountStep( + Calculation? accountName = null, + bool enabled = true) + : base(null, enabled) + { + AccountName = accountName ?? new Calculation(""); + } + + public override XElement ToXml() => + new("Step", + new XAttribute("enable", Enabled ? "True" : "False"), + new XAttribute("id", XmlId), + new XAttribute("name", XmlName), + new XElement("AccountName", AccountName.ToXml("Calculation"))); + + public override string ToDisplayLine() => + "Delete Account [ " + "Account Name: " + AccountName.Text + " ]"; + + public static new ScriptStep FromXml(XElement step) + { + var enabled = step.Attribute("enable")?.Value != "False"; + var accountName_vWrapEl = step.Element("AccountName"); + var accountName_vCalcEl = accountName_vWrapEl?.Element("Calculation"); + var accountName_v = accountName_vCalcEl is not null ? Calculation.FromXml(accountName_vCalcEl) : new Calculation(""); + return new DeleteAccountStep(accountName_v, enabled); + } + + public static ScriptStep FromDisplayParams(bool enabled, string[] hrParams) + { + var tokens = hrParams.Select(h => h.Trim()).ToArray(); + Calculation? accountName_v = null; + foreach (var tok in tokens) { if (tok.StartsWith("Account Name:", StringComparison.OrdinalIgnoreCase)) { accountName_v = new Calculation(tok.Substring(13).Trim()); break; } } + return new DeleteAccountStep(accountName_v, enabled); + } + + public static StepMetadata Metadata { get; } = new() + { + Name = XmlName, + Id = XmlId, + Category = "accounts", + HelpUrl = "https://help.claris.com/en/pro-help/content/delete-account.html", + Params = + [ + new ParamMetadata + { + Name = "Calculation", + XmlElement = "Calculation", + Type = "namedCalc", + HrLabel = "Account Name", + }, + ], + FromXml = FromXml, + FromDisplay = FromDisplayParams, + }; +} diff --git a/src/SharpFM.Model/Scripting/Steps/DialPhoneStep.cs b/src/SharpFM.Model/Scripting/Steps/DialPhoneStep.cs new file mode 100644 index 0000000..ecd9cf9 --- /dev/null +++ b/src/SharpFM.Model/Scripting/Steps/DialPhoneStep.cs @@ -0,0 +1,101 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Values; + +namespace SharpFM.Model.Scripting.Steps; + +public sealed class DialPhoneStep : ScriptStep, IStepFactory +{ + public const int XmlId = 65; + public const string XmlName = "Dial Phone"; + + public bool WithDialog { get; set; } + public bool UseDialPreferences { get; set; } + public Calculation Calculation { get; set; } + + public DialPhoneStep( + bool withDialog = true, + bool useDialPreferences = false, + Calculation? calculation = null, + bool enabled = true) + : base(null, enabled) + { + WithDialog = withDialog; + UseDialPreferences = useDialPreferences; + Calculation = calculation ?? new Calculation(""); + } + + public override XElement ToXml() => + new("Step", + new XAttribute("enable", Enabled ? "True" : "False"), + new XAttribute("id", XmlId), + new XAttribute("name", XmlName), + new XElement("NoInteract", new XAttribute("state", WithDialog ? "True" : "False")), + new XElement("UseDialPreferences", new XAttribute("value", UseDialPreferences ? "True" : "False")), + Calculation.ToXml("Calculation")); + + public override string ToDisplayLine() => + "Dial Phone [ " + "With dialog: " + (WithDialog ? "On" : "Off") + " ; " + (UseDialPreferences ? "On" : "Off") + " ; " + Calculation.Text + " ]"; + + public static new ScriptStep FromXml(XElement step) + { + var enabled = step.Attribute("enable")?.Value != "False"; + var withDialog_v = step.Element("NoInteract")?.Attribute("state")?.Value == "True"; + var useDialPreferences_v = step.Element("UseDialPreferences")?.Attribute("value")?.Value == "True"; + var calculation_vEl = step.Element("Calculation"); + var calculation_v = calculation_vEl is not null ? Calculation.FromXml(calculation_vEl) : new Calculation(""); + return new DialPhoneStep(withDialog_v, useDialPreferences_v, calculation_v, enabled); + } + + public static ScriptStep FromDisplayParams(bool enabled, string[] hrParams) + { + var tokens = hrParams.Select(h => h.Trim()).ToArray(); + bool withDialog_v = true; + foreach (var tok in tokens) { if (tok.StartsWith("With dialog:", StringComparison.OrdinalIgnoreCase)) { var v = tok.Substring(12).Trim(); withDialog_v = v.Equals("On", StringComparison.OrdinalIgnoreCase); break; } } + bool useDialPreferences_v = false; + Calculation? calculation_v = null; + foreach (var tok in tokens) { if (!(tok.StartsWith("With dialog:", StringComparison.OrdinalIgnoreCase))) { calculation_v = new Calculation(tok); break; } } + return new DialPhoneStep(withDialog_v, useDialPreferences_v, calculation_v, enabled); + } + + public static StepMetadata Metadata { get; } = new() + { + Name = XmlName, + Id = XmlId, + Category = "miscellaneous", + HelpUrl = "https://help.claris.com/en/pro-help/content/dial-phone.html", + Params = + [ + new ParamMetadata + { + Name = "NoInteract", + XmlElement = "NoInteract", + Type = "boolean", + XmlAttr = "state", + HrLabel = "With dialog", + ValidValues = ["On", "Off"], + DefaultValue = "True", + }, + new ParamMetadata + { + Name = "UseDialPreferences", + XmlElement = "UseDialPreferences", + Type = "boolean", + XmlAttr = "value", + ValidValues = ["On", "Off"], + DefaultValue = "False", + }, + new ParamMetadata + { + Name = "Calculation", + XmlElement = "Calculation", + Type = "calculation", + }, + ], + FromXml = FromXml, + FromDisplay = FromDisplayParams, + }; +} diff --git a/src/SharpFM.Model/Scripting/Steps/EnableAccountStep.cs b/src/SharpFM.Model/Scripting/Steps/EnableAccountStep.cs new file mode 100644 index 0000000..c956c70 --- /dev/null +++ b/src/SharpFM.Model/Scripting/Steps/EnableAccountStep.cs @@ -0,0 +1,99 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Values; + +namespace SharpFM.Model.Scripting.Steps; + +public sealed class EnableAccountStep : ScriptStep, IStepFactory +{ + public const int XmlId = 137; + public const string XmlName = "Enable Account"; + + public Calculation AccountName { get; set; } + public string AccountOperation { get; set; } + + public EnableAccountStep( + Calculation? accountName = null, + string accountOperation = "Activate", + bool enabled = true) + : base(null, enabled) + { + AccountName = accountName ?? new Calculation(""); + AccountOperation = accountOperation; + } + + private static readonly IReadOnlyDictionary _AccountOperationToHr = + new Dictionary(StringComparer.Ordinal) { + ["Activate"] = "Activate", + ["Deactivate"] = "Deactivate", + }; + private static readonly IReadOnlyDictionary _AccountOperationFromHr = + new Dictionary(StringComparer.OrdinalIgnoreCase) { + ["Activate"] = "Activate", + ["Deactivate"] = "Deactivate", + }; + private static string AccountOperationHr(string x) => _AccountOperationToHr.TryGetValue(x, out var h) ? h : x; + private static string AccountOperationXml(string h) => _AccountOperationFromHr.TryGetValue(h, out var x) ? x : h; + + public override XElement ToXml() => + new("Step", + new XAttribute("enable", Enabled ? "True" : "False"), + new XAttribute("id", XmlId), + new XAttribute("name", XmlName), + new XElement("AccountName", AccountName.ToXml("Calculation")), + new XElement("AccountOperation", new XAttribute("value", AccountOperation))); + + public override string ToDisplayLine() => + "Enable Account [ " + "Account Name: " + AccountName.Text + " ; " + AccountOperationHr(AccountOperation) + " ]"; + + public static new ScriptStep FromXml(XElement step) + { + var enabled = step.Attribute("enable")?.Value != "False"; + var accountName_vWrapEl = step.Element("AccountName"); + var accountName_vCalcEl = accountName_vWrapEl?.Element("Calculation"); + var accountName_v = accountName_vCalcEl is not null ? Calculation.FromXml(accountName_vCalcEl) : new Calculation(""); + var accountOperation_v = step.Element("AccountOperation")?.Attribute("value")?.Value ?? "Activate"; + return new EnableAccountStep(accountName_v, accountOperation_v, enabled); + } + + public static ScriptStep FromDisplayParams(bool enabled, string[] hrParams) + { + var tokens = hrParams.Select(h => h.Trim()).ToArray(); + Calculation? accountName_v = null; + foreach (var tok in tokens) { if (tok.StartsWith("Account Name:", StringComparison.OrdinalIgnoreCase)) { accountName_v = new Calculation(tok.Substring(13).Trim()); break; } } + string accountOperation_v = "Activate"; + return new EnableAccountStep(accountName_v, accountOperation_v, enabled); + } + + public static StepMetadata Metadata { get; } = new() + { + Name = XmlName, + Id = XmlId, + Category = "accounts", + HelpUrl = "https://help.claris.com/en/pro-help/content/enable-account.html", + Params = + [ + new ParamMetadata + { + Name = "Calculation", + XmlElement = "Calculation", + Type = "namedCalc", + HrLabel = "Account Name", + }, + new ParamMetadata + { + Name = "AccountOperation", + XmlElement = "AccountOperation", + Type = "enum", + XmlAttr = "value", + ValidValues = ["Activate", "Deactivate"], + DefaultValue = "Activate", + }, + ], + FromXml = FromXml, + FromDisplay = FromDisplayParams, + }; +} diff --git a/src/SharpFM.Model/Scripting/Steps/ExitScriptStep.cs b/src/SharpFM.Model/Scripting/Steps/ExitScriptStep.cs new file mode 100644 index 0000000..46d7e93 --- /dev/null +++ b/src/SharpFM.Model/Scripting/Steps/ExitScriptStep.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Values; + +namespace SharpFM.Model.Scripting.Steps; + +public sealed class ExitScriptStep : ScriptStep, IStepFactory +{ + public const int XmlId = 103; + public const string XmlName = "Exit Script"; + + public Calculation Calculation { get; set; } + + public ExitScriptStep( + Calculation? calculation = null, + bool enabled = true) + : base(null, enabled) + { + Calculation = calculation ?? new Calculation(""); + } + + public override XElement ToXml() => + new("Step", + new XAttribute("enable", Enabled ? "True" : "False"), + new XAttribute("id", XmlId), + new XAttribute("name", XmlName), + Calculation.ToXml("Calculation")); + + public override string ToDisplayLine() => + "Exit Script [ " + Calculation.Text + " ]"; + + public static new ScriptStep FromXml(XElement step) + { + var enabled = step.Attribute("enable")?.Value != "False"; + var calculation_vEl = step.Element("Calculation"); + var calculation_v = calculation_vEl is not null ? Calculation.FromXml(calculation_vEl) : new Calculation(""); + return new ExitScriptStep(calculation_v, enabled); + } + + public static ScriptStep FromDisplayParams(bool enabled, string[] hrParams) + { + var tokens = hrParams.Select(h => h.Trim()).ToArray(); + Calculation? calculation_v = null; + foreach (var tok in tokens) { if (!(false)) { calculation_v = new Calculation(tok); break; } } + return new ExitScriptStep(calculation_v, enabled); + } + + public static StepMetadata Metadata { get; } = new() + { + Name = XmlName, + Id = XmlId, + Category = "control", + HelpUrl = "https://help.claris.com/en/pro-help/content/exit-script.html", + Params = + [ + new ParamMetadata + { + Name = "Calculation", + XmlElement = "Calculation", + Type = "calculation", + }, + ], + FromXml = FromXml, + FromDisplay = FromDisplayParams, + }; +} diff --git a/src/SharpFM.Model/Scripting/Steps/GetFolderPathStep.cs b/src/SharpFM.Model/Scripting/Steps/GetFolderPathStep.cs new file mode 100644 index 0000000..a3f2a7d --- /dev/null +++ b/src/SharpFM.Model/Scripting/Steps/GetFolderPathStep.cs @@ -0,0 +1,129 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Values; + +namespace SharpFM.Model.Scripting.Steps; + +public sealed class GetFolderPathStep : ScriptStep, IStepFactory +{ + public const int XmlId = 181; + public const string XmlName = "Get Folder Path"; + + public bool AllowFolderCreation { get; set; } + public string Name { get; set; } + public Calculation Calculation { get; set; } + public Calculation Calculation2 { get; set; } + public Calculation Calculation3 { get; set; } + + public GetFolderPathStep( + bool allowFolderCreation = true, + string name = "", + Calculation? calculation = null, + Calculation? calculation2 = null, + Calculation? calculation3 = null, + bool enabled = true) + : base(null, enabled) + { + AllowFolderCreation = allowFolderCreation; + Name = name; + Calculation = calculation ?? new Calculation(""); + Calculation2 = calculation2 ?? new Calculation(""); + Calculation3 = calculation3 ?? new Calculation(""); + } + + public override XElement ToXml() => + new("Step", + new XAttribute("enable", Enabled ? "True" : "False"), + new XAttribute("id", XmlId), + new XAttribute("name", XmlName), + new XElement("AllowFolderCreation", new XAttribute("state", AllowFolderCreation ? "True" : "False")), + new XElement("Name", Name), + new XElement("DialogTitle", Calculation.ToXml("Calculation")), + new XElement("DefaultLocation", Calculation2.ToXml("Calculation")), + new XElement("Repetition", Calculation3.ToXml("Calculation"))); + + public override string ToDisplayLine() => + "Get Folder Path [ " + "Allow Folder Creation: " + (AllowFolderCreation ? "On" : "Off") + " ; " + Name + " ; " + Calculation.Text + " ; " + Calculation2.Text + " ; " + Calculation3.Text + " ]"; + + public static new ScriptStep FromXml(XElement step) + { + var enabled = step.Attribute("enable")?.Value != "False"; + var allowFolderCreation_v = step.Element("AllowFolderCreation")?.Attribute("state")?.Value == "True"; + var name_v = step.Element("Name")?.Value ?? ""; + var calculation_vWrapEl = step.Element("DialogTitle"); + var calculation_vCalcEl = calculation_vWrapEl?.Element("Calculation"); + var calculation_v = calculation_vCalcEl is not null ? Calculation.FromXml(calculation_vCalcEl) : new Calculation(""); + var calculation2_vWrapEl = step.Element("DefaultLocation"); + var calculation2_vCalcEl = calculation2_vWrapEl?.Element("Calculation"); + var calculation2_v = calculation2_vCalcEl is not null ? Calculation.FromXml(calculation2_vCalcEl) : new Calculation(""); + var calculation3_vWrapEl = step.Element("Repetition"); + var calculation3_vCalcEl = calculation3_vWrapEl?.Element("Calculation"); + var calculation3_v = calculation3_vCalcEl is not null ? Calculation.FromXml(calculation3_vCalcEl) : new Calculation(""); + return new GetFolderPathStep(allowFolderCreation_v, name_v, calculation_v, calculation2_v, calculation3_v, enabled); + } + + public static ScriptStep FromDisplayParams(bool enabled, string[] hrParams) + { + var tokens = hrParams.Select(h => h.Trim()).ToArray(); + bool allowFolderCreation_v = true; + foreach (var tok in tokens) { if (tok.StartsWith("Allow Folder Creation:", StringComparison.OrdinalIgnoreCase)) { var v = tok.Substring(22).Trim(); allowFolderCreation_v = v.Equals("On", StringComparison.OrdinalIgnoreCase); break; } } + string name_v = ""; + Calculation? calculation_v = null; + foreach (var tok in tokens) { if (!(tok.StartsWith("Allow Folder Creation:", StringComparison.OrdinalIgnoreCase))) { calculation_v = new Calculation(tok); break; } } + Calculation? calculation2_v = null; + foreach (var tok in tokens) { if (!(tok.StartsWith("Allow Folder Creation:", StringComparison.OrdinalIgnoreCase))) { calculation2_v = new Calculation(tok); break; } } + Calculation? calculation3_v = null; + foreach (var tok in tokens) { if (!(tok.StartsWith("Allow Folder Creation:", StringComparison.OrdinalIgnoreCase))) { calculation3_v = new Calculation(tok); break; } } + return new GetFolderPathStep(allowFolderCreation_v, name_v, calculation_v, calculation2_v, calculation3_v, enabled); + } + + public static StepMetadata Metadata { get; } = new() + { + Name = XmlName, + Id = XmlId, + Category = "miscellaneous", + HelpUrl = "https://help.claris.com/en/pro-help/content/get-directory.html", + Params = + [ + new ParamMetadata + { + Name = "AllowFolderCreation", + XmlElement = "AllowFolderCreation", + Type = "boolean", + XmlAttr = "state", + HrLabel = "Allow Folder Creation", + ValidValues = ["On", "Off"], + DefaultValue = "True", + }, + new ParamMetadata + { + Name = "Name", + XmlElement = "Name", + Type = "text", + }, + new ParamMetadata + { + Name = "Calculation", + XmlElement = "Calculation", + Type = "namedCalc", + }, + new ParamMetadata + { + Name = "Calculation", + XmlElement = "Calculation", + Type = "namedCalc", + }, + new ParamMetadata + { + Name = "Calculation", + XmlElement = "Calculation", + Type = "namedCalc", + }, + ], + FromXml = FromXml, + FromDisplay = FromDisplayParams, + }; +} diff --git a/src/SharpFM.Model/Scripting/Steps/GoToObjectStep.cs b/src/SharpFM.Model/Scripting/Steps/GoToObjectStep.cs new file mode 100644 index 0000000..f720c39 --- /dev/null +++ b/src/SharpFM.Model/Scripting/Steps/GoToObjectStep.cs @@ -0,0 +1,85 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Values; + +namespace SharpFM.Model.Scripting.Steps; + +public sealed class GoToObjectStep : ScriptStep, IStepFactory +{ + public const int XmlId = 145; + public const string XmlName = "Go to Object"; + + public Calculation Calculation { get; set; } + public Calculation Calculation2 { get; set; } + + public GoToObjectStep( + Calculation? calculation = null, + Calculation? calculation2 = null, + bool enabled = true) + : base(null, enabled) + { + Calculation = calculation ?? new Calculation(""); + Calculation2 = calculation2 ?? new Calculation(""); + } + + public override XElement ToXml() => + new("Step", + new XAttribute("enable", Enabled ? "True" : "False"), + new XAttribute("id", XmlId), + new XAttribute("name", XmlName), + new XElement("ObjectName", Calculation.ToXml("Calculation")), + new XElement("Repetition", Calculation2.ToXml("Calculation"))); + + public override string ToDisplayLine() => + "Go to Object [ " + Calculation.Text + " ; " + Calculation2.Text + " ]"; + + public static new ScriptStep FromXml(XElement step) + { + var enabled = step.Attribute("enable")?.Value != "False"; + var calculation_vWrapEl = step.Element("ObjectName"); + var calculation_vCalcEl = calculation_vWrapEl?.Element("Calculation"); + var calculation_v = calculation_vCalcEl is not null ? Calculation.FromXml(calculation_vCalcEl) : new Calculation(""); + var calculation2_vWrapEl = step.Element("Repetition"); + var calculation2_vCalcEl = calculation2_vWrapEl?.Element("Calculation"); + var calculation2_v = calculation2_vCalcEl is not null ? Calculation.FromXml(calculation2_vCalcEl) : new Calculation(""); + return new GoToObjectStep(calculation_v, calculation2_v, enabled); + } + + public static ScriptStep FromDisplayParams(bool enabled, string[] hrParams) + { + var tokens = hrParams.Select(h => h.Trim()).ToArray(); + Calculation? calculation_v = null; + foreach (var tok in tokens) { if (!(false)) { calculation_v = new Calculation(tok); break; } } + Calculation? calculation2_v = null; + foreach (var tok in tokens) { if (!(false)) { calculation2_v = new Calculation(tok); break; } } + return new GoToObjectStep(calculation_v, calculation2_v, enabled); + } + + public static StepMetadata Metadata { get; } = new() + { + Name = XmlName, + Id = XmlId, + Category = "navigation", + HelpUrl = "https://help.claris.com/en/pro-help/content/go-to-object.html", + Params = + [ + new ParamMetadata + { + Name = "Calculation", + XmlElement = "Calculation", + Type = "namedCalc", + }, + new ParamMetadata + { + Name = "Calculation", + XmlElement = "Calculation", + Type = "namedCalc", + }, + ], + FromXml = FromXml, + FromDisplay = FromDisplayParams, + }; +} diff --git a/src/SharpFM.Model/Scripting/Steps/MoveResizeWindowStep.cs b/src/SharpFM.Model/Scripting/Steps/MoveResizeWindowStep.cs new file mode 100644 index 0000000..d344289 --- /dev/null +++ b/src/SharpFM.Model/Scripting/Steps/MoveResizeWindowStep.cs @@ -0,0 +1,180 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Values; + +namespace SharpFM.Model.Scripting.Steps; + +public sealed class MoveResizeWindowStep : ScriptStep, IStepFactory +{ + public const int XmlId = 119; + public const string XmlName = "Move/Resize Window"; + + public string Window { get; set; } + public Calculation Name { get; set; } + public bool CurrentFile { get; set; } + public Calculation Height { get; set; } + public Calculation Width { get; set; } + public Calculation Top { get; set; } + public Calculation Left { get; set; } + + public MoveResizeWindowStep( + string window = "ByName", + Calculation? name = null, + bool currentFile = true, + Calculation? height = null, + Calculation? width = null, + Calculation? top = null, + Calculation? left = null, + bool enabled = true) + : base(null, enabled) + { + Window = window; + Name = name ?? new Calculation(""); + CurrentFile = currentFile; + Height = height ?? new Calculation(""); + Width = width ?? new Calculation(""); + Top = top ?? new Calculation(""); + Left = left ?? new Calculation(""); + } + + private static readonly IReadOnlyDictionary _WindowToHr = + new Dictionary(StringComparer.Ordinal) { + ["ByName"] = "ByName", + ["Current"] = "Current Window", + }; + private static readonly IReadOnlyDictionary _WindowFromHr = + new Dictionary(StringComparer.OrdinalIgnoreCase) { + ["ByName"] = "ByName", + ["Current Window"] = "Current", + }; + private static string WindowHr(string x) => _WindowToHr.TryGetValue(x, out var h) ? h : x; + private static string WindowXml(string h) => _WindowFromHr.TryGetValue(h, out var x) ? x : h; + + public override XElement ToXml() => + new("Step", + new XAttribute("enable", Enabled ? "True" : "False"), + new XAttribute("id", XmlId), + new XAttribute("name", XmlName), + new XElement("Window", new XAttribute("value", Window)), + new XElement("Name", Name.ToXml("Calculation")), + new XElement("LimitToWindowsOfCurrentFile", new XAttribute("state", CurrentFile ? "True" : "False")), + new XElement("Height", Height.ToXml("Calculation")), + new XElement("Width", Width.ToXml("Calculation")), + new XElement("DistanceFromTop", Top.ToXml("Calculation")), + new XElement("DistanceFromLeft", Left.ToXml("Calculation"))); + + public override string ToDisplayLine() => + "Move/Resize Window [ " + WindowHr(Window) + " ; " + "Name: " + Name.Text + " ; " + "Current file: " + (CurrentFile ? "On" : "Off") + " ; " + "Height: " + Height.Text + " ; " + "Width: " + Width.Text + " ; " + "Top: " + Top.Text + " ; " + "Left: " + Left.Text + " ]"; + + public static new ScriptStep FromXml(XElement step) + { + var enabled = step.Attribute("enable")?.Value != "False"; + var window_v = step.Element("Window")?.Attribute("value")?.Value ?? "ByName"; + var name_vWrapEl = step.Element("Name"); + var name_vCalcEl = name_vWrapEl?.Element("Calculation"); + var name_v = name_vCalcEl is not null ? Calculation.FromXml(name_vCalcEl) : new Calculation(""); + var currentFile_v = step.Element("LimitToWindowsOfCurrentFile")?.Attribute("state")?.Value == "True"; + var height_vWrapEl = step.Element("Height"); + var height_vCalcEl = height_vWrapEl?.Element("Calculation"); + var height_v = height_vCalcEl is not null ? Calculation.FromXml(height_vCalcEl) : new Calculation(""); + var width_vWrapEl = step.Element("Width"); + var width_vCalcEl = width_vWrapEl?.Element("Calculation"); + var width_v = width_vCalcEl is not null ? Calculation.FromXml(width_vCalcEl) : new Calculation(""); + var top_vWrapEl = step.Element("DistanceFromTop"); + var top_vCalcEl = top_vWrapEl?.Element("Calculation"); + var top_v = top_vCalcEl is not null ? Calculation.FromXml(top_vCalcEl) : new Calculation(""); + var left_vWrapEl = step.Element("DistanceFromLeft"); + var left_vCalcEl = left_vWrapEl?.Element("Calculation"); + var left_v = left_vCalcEl is not null ? Calculation.FromXml(left_vCalcEl) : new Calculation(""); + return new MoveResizeWindowStep(window_v, name_v, currentFile_v, height_v, width_v, top_v, left_v, enabled); + } + + public static ScriptStep FromDisplayParams(bool enabled, string[] hrParams) + { + var tokens = hrParams.Select(h => h.Trim()).ToArray(); + string window_v = "ByName"; + Calculation? name_v = null; + foreach (var tok in tokens) { if (tok.StartsWith("Name:", StringComparison.OrdinalIgnoreCase)) { name_v = new Calculation(tok.Substring(5).Trim()); break; } } + bool currentFile_v = true; + foreach (var tok in tokens) { if (tok.StartsWith("Current file:", StringComparison.OrdinalIgnoreCase)) { var v = tok.Substring(13).Trim(); currentFile_v = v.Equals("On", StringComparison.OrdinalIgnoreCase); break; } } + Calculation? height_v = null; + foreach (var tok in tokens) { if (tok.StartsWith("Height:", StringComparison.OrdinalIgnoreCase)) { height_v = new Calculation(tok.Substring(7).Trim()); break; } } + Calculation? width_v = null; + foreach (var tok in tokens) { if (tok.StartsWith("Width:", StringComparison.OrdinalIgnoreCase)) { width_v = new Calculation(tok.Substring(6).Trim()); break; } } + Calculation? top_v = null; + foreach (var tok in tokens) { if (tok.StartsWith("Top:", StringComparison.OrdinalIgnoreCase)) { top_v = new Calculation(tok.Substring(4).Trim()); break; } } + Calculation? left_v = null; + foreach (var tok in tokens) { if (tok.StartsWith("Left:", StringComparison.OrdinalIgnoreCase)) { left_v = new Calculation(tok.Substring(5).Trim()); break; } } + return new MoveResizeWindowStep(window_v, name_v, currentFile_v, height_v, width_v, top_v, left_v, enabled); + } + + public static StepMetadata Metadata { get; } = new() + { + Name = XmlName, + Id = XmlId, + Category = "windows", + HelpUrl = "https://help.claris.com/en/pro-help/content/move-resize-window.html", + Params = + [ + new ParamMetadata + { + Name = "Window", + XmlElement = "Window", + Type = "enum", + XmlAttr = "value", + ValidValues = ["ByName", "Current Window"], + DefaultValue = "ByName", + }, + new ParamMetadata + { + Name = "Calculation", + XmlElement = "Calculation", + Type = "namedCalc", + HrLabel = "Name", + }, + new ParamMetadata + { + Name = "LimitToWindowsOfCurrentFile", + XmlElement = "LimitToWindowsOfCurrentFile", + Type = "boolean", + XmlAttr = "state", + HrLabel = "Current file", + ValidValues = ["On", "Off"], + DefaultValue = "True", + }, + new ParamMetadata + { + Name = "Calculation", + XmlElement = "Calculation", + Type = "namedCalc", + HrLabel = "Height", + }, + new ParamMetadata + { + Name = "Calculation", + XmlElement = "Calculation", + Type = "namedCalc", + HrLabel = "Width", + }, + new ParamMetadata + { + Name = "Calculation", + XmlElement = "Calculation", + Type = "namedCalc", + HrLabel = "Top", + }, + new ParamMetadata + { + Name = "Calculation", + XmlElement = "Calculation", + Type = "namedCalc", + HrLabel = "Left", + }, + ], + FromXml = FromXml, + FromDisplay = FromDisplayParams, + }; +} diff --git a/src/SharpFM.Model/Scripting/Steps/NewWindowStep.cs b/src/SharpFM.Model/Scripting/Steps/NewWindowStep.cs new file mode 100644 index 0000000..0f50542 --- /dev/null +++ b/src/SharpFM.Model/Scripting/Steps/NewWindowStep.cs @@ -0,0 +1,168 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Values; + +namespace SharpFM.Model.Scripting.Steps; + +public sealed class NewWindowStep : ScriptStep, IStepFactory +{ + public const int XmlId = 122; + public const string XmlName = "New Window"; + + public string LayoutDestination { get; set; } + public Calculation Calculation { get; set; } + public Calculation Calculation2 { get; set; } + public Calculation Calculation3 { get; set; } + public Calculation Calculation4 { get; set; } + public Calculation Calculation5 { get; set; } + public string NewWndStyles { get; set; } + + public NewWindowStep( + string layoutDestination = "SelectedLayout", + Calculation? calculation = null, + Calculation? calculation2 = null, + Calculation? calculation3 = null, + Calculation? calculation4 = null, + Calculation? calculation5 = null, + string newWndStyles = "", + bool enabled = true) + : base(null, enabled) + { + LayoutDestination = layoutDestination; + Calculation = calculation ?? new Calculation(""); + Calculation2 = calculation2 ?? new Calculation(""); + Calculation3 = calculation3 ?? new Calculation(""); + Calculation4 = calculation4 ?? new Calculation(""); + Calculation5 = calculation5 ?? new Calculation(""); + NewWndStyles = newWndStyles; + } + + private static readonly IReadOnlyDictionary _LayoutDestinationToHr = + new Dictionary(StringComparer.Ordinal) { + ["SelectedLayout"] = "SelectedLayout", + }; + private static readonly IReadOnlyDictionary _LayoutDestinationFromHr = + new Dictionary(StringComparer.OrdinalIgnoreCase) { + ["SelectedLayout"] = "SelectedLayout", + }; + private static string LayoutDestinationHr(string x) => _LayoutDestinationToHr.TryGetValue(x, out var h) ? h : x; + private static string LayoutDestinationXml(string h) => _LayoutDestinationFromHr.TryGetValue(h, out var x) ? x : h; + + public override XElement ToXml() => + new("Step", + new XAttribute("enable", Enabled ? "True" : "False"), + new XAttribute("id", XmlId), + new XAttribute("name", XmlName), + new XElement("LayoutDestination", new XAttribute("value", LayoutDestination)), + new XElement("Name", Calculation.ToXml("Calculation")), + new XElement("Height", Calculation2.ToXml("Calculation")), + new XElement("Width", Calculation3.ToXml("Calculation")), + new XElement("DistanceFromTop", Calculation4.ToXml("Calculation")), + new XElement("DistanceFromLeft", Calculation5.ToXml("Calculation")), + new XElement("NewWndStyles", NewWndStyles)); + + public override string ToDisplayLine() => + "New Window [ " + LayoutDestinationHr(LayoutDestination) + " ; " + Calculation.Text + " ; " + Calculation2.Text + " ; " + Calculation3.Text + " ; " + Calculation4.Text + " ; " + Calculation5.Text + " ; " + NewWndStyles + " ]"; + + public static new ScriptStep FromXml(XElement step) + { + var enabled = step.Attribute("enable")?.Value != "False"; + var layoutDestination_v = step.Element("LayoutDestination")?.Attribute("value")?.Value ?? "SelectedLayout"; + var calculation_vWrapEl = step.Element("Name"); + var calculation_vCalcEl = calculation_vWrapEl?.Element("Calculation"); + var calculation_v = calculation_vCalcEl is not null ? Calculation.FromXml(calculation_vCalcEl) : new Calculation(""); + var calculation2_vWrapEl = step.Element("Height"); + var calculation2_vCalcEl = calculation2_vWrapEl?.Element("Calculation"); + var calculation2_v = calculation2_vCalcEl is not null ? Calculation.FromXml(calculation2_vCalcEl) : new Calculation(""); + var calculation3_vWrapEl = step.Element("Width"); + var calculation3_vCalcEl = calculation3_vWrapEl?.Element("Calculation"); + var calculation3_v = calculation3_vCalcEl is not null ? Calculation.FromXml(calculation3_vCalcEl) : new Calculation(""); + var calculation4_vWrapEl = step.Element("DistanceFromTop"); + var calculation4_vCalcEl = calculation4_vWrapEl?.Element("Calculation"); + var calculation4_v = calculation4_vCalcEl is not null ? Calculation.FromXml(calculation4_vCalcEl) : new Calculation(""); + var calculation5_vWrapEl = step.Element("DistanceFromLeft"); + var calculation5_vCalcEl = calculation5_vWrapEl?.Element("Calculation"); + var calculation5_v = calculation5_vCalcEl is not null ? Calculation.FromXml(calculation5_vCalcEl) : new Calculation(""); + var newWndStyles_v = step.Element("NewWndStyles")?.Value ?? ""; + return new NewWindowStep(layoutDestination_v, calculation_v, calculation2_v, calculation3_v, calculation4_v, calculation5_v, newWndStyles_v, enabled); + } + + public static ScriptStep FromDisplayParams(bool enabled, string[] hrParams) + { + var tokens = hrParams.Select(h => h.Trim()).ToArray(); + string layoutDestination_v = "SelectedLayout"; + Calculation? calculation_v = null; + foreach (var tok in tokens) { if (!(false)) { calculation_v = new Calculation(tok); break; } } + Calculation? calculation2_v = null; + foreach (var tok in tokens) { if (!(false)) { calculation2_v = new Calculation(tok); break; } } + Calculation? calculation3_v = null; + foreach (var tok in tokens) { if (!(false)) { calculation3_v = new Calculation(tok); break; } } + Calculation? calculation4_v = null; + foreach (var tok in tokens) { if (!(false)) { calculation4_v = new Calculation(tok); break; } } + Calculation? calculation5_v = null; + foreach (var tok in tokens) { if (!(false)) { calculation5_v = new Calculation(tok); break; } } + string newWndStyles_v = ""; + return new NewWindowStep(layoutDestination_v, calculation_v, calculation2_v, calculation3_v, calculation4_v, calculation5_v, newWndStyles_v, enabled); + } + + public static StepMetadata Metadata { get; } = new() + { + Name = XmlName, + Id = XmlId, + Category = "windows", + HelpUrl = "https://help.claris.com/en/pro-help/content/new-window.html", + Params = + [ + new ParamMetadata + { + Name = "LayoutDestination", + XmlElement = "LayoutDestination", + Type = "enum", + XmlAttr = "value", + ValidValues = ["SelectedLayout"], + DefaultValue = "SelectedLayout", + }, + new ParamMetadata + { + Name = "Calculation", + XmlElement = "Calculation", + Type = "namedCalc", + }, + new ParamMetadata + { + Name = "Calculation", + XmlElement = "Calculation", + Type = "namedCalc", + }, + new ParamMetadata + { + Name = "Calculation", + XmlElement = "Calculation", + Type = "namedCalc", + }, + new ParamMetadata + { + Name = "Calculation", + XmlElement = "Calculation", + Type = "namedCalc", + }, + new ParamMetadata + { + Name = "Calculation", + XmlElement = "Calculation", + Type = "namedCalc", + }, + new ParamMetadata + { + Name = "NewWndStyles", + XmlElement = "NewWndStyles", + Type = "text", + }, + ], + FromXml = FromXml, + FromDisplay = FromDisplayParams, + }; +} diff --git a/src/SharpFM.Model/Scripting/Steps/OmitMultipleRecordsStep.cs b/src/SharpFM.Model/Scripting/Steps/OmitMultipleRecordsStep.cs new file mode 100644 index 0000000..56b4fb9 --- /dev/null +++ b/src/SharpFM.Model/Scripting/Steps/OmitMultipleRecordsStep.cs @@ -0,0 +1,86 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Values; + +namespace SharpFM.Model.Scripting.Steps; + +public sealed class OmitMultipleRecordsStep : ScriptStep, IStepFactory +{ + public const int XmlId = 26; + public const string XmlName = "Omit Multiple Records"; + + public bool WithDialog { get; set; } + public Calculation Calculation { get; set; } + + public OmitMultipleRecordsStep( + bool withDialog = true, + Calculation? calculation = null, + bool enabled = true) + : base(null, enabled) + { + WithDialog = withDialog; + Calculation = calculation ?? new Calculation(""); + } + + public override XElement ToXml() => + new("Step", + new XAttribute("enable", Enabled ? "True" : "False"), + new XAttribute("id", XmlId), + new XAttribute("name", XmlName), + new XElement("NoInteract", new XAttribute("state", WithDialog ? "True" : "False")), + Calculation.ToXml("Calculation")); + + public override string ToDisplayLine() => + "Omit Multiple Records [ " + "With dialog: " + (WithDialog ? "On" : "Off") + " ; " + Calculation.Text + " ]"; + + public static new ScriptStep FromXml(XElement step) + { + var enabled = step.Attribute("enable")?.Value != "False"; + var withDialog_v = step.Element("NoInteract")?.Attribute("state")?.Value == "True"; + var calculation_vEl = step.Element("Calculation"); + var calculation_v = calculation_vEl is not null ? Calculation.FromXml(calculation_vEl) : new Calculation(""); + return new OmitMultipleRecordsStep(withDialog_v, calculation_v, enabled); + } + + public static ScriptStep FromDisplayParams(bool enabled, string[] hrParams) + { + var tokens = hrParams.Select(h => h.Trim()).ToArray(); + bool withDialog_v = true; + foreach (var tok in tokens) { if (tok.StartsWith("With dialog:", StringComparison.OrdinalIgnoreCase)) { var v = tok.Substring(12).Trim(); withDialog_v = v.Equals("On", StringComparison.OrdinalIgnoreCase); break; } } + Calculation? calculation_v = null; + foreach (var tok in tokens) { if (!(tok.StartsWith("With dialog:", StringComparison.OrdinalIgnoreCase))) { calculation_v = new Calculation(tok); break; } } + return new OmitMultipleRecordsStep(withDialog_v, calculation_v, enabled); + } + + public static StepMetadata Metadata { get; } = new() + { + Name = XmlName, + Id = XmlId, + Category = "found sets", + HelpUrl = "https://help.claris.com/en/pro-help/content/omit-multiple-records.html", + Params = + [ + new ParamMetadata + { + Name = "NoInteract", + XmlElement = "NoInteract", + Type = "boolean", + XmlAttr = "state", + HrLabel = "With dialog", + ValidValues = ["On", "Off"], + DefaultValue = "True", + }, + new ParamMetadata + { + Name = "Calculation", + XmlElement = "Calculation", + Type = "calculation", + }, + ], + FromXml = FromXml, + FromDisplay = FromDisplayParams, + }; +} diff --git a/src/SharpFM.Model/Scripting/Steps/OpenURLStep.cs b/src/SharpFM.Model/Scripting/Steps/OpenURLStep.cs new file mode 100644 index 0000000..b079d56 --- /dev/null +++ b/src/SharpFM.Model/Scripting/Steps/OpenURLStep.cs @@ -0,0 +1,103 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Values; + +namespace SharpFM.Model.Scripting.Steps; + +public sealed class OpenURLStep : ScriptStep, IStepFactory +{ + public const int XmlId = 111; + public const string XmlName = "Open URL"; + + public bool WithDialog { get; set; } + public bool InExternalBrowser { get; set; } + public Calculation Calculation { get; set; } + + public OpenURLStep( + bool withDialog = true, + bool inExternalBrowser = false, + Calculation? calculation = null, + bool enabled = true) + : base(null, enabled) + { + WithDialog = withDialog; + InExternalBrowser = inExternalBrowser; + Calculation = calculation ?? new Calculation(""); + } + + public override XElement ToXml() => + new("Step", + new XAttribute("enable", Enabled ? "True" : "False"), + new XAttribute("id", XmlId), + new XAttribute("name", XmlName), + new XElement("NoInteract", new XAttribute("state", WithDialog ? "True" : "False")), + new XElement("Option", new XAttribute("state", InExternalBrowser ? "True" : "False")), + Calculation.ToXml("Calculation")); + + public override string ToDisplayLine() => + "Open URL [ " + "With dialog: " + (WithDialog ? "On" : "Off") + " ; " + "In external browser: " + (InExternalBrowser ? "On" : "Off") + " ; " + Calculation.Text + " ]"; + + public static new ScriptStep FromXml(XElement step) + { + var enabled = step.Attribute("enable")?.Value != "False"; + var withDialog_v = step.Element("NoInteract")?.Attribute("state")?.Value == "True"; + var inExternalBrowser_v = step.Element("Option")?.Attribute("state")?.Value == "True"; + var calculation_vEl = step.Element("Calculation"); + var calculation_v = calculation_vEl is not null ? Calculation.FromXml(calculation_vEl) : new Calculation(""); + return new OpenURLStep(withDialog_v, inExternalBrowser_v, calculation_v, enabled); + } + + public static ScriptStep FromDisplayParams(bool enabled, string[] hrParams) + { + var tokens = hrParams.Select(h => h.Trim()).ToArray(); + bool withDialog_v = true; + foreach (var tok in tokens) { if (tok.StartsWith("With dialog:", StringComparison.OrdinalIgnoreCase)) { var v = tok.Substring(12).Trim(); withDialog_v = v.Equals("On", StringComparison.OrdinalIgnoreCase); break; } } + bool inExternalBrowser_v = false; + foreach (var tok in tokens) { if (tok.StartsWith("In external browser:", StringComparison.OrdinalIgnoreCase)) { var v = tok.Substring(20).Trim(); inExternalBrowser_v = v.Equals("On", StringComparison.OrdinalIgnoreCase); break; } } + Calculation? calculation_v = null; + foreach (var tok in tokens) { if (!(tok.StartsWith("With dialog:", StringComparison.OrdinalIgnoreCase) || tok.StartsWith("In external browser:", StringComparison.OrdinalIgnoreCase))) { calculation_v = new Calculation(tok); break; } } + return new OpenURLStep(withDialog_v, inExternalBrowser_v, calculation_v, enabled); + } + + public static StepMetadata Metadata { get; } = new() + { + Name = XmlName, + Id = XmlId, + Category = "miscellaneous", + HelpUrl = "https://help.claris.com/en/pro-help/content/open-url.html", + Params = + [ + new ParamMetadata + { + Name = "NoInteract", + XmlElement = "NoInteract", + Type = "boolean", + XmlAttr = "state", + HrLabel = "With dialog", + ValidValues = ["On", "Off"], + DefaultValue = "True", + }, + new ParamMetadata + { + Name = "Option", + XmlElement = "Option", + Type = "boolean", + XmlAttr = "state", + HrLabel = "In external browser", + ValidValues = ["On", "Off"], + DefaultValue = "False", + }, + new ParamMetadata + { + Name = "Calculation", + XmlElement = "Calculation", + Type = "calculation", + }, + ], + FromXml = FromXml, + FromDisplay = FromDisplayParams, + }; +} diff --git a/src/SharpFM.Model/Scripting/Steps/PauseResumeScriptStep.cs b/src/SharpFM.Model/Scripting/Steps/PauseResumeScriptStep.cs new file mode 100644 index 0000000..16b0d92 --- /dev/null +++ b/src/SharpFM.Model/Scripting/Steps/PauseResumeScriptStep.cs @@ -0,0 +1,98 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Values; + +namespace SharpFM.Model.Scripting.Steps; + +public sealed class PauseResumeScriptStep : ScriptStep, IStepFactory +{ + public const int XmlId = 62; + public const string XmlName = "Pause/Resume Script"; + + public string PauseTime { get; set; } + public Calculation DurationSeconds { get; set; } + + public PauseResumeScriptStep( + string pauseTime = "ForDuration", + Calculation? durationSeconds = null, + bool enabled = true) + : base(null, enabled) + { + PauseTime = pauseTime; + DurationSeconds = durationSeconds ?? new Calculation(""); + } + + private static readonly IReadOnlyDictionary _PauseTimeToHr = + new Dictionary(StringComparer.Ordinal) { + ["Indefinitely"] = "Indefinitely", + ["ForDuration"] = "Duration (seconds)", + }; + private static readonly IReadOnlyDictionary _PauseTimeFromHr = + new Dictionary(StringComparer.OrdinalIgnoreCase) { + ["Indefinitely"] = "Indefinitely", + ["Duration (seconds)"] = "ForDuration", + }; + private static string PauseTimeHr(string x) => _PauseTimeToHr.TryGetValue(x, out var h) ? h : x; + private static string PauseTimeXml(string h) => _PauseTimeFromHr.TryGetValue(h, out var x) ? x : h; + + public override XElement ToXml() => + new("Step", + new XAttribute("enable", Enabled ? "True" : "False"), + new XAttribute("id", XmlId), + new XAttribute("name", XmlName), + new XElement("PauseTime", new XAttribute("value", PauseTime)), + DurationSeconds.ToXml("Calculation")); + + public override string ToDisplayLine() => + "Pause/Resume Script [ " + PauseTimeHr(PauseTime) + " ; " + "Duration (seconds): " + DurationSeconds.Text + " ]"; + + public static new ScriptStep FromXml(XElement step) + { + var enabled = step.Attribute("enable")?.Value != "False"; + var pauseTime_v = step.Element("PauseTime")?.Attribute("value")?.Value ?? "ForDuration"; + var durationSeconds_vEl = step.Element("Calculation"); + var durationSeconds_v = durationSeconds_vEl is not null ? Calculation.FromXml(durationSeconds_vEl) : new Calculation(""); + return new PauseResumeScriptStep(pauseTime_v, durationSeconds_v, enabled); + } + + public static ScriptStep FromDisplayParams(bool enabled, string[] hrParams) + { + var tokens = hrParams.Select(h => h.Trim()).ToArray(); + string pauseTime_v = "ForDuration"; + Calculation? durationSeconds_v = null; + foreach (var tok in tokens) { if (tok.StartsWith("Duration (seconds):", StringComparison.OrdinalIgnoreCase)) { durationSeconds_v = new Calculation(tok.Substring(19).Trim()); break; } } + return new PauseResumeScriptStep(pauseTime_v, durationSeconds_v, enabled); + } + + public static StepMetadata Metadata { get; } = new() + { + Name = XmlName, + Id = XmlId, + Category = "control", + HelpUrl = "https://help.claris.com/en/pro-help/content/pause-resume-script.html", + Params = + [ + new ParamMetadata + { + Name = "PauseTime", + XmlElement = "PauseTime", + Type = "enum", + XmlAttr = "value", + ValidValues = ["Indefinitely", "Duration (seconds)"], + DefaultValue = "ForDuration", + }, + new ParamMetadata + { + Name = "Calculation", + XmlElement = "Calculation", + Type = "calculation", + HrLabel = "Duration (seconds)", + }, + ], + FromXml = FromXml, + FromDisplay = FromDisplayParams, + }; +} diff --git a/src/SharpFM.Model/Scripting/Steps/PerformAppleScriptStep.cs b/src/SharpFM.Model/Scripting/Steps/PerformAppleScriptStep.cs new file mode 100644 index 0000000..566b3e9 --- /dev/null +++ b/src/SharpFM.Model/Scripting/Steps/PerformAppleScriptStep.cs @@ -0,0 +1,109 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Values; + +namespace SharpFM.Model.Scripting.Steps; + +public sealed class PerformAppleScriptStep : ScriptStep, IStepFactory +{ + public const int XmlId = 67; + public const string XmlName = "Perform AppleScript"; + + public string ContentType { get; set; } + public Calculation Calculation { get; set; } + public string Text { get; set; } + + public PerformAppleScriptStep( + string contentType = "Calculation", + Calculation? calculation = null, + string text = "", + bool enabled = true) + : base(null, enabled) + { + ContentType = contentType; + Calculation = calculation ?? new Calculation(""); + Text = text; + } + + private static readonly IReadOnlyDictionary _ContentTypeToHr = + new Dictionary(StringComparer.Ordinal) { + ["Calculation"] = "Calculation", + ["Text"] = "Text", + }; + private static readonly IReadOnlyDictionary _ContentTypeFromHr = + new Dictionary(StringComparer.OrdinalIgnoreCase) { + ["Calculation"] = "Calculation", + ["Text"] = "Text", + }; + private static string ContentTypeHr(string x) => _ContentTypeToHr.TryGetValue(x, out var h) ? h : x; + private static string ContentTypeXml(string h) => _ContentTypeFromHr.TryGetValue(h, out var x) ? x : h; + + public override XElement ToXml() => + new("Step", + new XAttribute("enable", Enabled ? "True" : "False"), + new XAttribute("id", XmlId), + new XAttribute("name", XmlName), + new XElement("ContentType", new XAttribute("value", ContentType)), + Calculation.ToXml("Calculation"), + new XElement("Text", Text)); + + public override string ToDisplayLine() => + "Perform AppleScript [ " + ContentTypeHr(ContentType) + " ; " + Calculation.Text + " ; " + Text + " ]"; + + public static new ScriptStep FromXml(XElement step) + { + var enabled = step.Attribute("enable")?.Value != "False"; + var contentType_v = step.Element("ContentType")?.Attribute("value")?.Value ?? "Calculation"; + var calculation_vEl = step.Element("Calculation"); + var calculation_v = calculation_vEl is not null ? Calculation.FromXml(calculation_vEl) : new Calculation(""); + var text_v = step.Element("Text")?.Value ?? ""; + return new PerformAppleScriptStep(contentType_v, calculation_v, text_v, enabled); + } + + public static ScriptStep FromDisplayParams(bool enabled, string[] hrParams) + { + var tokens = hrParams.Select(h => h.Trim()).ToArray(); + string contentType_v = "Calculation"; + Calculation? calculation_v = null; + foreach (var tok in tokens) { if (!(false)) { calculation_v = new Calculation(tok); break; } } + string text_v = ""; + return new PerformAppleScriptStep(contentType_v, calculation_v, text_v, enabled); + } + + public static StepMetadata Metadata { get; } = new() + { + Name = XmlName, + Id = XmlId, + Category = "miscellaneous", + HelpUrl = "https://help.claris.com/en/pro-help/content/perform-applescript-os-x.html", + Params = + [ + new ParamMetadata + { + Name = "ContentType", + XmlElement = "ContentType", + Type = "enum", + XmlAttr = "value", + ValidValues = ["Calculation", "Text"], + DefaultValue = "Calculation", + }, + new ParamMetadata + { + Name = "Calculation", + XmlElement = "Calculation", + Type = "calculation", + }, + new ParamMetadata + { + Name = "Text", + XmlElement = "Text", + Type = "text", + }, + ], + FromXml = FromXml, + FromDisplay = FromDisplayParams, + }; +} diff --git a/src/SharpFM.Model/Scripting/Steps/PerformQuickFindStep.cs b/src/SharpFM.Model/Scripting/Steps/PerformQuickFindStep.cs new file mode 100644 index 0000000..d1812b3 --- /dev/null +++ b/src/SharpFM.Model/Scripting/Steps/PerformQuickFindStep.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Values; + +namespace SharpFM.Model.Scripting.Steps; + +public sealed class PerformQuickFindStep : ScriptStep, IStepFactory +{ + public const int XmlId = 150; + public const string XmlName = "Perform Quick Find"; + + public Calculation Calculation { get; set; } + + public PerformQuickFindStep( + Calculation? calculation = null, + bool enabled = true) + : base(null, enabled) + { + Calculation = calculation ?? new Calculation(""); + } + + public override XElement ToXml() => + new("Step", + new XAttribute("enable", Enabled ? "True" : "False"), + new XAttribute("id", XmlId), + new XAttribute("name", XmlName), + Calculation.ToXml("Calculation")); + + public override string ToDisplayLine() => + "Perform Quick Find [ " + Calculation.Text + " ]"; + + public static new ScriptStep FromXml(XElement step) + { + var enabled = step.Attribute("enable")?.Value != "False"; + var calculation_vEl = step.Element("Calculation"); + var calculation_v = calculation_vEl is not null ? Calculation.FromXml(calculation_vEl) : new Calculation(""); + return new PerformQuickFindStep(calculation_v, enabled); + } + + public static ScriptStep FromDisplayParams(bool enabled, string[] hrParams) + { + var tokens = hrParams.Select(h => h.Trim()).ToArray(); + Calculation? calculation_v = null; + foreach (var tok in tokens) { if (!(false)) { calculation_v = new Calculation(tok); break; } } + return new PerformQuickFindStep(calculation_v, enabled); + } + + public static StepMetadata Metadata { get; } = new() + { + Name = XmlName, + Id = XmlId, + Category = "found sets", + HelpUrl = "https://help.claris.com/en/pro-help/content/perform-quick-find.html", + Params = + [ + new ParamMetadata + { + Name = "Calculation", + XmlElement = "Calculation", + Type = "calculation", + }, + ], + FromXml = FromXml, + FromDisplay = FromDisplayParams, + }; +} diff --git a/src/SharpFM.Model/Scripting/Steps/ReLoginStep.cs b/src/SharpFM.Model/Scripting/Steps/ReLoginStep.cs new file mode 100644 index 0000000..7341d30 --- /dev/null +++ b/src/SharpFM.Model/Scripting/Steps/ReLoginStep.cs @@ -0,0 +1,104 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Values; + +namespace SharpFM.Model.Scripting.Steps; + +public sealed class ReLoginStep : ScriptStep, IStepFactory +{ + public const int XmlId = 138; + public const string XmlName = "Re-Login"; + + public bool WithDialog { get; set; } + public Calculation AccountName { get; set; } + public Calculation Password { get; set; } + + public ReLoginStep( + bool withDialog = true, + Calculation? accountName = null, + Calculation? password = null, + bool enabled = true) + : base(null, enabled) + { + WithDialog = withDialog; + AccountName = accountName ?? new Calculation(""); + Password = password ?? new Calculation(""); + } + + public override XElement ToXml() => + new("Step", + new XAttribute("enable", Enabled ? "True" : "False"), + new XAttribute("id", XmlId), + new XAttribute("name", XmlName), + new XElement("NoInteract", new XAttribute("state", WithDialog ? "True" : "False")), + new XElement("AccountName", AccountName.ToXml("Calculation")), + new XElement("Password", Password.ToXml("Calculation"))); + + public override string ToDisplayLine() => + "Re-Login [ " + "With dialog: " + (WithDialog ? "On" : "Off") + " ; " + "Account Name: " + AccountName.Text + " ; " + "Password: " + Password.Text + " ]"; + + public static new ScriptStep FromXml(XElement step) + { + var enabled = step.Attribute("enable")?.Value != "False"; + var withDialog_v = step.Element("NoInteract")?.Attribute("state")?.Value == "True"; + var accountName_vWrapEl = step.Element("AccountName"); + var accountName_vCalcEl = accountName_vWrapEl?.Element("Calculation"); + var accountName_v = accountName_vCalcEl is not null ? Calculation.FromXml(accountName_vCalcEl) : new Calculation(""); + var password_vWrapEl = step.Element("Password"); + var password_vCalcEl = password_vWrapEl?.Element("Calculation"); + var password_v = password_vCalcEl is not null ? Calculation.FromXml(password_vCalcEl) : new Calculation(""); + return new ReLoginStep(withDialog_v, accountName_v, password_v, enabled); + } + + public static ScriptStep FromDisplayParams(bool enabled, string[] hrParams) + { + var tokens = hrParams.Select(h => h.Trim()).ToArray(); + bool withDialog_v = true; + foreach (var tok in tokens) { if (tok.StartsWith("With dialog:", StringComparison.OrdinalIgnoreCase)) { var v = tok.Substring(12).Trim(); withDialog_v = v.Equals("On", StringComparison.OrdinalIgnoreCase); break; } } + Calculation? accountName_v = null; + foreach (var tok in tokens) { if (tok.StartsWith("Account Name:", StringComparison.OrdinalIgnoreCase)) { accountName_v = new Calculation(tok.Substring(13).Trim()); break; } } + Calculation? password_v = null; + foreach (var tok in tokens) { if (tok.StartsWith("Password:", StringComparison.OrdinalIgnoreCase)) { password_v = new Calculation(tok.Substring(9).Trim()); break; } } + return new ReLoginStep(withDialog_v, accountName_v, password_v, enabled); + } + + public static StepMetadata Metadata { get; } = new() + { + Name = XmlName, + Id = XmlId, + Category = "accounts", + HelpUrl = "https://help.claris.com/en/pro-help/content/re-login.html", + Params = + [ + new ParamMetadata + { + Name = "NoInteract", + XmlElement = "NoInteract", + Type = "boolean", + XmlAttr = "state", + HrLabel = "With dialog", + ValidValues = ["On", "Off"], + DefaultValue = "True", + }, + new ParamMetadata + { + Name = "Calculation", + XmlElement = "Calculation", + Type = "namedCalc", + HrLabel = "Account Name", + }, + new ParamMetadata + { + Name = "Calculation", + XmlElement = "Calculation", + Type = "namedCalc", + HrLabel = "Password", + }, + ], + FromXml = FromXml, + FromDisplay = FromDisplayParams, + }; +} diff --git a/src/SharpFM.Model/Scripting/Steps/RefreshObjectStep.cs b/src/SharpFM.Model/Scripting/Steps/RefreshObjectStep.cs new file mode 100644 index 0000000..25a99d8 --- /dev/null +++ b/src/SharpFM.Model/Scripting/Steps/RefreshObjectStep.cs @@ -0,0 +1,87 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Values; + +namespace SharpFM.Model.Scripting.Steps; + +public sealed class RefreshObjectStep : ScriptStep, IStepFactory +{ + public const int XmlId = 167; + public const string XmlName = "Refresh Object"; + + public Calculation ObjectName { get; set; } + public Calculation Repetition { get; set; } + + public RefreshObjectStep( + Calculation? objectName = null, + Calculation? repetition = null, + bool enabled = true) + : base(null, enabled) + { + ObjectName = objectName ?? new Calculation(""); + Repetition = repetition ?? new Calculation(""); + } + + public override XElement ToXml() => + new("Step", + new XAttribute("enable", Enabled ? "True" : "False"), + new XAttribute("id", XmlId), + new XAttribute("name", XmlName), + new XElement("ObjectName", ObjectName.ToXml("Calculation")), + new XElement("Repetition", Repetition.ToXml("Calculation"))); + + public override string ToDisplayLine() => + "Refresh Object [ " + "Object Name: " + ObjectName.Text + " ; " + "Repetition: " + Repetition.Text + " ]"; + + public static new ScriptStep FromXml(XElement step) + { + var enabled = step.Attribute("enable")?.Value != "False"; + var objectName_vWrapEl = step.Element("ObjectName"); + var objectName_vCalcEl = objectName_vWrapEl?.Element("Calculation"); + var objectName_v = objectName_vCalcEl is not null ? Calculation.FromXml(objectName_vCalcEl) : new Calculation(""); + var repetition_vWrapEl = step.Element("Repetition"); + var repetition_vCalcEl = repetition_vWrapEl?.Element("Calculation"); + var repetition_v = repetition_vCalcEl is not null ? Calculation.FromXml(repetition_vCalcEl) : new Calculation(""); + return new RefreshObjectStep(objectName_v, repetition_v, enabled); + } + + public static ScriptStep FromDisplayParams(bool enabled, string[] hrParams) + { + var tokens = hrParams.Select(h => h.Trim()).ToArray(); + Calculation? objectName_v = null; + foreach (var tok in tokens) { if (tok.StartsWith("Object Name:", StringComparison.OrdinalIgnoreCase)) { objectName_v = new Calculation(tok.Substring(12).Trim()); break; } } + Calculation? repetition_v = null; + foreach (var tok in tokens) { if (tok.StartsWith("Repetition:", StringComparison.OrdinalIgnoreCase)) { repetition_v = new Calculation(tok.Substring(11).Trim()); break; } } + return new RefreshObjectStep(objectName_v, repetition_v, enabled); + } + + public static StepMetadata Metadata { get; } = new() + { + Name = XmlName, + Id = XmlId, + Category = "miscellaneous", + HelpUrl = "https://help.claris.com/en/pro-help/content/refresh-object.html", + Params = + [ + new ParamMetadata + { + Name = "Calculation", + XmlElement = "Calculation", + Type = "namedCalc", + HrLabel = "Object Name", + }, + new ParamMetadata + { + Name = "Calculation", + XmlElement = "Calculation", + Type = "namedCalc", + HrLabel = "Repetition", + }, + ], + FromXml = FromXml, + FromDisplay = FromDisplayParams, + }; +} diff --git a/src/SharpFM.Model/Scripting/Steps/RefreshPortalStep.cs b/src/SharpFM.Model/Scripting/Steps/RefreshPortalStep.cs new file mode 100644 index 0000000..3d2d99e --- /dev/null +++ b/src/SharpFM.Model/Scripting/Steps/RefreshPortalStep.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Values; + +namespace SharpFM.Model.Scripting.Steps; + +public sealed class RefreshPortalStep : ScriptStep, IStepFactory +{ + public const int XmlId = 180; + public const string XmlName = "Refresh Portal"; + + public Calculation ObjectName { get; set; } + + public RefreshPortalStep( + Calculation? objectName = null, + bool enabled = true) + : base(null, enabled) + { + ObjectName = objectName ?? new Calculation(""); + } + + public override XElement ToXml() => + new("Step", + new XAttribute("enable", Enabled ? "True" : "False"), + new XAttribute("id", XmlId), + new XAttribute("name", XmlName), + new XElement("ObjectName", ObjectName.ToXml("Calculation"))); + + public override string ToDisplayLine() => + "Refresh Portal [ " + "Object Name: " + ObjectName.Text + " ]"; + + public static new ScriptStep FromXml(XElement step) + { + var enabled = step.Attribute("enable")?.Value != "False"; + var objectName_vWrapEl = step.Element("ObjectName"); + var objectName_vCalcEl = objectName_vWrapEl?.Element("Calculation"); + var objectName_v = objectName_vCalcEl is not null ? Calculation.FromXml(objectName_vCalcEl) : new Calculation(""); + return new RefreshPortalStep(objectName_v, enabled); + } + + public static ScriptStep FromDisplayParams(bool enabled, string[] hrParams) + { + var tokens = hrParams.Select(h => h.Trim()).ToArray(); + Calculation? objectName_v = null; + foreach (var tok in tokens) { if (tok.StartsWith("Object Name:", StringComparison.OrdinalIgnoreCase)) { objectName_v = new Calculation(tok.Substring(12).Trim()); break; } } + return new RefreshPortalStep(objectName_v, enabled); + } + + public static StepMetadata Metadata { get; } = new() + { + Name = XmlName, + Id = XmlId, + Category = "miscellaneous", + HelpUrl = "https://help.claris.com/en/pro-help/content/refresh-portal.html", + Params = + [ + new ParamMetadata + { + Name = "Calculation", + XmlElement = "Calculation", + Type = "namedCalc", + HrLabel = "Object Name", + }, + ], + FromXml = FromXml, + FromDisplay = FromDisplayParams, + }; +} diff --git a/src/SharpFM.Model/Scripting/Steps/RenameFileStep.cs b/src/SharpFM.Model/Scripting/Steps/RenameFileStep.cs new file mode 100644 index 0000000..912c7bf --- /dev/null +++ b/src/SharpFM.Model/Scripting/Steps/RenameFileStep.cs @@ -0,0 +1,84 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Values; + +namespace SharpFM.Model.Scripting.Steps; + +public sealed class RenameFileStep : ScriptStep, IStepFactory +{ + public const int XmlId = 199; + public const string XmlName = "Rename File"; + + public string SourceFile { get; set; } + public Calculation NewName { get; set; } + + public RenameFileStep( + string sourceFile = "", + Calculation? newName = null, + bool enabled = true) + : base(null, enabled) + { + SourceFile = sourceFile; + NewName = newName ?? new Calculation(""); + } + + public override XElement ToXml() => + new("Step", + new XAttribute("enable", Enabled ? "True" : "False"), + new XAttribute("id", XmlId), + new XAttribute("name", XmlName), + new XElement("UniversalPathList", SourceFile), + NewName.ToXml("Calculation")); + + public override string ToDisplayLine() => + "Rename File [ " + "Source file: " + SourceFile + " ; " + "New name: " + NewName.Text + " ]"; + + public static new ScriptStep FromXml(XElement step) + { + var enabled = step.Attribute("enable")?.Value != "False"; + var sourceFile_v = step.Element("UniversalPathList")?.Value ?? ""; + var newName_vEl = step.Element("Calculation"); + var newName_v = newName_vEl is not null ? Calculation.FromXml(newName_vEl) : new Calculation(""); + return new RenameFileStep(sourceFile_v, newName_v, enabled); + } + + public static ScriptStep FromDisplayParams(bool enabled, string[] hrParams) + { + var tokens = hrParams.Select(h => h.Trim()).ToArray(); + string sourceFile_v = ""; + foreach (var tok in tokens) { if (tok.StartsWith("Source file:", StringComparison.OrdinalIgnoreCase)) { sourceFile_v = tok.Substring(12).Trim(); break; } } + Calculation? newName_v = null; + foreach (var tok in tokens) { if (tok.StartsWith("New name:", StringComparison.OrdinalIgnoreCase)) { newName_v = new Calculation(tok.Substring(9).Trim()); break; } } + return new RenameFileStep(sourceFile_v, newName_v, enabled); + } + + public static StepMetadata Metadata { get; } = new() + { + Name = XmlName, + Id = XmlId, + Category = "files", + HelpUrl = "https://help.claris.com/en/pro-help/content/rename-file.html", + Params = + [ + new ParamMetadata + { + Name = "UniversalPathList", + XmlElement = "UniversalPathList", + Type = "text", + HrLabel = "Source file", + }, + new ParamMetadata + { + Name = "Calculation", + XmlElement = "Calculation", + Type = "calculation", + HrLabel = "New name", + }, + ], + FromXml = FromXml, + FromDisplay = FromDisplayParams, + }; +} diff --git a/src/SharpFM.Model/Scripting/Steps/ResetAccountPasswordStep.cs b/src/SharpFM.Model/Scripting/Steps/ResetAccountPasswordStep.cs new file mode 100644 index 0000000..5c0f68c --- /dev/null +++ b/src/SharpFM.Model/Scripting/Steps/ResetAccountPasswordStep.cs @@ -0,0 +1,104 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Values; + +namespace SharpFM.Model.Scripting.Steps; + +public sealed class ResetAccountPasswordStep : ScriptStep, IStepFactory +{ + public const int XmlId = 136; + public const string XmlName = "Reset Account Password"; + + public Calculation AccountName { get; set; } + public Calculation Password { get; set; } + public bool ExpirePassword { get; set; } + + public ResetAccountPasswordStep( + Calculation? accountName = null, + Calculation? password = null, + bool expirePassword = false, + bool enabled = true) + : base(null, enabled) + { + AccountName = accountName ?? new Calculation(""); + Password = password ?? new Calculation(""); + ExpirePassword = expirePassword; + } + + public override XElement ToXml() => + new("Step", + new XAttribute("enable", Enabled ? "True" : "False"), + new XAttribute("id", XmlId), + new XAttribute("name", XmlName), + new XElement("AccountName", AccountName.ToXml("Calculation")), + new XElement("Password", Password.ToXml("Calculation")), + new XElement("ChgPwdOnNextLogin", new XAttribute("value", ExpirePassword ? "True" : "False"))); + + public override string ToDisplayLine() => + "Reset Account Password [ " + "Account Name: " + AccountName.Text + " ; " + "Password: " + Password.Text + " ; " + "Expire password: " + (ExpirePassword ? "On" : "Off") + " ]"; + + public static new ScriptStep FromXml(XElement step) + { + var enabled = step.Attribute("enable")?.Value != "False"; + var accountName_vWrapEl = step.Element("AccountName"); + var accountName_vCalcEl = accountName_vWrapEl?.Element("Calculation"); + var accountName_v = accountName_vCalcEl is not null ? Calculation.FromXml(accountName_vCalcEl) : new Calculation(""); + var password_vWrapEl = step.Element("Password"); + var password_vCalcEl = password_vWrapEl?.Element("Calculation"); + var password_v = password_vCalcEl is not null ? Calculation.FromXml(password_vCalcEl) : new Calculation(""); + var expirePassword_v = step.Element("ChgPwdOnNextLogin")?.Attribute("value")?.Value == "True"; + return new ResetAccountPasswordStep(accountName_v, password_v, expirePassword_v, enabled); + } + + public static ScriptStep FromDisplayParams(bool enabled, string[] hrParams) + { + var tokens = hrParams.Select(h => h.Trim()).ToArray(); + Calculation? accountName_v = null; + foreach (var tok in tokens) { if (tok.StartsWith("Account Name:", StringComparison.OrdinalIgnoreCase)) { accountName_v = new Calculation(tok.Substring(13).Trim()); break; } } + Calculation? password_v = null; + foreach (var tok in tokens) { if (tok.StartsWith("Password:", StringComparison.OrdinalIgnoreCase)) { password_v = new Calculation(tok.Substring(9).Trim()); break; } } + bool expirePassword_v = false; + foreach (var tok in tokens) { if (tok.StartsWith("Expire password:", StringComparison.OrdinalIgnoreCase)) { var v = tok.Substring(16).Trim(); expirePassword_v = v.Equals("On", StringComparison.OrdinalIgnoreCase); break; } } + return new ResetAccountPasswordStep(accountName_v, password_v, expirePassword_v, enabled); + } + + public static StepMetadata Metadata { get; } = new() + { + Name = XmlName, + Id = XmlId, + Category = "accounts", + HelpUrl = "https://help.claris.com/en/pro-help/content/reset-account-password.html", + Params = + [ + new ParamMetadata + { + Name = "Calculation", + XmlElement = "Calculation", + Type = "namedCalc", + HrLabel = "Account Name", + }, + new ParamMetadata + { + Name = "Calculation", + XmlElement = "Calculation", + Type = "namedCalc", + HrLabel = "Password", + }, + new ParamMetadata + { + Name = "ChgPwdOnNextLogin", + XmlElement = "ChgPwdOnNextLogin", + Type = "boolean", + XmlAttr = "value", + HrLabel = "Expire password", + ValidValues = ["On", "Off"], + DefaultValue = "False", + }, + ], + FromXml = FromXml, + FromDisplay = FromDisplayParams, + }; +} diff --git a/src/SharpFM.Model/Scripting/Steps/RevertTransactionStep.cs b/src/SharpFM.Model/Scripting/Steps/RevertTransactionStep.cs new file mode 100644 index 0000000..2fac7f4 --- /dev/null +++ b/src/SharpFM.Model/Scripting/Steps/RevertTransactionStep.cs @@ -0,0 +1,118 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Values; + +namespace SharpFM.Model.Scripting.Steps; + +public sealed class RevertTransactionStep : ScriptStep, IStepFactory +{ + public const int XmlId = 207; + public const string XmlName = "Revert Transaction"; + + public bool Option { get; set; } + public Calculation Condition { get; set; } + public Calculation ErrorCode { get; set; } + public Calculation ErrorMessage { get; set; } + + public RevertTransactionStep( + bool option = false, + Calculation? condition = null, + Calculation? errorCode = null, + Calculation? errorMessage = null, + bool enabled = true) + : base(null, enabled) + { + Option = option; + Condition = condition ?? new Calculation(""); + ErrorCode = errorCode ?? new Calculation(""); + ErrorMessage = errorMessage ?? new Calculation(""); + } + + public override XElement ToXml() => + new("Step", + new XAttribute("enable", Enabled ? "True" : "False"), + new XAttribute("id", XmlId), + new XAttribute("name", XmlName), + new XElement("Option", new XAttribute("state", Option ? "True" : "False")), + new XElement("Condition", Condition.ToXml("Calculation")), + new XElement("ErrorCode", ErrorCode.ToXml("Calculation")), + new XElement("ErrorMessage", ErrorMessage.ToXml("Calculation"))); + + public override string ToDisplayLine() => + "Revert Transaction [ " + (Option ? "On" : "Off") + " ; " + "Condition: " + Condition.Text + " ; " + "Error Code: " + ErrorCode.Text + " ; " + "Error Message: " + ErrorMessage.Text + " ]"; + + public static new ScriptStep FromXml(XElement step) + { + var enabled = step.Attribute("enable")?.Value != "False"; + var option_v = step.Element("Option")?.Attribute("state")?.Value == "True"; + var condition_vWrapEl = step.Element("Condition"); + var condition_vCalcEl = condition_vWrapEl?.Element("Calculation"); + var condition_v = condition_vCalcEl is not null ? Calculation.FromXml(condition_vCalcEl) : new Calculation(""); + var errorCode_vWrapEl = step.Element("ErrorCode"); + var errorCode_vCalcEl = errorCode_vWrapEl?.Element("Calculation"); + var errorCode_v = errorCode_vCalcEl is not null ? Calculation.FromXml(errorCode_vCalcEl) : new Calculation(""); + var errorMessage_vWrapEl = step.Element("ErrorMessage"); + var errorMessage_vCalcEl = errorMessage_vWrapEl?.Element("Calculation"); + var errorMessage_v = errorMessage_vCalcEl is not null ? Calculation.FromXml(errorMessage_vCalcEl) : new Calculation(""); + return new RevertTransactionStep(option_v, condition_v, errorCode_v, errorMessage_v, enabled); + } + + public static ScriptStep FromDisplayParams(bool enabled, string[] hrParams) + { + var tokens = hrParams.Select(h => h.Trim()).ToArray(); + bool option_v = false; + Calculation? condition_v = null; + foreach (var tok in tokens) { if (tok.StartsWith("Condition:", StringComparison.OrdinalIgnoreCase)) { condition_v = new Calculation(tok.Substring(10).Trim()); break; } } + Calculation? errorCode_v = null; + foreach (var tok in tokens) { if (tok.StartsWith("Error Code:", StringComparison.OrdinalIgnoreCase)) { errorCode_v = new Calculation(tok.Substring(11).Trim()); break; } } + Calculation? errorMessage_v = null; + foreach (var tok in tokens) { if (tok.StartsWith("Error Message:", StringComparison.OrdinalIgnoreCase)) { errorMessage_v = new Calculation(tok.Substring(14).Trim()); break; } } + return new RevertTransactionStep(option_v, condition_v, errorCode_v, errorMessage_v, enabled); + } + + public static StepMetadata Metadata { get; } = new() + { + Name = XmlName, + Id = XmlId, + Category = "control", + HelpUrl = "https://help.claris.com/en/pro-help/content/revert-transaction.html", + Params = + [ + new ParamMetadata + { + Name = "Option", + XmlElement = "Option", + Type = "flagBoolean", + XmlAttr = "state", + ValidValues = ["On", "Off"], + DefaultValue = "False", + }, + new ParamMetadata + { + Name = "Calculation", + XmlElement = "Calculation", + Type = "namedCalc", + HrLabel = "Condition", + }, + new ParamMetadata + { + Name = "Calculation", + XmlElement = "Calculation", + Type = "namedCalc", + HrLabel = "Error Code", + }, + new ParamMetadata + { + Name = "Calculation", + XmlElement = "Calculation", + Type = "namedCalc", + HrLabel = "Error Message", + }, + ], + FromXml = FromXml, + FromDisplay = FromDisplayParams, + }; +} diff --git a/src/SharpFM.Model/Scripting/Steps/SaveACopyAsAddOnPackageStep.cs b/src/SharpFM.Model/Scripting/Steps/SaveACopyAsAddOnPackageStep.cs new file mode 100644 index 0000000..935839f --- /dev/null +++ b/src/SharpFM.Model/Scripting/Steps/SaveACopyAsAddOnPackageStep.cs @@ -0,0 +1,87 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Values; + +namespace SharpFM.Model.Scripting.Steps; + +public sealed class SaveACopyAsAddOnPackageStep : ScriptStep, IStepFactory +{ + public const int XmlId = 96; + public const string XmlName = "Save a Copy as Add-on Package"; + + public bool ReplaceUUIDs { get; set; } + public Calculation WindowName { get; set; } + + public SaveACopyAsAddOnPackageStep( + bool replaceUUIDs = false, + Calculation? windowName = null, + bool enabled = true) + : base(null, enabled) + { + ReplaceUUIDs = replaceUUIDs; + WindowName = windowName ?? new Calculation(""); + } + + public override XElement ToXml() => + new("Step", + new XAttribute("enable", Enabled ? "True" : "False"), + new XAttribute("id", XmlId), + new XAttribute("name", XmlName), + new XElement("LinkAvail", new XAttribute("state", ReplaceUUIDs ? "True" : "False")), + WindowName.ToXml("Calculation")); + + public override string ToDisplayLine() => + "Save a Copy as Add-on Package [ " + "Replace UUIDs: " + (ReplaceUUIDs ? "On" : "Off") + " ; " + "Window name: " + WindowName.Text + " ]"; + + public static new ScriptStep FromXml(XElement step) + { + var enabled = step.Attribute("enable")?.Value != "False"; + var replaceUUIDs_v = step.Element("LinkAvail")?.Attribute("state")?.Value == "True"; + var windowName_vEl = step.Element("Calculation"); + var windowName_v = windowName_vEl is not null ? Calculation.FromXml(windowName_vEl) : new Calculation(""); + return new SaveACopyAsAddOnPackageStep(replaceUUIDs_v, windowName_v, enabled); + } + + public static ScriptStep FromDisplayParams(bool enabled, string[] hrParams) + { + var tokens = hrParams.Select(h => h.Trim()).ToArray(); + bool replaceUUIDs_v = false; + foreach (var tok in tokens) { if (tok.StartsWith("Replace UUIDs:", StringComparison.OrdinalIgnoreCase)) { var v = tok.Substring(14).Trim(); replaceUUIDs_v = v.Equals("On", StringComparison.OrdinalIgnoreCase); break; } } + Calculation? windowName_v = null; + foreach (var tok in tokens) { if (tok.StartsWith("Window name:", StringComparison.OrdinalIgnoreCase)) { windowName_v = new Calculation(tok.Substring(12).Trim()); break; } } + return new SaveACopyAsAddOnPackageStep(replaceUUIDs_v, windowName_v, enabled); + } + + public static StepMetadata Metadata { get; } = new() + { + Name = XmlName, + Id = XmlId, + Category = "miscellaneous", + HelpUrl = "https://help.claris.com/en/pro-help/content/save-a-copy-as-add-on-package.html", + Params = + [ + new ParamMetadata + { + Name = "LinkAvail", + XmlElement = "LinkAvail", + Type = "boolean", + XmlAttr = "state", + HrLabel = "Replace UUIDs", + ValidValues = ["On", "Off"], + DefaultValue = "False", + }, + new ParamMetadata + { + Name = "Calculation", + XmlElement = "Calculation", + Type = "calculation", + HrLabel = "Window name", + }, + ], + FromXml = FromXml, + FromDisplay = FromDisplayParams, + }; +} diff --git a/src/SharpFM.Model/Scripting/Steps/SaveACopyAsXMLStep.cs b/src/SharpFM.Model/Scripting/Steps/SaveACopyAsXMLStep.cs new file mode 100644 index 0000000..5b0855f --- /dev/null +++ b/src/SharpFM.Model/Scripting/Steps/SaveACopyAsXMLStep.cs @@ -0,0 +1,101 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Values; + +namespace SharpFM.Model.Scripting.Steps; + +public sealed class SaveACopyAsXMLStep : ScriptStep, IStepFactory +{ + public const int XmlId = 3; + public const string XmlName = "Save a Copy as XML"; + + public bool IncludeDetailsForAnalysisTools { get; set; } + public string DestinationFile { get; set; } + public Calculation WindowName { get; set; } + + public SaveACopyAsXMLStep( + bool includeDetailsForAnalysisTools = false, + string destinationFile = "", + Calculation? windowName = null, + bool enabled = true) + : base(null, enabled) + { + IncludeDetailsForAnalysisTools = includeDetailsForAnalysisTools; + DestinationFile = destinationFile; + WindowName = windowName ?? new Calculation(""); + } + + public override XElement ToXml() => + new("Step", + new XAttribute("enable", Enabled ? "True" : "False"), + new XAttribute("id", XmlId), + new XAttribute("name", XmlName), + new XElement("Option", new XAttribute("state", IncludeDetailsForAnalysisTools ? "True" : "False")), + new XElement("UniversalPathList", DestinationFile), + WindowName.ToXml("Calculation")); + + public override string ToDisplayLine() => + "Save a Copy as XML [ " + "Include details for analysis tools: " + (IncludeDetailsForAnalysisTools ? "On" : "Off") + " ; " + "Destination file: " + DestinationFile + " ; " + "Window name: " + WindowName.Text + " ]"; + + public static new ScriptStep FromXml(XElement step) + { + var enabled = step.Attribute("enable")?.Value != "False"; + var includeDetailsForAnalysisTools_v = step.Element("Option")?.Attribute("state")?.Value == "True"; + var destinationFile_v = step.Element("UniversalPathList")?.Value ?? ""; + var windowName_vEl = step.Element("Calculation"); + var windowName_v = windowName_vEl is not null ? Calculation.FromXml(windowName_vEl) : new Calculation(""); + return new SaveACopyAsXMLStep(includeDetailsForAnalysisTools_v, destinationFile_v, windowName_v, enabled); + } + + public static ScriptStep FromDisplayParams(bool enabled, string[] hrParams) + { + var tokens = hrParams.Select(h => h.Trim()).ToArray(); + bool includeDetailsForAnalysisTools_v = false; + foreach (var tok in tokens) { if (tok.StartsWith("Include details for analysis tools:", StringComparison.OrdinalIgnoreCase)) { var v = tok.Substring(35).Trim(); includeDetailsForAnalysisTools_v = v.Equals("On", StringComparison.OrdinalIgnoreCase); break; } } + string destinationFile_v = ""; + foreach (var tok in tokens) { if (tok.StartsWith("Destination file:", StringComparison.OrdinalIgnoreCase)) { destinationFile_v = tok.Substring(17).Trim(); break; } } + Calculation? windowName_v = null; + foreach (var tok in tokens) { if (tok.StartsWith("Window name:", StringComparison.OrdinalIgnoreCase)) { windowName_v = new Calculation(tok.Substring(12).Trim()); break; } } + return new SaveACopyAsXMLStep(includeDetailsForAnalysisTools_v, destinationFile_v, windowName_v, enabled); + } + + public static StepMetadata Metadata { get; } = new() + { + Name = XmlName, + Id = XmlId, + Category = "files", + HelpUrl = "https://help.claris.com/en/pro-help/content/save-a-copy-as-xml.html", + Params = + [ + new ParamMetadata + { + Name = "Option", + XmlElement = "Option", + Type = "flagBoolean", + XmlAttr = "state", + HrLabel = "Include details for analysis tools", + ValidValues = ["On", "Off"], + DefaultValue = "False", + }, + new ParamMetadata + { + Name = "UniversalPathList", + XmlElement = "UniversalPathList", + Type = "text", + HrLabel = "Destination file", + }, + new ParamMetadata + { + Name = "Calculation", + XmlElement = "Calculation", + Type = "calculation", + HrLabel = "Window name", + }, + ], + FromXml = FromXml, + FromDisplay = FromDisplayParams, + }; +} diff --git a/src/SharpFM.Model/Scripting/Steps/SelectWindowStep.cs b/src/SharpFM.Model/Scripting/Steps/SelectWindowStep.cs new file mode 100644 index 0000000..c5b7c26 --- /dev/null +++ b/src/SharpFM.Model/Scripting/Steps/SelectWindowStep.cs @@ -0,0 +1,118 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Values; + +namespace SharpFM.Model.Scripting.Steps; + +public sealed class SelectWindowStep : ScriptStep, IStepFactory +{ + public const int XmlId = 123; + public const string XmlName = "Select Window"; + + public bool CurrentFile { get; set; } + public string Window { get; set; } + public Calculation Name { get; set; } + + public SelectWindowStep( + bool currentFile = true, + string window = "ByName", + Calculation? name = null, + bool enabled = true) + : base(null, enabled) + { + CurrentFile = currentFile; + Window = window; + Name = name ?? new Calculation(""); + } + + private static readonly IReadOnlyDictionary _WindowToHr = + new Dictionary(StringComparer.Ordinal) { + ["ByName"] = "Name: ", + ["Current"] = "Current Window", + }; + private static readonly IReadOnlyDictionary _WindowFromHr = + new Dictionary(StringComparer.OrdinalIgnoreCase) { + ["Name: "] = "ByName", + ["Current Window"] = "Current", + }; + private static string WindowHr(string x) => _WindowToHr.TryGetValue(x, out var h) ? h : x; + private static string WindowXml(string h) => _WindowFromHr.TryGetValue(h, out var x) ? x : h; + + public override XElement ToXml() => + new("Step", + new XAttribute("enable", Enabled ? "True" : "False"), + new XAttribute("id", XmlId), + new XAttribute("name", XmlName), + new XElement("LimitToWindowsOfCurrentFile", new XAttribute("state", CurrentFile ? "True" : "False")), + new XElement("Window", new XAttribute("value", Window)), + new XElement("Name", Name.ToXml("Calculation"))); + + public override string ToDisplayLine() => + "Select Window [ " + "Current file: " + (CurrentFile ? "On" : "Off") + " ; " + "Window: " + WindowHr(Window) + " ; " + "Name: " + Name.Text + " ]"; + + public static new ScriptStep FromXml(XElement step) + { + var enabled = step.Attribute("enable")?.Value != "False"; + var currentFile_v = step.Element("LimitToWindowsOfCurrentFile")?.Attribute("state")?.Value == "True"; + var window_v = step.Element("Window")?.Attribute("value")?.Value ?? "ByName"; + var name_vWrapEl = step.Element("Name"); + var name_vCalcEl = name_vWrapEl?.Element("Calculation"); + var name_v = name_vCalcEl is not null ? Calculation.FromXml(name_vCalcEl) : new Calculation(""); + return new SelectWindowStep(currentFile_v, window_v, name_v, enabled); + } + + public static ScriptStep FromDisplayParams(bool enabled, string[] hrParams) + { + var tokens = hrParams.Select(h => h.Trim()).ToArray(); + bool currentFile_v = true; + foreach (var tok in tokens) { if (tok.StartsWith("Current file:", StringComparison.OrdinalIgnoreCase)) { var v = tok.Substring(13).Trim(); currentFile_v = v.Equals("On", StringComparison.OrdinalIgnoreCase); break; } } + string window_v = "ByName"; + foreach (var tok in tokens) { if (tok.StartsWith("Window:", StringComparison.OrdinalIgnoreCase)) { var v = tok.Substring(7).Trim(); window_v = WindowXml(v); break; } } + Calculation? name_v = null; + foreach (var tok in tokens) { if (tok.StartsWith("Name:", StringComparison.OrdinalIgnoreCase)) { name_v = new Calculation(tok.Substring(5).Trim()); break; } } + return new SelectWindowStep(currentFile_v, window_v, name_v, enabled); + } + + public static StepMetadata Metadata { get; } = new() + { + Name = XmlName, + Id = XmlId, + Category = "windows", + HelpUrl = "https://help.claris.com/en/pro-help/content/select-window.html", + Params = + [ + new ParamMetadata + { + Name = "LimitToWindowsOfCurrentFile", + XmlElement = "LimitToWindowsOfCurrentFile", + Type = "flagBoolean", + XmlAttr = "state", + HrLabel = "Current file", + ValidValues = ["On", "Off"], + DefaultValue = "True", + }, + new ParamMetadata + { + Name = "Window", + XmlElement = "Window", + Type = "enum", + XmlAttr = "value", + HrLabel = "Window", + ValidValues = ["Name: ", "Current Window"], + DefaultValue = "ByName", + }, + new ParamMetadata + { + Name = "Calculation", + XmlElement = "Calculation", + Type = "namedCalc", + HrLabel = "Name", + }, + ], + FromXml = FromXml, + FromDisplay = FromDisplayParams, + }; +} diff --git a/src/SharpFM.Model/Scripting/Steps/SetDataFilePositionStep.cs b/src/SharpFM.Model/Scripting/Steps/SetDataFilePositionStep.cs new file mode 100644 index 0000000..2a66441 --- /dev/null +++ b/src/SharpFM.Model/Scripting/Steps/SetDataFilePositionStep.cs @@ -0,0 +1,86 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Values; + +namespace SharpFM.Model.Scripting.Steps; + +public sealed class SetDataFilePositionStep : ScriptStep, IStepFactory +{ + public const int XmlId = 195; + public const string XmlName = "Set Data File Position"; + + public Calculation FileID { get; set; } + public Calculation NewPosition { get; set; } + + public SetDataFilePositionStep( + Calculation? fileID = null, + Calculation? newPosition = null, + bool enabled = true) + : base(null, enabled) + { + FileID = fileID ?? new Calculation(""); + NewPosition = newPosition ?? new Calculation(""); + } + + public override XElement ToXml() => + new("Step", + new XAttribute("enable", Enabled ? "True" : "False"), + new XAttribute("id", XmlId), + new XAttribute("name", XmlName), + FileID.ToXml("Calculation"), + new XElement("position", NewPosition.ToXml("Calculation"))); + + public override string ToDisplayLine() => + "Set Data File Position [ " + "File ID: " + FileID.Text + " ; " + "New position: " + NewPosition.Text + " ]"; + + public static new ScriptStep FromXml(XElement step) + { + var enabled = step.Attribute("enable")?.Value != "False"; + var fileID_vEl = step.Element("Calculation"); + var fileID_v = fileID_vEl is not null ? Calculation.FromXml(fileID_vEl) : new Calculation(""); + var newPosition_vWrapEl = step.Element("position"); + var newPosition_vCalcEl = newPosition_vWrapEl?.Element("Calculation"); + var newPosition_v = newPosition_vCalcEl is not null ? Calculation.FromXml(newPosition_vCalcEl) : new Calculation(""); + return new SetDataFilePositionStep(fileID_v, newPosition_v, enabled); + } + + public static ScriptStep FromDisplayParams(bool enabled, string[] hrParams) + { + var tokens = hrParams.Select(h => h.Trim()).ToArray(); + Calculation? fileID_v = null; + foreach (var tok in tokens) { if (tok.StartsWith("File ID:", StringComparison.OrdinalIgnoreCase)) { fileID_v = new Calculation(tok.Substring(8).Trim()); break; } } + Calculation? newPosition_v = null; + foreach (var tok in tokens) { if (tok.StartsWith("New position:", StringComparison.OrdinalIgnoreCase)) { newPosition_v = new Calculation(tok.Substring(13).Trim()); break; } } + return new SetDataFilePositionStep(fileID_v, newPosition_v, enabled); + } + + public static StepMetadata Metadata { get; } = new() + { + Name = XmlName, + Id = XmlId, + Category = "files", + HelpUrl = "https://help.claris.com/en/pro-help/content/set-data-file-position.html", + Params = + [ + new ParamMetadata + { + Name = "Calculation", + XmlElement = "Calculation", + Type = "calculation", + HrLabel = "File ID", + }, + new ParamMetadata + { + Name = "Calculation", + XmlElement = "Calculation", + Type = "namedCalc", + HrLabel = "New position", + }, + ], + FromXml = FromXml, + FromDisplay = FromDisplayParams, + }; +} diff --git a/src/SharpFM.Model/Scripting/Steps/SetErrorLoggingStep.cs b/src/SharpFM.Model/Scripting/Steps/SetErrorLoggingStep.cs new file mode 100644 index 0000000..2243683 --- /dev/null +++ b/src/SharpFM.Model/Scripting/Steps/SetErrorLoggingStep.cs @@ -0,0 +1,87 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Values; + +namespace SharpFM.Model.Scripting.Steps; + +public sealed class SetErrorLoggingStep : ScriptStep, IStepFactory +{ + public const int XmlId = 200; + public const string XmlName = "Set Error Logging"; + + public bool Logging { get; set; } + public Calculation CustomDebugInfo { get; set; } + + public SetErrorLoggingStep( + bool logging = false, + Calculation? customDebugInfo = null, + bool enabled = true) + : base(null, enabled) + { + Logging = logging; + CustomDebugInfo = customDebugInfo ?? new Calculation(""); + } + + public override XElement ToXml() => + new("Step", + new XAttribute("enable", Enabled ? "True" : "False"), + new XAttribute("id", XmlId), + new XAttribute("name", XmlName), + new XElement("Option", new XAttribute("state", Logging ? "True" : "False")), + CustomDebugInfo.ToXml("Calculation")); + + public override string ToDisplayLine() => + "Set Error Logging [ " + "Logging: " + (Logging ? "On" : "Off") + " ; " + "Custom debug info: " + CustomDebugInfo.Text + " ]"; + + public static new ScriptStep FromXml(XElement step) + { + var enabled = step.Attribute("enable")?.Value != "False"; + var logging_v = step.Element("Option")?.Attribute("state")?.Value == "True"; + var customDebugInfo_vEl = step.Element("Calculation"); + var customDebugInfo_v = customDebugInfo_vEl is not null ? Calculation.FromXml(customDebugInfo_vEl) : new Calculation(""); + return new SetErrorLoggingStep(logging_v, customDebugInfo_v, enabled); + } + + public static ScriptStep FromDisplayParams(bool enabled, string[] hrParams) + { + var tokens = hrParams.Select(h => h.Trim()).ToArray(); + bool logging_v = false; + foreach (var tok in tokens) { if (tok.StartsWith("Logging:", StringComparison.OrdinalIgnoreCase)) { var v = tok.Substring(8).Trim(); logging_v = v.Equals("On", StringComparison.OrdinalIgnoreCase); break; } } + Calculation? customDebugInfo_v = null; + foreach (var tok in tokens) { if (tok.StartsWith("Custom debug info:", StringComparison.OrdinalIgnoreCase)) { customDebugInfo_v = new Calculation(tok.Substring(18).Trim()); break; } } + return new SetErrorLoggingStep(logging_v, customDebugInfo_v, enabled); + } + + public static StepMetadata Metadata { get; } = new() + { + Name = XmlName, + Id = XmlId, + Category = "control", + HelpUrl = "https://help.claris.com/en/pro-help/content/set-error-logging.html", + Params = + [ + new ParamMetadata + { + Name = "Option", + XmlElement = "Option", + Type = "boolean", + XmlAttr = "state", + HrLabel = "Logging", + ValidValues = ["On", "Off"], + DefaultValue = "False", + }, + new ParamMetadata + { + Name = "Calculation", + XmlElement = "Calculation", + Type = "calculation", + HrLabel = "Custom debug info", + }, + ], + FromXml = FromXml, + FromDisplay = FromDisplayParams, + }; +} diff --git a/src/SharpFM.Model/Scripting/Steps/SetFieldByNameStep.cs b/src/SharpFM.Model/Scripting/Steps/SetFieldByNameStep.cs new file mode 100644 index 0000000..a23bb71 --- /dev/null +++ b/src/SharpFM.Model/Scripting/Steps/SetFieldByNameStep.cs @@ -0,0 +1,87 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Values; + +namespace SharpFM.Model.Scripting.Steps; + +public sealed class SetFieldByNameStep : ScriptStep, IStepFactory +{ + public const int XmlId = 147; + public const string XmlName = "Set Field By Name"; + + public Calculation TargetFieldName { get; set; } + public Calculation CalculatedResult { get; set; } + + public SetFieldByNameStep( + Calculation? targetFieldName = null, + Calculation? calculatedResult = null, + bool enabled = true) + : base(null, enabled) + { + TargetFieldName = targetFieldName ?? new Calculation(""); + CalculatedResult = calculatedResult ?? new Calculation(""); + } + + public override XElement ToXml() => + new("Step", + new XAttribute("enable", Enabled ? "True" : "False"), + new XAttribute("id", XmlId), + new XAttribute("name", XmlName), + new XElement("TargetName", TargetFieldName.ToXml("Calculation")), + new XElement("Result", CalculatedResult.ToXml("Calculation"))); + + public override string ToDisplayLine() => + "Set Field By Name [ " + "Target field name: " + TargetFieldName.Text + " ; " + "Calculated result: " + CalculatedResult.Text + " ]"; + + public static new ScriptStep FromXml(XElement step) + { + var enabled = step.Attribute("enable")?.Value != "False"; + var targetFieldName_vWrapEl = step.Element("TargetName"); + var targetFieldName_vCalcEl = targetFieldName_vWrapEl?.Element("Calculation"); + var targetFieldName_v = targetFieldName_vCalcEl is not null ? Calculation.FromXml(targetFieldName_vCalcEl) : new Calculation(""); + var calculatedResult_vWrapEl = step.Element("Result"); + var calculatedResult_vCalcEl = calculatedResult_vWrapEl?.Element("Calculation"); + var calculatedResult_v = calculatedResult_vCalcEl is not null ? Calculation.FromXml(calculatedResult_vCalcEl) : new Calculation(""); + return new SetFieldByNameStep(targetFieldName_v, calculatedResult_v, enabled); + } + + public static ScriptStep FromDisplayParams(bool enabled, string[] hrParams) + { + var tokens = hrParams.Select(h => h.Trim()).ToArray(); + Calculation? targetFieldName_v = null; + foreach (var tok in tokens) { if (tok.StartsWith("Target field name:", StringComparison.OrdinalIgnoreCase)) { targetFieldName_v = new Calculation(tok.Substring(18).Trim()); break; } } + Calculation? calculatedResult_v = null; + foreach (var tok in tokens) { if (tok.StartsWith("Calculated result:", StringComparison.OrdinalIgnoreCase)) { calculatedResult_v = new Calculation(tok.Substring(18).Trim()); break; } } + return new SetFieldByNameStep(targetFieldName_v, calculatedResult_v, enabled); + } + + public static StepMetadata Metadata { get; } = new() + { + Name = XmlName, + Id = XmlId, + Category = "fields", + HelpUrl = "https://help.claris.com/en/pro-help/content/set-field-by-name.html", + Params = + [ + new ParamMetadata + { + Name = "Calculation", + XmlElement = "Calculation", + Type = "namedCalc", + HrLabel = "Target field name", + }, + new ParamMetadata + { + Name = "Calculation", + XmlElement = "Calculation", + Type = "namedCalc", + HrLabel = "Calculated result", + }, + ], + FromXml = FromXml, + FromDisplay = FromDisplayParams, + }; +} diff --git a/src/SharpFM.Model/Scripting/Steps/SetSessionIdentifierStep.cs b/src/SharpFM.Model/Scripting/Steps/SetSessionIdentifierStep.cs new file mode 100644 index 0000000..a48635d --- /dev/null +++ b/src/SharpFM.Model/Scripting/Steps/SetSessionIdentifierStep.cs @@ -0,0 +1,70 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Values; + +namespace SharpFM.Model.Scripting.Steps; + +public sealed class SetSessionIdentifierStep : ScriptStep, IStepFactory +{ + public const int XmlId = 208; + public const string XmlName = "Set Session Identifier"; + + public Calculation SessionIdentifier { get; set; } + + public SetSessionIdentifierStep( + Calculation? sessionIdentifier = null, + bool enabled = true) + : base(null, enabled) + { + SessionIdentifier = sessionIdentifier ?? new Calculation(""); + } + + public override XElement ToXml() => + new("Step", + new XAttribute("enable", Enabled ? "True" : "False"), + new XAttribute("id", XmlId), + new XAttribute("name", XmlName), + SessionIdentifier.ToXml("Calculation")); + + public override string ToDisplayLine() => + "Set Session Identifier [ " + "Session identifier: " + SessionIdentifier.Text + " ]"; + + public static new ScriptStep FromXml(XElement step) + { + var enabled = step.Attribute("enable")?.Value != "False"; + var sessionIdentifier_vEl = step.Element("Calculation"); + var sessionIdentifier_v = sessionIdentifier_vEl is not null ? Calculation.FromXml(sessionIdentifier_vEl) : new Calculation(""); + return new SetSessionIdentifierStep(sessionIdentifier_v, enabled); + } + + public static ScriptStep FromDisplayParams(bool enabled, string[] hrParams) + { + var tokens = hrParams.Select(h => h.Trim()).ToArray(); + Calculation? sessionIdentifier_v = null; + foreach (var tok in tokens) { if (tok.StartsWith("Session identifier:", StringComparison.OrdinalIgnoreCase)) { sessionIdentifier_v = new Calculation(tok.Substring(19).Trim()); break; } } + return new SetSessionIdentifierStep(sessionIdentifier_v, enabled); + } + + public static StepMetadata Metadata { get; } = new() + { + Name = XmlName, + Id = XmlId, + Category = "miscellaneous", + HelpUrl = "https://help.claris.com/en/pro-help/content/set-session-identifier.html", + Params = + [ + new ParamMetadata + { + Name = "Calculation", + XmlElement = "Calculation", + Type = "calculation", + HrLabel = "Session identifier", + }, + ], + FromXml = FromXml, + FromDisplay = FromDisplayParams, + }; +} diff --git a/src/SharpFM.Model/Scripting/Steps/SetWebViewerStep.cs b/src/SharpFM.Model/Scripting/Steps/SetWebViewerStep.cs new file mode 100644 index 0000000..e604687 --- /dev/null +++ b/src/SharpFM.Model/Scripting/Steps/SetWebViewerStep.cs @@ -0,0 +1,123 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Values; + +namespace SharpFM.Model.Scripting.Steps; + +public sealed class SetWebViewerStep : ScriptStep, IStepFactory +{ + public const int XmlId = 146; + public const string XmlName = "Set Web Viewer"; + + public Calculation ObjectName { get; set; } + public string Action { get; set; } + public Calculation URL { get; set; } + + public SetWebViewerStep( + Calculation? objectName = null, + string action = "GoToURL", + Calculation? uRL = null, + bool enabled = true) + : base(null, enabled) + { + ObjectName = objectName ?? new Calculation(""); + Action = action; + URL = uRL ?? new Calculation(""); + } + + private static readonly IReadOnlyDictionary _ActionToHr = + new Dictionary(StringComparer.Ordinal) { + ["GoToURL"] = "Go to URL", + ["Reset"] = "Reset", + ["Reload"] = "Reload", + ["GoForward"] = "Go Forward", + ["GoBack"] = "Go Back", + }; + private static readonly IReadOnlyDictionary _ActionFromHr = + new Dictionary(StringComparer.OrdinalIgnoreCase) { + ["Go to URL"] = "GoToURL", + ["Reset"] = "Reset", + ["Reload"] = "Reload", + ["Go Forward"] = "GoForward", + ["Go Back"] = "GoBack", + }; + private static string ActionHr(string x) => _ActionToHr.TryGetValue(x, out var h) ? h : x; + private static string ActionXml(string h) => _ActionFromHr.TryGetValue(h, out var x) ? x : h; + + public override XElement ToXml() => + new("Step", + new XAttribute("enable", Enabled ? "True" : "False"), + new XAttribute("id", XmlId), + new XAttribute("name", XmlName), + new XElement("ObjectName", ObjectName.ToXml("Calculation")), + new XElement("Action", new XAttribute("value", Action)), + new XElement("URL", URL.ToXml("Calculation"))); + + public override string ToDisplayLine() => + "Set Web Viewer [ " + "Object Name: " + ObjectName.Text + " ; " + "Action: " + ActionHr(Action) + " ; " + "URL: " + URL.Text + " ]"; + + public static new ScriptStep FromXml(XElement step) + { + var enabled = step.Attribute("enable")?.Value != "False"; + var objectName_vWrapEl = step.Element("ObjectName"); + var objectName_vCalcEl = objectName_vWrapEl?.Element("Calculation"); + var objectName_v = objectName_vCalcEl is not null ? Calculation.FromXml(objectName_vCalcEl) : new Calculation(""); + var action_v = step.Element("Action")?.Attribute("value")?.Value ?? "GoToURL"; + var uRL_vWrapEl = step.Element("URL"); + var uRL_vCalcEl = uRL_vWrapEl?.Element("Calculation"); + var uRL_v = uRL_vCalcEl is not null ? Calculation.FromXml(uRL_vCalcEl) : new Calculation(""); + return new SetWebViewerStep(objectName_v, action_v, uRL_v, enabled); + } + + public static ScriptStep FromDisplayParams(bool enabled, string[] hrParams) + { + var tokens = hrParams.Select(h => h.Trim()).ToArray(); + Calculation? objectName_v = null; + foreach (var tok in tokens) { if (tok.StartsWith("Object Name:", StringComparison.OrdinalIgnoreCase)) { objectName_v = new Calculation(tok.Substring(12).Trim()); break; } } + string action_v = "GoToURL"; + foreach (var tok in tokens) { if (tok.StartsWith("Action:", StringComparison.OrdinalIgnoreCase)) { var v = tok.Substring(7).Trim(); action_v = ActionXml(v); break; } } + Calculation? uRL_v = null; + foreach (var tok in tokens) { if (tok.StartsWith("URL:", StringComparison.OrdinalIgnoreCase)) { uRL_v = new Calculation(tok.Substring(4).Trim()); break; } } + return new SetWebViewerStep(objectName_v, action_v, uRL_v, enabled); + } + + public static StepMetadata Metadata { get; } = new() + { + Name = XmlName, + Id = XmlId, + Category = "miscellaneous", + HelpUrl = "https://help.claris.com/en/pro-help/content/set-web-viewer.html", + Params = + [ + new ParamMetadata + { + Name = "Calculation", + XmlElement = "Calculation", + Type = "namedCalc", + HrLabel = "Object Name", + }, + new ParamMetadata + { + Name = "Action", + XmlElement = "Action", + Type = "enum", + XmlAttr = "value", + HrLabel = "Action", + ValidValues = ["Go to URL", "Reset", "Reload", "Go Forward", "Go Back"], + DefaultValue = "GoToURL", + }, + new ParamMetadata + { + Name = "Calculation", + XmlElement = "Calculation", + Type = "namedCalc", + HrLabel = "URL", + }, + ], + FromXml = FromXml, + FromDisplay = FromDisplayParams, + }; +} diff --git a/src/SharpFM.Model/Scripting/Steps/SetWindowTitleStep.cs b/src/SharpFM.Model/Scripting/Steps/SetWindowTitleStep.cs new file mode 100644 index 0000000..68e48e5 --- /dev/null +++ b/src/SharpFM.Model/Scripting/Steps/SetWindowTitleStep.cs @@ -0,0 +1,134 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Values; + +namespace SharpFM.Model.Scripting.Steps; + +public sealed class SetWindowTitleStep : ScriptStep, IStepFactory +{ + public const int XmlId = 124; + public const string XmlName = "Set Window Title"; + + public string Window { get; set; } + public Calculation OfWindow { get; set; } + public bool CurrentFile { get; set; } + public Calculation NewTitle { get; set; } + + public SetWindowTitleStep( + string window = "ByName", + Calculation? ofWindow = null, + bool currentFile = true, + Calculation? newTitle = null, + bool enabled = true) + : base(null, enabled) + { + Window = window; + OfWindow = ofWindow ?? new Calculation(""); + CurrentFile = currentFile; + NewTitle = newTitle ?? new Calculation(""); + } + + private static readonly IReadOnlyDictionary _WindowToHr = + new Dictionary(StringComparer.Ordinal) { + ["Current"] = "Current Window", + ["ByName"] = "Of Window", + }; + private static readonly IReadOnlyDictionary _WindowFromHr = + new Dictionary(StringComparer.OrdinalIgnoreCase) { + ["Current Window"] = "Current", + ["Of Window"] = "ByName", + }; + private static string WindowHr(string x) => _WindowToHr.TryGetValue(x, out var h) ? h : x; + private static string WindowXml(string h) => _WindowFromHr.TryGetValue(h, out var x) ? x : h; + + public override XElement ToXml() => + new("Step", + new XAttribute("enable", Enabled ? "True" : "False"), + new XAttribute("id", XmlId), + new XAttribute("name", XmlName), + new XElement("Window", new XAttribute("value", Window)), + new XElement("Name", OfWindow.ToXml("Calculation")), + new XElement("LimitToWindowsOfCurrentFile", new XAttribute("state", CurrentFile ? "True" : "False")), + new XElement("NewName", NewTitle.ToXml("Calculation"))); + + public override string ToDisplayLine() => + "Set Window Title [ " + "Window: " + WindowHr(Window) + " ; " + "Of Window: " + OfWindow.Text + " ; " + "Current file: " + (CurrentFile ? "On" : "Off") + " ; " + "New Title: " + NewTitle.Text + " ]"; + + public static new ScriptStep FromXml(XElement step) + { + var enabled = step.Attribute("enable")?.Value != "False"; + var window_v = step.Element("Window")?.Attribute("value")?.Value ?? "ByName"; + var ofWindow_vWrapEl = step.Element("Name"); + var ofWindow_vCalcEl = ofWindow_vWrapEl?.Element("Calculation"); + var ofWindow_v = ofWindow_vCalcEl is not null ? Calculation.FromXml(ofWindow_vCalcEl) : new Calculation(""); + var currentFile_v = step.Element("LimitToWindowsOfCurrentFile")?.Attribute("state")?.Value == "True"; + var newTitle_vWrapEl = step.Element("NewName"); + var newTitle_vCalcEl = newTitle_vWrapEl?.Element("Calculation"); + var newTitle_v = newTitle_vCalcEl is not null ? Calculation.FromXml(newTitle_vCalcEl) : new Calculation(""); + return new SetWindowTitleStep(window_v, ofWindow_v, currentFile_v, newTitle_v, enabled); + } + + public static ScriptStep FromDisplayParams(bool enabled, string[] hrParams) + { + var tokens = hrParams.Select(h => h.Trim()).ToArray(); + string window_v = "ByName"; + foreach (var tok in tokens) { if (tok.StartsWith("Window:", StringComparison.OrdinalIgnoreCase)) { var v = tok.Substring(7).Trim(); window_v = WindowXml(v); break; } } + Calculation? ofWindow_v = null; + foreach (var tok in tokens) { if (tok.StartsWith("Of Window:", StringComparison.OrdinalIgnoreCase)) { ofWindow_v = new Calculation(tok.Substring(10).Trim()); break; } } + bool currentFile_v = true; + foreach (var tok in tokens) { if (tok.StartsWith("Current file:", StringComparison.OrdinalIgnoreCase)) { var v = tok.Substring(13).Trim(); currentFile_v = v.Equals("On", StringComparison.OrdinalIgnoreCase); break; } } + Calculation? newTitle_v = null; + foreach (var tok in tokens) { if (tok.StartsWith("New Title:", StringComparison.OrdinalIgnoreCase)) { newTitle_v = new Calculation(tok.Substring(10).Trim()); break; } } + return new SetWindowTitleStep(window_v, ofWindow_v, currentFile_v, newTitle_v, enabled); + } + + public static StepMetadata Metadata { get; } = new() + { + Name = XmlName, + Id = XmlId, + Category = "windows", + HelpUrl = "https://help.claris.com/en/pro-help/content/set-window-title.html", + Params = + [ + new ParamMetadata + { + Name = "Window", + XmlElement = "Window", + Type = "enum", + XmlAttr = "value", + HrLabel = "Window", + ValidValues = ["Current Window", "Of Window"], + DefaultValue = "ByName", + }, + new ParamMetadata + { + Name = "Calculation", + XmlElement = "Calculation", + Type = "namedCalc", + HrLabel = "Of Window", + }, + new ParamMetadata + { + Name = "LimitToWindowsOfCurrentFile", + XmlElement = "LimitToWindowsOfCurrentFile", + Type = "flagBoolean", + XmlAttr = "state", + HrLabel = "Current file", + ValidValues = ["On", "Off"], + DefaultValue = "True", + }, + new ParamMetadata + { + Name = "Calculation", + XmlElement = "Calculation", + Type = "namedCalc", + HrLabel = "New Title", + }, + ], + FromXml = FromXml, + FromDisplay = FromDisplayParams, + }; +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/AVPlayerPlayStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/AVPlayerPlayStepTests.cs new file mode 100644 index 0000000..8f8436b --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/AVPlayerPlayStepTests.cs @@ -0,0 +1,27 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +public class AVPlayerPlayStepTests +{ + private const string CanonicalXml = """"""; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = AVPlayerPlayStep.Metadata.FromXml!(source); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("AVPlayer Play", out var metadata)); + Assert.Equal(177, metadata!.Id); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/AVPlayerSetOptionsStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/AVPlayerSetOptionsStepTests.cs new file mode 100644 index 0000000..86f87c8 --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/AVPlayerSetOptionsStepTests.cs @@ -0,0 +1,27 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +public class AVPlayerSetOptionsStepTests +{ + private const string CanonicalXml = """"""; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = AVPlayerSetOptionsStep.Metadata.FromXml!(source); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("AVPlayer Set Options", out var metadata)); + Assert.Equal(179, metadata!.Id); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/AddAccountStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/AddAccountStepTests.cs new file mode 100644 index 0000000..6caf515 --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/AddAccountStepTests.cs @@ -0,0 +1,41 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +public class AddAccountStepTests +{ + private const string CanonicalXml = """$example"""; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = AddAccountStep.Metadata.FromXml!(source); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Display_RoundTripsThroughFromDisplayParams() + { + var step1 = AddAccountStep.Metadata.FromXml!(XElement.Parse(CanonicalXml)); + var display = step1.ToDisplayLine(); + var open = display.IndexOf('['); + var close = display.LastIndexOf(']'); + var inner = display.Substring(open + 1, close - open - 1).Trim(); + var tokens = inner.Split(';', System.StringSplitOptions.TrimEntries); + + var step2 = AddAccountStep.Metadata.FromDisplay!(true, tokens); + Assert.True(XNode.DeepEquals(step1.ToXml(), step2.ToXml())); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Add Account", out var metadata)); + Assert.Equal(134, metadata!.Id); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/ChangePasswordStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/ChangePasswordStepTests.cs new file mode 100644 index 0000000..da1a63f --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/ChangePasswordStepTests.cs @@ -0,0 +1,41 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +public class ChangePasswordStepTests +{ + private const string CanonicalXml = """"""; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = ChangePasswordStep.Metadata.FromXml!(source); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Display_RoundTripsThroughFromDisplayParams() + { + var step1 = ChangePasswordStep.Metadata.FromXml!(XElement.Parse(CanonicalXml)); + var display = step1.ToDisplayLine(); + var open = display.IndexOf('['); + var close = display.LastIndexOf(']'); + var inner = display.Substring(open + 1, close - open - 1).Trim(); + var tokens = inner.Split(';', System.StringSplitOptions.TrimEntries); + + var step2 = ChangePasswordStep.Metadata.FromDisplay!(true, tokens); + Assert.True(XNode.DeepEquals(step1.ToXml(), step2.ToXml())); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Change Password", out var metadata)); + Assert.Equal(83, metadata!.Id); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/CloseDataFileStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/CloseDataFileStepTests.cs new file mode 100644 index 0000000..a93f70b --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/CloseDataFileStepTests.cs @@ -0,0 +1,41 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +public class CloseDataFileStepTests +{ + private const string CanonicalXml = """"""; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = CloseDataFileStep.Metadata.FromXml!(source); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Display_RoundTripsThroughFromDisplayParams() + { + var step1 = CloseDataFileStep.Metadata.FromXml!(XElement.Parse(CanonicalXml)); + var display = step1.ToDisplayLine(); + var open = display.IndexOf('['); + var close = display.LastIndexOf(']'); + var inner = display.Substring(open + 1, close - open - 1).Trim(); + var tokens = inner.Split(';', System.StringSplitOptions.TrimEntries); + + var step2 = CloseDataFileStep.Metadata.FromDisplay!(true, tokens); + Assert.True(XNode.DeepEquals(step1.ToXml(), step2.ToXml())); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Close Data File", out var metadata)); + Assert.Equal(196, metadata!.Id); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/CloseWindowStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/CloseWindowStepTests.cs new file mode 100644 index 0000000..e3a45d8 --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/CloseWindowStepTests.cs @@ -0,0 +1,27 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +public class CloseWindowStepTests +{ + private const string CanonicalXml = """"""; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = CloseWindowStep.Metadata.FromXml!(source); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Close Window", out var metadata)); + Assert.Equal(121, metadata!.Id); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/ConfigureAIAccountStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/ConfigureAIAccountStepTests.cs new file mode 100644 index 0000000..bafd405 --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/ConfigureAIAccountStepTests.cs @@ -0,0 +1,41 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +public class ConfigureAIAccountStepTests +{ + private const string CanonicalXml = """"""; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = ConfigureAIAccountStep.Metadata.FromXml!(source); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Display_RoundTripsThroughFromDisplayParams() + { + var step1 = ConfigureAIAccountStep.Metadata.FromXml!(XElement.Parse(CanonicalXml)); + var display = step1.ToDisplayLine(); + var open = display.IndexOf('['); + var close = display.LastIndexOf(']'); + var inner = display.Substring(open + 1, close - open - 1).Trim(); + var tokens = inner.Split(';', System.StringSplitOptions.TrimEntries); + + var step2 = ConfigureAIAccountStep.Metadata.FromDisplay!(true, tokens); + Assert.True(XNode.DeepEquals(step1.ToXml(), step2.ToXml())); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Configure AI Account", out var metadata)); + Assert.Equal(212, metadata!.Id); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/ConfigurePromptTemplateStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/ConfigurePromptTemplateStepTests.cs new file mode 100644 index 0000000..27a9c7d --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/ConfigurePromptTemplateStepTests.cs @@ -0,0 +1,27 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +public class ConfigurePromptTemplateStepTests +{ + private const string CanonicalXml = """"""; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = ConfigurePromptTemplateStep.Metadata.FromXml!(source); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Configure Prompt Template", out var metadata)); + Assert.Equal(226, metadata!.Id); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/ConfigureRAGAccountStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/ConfigureRAGAccountStepTests.cs new file mode 100644 index 0000000..4ad8b21 --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/ConfigureRAGAccountStepTests.cs @@ -0,0 +1,41 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +public class ConfigureRAGAccountStepTests +{ + private const string CanonicalXml = """"""; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = ConfigureRAGAccountStep.Metadata.FromXml!(source); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Display_RoundTripsThroughFromDisplayParams() + { + var step1 = ConfigureRAGAccountStep.Metadata.FromXml!(XElement.Parse(CanonicalXml)); + var display = step1.ToDisplayLine(); + var open = display.IndexOf('['); + var close = display.LastIndexOf(']'); + var inner = display.Substring(open + 1, close - open - 1).Trim(); + var tokens = inner.Split(';', System.StringSplitOptions.TrimEntries); + + var step2 = ConfigureRAGAccountStep.Metadata.FromDisplay!(true, tokens); + Assert.True(XNode.DeepEquals(step1.ToXml(), step2.ToXml())); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Configure RAG Account ", out var metadata)); + Assert.Equal(227, metadata!.Id); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/DeleteAccountStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/DeleteAccountStepTests.cs new file mode 100644 index 0000000..46c37c0 --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/DeleteAccountStepTests.cs @@ -0,0 +1,41 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +public class DeleteAccountStepTests +{ + private const string CanonicalXml = """"""; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = DeleteAccountStep.Metadata.FromXml!(source); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Display_RoundTripsThroughFromDisplayParams() + { + var step1 = DeleteAccountStep.Metadata.FromXml!(XElement.Parse(CanonicalXml)); + var display = step1.ToDisplayLine(); + var open = display.IndexOf('['); + var close = display.LastIndexOf(']'); + var inner = display.Substring(open + 1, close - open - 1).Trim(); + var tokens = inner.Split(';', System.StringSplitOptions.TrimEntries); + + var step2 = DeleteAccountStep.Metadata.FromDisplay!(true, tokens); + Assert.True(XNode.DeepEquals(step1.ToXml(), step2.ToXml())); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Delete Account", out var metadata)); + Assert.Equal(135, metadata!.Id); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/DialPhoneStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/DialPhoneStepTests.cs new file mode 100644 index 0000000..b2125b5 --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/DialPhoneStepTests.cs @@ -0,0 +1,27 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +public class DialPhoneStepTests +{ + private const string CanonicalXml = """"""; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = DialPhoneStep.Metadata.FromXml!(source); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Dial Phone", out var metadata)); + Assert.Equal(65, metadata!.Id); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/EnableAccountStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/EnableAccountStepTests.cs new file mode 100644 index 0000000..b689096 --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/EnableAccountStepTests.cs @@ -0,0 +1,41 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +public class EnableAccountStepTests +{ + private const string CanonicalXml = """"""; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = EnableAccountStep.Metadata.FromXml!(source); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Display_RoundTripsThroughFromDisplayParams() + { + var step1 = EnableAccountStep.Metadata.FromXml!(XElement.Parse(CanonicalXml)); + var display = step1.ToDisplayLine(); + var open = display.IndexOf('['); + var close = display.LastIndexOf(']'); + var inner = display.Substring(open + 1, close - open - 1).Trim(); + var tokens = inner.Split(';', System.StringSplitOptions.TrimEntries); + + var step2 = EnableAccountStep.Metadata.FromDisplay!(true, tokens); + Assert.True(XNode.DeepEquals(step1.ToXml(), step2.ToXml())); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Enable Account", out var metadata)); + Assert.Equal(137, metadata!.Id); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/ExitScriptStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/ExitScriptStepTests.cs new file mode 100644 index 0000000..6b080c9 --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/ExitScriptStepTests.cs @@ -0,0 +1,41 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +public class ExitScriptStepTests +{ + private const string CanonicalXml = """"""; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = ExitScriptStep.Metadata.FromXml!(source); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Display_RoundTripsThroughFromDisplayParams() + { + var step1 = ExitScriptStep.Metadata.FromXml!(XElement.Parse(CanonicalXml)); + var display = step1.ToDisplayLine(); + var open = display.IndexOf('['); + var close = display.LastIndexOf(']'); + var inner = display.Substring(open + 1, close - open - 1).Trim(); + var tokens = inner.Split(';', System.StringSplitOptions.TrimEntries); + + var step2 = ExitScriptStep.Metadata.FromDisplay!(true, tokens); + Assert.True(XNode.DeepEquals(step1.ToXml(), step2.ToXml())); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Exit Script", out var metadata)); + Assert.Equal(103, metadata!.Id); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/GetFolderPathStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/GetFolderPathStepTests.cs new file mode 100644 index 0000000..fe7e226 --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/GetFolderPathStepTests.cs @@ -0,0 +1,27 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +public class GetFolderPathStepTests +{ + private const string CanonicalXml = """$example"""; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = GetFolderPathStep.Metadata.FromXml!(source); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Get Folder Path", out var metadata)); + Assert.Equal(181, metadata!.Id); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/GoToObjectStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/GoToObjectStepTests.cs new file mode 100644 index 0000000..b3870da --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/GoToObjectStepTests.cs @@ -0,0 +1,41 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +public class GoToObjectStepTests +{ + private const string CanonicalXml = """"""; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = GoToObjectStep.Metadata.FromXml!(source); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Display_RoundTripsThroughFromDisplayParams() + { + var step1 = GoToObjectStep.Metadata.FromXml!(XElement.Parse(CanonicalXml)); + var display = step1.ToDisplayLine(); + var open = display.IndexOf('['); + var close = display.LastIndexOf(']'); + var inner = display.Substring(open + 1, close - open - 1).Trim(); + var tokens = inner.Split(';', System.StringSplitOptions.TrimEntries); + + var step2 = GoToObjectStep.Metadata.FromDisplay!(true, tokens); + Assert.True(XNode.DeepEquals(step1.ToXml(), step2.ToXml())); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Go to Object", out var metadata)); + Assert.Equal(145, metadata!.Id); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/MoveResizeWindowStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/MoveResizeWindowStepTests.cs new file mode 100644 index 0000000..0510e60 --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/MoveResizeWindowStepTests.cs @@ -0,0 +1,41 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +public class MoveResizeWindowStepTests +{ + private const string CanonicalXml = """"""; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = MoveResizeWindowStep.Metadata.FromXml!(source); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Display_RoundTripsThroughFromDisplayParams() + { + var step1 = MoveResizeWindowStep.Metadata.FromXml!(XElement.Parse(CanonicalXml)); + var display = step1.ToDisplayLine(); + var open = display.IndexOf('['); + var close = display.LastIndexOf(']'); + var inner = display.Substring(open + 1, close - open - 1).Trim(); + var tokens = inner.Split(';', System.StringSplitOptions.TrimEntries); + + var step2 = MoveResizeWindowStep.Metadata.FromDisplay!(true, tokens); + Assert.True(XNode.DeepEquals(step1.ToXml(), step2.ToXml())); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Move/Resize Window", out var metadata)); + Assert.Equal(119, metadata!.Id); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/NewWindowStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/NewWindowStepTests.cs new file mode 100644 index 0000000..4979cdc --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/NewWindowStepTests.cs @@ -0,0 +1,27 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +public class NewWindowStepTests +{ + private const string CanonicalXml = """$example"""; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = NewWindowStep.Metadata.FromXml!(source); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("New Window", out var metadata)); + Assert.Equal(122, metadata!.Id); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/OmitMultipleRecordsStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/OmitMultipleRecordsStepTests.cs new file mode 100644 index 0000000..f651b40 --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/OmitMultipleRecordsStepTests.cs @@ -0,0 +1,41 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +public class OmitMultipleRecordsStepTests +{ + private const string CanonicalXml = """"""; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = OmitMultipleRecordsStep.Metadata.FromXml!(source); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Display_RoundTripsThroughFromDisplayParams() + { + var step1 = OmitMultipleRecordsStep.Metadata.FromXml!(XElement.Parse(CanonicalXml)); + var display = step1.ToDisplayLine(); + var open = display.IndexOf('['); + var close = display.LastIndexOf(']'); + var inner = display.Substring(open + 1, close - open - 1).Trim(); + var tokens = inner.Split(';', System.StringSplitOptions.TrimEntries); + + var step2 = OmitMultipleRecordsStep.Metadata.FromDisplay!(true, tokens); + Assert.True(XNode.DeepEquals(step1.ToXml(), step2.ToXml())); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Omit Multiple Records", out var metadata)); + Assert.Equal(26, metadata!.Id); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/OpenURLStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/OpenURLStepTests.cs new file mode 100644 index 0000000..d3b842d --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/OpenURLStepTests.cs @@ -0,0 +1,41 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +public class OpenURLStepTests +{ + private const string CanonicalXml = """"""; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = OpenURLStep.Metadata.FromXml!(source); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Display_RoundTripsThroughFromDisplayParams() + { + var step1 = OpenURLStep.Metadata.FromXml!(XElement.Parse(CanonicalXml)); + var display = step1.ToDisplayLine(); + var open = display.IndexOf('['); + var close = display.LastIndexOf(']'); + var inner = display.Substring(open + 1, close - open - 1).Trim(); + var tokens = inner.Split(';', System.StringSplitOptions.TrimEntries); + + var step2 = OpenURLStep.Metadata.FromDisplay!(true, tokens); + Assert.True(XNode.DeepEquals(step1.ToXml(), step2.ToXml())); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Open URL", out var metadata)); + Assert.Equal(111, metadata!.Id); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/PauseResumeScriptStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/PauseResumeScriptStepTests.cs new file mode 100644 index 0000000..b5d15c7 --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/PauseResumeScriptStepTests.cs @@ -0,0 +1,27 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +public class PauseResumeScriptStepTests +{ + private const string CanonicalXml = """"""; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = PauseResumeScriptStep.Metadata.FromXml!(source); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Pause/Resume Script", out var metadata)); + Assert.Equal(62, metadata!.Id); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/PerformAppleScriptStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/PerformAppleScriptStepTests.cs new file mode 100644 index 0000000..13c0e15 --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/PerformAppleScriptStepTests.cs @@ -0,0 +1,27 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +public class PerformAppleScriptStepTests +{ + private const string CanonicalXml = """$example"""; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = PerformAppleScriptStep.Metadata.FromXml!(source); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Perform AppleScript", out var metadata)); + Assert.Equal(67, metadata!.Id); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/PerformQuickFindStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/PerformQuickFindStepTests.cs new file mode 100644 index 0000000..d9e50df --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/PerformQuickFindStepTests.cs @@ -0,0 +1,41 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +public class PerformQuickFindStepTests +{ + private const string CanonicalXml = """"""; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = PerformQuickFindStep.Metadata.FromXml!(source); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Display_RoundTripsThroughFromDisplayParams() + { + var step1 = PerformQuickFindStep.Metadata.FromXml!(XElement.Parse(CanonicalXml)); + var display = step1.ToDisplayLine(); + var open = display.IndexOf('['); + var close = display.LastIndexOf(']'); + var inner = display.Substring(open + 1, close - open - 1).Trim(); + var tokens = inner.Split(';', System.StringSplitOptions.TrimEntries); + + var step2 = PerformQuickFindStep.Metadata.FromDisplay!(true, tokens); + Assert.True(XNode.DeepEquals(step1.ToXml(), step2.ToXml())); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Perform Quick Find", out var metadata)); + Assert.Equal(150, metadata!.Id); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/ReLoginStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/ReLoginStepTests.cs new file mode 100644 index 0000000..628b429 --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/ReLoginStepTests.cs @@ -0,0 +1,41 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +public class ReLoginStepTests +{ + private const string CanonicalXml = """"""; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = ReLoginStep.Metadata.FromXml!(source); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Display_RoundTripsThroughFromDisplayParams() + { + var step1 = ReLoginStep.Metadata.FromXml!(XElement.Parse(CanonicalXml)); + var display = step1.ToDisplayLine(); + var open = display.IndexOf('['); + var close = display.LastIndexOf(']'); + var inner = display.Substring(open + 1, close - open - 1).Trim(); + var tokens = inner.Split(';', System.StringSplitOptions.TrimEntries); + + var step2 = ReLoginStep.Metadata.FromDisplay!(true, tokens); + Assert.True(XNode.DeepEquals(step1.ToXml(), step2.ToXml())); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Re-Login", out var metadata)); + Assert.Equal(138, metadata!.Id); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/RefreshObjectStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/RefreshObjectStepTests.cs new file mode 100644 index 0000000..c9f3adc --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/RefreshObjectStepTests.cs @@ -0,0 +1,41 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +public class RefreshObjectStepTests +{ + private const string CanonicalXml = """"""; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = RefreshObjectStep.Metadata.FromXml!(source); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Display_RoundTripsThroughFromDisplayParams() + { + var step1 = RefreshObjectStep.Metadata.FromXml!(XElement.Parse(CanonicalXml)); + var display = step1.ToDisplayLine(); + var open = display.IndexOf('['); + var close = display.LastIndexOf(']'); + var inner = display.Substring(open + 1, close - open - 1).Trim(); + var tokens = inner.Split(';', System.StringSplitOptions.TrimEntries); + + var step2 = RefreshObjectStep.Metadata.FromDisplay!(true, tokens); + Assert.True(XNode.DeepEquals(step1.ToXml(), step2.ToXml())); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Refresh Object", out var metadata)); + Assert.Equal(167, metadata!.Id); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/RefreshPortalStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/RefreshPortalStepTests.cs new file mode 100644 index 0000000..0daca2b --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/RefreshPortalStepTests.cs @@ -0,0 +1,41 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +public class RefreshPortalStepTests +{ + private const string CanonicalXml = """"""; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = RefreshPortalStep.Metadata.FromXml!(source); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Display_RoundTripsThroughFromDisplayParams() + { + var step1 = RefreshPortalStep.Metadata.FromXml!(XElement.Parse(CanonicalXml)); + var display = step1.ToDisplayLine(); + var open = display.IndexOf('['); + var close = display.LastIndexOf(']'); + var inner = display.Substring(open + 1, close - open - 1).Trim(); + var tokens = inner.Split(';', System.StringSplitOptions.TrimEntries); + + var step2 = RefreshPortalStep.Metadata.FromDisplay!(true, tokens); + Assert.True(XNode.DeepEquals(step1.ToXml(), step2.ToXml())); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Refresh Portal", out var metadata)); + Assert.Equal(180, metadata!.Id); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/RenameFileStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/RenameFileStepTests.cs new file mode 100644 index 0000000..fb477c1 --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/RenameFileStepTests.cs @@ -0,0 +1,41 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +public class RenameFileStepTests +{ + private const string CanonicalXml = """$example"""; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = RenameFileStep.Metadata.FromXml!(source); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Display_RoundTripsThroughFromDisplayParams() + { + var step1 = RenameFileStep.Metadata.FromXml!(XElement.Parse(CanonicalXml)); + var display = step1.ToDisplayLine(); + var open = display.IndexOf('['); + var close = display.LastIndexOf(']'); + var inner = display.Substring(open + 1, close - open - 1).Trim(); + var tokens = inner.Split(';', System.StringSplitOptions.TrimEntries); + + var step2 = RenameFileStep.Metadata.FromDisplay!(true, tokens); + Assert.True(XNode.DeepEquals(step1.ToXml(), step2.ToXml())); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Rename File", out var metadata)); + Assert.Equal(199, metadata!.Id); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/ResetAccountPasswordStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/ResetAccountPasswordStepTests.cs new file mode 100644 index 0000000..d6bd5c9 --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/ResetAccountPasswordStepTests.cs @@ -0,0 +1,41 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +public class ResetAccountPasswordStepTests +{ + private const string CanonicalXml = """"""; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = ResetAccountPasswordStep.Metadata.FromXml!(source); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Display_RoundTripsThroughFromDisplayParams() + { + var step1 = ResetAccountPasswordStep.Metadata.FromXml!(XElement.Parse(CanonicalXml)); + var display = step1.ToDisplayLine(); + var open = display.IndexOf('['); + var close = display.LastIndexOf(']'); + var inner = display.Substring(open + 1, close - open - 1).Trim(); + var tokens = inner.Split(';', System.StringSplitOptions.TrimEntries); + + var step2 = ResetAccountPasswordStep.Metadata.FromDisplay!(true, tokens); + Assert.True(XNode.DeepEquals(step1.ToXml(), step2.ToXml())); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Reset Account Password", out var metadata)); + Assert.Equal(136, metadata!.Id); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/RevertTransactionStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/RevertTransactionStepTests.cs new file mode 100644 index 0000000..180b31f --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/RevertTransactionStepTests.cs @@ -0,0 +1,27 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +public class RevertTransactionStepTests +{ + private const string CanonicalXml = """"""; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = RevertTransactionStep.Metadata.FromXml!(source); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Revert Transaction", out var metadata)); + Assert.Equal(207, metadata!.Id); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/SaveACopyAsAddOnPackageStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/SaveACopyAsAddOnPackageStepTests.cs new file mode 100644 index 0000000..e176356 --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/SaveACopyAsAddOnPackageStepTests.cs @@ -0,0 +1,41 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +public class SaveACopyAsAddOnPackageStepTests +{ + private const string CanonicalXml = """"""; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = SaveACopyAsAddOnPackageStep.Metadata.FromXml!(source); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Display_RoundTripsThroughFromDisplayParams() + { + var step1 = SaveACopyAsAddOnPackageStep.Metadata.FromXml!(XElement.Parse(CanonicalXml)); + var display = step1.ToDisplayLine(); + var open = display.IndexOf('['); + var close = display.LastIndexOf(']'); + var inner = display.Substring(open + 1, close - open - 1).Trim(); + var tokens = inner.Split(';', System.StringSplitOptions.TrimEntries); + + var step2 = SaveACopyAsAddOnPackageStep.Metadata.FromDisplay!(true, tokens); + Assert.True(XNode.DeepEquals(step1.ToXml(), step2.ToXml())); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Save a Copy as Add-on Package", out var metadata)); + Assert.Equal(96, metadata!.Id); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/SaveACopyAsXMLStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/SaveACopyAsXMLStepTests.cs new file mode 100644 index 0000000..cb12f48 --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/SaveACopyAsXMLStepTests.cs @@ -0,0 +1,41 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +public class SaveACopyAsXMLStepTests +{ + private const string CanonicalXml = """"""; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = SaveACopyAsXMLStep.Metadata.FromXml!(source); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Display_RoundTripsThroughFromDisplayParams() + { + var step1 = SaveACopyAsXMLStep.Metadata.FromXml!(XElement.Parse(CanonicalXml)); + var display = step1.ToDisplayLine(); + var open = display.IndexOf('['); + var close = display.LastIndexOf(']'); + var inner = display.Substring(open + 1, close - open - 1).Trim(); + var tokens = inner.Split(';', System.StringSplitOptions.TrimEntries); + + var step2 = SaveACopyAsXMLStep.Metadata.FromDisplay!(true, tokens); + Assert.True(XNode.DeepEquals(step1.ToXml(), step2.ToXml())); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Save a Copy as XML", out var metadata)); + Assert.Equal(3, metadata!.Id); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/SelectWindowStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/SelectWindowStepTests.cs new file mode 100644 index 0000000..59b3b0e --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/SelectWindowStepTests.cs @@ -0,0 +1,41 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +public class SelectWindowStepTests +{ + private const string CanonicalXml = """"""; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = SelectWindowStep.Metadata.FromXml!(source); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Display_RoundTripsThroughFromDisplayParams() + { + var step1 = SelectWindowStep.Metadata.FromXml!(XElement.Parse(CanonicalXml)); + var display = step1.ToDisplayLine(); + var open = display.IndexOf('['); + var close = display.LastIndexOf(']'); + var inner = display.Substring(open + 1, close - open - 1).Trim(); + var tokens = inner.Split(';', System.StringSplitOptions.TrimEntries); + + var step2 = SelectWindowStep.Metadata.FromDisplay!(true, tokens); + Assert.True(XNode.DeepEquals(step1.ToXml(), step2.ToXml())); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Select Window", out var metadata)); + Assert.Equal(123, metadata!.Id); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/SetDataFilePositionStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/SetDataFilePositionStepTests.cs new file mode 100644 index 0000000..d611d1c --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/SetDataFilePositionStepTests.cs @@ -0,0 +1,41 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +public class SetDataFilePositionStepTests +{ + private const string CanonicalXml = """"""; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = SetDataFilePositionStep.Metadata.FromXml!(source); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Display_RoundTripsThroughFromDisplayParams() + { + var step1 = SetDataFilePositionStep.Metadata.FromXml!(XElement.Parse(CanonicalXml)); + var display = step1.ToDisplayLine(); + var open = display.IndexOf('['); + var close = display.LastIndexOf(']'); + var inner = display.Substring(open + 1, close - open - 1).Trim(); + var tokens = inner.Split(';', System.StringSplitOptions.TrimEntries); + + var step2 = SetDataFilePositionStep.Metadata.FromDisplay!(true, tokens); + Assert.True(XNode.DeepEquals(step1.ToXml(), step2.ToXml())); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Set Data File Position", out var metadata)); + Assert.Equal(195, metadata!.Id); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/SetErrorLoggingStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/SetErrorLoggingStepTests.cs new file mode 100644 index 0000000..8318f60 --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/SetErrorLoggingStepTests.cs @@ -0,0 +1,41 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +public class SetErrorLoggingStepTests +{ + private const string CanonicalXml = """"""; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = SetErrorLoggingStep.Metadata.FromXml!(source); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Display_RoundTripsThroughFromDisplayParams() + { + var step1 = SetErrorLoggingStep.Metadata.FromXml!(XElement.Parse(CanonicalXml)); + var display = step1.ToDisplayLine(); + var open = display.IndexOf('['); + var close = display.LastIndexOf(']'); + var inner = display.Substring(open + 1, close - open - 1).Trim(); + var tokens = inner.Split(';', System.StringSplitOptions.TrimEntries); + + var step2 = SetErrorLoggingStep.Metadata.FromDisplay!(true, tokens); + Assert.True(XNode.DeepEquals(step1.ToXml(), step2.ToXml())); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Set Error Logging", out var metadata)); + Assert.Equal(200, metadata!.Id); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/SetFieldByNameStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/SetFieldByNameStepTests.cs new file mode 100644 index 0000000..1486fab --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/SetFieldByNameStepTests.cs @@ -0,0 +1,41 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +public class SetFieldByNameStepTests +{ + private const string CanonicalXml = """"""; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = SetFieldByNameStep.Metadata.FromXml!(source); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Display_RoundTripsThroughFromDisplayParams() + { + var step1 = SetFieldByNameStep.Metadata.FromXml!(XElement.Parse(CanonicalXml)); + var display = step1.ToDisplayLine(); + var open = display.IndexOf('['); + var close = display.LastIndexOf(']'); + var inner = display.Substring(open + 1, close - open - 1).Trim(); + var tokens = inner.Split(';', System.StringSplitOptions.TrimEntries); + + var step2 = SetFieldByNameStep.Metadata.FromDisplay!(true, tokens); + Assert.True(XNode.DeepEquals(step1.ToXml(), step2.ToXml())); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Set Field By Name", out var metadata)); + Assert.Equal(147, metadata!.Id); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/SetSessionIdentifierStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/SetSessionIdentifierStepTests.cs new file mode 100644 index 0000000..4660574 --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/SetSessionIdentifierStepTests.cs @@ -0,0 +1,41 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +public class SetSessionIdentifierStepTests +{ + private const string CanonicalXml = """"""; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = SetSessionIdentifierStep.Metadata.FromXml!(source); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Display_RoundTripsThroughFromDisplayParams() + { + var step1 = SetSessionIdentifierStep.Metadata.FromXml!(XElement.Parse(CanonicalXml)); + var display = step1.ToDisplayLine(); + var open = display.IndexOf('['); + var close = display.LastIndexOf(']'); + var inner = display.Substring(open + 1, close - open - 1).Trim(); + var tokens = inner.Split(';', System.StringSplitOptions.TrimEntries); + + var step2 = SetSessionIdentifierStep.Metadata.FromDisplay!(true, tokens); + Assert.True(XNode.DeepEquals(step1.ToXml(), step2.ToXml())); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Set Session Identifier", out var metadata)); + Assert.Equal(208, metadata!.Id); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/SetWebViewerStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/SetWebViewerStepTests.cs new file mode 100644 index 0000000..4a13272 --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/SetWebViewerStepTests.cs @@ -0,0 +1,41 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +public class SetWebViewerStepTests +{ + private const string CanonicalXml = """"""; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = SetWebViewerStep.Metadata.FromXml!(source); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Display_RoundTripsThroughFromDisplayParams() + { + var step1 = SetWebViewerStep.Metadata.FromXml!(XElement.Parse(CanonicalXml)); + var display = step1.ToDisplayLine(); + var open = display.IndexOf('['); + var close = display.LastIndexOf(']'); + var inner = display.Substring(open + 1, close - open - 1).Trim(); + var tokens = inner.Split(';', System.StringSplitOptions.TrimEntries); + + var step2 = SetWebViewerStep.Metadata.FromDisplay!(true, tokens); + Assert.True(XNode.DeepEquals(step1.ToXml(), step2.ToXml())); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Set Web Viewer", out var metadata)); + Assert.Equal(146, metadata!.Id); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/SetWindowTitleStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/SetWindowTitleStepTests.cs new file mode 100644 index 0000000..81c5ebd --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/SetWindowTitleStepTests.cs @@ -0,0 +1,41 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +public class SetWindowTitleStepTests +{ + private const string CanonicalXml = """"""; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = SetWindowTitleStep.Metadata.FromXml!(source); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Display_RoundTripsThroughFromDisplayParams() + { + var step1 = SetWindowTitleStep.Metadata.FromXml!(XElement.Parse(CanonicalXml)); + var display = step1.ToDisplayLine(); + var open = display.IndexOf('['); + var close = display.LastIndexOf(']'); + var inner = display.Substring(open + 1, close - open - 1).Trim(); + var tokens = inner.Split(';', System.StringSplitOptions.TrimEntries); + + var step2 = SetWindowTitleStep.Metadata.FromDisplay!(true, tokens); + Assert.True(XNode.DeepEquals(step1.ToXml(), step2.ToXml())); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Set Window Title", out var metadata)); + Assert.Equal(124, metadata!.Id); + } +} From 39e5be90cb0e672c98ab9739c601579e041aa2c7 Mon Sep 17 00:00:00 2001 From: Nate Bross Date: Mon, 20 Apr 2026 22:54:41 -0500 Subject: [PATCH 12/38] feat(scripting): migrate 13 field-bearing Tier D steps to IStepFactory POCOs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Field-bearing Bucket C subset — each step has exactly one Field (or fieldOrVariable) param, optionally accompanied by labeled booleans. Reuses FieldRef for the field reference with its (#id) suffix round-trip through display text. Steps: Check Selection, Clear, Copy, Cut, Go to Field, Insert Current Date, Insert Current Time, Insert Current User Name, Insert from Index, Insert from Last Visited, Install Plug-In File, Paste, Relookup Field Contents. --- .../Scripting/Steps/CheckSelectionStep.cs | 85 +++++++++++++ .../Scripting/Steps/ClearStep.cs | 85 +++++++++++++ src/SharpFM.Model/Scripting/Steps/CopyStep.cs | 85 +++++++++++++ src/SharpFM.Model/Scripting/Steps/CutStep.cs | 85 +++++++++++++ .../Scripting/Steps/GoToFieldStep.cs | 85 +++++++++++++ .../Scripting/Steps/InsertCurrentDateStep.cs | 86 +++++++++++++ .../Scripting/Steps/InsertCurrentTimeStep.cs | 86 +++++++++++++ .../Steps/InsertCurrentUserNameStep.cs | 86 +++++++++++++ .../Scripting/Steps/InsertFromIndexStep.cs | 85 +++++++++++++ .../Steps/InsertFromLastVisitedStep.cs | 85 +++++++++++++ .../Scripting/Steps/InstallPlugInFileStep.cs | 68 ++++++++++ .../Scripting/Steps/PasteStep.cs | 120 ++++++++++++++++++ .../Steps/RelookupFieldContentsStep.cs | 85 +++++++++++++ .../Steps/CheckSelectionStepTests.cs | 27 ++++ .../Scripting/Steps/ClearStepTests.cs | 27 ++++ .../Scripting/Steps/CopyStepTests.cs | 27 ++++ .../Scripting/Steps/CutStepTests.cs | 27 ++++ .../Scripting/Steps/GoToFieldStepTests.cs | 27 ++++ .../Steps/InsertCurrentDateStepTests.cs | 27 ++++ .../Steps/InsertCurrentTimeStepTests.cs | 27 ++++ .../Steps/InsertCurrentUserNameStepTests.cs | 27 ++++ .../Steps/InsertFromIndexStepTests.cs | 27 ++++ .../Steps/InsertFromLastVisitedStepTests.cs | 27 ++++ .../Steps/InstallPlugInFileStepTests.cs | 27 ++++ .../Scripting/Steps/PasteStepTests.cs | 27 ++++ .../Steps/RelookupFieldContentsStepTests.cs | 27 ++++ 26 files changed, 1477 insertions(+) create mode 100644 src/SharpFM.Model/Scripting/Steps/CheckSelectionStep.cs create mode 100644 src/SharpFM.Model/Scripting/Steps/ClearStep.cs create mode 100644 src/SharpFM.Model/Scripting/Steps/CopyStep.cs create mode 100644 src/SharpFM.Model/Scripting/Steps/CutStep.cs create mode 100644 src/SharpFM.Model/Scripting/Steps/GoToFieldStep.cs create mode 100644 src/SharpFM.Model/Scripting/Steps/InsertCurrentDateStep.cs create mode 100644 src/SharpFM.Model/Scripting/Steps/InsertCurrentTimeStep.cs create mode 100644 src/SharpFM.Model/Scripting/Steps/InsertCurrentUserNameStep.cs create mode 100644 src/SharpFM.Model/Scripting/Steps/InsertFromIndexStep.cs create mode 100644 src/SharpFM.Model/Scripting/Steps/InsertFromLastVisitedStep.cs create mode 100644 src/SharpFM.Model/Scripting/Steps/InstallPlugInFileStep.cs create mode 100644 src/SharpFM.Model/Scripting/Steps/PasteStep.cs create mode 100644 src/SharpFM.Model/Scripting/Steps/RelookupFieldContentsStep.cs create mode 100644 tests/SharpFM.Tests/Scripting/Steps/CheckSelectionStepTests.cs create mode 100644 tests/SharpFM.Tests/Scripting/Steps/ClearStepTests.cs create mode 100644 tests/SharpFM.Tests/Scripting/Steps/CopyStepTests.cs create mode 100644 tests/SharpFM.Tests/Scripting/Steps/CutStepTests.cs create mode 100644 tests/SharpFM.Tests/Scripting/Steps/GoToFieldStepTests.cs create mode 100644 tests/SharpFM.Tests/Scripting/Steps/InsertCurrentDateStepTests.cs create mode 100644 tests/SharpFM.Tests/Scripting/Steps/InsertCurrentTimeStepTests.cs create mode 100644 tests/SharpFM.Tests/Scripting/Steps/InsertCurrentUserNameStepTests.cs create mode 100644 tests/SharpFM.Tests/Scripting/Steps/InsertFromIndexStepTests.cs create mode 100644 tests/SharpFM.Tests/Scripting/Steps/InsertFromLastVisitedStepTests.cs create mode 100644 tests/SharpFM.Tests/Scripting/Steps/InstallPlugInFileStepTests.cs create mode 100644 tests/SharpFM.Tests/Scripting/Steps/PasteStepTests.cs create mode 100644 tests/SharpFM.Tests/Scripting/Steps/RelookupFieldContentsStepTests.cs diff --git a/src/SharpFM.Model/Scripting/Steps/CheckSelectionStep.cs b/src/SharpFM.Model/Scripting/Steps/CheckSelectionStep.cs new file mode 100644 index 0000000..8542a6a --- /dev/null +++ b/src/SharpFM.Model/Scripting/Steps/CheckSelectionStep.cs @@ -0,0 +1,85 @@ +using System; +using System.Linq; +using System.Xml.Linq; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Values; + +namespace SharpFM.Model.Scripting.Steps; + +public sealed class CheckSelectionStep : ScriptStep, IStepFactory +{ + public const int XmlId = 18; + public const string XmlName = "Check Selection"; + + public bool Select { get; set; } + public FieldRef Target { get; set; } + + public CheckSelectionStep( + bool select = true, + FieldRef? target = null, + bool enabled = true) + : base(null, enabled) + { + Select = select; + Target = target ?? FieldRef.ForField("", 0, ""); + } + + public override XElement ToXml() => + new("Step", + new XAttribute("enable", Enabled ? "True" : "False"), + new XAttribute("id", XmlId), + new XAttribute("name", XmlName), + new XElement("SelectAll", new XAttribute("state", Select ? "True" : "False")), + Target.ToXml("Field")); + + public override string ToDisplayLine() => + "Check Selection [ " + "Select: " + (Select ? "On" : "Off") + " ; " + Target.ToDisplayString() + " ]"; + + public static new ScriptStep FromXml(XElement step) + { + var enabled = step.Attribute("enable")?.Value != "False"; + var select_v = step.Element("SelectAll")?.Attribute("state")?.Value == "True"; + var fieldEl = step.Element("Field"); + var target = fieldEl is not null ? FieldRef.FromXml(fieldEl) : FieldRef.ForField("", 0, ""); + return new CheckSelectionStep(select_v, target, enabled); + } + + public static ScriptStep FromDisplayParams(bool enabled, string[] hrParams) + { + var tokens = hrParams.Select(h => h.Trim()).ToArray(); + bool select_v = true; + foreach (var tok in tokens) { if (tok.StartsWith("Select:", StringComparison.OrdinalIgnoreCase)) { var v = tok.Substring(7).Trim(); select_v = v.Equals("On", StringComparison.OrdinalIgnoreCase); break; } } + FieldRef target = FieldRef.ForField("", 0, ""); + foreach (var tok in tokens) { if (!tok.StartsWith("Select:", StringComparison.OrdinalIgnoreCase) && !string.IsNullOrWhiteSpace(tok)) { target = FieldRef.FromDisplayToken(tok); break; } } + return new CheckSelectionStep(select_v, target, enabled); + } + + public static StepMetadata Metadata { get; } = new() + { + Name = XmlName, + Id = XmlId, + Category = "spelling", + HelpUrl = "https://help.claris.com/en/pro-help/content/check-selection.html", + Params = + [ + new ParamMetadata + { + Name = "SelectAll", + XmlElement = "SelectAll", + Type = "boolean", + XmlAttr = "state", + HrLabel = "Select", + ValidValues = ["On", "Off"], + DefaultValue = "True", + }, + new ParamMetadata + { + Name = "Field", + XmlElement = "Field", + Type = "field", + }, + ], + FromXml = FromXml, + FromDisplay = FromDisplayParams, + }; +} diff --git a/src/SharpFM.Model/Scripting/Steps/ClearStep.cs b/src/SharpFM.Model/Scripting/Steps/ClearStep.cs new file mode 100644 index 0000000..bf5b3c4 --- /dev/null +++ b/src/SharpFM.Model/Scripting/Steps/ClearStep.cs @@ -0,0 +1,85 @@ +using System; +using System.Linq; +using System.Xml.Linq; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Values; + +namespace SharpFM.Model.Scripting.Steps; + +public sealed class ClearStep : ScriptStep, IStepFactory +{ + public const int XmlId = 49; + public const string XmlName = "Clear"; + + public bool Select { get; set; } + public FieldRef Target { get; set; } + + public ClearStep( + bool select = true, + FieldRef? target = null, + bool enabled = true) + : base(null, enabled) + { + Select = select; + Target = target ?? FieldRef.ForField("", 0, ""); + } + + public override XElement ToXml() => + new("Step", + new XAttribute("enable", Enabled ? "True" : "False"), + new XAttribute("id", XmlId), + new XAttribute("name", XmlName), + new XElement("SelectAll", new XAttribute("state", Select ? "True" : "False")), + Target.ToXml("Field")); + + public override string ToDisplayLine() => + "Clear [ " + "Select: " + (Select ? "On" : "Off") + " ; " + Target.ToDisplayString() + " ]"; + + public static new ScriptStep FromXml(XElement step) + { + var enabled = step.Attribute("enable")?.Value != "False"; + var select_v = step.Element("SelectAll")?.Attribute("state")?.Value == "True"; + var fieldEl = step.Element("Field"); + var target = fieldEl is not null ? FieldRef.FromXml(fieldEl) : FieldRef.ForField("", 0, ""); + return new ClearStep(select_v, target, enabled); + } + + public static ScriptStep FromDisplayParams(bool enabled, string[] hrParams) + { + var tokens = hrParams.Select(h => h.Trim()).ToArray(); + bool select_v = true; + foreach (var tok in tokens) { if (tok.StartsWith("Select:", StringComparison.OrdinalIgnoreCase)) { var v = tok.Substring(7).Trim(); select_v = v.Equals("On", StringComparison.OrdinalIgnoreCase); break; } } + FieldRef target = FieldRef.ForField("", 0, ""); + foreach (var tok in tokens) { if (!tok.StartsWith("Select:", StringComparison.OrdinalIgnoreCase) && !string.IsNullOrWhiteSpace(tok)) { target = FieldRef.FromDisplayToken(tok); break; } } + return new ClearStep(select_v, target, enabled); + } + + public static StepMetadata Metadata { get; } = new() + { + Name = XmlName, + Id = XmlId, + Category = "editing", + HelpUrl = "https://help.claris.com/en/pro-help/content/clear.html", + Params = + [ + new ParamMetadata + { + Name = "SelectAll", + XmlElement = "SelectAll", + Type = "boolean", + XmlAttr = "state", + HrLabel = "Select", + ValidValues = ["On", "Off"], + DefaultValue = "True", + }, + new ParamMetadata + { + Name = "Field", + XmlElement = "Field", + Type = "field", + }, + ], + FromXml = FromXml, + FromDisplay = FromDisplayParams, + }; +} diff --git a/src/SharpFM.Model/Scripting/Steps/CopyStep.cs b/src/SharpFM.Model/Scripting/Steps/CopyStep.cs new file mode 100644 index 0000000..d961fd7 --- /dev/null +++ b/src/SharpFM.Model/Scripting/Steps/CopyStep.cs @@ -0,0 +1,85 @@ +using System; +using System.Linq; +using System.Xml.Linq; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Values; + +namespace SharpFM.Model.Scripting.Steps; + +public sealed class CopyStep : ScriptStep, IStepFactory +{ + public const int XmlId = 47; + public const string XmlName = "Copy"; + + public bool Select { get; set; } + public FieldRef Target { get; set; } + + public CopyStep( + bool select = true, + FieldRef? target = null, + bool enabled = true) + : base(null, enabled) + { + Select = select; + Target = target ?? FieldRef.ForField("", 0, ""); + } + + public override XElement ToXml() => + new("Step", + new XAttribute("enable", Enabled ? "True" : "False"), + new XAttribute("id", XmlId), + new XAttribute("name", XmlName), + new XElement("SelectAll", new XAttribute("state", Select ? "True" : "False")), + Target.ToXml("Field")); + + public override string ToDisplayLine() => + "Copy [ " + "Select: " + (Select ? "On" : "Off") + " ; " + Target.ToDisplayString() + " ]"; + + public static new ScriptStep FromXml(XElement step) + { + var enabled = step.Attribute("enable")?.Value != "False"; + var select_v = step.Element("SelectAll")?.Attribute("state")?.Value == "True"; + var fieldEl = step.Element("Field"); + var target = fieldEl is not null ? FieldRef.FromXml(fieldEl) : FieldRef.ForField("", 0, ""); + return new CopyStep(select_v, target, enabled); + } + + public static ScriptStep FromDisplayParams(bool enabled, string[] hrParams) + { + var tokens = hrParams.Select(h => h.Trim()).ToArray(); + bool select_v = true; + foreach (var tok in tokens) { if (tok.StartsWith("Select:", StringComparison.OrdinalIgnoreCase)) { var v = tok.Substring(7).Trim(); select_v = v.Equals("On", StringComparison.OrdinalIgnoreCase); break; } } + FieldRef target = FieldRef.ForField("", 0, ""); + foreach (var tok in tokens) { if (!tok.StartsWith("Select:", StringComparison.OrdinalIgnoreCase) && !string.IsNullOrWhiteSpace(tok)) { target = FieldRef.FromDisplayToken(tok); break; } } + return new CopyStep(select_v, target, enabled); + } + + public static StepMetadata Metadata { get; } = new() + { + Name = XmlName, + Id = XmlId, + Category = "editing", + HelpUrl = "https://help.claris.com/en/pro-help/content/copy.html", + Params = + [ + new ParamMetadata + { + Name = "SelectAll", + XmlElement = "SelectAll", + Type = "boolean", + XmlAttr = "state", + HrLabel = "Select", + ValidValues = ["On", "Off"], + DefaultValue = "True", + }, + new ParamMetadata + { + Name = "Field", + XmlElement = "Field", + Type = "field", + }, + ], + FromXml = FromXml, + FromDisplay = FromDisplayParams, + }; +} diff --git a/src/SharpFM.Model/Scripting/Steps/CutStep.cs b/src/SharpFM.Model/Scripting/Steps/CutStep.cs new file mode 100644 index 0000000..fec8d98 --- /dev/null +++ b/src/SharpFM.Model/Scripting/Steps/CutStep.cs @@ -0,0 +1,85 @@ +using System; +using System.Linq; +using System.Xml.Linq; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Values; + +namespace SharpFM.Model.Scripting.Steps; + +public sealed class CutStep : ScriptStep, IStepFactory +{ + public const int XmlId = 46; + public const string XmlName = "Cut"; + + public bool Select { get; set; } + public FieldRef Target { get; set; } + + public CutStep( + bool select = true, + FieldRef? target = null, + bool enabled = true) + : base(null, enabled) + { + Select = select; + Target = target ?? FieldRef.ForField("", 0, ""); + } + + public override XElement ToXml() => + new("Step", + new XAttribute("enable", Enabled ? "True" : "False"), + new XAttribute("id", XmlId), + new XAttribute("name", XmlName), + new XElement("SelectAll", new XAttribute("state", Select ? "True" : "False")), + Target.ToXml("Field")); + + public override string ToDisplayLine() => + "Cut [ " + "Select: " + (Select ? "On" : "Off") + " ; " + Target.ToDisplayString() + " ]"; + + public static new ScriptStep FromXml(XElement step) + { + var enabled = step.Attribute("enable")?.Value != "False"; + var select_v = step.Element("SelectAll")?.Attribute("state")?.Value == "True"; + var fieldEl = step.Element("Field"); + var target = fieldEl is not null ? FieldRef.FromXml(fieldEl) : FieldRef.ForField("", 0, ""); + return new CutStep(select_v, target, enabled); + } + + public static ScriptStep FromDisplayParams(bool enabled, string[] hrParams) + { + var tokens = hrParams.Select(h => h.Trim()).ToArray(); + bool select_v = true; + foreach (var tok in tokens) { if (tok.StartsWith("Select:", StringComparison.OrdinalIgnoreCase)) { var v = tok.Substring(7).Trim(); select_v = v.Equals("On", StringComparison.OrdinalIgnoreCase); break; } } + FieldRef target = FieldRef.ForField("", 0, ""); + foreach (var tok in tokens) { if (!tok.StartsWith("Select:", StringComparison.OrdinalIgnoreCase) && !string.IsNullOrWhiteSpace(tok)) { target = FieldRef.FromDisplayToken(tok); break; } } + return new CutStep(select_v, target, enabled); + } + + public static StepMetadata Metadata { get; } = new() + { + Name = XmlName, + Id = XmlId, + Category = "editing", + HelpUrl = "https://help.claris.com/en/pro-help/content/cut.html", + Params = + [ + new ParamMetadata + { + Name = "SelectAll", + XmlElement = "SelectAll", + Type = "boolean", + XmlAttr = "state", + HrLabel = "Select", + ValidValues = ["On", "Off"], + DefaultValue = "True", + }, + new ParamMetadata + { + Name = "Field", + XmlElement = "Field", + Type = "field", + }, + ], + FromXml = FromXml, + FromDisplay = FromDisplayParams, + }; +} diff --git a/src/SharpFM.Model/Scripting/Steps/GoToFieldStep.cs b/src/SharpFM.Model/Scripting/Steps/GoToFieldStep.cs new file mode 100644 index 0000000..d382e3c --- /dev/null +++ b/src/SharpFM.Model/Scripting/Steps/GoToFieldStep.cs @@ -0,0 +1,85 @@ +using System; +using System.Linq; +using System.Xml.Linq; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Values; + +namespace SharpFM.Model.Scripting.Steps; + +public sealed class GoToFieldStep : ScriptStep, IStepFactory +{ + public const int XmlId = 17; + public const string XmlName = "Go to Field"; + + public bool SelectPerform { get; set; } + public FieldRef Target { get; set; } + + public GoToFieldStep( + bool selectPerform = true, + FieldRef? target = null, + bool enabled = true) + : base(null, enabled) + { + SelectPerform = selectPerform; + Target = target ?? FieldRef.ForField("", 0, ""); + } + + public override XElement ToXml() => + new("Step", + new XAttribute("enable", Enabled ? "True" : "False"), + new XAttribute("id", XmlId), + new XAttribute("name", XmlName), + new XElement("SelectAll", new XAttribute("state", SelectPerform ? "True" : "False")), + Target.ToXml("Field")); + + public override string ToDisplayLine() => + "Go to Field [ " + "Select/perform: " + (SelectPerform ? "On" : "Off") + " ; " + Target.ToDisplayString() + " ]"; + + public static new ScriptStep FromXml(XElement step) + { + var enabled = step.Attribute("enable")?.Value != "False"; + var selectPerform_v = step.Element("SelectAll")?.Attribute("state")?.Value == "True"; + var fieldEl = step.Element("Field"); + var target = fieldEl is not null ? FieldRef.FromXml(fieldEl) : FieldRef.ForField("", 0, ""); + return new GoToFieldStep(selectPerform_v, target, enabled); + } + + public static ScriptStep FromDisplayParams(bool enabled, string[] hrParams) + { + var tokens = hrParams.Select(h => h.Trim()).ToArray(); + bool selectPerform_v = true; + foreach (var tok in tokens) { if (tok.StartsWith("Select/perform:", StringComparison.OrdinalIgnoreCase)) { var v = tok.Substring(15).Trim(); selectPerform_v = v.Equals("On", StringComparison.OrdinalIgnoreCase); break; } } + FieldRef target = FieldRef.ForField("", 0, ""); + foreach (var tok in tokens) { if (!tok.StartsWith("Select/perform:", StringComparison.OrdinalIgnoreCase) && !string.IsNullOrWhiteSpace(tok)) { target = FieldRef.FromDisplayToken(tok); break; } } + return new GoToFieldStep(selectPerform_v, target, enabled); + } + + public static StepMetadata Metadata { get; } = new() + { + Name = XmlName, + Id = XmlId, + Category = "navigation", + HelpUrl = "https://help.claris.com/en/pro-help/content/go-to-field.html", + Params = + [ + new ParamMetadata + { + Name = "SelectAll", + XmlElement = "SelectAll", + Type = "boolean", + XmlAttr = "state", + HrLabel = "Select/perform", + ValidValues = ["On", "Off"], + DefaultValue = "True", + }, + new ParamMetadata + { + Name = "Field", + XmlElement = "Field", + Type = "field", + }, + ], + FromXml = FromXml, + FromDisplay = FromDisplayParams, + }; +} diff --git a/src/SharpFM.Model/Scripting/Steps/InsertCurrentDateStep.cs b/src/SharpFM.Model/Scripting/Steps/InsertCurrentDateStep.cs new file mode 100644 index 0000000..555db3f --- /dev/null +++ b/src/SharpFM.Model/Scripting/Steps/InsertCurrentDateStep.cs @@ -0,0 +1,86 @@ +using System; +using System.Linq; +using System.Xml.Linq; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Values; + +namespace SharpFM.Model.Scripting.Steps; + +public sealed class InsertCurrentDateStep : ScriptStep, IStepFactory +{ + public const int XmlId = 13; + public const string XmlName = "Insert Current Date"; + + public bool Select { get; set; } + public FieldRef Target { get; set; } + + public InsertCurrentDateStep( + bool select = true, + FieldRef? target = null, + bool enabled = true) + : base(null, enabled) + { + Select = select; + Target = target ?? FieldRef.ForField("", 0, ""); + } + + public override XElement ToXml() => + new("Step", + new XAttribute("enable", Enabled ? "True" : "False"), + new XAttribute("id", XmlId), + new XAttribute("name", XmlName), + new XElement("SelectAll", new XAttribute("state", Select ? "True" : "False")), + Target.ToXml("Field")); + + public override string ToDisplayLine() => + "Insert Current Date [ " + "Select: " + (Select ? "On" : "Off") + " ; " + "Target: " + Target.ToDisplayString() + " ]"; + + public static new ScriptStep FromXml(XElement step) + { + var enabled = step.Attribute("enable")?.Value != "False"; + var select_v = step.Element("SelectAll")?.Attribute("state")?.Value == "True"; + var fieldEl = step.Element("Field"); + var target = fieldEl is not null ? FieldRef.FromXml(fieldEl) : FieldRef.ForField("", 0, ""); + return new InsertCurrentDateStep(select_v, target, enabled); + } + + public static ScriptStep FromDisplayParams(bool enabled, string[] hrParams) + { + var tokens = hrParams.Select(h => h.Trim()).ToArray(); + bool select_v = true; + foreach (var tok in tokens) { if (tok.StartsWith("Select:", StringComparison.OrdinalIgnoreCase)) { var v = tok.Substring(7).Trim(); select_v = v.Equals("On", StringComparison.OrdinalIgnoreCase); break; } } + FieldRef target = FieldRef.ForField("", 0, ""); + foreach (var tok in tokens) { if (tok.StartsWith("Target:", StringComparison.OrdinalIgnoreCase)) { var v = tok.Substring(7).Trim(); target = FieldRef.FromDisplayToken(v); break; } } + return new InsertCurrentDateStep(select_v, target, enabled); + } + + public static StepMetadata Metadata { get; } = new() + { + Name = XmlName, + Id = XmlId, + Category = "fields", + HelpUrl = "https://help.claris.com/en/pro-help/content/insert-current-date.html", + Params = + [ + new ParamMetadata + { + Name = "SelectAll", + XmlElement = "SelectAll", + Type = "boolean", + XmlAttr = "state", + HrLabel = "Select", + ValidValues = ["On", "Off"], + DefaultValue = "True", + }, + new ParamMetadata + { + Name = "Field", + XmlElement = "Field", + Type = "fieldOrVariable", + HrLabel = "Target", + }, + ], + FromXml = FromXml, + FromDisplay = FromDisplayParams, + }; +} diff --git a/src/SharpFM.Model/Scripting/Steps/InsertCurrentTimeStep.cs b/src/SharpFM.Model/Scripting/Steps/InsertCurrentTimeStep.cs new file mode 100644 index 0000000..fca41d1 --- /dev/null +++ b/src/SharpFM.Model/Scripting/Steps/InsertCurrentTimeStep.cs @@ -0,0 +1,86 @@ +using System; +using System.Linq; +using System.Xml.Linq; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Values; + +namespace SharpFM.Model.Scripting.Steps; + +public sealed class InsertCurrentTimeStep : ScriptStep, IStepFactory +{ + public const int XmlId = 14; + public const string XmlName = "Insert Current Time"; + + public bool Select { get; set; } + public FieldRef Target { get; set; } + + public InsertCurrentTimeStep( + bool select = true, + FieldRef? target = null, + bool enabled = true) + : base(null, enabled) + { + Select = select; + Target = target ?? FieldRef.ForField("", 0, ""); + } + + public override XElement ToXml() => + new("Step", + new XAttribute("enable", Enabled ? "True" : "False"), + new XAttribute("id", XmlId), + new XAttribute("name", XmlName), + new XElement("SelectAll", new XAttribute("state", Select ? "True" : "False")), + Target.ToXml("Field")); + + public override string ToDisplayLine() => + "Insert Current Time [ " + "Select: " + (Select ? "On" : "Off") + " ; " + "Target: " + Target.ToDisplayString() + " ]"; + + public static new ScriptStep FromXml(XElement step) + { + var enabled = step.Attribute("enable")?.Value != "False"; + var select_v = step.Element("SelectAll")?.Attribute("state")?.Value == "True"; + var fieldEl = step.Element("Field"); + var target = fieldEl is not null ? FieldRef.FromXml(fieldEl) : FieldRef.ForField("", 0, ""); + return new InsertCurrentTimeStep(select_v, target, enabled); + } + + public static ScriptStep FromDisplayParams(bool enabled, string[] hrParams) + { + var tokens = hrParams.Select(h => h.Trim()).ToArray(); + bool select_v = true; + foreach (var tok in tokens) { if (tok.StartsWith("Select:", StringComparison.OrdinalIgnoreCase)) { var v = tok.Substring(7).Trim(); select_v = v.Equals("On", StringComparison.OrdinalIgnoreCase); break; } } + FieldRef target = FieldRef.ForField("", 0, ""); + foreach (var tok in tokens) { if (tok.StartsWith("Target:", StringComparison.OrdinalIgnoreCase)) { var v = tok.Substring(7).Trim(); target = FieldRef.FromDisplayToken(v); break; } } + return new InsertCurrentTimeStep(select_v, target, enabled); + } + + public static StepMetadata Metadata { get; } = new() + { + Name = XmlName, + Id = XmlId, + Category = "fields", + HelpUrl = "https://help.claris.com/en/pro-help/content/insert-current-time.html", + Params = + [ + new ParamMetadata + { + Name = "SelectAll", + XmlElement = "SelectAll", + Type = "boolean", + XmlAttr = "state", + HrLabel = "Select", + ValidValues = ["On", "Off"], + DefaultValue = "True", + }, + new ParamMetadata + { + Name = "Field", + XmlElement = "Field", + Type = "fieldOrVariable", + HrLabel = "Target", + }, + ], + FromXml = FromXml, + FromDisplay = FromDisplayParams, + }; +} diff --git a/src/SharpFM.Model/Scripting/Steps/InsertCurrentUserNameStep.cs b/src/SharpFM.Model/Scripting/Steps/InsertCurrentUserNameStep.cs new file mode 100644 index 0000000..e052584 --- /dev/null +++ b/src/SharpFM.Model/Scripting/Steps/InsertCurrentUserNameStep.cs @@ -0,0 +1,86 @@ +using System; +using System.Linq; +using System.Xml.Linq; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Values; + +namespace SharpFM.Model.Scripting.Steps; + +public sealed class InsertCurrentUserNameStep : ScriptStep, IStepFactory +{ + public const int XmlId = 60; + public const string XmlName = "Insert Current User Name"; + + public bool Select { get; set; } + public FieldRef Target { get; set; } + + public InsertCurrentUserNameStep( + bool select = true, + FieldRef? target = null, + bool enabled = true) + : base(null, enabled) + { + Select = select; + Target = target ?? FieldRef.ForField("", 0, ""); + } + + public override XElement ToXml() => + new("Step", + new XAttribute("enable", Enabled ? "True" : "False"), + new XAttribute("id", XmlId), + new XAttribute("name", XmlName), + new XElement("SelectAll", new XAttribute("state", Select ? "True" : "False")), + Target.ToXml("Field")); + + public override string ToDisplayLine() => + "Insert Current User Name [ " + "Select: " + (Select ? "On" : "Off") + " ; " + "Target: " + Target.ToDisplayString() + " ]"; + + public static new ScriptStep FromXml(XElement step) + { + var enabled = step.Attribute("enable")?.Value != "False"; + var select_v = step.Element("SelectAll")?.Attribute("state")?.Value == "True"; + var fieldEl = step.Element("Field"); + var target = fieldEl is not null ? FieldRef.FromXml(fieldEl) : FieldRef.ForField("", 0, ""); + return new InsertCurrentUserNameStep(select_v, target, enabled); + } + + public static ScriptStep FromDisplayParams(bool enabled, string[] hrParams) + { + var tokens = hrParams.Select(h => h.Trim()).ToArray(); + bool select_v = true; + foreach (var tok in tokens) { if (tok.StartsWith("Select:", StringComparison.OrdinalIgnoreCase)) { var v = tok.Substring(7).Trim(); select_v = v.Equals("On", StringComparison.OrdinalIgnoreCase); break; } } + FieldRef target = FieldRef.ForField("", 0, ""); + foreach (var tok in tokens) { if (tok.StartsWith("Target:", StringComparison.OrdinalIgnoreCase)) { var v = tok.Substring(7).Trim(); target = FieldRef.FromDisplayToken(v); break; } } + return new InsertCurrentUserNameStep(select_v, target, enabled); + } + + public static StepMetadata Metadata { get; } = new() + { + Name = XmlName, + Id = XmlId, + Category = "fields", + HelpUrl = "https://help.claris.com/en/pro-help/content/insert-current-user-name.html", + Params = + [ + new ParamMetadata + { + Name = "SelectAll", + XmlElement = "SelectAll", + Type = "boolean", + XmlAttr = "state", + HrLabel = "Select", + ValidValues = ["On", "Off"], + DefaultValue = "True", + }, + new ParamMetadata + { + Name = "Field", + XmlElement = "Field", + Type = "fieldOrVariable", + HrLabel = "Target", + }, + ], + FromXml = FromXml, + FromDisplay = FromDisplayParams, + }; +} diff --git a/src/SharpFM.Model/Scripting/Steps/InsertFromIndexStep.cs b/src/SharpFM.Model/Scripting/Steps/InsertFromIndexStep.cs new file mode 100644 index 0000000..8161a01 --- /dev/null +++ b/src/SharpFM.Model/Scripting/Steps/InsertFromIndexStep.cs @@ -0,0 +1,85 @@ +using System; +using System.Linq; +using System.Xml.Linq; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Values; + +namespace SharpFM.Model.Scripting.Steps; + +public sealed class InsertFromIndexStep : ScriptStep, IStepFactory +{ + public const int XmlId = 11; + public const string XmlName = "Insert from Index"; + + public bool Select { get; set; } + public FieldRef Target { get; set; } + + public InsertFromIndexStep( + bool select = true, + FieldRef? target = null, + bool enabled = true) + : base(null, enabled) + { + Select = select; + Target = target ?? FieldRef.ForField("", 0, ""); + } + + public override XElement ToXml() => + new("Step", + new XAttribute("enable", Enabled ? "True" : "False"), + new XAttribute("id", XmlId), + new XAttribute("name", XmlName), + new XElement("SelectAll", new XAttribute("state", Select ? "True" : "False")), + Target.ToXml("Field")); + + public override string ToDisplayLine() => + "Insert from Index [ " + "Select: " + (Select ? "On" : "Off") + " ; " + Target.ToDisplayString() + " ]"; + + public static new ScriptStep FromXml(XElement step) + { + var enabled = step.Attribute("enable")?.Value != "False"; + var select_v = step.Element("SelectAll")?.Attribute("state")?.Value == "True"; + var fieldEl = step.Element("Field"); + var target = fieldEl is not null ? FieldRef.FromXml(fieldEl) : FieldRef.ForField("", 0, ""); + return new InsertFromIndexStep(select_v, target, enabled); + } + + public static ScriptStep FromDisplayParams(bool enabled, string[] hrParams) + { + var tokens = hrParams.Select(h => h.Trim()).ToArray(); + bool select_v = true; + foreach (var tok in tokens) { if (tok.StartsWith("Select:", StringComparison.OrdinalIgnoreCase)) { var v = tok.Substring(7).Trim(); select_v = v.Equals("On", StringComparison.OrdinalIgnoreCase); break; } } + FieldRef target = FieldRef.ForField("", 0, ""); + foreach (var tok in tokens) { if (!tok.StartsWith("Select:", StringComparison.OrdinalIgnoreCase) && !string.IsNullOrWhiteSpace(tok)) { target = FieldRef.FromDisplayToken(tok); break; } } + return new InsertFromIndexStep(select_v, target, enabled); + } + + public static StepMetadata Metadata { get; } = new() + { + Name = XmlName, + Id = XmlId, + Category = "fields", + HelpUrl = "https://help.claris.com/en/pro-help/content/insert-from-index.html", + Params = + [ + new ParamMetadata + { + Name = "SelectAll", + XmlElement = "SelectAll", + Type = "boolean", + XmlAttr = "state", + HrLabel = "Select", + ValidValues = ["On", "Off"], + DefaultValue = "True", + }, + new ParamMetadata + { + Name = "Field", + XmlElement = "Field", + Type = "field", + }, + ], + FromXml = FromXml, + FromDisplay = FromDisplayParams, + }; +} diff --git a/src/SharpFM.Model/Scripting/Steps/InsertFromLastVisitedStep.cs b/src/SharpFM.Model/Scripting/Steps/InsertFromLastVisitedStep.cs new file mode 100644 index 0000000..93cdc74 --- /dev/null +++ b/src/SharpFM.Model/Scripting/Steps/InsertFromLastVisitedStep.cs @@ -0,0 +1,85 @@ +using System; +using System.Linq; +using System.Xml.Linq; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Values; + +namespace SharpFM.Model.Scripting.Steps; + +public sealed class InsertFromLastVisitedStep : ScriptStep, IStepFactory +{ + public const int XmlId = 12; + public const string XmlName = "Insert from Last Visited"; + + public bool Select { get; set; } + public FieldRef Target { get; set; } + + public InsertFromLastVisitedStep( + bool select = true, + FieldRef? target = null, + bool enabled = true) + : base(null, enabled) + { + Select = select; + Target = target ?? FieldRef.ForField("", 0, ""); + } + + public override XElement ToXml() => + new("Step", + new XAttribute("enable", Enabled ? "True" : "False"), + new XAttribute("id", XmlId), + new XAttribute("name", XmlName), + new XElement("SelectAll", new XAttribute("state", Select ? "True" : "False")), + Target.ToXml("Field")); + + public override string ToDisplayLine() => + "Insert from Last Visited [ " + "Select: " + (Select ? "On" : "Off") + " ; " + Target.ToDisplayString() + " ]"; + + public static new ScriptStep FromXml(XElement step) + { + var enabled = step.Attribute("enable")?.Value != "False"; + var select_v = step.Element("SelectAll")?.Attribute("state")?.Value == "True"; + var fieldEl = step.Element("Field"); + var target = fieldEl is not null ? FieldRef.FromXml(fieldEl) : FieldRef.ForField("", 0, ""); + return new InsertFromLastVisitedStep(select_v, target, enabled); + } + + public static ScriptStep FromDisplayParams(bool enabled, string[] hrParams) + { + var tokens = hrParams.Select(h => h.Trim()).ToArray(); + bool select_v = true; + foreach (var tok in tokens) { if (tok.StartsWith("Select:", StringComparison.OrdinalIgnoreCase)) { var v = tok.Substring(7).Trim(); select_v = v.Equals("On", StringComparison.OrdinalIgnoreCase); break; } } + FieldRef target = FieldRef.ForField("", 0, ""); + foreach (var tok in tokens) { if (!tok.StartsWith("Select:", StringComparison.OrdinalIgnoreCase) && !string.IsNullOrWhiteSpace(tok)) { target = FieldRef.FromDisplayToken(tok); break; } } + return new InsertFromLastVisitedStep(select_v, target, enabled); + } + + public static StepMetadata Metadata { get; } = new() + { + Name = XmlName, + Id = XmlId, + Category = "fields", + HelpUrl = "https://help.claris.com/en/pro-help/content/insert-from-last-visited.html", + Params = + [ + new ParamMetadata + { + Name = "SelectAll", + XmlElement = "SelectAll", + Type = "boolean", + XmlAttr = "state", + HrLabel = "Select", + ValidValues = ["On", "Off"], + DefaultValue = "True", + }, + new ParamMetadata + { + Name = "Field", + XmlElement = "Field", + Type = "field", + }, + ], + FromXml = FromXml, + FromDisplay = FromDisplayParams, + }; +} diff --git a/src/SharpFM.Model/Scripting/Steps/InstallPlugInFileStep.cs b/src/SharpFM.Model/Scripting/Steps/InstallPlugInFileStep.cs new file mode 100644 index 0000000..7fd0a0a --- /dev/null +++ b/src/SharpFM.Model/Scripting/Steps/InstallPlugInFileStep.cs @@ -0,0 +1,68 @@ +using System; +using System.Linq; +using System.Xml.Linq; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Values; + +namespace SharpFM.Model.Scripting.Steps; + +public sealed class InstallPlugInFileStep : ScriptStep, IStepFactory +{ + public const int XmlId = 157; + public const string XmlName = "Install Plug-In File"; + + public FieldRef Target { get; set; } + + public InstallPlugInFileStep( + FieldRef? target = null, + bool enabled = true) + : base(null, enabled) + { + Target = target ?? FieldRef.ForField("", 0, ""); + } + + public override XElement ToXml() => + new("Step", + new XAttribute("enable", Enabled ? "True" : "False"), + new XAttribute("id", XmlId), + new XAttribute("name", XmlName), + Target.ToXml("Field")); + + public override string ToDisplayLine() => + "Install Plug-In File [ " + Target.ToDisplayString() + " ]"; + + public static new ScriptStep FromXml(XElement step) + { + var enabled = step.Attribute("enable")?.Value != "False"; + var fieldEl = step.Element("Field"); + var target = fieldEl is not null ? FieldRef.FromXml(fieldEl) : FieldRef.ForField("", 0, ""); + return new InstallPlugInFileStep(target, enabled); + } + + public static ScriptStep FromDisplayParams(bool enabled, string[] hrParams) + { + var tokens = hrParams.Select(h => h.Trim()).ToArray(); + FieldRef target = FieldRef.ForField("", 0, ""); + foreach (var tok in tokens) { if (true && !string.IsNullOrWhiteSpace(tok)) { target = FieldRef.FromDisplayToken(tok); break; } } + return new InstallPlugInFileStep(target, enabled); + } + + public static StepMetadata Metadata { get; } = new() + { + Name = XmlName, + Id = XmlId, + Category = "miscellaneous", + HelpUrl = "https://help.claris.com/en/pro-help/content/install-plug-in-file.html", + Params = + [ + new ParamMetadata + { + Name = "Field", + XmlElement = "Field", + Type = "field", + }, + ], + FromXml = FromXml, + FromDisplay = FromDisplayParams, + }; +} diff --git a/src/SharpFM.Model/Scripting/Steps/PasteStep.cs b/src/SharpFM.Model/Scripting/Steps/PasteStep.cs new file mode 100644 index 0000000..7e8a3ec --- /dev/null +++ b/src/SharpFM.Model/Scripting/Steps/PasteStep.cs @@ -0,0 +1,120 @@ +using System; +using System.Linq; +using System.Xml.Linq; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Values; + +namespace SharpFM.Model.Scripting.Steps; + +public sealed class PasteStep : ScriptStep, IStepFactory +{ + public const int XmlId = 48; + public const string XmlName = "Paste"; + + public bool Select { get; set; } + public bool NoStyle { get; set; } + public bool LinkIfAvailable { get; set; } + public FieldRef Target { get; set; } + + public PasteStep( + bool select = false, + bool noStyle = false, + bool linkIfAvailable = false, + FieldRef? target = null, + bool enabled = true) + : base(null, enabled) + { + Select = select; + NoStyle = noStyle; + LinkIfAvailable = linkIfAvailable; + Target = target ?? FieldRef.ForField("", 0, ""); + } + + public override XElement ToXml() => + new("Step", + new XAttribute("enable", Enabled ? "True" : "False"), + new XAttribute("id", XmlId), + new XAttribute("name", XmlName), + new XElement("SelectAll", new XAttribute("state", Select ? "True" : "False")), + new XElement("NoStyle", new XAttribute("state", NoStyle ? "True" : "False")), + new XElement("LinkAvail", new XAttribute("state", LinkIfAvailable ? "True" : "False")), + Target.ToXml("Field")); + + public override string ToDisplayLine() => + "Paste [ " + "Select: " + (Select ? "On" : "Off") + " ; " + "No style: " + (NoStyle ? "On" : "Off") + " ; " + "Link if available: " + (LinkIfAvailable ? "On" : "Off") + " ; " + "Table::Field: " + Target.ToDisplayString() + " ]"; + + public static new ScriptStep FromXml(XElement step) + { + var enabled = step.Attribute("enable")?.Value != "False"; + var select_v = step.Element("SelectAll")?.Attribute("state")?.Value == "True"; + var noStyle_v = step.Element("NoStyle")?.Attribute("state")?.Value == "True"; + var linkIfAvailable_v = step.Element("LinkAvail")?.Attribute("state")?.Value == "True"; + var fieldEl = step.Element("Field"); + var target = fieldEl is not null ? FieldRef.FromXml(fieldEl) : FieldRef.ForField("", 0, ""); + return new PasteStep(select_v, noStyle_v, linkIfAvailable_v, target, enabled); + } + + public static ScriptStep FromDisplayParams(bool enabled, string[] hrParams) + { + var tokens = hrParams.Select(h => h.Trim()).ToArray(); + bool select_v = false; + foreach (var tok in tokens) { if (tok.StartsWith("Select:", StringComparison.OrdinalIgnoreCase)) { var v = tok.Substring(7).Trim(); select_v = v.Equals("On", StringComparison.OrdinalIgnoreCase); break; } } + bool noStyle_v = false; + foreach (var tok in tokens) { if (tok.StartsWith("No style:", StringComparison.OrdinalIgnoreCase)) { var v = tok.Substring(9).Trim(); noStyle_v = v.Equals("On", StringComparison.OrdinalIgnoreCase); break; } } + bool linkIfAvailable_v = false; + foreach (var tok in tokens) { if (tok.StartsWith("Link if available:", StringComparison.OrdinalIgnoreCase)) { var v = tok.Substring(18).Trim(); linkIfAvailable_v = v.Equals("On", StringComparison.OrdinalIgnoreCase); break; } } + FieldRef target = FieldRef.ForField("", 0, ""); + foreach (var tok in tokens) { if (tok.StartsWith("Table::Field:", StringComparison.OrdinalIgnoreCase)) { var v = tok.Substring(13).Trim(); target = FieldRef.FromDisplayToken(v); break; } } + return new PasteStep(select_v, noStyle_v, linkIfAvailable_v, target, enabled); + } + + public static StepMetadata Metadata { get; } = new() + { + Name = XmlName, + Id = XmlId, + Category = "editing", + HelpUrl = "https://help.claris.com/en/pro-help/content/paste.html", + Params = + [ + new ParamMetadata + { + Name = "SelectAll", + XmlElement = "SelectAll", + Type = "flagBoolean", + XmlAttr = "state", + HrLabel = "Select", + ValidValues = ["On", "Off"], + DefaultValue = "False", + }, + new ParamMetadata + { + Name = "NoStyle", + XmlElement = "NoStyle", + Type = "flagBoolean", + XmlAttr = "state", + HrLabel = "No style", + ValidValues = ["On", "Off"], + DefaultValue = "False", + }, + new ParamMetadata + { + Name = "LinkAvail", + XmlElement = "LinkAvail", + Type = "flagBoolean", + XmlAttr = "state", + HrLabel = "Link if available", + ValidValues = ["On", "Off"], + DefaultValue = "False", + }, + new ParamMetadata + { + Name = "Field", + XmlElement = "Field", + Type = "field", + HrLabel = "Table::Field", + }, + ], + FromXml = FromXml, + FromDisplay = FromDisplayParams, + }; +} diff --git a/src/SharpFM.Model/Scripting/Steps/RelookupFieldContentsStep.cs b/src/SharpFM.Model/Scripting/Steps/RelookupFieldContentsStep.cs new file mode 100644 index 0000000..24bdb49 --- /dev/null +++ b/src/SharpFM.Model/Scripting/Steps/RelookupFieldContentsStep.cs @@ -0,0 +1,85 @@ +using System; +using System.Linq; +using System.Xml.Linq; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Values; + +namespace SharpFM.Model.Scripting.Steps; + +public sealed class RelookupFieldContentsStep : ScriptStep, IStepFactory +{ + public const int XmlId = 40; + public const string XmlName = "Relookup Field Contents"; + + public bool WithDialog { get; set; } + public FieldRef Target { get; set; } + + public RelookupFieldContentsStep( + bool withDialog = true, + FieldRef? target = null, + bool enabled = true) + : base(null, enabled) + { + WithDialog = withDialog; + Target = target ?? FieldRef.ForField("", 0, ""); + } + + public override XElement ToXml() => + new("Step", + new XAttribute("enable", Enabled ? "True" : "False"), + new XAttribute("id", XmlId), + new XAttribute("name", XmlName), + new XElement("NoInteract", new XAttribute("state", WithDialog ? "True" : "False")), + Target.ToXml("Field")); + + public override string ToDisplayLine() => + "Relookup Field Contents [ " + "With dialog: " + (WithDialog ? "On" : "Off") + " ; " + Target.ToDisplayString() + " ]"; + + public static new ScriptStep FromXml(XElement step) + { + var enabled = step.Attribute("enable")?.Value != "False"; + var withDialog_v = step.Element("NoInteract")?.Attribute("state")?.Value == "True"; + var fieldEl = step.Element("Field"); + var target = fieldEl is not null ? FieldRef.FromXml(fieldEl) : FieldRef.ForField("", 0, ""); + return new RelookupFieldContentsStep(withDialog_v, target, enabled); + } + + public static ScriptStep FromDisplayParams(bool enabled, string[] hrParams) + { + var tokens = hrParams.Select(h => h.Trim()).ToArray(); + bool withDialog_v = true; + foreach (var tok in tokens) { if (tok.StartsWith("With dialog:", StringComparison.OrdinalIgnoreCase)) { var v = tok.Substring(12).Trim(); withDialog_v = v.Equals("On", StringComparison.OrdinalIgnoreCase); break; } } + FieldRef target = FieldRef.ForField("", 0, ""); + foreach (var tok in tokens) { if (!tok.StartsWith("With dialog:", StringComparison.OrdinalIgnoreCase) && !string.IsNullOrWhiteSpace(tok)) { target = FieldRef.FromDisplayToken(tok); break; } } + return new RelookupFieldContentsStep(withDialog_v, target, enabled); + } + + public static StepMetadata Metadata { get; } = new() + { + Name = XmlName, + Id = XmlId, + Category = "fields", + HelpUrl = "https://help.claris.com/en/pro-help/content/relookup-field-contents.html", + Params = + [ + new ParamMetadata + { + Name = "NoInteract", + XmlElement = "NoInteract", + Type = "boolean", + XmlAttr = "state", + HrLabel = "With dialog", + ValidValues = ["On", "Off"], + DefaultValue = "True", + }, + new ParamMetadata + { + Name = "Field", + XmlElement = "Field", + Type = "field", + }, + ], + FromXml = FromXml, + FromDisplay = FromDisplayParams, + }; +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/CheckSelectionStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/CheckSelectionStepTests.cs new file mode 100644 index 0000000..01b23da --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/CheckSelectionStepTests.cs @@ -0,0 +1,27 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +public class CheckSelectionStepTests +{ + private const string CanonicalXml = """"""; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = CheckSelectionStep.Metadata.FromXml!(source); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Check Selection", out var metadata)); + Assert.Equal(18, metadata!.Id); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/ClearStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/ClearStepTests.cs new file mode 100644 index 0000000..c2f2c10 --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/ClearStepTests.cs @@ -0,0 +1,27 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +public class ClearStepTests +{ + private const string CanonicalXml = """"""; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = ClearStep.Metadata.FromXml!(source); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Clear", out var metadata)); + Assert.Equal(49, metadata!.Id); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/CopyStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/CopyStepTests.cs new file mode 100644 index 0000000..343a35a --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/CopyStepTests.cs @@ -0,0 +1,27 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +public class CopyStepTests +{ + private const string CanonicalXml = """"""; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = CopyStep.Metadata.FromXml!(source); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Copy", out var metadata)); + Assert.Equal(47, metadata!.Id); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/CutStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/CutStepTests.cs new file mode 100644 index 0000000..ef65dcf --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/CutStepTests.cs @@ -0,0 +1,27 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +public class CutStepTests +{ + private const string CanonicalXml = """"""; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = CutStep.Metadata.FromXml!(source); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Cut", out var metadata)); + Assert.Equal(46, metadata!.Id); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/GoToFieldStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/GoToFieldStepTests.cs new file mode 100644 index 0000000..cefc625 --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/GoToFieldStepTests.cs @@ -0,0 +1,27 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +public class GoToFieldStepTests +{ + private const string CanonicalXml = """"""; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = GoToFieldStep.Metadata.FromXml!(source); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Go to Field", out var metadata)); + Assert.Equal(17, metadata!.Id); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/InsertCurrentDateStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/InsertCurrentDateStepTests.cs new file mode 100644 index 0000000..26667b1 --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/InsertCurrentDateStepTests.cs @@ -0,0 +1,27 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +public class InsertCurrentDateStepTests +{ + private const string CanonicalXml = """"""; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = InsertCurrentDateStep.Metadata.FromXml!(source); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Insert Current Date", out var metadata)); + Assert.Equal(13, metadata!.Id); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/InsertCurrentTimeStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/InsertCurrentTimeStepTests.cs new file mode 100644 index 0000000..f08e6c3 --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/InsertCurrentTimeStepTests.cs @@ -0,0 +1,27 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +public class InsertCurrentTimeStepTests +{ + private const string CanonicalXml = """"""; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = InsertCurrentTimeStep.Metadata.FromXml!(source); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Insert Current Time", out var metadata)); + Assert.Equal(14, metadata!.Id); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/InsertCurrentUserNameStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/InsertCurrentUserNameStepTests.cs new file mode 100644 index 0000000..5eb5721 --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/InsertCurrentUserNameStepTests.cs @@ -0,0 +1,27 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +public class InsertCurrentUserNameStepTests +{ + private const string CanonicalXml = """"""; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = InsertCurrentUserNameStep.Metadata.FromXml!(source); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Insert Current User Name", out var metadata)); + Assert.Equal(60, metadata!.Id); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/InsertFromIndexStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/InsertFromIndexStepTests.cs new file mode 100644 index 0000000..144b1a4 --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/InsertFromIndexStepTests.cs @@ -0,0 +1,27 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +public class InsertFromIndexStepTests +{ + private const string CanonicalXml = """"""; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = InsertFromIndexStep.Metadata.FromXml!(source); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Insert from Index", out var metadata)); + Assert.Equal(11, metadata!.Id); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/InsertFromLastVisitedStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/InsertFromLastVisitedStepTests.cs new file mode 100644 index 0000000..d77a003 --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/InsertFromLastVisitedStepTests.cs @@ -0,0 +1,27 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +public class InsertFromLastVisitedStepTests +{ + private const string CanonicalXml = """"""; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = InsertFromLastVisitedStep.Metadata.FromXml!(source); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Insert from Last Visited", out var metadata)); + Assert.Equal(12, metadata!.Id); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/InstallPlugInFileStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/InstallPlugInFileStepTests.cs new file mode 100644 index 0000000..fac11dd --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/InstallPlugInFileStepTests.cs @@ -0,0 +1,27 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +public class InstallPlugInFileStepTests +{ + private const string CanonicalXml = """"""; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = InstallPlugInFileStep.Metadata.FromXml!(source); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Install Plug-In File", out var metadata)); + Assert.Equal(157, metadata!.Id); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/PasteStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/PasteStepTests.cs new file mode 100644 index 0000000..3344387 --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/PasteStepTests.cs @@ -0,0 +1,27 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +public class PasteStepTests +{ + private const string CanonicalXml = """"""; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = PasteStep.Metadata.FromXml!(source); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Paste", out var metadata)); + Assert.Equal(48, metadata!.Id); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/RelookupFieldContentsStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/RelookupFieldContentsStepTests.cs new file mode 100644 index 0000000..4e0dc2b --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/RelookupFieldContentsStepTests.cs @@ -0,0 +1,27 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +public class RelookupFieldContentsStepTests +{ + private const string CanonicalXml = """"""; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = RelookupFieldContentsStep.Metadata.FromXml!(source); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Relookup Field Contents", out var metadata)); + Assert.Equal(40, metadata!.Id); + } +} From be31edce52ed1b8646f3aafadda23fe1adc01dcf Mon Sep 17 00:00:00 2001 From: Nate Bross Date: Mon, 20 Apr 2026 23:05:16 -0500 Subject: [PATCH 13/38] feat(scripting): migrate control-flow siblings to IStepFactory Else, Else If, End If, Loop, End Loop, Exit Loop If each now carry their own StepMetadata. Removes the sweep-transitional ModuleInitializer that was registering siblings into StepXmlFactory/StepDisplayFactory; StepRegistry now discovers every control-flow step via IStepFactory. --- .../Scripting/Steps/ControlFlowSteps.cs | 286 +++++++++++------- 1 file changed, 184 insertions(+), 102 deletions(-) diff --git a/src/SharpFM.Model/Scripting/Steps/ControlFlowSteps.cs b/src/SharpFM.Model/Scripting/Steps/ControlFlowSteps.cs index 2b85fd1..bd2c1d2 100644 --- a/src/SharpFM.Model/Scripting/Steps/ControlFlowSteps.cs +++ b/src/SharpFM.Model/Scripting/Steps/ControlFlowSteps.cs @@ -1,5 +1,3 @@ -using System.Diagnostics.CodeAnalysis; -using System.Runtime.CompilerServices; using System.Xml.Linq; using SharpFM.Model.Scripting.Registry; using SharpFM.Model.Scripting.Serialization; @@ -9,97 +7,55 @@ namespace SharpFM.Model.Scripting.Steps; /// /// Typed POCOs for FileMaker's block-pair control-flow steps: -/// If, Else If, Else, End If. +/// If, Else If, Else, End If, Loop, +/// End Loop, Exit Loop If. /// -/// If and Else If carry a single -/// condition. The catalog defines a Restore param for these two, -/// but FM Pro never emits it in clipboard output and the old handler -/// explicitly ignored it — the typed POCO follows suit. +/// Every step here is an . The single-file +/// layout keeps the related steps and their shared helpers +/// (, ) +/// colocated. /// /// -/// Else and End If have no fields and no children; the -/// typed POCOs still need to exist so they bypass the generic catalog -/// display-render path, which previously corrupted If's rendering -/// in certain calculation shapes (e.g. the old path emitted -/// If [ Off ] for If [ Get ( FoundCount ) > 0 ]). Block-pair -/// indentation is driven by StepDefinition.BlockPair from the -/// catalog and is handled by , not -/// by the individual POCOs. -/// -/// -/// -/// Zero-loss audit for : +/// Zero-loss audit: /// -/// Step attributes (enable, id, name) — round-tripped. -/// <Calculation> CDATA body — round-tripped via . -/// <Restore state="..."/>intentionally dropped. Upstream agentic-fm -/// snippets include it, but FM Pro never changes the value and never emits the element in -/// clipboard output; it carries no information worth round-tripping. See -/// docs/advanced-filemaker-scripting-syntax.md for the "what to drop vs. surface" -/// guidance this follows. +/// If / Else If / Loop carry a Restore element in some upstream XML +/// sources. FM Pro never writes it and never changes the value; we drop it on read and never emit it. +/// See docs/advanced-filemaker-scripting-syntax.md. +/// Loop's FlushType enum is also absent from FM Pro clipboard output. Not surfaced +/// in display and not round-tripped — same "semantically fixed / never-changed" justification. /// +/// /// public sealed class IfStep : ScriptStep, IStepFactory { + public const int XmlId = 68; + public const string XmlName = "If"; + public Calculation Condition { get; set; } public IfStep(bool enabled, Calculation condition) - : base(BuildLegacyDefinition(), enabled) + : base(BuildLegacyDefinition(XmlName, XmlId, BlockPairRole.Open, ["Else", "Else If", "End If"]), enabled) { Condition = condition; } - // Transitional: legacy consumers (FmScript.ToDisplayLines, etc.) still - // read step.Definition.BlockPair for indent decisions. Project the - // pieces Metadata carries into a synthesized StepDefinition until those - // consumers migrate to StepRegistry in a later phase. - private static StepDefinition BuildLegacyDefinition() => new() + // Legacy consumers (FmScript.ToDisplayLines) still read step.Definition.BlockPair + // for indentation. Synthesize just enough StepDefinition to satisfy them until + // those consumers migrate to StepRegistry. + internal static StepDefinition BuildLegacyDefinition(string name, int id, BlockPairRole role, string[] partners) => new() { - Name = "If", - Id = 68, - BlockPair = new StepBlockPair - { - Role = BlockPairRole.Open, - Partners = ["Else", "Else If", "End If"], - }, + Name = name, + Id = id, + BlockPair = new StepBlockPair { Role = role, Partners = partners }, }; - [SuppressMessage("Usage", "CA2255:The 'ModuleInitializer' attribute should not be used in libraries", - Justification = "Register sibling control-flow steps that haven't migrated to IStepFactory yet.")] - [ModuleInitializer] - internal static void Register() - { - // IfStep itself registers via StepRegistry's reflection scan (IStepFactory). - // The siblings below migrate in the sweep phase; for now their factory - // registrations stay here so the legacy StepXmlFactory / StepDisplayFactory - // surfaces continue to return typed POCOs. - - StepXmlFactory.Register("Else If", ElseIfStep.FromXml); - StepDisplayFactory.Register("Else If", ElseIfStep.FromDisplayParams); - - StepXmlFactory.Register("Else", ElseStep.FromXml); - StepDisplayFactory.Register("Else", ElseStep.FromDisplayParams); - - StepXmlFactory.Register("End If", EndIfStep.FromXml); - StepDisplayFactory.Register("End If", EndIfStep.FromDisplayParams); - - StepXmlFactory.Register("Loop", LoopStep.FromXml); - StepDisplayFactory.Register("Loop", LoopStep.FromDisplayParams); - - StepXmlFactory.Register("End Loop", EndLoopStep.FromXml); - StepDisplayFactory.Register("End Loop", EndLoopStep.FromDisplayParams); - - StepXmlFactory.Register("Exit Loop If", ExitLoopIfStep.FromXml); - StepDisplayFactory.Register("Exit Loop If", ExitLoopIfStep.FromDisplayParams); - } - public static new ScriptStep FromXml(XElement step) => new IfStep( step.Attribute("enable")?.Value != "False", ReadCalculation(step)); public override XElement ToXml() => - BuildConditionedStep(this, "If", 68, Condition); + BuildConditionedStep(this, XmlName, XmlId, Condition); public override string ToDisplayLine() => $"If [ {Condition.Text} ]"; @@ -108,8 +64,8 @@ public static ScriptStep FromDisplayParams(bool enabled, string[] hrParams) => public static StepMetadata Metadata { get; } = new() { - Name = "If", - Id = 68, + Name = XmlName, + Id = XmlId, Category = "control", HelpUrl = "https://help.claris.com/en/pro-help/content/if-script-step.html", HrSignature = "[ condition ]", @@ -125,17 +81,10 @@ public static ScriptStep FromDisplayParams(bool enabled, string[] hrParams) => Name = "condition", XmlElement = "Calculation", Type = "calculation", - // Intentionally no HrLabel — FM Pro's display renders the - // calc without a label prefix (e.g. "If [ $x > 0 ]"). The - // completion snippet synthesizer uses Name as the - // placeholder hint so it reads "If [ ${1:condition} ]". Required = true, }, ], - Notes = new StepNotes - { - Constraints = "Requires a matching End If step.", - }, + Notes = new StepNotes { Constraints = "Requires a matching End If step." }, FromXml = FromXml, FromDisplay = FromDisplayParams, }; @@ -160,12 +109,15 @@ internal static XElement BuildConditionedStep(ScriptStep owner, string name, int } } -public sealed class ElseIfStep : ScriptStep +public sealed class ElseIfStep : ScriptStep, IStepFactory { + public const int XmlId = 125; + public const string XmlName = "Else If"; + public Calculation Condition { get; set; } public ElseIfStep(bool enabled, Calculation condition) - : base(StepCatalogLoader.ByName["Else If"], enabled) + : base(IfStep.BuildLegacyDefinition(XmlName, XmlId, BlockPairRole.Middle, ["If", "End If"]), enabled) { Condition = condition; } @@ -176,29 +128,74 @@ public ElseIfStep(bool enabled, Calculation condition) IfStep.ReadCalculation(step)); public override XElement ToXml() => - IfStep.BuildConditionedStep(this, "Else If", 125, Condition); + IfStep.BuildConditionedStep(this, XmlName, XmlId, Condition); public override string ToDisplayLine() => $"Else If [ {Condition.Text} ]"; public static ScriptStep FromDisplayParams(bool enabled, string[] hrParams) => new ElseIfStep(enabled, IfStep.ParseCondition(hrParams)); + + public static StepMetadata Metadata { get; } = new() + { + Name = XmlName, + Id = XmlId, + Category = "control", + HelpUrl = "https://help.claris.com/en/pro-help/content/else-if.html", + HrSignature = "[ condition ]", + BlockPair = new StepBlockPair + { + Role = BlockPairRole.Middle, + Partners = ["If", "End If"], + }, + Params = + [ + new ParamMetadata + { + Name = "condition", + XmlElement = "Calculation", + Type = "calculation", + Required = true, + }, + ], + FromXml = FromXml, + FromDisplay = FromDisplayParams, + }; } -public sealed class ElseStep : ScriptStep +public sealed class ElseStep : ScriptStep, IStepFactory { + public const int XmlId = 69; + public const string XmlName = "Else"; + public ElseStep(bool enabled) - : base(StepCatalogLoader.ByName["Else"], enabled) { } + : base(IfStep.BuildLegacyDefinition(XmlName, XmlId, BlockPairRole.Middle, ["If", "End If"]), enabled) { } public static new ScriptStep FromXml(XElement step) => new ElseStep(step.Attribute("enable")?.Value != "False"); - public override XElement ToXml() => BuildBareStep(this, "Else", 69); + public override XElement ToXml() => BuildBareStep(this, XmlName, XmlId); - public override string ToDisplayLine() => "Else"; + public override string ToDisplayLine() => XmlName; public static ScriptStep FromDisplayParams(bool enabled, string[] hrParams) => new ElseStep(enabled); + public static StepMetadata Metadata { get; } = new() + { + Name = XmlName, + Id = XmlId, + Category = "control", + HelpUrl = "https://help.claris.com/en/pro-help/content/else.html", + HrSignature = "Else", + BlockPair = new StepBlockPair + { + Role = BlockPairRole.Middle, + Partners = ["If", "End If"], + }, + FromXml = FromXml, + FromDisplay = FromDisplayParams, + }; + internal static XElement BuildBareStep(ScriptStep owner, string name, int id) => new("Step", new XAttribute("enable", owner.Enabled ? "True" : "False"), @@ -206,60 +203,120 @@ internal static XElement BuildBareStep(ScriptStep owner, string name, int id) => new XAttribute("name", name)); } -public sealed class EndIfStep : ScriptStep +public sealed class EndIfStep : ScriptStep, IStepFactory { + public const int XmlId = 70; + public const string XmlName = "End If"; + public EndIfStep(bool enabled) - : base(StepCatalogLoader.ByName["End If"], enabled) { } + : base(IfStep.BuildLegacyDefinition(XmlName, XmlId, BlockPairRole.Close, ["If"]), enabled) { } public static new ScriptStep FromXml(XElement step) => new EndIfStep(step.Attribute("enable")?.Value != "False"); - public override XElement ToXml() => ElseStep.BuildBareStep(this, "End If", 70); + public override XElement ToXml() => ElseStep.BuildBareStep(this, XmlName, XmlId); - public override string ToDisplayLine() => "End If"; + public override string ToDisplayLine() => XmlName; public static ScriptStep FromDisplayParams(bool enabled, string[] hrParams) => new EndIfStep(enabled); + + public static StepMetadata Metadata { get; } = new() + { + Name = XmlName, + Id = XmlId, + Category = "control", + HelpUrl = "https://help.claris.com/en/pro-help/content/end-if.html", + HrSignature = "End If", + BlockPair = new StepBlockPair + { + Role = BlockPairRole.Close, + Partners = ["If"], + }, + FromXml = FromXml, + FromDisplay = FromDisplayParams, + }; } -public sealed class LoopStep : ScriptStep +public sealed class LoopStep : ScriptStep, IStepFactory { + public const int XmlId = 71; + public const string XmlName = "Loop"; + public LoopStep(bool enabled) - : base(StepCatalogLoader.ByName["Loop"], enabled) { } + : base(IfStep.BuildLegacyDefinition(XmlName, XmlId, BlockPairRole.Open, ["End Loop"]), enabled) { } public static new ScriptStep FromXml(XElement step) => new LoopStep(step.Attribute("enable")?.Value != "False"); - public override XElement ToXml() => ElseStep.BuildBareStep(this, "Loop", 71); + public override XElement ToXml() => ElseStep.BuildBareStep(this, XmlName, XmlId); - public override string ToDisplayLine() => "Loop"; + public override string ToDisplayLine() => XmlName; public static ScriptStep FromDisplayParams(bool enabled, string[] hrParams) => new LoopStep(enabled); + + public static StepMetadata Metadata { get; } = new() + { + Name = XmlName, + Id = XmlId, + Category = "control", + HelpUrl = "https://help.claris.com/en/pro-help/content/loop.html", + HrSignature = "Loop", + BlockPair = new StepBlockPair + { + Role = BlockPairRole.Open, + Partners = ["End Loop"], + }, + FromXml = FromXml, + FromDisplay = FromDisplayParams, + }; } -public sealed class EndLoopStep : ScriptStep +public sealed class EndLoopStep : ScriptStep, IStepFactory { + public const int XmlId = 73; + public const string XmlName = "End Loop"; + public EndLoopStep(bool enabled) - : base(StepCatalogLoader.ByName["End Loop"], enabled) { } + : base(IfStep.BuildLegacyDefinition(XmlName, XmlId, BlockPairRole.Close, ["Loop"]), enabled) { } public static new ScriptStep FromXml(XElement step) => new EndLoopStep(step.Attribute("enable")?.Value != "False"); - public override XElement ToXml() => ElseStep.BuildBareStep(this, "End Loop", 73); + public override XElement ToXml() => ElseStep.BuildBareStep(this, XmlName, XmlId); - public override string ToDisplayLine() => "End Loop"; + public override string ToDisplayLine() => XmlName; public static ScriptStep FromDisplayParams(bool enabled, string[] hrParams) => new EndLoopStep(enabled); + + public static StepMetadata Metadata { get; } = new() + { + Name = XmlName, + Id = XmlId, + Category = "control", + HelpUrl = "https://help.claris.com/en/pro-help/content/end-loop.html", + HrSignature = "End Loop", + BlockPair = new StepBlockPair + { + Role = BlockPairRole.Close, + Partners = ["Loop"], + }, + FromXml = FromXml, + FromDisplay = FromDisplayParams, + }; } -public sealed class ExitLoopIfStep : ScriptStep +public sealed class ExitLoopIfStep : ScriptStep, IStepFactory { + public const int XmlId = 72; + public const string XmlName = "Exit Loop If"; + public Calculation Condition { get; set; } public ExitLoopIfStep(bool enabled, Calculation condition) - : base(StepCatalogLoader.ByName["Exit Loop If"], enabled) + : base(IfStep.BuildLegacyDefinition(XmlName, XmlId, BlockPairRole.Middle, ["Loop", "End Loop"]), enabled) { Condition = condition; } @@ -270,11 +327,36 @@ public ExitLoopIfStep(bool enabled, Calculation condition) IfStep.ReadCalculation(step)); public override XElement ToXml() => - IfStep.BuildConditionedStep(this, "Exit Loop If", 72, Condition); + IfStep.BuildConditionedStep(this, XmlName, XmlId, Condition); public override string ToDisplayLine() => $"Exit Loop If [ {Condition.Text} ]"; public static ScriptStep FromDisplayParams(bool enabled, string[] hrParams) => new ExitLoopIfStep(enabled, IfStep.ParseCondition(hrParams)); -} + public static StepMetadata Metadata { get; } = new() + { + Name = XmlName, + Id = XmlId, + Category = "control", + HelpUrl = "https://help.claris.com/en/pro-help/content/exit-loop-if.html", + HrSignature = "[ condition ]", + BlockPair = new StepBlockPair + { + Role = BlockPairRole.Middle, + Partners = ["Loop", "End Loop"], + }, + Params = + [ + new ParamMetadata + { + Name = "condition", + XmlElement = "Calculation", + Type = "calculation", + Required = true, + }, + ], + FromXml = FromXml, + FromDisplay = FromDisplayParams, + }; +} From 165d4d594bdba9bdd1242d389b21d376e0022174 Mon Sep 17 00:00:00 2001 From: Nate Bross Date: Mon, 20 Apr 2026 23:11:14 -0500 Subject: [PATCH 14/38] feat(scripting): migrate 9 file/table/menuset steps to IStepFactory POCOs Close File, Open File, Open Data File, Get File Exists, Get File Size, Get Data File Position, Truncate Table, Install Menu Set, Install OnTimer Script. Introduces a FileReference value type for the shape used by Close/Open File. --- .../Scripting/Steps/CloseFileStep.cs | 74 +++++++++++ .../Steps/GetDataFilePositionStep.cs | 82 +++++++++++++ .../Scripting/Steps/GetFileExistsStep.cs | 83 +++++++++++++ .../Scripting/Steps/GetFileSizeStep.cs | 83 +++++++++++++ .../Scripting/Steps/InstallMenuSetStep.cs | 90 ++++++++++++++ .../Steps/InstallOnTimerScriptStep.cs | 95 +++++++++++++++ .../Scripting/Steps/OpenDataFileStep.cs | 95 +++++++++++++++ .../Scripting/Steps/OpenFileStep.cs | 108 ++++++++++++++++ .../Scripting/Steps/TruncateTableStep.cs | 115 ++++++++++++++++++ .../Scripting/Values/FileReference.cs | 50 ++++++++ .../Scripting/Steps/CloseFileStepTests.cs | 44 +++++++ .../Steps/GetDataFilePositionStepTests.cs | 35 ++++++ .../Scripting/Steps/GetFileExistsStepTests.cs | 35 ++++++ .../Scripting/Steps/GetFileSizeStepTests.cs | 35 ++++++ .../Steps/InstallMenuSetStepTests.cs | 35 ++++++ .../Steps/InstallOnTimerScriptStepTests.cs | 35 ++++++ .../Scripting/Steps/OpenDataFileStepTests.cs | 35 ++++++ .../Scripting/Steps/OpenFileStepTests.cs | 44 +++++++ .../Scripting/Steps/TruncateTableStepTests.cs | 35 ++++++ 19 files changed, 1208 insertions(+) create mode 100644 src/SharpFM.Model/Scripting/Steps/CloseFileStep.cs create mode 100644 src/SharpFM.Model/Scripting/Steps/GetDataFilePositionStep.cs create mode 100644 src/SharpFM.Model/Scripting/Steps/GetFileExistsStep.cs create mode 100644 src/SharpFM.Model/Scripting/Steps/GetFileSizeStep.cs create mode 100644 src/SharpFM.Model/Scripting/Steps/InstallMenuSetStep.cs create mode 100644 src/SharpFM.Model/Scripting/Steps/InstallOnTimerScriptStep.cs create mode 100644 src/SharpFM.Model/Scripting/Steps/OpenDataFileStep.cs create mode 100644 src/SharpFM.Model/Scripting/Steps/OpenFileStep.cs create mode 100644 src/SharpFM.Model/Scripting/Steps/TruncateTableStep.cs create mode 100644 src/SharpFM.Model/Scripting/Values/FileReference.cs create mode 100644 tests/SharpFM.Tests/Scripting/Steps/CloseFileStepTests.cs create mode 100644 tests/SharpFM.Tests/Scripting/Steps/GetDataFilePositionStepTests.cs create mode 100644 tests/SharpFM.Tests/Scripting/Steps/GetFileExistsStepTests.cs create mode 100644 tests/SharpFM.Tests/Scripting/Steps/GetFileSizeStepTests.cs create mode 100644 tests/SharpFM.Tests/Scripting/Steps/InstallMenuSetStepTests.cs create mode 100644 tests/SharpFM.Tests/Scripting/Steps/InstallOnTimerScriptStepTests.cs create mode 100644 tests/SharpFM.Tests/Scripting/Steps/OpenDataFileStepTests.cs create mode 100644 tests/SharpFM.Tests/Scripting/Steps/OpenFileStepTests.cs create mode 100644 tests/SharpFM.Tests/Scripting/Steps/TruncateTableStepTests.cs diff --git a/src/SharpFM.Model/Scripting/Steps/CloseFileStep.cs b/src/SharpFM.Model/Scripting/Steps/CloseFileStep.cs new file mode 100644 index 0000000..b0fa41e --- /dev/null +++ b/src/SharpFM.Model/Scripting/Steps/CloseFileStep.cs @@ -0,0 +1,74 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Values; + +namespace SharpFM.Model.Scripting.Steps; + +public sealed class CloseFileStep : ScriptStep, IStepFactory +{ + public const int XmlId = 34; + public const string XmlName = "Close File"; + + public FileReference? File { get; set; } + + public CloseFileStep(FileReference? file = null, bool enabled = true) + : base(null, enabled) + { + File = file; + } + + public override XElement ToXml() + { + var step = new XElement("Step", + new XAttribute("enable", Enabled ? "True" : "False"), + new XAttribute("id", XmlId), + new XAttribute("name", XmlName)); + if (File is not null) step.Add(File.ToXml()); + return step; + } + + public override string ToDisplayLine() => + File is null + ? "Close File [ Current File ]" + : $"Close File [ {File.ToDisplayString()} ]"; + + public static new ScriptStep FromXml(XElement step) + { + var enabled = step.Attribute("enable")?.Value != "False"; + var fileEl = step.Element("FileReference"); + var file = fileEl is not null ? FileReference.FromXml(fileEl) : null; + return new CloseFileStep(file, enabled); + } + + public static ScriptStep FromDisplayParams(bool enabled, string[] hrParams) + { + FileReference? file = null; + foreach (var tok in hrParams) + { + var t = tok.Trim(); + if (t.Equals("Current File", System.StringComparison.OrdinalIgnoreCase) || string.IsNullOrEmpty(t)) continue; + file = FileReference.FromDisplayToken(t); + break; + } + return new CloseFileStep(file, enabled); + } + + public static StepMetadata Metadata { get; } = new() + { + Name = XmlName, + Id = XmlId, + Category = "files", + HelpUrl = "https://help.claris.com/en/pro-help/content/close-file.html", + Params = + [ + new ParamMetadata + { + Name = "FileReference", + XmlElement = "FileReference", + Type = "fileReference", + }, + ], + FromXml = FromXml, + FromDisplay = FromDisplayParams, + }; +} diff --git a/src/SharpFM.Model/Scripting/Steps/GetDataFilePositionStep.cs b/src/SharpFM.Model/Scripting/Steps/GetDataFilePositionStep.cs new file mode 100644 index 0000000..ddbf52f --- /dev/null +++ b/src/SharpFM.Model/Scripting/Steps/GetDataFilePositionStep.cs @@ -0,0 +1,82 @@ +using System; +using System.Xml.Linq; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Values; + +namespace SharpFM.Model.Scripting.Steps; + +public sealed class GetDataFilePositionStep : ScriptStep, IStepFactory +{ + public const int XmlId = 194; + public const string XmlName = "Get Data File Position"; + + public Calculation FileId { get; set; } + public FieldRef? Target { get; set; } + + public GetDataFilePositionStep(Calculation? fileId = null, FieldRef? target = null, bool enabled = true) + : base(null, enabled) + { + FileId = fileId ?? new Calculation(""); + Target = target; + } + + public override XElement ToXml() + { + var step = new XElement("Step", + new XAttribute("enable", Enabled ? "True" : "False"), + new XAttribute("id", XmlId), + new XAttribute("name", XmlName), + FileId.ToXml("Calculation")); + if (Target is not null) step.Add(Target.ToXml("Field")); + return step; + } + + public override string ToDisplayLine() => + Target is null + ? $"Get Data File Position [ File ID: {FileId.Text} ]" + : $"Get Data File Position [ File ID: {FileId.Text} ; Target: {Target.ToDisplayString()} ]"; + + public static new ScriptStep FromXml(XElement step) + { + var enabled = step.Attribute("enable")?.Value != "False"; + var calcEl = step.Element("Calculation"); + var fileId = calcEl is not null ? Calculation.FromXml(calcEl) : new Calculation(""); + var fieldEl = step.Element("Field"); + var target = fieldEl is not null ? FieldRef.FromXml(fieldEl) : null; + return new GetDataFilePositionStep(fileId, target, enabled); + } + + public static ScriptStep FromDisplayParams(bool enabled, string[] hrParams) + { + Calculation fileId = new(""); + FieldRef? target = null; + foreach (var tok in hrParams) + { + var t = tok.Trim(); + if (t.StartsWith("File ID:", StringComparison.OrdinalIgnoreCase)) + { + fileId = new Calculation(t.Substring(8).Trim()); + } + else if (t.StartsWith("Target:", StringComparison.OrdinalIgnoreCase)) + { + target = FieldRef.FromDisplayToken(t.Substring(7).Trim()); + } + } + return new GetDataFilePositionStep(fileId, target, enabled); + } + + public static StepMetadata Metadata { get; } = new() + { + Name = XmlName, + Id = XmlId, + Category = "files", + HelpUrl = "https://help.claris.com/en/pro-help/content/get-data-file-position.html", + Params = + [ + new ParamMetadata { Name = "Calculation", XmlElement = "Calculation", Type = "calculation", HrLabel = "File ID", Required = true }, + new ParamMetadata { Name = "Field", XmlElement = "Field", Type = "field", HrLabel = "Target" }, + ], + FromXml = FromXml, + FromDisplay = FromDisplayParams, + }; +} diff --git a/src/SharpFM.Model/Scripting/Steps/GetFileExistsStep.cs b/src/SharpFM.Model/Scripting/Steps/GetFileExistsStep.cs new file mode 100644 index 0000000..c0208c1 --- /dev/null +++ b/src/SharpFM.Model/Scripting/Steps/GetFileExistsStep.cs @@ -0,0 +1,83 @@ +using System; +using System.Xml.Linq; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Values; + +namespace SharpFM.Model.Scripting.Steps; + +public sealed class GetFileExistsStep : ScriptStep, IStepFactory +{ + public const int XmlId = 188; + public const string XmlName = "Get File Exists"; + + public string Path { get; set; } + public FieldRef? Target { get; set; } + + public GetFileExistsStep(string path = "", FieldRef? target = null, bool enabled = true) + : base(null, enabled) + { + Path = path; + Target = target; + } + + public override XElement ToXml() + { + var step = new XElement("Step", + new XAttribute("enable", Enabled ? "True" : "False"), + new XAttribute("id", XmlId), + new XAttribute("name", XmlName), + new XElement("UniversalPathList", Path)); + if (Target is not null) step.Add(Target.ToXml("Field")); + return step; + } + + public override string ToDisplayLine() => + Target is null + ? $"Get File Exists [ {Path} ]" + : $"Get File Exists [ {Path} ; Target: {Target.ToDisplayString()} ]"; + + public static new ScriptStep FromXml(XElement step) + { + var enabled = step.Attribute("enable")?.Value != "False"; + var path = step.Element("UniversalPathList")?.Value ?? ""; + var fieldEl = step.Element("Field"); + var target = fieldEl is not null ? FieldRef.FromXml(fieldEl) : null; + return new GetFileExistsStep(path, target, enabled); + } + + public static ScriptStep FromDisplayParams(bool enabled, string[] hrParams) + { + string path = ""; + FieldRef? target = null; + bool pathSeen = false; + foreach (var tok in hrParams) + { + var t = tok.Trim(); + if (t.StartsWith("Target:", StringComparison.OrdinalIgnoreCase)) + { + target = FieldRef.FromDisplayToken(t.Substring(7).Trim()); + } + else if (!pathSeen && !string.IsNullOrWhiteSpace(t)) + { + path = t; + pathSeen = true; + } + } + return new GetFileExistsStep(path, target, enabled); + } + + public static StepMetadata Metadata { get; } = new() + { + Name = XmlName, + Id = XmlId, + Category = "files", + HelpUrl = "https://help.claris.com/en/pro-help/content/get-file-exists.html", + Params = + [ + new ParamMetadata { Name = "UniversalPathList", XmlElement = "UniversalPathList", Type = "text", Required = true }, + new ParamMetadata { Name = "Field", XmlElement = "Field", Type = "field", HrLabel = "Target" }, + ], + FromXml = FromXml, + FromDisplay = FromDisplayParams, + }; +} diff --git a/src/SharpFM.Model/Scripting/Steps/GetFileSizeStep.cs b/src/SharpFM.Model/Scripting/Steps/GetFileSizeStep.cs new file mode 100644 index 0000000..db49b7d --- /dev/null +++ b/src/SharpFM.Model/Scripting/Steps/GetFileSizeStep.cs @@ -0,0 +1,83 @@ +using System; +using System.Xml.Linq; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Values; + +namespace SharpFM.Model.Scripting.Steps; + +public sealed class GetFileSizeStep : ScriptStep, IStepFactory +{ + public const int XmlId = 189; + public const string XmlName = "Get File Size"; + + public string Path { get; set; } + public FieldRef? Target { get; set; } + + public GetFileSizeStep(string path = "", FieldRef? target = null, bool enabled = true) + : base(null, enabled) + { + Path = path; + Target = target; + } + + public override XElement ToXml() + { + var step = new XElement("Step", + new XAttribute("enable", Enabled ? "True" : "False"), + new XAttribute("id", XmlId), + new XAttribute("name", XmlName), + new XElement("UniversalPathList", Path)); + if (Target is not null) step.Add(Target.ToXml("Field")); + return step; + } + + public override string ToDisplayLine() => + Target is null + ? $"Get File Size [ {Path} ]" + : $"Get File Size [ {Path} ; Target: {Target.ToDisplayString()} ]"; + + public static new ScriptStep FromXml(XElement step) + { + var enabled = step.Attribute("enable")?.Value != "False"; + var path = step.Element("UniversalPathList")?.Value ?? ""; + var fieldEl = step.Element("Field"); + var target = fieldEl is not null ? FieldRef.FromXml(fieldEl) : null; + return new GetFileSizeStep(path, target, enabled); + } + + public static ScriptStep FromDisplayParams(bool enabled, string[] hrParams) + { + string path = ""; + FieldRef? target = null; + bool pathSeen = false; + foreach (var tok in hrParams) + { + var t = tok.Trim(); + if (t.StartsWith("Target:", StringComparison.OrdinalIgnoreCase)) + { + target = FieldRef.FromDisplayToken(t.Substring(7).Trim()); + } + else if (!pathSeen && !string.IsNullOrWhiteSpace(t)) + { + path = t; + pathSeen = true; + } + } + return new GetFileSizeStep(path, target, enabled); + } + + public static StepMetadata Metadata { get; } = new() + { + Name = XmlName, + Id = XmlId, + Category = "files", + HelpUrl = "https://help.claris.com/en/pro-help/content/get-file-size.html", + Params = + [ + new ParamMetadata { Name = "UniversalPathList", XmlElement = "UniversalPathList", Type = "text", Required = true }, + new ParamMetadata { Name = "Field", XmlElement = "Field", Type = "field", HrLabel = "Target" }, + ], + FromXml = FromXml, + FromDisplay = FromDisplayParams, + }; +} diff --git a/src/SharpFM.Model/Scripting/Steps/InstallMenuSetStep.cs b/src/SharpFM.Model/Scripting/Steps/InstallMenuSetStep.cs new file mode 100644 index 0000000..d68e5c9 --- /dev/null +++ b/src/SharpFM.Model/Scripting/Steps/InstallMenuSetStep.cs @@ -0,0 +1,90 @@ +using System; +using System.Xml.Linq; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Values; + +namespace SharpFM.Model.Scripting.Steps; + +public sealed class InstallMenuSetStep : ScriptStep, IStepFactory +{ + public const int XmlId = 142; + public const string XmlName = "Install Menu Set"; + + public NamedRef MenuSet { get; set; } + public bool UseAsFileDefault { get; set; } + + public InstallMenuSetStep(NamedRef? menuSet = null, bool useAsFileDefault = false, bool enabled = true) + : base(null, enabled) + { + MenuSet = menuSet ?? new NamedRef(0, ""); + UseAsFileDefault = useAsFileDefault; + } + + public override XElement ToXml() => + new("Step", + new XAttribute("enable", Enabled ? "True" : "False"), + new XAttribute("id", XmlId), + new XAttribute("name", XmlName), + new XElement("UseAsFileDefault", new XAttribute("state", UseAsFileDefault ? "True" : "False")), + MenuSet.ToXml("CustomMenuSet")); + + public override string ToDisplayLine() => + $"Install Menu Set [ \"{MenuSet.Name}\" ; Use as file default: {(UseAsFileDefault ? "On" : "Off")} ]"; + + public static new ScriptStep FromXml(XElement step) + { + var enabled = step.Attribute("enable")?.Value != "False"; + var useDefault = step.Element("UseAsFileDefault")?.Attribute("state")?.Value == "True"; + var menuEl = step.Element("CustomMenuSet"); + var menu = menuEl is not null ? NamedRef.FromXml(menuEl) : new NamedRef(0, ""); + return new InstallMenuSetStep(menu, useDefault, enabled); + } + + public static ScriptStep FromDisplayParams(bool enabled, string[] hrParams) + { + NamedRef menu = new(0, ""); + bool useDefault = false; + bool menuSeen = false; + foreach (var tok in hrParams) + { + var t = tok.Trim(); + if (t.StartsWith("Use as file default:", StringComparison.OrdinalIgnoreCase)) + { + useDefault = t.Substring(20).Trim().Equals("On", StringComparison.OrdinalIgnoreCase); + } + else if (!menuSeen && !string.IsNullOrWhiteSpace(t)) + { + var name = t; + if (name.StartsWith("\"") && name.EndsWith("\"") && name.Length >= 2) + name = name.Substring(1, name.Length - 2); + menu = new NamedRef(0, name); + menuSeen = true; + } + } + return new InstallMenuSetStep(menu, useDefault, enabled); + } + + public static StepMetadata Metadata { get; } = new() + { + Name = XmlName, + Id = XmlId, + Category = "miscellaneous", + HelpUrl = "https://help.claris.com/en/pro-help/content/install-menu-set.html", + Params = + [ + new ParamMetadata { Name = "CustomMenuSet", XmlElement = "CustomMenuSet", Type = "menuSet", Required = true }, + new ParamMetadata + { + Name = "UseAsFileDefault", + XmlElement = "UseAsFileDefault", + XmlAttr = "state", + Type = "boolean", + HrLabel = "Use as file default", + ValidValues = ["On", "Off"], + DefaultValue = "False", + }, + ], + FromXml = FromXml, + FromDisplay = FromDisplayParams, + }; +} diff --git a/src/SharpFM.Model/Scripting/Steps/InstallOnTimerScriptStep.cs b/src/SharpFM.Model/Scripting/Steps/InstallOnTimerScriptStep.cs new file mode 100644 index 0000000..a5b4af7 --- /dev/null +++ b/src/SharpFM.Model/Scripting/Steps/InstallOnTimerScriptStep.cs @@ -0,0 +1,95 @@ +using System; +using System.Xml.Linq; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Values; + +namespace SharpFM.Model.Scripting.Steps; + +/// +/// Install OnTimer Script carries a Script ref and an optional Interval +/// calculation wrapped in an <Interval> element. Omitting +/// both cancels any running timer on the current window. +/// +public sealed class InstallOnTimerScriptStep : ScriptStep, IStepFactory +{ + public const int XmlId = 148; + public const string XmlName = "Install OnTimer Script"; + + public NamedRef? Script { get; set; } + public Calculation? Interval { get; set; } + + public InstallOnTimerScriptStep(NamedRef? script = null, Calculation? interval = null, bool enabled = true) + : base(null, enabled) + { + Script = script; + Interval = interval; + } + + public override XElement ToXml() + { + var step = new XElement("Step", + new XAttribute("enable", Enabled ? "True" : "False"), + new XAttribute("id", XmlId), + new XAttribute("name", XmlName)); + if (Script is not null) step.Add(Script.ToXml("Script")); + if (Interval is not null) step.Add(new XElement("Interval", Interval.ToXml("Calculation"))); + return step; + } + + public override string ToDisplayLine() + { + var script = Script is null ? "" : $"\"{Script.Name}\""; + return Interval is null + ? $"Install OnTimer Script [ {script} ]" + : $"Install OnTimer Script [ {script} ; Interval: {Interval.Text} ]"; + } + + public static new ScriptStep FromXml(XElement step) + { + var enabled = step.Attribute("enable")?.Value != "False"; + var scriptEl = step.Element("Script"); + var script = scriptEl is not null ? NamedRef.FromXml(scriptEl) : null; + var intervalEl = step.Element("Interval")?.Element("Calculation"); + var interval = intervalEl is not null ? Calculation.FromXml(intervalEl) : null; + return new InstallOnTimerScriptStep(script, interval, enabled); + } + + public static ScriptStep FromDisplayParams(bool enabled, string[] hrParams) + { + NamedRef? script = null; + Calculation? interval = null; + bool scriptSeen = false; + foreach (var tok in hrParams) + { + var t = tok.Trim(); + if (t.StartsWith("Interval:", StringComparison.OrdinalIgnoreCase)) + { + interval = new Calculation(t.Substring(9).Trim()); + } + else if (!scriptSeen && !string.IsNullOrWhiteSpace(t) && t != "") + { + var name = t; + if (name.StartsWith("\"") && name.EndsWith("\"") && name.Length >= 2) + name = name.Substring(1, name.Length - 2); + script = new NamedRef(0, name); + scriptSeen = true; + } + } + return new InstallOnTimerScriptStep(script, interval, enabled); + } + + public static StepMetadata Metadata { get; } = new() + { + Name = XmlName, + Id = XmlId, + Category = "control", + HelpUrl = "https://help.claris.com/en/pro-help/content/install-ontimer-script.html", + Params = + [ + new ParamMetadata { Name = "Script", XmlElement = "Script", Type = "script" }, + new ParamMetadata { Name = "Calculation", XmlElement = "Calculation", Type = "namedCalc", HrLabel = "Interval" }, + ], + FromXml = FromXml, + FromDisplay = FromDisplayParams, + }; +} diff --git a/src/SharpFM.Model/Scripting/Steps/OpenDataFileStep.cs b/src/SharpFM.Model/Scripting/Steps/OpenDataFileStep.cs new file mode 100644 index 0000000..e505e6a --- /dev/null +++ b/src/SharpFM.Model/Scripting/Steps/OpenDataFileStep.cs @@ -0,0 +1,95 @@ +using System; +using System.Xml.Linq; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Values; + +namespace SharpFM.Model.Scripting.Steps; + +public sealed class OpenDataFileStep : ScriptStep, IStepFactory +{ + public const int XmlId = 191; + public const string XmlName = "Open Data File"; + + public string Path { get; set; } + public FieldRef? Target { get; set; } + + public OpenDataFileStep(string path = "", FieldRef? target = null, bool enabled = true) + : base(null, enabled) + { + Path = path; + Target = target; + } + + public override XElement ToXml() + { + var step = new XElement("Step", + new XAttribute("enable", Enabled ? "True" : "False"), + new XAttribute("id", XmlId), + new XAttribute("name", XmlName), + new XElement("UniversalPathList", Path)); + if (Target is not null) step.Add(Target.ToXml("Field")); + return step; + } + + public override string ToDisplayLine() => + Target is null + ? $"Open Data File [ {Path} ]" + : $"Open Data File [ {Path} ; Target: {Target.ToDisplayString()} ]"; + + public static new ScriptStep FromXml(XElement step) + { + var enabled = step.Attribute("enable")?.Value != "False"; + var path = step.Element("UniversalPathList")?.Value ?? ""; + var fieldEl = step.Element("Field"); + var target = fieldEl is not null ? FieldRef.FromXml(fieldEl) : null; + return new OpenDataFileStep(path, target, enabled); + } + + public static ScriptStep FromDisplayParams(bool enabled, string[] hrParams) + { + string path = ""; + FieldRef? target = null; + bool pathSeen = false; + foreach (var tok in hrParams) + { + var t = tok.Trim(); + if (t.StartsWith("Target:", StringComparison.OrdinalIgnoreCase)) + { + target = FieldRef.FromDisplayToken(t.Substring(7).Trim()); + } + else if (!pathSeen && !string.IsNullOrWhiteSpace(t)) + { + path = t; + pathSeen = true; + } + } + return new OpenDataFileStep(path, target, enabled); + } + + public static StepMetadata Metadata { get; } = new() + { + Name = XmlName, + Id = XmlId, + Category = "files", + HelpUrl = "https://help.claris.com/en/pro-help/content/open-data-file.html", + Params = + [ + new ParamMetadata + { + Name = "UniversalPathList", + XmlElement = "UniversalPathList", + Type = "text", + Required = true, + }, + new ParamMetadata + { + Name = "Field", + XmlElement = "Field", + Type = "field", + HrLabel = "Target", + }, + ], + FromXml = FromXml, + FromDisplay = FromDisplayParams, + }; +} diff --git a/src/SharpFM.Model/Scripting/Steps/OpenFileStep.cs b/src/SharpFM.Model/Scripting/Steps/OpenFileStep.cs new file mode 100644 index 0000000..2e51d58 --- /dev/null +++ b/src/SharpFM.Model/Scripting/Steps/OpenFileStep.cs @@ -0,0 +1,108 @@ +using System; +using System.Xml.Linq; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Values; + +namespace SharpFM.Model.Scripting.Steps; + +public sealed class OpenFileStep : ScriptStep, IStepFactory +{ + public const int XmlId = 33; + public const string XmlName = "Open File"; + + public bool OpenHidden { get; set; } + public FileReference? File { get; set; } + + public OpenFileStep(bool openHidden = false, FileReference? file = null, bool enabled = true) + : base(null, enabled) + { + OpenHidden = openHidden; + File = file; + } + + public override XElement ToXml() + { + var step = new XElement("Step", + new XAttribute("enable", Enabled ? "True" : "False"), + new XAttribute("id", XmlId), + new XAttribute("name", XmlName), + new XElement("Option", new XAttribute("state", OpenHidden ? "True" : "False"))); + if (File is not null) step.Add(File.ToXml()); + return step; + } + + public override string ToDisplayLine() + { + var hidden = "Open hidden: " + (OpenHidden ? "On" : "Off"); + return File is null + ? $"Open File [ {hidden} ]" + : $"Open File [ {hidden} ; {File.ToDisplayString()} ]"; + } + + public static new ScriptStep FromXml(XElement step) + { + var enabled = step.Attribute("enable")?.Value != "False"; + var hidden = step.Element("Option")?.Attribute("state")?.Value == "True"; + var fileEl = step.Element("FileReference"); + var file = fileEl is not null ? FileReference.FromXml(fileEl) : null; + return new OpenFileStep(hidden, file, enabled); + } + + public static ScriptStep FromDisplayParams(bool enabled, string[] hrParams) + { + bool hidden = false; + FileReference? file = null; + foreach (var tok in hrParams) + { + var t = tok.Trim(); + if (t.StartsWith("Open hidden:", StringComparison.OrdinalIgnoreCase)) + { + hidden = t.Substring(12).Trim().Equals("On", StringComparison.OrdinalIgnoreCase); + } + else if (!string.IsNullOrWhiteSpace(t)) + { + file = FileReference.FromDisplayToken(t); + } + } + return new OpenFileStep(hidden, file, enabled); + } + + public static StepMetadata Metadata { get; } = new() + { + Name = XmlName, + Id = XmlId, + Category = "files", + HelpUrl = "https://help.claris.com/en/pro-help/content/open-file.html", + Params = + [ + new ParamMetadata + { + Name = "Option", + XmlElement = "Option", + XmlAttr = "state", + Type = "boolean", + HrLabel = "Open hidden", + ValidValues = ["On", "Off"], + DefaultValue = "False", + }, + new ParamMetadata + { + Name = "FileReference", + XmlElement = "FileReference", + Type = "fileReference", + }, + ], + Notes = new StepNotes + { + Behavioral = "Opens a FileMaker file or reestablishes the link to an ODBC data source. Script steps after Open File execute in the file containing the script, not the opened file.", + Constraints = "Cannot open a file from an unauthorized (unlinked) file.", + Platform = new StepPlatformNotes + { + WebDirect = "Not supported.", + Server = "Not supported.", + }, + }, + FromXml = FromXml, + FromDisplay = FromDisplayParams, + }; +} diff --git a/src/SharpFM.Model/Scripting/Steps/TruncateTableStep.cs b/src/SharpFM.Model/Scripting/Steps/TruncateTableStep.cs new file mode 100644 index 0000000..78af4d0 --- /dev/null +++ b/src/SharpFM.Model/Scripting/Steps/TruncateTableStep.cs @@ -0,0 +1,115 @@ +using System; +using System.Xml.Linq; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Values; + +namespace SharpFM.Model.Scripting.Steps; + +/// +/// Truncate Table carries a NoInteract boolean (HR-inverted: +/// state="True" means With dialog: Off) and a BaseTable +/// named reference. The BaseTable can also have an optional comment +/// attribute in FM Pro output which we preserve. +/// +public sealed class TruncateTableStep : ScriptStep, IStepFactory +{ + public const int XmlId = 182; + public const string XmlName = "Truncate Table"; + + public bool WithDialog { get; set; } + public NamedRef Table { get; set; } + public string? TableComment { get; set; } + + public TruncateTableStep(bool withDialog = true, NamedRef? table = null, string? tableComment = null, bool enabled = true) + : base(null, enabled) + { + WithDialog = withDialog; + Table = table ?? new NamedRef(-1, ""); + TableComment = tableComment; + } + + public override XElement ToXml() + { + var baseTable = new XElement("BaseTable", + new XAttribute("id", Table.Id)); + if (TableComment is not null) + baseTable.Add(new XAttribute("comment", TableComment)); + baseTable.Add(new XAttribute("name", Table.Name)); + + return new XElement("Step", + new XAttribute("enable", Enabled ? "True" : "False"), + new XAttribute("id", XmlId), + new XAttribute("name", XmlName), + new XElement("NoInteract", new XAttribute("state", WithDialog ? "True" : "False")), + baseTable); + } + + public override string ToDisplayLine() => + $"Truncate Table [ With dialog: {(WithDialog ? "On" : "Off")} ; Table: {Table.Name} ]"; + + public static new ScriptStep FromXml(XElement step) + { + var enabled = step.Attribute("enable")?.Value != "False"; + var withDialog = step.Element("NoInteract")?.Attribute("state")?.Value == "True"; + var tableEl = step.Element("BaseTable"); + var table = tableEl is not null ? NamedRef.FromXml(tableEl) : new NamedRef(-1, ""); + var comment = tableEl?.Attribute("comment")?.Value; + return new TruncateTableStep(withDialog, table, comment, enabled); + } + + public static ScriptStep FromDisplayParams(bool enabled, string[] hrParams) + { + bool withDialog = true; + NamedRef? table = null; + foreach (var tok in hrParams) + { + var t = tok.Trim(); + if (t.StartsWith("With dialog:", StringComparison.OrdinalIgnoreCase)) + { + withDialog = t.Substring(12).Trim().Equals("On", StringComparison.OrdinalIgnoreCase); + } + else if (t.StartsWith("Table:", StringComparison.OrdinalIgnoreCase)) + { + var name = t.Substring(6).Trim(); + table = new NamedRef(0, name); + } + } + return new TruncateTableStep(withDialog, table, null, enabled); + } + + public static StepMetadata Metadata { get; } = new() + { + Name = XmlName, + Id = XmlId, + Category = "records", + HelpUrl = "https://help.claris.com/en/pro-help/content/truncate-table.html", + Params = + [ + new ParamMetadata + { + Name = "NoInteract", + XmlElement = "NoInteract", + XmlAttr = "state", + Type = "boolean", + HrLabel = "With dialog", + ValidValues = ["On", "Off"], + DefaultValue = "True", + }, + new ParamMetadata + { + Name = "BaseTable", + XmlElement = "BaseTable", + Type = "tableReference", + HrLabel = "Table", + Required = true, + }, + ], + Notes = new StepNotes + { + Behavioral = "Deletes all records in specified source table regardless of current found set. Cannot undo.", + Gotchas = "Does NOT delete related records even if relationship is set up to do so.", + }, + FromXml = FromXml, + FromDisplay = FromDisplayParams, + }; +} diff --git a/src/SharpFM.Model/Scripting/Values/FileReference.cs b/src/SharpFM.Model/Scripting/Values/FileReference.cs new file mode 100644 index 0000000..deb99b4 --- /dev/null +++ b/src/SharpFM.Model/Scripting/Values/FileReference.cs @@ -0,0 +1,50 @@ +using System.Xml.Linq; + +namespace SharpFM.Model.Scripting.Values; + +/// +/// A FileMaker file or ODBC data source reference used by Open File and +/// Close File. Shape: <FileReference id="0" name=""><UniversalPathList>file:Name</UniversalPathList></FileReference>. +/// +/// All three components — id, name, and the path list — round-trip +/// losslessly. The path list is a semicolon-delimited platform-agnostic +/// list (e.g. file:Foo;filewin:C:/.../Foo.fmp12); we preserve it +/// as an opaque string since FM's compositing of it is version-specific +/// and not documented. +/// +public sealed record FileReference(int Id, string Name, string Paths) +{ + public XElement ToXml(string elementName = "FileReference") => + new(elementName, + new XAttribute("id", Id), + new XAttribute("name", Name), + new XElement("UniversalPathList", Paths)); + + public static FileReference FromXml(XElement element) + { + var idStr = element.Attribute("id")?.Value; + var id = int.TryParse(idStr, out var parsed) ? parsed : 0; + var name = element.Attribute("name")?.Value ?? ""; + var paths = element.Element("UniversalPathList")?.Value ?? ""; + return new FileReference(id, name, paths); + } + + /// + /// Display rendering: "Name" when the name is non-empty, otherwise + /// the first path in the UniversalPathList. Matches FM Pro's rendering + /// which prefers the human-readable file name. + /// + public string ToDisplayString() + { + if (!string.IsNullOrEmpty(Name)) return $"\"{Name}\""; + return string.IsNullOrEmpty(Paths) ? "\"\"" : $"\"{Paths}\""; + } + + public static FileReference FromDisplayToken(string token) + { + var t = token.Trim(); + if (t.StartsWith("\"") && t.EndsWith("\"") && t.Length >= 2) + t = t.Substring(1, t.Length - 2); + return new FileReference(0, t, ""); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/CloseFileStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/CloseFileStepTests.cs new file mode 100644 index 0000000..7508958 --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/CloseFileStepTests.cs @@ -0,0 +1,44 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +public class CloseFileStepTests +{ + private const string CanonicalXml = """ + file:NameOfFile + """; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = CloseFileStep.Metadata.FromXml!(source); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Display_BareStep_UsesCurrentFile() + { + var bare = XElement.Parse(""); + var step = CloseFileStep.Metadata.FromXml!(bare); + Assert.Equal("Close File [ Current File ]", step.ToDisplayLine()); + } + + [Fact] + public void Display_WithFileReference_UsesName() + { + var step = (CloseFileStep)CloseFileStep.Metadata.FromXml!(XElement.Parse( + "file:Employees")); + Assert.Equal("Close File [ \"Employees\" ]", step.ToDisplayLine()); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Close File", out var metadata)); + Assert.Equal(34, metadata!.Id); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/GetDataFilePositionStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/GetDataFilePositionStepTests.cs new file mode 100644 index 0000000..68d8829 --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/GetDataFilePositionStepTests.cs @@ -0,0 +1,35 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +public class GetDataFilePositionStepTests +{ + private const string CanonicalXml = """ + + """; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = GetDataFilePositionStep.Metadata.FromXml!(source); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Display_EmitsFileIdAndTarget() + { + var step = (GetDataFilePositionStep)GetDataFilePositionStep.Metadata.FromXml!(XElement.Parse(CanonicalXml)); + Assert.Equal("Get Data File Position [ File ID: $handle ; Target: Data::pos (#1) ]", step.ToDisplayLine()); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Get Data File Position", out var metadata)); + Assert.Equal(194, metadata!.Id); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/GetFileExistsStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/GetFileExistsStepTests.cs new file mode 100644 index 0000000..7e5b234 --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/GetFileExistsStepTests.cs @@ -0,0 +1,35 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +public class GetFileExistsStepTests +{ + private const string CanonicalXml = """ + $path + """; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = GetFileExistsStep.Metadata.FromXml!(source); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Display_EmitsPathAndTarget() + { + var step = (GetFileExistsStep)GetFileExistsStep.Metadata.FromXml!(XElement.Parse(CanonicalXml)); + Assert.Equal("Get File Exists [ $path ; Target: Results::exists (#5) ]", step.ToDisplayLine()); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Get File Exists", out var metadata)); + Assert.Equal(188, metadata!.Id); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/GetFileSizeStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/GetFileSizeStepTests.cs new file mode 100644 index 0000000..d0ee637 --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/GetFileSizeStepTests.cs @@ -0,0 +1,35 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +public class GetFileSizeStepTests +{ + private const string CanonicalXml = """ + $path + """; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = GetFileSizeStep.Metadata.FromXml!(source); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Display_EmitsPathAndTarget() + { + var step = (GetFileSizeStep)GetFileSizeStep.Metadata.FromXml!(XElement.Parse(CanonicalXml)); + Assert.Equal("Get File Size [ $path ; Target: Results::size (#6) ]", step.ToDisplayLine()); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Get File Size", out var metadata)); + Assert.Equal(189, metadata!.Id); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/InstallMenuSetStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/InstallMenuSetStepTests.cs new file mode 100644 index 0000000..e82083e --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/InstallMenuSetStepTests.cs @@ -0,0 +1,35 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +public class InstallMenuSetStepTests +{ + private const string CanonicalXml = """ + + """; + + [Fact] + public void RoundTrip_CanonicalXml_IsPreserved() + { + var source = XElement.Parse(CanonicalXml); + var step = InstallMenuSetStep.Metadata.FromXml!(source); + Assert.True(XNode.DeepEquals(source, step.ToXml())); + } + + [Fact] + public void Display_EmitsMenuSetAndFlag() + { + var step = (InstallMenuSetStep)InstallMenuSetStep.Metadata.FromXml!(XElement.Parse(CanonicalXml)); + Assert.Equal("Install Menu Set [ \"[Standard FileMaker Menus]\" ; Use as file default: Off ]", step.ToDisplayLine()); + } + + [Fact] + public void Registry_HasStep() + { + Assert.True(StepRegistry.ByName.TryGetValue("Install Menu Set", out var metadata)); + Assert.Equal(142, metadata!.Id); + } +} diff --git a/tests/SharpFM.Tests/Scripting/Steps/InstallOnTimerScriptStepTests.cs b/tests/SharpFM.Tests/Scripting/Steps/InstallOnTimerScriptStepTests.cs new file mode 100644 index 0000000..bcedaa6 --- /dev/null +++ b/tests/SharpFM.Tests/Scripting/Steps/InstallOnTimerScriptStepTests.cs @@ -0,0 +1,35 @@ +using System.Xml.Linq; +using SharpFM.Model.Scripting.Registry; +using SharpFM.Model.Scripting.Steps; +using Xunit; + +namespace SharpFM.Tests.Scripting.Steps; + +public class InstallOnTimerScriptStepTests +{ + private const string CanonicalXml = """ +