diff --git a/api/src/Sozsoft.Platform.Application.Contracts/DeveloperKit/ICrudEndpointAppService.cs b/api/src/Sozsoft.Platform.Application.Contracts/DeveloperKit/ICrudEndpointAppService.cs index 25b02069..b211d18f 100644 --- a/api/src/Sozsoft.Platform.Application.Contracts/DeveloperKit/ICrudEndpointAppService.cs +++ b/api/src/Sozsoft.Platform.Application.Contracts/DeveloperKit/ICrudEndpointAppService.cs @@ -18,7 +18,11 @@ public interface ICrudEndpointAppService : ICrudAppService< /// Aktif baslatilacak operasyonlarin virgulle ayrilmis listesi /// (GetList, GetById, Create, Update, Delete). Bos birakilirsa hepsi aktif olur. /// - Task> GenerateCrudEndpointsAsync(string entityName, string operationTypes = null); + /// + /// Veritabani nesnesinin turu (table, view, procedure, function). Tablo disindaki + /// nesneler icin yalnizca okuma endpoint'leri (GetList, GetById) uretilir. + /// + Task> GenerateCrudEndpointsAsync(string entityName, string operationTypes = null, string objectType = null); Task DeleteByEntityAsync(string entityName); } diff --git a/api/src/Sozsoft.Platform.Application/DeveloperKit/CrudEndpointGenerateAppService.cs b/api/src/Sozsoft.Platform.Application/DeveloperKit/CrudEndpointGenerateAppService.cs index 5a1c1677..1dc74886 100644 --- a/api/src/Sozsoft.Platform.Application/DeveloperKit/CrudEndpointGenerateAppService.cs +++ b/api/src/Sozsoft.Platform.Application/DeveloperKit/CrudEndpointGenerateAppService.cs @@ -73,9 +73,10 @@ public class CrudEndpointGenerateAppService : CrudAppService< [Authorize(PlatformConsts.AppCodes.SqlQueryManagers.CrudEndpoints)] public virtual async Task> GenerateCrudEndpointsAsync( string entityName, - [FromQuery] string operationTypes = null) + [FromQuery] string operationTypes = null, + [FromQuery] string objectType = null) { - // Bes endpoint her zaman uretilir; operationTypes verilmisse yalnizca + // Tablolar icin bes endpoint uretilir; operationTypes verilmisse yalnizca // listedekiler aktif baslar, digerleri pasif olarak kaydedilir ve // kullanici diyalogdan sonradan acabilir. Bos/null ise hepsi aktiftir. var activeOperations = operationTypes @@ -85,6 +86,11 @@ public class CrudEndpointGenerateAppService : CrudAppService< bool IsActiveOperation(string operationType) => activeOperations == null || activeOperations.Count == 0 || activeOperations.Contains(operationType); + // View / stored procedure / function yazma islemlerini desteklemez; + // bu nesneler icin yalnizca okuma endpoint'leri (GetList, GetById) uretilir. + var isReadOnlyObject = !string.IsNullOrWhiteSpace(objectType) + && !string.Equals(objectType.Trim(), "table", StringComparison.OrdinalIgnoreCase); + // CRUD endpointleri oluştur var endpoints = new List { @@ -130,6 +136,13 @@ public class CrudEndpointGenerateAppService : CrudAppService< } }; + if (isReadOnlyObject) + { + endpoints = endpoints + .Where(x => x.OperationType is "GetList" or "GetById") + .ToList(); + } + // Var olanları sil var existingEndpoints = await Repository .GetListAsync(x => x.EntityName == entityName); diff --git a/api/src/Sozsoft.Platform.DbMigrator/Migrations/LanguagesData.json b/api/src/Sozsoft.Platform.DbMigrator/Migrations/LanguagesData.json index bb4f980b..0782cd25 100644 --- a/api/src/Sozsoft.Platform.DbMigrator/Migrations/LanguagesData.json +++ b/api/src/Sozsoft.Platform.DbMigrator/Migrations/LanguagesData.json @@ -1,4 +1,4 @@ -{ +{ "Languages": [ { "cultureName": "ar", @@ -19698,6 +19698,24 @@ "en": "The Data tab can only be used on components in the Data category.", "tr": "Data sekmesi yalnızca Data kategorisindeki komponentlerde kullanılabilir." }, + { + "resourceName": "Platform", + "key": "App.DeveloperKitComponentDesigner.DefaultValue", + "en": "Default value (new record)", + "tr": "Varsayılan değer (yeni kayıt)" + }, + { + "resourceName": "Platform", + "key": "App.DeveloperKitComponentDesigner.DefaultValueHint", + "en": "Written to the {column} column only when a new record is opened; an existing record keeps its own value. @today and @now are resolved at that moment.", + "tr": "Yalnızca yeni kayıt açıldığında {column} sütununa yazılır; mevcut kayıt kendi değerini korur. @today ve @now o anda hesaplanır." + }, + { + "resourceName": "Platform", + "key": "App.DeveloperKitComponentDesigner.NoDefaultValue", + "en": "No default value", + "tr": "Varsayılan değer yok" + }, { "resourceName": "Platform", "key": "App.DeveloperKitComponentDesigner.DefineAndRunSelect", diff --git a/api/src/Sozsoft.Platform.EntityFrameworkCore/DeveloperKit/PlatformDynamicEntityManager.cs b/api/src/Sozsoft.Platform.EntityFrameworkCore/DeveloperKit/PlatformDynamicEntityManager.cs index d65e84e7..8399134b 100644 --- a/api/src/Sozsoft.Platform.EntityFrameworkCore/DeveloperKit/PlatformDynamicEntityManager.cs +++ b/api/src/Sozsoft.Platform.EntityFrameworkCore/DeveloperKit/PlatformDynamicEntityManager.cs @@ -55,8 +55,9 @@ public class DynamicEntityManager : IDynamicEntityManager var hasIsDeleted = await ColumnExistsAsync(entityName, "IsDeleted"); var hasCreationTime = await ColumnExistsAsync(entityName, "CreationTime"); + var parameters = new List(); var columns = new List { QuoteIdentifier("Id", isPostgreSql) }; - var values = new List { $"'{newId}'" }; + var values = new List { AddParameter(parameters, newId) }; if (hasCreationTime) { columns.Add(QuoteIdentifier("CreationTime", isPostgreSql)); values.Add(UtcNowExpression(isPostgreSql)); } if (hasIsDeleted) { columns.Add(QuoteIdentifier("IsDeleted", isPostgreSql)); values.Add(FalseLiteral(isPostgreSql)); } @@ -67,12 +68,12 @@ public class DynamicEntityManager : IDynamicEntityManager continue; columns.Add(QuoteIdentifier(prop.Name, isPostgreSql)); - values.Add(FormatValueForSql(prop.Value, isPostgreSql)); + values.Add(AddParameter(parameters, ToSqlParameterValue(prop.Value))); } var insertQuery = $"INSERT INTO {QuoteIdentifier(entityName, isPostgreSql)} ({string.Join(", ", columns)}) VALUES ({string.Join(", ", values)})"; - await dbContext.Database.ExecuteSqlRawAsync(insertQuery); + await dbContext.Database.ExecuteSqlRawAsync(insertQuery, parameters.ToArray()); return await GetEntityByIdAsync(entityName, newId); } @@ -85,6 +86,7 @@ public class DynamicEntityManager : IDynamicEntityManager if (existing == null) return null; + var parameters = new List(); var setParts = new List(); var hasLastModification = await ColumnExistsAsync(entityName, "LastModificationTime"); @@ -96,12 +98,12 @@ public class DynamicEntityManager : IDynamicEntityManager if (prop.NameEquals("id") || prop.NameEquals("Id")) continue; - setParts.Add($"{QuoteIdentifier(prop.Name, isPostgreSql)} = {FormatValueForSql(prop.Value, isPostgreSql)}"); + setParts.Add($"{QuoteIdentifier(prop.Name, isPostgreSql)} = {AddParameter(parameters, ToSqlParameterValue(prop.Value))}"); } - var updateQuery = $"UPDATE {QuoteIdentifier(entityName, isPostgreSql)} SET {string.Join(", ", setParts)} WHERE {QuoteIdentifier("Id", isPostgreSql)} = '{id}'"; + var updateQuery = $"UPDATE {QuoteIdentifier(entityName, isPostgreSql)} SET {string.Join(", ", setParts)} WHERE {QuoteIdentifier("Id", isPostgreSql)} = {AddParameter(parameters, id)}"; - await dbContext.Database.ExecuteSqlRawAsync(updateQuery); + await dbContext.Database.ExecuteSqlRawAsync(updateQuery, parameters.ToArray()); return await GetEntityByIdAsync(entityName, id); } @@ -196,19 +198,37 @@ public class DynamicEntityManager : IDynamicEntityManager } } - private static string FormatValueForSql(JsonElement value, bool isPostgreSql) + /// + /// Registers a value and returns its placeholder. The value never reaches the + /// SQL text: ExecuteSqlRaw parses that text as a composite format + /// string, so an inlined JSON literal such as '{}' is read as a format + /// hole and throws a FormatException before the command is ever built. + /// Parameterising is also what keeps the column values injection safe. + /// + private static string AddParameter(List parameters, object? value) + { + parameters.Add(value ?? DBNull.Value); + return "{" + (parameters.Count - 1).ToString(System.Globalization.CultureInfo.InvariantCulture) + "}"; + } + + /// + /// JSON value → parameter value. An object or an array is handed over as its + /// raw JSON text, which is exactly what a JSON column (ExtraProperties) holds; + /// it used to be discarded as NULL. + /// + private static object? ToSqlParameterValue(JsonElement value) { return value.ValueKind switch { - JsonValueKind.Number when value.TryGetInt64(out var l) => l.ToString(), - JsonValueKind.Number when value.TryGetDecimal(out var d) => d.ToString(System.Globalization.CultureInfo.InvariantCulture), - JsonValueKind.True => TrueLiteral(isPostgreSql), - JsonValueKind.False => FalseLiteral(isPostgreSql), - JsonValueKind.Null => "NULL", - JsonValueKind.String when value.TryGetGuid(out var g) => $"'{g}'", - JsonValueKind.String when value.TryGetDateTime(out var dt) => $"'{dt:yyyy-MM-dd HH:mm:ss}'", - JsonValueKind.String => $"'{value.GetString()?.Replace("'", "''")}'" , - _ => "NULL", + JsonValueKind.Number when value.TryGetInt64(out var l) => l, + JsonValueKind.Number when value.TryGetDecimal(out var d) => d, + JsonValueKind.True => true, + JsonValueKind.False => false, + JsonValueKind.Null or JsonValueKind.Undefined => null, + JsonValueKind.String when value.TryGetGuid(out var g) => g, + JsonValueKind.String when value.TryGetDateTime(out var dt) => dt, + JsonValueKind.String => value.GetString(), + _ => value.GetRawText(), }; } diff --git a/configs/seeds/host/custom/RoleComponent.json b/configs/seeds/host/custom/RoleComponent.json index c67f438e..69c1fa7c 100644 --- a/configs/seeds/host/custom/RoleComponent.json +++ b/configs/seeds/host/custom/RoleComponent.json @@ -1,11 +1,11 @@ { - "GeneratedAt": "2026-08-18T09:36:07.2745686Z", + "GeneratedAt": "2026-08-18T11:06:39.5304984Z", "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%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%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 RoleList = () => {\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 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 setData_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 return (\n <>\n \n {\"Rol List\"}\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 RoleList", - "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\":\"\"},\"events\":{\"onChange\":\"\"},\"bindings\":{\"value\":{\"sourceId\":\"cmp_msyfmegt_jkkn3\",\"path\":\"Name\"}},\"children\":[],\"ref\":\"input1\"}],\"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_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\":\"\"}]}}", "Description": "", "IsActive": true, "Dependencies": [], diff --git a/ui/src/components/visualDesigner/VisualCanvas.tsx b/ui/src/components/visualDesigner/VisualCanvas.tsx index 7bf826d4..07be8da0 100644 --- a/ui/src/components/visualDesigner/VisualCanvas.tsx +++ b/ui/src/components/visualDesigner/VisualCanvas.tsx @@ -9,6 +9,7 @@ import { formatLocaleValue } from '@/utils/localeFormat' import { FaArrowDown, FaArrowUp, FaClone, FaGripVertical, FaTrash } from 'react-icons/fa' import { beginDesignerDragSoon, endDesignerDrag, useDesignerDrag } from './designerDrag' import { + buildSqlDefaultRecord, fromDesignerDate, getDesignerBindingColumns, getDesignerCollectionProperty, @@ -16,6 +17,7 @@ import { getDesignerValueByPath, getSqlDataSourceEndpointId, getSqlDataSourceKeyField, + getSqlFormValueProperty, isDesignerDateComponent, isDesignerDateProperty, isDesignerOptionComponent, @@ -25,6 +27,8 @@ import { resolveDesignerDropdownTitle, resolveDesignerTabValue, resolveSqlDataSourceRows, + setDesignerRecordField, + SQL_DEFAULT_VALUE_PROP, toDesignerDate, type DesignerBinding, type DesignerNode, @@ -465,11 +469,7 @@ const getTabOptions = (props: Record) => /** Prop of a node that a SqlDataSource scope is allowed to write back to. */ const getFormScopeProperty = (node: DesignerNode, formScope?: DesignerFormScope) => - formScope - ? (['value', 'checked'] as const).find( - (propertyName) => node.bindings?.[propertyName]?.sourceId === formScope.sourceId, - ) - : undefined + getSqlFormValueProperty(node, formScope?.sourceId) const getPreviewProps = ( node: DesignerNode, @@ -483,6 +483,9 @@ const getPreviewProps = ( ) => { const props: Record = {} Object.entries(node.props).forEach(([key, value]) => { + // Design time metadata, not a component prop: it seeds a new record instead + // of being rendered. + if (key === SQL_DEFAULT_VALUE_PROP) return if (key === 'children' || key === 'html' || (value === '' && key !== 'value')) return if (key.startsWith('on') && typeof value === 'string') return props[key] = resolveStaticLanguageKeys(value, translate) @@ -802,24 +805,19 @@ const GridDataTablePreview = ({ ) } -/** Deep set on a dot path, used by both the canvas and the generated runtime. */ -const setRecordField = ( - record: Record, - path: string, - value: unknown, -): Record => { - const keys = path.split('.').filter(Boolean) - if (!keys.length) return record - const next = { ...record } - let target = next - for (const key of keys.slice(0, -1)) { - const child = target[key] - const branch = child && typeof child === 'object' && !Array.isArray(child) ? { ...child } : {} - target[key] = branch - target = branch as Record - } - target[keys[keys.length - 1]] = value - return next +/** + * A new record starts at the top of the form: the first enabled editor takes the + * cursor once React has painted the emptied fields. + */ +const focusFirstFormField = (host: React.RefObject) => { + window.requestAnimationFrame(() => { + const field = host.current?.querySelector( + "input:not([type='hidden']):not([disabled]):not([readonly]), textarea:not([disabled]):not([readonly]), select:not([disabled])", + ) + if (!field) return + field.focus() + if (field instanceof HTMLInputElement || field instanceof HTMLTextAreaElement) field.select() + }) } const SqlDataSourceView = ({ @@ -844,6 +842,8 @@ const SqlDataSourceView = ({ () => (selectId ? resolveSqlDataSourceRows(dataValues[selectId], collectionPath) : []), [collectionPath, dataValues, selectId], ) + // Container element, so New can hand the cursor to the first field of the form. + const hostRef = React.useRef(null) const [rowIndex, setRowIndex] = React.useState(0) const [mode, setMode] = React.useState('edit') // Holds the whole record while editing, so New mode can show an empty form @@ -864,7 +864,7 @@ const SqlDataSourceView = ({ () => ({ sourceId: node.id, onFieldChange: (path, value) => - setDraft((current) => setRecordField(current ?? activeRow, path, value)), + setDraft((current) => setDesignerRecordField(current ?? activeRow, path, value)), }), [activeRow, node.id], ) @@ -917,6 +917,7 @@ const SqlDataSourceView = ({ return (
@@ -996,8 +997,11 @@ const SqlDataSourceView = ({ 'plain', translate('::App.VisualDesignerCanvas.SelectInsertEndpoint'), () => { - setDraft({}) + // New mode starts from the defaults declared on the bound children, + // exactly like the generated runtime does. + setDraft(buildSqlDefaultRecord(node)) setMode('new') + focusFirstFormField(hostRef) }, )} {toolbarButton( diff --git a/ui/src/components/visualDesigner/codeGenerator.ts b/ui/src/components/visualDesigner/codeGenerator.ts index 95a54df5..857512b3 100644 --- a/ui/src/components/visualDesigner/codeGenerator.ts +++ b/ui/src/components/visualDesigner/codeGenerator.ts @@ -1,4 +1,5 @@ import { + collectSqlDefaultFields, getDesignerBindingColumns, getDesignerCollectionProperty, getDesignerTabSlotValue, @@ -7,6 +8,7 @@ import { getSqlDataSourceKeyParam, getSqlDataSourceKeySource, getSqlDataSourceScriptedEvents, + getSqlFormValueProperty, hasSqlDataSourceUrlParams, isDesignerDateComponent, isDesignerDateOnlyComponent, @@ -16,6 +18,7 @@ import { normalizeDesignerKeyList, DESIGNER_DROPDOWN_PLACEHOLDER, SQL_DATA_SOURCE_SLOTS, + SQL_DEFAULT_VALUE_PROP, type DesignerDataSource, type DesignerDocument, type DesignerNode, @@ -153,9 +156,7 @@ const getRuntimeStateSpec = (node: DesignerNode): RuntimeStateSpec | null => { /** Prop of a node that the enclosing SqlDataSource writes the record back from. */ const getFormScopeField = (node: DesignerNode, formScope?: FormScope) => { if (!formScope) return null - const propertyName = (['value', 'checked'] as const).find( - (name) => node.bindings?.[name]?.sourceId === formScope.sourceId, - ) + const propertyName = getSqlFormValueProperty(node, formScope.sourceId) if (!propertyName) return null return { propertyName, @@ -215,6 +216,8 @@ const propsToCode = ( ([key, value]) => key !== 'children' && key !== runtimeState?.propertyName && + // Seeds a new SqlDataSource record; it is not a prop of the component. + key !== SQL_DEFAULT_VALUE_PROP && !omitted.has(key) && !(node.type === 'Checkbox' && key === 'defaultChecked' && 'checked' in node.props) && !(hasBoundInputValue && !hasInputChangeHandler && key === 'readOnly') && @@ -314,6 +317,8 @@ const sqlIdentifiers = (node: DesignerNode) => { reload: `sqlReload_${identifier}`, refresh: `sqlRefresh_${identifier}`, create: `sqlNew_${identifier}`, + draft: `sqlDraft_${identifier}`, + host: `sqlHost_${identifier}`, save: `sqlSave_${identifier}`, remove: `sqlDelete_${identifier}`, previous: `sqlPrev_${identifier}`, @@ -403,7 +408,9 @@ const sqlDataSourceHooks = (node: DesignerNode, dataSources: DesignerDataSource[ const [${names.mode}, ${names.setMode}] = React.useState("edit") const [${names.index}, ${names.setIndex}] = React.useState(0) const [${names.busy}, ${names.setBusy}] = React.useState(false) - const [${names.error}, ${names.setError}] = React.useState("")${ + const [${names.error}, ${names.setError}] = React.useState("") + // Container element, so New can hand the cursor to the first field of the form. + const ${names.host} = React.useRef(null)${ hasSqlEvents(node) ? ` // Event scripts are declared after these hooks, so they read the record and the @@ -510,12 +517,17 @@ const sqlDataSourceHooks = (node: DesignerNode, dataSources: DesignerDataSource[ }, [${names.reload}]) const ${names.create} = React.useCallback(${sqlEventHandler(node, 'onNewRecord') ? 'async ' : ''}() => { ${names.setError}("") - ${names.setRecord}({}) + // Defaults are resolved on every New, so an @today column carries the day the + // record was opened rather than the day the component was generated. + const ${names.draft} = buildSqlDefaults(${JSON.stringify(collectSqlDefaultFields(node))}) + ${names.setRecord}(${names.draft}) ${names.setOriginal}({}) - ${names.setMode}("new")${ + ${names.setMode}("new") + // After the re-render that paints the empty form, not before it. + focusFirstSqlField(${names.host})${ sqlEventHandler(node, 'onNewRecord') ? ` - ${sqlEventCall(node, 'onNewRecord', '{ record: {} }')}` + ${sqlEventCall(node, 'onNewRecord', `{ record: ${names.draft} }`)}` : '' } }, []) @@ -647,7 +659,7 @@ ${indent('
', level + 1)}` const error = ` ${indent(`{${names.error} ?
{${names.error}}
: null}`, level + 1)}` - return `${indent(`
`, level)} + return `${indent(`
`, level)} ${children}${toolbar}${error} ${indent('
', level)}` } @@ -1553,6 +1565,27 @@ export const generateDesignerCode = (name: string, document: DesignerDocument) = if (!bound.keyBound && hasKey && target.method !== "POST") request.params = { [keyField]: keyValue } return apiService.fetchData(request) } + // A new record starts at the top of the form: the first enabled editor takes the + // cursor, so a Yeni click is immediately followed by typing. + const focusFirstSqlField = (host) => { + if (typeof window === "undefined") return + window.requestAnimationFrame(() => { + const field = host.current?.querySelector("input:not([type='hidden']):not([disabled]):not([readonly]), textarea:not([disabled]):not([readonly]), select:not([disabled])") + if (!field) return + field.focus() + if (typeof field.select === "function") field.select() + }) + } + // Defaults declared on the bound children; only a new record starts from them. + const buildSqlDefaults = (fields) => fields.reduce((record, field) => setSqlField(record, field.path, resolveSqlDefault(field)), {}) + const resolveSqlDefault = (field) => { + if (field.token !== "today" && field.token !== "now") return field.value + const now = new Date() + const pad = (value) => String(value).padStart(2, "0") + const day = now.getFullYear() + "-" + pad(now.getMonth() + 1) + "-" + pad(now.getDate()) + if (field.token === "today" || field.dateOnly) return day + return day + "T" + pad(now.getHours()) + ":" + pad(now.getMinutes()) + ":" + pad(now.getSeconds()) + } const toSqlErrorMessage = (error) => error?.response?.data?.error?.message || error?.response?.data?.message || error?.message || "İşlem tamamlanamadı."` : '' const sqlHooks = sqlDataSourceNodes diff --git a/ui/src/components/visualDesigner/types.ts b/ui/src/components/visualDesigner/types.ts index 090db180..4e8db157 100644 --- a/ui/src/components/visualDesigner/types.ts +++ b/ui/src/components/visualDesigner/types.ts @@ -734,3 +734,147 @@ export const normalizeDesignerKeyList = (value: unknown): string[] => { .map(String) .filter(Boolean) } + +/** + * Value a bound field starts with when the container opens a **new** record. It + * is kept on the child node — the component that owns the column binding — so it + * is edited where the field itself is configured, and it is never emitted as a + * React prop: an existing record always wins over it. + */ +export const SQL_DEFAULT_VALUE_PROP = 'sqlDefaultValue' + +/** Props a SqlDataSource scope reads from and writes back to the record. */ +export const SQL_FORM_VALUE_PROPERTIES = ['value', 'checked'] as const + +export type SqlFormValueProperty = (typeof SQL_FORM_VALUE_PROPERTIES)[number] + +/** Which prop of `node` is bound to the record of the SqlDataSource `sourceId`. */ +export const getSqlFormValueProperty = ( + node: DesignerNode, + sourceId?: string, +): SqlFormValueProperty | undefined => + sourceId + ? SQL_FORM_VALUE_PROPERTIES.find((name) => node.bindings?.[name]?.sourceId === sourceId) + : undefined + +/** Toggles carry their record value in `checked`; every other editor in `value`. */ +const SQL_CHECKED_COMPONENTS = new Set(['Checkbox', 'Switcher']) + +/** + * Prop of `node` that holds the record value. An existing binding decides it; an + * unbound node falls back to the prop it actually exposes, so a field can be + * configured before it has ever been bound. + */ +export const getSqlRecordProperty = ( + node: DesignerNode, + sourceId: string | undefined, + propertyNames: Set, +): SqlFormValueProperty | undefined => { + const bound = getSqlFormValueProperty(node, sourceId) + if (bound) return bound + const preferred: SqlFormValueProperty = SQL_CHECKED_COMPONENTS.has(node.type) + ? 'checked' + : 'value' + if (propertyNames.has(preferred)) return preferred + return SQL_FORM_VALUE_PROPERTIES.find((name) => propertyNames.has(name)) +} + +/** + * Defaults that cannot be stored as a literal because they depend on the moment + * the record is opened. Written as `@today` / `@now` in the property panel. + */ +export const SQL_DEFAULT_VALUE_TOKENS = ['today', 'now'] as const + +export type SqlDefaultValueToken = (typeof SQL_DEFAULT_VALUE_TOKENS)[number] + +const readSqlDefaultToken = (value: unknown): SqlDefaultValueToken | undefined => { + if (typeof value !== 'string' || !value.trim().startsWith('@')) return undefined + const token = value.trim().slice(1).toLocaleLowerCase('en') + return (SQL_DEFAULT_VALUE_TOKENS as readonly string[]).includes(token) + ? (token as SqlDefaultValueToken) + : undefined +} + +/** One record column seeded on New, in the form the generated runtime also uses. */ +export interface SqlDefaultField { + path: string + value: unknown + token?: SqlDefaultValueToken + dateOnly?: boolean +} + +/** Raw panel input → the value that actually reaches the record column. */ +const toSqlDefaultValue = (node: DesignerNode, property: SqlFormValueProperty, raw: unknown) => { + if (property === 'checked') return raw === true || raw === 'true' || raw === 1 || raw === '1' + if (typeof raw !== 'string') return raw + // A number input handed a string would send `"12"` to the endpoint, and the + // component itself renders it as text — both are wrong for a numeric column. + if (node.props?.type === 'number' && raw.trim() && !Number.isNaN(Number(raw))) { + return Number(raw) + } + return raw +} + +export const resolveSqlDefaultField = (field: SqlDefaultField): unknown => { + if (!field.token) return field.value + const now = new Date() + const day = new Date(now.getFullYear(), now.getMonth(), now.getDate()) + return fromDesignerDate( + field.token === 'today' ? day : now, + field.token === 'today' || field.dateOnly ? 'DatePicker' : 'DateTimepicker', + ) +} + +/** + * Every default declared by the descendants of a SqlDataSource. A nested + * container owns its own record, so its subtree is left to it. + */ +export const collectSqlDefaultFields = (node: DesignerNode): SqlDefaultField[] => { + const fields: SqlDefaultField[] = [] + const visit = (children: DesignerNode[]) => { + children.forEach((child) => { + if (isSqlDataSourceNode(child.type)) return + const property = getSqlFormValueProperty(child, node.id) + const raw = child.props?.[SQL_DEFAULT_VALUE_PROP] + const path = property ? String(child.bindings[property].path ?? '').trim() : '' + if (property && path && raw !== undefined && raw !== null && raw !== '') { + const token = readSqlDefaultToken(raw) + fields.push({ + path, + value: token ? null : toSqlDefaultValue(child, property, raw), + ...(token ? { token, dateOnly: isDesignerDateOnlyComponent(child.type) } : {}), + }) + } + visit(child.children || []) + }) + } + visit(node.children || []) + return fields +} + +/** Deep set on a dot path; shared by the canvas and the generated runtime. */ +export const setDesignerRecordField = ( + record: Record, + path: string, + value: unknown, +): Record => { + const keys = path.split('.').filter(Boolean) + if (!keys.length) return record + const next = { ...record } + let target = next + for (const key of keys.slice(0, -1)) { + const child = target[key] + const branch = child && typeof child === 'object' && !Array.isArray(child) ? { ...child } : {} + target[key] = branch + target = branch as Record + } + target[keys[keys.length - 1]] = value + return next +} + +/** Record a SqlDataSource opens New mode with. Empty when nothing declares one. */ +export const buildSqlDefaultRecord = (node: DesignerNode): Record => + collectSqlDefaultFields(node).reduce>( + (record, field) => setDesignerRecordField(record, field.path, resolveSqlDefaultField(field)), + {}, + ) diff --git a/ui/src/services/developerKit.service.ts b/ui/src/services/developerKit.service.ts index 458546fa..53f763da 100644 --- a/ui/src/services/developerKit.service.ts +++ b/ui/src/services/developerKit.service.ts @@ -78,15 +78,22 @@ class DeveloperKitService { /** * @param operationTypes Aktif baslatilacak operasyonlar; verilmezse hepsi aktif uretilir. * Listede olmayanlar pasif olarak kaydedilir. + * @param objectType Veritabani nesnesinin turu. Tablo disindaki nesneler icin + * (view/procedure/function) yalnizca GetList ve GetById uretilir. */ async generateCrudEndpoints( entityName: string, operationTypes?: string[], + objectType?: string, ): Promise> { + const params: Record = {} + if (operationTypes?.length) params.operationTypes = operationTypes.join(',') + if (objectType) params.objectType = objectType + const response = await apiService.fetchData>({ url: `/api/app/crud-endpoint-generate/generate-crud-endpoints/${entityName}`, method: 'POST', - params: operationTypes?.length ? { operationTypes: operationTypes.join(',') } : undefined, + params: Object.keys(params).length > 0 ? params : undefined, }) return response.data } diff --git a/ui/src/views/developerKit/CrudEndpointDialog.tsx b/ui/src/views/developerKit/CrudEndpointDialog.tsx index 58f5f195..06847f35 100644 --- a/ui/src/views/developerKit/CrudEndpointDialog.tsx +++ b/ui/src/views/developerKit/CrudEndpointDialog.tsx @@ -47,6 +47,15 @@ export const CRUD_OPERATION_TYPES = [ export type CrudOperationType = (typeof CRUD_OPERATION_TYPES)[number] +/** Endpoint uretilebilen SQL nesne turleri. */ +export type CrudObjectType = 'table' | 'view' + +/** + * View yazma islemlerini desteklemez; bu nesneler icin yalnizca okuma + * endpoint'leri uretilir. Backend de ayni kisiti objectType parametresiyle uygular. + */ +export const READONLY_OPERATION_TYPES: CrudOperationType[] = ['GetList', 'GetById'] + /** * Endpoint'lerde kullanilan entity adini turetir. * @@ -165,9 +174,11 @@ export const CrudEndpointBadges = ({ endpoints, buildTitle, onClick }: CrudEndpo interface CrudEndpointDialogProps { isOpen: boolean - /** Endpoint'leri yonetilecek tablo; entity adi tablo adindan turetilir. */ + /** Endpoint'leri yonetilecek tablo/view; entity adi nesne adindan turetilir. */ tableName: string schemaName?: string + /** Nesne turu; 'view' verildiginde yalnizca GetList/GetById uretilir. */ + objectType?: CrudObjectType /** Verilirse istek govdesi tablonun gercek kolonlarindan uretilir. */ dataSourceCode?: string | null onClose: () => void @@ -247,6 +258,7 @@ const CrudEndpointDialog = ({ isOpen, tableName, schemaName, + objectType = 'table', dataSourceCode, onClose, onChanged, @@ -255,6 +267,7 @@ const CrudEndpointDialog = ({ const { checkPermission } = usePermission() const canManageCrudEndpoints = checkPermission(CRUD_ENDPOINT_PERMISSION) const entityName = toEntityName(tableName) + const isReadOnlyObject = objectType === 'view' const [endpoints, setEndpoints] = useState([]) const [columns, setColumns] = useState([]) @@ -323,7 +336,7 @@ const CrudEndpointDialog = ({ // Govde sablonu icin tablo kolonlari; datasource bilinmiyorsa genel sablon kullanilir. useEffect(() => { - if (!isOpen || !dataSourceCode) { + if (!isOpen || !dataSourceCode || isReadOnlyObject) { setColumns([]) return } @@ -342,7 +355,7 @@ const CrudEndpointDialog = ({ return () => { cancelled = true } - }, [isOpen, dataSourceCode, schemaName, tableName]) + }, [isOpen, dataSourceCode, schemaName, tableName, isReadOnlyObject]) const sampleBodies = useMemo(() => { if (columns.length === 0) return { create: GENERIC_BODY, update: GENERIC_BODY } @@ -393,7 +406,7 @@ const CrudEndpointDialog = ({ const handleGenerate = async () => { setGenerating(true) try { - const result = await developerKitService.generateCrudEndpoints(entityName) + const result = await developerKitService.generateCrudEndpoints(entityName, undefined, objectType) publish(result.items || []) pushSuccess(translate('::App.Platform.OperationCompleted')) } catch (err) { @@ -541,7 +554,17 @@ const CrudEndpointDialog = ({ {schemaName ? `${schemaName}.${tableName}` : tableName} {' — '} {entityName} + {isReadOnlyObject && ( + + view + + )}

+ {isReadOnlyObject && ( +

+ {READONLY_OPERATION_TYPES.join(' / ')} +

+ )}
diff --git a/ui/src/views/developerKit/SqlObjectExplorer.tsx b/ui/src/views/developerKit/SqlObjectExplorer.tsx index 63d881d3..43923883 100644 --- a/ui/src/views/developerKit/SqlObjectExplorer.tsx +++ b/ui/src/views/developerKit/SqlObjectExplorer.tsx @@ -21,7 +21,12 @@ import { sqlObjectManagerService } from '@/services/sql-query-manager.service' import { useLocalization } from '@/utils/hooks/useLocalization' import Button from '@/components/ui/Button' import { Notification, toast } from '@/components/ui' -import { CrudEndpointBadges, getErrorMessage, toEntityName } from './CrudEndpointDialog' +import { + CrudEndpointBadges, + getErrorMessage, + toEntityName, + type CrudObjectType, +} from './CrudEndpointDialog' import { developerKitService } from '@/services/developerKit.service' type FolderKey = 'tables' | 'views' | 'procedures' | 'functions' @@ -44,10 +49,14 @@ interface SqlObjectExplorerProps { onDesignTable?: (schemaName: string, tableName: string) => void onNewTable?: () => void onSelectedObjectsChange?: (objects: SqlExplorerSelectedObject[]) => void - /** Entity adina gore acilmis CRUD endpoint'leri; tablo satirlarinda ikon olarak gosterilir. */ + /** Entity adina gore acilmis CRUD endpoint'leri; tablo/view satirlarinda ikon olarak gosterilir. */ crudEndpointsByEntity?: Map - onManageCrudEndpoints?: (schemaName: string, tableName: string) => void - /** Secili tablolar icin CRUD endpoint uretir; verilmezse buton gosterilmez. */ + onManageCrudEndpoints?: ( + schemaName: string, + objectName: string, + objectType: CrudObjectType, + ) => void + /** Secili tablo/view'ler icin CRUD endpoint uretir; verilmezse buton gosterilmez. */ onGenerateCrudEndpoints?: () => void isGeneratingCrudEndpoints?: boolean /** Kopyalama / dogrudan SQL diyalogunu acar; verilmezse buton gosterilmez. */ @@ -378,15 +387,14 @@ const SqlObjectExplorer = ({ // Non-blocking: object drop succeeded even if seed file cleanup fails. } - // Tablo dusurulduyse ona ait CRUD endpoint'leri de anlamsiz kalir; kayitlar + // Tablo/view dusurulduyse ona ait CRUD endpoint'leri de anlamsiz kalir; kayitlar // silinince backend crud/{EntityName}.json seed dosyasini da kaldirir. - const droppedTables = dropped.filter((node) => node.folder === 'tables') - if (droppedTables.length > 0) { - for (const node of droppedTables) { + const droppedCrudObjects = dropped.filter((node) => getCrudObjectType(node) !== null) + if (droppedCrudObjects.length > 0) { + for (const node of droppedCrudObjects) { try { - const table = node.data as DatabaseTableDto await developerKitService.deleteGeneratedEndpointsByEntity( - toEntityName(table.tableName), + toEntityName(getCrudObjectName(node).objectName), ) } catch { // Non-blocking: izin yoksa ya da endpoint yoksa drop yine basarilidir. @@ -439,10 +447,32 @@ const SqlObjectExplorer = ({ return } + /** + * Endpoint uretilebilen nesneler: tablolar (tam CRUD) ve view'ler (GetList/GetById). + * Stored procedure ve function calisma zamaninda desteklenmedigi icin disaridadir. + */ + const getCrudObjectType = (node: TreeNode): CrudObjectType | null => { + if (node.type !== 'object') return null + if (node.folder === 'tables') return 'table' + if (node.folder === 'views') return 'view' + return null + } + + /** Tablo/view dugumunun sema + nesne adi; endpoint yonetimi bu ikiliyle calisir. */ + const getCrudObjectName = (node: TreeNode): { schemaName: string; objectName: string } => + node.folder === 'tables' + ? { + schemaName: (node.data as DatabaseTableDto).schemaName, + objectName: (node.data as DatabaseTableDto).tableName, + } + : { + schemaName: (node.data as SqlNativeObjectDto).schemaName, + objectName: (node.data as SqlNativeObjectDto).objectName, + } + const getNodeCrudEndpoints = (node: TreeNode): CrudEndpoint[] => { - if (node.type !== 'object' || node.folder !== 'tables' || !crudEndpointsByEntity) return [] - const table = node.data as DatabaseTableDto - return crudEndpointsByEntity.get(toEntityName(table.tableName)) ?? [] + if (!crudEndpointsByEntity || !getCrudObjectType(node)) return [] + return crudEndpointsByEntity.get(toEntityName(getCrudObjectName(node).objectName)) ?? [] } const buildCrudEndpointTitle = (endpoint: CrudEndpoint) => @@ -491,8 +521,10 @@ const SqlObjectExplorer = ({ onClick={ onManageCrudEndpoints ? () => { - const table = node.data as DatabaseTableDto - onManageCrudEndpoints(table.schemaName, table.tableName) + const objectType = getCrudObjectType(node) + if (!objectType) return + const { schemaName, objectName } = getCrudObjectName(node) + onManageCrudEndpoints(schemaName, objectName, objectType) } : undefined } @@ -522,9 +554,9 @@ const SqlObjectExplorer = ({ const filteredTree = filterTree(treeData, filterText) - /** CRUD endpoint uretimi yalnizca tablolar icin anlamli. */ - const selectedTableCount = getSelectedNodes(treeData, selectedObjectIds).filter( - (node) => node.folder === 'tables', + /** CRUD endpoint uretimi tablolar ve view'ler icin anlamli. */ + const selectedCrudObjectCount = getSelectedNodes(treeData, selectedObjectIds).filter( + (node) => getCrudObjectType(node) !== null, ).length useEffect(() => { @@ -547,6 +579,7 @@ const SqlObjectExplorer = ({ const ctxNode = contextMenu.node const isTableObj = ctxNode?.type === 'object' && ctxNode.folder === 'tables' const isNativeObj = ctxNode?.type === 'object' && ctxNode.folder !== 'tables' + const ctxCrudObjectType = ctxNode ? getCrudObjectType(ctxNode) : null const isTablesDir = ctxNode?.id === 'tables' const isViewsDir = ctxNode?.id === 'views' const isProcsDir = ctxNode?.id === 'procedures' @@ -599,7 +632,7 @@ const SqlObjectExplorer = ({ {translate('::App.SqlQueryManager.DropSelected')} )} - {onGenerateCrudEndpoints && selectedTableCount > 0 && ( + {onGenerateCrudEndpoints && selectedCrudObjectCount > 0 && ( + ))} +
+ ) } @@ -3498,7 +3589,7 @@ const VisualComponentDesigner = () => { {isOptionDataComponent(selectedNode?.type) && selectConfiguration} {isTabularDataComponent(selectedNode?.type) && renderTabularDataConfiguration()} {isTabularDataComponent(selectedNode?.type) && renderTabularColumnConfiguration()} - {renderSqlRecordValueBinding()} + {renderSqlRecordField()} ) } @@ -3511,6 +3602,7 @@ const VisualComponentDesigner = () => {
{translate('::App.DeveloperKitComponentDesigner.NoSelectableGetEndpoint')}
+ {renderSqlRecordField()} ) } @@ -3812,7 +3904,7 @@ const VisualComponentDesigner = () => { )} )} - {renderSqlRecordValueBinding()} + {renderSqlRecordField()} ) }