Custom Component Default Value özelliği

This commit is contained in:
Sedat ÖZTÜRK 2026-08-18 14:48:16 +03:00
parent 3aa74318c2
commit 010594ac27
13 changed files with 555 additions and 147 deletions

View file

@ -18,7 +18,11 @@ public interface ICrudEndpointAppService : ICrudAppService<
/// Aktif baslatilacak operasyonlarin virgulle ayrilmis listesi
/// (GetList, GetById, Create, Update, Delete). Bos birakilirsa hepsi aktif olur.
/// </param>
Task<PagedResultDto<CrudEndpointDto>> GenerateCrudEndpointsAsync(string entityName, string operationTypes = null);
/// <param name="objectType">
/// Veritabani nesnesinin turu (table, view, procedure, function). Tablo disindaki
/// nesneler icin yalnizca okuma endpoint'leri (GetList, GetById) uretilir.
/// </param>
Task<PagedResultDto<CrudEndpointDto>> GenerateCrudEndpointsAsync(string entityName, string operationTypes = null, string objectType = null);
Task DeleteByEntityAsync(string entityName);
}

View file

@ -73,9 +73,10 @@ public class CrudEndpointGenerateAppService : CrudAppService<
[Authorize(PlatformConsts.AppCodes.SqlQueryManagers.CrudEndpoints)]
public virtual async Task<PagedResultDto<CrudEndpointDto>> 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<CrudEndpoint>
{
@ -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);

View file

@ -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",

View file

@ -55,8 +55,9 @@ public class DynamicEntityManager : IDynamicEntityManager
var hasIsDeleted = await ColumnExistsAsync(entityName, "IsDeleted");
var hasCreationTime = await ColumnExistsAsync(entityName, "CreationTime");
var parameters = new List<object>();
var columns = new List<string> { QuoteIdentifier("Id", isPostgreSql) };
var values = new List<string> { $"'{newId}'" };
var values = new List<string> { 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<object>();
var setParts = new List<string>();
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)
/// <summary>
/// Registers a value and returns its placeholder. The value never reaches the
/// SQL text: <c>ExecuteSqlRaw</c> parses that text as a composite format
/// string, so an inlined JSON literal such as <c>'{}'</c> 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.
/// </summary>
private static string AddParameter(List<object> parameters, object? value)
{
parameters.Add(value ?? DBNull.Value);
return "{" + (parameters.Count - 1).ToString(System.Globalization.CultureInfo.InvariantCulture) + "}";
}
/// <summary>
/// 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.
/// </summary>
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(),
};
}

File diff suppressed because one or more lines are too long

View file

