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"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
xmlns:fonts="clr-namespace:Xamarin.Neo4j.Fonts;assembly=Xamarin.Neo4j"
|
||||
Margin="0, 0, 0, 25"
|
||||
x:Class="Xamarin.Neo4j.Controls.QueryResultView">
|
||||
<StackLayout Orientation="Vertical" Spacing="0">
|
||||
<Border Stroke="{StaticResource QueryTextBorder}" StrokeThickness="4">
|
||||
<Label Text="{Binding Query}" Padding="8" HorizontalOptions="FillAndExpand" BackgroundColor="{StaticResource Extreme}" />
|
||||
x:Class="Xamarin.Neo4j.Controls.QueryResultView"
|
||||
x:Name="self">
|
||||
<Grid VerticalOptions="Start">
|
||||
|
||||
<!-- Graph WebView -->
|
||||
<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>
|
||||
|
||||
<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>
|
||||
</StackLayout>
|
||||
</Grid>
|
||||
</ContentView>
|
||||
|
||||
@@ -1,43 +1,49 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Microsoft.Maui.ApplicationModel;
|
||||
using Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui.Controls.Xaml;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Maui;
|
||||
using Neo4j.Driver;
|
||||
using Xamarin.Neo4j.Managers;
|
||||
using Xamarin.Neo4j.Services;
|
||||
using Xamarin.Neo4j.Models;
|
||||
using Xamarin.Neo4j.Pages;
|
||||
using Xamarin.Neo4j.Utilities;
|
||||
using Query = Xamarin.Neo4j.Models.Query;
|
||||
|
||||
namespace Xamarin.Neo4j.Controls
|
||||
{
|
||||
[XamlCompilation(XamlCompilationOptions.Compile)]
|
||||
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()
|
||||
{
|
||||
InitializeComponent();
|
||||
BindingContextChanged += OnBindingContextChanged;
|
||||
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)
|
||||
{
|
||||
Console.WriteLine($"[Graph] Inline Navigating: {e.Url}");
|
||||
|
||||
if (!e.Url.Contains("expand") || !e.Url.Contains("nodeId")) return;
|
||||
|
||||
e.Cancel = true;
|
||||
@@ -69,19 +75,11 @@ namespace Xamarin.Neo4j.Controls
|
||||
|
||||
private void OnBindingContextChanged(object sender, EventArgs e)
|
||||
{
|
||||
if (BindingContext == null || graphView == null)
|
||||
{
|
||||
Console.WriteLine($"[Graph] OnBindingContextChanged skipped: BindingContext={BindingContext}, graphView={graphView}");
|
||||
return;
|
||||
}
|
||||
|
||||
Console.WriteLine($"[Graph] OnBindingContextChanged fired, CanDisplayGraph={QueryResult?.CanDisplayGraph}");
|
||||
if (BindingContext == null || graphView == null) return;
|
||||
|
||||
ParseNeovisHtmlSafe();
|
||||
|
||||
Console.WriteLine($"[Graph] Setting graphView.Source, html length={_neovisHtml?.Length ?? 0}");
|
||||
|
||||
graphView.Source = new HtmlWebViewSource { Html = _neovisHtml };
|
||||
graphView.Source = new HtmlWebViewSource { Html = QueryResult?.NeovisHtml };
|
||||
}
|
||||
|
||||
private void ParseNeovisHtml()
|
||||
@@ -89,83 +87,46 @@ namespace Xamarin.Neo4j.Controls
|
||||
var assembly = Assembly.GetExecutingAssembly();
|
||||
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);
|
||||
if (stream == null)
|
||||
{
|
||||
_neovisHtml = $"<html><body style='background:#111;color:#f55;font-family:monospace;padding:16px'>" +
|
||||
$"<b>Resource not found:</b><br>{resourceName}<br><br>" +
|
||||
$"<b>Available:</b><br>{available.Replace("\n", "<br>")}</body></html>";
|
||||
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>Available:</b><br>{available}</body></html>";
|
||||
return;
|
||||
}
|
||||
|
||||
using (stream)
|
||||
using (var reader = new StreamReader(stream))
|
||||
{
|
||||
var result = reader.ReadToEnd();
|
||||
var html = reader.ReadToEnd();
|
||||
var connectionId = ConnectionStringManager.ActiveConnectionString?.Id ?? Guid.Empty;
|
||||
|
||||
var (nodesJson, edgesJson) = GraphDataHelper.BuildJson(QueryResult.Results, connectionId);
|
||||
|
||||
var isDark = Application.Current.RequestedTheme == AppTheme.Dark;
|
||||
result = result.Replace("{{nodes}}", nodesJson);
|
||||
result = result.Replace("{{edges}}", edgesJson);
|
||||
result = result.Replace("{{backgroundColor}}", isDark ? "#292C31" : "#FFFFFF");
|
||||
result = result.Replace("{{textColor}}", isDark ? "#FFFFFF" : "#000000");
|
||||
html = html.Replace("{{nodes}}", nodesJson);
|
||||
html = html.Replace("{{edges}}", edgesJson);
|
||||
html = html.Replace("{{backgroundColor}}", isDark ? "#292C31" : "#FFFFFF");
|
||||
html = html.Replace("{{textColor}}", isDark ? "#FFFFFF" : "#000000");
|
||||
|
||||
_neovisHtml = result;
|
||||
QueryResult.NeovisHtml = html;
|
||||
}
|
||||
}
|
||||
|
||||
private void ParseNeovisHtmlSafe()
|
||||
{
|
||||
if (QueryResult == null) return;
|
||||
try
|
||||
{
|
||||
ParseNeovisHtml();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[Graph] ParseNeovisHtml threw: {ex.GetType().Name}: {ex.Message}");
|
||||
_neovisHtml = $"<html><body style='background:#111;color:#f55;font-family:monospace;padding:16px'>" +
|
||||
$"<b>{ex.GetType().Name}</b><br>{ex.Message}<br><br>{ex.StackTrace?.Replace("\n", "<br>")}</body></html>";
|
||||
QueryResult.NeovisHtml = $"<html><body style='background:#111;color:#f55;font-family:monospace;padding:16px'>" +
|
||||
$"<b>{ex.GetType().Name}</b><br>{ex.Message}</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 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:fonts="clr-namespace:Xamarin.Neo4j.Fonts;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>
|
||||
<StackLayout Spacing="0">
|
||||
<Label TextColor="White" HorizontalOptions="CenterAndExpand" VerticalOptions="CenterAndExpand">
|
||||
@@ -35,40 +36,202 @@
|
||||
</ContentPage.ToolbarItems>
|
||||
|
||||
<ContentPage.Content>
|
||||
<StackLayout VerticalOptions="FillAndExpand">
|
||||
<Border BackgroundColor="White" StrokeShape="RoundRectangle 2" Padding="4" Margin="16">
|
||||
<Grid RowDefinitions="Auto,*">
|
||||
|
||||
<!-- 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">
|
||||
<controls:QueryEditor Text="{Binding Query}" ExecuteClicked="ExecuteQuery" FontSize="14" MaxHeight="200" HorizontalOptions="FillAndExpand" AutoSize="TextChanges" />
|
||||
</Border>
|
||||
</Border>
|
||||
|
||||
<Grid VerticalOptions="FillAndExpand">
|
||||
<CollectionView x:Name="resultsCollection" ItemsSource="{Binding QueryResults}" VerticalOptions="FillAndExpand">
|
||||
<CollectionView.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<controls:QueryResultView CloseRequested="CloseResultView" />
|
||||
</DataTemplate>
|
||||
</CollectionView.ItemTemplate>
|
||||
</CollectionView>
|
||||
<!-- Row 1: Single scroll area — results then saved queries always visible -->
|
||||
<ScrollView Grid.Row="1" x:Name="mainScroll">
|
||||
<StackLayout Spacing="0" Padding="0,0,0,16">
|
||||
|
||||
<!-- 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}"
|
||||
FontFamily="{StaticResource FontAwesomeSolid}"
|
||||
FontSize="48"
|
||||
TextColor="{DynamicResource SecondaryTextColor}"
|
||||
HorizontalOptions="Center" />
|
||||
<Label Text="No results yet"
|
||||
FontSize="18" FontAttributes="Bold"
|
||||
TextColor="{DynamicResource SecondaryTextColor}"
|
||||
HorizontalOptions="Center" />
|
||||
<Label Text="Write a query and tap play, or load a saved query above"
|
||||
FontSize="14"
|
||||
TextColor="{DynamicResource SecondaryTextColor}"
|
||||
HorizontalTextAlignment="Center"
|
||||
HorizontalOptions="Center" />
|
||||
</StackLayout>
|
||||
|
||||
<StackLayout IsVisible="{Binding IsEmpty}" VerticalOptions="Center" HorizontalOptions="Center" Spacing="12" Padding="32">
|
||||
<Label Text="{x:Static fonts:FontAwesomeSolid.Database}"
|
||||
FontFamily="{StaticResource FontAwesomeSolid}"
|
||||
FontSize="48"
|
||||
TextColor="{StaticResource SecondaryTextColor}"
|
||||
HorizontalOptions="Center" />
|
||||
<Label Text="No results yet"
|
||||
FontSize="18"
|
||||
FontAttributes="Bold"
|
||||
TextColor="{StaticResource SecondaryTextColor}"
|
||||
HorizontalOptions="Center" />
|
||||
<Label Text="Write a query and tap the play button to run it"
|
||||
FontSize="14"
|
||||
TextColor="{StaticResource SecondaryTextColor}"
|
||||
HorizontalTextAlignment="Center"
|
||||
HorizontalOptions="Center" />
|
||||
</StackLayout>
|
||||
</Grid>
|
||||
</StackLayout>
|
||||
</ScrollView>
|
||||
|
||||
</Grid>
|
||||
</ContentPage.Content>
|
||||
</ContentPage>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
using System;
|
||||
using Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui.Controls.Xaml;
|
||||
using Xamarin.Neo4j.Fonts;
|
||||
using Xamarin.Neo4j.Models;
|
||||
using Xamarin.Neo4j.Utilities;
|
||||
using Xamarin.Neo4j.ViewModels;
|
||||
|
||||
namespace Xamarin.Neo4j.Pages
|
||||
@@ -10,7 +10,7 @@ namespace Xamarin.Neo4j.Pages
|
||||
[XamlCompilation(XamlCompilationOptions.Compile)]
|
||||
public partial class SessionPage : ContentPage
|
||||
{
|
||||
private SessionViewModel ViewModel => (SessionViewModel) BindingContext;
|
||||
private SessionViewModel ViewModel => (SessionViewModel)BindingContext;
|
||||
|
||||
public SessionPage(Neo4jConnectionString connectionString, string initialQuery = null)
|
||||
{
|
||||
@@ -18,25 +18,58 @@ namespace Xamarin.Neo4j.Pages
|
||||
|
||||
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)
|
||||
{
|
||||
databasePicker.Focus();
|
||||
}
|
||||
|
||||
private void CloseResultView(object sender, GenericEventArgs<QueryResult> e)
|
||||
{
|
||||
ViewModel.DeleteQueryResult(e.Data);
|
||||
}
|
||||
|
||||
private void ExecuteQuery(object sender, EventArgs e)
|
||||
{
|
||||
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.Controls;
|
||||
using Xamarin.Neo4j.Annotations;
|
||||
using Xamarin.Neo4j.Managers;
|
||||
using Xamarin.Neo4j.Models;
|
||||
using Xamarin.Neo4j.Pages;
|
||||
using Xamarin.Neo4j.Services;
|
||||
@@ -41,6 +42,8 @@ namespace Xamarin.Neo4j.ViewModels
|
||||
|
||||
private ObservableCollection<QueryResult> _queryResults;
|
||||
|
||||
private List<Query> _savedQueries;
|
||||
|
||||
private Neo4jConnectionString _connectionString;
|
||||
|
||||
private string _query;
|
||||
@@ -54,7 +57,11 @@ namespace Xamarin.Neo4j.ViewModels
|
||||
|
||||
Query = initialQuery;
|
||||
QueryResults = new ObservableCollection<QueryResult>();
|
||||
QueryResults.CollectionChanged += (_, _) => OnPropertyChanged(nameof(IsEmpty));
|
||||
QueryResults.CollectionChanged += (_, _) =>
|
||||
{
|
||||
OnPropertyChanged(nameof(IsEmpty));
|
||||
OnPropertyChanged(nameof(HasResults));
|
||||
};
|
||||
|
||||
Commands.Add("ExecuteQuery", new Command(async () =>
|
||||
{
|
||||
@@ -65,20 +72,91 @@ namespace Xamarin.Neo4j.ViewModels
|
||||
|
||||
var result = await _neo4jService.ExecuteQuery(Query, _connectionString);
|
||||
|
||||
if (result.Success)
|
||||
{
|
||||
QueryResults.Insert(0, result);
|
||||
|
||||
ScrollToTop?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
else
|
||||
await Application.Current.MainPage.DisplayAlert("", result.ErrorMessage, "OK");
|
||||
Query = null;
|
||||
QueryResults.Insert(0, result);
|
||||
ScrollToTop?.Invoke(this, EventArgs.Empty);
|
||||
}));
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
public void LoadSavedQueries()
|
||||
{
|
||||
SavedQueries = SavedQueryManager.GetSavedQueries();
|
||||
}
|
||||
|
||||
public void LoadQuery(Query query)
|
||||
{
|
||||
Query = query.QueryText;
|
||||
}
|
||||
|
||||
private async void InitializeConnection(Neo4jConnectionString connectionString)
|
||||
{
|
||||
var (isConnected, message) = await _neo4jService.EstablishConnection(connectionString);
|
||||
@@ -106,6 +184,11 @@ namespace Xamarin.Neo4j.ViewModels
|
||||
QueryResults.Remove(item);
|
||||
}
|
||||
|
||||
public void ClearAllResults()
|
||||
{
|
||||
QueryResults.Clear();
|
||||
}
|
||||
|
||||
[NotifyPropertyChangedInvocator]
|
||||
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 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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user