diff --git a/src/ScadaLink.CentralUI/Components/Pages/Design/TemplateEdit.razor b/src/ScadaLink.CentralUI/Components/Pages/Design/TemplateEdit.razor
index 1a14f48..6938ea3 100644
--- a/src/ScadaLink.CentralUI/Components/Pages/Design/TemplateEdit.razor
+++ b/src/ScadaLink.CentralUI/Components/Pages/Design/TemplateEdit.razor
@@ -212,13 +212,11 @@
-
+
+ @(_selectedTemplate.ParentTemplateId is int pid
+ ? _templates.FirstOrDefault(t => t.Id == pid)?.Name ?? $"#{pid}"
+ : "(none)")
+
diff --git a/src/ScadaLink.TemplateEngine/TemplateService.cs b/src/ScadaLink.TemplateEngine/TemplateService.cs
index 48ced9d..28df3cd 100644
--- a/src/ScadaLink.TemplateEngine/TemplateService.cs
+++ b/src/ScadaLink.TemplateEngine/TemplateService.cs
@@ -83,28 +83,16 @@ public class TemplateService
if (template == null)
return Result.Failure($"Template with ID {templateId} not found.");
- // Validate parent change
- if (parentTemplateId.HasValue && parentTemplateId.Value != (template.ParentTemplateId ?? 0))
+ // ParentTemplateId is immutable after creation — set once at create time.
+ // Reject any attempt to change it (null→value, value→null, or value→other).
+ if (parentTemplateId != template.ParentTemplateId)
{
- var parent = await _repository.GetTemplateByIdAsync(parentTemplateId.Value, cancellationToken);
- if (parent == null)
- return Result.Failure($"Parent template with ID {parentTemplateId.Value} not found.");
-
- // Check inheritance acyclicity
- var allTemplates = await _repository.GetAllTemplatesAsync(cancellationToken);
- var cycleError = CycleDetector.DetectInheritanceCycle(templateId, parentTemplateId.Value, allTemplates);
- if (cycleError != null)
- return Result.Failure(cycleError);
-
- // Check cross-graph cycle
- var crossCycleError = CycleDetector.DetectCrossGraphCycle(templateId, parentTemplateId, null, allTemplates);
- if (crossCycleError != null)
- return Result.Failure(crossCycleError);
+ return Result.Failure(
+ "Parent template cannot be changed after creation.");
}
template.Name = name;
template.Description = description;
- template.ParentTemplateId = parentTemplateId;
// Check for naming collisions after the change
var collisionResult = await ValidateCollisionsAsync(template, cancellationToken);
diff --git a/tests/ScadaLink.TemplateEngine.Tests/TemplateServiceTests.cs b/tests/ScadaLink.TemplateEngine.Tests/TemplateServiceTests.cs
index 8dd3533..d28f51c 100644
--- a/tests/ScadaLink.TemplateEngine.Tests/TemplateServiceTests.cs
+++ b/tests/ScadaLink.TemplateEngine.Tests/TemplateServiceTests.cs
@@ -468,35 +468,50 @@ public class TemplateServiceTests
// ========================================================================
[Fact]
- public async Task UpdateTemplate_InheritanceCycle_Fails()
+ public async Task UpdateTemplate_ChangeParent_Fails()
{
- var templateA = new Template("A") { Id = 1, ParentTemplateId = null };
- var templateB = new Template("B") { Id = 2, ParentTemplateId = 1 };
+ var parentA = new Template("A") { Id = 1 };
+ var child = new Template("Child") { Id = 2, ParentTemplateId = 1 };
+ _repoMock.Setup(r => r.GetTemplateByIdAsync(2, It.IsAny())).ReturnsAsync(child);
- _repoMock.Setup(r => r.GetTemplateByIdAsync(1, It.IsAny())).ReturnsAsync(templateA);
- _repoMock.Setup(r => r.GetTemplateByIdAsync(2, It.IsAny())).ReturnsAsync(templateB);
- _repoMock.Setup(r => r.GetAllTemplatesAsync(It.IsAny()))
- .ReturnsAsync(new List { templateA, templateB });
-
- // Try to make A inherit from B (B already inherits from A) => cycle
- var result = await _service.UpdateTemplateAsync(1, "A", null, 2, "admin");
+ // Attempt to re-parent Child from A (id=1) to B (id=3).
+ var result = await _service.UpdateTemplateAsync(2, "Child", null, 3, "admin");
Assert.True(result.IsFailure);
- Assert.Contains("cycle", result.Error, StringComparison.OrdinalIgnoreCase);
+ Assert.Contains("cannot be changed", result.Error, StringComparison.OrdinalIgnoreCase);
+ _repoMock.Verify(r => r.UpdateTemplateAsync(It.IsAny(), It.IsAny()), Times.Never);
}
[Fact]
- public async Task UpdateTemplate_SelfInheritance_Fails()
+ public async Task UpdateTemplate_ClearParent_Fails()
{
- var template = new Template("Self") { Id = 1 };
- _repoMock.Setup(r => r.GetTemplateByIdAsync(1, It.IsAny())).ReturnsAsync(template);
- _repoMock.Setup(r => r.GetAllTemplatesAsync(It.IsAny()))
- .ReturnsAsync(new List { template });
+ var child = new Template("Child") { Id = 2, ParentTemplateId = 1 };
+ _repoMock.Setup(r => r.GetTemplateByIdAsync(2, It.IsAny())).ReturnsAsync(child);
- var result = await _service.UpdateTemplateAsync(1, "Self", null, 1, "admin");
+ // Attempt to clear the parent.
+ var result = await _service.UpdateTemplateAsync(2, "Child", null, null, "admin");
Assert.True(result.IsFailure);
- Assert.Contains("itself", result.Error, StringComparison.OrdinalIgnoreCase);
+ Assert.Contains("cannot be changed", result.Error, StringComparison.OrdinalIgnoreCase);
+ _repoMock.Verify(r => r.UpdateTemplateAsync(It.IsAny(), It.IsAny()), Times.Never);
+ }
+
+ [Fact]
+ public async Task UpdateTemplate_SameParent_Succeeds()
+ {
+ var child = new Template("Child") { Id = 2, ParentTemplateId = 1, Description = "old" };
+ _repoMock.Setup(r => r.GetTemplateByIdAsync(2, It.IsAny())).ReturnsAsync(child);
+ _repoMock.Setup(r => r.GetAllTemplatesAsync(It.IsAny()))
+ .ReturnsAsync(new List { child });
+
+ // Idempotent pass — same parent value sent on update should succeed and apply name/description changes.
+ var result = await _service.UpdateTemplateAsync(2, "ChildRenamed", "new", 1, "admin");
+
+ Assert.True(result.IsSuccess);
+ Assert.Equal("ChildRenamed", result.Value.Name);
+ Assert.Equal("new", result.Value.Description);
+ Assert.Equal(1, result.Value.ParentTemplateId);
+ _repoMock.Verify(r => r.UpdateTemplateAsync(It.IsAny(), It.IsAny()), Times.Once);
}
[Fact]