Add ExpandoTree for hierarchical expandable nodes

This commit is contained in:
Alexander Shabarshov 2025-04-07 10:06:02 +01:00
parent dd22a91ca8
commit 878a7feded
6 changed files with 165 additions and 47 deletions

View File

@ -1,27 +1,29 @@
<div class="expander @Class"> <CascadingValue Name="OpenAPIUI_ExpandoTree_Parent" Value="_anchor">
<div class="ex-header @HeaderClass"> <div class="expander @Class">
@Title <div class="ex-header @HeaderClass">
<div onclick="@(() => Toggle())"> @Title
@if (Collapsed) <div onclick="@(() => Toggle())">
{ @if (Collapsed)
<svg width="24px" height="24px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> {
<path d="M18 9L12 15L6 9" stroke="var(--oa-fg-lighter)" stroke-width="2" /> <svg width="24px" height="24px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
</svg> <path d="M18 9L12 15L6 9" stroke="var(--oa-fg-lighter)" stroke-width="2" />
} </svg>
else }
{ else
<svg width="24px" height="24px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> {
<path d="M18 15L12 9L6 15" stroke="var(--oa-fg-lighter)" stroke-width="2" /> <svg width="24px" height="24px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
</svg> <path d="M18 15L12 9L6 15" stroke="var(--oa-fg-lighter)" stroke-width="2" />
} </svg>
}
</div>
</div> </div>
</div>
@if (!Collapsed && ChildContent != null) @if (!Collapsed && ChildContent != null)
{ {
@ChildContent @ChildContent
} }
</div> </div>
</CascadingValue>
@code { @code {
[Parameter] [Parameter]
@ -35,9 +37,24 @@
[Parameter] [Parameter]
public string Class { get; set; } = ""; public string Class { get; set; } = "";
[CascadingParameter(Name = "OpenAPIUI_ExpandoTree")] public IExpandoTree Tree { get; set; } = null!;
[CascadingParameter(Name = "OpenAPIUI_ExpandoTree_Parent")] public string Parent { get; set; } = "";
private string _anchor = $"anc{Random.Shared.Next():X8}";
protected override void OnAfterRender(bool firstRender)
{
if (!firstRender)
return;
Tree.Add(Title, _anchor, Parent, Collapsed);
Collapsed = Tree.IsCollapsed(Title);
}
void Toggle() void Toggle()
{ {
Collapsed = !Collapsed; Collapsed = !Collapsed;
Tree.Collapse(_anchor, Collapsed);
StateHasChanged(); StateHasChanged();
} }
} }

View File

@ -48,10 +48,18 @@
<RequestBodyControl Value="@Value.RequestBody"/> <RequestBodyControl Value="@Value.RequestBody"/>
<ResponsesControl Value="@Value.Responses" /> <ResponsesControl Value="@Value.Responses" />
<div class="example-data"> @if (true)
<h3>Example Data</h3> {
<pre>@GenerateExampleData()</pre> var example = GenerateExampleData();
</div> if ( !string.IsNullOrWhiteSpace(example) )
{
<div class="example-data">
<h3>Example Data</h3>
<pre>@GenerateExampleData()</pre>
</div>
}
}
} }
</div> </div>
} }
@ -85,7 +93,7 @@
var exampleData = new Dictionary<string, object>(); var exampleData = new Dictionary<string, object>();
// Generate example data for parameters // Generate example data for parameters
foreach (var parameter in Value.Parameters) foreach (var parameter in Value.Parameters.Where(x => x.In == ParameterLocation.Query))
{ {
if (parameter.Example != null) if (parameter.Example != null)
{ {
@ -121,7 +129,10 @@
} }
} }
return JsonSerializer.Serialize(exampleData, new JsonSerializerOptions { WriteIndented = true }); return exampleData.Count == 0
? ""
: JsonSerializer.Serialize(exampleData, new JsonSerializerOptions { WriteIndented = true })
;
} }
private object GenerateExampleFromSchema(OpenApiSchema? schema) private object GenerateExampleFromSchema(OpenApiSchema? schema)
@ -153,11 +164,11 @@
{ {
return type switch return type switch
{ {
"string" => "string", "string" => "string",
"integer" => 0, "integer" => 0,
"number" => 0.0, "number" => 0.0,
"boolean" => false, "boolean" => false,
_ => new object() _ => new object()
}; };
} }
} }

View File

