Skip to content

Commit 5a6b74a

Browse files
author
erickas
committed
wip
1 parent db6cc43 commit 5a6b74a

15 files changed

Lines changed: 3063 additions & 77 deletions

src/AkkornStudio.UI/Controls/SqlEditor/SqlEditorControl.axaml

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -447,6 +447,43 @@
447447
IsVisible="{Binding HasPendingMutationConfirmation}"
448448
Margin="0,30,0,0"/>
449449

450+
<Border Grid.Row="3"
451+
Classes="callout-subtle warning"
452+
IsVisible="{Binding HasPendingQueryParameterPrompt}"
453+
Margin="0,30,0,0"
454+
Padding="10,8">
455+
<StackPanel Spacing="8">
456+
<TextBlock Text="{Binding PendingQueryParameterPromptMessage}"
457+
FontWeight="{StaticResource FontWeightTitle}"
458+
Foreground="{StaticResource TextPrimaryBrush}"/>
459+
<ItemsControl ItemsSource="{Binding PendingQueryParameters}">
460+
<ItemsControl.ItemTemplate>
461+
<DataTemplate>
462+
<Grid ColumnDefinitions="180,*"
463+
ColumnSpacing="8"
464+
Margin="0,0,0,4">
465+
<TextBlock Grid.Column="0"
466+
Text="{Binding Name}"
467+
VerticalAlignment="Center"
468+
FontFamily="{StaticResource MonoFont}"/>
469+
<TextBox Grid.Column="1"
470+
Text="{Binding InputValue, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
471+
Watermark="valor"/>
472+
</Grid>
473+
</DataTemplate>
474+
</ItemsControl.ItemTemplate>
475+
</ItemsControl>
476+
<StackPanel Orientation="Horizontal" Spacing="8">
477+
<Button Classes="primary compact"
478+
Command="{Binding ConfirmPendingQueryParametersCommand}"
479+
Content="Executar com parâmetros"/>
480+
<Button Classes="secondary compact"
481+
Command="{Binding CancelPendingQueryParametersCommand}"
482+
Content="Cancelar"/>
483+
</StackPanel>
484+
</StackPanel>
485+
</Border>
486+
450487
<Border Grid.Row="3"
451488
Classes="callout-subtle warning"
452489
IsVisible="{Binding HasPendingCloseTabConfirmation}"

src/AkkornStudio.UI/Controls/SqlEditor/SqlResultPageControl.axaml

Lines changed: 321 additions & 50 deletions
Large diffs are not rendered by default.

src/AkkornStudio.UI/Controls/SqlEditor/SqlResultPageControl.axaml.cs

Lines changed: 145 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,22 @@
11
using Avalonia;
22
using Avalonia.Controls;
3+
using Avalonia.Controls.Templates;
34
using Avalonia.Media;
5+
using Avalonia.Platform.Storage;
46
using Avalonia.Threading;
57
using Avalonia.VisualTree;
8+
using AkkornStudio.UI.Services.SqlEditor.Reports;
69
using AkkornStudio.UI.ViewModels;
710
using System.ComponentModel;
811
using System.Data;
12+
using System.IO;
913

1014
namespace AkkornStudio.UI.Controls.SqlEditor;
1115

1216
public partial class SqlResultPageControl : UserControl
1317
{
1418
private SqlResultPageViewModel? _subscribedViewModel;
19+
private readonly SqlEditorReportExportService _reportExportService = new();
1520
private readonly IBrush _pendingCellBackgroundBrush = ResolveBrush(
1621
"BtnWarningBgBrush",
1722
new SolidColorBrush(Color.Parse("#3A2A12")));
@@ -32,16 +37,19 @@ private void OnDataContextChanged(object? sender, EventArgs e)
3237
if (_subscribedViewModel is not null)
3338
{
3439
_subscribedViewModel.ClipboardCopyRequested -= OnClipboardCopyRequested;
40+
_subscribedViewModel.ExportRequested -= OnExportRequested;
3541
_subscribedViewModel.PropertyChanged -= OnViewModelPropertyChanged;
3642
}
3743

3844
_subscribedViewModel = DataContext as SqlResultPageViewModel;
3945
if (_subscribedViewModel is not null)
4046
{
4147
_subscribedViewModel.ClipboardCopyRequested += OnClipboardCopyRequested;
48+
_subscribedViewModel.ExportRequested += OnExportRequested;
4249
_subscribedViewModel.PropertyChanged += OnViewModelPropertyChanged;
4350
}
4451

52+
RebuildResultGridColumns();
4553
Dispatcher.UIThread.Post(ApplyPendingHighlightsForLoadedRows);
4654
}
4755