@ -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<string, unknown>) =>
/** 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<string, unknown> = {}
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<string, unknown>,
path: string,
value: unknown,
): Record<string, unknown> => {
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<string, unknown>
}
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<HTMLElement | null>) => {
window.requestAnimationFrame(() => {
const field = host.current?.querySelector<HTMLElement>(
"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<HTMLDivElement | null>(null)
const [rowIndex, setRowIndex] = React.useState(0)
const [mode, setMode] = React.useState<SqlDataSourceMode>('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 (
<div
ref={hostRef}
className={String(node.props.className || '')}
style={{ display: 'flex', flexDirection: 'column', gap: Number(node.props.gap) || 0 }}
>
@ -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(

View file

@ -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('</div>', level + 1)}`
const error = `
${indent(`{${names.error} ? <div className="rounded-md bg-red-50 px-3 py-2 text-xs text-red-700 dark:bg-red-950 dark:text-red-200">{${names.error}}</div> : null}`, level + 1)}`
return `${indent(`<div className=${className} style={${style}}>`, level)}
return `${indent(`<div ref={${names.host}} className=${className} style={${style}}>`, level)}
${children}${toolbar}${error}
${indent('</div>', 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

View file

@ -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<string>,
): 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<string, unknown>,
path: string,
value: unknown,
): Record<string, unknown> => {
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<string, unknown>
}
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<string, unknown> =>
collectSqlDefaultFields(node).reduce<Record<string, unknown>>(
(record, field) => setDesignerRecordField(record, field.path, resolveSqlDefaultField(field)),
{},
)

View file

@ -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<PagedResultDto<CrudEndpoint>> {
const params: Record<string, string> = {}
if (operationTypes?.length) params.operationTypes = operationTypes.join(',')
if (objectType) params.objectType = objectType
const response = await apiService.fetchData<PagedResultDto<CrudEndpoint>>({
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
}

View file

@ -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<CrudEndpoint[]>([])
const [columns, setColumns] = useState<DatabaseColumnDto[]>([])
@ -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 && (
<span className="ml-2 rounded border border-purple-200 bg-purple-50 px-1.5 py-0.5 text-[10px] font-semibold uppercase text-purple-700 dark:border-purple-800 dark:bg-purple-900/30 dark:text-purple-300">
view
</span>
)}
</p>
{isReadOnlyObject && (
<p className="mt-1 mb-0 text-xs text-gray-500 dark:text-gray-400">
{READONLY_OPERATION_TYPES.join(' / ')}
</p>
)}
</div>
<div className="min-h-[180px] max-h-[55vh] overflow-y-auto">

View file

@ -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<string, CrudEndpoint[]>
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 <FaColumns className="text-gray-400" />
}
/**
* 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')}
</Button>
)}
{onGenerateCrudEndpoints && selectedTableCount > 0 && (
{onGenerateCrudEndpoints && selectedCrudObjectCount > 0 && (
<Button
size="xs"
variant="plain"
@ -697,14 +730,14 @@ const SqlObjectExplorer = ({
</Button>
)}
{isTableObj && onManageCrudEndpoints && (
{ctxCrudObjectType && onManageCrudEndpoints && (
<Button
variant="plain"
shape="none"
className="w-full !h-auto !justify-start !rounded-none !px-4 !py-2 text-left hover:bg-gray-100 dark:hover:bg-gray-700 text-sm flex items-center gap-2"
onClick={() => {
const t = ctxNode!.data as DatabaseTableDto
onManageCrudEndpoints(t.schemaName, t.tableName)
const { schemaName, objectName } = getCrudObjectName(ctxNode!)
onManageCrudEndpoints(schemaName, objectName, ctxCrudObjectType)
closeCtx()
}}
>

View file

@ -30,6 +30,7 @@ import CrudEndpointDialog, {
getErrorMessage,
groupEndpointsByEntity,
toEntityName,
type CrudObjectType,
} from './CrudEndpointDialog'
import { usePermission } from '@/utils/hooks/usePermission'
@ -96,9 +97,10 @@ const SqlQueryManager = () => {
const [copyDialogMode, setCopyDialogMode] = useState<'objects' | 'sql'>('objects')
const [sqlScriptForCopy, setSqlScriptForCopy] = useState('')
const [crudEndpoints, setCrudEndpoints] = useState<CrudEndpoint[]>([])
const [crudDialogTable, setCrudDialogTable] = useState<{
const [crudDialogObject, setCrudDialogObject] = useState<{
schemaName: string
tableName: string
objectName: string
objectType: CrudObjectType
} | null>(null)
const [isGeneratingCrud, setIsGeneratingCrud] = useState(false)
@ -154,26 +156,37 @@ const SqlQueryManager = () => {
setCrudEndpoints((prev) => [...prev.filter((ep) => ep.entityName !== entityName), ...endpoints])
}, [])
const selectedExplorerTables = useMemo(
() => selectedExplorerObjects.filter((obj) => obj.objectType === 'table'),
/**
* Endpoint uretilebilen secimler: tablolar (tam CRUD) ve view'ler (GetList/GetById).
* Stored procedure ve function calisma zamaninda desteklenmedigi icin disaridadir.
*/
const selectedCrudObjects = useMemo(
() =>
selectedExplorerObjects.filter(
(obj) => obj.objectType === 'table' || obj.objectType === 'view',
),
[selectedExplorerObjects],
)
/** Explorer'da secili tum tablolar icin CRUD endpoint'lerini uretir. */
/** Explorer'da secili tum tablo/view'ler icin CRUD endpoint'lerini uretir. */
const handleGenerateCrudEndpoints = async () => {
if (!canManageCrudEndpoints || selectedExplorerTables.length === 0) return
if (!canManageCrudEndpoints || selectedCrudObjects.length === 0) return
setIsGeneratingCrud(true)
try {
const failures: string[] = []
for (const table of selectedExplorerTables) {
const entityName = toEntityName(table.objectName)
for (const object of selectedCrudObjects) {
const entityName = toEntityName(object.objectName)
try {
const result = await developerKitService.generateCrudEndpoints(entityName)
const result = await developerKitService.generateCrudEndpoints(
entityName,
undefined,
object.objectType,
)
applyEntityEndpoints(entityName, result.items || [])
} catch (error) {
failures.push(`${table.fullName}: ${getErrorMessage(error, entityName)}`)
failures.push(`${object.fullName}: ${getErrorMessage(error, entityName)}`)
}
}
@ -188,7 +201,7 @@ const SqlQueryManager = () => {
>
{failures.length > 0
? failures.join(' | ')
: `${translate('::App.Platform.OperationCompleted')} (${selectedExplorerTables.length})`}
: `${translate('::App.Platform.OperationCompleted')} (${selectedCrudObjects.length})`}
</Notification>,
{ placement: 'bottom-end' },
)
@ -197,9 +210,12 @@ const SqlQueryManager = () => {
}
}
const handleManageCrudEndpoints = useCallback((schemaName: string, tableName: string) => {
setCrudDialogTable({ schemaName, tableName })
}, [])
const handleManageCrudEndpoints = useCallback(
(schemaName: string, objectName: string, objectType: CrudObjectType) => {
setCrudDialogObject({ schemaName, objectName, objectType })
},
[],
)
const handleDataSourceChange = useCallback((dataSource: DataSourceDto) => {
setState((prev) => ({
@ -1273,13 +1289,14 @@ GO`,
)}
{/* CRUD Endpoint Dialog */}
{crudDialogTable && canManageCrudEndpoints && (
{crudDialogObject && canManageCrudEndpoints && (
<CrudEndpointDialog
isOpen
schemaName={crudDialogTable.schemaName}
tableName={crudDialogTable.tableName}
schemaName={crudDialogObject.schemaName}
tableName={crudDialogObject.objectName}
objectType={crudDialogObject.objectType}
dataSourceCode={state.selectedDataSource}
onClose={() => setCrudDialogTable(null)}
onClose={() => setCrudDialogObject(null)}
onChanged={applyEntityEndpoints}
/>
)}

View file

@ -72,6 +72,9 @@ import {
getSqlDataSourceKeySource,
getSqlDataSourcePreviewKey,
getSqlDataSourceRecord,
getSqlRecordProperty,
isDesignerDateComponent,
isDesignerDateOnlyComponent,
isDesignerOptionComponent,
isDesignerTabularComponent,
isSqlDataSourceNode,
@ -84,6 +87,8 @@ import {
DESIGNER_HTTP_METHODS,
SQL_DATA_SOURCE_KEY_SOURCES,
SQL_DATA_SOURCE_SLOTS,
SQL_DEFAULT_VALUE_PROP,
SQL_DEFAULT_VALUE_TOKENS,
type DesignerComponentDefinition,
type DesignerBinding,
type DesignerDataSource,
@ -91,6 +96,7 @@ import {
type DesignerHttpMethod,
type DesignerNode,
type DesignerPropertyInfo,
type SqlFormValueProperty,
} from '@/components/visualDesigner/types'
import { useLocalization } from '@/utils/hooks/useLocalization'
@ -3421,54 +3427,139 @@ const VisualComponentDesigner = () => {
}
/**
* Value binding for an option component inside a SqlDataSource. Its option list
* still comes from its own GET endpoint above; this only says which record
* column the selection reads from and writes back to.
* Every column the record can offer, including the ones a sampled row cannot
* reveal: an object column (`extraProperties`) has no leaf to discover, and a
* column added after the last Select run is not in the sample at all. Both stay
* typeable, so a field is never blocked by a stale or empty sample.
*/
const renderSqlRecordValueBinding = () => {
if (!sqlScopeNode || !selectedNode) return null
if (!['AutoComplete', 'Radio.Group', 'Select'].includes(selectedNode.type)) return null
const record = sqlDataSourceRecords[sqlScopeNode.id]
const columns =
record === undefined
? []
: discoverDataFields(record).filter((field) => !['array', 'object'].includes(field.type))
const binding = selectedNode.bindings?.value
const current = binding?.sourceId === sqlScopeNode.id ? binding.path : ''
const sqlRecordColumns = useMemo(() => {
const record = sqlScopeNode ? sqlDataSourceRecords[sqlScopeNode.id] : undefined
if (!record || typeof record !== 'object') return []
const columns = new Map<string, string>()
Object.entries(record as Record<string, unknown>).forEach(([path, value]) =>
columns.set(path, getDataValueType(value)),
)
discoverDataFields(record)
.filter((field) => field.type !== 'array')
.forEach((field) => columns.set(field.path, field.type))
return [...columns.entries()]
.map(([path, type]) => ({ path, type }))
.sort((left, right) => left.path.localeCompare(right.path))
}, [sqlDataSourceRecords, sqlScopeNode])
/**
* The prop a SqlDataSource reads from and writes back to. An already bound node
* keeps its prop; an unbound one is offered the prop it actually carries, which
* is `checked` for the toggles and `value` for every other editor.
*/
const sqlRecordProperty = useMemo<SqlFormValueProperty | undefined>(() => {
if (!sqlScopeNode || !selectedNode) return undefined
const names = new Set((selectedDefinition?.properties || []).map((property) => property.name))
return getSqlRecordProperty(selectedNode, sqlScopeNode.id, names)
}, [selectedDefinition, selectedNode, sqlScopeNode])
/**
* Record column and its new-record default, for anything editable dropped into
* a SqlDataSource. The column is a free text field backed by a datalist rather
* than a dropdown: the sampled row is a convenience, not the list of columns
* that exist.
*/
const renderSqlRecordField = () => {
if (!sqlScopeNode || !selectedNode || !sqlRecordProperty) return null
const binding = selectedNode.bindings?.[sqlRecordProperty]
const column = binding?.sourceId === sqlScopeNode.id ? binding.path : ''
const rawDefault = selectedNode.props?.[SQL_DEFAULT_VALUE_PROP]
const currentDefault = rawDefault === undefined || rawDefault === null ? '' : String(rawDefault)
const columnListId = `sql-columns-${sqlScopeNode.id}`
// A picker gets a real date editor, unless it holds a token — `@today` is not
// a date the browser can render, and typing it needs a plain text field.
const isDateDefault = isDesignerDateComponent(selectedNode.type) && !currentDefault.startsWith('@')
const defaultInputType = isDateDefault
? isDesignerDateOnlyComponent(selectedNode.type)
? 'date'
: 'datetime-local'
: selectedNode.props?.type === 'number'
? 'number'
: 'text'
return (
<div className="rounded-lg border border-slate-200 p-3 dark:border-slate-800">
<div className="mb-1 text-[10px] font-semibold uppercase tracking-wider text-slate-400">
{translate('::App.DeveloperKitComponentDesigner.RecordField')}
</div>
<p className="mb-2 text-[10px] leading-4 text-slate-500">
{translate('::App.DeveloperKitComponentDesigner.RecordFieldHint')}
</p>
<select
className="w-full rounded-md border border-slate-300 bg-white px-2 py-2 text-[10px] dark:border-slate-700 dark:bg-slate-900"
value={current}
onChange={(event) =>
updateSelectedBinding(
'value',
event.target.value ? sqlScopeNode.id : '',
event.target.value,
)
}
>
<option value="">
{translate('::App.DeveloperKitComponentDesigner.DoNotBindToRecord')}
</option>
{columns.map((field) => (
<option key={field.path} value={field.path}>
{field.path} · {field.type}
</option>
))}
</select>
{!columns.length && (
<p className="mt-1.5 text-[10px] leading-4 text-amber-600">
{translate('::App.DeveloperKitComponentDesigner.DefineAndRunSelect')}
<div className="space-y-3 rounded-lg border border-slate-200 p-3 dark:border-slate-800">
<div>
<div className="mb-1 text-[10px] font-semibold uppercase tracking-wider text-slate-400">
{translate('::App.DeveloperKitComponentDesigner.RecordField')} · {sqlRecordProperty}
</div>
<p className="mb-2 text-[10px] leading-4 text-slate-500">
{translate('::App.DeveloperKitComponentDesigner.RecordFieldHint')}
</p>
)}
<input
className="w-full rounded-md border border-slate-300 bg-white px-2 py-2 text-[10px] dark:border-slate-700 dark:bg-slate-900"
list={columnListId}
placeholder={translate('::App.DeveloperKitComponentDesigner.DoNotBindToRecord')}
value={column}
onChange={(event) => {
const path = event.target.value.trim()
updateSelectedBinding(sqlRecordProperty, path ? sqlScopeNode.id : '', path)
}}
/>
<datalist id={columnListId}>
{sqlRecordColumns.map((column) => (
<option key={column.path} label={column.type} value={column.path} />
))}
</datalist>
{!sqlRecordColumns.length && (
<p className="mt-1.5 text-[10px] leading-4 text-amber-600">
{translate('::App.DeveloperKitComponentDesigner.DefineAndRunSelect')}
</p>
)}
</div>
<div>
<div className="mb-1 text-[10px] font-semibold uppercase tracking-wider text-slate-400">
{translate('::App.DeveloperKitComponentDesigner.DefaultValue')}
</div>
<p className="mb-2 text-[10px] leading-4 text-slate-500">
{translate('::App.DeveloperKitComponentDesigner.DefaultValueHint', {
column: column || '—',
})}
</p>
{sqlRecordProperty === 'checked' ? (
<select
className="w-full rounded-md border border-slate-300 bg-white px-2 py-2 text-[10px] disabled:opacity-50 dark:border-slate-700 dark:bg-slate-900"
disabled={!column}
value={currentDefault}
onChange={(event) => updateSelectedProp(SQL_DEFAULT_VALUE_PROP, event.target.value)}
>
<option value="">
{translate('::App.DeveloperKitComponentDesigner.NoDefaultValue')}
</option>
<option value="true">{translate('::App.Platform.Yes')}</option>
<option value="false">{translate('::App.Platform.No')}</option>
</select>
) : (
<input
className="w-full rounded-md border border-slate-300 bg-white px-2 py-2 text-[10px] disabled:opacity-50 dark:border-slate-700 dark:bg-slate-900"
disabled={!column}
placeholder={translate('::App.DeveloperKitComponentDesigner.NoDefaultValue')}
type={defaultInputType}
value={
defaultInputType === 'datetime-local' ? currentDefault.slice(0, 16) : currentDefault
}
onChange={(event) => updateSelectedProp(SQL_DEFAULT_VALUE_PROP, event.target.value)}
/>
)}
<div className="mt-1.5 flex flex-wrap gap-1">
{SQL_DEFAULT_VALUE_TOKENS.map((token) => (
<button
key={token}
className="rounded border border-slate-300 px-1.5 py-0.5 font-mono text-[9px] text-slate-600 hover:border-sky-400 hover:text-sky-700 disabled:opacity-40 dark:border-slate-700 dark:text-slate-300"
disabled={!column || sqlRecordProperty === 'checked'}
type="button"
onClick={() => updateSelectedProp(SQL_DEFAULT_VALUE_PROP, `@${token}`)}
>
@{token}
</button>
))}
</div>
</div>
</div>
)
}
@ -3498,7 +3589,7 @@ const VisualComponentDesigner = () => {
{isOptionDataComponent(selectedNode?.type) && selectConfiguration}
{isTabularDataComponent(selectedNode?.type) && renderTabularDataConfiguration()}
{isTabularDataComponent(selectedNode?.type) && renderTabularColumnConfiguration()}
{renderSqlRecordValueBinding()}
{renderSqlRecordField()}
</div>
)
}
@ -3511,6 +3602,7 @@ const VisualComponentDesigner = () => {
<div className="rounded-lg border border-dashed border-slate-300 p-3 text-xs leading-5 text-slate-500 dark:border-slate-700">
{translate('::App.DeveloperKitComponentDesigner.NoSelectableGetEndpoint')}
</div>
{renderSqlRecordField()}
</div>
)
}
@ -3812,7 +3904,7 @@ const VisualComponentDesigner = () => {
)}
</div>
)}
{renderSqlRecordValueBinding()}
{renderSqlRecordField()}
</div>
)
}