@ -3,7 +3,7 @@
<div class="request"> <div class="request">
<div class="rb-title">Request body</div> <div class="rb-title">Request body</div>
<div class="rb-body"> <div class="rb-body">
<MarkdownControl Value="@Value.Description" /> <MarkdownControl Value="@Value.Description" />
@foreach ( var media in Value.Content) @foreach ( var media in Value.Content)
{ {
<MediaTypeControl Key="@media.Key" Value="@media.Value" /> <MediaTypeControl Key="@media.Key" Value="@media.Value" />

View File

@ -0,0 +1,54 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace BlazorOpenApi;
internal class ExpandoTree : IExpandoTree
{
private readonly Dictionary<string, ExpandoTreeNode> _nodes = new();
private readonly List<string> _order = new();
public void Add(string name, string anchor, string parentAnchor, bool collapsed)
{
if (_nodes.ContainsKey(anchor))
{
throw new InvalidOperationException($"A node with anchor '{anchor}' already exists.");
}
var node = new ExpandoTreeNode
{
Name = name,
Anchor = anchor,
ParentAnchor = parentAnchor,
Collapsed = collapsed
};
_nodes[anchor] = node;
_order.Add(anchor);
}
public void Collapse(string anchor, bool collapsed)
{
if (_nodes.TryGetValue(anchor, out var node))
{
node.Collapsed = collapsed;
}
else
{
throw new KeyNotFoundException($"No node found with anchor '{anchor}'.");
}
}
public bool IsCollapsed(string anchor)
{
return _nodes.TryGetValue(anchor, out var node) && node.Collapsed;
}
public bool Exists(string anchor) => _nodes.ContainsKey(anchor);
public ExpandoTreeNode[] GetChildren(string anchor) => _order
.Where(x => _nodes[x].ParentAnchor == anchor)
.Select(x => _nodes[x])
.ToArray();
}

View File

@ -0,0 +1,26 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BlazorOpenApi;
public class ExpandoTreeNode
{
public string Name { get; set; } = string.Empty;
public string Anchor { get; set; } = string.Empty;
public string ParentAnchor { get; set; } = string.Empty;
public bool Collapsed { get; set; }
}
public interface IExpandoTree
{
void Add(string name, string anchor, string parentAnchor, bool collapsed);
void Collapse(string anchor, bool collapsed);
bool IsCollapsed(string anchor);
bool Exists(string anchor);
ExpandoTreeNode[] GetChildren(string anchor);
}
//implement IExpandoTree

View File

@ -2,31 +2,36 @@
@using Microsoft.OpenApi.Models @using Microsoft.OpenApi.Models
@using Microsoft.OpenApi.Readers @using Microsoft.OpenApi.Readers
@if (Palette != null ) @if (Palette != null )
{ {
@PaletteStr @PaletteStr
} }
<div class="openapi-ui"> <div class="openapi-ui">
<CascadingValue Value="@_api"> <CascadingValue Value="@_api" IsFixed="true">
<HeaderControl Value="@_api.Info" DownloadUrl="@Url"/> <CascadingValue Name="OpenAPIUI_ExpandoTree" Value="_tree" IsFixed="true">
<ServersControl Value="@_api.Servers" />
@if (_api.Paths?.Count > 0) <HeaderControl Value="@_api.Info" DownloadUrl="@Url" />
{ <ServersControl Value="@_api.Servers" />
<h2>Endpoints</h2> @if (_api.Paths?.Count > 0)
foreach (var path in _api.Paths)
{ {
if (!string.IsNullOrWhiteSpace(path.Key) || path.Value != null) <h2>Endpoints</h2>
foreach (var path in _api.Paths)
{ {
<PathControl Key="@path.Key" Value="@path.Value" /> if (!string.IsNullOrWhiteSpace(path.Key) || path.Value != null)
{
<PathControl Key="@path.Key" Value="@path.Value" />
}
} }
} }
} @if (_api.Components != null)
@if (_api.Components != null) {
{ <h2>Components</h2>
<h2>Components</h2> <ComponentsControl Value="@_api.Components" />
<ComponentsControl Value="@_api.Components"/> }
}
</CascadingValue>
</CascadingValue> </CascadingValue>
</div> </div>
@ -35,8 +40,11 @@
[Parameter] public string Json { get; set; } = ""; [Parameter] public string Json { get; set; } = "";
[Parameter] public OpenApiUiPalette? Palette { get; set; } [Parameter] public OpenApiUiPalette? Palette { get; set; }
private string _loadedFor = ""; private string _loadedFor = "";
private OpenApiDocument _api = new(); private OpenApiDocument _api = new();
private IExpandoTree _tree = new ExpandoTree();
private MarkupString PaletteStr => Palette?.AsMarkupString ?? new(); private MarkupString PaletteStr => Palette?.AsMarkupString ?? new();
@ -98,4 +106,6 @@
_api = new(); _api = new();
} }
} }
} }