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:
@@ -113,11 +113,6 @@
|
||||
private ScadaLink.CentralUI.ScriptAnalysis.CompositionContext? ActiveEditorParent =>
|
||||
_editorParents.FirstOrDefault();
|
||||
|
||||
private bool _showCompForm;
|
||||
private int _compComposedTemplateId;
|
||||
private string _compInstanceName = string.Empty;
|
||||
private string? _compFormError;
|
||||
|
||||
private ToastNotification _toast = default!;
|
||||
|
||||
protected override async Task OnParametersSetAsync()
|
||||
@@ -341,15 +336,6 @@
|
||||
Scripts <span class="badge bg-secondary">@_scripts.Count</span>
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link @(_activeTab == "compositions" ? "active" : "")"
|
||||
role="tab"
|
||||
aria-selected="@(_activeTab == "compositions" ? "true" : "false")"
|
||||
aria-controls="tmpl-tab-compositions"
|
||||
@onclick='() => _activeTab = "compositions"'>
|
||||
Compositions <span class="badge bg-secondary">@_compositions.Count</span>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@if (_activeTab == "attributes")
|
||||
@@ -364,10 +350,6 @@
|
||||
{
|
||||
<div role="tabpanel" id="tmpl-tab-scripts">@RenderScriptsTab()</div>
|
||||
}
|
||||
else if (_activeTab == "compositions")
|
||||
{
|
||||
<div role="tabpanel" id="tmpl-tab-compositions">@RenderCompositionsTab()</div>
|
||||
}
|
||||
};
|
||||
|
||||
private async Task DeleteTemplate()
|
||||
@@ -1029,81 +1011,6 @@
|
||||
else _toast.ShowError(result.Error);
|
||||
}
|
||||
|
||||
// ---- Compositions Tab ----
|
||||
private RenderFragment RenderCompositionsTab() => __builder =>
|
||||
{
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<h5 class="mb-0">Compositions</h5>
|
||||
<button class="btn btn-primary btn-sm" @onclick="() => { _showCompForm = true; _compFormError = null; _compInstanceName = string.Empty; _compComposedTemplateId = 0; }">Add Composition</button>
|
||||
</div>
|
||||
|
||||
@if (_showCompForm)
|
||||
{
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">Add Composition</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-12">
|
||||
<label class="form-label">Instance Name</label>
|
||||
<input type="text" class="form-control" @bind="_compInstanceName" />
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label">Composed Template</label>
|
||||
<select class="form-select" @bind="_compComposedTemplateId">
|
||||
<option value="0">Select template...</option>
|
||||
@foreach (var t in _templates.Where(t => _selectedTemplate == null || t.Id != _selectedTemplate.Id))
|
||||
{
|
||||
<option value="@t.Id">@t.Name</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
@if (_compFormError != null)
|
||||
{
|
||||
<div class="col-12"><div class="text-danger small">@_compFormError</div></div>
|
||||
}
|
||||
<div class="col-12 text-end">
|
||||
<button class="btn btn-outline-secondary me-1" @onclick="() => _showCompForm = false">Cancel</button>
|
||||
<button class="btn btn-success" @onclick="AddComposition">Add</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<table class="table table-sm table-striped">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Instance Name</th>
|
||||
<th>Composed Template</th>
|
||||
<th style="width: 60px;">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var comp in _compositions)
|
||||
{
|
||||
<tr>
|
||||
<td><code>@comp.InstanceName</code></td>
|
||||
<td>@(_templates.FirstOrDefault(t => t.Id == comp.ComposedTemplateId)?.Name ?? $"#{comp.ComposedTemplateId}")</td>
|
||||
<td>
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-outline-secondary btn-sm py-0 px-1"
|
||||
data-bs-toggle="dropdown"
|
||||
aria-expanded="false"
|
||||
aria-label="@($"More actions for {comp.InstanceName}")">⋮</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end">
|
||||
<li>
|
||||
<button class="dropdown-item text-danger"
|
||||
@onclick="() => DeleteComposition(comp)">Delete</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
};
|
||||
|
||||
// ---- CRUD handlers ----
|
||||
|
||||
private async Task AddAttribute()
|
||||
@@ -1237,42 +1144,6 @@
|
||||
else { _toast.ShowError(result.Error); }
|
||||
}
|
||||
|
||||
private async Task AddComposition()
|
||||
{
|
||||
if (_selectedTemplate == null) return;
|
||||
_compFormError = null;
|
||||
if (string.IsNullOrWhiteSpace(_compInstanceName)) { _compFormError = "Instance name is required."; return; }
|
||||
if (_compComposedTemplateId == 0) { _compFormError = "Select a template."; return; }
|
||||
|
||||
var user = await GetCurrentUserAsync();
|
||||
var result = await TemplateService.AddCompositionAsync(
|
||||
_selectedTemplate.Id, _compComposedTemplateId, _compInstanceName.Trim(), user);
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
_showCompForm = false;
|
||||
_toast.ShowSuccess($"Composition '{_compInstanceName}' added.");
|
||||
await LoadAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
_compFormError = result.Error;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DeleteComposition(TemplateComposition comp)
|
||||
{
|
||||
var confirmed = await Dialog.ConfirmAsync("Delete Composition", $"Remove composition '{comp.InstanceName}'?", danger: true);
|
||||
if (!confirmed) return;
|
||||
var user = await GetCurrentUserAsync();
|
||||
var result = await TemplateService.DeleteCompositionAsync(comp.Id, user);
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
_toast.ShowSuccess($"Composition '{comp.InstanceName}' removed.");
|
||||
await LoadAsync();
|
||||
}
|
||||
else { _toast.ShowError(result.Error); }
|
||||
}
|
||||
|
||||
// ---- Editor metadata builders ----
|
||||
|
||||
private async Task<IReadOnlyList<ScadaLink.CentralUI.ScriptAnalysis.CompositionContext>> BuildChildContextsAsync(
|
||||
|
||||
Reference in New Issue
Block a user