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); + } +}