import { normalizeDesignerKeyList, type DesignerDocument, type DesignerNode } from './types' const EVENT_NAMES: Record = { onClick: 'event', onChange: 'event', onFocus: 'event', onBlur: 'event', onMouseEnter: 'event', onMouseLeave: 'event', } 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 || '')})` } const propsToCode = (node: DesignerNode, itemVariable?: string) => { 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' && !(node.type === 'Checkbox' && key === 'defaultChecked' && 'checked' in node.props) && !(hasBoundInputValue && !hasInputChangeHandler && key === 'readOnly') && !node.bindings?.[key]?.sourceId && value !== '' && value !== undefined, ) .map(([key, value]) => { if (node.type === 'Checkbox' && key === 'checked') { return `defaultChecked=${serializeValue(value)}` } if (node.type === 'Input' && key === 'value') { return `defaultValue=${serializeValue(value)}` } if (node.type === 'Select' && key === 'value') { const staticOptions = Array.isArray(node.props.options) ? node.props.options : [] const selectedOption = staticOptions.find( (option) => option && typeof option === 'object' && !Array.isArray(option) && (option as Record).value === value, ) return `defaultValue=${serializeValue(selectedOption ?? null)}` } return `${key}=${serializeValue(value)}` }) Object.keys(node.bindings || {}).forEach((propertyName) => { if (propertyName === 'children') return const expression = bindingExpression(node, propertyName, itemVariable) if (expression) { const propertyExpression = ['Select', 'AutoComplete'].includes(node.type) && propertyName === 'options' ? `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}') Object.entries(node.events) .filter(([, script]) => script.trim()) .forEach(([eventName]) => { props.push(`${eventName}={handle_${safeIdentifier(node.id)}_${eventName}}`) }) 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( `', 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(`
`, level)}\n${children}\n${indent('
', level)}` } if (node.type === 'Table') { const legacyData = Array.isArray(node.props.data) ? node.props.data : [] const staticItems = Array.isArray(node.props.items) && (node.props.items.length > 0 || !legacyData.length) ? node.props.items : legacyData const itemsExpression = bindingExpression(node, 'items', itemVariable) || bindingExpression(node, 'data', itemVariable) || JSON.stringify(staticItems) const identifier = safeIdentifier(node.id) const itemsVariable = `tableItems_${identifier}` const columnsVariable = `tableColumns_${identifier}` const rowVariable = `tableRow_${identifier}` const configuredColumns = Array.isArray(node.props.dataColumns) ? node.props.dataColumns.filter((column): column is string => typeof column === 'string') : Array.isArray(node.props.columns) ? node.props.columns .map((column) => { if (typeof column === 'string') return column if (!column || typeof column !== 'object' || Array.isArray(column)) return '' const record = column as Record return String(record.dataIndex ?? record.key ?? '') }) .filter(Boolean) : null const columnsExpression = configuredColumns ? JSON.stringify(configuredColumns) : `${itemsVariable}[0] && typeof ${itemsVariable}[0] === "object" && !Array.isArray(${itemsVariable}[0]) ? Object.keys(${itemsVariable}[0]) : ["value"]` return `${indent('{(() => {', level)} ${indent(`const ${itemsVariable} = ${itemsExpression}`, level + 1)} ${indent(`if (!Array.isArray(${itemsVariable})) return
Table items bağlantısı bir koleksiyon döndürmelidir.
`, level + 1)} ${indent(`const ${columnsVariable} = ${columnsExpression}`, level + 1)} ${indent('return (', level + 1)} ${indent(``, level + 2)} ${indent(`{${columnsVariable}.map((column) => {column === "value" ? "Value" : column})}`, level + 3)} ${indent(`{${itemsVariable}.map((${rowVariable}, rowIndex) => {${columnsVariable}.map((column) => { const value = column === "value" ? ${rowVariable} : getByPath(${rowVariable}, column); return {typeof value === "object" && value !== null ? JSON.stringify(value) : String(value ?? "")} })})}`, level + 3)} ${indent('', level + 2)} ${indent(')', level + 1)} ${indent('})()}', level)}` } 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 = `
` 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
Grid items bağlantısı bir koleksiyon döndürmelidir.
`, level + 2)} ${indent(`const ${columnsVariable} = ${columnsExpression}`, level + 2)} ${indent(`if (!${columnsVariable}.length) return
Preview için en az bir sütun seçin.
`, level + 2)} ${indent('return (', level + 2)} ${indent(`
`, level + 3)} ${indent(``, level + 4)} ${indent('', level + 5)} ${indent(`{${columnsVariable}.map((column) => )}`, level + 6)} ${indent('', level + 5)} ${indent('', level + 5)} ${indent(`{${itemsVariable}.map((${itemVariable}, rowIndex) => (`, level + 6)} ${indent(``, 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 `, level + 9)} ${indent('})}', level + 8)} ${indent('', level + 7)} ${indent('))}', level + 6)} ${indent('', level + 5)} ${indent('
{column === "value" ? "Value" : column}
{text}
', level + 4)} ${indent('
', level + 3)} ${indent(')', level + 2)} ${indent('})()}', level + 1)}` return `${indent(gridStart, level)}\n${table}\n${indent('
', 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( `
{typeof ${repeatedItemVariable} === "string" ? ${repeatedItemVariable} : JSON.stringify(${repeatedItemVariable}, null, 2)}
`, level + 3, ) const children = collectionExpression ? `${indent(`{Array.isArray(${collectionExpression}) ? ${collectionExpression}.map((${repeatedItemVariable}, index) => (`, level + 1)} ${indent(``, level + 2)} ${repeatedChildren} ${indent('', level + 2)} ${indent(')) : (', level + 1)} ${indent('
Grid items bağlantısı bir koleksiyon döndürmelidir.
', level + 2)} ${indent(')}', level + 1)}` : staticChildren return `${indent(gridStart, level)}\n${children}\n${indent('', level)}` } if (node.kind === 'platform') { const viewNames: Record = { ListView: 'List', DataGridView: 'Grid', TreeView: 'Tree', GanttView: 'GanttView', TodoBoard: 'TodoBoard', CardView: 'CardView', SchedulerView: 'SchedulerView', PivotView: 'Pivot', ChartView: 'Chart', } return indent( ``, 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(``, level)}\n${indent(`{${optionsExpression}.map((option, optionIndex) => {String(option.label ?? option.value ?? \`Menü \${optionIndex + 1}\`)})}`, level + 1)}\n${indent('', level)}` } 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(``, level)}` } export const generateDesignerCode = (name: string, document: DesignerDocument) => { const componentName = safeIdentifier(name || 'VisualComponent') const handlers: string[] = [] let hasSelect = false let hasDataTable = false const visit = (nodes: DesignerNode[]) => { nodes.forEach((node) => { if (node.type === 'Select' || node.type === 'AutoComplete' || node.type === 'Menu') { hasSelect = true } if (node.type === 'Grid' || node.type === 'Table') hasDataTable = true Object.entries(node.events).forEach(([eventName, script]) => { if (!script.trim()) return if (node.type === 'Checkbox' && eventName === 'onChange') { handlers.push( ` const handle_${safeIdentifier(node.id)}_${eventName} = (checked, originalEvent) => {\n const event = { checked, originalEvent, target: originalEvent?.target }\n${indent(script, 2)}\n }`, ) return } handlers.push( ` const handle_${safeIdentifier(node.id)}_${eventName} = (${EVENT_NAMES[eventName] || 'event'}) => {\n${indent(script, 2)}\n }`, ) }) 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])) const labelKey = ["label", "name", "title", "text", "description"].find((key) => item[key] !== undefined) const valueKey = ["value", "id", "code", "key"].find((key) => item[key] !== undefined) 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)) return `/*__SOZSOFT_VISUAL_DESIGNER__${designerBackup}__*/\nconst ${componentName} = () => {\n${[dataHelpers, selectHelpers, dataHooks, mount, ...handlers].filter(Boolean).join('\n\n')}\n\n return (\n <>\n${body}\n \n )\n}\n\nexport default ${componentName}\n` }