diff --git a/src/TALXIS.Platform.Metadata.Serialization.Xml/Scaffolding/ComponentScaffold.cs b/src/TALXIS.Platform.Metadata.Serialization.Xml/Scaffolding/ComponentScaffold.cs index 6932930..74d5e3f 100644 --- a/src/TALXIS.Platform.Metadata.Serialization.Xml/Scaffolding/ComponentScaffold.cs +++ b/src/TALXIS.Platform.Metadata.Serialization.Xml/Scaffolding/ComponentScaffold.cs @@ -24,6 +24,7 @@ public static class ComponentScaffold ["FormParameter"] = ApplyFormParameter, ["FormEventHandler"] = ApplyFormEventHandler, ["ControlParameter"] = ApplyControlParameter, + ["Role"] = ApplySecurityRole, }; public static IReadOnlyCollection SupportedComponentTypes => Appliers.Keys; @@ -203,6 +204,14 @@ private static ScaffoldResult ApplyControlParameter(ComponentScaffoldRequest req ParametersFilePath = RequiredFile(request, "parameters"), }); + // Parameters: role-id (required). + private static ScaffoldResult ApplySecurityRole(ComponentScaffoldRequest request) => + SecurityRoleScaffold.Apply(new SecurityRoleScaffoldRequest + { + SolutionRootPath = request.SolutionRootPath, + RoleId = RequiredParameter(request, "role-id").Trim('{', '}'), + }); + private static FormPlacement PlacementFrom(ComponentScaffoldRequest request) => new() { TabId = OptionalId(request.Parameters, "tab-id"), diff --git a/src/TALXIS.Platform.Metadata.Serialization.Xml/Scaffolding/Helpers/SolutionRootComponentPatcher.cs b/src/TALXIS.Platform.Metadata.Serialization.Xml/Scaffolding/Helpers/SolutionRootComponentPatcher.cs index 44dabae..407db9a 100644 --- a/src/TALXIS.Platform.Metadata.Serialization.Xml/Scaffolding/Helpers/SolutionRootComponentPatcher.cs +++ b/src/TALXIS.Platform.Metadata.Serialization.Xml/Scaffolding/Helpers/SolutionRootComponentPatcher.cs @@ -22,7 +22,9 @@ public static bool EnsureRootComponent(string solutionRootPath, RootComponent co if (solution.RootComponents.Any(rc => Matches(rc, component))) return false; solution.AddRootComponent(component); - new XmlWorkspaceWriter().Write(workspace, solutionRootPath); + // Only the manifest changed - a full workspace write would rewrite (and reformat) + // every other component file in the solution as collateral. + new XmlWorkspaceWriter().WriteSolutionManifest(workspace, solution.UniqueName, solutionRootPath); return true; } diff --git a/src/TALXIS.Platform.Metadata.Serialization.Xml/Scaffolding/SecurityRole/SecurityRoleScaffold.cs b/src/TALXIS.Platform.Metadata.Serialization.Xml/Scaffolding/SecurityRole/SecurityRoleScaffold.cs new file mode 100644 index 0000000..ca5b742 --- /dev/null +++ b/src/TALXIS.Platform.Metadata.Serialization.Xml/Scaffolding/SecurityRole/SecurityRoleScaffold.cs @@ -0,0 +1,22 @@ +using TALXIS.Platform.Metadata.Solutions; + +namespace TALXIS.Platform.Metadata.Serialization.Xml.Scaffolding; + +/// +/// In-process replacement for the pp-security-role template post-action script: +/// registers the rendered role in Solution.xml as a root component (type 20, by id). +/// Pilot of the SolutionRootComponentPatcher consumers - the applier only wires the patcher. +/// +public static class SecurityRoleScaffold +{ + public static ScaffoldResult Apply(SecurityRoleScaffoldRequest request) + { + SolutionRootComponentPatcher.EnsureRootComponent(request.SolutionRootPath, new RootComponent + { + Type = ComponentType.Role, + Id = Guid.Parse(request.RoleId), + Behavior = 0, + }); + return new ScaffoldResult(); + } +} diff --git a/src/TALXIS.Platform.Metadata.Serialization.Xml/Scaffolding/SecurityRole/SecurityRoleScaffoldRequest.cs b/src/TALXIS.Platform.Metadata.Serialization.Xml/Scaffolding/SecurityRole/SecurityRoleScaffoldRequest.cs new file mode 100644 index 0000000..485df83 --- /dev/null +++ b/src/TALXIS.Platform.Metadata.Serialization.Xml/Scaffolding/SecurityRole/SecurityRoleScaffoldRequest.cs @@ -0,0 +1,18 @@ +namespace TALXIS.Platform.Metadata.Serialization.Xml.Scaffolding; + +/// +/// Input for : the solution to patch and the +/// rendered role to register in its manifest. +/// +public sealed class SecurityRoleScaffoldRequest +{ + /// + /// Folder containing the unpacked solution files (Other/, Entities/, OptionSets/). + /// + public string SolutionRootPath { get; set; } = ""; + + /// + /// GUID of the rendered security role, without braces. + /// + public string RoleId { get; set; } = ""; +} diff --git a/src/TALXIS.Platform.Metadata.Serialization.Xml/XmlWorkspaceWriter.cs b/src/TALXIS.Platform.Metadata.Serialization.Xml/XmlWorkspaceWriter.cs index ced5066..f3654c6 100644 --- a/src/TALXIS.Platform.Metadata.Serialization.Xml/XmlWorkspaceWriter.cs +++ b/src/TALXIS.Platform.Metadata.Serialization.Xml/XmlWorkspaceWriter.cs @@ -2024,6 +2024,17 @@ private static void ReplaceChildElementsPreservingWhitespace(XElement parent, IE .Select(text => text.Value) .LastOrDefault(ContainsNewLine); + // A previously childless container has no whitespace pattern to mimic - + // derive it from the container's own indentation so first-time children + // come out on indented lines instead of one inline run. + if ((childIndent == null || closingIndent == null) + && parent.PreviousNode is XText parentIndentText + && ContainsNewLine(parentIndentText.Value)) + { + childIndent = parentIndentText.Value + " "; + closingIndent = parentIndentText.Value; + } + parent.RemoveNodes(); if (childIndent == null || closingIndent == null || replacements.Count == 0) diff --git a/tests/TALXIS.Platform.Metadata.Tests/SecurityRoleScaffoldTests.cs b/tests/TALXIS.Platform.Metadata.Tests/SecurityRoleScaffoldTests.cs new file mode 100644 index 0000000..ed7d48a --- /dev/null +++ b/tests/TALXIS.Platform.Metadata.Tests/SecurityRoleScaffoldTests.cs @@ -0,0 +1,65 @@ +using System.Xml.Linq; +using TALXIS.Platform.Metadata.Serialization.Xml.Scaffolding; + +namespace TALXIS.Platform.Metadata.Tests; + +public class SecurityRoleScaffoldTests : IDisposable +{ + private const string RoleId = "b1b2c3d4-e5f6-4a1b-8c2d-000000000020"; + + private readonly string _root = Directory.CreateTempSubdirectory("metadata-security-role-scaffold").FullName; + private readonly string _solutionXmlPath; + + public SecurityRoleScaffoldTests() + { + Directory.CreateDirectory(Path.Combine(_root, "Other")); + _solutionXmlPath = Path.Combine(_root, "Other", "Solution.xml"); + File.WriteAllText(_solutionXmlPath, """ + udpp_Sandbox + """); + } + + public void Dispose() => Directory.Delete(_root, recursive: true); + + private IEnumerable RootComponentNodes() => + XDocument.Load(_solutionXmlPath).Descendants("RootComponent"); + + [Fact] + public void Apply_RegistersRoleRootComponent() + { + SecurityRoleScaffold.Apply(new SecurityRoleScaffoldRequest + { + SolutionRootPath = _root, + RoleId = RoleId, + }); + + var node = RootComponentNodes().Single(); + Assert.Equal("20", node.Attribute("type")?.Value); + Assert.Contains(RoleId, node.Attribute("id")?.Value, StringComparison.OrdinalIgnoreCase); + Assert.Equal("0", node.Attribute("behavior")?.Value); + } + + [Fact] + public void Apply_SameRole_IsNotDuplicated() + { + var request = new SecurityRoleScaffoldRequest { SolutionRootPath = _root, RoleId = RoleId }; + SecurityRoleScaffold.Apply(request); + SecurityRoleScaffold.Apply(request); + + Assert.Single(RootComponentNodes()); + } + + [Fact] + public void Dispatcher_ResolvesSecurityRoleAliasAndBracedId() + { + ComponentScaffold.Apply(new ComponentScaffoldRequest + { + ComponentType = "SecurityRole", + SolutionRootPath = _root, + Parameters = new Dictionary { ["role-id"] = "{" + RoleId + "}" }, + }); + + var node = RootComponentNodes().Single(); + Assert.Equal("20", node.Attribute("type")?.Value); + } +} diff --git a/tests/TALXIS.Platform.Metadata.Tests/SolutionRootComponentPatcherTests.cs b/tests/TALXIS.Platform.Metadata.Tests/SolutionRootComponentPatcherTests.cs index 88b5de6..0c39723 100644 --- a/tests/TALXIS.Platform.Metadata.Tests/SolutionRootComponentPatcherTests.cs +++ b/tests/TALXIS.Platform.Metadata.Tests/SolutionRootComponentPatcherTests.cs @@ -73,6 +73,60 @@ public void EnsureRootComponent_AddsIdComponent() Assert.Single(RootComponentNodes()); } + [Fact] + public void EnsureRootComponent_DoesNotTouchSiblingFiles() + { + var rolePath = Path.Combine(_root, "Roles", "Example.xml"); + Directory.CreateDirectory(Path.Combine(_root, "Roles")); + File.WriteAllText(rolePath, """ + + + 1 + + + + """); + var relationshipsPath = Path.Combine(_root, "Other", "Relationships.xml"); + File.WriteAllText(relationshipsPath, """ + + + """); + var roleBytes = File.ReadAllBytes(rolePath); + var relationshipsBytes = File.ReadAllBytes(relationshipsPath); + + SolutionRootComponentPatcher.EnsureRootComponent(_root, new RootComponent + { + Type = ComponentType.Role, + Id = RoleId, + }); + + Assert.Equal(roleBytes, File.ReadAllBytes(rolePath)); + Assert.Equal(relationshipsBytes, File.ReadAllBytes(relationshipsPath)); + } + + [Fact] + public void EnsureRootComponent_IndentsFirstComponentInEmptyContainer() + { + File.WriteAllText(_solutionXmlPath, """ + + + udpp_Sandbox + + + + """); + + SolutionRootComponentPatcher.EnsureRootComponent(_root, new RootComponent + { + Type = ComponentType.Role, + Id = RoleId, + }); + + var text = File.ReadAllText(_solutionXmlPath); + Assert.Contains("\n ", text.Replace("\r\n", "\n")); + } + [Fact] public void EnsureRootComponent_MissingManifest_Throws() {