2026-08-05 20:51:43 +00:00
|
|
|
|
import { normalizeDesignerKeyList, type DesignerDocument, type DesignerNode } from './types'
|
|
|
|
|
|
|
|
|
|
|
|
const safeIdentifier = (value: string) => {
|
|
|
|
|
|
const cleaned = value.replace(/[^A-Za-z0-9_$]/g, '_')
|
|
|
|
|
|
return /^[A-Za-z_$]/.test(cleaned) ? cleaned : `Component_${cleaned}`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const serializeValue = (value: unknown) => {
|
|
|
|
|
|
if (typeof value === 'string') return `{${JSON.stringify(value)}}`
|
|
|
|
|
|
if (typeof value === 'number' || typeof value === 'boolean') return `{${String(value)}}`
|
|
|
|
|
|
if (value === null) return '{null}'
|
|
|
|
|
|
if (Array.isArray(value) || typeof value === 'object') return `{${JSON.stringify(value)}}`
|
|
|
|
|
|
return JSON.stringify(String(value ?? ''))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const bindingExpression = (node: DesignerNode, propertyName: string, itemVariable?: string) => {
|
|
|
|
|
|
const binding = node.bindings?.[propertyName]
|
|
|
|
|
|
if (!binding?.sourceId) return ''
|
|
|
|
|
|
const path = binding.path.trim()
|
|
|
|
|
|
if (itemVariable && (path === '$item' || path.startsWith('$item.'))) {
|
|
|
|
|
|
return `getByPath(${itemVariable}, ${JSON.stringify(path === '$item' ? '' : path.slice(6))})`
|
|
|
|
|
|
}
|
|
|
|
|
|
return `getByPath(data_${safeIdentifier(binding.sourceId)}, ${JSON.stringify(binding.path || '')})`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-06 09:16:10 +00:00
|
|
|
|
interface RuntimeStateSpec {
|
|
|
|
|
|
propertyName: string
|
|
|
|
|
|
eventName: string
|
|
|
|
|
|
stateName: string
|
|
|
|
|
|
setterName: string
|
|
|
|
|
|
initialValue: unknown
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const getRuntimeStateSpec = (node: DesignerNode): RuntimeStateSpec | null => {
|
|
|
|
|
|
const configured: Record<string, { propertyName: string; eventName: string }> = {
|
|
|
|
|
|
AutoComplete: { propertyName: 'value', eventName: 'onInputChange' },
|
|
|
|
|
|
Dropdown: { propertyName: 'activeKey', eventName: 'onSelect' },
|
|
|
|
|
|
Input: { propertyName: 'value', eventName: 'onChange' },
|
|
|
|
|
|
Menu: { propertyName: 'defaultActiveKeys', eventName: 'onSelect' },
|
|
|
|
|
|
Pagination: { propertyName: 'currentPage', eventName: 'onChange' },
|
|
|
|
|
|
'Radio.Group': { propertyName: 'value', eventName: 'onChange' },
|
|
|
|
|
|
Select: { propertyName: 'value', eventName: 'onChange' },
|
|
|
|
|
|
}
|
|
|
|
|
|
const fallback =
|
|
|
|
|
|
'checked' in node.props
|
|
|
|
|
|
? { propertyName: 'checked', eventName: 'onChange' }
|
|
|
|
|
|
: 'value' in node.props
|
|
|
|
|
|
? { propertyName: 'value', eventName: 'onChange' }
|
|
|
|
|
|
: null
|
|
|
|
|
|
const state = configured[node.type] || fallback
|
|
|
|
|
|
if (!state || node.bindings?.[state.propertyName]?.sourceId) return null
|
|
|
|
|
|
|
|
|
|
|
|
let initialValue = node.props[state.propertyName]
|
|
|
|
|
|
if (node.type === 'Select') {
|
|
|
|
|
|
const options = Array.isArray(node.props.options) ? node.props.options : []
|
|
|
|
|
|
initialValue = node.props.isMulti
|
|
|
|
|
|
? options.filter(
|
|
|
|
|
|
(option) =>
|
|
|
|
|
|
option &&
|
|
|
|
|
|
typeof option === 'object' &&
|
|
|
|
|
|
!Array.isArray(option) &&
|
|
|
|
|
|
Array.isArray(node.props.value) &&
|
|
|
|
|
|
node.props.value.includes((option as Record<string, unknown>).value),
|
|
|
|
|
|
)
|
|
|
|
|
|
: options.find(
|
|
|
|
|
|
(option) =>
|
|
|
|
|
|
option &&
|
|
|
|
|
|
typeof option === 'object' &&
|
|
|
|
|
|
!Array.isArray(option) &&
|
|
|
|
|
|
(option as Record<string, unknown>).value === node.props.value,
|
|
|
|
|
|
) || null
|
|
|
|
|
|
} else if (node.type === 'Menu') {
|
|
|
|
|
|
initialValue = normalizeDesignerKeyList(initialValue)
|
|
|
|
|
|
} else if (node.type === 'Pagination') {
|
|
|
|
|
|
initialValue = Number(initialValue) || 1
|
|
|
|
|
|
} else if (initialValue === undefined) {
|
|
|
|
|
|
initialValue = state.propertyName === 'checked' ? false : ''
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const identifier = safeIdentifier(node.id)
|
|
|
|
|
|
return {
|
|
|
|
|
|
...state,
|
|
|
|
|
|
stateName: `runtime_${identifier}_${safeIdentifier(state.propertyName)}`,
|
|
|
|
|
|
setterName: `setRuntime_${identifier}_${safeIdentifier(state.propertyName)}`,
|
|
|
|
|
|
initialValue,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const isOptionCollectionProperty = (node: DesignerNode, propertyName: string) =>
|
|
|
|
|
|
(['Select', 'AutoComplete'].includes(node.type) && propertyName === 'options') ||
|
|
|
|
|
|
(['Menu', 'Dropdown', 'Radio.Group'].includes(node.type) && propertyName === 'items')
|
|
|
|
|
|
|
2026-08-05 20:51:43 +00:00
|
|
|
|
const propsToCode = (node: DesignerNode, itemVariable?: string) => {
|
2026-08-06 09:16:10 +00:00
|
|
|
|
const runtimeState = getRuntimeStateSpec(node)
|
2026-08-05 20:51:43 +00:00
|
|
|
|
const hasBoundInputValue = node.type === 'Input' && Boolean(node.bindings?.value?.sourceId)
|
|
|
|
|
|
const hasInputChangeHandler = Boolean(node.events?.onChange?.trim())
|
|
|
|
|
|
const props = Object.entries(node.props)
|
|
|
|
|
|
.filter(
|
|
|
|
|
|
([key, value]) =>
|
|
|
|
|
|
key !== 'children' &&
|
2026-08-06 09:16:10 +00:00
|
|
|
|
key !== runtimeState?.propertyName &&
|
2026-08-05 20:51:43 +00:00
|
|
|
|
!(node.type === 'Checkbox' && key === 'defaultChecked' && 'checked' in node.props) &&
|
|
|
|
|
|
!(hasBoundInputValue && !hasInputChangeHandler && key === 'readOnly') &&
|
|
|
|
|
|
!node.bindings?.[key]?.sourceId &&
|
|
|
|
|
|
value !== '' &&
|
|
|
|
|
|
value !== undefined,
|
|
|
|
|
|
)
|
|
|
|
|
|
.map(([key, value]) => {
|
|
|
|
|
|
return `${key}=${serializeValue(value)}`
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
Object.keys(node.bindings || {}).forEach((propertyName) => {
|
|
|
|
|
|
if (propertyName === 'children') return
|
|
|
|
|
|
const expression = bindingExpression(node, propertyName, itemVariable)
|
|
|
|
|
|
if (expression) {
|
|
|
|
|
|
const propertyExpression =
|
2026-08-06 09:16:10 +00:00
|
|
|
|
isOptionCollectionProperty(node, propertyName)
|
2026-08-05 20:51:43 +00:00
|
|
|
|
? `toSelectOptions(${expression}, ${JSON.stringify(node.bindings.options?.labelPath || '')}, ${JSON.stringify(node.bindings.options?.valuePath || '')})`
|
|
|
|
|
|
: expression
|
|
|
|
|
|
props.push(`${propertyName}={${propertyExpression}}`)
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
if (hasBoundInputValue && !hasInputChangeHandler) props.push('readOnly={true}')
|
|
|
|
|
|
|
2026-08-06 09:16:10 +00:00
|
|
|
|
if (runtimeState) props.push(`${runtimeState.propertyName}={${runtimeState.stateName}}`)
|
|
|
|
|
|
|
|
|
|
|
|
const handlerNames = new Set(
|
|
|
|
|
|
Object.entries(node.events)
|
|
|
|
|
|
.filter(([, script]) => script.trim())
|
|
|
|
|
|
.map(([eventName]) => eventName),
|
|
|
|
|
|
)
|
|
|
|
|
|
if (runtimeState) handlerNames.add(runtimeState.eventName)
|
|
|
|
|
|
handlerNames.forEach((eventName) =>
|
|
|
|
|
|
props.push(`${eventName}={handle_${safeIdentifier(node.id)}_${eventName}}`),
|
|
|
|
|
|
)
|
2026-08-05 20:51:43 +00:00
|
|
|
|
|
|
|
|
|
|
return props.length ? ` ${props.join(' ')}` : ''
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const indent = (text: string, level: number) =>
|
|
|
|
|
|
text
|
|
|
|
|
|
.split('\n')
|
|
|
|
|
|
.map((line) => `${' '.repeat(level)}${line}`)
|
|
|
|
|
|
.join('\n')
|
|
|
|
|
|
|
|
|
|
|
|
const nodeToCode = (node: DesignerNode, level = 0, itemVariable?: string): string => {
|
|
|
|
|
|
if (node.type === 'Spacer') {
|
|
|
|
|
|
return indent(
|
2026-08-06 09:16:10 +00:00
|
|
|
|
`<div aria-hidden="true" className=${JSON.stringify(String(node.props.className || ''))} style={{ height: ${Number(node.props.height) || 24} }} />`,
|
2026-08-05 20:51:43 +00:00
|
|
|
|
level,
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (node.type === 'FlexRow') {
|
|
|
|
|
|
const style = `{ display: "flex", gap: ${Number(node.props.gap) || 0}, flexWrap: "${node.props.wrap ? 'wrap' : 'nowrap'}", alignItems: "${String(node.props.align || 'stretch')}" }`
|
|
|
|
|
|
const className = node.props.className
|
|
|
|
|
|
? ` className=${JSON.stringify(node.props.className)}`
|
|
|
|
|
|
: ''
|
|
|
|
|
|
const children = node.children
|
|
|
|
|
|
.map((child) => nodeToCode(child, level + 1, itemVariable))
|
|
|
|
|
|
.join('\n')
|
|
|
|
|
|
return `${indent(`<div${className} style={${style}}>`, level)}\n${children}\n${indent('</div>', level)}`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
|
node.type === 'PageContainer' ||
|
|
|
|
|
|
node.type === 'TwoColumns' ||
|
|
|
|
|
|
node.type === 'SidebarContent' ||
|
|
|
|
|
|
node.type === 'HeaderContent'
|
|
|
|
|
|
) {
|
|
|
|
|
|
const gap = Number(node.props.gap) || 0
|
|
|
|
|
|
const className = JSON.stringify(String(node.props.className || ''))
|
|
|
|
|
|
const style =
|
|
|
|
|
|
node.type === 'PageContainer'
|
|
|
|
|
|
? `{ display: "flex", flexDirection: "column", gap: ${gap}, margin: "0 auto", maxWidth: ${JSON.stringify(String(node.props.maxWidth || '1280px'))}, padding: ${Number(node.props.padding) || 0}, width: "100%" }`
|
|
|
|
|
|
: node.type === 'HeaderContent'
|
|
|
|
|
|
? `{ display: "flex", flexDirection: "column", gap: ${gap} }`
|
|
|
|
|
|
: `{ display: "grid", gap: ${gap}, gridTemplateColumns: ${JSON.stringify(node.type === 'SidebarContent' ? `${String(node.props.sidebarWidth || '280px')} minmax(0, 1fr)` : 'repeat(2, minmax(0, 1fr))')} }`
|
|
|
|
|
|
const children = node.children
|
|
|
|
|
|
.map((child) => nodeToCode(child, level + 1, itemVariable))
|
|
|
|
|
|
.join('\n')
|
|
|
|
|
|
return `${indent(`<div className=${className} style={${style}}>`, level)}\n${children}\n${indent('</div>', level)}`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (node.type === 'Table') {
|
2026-08-06 09:16:10 +00:00
|
|
|
|
const rowCount = Math.min(20, Math.max(1, Math.floor(Number(node.props.rowCount) || 2)))
|
|
|
|
|
|
const columnCount = Math.min(
|
|
|
|
|
|
20,
|
|
|
|
|
|
Math.max(1, Math.floor(Number(node.props.columnCount) || 3)),
|
|
|
|
|
|
)
|
|
|
|
|
|
const rows = Array.from({ length: rowCount }, (_, rowIndex) => {
|
|
|
|
|
|
const cells = Array.from({ length: columnCount }, (_, columnIndex) => {
|
|
|
|
|
|
const slot = `table:${rowIndex}:${columnIndex}`
|
|
|
|
|
|
const cellChildren = node.children.filter(
|
|
|
|
|
|
(child, childIndex) =>
|
|
|
|
|
|
(child.slot ||
|
|
|
|
|
|
`table:${Math.floor(childIndex / columnCount)}:${childIndex % columnCount}`) === slot,
|
|
|
|
|
|
)
|
|
|
|
|
|
const content = cellChildren.length
|
|
|
|
|
|
? cellChildren.map((child) => nodeToCode(child, level + 3, itemVariable)).join('\n')
|
|
|
|
|
|
: indent('<span />', level + 3)
|
|
|
|
|
|
return `${indent('<td>', level + 2)}\n${content}\n${indent('</td>', level + 2)}`
|
|
|
|
|
|
}).join('\n')
|
|
|
|
|
|
return `${indent('<tr>', level + 1)}\n${cells}\n${indent('</tr>', level + 1)}`
|
|
|
|
|
|
}).join('\n')
|
|
|
|
|
|
const className = JSON.stringify(String(node.props.className || ''))
|
|
|
|
|
|
return `${indent(`<UiKit.Table className={${className} + " [&>tbody>tr>td]:!px-1.5 [&>tbody>tr>td]:!py-1.5"} borderlessRow compact={${Boolean(node.props.compact)}} hoverable={${node.props.hoverable !== false}} overflow={${node.props.overflow !== false}} style={{ minWidth: "100%", tableLayout: "fixed", width: "100%" }}>`, level)}
|
|
|
|
|
|
${indent('<tbody>', level + 1)}
|
|
|
|
|
|
${rows}
|
|
|
|
|
|
${indent('</tbody>', level + 1)}
|
|
|
|
|
|
${indent('</UiKit.Table>', level)}`
|
2026-08-05 20:51:43 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (node.type === 'Grid') {
|
|
|
|
|
|
const columns = Number(node.props.cols) || 3
|
|
|
|
|
|
const gap = (Number(node.props.gap) || 0) * 4
|
|
|
|
|
|
const itemsExpression =
|
|
|
|
|
|
bindingExpression(node, 'items', itemVariable) ||
|
|
|
|
|
|
(Array.isArray(node.props.items) ? JSON.stringify(node.props.items) : '')
|
|
|
|
|
|
const itemsBinding = node.bindings?.items
|
|
|
|
|
|
const sourceRootExpression =
|
|
|
|
|
|
!itemVariable && itemsBinding?.sourceId ? `data_${safeIdentifier(itemsBinding.sourceId)}` : ''
|
|
|
|
|
|
const collectionExpression =
|
|
|
|
|
|
sourceRootExpression && sourceRootExpression !== itemsExpression
|
|
|
|
|
|
? `(Array.isArray(${itemsExpression}) ? ${itemsExpression} : Array.isArray(${sourceRootExpression}) ? ${sourceRootExpression} : null)`
|
|
|
|
|
|
: itemsExpression
|
|
|
|
|
|
const gridClassName = JSON.stringify(String(node.props.className || ''))
|
|
|
|
|
|
const gridStart = `<div className=${gridClassName} style={{ display: "grid", gridTemplateColumns: "repeat(${columns}, minmax(0, 1fr))", gap: ${gap} }}>`
|
|
|
|
|
|
|
|
|
|
|
|
if (collectionExpression && node.children.length === 0) {
|
|
|
|
|
|
const identifier = safeIdentifier(node.id)
|
|
|
|
|
|
const itemsVariable = `gridItems_${identifier}`
|
|
|
|
|
|
const columnsVariable = `gridColumns_${identifier}`
|
|
|
|
|
|
const itemVariable = `gridItem_${identifier}`
|
|
|
|
|
|
const valueVariable = `cellValue_${identifier}`
|
|
|
|
|
|
const configuredColumns = Array.isArray(node.props.dataColumns)
|
|
|
|
|
|
? node.props.dataColumns.filter((column): column is string => typeof column === 'string')
|
|
|
|
|
|
: null
|
|
|
|
|
|
const columnsExpression = configuredColumns
|
|
|
|
|
|
? JSON.stringify(configuredColumns)
|
|
|
|
|
|
: `${itemsVariable}[0] && typeof ${itemsVariable}[0] === "object" && !Array.isArray(${itemsVariable}[0]) ? Object.keys(${itemsVariable}[0]) : ["value"]`
|
|
|
|
|
|
const compact = Boolean(node.props.compact)
|
|
|
|
|
|
const borderlessRow = Boolean(node.props.borderlessRow)
|
|
|
|
|
|
const hoverable = node.props.hoverable !== false
|
|
|
|
|
|
const overflow = node.props.overflow !== false
|
|
|
|
|
|
const tableTextClass = compact ? 'text-[11px]' : 'text-xs'
|
|
|
|
|
|
const cellSpacingClass = compact ? 'px-2 py-1' : 'px-3 py-2'
|
|
|
|
|
|
const rowClass = [
|
|
|
|
|
|
borderlessRow ? '' : 'border-b border-slate-100 last:border-b-0 dark:border-slate-800',
|
|
|
|
|
|
hoverable ? 'transition-colors hover:bg-slate-50 dark:hover:bg-slate-800/70' : '',
|
|
|
|
|
|
]
|
|
|
|
|
|
.filter(Boolean)
|
|
|
|
|
|
.join(' ')
|
|
|
|
|
|
const overflowClass = overflow ? 'overflow-auto' : 'overflow-visible'
|
|
|
|
|
|
const table = `${indent('{(() => {', level + 1)}
|
|
|
|
|
|
${indent(`const ${itemsVariable} = ${collectionExpression}`, level + 2)}
|
|
|
|
|
|
${indent(`if (!Array.isArray(${itemsVariable})) return <div className="rounded-lg border border-amber-300 bg-amber-50 p-3 text-xs text-amber-800 dark:border-amber-800 dark:bg-amber-950 dark:text-amber-200">Grid items bağlantısı bir koleksiyon döndürmelidir.</div>`, level + 2)}
|
|
|
|
|
|
${indent(`const ${columnsVariable} = ${columnsExpression}`, level + 2)}
|
|
|
|
|
|
${indent(`if (!${columnsVariable}.length) return <div className="rounded-lg border border-dashed border-slate-300 bg-slate-50 p-4 text-center text-xs text-slate-500 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-400">Preview için en az bir sütun seçin.</div>`, level + 2)}
|
|
|
|
|
|
${indent('return (', level + 2)}
|
|
|
|
|
|
${indent(`<div className="min-w-0 ${overflowClass} rounded-lg border border-slate-200 bg-white shadow-sm dark:border-slate-700 dark:bg-slate-900" style={{ gridColumn: "1 / -1" }}>`, level + 3)}
|
|
|
|
|
|
${indent(`<table className="w-full border-collapse text-left ${tableTextClass}">`, level + 4)}
|
|
|
|
|
|
${indent('<thead className="bg-slate-100 text-slate-600 dark:bg-slate-800 dark:text-slate-200"><tr>', level + 5)}
|
|
|
|
|
|
${indent(`{${columnsVariable}.map((column) => <th key={column} className="whitespace-nowrap border-b border-slate-200 dark:border-slate-700 ${cellSpacingClass}">{column === "value" ? "Value" : column}</th>)}`, level + 6)}
|
|
|
|
|
|
${indent('</tr></thead>', level + 5)}
|
|
|
|
|
|
${indent('<tbody>', level + 5)}
|
|
|
|
|
|
${indent(`{${itemsVariable}.map((${itemVariable}, rowIndex) => (`, level + 6)}
|
|
|
|
|
|
${indent(`<tr key={rowIndex} className=${JSON.stringify(rowClass)}>`, level + 7)}
|
|
|
|
|
|
${indent(`{${columnsVariable}.map((column) => {`, level + 8)}
|
|
|
|
|
|
${indent(`const ${valueVariable} = column === "value" ? ${itemVariable} : getByPath(${itemVariable}, column)`, level + 9)}
|
|
|
|
|
|
${indent(`const text = typeof ${valueVariable} === "object" && ${valueVariable} !== null ? JSON.stringify(${valueVariable}) : String(${valueVariable} ?? "") || "—"`, level + 9)}
|
|
|
|
|
|
${indent(`return <td key={column} className="max-w-64 truncate text-slate-800 dark:text-slate-100 ${cellSpacingClass}" title={text}>{text}</td>`, level + 9)}
|
|
|
|
|
|
${indent('})}', level + 8)}
|
|
|
|
|
|
${indent('</tr>', level + 7)}
|
|
|
|
|
|
${indent('))}', level + 6)}
|
|
|
|
|
|
${indent('</tbody>', level + 5)}
|
|
|
|
|
|
${indent('</table>', level + 4)}
|
|
|
|
|
|
${indent('</div>', level + 3)}
|
|
|
|
|
|
${indent(')', level + 2)}
|
|
|
|
|
|
${indent('})()}', level + 1)}`
|
|
|
|
|
|
return `${indent(gridStart, level)}\n${table}\n${indent('</div>', level)}`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const repeatedItemVariable = `item_${safeIdentifier(node.id)}`
|
|
|
|
|
|
const staticChildren = node.children
|
|
|
|
|
|
.map((child) => nodeToCode(child, level + 1, itemVariable))
|
|
|
|
|
|
.join('\n')
|
|
|
|
|
|
const repeatedChildren = node.children.length
|
|
|
|
|
|
? node.children.map((child) => nodeToCode(child, level + 3, repeatedItemVariable)).join('\n')
|
|
|
|
|
|
: indent(
|
|
|
|
|
|
`<pre className="min-w-0 overflow-auto rounded-lg border border-slate-200 bg-white p-3 text-xs text-slate-800 shadow-sm dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100">{typeof ${repeatedItemVariable} === "string" ? ${repeatedItemVariable} : JSON.stringify(${repeatedItemVariable}, null, 2)}</pre>`,
|
|
|
|
|
|
level + 3,
|
|
|
|
|
|
)
|
|
|
|
|
|
const children = collectionExpression
|
|
|
|
|
|
? `${indent(`{Array.isArray(${collectionExpression}) ? ${collectionExpression}.map((${repeatedItemVariable}, index) => (`, level + 1)}
|
|
|
|
|
|
${indent(`<React.Fragment key={${repeatedItemVariable}?.id ?? index}>`, level + 2)}
|
|
|
|
|
|
${repeatedChildren}
|
|
|
|
|
|
${indent('</React.Fragment>', level + 2)}
|
|
|
|
|
|
${indent(')) : (', level + 1)}
|
|
|
|
|
|
${indent('<div className="rounded-lg border border-amber-300 bg-amber-50 p-3 text-xs text-amber-800 dark:border-amber-800 dark:bg-amber-950 dark:text-amber-200">Grid items bağlantısı bir koleksiyon döndürmelidir.</div>', level + 2)}
|
|
|
|
|
|
${indent(')}', level + 1)}`
|
|
|
|
|
|
: staticChildren
|
|
|
|
|
|
return `${indent(gridStart, level)}\n${children}\n${indent('</div>', level)}`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (node.kind === 'platform') {
|
|
|
|
|
|
const viewNames: Record<string, string> = {
|
|
|
|
|
|
ListView: 'List',
|
|
|
|
|
|
DataGridView: 'Grid',
|
|
|
|
|
|
TreeView: 'Tree',
|
|
|
|
|
|
GanttView: 'GanttView',
|
|
|
|
|
|
TodoBoard: 'TodoBoard',
|
|
|
|
|
|
CardView: 'CardView',
|
|
|
|
|
|
SchedulerView: 'SchedulerView',
|
|
|
|
|
|
PivotView: 'Pivot',
|
|
|
|
|
|
ChartView: 'Chart',
|
|
|
|
|
|
}
|
|
|
|
|
|
return indent(
|
|
|
|
|
|
`<PlatformViewHost view=${JSON.stringify(viewNames[node.type] || node.type)} listFormCode=${JSON.stringify(String(node.props.listFormCode || ''))} height=${JSON.stringify(String(node.props.height || '420px'))} />`,
|
|
|
|
|
|
level,
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (node.type === 'Menu') {
|
|
|
|
|
|
const { items: staticOptions = [], variant: _variant, ...menuProps } = node.props
|
|
|
|
|
|
menuProps.defaultActiveKeys = normalizeDesignerKeyList(menuProps.defaultActiveKeys)
|
|
|
|
|
|
menuProps.defaultExpandedKeys = normalizeDesignerKeyList(menuProps.defaultExpandedKeys)
|
|
|
|
|
|
const { items: optionsBinding, ...menuBindings } = node.bindings || {}
|
|
|
|
|
|
const menuNode = { ...node, props: menuProps, bindings: menuBindings }
|
|
|
|
|
|
const menuPropsCode = propsToCode(menuNode, itemVariable)
|
|
|
|
|
|
const binding = optionsBinding?.sourceId
|
|
|
|
|
|
? bindingExpression(node, 'items', itemVariable)
|
|
|
|
|
|
: ''
|
|
|
|
|
|
const optionsExpression = binding
|
|
|
|
|
|
? `toSelectOptions(${binding}, ${JSON.stringify(optionsBinding?.labelPath || '')}, ${JSON.stringify(optionsBinding?.valuePath || '')})`
|
|
|
|
|
|
: JSON.stringify(Array.isArray(staticOptions) ? staticOptions : [])
|
|
|
|
|
|
return `${indent(`<UiKit.Menu${menuPropsCode}>`, level)}\n${indent(`{${optionsExpression}.map((option, optionIndex) => <UiKit.Menu.MenuItem key={String(option.value ?? optionIndex)} eventKey={String(option.value ?? optionIndex)}>{String(option.label ?? option.value ?? \`Menü \${optionIndex + 1}\`)}</UiKit.Menu.MenuItem>)}`, level + 1)}\n${indent('</UiKit.Menu>', level)}`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-06 09:16:10 +00:00
|
|
|
|
if (node.type === 'Dropdown') {
|
|
|
|
|
|
const { items: staticOptions = [], ...dropdownProps } = node.props
|
|
|
|
|
|
const { items: optionsBinding, ...dropdownBindings } = node.bindings || {}
|
|
|
|
|
|
const dropdownPropsCode = propsToCode(
|
|
|
|
|
|
{ ...node, props: dropdownProps, bindings: dropdownBindings },
|
|
|
|
|
|
itemVariable,
|
|
|
|
|
|
)
|
|
|
|
|
|
const binding = optionsBinding?.sourceId
|
|
|
|
|
|
? bindingExpression(node, 'items', itemVariable)
|
|
|
|
|
|
: ''
|
|
|
|
|
|
const optionsExpression = binding
|
|
|
|
|
|
? `toSelectOptions(${binding}, ${JSON.stringify(optionsBinding?.labelPath || '')}, ${JSON.stringify(optionsBinding?.valuePath || '')})`
|
|
|
|
|
|
: JSON.stringify(Array.isArray(staticOptions) ? staticOptions : [])
|
|
|
|
|
|
return `${indent(`<UiKit.Dropdown${dropdownPropsCode}>`, level)}\n${indent(`{${optionsExpression}.map((option, optionIndex) => <UiKit.Dropdown.Item key={String(option.value ?? optionIndex)} eventKey={String(option.value ?? optionIndex)} disabled={Boolean(option.disabled)}>{String(option.label ?? option.value ?? \`Öğe \${optionIndex + 1}\`)}</UiKit.Dropdown.Item>)}`, level + 1)}\n${indent('</UiKit.Dropdown>', level)}`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (node.type === 'Radio.Group') {
|
|
|
|
|
|
const {
|
|
|
|
|
|
items: staticOptions = [],
|
|
|
|
|
|
checked: _checked,
|
|
|
|
|
|
defaultChecked: _defaultChecked,
|
|
|
|
|
|
readOnly: _readOnly,
|
|
|
|
|
|
...groupProps
|
|
|
|
|
|
} = node.props
|
|
|
|
|
|
const { items: optionsBinding, ...groupBindings } = node.bindings || {}
|
|
|
|
|
|
const groupPropsCode = propsToCode(
|
|
|
|
|
|
{ ...node, props: groupProps, bindings: groupBindings },
|
|
|
|
|
|
itemVariable,
|
|
|
|
|
|
)
|
|
|
|
|
|
const binding = optionsBinding?.sourceId
|
|
|
|
|
|
? bindingExpression(node, 'items', itemVariable)
|
|
|
|
|
|
: ''
|
|
|
|
|
|
const optionsExpression = binding
|
|
|
|
|
|
? `toSelectOptions(${binding}, ${JSON.stringify(optionsBinding?.labelPath || '')}, ${JSON.stringify(optionsBinding?.valuePath || '')})`
|
|
|
|
|
|
: JSON.stringify(Array.isArray(staticOptions) ? staticOptions : [])
|
|
|
|
|
|
return `${indent(`<UiKit.Radio.Group${groupPropsCode}>`, level)}\n${indent(`{${optionsExpression}.map((option, optionIndex) => <UiKit.Radio key={String(option.value ?? optionIndex)} value={option.value ?? optionIndex} disabled={Boolean(option.disabled)}>{String(option.label ?? option.value ?? \`Seçenek \${optionIndex + 1}\`)}</UiKit.Radio>)}`, level + 1)}\n${indent('</UiKit.Radio.Group>', level)}`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-05 20:51:43 +00:00
|
|
|
|
const tag =
|
|
|
|
|
|
node.type === 'checkbox'
|
|
|
|
|
|
? 'input'
|
|
|
|
|
|
: node.kind === 'ui'
|
|
|
|
|
|
? `UiKit.${node.type}`
|
|
|
|
|
|
: node.kind === 'custom'
|
|
|
|
|
|
? safeIdentifier(node.type)
|
|
|
|
|
|
: node.type
|
|
|
|
|
|
const props = { ...node.props }
|
|
|
|
|
|
if (node.type === 'checkbox') props.type = 'checkbox'
|
|
|
|
|
|
const normalizedNode = { ...node, props }
|
|
|
|
|
|
const propCode = propsToCode(normalizedNode, itemVariable)
|
|
|
|
|
|
const childrenText = String(node.props.children ?? '')
|
|
|
|
|
|
const childrenBinding = bindingExpression(node, 'children', itemVariable)
|
|
|
|
|
|
const hasChildren = node.children.length > 0 || childrenText.length > 0 || !!childrenBinding
|
|
|
|
|
|
const voidElement = ['input', 'img'].includes(tag)
|
|
|
|
|
|
|
|
|
|
|
|
if (voidElement) return indent(`<${tag}${propCode} />`, level)
|
|
|
|
|
|
if (!hasChildren) return indent(`<${tag}${propCode} />`, level)
|
|
|
|
|
|
|
|
|
|
|
|
const nested = node.children.map((child) => nodeToCode(child, level + 1, itemVariable)).join('\n')
|
|
|
|
|
|
const text = childrenBinding
|
|
|
|
|
|
? indent(`{${childrenBinding}}`, level + 1)
|
|
|
|
|
|
: childrenText
|
|
|
|
|
|
? indent(`{${JSON.stringify(childrenText)}}`, level + 1)
|
|
|
|
|
|
: ''
|
|
|
|
|
|
const body = [text, nested].filter(Boolean).join('\n')
|
|
|
|
|
|
return `${indent(`<${tag}${propCode}>`, level)}\n${body}\n${indent(`</${tag}>`, level)}`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export const generateDesignerCode = (name: string, document: DesignerDocument) => {
|
|
|
|
|
|
const componentName = safeIdentifier(name || 'VisualComponent')
|
|
|
|
|
|
const handlers: string[] = []
|
2026-08-06 09:16:10 +00:00
|
|
|
|
const runtimeStateHooks: string[] = []
|
2026-08-05 20:51:43 +00:00
|
|
|
|
let hasSelect = false
|
|
|
|
|
|
let hasDataTable = false
|
|
|
|
|
|
|
|
|
|
|
|
const visit = (nodes: DesignerNode[]) => {
|
|
|
|
|
|
nodes.forEach((node) => {
|
2026-08-06 09:16:10 +00:00
|
|
|
|
if (
|
|
|
|
|
|
['Select', 'AutoComplete', 'Menu', 'Dropdown', 'Radio.Group'].includes(node.type)
|
|
|
|
|
|
) {
|
2026-08-05 20:51:43 +00:00
|
|
|
|
hasSelect = true
|
|
|
|
|
|
}
|
2026-08-06 09:16:10 +00:00
|
|
|
|
if (node.type === 'Grid') hasDataTable = true
|
|
|
|
|
|
const runtimeState = getRuntimeStateSpec(node)
|
|
|
|
|
|
if (runtimeState) {
|
|
|
|
|
|
runtimeStateHooks.push(
|
|
|
|
|
|
` const [${runtimeState.stateName}, ${runtimeState.setterName}] = React.useState(${JSON.stringify(runtimeState.initialValue)})`,
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
const eventNames = new Set(
|
|
|
|
|
|
Object.entries(node.events)
|
|
|
|
|
|
.filter(([, script]) => script.trim())
|
|
|
|
|
|
.map(([eventName]) => eventName),
|
|
|
|
|
|
)
|
|
|
|
|
|
if (runtimeState) eventNames.add(runtimeState.eventName)
|
|
|
|
|
|
eventNames.forEach((eventName) => {
|
|
|
|
|
|
const script = node.events[eventName]?.trim() || ''
|
|
|
|
|
|
const stateUpdate =
|
|
|
|
|
|
runtimeState?.eventName === eventName
|
|
|
|
|
|
? node.type === 'Select'
|
|
|
|
|
|
? `${runtimeState.setterName}(valueOrEvent)`
|
|
|
|
|
|
: node.type === 'AutoComplete'
|
|
|
|
|
|
? `${runtimeState.setterName}(valueOrEvent ?? "")`
|
|
|
|
|
|
: node.type === 'Menu'
|
|
|
|
|
|
? `${runtimeState.setterName}([String(valueOrEvent ?? "")])`
|
|
|
|
|
|
: node.type === 'Dropdown'
|
|
|
|
|
|
? `${runtimeState.setterName}(String(valueOrEvent ?? ""))`
|
|
|
|
|
|
: node.type === 'Pagination'
|
|
|
|
|
|
? `${runtimeState.setterName}(Number(valueOrEvent) || 1)`
|
|
|
|
|
|
: runtimeState.propertyName === 'checked'
|
|
|
|
|
|
? `${runtimeState.setterName}(typeof valueOrEvent === "object" && valueOrEvent?.target ? Boolean(valueOrEvent.target.checked) : Boolean(valueOrEvent))`
|
|
|
|
|
|
: node.type === 'Input' || node.kind === 'html'
|
|
|
|
|
|
? `${runtimeState.setterName}(valueOrEvent?.target?.value ?? valueOrEvent)`
|
|
|
|
|
|
: `${runtimeState.setterName}(valueOrEvent)`
|
|
|
|
|
|
: ''
|
|
|
|
|
|
const eventDeclaration =
|
|
|
|
|
|
node.type === 'Checkbox' && eventName === 'onChange'
|
|
|
|
|
|
? ' const event = { checked: Boolean(valueOrEvent), originalEvent, target: originalEvent?.target }'
|
|
|
|
|
|
: ' const event = valueOrEvent'
|
2026-08-05 20:51:43 +00:00
|
|
|
|
handlers.push(
|
2026-08-06 09:16:10 +00:00
|
|
|
|
` const handle_${safeIdentifier(node.id)}_${eventName} = (valueOrEvent, originalEvent) => {\n${stateUpdate ? ` ${stateUpdate}\n` : ''}${eventDeclaration}${script ? `\n${indent(script, 2)}` : ''}\n }`,
|
2026-08-05 20:51:43 +00:00
|
|
|
|
)
|
|
|
|
|
|
})
|
|
|
|
|
|
visit(node.children)
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
visit(document.nodes)
|
|
|
|
|
|
|
|
|
|
|
|
const dataSources = document.dataSources || []
|
|
|
|
|
|
const selectHelpers = hasSelect
|
|
|
|
|
|
? ` const toSelectOptions = (value, labelPath = "", valuePath = "") => {
|
|
|
|
|
|
if (!Array.isArray(value)) return []
|
|
|
|
|
|
return value.map((item, index) => {
|
|
|
|
|
|
if (item === null || typeof item !== "object" || Array.isArray(item)) return { label: String(item ?? ""), value: item ?? index }
|
|
|
|
|
|
const primitiveKeys = Object.keys(item).filter((key) => item[key] === null || ["string", "number", "boolean"].includes(typeof item[key]))
|
2026-08-06 09:16:10 +00:00
|
|
|
|
const labelKey = ["label", "children", "name", "title", "text", "description"].find((key) => item[key] !== undefined)
|
|
|
|
|
|
const valueKey = ["value", "eventKey", "id", "code", "key"].find((key) => item[key] !== undefined)
|
2026-08-05 20:51:43 +00:00
|
|
|
|
const optionLabel = labelPath ? getByPath(item, labelPath) : item[labelKey ?? primitiveKeys[0]]
|
|
|
|
|
|
const optionValue = valuePath ? getByPath(item, valuePath) : item[valueKey ?? primitiveKeys[1] ?? primitiveKeys[0]]
|
|
|
|
|
|
return { ...item, label: String(optionLabel ?? \`Seçenek \${index + 1}\`), value: optionValue ?? index }
|
|
|
|
|
|
})
|
|
|
|
|
|
}`
|
|
|
|
|
|
: ''
|
|
|
|
|
|
const dataHelpers =
|
|
|
|
|
|
dataSources.length || hasDataTable
|
|
|
|
|
|
? ` const getByPath = (value, path) => {
|
|
|
|
|
|
if (!path) return value
|
|
|
|
|
|
const readPath = (target, targetPath) => targetPath.split('.').filter(Boolean).reduce((current, key) => current?.[key], target)
|
|
|
|
|
|
const directValue = readPath(value, path)
|
|
|
|
|
|
if (directValue !== undefined) return directValue
|
|
|
|
|
|
if (path.startsWith('result.')) return readPath(value, path.slice('result.'.length))
|
|
|
|
|
|
return value?.result === undefined ? undefined : readPath(value.result, path)
|
|
|
|
|
|
}`
|
|
|
|
|
|
: ''
|
|
|
|
|
|
const dataHooks = dataSources
|
|
|
|
|
|
.map((source) => {
|
|
|
|
|
|
const identifier = safeIdentifier(source.id)
|
|
|
|
|
|
if (!source.url.trim().startsWith('/api/')) {
|
|
|
|
|
|
return ` const [data_${identifier}] = React.useState(null)`
|
|
|
|
|
|
}
|
|
|
|
|
|
return ` const [data_${identifier}, setData_${identifier}] = React.useState(null)
|
|
|
|
|
|
React.useEffect(() => {
|
|
|
|
|
|
let active = true
|
|
|
|
|
|
apiService.fetchData({ url: ${JSON.stringify(source.url)}, method: 'GET' }).then((response) => {
|
|
|
|
|
|
if (active) setData_${identifier}(getByPath(response.data, ${JSON.stringify(source.responsePath || '')}))
|
|
|
|
|
|
}).catch((error) => console.error(${JSON.stringify(`${source.name} endpoint error:`)}, error))
|
|
|
|
|
|
return () => { active = false }
|
|
|
|
|
|
}, [])`
|
|
|
|
|
|
})
|
|
|
|
|
|
.join('\n\n')
|
|
|
|
|
|
|
|
|
|
|
|
const mount = document.lifecycle.onMount.trim()
|
|
|
|
|
|
? ` React.useEffect(() => {\n${indent(document.lifecycle.onMount, 2)}\n }, [])`
|
|
|
|
|
|
: ''
|
|
|
|
|
|
const body = document.nodes.map((node) => nodeToCode(node, 3)).join('\n')
|
|
|
|
|
|
|
|
|
|
|
|
const designerBackup = encodeURIComponent(JSON.stringify(document))
|
|
|
|
|
|
|
2026-08-06 09:16:10 +00:00
|
|
|
|
return `/*__SOZSOFT_VISUAL_DESIGNER__${designerBackup}__*/\nconst ${componentName} = () => {\n${[dataHelpers, selectHelpers, dataHooks, ...runtimeStateHooks, mount, ...handlers].filter(Boolean).join('\n\n')}\n\n return (\n <>\n${body}\n </>\n )\n}\n\nexport default ${componentName}\n`
|
2026-08-05 20:51:43 +00:00
|
|
|
|
}
|