feat(templates/ui): manage compositions from the tree
Move composition CRUD off the TemplateEdit page and onto the tree context menu, matching Aveva's Template Toolbox flow. - New ComposeIntoDialog: pick a parent template, slot name (defaults to the source template's name). - "Compose into…" on every base template's context menu (kebab + right click) opens the dialog and calls AddCompositionAsync. - "Rename…" on composition leaves opens a prompt and calls TemplateService.RenameCompositionAsync. The owning composition row AND its owned derived template are renamed atomically; duplicate slot names or derived-name collisions abort with a clear error. - "Delete" on composition leaves confirms + cascade-deletes the composition (and its derived template via DeleteCompositionAsync). - "New Derived Template" menu item renamed to "New Inheriting Template" to disambiguate from the new derive-on-compose meaning. TemplateEdit's Compositions tab, Add Composition form, and Add/DeleteComposition handlers + state fields are deleted — the tree is now the single source of truth.
This commit is contained in:
@@ -35,6 +35,13 @@
|
||||
ErrorMessage="@_moveFolderError"
|
||||
OnSubmit="SubmitMoveFolder" />
|
||||
|
||||
<ComposeIntoDialog @bind-IsVisible="_showComposeDialog"
|
||||
SourceTemplateId="_composeSourceId"
|
||||
SourceName="@_composeSourceName"
|
||||
ParentOptions="EnumerateComposableParents(_composeSourceId)"
|
||||
ErrorMessage="@_composeError"
|
||||
OnSubmit="SubmitCompose" />
|
||||
|
||||
@if (_loading)
|
||||
{
|
||||
<LoadingSpinner IsLoading="true" />
|
||||
@@ -299,7 +306,8 @@
|
||||
break;
|
||||
|
||||
case TmplNodeKind.Template:
|
||||
<li><button class="dropdown-item" @onclick='() => NavigationManager.NavigateTo($"/design/templates/create?parentId={node.EntityId}")'>New Derived Template</button></li>
|
||||
<li><button class="dropdown-item" @onclick='() => NavigationManager.NavigateTo($"/design/templates/create?parentId={node.EntityId}")'>New Inheriting Template</button></li>
|
||||
<li><button class="dropdown-item" @onclick="() => OpenComposeDialog(node.Template!)">Compose into…</button></li>
|
||||
<li><button class="dropdown-item" @onclick="() => OpenMoveTemplateDialog(node.EntityId, node.Label)">Move to Folder…</button></li>
|
||||
<li><hr class="dropdown-divider" /></li>
|
||||
<li><button class="dropdown-item text-danger" @onclick="() => DeleteTemplate(node.Template!)">Delete</button></li>
|
||||
@@ -307,6 +315,9 @@
|
||||
|
||||
case TmplNodeKind.Composition:
|
||||
<li><button class="dropdown-item" @onclick='() => NavigationManager.NavigateTo($"/design/templates/{node.Composition!.ComposedTemplateId}")'>Open composed template</button></li>
|
||||
<li><button class="dropdown-item" @onclick="() => RenameComposition(node.Composition!)">Rename…</button></li>
|
||||
<li><hr class="dropdown-divider" /></li>
|
||||
<li><button class="dropdown-item text-danger" @onclick="() => DeleteComposition(node.Composition!)">Delete</button></li>
|
||||
break;
|
||||
}
|
||||
</ul>
|
||||
@@ -345,7 +356,8 @@
|
||||
break;
|
||||
|
||||
case TmplNodeKind.Template:
|
||||
<button class="dropdown-item" @onclick='() => NavigationManager.NavigateTo($"/design/templates/create?parentId={node.EntityId}")'>New Derived Template</button>
|
||||
<button class="dropdown-item" @onclick='() => NavigationManager.NavigateTo($"/design/templates/create?parentId={node.EntityId}")'>New Inheriting Template</button>
|
||||
<button class="dropdown-item" @onclick="() => OpenComposeDialog(node.Template!)">Compose into…</button>
|
||||
<button class="dropdown-item" @onclick="() => OpenMoveTemplateDialog(node.EntityId, node.Label)">Move to Folder…</button>
|
||||
<div class="dropdown-divider"></div>
|
||||
<button class="dropdown-item text-danger" @onclick="() => DeleteTemplate(node.Template!)">Delete</button>
|
||||
@@ -353,6 +365,9 @@
|
||||
|
||||
case TmplNodeKind.Composition:
|
||||
<button class="dropdown-item" @onclick='() => NavigationManager.NavigateTo($"/design/templates/{node.Composition!.ComposedTemplateId}")'>Open composed template</button>
|
||||
<button class="dropdown-item" @onclick="() => RenameComposition(node.Composition!)">Rename…</button>
|
||||
<div class="dropdown-divider"></div>
|
||||
<button class="dropdown-item text-danger" @onclick="() => DeleteComposition(node.Composition!)">Delete</button>
|
||||
break;
|
||||
}
|
||||
};
|
||||
@@ -544,4 +559,89 @@
|
||||
_toast.ShowError($"Delete failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Compose-into dialog ----
|
||||
private bool _showComposeDialog;
|
||||
private int _composeSourceId;
|
||||
private string _composeSourceName = string.Empty;
|
||||
private string? _composeError;
|
||||
|
||||
private void OpenComposeDialog(Template source)
|
||||
{
|
||||
_composeSourceId = source.Id;
|
||||
_composeSourceName = source.Name;
|
||||
_composeError = null;
|
||||
_showComposeDialog = true;
|
||||
}
|
||||
|
||||
// Possible parents for a compose: every non-derived template except the source itself.
|
||||
// Server still validates cycles + collisions; the picker just trims obvious bad choices.
|
||||
private IEnumerable<(int Id, string Label)> EnumerateComposableParents(int sourceId)
|
||||
{
|
||||
return _templates
|
||||
.Where(t => !t.IsDerived && t.Id != sourceId)
|
||||
.OrderBy(t => t.Name, StringComparer.OrdinalIgnoreCase)
|
||||
.Select(t => (t.Id, t.Name));
|
||||
}
|
||||
|
||||
private async Task SubmitCompose((int SourceTemplateId, int ParentTemplateId, string SlotName) req)
|
||||
{
|
||||
_composeError = null;
|
||||
var user = await GetCurrentUserAsync();
|
||||
var result = await TemplateService.AddCompositionAsync(req.ParentTemplateId, req.SourceTemplateId, req.SlotName, user);
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
_showComposeDialog = false;
|
||||
_toast.ShowSuccess($"Composed '{_composeSourceName}' as '{req.SlotName}'.");
|
||||
await LoadTemplatesAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
_composeError = result.Error;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Composition leaf: rename + delete ----
|
||||
private async Task RenameComposition(TemplateComposition composition)
|
||||
{
|
||||
var newName = await Dialog.PromptAsync(
|
||||
"Rename slot",
|
||||
$"New name for slot '{composition.InstanceName}':",
|
||||
initialValue: composition.InstanceName,
|
||||
placeholder: "Slot name");
|
||||
if (string.IsNullOrWhiteSpace(newName) || newName.Trim() == composition.InstanceName) return;
|
||||
|
||||
var user = await GetCurrentUserAsync();
|
||||
var result = await TemplateService.RenameCompositionAsync(composition.Id, newName.Trim(), user);
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
_toast.ShowSuccess($"Slot renamed to '{newName.Trim()}'.");
|
||||
await LoadTemplatesAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
_toast.ShowError(result.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DeleteComposition(TemplateComposition composition)
|
||||
{
|
||||
var confirmed = await Dialog.ConfirmAsync(
|
||||
"Delete composition",
|
||||
$"Delete slot '{composition.InstanceName}'? This removes the derived template and any overrides on it.",
|
||||
danger: true);
|
||||
if (!confirmed) return;
|
||||
|
||||
var user = await GetCurrentUserAsync();
|
||||
var result = await TemplateService.DeleteCompositionAsync(composition.Id, user);
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
_toast.ShowSuccess($"Composition '{composition.InstanceName}' removed.");
|
||||
await LoadTemplatesAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
_toast.ShowError(result.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user