@@ -54,36 +62,48 @@ private async void OnClipboardCopyRequested(string text)
5462
await topLevel.Clipboard.SetTextAsync(text);
5563
}
5664

57-
private void ResultGrid_OnCellPointerPressed(object? sender, DataGridCellPointerPressedEventArgs e)
65+
private async void OnExportRequested(SqlResultPageViewModel.SqlResultExportRequest request)
5866
{
59-
_ = sender;
60-
if (DataContext is not SqlResultPageViewModel viewModel)
67+
TopLevel? topLevel = TopLevel.GetTopLevel(this);
68+
if (topLevel?.StorageProvider is null)
6169
return;
6270

63-
if (e.Row?.DataContext is not DataRowView rowView)
64-
return;
71+
var fileType = new FilePickerFileType(request.FileTypeTitle)
72+
{
73+
Patterns = request.Patterns.ToList(),
74+
MimeTypes = request.MimeTypes.ToList(),
75+
};
6576

66-
string? columnName = e.Column?.SortMemberPath;
67-
if (string.IsNullOrWhiteSpace(columnName))
77+
IStorageFile? file = await topLevel.StorageProvider.SaveFilePickerAsync(
78+
new FilePickerSaveOptions
79+
{
80+
Title = "Export SQL Result",
81+
DefaultExtension = request.DefaultExtension,
82+
SuggestedFileName = request.SuggestedFileName,
83+
FileTypeChoices = [fileType],
84+
});
85+
86+
string? path = file?.TryGetLocalPath();
87+
if (string.IsNullOrWhiteSpace(path))
6888
return;
6989

70-
viewModel.SelectCell(rowView, columnName);
90+
await File.WriteAllTextAsync(path, request.Content);
7191
}
7292

73-
private void ResultGrid_OnAutoGeneratingColumn(object? sender, DataGridAutoGeneratingColumnEventArgs e)
93+
private void ResultGrid_OnCellPointerPressed(object? sender, DataGridCellPointerPressedEventArgs e)
7494
{
7595
_ = sender;
7696
if (DataContext is not SqlResultPageViewModel viewModel)
7797
return;
7898

79-
string columnName = string.IsNullOrWhiteSpace(e.PropertyName)
80-
? e.Column.SortMemberPath
81-
: e.PropertyName;
99+
if (e.Row?.DataContext is not DataRowView rowView)
100+
return;
101+
102+
string? columnName = e.Column?.SortMemberPath;
82103
if (string.IsNullOrWhiteSpace(columnName))
83104
return;
84105

85-
e.Column.SortMemberPath = columnName;
86-
e.Column.IsReadOnly = !viewModel.IsColumnEditable(columnName);
106+
viewModel.SelectCell(rowView, columnName);
87107
}
88108

89109
private void ResultGrid_OnLoadingRow(object? sender, DataGridRowEventArgs e)
@@ -128,12 +148,14 @@ private void ResultGrid_OnCellEditEnding(object? sender, DataGridCellEditEndingE
128148
private void OnViewModelPropertyChanged(object? sender, PropertyChangedEventArgs e)
129149
{
130150
_ = sender;
151+
if (e.PropertyName is nameof(SqlResultPageViewModel.RowsView)
152+
or nameof(SqlResultPageViewModel.Session))
153+
RebuildResultGridColumns();
154+
131155
if (e.PropertyName is nameof(SqlResultPageViewModel.PendingEditsCount)
132156
or nameof(SqlResultPageViewModel.RowsView)
133157
or nameof(SqlResultPageViewModel.Session))
134-
{
135158
Dispatcher.UIThread.Post(ApplyPendingHighlightsForLoadedRows);
136-
}
137159
}
138160

139161
private void ApplyPendingHighlightsForLoadedRows()
@@ -199,6 +221,113 @@ private static bool TryExtractEditedText(Control editingElement, out string? edi
199221
}
200222
}
201223

