From 7659dbdaa103764d48748db6d92eeab455eb7898 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sedat=20=C3=96ZT=C3=9CRK?= <76204082+iamsedatozturk@users.noreply.github.com> Date: Wed, 19 Aug 2026 15:13:32 +0300 Subject: [PATCH] =?UTF-8?q?SqlDataSource=20->=20Form=20olarak=20de=C4=9Fi?= =?UTF-8?q?=C5=9Ftirildi.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DeveloperKit/CrudEndpointAppService.cs | 28 +- .../Migrations/LanguagesData.json | 156 ++++- .../DeveloperKit/IDynamicEntityManager.cs | 9 +- .../PlatformDynamicEntityManager.cs | 180 +++++- configs/seeds/host/crud/AbpUserRoles.json | 36 ++ configs/seeds/host/custom/RoleComponent.json | 15 +- .../componentEditor/PlatformViewHost.tsx | 19 +- .../visualDesigner/VisualCanvas.tsx | 64 +- ui/src/components/visualDesigner/catalog.ts | 27 +- .../visualDesigner/codeGenerator.ts | 330 +++++++++-- ui/src/components/visualDesigner/types.ts | 246 +++++++- .../developerKit/VisualComponentDesigner.tsx | 548 +++++++++++++++++- ui/src/views/list/Chart.tsx | 12 +- ui/src/views/list/List.tsx | 10 +- 14 files changed, 1566 insertions(+), 114 deletions(-) create mode 100644 configs/seeds/host/crud/AbpUserRoles.json diff --git a/api/src/Sozsoft.Platform.Application/DeveloperKit/CrudEndpointAppService.cs b/api/src/Sozsoft.Platform.Application/DeveloperKit/CrudEndpointAppService.cs index 65d9ec48..f405b114 100644 --- a/api/src/Sozsoft.Platform.Application/DeveloperKit/CrudEndpointAppService.cs +++ b/api/src/Sozsoft.Platform.Application/DeveloperKit/CrudEndpointAppService.cs @@ -1,5 +1,6 @@ using Sozsoft.Platform.Domain.DeveloperKit; using Sozsoft.Platform.Entities; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using System; using System.Collections.Generic; @@ -26,11 +27,36 @@ public class CrudEndpointAppService : ApplicationService _dynamicManager = dynamicManager; } + /// + /// Every query string entry that is not a paging parameter is a column filter: + /// ?RoleId=… compares for equality, ?Name.contains=… applies the + /// named operator. The column names are validated against the table schema in + /// the manager, so an unknown column fails loudly instead of returning the + /// unfiltered collection. + /// [HttpGet("{entityName}")] public async Task?> GetEntityListAsync(string entityName) { await EnsureEndpointAsync(entityName, "GET", "GetList"); - return await _dynamicManager.GetEntityListAsync(entityName); + return await _dynamicManager.GetEntityListAsync(entityName, ReadQueryFilters()); + } + + /// + /// Route values are not filters, so only the query string is read. A repeated + /// parameter keeps its comma joined form, which is what the in operator + /// expects. + /// + private Dictionary ReadQueryFilters() + { + var filters = new Dictionary(StringComparer.OrdinalIgnoreCase); + var query = LazyServiceProvider.LazyGetService()?.HttpContext?.Request?.Query; + if (query == null) + return filters; + + foreach (var entry in query) + filters[entry.Key] = string.Join(",", entry.Value.ToArray()); + + return filters; } [HttpGet("{entityName}/{id}")] diff --git a/api/src/Sozsoft.Platform.DbMigrator/Migrations/LanguagesData.json b/api/src/Sozsoft.Platform.DbMigrator/Migrations/LanguagesData.json index 9ac32403..bf6fab4d 100644 --- a/api/src/Sozsoft.Platform.DbMigrator/Migrations/LanguagesData.json +++ b/api/src/Sozsoft.Platform.DbMigrator/Migrations/LanguagesData.json @@ -19530,6 +19530,108 @@ "en": "Download as JPG", "tr": "JPG olarak indir" }, + { + "resourceName": "Platform", + "key": "App.DeveloperKitComponentDesigner.FilterMaster", + "en": "Master component", + "tr": "Ana komponent" + }, + { + "resourceName": "Platform", + "key": "App.DeveloperKitComponentDesigner.FilterMasterIncomplete", + "en": "Pick both the master component and its column; the filter does not run until then.", + "tr": "Ana komponenti ve sütununu birlikte seçin; ikisi tamamlanmadan filtre çalışmaz." + }, + { + "resourceName": "Platform", + "key": "App.DeveloperKitComponentDesigner.Filters", + "en": "Filters", + "tr": "Filtreler" + }, + { + "resourceName": "Platform", + "key": "App.DeveloperKitComponentDesigner.AddFilter", + "en": "Add filter", + "tr": "Filtre ekle" + }, + { + "resourceName": "Platform", + "key": "App.DeveloperKitComponentDesigner.FiltersHint", + "en": "Filters are sent as query parameters to the endpoint and apply to every component bound to it. The value can be static, come from the page URL, or come from the record of another Form.", + "tr": "Filtreler endpointe query parametresi olarak gönderilir ve bu endpointe bağlı tüm komponentler için geçerlidir. Değer sabit olabilir, sayfa adresinden gelebilir veya başka bir Form kaydından okunabilir." + }, + { + "resourceName": "Platform", + "key": "App.DeveloperKitComponentDesigner.NoFilter", + "en": "No filter defined; the endpoint returns the whole collection.", + "tr": "Tanımlı filtre yok; endpoint tüm koleksiyonu döndürür." + }, + { + "resourceName": "Platform", + "key": "App.DeveloperKitComponentDesigner.FilterColumn", + "en": "Column", + "tr": "Sütun" + }, + { + "resourceName": "Platform", + "key": "App.DeveloperKitComponentDesigner.FilterValue", + "en": "Value", + "tr": "Değer" + }, + { + "resourceName": "Platform", + "key": "App.DeveloperKitComponentDesigner.FilterUrlParam", + "en": "URL parameter name", + "tr": "Adres parametresi adı" + }, + { + "resourceName": "Platform", + "key": "App.DeveloperKitComponentDesigner.FilterMasterColumn", + "en": "Master column", + "tr": "Ana kayıt sütunu" + }, + { + "resourceName": "Platform", + "key": "App.DeveloperKitComponentDesigner.FilterPreviewValue", + "en": "Designer preview value", + "tr": "Tasarımcı önizleme değeri" + }, + { + "resourceName": "Platform", + "key": "App.DeveloperKitComponentDesigner.FilterRequired", + "en": "Required: cancel the request while the value is empty", + "tr": "Zorunlu: değer boşken istek gönderilmesin" + }, + { + "resourceName": "Platform", + "key": "App.DeveloperKitComponentDesigner.FilterSourceStatic", + "en": "Static", + "tr": "Sabit" + }, + { + "resourceName": "Platform", + "key": "App.DeveloperKitComponentDesigner.FilterSourceQuery", + "en": "URL query", + "tr": "Adres sorgusu" + }, + { + "resourceName": "Platform", + "key": "App.DeveloperKitComponentDesigner.FilterSourceRoute", + "en": "URL path", + "tr": "Adres yolu" + }, + { + "resourceName": "Platform", + "key": "App.DeveloperKitComponentDesigner.FilterSourceRecord", + "en": "Form record", + "tr": "Form kaydı" + }, + { + "resourceName": "Platform", + "key": "App.DeveloperKitComponentDesigner.RequiredFilterPreviewMissing", + "en": "A required filter has no preview value, so the request was not sent.", + "tr": "Zorunlu bir filtrenin önizleme değeri yok, bu yüzden istek gönderilmedi." + }, { "resourceName": "Platform", "key": "App.DeveloperKitComponentDesigner.AcceptsChildren", @@ -19629,8 +19731,8 @@ { "resourceName": "Platform", "key": "App.DeveloperKitComponentDesigner.BoundToSqlDataSource", - "en": "This component is bound to a SqlDataSource record; fields come from the columns in the Select (GET) response.", - "tr": "Bu komponent SqlDataSource kaydına bağlıdır; alanlar Select (GET) cevabındaki sütunlardan gelir." + "en": "This component is bound to a Form record; fields come from the columns in the Select (GET) response.", + "tr": "Bu komponent Form kaydına bağlıdır; alanlar Select (GET) cevabındaki sütunlardan gelir." }, { "resourceName": "Platform", @@ -19653,8 +19755,8 @@ { "resourceName": "Platform", "key": "App.DeveloperKitComponentDesigner.ClearNonSqlBinding", - "en": "Non-SqlDataSource binding — clear", - "tr": "SqlDataSource dışı bağlantı — temizle" + "en": "Non-Form binding — clear", + "tr": "Form dışı bağlantı — temizle" }, { "resourceName": "Platform", @@ -19665,8 +19767,8 @@ { "resourceName": "Platform", "key": "App.DeveloperKitComponentDesigner.CommandSlotsHint", - "en": "Define endpoints in the Data workspace and bind them to command slots here. Select the components you drop inside and bind them to a column by choosing the SqlDataSource record source from the Data tab.", - "tr": "Endpointleri Data çalışma alanında tanımlayın, burada komut yuvalarına bağlayın. İçine bıraktığınız komponentleri seçip Data sekmesinden SqlDataSource kaydı kaynağını seçerek sütuna bağlayabilirsiniz." + "en": "Define endpoints in the Data workspace and bind them to command slots here. Select the components you drop inside and bind them to a column by choosing the Form record source from the Data tab.", + "tr": "Endpointleri Data çalışma alanında tanımlayın, burada komut yuvalarına bağlayın. İçine bıraktığınız komponentleri seçip Data sekmesinden Form kaydı kaynağını seçerek sütuna bağlayabilirsiniz." }, { "resourceName": "Platform", @@ -19719,14 +19821,14 @@ { "resourceName": "Platform", "key": "App.DeveloperKitComponentDesigner.DefineAndRunSelect", - "en": "Define and run the SqlDataSource Select endpoint.", - "tr": "SqlDataSource Select endpointini tanımlayıp çalıştırın." + "en": "Define and run the Form Select endpoint.", + "tr": "Form Select endpointini tanımlayıp çalıştırın." }, { "resourceName": "Platform", "key": "App.DeveloperKitComponentDesigner.DefineSelectEndpointFirst", - "en": "This component is inside a SqlDataSource. To bind it to columns, first select the SqlDataSource and define the Select (GET) endpoint from the Data tab.", - "tr": "Bu komponent bir SqlDataSource içinde. Sütunlara bağlanabilmesi için önce SqlDataSource’u seçip Data sekmesinden Select (GET) endpointini tanımlayın." + "en": "This component is inside a Form. To bind it to columns, first select the Form and define the Select (GET) endpoint from the Data tab.", + "tr": "Bu komponent bir Form içinde. Sütunlara bağlanabilmesi için önce Form’u seçip Data sekmesinden Select (GET) endpointini tanımlayın." }, { "resourceName": "Platform", @@ -19791,8 +19893,8 @@ { "resourceName": "Platform", "key": "App.DeveloperKitComponentDesigner.EndpointNeedsKey", - "en": "This endpoint expects a key parameter. Enter a preview key value on the SqlDataSource.", - "tr": "Bu endpoint bir key parametresi bekliyor. SqlDataSource üzerinde önizleme key değeri girin." + "en": "This endpoint expects a key parameter. Enter a preview key value on the Form.", + "tr": "Bu endpoint bir key parametresi bekliyor. Form üzerinde önizleme key değeri girin." }, { "resourceName": "Platform", @@ -19893,8 +19995,8 @@ { "resourceName": "Platform", "key": "App.DeveloperKitComponentDesigner.GetPostHint", - "en": "GET sources are read when the page opens; POST/PUT/DELETE sources only run when bound to a SqlDataSource command slot and are not triggered from the design screen.", - "tr": "GET kaynakları sayfa açılışında okunur; POST/PUT/DELETE kaynakları yalnızca bir SqlDataSource komut yuvasına bağlandığında çalışır ve tasarım ekranından tetiklenmez." + "en": "GET sources are read when the page opens; POST/PUT/DELETE sources only run when bound to a Form command slot and are not triggered from the design screen.", + "tr": "GET kaynakları sayfa açılışında okunur; POST/PUT/DELETE kaynakları yalnızca bir Form komut yuvasına bağlandığında çalışır ve tasarım ekranından tetiklenmez." }, { "resourceName": "Platform", @@ -19995,8 +20097,8 @@ { "resourceName": "Platform", "key": "App.DeveloperKitComponentDesigner.MethodNotRunnableBindToSql", - "en": "{method} endpoints are not run from the design screen; bind them to a SqlDataSource command.", - "tr": "{method} endpointleri tasarım ekranından çalıştırılmaz; SqlDataSource komutuna bağlayın." + "en": "{method} endpoints are not run from the design screen; bind them to a Form command.", + "tr": "{method} endpointleri tasarım ekranından çalıştırılmaz; Form komutuna bağlayın." }, { "resourceName": "Platform", @@ -20193,8 +20295,8 @@ { "resourceName": "Platform", "key": "App.DeveloperKitComponentDesigner.RecordFieldHint", - "en": "The SqlDataSource column the selection is read from and saved to.", - "tr": "Seçimin okunacağı ve kaydedileceği SqlDataSource sütunu." + "en": "The Form column the selection is read from and saved to.", + "tr": "Seçimin okunacağı ve kaydedileceği Form sütunu." }, { "resourceName": "Platform", @@ -20349,8 +20451,8 @@ { "resourceName": "Platform", "key": "App.DeveloperKitComponentDesigner.SqlDataSourceRecord", - "en": "SqlDataSource record · {field}", - "tr": "SqlDataSource kaydı · {field}" + "en": "Form record · {field}", + "tr": "Form kaydı · {field}" }, { "resourceName": "Platform", @@ -24759,8 +24861,8 @@ { "resourceName": "Platform", "key": "App.CatalogGroup.SqlDataSource", - "en": "SqlDataSource", - "tr": "SqlDataSource" + "en": "Form", + "tr": "Form" }, { "resourceName": "Platform", @@ -24906,6 +25008,12 @@ "en": "New record / editing, busy and error state.", "tr": "Yeni kayıt / düzenleme, meşgul ve hata durumu." }, + { + "resourceName": "Platform", + "key": "App.CatalogSqlDataSource.SqlDataSourceTitleDescription", + "en": "Header text of the card wrapping the container; leave empty to hide the header. A value starting with :: is resolved as a localization key.", + "tr": "Kabı saran kartın başlık metni; boş bırakılırsa başlık gizlenir. :: ile başlayan değer dil anahtarı olarak çözümlenir." + }, { "resourceName": "Platform", "key": "App.CatalogSqlDataSource.SqlDataSourceTitle", @@ -25707,8 +25815,8 @@ { "resourceName": "Platform", "key": "App.VisualDesigner.SqlRefRequired", - "en": "This rule requires a SqlDataSource ref name.", - "tr": "Bu kural bir SqlDataSource ref adı gerektiriyor." + "en": "This rule requires a Form ref name.", + "tr": "Bu kural bir Form ref adı gerektiriyor." }, { "resourceName": "Platform", diff --git a/api/src/Sozsoft.Platform.Domain/DeveloperKit/IDynamicEntityManager.cs b/api/src/Sozsoft.Platform.Domain/DeveloperKit/IDynamicEntityManager.cs index 7d0eaa2c..38c33cb8 100644 --- a/api/src/Sozsoft.Platform.Domain/DeveloperKit/IDynamicEntityManager.cs +++ b/api/src/Sozsoft.Platform.Domain/DeveloperKit/IDynamicEntityManager.cs @@ -7,7 +7,14 @@ namespace Sozsoft.Platform.Domain.DeveloperKit; public interface IDynamicEntityManager { - Task?> GetEntityListAsync(string entityName); + /// + /// Column filters read from the request query string. The key is the column + /// name, optionally suffixed with an operator (Name.contains); every + /// column is validated against the table schema before it reaches the SQL. + /// + Task?> GetEntityListAsync( + string entityName, + IReadOnlyDictionary? filters = null); Task GetEntityByIdAsync(string entityName, Guid id); Task CreateEntityAsync(string entityName, JsonElement data); Task UpdateEntityAsync(string entityName, Guid id, JsonElement data); diff --git a/api/src/Sozsoft.Platform.EntityFrameworkCore/DeveloperKit/PlatformDynamicEntityManager.cs b/api/src/Sozsoft.Platform.EntityFrameworkCore/DeveloperKit/PlatformDynamicEntityManager.cs index 8399134b..22cdec10 100644 --- a/api/src/Sozsoft.Platform.EntityFrameworkCore/DeveloperKit/PlatformDynamicEntityManager.cs +++ b/api/src/Sozsoft.Platform.EntityFrameworkCore/DeveloperKit/PlatformDynamicEntityManager.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; using System.Linq; using System.Text.Json; using System.Threading.Tasks; +using Volo.Abp; using Volo.Abp.EntityFrameworkCore; public class DynamicEntityManager : IDynamicEntityManager @@ -19,17 +20,172 @@ public class DynamicEntityManager : IDynamicEntityManager _dbContextProvider = dbContextProvider; } - public async Task?> GetEntityListAsync(string entityName) + public async Task?> GetEntityListAsync( + string entityName, + IReadOnlyDictionary? filters = null) { var dbContext = await _dbContextProvider.GetDbContextAsync(); var isPostgreSql = IsPostgreSql(dbContext); var tableName = QuoteIdentifier(entityName, isPostgreSql); var idDeletedColumn = QuoteIdentifier("IsDeleted", isPostgreSql); var hasIsDeleted = await ColumnExistsAsync(entityName, "IsDeleted"); - var query = hasIsDeleted - ? $"SELECT * FROM {tableName} WHERE {idDeletedColumn} = {FalseLiteral(isPostgreSql)} OR {idDeletedColumn} IS NULL" + + var conditions = new List(); + if (hasIsDeleted) + conditions.Add($"({idDeletedColumn} = {FalseLiteral(isPostgreSql)} OR {idDeletedColumn} IS NULL)"); + + var parameters = new List(); + if (filters is { Count: > 0 }) + { + var columns = await GetColumnNamesAsync(entityName); + foreach (var filter in filters) + { + var condition = BuildFilterCondition(filter.Key, filter.Value, entityName, columns, isPostgreSql, parameters); + if (condition != null) + conditions.Add(condition); + } + } + + var query = conditions.Count > 0 + ? $"SELECT * FROM {tableName} WHERE {string.Join(" AND ", conditions)}" : $"SELECT * FROM {tableName}"; - return await ExecuteRawQueryAsync(query); + return await ExecuteRawQueryAsync(query, parameters); + } + + /// + /// Query string entry to SQL predicate, or null when the entry carries + /// no filter (a paging parameter, or an operator whose value is empty). The + /// column is matched against the real table schema and only ever reaches the + /// SQL as a quoted identifier; the compared value is always a parameter, so + /// neither half of a filter can carry injected SQL. + /// + private static string? BuildFilterCondition( + string key, + string? rawValue, + string entityName, + IReadOnlyDictionary columns, + bool isPostgreSql, + List parameters) + { + if (string.IsNullOrWhiteSpace(key) || ReservedQueryKeys.Contains(key)) + return null; + + var separator = key.LastIndexOf('.'); + var columnName = separator > 0 ? key[..separator] : key; + var operatorName = separator > 0 ? key[(separator + 1)..].ToLowerInvariant() : "eq"; + + // An unknown column is a configuration mistake, not a filter to ignore: + // dropping it silently would return the whole table as if it had matched. + if (!columns.TryGetValue(columnName, out var actualColumn)) + throw new UserFriendlyException($"'{columnName}' is not a column of {entityName}."); + + var column = QuoteIdentifier(actualColumn, isPostgreSql); + + if (operatorName == "isnull") + return $"{column} IS NULL"; + if (operatorName == "notnull") + return $"{column} IS NOT NULL"; + + var value = rawValue?.Trim(); + if (string.IsNullOrEmpty(value)) + return null; + + switch (operatorName) + { + case "eq": + return $"{column} = {AddParameter(parameters, ToFilterValue(value))}"; + case "ne": + return $"{column} <> {AddParameter(parameters, ToFilterValue(value))}"; + case "gt": + return $"{column} > {AddParameter(parameters, ToFilterValue(value))}"; + case "gte": + return $"{column} >= {AddParameter(parameters, ToFilterValue(value))}"; + case "lt": + return $"{column} < {AddParameter(parameters, ToFilterValue(value))}"; + case "lte": + return $"{column} <= {AddParameter(parameters, ToFilterValue(value))}"; + case "contains": + return LikeCondition(column, "%" + EscapeLike(value) + "%", isPostgreSql, parameters); + case "startswith": + return LikeCondition(column, EscapeLike(value) + "%", isPostgreSql, parameters); + case "endswith": + return LikeCondition(column, "%" + EscapeLike(value), isPostgreSql, parameters); + case "in": + var entries = value + .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) + .Select(entry => AddParameter(parameters, ToFilterValue(entry))) + .ToList(); + return entries.Count == 0 ? null : $"{column} IN ({string.Join(", ", entries)})"; + default: + throw new UserFriendlyException($"'{operatorName}' is not a supported filter operator."); + } + } + + /// + /// Case insensitive LIKE. SQL Server collations are case insensitive by + /// default, PostgreSQL ones are not, so it uses ILIKE there. The column is + /// cast to text so a non-string column (a Guid key) can still be searched. + /// + private static string LikeCondition(string column, string pattern, bool isPostgreSql, List parameters) + { + var placeholder = AddParameter(parameters, pattern); + return isPostgreSql + ? $"{column}::text ILIKE {placeholder} ESCAPE '\\'" + : $"CAST({column} AS NVARCHAR(MAX)) LIKE {placeholder} ESCAPE '\\'"; + } + + /// Wildcards typed by the user are matched literally, not as patterns. + private static string EscapeLike(string value) + { + return value + .Replace("\\", "\\\\") + .Replace("%", "\\%") + .Replace("_", "\\_") + .Replace("[", "\\["); + } + + /// + /// A query string value is always text; typing it here is what lets a Guid + /// column be compared against a Guid rather than against its string form, + /// which the provider rejects outright. + /// + private static object ToFilterValue(string value) + { + if (Guid.TryParse(value, out var guid)) return guid; + if (bool.TryParse(value, out var boolean)) return boolean; + if (long.TryParse(value, System.Globalization.NumberStyles.Integer, System.Globalization.CultureInfo.InvariantCulture, out var integer)) return integer; + if (decimal.TryParse(value, System.Globalization.NumberStyles.Number, System.Globalization.CultureInfo.InvariantCulture, out var number)) return number; + if (DateTime.TryParse(value, System.Globalization.CultureInfo.InvariantCulture, System.Globalization.DateTimeStyles.RoundtripKind, out var date)) return date; + return value; + } + + /// Query string keys that carry paging/sorting, not a column filter. + private static readonly HashSet ReservedQueryKeys = new(StringComparer.OrdinalIgnoreCase) + { + "skipCount", + "maxResultCount", + "sorting", + "api-version", + "_", + }; + + /// Real column names of a table, keyed case insensitively. + private async Task> GetColumnNamesAsync(string tableName) + { + var dbContext = await _dbContextProvider.GetDbContextAsync(); + var isPostgreSql = IsPostgreSql(dbContext); + var query = isPostgreSql + ? $"SELECT column_name FROM information_schema.columns WHERE table_schema NOT IN ('pg_catalog', 'information_schema') AND table_name = '{tableName.Replace("'", "''")}'" + : $"SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = '{tableName.Replace("'", "''")}'"; + var rows = await ExecuteRawQueryAsync(query); + var columns = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var row in rows) + { + if (row is not Dictionary item) continue; + var name = item.Values.FirstOrDefault()?.ToString(); + if (!string.IsNullOrWhiteSpace(name)) columns[name] = name; + } + return columns; } public async Task GetEntityByIdAsync(string entityName, Guid id) @@ -167,7 +323,13 @@ public class DynamicEntityManager : IDynamicEntityManager } } - private async Task> ExecuteRawQueryAsync(string query) + /// + /// are registered by , + /// so the query text carries {0} style placeholders. This path reads + /// through a raw command rather than through ExecuteSqlRaw, so the + /// placeholders are rewritten to real command parameters here. + /// + private async Task> ExecuteRawQueryAsync(string query, IReadOnlyList? parameters = null) { var dbContext = await _dbContextProvider.GetDbContextAsync(); var connection = dbContext.Database.GetDbConnection(); @@ -177,6 +339,14 @@ public class DynamicEntityManager : IDynamicEntityManager { using var command = connection.CreateCommand(); command.CommandText = query; + for (var index = 0; index < (parameters?.Count ?? 0); index++) + { + var parameter = command.CreateParameter(); + parameter.ParameterName = "p" + index; + parameter.Value = parameters![index] ?? DBNull.Value; + command.Parameters.Add(parameter); + command.CommandText = command.CommandText.Replace("{" + index + "}", "@p" + index); + } command.Transaction = dbContext.Database.CurrentTransaction?.GetDbTransaction(); using var reader = await command.ExecuteReaderAsync(); diff --git a/configs/seeds/host/crud/AbpUserRoles.json b/configs/seeds/host/crud/AbpUserRoles.json new file mode 100644 index 00000000..e2c03631 --- /dev/null +++ b/configs/seeds/host/crud/AbpUserRoles.json @@ -0,0 +1,36 @@ +{ + "EntityName": "AbpUserRoles", + "GeneratedAt": "2026-08-19T09:53:39.2444186Z", + "Endpoints": [ + { + "Method": "POST", + "Path": "/api/app/crudendpoint/AbpUserRoles", + "OperationType": "Create", + "IsActive": true + }, + { + "Method": "DELETE", + "Path": "/api/app/crudendpoint/AbpUserRoles/{id}", + "OperationType": "Delete", + "IsActive": true + }, + { + "Method": "GET", + "Path": "/api/app/crudendpoint/AbpUserRoles/{id}", + "OperationType": "GetById", + "IsActive": true + }, + { + "Method": "GET", + "Path": "/api/app/crudendpoint/AbpUserRoles", + "OperationType": "GetList", + "IsActive": true + }, + { + "Method": "PUT", + "Path": "/api/app/crudendpoint/AbpUserRoles/{id}", + "OperationType": "Update", + "IsActive": true + } + ] +} \ No newline at end of file diff --git a/configs/seeds/host/custom/RoleComponent.json b/configs/seeds/host/custom/RoleComponent.json index 69c1fa7c..6d635200 100644 --- a/configs/seeds/host/custom/RoleComponent.json +++ b/configs/seeds/host/custom/RoleComponent.json @@ -1,11 +1,11 @@ { - "GeneratedAt": "2026-08-18T11:06:39.5304984Z", + "GeneratedAt": "2026-08-19T12:12:39.0024499Z", "CustomComponents": [ { "Name": "RoleComponent", "RoutePath": "/admin/roles", - "Code": "/*__SOZSOFT_VISUAL_DESIGNER__%7B%22version%22%3A1%2C%22sourceMode%22%3A%22visual%22%2C%22nodes%22%3A%5B%7B%22id%22%3A%22cmp_msyfke8z_96c0a%22%2C%22type%22%3A%22span%22%2C%22kind%22%3A%22html%22%2C%22props%22%3A%7B%22children%22%3A%22Rol%20List%22%7D%2C%22events%22%3A%7B%7D%2C%22bindings%22%3A%7B%7D%2C%22children%22%3A%5B%5D%2C%22ref%22%3A%22span1%22%7D%2C%7B%22id%22%3A%22cmp_msyfmegt_jkkn3%22%2C%22type%22%3A%22SqlDataSource%22%2C%22kind%22%3A%22layout%22%2C%22props%22%3A%7B%22selectEndpoint%22%3A%22source_5w_5ibpv%22%2C%22insertEndpoint%22%3A%22source_h3_484qa%22%2C%22updateEndpoint%22%3A%22source_gb_d9cvs%22%2C%22deleteEndpoint%22%3A%22source_0t_avok9%22%2C%22keyFieldName%22%3A%22id%22%2C%22collectionPath%22%3A%22%22%2C%22keySource%22%3A%22query%22%2C%22keyParamName%22%3A%22id%22%2C%22previewKeyValue%22%3A%22%22%2C%22autoLoad%22%3Atrue%2C%22showToolbar%22%3Atrue%2C%22gap%22%3A16%2C%22className%22%3A%22%22%7D%2C%22events%22%3A%7B%22onLoad%22%3A%22%22%2C%22onRecordChange%22%3A%22%22%2C%22onFieldChange%22%3A%22%22%2C%22onNewRecord%22%3A%22%22%2C%22onModeChange%22%3A%22%22%2C%22onBeforeSave%22%3A%22%22%2C%22onAfterSave%22%3A%22%22%2C%22onBeforeDelete%22%3A%22%22%2C%22onAfterDelete%22%3A%22%22%2C%22onError%22%3A%22%22%7D%2C%22bindings%22%3A%7B%7D%2C%22children%22%3A%5B%7B%22id%22%3A%22cmp_msyfpfia_ifhdq%22%2C%22type%22%3A%22Input%22%2C%22kind%22%3A%22ui%22%2C%22props%22%3A%7B%22asElement%22%3A%22%22%2C%22disabled%22%3Afalse%2C%22invalid%22%3Afalse%2C%22rows%22%3A3%2C%22prefix%22%3A%22Name%20%3A%22%2C%22size%22%3A%22md%22%2C%22suffix%22%3A%22%22%2C%22textArea%22%3Afalse%2C%22type%22%3A%22text%22%2C%22unstyle%22%3Afalse%2C%22field%22%3A%22%22%2C%22form%22%3A%22%22%2C%22className%22%3A%22%22%2C%22children%22%3A%22%22%2C%22style%22%3A%7B%7D%2C%22placeholder%22%3A%22Metin%20girin...%22%2C%22value%22%3A%22%22%2C%22name%22%3A%22%22%2C%22autoComplete%22%3A%22off%22%2C%22autoFocus%22%3Afalse%2C%22readOnly%22%3Afalse%2C%22required%22%3Afalse%2C%22maxLength%22%3A%22%22%2C%22minLength%22%3A%22%22%2C%22pattern%22%3A%22%22%2C%22sqlDefaultValue%22%3A%22%22%7D%2C%22events%22%3A%7B%22onChange%22%3A%22%22%7D%2C%22bindings%22%3A%7B%22value%22%3A%7B%22sourceId%22%3A%22cmp_msyfmegt_jkkn3%22%2C%22path%22%3A%22Name%22%7D%7D%2C%22children%22%3A%5B%5D%2C%22ref%22%3A%22input1%22%7D%2C%7B%22id%22%3A%22cmp_msyjl707_crcn7%22%2C%22type%22%3A%22Input%22%2C%22kind%22%3A%22ui%22%2C%22props%22%3A%7B%22asElement%22%3A%22%22%2C%22disabled%22%3Afalse%2C%22invalid%22%3Afalse%2C%22rows%22%3A3%2C%22prefix%22%3A%22Normalized%20Name%20%3A%22%2C%22size%22%3A%22md%22%2C%22suffix%22%3A%22%22%2C%22textArea%22%3Afalse%2C%22type%22%3A%22text%22%2C%22unstyle%22%3Afalse%2C%22field%22%3A%22%22%2C%22form%22%3A%22%22%2C%22className%22%3A%22%22%2C%22children%22%3A%22%22%2C%22style%22%3A%7B%7D%2C%22placeholder%22%3A%22Metin%20girin...%22%2C%22value%22%3A%22%22%2C%22name%22%3A%22%22%2C%22autoComplete%22%3A%22off%22%2C%22autoFocus%22%3Afalse%2C%22readOnly%22%3Afalse%2C%22required%22%3Afalse%2C%22maxLength%22%3A%22%22%2C%22minLength%22%3A%22%22%2C%22pattern%22%3A%22%22%7D%2C%22events%22%3A%7B%22onChange%22%3A%22%22%7D%2C%22bindings%22%3A%7B%22value%22%3A%7B%22sourceId%22%3A%22cmp_msyfmegt_jkkn3%22%2C%22path%22%3A%22NormalizedName%22%7D%7D%2C%22children%22%3A%5B%5D%2C%22ref%22%3A%22input2%22%7D%2C%7B%22id%22%3A%22cmp_msyjdodn_u3o74%22%2C%22type%22%3A%22Input%22%2C%22kind%22%3A%22ui%22%2C%22props%22%3A%7B%22asElement%22%3A%22%22%2C%22disabled%22%3Afalse%2C%22invalid%22%3Afalse%2C%22rows%22%3A3%2C%22prefix%22%3A%22Concurrent%20Stamp%20%3A%20%22%2C%22size%22%3A%22md%22%2C%22suffix%22%3A%22%22%2C%22textArea%22%3Afalse%2C%22type%22%3A%22text%22%2C%22unstyle%22%3Afalse%2C%22field%22%3A%22%22%2C%22form%22%3A%22%22%2C%22className%22%3A%22%22%2C%22children%22%3A%22%22%2C%22style%22%3A%7B%7D%2C%22placeholder%22%3A%22Metin%20girin...%22%2C%22value%22%3A%22%22%2C%22name%22%3A%22%22%2C%22autoComplete%22%3A%22off%22%2C%22autoFocus%22%3Afalse%2C%22readOnly%22%3Afalse%2C%22required%22%3Afalse%2C%22maxLength%22%3A%22%22%2C%22minLength%22%3A%22%22%2C%22pattern%22%3A%22%22%7D%2C%22events%22%3A%7B%22onChange%22%3A%22%22%7D%2C%22bindings%22%3A%7B%22value%22%3A%7B%22sourceId%22%3A%22cmp_msyfmegt_jkkn3%22%2C%22path%22%3A%22ConcurrencyStamp%22%7D%7D%2C%22children%22%3A%5B%5D%2C%22ref%22%3A%22input3%22%7D%2C%7B%22id%22%3A%22cmp_msyje9ys_2c08a%22%2C%22type%22%3A%22Checkbox%22%2C%22kind%22%3A%22ui%22%2C%22props%22%3A%7B%22checked%22%3Afalse%2C%22color%22%3A%22blue-500%22%2C%22disabled%22%3Afalse%2C%22labelRef%22%3A%22%22%2C%22name%22%3A%22%22%2C%22readOnly%22%3Afalse%2C%22value%22%3A%22%22%2C%22field%22%3Anull%2C%22className%22%3A%22%22%2C%22children%22%3A%22Is%20Default%22%2C%22style%22%3A%7B%7D%2C%22sqlDefaultValue%22%3A%22true%22%7D%2C%22events%22%3A%7B%22onChange%22%3A%22%22%7D%2C%22bindings%22%3A%7B%22checked%22%3A%7B%22sourceId%22%3A%22cmp_msyfmegt_jkkn3%22%2C%22path%22%3A%22IsDefault%22%7D%7D%2C%22children%22%3A%5B%5D%2C%22ref%22%3A%22checkbox1%22%7D%2C%7B%22id%22%3A%22cmp_msyjebu2_27tqi%22%2C%22type%22%3A%22Checkbox%22%2C%22kind%22%3A%22ui%22%2C%22props%22%3A%7B%22checked%22%3Afalse%2C%22color%22%3A%22blue-500%22%2C%22disabled%22%3Afalse%2C%22labelRef%22%3A%22%22%2C%22name%22%3A%22%22%2C%22readOnly%22%3Afalse%2C%22value%22%3A%22%22%2C%22field%22%3Anull%2C%22className%22%3A%22%22%2C%22children%22%3A%22Is%20Static%22%2C%22style%22%3A%7B%7D%2C%22sqlDefaultValue%22%3A%22true%22%7D%2C%22events%22%3A%7B%22onChange%22%3A%22%22%7D%2C%22bindings%22%3A%7B%22checked%22%3A%7B%22sourceId%22%3A%22cmp_msyfmegt_jkkn3%22%2C%22path%22%3A%22IsStatic%22%7D%7D%2C%22children%22%3A%5B%5D%2C%22ref%22%3A%22checkbox2%22%7D%2C%7B%22id%22%3A%22cmp_msyjmetn_gojol%22%2C%22type%22%3A%22Checkbox%22%2C%22kind%22%3A%22ui%22%2C%22props%22%3A%7B%22checked%22%3Afalse%2C%22color%22%3A%22blue-500%22%2C%22disabled%22%3Afalse%2C%22labelRef%22%3A%22%22%2C%22name%22%3A%22%22%2C%22readOnly%22%3Afalse%2C%22value%22%3A%22%22%2C%22field%22%3Anull%2C%22className%22%3A%22%22%2C%22children%22%3A%22Is%20Public%22%2C%22style%22%3A%7B%7D%2C%22sqlDefaultValue%22%3A%22false%22%7D%2C%22events%22%3A%7B%22onChange%22%3A%22%22%7D%2C%22bindings%22%3A%7B%22checked%22%3A%7B%22sourceId%22%3A%22cmp_msyfmegt_jkkn3%22%2C%22path%22%3A%22IsPublic%22%7D%7D%2C%22children%22%3A%5B%5D%2C%22ref%22%3A%22checkbox3%22%7D%2C%7B%22id%22%3A%22cmp_msyjo0g8_oathk%22%2C%22type%22%3A%22Input%22%2C%22kind%22%3A%22ui%22%2C%22props%22%3A%7B%22asElement%22%3A%22%22%2C%22disabled%22%3Afalse%2C%22invalid%22%3Afalse%2C%22rows%22%3A3%2C%22prefix%22%3A%22Entity%20Version%20%3A%20%22%2C%22size%22%3A%22md%22%2C%22suffix%22%3A%22%22%2C%22textArea%22%3Afalse%2C%22type%22%3A%22number%22%2C%22unstyle%22%3Afalse%2C%22field%22%3A%22%22%2C%22form%22%3A%22%22%2C%22className%22%3A%22%22%2C%22children%22%3A%22%22%2C%22style%22%3A%7B%7D%2C%22placeholder%22%3A%22Metin%20girin...%22%2C%22value%22%3A%22%22%2C%22name%22%3A%22%22%2C%22autoComplete%22%3A%22off%22%2C%22autoFocus%22%3Afalse%2C%22readOnly%22%3Afalse%2C%22required%22%3Afalse%2C%22maxLength%22%3A%22%22%2C%22minLength%22%3A%22%22%2C%22pattern%22%3A%22%22%2C%22sqlDefaultValue%22%3A%223%22%7D%2C%22events%22%3A%7B%22onChange%22%3A%22%22%7D%2C%22bindings%22%3A%7B%22value%22%3A%7B%22sourceId%22%3A%22cmp_msyfmegt_jkkn3%22%2C%22path%22%3A%22EntityVersion%22%7D%7D%2C%22children%22%3A%5B%5D%2C%22ref%22%3A%22input4%22%7D%2C%7B%22id%22%3A%22cmp_msyjplfq_py8lz%22%2C%22type%22%3A%22Input%22%2C%22kind%22%3A%22ui%22%2C%22props%22%3A%7B%22asElement%22%3A%22%22%2C%22disabled%22%3Afalse%2C%22invalid%22%3Afalse%2C%22rows%22%3A3%2C%22prefix%22%3A%22Extra%20Properties%20%3A%22%2C%22size%22%3A%22md%22%2C%22suffix%22%3A%22%22%2C%22textArea%22%3Afalse%2C%22type%22%3A%22text%22%2C%22unstyle%22%3Afalse%2C%22field%22%3A%22%22%2C%22form%22%3A%22%22%2C%22className%22%3A%22%22%2C%22children%22%3A%22%22%2C%22style%22%3A%7B%7D%2C%22placeholder%22%3A%22Metin%20girin...%22%2C%22value%22%3A%22%22%2C%22name%22%3A%22%22%2C%22autoComplete%22%3A%22off%22%2C%22autoFocus%22%3Afalse%2C%22readOnly%22%3Afalse%2C%22required%22%3Afalse%2C%22maxLength%22%3A%22%22%2C%22minLength%22%3A%22%22%2C%22pattern%22%3A%22%22%2C%22sqlDefaultValue%22%3A%22%7B%7D%22%7D%2C%22events%22%3A%7B%22onChange%22%3A%22%22%7D%2C%22bindings%22%3A%7B%22value%22%3A%7B%22sourceId%22%3A%22cmp_msyfmegt_jkkn3%22%2C%22path%22%3A%22ExtraProperties%22%7D%7D%2C%22children%22%3A%5B%5D%2C%22ref%22%3A%22input5%22%7D%5D%2C%22ref%22%3A%22sqlDataSource1%22%7D%5D%2C%22canvas%22%3A%7B%22width%22%3A%22responsive%22%7D%2C%22lifecycle%22%3A%7B%22onMount%22%3A%22%22%7D%2C%22dataSources%22%3A%5B%7B%22id%22%3A%22source_5w_5ibpv%22%2C%22name%22%3A%22AbpRoles%20%C2%B7%20GetList%22%2C%22method%22%3A%22GET%22%2C%22url%22%3A%22%2Fapi%2Fapp%2Fcrudendpoint%2FAbpRoles%22%2C%22responsePath%22%3A%22%22%7D%2C%7B%22id%22%3A%22source_i7_msox4%22%2C%22name%22%3A%22AbpRoles%20%C2%B7%20GetById%22%2C%22method%22%3A%22GET%22%2C%22url%22%3A%22%2Fapi%2Fapp%2Fcrudendpoint%2FAbpRoles%2F%7Bid%7D%22%2C%22responsePath%22%3A%22%22%7D%2C%7B%22id%22%3A%22source_0t_avok9%22%2C%22name%22%3A%22AbpRoles%20%C2%B7%20Delete%22%2C%22method%22%3A%22DELETE%22%2C%22url%22%3A%22%2Fapi%2Fapp%2Fcrudendpoint%2FAbpRoles%2F%7Bid%7D%22%2C%22responsePath%22%3A%22%22%7D%2C%7B%22id%22%3A%22source_h3_484qa%22%2C%22name%22%3A%22AbpRoles%20%C2%B7%20Create%22%2C%22method%22%3A%22POST%22%2C%22url%22%3A%22%2Fapi%2Fapp%2Fcrudendpoint%2FAbpRoles%22%2C%22responsePath%22%3A%22%22%7D%2C%7B%22id%22%3A%22source_gb_d9cvs%22%2C%22name%22%3A%22AbpRoles%20%C2%B7%20Update%22%2C%22method%22%3A%22PUT%22%2C%22url%22%3A%22%2Fapi%2Fapp%2Fcrudendpoint%2FAbpRoles%2F%7Bid%7D%22%2C%22responsePath%22%3A%22%22%7D%5D%7D__*/\nconst RoleComponent = () => {\n const getByPath = (value, path) => {\n if (!path) return value\n const readPath = (target, targetPath) => targetPath.split('.').filter(Boolean).reduce((current, key) => current?.[key], target)\n const directValue = readPath(value, path)\n if (directValue !== undefined) return directValue\n if (path.startsWith('result.')) return readPath(value, path.slice('result.'.length))\n return value?.result === undefined ? undefined : readPath(value.result, path)\n }\n\n const readSqlField = (record, field) => {\n if (!record || typeof record !== \"object\" || Array.isArray(record)) return undefined\n if (field in record) return record[field]\n const matched = Object.keys(record).find((key) => key.toLowerCase() === String(field).toLowerCase())\n return matched === undefined ? undefined : record[matched]\n }\n const toSqlRows = (value, collectionPath) => {\n const source = collectionPath ? getByPath(value, collectionPath) : value\n const rows = Array.isArray(source)\n ? source\n : source && typeof source === \"object\"\n ? (Array.isArray(source.items) ? source.items : [source])\n : []\n return rows.filter((item) => item && typeof item === \"object\" && !Array.isArray(item))\n }\n const setSqlField = (record, path, value) => {\n const keys = String(path || \"\").split(\".\").filter(Boolean)\n if (!keys.length) return record\n const next = { ...(record || {}) }\n let target = next\n for (let index = 0; index < keys.length - 1; index += 1) {\n const child = target[keys[index]]\n target[keys[index]] = child && typeof child === \"object\" && !Array.isArray(child) ? { ...child } : {}\n target = target[keys[index]]\n }\n target[keys[keys.length - 1]] = value\n return next\n }\n const bindSqlUrl = (url, record, keyField) => {\n let keyBound = false\n const boundUrl = String(url).replace(/\\{([^}]+)\\}|(?<=\\/):([A-Za-z_][A-Za-z0-9_]*)/g, (match, braced, colon) => {\n const name = (braced || colon || \"\").trim()\n if (!name) return match\n const value = readSqlField(record, name) ?? readSqlField(record, keyField)\n if (value === undefined || value === null || value === \"\") return match\n if (name.toLowerCase() === String(keyField).toLowerCase()) keyBound = true\n return encodeURIComponent(String(value))\n })\n return { url: boundUrl, keyBound }\n }\n const appendQueryParam = (url, name, value) =>\n url + (url.includes(\"?\") ? \"&\" : \"?\") + encodeURIComponent(name) + \"=\" + encodeURIComponent(value)\n const hasSqlUrlParams = (url) => /\\{[^}]+\\}|(?<=\\/):[A-Za-z_][A-Za-z0-9_]*/.test(String(url))\n const readUrlKey = (source, name) => {\n if (typeof window === \"undefined\") return \"\"\n if (source === \"query\") return new URLSearchParams(window.location.search).get(name) ?? \"\"\n const segments = window.location.pathname.split(\"/\").filter(Boolean)\n return segments.length ? decodeURIComponent(segments[segments.length - 1]) : \"\"\n }\n const filterSqlRowsByKey = (rows, keyField, keyValue) => {\n if (!keyValue) return rows\n const matched = rows.filter((row) => String(readSqlField(row, keyField) ?? \"\") === String(keyValue))\n // No match means the endpoint already filtered server side, or the key column\n // is named differently — keeping the rows beats showing an empty form.\n return matched.length ? matched : rows\n }\n // Update payload: only the columns the user actually edited, plus the key.\n const toSqlChanges = (original, current, keyField) => {\n const changes = {}\n const isSame = (left, right) => JSON.stringify(left ?? null) === JSON.stringify(right ?? null)\n Object.keys(current || {}).forEach((column) => {\n if (!isSame(original ? original[column] : undefined, current[column])) changes[column] = current[column]\n })\n const keyColumn = Object.keys(current || {}).find((column) => column.toLowerCase() === String(keyField).toLowerCase())\n if (keyColumn !== undefined && changes[keyColumn] === undefined) changes[keyColumn] = current[keyColumn]\n return changes\n }\n const callSqlEndpoint = (target, record, keyField, payload) => {\n // The URL is always bound from the full record: a placeholder may reference a\n // column that the update payload does not carry.\n const bound = bindSqlUrl(target.url, record, keyField)\n const request = { url: bound.url, method: target.method }\n if (target.method === \"POST\" || target.method === \"PUT\") request.data = payload === undefined ? record : payload\n const keyValue = readSqlField(record, keyField)\n const hasKey = keyValue !== undefined && keyValue !== null && keyValue !== \"\"\n // The key still has to reach the endpoint when the URL carries no placeholder.\n if (!bound.keyBound && hasKey && target.method !== \"POST\") request.params = { [keyField]: keyValue }\n return apiService.fetchData(request)\n }\n // Defaults declared on the bound children; only a new record starts from them.\n const buildSqlDefaults = (fields) => fields.reduce((record, field) => setSqlField(record, field.path, resolveSqlDefault(field)), {})\n const resolveSqlDefault = (field) => {\n if (field.token !== \"today\" && field.token !== \"now\") return field.value\n const now = new Date()\n const pad = (value) => String(value).padStart(2, \"0\")\n const day = now.getFullYear() + \"-\" + pad(now.getMonth() + 1) + \"-\" + pad(now.getDate())\n if (field.token === \"today\" || field.dateOnly) return day\n return day + \"T\" + pad(now.getHours()) + \":\" + pad(now.getMinutes()) + \":\" + pad(now.getSeconds())\n }\n const toSqlErrorMessage = (error) => error?.response?.data?.error?.message || error?.response?.data?.message || error?.message || \"İşlem tamamlanamadı.\"\n\n const [data_source_5w_5ibpv, setData_source_5w_5ibpv] = React.useState(null)\n\n const [data_source_i7_msox4, setData_source_i7_msox4] = React.useState(null)\n\n const [data_source_0t_avok9] = React.useState(null)\n\n const [data_source_h3_484qa] = React.useState(null)\n\n const [data_source_gb_d9cvs] = React.useState(null)\n\n const sqlselect_cmp_msyfmegt_jkkn3 = { url: \"/api/app/crudendpoint/AbpRoles\", method: \"GET\", responsePath: \"\" }\n const sqlinsert_cmp_msyfmegt_jkkn3 = { url: \"/api/app/crudendpoint/AbpRoles\", method: \"POST\", responsePath: \"\" }\n const sqlupdate_cmp_msyfmegt_jkkn3 = { url: \"/api/app/crudendpoint/AbpRoles/{id}\", method: \"PUT\", responsePath: \"\" }\n const sqldelete_cmp_msyfmegt_jkkn3 = { url: \"/api/app/crudendpoint/AbpRoles/{id}\", method: \"DELETE\", responsePath: \"\" }\n const sqlKey_cmp_msyfmegt_jkkn3 = \"id\"\n const sqlKeyParam_cmp_msyfmegt_jkkn3 = \"id\"\n const [data_cmp_msyfmegt_jkkn3, setData_cmp_msyfmegt_jkkn3] = React.useState({})\n const [sqlOriginal_cmp_msyfmegt_jkkn3, setSqlOriginal_cmp_msyfmegt_jkkn3] = React.useState({})\n const [sqlMode_cmp_msyfmegt_jkkn3, setSqlMode_cmp_msyfmegt_jkkn3] = React.useState(\"edit\")\n const [sqlIndex_cmp_msyfmegt_jkkn3, setSqlIndex_cmp_msyfmegt_jkkn3] = React.useState(0)\n const [sqlBusy_cmp_msyfmegt_jkkn3, setSqlBusy_cmp_msyfmegt_jkkn3] = React.useState(false)\n const [sqlError_cmp_msyfmegt_jkkn3, setSqlError_cmp_msyfmegt_jkkn3] = React.useState(\"\")\n // The Select key can come from the page URL, which is how a detail page reads\n // /api/app/orders/{id} or /api/app/orders?id=… for a single record.\n const sqlUrlKey_cmp_msyfmegt_jkkn3 = readUrlKey(\"query\", sqlKeyParam_cmp_msyfmegt_jkkn3)\n const sqlSelectUrl_cmp_msyfmegt_jkkn3 = React.useMemo(() => {\n if (!sqlselect_cmp_msyfmegt_jkkn3) return \"\"\n if (!sqlUrlKey_cmp_msyfmegt_jkkn3) return sqlselect_cmp_msyfmegt_jkkn3.url\n const bound = bindSqlUrl(sqlselect_cmp_msyfmegt_jkkn3.url, { [sqlKeyParam_cmp_msyfmegt_jkkn3]: sqlUrlKey_cmp_msyfmegt_jkkn3 }, sqlKeyParam_cmp_msyfmegt_jkkn3)\n return bound.keyBound ? bound.url : appendQueryParam(bound.url, sqlKeyParam_cmp_msyfmegt_jkkn3, sqlUrlKey_cmp_msyfmegt_jkkn3)\n }, [sqlUrlKey_cmp_msyfmegt_jkkn3])\n // A key in the page URL always narrows the result: a list endpoint ignores the\n // parameter server side, so the requested record is picked out here. Without a\n // key every row is kept and the navigation below takes over.\n const sqlRows_cmp_msyfmegt_jkkn3 = React.useMemo(\n () => filterSqlRowsByKey(toSqlRows(data_source_5w_5ibpv, \"\"), sqlKey_cmp_msyfmegt_jkkn3, sqlUrlKey_cmp_msyfmegt_jkkn3),\n [data_source_5w_5ibpv, sqlUrlKey_cmp_msyfmegt_jkkn3],\n )\n // A fresh result set resets the position; New mode is left untouched so an\n // unsaved draft is not overwritten by a re-render.\n React.useEffect(() => {\n setSqlIndex_cmp_msyfmegt_jkkn3((current) => (current < sqlRows_cmp_msyfmegt_jkkn3.length ? current : 0))\n }, [sqlRows_cmp_msyfmegt_jkkn3])\n React.useEffect(() => {\n const row = sqlRows_cmp_msyfmegt_jkkn3[sqlIndex_cmp_msyfmegt_jkkn3]\n if (!row) return\n setData_cmp_msyfmegt_jkkn3(row)\n setSqlOriginal_cmp_msyfmegt_jkkn3(row)\n setSqlMode_cmp_msyfmegt_jkkn3(\"edit\")\n }, [sqlRows_cmp_msyfmegt_jkkn3, sqlIndex_cmp_msyfmegt_jkkn3])\n const sqlHasKey_cmp_msyfmegt_jkkn3 = (() => {\n const value = readSqlField(data_cmp_msyfmegt_jkkn3, sqlKey_cmp_msyfmegt_jkkn3)\n return value !== undefined && value !== null && value !== \"\"\n })()\n const sqlSetField_cmp_msyfmegt_jkkn3 = React.useCallback((path, value) => setData_cmp_msyfmegt_jkkn3((current) => setSqlField(current, path, value)), [])\n const sqlReload_cmp_msyfmegt_jkkn3 = React.useCallback(async () => {\n if (!sqlSelectUrl_cmp_msyfmegt_jkkn3) return\n // Calling a URL that still holds a placeholder is a guaranteed 400, so the\n // missing key is reported in the component instead.\n if (hasSqlUrlParams(sqlSelectUrl_cmp_msyfmegt_jkkn3)) {\n throw new Error(\"Select endpointi \" + sqlKeyParam_cmp_msyfmegt_jkkn3 + \" parametresini bekliyor; sayfa adresinde bulunamadı.\")\n }\n const response = await apiService.fetchData({ url: sqlSelectUrl_cmp_msyfmegt_jkkn3, method: \"GET\" })\n setData_source_5w_5ibpv(getByPath(response.data, sqlselect_cmp_msyfmegt_jkkn3.responsePath))\n }, [sqlSelectUrl_cmp_msyfmegt_jkkn3])\n // Reload wrapped with the busy/error handling the toolbar and mount effect need,\n // so a failing Select is reported in the component instead of the console.\n const sqlRefresh_cmp_msyfmegt_jkkn3 = React.useCallback(async () => {\n setSqlBusy_cmp_msyfmegt_jkkn3(true)\n setSqlError_cmp_msyfmegt_jkkn3(\"\")\n try {\n await sqlReload_cmp_msyfmegt_jkkn3()\n } catch (error) {\n setSqlError_cmp_msyfmegt_jkkn3(toSqlErrorMessage(error))\n } finally {\n setSqlBusy_cmp_msyfmegt_jkkn3(false)\n }\n }, [sqlReload_cmp_msyfmegt_jkkn3])\n const sqlNew_cmp_msyfmegt_jkkn3 = React.useCallback(() => {\n setSqlError_cmp_msyfmegt_jkkn3(\"\")\n // Defaults are resolved on every New, so an @today column carries the day the\n // record was opened rather than the day the component was generated.\n const sqlDraft_cmp_msyfmegt_jkkn3 = buildSqlDefaults([{\"path\":\"IsDefault\",\"value\":true},{\"path\":\"IsStatic\",\"value\":true},{\"path\":\"IsPublic\",\"value\":false},{\"path\":\"EntityVersion\",\"value\":3},{\"path\":\"ExtraProperties\",\"value\":\"{}\"}])\n setData_cmp_msyfmegt_jkkn3(sqlDraft_cmp_msyfmegt_jkkn3)\n setSqlOriginal_cmp_msyfmegt_jkkn3({})\n setSqlMode_cmp_msyfmegt_jkkn3(\"new\")\n }, [])\n const sqlPrev_cmp_msyfmegt_jkkn3 = React.useCallback(() => setSqlIndex_cmp_msyfmegt_jkkn3((current) => Math.max(0, current - 1)), [])\n const sqlNext_cmp_msyfmegt_jkkn3 = React.useCallback(() => setSqlIndex_cmp_msyfmegt_jkkn3((current) => Math.min(sqlRows_cmp_msyfmegt_jkkn3.length - 1, current + 1)), [sqlRows_cmp_msyfmegt_jkkn3])\n const sqlSave_cmp_msyfmegt_jkkn3 = async () => {\n const isNew = sqlMode_cmp_msyfmegt_jkkn3 === \"new\"\n const target = isNew ? sqlinsert_cmp_msyfmegt_jkkn3 : sqlupdate_cmp_msyfmegt_jkkn3\n if (!target) {\n setSqlError_cmp_msyfmegt_jkkn3(isNew ? \"Insert için POST endpointi tanımlı değil.\" : \"Update için PUT endpointi tanımlı değil.\")\n return\n }\n // Update carries only the edited columns plus the key; Insert sends the record.\n const payload = isNew ? data_cmp_msyfmegt_jkkn3 : toSqlChanges(sqlOriginal_cmp_msyfmegt_jkkn3, data_cmp_msyfmegt_jkkn3, sqlKey_cmp_msyfmegt_jkkn3)\n if (!isNew && Object.keys(payload).filter((column) => column.toLowerCase() !== String(sqlKey_cmp_msyfmegt_jkkn3).toLowerCase()).length === 0) {\n setSqlError_cmp_msyfmegt_jkkn3(\"Kaydedilecek bir değişiklik yok.\")\n return\n }\n setSqlBusy_cmp_msyfmegt_jkkn3(true)\n setSqlError_cmp_msyfmegt_jkkn3(\"\")\n try {\n const response = await callSqlEndpoint(target, data_cmp_msyfmegt_jkkn3, sqlKey_cmp_msyfmegt_jkkn3, payload)\n // With a Select endpoint the reload is the source of truth and its effect\n // repopulates the form; without one, the response row is all there is.\n if (sqlselect_cmp_msyfmegt_jkkn3) {\n await sqlReload_cmp_msyfmegt_jkkn3()\n } else {\n const saved = toSqlRows(response?.data, \"\")[0]\n if (saved) {\n setData_cmp_msyfmegt_jkkn3(saved)\n setSqlOriginal_cmp_msyfmegt_jkkn3(saved)\n }\n }\n setSqlMode_cmp_msyfmegt_jkkn3(\"edit\")\n } catch (error) {\n setSqlError_cmp_msyfmegt_jkkn3(toSqlErrorMessage(error))\n } finally {\n setSqlBusy_cmp_msyfmegt_jkkn3(false)\n }\n }\n const sqlDelete_cmp_msyfmegt_jkkn3 = async () => {\n if (!sqldelete_cmp_msyfmegt_jkkn3 || !sqlHasKey_cmp_msyfmegt_jkkn3 || sqlMode_cmp_msyfmegt_jkkn3 === \"new\") return\n // Deleting cannot be undone from the form, so it always asks first.\n const confirmMessage = sqlKey_cmp_msyfmegt_jkkn3 + \" = \" + readSqlField(data_cmp_msyfmegt_jkkn3, sqlKey_cmp_msyfmegt_jkkn3) + \" kaydı silinecek.\\n\\nOnaylıyor musunuz?\"\n if (typeof window !== \"undefined\" && !window.confirm(confirmMessage)) return\n setSqlBusy_cmp_msyfmegt_jkkn3(true)\n setSqlError_cmp_msyfmegt_jkkn3(\"\")\n try {\n await callSqlEndpoint(sqldelete_cmp_msyfmegt_jkkn3, data_cmp_msyfmegt_jkkn3, sqlKey_cmp_msyfmegt_jkkn3)\n setData_cmp_msyfmegt_jkkn3({})\n setSqlOriginal_cmp_msyfmegt_jkkn3({})\n setSqlMode_cmp_msyfmegt_jkkn3(\"new\")\n await sqlReload_cmp_msyfmegt_jkkn3()\n } catch (error) {\n setSqlError_cmp_msyfmegt_jkkn3(toSqlErrorMessage(error))\n } finally {\n setSqlBusy_cmp_msyfmegt_jkkn3(false)\n }\n }\n React.useEffect(() => { void sqlRefresh_cmp_msyfmegt_jkkn3() }, [sqlRefresh_cmp_msyfmegt_jkkn3])\n\n const handle_cmp_msyfpfia_ifhdq_onChange = (valueOrEvent, originalEvent) => {\n sqlSetField_cmp_msyfmegt_jkkn3(\"Name\", valueOrEvent?.target?.value ?? valueOrEvent)\n const event = valueOrEvent\n }\n\n const handle_cmp_msyjl707_crcn7_onChange = (valueOrEvent, originalEvent) => {\n sqlSetField_cmp_msyfmegt_jkkn3(\"NormalizedName\", valueOrEvent?.target?.value ?? valueOrEvent)\n const event = valueOrEvent\n }\n\n const handle_cmp_msyjdodn_u3o74_onChange = (valueOrEvent, originalEvent) => {\n sqlSetField_cmp_msyfmegt_jkkn3(\"ConcurrencyStamp\", valueOrEvent?.target?.value ?? valueOrEvent)\n const event = valueOrEvent\n }\n\n const handle_cmp_msyje9ys_2c08a_onChange = (valueOrEvent, originalEvent) => {\n sqlSetField_cmp_msyfmegt_jkkn3(\"IsDefault\", typeof originalEvent === \"object\" && originalEvent?.target ? Boolean(originalEvent.target.checked) : typeof valueOrEvent === \"object\" && valueOrEvent?.target ? Boolean(valueOrEvent.target.checked) : Boolean(valueOrEvent))\n const event = { checked: Boolean(valueOrEvent), originalEvent, target: originalEvent?.target }\n }\n\n const handle_cmp_msyjebu2_27tqi_onChange = (valueOrEvent, originalEvent) => {\n sqlSetField_cmp_msyfmegt_jkkn3(\"IsStatic\", typeof originalEvent === \"object\" && originalEvent?.target ? Boolean(originalEvent.target.checked) : typeof valueOrEvent === \"object\" && valueOrEvent?.target ? Boolean(valueOrEvent.target.checked) : Boolean(valueOrEvent))\n const event = { checked: Boolean(valueOrEvent), originalEvent, target: originalEvent?.target }\n }\n\n const handle_cmp_msyjmetn_gojol_onChange = (valueOrEvent, originalEvent) => {\n sqlSetField_cmp_msyfmegt_jkkn3(\"IsPublic\", typeof originalEvent === \"object\" && originalEvent?.target ? Boolean(originalEvent.target.checked) : typeof valueOrEvent === \"object\" && valueOrEvent?.target ? Boolean(valueOrEvent.target.checked) : Boolean(valueOrEvent))\n const event = { checked: Boolean(valueOrEvent), originalEvent, target: originalEvent?.target }\n }\n\n const handle_cmp_msyjo0g8_oathk_onChange = (valueOrEvent, originalEvent) => {\n sqlSetField_cmp_msyfmegt_jkkn3(\"EntityVersion\", valueOrEvent?.target?.value ?? valueOrEvent)\n const event = valueOrEvent\n }\n\n const handle_cmp_msyjplfq_py8lz_onChange = (valueOrEvent, originalEvent) => {\n sqlSetField_cmp_msyfmegt_jkkn3(\"ExtraProperties\", valueOrEvent?.target?.value ?? valueOrEvent)\n const event = valueOrEvent\n }\n\n return (\n <>\n \n {\"Rol List\"}\n \n
\n \n \n \n \n {\"Is Default\"}\n \n \n {\"Is Static\"}\n \n \n {\"Is Public\"}\n \n \n \n
\n {sqlRows_cmp_msyfmegt_jkkn3.length > 1 ? (\n <>\n \n {`${sqlIndex_cmp_msyfmegt_jkkn3 + 1} / ${sqlRows_cmp_msyfmegt_jkkn3.length}`}\n \n \n \n ) : null}\n \n \n \n \n {sqlMode_cmp_msyfmegt_jkkn3 === \"new\" ? \"Yeni kayıt\" : \"Düzenleme\"}\n
\n {sqlError_cmp_msyfmegt_jkkn3 ?
{sqlError_cmp_msyfmegt_jkkn3}
: null}\n
\n \n )\n}\n\nexport default RoleComponent", - "Props": "{\"visualDesigner\":{\"version\":1,\"sourceMode\":\"visual\",\"nodes\":[{\"id\":\"cmp_msyfke8z_96c0a\",\"type\":\"span\",\"kind\":\"html\",\"props\":{\"children\":\"Rol List\"},\"events\":{},\"bindings\":{},\"children\":[],\"ref\":\"span1\"},{\"id\":\"cmp_msyfmegt_jkkn3\",\"type\":\"SqlDataSource\",\"kind\":\"layout\",\"props\":{\"selectEndpoint\":\"source_5w_5ibpv\",\"insertEndpoint\":\"source_h3_484qa\",\"updateEndpoint\":\"source_gb_d9cvs\",\"deleteEndpoint\":\"source_0t_avok9\",\"keyFieldName\":\"id\",\"collectionPath\":\"\",\"keySource\":\"query\",\"keyParamName\":\"id\",\"previewKeyValue\":\"\",\"autoLoad\":true,\"showToolbar\":true,\"gap\":16,\"className\":\"\"},\"events\":{\"onLoad\":\"\",\"onRecordChange\":\"\",\"onFieldChange\":\"\",\"onNewRecord\":\"\",\"onModeChange\":\"\",\"onBeforeSave\":\"\",\"onAfterSave\":\"\",\"onBeforeDelete\":\"\",\"onAfterDelete\":\"\",\"onError\":\"\"},\"bindings\":{},\"children\":[{\"id\":\"cmp_msyfpfia_ifhdq\",\"type\":\"Input\",\"kind\":\"ui\",\"props\":{\"asElement\":\"\",\"disabled\":false,\"invalid\":false,\"rows\":3,\"prefix\":\"Name :\",\"size\":\"md\",\"suffix\":\"\",\"textArea\":false,\"type\":\"text\",\"unstyle\":false,\"field\":\"\",\"form\":\"\",\"className\":\"\",\"children\":\"\",\"style\":{},\"placeholder\":\"Metin girin...\",\"value\":\"\",\"name\":\"\",\"autoComplete\":\"off\",\"autoFocus\":false,\"readOnly\":false,\"required\":false,\"maxLength\":\"\",\"minLength\":\"\",\"pattern\":\"\",\"sqlDefaultValue\":\"\"},\"events\":{\"onChange\":\"\"},\"bindings\":{\"value\":{\"sourceId\":\"cmp_msyfmegt_jkkn3\",\"path\":\"Name\"}},\"children\":[],\"ref\":\"input1\"},{\"id\":\"cmp_msyjl707_crcn7\",\"type\":\"Input\",\"kind\":\"ui\",\"props\":{\"asElement\":\"\",\"disabled\":false,\"invalid\":false,\"rows\":3,\"prefix\":\"Normalized Name :\",\"size\":\"md\",\"suffix\":\"\",\"textArea\":false,\"type\":\"text\",\"unstyle\":false,\"field\":\"\",\"form\":\"\",\"className\":\"\",\"children\":\"\",\"style\":{},\"placeholder\":\"Metin girin...\",\"value\":\"\",\"name\":\"\",\"autoComplete\":\"off\",\"autoFocus\":false,\"readOnly\":false,\"required\":false,\"maxLength\":\"\",\"minLength\":\"\",\"pattern\":\"\"},\"events\":{\"onChange\":\"\"},\"bindings\":{\"value\":{\"sourceId\":\"cmp_msyfmegt_jkkn3\",\"path\":\"NormalizedName\"}},\"children\":[],\"ref\":\"input2\"},{\"id\":\"cmp_msyjdodn_u3o74\",\"type\":\"Input\",\"kind\":\"ui\",\"props\":{\"asElement\":\"\",\"disabled\":false,\"invalid\":false,\"rows\":3,\"prefix\":\"Concurrent Stamp : \",\"size\":\"md\",\"suffix\":\"\",\"textArea\":false,\"type\":\"text\",\"unstyle\":false,\"field\":\"\",\"form\":\"\",\"className\":\"\",\"children\":\"\",\"style\":{},\"placeholder\":\"Metin girin...\",\"value\":\"\",\"name\":\"\",\"autoComplete\":\"off\",\"autoFocus\":false,\"readOnly\":false,\"required\":false,\"maxLength\":\"\",\"minLength\":\"\",\"pattern\":\"\"},\"events\":{\"onChange\":\"\"},\"bindings\":{\"value\":{\"sourceId\":\"cmp_msyfmegt_jkkn3\",\"path\":\"ConcurrencyStamp\"}},\"children\":[],\"ref\":\"input3\"},{\"id\":\"cmp_msyje9ys_2c08a\",\"type\":\"Checkbox\",\"kind\":\"ui\",\"props\":{\"checked\":false,\"color\":\"blue-500\",\"disabled\":false,\"labelRef\":\"\",\"name\":\"\",\"readOnly\":false,\"value\":\"\",\"field\":null,\"className\":\"\",\"children\":\"Is Default\",\"style\":{},\"sqlDefaultValue\":\"true\"},\"events\":{\"onChange\":\"\"},\"bindings\":{\"checked\":{\"sourceId\":\"cmp_msyfmegt_jkkn3\",\"path\":\"IsDefault\"}},\"children\":[],\"ref\":\"checkbox1\"},{\"id\":\"cmp_msyjebu2_27tqi\",\"type\":\"Checkbox\",\"kind\":\"ui\",\"props\":{\"checked\":false,\"color\":\"blue-500\",\"disabled\":false,\"labelRef\":\"\",\"name\":\"\",\"readOnly\":false,\"value\":\"\",\"field\":null,\"className\":\"\",\"children\":\"Is Static\",\"style\":{},\"sqlDefaultValue\":\"true\"},\"events\":{\"onChange\":\"\"},\"bindings\":{\"checked\":{\"sourceId\":\"cmp_msyfmegt_jkkn3\",\"path\":\"IsStatic\"}},\"children\":[],\"ref\":\"checkbox2\"},{\"id\":\"cmp_msyjmetn_gojol\",\"type\":\"Checkbox\",\"kind\":\"ui\",\"props\":{\"checked\":false,\"color\":\"blue-500\",\"disabled\":false,\"labelRef\":\"\",\"name\":\"\",\"readOnly\":false,\"value\":\"\",\"field\":null,\"className\":\"\",\"children\":\"Is Public\",\"style\":{},\"sqlDefaultValue\":\"false\"},\"events\":{\"onChange\":\"\"},\"bindings\":{\"checked\":{\"sourceId\":\"cmp_msyfmegt_jkkn3\",\"path\":\"IsPublic\"}},\"children\":[],\"ref\":\"checkbox3\"},{\"id\":\"cmp_msyjo0g8_oathk\",\"type\":\"Input\",\"kind\":\"ui\",\"props\":{\"asElement\":\"\",\"disabled\":false,\"invalid\":false,\"rows\":3,\"prefix\":\"Entity Version : \",\"size\":\"md\",\"suffix\":\"\",\"textArea\":false,\"type\":\"number\",\"unstyle\":false,\"field\":\"\",\"form\":\"\",\"className\":\"\",\"children\":\"\",\"style\":{},\"placeholder\":\"Metin girin...\",\"value\":\"\",\"name\":\"\",\"autoComplete\":\"off\",\"autoFocus\":false,\"readOnly\":false,\"required\":false,\"maxLength\":\"\",\"minLength\":\"\",\"pattern\":\"\",\"sqlDefaultValue\":\"3\"},\"events\":{\"onChange\":\"\"},\"bindings\":{\"value\":{\"sourceId\":\"cmp_msyfmegt_jkkn3\",\"path\":\"EntityVersion\"}},\"children\":[],\"ref\":\"input4\"},{\"id\":\"cmp_msyjplfq_py8lz\",\"type\":\"Input\",\"kind\":\"ui\",\"props\":{\"asElement\":\"\",\"disabled\":false,\"invalid\":false,\"rows\":3,\"prefix\":\"Extra Properties :\",\"size\":\"md\",\"suffix\":\"\",\"textArea\":false,\"type\":\"text\",\"unstyle\":false,\"field\":\"\",\"form\":\"\",\"className\":\"\",\"children\":\"\",\"style\":{},\"placeholder\":\"Metin girin...\",\"value\":\"\",\"name\":\"\",\"autoComplete\":\"off\",\"autoFocus\":false,\"readOnly\":false,\"required\":false,\"maxLength\":\"\",\"minLength\":\"\",\"pattern\":\"\",\"sqlDefaultValue\":\"{}\"},\"events\":{\"onChange\":\"\"},\"bindings\":{\"value\":{\"sourceId\":\"cmp_msyfmegt_jkkn3\",\"path\":\"ExtraProperties\"}},\"children\":[],\"ref\":\"input5\"}],\"ref\":\"sqlDataSource1\"}],\"canvas\":{\"width\":\"responsive\"},\"lifecycle\":{\"onMount\":\"\"},\"dataSources\":[{\"id\":\"source_5w_5ibpv\",\"name\":\"AbpRoles · GetList\",\"method\":\"GET\",\"url\":\"/api/app/crudendpoint/AbpRoles\",\"responsePath\":\"\"},{\"id\":\"source_i7_msox4\",\"name\":\"AbpRoles · GetById\",\"method\":\"GET\",\"url\":\"/api/app/crudendpoint/AbpRoles/{id}\",\"responsePath\":\"\"},{\"id\":\"source_0t_avok9\",\"name\":\"AbpRoles · Delete\",\"method\":\"DELETE\",\"url\":\"/api/app/crudendpoint/AbpRoles/{id}\",\"responsePath\":\"\"},{\"id\":\"source_h3_484qa\",\"name\":\"AbpRoles · Create\",\"method\":\"POST\",\"url\":\"/api/app/crudendpoint/AbpRoles\",\"responsePath\":\"\"},{\"id\":\"source_gb_d9cvs\",\"name\":\"AbpRoles · Update\",\"method\":\"PUT\",\"url\":\"/api/app/crudendpoint/AbpRoles/{id}\",\"responsePath\":\"\"}]}}", + "Code": "/*__SOZSOFT_VISUAL_DESIGNER__%7B%22version%22%3A1%2C%22sourceMode%22%3A%22visual%22%2C%22nodes%22%3A%5B%7B%22id%22%3A%22cmp_msyfmegt_jkkn3%22%2C%22type%22%3A%22Form%22%2C%22kind%22%3A%22layout%22%2C%22props%22%3A%7B%22selectEndpoint%22%3A%22source_5w_5ibpv%22%2C%22insertEndpoint%22%3A%22source_h3_484qa%22%2C%22updateEndpoint%22%3A%22source_gb_d9cvs%22%2C%22deleteEndpoint%22%3A%22source_0t_avok9%22%2C%22keyFieldName%22%3A%22id%22%2C%22collectionPath%22%3A%22%22%2C%22keySource%22%3A%22query%22%2C%22keyParamName%22%3A%22id%22%2C%22previewKeyValue%22%3A%22%22%2C%22autoLoad%22%3Atrue%2C%22showToolbar%22%3Atrue%2C%22gap%22%3A16%2C%22className%22%3A%22%22%2C%22title%22%3A%22Rol%20Tan%C4%B1mlama%22%7D%2C%22events%22%3A%7B%22onLoad%22%3A%22%22%2C%22onRecordChange%22%3A%22%22%2C%22onFieldChange%22%3A%22%22%2C%22onNewRecord%22%3A%22%22%2C%22onModeChange%22%3A%22%22%2C%22onBeforeSave%22%3A%22%22%2C%22onAfterSave%22%3A%22%22%2C%22onBeforeDelete%22%3A%22%22%2C%22onAfterDelete%22%3A%22%22%2C%22onError%22%3A%22%22%7D%2C%22bindings%22%3A%7B%7D%2C%22children%22%3A%5B%7B%22id%22%3A%22cmp_msyfpfia_ifhdq%22%2C%22type%22%3A%22Input%22%2C%22kind%22%3A%22ui%22%2C%22props%22%3A%7B%22asElement%22%3A%22%22%2C%22disabled%22%3Afalse%2C%22invalid%22%3Afalse%2C%22rows%22%3A3%2C%22prefix%22%3A%22Name%20%3A%22%2C%22size%22%3A%22md%22%2C%22suffix%22%3A%22%22%2C%22textArea%22%3Afalse%2C%22type%22%3A%22text%22%2C%22unstyle%22%3Afalse%2C%22field%22%3A%22%22%2C%22form%22%3A%22%22%2C%22className%22%3A%22%22%2C%22children%22%3A%22%22%2C%22style%22%3A%7B%7D%2C%22placeholder%22%3A%22Metin%20girin...%22%2C%22value%22%3A%22%22%2C%22name%22%3A%22%22%2C%22autoComplete%22%3A%22off%22%2C%22autoFocus%22%3Afalse%2C%22readOnly%22%3Afalse%2C%22required%22%3Afalse%2C%22maxLength%22%3A%22%22%2C%22minLength%22%3A%22%22%2C%22pattern%22%3A%22%22%2C%22sqlDefaultValue%22%3A%22%22%7D%2C%22events%22%3A%7B%22onChange%22%3A%22%22%7D%2C%22bindings%22%3A%7B%22value%22%3A%7B%22sourceId%22%3A%22cmp_msyfmegt_jkkn3%22%2C%22path%22%3A%22Name%22%7D%7D%2C%22children%22%3A%5B%5D%2C%22ref%22%3A%22input1%22%7D%2C%7B%22id%22%3A%22cmp_msyjl707_crcn7%22%2C%22type%22%3A%22Input%22%2C%22kind%22%3A%22ui%22%2C%22props%22%3A%7B%22asElement%22%3A%22%22%2C%22disabled%22%3Afalse%2C%22invalid%22%3Afalse%2C%22rows%22%3A3%2C%22prefix%22%3A%22Normalized%20Name%20%3A%22%2C%22size%22%3A%22md%22%2C%22suffix%22%3A%22%22%2C%22textArea%22%3Afalse%2C%22type%22%3A%22text%22%2C%22unstyle%22%3Afalse%2C%22field%22%3A%22%22%2C%22form%22%3A%22%22%2C%22className%22%3A%22%22%2C%22children%22%3A%22%22%2C%22style%22%3A%7B%7D%2C%22placeholder%22%3A%22Metin%20girin...%22%2C%22value%22%3A%22%22%2C%22name%22%3A%22%22%2C%22autoComplete%22%3A%22off%22%2C%22autoFocus%22%3Afalse%2C%22readOnly%22%3Afalse%2C%22required%22%3Afalse%2C%22maxLength%22%3A%22%22%2C%22minLength%22%3A%22%22%2C%22pattern%22%3A%22%22%7D%2C%22events%22%3A%7B%22onChange%22%3A%22%22%7D%2C%22bindings%22%3A%7B%22value%22%3A%7B%22sourceId%22%3A%22cmp_msyfmegt_jkkn3%22%2C%22path%22%3A%22NormalizedName%22%7D%7D%2C%22children%22%3A%5B%5D%2C%22ref%22%3A%22input2%22%7D%2C%7B%22id%22%3A%22cmp_msyjdodn_u3o74%22%2C%22type%22%3A%22Input%22%2C%22kind%22%3A%22ui%22%2C%22props%22%3A%7B%22asElement%22%3A%22%22%2C%22disabled%22%3Afalse%2C%22invalid%22%3Afalse%2C%22rows%22%3A3%2C%22prefix%22%3A%22Concurrent%20Stamp%20%3A%20%22%2C%22size%22%3A%22md%22%2C%22suffix%22%3A%22%22%2C%22textArea%22%3Afalse%2C%22type%22%3A%22text%22%2C%22unstyle%22%3Afalse%2C%22field%22%3A%22%22%2C%22form%22%3A%22%22%2C%22className%22%3A%22%22%2C%22children%22%3A%22%22%2C%22style%22%3A%7B%7D%2C%22placeholder%22%3A%22Metin%20girin...%22%2C%22value%22%3A%22%22%2C%22name%22%3A%22%22%2C%22autoComplete%22%3A%22off%22%2C%22autoFocus%22%3Afalse%2C%22readOnly%22%3Afalse%2C%22required%22%3Afalse%2C%22maxLength%22%3A%22%22%2C%22minLength%22%3A%22%22%2C%22pattern%22%3A%22%22%7D%2C%22events%22%3A%7B%22onChange%22%3A%22%22%7D%2C%22bindings%22%3A%7B%22value%22%3A%7B%22sourceId%22%3A%22cmp_msyfmegt_jkkn3%22%2C%22path%22%3A%22ConcurrencyStamp%22%7D%7D%2C%22children%22%3A%5B%5D%2C%22ref%22%3A%22input3%22%7D%2C%7B%22id%22%3A%22cmp_msyje9ys_2c08a%22%2C%22type%22%3A%22Checkbox%22%2C%22kind%22%3A%22ui%22%2C%22props%22%3A%7B%22checked%22%3Afalse%2C%22color%22%3A%22blue-500%22%2C%22disabled%22%3Afalse%2C%22labelRef%22%3A%22%22%2C%22name%22%3A%22%22%2C%22readOnly%22%3Afalse%2C%22value%22%3A%22%22%2C%22field%22%3Anull%2C%22className%22%3A%22%22%2C%22children%22%3A%22Is%20Default%22%2C%22style%22%3A%7B%7D%2C%22sqlDefaultValue%22%3A%22true%22%7D%2C%22events%22%3A%7B%22onChange%22%3A%22%22%7D%2C%22bindings%22%3A%7B%22checked%22%3A%7B%22sourceId%22%3A%22cmp_msyfmegt_jkkn3%22%2C%22path%22%3A%22IsDefault%22%7D%7D%2C%22children%22%3A%5B%5D%2C%22ref%22%3A%22checkbox1%22%7D%2C%7B%22id%22%3A%22cmp_msyjebu2_27tqi%22%2C%22type%22%3A%22Checkbox%22%2C%22kind%22%3A%22ui%22%2C%22props%22%3A%7B%22checked%22%3Afalse%2C%22color%22%3A%22blue-500%22%2C%22disabled%22%3Afalse%2C%22labelRef%22%3A%22%22%2C%22name%22%3A%22%22%2C%22readOnly%22%3Afalse%2C%22value%22%3A%22%22%2C%22field%22%3Anull%2C%22className%22%3A%22%22%2C%22children%22%3A%22Is%20Static%22%2C%22style%22%3A%7B%7D%2C%22sqlDefaultValue%22%3A%22true%22%7D%2C%22events%22%3A%7B%22onChange%22%3A%22%22%7D%2C%22bindings%22%3A%7B%22checked%22%3A%7B%22sourceId%22%3A%22cmp_msyfmegt_jkkn3%22%2C%22path%22%3A%22IsStatic%22%7D%7D%2C%22children%22%3A%5B%5D%2C%22ref%22%3A%22checkbox2%22%7D%2C%7B%22id%22%3A%22cmp_msyjmetn_gojol%22%2C%22type%22%3A%22Checkbox%22%2C%22kind%22%3A%22ui%22%2C%22props%22%3A%7B%22checked%22%3Afalse%2C%22color%22%3A%22blue-500%22%2C%22disabled%22%3Afalse%2C%22labelRef%22%3A%22%22%2C%22name%22%3A%22%22%2C%22readOnly%22%3Afalse%2C%22value%22%3A%22%22%2C%22field%22%3Anull%2C%22className%22%3A%22%22%2C%22children%22%3A%22Is%20Public%22%2C%22style%22%3A%7B%7D%2C%22sqlDefaultValue%22%3A%22false%22%7D%2C%22events%22%3A%7B%22onChange%22%3A%22%22%7D%2C%22bindings%22%3A%7B%22checked%22%3A%7B%22sourceId%22%3A%22cmp_msyfmegt_jkkn3%22%2C%22path%22%3A%22IsPublic%22%7D%7D%2C%22children%22%3A%5B%5D%2C%22ref%22%3A%22checkbox3%22%7D%2C%7B%22id%22%3A%22cmp_msyjo0g8_oathk%22%2C%22type%22%3A%22Input%22%2C%22kind%22%3A%22ui%22%2C%22props%22%3A%7B%22asElement%22%3A%22%22%2C%22disabled%22%3Afalse%2C%22invalid%22%3Afalse%2C%22rows%22%3A3%2C%22prefix%22%3A%22Entity%20Version%20%3A%20%22%2C%22size%22%3A%22md%22%2C%22suffix%22%3A%22%22%2C%22textArea%22%3Afalse%2C%22type%22%3A%22number%22%2C%22unstyle%22%3Afalse%2C%22field%22%3A%22%22%2C%22form%22%3A%22%22%2C%22className%22%3A%22%22%2C%22children%22%3A%22%22%2C%22style%22%3A%7B%7D%2C%22placeholder%22%3A%22Metin%20girin...%22%2C%22value%22%3A%22%22%2C%22name%22%3A%22%22%2C%22autoComplete%22%3A%22off%22%2C%22autoFocus%22%3Afalse%2C%22readOnly%22%3Afalse%2C%22required%22%3Afalse%2C%22maxLength%22%3A%22%22%2C%22minLength%22%3A%22%22%2C%22pattern%22%3A%22%22%2C%22sqlDefaultValue%22%3A%223%22%7D%2C%22events%22%3A%7B%22onChange%22%3A%22%22%7D%2C%22bindings%22%3A%7B%22value%22%3A%7B%22sourceId%22%3A%22cmp_msyfmegt_jkkn3%22%2C%22path%22%3A%22EntityVersion%22%7D%7D%2C%22children%22%3A%5B%5D%2C%22ref%22%3A%22input4%22%7D%2C%7B%22id%22%3A%22cmp_msyjplfq_py8lz%22%2C%22type%22%3A%22Input%22%2C%22kind%22%3A%22ui%22%2C%22props%22%3A%7B%22asElement%22%3A%22%22%2C%22disabled%22%3Afalse%2C%22invalid%22%3Afalse%2C%22rows%22%3A3%2C%22prefix%22%3A%22Extra%20Properties%20%3A%22%2C%22size%22%3A%22md%22%2C%22suffix%22%3A%22%22%2C%22textArea%22%3Afalse%2C%22type%22%3A%22text%22%2C%22unstyle%22%3Afalse%2C%22field%22%3A%22%22%2C%22form%22%3A%22%22%2C%22className%22%3A%22%22%2C%22children%22%3A%22%22%2C%22style%22%3A%7B%7D%2C%22placeholder%22%3A%22Metin%20girin...%22%2C%22value%22%3A%22%22%2C%22name%22%3A%22%22%2C%22autoComplete%22%3A%22off%22%2C%22autoFocus%22%3Afalse%2C%22readOnly%22%3Afalse%2C%22required%22%3Afalse%2C%22maxLength%22%3A%22%22%2C%22minLength%22%3A%22%22%2C%22pattern%22%3A%22%22%2C%22sqlDefaultValue%22%3A%22%7B%7D%22%7D%2C%22events%22%3A%7B%22onChange%22%3A%22%22%7D%2C%22bindings%22%3A%7B%22value%22%3A%7B%22sourceId%22%3A%22cmp_msyfmegt_jkkn3%22%2C%22path%22%3A%22ExtraProperties%22%7D%7D%2C%22children%22%3A%5B%5D%2C%22ref%22%3A%22input5%22%7D%5D%2C%22ref%22%3A%22form1%22%7D%2C%7B%22id%22%3A%22cmp_mszv8oos_flc9k%22%2C%22type%22%3A%22Grid%22%2C%22kind%22%3A%22ui%22%2C%22props%22%3A%7B%22hoverable%22%3Atrue%2C%22overflow%22%3Atrue%2C%22borderlessRow%22%3Afalse%2C%22compact%22%3Afalse%2C%22cols%22%3A%223%22%2C%22rows%22%3A%222%22%2C%22gap%22%3A%224%22%2C%22colGap%22%3A%224%22%2C%22rowGap%22%3A%224%22%2C%22responsive%22%3Atrue%2C%22smCols%22%3A%221%22%2C%22mdCols%22%3A%222%22%2C%22lgCols%22%3A%223%22%2C%22xlCols%22%3A%224%22%2C%22autoFit%22%3Afalse%2C%22minColWidth%22%3A%22200px%22%2C%22height%22%3A%22auto%22%2C%22className%22%3A%22mt-4%22%2C%22items%22%3A%5B%7B%22id%22%3A%22grid-item-1%22%2C%22content%22%3A%22Grid%20Item%201%22%2C%22colSpan%22%3A1%2C%22rowSpan%22%3A1%2C%22colStart%22%3A1%2C%22rowStart%22%3A1%2C%22className%22%3A%22bg-blue-100%20p-4%20border%20rounded%22%7D%2C%7B%22id%22%3A%22grid-item-2%22%2C%22content%22%3A%22Grid%20Item%202%22%2C%22colSpan%22%3A1%2C%22rowSpan%22%3A1%2C%22colStart%22%3A2%2C%22rowStart%22%3A1%2C%22className%22%3A%22bg-green-100%20p-4%20border%20rounded%22%7D%2C%7B%22id%22%3A%22grid-item-3%22%2C%22content%22%3A%22Grid%20Item%203%22%2C%22colSpan%22%3A1%2C%22rowSpan%22%3A1%2C%22colStart%22%3A3%2C%22rowStart%22%3A1%2C%22className%22%3A%22bg-yellow-100%20p-4%20border%20rounded%22%7D%2C%7B%22id%22%3A%22grid-item-4%22%2C%22content%22%3A%22Grid%20Item%204%22%2C%22colSpan%22%3A2%2C%22rowSpan%22%3A1%2C%22colStart%22%3A1%2C%22rowStart%22%3A2%2C%22className%22%3A%22bg-purple-100%20p-4%20border%20rounded%22%7D%2C%7B%22id%22%3A%22grid-item-5%22%2C%22content%22%3A%22Grid%20Item%205%22%2C%22colSpan%22%3A1%2C%22rowSpan%22%3A1%2C%22colStart%22%3A3%2C%22rowStart%22%3A2%2C%22className%22%3A%22bg-red-100%20p-4%20border%20rounded%22%7D%5D%2C%22dataColumns%22%3A%5B%22UserId%22%2C%22RoleId%22%2C%22TenantId%22%5D%7D%2C%22events%22%3A%7B%7D%2C%22bindings%22%3A%7B%22items%22%3A%7B%22sourceId%22%3A%22source_ai_ikts7%22%2C%22path%22%3A%22%22%7D%7D%2C%22children%22%3A%5B%5D%2C%22ref%22%3A%22grid1%22%7D%2C%7B%22id%22%3A%22cmp_mt01eo16_g44d1%22%2C%22type%22%3A%22DataGridView%22%2C%22kind%22%3A%22platform%22%2C%22props%22%3A%7B%22listFormCode%22%3A%22AbpIdentity.Roles%22%2C%22height%22%3A%22520px%22%2C%22filters%22%3A%5B%7B%22id%22%3A%22filter_pa_8bktt%22%2C%22field%22%3A%22Id%22%2C%22operator%22%3A%22eq%22%2C%22source%22%3A%22record%22%2C%22value%22%3A%22form1.id%22%2C%22previewValue%22%3A%22%22%2C%22required%22%3Atrue%7D%5D%2C%22className%22%3A%22mt-4%22%7D%2C%22events%22%3A%7B%7D%2C%22bindings%22%3A%7B%7D%2C%22children%22%3A%5B%5D%2C%22ref%22%3A%22dataGridView1%22%7D%5D%2C%22canvas%22%3A%7B%22width%22%3A%22responsive%22%7D%2C%22lifecycle%22%3A%7B%22onMount%22%3A%22%22%7D%2C%22dataSources%22%3A%5B%7B%22id%22%3A%22source_5w_5ibpv%22%2C%22name%22%3A%22AbpRoles%20%C2%B7%20GetList%22%2C%22method%22%3A%22GET%22%2C%22url%22%3A%22%2Fapi%2Fapp%2Fcrudendpoint%2FAbpRoles%22%2C%22responsePath%22%3A%22%22%7D%2C%7B%22id%22%3A%22source_i7_msox4%22%2C%22name%22%3A%22AbpRoles%20%C2%B7%20GetById%22%2C%22method%22%3A%22GET%22%2C%22url%22%3A%22%2Fapi%2Fapp%2Fcrudendpoint%2FAbpRoles%2F%7Bid%7D%22%2C%22responsePath%22%3A%22%22%2C%22filters%22%3A%5B%7B%22id%22%3A%22filter_gh_2edo0%22%2C%22field%22%3A%22%22%2C%22operator%22%3A%22eq%22%2C%22source%22%3A%22static%22%2C%22value%22%3A%22%22%2C%22previewValue%22%3A%22%22%2C%22required%22%3Afalse%7D%5D%7D%2C%7B%22id%22%3A%22source_0t_avok9%22%2C%22name%22%3A%22AbpRoles%20%C2%B7%20Delete%22%2C%22method%22%3A%22DELETE%22%2C%22url%22%3A%22%2Fapi%2Fapp%2Fcrudendpoint%2FAbpRoles%2F%7Bid%7D%22%2C%22responsePath%22%3A%22%22%7D%2C%7B%22id%22%3A%22source_h3_484qa%22%2C%22name%22%3A%22AbpRoles%20%C2%B7%20Create%22%2C%22method%22%3A%22POST%22%2C%22url%22%3A%22%2Fapi%2Fapp%2Fcrudendpoint%2FAbpRoles%22%2C%22responsePath%22%3A%22%22%7D%2C%7B%22id%22%3A%22source_gb_d9cvs%22%2C%22name%22%3A%22AbpRoles%20%C2%B7%20Update%22%2C%22method%22%3A%22PUT%22%2C%22url%22%3A%22%2Fapi%2Fapp%2Fcrudendpoint%2FAbpRoles%2F%7Bid%7D%22%2C%22responsePath%22%3A%22%22%7D%2C%7B%22id%22%3A%22source_ai_ikts7%22%2C%22name%22%3A%22AbpUserRoles%20%C2%B7%20GetList%22%2C%22method%22%3A%22GET%22%2C%22url%22%3A%22%2Fapi%2Fapp%2Fcrudendpoint%2FAbpUserRoles%22%2C%22responsePath%22%3A%22%22%2C%22filters%22%3A%5B%7B%22id%22%3A%22filter_es_6evg5%22%2C%22field%22%3A%22RoleId%22%2C%22operator%22%3A%22eq%22%2C%22source%22%3A%22record%22%2C%22value%22%3A%22form1.id%22%2C%22previewValue%22%3A%22%22%2C%22required%22%3Atrue%7D%5D%7D%5D%7D__*/\nconst RoleComponent = () => {\n const getByPath = (value, path) => {\n if (!path) return value\n const readPath = (target, targetPath) => targetPath.split('.').filter(Boolean).reduce((current, key) => current?.[key], target)\n const directValue = readPath(value, path)\n if (directValue !== undefined) return directValue\n if (path.startsWith('result.')) return readPath(value, path.slice('result.'.length))\n return value?.result === undefined ? undefined : readPath(value.result, path)\n }\n\n const localeCulture = () => (typeof document !== \"undefined\" && document.documentElement.lang) || \"en\"\n const localeDateOptions = { year: \"numeric\", month: \"2-digit\", day: \"2-digit\" }\n const localeTimeOptions = { year: \"numeric\", month: \"2-digit\", day: \"2-digit\", hour: \"2-digit\", minute: \"2-digit\" }\n const localeIsoPattern = /^(\\d{4})-(\\d{2})-(\\d{2})(?:[T ](\\d{2}):(\\d{2})(?::(\\d{2})(?:\\.\\d+)?)?(Z|[+-]\\d{2}:?\\d{2})?)?$/\n const formatLocaleValue = (value) => {\n if (value === null || value === undefined) return \"\"\n const culture = localeCulture()\n if (typeof value === \"number\") {\n if (!Number.isFinite(value)) return String(value)\n return new Intl.NumberFormat(culture, { useGrouping: false, maximumFractionDigits: 20 }).format(value)\n }\n if (value instanceof Date) {\n return Number.isNaN(value.getTime()) ? \"\" : new Intl.DateTimeFormat(culture, localeTimeOptions).format(value)\n }\n if (typeof value !== \"string\") return String(value)\n const parts = localeIsoPattern.exec(value.trim())\n if (!parts) return value\n const parsed = new Date(value)\n if (Number.isNaN(parsed.getTime())) return value\n if (parts[4] === undefined) {\n const day = new Date(Number(parts[1]), Number(parts[2]) - 1, Number(parts[3]))\n return new Intl.DateTimeFormat(culture, localeDateOptions).format(day)\n }\n return new Intl.DateTimeFormat(culture, localeTimeOptions).format(parsed)\n }\n\n const readSqlField = (record, field) => {\n if (!record || typeof record !== \"object\" || Array.isArray(record)) return undefined\n if (field in record) return record[field]\n const matched = Object.keys(record).find((key) => key.toLowerCase() === String(field).toLowerCase())\n return matched === undefined ? undefined : record[matched]\n }\n const toSqlRows = (value, collectionPath) => {\n const source = collectionPath ? getByPath(value, collectionPath) : value\n const rows = Array.isArray(source)\n ? source\n : source && typeof source === \"object\"\n ? (Array.isArray(source.items) ? source.items : [source])\n : []\n return rows.filter((item) => item && typeof item === \"object\" && !Array.isArray(item))\n }\n const setSqlField = (record, path, value) => {\n const keys = String(path || \"\").split(\".\").filter(Boolean)\n if (!keys.length) return record\n const next = { ...(record || {}) }\n let target = next\n for (let index = 0; index < keys.length - 1; index += 1) {\n const child = target[keys[index]]\n target[keys[index]] = child && typeof child === \"object\" && !Array.isArray(child) ? { ...child } : {}\n target = target[keys[index]]\n }\n target[keys[keys.length - 1]] = value\n return next\n }\n const bindSqlUrl = (url, record, keyField) => {\n let keyBound = false\n const boundUrl = String(url).replace(/\\{([^}]+)\\}|(?<=\\/):([A-Za-z_][A-Za-z0-9_]*)/g, (match, braced, colon) => {\n const name = (braced || colon || \"\").trim()\n if (!name) return match\n const value = readSqlField(record, name) ?? readSqlField(record, keyField)\n if (value === undefined || value === null || value === \"\") return match\n if (name.toLowerCase() === String(keyField).toLowerCase()) keyBound = true\n return encodeURIComponent(String(value))\n })\n return { url: boundUrl, keyBound }\n }\n const appendQueryParam = (url, name, value) =>\n url + (url.includes(\"?\") ? \"&\" : \"?\") + encodeURIComponent(name) + \"=\" + encodeURIComponent(value)\n const hasSqlUrlParams = (url) => /\\{[^}]+\\}|(?<=\\/):[A-Za-z_][A-Za-z0-9_]*/.test(String(url))\n const readUrlKey = (source, name) => {\n if (typeof window === \"undefined\") return \"\"\n if (source === \"query\") return new URLSearchParams(window.location.search).get(name) ?? \"\"\n const segments = window.location.pathname.split(\"/\").filter(Boolean)\n return segments.length ? decodeURIComponent(segments[segments.length - 1]) : \"\"\n }\n const filterSqlRowsByKey = (rows, keyField, keyValue) => {\n if (!keyValue) return rows\n const matched = rows.filter((row) => String(readSqlField(row, keyField) ?? \"\") === String(keyValue))\n // No match means the endpoint already filtered server side, or the key column\n // is named differently — keeping the rows beats showing an empty form.\n return matched.length ? matched : rows\n }\n // Update payload: only the columns the user actually edited, plus the key.\n const toSqlChanges = (original, current, keyField) => {\n const changes = {}\n const isSame = (left, right) => JSON.stringify(left ?? null) === JSON.stringify(right ?? null)\n Object.keys(current || {}).forEach((column) => {\n if (!isSame(original ? original[column] : undefined, current[column])) changes[column] = current[column]\n })\n const keyColumn = Object.keys(current || {}).find((column) => column.toLowerCase() === String(keyField).toLowerCase())\n if (keyColumn !== undefined && changes[keyColumn] === undefined) changes[keyColumn] = current[keyColumn]\n return changes\n }\n const callSqlEndpoint = (target, record, keyField, payload) => {\n // The URL is always bound from the full record: a placeholder may reference a\n // column that the update payload does not carry.\n const bound = bindSqlUrl(target.url, record, keyField)\n const request = { url: bound.url, method: target.method }\n if (target.method === \"POST\" || target.method === \"PUT\") request.data = payload === undefined ? record : payload\n const keyValue = readSqlField(record, keyField)\n const hasKey = keyValue !== undefined && keyValue !== null && keyValue !== \"\"\n // The key still has to reach the endpoint when the URL carries no placeholder.\n if (!bound.keyBound && hasKey && target.method !== \"POST\") request.params = { [keyField]: keyValue }\n return apiService.fetchData(request)\n }\n // A new record starts at the top of the form: the first enabled editor takes the\n // cursor, so a Yeni click is immediately followed by typing.\n const focusFirstSqlField = (host) => {\n if (typeof window === \"undefined\") return\n window.requestAnimationFrame(() => {\n const field = host.current?.querySelector(\"input:not([type='hidden']):not([disabled]):not([readonly]), textarea:not([disabled]):not([readonly]), select:not([disabled])\")\n if (!field) return\n field.focus()\n if (typeof field.select === \"function\") field.select()\n })\n }\n // Defaults declared on the bound children; only a new record starts from them.\n const buildSqlDefaults = (fields) => fields.reduce((record, field) => setSqlField(record, field.path, resolveSqlDefault(field)), {})\n const resolveSqlDefault = (field) => {\n if (field.token !== \"today\" && field.token !== \"now\") return field.value\n const now = new Date()\n const pad = (value) => String(value).padStart(2, \"0\")\n const day = now.getFullYear() + \"-\" + pad(now.getMonth() + 1) + \"-\" + pad(now.getDate())\n if (field.token === \"today\" || field.dateOnly) return day\n return day + \"T\" + pad(now.getHours()) + \":\" + pad(now.getMinutes()) + \":\" + pad(now.getSeconds())\n }\n const toSqlErrorMessage = (error) => error?.response?.data?.error?.message || error?.response?.data?.message || error?.message || \"İşlem tamamlanamadı.\"\n\n // Value of a filter that reads the page URL: a query string parameter, or a\n // path segment — the one after a segment named like the parameter, otherwise\n // the last one, which is the /orders/{id} detail page shape.\n const readDesignerUrlValue = (kind, name) => {\n if (typeof window === \"undefined\") return \"\"\n if (kind === \"query\") return new URLSearchParams(window.location.search).get(name) ?? \"\"\n const segments = window.location.pathname.split(\"/\").filter(Boolean)\n if (!segments.length) return \"\"\n const index = name ? segments.findIndex((segment) => segment.toLowerCase() === String(name).toLowerCase()) : -1\n if (index >= 0 && index < segments.length - 1) return decodeURIComponent(segments[index + 1])\n return decodeURIComponent(segments[segments.length - 1])\n }\n // Filters are appended as query parameters: `Column` for an equality and\n // `Column.operator` for anything else, which is what the endpoint parses.\n // Returns \"\" when a required filter has no value yet — the caller must skip the\n // request instead of loading the unfiltered collection.\n const buildFilteredUrl = (url, filters) => {\n const parts = []\n for (const filter of filters || []) {\n const value = filter.value === undefined || filter.value === null ? \"\" : String(filter.value)\n if (!value) {\n if (filter.required) return \"\"\n continue\n }\n parts.push(encodeURIComponent(filter.param) + \"=\" + encodeURIComponent(value))\n }\n if (!parts.length) return url\n return url + (url.includes(\"?\") ? \"&\" : \"?\") + parts.join(\"&\")\n }\n\n const buildPlatformFilterParams = (filters) => {\n const expressions = []\n for (const filter of filters || []) {\n if (filter.kind === \"isnull\" || filter.kind === \"notnull\") {\n expressions.push([filter.field, filter.operator, null])\n continue\n }\n const value = filter.value === undefined || filter.value === null ? \"\" : String(filter.value)\n if (!value) {\n if (filter.required) return null\n continue\n }\n if (filter.kind === \"in\") {\n const entries = value.split(\",\").map((entry) => entry.trim()).filter(Boolean).map((entry) => [filter.field, \"=\", entry])\n if (!entries.length) continue\n expressions.push(entries.reduce((result, entry) => (result ? [result, \"or\", entry] : entry), null))\n continue\n }\n expressions.push([filter.field, filter.operator, value])\n }\n const params = new URLSearchParams()\n const merged = expressions.reduce((result, expression) => (result ? [result, \"and\", expression] : expression), null)\n if (merged) params.set(\"filter\", JSON.stringify(merged))\n return params\n }\n\n const [data_source_5w_5ibpv, setData_source_5w_5ibpv] = React.useState(null)\n const [data_source_i7_msox4, setData_source_i7_msox4] = React.useState(null)\n const [data_source_0t_avok9] = React.useState(null)\n const [data_source_h3_484qa] = React.useState(null)\n const [data_source_gb_d9cvs] = React.useState(null)\n const [data_source_ai_ikts7, setData_source_ai_ikts7] = React.useState(null)\n\n const sqlselect_cmp_msyfmegt_jkkn3 = { url: \"/api/app/crudendpoint/AbpRoles\", method: \"GET\", responsePath: \"\" }\n const sqlinsert_cmp_msyfmegt_jkkn3 = { url: \"/api/app/crudendpoint/AbpRoles\", method: \"POST\", responsePath: \"\" }\n const sqlupdate_cmp_msyfmegt_jkkn3 = { url: \"/api/app/crudendpoint/AbpRoles/{id}\", method: \"PUT\", responsePath: \"\" }\n const sqldelete_cmp_msyfmegt_jkkn3 = { url: \"/api/app/crudendpoint/AbpRoles/{id}\", method: \"DELETE\", responsePath: \"\" }\n const sqlKey_cmp_msyfmegt_jkkn3 = \"id\"\n const sqlKeyParam_cmp_msyfmegt_jkkn3 = \"id\"\n const [data_cmp_msyfmegt_jkkn3, setData_cmp_msyfmegt_jkkn3] = React.useState({})\n const [sqlOriginal_cmp_msyfmegt_jkkn3, setSqlOriginal_cmp_msyfmegt_jkkn3] = React.useState({})\n const [sqlMode_cmp_msyfmegt_jkkn3, setSqlMode_cmp_msyfmegt_jkkn3] = React.useState(\"edit\")\n const [sqlIndex_cmp_msyfmegt_jkkn3, setSqlIndex_cmp_msyfmegt_jkkn3] = React.useState(0)\n const [sqlBusy_cmp_msyfmegt_jkkn3, setSqlBusy_cmp_msyfmegt_jkkn3] = React.useState(false)\n const [sqlError_cmp_msyfmegt_jkkn3, setSqlError_cmp_msyfmegt_jkkn3] = React.useState(\"\")\n // Container element, so New can hand the cursor to the first field of the form.\n const sqlHost_cmp_msyfmegt_jkkn3 = React.useRef(null)\n // The Select key can come from the page URL, which is how a detail page reads\n // /api/app/orders/{id} or /api/app/orders?id=… for a single record.\n const sqlUrlKey_cmp_msyfmegt_jkkn3 = readUrlKey(\"query\", sqlKeyParam_cmp_msyfmegt_jkkn3)\n const sqlSelectUrl_cmp_msyfmegt_jkkn3 = React.useMemo(() => {\n if (!sqlselect_cmp_msyfmegt_jkkn3) return \"\"\n const base = sqlUrlKey_cmp_msyfmegt_jkkn3\n ? (() => {\n const bound = bindSqlUrl(sqlselect_cmp_msyfmegt_jkkn3.url, { [sqlKeyParam_cmp_msyfmegt_jkkn3]: sqlUrlKey_cmp_msyfmegt_jkkn3 }, sqlKeyParam_cmp_msyfmegt_jkkn3)\n return bound.keyBound ? bound.url : appendQueryParam(bound.url, sqlKeyParam_cmp_msyfmegt_jkkn3, sqlUrlKey_cmp_msyfmegt_jkkn3)\n })()\n : sqlselect_cmp_msyfmegt_jkkn3.url\n return base\n }, [sqlUrlKey_cmp_msyfmegt_jkkn3])\n // A key in the page URL always narrows the result: a list endpoint ignores the\n // parameter server side, so the requested record is picked out here. Without a\n // key every row is kept and the navigation below takes over.\n const sqlRows_cmp_msyfmegt_jkkn3 = React.useMemo(\n () => filterSqlRowsByKey(toSqlRows(data_source_5w_5ibpv, \"\"), sqlKey_cmp_msyfmegt_jkkn3, sqlUrlKey_cmp_msyfmegt_jkkn3),\n [data_source_5w_5ibpv, sqlUrlKey_cmp_msyfmegt_jkkn3],\n )\n // A fresh result set resets the position; New mode is left untouched so an\n // unsaved draft is not overwritten by a re-render.\n React.useEffect(() => {\n setSqlIndex_cmp_msyfmegt_jkkn3((current) => (current < sqlRows_cmp_msyfmegt_jkkn3.length ? current : 0))\n }, [sqlRows_cmp_msyfmegt_jkkn3])\n React.useEffect(() => {\n const row = sqlRows_cmp_msyfmegt_jkkn3[sqlIndex_cmp_msyfmegt_jkkn3]\n if (!row) return\n setData_cmp_msyfmegt_jkkn3(row)\n setSqlOriginal_cmp_msyfmegt_jkkn3(row)\n setSqlMode_cmp_msyfmegt_jkkn3(\"edit\")\n }, [sqlRows_cmp_msyfmegt_jkkn3, sqlIndex_cmp_msyfmegt_jkkn3])\n const sqlHasKey_cmp_msyfmegt_jkkn3 = (() => {\n const value = readSqlField(data_cmp_msyfmegt_jkkn3, sqlKey_cmp_msyfmegt_jkkn3)\n return value !== undefined && value !== null && value !== \"\"\n })()\n const sqlSetField_cmp_msyfmegt_jkkn3 = React.useCallback((path, value) => setData_cmp_msyfmegt_jkkn3((current) => setSqlField(current, path, value)), [])\n const sqlReload_cmp_msyfmegt_jkkn3 = React.useCallback(async () => {\n if (!sqlSelectUrl_cmp_msyfmegt_jkkn3) return\n // Calling a URL that still holds a placeholder is a guaranteed 400, so the\n // missing key is reported in the component instead.\n if (hasSqlUrlParams(sqlSelectUrl_cmp_msyfmegt_jkkn3)) {\n throw new Error(\"Select endpointi \" + sqlKeyParam_cmp_msyfmegt_jkkn3 + \" parametresini bekliyor; sayfa adresinde bulunamadı.\")\n }\n const response = await apiService.fetchData({ url: sqlSelectUrl_cmp_msyfmegt_jkkn3, method: \"GET\" })\n setData_source_5w_5ibpv(getByPath(response.data, sqlselect_cmp_msyfmegt_jkkn3.responsePath))\n }, [sqlSelectUrl_cmp_msyfmegt_jkkn3])\n // Reload wrapped with the busy/error handling the toolbar and mount effect need,\n // so a failing Select is reported in the component instead of the console.\n const sqlRefresh_cmp_msyfmegt_jkkn3 = React.useCallback(async () => {\n setSqlBusy_cmp_msyfmegt_jkkn3(true)\n setSqlError_cmp_msyfmegt_jkkn3(\"\")\n try {\n await sqlReload_cmp_msyfmegt_jkkn3()\n } catch (error) {\n setSqlError_cmp_msyfmegt_jkkn3(toSqlErrorMessage(error))\n } finally {\n setSqlBusy_cmp_msyfmegt_jkkn3(false)\n }\n }, [sqlReload_cmp_msyfmegt_jkkn3])\n const sqlNew_cmp_msyfmegt_jkkn3 = React.useCallback(() => {\n setSqlError_cmp_msyfmegt_jkkn3(\"\")\n // Defaults are resolved on every New, so an @today column carries the day the\n // record was opened rather than the day the component was generated.\n const sqlDraft_cmp_msyfmegt_jkkn3 = buildSqlDefaults([{\"path\":\"IsDefault\",\"value\":true},{\"path\":\"IsStatic\",\"value\":true},{\"path\":\"IsPublic\",\"value\":false},{\"path\":\"EntityVersion\",\"value\":3},{\"path\":\"ExtraProperties\",\"value\":\"{}\"}])\n setData_cmp_msyfmegt_jkkn3(sqlDraft_cmp_msyfmegt_jkkn3)\n setSqlOriginal_cmp_msyfmegt_jkkn3({})\n setSqlMode_cmp_msyfmegt_jkkn3(\"new\")\n // After the re-render that paints the empty form, not before it.\n focusFirstSqlField(sqlHost_cmp_msyfmegt_jkkn3)\n }, [])\n const sqlPrev_cmp_msyfmegt_jkkn3 = React.useCallback(() => setSqlIndex_cmp_msyfmegt_jkkn3((current) => Math.max(0, current - 1)), [])\n const sqlNext_cmp_msyfmegt_jkkn3 = React.useCallback(() => setSqlIndex_cmp_msyfmegt_jkkn3((current) => Math.min(sqlRows_cmp_msyfmegt_jkkn3.length - 1, current + 1)), [sqlRows_cmp_msyfmegt_jkkn3])\n const sqlSave_cmp_msyfmegt_jkkn3 = async () => {\n const isNew = sqlMode_cmp_msyfmegt_jkkn3 === \"new\"\n const target = isNew ? sqlinsert_cmp_msyfmegt_jkkn3 : sqlupdate_cmp_msyfmegt_jkkn3\n if (!target) {\n setSqlError_cmp_msyfmegt_jkkn3(isNew ? \"Insert için POST endpointi tanımlı değil.\" : \"Update için PUT endpointi tanımlı değil.\")\n return\n }\n // Update carries only the edited columns plus the key; Insert sends the record.\n const payload = isNew ? data_cmp_msyfmegt_jkkn3 : toSqlChanges(sqlOriginal_cmp_msyfmegt_jkkn3, data_cmp_msyfmegt_jkkn3, sqlKey_cmp_msyfmegt_jkkn3)\n if (!isNew && Object.keys(payload).filter((column) => column.toLowerCase() !== String(sqlKey_cmp_msyfmegt_jkkn3).toLowerCase()).length === 0) {\n setSqlError_cmp_msyfmegt_jkkn3(\"Kaydedilecek bir değişiklik yok.\")\n return\n }\n setSqlBusy_cmp_msyfmegt_jkkn3(true)\n setSqlError_cmp_msyfmegt_jkkn3(\"\")\n try {\n const response = await callSqlEndpoint(target, data_cmp_msyfmegt_jkkn3, sqlKey_cmp_msyfmegt_jkkn3, payload)\n // With a Select endpoint the reload is the source of truth and its effect\n // repopulates the form; without one, the response row is all there is.\n if (sqlselect_cmp_msyfmegt_jkkn3) {\n await sqlReload_cmp_msyfmegt_jkkn3()\n } else {\n const saved = toSqlRows(response?.data, \"\")[0]\n if (saved) {\n setData_cmp_msyfmegt_jkkn3(saved)\n setSqlOriginal_cmp_msyfmegt_jkkn3(saved)\n }\n }\n setSqlMode_cmp_msyfmegt_jkkn3(\"edit\")\n } catch (error) {\n setSqlError_cmp_msyfmegt_jkkn3(toSqlErrorMessage(error))\n } finally {\n setSqlBusy_cmp_msyfmegt_jkkn3(false)\n }\n }\n const sqlDelete_cmp_msyfmegt_jkkn3 = async () => {\n if (!sqldelete_cmp_msyfmegt_jkkn3 || !sqlHasKey_cmp_msyfmegt_jkkn3 || sqlMode_cmp_msyfmegt_jkkn3 === \"new\") return\n // Deleting cannot be undone from the form, so it always asks first.\n const confirmMessage = sqlKey_cmp_msyfmegt_jkkn3 + \" = \" + readSqlField(data_cmp_msyfmegt_jkkn3, sqlKey_cmp_msyfmegt_jkkn3) + \" kaydı silinecek.\\n\\nOnaylıyor musunuz?\"\n if (typeof window !== \"undefined\" && !window.confirm(confirmMessage)) return\n setSqlBusy_cmp_msyfmegt_jkkn3(true)\n setSqlError_cmp_msyfmegt_jkkn3(\"\")\n try {\n await callSqlEndpoint(sqldelete_cmp_msyfmegt_jkkn3, data_cmp_msyfmegt_jkkn3, sqlKey_cmp_msyfmegt_jkkn3)\n setData_cmp_msyfmegt_jkkn3({})\n setSqlOriginal_cmp_msyfmegt_jkkn3({})\n setSqlMode_cmp_msyfmegt_jkkn3(\"new\")\n await sqlReload_cmp_msyfmegt_jkkn3()\n } catch (error) {\n setSqlError_cmp_msyfmegt_jkkn3(toSqlErrorMessage(error))\n } finally {\n setSqlBusy_cmp_msyfmegt_jkkn3(false)\n }\n }\n React.useEffect(() => { void sqlRefresh_cmp_msyfmegt_jkkn3() }, [sqlRefresh_cmp_msyfmegt_jkkn3])\n\n const filters_source_ai_ikts7 = [{ param: \"RoleId\", value: readSqlField(data_cmp_msyfmegt_jkkn3, \"id\"), required: true }]\n const filterKey_source_ai_ikts7 = JSON.stringify(filters_source_ai_ikts7)\n React.useEffect(() => {\n const url = buildFilteredUrl(\"/api/app/crudendpoint/AbpUserRoles\", filters_source_ai_ikts7)\n // An unmet required filter leaves the component empty on purpose.\n if (!url) {\n setData_source_ai_ikts7(null)\n return\n }\n let active = true\n apiService.fetchData({ url, method: 'GET' }).then((response) => {\n if (active) setData_source_ai_ikts7(getByPath(response.data, \"\"))\n }).catch((error) => console.error(\"AbpUserRoles · GetList endpoint error:\", error))\n return () => { active = false }\n }, [filterKey_source_ai_ikts7])\n\n const platformFilters_cmp_mt01eo16_g44d1 = [{ field: \"Id\", operator: \"=\", kind: \"eq\", value: readSqlField(data_cmp_msyfmegt_jkkn3, \"id\"), required: true }]\n const platformKey_cmp_mt01eo16_g44d1 = JSON.stringify(platformFilters_cmp_mt01eo16_g44d1)\n // The views keep the params object in their own state and write back to it, so\n // it must stay the same object until a filter value actually changes.\n const platformParams_cmp_mt01eo16_g44d1 = React.useMemo(() => buildPlatformFilterParams(platformFilters_cmp_mt01eo16_g44d1), [platformKey_cmp_mt01eo16_g44d1])\n\n const handle_cmp_msyfpfia_ifhdq_onChange = (valueOrEvent, originalEvent) => {\n sqlSetField_cmp_msyfmegt_jkkn3(\"Name\", valueOrEvent?.target?.value ?? valueOrEvent)\n const event = valueOrEvent\n }\n\n const handle_cmp_msyjl707_crcn7_onChange = (valueOrEvent, originalEvent) => {\n sqlSetField_cmp_msyfmegt_jkkn3(\"NormalizedName\", valueOrEvent?.target?.value ?? valueOrEvent)\n const event = valueOrEvent\n }\n\n const handle_cmp_msyjdodn_u3o74_onChange = (valueOrEvent, originalEvent) => {\n sqlSetField_cmp_msyfmegt_jkkn3(\"ConcurrencyStamp\", valueOrEvent?.target?.value ?? valueOrEvent)\n const event = valueOrEvent\n }\n\n const handle_cmp_msyje9ys_2c08a_onChange = (valueOrEvent, originalEvent) => {\n sqlSetField_cmp_msyfmegt_jkkn3(\"IsDefault\", typeof originalEvent === \"object\" && originalEvent?.target ? Boolean(originalEvent.target.checked) : typeof valueOrEvent === \"object\" && valueOrEvent?.target ? Boolean(valueOrEvent.target.checked) : Boolean(valueOrEvent))\n const event = { checked: Boolean(valueOrEvent), originalEvent, target: originalEvent?.target }\n }\n\n const handle_cmp_msyjebu2_27tqi_onChange = (valueOrEvent, originalEvent) => {\n sqlSetField_cmp_msyfmegt_jkkn3(\"IsStatic\", typeof originalEvent === \"object\" && originalEvent?.target ? Boolean(originalEvent.target.checked) : typeof valueOrEvent === \"object\" && valueOrEvent?.target ? Boolean(valueOrEvent.target.checked) : Boolean(valueOrEvent))\n const event = { checked: Boolean(valueOrEvent), originalEvent, target: originalEvent?.target }\n }\n\n const handle_cmp_msyjmetn_gojol_onChange = (valueOrEvent, originalEvent) => {\n sqlSetField_cmp_msyfmegt_jkkn3(\"IsPublic\", typeof originalEvent === \"object\" && originalEvent?.target ? Boolean(originalEvent.target.checked) : typeof valueOrEvent === \"object\" && valueOrEvent?.target ? Boolean(valueOrEvent.target.checked) : Boolean(valueOrEvent))\n const event = { checked: Boolean(valueOrEvent), originalEvent, target: originalEvent?.target }\n }\n\n const handle_cmp_msyjo0g8_oathk_onChange = (valueOrEvent, originalEvent) => {\n sqlSetField_cmp_msyfmegt_jkkn3(\"EntityVersion\", valueOrEvent?.target?.value ?? valueOrEvent)\n const event = valueOrEvent\n }\n\n const handle_cmp_msyjplfq_py8lz_onChange = (valueOrEvent, originalEvent) => {\n sqlSetField_cmp_msyfmegt_jkkn3(\"ExtraProperties\", valueOrEvent?.target?.value ?? valueOrEvent)\n const event = valueOrEvent\n }\n\n return (\n <>\n {sqlHasKey_cmp_msyfmegt_jkkn3 ? sqlKey_cmp_msyfmegt_jkkn3 + \": \" + readSqlField(data_cmp_msyfmegt_jkkn3, sqlKey_cmp_msyfmegt_jkkn3) : sqlMode_cmp_msyfmegt_jkkn3 === \"new\" ? \"Yeni kayıt\" : \"\"}}>\n
\n \n \n \n \n {\"Is Default\"}\n \n \n {\"Is Static\"}\n \n \n {\"Is Public\"}\n \n \n \n
\n {sqlRows_cmp_msyfmegt_jkkn3.length > 1 ? (\n <>\n \n {`${sqlIndex_cmp_msyfmegt_jkkn3 + 1} / ${sqlRows_cmp_msyfmegt_jkkn3.length}`}\n \n \n \n ) : null}\n \n \n \n \n {sqlMode_cmp_msyfmegt_jkkn3 === \"new\" ? \"Yeni kayıt\" : \"Düzenleme\"}\n
\n {sqlError_cmp_msyfmegt_jkkn3 ?
{sqlError_cmp_msyfmegt_jkkn3}
: null}\n
\n
\n
\n {(() => {\n const gridItems_cmp_mszv8oos_flc9k = (Array.isArray(getByPath(data_source_ai_ikts7, \"\")) ? getByPath(data_source_ai_ikts7, \"\") : Array.isArray(data_source_ai_ikts7) ? data_source_ai_ikts7 : null)\n // Nothing loaded yet — an endpoint still in flight, or a required filter\n // holding the request back — is an empty grid, not a binding error.\n if (gridItems_cmp_mszv8oos_flc9k === null || gridItems_cmp_mszv8oos_flc9k === undefined) return null\n if (!Array.isArray(gridItems_cmp_mszv8oos_flc9k)) return
Grid items bağlantısı bir koleksiyon döndürmelidir.
\n const gridColumns_cmp_mszv8oos_flc9k = [\"UserId\",\"RoleId\",\"TenantId\"]\n if (!gridColumns_cmp_mszv8oos_flc9k.length) return
Preview için en az bir sütun seçin.
\n return (\n
\n \n \n {gridColumns_cmp_mszv8oos_flc9k.map((column) => )}\n \n \n {gridItems_cmp_mszv8oos_flc9k.map((gridItem_cmp_mszv8oos_flc9k, rowIndex) => (\n \n {gridColumns_cmp_mszv8oos_flc9k.map((column) => {\n const cellValue_cmp_mszv8oos_flc9k = column === \"value\" ? gridItem_cmp_mszv8oos_flc9k : getByPath(gridItem_cmp_mszv8oos_flc9k, column)\n const text = typeof cellValue_cmp_mszv8oos_flc9k === \"object\" && cellValue_cmp_mszv8oos_flc9k !== null && !(cellValue_cmp_mszv8oos_flc9k instanceof Date) ? JSON.stringify(cellValue_cmp_mszv8oos_flc9k) : formatLocaleValue(cellValue_cmp_mszv8oos_flc9k) || \"—\"\n return \n })}\n \n ))}\n \n
{column === \"value\" ? \"Value\" : column}
{text}
\n
\n )\n })()}\n
\n {platformParams_cmp_mt01eo16_g44d1 === null ? null : (\n \n )}\n \n )\n}\n\nexport default RoleComponent", + "Props": "{\"visualDesigner\":{\"version\":1,\"sourceMode\":\"visual\",\"nodes\":[{\"id\":\"cmp_msyfmegt_jkkn3\",\"type\":\"Form\",\"kind\":\"layout\",\"props\":{\"selectEndpoint\":\"source_5w_5ibpv\",\"insertEndpoint\":\"source_h3_484qa\",\"updateEndpoint\":\"source_gb_d9cvs\",\"deleteEndpoint\":\"source_0t_avok9\",\"keyFieldName\":\"id\",\"collectionPath\":\"\",\"keySource\":\"query\",\"keyParamName\":\"id\",\"previewKeyValue\":\"\",\"autoLoad\":true,\"showToolbar\":true,\"gap\":16,\"className\":\"\",\"title\":\"Rol Tanımlama\"},\"events\":{\"onLoad\":\"\",\"onRecordChange\":\"\",\"onFieldChange\":\"\",\"onNewRecord\":\"\",\"onModeChange\":\"\",\"onBeforeSave\":\"\",\"onAfterSave\":\"\",\"onBeforeDelete\":\"\",\"onAfterDelete\":\"\",\"onError\":\"\"},\"bindings\":{},\"children\":[{\"id\":\"cmp_msyfpfia_ifhdq\",\"type\":\"Input\",\"kind\":\"ui\",\"props\":{\"asElement\":\"\",\"disabled\":false,\"invalid\":false,\"rows\":3,\"prefix\":\"Name :\",\"size\":\"md\",\"suffix\":\"\",\"textArea\":false,\"type\":\"text\",\"unstyle\":false,\"field\":\"\",\"form\":\"\",\"className\":\"\",\"children\":\"\",\"style\":{},\"placeholder\":\"Metin girin...\",\"value\":\"\",\"name\":\"\",\"autoComplete\":\"off\",\"autoFocus\":false,\"readOnly\":false,\"required\":false,\"maxLength\":\"\",\"minLength\":\"\",\"pattern\":\"\",\"sqlDefaultValue\":\"\"},\"events\":{\"onChange\":\"\"},\"bindings\":{\"value\":{\"sourceId\":\"cmp_msyfmegt_jkkn3\",\"path\":\"Name\"}},\"children\":[],\"ref\":\"input1\"},{\"id\":\"cmp_msyjl707_crcn7\",\"type\":\"Input\",\"kind\":\"ui\",\"props\":{\"asElement\":\"\",\"disabled\":false,\"invalid\":false,\"rows\":3,\"prefix\":\"Normalized Name :\",\"size\":\"md\",\"suffix\":\"\",\"textArea\":false,\"type\":\"text\",\"unstyle\":false,\"field\":\"\",\"form\":\"\",\"className\":\"\",\"children\":\"\",\"style\":{},\"placeholder\":\"Metin girin...\",\"value\":\"\",\"name\":\"\",\"autoComplete\":\"off\",\"autoFocus\":false,\"readOnly\":false,\"required\":false,\"maxLength\":\"\",\"minLength\":\"\",\"pattern\":\"\"},\"events\":{\"onChange\":\"\"},\"bindings\":{\"value\":{\"sourceId\":\"cmp_msyfmegt_jkkn3\",\"path\":\"NormalizedName\"}},\"children\":[],\"ref\":\"input2\"},{\"id\":\"cmp_msyjdodn_u3o74\",\"type\":\"Input\",\"kind\":\"ui\",\"props\":{\"asElement\":\"\",\"disabled\":false,\"invalid\":false,\"rows\":3,\"prefix\":\"Concurrent Stamp : \",\"size\":\"md\",\"suffix\":\"\",\"textArea\":false,\"type\":\"text\",\"unstyle\":false,\"field\":\"\",\"form\":\"\",\"className\":\"\",\"children\":\"\",\"style\":{},\"placeholder\":\"Metin girin...\",\"value\":\"\",\"name\":\"\",\"autoComplete\":\"off\",\"autoFocus\":false,\"readOnly\":false,\"required\":false,\"maxLength\":\"\",\"minLength\":\"\",\"pattern\":\"\"},\"events\":{\"onChange\":\"\"},\"bindings\":{\"value\":{\"sourceId\":\"cmp_msyfmegt_jkkn3\",\"path\":\"ConcurrencyStamp\"}},\"children\":[],\"ref\":\"input3\"},{\"id\":\"cmp_msyje9ys_2c08a\",\"type\":\"Checkbox\",\"kind\":\"ui\",\"props\":{\"checked\":false,\"color\":\"blue-500\",\"disabled\":false,\"labelRef\":\"\",\"name\":\"\",\"readOnly\":false,\"value\":\"\",\"field\":null,\"className\":\"\",\"children\":\"Is Default\",\"style\":{},\"sqlDefaultValue\":\"true\"},\"events\":{\"onChange\":\"\"},\"bindings\":{\"checked\":{\"sourceId\":\"cmp_msyfmegt_jkkn3\",\"path\":\"IsDefault\"}},\"children\":[],\"ref\":\"checkbox1\"},{\"id\":\"cmp_msyjebu2_27tqi\",\"type\":\"Checkbox\",\"kind\":\"ui\",\"props\":{\"checked\":false,\"color\":\"blue-500\",\"disabled\":false,\"labelRef\":\"\",\"name\":\"\",\"readOnly\":false,\"value\":\"\",\"field\":null,\"className\":\"\",\"children\":\"Is Static\",\"style\":{},\"sqlDefaultValue\":\"true\"},\"events\":{\"onChange\":\"\"},\"bindings\":{\"checked\":{\"sourceId\":\"cmp_msyfmegt_jkkn3\",\"path\":\"IsStatic\"}},\"children\":[],\"ref\":\"checkbox2\"},{\"id\":\"cmp_msyjmetn_gojol\",\"type\":\"Checkbox\",\"kind\":\"ui\",\"props\":{\"checked\":false,\"color\":\"blue-500\",\"disabled\":false,\"labelRef\":\"\",\"name\":\"\",\"readOnly\":false,\"value\":\"\",\"field\":null,\"className\":\"\",\"children\":\"Is Public\",\"style\":{},\"sqlDefaultValue\":\"false\"},\"events\":{\"onChange\":\"\"},\"bindings\":{\"checked\":{\"sourceId\":\"cmp_msyfmegt_jkkn3\",\"path\":\"IsPublic\"}},\"children\":[],\"ref\":\"checkbox3\"},{\"id\":\"cmp_msyjo0g8_oathk\",\"type\":\"Input\",\"kind\":\"ui\",\"props\":{\"asElement\":\"\",\"disabled\":false,\"invalid\":false,\"rows\":3,\"prefix\":\"Entity Version : \",\"size\":\"md\",\"suffix\":\"\",\"textArea\":false,\"type\":\"number\",\"unstyle\":false,\"field\":\"\",\"form\":\"\",\"className\":\"\",\"children\":\"\",\"style\":{},\"placeholder\":\"Metin girin...\",\"value\":\"\",\"name\":\"\",\"autoComplete\":\"off\",\"autoFocus\":false,\"readOnly\":false,\"required\":false,\"maxLength\":\"\",\"minLength\":\"\",\"pattern\":\"\",\"sqlDefaultValue\":\"3\"},\"events\":{\"onChange\":\"\"},\"bindings\":{\"value\":{\"sourceId\":\"cmp_msyfmegt_jkkn3\",\"path\":\"EntityVersion\"}},\"children\":[],\"ref\":\"input4\"},{\"id\":\"cmp_msyjplfq_py8lz\",\"type\":\"Input\",\"kind\":\"ui\",\"props\":{\"asElement\":\"\",\"disabled\":false,\"invalid\":false,\"rows\":3,\"prefix\":\"Extra Properties :\",\"size\":\"md\",\"suffix\":\"\",\"textArea\":false,\"type\":\"text\",\"unstyle\":false,\"field\":\"\",\"form\":\"\",\"className\":\"\",\"children\":\"\",\"style\":{},\"placeholder\":\"Metin girin...\",\"value\":\"\",\"name\":\"\",\"autoComplete\":\"off\",\"autoFocus\":false,\"readOnly\":false,\"required\":false,\"maxLength\":\"\",\"minLength\":\"\",\"pattern\":\"\",\"sqlDefaultValue\":\"{}\"},\"events\":{\"onChange\":\"\"},\"bindings\":{\"value\":{\"sourceId\":\"cmp_msyfmegt_jkkn3\",\"path\":\"ExtraProperties\"}},\"children\":[],\"ref\":\"input5\"}],\"ref\":\"form1\"},{\"id\":\"cmp_mszv8oos_flc9k\",\"type\":\"Grid\",\"kind\":\"ui\",\"props\":{\"hoverable\":true,\"overflow\":true,\"borderlessRow\":false,\"compact\":false,\"cols\":\"3\",\"rows\":\"2\",\"gap\":\"4\",\"colGap\":\"4\",\"rowGap\":\"4\",\"responsive\":true,\"smCols\":\"1\",\"mdCols\":\"2\",\"lgCols\":\"3\",\"xlCols\":\"4\",\"autoFit\":false,\"minColWidth\":\"200px\",\"height\":\"auto\",\"className\":\"mt-4\",\"items\":[{\"id\":\"grid-item-1\",\"content\":\"Grid Item 1\",\"colSpan\":1,\"rowSpan\":1,\"colStart\":1,\"rowStart\":1,\"className\":\"bg-blue-100 p-4 border rounded\"},{\"id\":\"grid-item-2\",\"content\":\"Grid Item 2\",\"colSpan\":1,\"rowSpan\":1,\"colStart\":2,\"rowStart\":1,\"className\":\"bg-green-100 p-4 border rounded\"},{\"id\":\"grid-item-3\",\"content\":\"Grid Item 3\",\"colSpan\":1,\"rowSpan\":1,\"colStart\":3,\"rowStart\":1,\"className\":\"bg-yellow-100 p-4 border rounded\"},{\"id\":\"grid-item-4\",\"content\":\"Grid Item 4\",\"colSpan\":2,\"rowSpan\":1,\"colStart\":1,\"rowStart\":2,\"className\":\"bg-purple-100 p-4 border rounded\"},{\"id\":\"grid-item-5\",\"content\":\"Grid Item 5\",\"colSpan\":1,\"rowSpan\":1,\"colStart\":3,\"rowStart\":2,\"className\":\"bg-red-100 p-4 border rounded\"}],\"dataColumns\":[\"UserId\",\"RoleId\",\"TenantId\"]},\"events\":{},\"bindings\":{\"items\":{\"sourceId\":\"source_ai_ikts7\",\"path\":\"\"}},\"children\":[],\"ref\":\"grid1\"},{\"id\":\"cmp_mt01eo16_g44d1\",\"type\":\"DataGridView\",\"kind\":\"platform\",\"props\":{\"listFormCode\":\"AbpIdentity.Roles\",\"height\":\"520px\",\"filters\":[{\"id\":\"filter_pa_8bktt\",\"field\":\"Id\",\"operator\":\"eq\",\"source\":\"record\",\"value\":\"form1.id\",\"previewValue\":\"\",\"required\":true}],\"className\":\"mt-4\"},\"events\":{},\"bindings\":{},\"children\":[],\"ref\":\"dataGridView1\"}],\"canvas\":{\"width\":\"responsive\"},\"lifecycle\":{\"onMount\":\"\"},\"dataSources\":[{\"id\":\"source_5w_5ibpv\",\"name\":\"AbpRoles · GetList\",\"method\":\"GET\",\"url\":\"/api/app/crudendpoint/AbpRoles\",\"responsePath\":\"\"},{\"id\":\"source_i7_msox4\",\"name\":\"AbpRoles · GetById\",\"method\":\"GET\",\"url\":\"/api/app/crudendpoint/AbpRoles/{id}\",\"responsePath\":\"\",\"filters\":[{\"id\":\"filter_gh_2edo0\",\"field\":\"\",\"operator\":\"eq\",\"source\":\"static\",\"value\":\"\",\"previewValue\":\"\",\"required\":false}]},{\"id\":\"source_0t_avok9\",\"name\":\"AbpRoles · Delete\",\"method\":\"DELETE\",\"url\":\"/api/app/crudendpoint/AbpRoles/{id}\",\"responsePath\":\"\"},{\"id\":\"source_h3_484qa\",\"name\":\"AbpRoles · Create\",\"method\":\"POST\",\"url\":\"/api/app/crudendpoint/AbpRoles\",\"responsePath\":\"\"},{\"id\":\"source_gb_d9cvs\",\"name\":\"AbpRoles · Update\",\"method\":\"PUT\",\"url\":\"/api/app/crudendpoint/AbpRoles/{id}\",\"responsePath\":\"\"},{\"id\":\"source_ai_ikts7\",\"name\":\"AbpUserRoles · GetList\",\"method\":\"GET\",\"url\":\"/api/app/crudendpoint/AbpUserRoles\",\"responsePath\":\"\",\"filters\":[{\"id\":\"filter_es_6evg5\",\"field\":\"RoleId\",\"operator\":\"eq\",\"source\":\"record\",\"value\":\"form1.id\",\"previewValue\":\"\",\"required\":true}]}]}}", "Description": "", "IsActive": true, "Dependencies": [], @@ -54,6 +54,15 @@ "EntityName": "AbpRoles", "OperationType": "Update", "SeedFile": "crud/AbpRoles.json" + }, + { + "Name": "AbpUserRoles · GetList", + "Method": "GET", + "Url": "/api/app/crudendpoint/AbpUserRoles", + "ResponsePath": null, + "EntityName": "AbpUserRoles", + "OperationType": "GetList", + "SeedFile": "crud/AbpUserRoles.json" } ] } diff --git a/ui/src/components/componentEditor/PlatformViewHost.tsx b/ui/src/components/componentEditor/PlatformViewHost.tsx index 993fb5a9..a9c9da58 100644 --- a/ui/src/components/componentEditor/PlatformViewHost.tsx +++ b/ui/src/components/componentEditor/PlatformViewHost.tsx @@ -28,9 +28,21 @@ interface PlatformViewHostProps { view: PlatformViewName listFormCode: string height?: string + /** + * Filter of the hosted view, carried the same way a standalone list carries it: + * a `filter` entry holding a DevExtreme expression. The designer builds it from + * static values, the page URL or a Form record, so an embedded view is filtered + * without the list form itself being changed. + */ + searchParams?: URLSearchParams } -const PlatformViewHost = ({ view, listFormCode, height = '520px' }: PlatformViewHostProps) => { +const PlatformViewHost = ({ + view, + listFormCode, + height = '520px', + searchParams, +}: PlatformViewHostProps) => { const [gridDto, setGridDto] = useState() const [error, setError] = useState('') @@ -68,15 +80,16 @@ const PlatformViewHost = ({ view, listFormCode, height = '520px' }: PlatformView if (error) return
{error}
if (!gridDto) return - const sharedProps = { listFormCode, isSubForm: true, gridDto } + const sharedProps = { listFormCode, isSubForm: true, gridDto, searchParams } const content = (() => { switch (view) { case 'List': return ( ) diff --git a/ui/src/components/visualDesigner/VisualCanvas.tsx b/ui/src/components/visualDesigner/VisualCanvas.tsx index 07be8da0..d7722515 100644 --- a/ui/src/components/visualDesigner/VisualCanvas.tsx +++ b/ui/src/components/visualDesigner/VisualCanvas.tsx @@ -9,8 +9,10 @@ import { formatLocaleValue } from '@/utils/localeFormat' import { FaArrowDown, FaArrowUp, FaClone, FaGripVertical, FaTrash } from 'react-icons/fa' import { beginDesignerDragSoon, endDesignerDrag, useDesignerDrag } from './designerDrag' import { + buildDesignerFilterParams, buildSqlDefaultRecord, fromDesignerDate, + getDesignerNodeFilters, getDesignerBindingColumns, getDesignerCollectionProperty, getDesignerTabSlot, @@ -22,15 +24,19 @@ import { isDesignerDateProperty, isDesignerOptionComponent, isSqlDataSourceNode, + LEGACY_SQL_DATA_SOURCE_TYPE, normalizeDesignerKeyList, readSqlDataSourceField, resolveDesignerDropdownTitle, + resolveDesignerPreviewFilterValue, resolveDesignerTabValue, resolveSqlDataSourceRows, setDesignerRecordField, + SQL_DATA_SOURCE_TYPE, SQL_DEFAULT_VALUE_PROP, toDesignerDate, type DesignerBinding, + type DesignerDataSourceFilter, type DesignerNode, type SqlDataSourceMode, } from './types' @@ -886,6 +892,14 @@ const SqlDataSourceView = ({ // Save follows the explicit mode, exactly like the generated runtime does. const canSave = mode === 'new' ? canInsert : canUpdate const designTimeTitle = translate('::App.VisualDesignerCanvas.DesignTimeNoCall') + // A filled Title turns the container into a Card; the header extra carries the + // key of the record on screen, which is the only identity the user can check. + const cardTitle = String( + resolveStaticLanguageKeys(node.props.title ?? '', translate) ?? '', + ).trim() + const headerExtra = hasKey + ? `${keyField}: ${String(keyValue)}` + : translate('::App.VisualDesignerCanvas.NewRecord') const toolbarButton = ( label: string, @@ -915,7 +929,7 @@ const SqlDataSourceView = ({ ) - return ( + const body = (
{interactive && (
- SqlDataSource + {SQL_DATA_SOURCE_TYPE} key: {keyField} @@ -1033,6 +1047,16 @@ const SqlDataSourceView = ({ )}
) + + return ( + + {body} + + ) } /** @@ -1061,6 +1085,31 @@ const resolveNodeContent = ( return typeof boundChildren === 'object' ? JSON.stringify(boundChildren) : String(boundChildren) } +/** + * Platform view in preview mode. Its filters are resolved the same way the data + * source preview resolves them — a static value as typed, anything else from the + * sampling value — and handed to the view as the `filter` expression it already + * understands. A required filter without a sampling value renders nothing, which + * is what the generated page does too. + */ +const PlatformViewPreview = ({ node }: { node: DesignerNode }) => { + const filterKey = JSON.stringify(getDesignerNodeFilters(node)) + const params = React.useMemo(() => { + const filters = JSON.parse(filterKey) as DesignerDataSourceFilter[] + if (!filters.length) return undefined + return buildDesignerFilterParams(filters, resolveDesignerPreviewFilterValue) + }, [filterKey]) + if (params === null) return null + return ( + + ) +} + const renderElement = ( node: DesignerNode, children: React.ReactNode, @@ -1162,13 +1211,7 @@ const renderElement = ( } if (node.kind === 'platform') { if (interactive) return - return ( - - ) + return } // Own content and child nodes coexist, exactly as the generator emits them: // the text first, then the nodes dropped underneath it. @@ -1364,7 +1407,8 @@ const NodeView = ({ 'Timeline', // Dropped components land in whichever tab is open. 'Tabs', - 'SqlDataSource', + SQL_DATA_SOURCE_TYPE, + LEGACY_SQL_DATA_SOURCE_TYPE, ].includes(node.type) /* ---- Sürükleme geri bildirimi ---------------------------------- */ diff --git a/ui/src/components/visualDesigner/catalog.ts b/ui/src/components/visualDesigner/catalog.ts index 230cd837..171270a8 100644 --- a/ui/src/components/visualDesigner/catalog.ts +++ b/ui/src/components/visualDesigner/catalog.ts @@ -3,6 +3,7 @@ import generatedComponentProps from './generated/componentProps.json' import { DESIGNER_DATA_COMPONENT_NAMES, SQL_DATA_SOURCE_EVENTS, + LEGACY_SQL_DATA_SOURCE_TYPE, SQL_DATA_SOURCE_TYPE, getDesignerCollectionProperty, isDesignerOptionComponent, @@ -326,7 +327,7 @@ refs.{{sql}}.setFields({ UnitPrice: fiyat, Stock: stok })`, console.log(secilen, refs.{{ref}}.getLabel())`, }, { - group: 'SqlDataSource', + group: 'App.CatalogGroup.SqlDataSource', title: 'App.CatalogSqlDataSource.SqlDataSourceTitle', description: 'App.CatalogSqlDataSource.SqlDataSourceDescription', code: `await refs.{{sql}}.save() @@ -335,7 +336,7 @@ console.log(secilen, refs.{{ref}}.getLabel())`, // refs.{{sql}}.newRecord()`, }, { - group: 'SqlDataSource', + group: 'App.CatalogGroup.SqlDataSource', title: 'App.CatalogSqlDataSource.SqlDataSourceTitle2', description: 'App.CatalogSqlDataSource.SqlDataSourceDescription2', code: `const record = refs.{{sql}}.getRecord() @@ -343,14 +344,14 @@ console.log(record) refs.{{sql}}.setField('name', 'Yeni ad')`, }, { - group: 'SqlDataSource', + group: 'App.CatalogGroup.SqlDataSource', title: 'App.CatalogSqlDataSource.SqlDataSourceTitle3', description: 'App.CatalogSqlDataSource.SqlDataSourceDescription3', code: `const rows = refs.{{sql}}.getRows() refs.{{sql}}.goToRow(rows.length - 1)`, }, { - group: 'SqlDataSource', + group: 'App.CatalogGroup.SqlDataSource', title: 'App.CatalogSqlDataSource.SqlDataSourceTitle4', description: 'App.CatalogSqlDataSource.SqlDataSourceDescription4', code: `const record = refs.{{sql}}.getRecord() @@ -361,7 +362,7 @@ if (!record.name) { await refs.{{sql}}.save()`, }, { - group: 'SqlDataSource', + group: 'App.CatalogGroup.SqlDataSource', title: 'App.CatalogSqlDataSource.SqlDataSourceTitle5', description: 'App.CatalogSqlDataSource.SqlDataSourceDescription5', code: `console.log(refs.{{sql}}.getMode(), refs.{{sql}}.isBusy(), refs.{{sql}}.getError())`, @@ -397,7 +398,11 @@ console.log(id)`, ] export const getDesignerEventSnippet = (componentName: string, eventName: string) => - DESIGNER_EVENT_SNIPPETS[componentName]?.[eventName] || + // A page saved before the container was renamed still carries the old type, + // and its event scripts deserve the same starting snippet. + DESIGNER_EVENT_SNIPPETS[ + componentName === LEGACY_SQL_DATA_SOURCE_TYPE ? SQL_DATA_SOURCE_TYPE : componentName + ]?.[eventName] || `// ${componentName}.${eventName}(event)\nconsole.log(event)` const COLLECTION_ITEMS_PROPERTY = { name: 'items', @@ -756,6 +761,16 @@ export const SQL_DATA_SOURCE_DEFINITION: DesignerComponentDefinition = { description: 'App.CatalogSqlDataSource.SqlDataSourceDescription', acceptsChildren: true, properties: [ + // Filled in, the container renders inside a Card whose header carries this + // text; empty, the Card header is dropped. A `::` prefixed value is a + // localization key and is resolved through the active language. + { + name: 'title', + type: 'string', + value: '', + category: 'properties', + description: 'App.CatalogSqlDataSource.SqlDataSourceTitleDescription', + }, { name: 'selectEndpoint', type: 'string', diff --git a/ui/src/components/visualDesigner/codeGenerator.ts b/ui/src/components/visualDesigner/codeGenerator.ts index 857512b3..7fb73f2e 100644 --- a/ui/src/components/visualDesigner/codeGenerator.ts +++ b/ui/src/components/visualDesigner/codeGenerator.ts @@ -3,6 +3,8 @@ import { getDesignerBindingColumns, getDesignerCollectionProperty, getDesignerTabSlotValue, + getDesignerDataSourceFilters, + getDesignerNodeFilters, getSqlDataSourceEndpointId, getSqlDataSourceKeyField, getSqlDataSourceKeyParam, @@ -15,11 +17,15 @@ import { isDesignerDateProperty, isDesignerOptionComponent, isSqlDataSourceNode, + isValuelessDesignerFilter, + DESIGNER_FILTER_DX_OPERATORS, + toDesignerFilterParamName, normalizeDesignerKeyList, DESIGNER_DROPDOWN_PLACEHOLDER, SQL_DATA_SOURCE_SLOTS, SQL_DEFAULT_VALUE_PROP, type DesignerDataSource, + type DesignerDataSourceFilter, type DesignerDocument, type DesignerNode, } from './types' @@ -312,6 +318,8 @@ const sqlIdentifiers = (node: DesignerNode) => { index: `sqlIndex_${identifier}`, setIndex: `setSqlIndex_${identifier}`, selectUrl: `sqlSelectUrl_${identifier}`, + filters: `sqlFilters_${identifier}`, + filterKey: `sqlFilterKey_${identifier}`, urlKey: `sqlUrlKey_${identifier}`, setField: `sqlSetField_${identifier}`, reload: `sqlReload_${identifier}`, @@ -364,6 +372,68 @@ const sqlErrorEventCall = (node: DesignerNode, action: string) => { : '' } +/** + * Runtime expression a filter reads its value from. A `record` filter addresses + * another SqlDataSource by its ref, which is what turns a master container and a + * detail Grid into a filtered pair without a line of script. + */ +const filterValueExpression = ( + filter: DesignerDataSourceFilter, + sqlRecordByRef: Map, +) => { + if (filter.source === 'static') return JSON.stringify(filter.value) + if (filter.source === 'query' || filter.source === 'route') { + return `readDesignerUrlValue(${JSON.stringify(filter.source)}, ${JSON.stringify(filter.value)})` + } + const [ref, ...rest] = String(filter.value).split('.') + const record = sqlRecordByRef.get(ref) + const column = rest.join('.') + // A ref that no longer exists resolves to nothing; a required filter then + // cancels the request instead of loading the whole collection. + if (!record || !column) return '""' + return `readSqlField(${record}, ${JSON.stringify(column)})` +} + +/** + * Filter descriptor array `buildPlatformFilterParams` consumes. A platform view + * is filtered through a DevExtreme expression rather than through query + * parameters, so the operator is emitted in its DevExtreme form. + */ +const platformFilterEntriesExpression = ( + node: DesignerNode, + sqlRecordByRef: Map, +) => { + const filters = getDesignerNodeFilters(node) + if (!filters.length) return '' + return `[${filters + .map( + (filter) => + `{ field: ${JSON.stringify(filter.field)}, operator: ${JSON.stringify( + DESIGNER_FILTER_DX_OPERATORS[filter.operator], + )}, kind: ${JSON.stringify(filter.operator)}, value: ${ + isValuelessDesignerFilter(filter) ? '""' : filterValueExpression(filter, sqlRecordByRef) + }, required: ${Boolean(filter.required)} }`, + ) + .join(', ')}]` +} + +/** Filter descriptor array `buildFilteredUrl` consumes, or `''` when there is none. */ +const filterEntriesExpression = ( + source: DesignerDataSource | undefined, + sqlRecordByRef: Map, +) => { + const filters = getDesignerDataSourceFilters(source) + if (!filters.length) return '' + return `[${filters + .map( + (filter) => + `{ param: ${JSON.stringify(toDesignerFilterParamName(filter))}, value: ${ + isValuelessDesignerFilter(filter) ? '"true"' : filterValueExpression(filter, sqlRecordByRef) + }, required: ${Boolean(filter.required)} }`, + ) + .join(', ')}]` +} + /** `{ url, method }` literal of an endpoint slot, or `null` when it is unset. */ const sqlSlotLiteral = ( node: DesignerNode, @@ -378,12 +448,17 @@ const sqlSlotLiteral = ( return `{ url: ${JSON.stringify(source.url.trim())}, method: ${JSON.stringify(method)}, responsePath: ${JSON.stringify(source.responsePath || '')} }` } -const sqlDataSourceHooks = (node: DesignerNode, dataSources: DesignerDataSource[]) => { +const sqlDataSourceHooks = ( + node: DesignerNode, + dataSources: DesignerDataSource[], + sqlRecordByRef: Map, +) => { const names = sqlIdentifiers(node) const keyField = getSqlDataSourceKeyField(node) const selectSource = dataSources.find( (candidate) => candidate.id === getSqlDataSourceEndpointId(node, 'selectEndpoint'), ) + const selectFilters = filterEntriesExpression(selectSource, sqlRecordByRef) // The Select endpoint already owns a fetch hook, so the container reuses that // state instead of issuing a second request for the same URL. const selectData = selectSource ? `data_${safeIdentifier(selectSource.id)}` : 'null' @@ -423,12 +498,31 @@ const sqlDataSourceHooks = (node: DesignerNode, dataSources: DesignerDataSource[ // The Select key can come from the page URL, which is how a detail page reads // /api/app/orders/{id} or /api/app/orders?id=… for a single record. const ${names.urlKey} = readUrlKey(${JSON.stringify(keySource)}, ${names.keyParam}) - const ${names.selectUrl} = React.useMemo(() => { +${ + selectFilters + ? ` // Column filters configured on the Select source. They are rebuilt on every + // render so a master record change reaches the URL, and the memo below is keyed + // on their serialized form rather than on each value. + const ${names.filters} = ${selectFilters} + const ${names.filterKey} = JSON.stringify(${names.filters}) +` + : '' + } const ${names.selectUrl} = React.useMemo(() => { if (!${names.slot('selectEndpoint')}) return "" - if (!${names.urlKey}) return ${names.slot('selectEndpoint')}.url - const bound = bindSqlUrl(${names.slot('selectEndpoint')}.url, { [${names.keyParam}]: ${names.urlKey} }, ${names.keyParam}) - return bound.keyBound ? bound.url : appendQueryParam(bound.url, ${names.keyParam}, ${names.urlKey}) - }, [${names.urlKey}]) + const base = ${names.urlKey} + ? (() => { + const bound = bindSqlUrl(${names.slot('selectEndpoint')}.url, { [${names.keyParam}]: ${names.urlKey} }, ${names.keyParam}) + return bound.keyBound ? bound.url : appendQueryParam(bound.url, ${names.keyParam}, ${names.urlKey}) + })() + : ${names.slot('selectEndpoint')}.url + ${ + selectFilters + ? `// An unmet required filter yields no URL at all, which stops the Select + // from loading the unfiltered collection. + return buildFilteredUrl(base, ${names.filters})` + : 'return base' + } + }, [${names.urlKey}${selectFilters ? `, ${names.filterKey}` : ''}]) // A key in the page URL always narrows the result: a list endpoint ignores the // parameter server side, so the requested record is picked out here. Without a // key every row is kept and the navigation below takes over. @@ -630,38 +724,49 @@ const sqlDataSourceToCode = (node: DesignerNode, level: number, itemVariable?: s const names = sqlIdentifiers(node) const formScope: FormScope = { sourceId: node.id, setterName: names.setField } const children = node.children - .map((child) => nodeToCode(child, level + 1, itemVariable, formScope)) + .map((child) => nodeToCode(child, level + 2, itemVariable, formScope)) .join('\n') const className = JSON.stringify(String(node.props.className || '')) const style = `{ display: "flex", flexDirection: "column", gap: ${Number(node.props.gap) || 0} }` const plainButtonClass = `${SQL_TOOLBAR_BUTTON_CLASS} border border-slate-300 text-slate-600 hover:border-sky-400 hover:text-sky-700 dark:border-slate-700 dark:text-slate-300` // Navigation appears on its own whenever there is more than one record to walk. const navigation = ` -${indent(`{${names.rows}.length > 1 ? (`, level + 2)} -${indent('<>', level + 3)} -${indent(``, level + 4)} -${indent(`{\`\${${names.index} + 1} / \${${names.rows}.length}\`}`, level + 4)} -${indent(``, level + 4)} -${indent('', level + 4)} -${indent('', level + 3)} -${indent(') : null}', level + 2)}` +${indent(`{${names.rows}.length > 1 ? (`, level + 3)} +${indent('<>', level + 4)} +${indent(``, level + 5)} +${indent(`{\`\${${names.index} + 1} / \${${names.rows}.length}\`}`, level + 5)} +${indent(``, level + 5)} +${indent('', level + 5)} +${indent('', level + 4)} +${indent(') : null}', level + 3)}` const toolbar = node.props.showToolbar === false ? '' : ` -${indent('
', level + 1)}${navigation} -${indent(``, level + 2)} -${indent(``, level + 2)} -${indent(``, level + 2)} -${indent(``, level + 2)} -${indent(`{${names.mode} === "new" ? "Yeni kayıt" : "Düzenleme"}`, level + 2)} -${indent('
', level + 1)}` +${indent('
', level + 2)}${navigation} +${indent(``, level + 3)} +${indent(``, level + 3)} +${indent(``, level + 3)} +${indent(``, level + 3)} +${indent(`{${names.mode} === "new" ? "Yeni kayıt" : "Düzenleme"}`, level + 3)} +${indent('
', level + 2)}` const error = ` -${indent(`{${names.error} ?
{${names.error}}
: null}`, level + 1)}` +${indent(`{${names.error} ?
{${names.error}}
: null}`, level + 2)}` + // A filled Title turns the container into a Card header; a `::` value is a + // localization key, so `staticValueExpression` emits a translate() call for it + // and the header follows the active language. The header extra carries the key + // of the record on screen. + const title = String(node.props.title ?? '').trim() + const headerExtra = `{${names.hasKey} ? ${names.key} + ": " + readSqlField(${names.record}, ${names.key}) : ${names.mode} === "new" ? "Yeni kayıt" : ""}` + const cardStart = title + ? `${headerExtra}
}>` + : '' - return `${indent(`
`, level)} + return `${indent(cardStart, level)} +${indent(`
`, level + 1)} ${children}${toolbar}${error} -${indent('
', level)}` +${indent('
', level + 1)} +${indent('
', level)}` } /** @@ -810,6 +915,9 @@ ${indent('', level)}` const overflowClass = overflow ? 'overflow-auto' : 'overflow-visible' const table = `${indent('{(() => {', level + 1)} ${indent(`const ${itemsVariable} = ${collectionExpression}`, level + 2)} +${indent(`// Nothing loaded yet — an endpoint still in flight, or a required filter`, level + 2)} +${indent(`// holding the request back — is an empty grid, not a binding error.`, level + 2)} +${indent(`if (${itemsVariable} === null || ${itemsVariable} === undefined) return null`, level + 2)} ${indent(`if (!Array.isArray(${itemsVariable})) return
Grid items bağlantısı bir koleksiyon döndürmelidir.
`, level + 2)} ${indent(`const ${columnsVariable} = ${columnsExpression}`, level + 2)} ${indent(`if (!${columnsVariable}.length) return
Preview için en az bir sütun seçin.
`, level + 2)} @@ -843,20 +951,29 @@ ${indent('})()}', level + 1)}` .join('\n') const repeatedChildren = node.children.length ? node.children - .map((child) => nodeToCode(child, level + 3, repeatedItemVariable, formScope)) + .map((child) => nodeToCode(child, level + 4, repeatedItemVariable, formScope)) .join('\n') : indent( `
{typeof ${repeatedItemVariable} === "string" ? ${repeatedItemVariable} : JSON.stringify(${repeatedItemVariable}, null, 2)}
`, - level + 3, + level + 4, ) + // The collection is read once into a local: it is tested for three cases + // before it is mapped, and repeating the expression for each of them made + // the emitted line unreadable. + const repeatedRowsVariable = `rows_${safeIdentifier(node.id)}` const children = collectionExpression - ? `${indent(`{Array.isArray(${collectionExpression}) ? ${collectionExpression}.map((${repeatedItemVariable}, index) => (`, level + 1)} -${indent(``, level + 2)} + ? `${indent('{(() => {', level + 1)} +${indent(`const ${repeatedRowsVariable} = ${collectionExpression}`, level + 2)} +${indent(`// Nothing loaded yet — an endpoint still in flight, or a required filter`, level + 2)} +${indent(`// holding the request back — is an empty grid, not a binding error.`, level + 2)} +${indent(`if (${repeatedRowsVariable} === null || ${repeatedRowsVariable} === undefined) return null`, level + 2)} +${indent(`if (!Array.isArray(${repeatedRowsVariable})) return
Grid items bağlantısı bir koleksiyon döndürmelidir.
`, level + 2)} +${indent(`return ${repeatedRowsVariable}.map((${repeatedItemVariable}, index) => (`, level + 2)} +${indent(``, level + 3)} ${repeatedChildren} -${indent('', level + 2)} -${indent(')) : (', level + 1)} -${indent('
Grid items bağlantısı bir koleksiyon döndürmelidir.
', level + 2)} -${indent(')}', level + 1)}` +${indent('
', level + 3)} +${indent('))', level + 2)} +${indent('})()}', level + 1)}` : staticChildren return `${indent(gridStart, level)}\n${children}\n${indent('
', level)}` } @@ -873,10 +990,15 @@ ${indent(')}', level + 1)}` PivotView: 'Pivot', ChartView: 'Chart', } - return indent( - ``, - level, - ) + const platformParams = `platformParams_${safeIdentifier(node.id)}` + const hasFilters = getDesignerNodeFilters(node).length > 0 + const host = `` + if (!hasFilters) return indent(host, level) + // A required filter without a value renders nothing: showing the unfiltered + // list instead would read as a filter that is not applied. + return `${indent(`{${platformParams} === null ? null : (`, level)} +${indent(host, level + 1)} +${indent(')}', level)}` } if (node.type === 'Menu') { @@ -1259,6 +1381,8 @@ export const generateDesignerCode = (name: string, document: DesignerDocument) = let hasDatePicker = false let hasTabs = false const sqlDataSourceNodes: DesignerNode[] = [] + /** Platform views carrying their own filters; they get a params hook each. */ + const platformFilterNodes: DesignerNode[] = [] /** Sources loaded by a SqlDataSource; their own mount fetch would duplicate it. */ const sqlManagedSelectSourceIds = new Set() @@ -1270,6 +1394,9 @@ export const generateDesignerCode = (name: string, document: DesignerDocument) = if (node.type === 'Grid') hasDataTable = true if (node.type === 'Tabs') hasTabs = true if (isDesignerDateComponent(node.type)) hasDatePicker = true + if (node.kind === 'platform' && getDesignerNodeFilters(node).length) { + platformFilterNodes.push(node) + } if (isSqlDataSourceNode(node.type)) { sqlDataSourceNodes.push(node) const selectId = getSqlDataSourceEndpointId(node, 'selectEndpoint') @@ -1374,6 +1501,18 @@ export const generateDesignerCode = (name: string, document: DesignerDocument) = visit(document.nodes) const dataSources = document.dataSources || [] + // Ref → record state variable of every SqlDataSource on the page. A `record` + // filter addresses its master through this map, so a detail source can be + // filtered by the row the container currently shows. + const sqlRecordByRef = new Map( + sqlDataSourceNodes + .filter((node) => node.ref) + .map((node) => [String(node.ref), sqlIdentifiers(node).record] as const), + ) + const hasDataSourceFilters = dataSources.some( + (source) => getDesignerDataSourceFilters(source).length > 0, + ) + const hasFilters = hasDataSourceFilters || platformFilterNodes.length > 0 const selectHelpers = hasSelect ? ` const toSelectOptions = (value, labelPath = "", valuePath = "") => { if (!Array.isArray(value)) return [] @@ -1588,8 +1727,81 @@ export const generateDesignerCode = (name: string, document: DesignerDocument) = } const toSqlErrorMessage = (error) => error?.response?.data?.error?.message || error?.response?.data?.message || error?.message || "İşlem tamamlanamadı."` : '' + const filterHelpers = hasFilters + ? ` // Value of a filter that reads the page URL: a query string parameter, or a + // path segment — the one after a segment named like the parameter, otherwise + // the last one, which is the /orders/{id} detail page shape. + const readDesignerUrlValue = (kind, name) => { + if (typeof window === "undefined") return "" + if (kind === "query") return new URLSearchParams(window.location.search).get(name) ?? "" + const segments = window.location.pathname.split("/").filter(Boolean) + if (!segments.length) return "" + const index = name ? segments.findIndex((segment) => segment.toLowerCase() === String(name).toLowerCase()) : -1 + if (index >= 0 && index < segments.length - 1) return decodeURIComponent(segments[index + 1]) + return decodeURIComponent(segments[segments.length - 1]) + } + // Filters are appended as query parameters: \`Column\` for an equality and + // \`Column.operator\` for anything else, which is what the endpoint parses. + // Returns "" when a required filter has no value yet — the caller must skip the + // request instead of loading the unfiltered collection. + const buildFilteredUrl = (url, filters) => { + const parts = [] + for (const filter of filters || []) { + const value = filter.value === undefined || filter.value === null ? "" : String(filter.value) + if (!value) { + if (filter.required) return "" + continue + } + parts.push(encodeURIComponent(filter.param) + "=" + encodeURIComponent(value)) + } + if (!parts.length) return url + return url + (url.includes("?") ? "&" : "?") + parts.join("&") + }` + : '' + // A platform view is filtered through the same `filter` expression a list form + // page carries in its address, so an embedded view needs no change to the list + // form itself. Returns null when a required filter has no value yet. + const platformFilterHelpers = platformFilterNodes.length + ? ` const buildPlatformFilterParams = (filters) => { + const expressions = [] + for (const filter of filters || []) { + if (filter.kind === "isnull" || filter.kind === "notnull") { + expressions.push([filter.field, filter.operator, null]) + continue + } + const value = filter.value === undefined || filter.value === null ? "" : String(filter.value) + if (!value) { + if (filter.required) return null + continue + } + if (filter.kind === "in") { + const entries = value.split(",").map((entry) => entry.trim()).filter(Boolean).map((entry) => [filter.field, "=", entry]) + if (!entries.length) continue + expressions.push(entries.reduce((result, entry) => (result ? [result, "or", entry] : entry), null)) + continue + } + expressions.push([filter.field, filter.operator, value]) + } + const params = new URLSearchParams() + const merged = expressions.reduce((result, expression) => (result ? [result, "and", expression] : expression), null) + if (merged) params.set("filter", JSON.stringify(merged)) + return params + }` + : '' + // Declared after the SqlDataSource hooks: a filter may read the record of the + // container the view sits next to. + const platformFilterHooks = platformFilterNodes + .map((node) => { + const identifier = safeIdentifier(node.id) + return ` const platformFilters_${identifier} = ${platformFilterEntriesExpression(node, sqlRecordByRef)} + const platformKey_${identifier} = JSON.stringify(platformFilters_${identifier}) + // The views keep the params object in their own state and write back to it, so + // it must stay the same object until a filter value actually changes. + const platformParams_${identifier} = React.useMemo(() => buildPlatformFilterParams(platformFilters_${identifier}), [platformKey_${identifier}])` + }) + .join('\n\n') const sqlHooks = sqlDataSourceNodes - .map((node) => sqlDataSourceHooks(node, dataSources)) + .map((node) => sqlDataSourceHooks(node, dataSources, sqlRecordByRef)) .join('\n\n') const dataHelpers = dataSources.length || hasDataTable || sqlDataSourceNodes.length @@ -1602,7 +1814,10 @@ export const generateDesignerCode = (name: string, document: DesignerDocument) = return value?.result === undefined ? undefined : readPath(value.result, path) }` : '' - const dataHooks = dataSources + // The state of every source is declared before the SqlDataSource hooks, which + // write through the setters; the fetch effects come after them, because a + // filtered source may read the record a container owns. + const dataStateHooks = dataSources .map((source) => { const identifier = safeIdentifier(source.id) // POST/PUT/DELETE sources are only ever invoked by a SqlDataSource command; @@ -1610,21 +1825,48 @@ export const generateDesignerCode = (name: string, document: DesignerDocument) = if (source.method !== 'GET' || !source.url.trim().startsWith('/api/')) { return ` const [data_${identifier}] = React.useState(null)` } + return ` const [data_${identifier}, setData_${identifier}] = React.useState(null)` + }) + .join('\n') + const dataFetchHooks = dataSources + .map((source) => { + const identifier = safeIdentifier(source.id) + if (source.method !== 'GET' || !source.url.trim().startsWith('/api/')) return '' // A GetById style URL is not fetched on mount — requesting a literal `{id}` // is a guaranteed 400 — and neither is a source the owning SqlDataSource - // loads itself. Both still need the setter that container writes through. + // loads itself, through the setter declared above. if (hasSqlDataSourceUrlParams(source.url) || sqlManagedSelectSourceIds.has(source.id)) { - return ` const [data_${identifier}, setData_${identifier}] = React.useState(null)` + return '' } - return ` const [data_${identifier}, setData_${identifier}] = React.useState(null) - React.useEffect(() => { + const entries = filterEntriesExpression(source, sqlRecordByRef) + // Without filters the source keeps its previous mount-only fetch, byte for + // byte; with them the effect re-runs whenever a filter value changes. + if (!entries) { + return ` React.useEffect(() => { let active = true apiService.fetchData({ url: ${JSON.stringify(source.url)}, method: 'GET' }).then((response) => { if (active) setData_${identifier}(getByPath(response.data, ${JSON.stringify(source.responsePath || '')})) }).catch((error) => console.error(${JSON.stringify(`${source.name} endpoint error:`)}, error)) return () => { active = false } }, [])` + } + return ` const filters_${identifier} = ${entries} + const filterKey_${identifier} = JSON.stringify(filters_${identifier}) + React.useEffect(() => { + const url = buildFilteredUrl(${JSON.stringify(source.url)}, filters_${identifier}) + // An unmet required filter leaves the component empty on purpose. + if (!url) { + setData_${identifier}(null) + return + } + let active = true + apiService.fetchData({ url, method: 'GET' }).then((response) => { + if (active) setData_${identifier}(getByPath(response.data, ${JSON.stringify(source.responsePath || '')})) + }).catch((error) => console.error(${JSON.stringify(`${source.name} endpoint error:`)}, error)) + return () => { active = false } + }, [filterKey_${identifier}])` }) + .filter(Boolean) .join('\n\n') // Declared after the data/SQL/runtime state it reads, and before the handlers @@ -1641,5 +1883,5 @@ export const generateDesignerCode = (name: string, document: DesignerDocument) = const designerBackup = encodeURIComponent(JSON.stringify(document)) - return `/*__SOZSOFT_VISUAL_DESIGNER__${designerBackup}__*/\nconst ${componentName} = () => {\n${[dataHelpers, scriptHelpers,selectHelpers, dropdownHelpers, selectValueHelpers, selectMenuHelpers, localeHelpers, dateHelpers, tabHelpers, sqlHelpers, dataHooks, sqlHooks, ...runtimeStateHooks, refRuntime, mount, ...handlers].filter(Boolean).join('\n\n')}\n\n return (\n <>\n${body}\n \n )\n}\n\nexport default ${componentName}\n` + return `/*__SOZSOFT_VISUAL_DESIGNER__${designerBackup}__*/\nconst ${componentName} = () => {\n${[dataHelpers, scriptHelpers,selectHelpers, dropdownHelpers, selectValueHelpers, selectMenuHelpers, localeHelpers, dateHelpers, tabHelpers, sqlHelpers, filterHelpers, platformFilterHelpers, dataStateHooks, sqlHooks, dataFetchHooks, platformFilterHooks, ...runtimeStateHooks, refRuntime, mount, ...handlers].filter(Boolean).join('\n\n')}\n\n return (\n <>\n${body}\n \n )\n}\n\nexport default ${componentName}\n` } diff --git a/ui/src/components/visualDesigner/types.ts b/ui/src/components/visualDesigner/types.ts index 4e8db157..5d0f10a0 100644 --- a/ui/src/components/visualDesigner/types.ts +++ b/ui/src/components/visualDesigner/types.ts @@ -40,12 +40,246 @@ export const toDesignerHttpMethod = (value: unknown): DesignerHttpMethod => { : 'GET' } +/** + * Comparison a filter applies to its column. `eq` is sent as a bare query + * parameter (`?RoleId=…`), every other operator carries its name as a suffix + * (`?Name.contains=…`) — which is exactly what the CrudEndpoint GetList reads. + */ +export const DESIGNER_FILTER_OPERATORS = [ + 'eq', + 'ne', + 'contains', + 'startswith', + 'endswith', + 'gt', + 'gte', + 'lt', + 'lte', + 'in', + 'isnull', + 'notnull', +] as const + +export type DesignerFilterOperator = (typeof DESIGNER_FILTER_OPERATORS)[number] + +/** Operators that compare against nothing; their value input is hidden. */ +export const DESIGNER_VALUELESS_FILTER_OPERATORS: readonly DesignerFilterOperator[] = [ + 'isnull', + 'notnull', +] + +/** + * Where the filter reads its value from at runtime: + * - `static` → the literal typed in the designer + * - `query` → a query string parameter of the page URL (`?id=…`) + * - `route` → a path segment of the page URL (last segment, or a named one) + * - `record` → a column of another SqlDataSource's active record, which is what + * makes a master/detail page work without a line of code + */ +export const DESIGNER_FILTER_SOURCES = ['static', 'query', 'route', 'record'] as const + +export type DesignerFilterSource = (typeof DESIGNER_FILTER_SOURCES)[number] + +export interface DesignerDataSourceFilter { + id: string + /** Column the endpoint filters on. */ + field: string + operator: DesignerFilterOperator + source: DesignerFilterSource + /** + * `static` → the literal value · `query`/`route` → the URL parameter name · + * `record` → `.` of the master container. + */ + value: string + /** + * Sampling value used by the designer preview, where there is no page URL and + * no master record yet. Never emitted into the generated component. + */ + previewValue?: string + /** When set, an empty resolved value cancels the request instead of loading everything. */ + required?: boolean +} + export interface DesignerDataSource { id: string name: string method: DesignerHttpMethod url: string responsePath: string + /** Query parameters applied to every call of this source; empty for most sources. */ + filters?: DesignerDataSourceFilter[] +} + +/** + * Blank filter row, ready to be edited in the inspector. It starts out required: + * a filter whose value is not there yet should hold the request back, because + * loading the unfiltered collection instead reads as a filter that does not work. + */ +export const createDesignerFilter = (id: string): DesignerDataSourceFilter => ({ + id, + field: '', + operator: 'eq', + source: 'static', + value: '', + previewValue: '', + required: true, +}) + +export const toDesignerFilterOperator = (value: unknown): DesignerFilterOperator => { + const operator = String(value ?? '').trim().toLowerCase() + return (DESIGNER_FILTER_OPERATORS as readonly string[]).includes(operator) + ? (operator as DesignerFilterOperator) + : 'eq' +} + +export const toDesignerFilterSource = (value: unknown): DesignerFilterSource => { + const source = String(value ?? '').trim().toLowerCase() + return (DESIGNER_FILTER_SOURCES as readonly string[]).includes(source) + ? (source as DesignerFilterSource) + : 'static' +} + +/** + * DevExtreme operator each filter maps to. Platform views (List, Grid, Tree, …) + * are filtered through a `filter` expression rather than through query + * parameters, which is the same channel a standalone list form already uses. + * `in` becomes an or-chain of equalities and the null operators compare against + * null, so both are handled by the builders rather than by this table. + */ +export const DESIGNER_FILTER_DX_OPERATORS: Record = { + eq: '=', + ne: '<>', + contains: 'contains', + startswith: 'startswith', + endswith: 'endswith', + gt: '>', + gte: '>=', + lt: '<', + lte: '<=', + in: '=', + isnull: '=', + notnull: '<>', +} + +/** Normalized filter rows of anything that carries a `filters` list. */ +export const normalizeDesignerFilters = (value: unknown): DesignerDataSourceFilter[] => + (Array.isArray(value) ? (value as DesignerDataSourceFilter[]) : []) + .filter((filter) => filter && String(filter.field || '').trim()) + .map((filter) => ({ + ...filter, + field: String(filter.field).trim(), + operator: toDesignerFilterOperator(filter.operator), + source: toDesignerFilterSource(filter.source), + value: String(filter.value ?? ''), + })) + +/** + * Filters of a platform view. They live on the node rather than on a data source + * because the view owns its own data: it is driven by a list form code, not by an + * endpoint declared in the Data workspace. + */ +export const getDesignerNodeFilters = (node: { props: Record }) => + normalizeDesignerFilters(node.props?.filters) + +/** One filter as a DevExtreme triplet, or `null` when it compares against nothing. */ +const toDesignerFilterExpression = (filter: DesignerDataSourceFilter, value: string): unknown => { + if (filter.operator === 'isnull') return [filter.field, '=', null] + if (filter.operator === 'notnull') return [filter.field, '<>', null] + if (filter.operator === 'in') { + const entries = value + .split(',') + .map((entry) => entry.trim()) + .filter(Boolean) + .map((entry) => [filter.field, '=', entry]) + if (!entries.length) return null + return entries.reduce((result, entry) => (result ? [result, 'or', entry] : entry), null) + } + return [filter.field, DESIGNER_FILTER_DX_OPERATORS[filter.operator], value] +} + +/** + * `filter` search parameter of a platform view, or `null` when a required filter + * has no value — the caller must then render nothing rather than the unfiltered + * list. + */ +export const buildDesignerFilterParams = ( + filters: DesignerDataSourceFilter[], + resolveValue: (filter: DesignerDataSourceFilter) => string, +): URLSearchParams | null => { + const expressions: unknown[] = [] + for (const filter of filters) { + if (!isValuelessDesignerFilter(filter)) { + const value = resolveValue(filter) + if (!value) { + if (filter.required) return null + continue + } + const expression = toDesignerFilterExpression(filter, value) + if (expression) expressions.push(expression) + continue + } + expressions.push(toDesignerFilterExpression(filter, '')) + } + const params = new URLSearchParams() + const merged = expressions.reduce( + (result, expression) => (result ? [result, 'and', expression] : expression), + null, + ) + if (merged) params.set('filter', JSON.stringify(merged)) + return params +} + +/** Filters of a source, normalized: an incomplete row is dropped, not emitted. */ +export const getDesignerDataSourceFilters = ( + source?: DesignerDataSource | null, +): DesignerDataSourceFilter[] => normalizeDesignerFilters(source?.filters) + +export const isValuelessDesignerFilter = (filter: DesignerDataSourceFilter) => + DESIGNER_VALUELESS_FILTER_OPERATORS.includes(filter.operator) + +/** Query parameter name of a filter: `Column` for `eq`, `Column.op` otherwise. */ +export const toDesignerFilterParamName = (filter: DesignerDataSourceFilter) => + filter.operator === 'eq' ? filter.field : `${filter.field}.${filter.operator}` + +/** + * Value the designer preview uses. Only a static filter has a real value at + * design time; everything else falls back to the sampling value, so an + * unconfigured preview filter simply drops out of the request. + */ +export const resolveDesignerPreviewFilterValue = (filter: DesignerDataSourceFilter) => + (filter.source === 'static' ? filter.value : (filter.previewValue ?? '')).trim() + +/** + * Query string of a source for the designer preview. Returns `null` when a + * required filter has no sampling value — the caller must skip the request + * rather than fetch the unfiltered collection. + */ +export const buildDesignerPreviewQuery = (source: DesignerDataSource): string | null => { + const parts: string[] = [] + for (const filter of getDesignerDataSourceFilters(source)) { + if (isValuelessDesignerFilter(filter)) { + parts.push(`${encodeURIComponent(toDesignerFilterParamName(filter))}=true`) + continue + } + const value = resolveDesignerPreviewFilterValue(filter) + if (!value) { + if (filter.required) return null + continue + } + parts.push( + `${encodeURIComponent(toDesignerFilterParamName(filter))}=${encodeURIComponent(value)}`, + ) + } + return parts.join('&') +} + +/** Source URL with the preview filters applied, or `null` when it must not run. */ +export const buildDesignerPreviewUrl = (source: DesignerDataSource, url?: string) => { + const target = (url ?? source.url).trim() + const query = buildDesignerPreviewQuery(source) + if (query === null) return null + if (!query) return target + return target + (target.includes('?') ? '&' : '?') + query } export interface DesignerNode { @@ -428,9 +662,17 @@ export const resolveDesignerResponse = (value: unknown, responsePath: string): u * which is also the name of the record state emitted by the code generator — so * the existing binding machinery keeps working unchanged. */ -export const SQL_DATA_SOURCE_TYPE = 'SqlDataSource' +export const SQL_DATA_SOURCE_TYPE = 'Form' -export const isSqlDataSourceNode = (type?: string) => type === SQL_DATA_SOURCE_TYPE +/** + * The container used to be called `SqlDataSource`, and that name is what every + * page saved before the rename carries in its document. It is still accepted, so + * an existing page keeps rendering, keeps generating code and keeps its bindings. + */ +export const LEGACY_SQL_DATA_SOURCE_TYPE = 'SqlDataSource' + +export const isSqlDataSourceNode = (type?: string) => + type === SQL_DATA_SOURCE_TYPE || type === LEGACY_SQL_DATA_SOURCE_TYPE export interface SqlDataSourceSlot { property: 'selectEndpoint' | 'insertEndpoint' | 'updateEndpoint' | 'deleteEndpoint' diff --git a/ui/src/views/developerKit/VisualComponentDesigner.tsx b/ui/src/views/developerKit/VisualComponentDesigner.tsx index 39bac65d..84af0acf 100644 --- a/ui/src/views/developerKit/VisualComponentDesigner.tsx +++ b/ui/src/views/developerKit/VisualComponentDesigner.tsx @@ -35,6 +35,9 @@ import { useComponents } from '@/contexts/ComponentContext' import type { CrudEndpoint, CustomComponent } from '@/proxy/developerKit/models' import { ROUTES_ENUM } from '@/routes/route.constant' import apiService from '@/services/api.service' +import { getList } from '@/services/form.service' +import type { GridDto } from '@/proxy/form/models' +import { getListForms } from '@/services/admin/list-form.service' import { developerKitService } from '@/services/developerKit.service' import { Button, Notification, toast } from '@/components/ui' import StyleModal from '@/components/codeLayout/StyleModal' @@ -78,6 +81,7 @@ import { isDesignerOptionComponent, isDesignerTabularComponent, isSqlDataSourceNode, + LEGACY_SQL_DATA_SOURCE_TYPE, resolveDesignerResponse, toDesignerHttpMethod, walkDesignerNodes, @@ -87,11 +91,21 @@ import { DESIGNER_HTTP_METHODS, SQL_DATA_SOURCE_KEY_SOURCES, SQL_DATA_SOURCE_SLOTS, + SQL_DATA_SOURCE_TYPE, SQL_DEFAULT_VALUE_PROP, SQL_DEFAULT_VALUE_TOKENS, type DesignerComponentDefinition, type DesignerBinding, + buildDesignerPreviewUrl, + createDesignerFilter, + DESIGNER_FILTER_OPERATORS, + DESIGNER_FILTER_SOURCES, + getDesignerDataSourceFilters, + isValuelessDesignerFilter, type DesignerDataSource, + type DesignerDataSourceFilter, + type DesignerFilterOperator, + type DesignerFilterSource, type DesignerDocument, type DesignerHttpMethod, type DesignerNode, @@ -634,6 +648,10 @@ const normalizeDesignerDocument = (document: DesignerDocument): DesignerDocument } return { ...node, + // The container used to be called SqlDataSource. Renaming it on load is + // what makes an existing page show `Form` everywhere it names its own + // type — the toolbox, the layer tree and the inspector all read it. + type: node.type === LEGACY_SQL_DATA_SOURCE_TYPE ? SQL_DATA_SOURCE_TYPE : node.type, props, events, bindings, @@ -1178,6 +1196,25 @@ const PropertyEditor = ({ ) } +/** + * Operator labels. They are symbols and SQL keywords rather than sentences, so + * they read the same in every language the designer runs in. + */ +const FILTER_OPERATOR_LABELS: Record = { + eq: '=', + ne: '≠', + contains: '⊃ contains', + startswith: 'starts', + endswith: 'ends', + gt: '>', + gte: '≥', + lt: '<', + lte: '≤', + in: 'in (a,b)', + isnull: 'is null', + notnull: 'is not null', +} + const VisualComponentDesigner = () => { const { id } = useParams() const { translate } = useLocalization() @@ -1512,6 +1549,110 @@ const VisualComponentDesigner = () => { }) return records }, [dataSourceSamples, document.nodes]) + /** + * SqlDataSource containers a filter can read its value from. Only a named + * container is addressable: the generated code resolves the master through its + * ref, exactly like a script does. + */ + const sqlContainerRefs = useMemo(() => { + const refs: { ref: string; id: string }[] = [] + walkDesignerNodes(document.nodes, (node) => { + if (isSqlDataSourceNode(node.type) && node.ref) refs.push({ ref: node.ref, id: node.id }) + }) + return refs + }, [document.nodes]) + + /** + * Filters belong to the data source, not to the component bound to it, so every + * component reading that endpoint is filtered the same way — which is what makes + * a master/detail page a matter of configuration. + */ + /** + * List forms a platform view can be pointed at, and the fields of the one it is + * pointed at. Both are looked up on demand — only a selected platform view needs + * them — and cached, so switching between views costs no extra request. + */ + const [listFormCodes, setListFormCodes] = useState([]) + const [listFormFields, setListFormFields] = useState>({}) + const platformListFormCode = + selectedNode?.kind === 'platform' ? String(selectedNode.props.listFormCode || '') : '' + + useEffect(() => { + if (selectedNode?.kind !== 'platform' || listFormCodes.length) return + let active = true + void getListForms({ maxResultCount: 1000, skipCount: 0, sorting: 'listFormCode' }) + .then((response) => { + if (!active) return + const codes = (response?.data?.items || []) + .map((item) => String(item.listFormCode || '').trim()) + .filter(Boolean) + setListFormCodes([...new Set(codes)].sort((left, right) => left.localeCompare(right))) + }) + .catch((error) => console.error('List form list could not be loaded:', error)) + return () => { + active = false + } + }, [selectedNode?.kind, listFormCodes.length]) + + useEffect(() => { + if (!platformListFormCode || listFormFields[platformListFormCode]) return + let active = true + // `getList` swallows its own errors and can resolve with the error payload, so + // the response is read defensively rather than through its declared shape. + void Promise.resolve(getList({ listFormCode: platformListFormCode })) + .then((response: { data?: GridDto } | undefined) => { + if (!active) return + const fields = (response?.data?.columnFormats || []) + .map((column: { fieldName?: string }) => String(column.fieldName || '').trim()) + .filter(Boolean) + setListFormFields((current) => ({ + ...current, + // The key field is filterable but is not always a listed column. + [platformListFormCode]: [ + ...new Set( + [String(response?.data?.gridOptions?.keyFieldName || '').trim(), ...fields].filter( + Boolean, + ), + ), + ].sort((left, right) => left.localeCompare(right)), + })) + }) + .catch((error: unknown) => console.error('List form fields could not be loaded:', error)) + return () => { + active = false + } + }, [platformListFormCode, listFormFields]) + + /** + * Whether a property is the list form code of the selected platform view. Until + * the list arrives the property stays a text box, so a code can still be typed. + */ + const isPlatformListFormProperty = (propertyName: string) => + propertyName === 'listFormCode' && + selectedNode?.kind === 'platform' && + listFormCodes.length > 0 + + /** Per source debounce of the filter preview request. */ + const filterPreviewTimers = useRef>({}) + // Mirrors of the two memos a preview filter resolves against; see + // `withPreviewFilterValues` for why they are read through refs. + const sqlPreviewRecords = useRef>({}) + const sqlPreviewRefs = useRef<{ ref: string; id: string }[]>([]) + sqlPreviewRecords.current = sqlDataSourceRecords + sqlPreviewRefs.current = sqlContainerRefs + + const updateDataSourceFilters = useCallback( + (sourceId: string, next: DesignerDataSourceFilter[]) => { + commitDocument((current) => ({ + ...current, + dataSources: current.dataSources.map((source) => + source.id === sourceId ? { ...source, filters: next } : source, + ), + })) + }, + [commitDocument], + ) + const previewDataValues = useMemo( () => ({ ...dataSourceSamples, ...sqlDataSourceRecords }), [dataSourceSamples, sqlDataSourceRecords], @@ -1577,14 +1718,21 @@ const VisualComponentDesigner = () => { ) const selectedIsDataComponent = Boolean( selectedNode && - (isDataCategoryComponent(selectedDefinition) || selectedIsSqlDataSource || sqlScopeNode), + // A platform view is data driven too: its list form supplies the rows and + // the Data tab is where its filters are configured. + (isDataCategoryComponent(selectedDefinition) || + selectedNode.kind === 'platform' || + selectedIsSqlDataSource || + sqlScopeNode), ) const selectedProperties = useMemo(() => { if (!selectedNode) return [] const definedProperties = selectedDefinition?.properties || [] const definedNames = new Set(definedProperties.map((property) => property.name)) const storedProperties = Object.entries(selectedNode.props) - .filter(([propertyName]) => !definedNames.has(propertyName)) + // `filters` is edited in the Data tab; as a raw JSON textarea here it is + // only a way to corrupt it. + .filter(([propertyName]) => !definedNames.has(propertyName) && propertyName !== 'filters') .map(([propertyName, value]) => ({ name: propertyName, type: (Array.isArray(value) @@ -1828,6 +1976,34 @@ const VisualComponentDesigner = () => { setDataPanelSourceId(dataSource.id) } + /** + * A `record` filter has no typed value: at design time it is resolved from the + * column of the master container's sampled record, so a master/detail pair + * previews without the designer having to repeat the key by hand. An explicit + * sampling value still wins. + * + * The latest records are read through a ref because `testDataSource` is created + * once; a closed over copy would sample against the record of an earlier run. + */ + const withPreviewFilterValues = useCallback((source: DesignerDataSource): DesignerDataSource => { + const filters = source.filters || [] + if (!filters.length) return source + return { + ...source, + filters: filters.map((filter) => { + if (filter.source !== 'record' || String(filter.previewValue ?? '').trim()) return filter + const [ref = '', ...columnParts] = String(filter.value || '').split('.') + const column = columnParts.join('.') + const nodeId = sqlPreviewRefs.current.find((item) => item.ref === ref)?.id + const record = nodeId ? sqlPreviewRecords.current[nodeId] : undefined + const value = column ? getDesignerValueByPath(record, column) : undefined + return value === undefined || value === null || value === '' + ? filter + : { ...filter, previewValue: String(value) } + }), + } + }, []) + /** * `urlOverride` carries a URL whose `{id}` was already filled in — a GetById * endpoint cannot be sampled otherwise, and without a sample the designer has @@ -1857,7 +2033,28 @@ const VisualComponentDesigner = () => { message: translate('::App.DeveloperKitComponentDesigner.SendingRequest'), }, })) - const requestUrl = (urlOverride ?? source.url).trim() + // Design time preview runs with the filters applied: a static one uses its + // real value, a record one the column of the master's sampled record, and a + // URL one the sampling value typed next to it. + const previewUrl = buildDesignerPreviewUrl(withPreviewFilterValues(source), urlOverride) + if (previewUrl === null) { + // The stale sample has to go with it, otherwise the canvas keeps painting + // the rows of the last unfiltered run as if they had passed the filter. + setDataSourceSamples((current) => { + const next = { ...current } + delete next[source.id] + return next + }) + setDataTestResults((current) => ({ + ...current, + [source.id]: { + status: 'error', + message: translate('::App.DeveloperKitComponentDesigner.RequiredFilterPreviewMissing'), + }, + })) + return + } + const requestUrl = previewUrl.trim() try { if (!requestUrl) { throw new Error(translate('::App.DeveloperKitComponentDesigner.EndpointUrlRequired')) @@ -1867,7 +2064,7 @@ const VisualComponentDesigner = () => { translate('::App.DeveloperKitComponentDesigner.EndpointMustStartWithApi'), ) } - if (!isRunnableDataSourceUrl(requestUrl)) { + if (!isRunnableDataSourceUrl((urlOverride ?? source.url).trim())) { throw new Error(translate('::App.DeveloperKitComponentDesigner.EndpointNotRunnable')) } if (hasSqlDataSourceUrlParams(requestUrl)) { @@ -3188,6 +3385,312 @@ const VisualComponentDesigner = () => { ) } + /** + * Column filters of a data source. They are sent as query parameters — `Column` + * for an equality, `Column.operator` for anything else — which is what the CRUD + * GetList endpoint parses, so any column can be filtered, not just the key. + * + * A value comes from a static literal, from the page URL (query string or route + * segment) or from the record of another SqlDataSource, which is how a detail + * list follows the master container without a line of script. + */ + /** + * Filters of a data source: they belong to the endpoint, so every component + * bound to it is filtered the same way. + */ + const renderDataSourceFilters = (source?: DesignerDataSource | null) => { + if (!source || source.method !== 'GET') return null + const sample = dataSourceSamples[source.id] + const sampleRow = Array.isArray(sample) + ? sample[0] + : sample && + typeof sample === 'object' && + Array.isArray((sample as Record).items) + ? ((sample as Record).items as unknown[])[0] + : sample + const columns = + sampleRow && typeof sampleRow === 'object' && !Array.isArray(sampleRow) + ? Object.keys(sampleRow as Record) + : [] + return renderFilterEditor({ + scopeId: source.id, + filters: (source.filters || []) as DesignerDataSourceFilter[], + columns, + onChange: (next) => { + updateDataSourceFilters(source.id, next) + // The preview is only meaningful with the new filters applied, but a value + // is typed one character at a time — so the sample is refreshed once the + // typing stops instead of on every keystroke. + window.clearTimeout(filterPreviewTimers.current[source.id]) + filterPreviewTimers.current[source.id] = window.setTimeout( + () => void testDataSource({ ...source, filters: next }), + 600, + ) + }, + }) + } + + /** + * Filters of a platform view. The view owns its data through its list form, so + * the filters live on the node and are carried into the view as the same + * `filter` expression a list form page already reads from its address. + */ + const renderPlatformViewFilters = (node?: DesignerNode | null) => { + if (!node || node.kind !== 'platform') return null + const fields = listFormFields[String(node.props.listFormCode || '')] || [] + return renderFilterEditor({ + scopeId: node.id, + filters: (Array.isArray(node.props.filters) + ? node.props.filters + : []) as DesignerDataSourceFilter[], + columns: fields, + // The list form knows its fields, and a name that is not one of them is + // dropped server side without a trace — so the column is picked, not typed. + strictColumns: fields.length > 0, + onChange: (next) => updateNodeProp(node.id, 'filters', next), + }) + } + + const renderFilterEditor = ({ + scopeId, + filters, + columns, + strictColumns = false, + onChange, + }: { + scopeId: string + filters: DesignerDataSourceFilter[] + /** Column names offered as suggestions; empty when nothing was sampled. */ + columns: string[] + /** The columns are the complete list, so the field is picked rather than typed. */ + strictColumns?: boolean + onChange: (next: DesignerDataSourceFilter[]) => void + }) => { + const listId = `filter-columns-${scopeId}` + const writeFilters = onChange + const patchFilter = (id: string, updates: Partial) => + writeFilters(filters.map((filter) => (filter.id === id ? { ...filter, ...updates } : filter))) + + return ( +
+
+ + {translate('::App.DeveloperKitComponentDesigner.Filters')} + + +
+

+ {translate('::App.DeveloperKitComponentDesigner.FiltersHint')} +

+ + {columns.map((column) => ( + + {!filters.length && ( +

+ {translate('::App.DeveloperKitComponentDesigner.NoFilter')} +

+ )} +
+ {filters.map((filter) => { + const valueless = isValuelessDesignerFilter({ + ...filter, + operator: filter.operator as DesignerFilterOperator, + }) + const [masterRef = '', ...columnParts] = String(filter.value || '').split('.') + const masterColumn = columnParts.join('.') + const masterRecord = sqlDataSourceRecords[ + sqlContainerRefs.find((item) => item.ref === masterRef)?.id || '' + ] + const masterColumns = + masterRecord && typeof masterRecord === 'object' && !Array.isArray(masterRecord) + ? Object.keys(masterRecord as Record) + : [] + return ( +
+
+ {strictColumns ? ( + + ) : ( + patchFilter(filter.id, { field: event.target.value })} + /> + )} + + +
+ {!valueless && ( +
+ + {filter.source !== 'record' && ( + patchFilter(filter.id, { value: event.target.value })} + /> + )} +
+ )} + {/* The master reference needs a row of its own: the container and + the column it is read from are two picks, and squeezing them + next to the source selector left neither of them usable. */} + {!valueless && filter.source === 'record' && ( +
+ + + patchFilter(filter.id, { value: `${masterRef}.${event.target.value}` }) + } + /> + + {masterColumns.map((column) => ( + +
+ )} + {/* Both halves are needed; without the column the filter resolves + to nothing and silently stops filtering. */} + {!valueless && filter.source === 'record' && (!masterRef || !masterColumn) && ( +

+ {translate('::App.DeveloperKitComponentDesigner.FilterMasterIncomplete')} +

+ )} + {!valueless && filter.source !== 'static' && ( + + patchFilter(filter.id, { previewValue: event.target.value }) + } + /> + )} + +
+ ) + })} +
+
+ ) + } + /** * SqlDataSource command panel: the ASP.NET style Select/Insert/Update/Delete * slots. Each slot only accepts a data source declared with the matching HTTP @@ -3331,6 +3834,7 @@ const VisualComponentDesigner = () => { onChange={(event) => updateSelectedProp('keyParamName', event.target.value)} /> + {renderDataSourceFilters(selectSource)} {selectNeedsPreviewKey && (
@@ -3567,13 +4071,19 @@ const VisualComponentDesigner = () => { const renderDataInspector = () => { if (selectedNode?.kind === 'platform') { return ( -
- - {translate('::App.DeveloperKitComponentDesigner.PlatformDataModel')} - - {translate('::App.DeveloperKitComponentDesigner.PlatformDataModelHint', { - type: selectedNode.type, - })} +
+
+ + {translate('::App.DeveloperKitComponentDesigner.PlatformDataModel')} + + {translate('::App.DeveloperKitComponentDesigner.PlatformDataModelHint', { + type: selectedNode.type, + })} +
+ {/* The list form supplies the data; the filters narrow it, so a view can + follow a Form record or a page parameter without the list form being + duplicated for every case. */} + {renderPlatformViewFilters(selectedNode)}
) } @@ -3681,6 +4191,8 @@ const VisualComponentDesigner = () => {
+ {!sqlScopeLocked && renderDataSourceFilters(activeDataSource)} + {isOptionDataComponent(selectedNode?.type) && selectConfiguration} {isTabularDataComponent(selectedNode?.type) && @@ -4761,9 +5273,19 @@ const VisualComponentDesigner = () => { updateSelectedProp(property.name, value)} />
diff --git a/ui/src/views/list/Chart.tsx b/ui/src/views/list/Chart.tsx index 816e08b3..d59c2711 100644 --- a/ui/src/views/list/Chart.tsx +++ b/ui/src/views/list/Chart.tsx @@ -39,6 +39,8 @@ interface ChartProps extends CommonProps, Meta { refreshData?: () => Promise gridDto?: GridDto refreshGridDto?: () => Promise + /** Filter carrier of an embedded chart; see the same prop on List. */ + searchParams?: URLSearchParams } type LookupDisplayValues = Record> @@ -52,7 +54,9 @@ const Chart = (props: ChartProps) => { const isPwaMode = usePWA() const [initialized, setInitialized] = useState(false) - const [searchParams] = useSearchParams() + const [routeSearchParams] = useSearchParams() + // An embedded chart is filtered by its host; a standalone one by the page address. + const searchParams = props.searchParams ?? routeSearchParams const [chartOptions, setChartOptions] = useState() const { createSelectDataSource } = useListFormCustomDataSource({} as any) const { loadLookupDisplayValues } = useListFormColumns({ @@ -101,6 +105,12 @@ const Chart = (props: ChartProps) => { const [urlSearchParams, setUrlSearchParams] = useState( searchParams ? new URLSearchParams(searchParams) : new URLSearchParams(), ) + // The state is seeded once, so a host that refilters the chart — a designer page + // whose Form record changed — would otherwise keep querying with the filter the + // chart was mounted with. + useEffect(() => { + setUrlSearchParams(new URLSearchParams(searchParams ?? undefined)) + }, [searchParams]) const lookupColumns = useMemo( () => diff --git a/ui/src/views/list/List.tsx b/ui/src/views/list/List.tsx index 3d61f9b0..1beb879f 100644 --- a/ui/src/views/list/List.tsx +++ b/ui/src/views/list/List.tsx @@ -86,6 +86,12 @@ interface ListProps { gridDto?: GridDto isSubForm?: boolean onRefreshGridDto?: () => Promise | void + /** + * Filter carrier of a list embedded in a page. A standalone list reads the + * page address, but an embedded one is filtered by its host — the designer + * builds these from static values, the URL or a Form record. + */ + searchParams?: URLSearchParams } const List: React.FC = ({ @@ -93,10 +99,12 @@ const List: React.FC = ({ gridDto: externalGridDto, isSubForm = false, onRefreshGridDto, + searchParams: externalSearchParams, }) => { const { listFormCode: routeListFormCode = '' } = useParams() const listFormCode = externalListFormCode || routeListFormCode - const [searchParams] = useSearchParams() + const [routeSearchParams] = useSearchParams() + const searchParams = externalSearchParams ?? routeSearchParams const { translate } = useLocalization() const mode = useStoreState((state) => state.theme.mode)