Show the toolbar. Without it the explorer has the pdfe-no-toolbar class and its panes take the full height
+
+
+
+
+
+
Methods
+
+
+
Method
Description
+
+
+
NavigateToAsync(path)
Opens a folder by opening each folder above it in turn
+
NavigateToItemAsync(itemPath)
Opens the folder holding a file or folder and selects it, loading the folder's contents once. Returns false, leaving the deepest folder found open, when it is not there
- Navigate to /Library/Templates/ or /Sharepoint/Public/ to see read-only files.
+ Navigate to /Library/Templates/ or /Sharepoint/Public/ to see read-only files.
Try different combinations to compare: position (before/after), style (text/icon), and badge overlay.
diff --git a/PanoramicData.Blazor.Demo/Pages/PDFileExplorerPage.razor.cs b/PanoramicData.Blazor.Demo/Pages/PDFileExplorerPage.razor.cs
index b1f523428..e88ab9076 100644
--- a/PanoramicData.Blazor.Demo/Pages/PDFileExplorerPage.razor.cs
+++ b/PanoramicData.Blazor.Demo/Pages/PDFileExplorerPage.razor.cs
@@ -84,6 +84,17 @@ protected override void OnAfterRender(bool firstRender)
private void OnFolderChanged(FileExplorerItem folder) => NavigationManager.SetUri(new Dictionary { { "path", $"{folder.Path}" } });
+ private string ItemPathToShow { get; set; } = "/Library/Templates/word_template.docx";
+
+ private string? _showItemResult;
+
+ // Opens the file's folder once and selects the file, without opening each folder on the way
+ private async Task ShowItemAsync()
+ {
+ var isFound = await FileExplorer!.NavigateToItemAsync(ItemPathToShow).ConfigureAwait(true);
+ _showItemResult = isFound ? "Selected" : "Not found: the deepest folder found is open";
+ }
+
private async Task OnReady()
{
if (!string.IsNullOrWhiteSpace(_deepLinkPath))
diff --git a/PanoramicData.Blazor.Test/Components/PDFileExplorerTests.Failures.cs b/PanoramicData.Blazor.Test/Components/PDFileExplorerTests.Failures.cs
new file mode 100644
index 000000000..f6ba084f1
--- /dev/null
+++ b/PanoramicData.Blazor.Test/Components/PDFileExplorerTests.Failures.cs
@@ -0,0 +1,95 @@
+using AwesomeAssertions;
+using Bunit;
+using Microsoft.JSInterop;
+using PanoramicData.Blazor.Models;
+
+namespace PanoramicData.Blazor.Test.Components;
+
+///
+/// How contains failures: given an ExceptionHandler, a failure in a handler is passed to it
+/// rather than ending the Blazor circuit, and without one it is thrown as before.
+///
+public partial class PDFileExplorerTests
+{
+ private static void FailOnFolderChange(FileExplorerItem folder) => throw new InvalidOperationException($"Cannot open {folder.Path}");
+
+ private static Task SelectTreeNodeAsync(IRenderedComponent cut, string path)
+ => Tree(cut).InvokeAsync(() => Tree(cut).Instance.SelectNode(cut.Instance.TreeRootNode!.Find(path)!, false));
+
+ /// A failing application handler is passed to ExceptionHandler, and the folder still opens.
+ [Fact]
+ public async Task AFailingHandler_IsPassedToTheExceptionHandler()
+ {
+ var cut = RenderExplorer(p => p.Add(x => x.FolderChanged, folder => FailOnFolderChange(folder)));
+ _exceptions.Should().ContainSingle().Which.Message.Should().Be("Cannot open /", "opening the root on the first render fails too");
+ _exceptions.Clear();
+
+ await SelectTreeNodeAsync(cut, "/Docs");
+
+ _exceptions.Should().ContainSingle().Which.Message.Should().Be("Cannot open /Docs");
+ cut.Instance.FolderPath.Should().Be("/Docs");
+ RowNames(cut).Should().Equal("..", "Sub", "a.docx", "b.xlsx");
+ }
+
+ /// Without an ExceptionHandler a failing handler is thrown, as it always was.
+ [Fact]
+ public async Task WithoutAnExceptionHandler_AFailingHandler_IsThrownAsBefore()
+ {
+ var cut = Render(p => p
+ .Add(x => x.DataProvider, _provider)
+ .Add(x => x.AutoExpand, true)
+ .Add(x => x.FolderChanged, folder => FailOnFolderChange(folder)));
+ cut.WaitForAssertion(() => Tree(cut).Instance.SelectedNode.Should().NotBeNull());
+
+ var act = () => SelectTreeNodeAsync(cut, "/Docs");
+
+ await act.Should().ThrowAsync().WithMessage("Cannot open /Docs");
+ }
+
+ /// A toolbar state handler that fails on the first render is passed on, and the explorer still loads.
+ [Fact]
+ public void AFailingToolbarStateHandler_OnTheFirstRender_IsPassedOn()
+ {
+ static void FailToUpdateToolbar(List items) => throw new InvalidOperationException("No toolbar state");
+
+ var cut = RenderExplorer(p => p.Add(x => x.UpdateToolbarState, items => FailToUpdateToolbar(items)));
+
+ cut.WaitForAssertion(() => _exceptions.Should().NotBeEmpty());
+ _exceptions.Should().AllSatisfy(ex => ex.Message.Should().Be("No toolbar state"));
+ RowNames(cut).Should().Equal("Docs", "Empty", "Media");
+ }
+
+ /// An item is still selected when the browser cannot scroll it into view.
+ [Fact]
+ public async Task NavigateToItemAsync_WhenScrollingFails_StillSelectsTheItem()
+ {
+ var cut = RenderExplorer();
+ _common.SetupVoid("scrollIntoView", _ => true).SetException(new JSException("No scrolling"));
+ _common.SetupVoid("scrollIntoViewEx", _ => true).SetException(new JSException("No scrolling"));
+
+ var found = await cut.InvokeAsync(() => cut.Instance.NavigateToItemAsync("/Docs/a.docx"));
+
+ found.Should().BeTrue();
+ cut.Instance.SelectedFilesAndFolders.Should().Equal("/Docs/a.docx");
+ _exceptions.Should().BeEmpty();
+ }
+
+ /// A navigation started while another is still loading the tree supersedes it.
+ [Fact]
+ public async Task NavigateToItemAsync_ALaterCall_SupersedesOneStillLoading()
+ {
+ var docsAnswer = new TaskCompletionSource();
+ var cut = RenderExplorer();
+ _provider.BeforeAnswer = folder => folder == "/Docs" ? docsAnswer.Task : Task.CompletedTask;
+
+ var first = cut.InvokeAsync(() => cut.Instance.NavigateToItemAsync("/Docs/Sub/notes.txt"));
+ var second = await cut.InvokeAsync(() => cut.Instance.NavigateToItemAsync("/Media/pic.png"));
+ docsAnswer.SetResult();
+
+ (await first).Should().BeFalse();
+ second.Should().BeTrue();
+ cut.Instance.FolderPath.Should().Be("/Media");
+ cut.Instance.SelectedFilesAndFolders.Should().Equal("/Media/pic.png");
+ cut.Instance.IsNavigating.Should().BeFalse();
+ }
+}
diff --git a/PanoramicData.Blazor.Test/Components/PDFileExplorerTests.FileProvider.cs b/PanoramicData.Blazor.Test/Components/PDFileExplorerTests.FileProvider.cs
index f9dc860ac..030c393a5 100644
--- a/PanoramicData.Blazor.Test/Components/PDFileExplorerTests.FileProvider.cs
+++ b/PanoramicData.Blazor.Test/Components/PDFileExplorerTests.FileProvider.cs
@@ -53,10 +53,18 @@ public FileProvider()
public FileExplorerItem Get(string path) => Items.Single(x => x.Path == path);
- public override Task> GetDataAsync(DataRequest request, CancellationToken cancellationToken)
+ /// Awaited before a request is answered, given the request's folder, to hold an answer back.
+ public Func? BeforeAnswer { get; set; }
+
+ public override async Task> GetDataAsync(DataRequest request, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
Requests.Add(request.SearchText);
+ if (BeforeAnswer is not null)
+ {
+ await BeforeAnswer(request.SearchText).ConfigureAwait(true);
+ }
+
List items = request.SearchText switch
{
null => [.. Items.Select(Copy)],
@@ -69,7 +77,7 @@ public override Task> GetDataAsync(DataRequest(items, items.Count));
+ return new DataResponse(items, items.Count);
}
private static FileExplorerItem Copy(FileExplorerItem x) => new()
diff --git a/PanoramicData.Blazor.Test/Components/PDFileExplorerTests.Navigation.cs b/PanoramicData.Blazor.Test/Components/PDFileExplorerTests.Navigation.cs
index 08c4b8aa5..23d6aa4e7 100644
--- a/PanoramicData.Blazor.Test/Components/PDFileExplorerTests.Navigation.cs
+++ b/PanoramicData.Blazor.Test/Components/PDFileExplorerTests.Navigation.cs
@@ -74,6 +74,94 @@ public async Task NavigateToAsync_BlankPath_DoesNothing()
cut.Instance.FolderPath.Should().Be("/");
}
+ /// Navigating to an item opens its folder once, without opening the folders above it, and selects it.
+ [Fact]
+ public async Task NavigateToItemAsync_OpensOnlyTheItemsFolder_AndSelectsTheItem()
+ {
+ _provider.Items.Remove(_provider.Get("/Docs/Sub"));
+ _provider.Items.AddRange(
+ [
+ FileProvider.Dir("/Docs/Sub", hasSubFolders: true),
+ FileProvider.Dir("/Docs/Sub/Deep"),
+ FileProvider.File("/Docs/Sub/Deep/report.pdf", 100)
+ ]);
+ var folders = new List();
+ FileExplorerItem[]? reported = null;
+ var cut = RenderExplorer(p => p
+ .Add(x => x.FolderChanged, (FileExplorerItem f) => folders.Add(f.Path))
+ .Add(x => x.SelectionChanged, (FileExplorerItem[] s) => reported = s));
+ await NavigateAsync(cut, "/Media");
+ folders.Clear();
+ _provider.Requests.Clear();
+
+ var found = await cut.InvokeAsync(() => cut.Instance.NavigateToItemAsync("/Docs/Sub/Deep/report.pdf"));
+
+ found.Should().BeTrue();
+ cut.Instance.FolderPath.Should().Be("/Docs/Sub/Deep");
+ folders.Should().Equal("/Docs/Sub/Deep");
+ _provider.Requests.Should().Equal("/Docs", "/Docs/Sub", "/Docs/Sub/Deep");
+ cut.Instance.SelectedFilesAndFolders.Should().Equal("/Docs/Sub/Deep/report.pdf");
+ reported!.Select(x => x.Path).Should().Equal("/Docs/Sub/Deep/report.pdf");
+ Tree(cut).Instance.SelectedNode!.Key.Should().Be("/Docs/Sub/Deep");
+ cut.Instance.IsNavigating.Should().BeFalse();
+ _common.Invocations["scrollIntoViewEx"].Should().ContainSingle()
+ .Which.Arguments[0].Should().Be($"#{Table(cut).Instance.Id} tr[id=\"/Docs/Sub/Deep/report.pdf\"]");
+ }
+
+ /// A folder can be the item: its parent folder opens with the folder selected.
+ [Fact]
+ public async Task NavigateToItemAsync_AFolder_OpensItsParent_AndSelectsIt()
+ {
+ var cut = RenderExplorer();
+
+ var found = await cut.InvokeAsync(() => cut.Instance.NavigateToItemAsync("/Docs/Sub"));
+
+ found.Should().BeTrue();
+ cut.Instance.FolderPath.Should().Be("/Docs");
+ cut.Instance.SelectedFilesAndFolders.Should().Equal("/Docs/Sub");
+ }
+
+ /// An item that is no longer in its folder leaves the folder open, with nothing selected.
+ [Fact]
+ public async Task NavigateToItemAsync_AMissingItem_OpensItsFolder_AndReturnsFalse()
+ {
+ var cut = RenderExplorer();
+
+ var found = await cut.InvokeAsync(() => cut.Instance.NavigateToItemAsync("/Docs/gone.docx"));
+
+ found.Should().BeFalse();
+ cut.Instance.FolderPath.Should().Be("/Docs");
+ cut.Instance.SelectedFilesAndFolders.Should().BeEmpty();
+ }
+
+ /// A missing folder on the way stops at the deepest folder found.
+ [Fact]
+ public async Task NavigateToItemAsync_AMissingFolder_OpensTheDeepestFolderFound_AndReturnsFalse()
+ {
+ var cut = RenderExplorer();
+
+ var found = await cut.InvokeAsync(() => cut.Instance.NavigateToItemAsync("/Docs/Gone/a.docx"));
+
+ found.Should().BeFalse();
+ cut.Instance.FolderPath.Should().Be("/Docs");
+ cut.Instance.IsNavigating.Should().BeFalse();
+ }
+
+ /// A blank path, or the root itself, which is in no folder, is ignored.
+ [Theory]
+ [InlineData(" ")]
+ [InlineData("/")]
+ public async Task NavigateToItemAsync_NoItem_DoesNothing(string path)
+ {
+ var cut = RenderExplorer();
+ await NavigateAsync(cut, "/Docs");
+
+ var found = await cut.InvokeAsync(() => cut.Instance.NavigateToItemAsync(path));
+
+ found.Should().BeFalse();
+ cut.Instance.FolderPath.Should().Be("/Docs");
+ }
+
/// SelectedFilesAndFolders lists the selected paths, and SelectionChanged carries the selected items.
[Fact]
public async Task Selection_IsReportedToTheApplication()
diff --git a/PanoramicData.Blazor.Test/Components/PDFileExplorerTests.cs b/PanoramicData.Blazor.Test/Components/PDFileExplorerTests.cs
index f702ab21d..b210a9c15 100644
--- a/PanoramicData.Blazor.Test/Components/PDFileExplorerTests.cs
+++ b/PanoramicData.Blazor.Test/Components/PDFileExplorerTests.cs
@@ -88,6 +88,18 @@ public void FirstRender_LoadsRootFolder_IntoTreeAndTable()
cut.Instance.FileItems.Should().HaveCount(3);
}
+ /// Without its toolbar the explorer says so, so that its panes can take the toolbar's height.
+ [Fact]
+ public void WithoutTheToolbar_TheExplorerHasTheNoToolbarClass()
+ {
+ var withToolbar = RenderExplorer();
+ var withoutToolbar = RenderExplorer(p => p.Add(x => x.ShowToolbar, false));
+
+ withToolbar.Find("div.pdfileexplorer").ClassList.Should().NotContain("pdfe-no-toolbar");
+ withoutToolbar.Find("div.pdfileexplorer").ClassList.Should().Contain("pdfe-no-toolbar");
+ withoutToolbar.Find("div.pdfe-toolbar").ClassList.Should().Contain("d-none");
+ }
+
/// A whitespace new-folder name is rejected as a parameter.
[Fact]
public void BlankNewFolderName_Throws()
diff --git a/PanoramicData.Blazor.Test/Components/PDTableTests.DataLoading.cs b/PanoramicData.Blazor.Test/Components/PDTableTests.DataLoading.cs
index ff87dfd7f..b68f0bd9e 100644
--- a/PanoramicData.Blazor.Test/Components/PDTableTests.DataLoading.cs
+++ b/PanoramicData.Blazor.Test/Components/PDTableTests.DataLoading.cs
@@ -148,6 +148,23 @@ public async Task Changing_the_page_refetches_and_raises_PageChanged()
pages.Should().Equal(2u);
}
+ /// A PageChanged handler that fails is passed to ExceptionHandler, rather than ending the circuit.
+ [Fact]
+ public async Task A_failing_PageChanged_handler_is_passed_to_ExceptionHandler()
+ {
+ static void FailToChangePage(PageCriteria criteria) => throw new InvalidOperationException("Paging failed");
+ var criteria = new PageCriteria(1, 2);
+ var failures = new List();
+ var table = RenderTable(p => p
+ .Add(x => x.PageCriteria, criteria)
+ .Add(x => x.PageChanged, c => FailToChangePage(c))
+ .Add(x => x.ExceptionHandler, failures.Add));
+
+ await table.InvokeAsync(() => criteria.Page = 2);
+
+ table.WaitForAssertion(() => failures.Should().ContainSingle().Which.Message.Should().Be("Paging failed"));
+ }
+
/// Changing the page size refetches and raises PageSizeChanged.
[Fact]
public async Task Changing_the_page_size_refetches_and_raises_PageSizeChanged()
diff --git a/PanoramicData.Blazor.Test/Components/PDTableTests.Selection.cs b/PanoramicData.Blazor.Test/Components/PDTableTests.Selection.cs
index 15a67ec97..f598be785 100644
--- a/PanoramicData.Blazor.Test/Components/PDTableTests.Selection.cs
+++ b/PanoramicData.Blazor.Test/Components/PDTableTests.Selection.cs
@@ -182,6 +182,19 @@ public async Task SelectItemAsync_with_shift_selects_a_range()
table.Instance.Selection.Should().Equal("1", "2", "3");
}
+ /// ScrollItemIntoViewAsync scrolls the row with the key into view, matching a key that is no CSS identifier.
+ [Fact]
+ public async Task ScrollItemIntoViewAsync_scrolls_the_row_by_its_id_attribute()
+ {
+ var table = RenderTable();
+
+ await table.InvokeAsync(() => table.Instance.ScrollItemIntoViewAsync("/a \"b\".txt"));
+ await table.InvokeAsync(() => table.Instance.ScrollItemIntoViewAsync(" "));
+
+ var arguments = _common.Invocations["scrollIntoViewEx"].Should().ContainSingle().Which.Arguments;
+ arguments.Should().Equal($"#{table.Instance.Id} tr[id=\"/a \\\"b\\\".txt\"]", "auto", "nearest", "nearest");
+ }
+
/// Without a key field no items are reported as selected.
[Fact]
public void GetSelectedItems_without_a_key_field_is_empty()
diff --git a/PanoramicData.Blazor.Test/Components/PDTreeTests.Failures.cs b/PanoramicData.Blazor.Test/Components/PDTreeTests.Failures.cs
new file mode 100644
index 000000000..e9ca93d23
--- /dev/null
+++ b/PanoramicData.Blazor.Test/Components/PDTreeTests.Failures.cs
@@ -0,0 +1,83 @@
+using AwesomeAssertions;
+using Bunit;
+using Microsoft.AspNetCore.Components.Web;
+using PanoramicData.Blazor.Models;
+
+namespace PanoramicData.Blazor.Test.Components;
+
+///
+/// How contains failures: given an ExceptionHandler, a failure in a handler is passed to it
+/// rather than ending the Blazor circuit, and without one it is thrown as before.
+///
+public partial class PDTreeTests
+{
+ private static readonly TimeSpan _clickPatience = TimeSpan.FromSeconds(5);
+
+ private static void FailToSelectB(TreeNode node)
+ {
+ if (node.Key == "b")
+ {
+ throw new InvalidOperationException($"Cannot select {node.Key}");
+ }
+ }
+
+ private IRenderedComponent> RenderFailingTree(bool withExceptionHandler)
+ {
+ var tree = Render>(parameters =>
+ {
+ parameters
+ .Add(p => p.DataProvider, _provider)
+ .Add(p => p.KeyField, item => item.Id)
+ .Add(p => p.ParentKeyField, item => item.ParentId ?? string.Empty)
+ .Add(p => p.TextField, item => item.Name)
+ .Add(p => p.AllowSelection, true)
+ .Add(p => p.SelectionChange, node => FailToSelectB(node));
+ if (withExceptionHandler)
+ {
+ parameters.Add(p => p.ExceptionHandler, _exceptions.Add);
+ }
+ });
+ tree.WaitForAssertion(() => tree.Instance.RootNode.Nodes.Should().NotBeNull());
+ return tree;
+ }
+
+ private static Task ArrowDownAsync(IRenderedComponent> tree)
+ => tree.Find("div.pdtree").TriggerEventAsync("onkeydown", new KeyboardEventArgs { Code = "ArrowDown", Key = "ArrowDown" });
+
+ /// A click, which is handled after a short wait to tell it from a double click, passes a failure on.
+ [Fact]
+ public async Task AClick_WhoseSelectionHandlerFails_IsPassedToTheExceptionHandler()
+ {
+ var tree = RenderFailingTree(withExceptionHandler: true);
+
+ await tree.InvokeAsync(() => tree.Instance.NodeMouseDown(Node(tree, "b"), new MouseEventArgs { Button = 0 }));
+
+ tree.WaitForAssertion(() => _exceptions.Should().ContainSingle().Which.Message.Should().Be("Cannot select b"), _clickPatience);
+ tree.Instance.SelectedNode!.Key.Should().Be("b");
+ }
+
+ /// A key press whose selection handler fails passes the failure on.
+ [Fact]
+ public async Task AKeyPress_WhoseSelectionHandlerFails_IsPassedToTheExceptionHandler()
+ {
+ var tree = RenderFailingTree(withExceptionHandler: true);
+ await SelectAsync(tree, "a");
+
+ await ArrowDownAsync(tree);
+
+ _exceptions.Should().ContainSingle().Which.Message.Should().Be("Cannot select b");
+ tree.Instance.SelectedNode!.Key.Should().Be("b");
+ }
+
+ /// Without an ExceptionHandler a failing handler is thrown, as it always was.
+ [Fact]
+ public async Task WithoutAnExceptionHandler_AFailingHandler_IsThrownAsBefore()
+ {
+ var tree = RenderFailingTree(withExceptionHandler: false);
+ await SelectAsync(tree, "a");
+
+ var act = () => ArrowDownAsync(tree);
+
+ await act.Should().ThrowAsync().WithMessage("Cannot select b");
+ }
+}
diff --git a/PanoramicData.Blazor.Test/Components/PDTreeTests.Interaction.cs b/PanoramicData.Blazor.Test/Components/PDTreeTests.Interaction.cs
index 6d7c2756e..ddcb917f9 100644
--- a/PanoramicData.Blazor.Test/Components/PDTreeTests.Interaction.cs
+++ b/PanoramicData.Blazor.Test/Components/PDTreeTests.Interaction.cs
@@ -215,6 +215,24 @@ public async Task LoadOnDemand_FetchesChildrenOnFirstExpand()
_provider.Requests.Should().Equal(string.Empty, "a");
}
+ /// LoadChildNodesAsync loads a node's children once, without expanding it, and does nothing for a leaf.
+ [Fact]
+ public async Task LoadChildNodesAsync_LoadsChildrenOnce_WithoutExpanding()
+ {
+ _provider.UseLoadOnDemand();
+ var tree = RenderItemTree(p => p
+ .Add(x => x.LoadOnDemand, true)
+ .Add(x => x.IsLeaf, item => item.Id == "b"));
+
+ await tree.InvokeAsync(() => tree.Instance.LoadChildNodesAsync(Node(tree, "a")));
+ await tree.InvokeAsync(() => tree.Instance.LoadChildNodesAsync(Node(tree, "a")));
+ await tree.InvokeAsync(() => tree.Instance.LoadChildNodesAsync(Node(tree, "b")));
+
+ Node(tree, "a").Nodes!.Select(n => n.Key).Should().Equal("a1");
+ Node(tree, "a").IsExpanded.Should().BeFalse();
+ _provider.Requests.Should().Equal(string.Empty, "a");
+ }
+
/// With ClearOnCollapse, collapsing discards loaded children so they are fetched again.
[Fact]
public async Task ClearOnCollapse_DiscardsChildren()
diff --git a/PanoramicData.Blazor/Helpers/ComponentExceptionGuard.cs b/PanoramicData.Blazor/Helpers/ComponentExceptionGuard.cs
new file mode 100644
index 000000000..bb0d46810
--- /dev/null
+++ b/PanoramicData.Blazor/Helpers/ComponentExceptionGuard.cs
@@ -0,0 +1,76 @@
+namespace PanoramicData.Blazor.Helpers;
+
+///
+/// Handles a component's events as does, rendering after each one, but passes a failure to
+/// the component's ExceptionHandler, when the application has given one, rather than letting it end the Blazor
+/// circuit. Without one, a failure is thrown exactly as before.
+///
+internal static class ComponentExceptionGuard
+{
+ ///
+ /// Runs an event handler, rendering straight after it and again once an asynchronous one finishes.
+ ///
+ /// The event handler.
+ /// The event's argument.
+ /// Where a failure is passed; without a delegate a failure is thrown.
+ /// The component's .
+ public static async Task HandleEventAsync(
+ EventCallbackWorkItem callback,
+ object? arg,
+ EventCallback exceptionHandler,
+ Action stateHasChanged)
+ {
+ ArgumentNullException.ThrowIfNull(stateHasChanged);
+
+ Task task;
+ try
+ {
+ task = callback.InvokeAsync(arg);
+ }
+ catch (Exception ex) when (exceptionHandler.HasDelegate)
+ {
+ await ReportAsync(exceptionHandler, ex).ConfigureAwait(true);
+ stateHasChanged();
+ return;
+ }
+
+ var isPending = task.Status is not (TaskStatus.RanToCompletion or TaskStatus.Canceled);
+ stateHasChanged();
+ if (!isPending)
+ {
+ return;
+ }
+
+ try
+ {
+ await task.ConfigureAwait(true);
+ }
+ catch when (task.IsCanceled)
+ {
+ return;
+ }
+ catch (Exception ex) when (exceptionHandler.HasDelegate)
+ {
+ await ReportAsync(exceptionHandler, ex).ConfigureAwait(true);
+ }
+
+ stateHasChanged();
+ }
+
+ ///
+ /// Passes a failure to the handler. A failure of the handler itself is dropped: rethrowing it would end the circuit.
+ ///
+ /// Where the failure is passed.
+ /// The failure.
+ public static async Task ReportAsync(EventCallback exceptionHandler, Exception exception)
+ {
+ try
+ {
+ await exceptionHandler.InvokeAsync(exception).ConfigureAwait(true);
+ }
+ catch
+ {
+ // Nothing safer is left to do with it
+ }
+ }
+}
diff --git a/PanoramicData.Blazor/PDFileExplorer.razor b/PanoramicData.Blazor/PDFileExplorer.razor
index a3e9bf648..296f5bb32 100644
--- a/PanoramicData.Blazor/PDFileExplorer.razor
+++ b/PanoramicData.Blazor/PDFileExplorer.razor
@@ -116,7 +116,7 @@
-
+
/// File explorer component with tree/table navigation, uploads, preview, and context actions.
///
-public partial class PDFileExplorer : IAsyncDisposable
+///
+/// When the application gives an , a failure in any of the explorer's event handlers, or
+/// in the tree and table inside it, is passed to it instead of ending the Blazor circuit.
+///
+public partial class PDFileExplorer : IAsyncDisposable, IHandleEvent
{
private static int _idSequence;
+ private int _itemNavigation;
private readonly SortCriteria _tableSort = new("Name", SortDirection.Ascending);
private readonly MenuItem _menuOpen = new() { Text = "Open", IconCssClass = "fas fa-fw fa-folder-open" };
private readonly MenuItem _menuDownload = new() { Text = "Download", IconCssClass = "fas fa-fw fa-file-download" };
@@ -530,17 +536,17 @@ protected override async Task OnAfterRenderAsync(bool firstRender)
return;
}
+ var isTouchDevice = false;
try
{
_commonModule = await JSRuntime.InvokeAsync("import", JSInteropVersionHelper.CommonJsUrl).ConfigureAwait(true);
+ isTouchDevice = await _commonModule.InvokeAsync("isTouchDevice").ConfigureAwait(true);
}
catch
{
- // Do nothing
+ // Without the module the explorer works, with the toolbar for a mouse
}
- var isTouchDevice = _commonModule != null && await _commonModule.InvokeAsync("isTouchDevice").ConfigureAwait(true);
-
AddToolbarItems(isTouchDevice);
if (PreviewPanel == FilePreviewModes.OptionalOff)
@@ -550,9 +556,31 @@ protected override async Task OnAfterRenderAsync(bool firstRender)
SetUpDialogButtons();
- await RefreshToolbarAsync().ConfigureAwait(true);
+ // The toolbar state comes from the application, so a failure there is passed on rather than ending the circuit
+ try
+ {
+ await RefreshToolbarAsync().ConfigureAwait(true);
+ }
+ catch (Exception ex) when (ExceptionHandler.HasDelegate)
+ {
+ await ComponentExceptionGuard.ReportAsync(ExceptionHandler, ex).ConfigureAwait(true);
+ }
}
+ ///
+ /// Handles an event as does, passing a failure to when
+ /// the application gives one.
+ ///
+ Task IHandleEvent.HandleEventAsync(EventCallbackWorkItem callback, object? arg)
+ => ComponentExceptionGuard.HandleEventAsync(callback, arg, ExceptionHandler, StateHasChanged);
+
+ ///
+ /// The handler given to the tree and table: the explorer's own, which passes a failure on to the application's,
+ /// only when the application has given one, so that without one a failure behaves as it always has.
+ ///
+ private EventCallback ChildExceptionHandler
+ => ExceptionHandler.HasDelegate ? EventCallback.Factory.Create(this, OnException) : default;
+
///
/// Adds the explorer's own toolbar buttons, and its upload menu items when uploads are possible.
///
@@ -670,6 +698,112 @@ public async Task NavigateToAsync(string path)
IsNavigating = false;
}
+ ///
+ /// Opens the folder holding the given file or folder and selects it in the table. Unlike
+ /// , which opens each folder on the way in turn, the folders above it are only
+ /// loaded into the tree, so the table loads once and is raised once.
+ ///
+ /// The path of the file or folder to show, such as "/Docs/a.docx".
+ ///
+ /// True when the item was found and selected. False when it was not, in which case the deepest folder found on the
+ /// way to it is open.
+ ///
+ public async Task NavigateToItemAsync(string itemPath)
+ {
+ if (string.IsNullOrWhiteSpace(itemPath) || Tree?.RootNode.Find("/") is not { } rootNode)
+ {
+ return false;
+ }
+
+ // The root itself is in no folder
+ var segments = itemPath.Split('/', StringSplitOptions.RemoveEmptyEntries);
+ if (segments.Length == 0)
+ {
+ return false;
+ }
+
+ // A later call supersedes this one: each stops at its next step once it is no longer the latest
+ var navigation = ++_itemNavigation;
+ IsNavigating = true;
+ try
+ {
+ var (folderNode, isFolderFound) = await LoadFolderNodesAsync(rootNode, segments[..^1], navigation).ConfigureAwait(true);
+ if (navigation != _itemNavigation)
+ {
+ return false;
+ }
+
+ if (Tree.SelectedNode != folderNode)
+ {
+ await Tree.SelectNode(folderNode, false).ConfigureAwait(true);
+ }
+
+ await ScrollIntoViewAsync(() => Tree.ScrollNodeIntoViewAsync(folderNode)).ConfigureAwait(true);
+ return isFolderFound
+ && navigation == _itemNavigation
+ && await SelectTableItemAsync("/" + string.Join('/', segments)).ConfigureAwait(true);
+ }
+ finally
+ {
+ if (navigation == _itemNavigation)
+ {
+ IsNavigating = false;
+ }
+ }
+ }
+
+ ///
+ /// Scrolls something into view. Scrolling only helps the eye, so a browser that cannot do it, or a connection that
+ /// has closed, is no reason to fail.
+ ///
+ private static async Task ScrollIntoViewAsync(Func scroll)
+ {
+ try
+ {
+ await scroll().ConfigureAwait(true);
+ }
+ catch (Exception ex) when (ex is JSException or JSDisconnectedException or TaskCanceledException or ObjectDisposedException)
+ {
+ // Left where it is
+ }
+ }
+
+ ///
+ /// Loads each folder below the given node into the tree in turn, without selecting any of them, and returns the
+ /// deepest one found and whether every folder was found. It stops early once a later navigation has started.
+ ///
+ private async Task<(TreeNode Node, bool IsFound)> LoadFolderNodesAsync(TreeNode node, string[] folderNames, int navigation)
+ {
+ for (var i = 0; i < folderNames.Length && navigation == _itemNavigation; i++)
+ {
+ await Tree!.LoadChildNodesAsync(node).ConfigureAwait(true);
+ var childPath = "/" + string.Join('/', folderNames.Take(i + 1));
+ if (node.Nodes?.Find(x => x.Key == childPath) is not { } child)
+ {
+ return (node, false);
+ }
+
+ node = child;
+ }
+
+ return (node, true);
+ }
+
+ ///
+ /// Selects the row with the given path, as a click on it would, and scrolls it into view, when the table shows it.
+ ///
+ private async Task SelectTableItemAsync(string path)
+ {
+ if (Table is null || !Table.ItemsToDisplay.Any(x => x.Path == path))
+ {
+ return false;
+ }
+
+ await Table.SelectItemAsync(path).ConfigureAwait(true);
+ await ScrollIntoViewAsync(() => Table.ScrollItemIntoViewAsync(path)).ConfigureAwait(true);
+ return true;
+ }
+
///
/// Forces the tree component of the file explorer to be refreshed.
///
diff --git a/PanoramicData.Blazor/PDTable.Data.cs b/PanoramicData.Blazor/PDTable.Data.cs
index ec464e4cf..5276978b6 100644
--- a/PanoramicData.Blazor/PDTable.Data.cs
+++ b/PanoramicData.Blazor/PDTable.Data.cs
@@ -1,3 +1,5 @@
+using PanoramicData.Blazor.Helpers;
+
namespace PanoramicData.Blazor;
public partial class PDTable
@@ -324,24 +326,46 @@ private async Task ScrollColumnIntoViewAsync(PDColumn column)
}
}
+ // An exception leaving an async void method ends the Blazor circuit, so these pass one to ExceptionHandler when
+ // the application gives one
+ private async Task ReportPagingFailureAsync(Exception ex)
+ {
+ Logger.LogError(ex, "{Message}", ex.Message);
+ await ComponentExceptionGuard.ReportAsync(ExceptionHandler, ex).ConfigureAwait(true);
+ }
+
private async void PageCriteria_PageSizeChanged(object? sender, EventArgs e)
{
- await RefreshAsync(SearchText).ConfigureAwait(true);
- if (PageCriteria != null)
+ try
{
- await PageSizeChanged.InvokeAsync(PageCriteria).ConfigureAwait(true);
- }
+ await RefreshAsync(SearchText).ConfigureAwait(true);
+ if (PageCriteria != null)
+ {
+ await PageSizeChanged.InvokeAsync(PageCriteria).ConfigureAwait(true);
+ }
- StateHasChanged();
+ StateHasChanged();
+ }
+ catch (Exception ex) when (ExceptionHandler.HasDelegate)
+ {
+ await ReportPagingFailureAsync(ex).ConfigureAwait(true);
+ }
}
private async void PageCriteria_PageChanged(object? sender, EventArgs e)
{
- if (PageCriteria != null)
+ try
{
- await RefreshAsync(SearchText).ConfigureAwait(true);
- await PageChanged.InvokeAsync(PageCriteria).ConfigureAwait(true);
- StateHasChanged();
+ if (PageCriteria != null)
+ {
+ await RefreshAsync(SearchText).ConfigureAwait(true);
+ await PageChanged.InvokeAsync(PageCriteria).ConfigureAwait(true);
+ StateHasChanged();
+ }
+ }
+ catch (Exception ex) when (ExceptionHandler.HasDelegate)
+ {
+ await ReportPagingFailureAsync(ex).ConfigureAwait(true);
}
}
}
diff --git a/PanoramicData.Blazor/PDTable.Selection.cs b/PanoramicData.Blazor/PDTable.Selection.cs
index 9081c8081..d1f64114a 100644
--- a/PanoramicData.Blazor/PDTable.Selection.cs
+++ b/PanoramicData.Blazor/PDTable.Selection.cs
@@ -9,6 +9,30 @@ public TItem[] GetSelectedItems() => KeyField is null
? []
: [.. ItemsToDisplay.Where(x => Selection.Contains(KeyField(x).ToString() ?? string.Empty))];
+ ///
+ /// Scrolls the row with the given key into view, moving the table no further than needed to show it. Scrolling only
+ /// helps the eye, so a connection that has closed is not reported as a failure.
+ ///
+ /// Row key value.
+ public async Task ScrollItemIntoViewAsync(string key)
+ {
+ if (_commonModule is null || string.IsNullOrWhiteSpace(key))
+ {
+ return;
+ }
+
+ // A key such as a file path is not a valid CSS identifier, so the row is matched on its id attribute
+ var escapedKey = key.Replace("\\", "\\\\", StringComparison.Ordinal).Replace("\"", "\\\"", StringComparison.Ordinal);
+ try
+ {
+ await _commonModule.InvokeVoidAsync("scrollIntoViewEx", $"#{Id} tr[id=\"{escapedKey}\"]", "auto", "nearest", "nearest").ConfigureAwait(true);
+ }
+ catch (Exception ex) when (ex is JSDisconnectedException or TaskCanceledException or ObjectDisposedException)
+ {
+ // The page closed while scrolling
+ }
+ }
+
///
/// Clears the current selection.
///
diff --git a/PanoramicData.Blazor/PDTree.Interaction.cs b/PanoramicData.Blazor/PDTree.Interaction.cs
index 9e9820a61..55bf9036b 100644
--- a/PanoramicData.Blazor/PDTree.Interaction.cs
+++ b/PanoramicData.Blazor/PDTree.Interaction.cs
@@ -1,3 +1,5 @@
+using PanoramicData.Blazor.Helpers;
+
namespace PanoramicData.Blazor;
///
@@ -14,30 +16,54 @@ public partial class PDTree
///
/// The state object, typically a .
private void ClickTimerCallback(object? state)
+ {
+ // A timer calls back on a thread-pool thread, where an exception would end the whole process, so the click is
+ // only handed to the component's dispatcher, where the clicks are counted too
+ try
+ {
+ _ = InvokeAsync(() => OnClickTimerElapsedAsync(state));
+ }
+ catch
+ {
+ // The component has gone
+ }
+ }
+
+ ///
+ /// Acts on the clicks counted since the first one: one selects the node, more select and expand or collapse it.
+ /// Not an event, so a failure is passed to here, when the application gives one.
+ ///
+ private async Task OnClickTimerElapsedAsync(object? state)
{
_clickTimer?.Dispose();
_clickTimer = null;
+ var node = _clickedNode;
+ var clickCount = _clickCount;
+ _clickCount = 0;
+ if (node is null)
+ {
+ return;
+ }
- InvokeAsync(async () =>
+ try
{
- if (_clickedNode != null)
+ if (clickCount > 1)
{
- if (_clickCount > 1)
- {
- await SelectNode(_clickedNode, false).ConfigureAwait(true);
- await ToggleNodeIsExpandedAsync(_clickedNode).ConfigureAwait(true);
- }
- else
- {
- var autoEdit = state is MouseEventArgs args && args.Button == 0;
- await SelectNode(_clickedNode, autoEdit).ConfigureAwait(true);
- }
-
- StateHasChanged();
+ await SelectNode(node, false).ConfigureAwait(true);
+ await ToggleNodeIsExpandedAsync(node).ConfigureAwait(true);
+ }
+ else
+ {
+ var autoEdit = state is MouseEventArgs args && args.Button == 0;
+ await SelectNode(node, autoEdit).ConfigureAwait(true);
}
- });
+ }
+ catch (Exception ex) when (ExceptionHandler.HasDelegate)
+ {
+ await ComponentExceptionGuard.ReportAsync(ExceptionHandler, ex).ConfigureAwait(true);
+ }
- _clickCount = 0;
+ StateHasChanged();
}
///
@@ -147,6 +173,32 @@ private async Task ChangeSelectionAsync(TreeNode node)
StateHasChanged();
}
+ ///
+ /// Loads the given node's children from the data provider, unless they are loaded already, without expanding the
+ /// node or redrawing the tree. Selecting a node expands every node above it, so loading each node on a path and
+ /// then selecting the last one opens the whole path with a single redraw.
+ ///
+ /// The node whose children to load.
+ public async Task LoadChildNodesAsync(TreeNode node)
+ {
+ ArgumentNullException.ThrowIfNull(node);
+
+ if (node.Isleaf || node.Nodes != null)
+ {
+ return;
+ }
+
+ // fetch direct child items
+ var key = node.Data is null ? null : KeyField!(node.Data!).ToString();
+ var items = await GetDataAsync(key).ConfigureAwait(true);
+ // add new nodes to existing node
+ node.Nodes = []; // indicates data fetched, even if no items returned
+ UpdateModel(items);
+
+ // notify any listeners that new data fetched
+ await NodeUpdated.InvokeAsync(node).ConfigureAwait(true);
+ }
+
///
/// Toggles the given node's expanded state, loading children if necessary.
///
@@ -160,18 +212,9 @@ public async Task ToggleNodeIsExpandedAsync(TreeNode node)
return;
}
- // if expanding and Nodes is null then request data
- if (!wasExpanded && node.Nodes == null)
+ if (!wasExpanded)
{
- // fetch direct child items
- var key = node.Data is null ? null : KeyField!(node.Data!).ToString();
- var items = await GetDataAsync(key).ConfigureAwait(true);
- // add new nodes to existing node
- node.Nodes = []; // indicates data fetched, even if no items returned
- UpdateModel(items);
-
- // notify any listeners that new data fetched
- await NodeUpdated.InvokeAsync(node).ConfigureAwait(true);
+ await LoadChildNodesAsync(node).ConfigureAwait(true);
}
// expand / collapse and notify
diff --git a/PanoramicData.Blazor/PDTree.razor.cs b/PanoramicData.Blazor/PDTree.razor.cs
index 2fe1305dc..61bf95c27 100644
--- a/PanoramicData.Blazor/PDTree.razor.cs
+++ b/PanoramicData.Blazor/PDTree.razor.cs
@@ -6,8 +6,19 @@ namespace PanoramicData.Blazor;
/// Represents a tree component for displaying hierarchical data with support for selection, editing, drag-and-drop, and on-demand loading.
///
/// The type of the data item associated with each tree node.
-public partial class PDTree : IDisposable where TItem : class
+///
+/// When the application gives an , a failure in any of the tree's event handlers is
+/// passed to it instead of ending the Blazor circuit.
+///
+public partial class PDTree : IDisposable, IHandleEvent where TItem : class
{
+ ///
+ /// Handles an event as does, passing a failure to when
+ /// the application gives one.
+ ///
+ Task IHandleEvent.HandleEventAsync(EventCallbackWorkItem callback, object? arg)
+ => ComponentExceptionGuard.HandleEventAsync(callback, arg, ExceptionHandler, StateHasChanged);
+
private const string _idPrefix = "pd-tree-";
private IJSObjectReference? _commonModule;
diff --git a/PanoramicData.Blazor/PanoramicData.Blazor.csproj b/PanoramicData.Blazor/PanoramicData.Blazor.csproj
index c5f0c61f5..2d2d50508 100644
--- a/PanoramicData.Blazor/PanoramicData.Blazor.csproj
+++ b/PanoramicData.Blazor/PanoramicData.Blazor.csproj
@@ -28,7 +28,7 @@
truesnupkgREADME.md
- PDChat Voice Mode: the microphone no longer hears the assistant read an answer aloud when it was waiting for the wake phrase or paused in Dictate mode, as it did when the question was typed. Whatever is heard while an answer is read aloud is never typed or sent, and a microphone that was waiting for the wake phrase waits for it again afterwards. IChatService gains InterruptPhrases, off by default and with a default implementation so other implementers still compile: when set, saying one of the phrases, such as Merlin, while an answer is read aloud stops it and starts listening, while every other word heard meanwhile is ignored. An answer that itself says one of the phrases is read with the microphone paused, so it cannot interrupt itself. An answer that replaces one still being read no longer hands the microphone back early.
+ PDChat Voice Mode: the microphone no longer hears the assistant read an answer aloud when it was waiting for the wake phrase or paused in Dictate mode, as it did when the question was typed. Whatever is heard while an answer is read aloud is never typed or sent, and a microphone that was waiting for the wake phrase waits for it again afterwards. IChatService gains InterruptPhrases, off by default and with a default implementation so other implementers still compile: when set, saying one of the phrases, such as Merlin, while an answer is read aloud stops it and starts listening, while every other word heard meanwhile is ignored. An answer that itself says one of the phrases is read with the microphone paused, so it cannot interrupt itself. An answer that replaces one still being read no longer hands the microphone back early. PDFileExplorer: NavigateToItemAsync opens the folder holding a file or folder and selects it, loading that folder's contents once, where NavigateToAsync opens each folder on the way in turn; it returns false, with the deepest folder found open, when the item is not there. Without its toolbar the explorer's panes now take the full height. Given an ExceptionHandler, PDFileExplorer and PDTree pass a failure in any of their event handlers to it instead of ending the Blazor circuit; without one, a failure is thrown as before. PDTree gains LoadChildNodesAsync and PDTable gains ScrollItemIntoViewAsync, and a failing PageChanged or PageSizeChanged handler on PDTable is passed to its ExceptionHandler.
diff --git a/PanoramicData.Blazor/wwwroot/css/main.css b/PanoramicData.Blazor/wwwroot/css/main.css
index 142a4a727..d24a83b6b 100644
--- a/PanoramicData.Blazor/wwwroot/css/main.css
+++ b/PanoramicData.Blazor/wwwroot/css/main.css
@@ -535,6 +535,10 @@
height: calc(100% - 50px);
}
+.pdfileexplorer.pdfileexplorer.pdfe-no-toolbar .pdsplitter.horizontal {
+ height: 100%;
+}
+
/* Suppress the whole-panel Dropzone.js hover highlight on the tree side;
individual node highlighting is handled by PDTreeNode.drag-over instead. */
.pdfileexplorer.pdfileexplorer .pddropzone.dz-drag-hover {
diff --git a/PanoramicData.Blazor/wwwroot/css/main.min.css b/PanoramicData.Blazor/wwwroot/css/main.min.css
index 9771f5e22..28ec6f781 100644
--- a/PanoramicData.Blazor/wwwroot/css/main.min.css
+++ b/PanoramicData.Blazor/wwwroot/css/main.min.css
@@ -1 +1 @@
-.cursor-pointer{cursor:pointer;}.fa-hidden{color:transparent;}.pd-pointer{cursor:pointer;}.pd-hidden{display:none!important;}.pd-empty-icon{height:16px;width:16px;}.resize-h{overflow:auto;resize:vertical;}.resize-w{overflow:auto;resize:horizontal;}.h-100{height:100px!important;}.h-200{height:200px!important;}.h-300{height:300px!important;}.h-400{height:400px!important;}.h-500{height:500px!important;}.h-100-pct{height:100%;}.w-100-pct{width:100%;}.w-100-px{width:100px;}.w-125-px{width:125px;}.w-150-px{width:150px;}.w-200-px{width:200px;}.mh-50-px{min-height:50px;}.mh-100-px{min-height:100px;}.mh-150-px{min-height:150px;}.mh-200-px{min-height:200px;}.mh-250-px{min-height:250px;}.mh-300-px{min-height:300px;}.mw-100-px{min-width:100px;}.mw-125-px{min-width:125px;}.mw-150-px{min-width:150px;}.mw-200-px{min-width:200px;}.text-small{font-size:.75rem;}.text-tiny{font-size:.5rem;}.z-1000{z-index:1000;}.z-1100{z-index:1100;}.z-1200{z-index:1200;}.pd-form-body .title-box{width:var(--title-width,200px)!important;min-width:var(--title-width,200px)!important;max-width:var(--title-width,200px)!important;flex:0 0 var(--title-width,200px)!important;}.pdsplitter .gutter{background-color:var(--pd-splitter-gutter-bg,#eee);transition:background-color .1s ease-out;}.pdsplitter .gutter-vertical{cursor:row-resize;}.pdsplitter .gutter-horizontal{cursor:col-resize;}.pd-studio .pdsplitter>.gutter{background-color:var(--pd-studio-gutter-bg,var(--pd-splitter-gutter-bg,var(--bs-tertiary-bg,#eee)));}.pdsplitter .gutter:active,.pdsplitter .gutter:hover{background-color:var(--pd-splitter-gutter-hover-bg,var(--bs-primary,#0d6efd));transition:background-color .15s ease-in var(--pd-splitter-gutter-hover-delay,.2s);}.pdsplitter .gutter:active{transition-delay:0s;}.pd-studio .pdtoolbar.pd-studio-toolbar{padding:var(--pd-studio-toolbar-padding,.375rem var(--_pds-toolbar-padding-x,0));border-bottom:var(--pd-studio-toolbar-border-bottom,1px solid var(--_pds-border-color));}.pd-studio .pdtoolbar.pd-studio-toolbar{column-gap:.75rem;}.pd-studio .pdtoolbar.pd-studio-toolbar>.pdtoolbaritem{padding-left:0;padding-right:0;}.pd-studio .pdtoolbar.pd-studio-toolbar .pdts-bar{border-left-color:var(--pd-studio-separator-color,var(--bs-secondary,#6c757d));}.pd-studio .pdtoolbarbutton.pd-studio-execute{min-width:var(--pd-studio-execute-min-width,5.5rem)}.pd-studio .pd-studio-editor-container .pd-monacoeditor{border:none;}.pdfilepreview .stacked{align-items:center;display:flex;flex-direction:column;text-align:center;}.pdfilepreview .stacked span{margin-top:4px;}.pdfilepreview .h1,.pdfilepreview .h4,.pdfilepreview h2,.pdfilepreview h3{margin:0!important;padding:0!important;}.pdchat-message .pdchat-partial blockquote,.pdchat-message .pdchat-partial h1,.pdchat-message .pdchat-partial h2,.pdchat-message .pdchat-partial h3,.pdchat-message .pdchat-partial h4,.pdchat-message .pdchat-partial h5,.pdchat-message .pdchat-partial h6,.pdchat-message .pdchat-partial li,.pdchat-message .pdchat-partial ol,.pdchat-message .pdchat-partial p,.pdchat-message .pdchat-partial table,.pdchat-message .pdchat-partial ul,.pdchat-message .pdchat-text blockquote,.pdchat-message .pdchat-text h1,.pdchat-message .pdchat-text h2,.pdchat-message .pdchat-text h3,.pdchat-message .pdchat-text h4,.pdchat-message .pdchat-text h5,.pdchat-message .pdchat-text h6,.pdchat-message .pdchat-text li,.pdchat-message .pdchat-text ol,.pdchat-message .pdchat-text p,.pdchat-message .pdchat-text table,.pdchat-message .pdchat-text ul{white-space:normal;}.pdchat-message .pdchat-partial p,.pdchat-message .pdchat-text p{margin:0 0 .5em;}.pdchat-message .pdchat-partial ol,.pdchat-message .pdchat-partial ul,.pdchat-message .pdchat-text ol,.pdchat-message .pdchat-text ul{margin:.25em 0 .5em;padding-left:1.5em;}.pdchat-message .pdchat-partial li+li,.pdchat-message .pdchat-text li+li{margin-top:.15em;}.pdchat-message .pdchat-partial li>p,.pdchat-message .pdchat-text li>p{margin:0;}.pdchat-message .pdchat-partial ol:last-child,.pdchat-message .pdchat-partial p:last-child,.pdchat-message .pdchat-partial ul:last-child,.pdchat-message .pdchat-text ol:last-child,.pdchat-message .pdchat-text p:last-child,.pdchat-message .pdchat-text ul:last-child{margin-bottom:0;}.pdchat-message .pdchat-partial table{border-collapse:collapse;font-size:.95em;}.pdchat-message .pdchat-partial td,.pdchat-message .pdchat-partial th{border:1px solid var(--pd-chat-border,#dee2e6);padding:.15rem .4rem;text-align:left;}.pdchat-container .pdchat-conversation-main .pdchat-conversation-tabset{display:flex;flex-direction:column;flex:1 1 auto;min-height:0;}.pdchat-container .pdchat-conversation-main .pdchat-conversation-tabset .pdtabset-content{flex:1 1 auto;min-height:0;display:flex;flex-direction:column;overflow:hidden;}.pdchat-container .pdchat-conversation-main .pdtabset-tab.pdchat-conversation-tab-unread::before{content:"\25CF";margin-right:4px;font-size:.625rem;line-height:1;color:var(--pd-chat-info-border,#0078d4);}.pdchat-container .pdchat-conversation-main .pdtabset-tab.pdchat-conversation-tab-unread .pdtabset-tab-title{font-weight:600;}.pdchat-container-wrapper.pdchat-container-wrapper.pdchat-split-inactive .pdchat-chat-panel{flex:0 0 0%!important;width:0!important;min-width:0!important;padding:0!important;border:none!important;overflow:hidden!important;}.pdchat-container-wrapper.pdchat-container-wrapper.pdchat-split-inactive .pdchat-content-panel{flex:1 1 100%!important;width:100%!important;}.pdchat-container-wrapper.pdchat-container-wrapper.pdchat-split-inactive .pdchat-container-splitter>.gutter{display:none!important;width:0!important;min-width:0!important;}.pdchat-container-wrapper.pdchat-container-wrapper .pdchat-container.dock-left,.pdchat-container-wrapper.pdchat-container-wrapper .pdchat-container.dock-right{position:static!important;transform:none!important;top:auto!important;left:auto!important;right:auto!important;bottom:auto!important;width:100%!important;height:100%!important;z-index:auto!important;}.pdchat-container-wrapper.pdchat-container-wrapper .pdchat-window{width:100%!important;height:100%!important;max-width:none!important;max-height:none!important;border-radius:0!important;box-shadow:none!important;}@media(max-width:767px){.pdchat-container-wrapper.pdchat-container-wrapper:not(.pdchat-split-inactive) .pdchat-chat-panel{position:fixed!important;inset:0!important;width:100%!important;height:100%!important;z-index:10000!important;}.pdchat-container-wrapper.pdchat-container-wrapper:not(.pdchat-split-inactive) .pdchat-content-panel{flex:1 1 100%!important;width:100%!important;}.pdchat-container-wrapper.pdchat-container-wrapper:not(.pdchat-split-inactive) .pdchat-container-splitter>.gutter{display:none!important;}}.pd-list .filter input{border-top-right-radius:0!important;border-bottom-right-radius:0!important;}.pd-list .filter .input-group-text{border-top-left-radius:0!important;border-bottom-left-radius:0!important;}.pdtoolbartextbox.pdtoolbartextbox input{min-width:0;width:100%;}.pdtoolbarbuttonitem .pdtoolbarbutton{white-space:nowrap;}.pdtoolbardropdown .pdtoolbarbutton{white-space:nowrap;border-radius:var(--bs-border-radius,.375rem);display:inline-flex;align-items:center;line-height:1.5;}.pdtoolbardropdown .pdtoolbarbutton.dropdown-toggle{padding:.375rem .75rem;border:1px solid var(--bs-border-color,#dee2e6);border-radius:var(--bs-border-radius,.375rem);background-color:var(--bs-body-bg,#fff);color:var(--bs-body-color,#212529);transition:all .15s ease-in-out;display:inline-flex;align-items:center;gap:.5rem;line-height:1.5;}.pdtoolbardropdown .pdtoolbarbutton.dropdown-toggle:hover{background-color:var(--bs-secondary-bg,#e9ecef);border-color:var(--bs-border-color-translucent,#adb5bd);}.pdtoolbardropdown .pdtoolbarbutton.dropdown-toggle::after{margin-left:auto;}.pdtoolbardropdown .pdtoolbarbutton.dropdown-toggle>span{display:inline-flex;align-items:center;gap:.5rem;}.pd-dropdown.pd-dropdown .dropdown-menu ul{list-style:none;padding:0;margin:0;}.pd-dropdown.pd-dropdown .dropdown-menu li{list-style:none;padding:0;margin:0;cursor:pointer;transition:background-color .15s ease-in-out;}.pd-dropdown.pd-dropdown .dropdown-menu li:hover{background-color:var(--bs-secondary-bg);}.pd-dropdown.pd-dropdown .dropdown-menu li .pd-label{display:block;padding:.5rem 1rem;color:var(--bs-body-color);text-decoration:none;white-space:nowrap;width:100%;}.pdfileexplorer.pdfileexplorer .pdtree{height:0;}.pdfileexplorer.pdfileexplorer .pdsplitter.horizontal{height:calc(100% - 50px);}.pdfileexplorer.pdfileexplorer .pddropzone.dz-drag-hover{background-color:unset;border-color:transparent;}.pdfileexplorer.pdfileexplorer .pdtablerow:hover td{background-color:var(--bs-tertiary-bg,rgba(128,128,128,.15));}.pdfileexplorer.pdfileexplorer .pdtablerow.selected:hover td{background-color:var(--pd-selected-bg,var(--bs-primary));}.pdfilemodalexplorer.pdfilemodalexplorer .pdfileexplorer{height:100%;}.pdfilemodalexplorer.pdfilemodalexplorer .pdsplitter{flex-grow:1;}.pd-filter.pd-filter .filter-icon{color:var(--bs-primary)!important;}.pd-filter.pd-filter:not(.filtered) .filter-icon{opacity:.3;}.pd-filter.pd-filter .pd-dropdown>button{border:none!important;background:unset!important;}.pd-filter .filter-toolbar .pd-button{flex:1;}.pd-form-body .pdtoggleswitch.disabled{background-color:#e9ecef;}.pd-form-body .fg-1{flex-grow:1;}.pd-form-body .fg-2{flex-grow:2;}.pd-form-body .fg-3{flex-grow:3;}.pd-form-body .fg-4{flex-grow:4;}.pd-form-body .fg-5{flex-grow:5;}.pd-form-body .fg-6{flex-grow:6;}.pd-form-body .fg-7{flex-grow:7;}.pd-form-body .fg-8{flex-grow:8;}.pd-form-body .fg-9{flex-grow:9;}.pd-form-field-editor .monaco-editor-container{display:grid;height:100%;grid-template-columns:minmax(0,auto);}.pd-form-field-editor.editor .form-control{border:none;}.pd-form-field-editor .ms-nullable{margin:0 4px 2px 12px;line-height:.5rem;vertical-align:middle;padding:0;width:16px;}.pd-form-field-editor.editor .pddatetime input{flex-grow:1;}.pd-form-field-editor.editor .pddatetimeoffset input{flex-grow:1;}.pd-form-field-editor.editor .no-resize{resize:none;}.pd-form-field-editor.editor.readonly .view-lines{background-color:#e9ecef;}.pd-form-field-editor.editor.readonly .margin{background-color:#e9ecef;}.pd-form-field-editor.editor.readonly .minimap canvas{background-color:#e9ecef;}.pd-form-field-editor .form-select:disabled{appearance:none;-webkit-appearance:none;-moz-appearance:none;background-image:none!important;}.pd-form-field-editor.editor.readonly .form-control:disabled{border-radius:0!important;}.pd-form-footer.pd-form-footer .pdtoolbar{flex-wrap:nowrap;padding:0;flex-shrink:0;}.pd-monacoeditor .monaco-editor-container{height:100px;}.pdpager .direction-buttons button:first-child{border-radius:.25rem 0 0 .25rem;}.pdpager .direction-buttons button:nth-child(2){border-radius:0;}.pdpager .direction-buttons button:nth-child(4){border-radius:0;}.pdpager .direction-buttons button:nth-child(5){border-radius:0 .25rem .25rem 0;}.pd-studio.pd-studio .pd-studio-editor-container .monaco-editor-container{height:100%!important;}.pd-studio.pd-studio .pd-studio-editor-container .resizable{height:100%!important;}.pdtable .pd-sort{color:var(--bs-primary);}.pdtable .pd-sort .fa-stack{width:20px;font-size:1rem;vertical-align:middle;}.pdtable .pd-sort.fa-sort{opacity:.3;}.pdtable .pd-sort i{vertical-align:middle;}.pd-column-selector.pd-column-selector .pd-dragpanel{border:none;margin:0;padding:0;background:transparent;}.pd-column-selector.pd-column-selector .pd-dragitem{background-color:transparent;border:none;margin:0;padding:.5rem 1rem;min-height:auto;transition:background-color .15s ease-in-out;cursor:pointer;display:flex;align-items:center;gap:.5rem;}.pd-column-selector.pd-column-selector .pd-dragitem:hover{background-color:var(--bs-secondary-bg,#e9ecef);}.pd-column-selector.pd-column-selector .pd-dragitem.dragging{background-color:var(--bs-primary-bg-subtle,#cfe2ff);border-color:var(--bs-primary,#0d6efd);opacity:.6;}.pd-column-selector.pd-column-selector .pd-dragitem input[type=checkbox]{width:1rem;height:1rem;cursor:pointer;flex-shrink:0;}.pd-column-selector.pd-column-selector .pd-dragitem label{margin:0;cursor:pointer;flex:1;user-select:none;color:var(--bs-body-color,#212529);}[data-bs-theme=dark] .pd-column-selector.pd-column-selector .pd-dragitem:hover{background-color:var(--bs-secondary-bg,#343a40);}[data-bs-theme=dark] .pd-column-selector.pd-column-selector .pd-dragitem label{color:var(--bs-body-color,#f8f9fa);}.pd-column-selector.pd-column-selector .pd-dragitem input[type=checkbox]:disabled,.pd-column-selector.pd-column-selector .pd-dragitem input[type=checkbox]:disabled+label{opacity:.5;cursor:not-allowed;}.pd-timeline.pd-timeline svg.disabled{background-color:#d3d3d3;filter:grayscale(1);}.pdtoolbar .pdtoolbaritem{padding:0 .25rem;align-items:center;flex-shrink:0;vertical-align:middle;white-space:nowrap;position:relative;}.pdtoolbar .pdtoolbaritem:not(.pd-hidden,.d-none){display:inline-flex!important;}.pdtoolbar .pdtoolbaritem:has(.pdtoolbartextbox){flex:1 1 200px;min-width:150px;max-width:400px;}.pdtoolbar .pdtoolbaritem:first-child{padding-left:0;}.pdtoolbar .pdtoolbaritem:last-child{padding-right:0;}.pdtoolbar .pdtoolbaritem.align-right{margin-left:auto;flex-shrink:0;}.pdtoolbar .pdtoolbaritem .btn,.pdtoolbar .pdtoolbaritem .dropdown{margin-bottom:0!important;display:inline-flex;vertical-align:middle;align-items:center;flex-shrink:0;}.pdtoolbar .pdtoolbaritem .dropdown-toggle{display:inline-flex;align-items:center;gap:var(--pd-toolbar-dropdown-gap,0);border-radius:var(--bs-border-radius,.375rem);}.pdtoolbar .pdtoolbaritem .dropdown-toggle>.icon+span{padding-left:var(--pd-toolbar-dropdown-label-gap,.25rem);}.pdtoolbar .pdtoolbaritem .dropdown-menu{position:absolute;z-index:1050;}.pdtoolbar .pdtoolbaritem .pdtoolbarbutton{border-radius:var(--bs-border-radius,.375rem);height:100%;white-space:nowrap;}.pdtoolbar .pdtoolbargroup>.pdtoolbaritem{padding:0;}.pdtoolbar .pdtoolbargroup>.pdtoolbaritem:not(:first-child) .btn{border-top-left-radius:0;border-bottom-left-radius:0;margin-left:calc(var(--bs-border-width,1px)*-1);border-left-color:var(--pd-toolbar-group-divider-color,rgba(0,0,0,.3));}.pdtoolbar .pdtoolbargroup>.pdtoolbaritem:not(:last-child) .btn{border-top-right-radius:0;border-bottom-right-radius:0;}@media (max-width:768px){.pdtoolbar .pdtoolbaritem{margin-bottom:.25rem;}.pdtoolbar .pdtoolbaritem:has(.pdtoolbartextbox){flex:1 1 100%;max-width:none;min-width:0;}}.pd-diffpanel-editor .monaco-editor-container{height:100%;}@property --pdchat-progress-seconds{syntax:"";inherits:false;initial-value:0;}@property --pdchat-progress-minutes{syntax:"";inherits:false;initial-value:0;}
\ No newline at end of file
+.cursor-pointer{cursor:pointer;}.fa-hidden{color:transparent;}.pd-pointer{cursor:pointer;}.pd-hidden{display:none!important;}.pd-empty-icon{height:16px;width:16px;}.resize-h{overflow:auto;resize:vertical;}.resize-w{overflow:auto;resize:horizontal;}.h-100{height:100px!important;}.h-200{height:200px!important;}.h-300{height:300px!important;}.h-400{height:400px!important;}.h-500{height:500px!important;}.h-100-pct{height:100%;}.w-100-pct{width:100%;}.w-100-px{width:100px;}.w-125-px{width:125px;}.w-150-px{width:150px;}.w-200-px{width:200px;}.mh-50-px{min-height:50px;}.mh-100-px{min-height:100px;}.mh-150-px{min-height:150px;}.mh-200-px{min-height:200px;}.mh-250-px{min-height:250px;}.mh-300-px{min-height:300px;}.mw-100-px{min-width:100px;}.mw-125-px{min-width:125px;}.mw-150-px{min-width:150px;}.mw-200-px{min-width:200px;}.text-small{font-size:.75rem;}.text-tiny{font-size:.5rem;}.z-1000{z-index:1000;}.z-1100{z-index:1100;}.z-1200{z-index:1200;}.pd-form-body .title-box{width:var(--title-width,200px)!important;min-width:var(--title-width,200px)!important;max-width:var(--title-width,200px)!important;flex:0 0 var(--title-width,200px)!important;}.pdsplitter .gutter{background-color:var(--pd-splitter-gutter-bg,#eee);transition:background-color .1s ease-out;}.pdsplitter .gutter-vertical{cursor:row-resize;}.pdsplitter .gutter-horizontal{cursor:col-resize;}.pd-studio .pdsplitter>.gutter{background-color:var(--pd-studio-gutter-bg,var(--pd-splitter-gutter-bg,var(--bs-tertiary-bg,#eee)));}.pdsplitter .gutter:active,.pdsplitter .gutter:hover{background-color:var(--pd-splitter-gutter-hover-bg,var(--bs-primary,#0d6efd));transition:background-color .15s ease-in var(--pd-splitter-gutter-hover-delay,.2s);}.pdsplitter .gutter:active{transition-delay:0s;}.pd-studio .pdtoolbar.pd-studio-toolbar{padding:var(--pd-studio-toolbar-padding,.375rem var(--_pds-toolbar-padding-x,0));border-bottom:var(--pd-studio-toolbar-border-bottom,1px solid var(--_pds-border-color));}.pd-studio .pdtoolbar.pd-studio-toolbar{column-gap:.75rem;}.pd-studio .pdtoolbar.pd-studio-toolbar>.pdtoolbaritem{padding-left:0;padding-right:0;}.pd-studio .pdtoolbar.pd-studio-toolbar .pdts-bar{border-left-color:var(--pd-studio-separator-color,var(--bs-secondary,#6c757d));}.pd-studio .pdtoolbarbutton.pd-studio-execute{min-width:var(--pd-studio-execute-min-width,5.5rem)}.pd-studio .pd-studio-editor-container .pd-monacoeditor{border:none;}.pdfilepreview .stacked{align-items:center;display:flex;flex-direction:column;text-align:center;}.pdfilepreview .stacked span{margin-top:4px;}.pdfilepreview .h1,.pdfilepreview .h4,.pdfilepreview h2,.pdfilepreview h3{margin:0!important;padding:0!important;}.pdchat-message .pdchat-partial blockquote,.pdchat-message .pdchat-partial h1,.pdchat-message .pdchat-partial h2,.pdchat-message .pdchat-partial h3,.pdchat-message .pdchat-partial h4,.pdchat-message .pdchat-partial h5,.pdchat-message .pdchat-partial h6,.pdchat-message .pdchat-partial li,.pdchat-message .pdchat-partial ol,.pdchat-message .pdchat-partial p,.pdchat-message .pdchat-partial table,.pdchat-message .pdchat-partial ul,.pdchat-message .pdchat-text blockquote,.pdchat-message .pdchat-text h1,.pdchat-message .pdchat-text h2,.pdchat-message .pdchat-text h3,.pdchat-message .pdchat-text h4,.pdchat-message .pdchat-text h5,.pdchat-message .pdchat-text h6,.pdchat-message .pdchat-text li,.pdchat-message .pdchat-text ol,.pdchat-message .pdchat-text p,.pdchat-message .pdchat-text table,.pdchat-message .pdchat-text ul{white-space:normal;}.pdchat-message .pdchat-partial p,.pdchat-message .pdchat-text p{margin:0 0 .5em;}.pdchat-message .pdchat-partial ol,.pdchat-message .pdchat-partial ul,.pdchat-message .pdchat-text ol,.pdchat-message .pdchat-text ul{margin:.25em 0 .5em;padding-left:1.5em;}.pdchat-message .pdchat-partial li+li,.pdchat-message .pdchat-text li+li{margin-top:.15em;}.pdchat-message .pdchat-partial li>p,.pdchat-message .pdchat-text li>p{margin:0;}.pdchat-message .pdchat-partial ol:last-child,.pdchat-message .pdchat-partial p:last-child,.pdchat-message .pdchat-partial ul:last-child,.pdchat-message .pdchat-text ol:last-child,.pdchat-message .pdchat-text p:last-child,.pdchat-message .pdchat-text ul:last-child{margin-bottom:0;}.pdchat-message .pdchat-partial table{border-collapse:collapse;font-size:.95em;}.pdchat-message .pdchat-partial td,.pdchat-message .pdchat-partial th{border:1px solid var(--pd-chat-border,#dee2e6);padding:.15rem .4rem;text-align:left;}.pdchat-container .pdchat-conversation-main .pdchat-conversation-tabset{display:flex;flex-direction:column;flex:1 1 auto;min-height:0;}.pdchat-container .pdchat-conversation-main .pdchat-conversation-tabset .pdtabset-content{flex:1 1 auto;min-height:0;display:flex;flex-direction:column;overflow:hidden;}.pdchat-container .pdchat-conversation-main .pdtabset-tab.pdchat-conversation-tab-unread::before{content:"\25CF";margin-right:4px;font-size:.625rem;line-height:1;color:var(--pd-chat-info-border,#0078d4);}.pdchat-container .pdchat-conversation-main .pdtabset-tab.pdchat-conversation-tab-unread .pdtabset-tab-title{font-weight:600;}.pdchat-container-wrapper.pdchat-container-wrapper.pdchat-split-inactive .pdchat-chat-panel{flex:0 0 0%!important;width:0!important;min-width:0!important;padding:0!important;border:none!important;overflow:hidden!important;}.pdchat-container-wrapper.pdchat-container-wrapper.pdchat-split-inactive .pdchat-content-panel{flex:1 1 100%!important;width:100%!important;}.pdchat-container-wrapper.pdchat-container-wrapper.pdchat-split-inactive .pdchat-container-splitter>.gutter{display:none!important;width:0!important;min-width:0!important;}.pdchat-container-wrapper.pdchat-container-wrapper .pdchat-container.dock-left,.pdchat-container-wrapper.pdchat-container-wrapper .pdchat-container.dock-right{position:static!important;transform:none!important;top:auto!important;left:auto!important;right:auto!important;bottom:auto!important;width:100%!important;height:100%!important;z-index:auto!important;}.pdchat-container-wrapper.pdchat-container-wrapper .pdchat-window{width:100%!important;height:100%!important;max-width:none!important;max-height:none!important;border-radius:0!important;box-shadow:none!important;}@media(max-width:767px){.pdchat-container-wrapper.pdchat-container-wrapper:not(.pdchat-split-inactive) .pdchat-chat-panel{position:fixed!important;inset:0!important;width:100%!important;height:100%!important;z-index:10000!important;}.pdchat-container-wrapper.pdchat-container-wrapper:not(.pdchat-split-inactive) .pdchat-content-panel{flex:1 1 100%!important;width:100%!important;}.pdchat-container-wrapper.pdchat-container-wrapper:not(.pdchat-split-inactive) .pdchat-container-splitter>.gutter{display:none!important;}}.pd-list .filter input{border-top-right-radius:0!important;border-bottom-right-radius:0!important;}.pd-list .filter .input-group-text{border-top-left-radius:0!important;border-bottom-left-radius:0!important;}.pdtoolbartextbox.pdtoolbartextbox input{min-width:0;width:100%;}.pdtoolbarbuttonitem .pdtoolbarbutton{white-space:nowrap;}.pdtoolbardropdown .pdtoolbarbutton{white-space:nowrap;border-radius:var(--bs-border-radius,.375rem);display:inline-flex;align-items:center;line-height:1.5;}.pdtoolbardropdown .pdtoolbarbutton.dropdown-toggle{padding:.375rem .75rem;border:1px solid var(--bs-border-color,#dee2e6);border-radius:var(--bs-border-radius,.375rem);background-color:var(--bs-body-bg,#fff);color:var(--bs-body-color,#212529);transition:all .15s ease-in-out;display:inline-flex;align-items:center;gap:.5rem;line-height:1.5;}.pdtoolbardropdown .pdtoolbarbutton.dropdown-toggle:hover{background-color:var(--bs-secondary-bg,#e9ecef);border-color:var(--bs-border-color-translucent,#adb5bd);}.pdtoolbardropdown .pdtoolbarbutton.dropdown-toggle::after{margin-left:auto;}.pdtoolbardropdown .pdtoolbarbutton.dropdown-toggle>span{display:inline-flex;align-items:center;gap:.5rem;}.pd-dropdown.pd-dropdown .dropdown-menu ul{list-style:none;padding:0;margin:0;}.pd-dropdown.pd-dropdown .dropdown-menu li{list-style:none;padding:0;margin:0;cursor:pointer;transition:background-color .15s ease-in-out;}.pd-dropdown.pd-dropdown .dropdown-menu li:hover{background-color:var(--bs-secondary-bg);}.pd-dropdown.pd-dropdown .dropdown-menu li .pd-label{display:block;padding:.5rem 1rem;color:var(--bs-body-color);text-decoration:none;white-space:nowrap;width:100%;}.pdfileexplorer.pdfileexplorer .pdtree{height:0;}.pdfileexplorer.pdfileexplorer .pdsplitter.horizontal{height:calc(100% - 50px);}.pdfileexplorer.pdfileexplorer.pdfe-no-toolbar .pdsplitter.horizontal{height:100%;}.pdfileexplorer.pdfileexplorer .pddropzone.dz-drag-hover{background-color:unset;border-color:transparent;}.pdfileexplorer.pdfileexplorer .pdtablerow:hover td{background-color:var(--bs-tertiary-bg,rgba(128,128,128,.15));}.pdfileexplorer.pdfileexplorer .pdtablerow.selected:hover td{background-color:var(--pd-selected-bg,var(--bs-primary));}.pdfilemodalexplorer.pdfilemodalexplorer .pdfileexplorer{height:100%;}.pdfilemodalexplorer.pdfilemodalexplorer .pdsplitter{flex-grow:1;}.pd-filter.pd-filter .filter-icon{color:var(--bs-primary)!important;}.pd-filter.pd-filter:not(.filtered) .filter-icon{opacity:.3;}.pd-filter.pd-filter .pd-dropdown>button{border:none!important;background:unset!important;}.pd-filter .filter-toolbar .pd-button{flex:1;}.pd-form-body .pdtoggleswitch.disabled{background-color:#e9ecef;}.pd-form-body .fg-1{flex-grow:1;}.pd-form-body .fg-2{flex-grow:2;}.pd-form-body .fg-3{flex-grow:3;}.pd-form-body .fg-4{flex-grow:4;}.pd-form-body .fg-5{flex-grow:5;}.pd-form-body .fg-6{flex-grow:6;}.pd-form-body .fg-7{flex-grow:7;}.pd-form-body .fg-8{flex-grow:8;}.pd-form-body .fg-9{flex-grow:9;}.pd-form-field-editor .monaco-editor-container{display:grid;height:100%;grid-template-columns:minmax(0,auto);}.pd-form-field-editor.editor .form-control{border:none;}.pd-form-field-editor .ms-nullable{margin:0 4px 2px 12px;line-height:.5rem;vertical-align:middle;padding:0;width:16px;}.pd-form-field-editor.editor .pddatetime input{flex-grow:1;}.pd-form-field-editor.editor .pddatetimeoffset input{flex-grow:1;}.pd-form-field-editor.editor .no-resize{resize:none;}.pd-form-field-editor.editor.readonly .view-lines{background-color:#e9ecef;}.pd-form-field-editor.editor.readonly .margin{background-color:#e9ecef;}.pd-form-field-editor.editor.readonly .minimap canvas{background-color:#e9ecef;}.pd-form-field-editor .form-select:disabled{appearance:none;-webkit-appearance:none;-moz-appearance:none;background-image:none!important;}.pd-form-field-editor.editor.readonly .form-control:disabled{border-radius:0!important;}.pd-form-footer.pd-form-footer .pdtoolbar{flex-wrap:nowrap;padding:0;flex-shrink:0;}.pd-monacoeditor .monaco-editor-container{height:100px;}.pdpager .direction-buttons button:first-child{border-radius:.25rem 0 0 .25rem;}.pdpager .direction-buttons button:nth-child(2){border-radius:0;}.pdpager .direction-buttons button:nth-child(4){border-radius:0;}.pdpager .direction-buttons button:nth-child(5){border-radius:0 .25rem .25rem 0;}.pd-studio.pd-studio .pd-studio-editor-container .monaco-editor-container{height:100%!important;}.pd-studio.pd-studio .pd-studio-editor-container .resizable{height:100%!important;}.pdtable .pd-sort{color:var(--bs-primary);}.pdtable .pd-sort .fa-stack{width:20px;font-size:1rem;vertical-align:middle;}.pdtable .pd-sort.fa-sort{opacity:.3;}.pdtable .pd-sort i{vertical-align:middle;}.pd-column-selector.pd-column-selector .pd-dragpanel{border:none;margin:0;padding:0;background:transparent;}.pd-column-selector.pd-column-selector .pd-dragitem{background-color:transparent;border:none;margin:0;padding:.5rem 1rem;min-height:auto;transition:background-color .15s ease-in-out;cursor:pointer;display:flex;align-items:center;gap:.5rem;}.pd-column-selector.pd-column-selector .pd-dragitem:hover{background-color:var(--bs-secondary-bg,#e9ecef);}.pd-column-selector.pd-column-selector .pd-dragitem.dragging{background-color:var(--bs-primary-bg-subtle,#cfe2ff);border-color:var(--bs-primary,#0d6efd);opacity:.6;}.pd-column-selector.pd-column-selector .pd-dragitem input[type=checkbox]{width:1rem;height:1rem;cursor:pointer;flex-shrink:0;}.pd-column-selector.pd-column-selector .pd-dragitem label{margin:0;cursor:pointer;flex:1;user-select:none;color:var(--bs-body-color,#212529);}[data-bs-theme=dark] .pd-column-selector.pd-column-selector .pd-dragitem:hover{background-color:var(--bs-secondary-bg,#343a40);}[data-bs-theme=dark] .pd-column-selector.pd-column-selector .pd-dragitem label{color:var(--bs-body-color,#f8f9fa);}.pd-column-selector.pd-column-selector .pd-dragitem input[type=checkbox]:disabled,.pd-column-selector.pd-column-selector .pd-dragitem input[type=checkbox]:disabled+label{opacity:.5;cursor:not-allowed;}.pd-timeline.pd-timeline svg.disabled{background-color:#d3d3d3;filter:grayscale(1);}.pdtoolbar .pdtoolbaritem{padding:0 .25rem;align-items:center;flex-shrink:0;vertical-align:middle;white-space:nowrap;position:relative;}.pdtoolbar .pdtoolbaritem:not(.pd-hidden,.d-none){display:inline-flex!important;}.pdtoolbar .pdtoolbaritem:has(.pdtoolbartextbox){flex:1 1 200px;min-width:150px;max-width:400px;}.pdtoolbar .pdtoolbaritem:first-child{padding-left:0;}.pdtoolbar .pdtoolbaritem:last-child{padding-right:0;}.pdtoolbar .pdtoolbaritem.align-right{margin-left:auto;flex-shrink:0;}.pdtoolbar .pdtoolbaritem .btn,.pdtoolbar .pdtoolbaritem .dropdown{margin-bottom:0!important;display:inline-flex;vertical-align:middle;align-items:center;flex-shrink:0;}.pdtoolbar .pdtoolbaritem .dropdown-toggle{display:inline-flex;align-items:center;gap:var(--pd-toolbar-dropdown-gap,0);border-radius:var(--bs-border-radius,.375rem);}.pdtoolbar .pdtoolbaritem .dropdown-toggle>.icon+span{padding-left:var(--pd-toolbar-dropdown-label-gap,.25rem);}.pdtoolbar .pdtoolbaritem .dropdown-menu{position:absolute;z-index:1050;}.pdtoolbar .pdtoolbaritem .pdtoolbarbutton{border-radius:var(--bs-border-radius,.375rem);height:100%;white-space:nowrap;}.pdtoolbar .pdtoolbargroup>.pdtoolbaritem{padding:0;}.pdtoolbar .pdtoolbargroup>.pdtoolbaritem:not(:first-child) .btn{border-top-left-radius:0;border-bottom-left-radius:0;margin-left:calc(var(--bs-border-width,1px)*-1);border-left-color:var(--pd-toolbar-group-divider-color,rgba(0,0,0,.3));}.pdtoolbar .pdtoolbargroup>.pdtoolbaritem:not(:last-child) .btn{border-top-right-radius:0;border-bottom-right-radius:0;}@media (max-width:768px){.pdtoolbar .pdtoolbaritem{margin-bottom:.25rem;}.pdtoolbar .pdtoolbaritem:has(.pdtoolbartextbox){flex:1 1 100%;max-width:none;min-width:0;}}.pd-diffpanel-editor .monaco-editor-container{height:100%;}@property --pdchat-progress-seconds{syntax:"";inherits:false;initial-value:0;}@property --pdchat-progress-minutes{syntax:"";inherits:false;initial-value:0;}
\ No newline at end of file
diff --git a/PanoramicData.Blazor/wwwroot/css/main.scss b/PanoramicData.Blazor/wwwroot/css/main.scss
index 0dfb77dfb..48487f9bc 100644
--- a/PanoramicData.Blazor/wwwroot/css/main.scss
+++ b/PanoramicData.Blazor/wwwroot/css/main.scss
@@ -537,6 +537,10 @@
height: calc(100% - 50px);
}
+.pdfileexplorer.pdfileexplorer.pdfe-no-toolbar .pdsplitter.horizontal {
+ height: 100%;
+}
+
/* Suppress the whole-panel Dropzone.js hover highlight on the tree side;
individual node highlighting is handled by PDTreeNode.drag-over instead. */
.pdfileexplorer.pdfileexplorer .pddropzone.dz-drag-hover {