Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ public static class ComponentScaffold
["FormParameter"] = ApplyFormParameter,
["FormEventHandler"] = ApplyFormEventHandler,
["ControlParameter"] = ApplyControlParameter,
["Role"] = ApplySecurityRole,
};

public static IReadOnlyCollection<string> SupportedComponentTypes => Appliers.Keys;
Expand Down Expand Up @@ -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"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using TALXIS.Platform.Metadata.Solutions;

namespace TALXIS.Platform.Metadata.Serialization.Xml.Scaffolding;

/// <summary>
/// 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.
/// </summary>
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();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
namespace TALXIS.Platform.Metadata.Serialization.Xml.Scaffolding;

/// <summary>
/// Input for <see cref="SecurityRoleScaffold"/>: the solution to patch and the
/// rendered role to register in its manifest.
/// </summary>
public sealed class SecurityRoleScaffoldRequest
{
/// <summary>
/// Folder containing the unpacked solution files (Other/, Entities/, OptionSets/).
/// </summary>
public string SolutionRootPath { get; set; } = "";

/// <summary>
/// GUID of the rendered security role, without braces.
/// </summary>
public string RoleId { get; set; } = "";
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
65 changes: 65 additions & 0 deletions tests/TALXIS.Platform.Metadata.Tests/SecurityRoleScaffoldTests.cs
Original file line number Diff line number Diff line change
@@ -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, """
<ImportExportXml><SolutionManifest><UniqueName>udpp_Sandbox</UniqueName><RootComponents /></SolutionManifest></ImportExportXml>
""");
}

public void Dispose() => Directory.Delete(_root, recursive: true);

private IEnumerable<XElement> 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<string, string> { ["role-id"] = "{" + RoleId + "}" },
});

var node = RootComponentNodes().Single();
Assert.Equal("20", node.Attribute("type")?.Value);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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, """
<?xml version="1.0" encoding="utf-8"?>
<Role id="{c1b2c3d4-e5f6-4a1b-8c2d-000000000002}" name="Example" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<IsCustomizable>1</IsCustomizable>
<RolePrivileges>
</RolePrivileges>
</Role>
""");
var relationshipsPath = Path.Combine(_root, "Other", "Relationships.xml");
File.WriteAllText(relationshipsPath, """
<?xml version="1.0" encoding="utf-8"?>
<EntityRelationships xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" />
""");
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, """
<ImportExportXml>
<SolutionManifest>
<UniqueName>udpp_Sandbox</UniqueName>
<RootComponents />
</SolutionManifest>
</ImportExportXml>
""");

SolutionRootComponentPatcher.EnsureRootComponent(_root, new RootComponent
{
Type = ComponentType.Role,
Id = RoleId,
});

var text = File.ReadAllText(_solutionXmlPath);
Assert.Contains("\n <RootComponent ", text.Replace("\r\n", "\n"));
Assert.Contains("\n </RootComponents>", text.Replace("\r\n", "\n"));
}

[Fact]
public void EnsureRootComponent_MissingManifest_Throws()
{
Expand Down