224+
private async void ExportReportButton_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
225+
{
226+
_ = sender;
227+
_ = e;
228+
if (DataContext is not SqlResultPageViewModel viewModel)
229+
return;
230+
231+
if (!viewModel.TryBuildReportExportContext(out SqlEditorReportExportContext? context) || context is null)
232+
return;
233+
234+
TopLevel? topLevel = TopLevel.GetTopLevel(this);
235+
if (topLevel is not Window owner || topLevel.StorageProvider is null)
236+
return;
237+
238+
var dialogVm = new SqlEditorReportExportDialogViewModel(context.TabTitle);
239+
var dialog = new SqlEditorReportExportDialogWindow(dialogVm);
240+
await dialog.ShowDialog(owner);
241+
if (!dialog.WasConfirmed)
242+
return;
243+
244+
string normalizedExtension = dialogVm.SuggestedExtension.TrimStart('.');
245+
FilePickerFileType reportFileType = GetExportFileType(dialogVm.SelectedType?.Type ?? SqlEditorReportType.HtmlFullFeature);
246+
IStorageFile? file = await topLevel.StorageProvider.SaveFilePickerAsync(
247+
new FilePickerSaveOptions
248+
{
249+
Title = "Export SQL Report",
250+
DefaultExtension = normalizedExtension,
251+
SuggestedFileName = dialogVm.FileName,
252+
FileTypeChoices = [reportFileType],
253+
});
254+
255+
string? outputPath = file?.TryGetLocalPath();
256+
if (string.IsNullOrWhiteSpace(outputPath))
257+
return;
258+
259+
SqlEditorReportExportRequest request = dialogVm.BuildRequest(outputPath);
260+
await _reportExportService.ExportAsync(context, request);
261+
}
262+
263+
private void RebuildResultGridColumns()
264+
{
265+
ResultGrid.Columns.Clear();
266+
if (DataContext is not SqlResultPageViewModel viewModel || viewModel.RowsView?.Table is not DataTable table)
267+
return;
268+
269+
foreach (DataColumn column in table.Columns)
270+
{
271+
string columnName = column.ColumnName;
272+
ResultGrid.Columns.Add(new DataGridTemplateColumn
273+
{
274+
Header = columnName,
275+
SortMemberPath = columnName,
276+
IsReadOnly = !viewModel.IsColumnEditable(columnName),
277+
CellTemplate = new FuncDataTemplate<DataRowView>((row, _) =>
278+
new TextBlock
279+
{
280+
Text = GetCellDisplayText(row, columnName),
281+
TextTrimming = TextTrimming.CharacterEllipsis,
282+
}),
283+
CellEditingTemplate = new FuncDataTemplate<DataRowView>((row, _) =>
284+
new TextBox
285+
{
286+
Text = GetCellDisplayText(row, columnName),
287+
}),
288+
});
289+
}
290+
}
291+
292+
private static string GetCellDisplayText(DataRowView row, string columnName)
293+
{
294+
if (!row.Row.Table.Columns.Contains(columnName))
295+
return string.Empty;
296+
297+
object? value = row.Row[columnName];
298+
if (value is null || value == DBNull.Value)
299+
return string.Empty;
300+
301+
return value.ToString() ?? string.Empty;
302+
}
303+
304+
private static FilePickerFileType GetExportFileType(SqlEditorReportType reportType)
305+
{
306+
return reportType switch
307+
{
308+
SqlEditorReportType.JsonContract => new FilePickerFileType("JSON")
309+
{
310+
Patterns = ["*.json"],
311+
MimeTypes = ["application/json", "text/plain"],
312+
},
313+
SqlEditorReportType.CsvData => new FilePickerFileType("CSV")
314+
{
315+
Patterns = ["*.csv"],
316+
MimeTypes = ["text/csv", "text/plain"],
317+
},
318+
SqlEditorReportType.ExcelWorkbook => new FilePickerFileType("Excel")
319+
{
320+
Patterns = ["*.xlsx"],
321+
MimeTypes = ["application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"],
322+
},
323+
_ => new FilePickerFileType("HTML")
324+
{
325+
Patterns = ["*.html", "*.htm"],
326+
MimeTypes = ["text/html", "text/plain"],
327+
},
328+
};
329+
}
330+
202331
private static IBrush ResolveBrush(string key, IBrush fallback)
203332
{
204333
if (Application.Current?.TryFindResource(key, out object? resource) == true && resource is IBrush brush)
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
using System.Text.Json;
2+
using System.Text.Json.Serialization;
3+
4+
namespace AkkornStudio.UI.Services.SqlEditor.Results;
5+
6+
public sealed record SqlSavedQuerySnippet(
7+
string Id,
8+
string Name,
9+
string Description,
10+
string Tags,
11+
string SqlText,
12+
string? ConnectionId,
13+
string? DatabaseName,
14+
DateTimeOffset CreatedAtUtc,
15+
DateTimeOffset UpdatedAtUtc,
16+
bool IsFavorite);
17+
18+
public interface ISqlResultSnippetStore
19+
{
20+
IReadOnlyList<SqlSavedQuerySnippet> Load();
21+
22+
void Upsert(SqlSavedQuerySnippet snippet);
23+
24+
bool Delete(string snippetId);
25+
}
26+
27+
public sealed class FileSqlResultSnippetStore(string? storeFilePath = null) : ISqlResultSnippetStore
28+
{
29+
private static readonly JsonSerializerOptions JsonOptions = new()
30+
{
31+
WriteIndented = true,
32+
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
33+
};
34+
35+
private string StoreFilePath
36+
{
37+
get
38+
{
39+
if (!string.IsNullOrWhiteSpace(storeFilePath))
40+
return storeFilePath;
41+
42+
string dir = global::AkkornStudio.UI.AppConstants.AppDataDirectory;
43+
Directory.CreateDirectory(dir);
44+
return Path.Combine(dir, "sql-result-snippets.json");
45+
}
46+
}
47+
48+
public IReadOnlyList<SqlSavedQuerySnippet> Load()
49+
{
50+
string path = StoreFilePath;
51+
if (!File.Exists(path))
52+
return [];
53+
54+
try
55+
{
56+
string json = File.ReadAllText(path);
57+
List<SqlSavedQuerySnippet>? snippets = JsonSerializer.Deserialize<List<SqlSavedQuerySnippet>>(json, JsonOptions);
58+
return (snippets ?? [])
59+
.Where(snippet => !string.IsNullOrWhiteSpace(snippet.SqlText))
60+
.OrderByDescending(snippet => snippet.UpdatedAtUtc)
61+
.ToList();
62+
}
63+
catch
64+
{
65+
return [];
66+
}
67+
}
68+
69+
public void Upsert(SqlSavedQuerySnippet snippet)
70+
{
71+
ArgumentNullException.ThrowIfNull(snippet);
72+
73+
List<SqlSavedQuerySnippet> snippets = Load().ToList();
74+
int index = snippets.FindIndex(existing =>
75+
string.Equals(existing.Id, snippet.Id, StringComparison.OrdinalIgnoreCase));
76+
77+
if (index >= 0)
78+
snippets[index] = snippet;
79+
else
80+
snippets.Insert(0, snippet);
81+
82+
Persist(snippets);
83+
}
84+
85+
public bool Delete(string snippetId)
86+
{
87+
if (string.IsNullOrWhiteSpace(snippetId))
88+
return false;
89+
90+
List<SqlSavedQuerySnippet> snippets = Load().ToList();
91+
int removed = snippets.RemoveAll(existing =>
92+
string.Equals(existing.Id, snippetId, StringComparison.OrdinalIgnoreCase));
93+
if (removed == 0)
94+
return false;
95+
96+
Persist(snippets);
97+
return true;
98+
}
99+
100+
private void Persist(IReadOnlyList<SqlSavedQuerySnippet> snippets)
101+
{
102+
try
103+
{
104+
Directory.CreateDirectory(Path.GetDirectoryName(StoreFilePath)!);
105+
string json = JsonSerializer.Serialize(snippets, JsonOptions);
106+
File.WriteAllText(StoreFilePath, json);
107+
}
108+
catch
109+
{
110+
// Best effort persistence.
111+
}
112+
}
113+
}

0 commit comments

Comments
 (0)