diff --git a/api/src/Sozsoft.Platform.Domain/ListForms/ListFormCustomizationManager.cs b/api/src/Sozsoft.Platform.Domain/ListForms/ListFormCustomizationManager.cs index 2a44a57..09c9993 100644 --- a/api/src/Sozsoft.Platform.Domain/ListForms/ListFormCustomizationManager.cs +++ b/api/src/Sozsoft.Platform.Domain/ListForms/ListFormCustomizationManager.cs @@ -16,6 +16,7 @@ namespace Sozsoft.Platform.ListForms; public interface IListFormCustomizationManager { Task> GetUsersServerCustomizations(string listFormCode); + Task GetUserGridState(string listFormCode, string filterName); Task Delete(Guid id); } @@ -61,6 +62,21 @@ public class ListFormCustomizationManager : PlatformDomainService, IListFormCust return customizations; } + public async Task GetUserGridState(string listFormCode, string filterName) + { + var items = await listFormCustomizationRepository.GetListAsync(a => + a.ListFormCode == listFormCode && + a.CustomizationType == ListFormCustomizationTypeEnum.GridState && + a.FilterName == filterName && + ((a.UserId == userId && a.RoleId == null) || + (a.UserId == null && roleIds.Contains(a.RoleId)) || + (a.UserId == null && a.RoleId == null))); + + return items + .OrderByDescending(a => a.UserId != null ? 3 : a.RoleId != null ? 2 : 1) + .FirstOrDefault(); + } + public async Task Delete(Guid id) { await listFormCustomizationRepository.DeleteAsync(id); diff --git a/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicFormReport.cs b/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicFormReport.cs index be691ee..7954a87 100644 --- a/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicFormReport.cs +++ b/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicFormReport.cs @@ -32,11 +32,11 @@ public class DynamicFormReport : XtraReport private const float RowHeight = 24; private const float ColumnGap = 10; private static readonly System.Drawing.Color FieldBorderColor = System.Drawing.Color.FromArgb(226, 232, 240); - private static IServiceProvider _serviceProvider; + private static IServiceScopeFactory _scopeFactory; - public static void SetServiceProvider(IServiceProvider serviceProvider) + public static void SetScopeFactory(IServiceScopeFactory scopeFactory) { - _serviceProvider = serviceProvider; + _scopeFactory = scopeFactory; } public static async Task CreateAsync(IDictionary parameters) @@ -52,7 +52,7 @@ public class DynamicFormReport : XtraReport private async Task BuildAsync(IDictionary parameters) { - if (_serviceProvider == null) + if (_scopeFactory == null) { throw new InvalidOperationException("DynamicFormReport service provider is not configured."); } @@ -67,7 +67,7 @@ public class DynamicFormReport : XtraReport throw new ArgumentException("id parameter is required."); } - using var scope = _serviceProvider.CreateScope(); + using var scope = _scopeFactory.CreateScope(); var authManager = scope.ServiceProvider.GetRequiredService(); var listFormManager = scope.ServiceProvider.GetRequiredService(); var listFormFieldManager = scope.ServiceProvider.GetRequiredService(); diff --git a/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicGridReport.cs b/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicGridReport.cs index 2895e64..26174fc 100644 --- a/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicGridReport.cs +++ b/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicGridReport.cs @@ -30,11 +30,11 @@ public class DynamicGridReport : XtraReport private const int HorizontalMargin = 50; private const int LandscapeColumnThreshold = 5; private static readonly System.Drawing.Color TableBorderColor = System.Drawing.Color.FromArgb(226, 232, 240); - private static IServiceProvider _serviceProvider; + private static IServiceScopeFactory _scopeFactory; - public static void SetServiceProvider(IServiceProvider serviceProvider) + public static void SetScopeFactory(IServiceScopeFactory scopeFactory) { - _serviceProvider = serviceProvider; + _scopeFactory = scopeFactory; } public static async Task CreateAsync(IDictionary parameters) @@ -50,7 +50,7 @@ public class DynamicGridReport : XtraReport private async Task BuildAsync(IDictionary parameters) { - if (_serviceProvider == null) + if (_scopeFactory == null) { throw new InvalidOperationException("DynamicListFormReport service provider is not configured."); } @@ -60,7 +60,7 @@ public class DynamicGridReport : XtraReport throw new ArgumentException("listFormCode parameter is required."); } - using var scope = _serviceProvider.CreateScope(); + using var scope = _scopeFactory.CreateScope(); var authManager = scope.ServiceProvider.GetRequiredService(); var listFormManager = scope.ServiceProvider.GetRequiredService(); var listFormFieldManager = scope.ServiceProvider.GetRequiredService(); @@ -79,6 +79,9 @@ public class DynamicGridReport : XtraReport } var listForm = await listFormManager.GetUserListForm(listFormCode); + var gridState = await customizationManager.GetUserGridState( + listFormCode, + DynamicReportGridStateHelper.GetFilterName(listForm, "list")); var fields = (await listFormFieldManager.GetUserListFormFields(listFormCode)) .Where(x => x.CanRead) .OrderBy(x => x.ListOrderNo ?? int.MaxValue) @@ -103,18 +106,19 @@ public class DynamicGridReport : XtraReport var selectFieldsByName = DynamicReportImageHelper.CreateSelectFieldMap(selectQueryManager.SelectFields); var editorTypesByField = DynamicReportImageHelper.CreateEditorTypeMap(listForm); - var visibleFields = fields + var availableFields = fields .Where(x => !string.IsNullOrWhiteSpace(x.FieldName) && x.FieldName != "undefined") - .Where(x => x.IsActive != false && x.Visible == true) + .Where(x => x.IsActive != false) .Where(x => selectFieldsByName.ContainsKey(x.FieldName)) .ToList(); + var visibleFields = DynamicReportGridStateHelper.GetVisibleFields( + availableFields, + gridState?.CustomizationData); if (visibleFields.Count == 0) { - visibleFields = fields - .Where(x => !string.IsNullOrWhiteSpace(x.FieldName) && x.FieldName != "undefined") - .Where(x => x.IsActive != false) - .Where(x => selectFieldsByName.ContainsKey(x.FieldName)) + visibleFields = availableFields + .Where(x => x.Visible == true) .Take(8) .ToList(); } diff --git a/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicReport.cs b/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicReport.cs index 3dfd2f2..1b5ccc8 100644 --- a/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicReport.cs +++ b/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicReport.cs @@ -18,14 +18,14 @@ public partial class DynamicReport : XtraReport private const int VerticalMargin = 35; private Dictionary _parameters; - private static IServiceProvider _serviceProvider; + private static IServiceScopeFactory _scopeFactory; /// /// Uygulama başlangıcında bir kez set edilmeli (Program.cs'de) /// - public static void SetServiceProvider(IServiceProvider serviceProvider) + public static void SetScopeFactory(IServiceScopeFactory scopeFactory) { - _serviceProvider = serviceProvider; + _scopeFactory = scopeFactory; } public DynamicReport() @@ -88,12 +88,12 @@ public partial class DynamicReport : XtraReport { try { - if (_serviceProvider == null) + if (_scopeFactory == null) { return; } - using var scope = _serviceProvider.CreateScope(); + using var scope = _scopeFactory.CreateScope(); var currentUser = scope.ServiceProvider.GetRequiredService(); currentUserNameLabel.Text = currentUser.UserName ?? string.Empty; } @@ -106,10 +106,10 @@ public partial class DynamicReport : XtraReport { try { - if (_serviceProvider == null) + if (_scopeFactory == null) return; - using var scope = _serviceProvider.CreateScope(); + using var scope = _scopeFactory.CreateScope(); var dbContext = scope.ServiceProvider.GetRequiredService(); var language = dbContext.Set() diff --git a/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicReportGridStateHelper.cs b/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicReportGridStateHelper.cs new file mode 100644 index 0000000..90a41c8 --- /dev/null +++ b/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicReportGridStateHelper.cs @@ -0,0 +1,153 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +using Sozsoft.Platform.Entities; + +namespace Sozsoft.Reports.PredefinedReports; + +internal static class DynamicReportGridStateHelper +{ + public static string GetFilterName(ListForm listForm, string prefix) => + $"{prefix}-{GetStateStorageKey(listForm)}"; + + public static List GetVisibleFields( + IReadOnlyList availableFields, + string gridStateJson) + { + if (!TryGetColumnStates(gridStateJson, out var statesByField)) + { + return availableFields + .Where(x => x.Visible == true) + .ToList(); + } + + return availableFields + .Where(field => statesByField.TryGetValue(field.FieldName, out var state) + ? state.Visible + : field.Visible == true) + .OrderBy(field => statesByField.TryGetValue(field.FieldName, out var state) + ? state.VisibleIndex + : int.MaxValue) + .ToList(); + } + + private static string GetStateStorageKey(ListForm listForm) + { + if (!string.IsNullOrWhiteSpace(listForm.StateStoringJson)) + { + try + { + using var document = JsonDocument.Parse(listForm.StateStoringJson); + if (TryGetProperty(document.RootElement, "storageKey", out var storageKey) && + storageKey.ValueKind == JsonValueKind.String && + !string.IsNullOrWhiteSpace(storageKey.GetString())) + { + return storageKey.GetString(); + } + } + catch (JsonException) + { + // Fall back to the same default value used by GridOptionsDto. + } + } + + return $"listFormGridStorage-{listForm.ListFormCode}"; + } + + private static bool TryGetColumnStates( + string gridStateJson, + out Dictionary statesByField) + { + statesByField = null; + if (string.IsNullOrWhiteSpace(gridStateJson)) + { + return false; + } + + try + { + using var document = JsonDocument.Parse(gridStateJson); + if (!TryGetProperty(document.RootElement, "columns", out var columns) || + columns.ValueKind != JsonValueKind.Array) + { + return false; + } + + var states = new List(); + AddColumnStates(columns, true, states); + statesByField = states + .Where(x => !string.IsNullOrWhiteSpace(x.FieldName)) + .GroupBy(x => x.FieldName, StringComparer.OrdinalIgnoreCase) + .ToDictionary(x => x.Key, x => x.First(), StringComparer.OrdinalIgnoreCase); + return statesByField.Count > 0; + } + catch (JsonException) + { + return false; + } + } + + private static void AddColumnStates(JsonElement columns, bool parentVisible, ICollection states) + { + foreach (var column in columns.EnumerateArray()) + { + var visible = parentVisible && + (!TryGetProperty(column, "visible", out var visibleProperty) || + visibleProperty.ValueKind != JsonValueKind.False); + var fieldName = GetColumnFieldName(column); + var visibleIndex = TryGetProperty(column, "visibleIndex", out var visibleIndexProperty) && + visibleIndexProperty.TryGetInt32(out var value) + ? value + : states.Count; + + if (!string.IsNullOrWhiteSpace(fieldName)) + { + states.Add(new GridColumnState(fieldName, visible, visibleIndex)); + } + + if (TryGetProperty(column, "columns", out var childColumns) && + childColumns.ValueKind == JsonValueKind.Array) + { + AddColumnStates(childColumns, visible, states); + } + } + } + + private static string GetColumnFieldName(JsonElement column) + { + if (TryGetProperty(column, "dataField", out var dataField) && + dataField.ValueKind == JsonValueKind.String) + { + return dataField.GetString(); + } + + if (TryGetProperty(column, "name", out var name) && + name.ValueKind == JsonValueKind.String) + { + return name.GetString(); + } + + return null; + } + + private static bool TryGetProperty(JsonElement element, string name, out JsonElement value) + { + if (element.ValueKind == JsonValueKind.Object) + { + foreach (var property in element.EnumerateObject()) + { + if (string.Equals(property.Name, name, StringComparison.OrdinalIgnoreCase)) + { + value = property.Value; + return true; + } + } + } + + value = default; + return false; + } + + private sealed record GridColumnState(string FieldName, bool Visible, int VisibleIndex); +} diff --git a/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicTreeReport.cs b/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicTreeReport.cs index 5c1738a..d76fd68 100644 --- a/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicTreeReport.cs +++ b/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicTreeReport.cs @@ -34,11 +34,11 @@ public class DynamicTreeReport : XtraReport private const string TreePrefixColumn = "__TreePrefix"; private const string TreeDataSourceName = "DynamicTree"; private static readonly System.Drawing.Color TableBorderColor = System.Drawing.Color.FromArgb(226, 232, 240); - private static IServiceProvider _serviceProvider; + private static IServiceScopeFactory _scopeFactory; - public static void SetServiceProvider(IServiceProvider serviceProvider) + public static void SetScopeFactory(IServiceScopeFactory scopeFactory) { - _serviceProvider = serviceProvider; + _scopeFactory = scopeFactory; } public static async Task CreateAsync(IDictionary parameters) @@ -54,7 +54,7 @@ public class DynamicTreeReport : XtraReport private async Task BuildAsync(IDictionary parameters) { - if (_serviceProvider == null) + if (_scopeFactory == null) { throw new InvalidOperationException("DynamicTreeReport service provider is not configured."); } @@ -64,7 +64,7 @@ public class DynamicTreeReport : XtraReport throw new ArgumentException("listFormCode parameter is required."); } - using var scope = _serviceProvider.CreateScope(); + using var scope = _scopeFactory.CreateScope(); var authManager = scope.ServiceProvider.GetRequiredService(); var listFormManager = scope.ServiceProvider.GetRequiredService(); var listFormFieldManager = scope.ServiceProvider.GetRequiredService(); @@ -83,6 +83,9 @@ public class DynamicTreeReport : XtraReport } var listForm = await listFormManager.GetUserListForm(listFormCode); + var gridState = await customizationManager.GetUserGridState( + listFormCode, + DynamicReportGridStateHelper.GetFilterName(listForm, "tree")); var treeOptions = GetTreeOptions(listForm); if (string.IsNullOrWhiteSpace(treeOptions?.KeyExpr) || string.IsNullOrWhiteSpace(treeOptions.ParentIdExpr)) { @@ -113,18 +116,19 @@ public class DynamicTreeReport : XtraReport var selectFieldsByName = DynamicReportImageHelper.CreateSelectFieldMap(selectQueryManager.SelectFields); var editorTypesByField = DynamicReportImageHelper.CreateEditorTypeMap(listForm); - var visibleFields = fields + var availableFields = fields .Where(x => !string.IsNullOrWhiteSpace(x.FieldName) && x.FieldName != "undefined") - .Where(x => x.IsActive != false && x.Visible == true) + .Where(x => x.IsActive != false) .Where(x => selectFieldsByName.ContainsKey(x.FieldName)) .ToList(); + var visibleFields = DynamicReportGridStateHelper.GetVisibleFields( + availableFields, + gridState?.CustomizationData); if (visibleFields.Count == 0) { - visibleFields = fields - .Where(x => !string.IsNullOrWhiteSpace(x.FieldName) && x.FieldName != "undefined") - .Where(x => x.IsActive != false) - .Where(x => selectFieldsByName.ContainsKey(x.FieldName)) + visibleFields = availableFields + .Where(x => x.Visible == true) .Take(8) .ToList(); } diff --git a/api/src/Sozsoft.Platform.HttpApi.Host/ReportServices/CustomReportStorageWebExtension.cs b/api/src/Sozsoft.Platform.HttpApi.Host/ReportServices/CustomReportStorageWebExtension.cs index 6114099..5873b21 100644 --- a/api/src/Sozsoft.Platform.HttpApi.Host/ReportServices/CustomReportStorageWebExtension.cs +++ b/api/src/Sozsoft.Platform.HttpApi.Host/ReportServices/CustomReportStorageWebExtension.cs @@ -21,17 +21,13 @@ public class CustomReportStorageWebExtension : DevExpress.XtraReports.Web.Extens { readonly string reportDirectory = "PredefinedReports\\Reports"; const string FileExtension = ".repx"; - private readonly IServiceProvider _serviceProvider; + private readonly IServiceScopeFactory _scopeFactory; - public CustomReportStorageWebExtension(IServiceProvider serviceProvider) + public CustomReportStorageWebExtension(IServiceScopeFactory scopeFactory) { - _serviceProvider = serviceProvider; + _scopeFactory = scopeFactory; - // DynamicReport için ServiceProvider'ı set et - Sozsoft.Reports.PredefinedReports.DynamicReport.SetServiceProvider(serviceProvider); - Sozsoft.Reports.PredefinedReports.DynamicGridReport.SetServiceProvider(serviceProvider); - Sozsoft.Reports.PredefinedReports.DynamicTreeReport.SetServiceProvider(serviceProvider); - Sozsoft.Reports.PredefinedReports.DynamicFormReport.SetServiceProvider(serviceProvider); + ConfigureDynamicReportScopes(scopeFactory); if (!Directory.Exists(reportDirectory)) { @@ -39,15 +35,11 @@ public class CustomReportStorageWebExtension : DevExpress.XtraReports.Web.Extens } } - public CustomReportStorageWebExtension(IServiceProvider serviceProvider, string reportDirectory) + public CustomReportStorageWebExtension(IServiceScopeFactory scopeFactory, string reportDirectory) { - _serviceProvider = serviceProvider; + _scopeFactory = scopeFactory; - // DynamicReport için ServiceProvider'ı set et - Sozsoft.Reports.PredefinedReports.DynamicReport.SetServiceProvider(serviceProvider); - Sozsoft.Reports.PredefinedReports.DynamicGridReport.SetServiceProvider(serviceProvider); - Sozsoft.Reports.PredefinedReports.DynamicTreeReport.SetServiceProvider(serviceProvider); - Sozsoft.Reports.PredefinedReports.DynamicFormReport.SetServiceProvider(serviceProvider); + ConfigureDynamicReportScopes(scopeFactory); if (!Directory.Exists(reportDirectory)) { @@ -56,6 +48,14 @@ public class CustomReportStorageWebExtension : DevExpress.XtraReports.Web.Extens this.reportDirectory = reportDirectory; } + private static void ConfigureDynamicReportScopes(IServiceScopeFactory scopeFactory) + { + Sozsoft.Reports.PredefinedReports.DynamicReport.SetScopeFactory(scopeFactory); + Sozsoft.Reports.PredefinedReports.DynamicGridReport.SetScopeFactory(scopeFactory); + Sozsoft.Reports.PredefinedReports.DynamicTreeReport.SetScopeFactory(scopeFactory); + Sozsoft.Reports.PredefinedReports.DynamicFormReport.SetScopeFactory(scopeFactory); + } + private static bool IsSafeReportFileName(string reportName) { return !string.IsNullOrWhiteSpace(reportName) && @@ -242,7 +242,7 @@ public class CustomReportStorageWebExtension : DevExpress.XtraReports.Web.Extens throw new InvalidOperationException("A valid report template id is required."); } - using var scope = _serviceProvider.CreateScope(); + using var scope = _scopeFactory.CreateScope(); var repository = scope.ServiceProvider.GetRequiredService>(); var template = await repository.FindAsync(reportTemplateId); if (template == null) @@ -309,7 +309,7 @@ public class CustomReportStorageWebExtension : DevExpress.XtraReports.Web.Extens bottomMargin.Controls.Add(userNameLabel); } - using var scope = _serviceProvider.CreateScope(); + using var scope = _scopeFactory.CreateScope(); var currentUser = scope.ServiceProvider.GetService(); userNameLabel.Text = currentUser?.UserName ?? string.Empty; userNameLabel.BoundsF = new System.Drawing.RectangleF(0, 8, pageWidth / 2, 18); @@ -379,14 +379,14 @@ public class CustomReportStorageWebExtension : DevExpress.XtraReports.Web.Extens private static (string ReportName, Dictionary Parameters) ParseReportUrl(string url) { - var decodedUrl = DecodeRepeatedly(url); var parameters = new Dictionary(StringComparer.OrdinalIgnoreCase); - var queryIndex = decodedUrl.IndexOf('?'); - var reportPath = queryIndex < 0 ? decodedUrl : decodedUrl[..queryIndex]; + // Split on URL delimiters before decoding so an encoded slash remains part of its parameter. + var queryIndex = url.IndexOf('?'); + var reportPath = queryIndex < 0 ? url : url[..queryIndex]; if (queryIndex >= 0) { - var queryString = decodedUrl[(queryIndex + 1)..]; + var queryString = url[(queryIndex + 1)..]; var paramPairs = queryString.Split('&', StringSplitOptions.RemoveEmptyEntries); foreach (var pair in paramPairs) @@ -401,7 +401,7 @@ public class CustomReportStorageWebExtension : DevExpress.XtraReports.Web.Extens var pathParts = reportPath .Split('/', StringSplitOptions.RemoveEmptyEntries) - .Select(Uri.UnescapeDataString) + .Select(DecodeRepeatedly) .ToArray(); if (pathParts.Length == 0) diff --git a/ui/src/views/form/FormButtons.tsx b/ui/src/views/form/FormButtons.tsx index ca59476..3a79913 100644 --- a/ui/src/views/form/FormButtons.tsx +++ b/ui/src/views/form/FormButtons.tsx @@ -222,7 +222,12 @@ const FormButtons = (props: { if (onActionNew) { onActionNew() } else { - navigate(ROUTES_ENUM.protected.admin.formNew.replace(':listFormCode', listFormCode)) + navigate( + ROUTES_ENUM.protected.admin.formNew.replace( + ':listFormCode', + encodeURIComponent(listFormCode), + ), + ) } }} {...(permissions.c ? {} : { disabled: true })} @@ -241,8 +246,8 @@ const FormButtons = (props: { } else { navigate( ROUTES_ENUM.protected.admin.formEdit - .replace(':listFormCode', listFormCode) - .replace(':id', id!), + .replace(':listFormCode', encodeURIComponent(listFormCode)) + .replace(':id', encodeURIComponent(id!)), ) } }} @@ -262,8 +267,8 @@ const FormButtons = (props: { } else { navigate( ROUTES_ENUM.protected.admin.formView - .replace(':listFormCode', listFormCode) - .replace(':id', id!), + .replace(':listFormCode', encodeURIComponent(listFormCode)) + .replace(':id', encodeURIComponent(id!)), ) } }} diff --git a/ui/src/views/form/useFormData.tsx b/ui/src/views/form/useFormData.tsx index ae813ec..c602389 100644 --- a/ui/src/views/form/useFormData.tsx +++ b/ui/src/views/form/useFormData.tsx @@ -140,8 +140,8 @@ const useGridData = (props: { if (!isSubForm) { navigate( ROUTES_ENUM.protected.admin.formView - .replace(':listFormCode', listFormCode) - .replace(':id', id!), + .replace(':listFormCode', encodeURIComponent(listFormCode)) + .replace(':id', encodeURIComponent(id!)), ) } else if (props.onSubmitAction) { props.onSubmitAction() diff --git a/ui/src/views/list/useListFormColumns.ts b/ui/src/views/list/useListFormColumns.ts index 8e25a33..1049197 100644 --- a/ui/src/views/list/useListFormColumns.ts +++ b/ui/src/views/list/useListFormColumns.ts @@ -653,11 +653,12 @@ const useListFormColumns = ({ } if (!gridDto.gridOptions.keyFieldName) return + if (!gridDto.gridOptions.listFormCode) return const detailFormName = gridDto.gridOptions.listFormCode const id = e.row.data[gridDto.gridOptions.keyFieldName] - const url = `/admin/form/${detailFormName}/${id}` + const url = `/admin/form/${encodeURIComponent(detailFormName)}/${encodeURIComponent(String(id))}` window.open(url, isPwaMode ? '_self' : '_blank') },