refactor(ui/design): card grid, SMTP split, TemplateEdit vertical-stack
Templates: <h4> in flex header, Expand/Collapse moved into a Bulk actions dropdown, hover-visible kebab on tree nodes with aria-labels. TreeView CSS gets a .tv-kebab opacity-on-hover utility. TemplateCreate: form-control (not -sm) for primary inputs; accessible Back button. TemplateEdit: Properties card vertical-stacked with Save at the bottom-right and Parent rendered as readonly plaintext. Add-member forms (Attributes, Alarms, Scripts, Compositions) reflowed from horizontal row g-2 align-items-end into cards with stacked col-12 inputs (Scripts gets rows=10). Lock/Unlock badges show full words. Per-row Delete moved into a kebab dropdown. Tab nav gains role="tablist" / role="tab" / aria-selected / aria-controls and panels get role="tabpanel". Validation entries get consistent strong-and- muted styling. SharedScripts: migrated from table to card grid (col-lg-6) matching Sites; cards show code preview + param/return badges + Edit + kebab. Search filter, empty state CTA, @key. SharedScriptForm: small ?-icon tooltips next to Parameters and Return Definition labels. ExternalSystems: SMTP split out to its own page; remaining tabs ( External Systems, DB Connections, Notification Lists, API Methods, API Keys) unified as card grids with per-tab search + empty-state CTA. Tab nav gets full ARIA instrumentation. Header gains a link to the new SMTP page. New page SmtpConfiguration.razor at /design/smtp: vertical-stacked form using the existing Credentials field on the entity. ExternalSystemForm: AuthConfig placeholder updates based on the selected AuthType (None / ApiKey / BasicAuth). DbConnectionForm: form-text below Connection String noting that the value is stored in plain text and is admin-only. ApiMethodForm: Script textarea rows=10; JSON example placeholders for Params and Returns. NotificationListForm: form-control sizing on Name/email inputs; thead.table-dark -> table-light on the recipients table.
This commit is contained in:
@@ -16,7 +16,7 @@
|
||||
</div>
|
||||
|
||||
<ToastNotification @ref="_toast" />
|
||||
<ConfirmDialog @ref="_confirmDialog" />
|
||||
<ConfirmDialog @ref="_confirmDialog" ConfirmButtonClass="btn-danger" />
|
||||
|
||||
@if (_loading)
|
||||
{
|
||||
@@ -26,46 +26,81 @@
|
||||
{
|
||||
<div class="alert alert-danger">@_errorMessage</div>
|
||||
}
|
||||
else if (_scripts.Count == 0)
|
||||
{
|
||||
<div class="text-center py-5 text-muted">
|
||||
<p class="mb-3">No shared scripts configured.</p>
|
||||
<button class="btn btn-primary btn-sm"
|
||||
@onclick='() => NavigationManager.NavigateTo("/design/shared-scripts/create")'>
|
||||
Create your first script
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<table class="table table-sm table-striped table-hover">
|
||||
<thead class="table-dark">
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Name</th>
|
||||
<th>Code (preview)</th>
|
||||
<th>Parameters</th>
|
||||
<th>Returns</th>
|
||||
<th style="width: 160px;">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@if (_scripts.Count == 0)
|
||||
{
|
||||
<tr>
|
||||
<td colspan="6" class="text-muted text-center">No shared scripts configured.</td>
|
||||
</tr>
|
||||
}
|
||||
@foreach (var script in _scripts)
|
||||
{
|
||||
<tr>
|
||||
<td>@script.Id</td>
|
||||
<td><strong>@script.Name</strong></td>
|
||||
<td class="small text-muted font-monospace text-truncate" style="max-width: 300px;">
|
||||
@script.Code[..Math.Min(60, script.Code.Length)]@(script.Code.Length > 60 ? "..." : "")
|
||||
</td>
|
||||
<td class="small text-muted">@(script.ParameterDefinitions ?? "—")</td>
|
||||
<td class="small text-muted">@(script.ReturnDefinition ?? "—")</td>
|
||||
<td>
|
||||
<button class="btn btn-outline-primary btn-sm py-0 px-1 me-1"
|
||||
@onclick='() => NavigationManager.NavigateTo($"/design/shared-scripts/{script.Id}/edit")'>Edit</button>
|
||||
<button class="btn btn-outline-danger btn-sm py-0 px-1"
|
||||
@onclick="() => DeleteScript(script)">Delete</button>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="mb-3" style="max-width: 320px;">
|
||||
<input class="form-control form-control-sm"
|
||||
placeholder="Filter by name or code…"
|
||||
@bind="_search" @bind:event="oninput" />
|
||||
</div>
|
||||
|
||||
@if (!FilteredScripts.Any())
|
||||
{
|
||||
<p class="text-muted small">No shared scripts match the filter.</p>
|
||||
}
|
||||
|
||||
<div class="row g-3">
|
||||
@foreach (var s in FilteredScripts)
|
||||
{
|
||||
var preview = s.Code.Length > 80
|
||||
? s.Code[..80] + "…"
|
||||
: s.Code;
|
||||
var paramCount = CountJsonArrayEntries(s.ParameterDefinitions);
|
||||
<div class="col-lg-6 col-12" @key="s.Id">
|
||||
<div class="card h-100">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-start mb-2">
|
||||
<h5 class="card-title mb-0">@s.Name</h5>
|
||||
<div class="d-flex gap-1">
|
||||
<button class="btn btn-outline-primary btn-sm"
|
||||
@onclick='() => NavigationManager.NavigateTo($"/design/shared-scripts/{s.Id}/edit")'>
|
||||
Edit
|
||||
</button>
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-outline-secondary btn-sm"
|
||||
data-bs-toggle="dropdown"
|
||||
aria-expanded="false"
|
||||
aria-label="@($"More actions for {s.Name}")">⋮</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end">
|
||||
<li>
|
||||
<button class="dropdown-item text-danger"
|
||||
@onclick="() => DeleteScript(s)">Delete</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<pre class="small text-muted font-monospace mb-2"
|
||||
style="white-space: pre-wrap; word-break: break-all;"
|
||||
title="@s.Code">@preview</pre>
|
||||
|
||||
<div>
|
||||
<span class="badge bg-light text-dark me-1">@paramCount params</span>
|
||||
@if (!string.IsNullOrWhiteSpace(s.ReturnDefinition))
|
||||
{
|
||||
<span class="badge bg-light text-dark">returns</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="badge bg-light text-dark">void</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -79,10 +114,18 @@
|
||||
private List<SharedScript> _scripts = new();
|
||||
private bool _loading = true;
|
||||
private string? _errorMessage;
|
||||
private string _search = "";
|
||||
|
||||
private ToastNotification _toast = default!;
|
||||
private ConfirmDialog _confirmDialog = default!;
|
||||
|
||||
private IEnumerable<SharedScript> FilteredScripts =>
|
||||
string.IsNullOrWhiteSpace(_search)
|
||||
? _scripts
|
||||
: _scripts.Where(s =>
|
||||
(s.Name?.Contains(_search, StringComparison.OrdinalIgnoreCase) ?? false) ||
|
||||
(s.Code?.Contains(_search, StringComparison.OrdinalIgnoreCase) ?? false));
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await LoadDataAsync();
|
||||
@@ -128,4 +171,21 @@
|
||||
_toast.ShowError($"Delete failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Best-effort count of JSON array entries by tallying top-level objects.
|
||||
/// Returns 0 if the parameter definition is null/empty/malformed.
|
||||
/// </summary>
|
||||
private static int CountJsonArrayEntries(string? json)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(json)) return 0;
|
||||
try
|
||||
{
|
||||
using var doc = System.Text.Json.JsonDocument.Parse(json);
|
||||
if (doc.RootElement.ValueKind == System.Text.Json.JsonValueKind.Array)
|
||||
return doc.RootElement.GetArrayLength();
|
||||
}
|
||||
catch { /* fall through */ }
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user