From dfca4cd305d876a6e11076954e6f0a6aadaf1bc1 Mon Sep 17 00:00:00 2001 From: Alberto Spelta Date: Fri, 4 Sep 2026 19:00:08 +0200 Subject: [PATCH] Save crash reports and simplify startup dialogs Unhandled exceptions now create ErrorReport.txt in the application data folder with exception and environment details. The error dialog explains the failure, links to the report, and lets the user copy it to the clipboard. Windows Event Log reporting has been removed. Task dialogs now use a small fluent builder. The WebView2 Runtime prompt has also been simplified, following the .NET apphost pattern with a single download action and additional details in the expander. The CSV export no longer includes the "Save in a Subfolder" option, removing an unnecessary choice and its related conflict handling. --- src/Controllers/ExportDataController.cs | 66 +------ src/Host/BravoApplicationInitializer.cs | 8 +- src/Infrastructure/AppEnvironment.cs | 40 +--- src/Infrastructure/AppWindow.cs | 7 + .../Diagnostics/EnvironmentInfo.cs | 93 +++++++++ src/Infrastructure/Diagnostics/ErrorReport.cs | 103 ++++++++++ .../Diagnostics/ErrorReportDialog.cs | 85 ++++++++ src/Infrastructure/Helpers/ExceptionHelper.cs | 44 ----- src/Infrastructure/Helpers/ProcessHelper.cs | 3 +- src/Infrastructure/Helpers/WebView2Helper.cs | 99 +++++----- .../Windows/Dialogs/TaskDialogBuilder.cs | 186 ++++++++++++++++++ src/Infrastructure/Windows/WindowDialogs.cs | 54 ----- src/Models/ExportData/ExportDataSettings.cs | 6 - src/Program.cs | 8 +- src/Scripts/controllers/host.ts | 1 - src/Scripts/model/i18n/cz.ts | 2 - src/Scripts/model/i18n/da.ts | 2 - src/Scripts/model/i18n/de.ts | 2 - src/Scripts/model/i18n/en.ts | 2 - src/Scripts/model/i18n/es.ts | 2 - src/Scripts/model/i18n/fa.ts | 2 - src/Scripts/model/i18n/fr.ts | 2 - src/Scripts/model/i18n/gr.ts | 2 - src/Scripts/model/i18n/it.ts | 2 - src/Scripts/model/i18n/nl.ts | 2 - src/Scripts/model/i18n/pl.ts | 2 - src/Scripts/model/i18n/pt.ts | 2 - src/Scripts/model/i18n/ru.ts | 2 - src/Scripts/model/i18n/tr.ts | 2 - src/Scripts/model/i18n/uk.ts | 2 - src/Scripts/model/i18n/zh.ts | 2 - src/Scripts/model/strings.ts | 2 - src/Scripts/view/scene-export-data.ts | 18 +- .../Windows/TaskDialogBuilderTests.cs | 42 ++++ 34 files changed, 580 insertions(+), 317 deletions(-) create mode 100644 src/Infrastructure/Diagnostics/EnvironmentInfo.cs create mode 100644 src/Infrastructure/Diagnostics/ErrorReport.cs create mode 100644 src/Infrastructure/Diagnostics/ErrorReportDialog.cs create mode 100644 src/Infrastructure/Windows/Dialogs/TaskDialogBuilder.cs create mode 100644 test/Bravo.Tests/Infrastructure/Windows/TaskDialogBuilderTests.cs diff --git a/src/Controllers/ExportDataController.cs b/src/Controllers/ExportDataController.cs index e006ecb9..de4e0411 100644 --- a/src/Controllers/ExportDataController.cs +++ b/src/Controllers/ExportDataController.cs @@ -1,13 +1,9 @@ -using System.IO; -using System.Net.Mime; +using System.Net.Mime; using System.Threading; using System.Threading.Tasks; -using System.Windows.Forms; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using Sqlbi.Bravo.Infrastructure.Extensions; using Sqlbi.Bravo.Infrastructure.Helpers; -using Sqlbi.Bravo.Infrastructure.Windows; using Sqlbi.Bravo.Models; using Sqlbi.Bravo.Models.ExportData; using Sqlbi.Bravo.Services; @@ -47,12 +43,6 @@ public IActionResult ExportDelimitedTextFile(ExportDelimitedTextFromPBIReportReq { if (WindowDialogHelper.BrowseFolderDialog(out var path, cancellationToken)) { - if (request.Settings!.CreateSubfolder) - { - if (!GetExportSubfolderPath(ref path, name: request.Report!.ReportName)) - return NoContent(); - } - var job = _exportDataService.ExportDelimitedTextFile(request.Report!, request.Settings!, path, cancellationToken); return Ok(job); } @@ -81,12 +71,6 @@ public async Task ExportDelimitedTextFile(ExportDelimitedTextFrom if (WindowDialogHelper.BrowseFolderDialog(out var path, cancellationToken)) { - if (request.Settings!.CreateSubfolder) - { - if (!GetExportSubfolderPath(ref path, name: request.Dataset!.DisplayName)) - return NoContent(); - } - var job = _exportDataService.ExportDelimitedTextFile(request.Dataset!, request.Settings!, path, session.AuthenticationResult.AccessToken, cancellationToken); return Ok(job); } @@ -185,52 +169,4 @@ public IActionResult QueryExportJob(PBICloudDataset dataset) return Ok(job); } - - private static bool GetExportSubfolderPath(ref string path, string? name) - { - if (name.IsNullOrWhiteSpace()) - { - name = "New Folder"; - } - - var subfolderName = name.ReplaceInvalidPathChars(); - var subfolderPath = Path.Combine(path, subfolderName); - - if (Directory.Exists(subfolderPath)) - { - var overwriteButton = new TaskDialogCommandLinkButton("&Overwrite", "Overwrite and replace files in the destination folder"); - var keepbothButton = new TaskDialogCommandLinkButton("&Keep Both", "Files will be exported to a new folder"); - var cancelButton = new TaskDialogCommandLinkButton("&Cancel", "Cancel export"); - var heading = $"The destination folder you chose already contains a subfolder named '{subfolderName}'"; - var text = "Choose an option to proceed"; - - var clickedButton = MessageDialog.ShowDialog(heading, text, footnoteText: null, allowCancel: true, overwriteButton, keepbothButton, cancelButton); - - if (clickedButton == overwriteButton) - { - // - } - else if (clickedButton == keepbothButton) - { - for (var i = 1; /**/ ; i++) - { - var uniquePath = Path.Combine(path, $"{subfolderName} - {i}"); - - if (!Directory.Exists(uniquePath)) - { - subfolderPath = uniquePath; - break; - } - } - } - else - { - path = string.Empty; - return false; - } - } - - path = subfolderPath; - return true; - } } diff --git a/src/Host/BravoApplicationInitializer.cs b/src/Host/BravoApplicationInitializer.cs index 6c525ab9..aa2e1e17 100644 --- a/src/Host/BravoApplicationInitializer.cs +++ b/src/Host/BravoApplicationInitializer.cs @@ -9,15 +9,13 @@ namespace Sqlbi.Bravo.Host; /// -/// Provides the initialization phase that precedes the application. +/// Initializes the application process, applying process-wide settings +/// and composing the necessary components before the host is created. /// internal static class BravoApplicationInitializer { /// - /// Initializes the process: applies the process-wide settings and composes what the process owns - /// before a host exists — as explicit objects in dependency order, no container — returning them - /// in the context. later publishes them to the one real - /// service provider. + /// Initializes the application process, applying process-wide settings /// public static BravoApplicationInitializationContext Initialize() { diff --git a/src/Infrastructure/AppEnvironment.cs b/src/Infrastructure/AppEnvironment.cs index 7dbec0da..ce638ed6 100644 --- a/src/Infrastructure/AppEnvironment.cs +++ b/src/Infrastructure/AppEnvironment.cs @@ -3,13 +3,11 @@ using System.Diagnostics; using System.Drawing; using System.IO; -using System.Linq; -using System.Runtime.InteropServices; -using System.Runtime.Versioning; using System.Text.Json; using Microsoft.Win32; using Sqlbi.Bravo.Infrastructure.Configuration; using Sqlbi.Bravo.Infrastructure.Configuration.Settings; +using Sqlbi.Bravo.Infrastructure.Diagnostics; using Sqlbi.Bravo.Infrastructure.Extensions; using Sqlbi.Bravo.Infrastructure.Helpers; using Sqlbi.Bravo.Infrastructure.Security; @@ -81,7 +79,6 @@ static AppEnvironment() ProcessId = Environment.ProcessId; SessionId = currentProcess.SessionId; - ProcessPath = Environment.ProcessPath!; ApplicationDataPath = Path.Combine(Environment.GetFolderPath(DeploymentMode == AppDeploymentMode.Packaged ? Environment.SpecialFolder.UserProfile : Environment.SpecialFolder.LocalApplicationData, Environment.SpecialFolderOption.DoNotVerify), ApplicationName); ApplicationTempPath = Path.Combine(ApplicationDataPath, ".temp"); @@ -91,8 +88,6 @@ static AppEnvironment() Diagnostics = new ConcurrentDictionary(); DefaultJsonOptions = new(JsonSerializerDefaults.Web) { MaxDepth = 32 }; // see Microsoft.AspNetCore.Mvc.JsonOptions.JsonSerializerOptions - - AddEnvironmentDiagnosticInfo(); } /// @@ -103,8 +98,6 @@ static AppEnvironment() public static int ProcessId { get; } - public static string ProcessPath { get; } - public static AppPublishMode PublishMode { get @@ -168,37 +161,6 @@ public static void AddDiagnostics(DiagnosticMessageType type, string name, strin _ = Diagnostics.TryAdd(message, message); } - private static void AddEnvironmentDiagnosticInfo() - { - if (!IsDiagnosticLevelVerbose) - return; - - var targetFramework = typeof(Program).Assembly.GetCustomAttributes(typeof(TargetFrameworkAttribute), inherit: false).OfType().FirstOrDefault(); - - var info = new - { - SystemOSVersion = Environment.OSVersion.VersionString, - SystemProcessorCount = Environment.ProcessorCount, - ProcessId, - ProcessPath, - ProcessSessionId = SessionId, - RuntimeOSDescription = RuntimeInformation.OSDescription.ToString(), - RuntimeOSVersion = RuntimeInformation.RuntimeIdentifier, - RuntimeFrameworkDescription = RuntimeInformation.FrameworkDescription, - TargetFrameworkName = targetFramework?.FrameworkName ?? "n/a", - WebView2VersionInfo, - // - ApplicationPublishMode = PublishMode.ToString(), - ApplicationDeploymentMode = DeploymentMode.ToString(), - ApplicationVersion = AppVersion.InformationalVersion, - ApplicationDataPath, - ApplicationTempPath, - ApplicationUserSettingsFilePath = UserSettingsFilePath, - }; - - AddDiagnostics(DiagnosticMessageType.Json, name: $"{nameof(AppEnvironment)}.EnvironmentInfo", content: JsonSerializer.Serialize(info)); - } - private static AppDeploymentMode GetDeploymentMode() { if (DesktopBridgeHelper.IsRunningAsMsixPackage()) diff --git a/src/Infrastructure/AppWindow.cs b/src/Infrastructure/AppWindow.cs index b345f86e..133fc2bd 100644 --- a/src/Infrastructure/AppWindow.cs +++ b/src/Infrastructure/AppWindow.cs @@ -14,6 +14,7 @@ using Sqlbi.Bravo.Host; using Sqlbi.Bravo.Infrastructure.Configuration; using Sqlbi.Bravo.Infrastructure.Configuration.Settings; +using Sqlbi.Bravo.Infrastructure.Diagnostics; using Sqlbi.Bravo.Infrastructure.Extensions; using Sqlbi.Bravo.Infrastructure.Helpers; using Sqlbi.Bravo.Infrastructure.Messages; @@ -183,6 +184,12 @@ private void OnFormLoad(object? sender, EventArgs e) CenterToScreen(); _instanceActivationEvents.ActivationRequested += OnActivationRequestedRestoreWindowToForeground; + + if (AppEnvironment.IsDiagnosticLevelVerbose) + { + var content = EnvironmentInfo.Collect().ToDictionary(); + AppEnvironment.AddDiagnostics(DiagnosticMessageType.Json, name: $"{nameof(AppWindow)}.{nameof(EnvironmentInfo)}", content: JsonSerializer.Serialize(content)); + } } private void OnFormClosed(object? sender, FormClosedEventArgs e) diff --git a/src/Infrastructure/Diagnostics/EnvironmentInfo.cs b/src/Infrastructure/Diagnostics/EnvironmentInfo.cs new file mode 100644 index 00000000..bba4e43a --- /dev/null +++ b/src/Infrastructure/Diagnostics/EnvironmentInfo.cs @@ -0,0 +1,93 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Text.Json; +using Sqlbi.Bravo.Infrastructure.Telemetry; + +namespace Sqlbi.Bravo.Infrastructure.Diagnostics; + +/// +/// Provides diagnostic information about the environment in which the application is running. +/// +internal sealed class EnvironmentInfo +{ + private readonly IReadOnlyList> _entries; + + /// + /// Collects the environment information and returns an instance of . + /// + public static EnvironmentInfo Collect() + { + var entries = new KeyValuePair[] + { + new("TimestampUtc", DateTime.UtcNow.ToString("O", CultureInfo.InvariantCulture)), + new("TimestampLocal", DateTime.Now.ToString("O", CultureInfo.InvariantCulture)), + // Application + new("ApplicationSessionId", SafeRead(() => TelemetrySessionInfo.SessionId)), + new("ApplicationVersion", SafeRead(() => AppVersion.InformationalVersion)), + new("ApplicationPublishMode", SafeRead(() => AppEnvironment.PublishMode.ToString())), + new("ApplicationDeploymentMode", SafeRead(() => AppEnvironment.DeploymentMode.ToString())), + new("ApplicationDataPath", SafeRead(() => AppEnvironment.ApplicationDataPath)), + // Process + new("ProcessId", SafeRead(() => Environment.ProcessId.ToString(CultureInfo.InvariantCulture))), + new("ProcessPath", SafeRead(() => Environment.ProcessPath)), + new("ProcessSessionId", SafeRead(() => AppEnvironment.SessionId.ToString(CultureInfo.InvariantCulture))), + new("ProcessArchitecture", SafeRead(() => RuntimeInformation.ProcessArchitecture.ToString())), + new("ProcessProcessorCount", SafeRead(() => Environment.ProcessorCount.ToString(CultureInfo.InvariantCulture))), + // Machine + new("OSDescription", SafeRead(() => RuntimeInformation.OSDescription)), + new("OSArchitecture", SafeRead(() => RuntimeInformation.OSArchitecture.ToString())), + // Runtime + new("RuntimeDescription", SafeRead(() => RuntimeInformation.FrameworkDescription)), + new("RuntimeIdentifier", SafeRead(() => RuntimeInformation.RuntimeIdentifier)), + // Components + new("WebView2Version", SafeRead(() => AppEnvironment.WebView2VersionInfo)), + }; + + return new EnvironmentInfo(entries); + } + + private EnvironmentInfo(IReadOnlyList> entries) + { + _entries = entries; + } + + /// + /// Returns the environment information as a text block. + /// + public string ToText() + { + var builder = new StringBuilder(); + + builder.AppendLine("# Environment Information"); + builder.AppendLine(); + + foreach (var (name, value) in _entries) + builder.Append($"- {name}: ").AppendLine(value); + + return builder.ToString(); + } + + /// + /// Returns the environment information as dictionary. + /// + public Dictionary ToDictionary() + { + return _entries.ToDictionary((entry) => entry.Key, (entry) => entry.Value); + } + + internal static string SafeRead(Func read, string fallback = "n/a") + { + try + { + return read() ?? fallback; + } + catch (Exception ex) + { + return $"unavailable ({ex.GetType().Name})"; + } + } +} diff --git a/src/Infrastructure/Diagnostics/ErrorReport.cs b/src/Infrastructure/Diagnostics/ErrorReport.cs new file mode 100644 index 00000000..9980086d --- /dev/null +++ b/src/Infrastructure/Diagnostics/ErrorReport.cs @@ -0,0 +1,103 @@ +using System; +using System.IO; +using System.Runtime.InteropServices; +using System.Text; +using System.Windows.Forms; + +namespace Sqlbi.Bravo.Infrastructure.Diagnostics; + +/// +/// Represents a report of an error that occurred in the application. +/// +internal sealed class ErrorReport +{ + private const string FileName = "ErrorReport.txt"; + + private readonly EnvironmentInfo _environment; + private readonly Lazy _text; + + /// + /// Creates an instance for the given exception. + /// + public static ErrorReport Create(Exception exception) + { + var environment = EnvironmentInfo.Collect(); + return new ErrorReport(exception, environment); + } + + internal ErrorReport(Exception exception, EnvironmentInfo environment) + { + Exception = exception; + _environment = environment; + _text = new Lazy(GenerateText); + } + + public Exception Exception { get; } + + /// + /// The text representation of the error report. + /// + public string Text => _text.Value; + + /// + /// The file path where the error report was saved, or null if saving failed. + /// + public string? FilePath { get; private set; } + + /// + /// Attempts to save the error report to a file in the application data folder. + /// + public bool TrySave() + { + try + { + var filePath = Path.Combine(AppEnvironment.ApplicationDataPath, FileName); + File.WriteAllText(filePath, Text, Encoding.UTF8); + + FilePath = filePath; + return true; + } + catch (Exception) + { + return false; + } + } + + /// + /// Attempts to copy the report to the clipboard. + /// + public bool TryCopyToClipboard() + { + try + { + Clipboard.SetDataObject( + data: new DataObject(DataFormats.UnicodeText, Text), + copy: true, + retryTimes: 10, + retryDelay: 100); + + return true; + } + catch (ExternalException) + { + return false; + } + } + + public override string ToString() => Text; + + private string GenerateText() + { + var builder = new StringBuilder(); + + builder.AppendLine(_environment.ToText()); + + builder.AppendLine("# Error Details"); + builder.AppendLine(); + builder.AppendLine("```"); + builder.AppendLine(Exception.ToString()); + builder.AppendLine("```"); + + return builder.ToString(); + } +} diff --git a/src/Infrastructure/Diagnostics/ErrorReportDialog.cs b/src/Infrastructure/Diagnostics/ErrorReportDialog.cs new file mode 100644 index 00000000..55901869 --- /dev/null +++ b/src/Infrastructure/Diagnostics/ErrorReportDialog.cs @@ -0,0 +1,85 @@ +using System; +using System.Text; +using System.Windows.Forms; +using Sqlbi.Bravo.Infrastructure.Helpers; +using Sqlbi.Bravo.Infrastructure.Windows.Dialogs; + +namespace Sqlbi.Bravo.Infrastructure.Diagnostics; + +/// +/// Provides a dialog to report an error that occurred in the application. +/// +internal static class ErrorReportDialog +{ + //private const string BugReportUrl = "https://github.com/sql-bi/Bravo/issues/new"; + + public static void Show(ErrorReport report) + { + //var reportButton = new TaskDialogCommandLinkButton("&Report this issue", "Copies the report to the clipboard and opens the Bravo issue tracker in your browser, where you can paste it."); + var copyButton = new TaskDialogCommandLinkButton("&Copy to clipboard", "Copies the crash report to the clipboard.") + { + // Keep the dialog open after clicking the button + AllowCloseDialog = false, + }; + + copyButton.Click += (_, _) => _ = report.TryCopyToClipboard(); + + var clickedButton = TaskDialogBuilder.Create() + .WithCaption(AppEnvironment.ApplicationMainWindowTitle) + .WithIcon(TaskDialogIcon.Error) + .WithStartupLocation(TaskDialogStartupLocation.CenterScreen) + .WithAllowCancel() + .WithSizeToContent() + .WithEnableLinks((href) => _ = ProcessHelper.Open(href)) + .WithHeading("Bravo encountered an unexpected error.") + .WithText(GetText(report)) + .AddButtons(/*reportButton,*/ copyButton, TaskDialogButton.Close) + .WithDefaultButton(copyButton) + .Show(); + + //if (clickedButton == reportButton) + //{ + // _ = report.TryCopyToClipboard(); + // _ = ProcessHelper.OpenBrowser(new Uri(BugReportUrl, UriKind.Absolute)); + //} + } + + private static string GetText(ErrorReport report) + { + var text = new StringBuilder(); + + text.AppendLine(report.FilePath is not null + ? "A crash report has been saved and can be used to diagnose the problem." + : "The crash report could not be saved."); + + if (report.FilePath is not null) + { + text.AppendLine(); + text.AppendLine("Report file:"); + text.Append("").Append(report.FilePath).AppendLine(""); + } + + text.AppendLine(); + text.AppendLine("Exception:"); + text.AppendLine($"{report.Exception.GetType().FullName}: {report.Exception.Message}"); + + text.AppendLine(); + text.AppendLine("Location:"); + text.AppendLine(GetExceptionLocation(report.Exception)); + + return text.ToString(); + + static string GetExceptionLocation(Exception exception) + { + if (exception.TargetSite is { } method) + { + if (method.DeclaringType is { } type) + { + return $"{type.FullName}.{method.Name}"; + } + } + + return exception.Source ?? ""; + } + } +} diff --git a/src/Infrastructure/Helpers/ExceptionHelper.cs b/src/Infrastructure/Helpers/ExceptionHelper.cs index 8884870c..a27227ba 100644 --- a/src/Infrastructure/Helpers/ExceptionHelper.cs +++ b/src/Infrastructure/Helpers/ExceptionHelper.cs @@ -3,7 +3,6 @@ using System.Runtime.InteropServices; using System.Security; using System.Threading; -using System.Windows.Forms; namespace Sqlbi.Bravo.Infrastructure.Helpers; @@ -62,47 +61,4 @@ public static bool IsSafeException(Exception ex) return false; } - - public static void ShowDialog(Exception exception) - { - var page = new TaskDialogPage() - { - Caption = AppEnvironment.ApplicationMainWindowTitle, - Heading = @$"Unhandled exception has occurred. The application will be shut down and the error details will be logged in the Windows Event Log. - -[{exception.GetType().Name}] {exception.Message}", - Icon = TaskDialogIcon.Error, - AllowCancel = false, - Buttons = - { - new TaskDialogCommandLinkButton("&Copy details", "Copy error details to clipboard and close") - { - Tag = 10 - }, - new TaskDialogCommandLinkButton("&Close", "Terminate the application") - { - Tag = 20 - }, - }, - Expander = new TaskDialogExpander() - { - Expanded = false, - Text = $"{exception}", - Position = TaskDialogExpanderPosition.AfterFootnote, - } - }; - - var dialogButton = TaskDialog.ShowDialog(page, TaskDialogStartupLocation.CenterScreen); - - switch (dialogButton.Tag) - { - case 10: - Clipboard.SetText(page.Expander.Text, TextDataFormat.Text); - break; - case 20: - break; - default: - throw new BravoUnexpectedInvalidOperationException($"Unhandled {nameof(TaskDialogButton)} result ({dialogButton.Tag})"); - } - } } diff --git a/src/Infrastructure/Helpers/ProcessHelper.cs b/src/Infrastructure/Helpers/ProcessHelper.cs index a3a009dc..7a171728 100644 --- a/src/Infrastructure/Helpers/ProcessHelper.cs +++ b/src/Infrastructure/Helpers/ProcessHelper.cs @@ -179,12 +179,13 @@ public static bool OpenShellExecute(string path, bool waitForStarted, [NotNullWh { if (File.Exists(path)) { + const string Txt = ".txt"; const string Pbix = ".pbix"; const string Xlsx = ".xlsx"; const string CodeWorkspace = ".code-workspace"; var extension = Path.GetExtension(path); - var isAllowed = (new[] { Pbix, Xlsx, CodeWorkspace }).Any((ext) => ext.EqualsI(extension)); + var isAllowed = (new[] { Txt, Pbix, Xlsx, CodeWorkspace }).Any((ext) => ext.EqualsI(extension)); var isPbix = extension.EqualsI(Pbix); if (isAllowed) diff --git a/src/Infrastructure/Helpers/WebView2Helper.cs b/src/Infrastructure/Helpers/WebView2Helper.cs index 76ffd139..16d8a1fe 100644 --- a/src/Infrastructure/Helpers/WebView2Helper.cs +++ b/src/Infrastructure/Helpers/WebView2Helper.cs @@ -1,16 +1,14 @@ using System; using System.Collections.Generic; -using System.Diagnostics; -using System.IO; using System.Linq; using System.Net; -using System.Net.Http; using System.Reflection; +using System.Runtime.InteropServices; using System.Windows.Forms; using Microsoft.Web.WebView2.Core; using Sqlbi.Bravo.Infrastructure.Configuration.Settings; using Sqlbi.Bravo.Infrastructure.Extensions; -using Sqlbi.Bravo.Infrastructure.Windows; +using Sqlbi.Bravo.Infrastructure.Windows.Dialogs; using Sqlbi.Bravo.Infrastructure.Windows.Interop; namespace Sqlbi.Bravo.Infrastructure.Helpers; @@ -21,11 +19,14 @@ internal static class WebView2Helper //internal static extern int GetAvailableCoreWebView2BrowserVersionString([In][MarshalAs(UnmanagedType.LPWStr)] string? browserExecutableFolder, [MarshalAs(UnmanagedType.LPWStr)] ref string versionInfo); /// - /// The Bootstrapper is a tiny installer that downloads the Evergreen Runtime matching device architecture and installs it locally. - /// https://developer.microsoft.com/en-us/microsoft-edge/webview2/#download-section + /// The bootstrapper URL Microsoft provides to download the Evergreen WebView2 Runtime. /// - public static string EvergreenRuntimeBootstrapperUrl = "https://go.microsoft.com/fwlink/p/?LinkId=2124703"; - public static string MicrosoftReferenceUrl = "https://developer.microsoft.com/en-us/microsoft-edge/webview2"; + private const string BootstrapperDownloadUrl = "https://go.microsoft.com/fwlink/p/?LinkId=2124703"; + + /// + /// The page Microsoft addresses to end users who need to install the runtime themselves. + /// + private const string ConsumerDownloadPageUrl = "https://developer.microsoft.com/microsoft-edge/webview2/consumer/"; public static void TryAndIgnoreUnsupportedError(Action action) { @@ -74,53 +75,51 @@ public static void TryAndIgnoreUnsupportedError(Action action) */ } + /// + /// Ensures that the WebView2 Runtime is installed. If not, prompts the user to download and install it. + /// public static void EnsureRuntimeIsInstalled() { if (AppEnvironment.IsWebView2RuntimeInstalled) return; - var heading = $"{AppEnvironment.ApplicationMainWindowTitle} requires the Microsoft Edge WebView2 runtime which is not currently installed.\r\n\r\nChoose an option to proceed with the installation:"; - var footnoteText = $"For more details please refer to the following address:\r\n\r\n - {AppEnvironment.ApplicationWebsiteUrl}\r\n - {MicrosoftReferenceUrl}"; - var automaticButton = new TaskDialogCommandLinkButton("&Automatic", "Download and install Microsoft Edge WebView2 runtime now"); - var manualButton = new TaskDialogCommandLinkButton("&Manual", "Open the browser on the download page"); - var cancelButton = new TaskDialogCommandLinkButton("&Cancel", "Close the application without installing"); - - var dialogButton = MessageDialog.ShowDialog(heading, text: null, footnoteText, allowCancel: false, automaticButton, manualButton, cancelButton); - - if (dialogButton == automaticButton) - { - DownloadAndInstallRuntime(); - } - else if (dialogButton == manualButton) - { - var address = new Uri(MicrosoftReferenceUrl, uriKind: UriKind.Absolute); - _ = ProcessHelper.OpenBrowser(address); - } - else if (dialogButton == cancelButton) - { - // - } - - Environment.Exit(NativeMethods.ERROR_SUCCESS); - } - - private static void DownloadAndInstallRuntime() - { - // TODO: use http client from pool, add proxy support - using var httpClient = new HttpClient(); - - var fileBytes = httpClient.GetByteArrayAsync(EvergreenRuntimeBootstrapperUrl).GetAwaiter().GetResult(); - var filePath = Path.Combine(AppEnvironment.ApplicationTempPath, $"MicrosoftEdgeWebview2Setup-{DateTime.Now:yyyyMMddHHmmss}.exe"); - - File.WriteAllBytes(filePath, fileBytes); - - using var process = Process.Start(filePath); // add switches ? i.e. /silent /install - process.WaitForExit(); - - if (process.ExitCode != NativeMethods.ERROR_SUCCESS) - { - ExceptionHelper.WriteToEventLog($"WebView2 bootstrapper exit code '{process.ExitCode}'", EventLogEntryType.Warning); - } + var downloadButton = new TaskDialogCommandLinkButton("&Download it now", "You will need to run the downloaded installer, then start Bravo again."); + + var clickedButton = TaskDialogBuilder.Create() + .WithCaption(AppEnvironment.ApplicationMainWindowTitle) + .WithCurrentProcessIcon() + .WithStartupLocation(TaskDialogStartupLocation.CenterScreen) + .WithAllowCancel() + .WithSizeToContent() + .WithEnableLinks(OpenBrowser) + .WithHeading("You must install WebView2 Runtime to run this application.") + .WithText("Bravo needs Microsoft Edge WebView2 Runtime to display its user interface.") + .WithExpander(GetDetails(), expanded: false, TaskDialogExpanderPosition.AfterText, expandedButtonText: "Hide details", collapsedButtonText: "Show details") + .AddButtons(downloadButton, TaskDialogButton.Close) + .WithDefaultButton(downloadButton) + .Show(); + + if (clickedButton == downloadButton) + OpenBrowser(BootstrapperDownloadUrl); + + // The application cannot run without WebView2 Runtime, so exit with a specific error code + Environment.Exit(NativeMethods.ERROR_CANCELLED); + + static string GetDetails() + => $""" + Architecture: {RuntimeInformation.OSArchitecture.ToString().ToLowerInvariant()} + Windows version: {Environment.OSVersion.Version} + Bravo version: {AppVersion.SemanticVersion} + + Learn more: + {ConsumerDownloadPageUrl} + + Download link: + {BootstrapperDownloadUrl} + """; + + static void OpenBrowser(string url) + => ProcessHelper.OpenBrowser(new Uri(url, UriKind.Absolute)); } public static string GetProxyArguments(ProxySettings? proxySettings, IWebProxy systemProxy) diff --git a/src/Infrastructure/Windows/Dialogs/TaskDialogBuilder.cs b/src/Infrastructure/Windows/Dialogs/TaskDialogBuilder.cs new file mode 100644 index 00000000..7ecb6e1f --- /dev/null +++ b/src/Infrastructure/Windows/Dialogs/TaskDialogBuilder.cs @@ -0,0 +1,186 @@ +using System; +using System.Diagnostics; +using System.Drawing; +using System.Windows.Forms; + +namespace Sqlbi.Bravo.Infrastructure.Windows.Dialogs; + +/// +/// Provides a fluent builder for creating and showing a dialog. +/// +public sealed class TaskDialogBuilder +{ + private static readonly Lazy s_currentProcessIcon = new(LoadCurrentProcessIcon); + + private readonly TaskDialogPage _page; + private IntPtr _ownerHandle = IntPtr.Zero; + private TaskDialogStartupLocation _startupLocation = TaskDialogStartupLocation.CenterOwner; + + /// + /// Creates a new instance. + /// + public static TaskDialogBuilder Create() => new(); + + private TaskDialogBuilder() + { + _page = new TaskDialogPage + { + AllowCancel = false, + AllowMinimize = false + }; + } + + public TaskDialogBuilder WithCaption(string caption) + { + _page.Caption = caption; + return this; + } + + public TaskDialogBuilder WithHeading(string heading) + { + _page.Heading = heading; + return this; + } + + public TaskDialogBuilder WithText(string text) + { + _page.Text = text; + return this; + } + + /// + /// Sets the dialog icon to the icon of the current process, if available. + /// + public TaskDialogBuilder WithCurrentProcessIcon() + { + if (s_currentProcessIcon.Value is { } icon) + _page.Icon = icon; + + return this; + } + + public TaskDialogBuilder WithIcon(TaskDialogIcon icon) + { + _page.Icon = icon; + return this; + } + + public TaskDialogBuilder WithFootnote(string text, TaskDialogIcon? icon = null) + { + _page.Footnote = new TaskDialogFootnote(text) + { + Icon = icon, + }; + return this; + } + + public TaskDialogBuilder WithExpander( + string text, + bool expanded = false, + TaskDialogExpanderPosition position = TaskDialogExpanderPosition.AfterText, + string? expandedButtonText = null, + string? collapsedButtonText = null) + { + _page.Expander = new TaskDialogExpander(text) + { + Expanded = expanded, + Position = position, + ExpandedButtonText = expandedButtonText, + CollapsedButtonText = collapsedButtonText, + }; + return this; + } + + /// + /// Enables clickable links in the dialog text and footnote, and invokes the specified callback when a link is clicked. + /// + public TaskDialogBuilder WithEnableLinks(Action linkClicked) + { + _page.EnableLinks = true; + _page.LinkClicked += (_, e) => linkClicked(e.LinkHref); + return this; + } + + /// + /// Sets the dialog to automatically size to its content. If false, the dialog will have a fixed size. + /// + public TaskDialogBuilder WithSizeToContent(bool sizeToContent = true) + { + _page.SizeToContent = sizeToContent; + return this; + } + + /// + /// Sets the default button that is focused when the dialog is shown. + /// + public TaskDialogBuilder WithDefaultButton(TaskDialogButton button) + { + _page.DefaultButton = button; + return this; + } + + /// + /// Sets the window that owns the dialog to the main window of the current process. + /// + public TaskDialogBuilder WithCurrentProcessMainWindowOwner() + { + using var process = Process.GetCurrentProcess(); + + return WithOwner(process.MainWindowHandle); + } + + /// + /// Sets the window that owns the dialog. Without an owner the dialog is top-level. + /// + public TaskDialogBuilder WithOwner(IntPtr ownerHandle) + { + _ownerHandle = ownerHandle; + return this; + } + + public TaskDialogBuilder WithStartupLocation(TaskDialogStartupLocation startupLocation) + { + _startupLocation = startupLocation; + return this; + } + + public TaskDialogBuilder WithAllowCancel(bool allowCancel = true) + { + _page.AllowCancel = allowCancel; + return this; + } + + public TaskDialogBuilder AddButtons(params TaskDialogButton[] buttons) + { + foreach (var button in buttons) + _page.Buttons.Add(button); + + return this; + } + + public TaskDialogPage Build() + { + return _page; + } + + /// + /// Shows the dialog modally and returns the button the user chose. + /// + public TaskDialogButton Show() + { + if (_ownerHandle != IntPtr.Zero) + { + return TaskDialog.ShowDialog(_ownerHandle, _page, _startupLocation); + } + + return TaskDialog.ShowDialog(_page, _startupLocation); + } + + private static TaskDialogIcon? LoadCurrentProcessIcon() + { + if (Environment.ProcessPath is { } path && Icon.ExtractAssociatedIcon(path) is { } icon) + return new TaskDialogIcon(icon); + + return null; + } +} diff --git a/src/Infrastructure/Windows/WindowDialogs.cs b/src/Infrastructure/Windows/WindowDialogs.cs index a8249f76..9e566e4f 100644 --- a/src/Infrastructure/Windows/WindowDialogs.cs +++ b/src/Infrastructure/Windows/WindowDialogs.cs @@ -3,7 +3,6 @@ using System.Runtime.InteropServices; using System.Windows.Forms; using Sqlbi.Bravo.Infrastructure.Extensions; -using Sqlbi.Bravo.Infrastructure.Helpers; using Sqlbi.Bravo.Infrastructure.Windows.Interop; namespace Sqlbi.Bravo.Infrastructure.Windows; @@ -129,56 +128,3 @@ public DialogResult ShowDialog(IntPtr hWnd) } } } - -internal class MessageDialog -{ - public static void Show(string heading, string text) - { - var appIcon = Icon.ExtractAssociatedIcon(AppEnvironment.ProcessPath); - var icon = new TaskDialogIcon(appIcon!); - - var page = new TaskDialogPage() - { - Caption = AppEnvironment.ApplicationMainWindowTitle, - Heading = heading, - Text = text, - Icon = icon, - AllowCancel = true - }; - - var hwndOwner = ProcessHelper.GetCurrentProcessMainWindowHandle(); - _ = TaskDialog.ShowDialog(hwndOwner, page, TaskDialogStartupLocation.CenterScreen); - } - - public static TaskDialogButton ShowDialog(string heading, string? text, string? footnoteText, bool allowCancel, params TaskDialogButton[] buttons) - { - var appIcon = Icon.ExtractAssociatedIcon(AppEnvironment.ProcessPath); - var icon = new TaskDialogIcon(appIcon!); - - var page = new TaskDialogPage() - { - Caption = AppEnvironment.ApplicationMainWindowTitle, - Heading = heading, - Text = text, - Icon = icon, - AllowCancel = allowCancel, // || buttons.Any((button) => button == TaskDialogButton.Cancel), - AllowMinimize = false - }; - - if (footnoteText is not null) - { - page.Footnote = new TaskDialogFootnote() - { - Text = footnoteText, - }; - } - - foreach (var button in buttons) - page.Buttons.Add(button); - - var hwndOwner = ProcessHelper.GetCurrentProcessMainWindowHandle(); - var clickedButton = TaskDialog.ShowDialog(hwndOwner, page, TaskDialogStartupLocation.CenterScreen); - - return clickedButton; - } -} diff --git a/src/Models/ExportData/ExportDataSettings.cs b/src/Models/ExportData/ExportDataSettings.cs index 9fcdfdab..87c0fa44 100644 --- a/src/Models/ExportData/ExportDataSettings.cs +++ b/src/Models/ExportData/ExportDataSettings.cs @@ -44,12 +44,6 @@ public class ExportDelimitedTextSettings : ExportDataSettings /// [JsonPropertyName("quoteStringFields")] public bool QuoteStringFields { get; set; } = false; - - /// - /// Specifies whether to export the data to a subfolder with the same name as the source - /// - [JsonPropertyName("createSubfolder")] - public bool CreateSubfolder { get; set; } = false; } public class ExportExcelSettings : ExportDataSettings diff --git a/src/Program.cs b/src/Program.cs index af5def9b..cdb81d60 100644 --- a/src/Program.cs +++ b/src/Program.cs @@ -1,6 +1,6 @@ using System; using Sqlbi.Bravo.Host; -using Sqlbi.Bravo.Infrastructure.Helpers; +using Sqlbi.Bravo.Infrastructure.Diagnostics; using Sqlbi.Bravo.Infrastructure.Telemetry; namespace Sqlbi.Bravo; @@ -30,7 +30,11 @@ private static void Main() catch (Exception ex) { TelemetryService.Instance.TrackException(ex); - ExceptionHelper.ShowDialog(ex); + + var report = ErrorReport.Create(ex); + report.TrySave(); + ErrorReportDialog.Show(report); + throw; } } diff --git a/src/Scripts/controllers/host.ts b/src/Scripts/controllers/host.ts index 55157513..ece65985 100644 --- a/src/Scripts/controllers/host.ts +++ b/src/Scripts/controllers/host.ts @@ -131,7 +131,6 @@ export interface ExportDelimitedTextSettings { unicodeEncoding: boolean delimiter?: string quoteStringFields: boolean - createSubfolder: boolean } export interface ExportExcelSettings { diff --git a/src/Scripts/model/i18n/cz.ts b/src/Scripts/model/i18n/cz.ts index 57011d96..fdc76929 100644 --- a/src/Scripts/model/i18n/cz.ts +++ b/src/Scripts/model/i18n/cz.ts @@ -182,8 +182,6 @@ const locale: Locale = { [_.exportDataCSVDelimiterTab]: "Tab", [_.exportDataCSVEncoding]: "Kódování", [_.exportDataCSVEncodingDesc]: "", - [_.exportDataCSVFolder]: "Ušetřete v podsložce", - [_.exportDataCSVFolderDesc]: "Uložte generované soubory CSV v podsložce.", [_.exportDataCSVQuote]: "Uzavřete textový řetězec do uvozovek", [_.exportDataCSVQuoteDesc]: "Ujistěte se, že každý řetězec je uzavřen do dvojitých uvozovek.", [_.exportDataExcelCreateExportSummary]: "Exportovat souhrn", diff --git a/src/Scripts/model/i18n/da.ts b/src/Scripts/model/i18n/da.ts index 57816158..00fc1f27 100644 --- a/src/Scripts/model/i18n/da.ts +++ b/src/Scripts/model/i18n/da.ts @@ -182,8 +182,6 @@ const locale: Locale = { [_.exportDataCSVDelimiterTab]: "Tab", [_.exportDataCSVEncoding]: "Tegnsæt", [_.exportDataCSVEncodingDesc]: "", - [_.exportDataCSVFolder]: "Gem i en undermappe", - [_.exportDataCSVFolderDesc]: "Gem genererede CSV -filer i en undermappe.", [_.exportDataCSVQuote]: "Afgræns strenge med citationstegn", [_.exportDataCSVQuoteDesc]: "Sørger for at strenge er afgrænset af citationstegn.", [_.exportDataExcelCreateExportSummary]: "Eksport Opsummering", diff --git a/src/Scripts/model/i18n/de.ts b/src/Scripts/model/i18n/de.ts index 7ac52ffe..79b5c128 100644 --- a/src/Scripts/model/i18n/de.ts +++ b/src/Scripts/model/i18n/de.ts @@ -184,8 +184,6 @@ const locale: Locale = { [_.exportDataCSVDelimiterTab]: "Tab", [_.exportDataCSVEncoding]: "Kodierung", [_.exportDataCSVEncodingDesc]: "", - [_.exportDataCSVFolder]: "In einem Unterordner sparen", - [_.exportDataCSVFolderDesc]: "Speichern Sie generierte CSV -Dateien in einem Unterordner.", [_.exportDataCSVQuote]: "Zeichenketten in Anführungszeichen einschließen", [_.exportDataCSVQuoteDesc]: "Stellt sicher, dass jede Zeichenkette in doppelte Anführungszeichen gesetzt wird.", [_.exportDataExcelCreateExportSummary]: "Export Zusammenfassung", diff --git a/src/Scripts/model/i18n/en.ts b/src/Scripts/model/i18n/en.ts index d9900152..56a283ad 100644 --- a/src/Scripts/model/i18n/en.ts +++ b/src/Scripts/model/i18n/en.ts @@ -185,8 +185,6 @@ const locale: Locale = { [_.exportDataCSVDelimiterTab]: "Tab", [_.exportDataCSVEncoding]: "Encoding", [_.exportDataCSVEncodingDesc]: "", - [_.exportDataCSVFolder]: "Save in a Subfolder", - [_.exportDataCSVFolderDesc]: "Save generated CSV files in a subfolder.", [_.exportDataCSVQuote]: "Enclose Strings in Quotes", [_.exportDataCSVQuoteDesc]: "Make sure every string is enclosed in double quotes.", [_.exportDataExcelCreateExportSummary]: "Export Summary", diff --git a/src/Scripts/model/i18n/es.ts b/src/Scripts/model/i18n/es.ts index 57404afe..90669052 100644 --- a/src/Scripts/model/i18n/es.ts +++ b/src/Scripts/model/i18n/es.ts @@ -182,8 +182,6 @@ const locale: Locale = { [_.exportDataCSVDelimiterTab]: "Tabulador", [_.exportDataCSVEncoding]: "Codificación", [_.exportDataCSVEncodingDesc]: "", - [_.exportDataCSVFolder]: "Ahorrar en una subcarpeta", - [_.exportDataCSVFolderDesc]: "Guarde los archivos CSV generados en una subcarpeta.", [_.exportDataCSVQuote]: "Encerrar la cadena de datos entre comillas", [_.exportDataCSVQuoteDesc]: "Hay que asegurarse de que cada cadena de datos esté encerrada entre comillas dobles.", [_.exportDataExcelCreateExportSummary]: "Exportar el resumen", diff --git a/src/Scripts/model/i18n/fa.ts b/src/Scripts/model/i18n/fa.ts index 32fbf7b2..bd9d4392 100644 --- a/src/Scripts/model/i18n/fa.ts +++ b/src/Scripts/model/i18n/fa.ts @@ -186,8 +186,6 @@ const locale: Locale = { [_.exportDataCSVDelimiterTab]: "Tab", [_.exportDataCSVEncoding]: "رمزگذاری", [_.exportDataCSVEncodingDesc]: "", - [_.exportDataCSVFolder]: "ذخیره در زیرپوشه", - [_.exportDataCSVFolderDesc]: "ذخیره اکسل خروجی گرفته شده در زیرپوشه.", [_.exportDataCSVQuote]: "رشته ها را در دو گیومه قرار دهید", [_.exportDataCSVQuoteDesc]: "اطمینان حاصل کنید که هر رشته در گیومه های دوتایی محصور شده است.", [_.exportDataExcelCreateExportSummary]: "خلاصه خروجی گرفتن", diff --git a/src/Scripts/model/i18n/fr.ts b/src/Scripts/model/i18n/fr.ts index b28b6f07..22e5ce88 100644 --- a/src/Scripts/model/i18n/fr.ts +++ b/src/Scripts/model/i18n/fr.ts @@ -184,8 +184,6 @@ const locale: Locale = { [_.exportDataCSVDelimiterTab]: "Tabulation", [_.exportDataCSVEncoding]: "Encodage", [_.exportDataCSVEncodingDesc]: "", - [_.exportDataCSVFolder]: "Sauver dans un sous-dossier", - [_.exportDataCSVFolderDesc]: "Enregistrer les fichiers CSV générés dans un sous-dossier.", [_.exportDataCSVQuote]: "Mettre les chaînes de caractères entre apostrophes", [_.exportDataCSVQuoteDesc]: "S'assurer que chaque chaîne de caractères est mise entre guillemets.", [_.exportDataExcelCreateExportSummary]: "Exporter le résumé", diff --git a/src/Scripts/model/i18n/gr.ts b/src/Scripts/model/i18n/gr.ts index 91f08cbc..f19c1f28 100644 --- a/src/Scripts/model/i18n/gr.ts +++ b/src/Scripts/model/i18n/gr.ts @@ -182,8 +182,6 @@ const locale: Locale = { [_.exportDataCSVDelimiterTab]: "Χαρακτήρας Tab", [_.exportDataCSVEncoding]: "Κωδικοποίηση", [_.exportDataCSVEncodingDesc]: "", - [_.exportDataCSVFolder]: "Αποθήκευση σε υποψήφιο", - [_.exportDataCSVFolderDesc]: "Αποθηκεύστε τα δημιουργημένα αρχεία CSV σε έναν υποτονό.", [_.exportDataCSVQuote]: "Βάλε εισαγωγικά στα αλφαριθμητικά", [_.exportDataCSVQuoteDesc]: "Βεβαιώσου ότι τα αλφαριθμητικά είναι μέσα σε διπλά εισαγωγικά.", [_.exportDataExcelCreateExportSummary]: "Εξαγωγή Περίληψης", diff --git a/src/Scripts/model/i18n/it.ts b/src/Scripts/model/i18n/it.ts index a2929158..3e886e0e 100644 --- a/src/Scripts/model/i18n/it.ts +++ b/src/Scripts/model/i18n/it.ts @@ -182,8 +182,6 @@ const locale: Locale = { [_.exportDataCSVDelimiterTab]: "Tab", [_.exportDataCSVEncoding]: "Codifica", [_.exportDataCSVEncodingDesc]: "", - [_.exportDataCSVFolder]: "Salva in una sottocartella", - [_.exportDataCSVFolderDesc]: "Salva file CSV generati in una sottocartella.", [_.exportDataCSVQuote]: "Stringhe tra virgolette", [_.exportDataCSVQuoteDesc]: "Racchiudi tutte le stringhe tra virgolette.", [_.exportDataExcelCreateExportSummary]: "Foglio di Riepilogo ", diff --git a/src/Scripts/model/i18n/nl.ts b/src/Scripts/model/i18n/nl.ts index 63d76b7e..d608bd81 100644 --- a/src/Scripts/model/i18n/nl.ts +++ b/src/Scripts/model/i18n/nl.ts @@ -182,8 +182,6 @@ const locale: Locale = { [_.exportDataCSVDelimiterTab]: "Tab", [_.exportDataCSVEncoding]: "Codering", [_.exportDataCSVEncodingDesc]: "", - [_.exportDataCSVFolder]: "Opslaan in een submap", - [_.exportDataCSVFolderDesc]: "Opslaan gegenereerde CSV -bestanden in een submap.", [_.exportDataCSVQuote]: "Tekenreeksen tussen aanhalingstekens insluiten", [_.exportDataCSVQuoteDesc]: "Zorg ervoor dat elke tekenreeks tussen dubbele aanhalingstekens staat.", [_.exportDataExcelCreateExportSummary]: "Exporteer samenvatting", diff --git a/src/Scripts/model/i18n/pl.ts b/src/Scripts/model/i18n/pl.ts index 70b4eaff..05a39000 100644 --- a/src/Scripts/model/i18n/pl.ts +++ b/src/Scripts/model/i18n/pl.ts @@ -182,8 +182,6 @@ const locale: Locale = { [_.exportDataCSVDelimiterTab]: "Tab", [_.exportDataCSVEncoding]: "Kodowanie", [_.exportDataCSVEncodingDesc]: "", - [_.exportDataCSVFolder]: "Zapisz w podfolderze", - [_.exportDataCSVFolderDesc]: "Zapisz wygenerowane pliki CSV w podfolderze.", [_.exportDataCSVQuote]: "Umieść tekst w cudzysłowie", [_.exportDataCSVQuoteDesc]: "Upewnij się, że każdy tekst umieszczony jest w cudzysłowie.", [_.exportDataExcelCreateExportSummary]: "Eksportuj podsumowanie", diff --git a/src/Scripts/model/i18n/pt.ts b/src/Scripts/model/i18n/pt.ts index 9197a461..1eff8a08 100644 --- a/src/Scripts/model/i18n/pt.ts +++ b/src/Scripts/model/i18n/pt.ts @@ -182,8 +182,6 @@ const locale: Locale = { [_.exportDataCSVDelimiterTab]: "Tabulador", [_.exportDataCSVEncoding]: "Codificação", [_.exportDataCSVEncodingDesc]: "", - [_.exportDataCSVFolder]: "Salvar em uma subpasta", - [_.exportDataCSVFolderDesc]: "Salvar arquivos CSV gerados em uma subpasta.", [_.exportDataCSVQuote]: "Coloque Texto Entre Aspas", [_.exportDataCSVQuoteDesc]: "Certifique-se que todo o texto está entre aspas.", [_.exportDataExcelCreateExportSummary]: "Exportar Sumário", diff --git a/src/Scripts/model/i18n/ru.ts b/src/Scripts/model/i18n/ru.ts index 546adaba..3474cf49 100644 --- a/src/Scripts/model/i18n/ru.ts +++ b/src/Scripts/model/i18n/ru.ts @@ -182,8 +182,6 @@ const locale: Locale = { [_.exportDataCSVDelimiterTab]: "Табуляция", [_.exportDataCSVEncoding]: "Кодировка", [_.exportDataCSVEncodingDesc]: "", - [_.exportDataCSVFolder]: "Сохранить в подпапке", - [_.exportDataCSVFolderDesc]: "Сохраните сгенерированные файлы CSV в подпапке.", [_.exportDataCSVQuote]: "Заключить строки в кавычки", [_.exportDataCSVQuoteDesc]: "Убедитесь, что каждая строка заключена в двойные кавычки.", [_.exportDataExcelCreateExportSummary]: "Экспорт сведенных данных", diff --git a/src/Scripts/model/i18n/tr.ts b/src/Scripts/model/i18n/tr.ts index 07520a88..14776158 100644 --- a/src/Scripts/model/i18n/tr.ts +++ b/src/Scripts/model/i18n/tr.ts @@ -184,8 +184,6 @@ const locale: Locale = { [_.exportDataCSVDelimiterTab]: "Sekme", [_.exportDataCSVEncoding]: "Dil Kodlaması", [_.exportDataCSVEncodingDesc]: "", - [_.exportDataCSVFolder]: "Bir alt klasöre kaydedin", - [_.exportDataCSVFolderDesc]: "Oluşturulan CSV dosyalarını bir alt klasöre kaydedin.", [_.exportDataCSVQuote]: "Dizeleri Çift Tırnak İçine Alın", [_.exportDataCSVQuoteDesc]: "Her dizenin çift tırnak içine alındığından emin olun.", [_.exportDataExcelCreateExportSummary]: "Dışa Aktarma Özeti", diff --git a/src/Scripts/model/i18n/uk.ts b/src/Scripts/model/i18n/uk.ts index 1c60eb8d..c976af2e 100644 --- a/src/Scripts/model/i18n/uk.ts +++ b/src/Scripts/model/i18n/uk.ts @@ -183,8 +183,6 @@ const locale: Locale = { [_.exportDataCSVDelimiterTab]: "Табуляція", [_.exportDataCSVEncoding]: "Кодування", [_.exportDataCSVEncodingDesc]: "", - [_.exportDataCSVFolder]: "Зберегти в підпапці", - [_.exportDataCSVFolderDesc]: "Збережіть сформовані CSV-файли в підпапці.", [_.exportDataCSVQuote]: "Взяти рядки в лапки", [_.exportDataCSVQuoteDesc]: "Переконайтеся, що кожен рядок укладений у подвійні лапки.", [_.exportDataExcelCreateExportSummary]: "Експортувати звіт", diff --git a/src/Scripts/model/i18n/zh.ts b/src/Scripts/model/i18n/zh.ts index 6fade7f1..20bf6cc1 100644 --- a/src/Scripts/model/i18n/zh.ts +++ b/src/Scripts/model/i18n/zh.ts @@ -182,8 +182,6 @@ const locale: Locale = { [_.exportDataCSVDelimiterTab]: "制表符", [_.exportDataCSVEncoding]: "编码格式", [_.exportDataCSVEncodingDesc]: "", - [_.exportDataCSVFolder]: "保存在子文件夹中", - [_.exportDataCSVFolderDesc]: "将生成的CSV文件保存在子文件夹中。", [_.exportDataCSVQuote]: "用引号包含字符串", [_.exportDataCSVQuoteDesc]: "确保每一个字符串都包含在双引号中", [_.exportDataExcelCreateExportSummary]: "导出记录日志", diff --git a/src/Scripts/model/strings.ts b/src/Scripts/model/strings.ts index e78f2929..eb220a5a 100644 --- a/src/Scripts/model/strings.ts +++ b/src/Scripts/model/strings.ts @@ -177,8 +177,6 @@ export enum strings { exportDataCSVDelimiterTab, exportDataCSVEncoding, exportDataCSVEncodingDesc, - exportDataCSVFolder, - exportDataCSVFolderDesc, exportDataCSVQuote, exportDataCSVQuoteDesc, exportDataExcelCreateExportSummary, diff --git a/src/Scripts/view/scene-export-data.ts b/src/Scripts/view/scene-export-data.ts index 8a648d45..e3b6caa2 100644 --- a/src/Scripts/view/scene-export-data.ts +++ b/src/Scripts/view/scene-export-data.ts @@ -30,7 +30,6 @@ interface ExportSettings { delimiter: string customDelimiter: string quoteStringFields: boolean - createSubfolder: boolean } export class ExportDataScene extends DocScene { @@ -55,8 +54,7 @@ export class ExportDataScene extends DocScene { encoding: "utf8", delimiter: "", customDelimiter: "", - quoteStringFields: false, - createSubfolder: false + quoteStringFields: false }); } @@ -194,17 +192,6 @@ export class ExportDataScene extends DocScene { option: "format", value: ExportDataFormat.Csv } - }, - { - option: "createSubfolder", - parent: "format", - name: i18n(strings.exportDataCSVFolder), - description: i18n(strings.exportDataCSVFolderDesc), - type: OptionType.switch, - toggledBy: { - option: "format", - value: ExportDataFormat.Csv - } } ]; @@ -473,8 +460,7 @@ export class ExportDataScene extends DocScene { tables: tables, unicodeEncoding: (this.config.options.encoding == "utf16"), delimiter: delimiter, - quoteStringFields: this.config.options.quoteStringFields, - createSubfolder: this.config.options.createSubfolder + quoteStringFields: this.config.options.quoteStringFields }; if (this.doc.type == DocType.dataset) { diff --git a/test/Bravo.Tests/Infrastructure/Windows/TaskDialogBuilderTests.cs b/test/Bravo.Tests/Infrastructure/Windows/TaskDialogBuilderTests.cs new file mode 100644 index 00000000..976aed16 --- /dev/null +++ b/test/Bravo.Tests/Infrastructure/Windows/TaskDialogBuilderTests.cs @@ -0,0 +1,42 @@ +using System.Windows.Forms; +using Sqlbi.Bravo.Infrastructure.Windows.Dialogs; +using Xunit; + +namespace Bravo.Tests.Infrastructure.Windows; + +public class TaskDialogBuilderTests +{ + [Fact] + public void Build_CopiesEverySettingToThePage() + { + var first = new TaskDialogCommandLinkButton("first", "first description"); + var second = new TaskDialogButton("second"); + + var page = TaskDialogBuilder.Create() + .WithCaption("caption") + .WithHeading("heading") + .WithText("text") + .WithIcon(TaskDialogIcon.Warning) + .WithFootnote("footnote", TaskDialogIcon.Information) + .WithExpander("details", expanded: true, TaskDialogExpanderPosition.AfterFootnote, expandedButtonText: "Hide", collapsedButtonText: "Show") + .AddButtons(first, second) + .WithDefaultButton(second) + .WithSizeToContent() + .Build(); + + Assert.Equal("caption", page.Caption); + Assert.Equal("heading", page.Heading); + Assert.Equal("text", page.Text); + Assert.Same(TaskDialogIcon.Warning, page.Icon); + Assert.Equal("footnote", page.Footnote?.Text); + Assert.Same(TaskDialogIcon.Information, page.Footnote?.Icon); + Assert.Equal("details", page.Expander?.Text); + Assert.True(page.Expander?.Expanded); + Assert.Equal(TaskDialogExpanderPosition.AfterFootnote, page.Expander?.Position); + Assert.Equal("Hide", page.Expander?.ExpandedButtonText); + Assert.Equal("Show", page.Expander?.CollapsedButtonText); + Assert.Equal(new TaskDialogButton[] { first, second }, page.Buttons); + Assert.Same(second, page.DefaultButton); + Assert.True(page.SizeToContent); + } +}