mirror of
https://github.com/awatertrevi/xamarin-neo4j.git
synced 2026-09-22 09:05:29 +00:00
Session: redesign result cards, saved queries always visible, collapsible sections
- Move all action buttons (star, expand, JSON, trash) into each result card header - Always show query text in result cards (no conditional hiding) - Saved queries section permanently visible above results, collapsible via chevron - Result cards collapsible via per-card chevron toggle - Add drop shadow and remove border around query text - Clear query field after execute; tap result query label to reload it into editor - WebView re-renders HTML on App.ThemeChanged for instant dark/light switch - Result card trash, delete result, save/load query, open graph/table all work per-card Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,28 +2,28 @@
|
|||||||
|
|
||||||
<ContentView xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
<ContentView xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||||
xmlns:fonts="clr-namespace:Xamarin.Neo4j.Fonts;assembly=Xamarin.Neo4j"
|
x:Class="Xamarin.Neo4j.Controls.QueryResultView"
|
||||||
Margin="0, 0, 0, 25"
|
x:Name="self">
|
||||||
x:Class="Xamarin.Neo4j.Controls.QueryResultView">
|
<Grid VerticalOptions="Start">
|
||||||
<StackLayout Orientation="Vertical" Spacing="0">
|
|
||||||
<Border Stroke="{StaticResource QueryTextBorder}" StrokeThickness="4">
|
<!-- Graph WebView -->
|
||||||
<Label Text="{Binding Query}" Padding="8" HorizontalOptions="FillAndExpand" BackgroundColor="{StaticResource Extreme}" />
|
<WebView x:Name="graphView"
|
||||||
|
HorizontalOptions="FillAndExpand"
|
||||||
|
IsVisible="{Binding CanDisplayGraph}"
|
||||||
|
HeightRequest="{Binding GraphViewHeight, Source={x:Reference self}}" />
|
||||||
|
|
||||||
|
<!-- Error display -->
|
||||||
|
<Border IsVisible="{Binding IsError}"
|
||||||
|
BackgroundColor="#ffebee"
|
||||||
|
StrokeThickness="0"
|
||||||
|
Padding="12">
|
||||||
|
<StackLayout Spacing="6">
|
||||||
|
<Label Text="{Binding ErrorMessage}"
|
||||||
|
TextColor="#c62828"
|
||||||
|
FontSize="14"
|
||||||
|
FontFamily="RobotoMono" />
|
||||||
|
</StackLayout>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<WebView x:Name="graphView" HorizontalOptions="FillAndExpand" IsVisible="{Binding CanDisplayGraph}" HeightRequest="200" />
|
|
||||||
|
|
||||||
<Grid BackgroundColor="{StaticResource QueryActionBar}" Padding="5">
|
|
||||||
<Grid.ColumnDefinitions>
|
|
||||||
<ColumnDefinition Width="1*" />
|
|
||||||
<ColumnDefinition Width="1*" />
|
|
||||||
<ColumnDefinition Width="1*" />
|
|
||||||
<ColumnDefinition Width="1*" />
|
|
||||||
</Grid.ColumnDefinitions>
|
|
||||||
|
|
||||||
<Button Grid.Column="0" Clicked="SaveQuery" Text="{x:Static fonts:FontAwesomeSolid.Star}" BackgroundColor="Transparent" TextColor="White" FontFamily="{StaticResource FontAwesomeSolid}" FontSize="20" />
|
|
||||||
<Button Grid.Column="1" Clicked="OpenNeovis" Text="{x:Static fonts:FontAwesomeSolid.Expand}" BackgroundColor="Transparent" IsEnabled="{Binding CanDisplayGraph}" TextColor="White" FontFamily="{StaticResource FontAwesomeSolid}" FontSize="20 "/>
|
|
||||||
<Button Grid.Column="2" Clicked="OpenTableView" Text="{x:Static fonts:FontAwesomeSolid.List}" BackgroundColor="Transparent" TextColor="White" FontFamily="{StaticResource FontAwesomeSolid}" FontSize="20 "/>
|
|
||||||
<Button Grid.Column="3" Clicked="CloseResultView" Text="{x:Static fonts:FontAwesomeSolid.Times}" BackgroundColor="Transparent" TextColor="White" FontFamily="{StaticResource FontAwesomeSolid}" FontSize="20 "/>
|
|
||||||
</Grid>
|
</Grid>
|
||||||
</StackLayout>
|
|
||||||
</ContentView>
|
</ContentView>
|
||||||
|
|||||||
@@ -1,43 +1,49 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using Microsoft.Maui.ApplicationModel;
|
using Microsoft.Maui.ApplicationModel;
|
||||||
using Microsoft.Maui.Controls;
|
using Microsoft.Maui.Controls;
|
||||||
using Microsoft.Maui.Controls.Xaml;
|
using Microsoft.Maui.Controls.Xaml;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Maui;
|
using Microsoft.Maui;
|
||||||
using Neo4j.Driver;
|
|
||||||
using Xamarin.Neo4j.Managers;
|
using Xamarin.Neo4j.Managers;
|
||||||
using Xamarin.Neo4j.Services;
|
using Xamarin.Neo4j.Services;
|
||||||
using Xamarin.Neo4j.Models;
|
using Xamarin.Neo4j.Models;
|
||||||
using Xamarin.Neo4j.Pages;
|
|
||||||
using Xamarin.Neo4j.Utilities;
|
using Xamarin.Neo4j.Utilities;
|
||||||
using Query = Xamarin.Neo4j.Models.Query;
|
|
||||||
|
|
||||||
namespace Xamarin.Neo4j.Controls
|
namespace Xamarin.Neo4j.Controls
|
||||||
{
|
{
|
||||||
[XamlCompilation(XamlCompilationOptions.Compile)]
|
[XamlCompilation(XamlCompilationOptions.Compile)]
|
||||||
public partial class QueryResultView : ContentView
|
public partial class QueryResultView : ContentView
|
||||||
{
|
{
|
||||||
private QueryResult QueryResult => (QueryResult) BindingContext;
|
public static readonly BindableProperty GraphViewHeightProperty =
|
||||||
|
BindableProperty.Create(nameof(GraphViewHeight), typeof(double), typeof(QueryResultView), 300.0);
|
||||||
|
|
||||||
public event EventHandler<GenericEventArgs<QueryResult>> CloseRequested;
|
public double GraphViewHeight
|
||||||
|
{
|
||||||
|
get => (double)GetValue(GraphViewHeightProperty);
|
||||||
|
set => SetValue(GraphViewHeightProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
private string _neovisHtml;
|
private QueryResult QueryResult => (QueryResult)BindingContext;
|
||||||
|
|
||||||
public QueryResultView()
|
public QueryResultView()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
BindingContextChanged += OnBindingContextChanged;
|
BindingContextChanged += OnBindingContextChanged;
|
||||||
graphView.Navigating += OnGraphViewNavigating;
|
graphView.Navigating += OnGraphViewNavigating;
|
||||||
|
App.ThemeChanged += OnThemeChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnThemeChanged(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
if (QueryResult == null || graphView == null) return;
|
||||||
|
ParseNeovisHtmlSafe();
|
||||||
|
graphView.Source = new HtmlWebViewSource { Html = QueryResult?.NeovisHtml };
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void OnGraphViewNavigating(object sender, WebNavigatingEventArgs e)
|
private async void OnGraphViewNavigating(object sender, WebNavigatingEventArgs e)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"[Graph] Inline Navigating: {e.Url}");
|
|
||||||
|
|
||||||
if (!e.Url.Contains("expand") || !e.Url.Contains("nodeId")) return;
|
if (!e.Url.Contains("expand") || !e.Url.Contains("nodeId")) return;
|
||||||
|
|
||||||
e.Cancel = true;
|
e.Cancel = true;
|
||||||
@@ -69,19 +75,11 @@ namespace Xamarin.Neo4j.Controls
|
|||||||
|
|
||||||
private void OnBindingContextChanged(object sender, EventArgs e)
|
private void OnBindingContextChanged(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
if (BindingContext == null || graphView == null)
|
if (BindingContext == null || graphView == null) return;
|
||||||
{
|
|
||||||
Console.WriteLine($"[Graph] OnBindingContextChanged skipped: BindingContext={BindingContext}, graphView={graphView}");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Console.WriteLine($"[Graph] OnBindingContextChanged fired, CanDisplayGraph={QueryResult?.CanDisplayGraph}");
|
|
||||||
|
|
||||||
ParseNeovisHtmlSafe();
|
ParseNeovisHtmlSafe();
|
||||||
|
|
||||||
Console.WriteLine($"[Graph] Setting graphView.Source, html length={_neovisHtml?.Length ?? 0}");
|
graphView.Source = new HtmlWebViewSource { Html = QueryResult?.NeovisHtml };
|
||||||
|
|
||||||
graphView.Source = new HtmlWebViewSource { Html = _neovisHtml };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ParseNeovisHtml()
|
private void ParseNeovisHtml()
|
||||||
@@ -89,83 +87,46 @@ namespace Xamarin.Neo4j.Controls
|
|||||||
var assembly = Assembly.GetExecutingAssembly();
|
var assembly = Assembly.GetExecutingAssembly();
|
||||||
var resourceName = "Xamarin.Neo4j.Visualization.visgraph.html";
|
var resourceName = "Xamarin.Neo4j.Visualization.visgraph.html";
|
||||||
|
|
||||||
var available = string.Join("\n", assembly.GetManifestResourceNames());
|
|
||||||
Console.WriteLine($"[Graph] Looking for: {resourceName}");
|
|
||||||
Console.WriteLine($"[Graph] Available resources: {available.Replace("\n", ", ")}");
|
|
||||||
|
|
||||||
var stream = assembly.GetManifestResourceStream(resourceName);
|
var stream = assembly.GetManifestResourceStream(resourceName);
|
||||||
if (stream == null)
|
if (stream == null)
|
||||||
{
|
{
|
||||||
_neovisHtml = $"<html><body style='background:#111;color:#f55;font-family:monospace;padding:16px'>" +
|
var available = string.Join(", ", assembly.GetManifestResourceNames());
|
||||||
|
QueryResult.NeovisHtml = $"<html><body style='background:#111;color:#f55;font-family:monospace;padding:16px'>" +
|
||||||
$"<b>Resource not found:</b><br>{resourceName}<br><br>" +
|
$"<b>Resource not found:</b><br>{resourceName}<br><br>" +
|
||||||
$"<b>Available:</b><br>{available.Replace("\n", "<br>")}</body></html>";
|
$"<b>Available:</b><br>{available}</body></html>";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
using (stream)
|
using (stream)
|
||||||
using (var reader = new StreamReader(stream))
|
using (var reader = new StreamReader(stream))
|
||||||
{
|
{
|
||||||
var result = reader.ReadToEnd();
|
var html = reader.ReadToEnd();
|
||||||
var connectionId = ConnectionStringManager.ActiveConnectionString?.Id ?? Guid.Empty;
|
var connectionId = ConnectionStringManager.ActiveConnectionString?.Id ?? Guid.Empty;
|
||||||
|
|
||||||
var (nodesJson, edgesJson) = GraphDataHelper.BuildJson(QueryResult.Results, connectionId);
|
var (nodesJson, edgesJson) = GraphDataHelper.BuildJson(QueryResult.Results, connectionId);
|
||||||
|
|
||||||
var isDark = Application.Current.RequestedTheme == AppTheme.Dark;
|
var isDark = Application.Current.RequestedTheme == AppTheme.Dark;
|
||||||
result = result.Replace("{{nodes}}", nodesJson);
|
html = html.Replace("{{nodes}}", nodesJson);
|
||||||
result = result.Replace("{{edges}}", edgesJson);
|
html = html.Replace("{{edges}}", edgesJson);
|
||||||
result = result.Replace("{{backgroundColor}}", isDark ? "#292C31" : "#FFFFFF");
|
html = html.Replace("{{backgroundColor}}", isDark ? "#292C31" : "#FFFFFF");
|
||||||
result = result.Replace("{{textColor}}", isDark ? "#FFFFFF" : "#000000");
|
html = html.Replace("{{textColor}}", isDark ? "#FFFFFF" : "#000000");
|
||||||
|
|
||||||
_neovisHtml = result;
|
QueryResult.NeovisHtml = html;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ParseNeovisHtmlSafe()
|
private void ParseNeovisHtmlSafe()
|
||||||
{
|
{
|
||||||
|
if (QueryResult == null) return;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
ParseNeovisHtml();
|
ParseNeovisHtml();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"[Graph] ParseNeovisHtml threw: {ex.GetType().Name}: {ex.Message}");
|
QueryResult.NeovisHtml = $"<html><body style='background:#111;color:#f55;font-family:monospace;padding:16px'>" +
|
||||||
_neovisHtml = $"<html><body style='background:#111;color:#f55;font-family:monospace;padding:16px'>" +
|
$"<b>{ex.GetType().Name}</b><br>{ex.Message}</body></html>";
|
||||||
$"<b>{ex.GetType().Name}</b><br>{ex.Message}<br><br>{ex.StackTrace?.Replace("\n", "<br>")}</body></html>";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void OpenNeovis(object sender, EventArgs e)
|
|
||||||
{
|
|
||||||
var neo4jService = IPlatformApplication.Current.Services.GetRequiredService<Neo4jService>();
|
|
||||||
var connectionString = ConnectionStringManager.ActiveConnectionString;
|
|
||||||
await Application.Current.MainPage.Navigation.PushAsync(new GraphPage(_neovisHtml, connectionString, neo4jService));
|
|
||||||
}
|
|
||||||
|
|
||||||
private async void SaveQuery(object sender, EventArgs e)
|
|
||||||
{
|
|
||||||
var name = await Application.Current.MainPage.DisplayPromptAsync("Save Query", "How should the query be called?");
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(name))
|
|
||||||
{
|
|
||||||
var query = new Query()
|
|
||||||
{
|
|
||||||
Id = Guid.NewGuid(),
|
|
||||||
QueryText = QueryResult.Query,
|
|
||||||
Name = name,
|
|
||||||
};
|
|
||||||
|
|
||||||
SavedQueryManager.AddSavedQuery(query);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void CloseResultView(object sender, EventArgs e)
|
|
||||||
{
|
|
||||||
CloseRequested?.Invoke(sender, new GenericEventArgs<QueryResult>(QueryResult));
|
|
||||||
}
|
|
||||||
|
|
||||||
private async void OpenTableView(object sender, EventArgs e)
|
|
||||||
{
|
|
||||||
await Application.Current.MainPage.Navigation.PushAsync(new TablePage(QueryResult));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,5 +31,10 @@ namespace Xamarin.Neo4j.Models
|
|||||||
public Neo4jConnectionString ConnectionString { get; set; }
|
public Neo4jConnectionString ConnectionString { get; set; }
|
||||||
|
|
||||||
public Dictionary<string, List<object>> Results { get; set; }
|
public Dictionary<string, List<object>> Results { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Cached NeoVis HTML generated by QueryResultView; set when the result is first rendered.</summary>
|
||||||
|
public string NeovisHtml { get; set; }
|
||||||
|
|
||||||
|
public bool IsError => !Success;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,8 @@
|
|||||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||||
xmlns:fonts="clr-namespace:Xamarin.Neo4j.Fonts;assembly=Xamarin.Neo4j"
|
xmlns:fonts="clr-namespace:Xamarin.Neo4j.Fonts;assembly=Xamarin.Neo4j"
|
||||||
xmlns:controls="clr-namespace:Xamarin.Neo4j.Controls;assembly=Xamarin.Neo4j"
|
xmlns:controls="clr-namespace:Xamarin.Neo4j.Controls;assembly=Xamarin.Neo4j"
|
||||||
x:Class="Xamarin.Neo4j.Pages.SessionPage">
|
x:Class="Xamarin.Neo4j.Pages.SessionPage"
|
||||||
|
x:Name="sessionPage">
|
||||||
<NavigationPage.TitleView>
|
<NavigationPage.TitleView>
|
||||||
<StackLayout Spacing="0">
|
<StackLayout Spacing="0">
|
||||||
<Label TextColor="White" HorizontalOptions="CenterAndExpand" VerticalOptions="CenterAndExpand">
|
<Label TextColor="White" HorizontalOptions="CenterAndExpand" VerticalOptions="CenterAndExpand">
|
||||||
@@ -35,40 +36,202 @@
|
|||||||
</ContentPage.ToolbarItems>
|
</ContentPage.ToolbarItems>
|
||||||
|
|
||||||
<ContentPage.Content>
|
<ContentPage.Content>
|
||||||
<StackLayout VerticalOptions="FillAndExpand">
|
<Grid RowDefinitions="Auto,*">
|
||||||
<Border BackgroundColor="White" StrokeShape="RoundRectangle 2" Padding="4" Margin="16">
|
|
||||||
|
<!-- Row 0: Query editor -->
|
||||||
|
<Border Grid.Row="0" BackgroundColor="White" StrokeShape="RoundRectangle 2" Padding="4" Margin="16">
|
||||||
<Border Stroke="#d8e5f1" StrokeThickness="2" StrokeShape="RoundRectangle 2">
|
<Border Stroke="#d8e5f1" StrokeThickness="2" StrokeShape="RoundRectangle 2">
|
||||||
<controls:QueryEditor Text="{Binding Query}" ExecuteClicked="ExecuteQuery" FontSize="14" MaxHeight="200" HorizontalOptions="FillAndExpand" AutoSize="TextChanges" />
|
<controls:QueryEditor Text="{Binding Query}" ExecuteClicked="ExecuteQuery" FontSize="14" MaxHeight="200" HorizontalOptions="FillAndExpand" AutoSize="TextChanges" />
|
||||||
</Border>
|
</Border>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<Grid VerticalOptions="FillAndExpand">
|
<!-- Row 1: Single scroll area — results then saved queries always visible -->
|
||||||
<CollectionView x:Name="resultsCollection" ItemsSource="{Binding QueryResults}" VerticalOptions="FillAndExpand">
|
<ScrollView Grid.Row="1" x:Name="mainScroll">
|
||||||
<CollectionView.ItemTemplate>
|
<StackLayout Spacing="0" Padding="0,0,0,16">
|
||||||
<DataTemplate>
|
|
||||||
<controls:QueryResultView CloseRequested="CloseResultView" />
|
|
||||||
</DataTemplate>
|
|
||||||
</CollectionView.ItemTemplate>
|
|
||||||
</CollectionView>
|
|
||||||
|
|
||||||
<StackLayout IsVisible="{Binding IsEmpty}" VerticalOptions="Center" HorizontalOptions="Center" Spacing="12" Padding="32">
|
<!-- Saved queries section — collapsible, always above results -->
|
||||||
|
<Grid ColumnDefinitions="*,Auto" Padding="16,10,4,4">
|
||||||
|
<Label Grid.Column="0"
|
||||||
|
Text="Saved Queries"
|
||||||
|
FontSize="12" FontAttributes="Bold"
|
||||||
|
TextColor="{DynamicResource SecondaryTextColor}"
|
||||||
|
VerticalOptions="Center" />
|
||||||
|
<Button Grid.Column="1"
|
||||||
|
x:Name="savedQueriesChevron"
|
||||||
|
Text="{x:Static fonts:FontAwesomeSolid.ChevronUp}"
|
||||||
|
FontFamily="{StaticResource FontAwesomeSolid}" FontSize="11"
|
||||||
|
BackgroundColor="Transparent"
|
||||||
|
TextColor="{DynamicResource SecondaryTextColor}"
|
||||||
|
WidthRequest="40" HeightRequest="32"
|
||||||
|
Clicked="ToggleSavedQueries" />
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<StackLayout x:Name="savedQueriesContent" Spacing="0" Margin="16,0">
|
||||||
|
<StackLayout BindableLayout.ItemsSource="{Binding SavedQueries}"
|
||||||
|
IsVisible="{Binding HasSavedQueries}"
|
||||||
|
Spacing="0">
|
||||||
|
<BindableLayout.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<Grid ColumnDefinitions="*,Auto" Padding="0,10">
|
||||||
|
<StackLayout Grid.Column="0" Spacing="2">
|
||||||
|
<StackLayout.GestureRecognizers>
|
||||||
|
<TapGestureRecognizer
|
||||||
|
Command="{Binding BindingContext.Commands[LoadQuery], Source={x:Reference sessionPage}}"
|
||||||
|
CommandParameter="{Binding .}" />
|
||||||
|
</StackLayout.GestureRecognizers>
|
||||||
|
<Label Text="{Binding Name}" FontSize="14" FontAttributes="Bold" />
|
||||||
|
<Label Text="{Binding QueryText}"
|
||||||
|
FontSize="12"
|
||||||
|
TextColor="{DynamicResource SecondaryTextColor}"
|
||||||
|
MaxLines="1"
|
||||||
|
LineBreakMode="TailTruncation" />
|
||||||
|
</StackLayout>
|
||||||
|
<Button Grid.Column="1"
|
||||||
|
Text="{x:Static fonts:FontAwesomeSolid.TrashAlt}"
|
||||||
|
FontFamily="{StaticResource FontAwesomeSolid}"
|
||||||
|
FontSize="16"
|
||||||
|
WidthRequest="44"
|
||||||
|
BackgroundColor="Transparent"
|
||||||
|
TextColor="{DynamicResource SecondaryTextColor}"
|
||||||
|
Command="{Binding BindingContext.Commands[DeleteQuery], Source={x:Reference sessionPage}}"
|
||||||
|
CommandParameter="{Binding .}" />
|
||||||
|
</Grid>
|
||||||
|
</DataTemplate>
|
||||||
|
</BindableLayout.ItemTemplate>
|
||||||
|
</StackLayout>
|
||||||
|
|
||||||
|
<Label Text="No saved queries yet"
|
||||||
|
IsVisible="{Binding HasNoSavedQueries}"
|
||||||
|
TextColor="{DynamicResource SecondaryTextColor}"
|
||||||
|
FontSize="14"
|
||||||
|
Margin="0,4,0,8" />
|
||||||
|
</StackLayout>
|
||||||
|
|
||||||
|
<!-- Divider between saved queries and results -->
|
||||||
|
<BoxView HeightRequest="1"
|
||||||
|
BackgroundColor="{DynamicResource SecondaryTextColor}"
|
||||||
|
Opacity="0.15"
|
||||||
|
Margin="16,8,16,0" />
|
||||||
|
|
||||||
|
<!-- Results section -->
|
||||||
|
<StackLayout IsVisible="{Binding HasResults}" Spacing="0">
|
||||||
|
<Grid ColumnDefinitions="*,Auto" Padding="16,8,16,4">
|
||||||
|
<Label Grid.Column="0"
|
||||||
|
Text="Results"
|
||||||
|
FontSize="12" FontAttributes="Bold"
|
||||||
|
TextColor="{DynamicResource SecondaryTextColor}"
|
||||||
|
VerticalOptions="Center" />
|
||||||
|
<Button Grid.Column="1"
|
||||||
|
Text="Clear all"
|
||||||
|
FontSize="11"
|
||||||
|
BackgroundColor="Transparent"
|
||||||
|
TextColor="{DynamicResource SecondaryTextColor}"
|
||||||
|
Padding="8,0"
|
||||||
|
HeightRequest="32"
|
||||||
|
Command="{Binding Commands[ClearResults]}" />
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<StackLayout BindableLayout.ItemsSource="{Binding QueryResults}" Spacing="0">
|
||||||
|
<BindableLayout.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<Border Margin="12,0,12,8"
|
||||||
|
BackgroundColor="{DynamicResource Extreme}"
|
||||||
|
StrokeShape="RoundRectangle 8"
|
||||||
|
StrokeThickness="0">
|
||||||
|
<Border.Shadow>
|
||||||
|
<Shadow Brush="Black" Offset="0,2" Radius="6" Opacity="0.12" />
|
||||||
|
</Border.Shadow>
|
||||||
|
<StackLayout Spacing="0">
|
||||||
|
<!-- Action row -->
|
||||||
|
<Grid ColumnDefinitions="*,*,*,*,Auto" Padding="4,2">
|
||||||
|
<Button Grid.Column="0"
|
||||||
|
Text="{x:Static fonts:FontAwesomeSolid.Star}"
|
||||||
|
FontFamily="{StaticResource FontAwesomeSolid}" FontSize="15"
|
||||||
|
BackgroundColor="Transparent"
|
||||||
|
TextColor="{DynamicResource SecondaryTextColor}"
|
||||||
|
HeightRequest="40"
|
||||||
|
Command="{Binding BindingContext.Commands[SaveQuery], Source={x:Reference sessionPage}}"
|
||||||
|
CommandParameter="{Binding .}" />
|
||||||
|
<Button Grid.Column="1"
|
||||||
|
Text="{x:Static fonts:FontAwesomeSolid.Expand}"
|
||||||
|
FontFamily="{StaticResource FontAwesomeSolid}" FontSize="15"
|
||||||
|
BackgroundColor="Transparent"
|
||||||
|
IsEnabled="{Binding CanDisplayGraph}"
|
||||||
|
TextColor="{DynamicResource SecondaryTextColor}"
|
||||||
|
HeightRequest="40"
|
||||||
|
Command="{Binding BindingContext.Commands[OpenGraph], Source={x:Reference sessionPage}}"
|
||||||
|
CommandParameter="{Binding .}" />
|
||||||
|
<Button Grid.Column="2"
|
||||||
|
Text="{x:Static fonts:FontAwesomeSolid.Code}"
|
||||||
|
FontFamily="{StaticResource FontAwesomeSolid}" FontSize="15"
|
||||||
|
BackgroundColor="Transparent"
|
||||||
|
IsEnabled="{Binding Success}"
|
||||||
|
TextColor="{DynamicResource SecondaryTextColor}"
|
||||||
|
HeightRequest="40"
|
||||||
|
Command="{Binding BindingContext.Commands[OpenTable], Source={x:Reference sessionPage}}"
|
||||||
|
CommandParameter="{Binding .}" />
|
||||||
|
<Button Grid.Column="3"
|
||||||
|
Text="{x:Static fonts:FontAwesomeSolid.TrashAlt}"
|
||||||
|
FontFamily="{StaticResource FontAwesomeSolid}" FontSize="15"
|
||||||
|
BackgroundColor="Transparent"
|
||||||
|
TextColor="{DynamicResource SecondaryTextColor}"
|
||||||
|
HeightRequest="40"
|
||||||
|
Command="{Binding BindingContext.Commands[DeleteResult], Source={x:Reference sessionPage}}"
|
||||||
|
CommandParameter="{Binding .}" />
|
||||||
|
<Button Grid.Column="4"
|
||||||
|
Text="{x:Static fonts:FontAwesomeSolid.ChevronUp}"
|
||||||
|
FontFamily="{StaticResource FontAwesomeSolid}" FontSize="12"
|
||||||
|
BackgroundColor="Transparent"
|
||||||
|
TextColor="{DynamicResource SecondaryTextColor}"
|
||||||
|
WidthRequest="40" HeightRequest="40"
|
||||||
|
Clicked="ToggleResultCollapse" />
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<!-- Query label -->
|
||||||
|
<Label Text="{Binding DisplayQuery}"
|
||||||
|
Padding="12,0,12,8"
|
||||||
|
FontSize="12"
|
||||||
|
TextColor="{DynamicResource SecondaryTextColor}"
|
||||||
|
LineBreakMode="TailTruncation"
|
||||||
|
MaxLines="2">
|
||||||
|
<Label.GestureRecognizers>
|
||||||
|
<TapGestureRecognizer
|
||||||
|
Command="{Binding BindingContext.Commands[LoadResultQuery], Source={x:Reference sessionPage}}"
|
||||||
|
CommandParameter="{Binding .}" />
|
||||||
|
</Label.GestureRecognizers>
|
||||||
|
</Label>
|
||||||
|
|
||||||
|
<!-- Result content -->
|
||||||
|
<controls:QueryResultView
|
||||||
|
GraphViewHeight="{Binding BindingContext.GraphViewHeight, Source={x:Reference sessionPage}}" />
|
||||||
|
</StackLayout>
|
||||||
|
</Border>
|
||||||
|
</DataTemplate>
|
||||||
|
</BindableLayout.ItemTemplate>
|
||||||
|
</StackLayout>
|
||||||
|
</StackLayout>
|
||||||
|
|
||||||
|
<!-- Empty hint (no results yet) -->
|
||||||
|
<StackLayout IsVisible="{Binding IsEmpty}" HorizontalOptions="Center" Spacing="12" Padding="32,24">
|
||||||
<Label Text="{x:Static fonts:FontAwesomeSolid.Database}"
|
<Label Text="{x:Static fonts:FontAwesomeSolid.Database}"
|
||||||
FontFamily="{StaticResource FontAwesomeSolid}"
|
FontFamily="{StaticResource FontAwesomeSolid}"
|
||||||
FontSize="48"
|
FontSize="48"
|
||||||
TextColor="{StaticResource SecondaryTextColor}"
|
TextColor="{DynamicResource SecondaryTextColor}"
|
||||||
HorizontalOptions="Center" />
|
HorizontalOptions="Center" />
|
||||||
<Label Text="No results yet"
|
<Label Text="No results yet"
|
||||||
FontSize="18"
|
FontSize="18" FontAttributes="Bold"
|
||||||
FontAttributes="Bold"
|
TextColor="{DynamicResource SecondaryTextColor}"
|
||||||
TextColor="{StaticResource SecondaryTextColor}"
|
|
||||||
HorizontalOptions="Center" />
|
HorizontalOptions="Center" />
|
||||||
<Label Text="Write a query and tap the play button to run it"
|
<Label Text="Write a query and tap play, or load a saved query above"
|
||||||
FontSize="14"
|
FontSize="14"
|
||||||
TextColor="{StaticResource SecondaryTextColor}"
|
TextColor="{DynamicResource SecondaryTextColor}"
|
||||||
HorizontalTextAlignment="Center"
|
HorizontalTextAlignment="Center"
|
||||||
HorizontalOptions="Center" />
|
HorizontalOptions="Center" />
|
||||||
</StackLayout>
|
</StackLayout>
|
||||||
</Grid>
|
|
||||||
</StackLayout>
|
</StackLayout>
|
||||||
|
</ScrollView>
|
||||||
|
|
||||||
|
</Grid>
|
||||||
</ContentPage.Content>
|
</ContentPage.Content>
|
||||||
</ContentPage>
|
</ContentPage>
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
using System;
|
using System;
|
||||||
using Microsoft.Maui.Controls;
|
using Microsoft.Maui.Controls;
|
||||||
using Microsoft.Maui.Controls.Xaml;
|
using Microsoft.Maui.Controls.Xaml;
|
||||||
|
using Xamarin.Neo4j.Fonts;
|
||||||
using Xamarin.Neo4j.Models;
|
using Xamarin.Neo4j.Models;
|
||||||
using Xamarin.Neo4j.Utilities;
|
|
||||||
using Xamarin.Neo4j.ViewModels;
|
using Xamarin.Neo4j.ViewModels;
|
||||||
|
|
||||||
namespace Xamarin.Neo4j.Pages
|
namespace Xamarin.Neo4j.Pages
|
||||||
@@ -10,7 +10,7 @@ namespace Xamarin.Neo4j.Pages
|
|||||||
[XamlCompilation(XamlCompilationOptions.Compile)]
|
[XamlCompilation(XamlCompilationOptions.Compile)]
|
||||||
public partial class SessionPage : ContentPage
|
public partial class SessionPage : ContentPage
|
||||||
{
|
{
|
||||||
private SessionViewModel ViewModel => (SessionViewModel) BindingContext;
|
private SessionViewModel ViewModel => (SessionViewModel)BindingContext;
|
||||||
|
|
||||||
public SessionPage(Neo4jConnectionString connectionString, string initialQuery = null)
|
public SessionPage(Neo4jConnectionString connectionString, string initialQuery = null)
|
||||||
{
|
{
|
||||||
@@ -18,25 +18,58 @@ namespace Xamarin.Neo4j.Pages
|
|||||||
|
|
||||||
BindingContext = new SessionViewModel(Navigation, connectionString, initialQuery);
|
BindingContext = new SessionViewModel(Navigation, connectionString, initialQuery);
|
||||||
|
|
||||||
ViewModel.ScrollToTop += (_, _) =>
|
ViewModel.ScrollToTop += async (_, _) =>
|
||||||
{
|
{
|
||||||
resultsCollection.ScrollTo(0, 0, ScrollToPosition.Start, true);
|
await mainScroll.ScrollToAsync(0, 0, true);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override void OnAppearing()
|
||||||
|
{
|
||||||
|
base.OnAppearing();
|
||||||
|
ViewModel.LoadSavedQueries();
|
||||||
|
}
|
||||||
|
|
||||||
private void FocusDatabasePicker(object sender, EventArgs e)
|
private void FocusDatabasePicker(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
databasePicker.Focus();
|
databasePicker.Focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void CloseResultView(object sender, GenericEventArgs<QueryResult> e)
|
|
||||||
{
|
|
||||||
ViewModel.DeleteQueryResult(e.Data);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ExecuteQuery(object sender, EventArgs e)
|
private void ExecuteQuery(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
ViewModel.Commands["ExecuteQuery"].Execute(null);
|
ViewModel.Commands["ExecuteQuery"].Execute(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool _savedQueriesExpanded = true;
|
||||||
|
|
||||||
|
private void ToggleSavedQueries(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
_savedQueriesExpanded = !_savedQueriesExpanded;
|
||||||
|
savedQueriesContent.IsVisible = _savedQueriesExpanded;
|
||||||
|
savedQueriesChevron.Text = _savedQueriesExpanded
|
||||||
|
? FontAwesomeSolid.ChevronUp
|
||||||
|
: FontAwesomeSolid.ChevronDown;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ToggleResultCollapse(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
if (sender is not Button btn) return;
|
||||||
|
|
||||||
|
// Walk up to find the StackLayout that wraps action row + query + content
|
||||||
|
var card = btn.Parent?.Parent as StackLayout; // btn -> Grid -> StackLayout
|
||||||
|
if (card == null) return;
|
||||||
|
|
||||||
|
// The collapsible content is the last child (QueryResultView)
|
||||||
|
var content = card.Children[card.Children.Count - 1] as View;
|
||||||
|
// The query label is second-to-last
|
||||||
|
var queryLabel = card.Children.Count >= 2 ? card.Children[card.Children.Count - 2] as View : null;
|
||||||
|
|
||||||
|
if (content == null) return;
|
||||||
|
|
||||||
|
var collapse = content.IsVisible;
|
||||||
|
content.IsVisible = !collapse;
|
||||||
|
if (queryLabel != null) queryLabel.IsVisible = !collapse;
|
||||||
|
btn.Text = collapse ? FontAwesomeSolid.ChevronDown : FontAwesomeSolid.ChevronUp;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ using Microsoft.Extensions.DependencyInjection;
|
|||||||
using Microsoft.Maui;
|
using Microsoft.Maui;
|
||||||
using Microsoft.Maui.Controls;
|
using Microsoft.Maui.Controls;
|
||||||
using Xamarin.Neo4j.Annotations;
|
using Xamarin.Neo4j.Annotations;
|
||||||
|
using Xamarin.Neo4j.Managers;
|
||||||
using Xamarin.Neo4j.Models;
|
using Xamarin.Neo4j.Models;
|
||||||
using Xamarin.Neo4j.Pages;
|
using Xamarin.Neo4j.Pages;
|
||||||
using Xamarin.Neo4j.Services;
|
using Xamarin.Neo4j.Services;
|
||||||
@@ -41,6 +42,8 @@ namespace Xamarin.Neo4j.ViewModels
|
|||||||
|
|
||||||
private ObservableCollection<QueryResult> _queryResults;
|
private ObservableCollection<QueryResult> _queryResults;
|
||||||
|
|
||||||
|
private List<Query> _savedQueries;
|
||||||
|
|
||||||
private Neo4jConnectionString _connectionString;
|
private Neo4jConnectionString _connectionString;
|
||||||
|
|
||||||
private string _query;
|
private string _query;
|
||||||
@@ -54,7 +57,11 @@ namespace Xamarin.Neo4j.ViewModels
|
|||||||
|
|
||||||
Query = initialQuery;
|
Query = initialQuery;
|
||||||
QueryResults = new ObservableCollection<QueryResult>();
|
QueryResults = new ObservableCollection<QueryResult>();
|
||||||
QueryResults.CollectionChanged += (_, _) => OnPropertyChanged(nameof(IsEmpty));
|
QueryResults.CollectionChanged += (_, _) =>
|
||||||
|
{
|
||||||
|
OnPropertyChanged(nameof(IsEmpty));
|
||||||
|
OnPropertyChanged(nameof(HasResults));
|
||||||
|
};
|
||||||
|
|
||||||
Commands.Add("ExecuteQuery", new Command(async () =>
|
Commands.Add("ExecuteQuery", new Command(async () =>
|
||||||
{
|
{
|
||||||
@@ -65,20 +72,91 @@ namespace Xamarin.Neo4j.ViewModels
|
|||||||
|
|
||||||
var result = await _neo4jService.ExecuteQuery(Query, _connectionString);
|
var result = await _neo4jService.ExecuteQuery(Query, _connectionString);
|
||||||
|
|
||||||
if (result.Success)
|
Query = null;
|
||||||
{
|
|
||||||
QueryResults.Insert(0, result);
|
QueryResults.Insert(0, result);
|
||||||
|
|
||||||
ScrollToTop?.Invoke(this, EventArgs.Empty);
|
ScrollToTop?.Invoke(this, EventArgs.Empty);
|
||||||
}
|
|
||||||
|
|
||||||
else
|
|
||||||
await Application.Current.MainPage.DisplayAlert("", result.ErrorMessage, "OK");
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
Commands.Add("DeleteQuery", new Command(async (o) =>
|
||||||
|
{
|
||||||
|
if (!(o is Query query))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var confirmed = await Application.Current.MainPage.DisplayAlert(
|
||||||
|
"Delete Query",
|
||||||
|
$"Delete \"{query.Name}\"?",
|
||||||
|
"Delete", "Cancel");
|
||||||
|
|
||||||
|
if (confirmed)
|
||||||
|
{
|
||||||
|
SavedQueryManager.DeleteSavedQuery(query);
|
||||||
|
LoadSavedQueries();
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
Commands.Add("LoadQuery", new Command((o) =>
|
||||||
|
{
|
||||||
|
if (o is Query query)
|
||||||
|
LoadQuery(query);
|
||||||
|
}));
|
||||||
|
|
||||||
|
Commands.Add("LoadResultQuery", new Command((o) =>
|
||||||
|
{
|
||||||
|
if (o is QueryResult result)
|
||||||
|
Query = result.Query;
|
||||||
|
}));
|
||||||
|
|
||||||
|
Commands.Add("DeleteResult", new Command((o) =>
|
||||||
|
{
|
||||||
|
if (o is QueryResult result)
|
||||||
|
DeleteQueryResult(result);
|
||||||
|
}));
|
||||||
|
|
||||||
|
Commands.Add("SaveQuery", new Command(async (o) =>
|
||||||
|
{
|
||||||
|
if (!(o is QueryResult result)) return;
|
||||||
|
var name = await Application.Current.MainPage.DisplayPromptAsync(
|
||||||
|
"Save Query", "What would you like to call this query?");
|
||||||
|
if (!string.IsNullOrWhiteSpace(name))
|
||||||
|
{
|
||||||
|
SavedQueryManager.AddSavedQuery(new Query
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
QueryText = result.Query,
|
||||||
|
Name = name
|
||||||
|
});
|
||||||
|
LoadSavedQueries();
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
Commands.Add("OpenGraph", new Command(async (o) =>
|
||||||
|
{
|
||||||
|
if (!(o is QueryResult result) || !result.CanDisplayGraph || result.NeovisHtml == null) return;
|
||||||
|
var connectionString2 = ConnectionStringManager.ActiveConnectionString;
|
||||||
|
await Navigation.PushAsync(new GraphPage(result.NeovisHtml, connectionString2, _neo4jService));
|
||||||
|
}));
|
||||||
|
|
||||||
|
Commands.Add("OpenTable", new Command(async (o) =>
|
||||||
|
{
|
||||||
|
if (!(o is QueryResult result) || !result.Success) return;
|
||||||
|
await Navigation.PushAsync(new TablePage(result));
|
||||||
|
}));
|
||||||
|
|
||||||
|
Commands.Add("ClearResults", new Command(() => ClearAllResults()));
|
||||||
|
|
||||||
InitializeConnection(connectionString);
|
InitializeConnection(connectionString);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void LoadSavedQueries()
|
||||||
|
{
|
||||||
|
SavedQueries = SavedQueryManager.GetSavedQueries();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void LoadQuery(Query query)
|
||||||
|
{
|
||||||
|
Query = query.QueryText;
|
||||||
|
}
|
||||||
|
|
||||||
private async void InitializeConnection(Neo4jConnectionString connectionString)
|
private async void InitializeConnection(Neo4jConnectionString connectionString)
|
||||||
{
|
{
|
||||||
var (isConnected, message) = await _neo4jService.EstablishConnection(connectionString);
|
var (isConnected, message) = await _neo4jService.EstablishConnection(connectionString);
|
||||||
@@ -106,6 +184,11 @@ namespace Xamarin.Neo4j.ViewModels
|
|||||||
QueryResults.Remove(item);
|
QueryResults.Remove(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void ClearAllResults()
|
||||||
|
{
|
||||||
|
QueryResults.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
[NotifyPropertyChangedInvocator]
|
[NotifyPropertyChangedInvocator]
|
||||||
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
|
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
|
||||||
{
|
{
|
||||||
@@ -164,10 +247,39 @@ namespace Xamarin.Neo4j.ViewModels
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<Query> SavedQueries
|
||||||
|
{
|
||||||
|
get => _savedQueries;
|
||||||
|
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_savedQueries = value;
|
||||||
|
|
||||||
|
OnPropertyChanged();
|
||||||
|
OnPropertyChanged(nameof(HasSavedQueries));
|
||||||
|
OnPropertyChanged(nameof(HasNoSavedQueries));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public bool CanExecuteQuery => !string.IsNullOrWhiteSpace(Query) && CurrentDatabase != null;
|
public bool CanExecuteQuery => !string.IsNullOrWhiteSpace(Query) && CurrentDatabase != null;
|
||||||
|
|
||||||
public bool IsEmpty => QueryResults?.Count == 0;
|
public bool IsEmpty => QueryResults?.Count == 0;
|
||||||
|
|
||||||
|
public bool HasResults => !IsEmpty;
|
||||||
|
|
||||||
|
public bool HasSavedQueries => _savedQueries?.Count > 0;
|
||||||
|
|
||||||
|
public bool HasNoSavedQueries => !HasSavedQueries;
|
||||||
|
|
||||||
|
public double GraphViewHeight
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var screenHeight = _screenSizeService.GetScreenHeight();
|
||||||
|
return Math.Max(200, screenHeight - 300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user