9e243493fb
Applies the family-wide admin-UI cleanup playbook to the Central UI so the Blazor surfaces stop diverging from the shared kit: buttons are grouped rather than individually sized, long cell values are contained instead of widening tables, and hard-coded colours give way to theme tokens. The headline fix is that MainLayout passed Accent="#2f5fd0" to ThemeShell, which the kit emits as an inline style on the shell root. Being a descendant of <html>, it beat the [data-bs-theme="dark"] override for the entire app, so the dark accent had never rendered. Declaring --accent in site.css :root instead lets both schemes resolve; light is unchanged because the value already matched the kit's light default. Theme pins to 0.4.1, which upstreams the local .btn sizing block verbatim, so that block is deleted here rather than duplicated. Verified byte-identical before removal; the repo now declares no --bs-btn-* anywhere. NOT purely cosmetic, contrary to the sweep's stated scope: four detail-modal surfaces (NotificationReport, ConfigurationAuditLog, ParkedMessages, SiteCallsReport) were additionally refactored from holding the selected record to holding its id and re-resolving from the current page each render, with the resolve doubling as the visibility gate. A background refresh that drops the row now closes the modal instead of showing a stale snapshot. This is a behaviour change and is called out rather than buried: a full-suite run turned up one intermittent CentralUI failure, CloseButton_DismissesModal, whose stack (GetRequiredEventBindingEntry during DispatchEventAsync) indicates the handler was disposed between render and click — a window the previous field-held record made structurally impossible. Treat the modal lifecycle here as unreviewed. Build 0/0; suite green apart from that one intermittent failure.
208 lines
7.9 KiB
Plaintext
208 lines
7.9 KiB
Plaintext
@namespace ZB.MOM.WW.ScadaBridge.CentralUI.Components.Shared
|
|
|
|
@* Bootstrap-only JSON Schema editor. Two modes:
|
|
- "object" parameters: edits a top-level object schema (named properties).
|
|
- "value" return type: edits a single value schema; object/array fall back
|
|
to the same property editor as Mode=object.
|
|
Recurses through methods (not nested components) so we stay in one file. *@
|
|
|
|
@if (_root.Type == "object" && Mode == "object")
|
|
{
|
|
@PropertyList(_root, isRoot: true)
|
|
}
|
|
else
|
|
{
|
|
@ValueRoot(_root)
|
|
}
|
|
|
|
@code {
|
|
/// <summary><c>"object"</c> for parameters, <c>"value"</c> for return type.</summary>
|
|
[Parameter] public string Mode { get; set; } = "object";
|
|
|
|
/// <summary>JSON Schema text. Empty/null seeds the mode's default.</summary>
|
|
[Parameter] public string? Value { get; set; }
|
|
|
|
[Parameter] public EventCallback<string?> ValueChanged { get; set; }
|
|
|
|
private SchemaNode _root = new();
|
|
private string? _lastSeenJson;
|
|
private bool _initialized;
|
|
|
|
protected override void OnParametersSet()
|
|
{
|
|
// OnInitialized fires before this on first mount; OnParametersSet runs
|
|
// on every parameter change. Guard against the initial null==null case
|
|
// where the early-exit would skip applying the mode-appropriate default.
|
|
if (_initialized && Value == _lastSeenJson) return;
|
|
_initialized = true;
|
|
_lastSeenJson = Value;
|
|
_root = SchemaBuilderModel.Parse(
|
|
Value,
|
|
Mode == "object" ? SchemaBuilderModel.NewObject() : SchemaBuilderModel.NewValue());
|
|
}
|
|
|
|
private async Task Emit()
|
|
{
|
|
var json = SchemaBuilderModel.Serialize(_root);
|
|
_lastSeenJson = json;
|
|
await ValueChanged.InvokeAsync(json);
|
|
}
|
|
|
|
private async Task OnTypeChange(SchemaNode node)
|
|
{
|
|
if (node.Type == "array" && node.Items == null)
|
|
node.Items = new SchemaNode { Type = "string" };
|
|
await Emit();
|
|
}
|
|
|
|
private async Task AddProperty(SchemaNode parent)
|
|
{
|
|
parent.Properties.Add(new SchemaProperty { Schema = new SchemaNode { Type = "string" } });
|
|
await Emit();
|
|
}
|
|
|
|
private async Task RemoveProperty(SchemaNode parent, SchemaProperty prop)
|
|
{
|
|
parent.Properties.Remove(prop);
|
|
await Emit();
|
|
}
|
|
|
|
// ── Render helpers ─────────────────────────────────────────────────────────
|
|
|
|
/// <summary>
|
|
/// Renders the property list for an object schema node. <paramref name="isRoot"/>
|
|
/// just tweaks the wording on the Add button ("parameter" at root vs "field"
|
|
/// inside a nested object).
|
|
/// </summary>
|
|
private RenderFragment PropertyList(SchemaNode node, bool isRoot = false) => __builder =>
|
|
{
|
|
<div class="border rounded bg-body p-2">
|
|
@if (node.Properties.Count == 0)
|
|
{
|
|
<div class="text-muted small fst-italic px-1 py-2">
|
|
@(isRoot ? "No parameters defined." : "No fields defined.")
|
|
</div>
|
|
}
|
|
@foreach (var prop in node.Properties)
|
|
{
|
|
<div @key="prop.Id" class="border rounded p-2 mb-2 bg-light-subtle">
|
|
@PropertyRow(node, prop)
|
|
@NestedEditor(prop.Schema)
|
|
</div>
|
|
}
|
|
<button type="button" class="btn btn-outline-secondary btn-sm" @onclick="() => AddProperty(node)">
|
|
+ Add @(isRoot ? "parameter" : "field")
|
|
</button>
|
|
</div>
|
|
};
|
|
|
|
/// <summary>
|
|
/// One property's compact horizontal row: name, type, (items type if array),
|
|
/// required toggle, remove button. Nested object / array-of-object editors
|
|
/// render below the row via <see cref="NestedEditor"/>.
|
|
/// </summary>
|
|
private RenderFragment PropertyRow(SchemaNode parent, SchemaProperty prop) => __builder =>
|
|
{
|
|
<div class="d-flex flex-wrap align-items-center gap-2">
|
|
<input type="text" class="form-control form-control-sm"
|
|
style="max-width: 14rem;" placeholder="name"
|
|
@bind="prop.Name" @bind:event="oninput" @bind:after="Emit" />
|
|
|
|
<select class="form-select form-select-sm" style="max-width: 9rem;"
|
|
@bind="prop.Schema.Type" @bind:after="() => OnTypeChange(prop.Schema)">
|
|
@foreach (var t in SchemaBuilderModel.PrimitiveTypes)
|
|
{
|
|
<option value="@t">@t</option>
|
|
}
|
|
</select>
|
|
|
|
@if (prop.Schema.Type == "array")
|
|
{
|
|
<span class="small text-muted">items:</span>
|
|
<select class="form-select form-select-sm" style="max-width: 9rem;"
|
|
@bind="prop.Schema.Items!.Type" @bind:after="() => OnTypeChange(prop.Schema.Items!)">
|
|
@foreach (var t in SchemaBuilderModel.PrimitiveTypes)
|
|
{
|
|
<option value="@t">@t</option>
|
|
}
|
|
</select>
|
|
}
|
|
|
|
<div class="form-check form-check-inline mb-0">
|
|
<input class="form-check-input" type="checkbox" id="req-@prop.Id"
|
|
@bind="prop.Required" @bind:after="Emit" />
|
|
<label class="form-check-label small" for="req-@prop.Id">required</label>
|
|
</div>
|
|
|
|
<button type="button"
|
|
class="btn btn-sm btn-outline-danger ms-auto"
|
|
title="Remove" aria-label="Remove field"
|
|
@onclick="() => RemoveProperty(parent, prop)">
|
|
<i class="bi bi-x-lg"></i>
|
|
</button>
|
|
</div>
|
|
};
|
|
|
|
/// <summary>
|
|
/// Renders the indented sub-editor for object / array-of-object properties.
|
|
/// No-op for scalar properties.
|
|
/// </summary>
|
|
private RenderFragment NestedEditor(SchemaNode schema) => __builder =>
|
|
{
|
|
if (schema.Type == "object")
|
|
{
|
|
<div class="ms-3 mt-2">
|
|
@PropertyList(schema)
|
|
</div>
|
|
}
|
|
else if (schema.Type == "array" && schema.Items?.Type == "object")
|
|
{
|
|
<div class="ms-3 mt-2">
|
|
<div class="small text-muted mb-1">item properties:</div>
|
|
@PropertyList(schema.Items)
|
|
</div>
|
|
}
|
|
};
|
|
|
|
/// <summary>
|
|
/// Mode=value root: a single type picker. When the user picks <c>object</c>
|
|
/// or <c>array</c> we expose the same nested editors used by Mode=object.
|
|
/// </summary>
|
|
private RenderFragment ValueRoot(SchemaNode node) => __builder =>
|
|
{
|
|
<div class="d-flex flex-wrap align-items-center gap-2 mb-2">
|
|
<label class="form-label mb-0">Return type:</label>
|
|
<select class="form-select form-select-sm" style="max-width: 10rem;"
|
|
@bind="node.Type" @bind:after="() => OnTypeChange(node)">
|
|
@foreach (var t in SchemaBuilderModel.PrimitiveTypes)
|
|
{
|
|
<option value="@t">@t</option>
|
|
}
|
|
</select>
|
|
|
|
@if (node.Type == "array")
|
|
{
|
|
<label class="form-label mb-0 ms-2">Item type:</label>
|
|
<select class="form-select form-select-sm" style="max-width: 10rem;"
|
|
@bind="node.Items!.Type" @bind:after="() => OnTypeChange(node.Items!)">
|
|
@foreach (var t in SchemaBuilderModel.PrimitiveTypes)
|
|
{
|
|
<option value="@t">@t</option>
|
|
}
|
|
</select>
|
|
}
|
|
</div>
|
|
|
|
@if (node.Type == "object")
|
|
{
|
|
<div class="text-muted small mb-1">Properties of return value:</div>
|
|
@PropertyList(node)
|
|
}
|
|
else if (node.Type == "array" && node.Items?.Type == "object")
|
|
{
|
|
<div class="text-muted small mb-1">Item properties:</div>
|
|
@PropertyList(node.Items)
|
|
}
|
|
};
|
|
}
|