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 staticValueExpression = (value: unknown): string => { if (typeof value === 'string') { return value.startsWith('::') ? `translate(${JSON.stringify(value)})` : JSON.stringify(value) } if (Array.isArray(value)) return `[${value.map(staticValueExpression).join(', ')}]` if (value && typeof value === 'object') { return `{ ${Object.entries(value) .map(([key, item]) => `${JSON.stringify(key)}: ${staticValueExpression(item)}`) .join(', ')} }` } if (value === undefined) return 'undefined' return JSON.stringify(value) } const serializeValue = (value: unknown) => `{${staticValueExpression(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 || '')})` } interface RuntimeStateSpec { propertyName: string eventName: string stateName: string setterName: string initialValue: unknown } const getRuntimeStateSpec = (node: DesignerNode): RuntimeStateSpec | null => { const configured: Record = { AutoComplete: { propertyName: 'value', eventName: 'onInputChange' }, Dropdown: { propertyName: 'activeKey', eventName: 'onSelect' }, Input: { propertyName: 'value', eventName: 'onChange' }, ImageViewer: { propertyName: 'activeIndex', eventName: 'onIndexChange' }, Menu: { propertyName: 'defaultActiveKeys', eventName: 'onSelect' }, Pagination: { propertyName: 'currentPage', eventName: 'onChange' }, 'Radio.Group': { propertyName: 'value', eventName: 'onChange' }, Select: { propertyName: 'value', eventName: 'onChange' }, Steps: { propertyName: 'current', eventName: 'onChange' }, Upload: { propertyName: 'fileList', 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).value), ) : options.find( (option) => option && typeof option === 'object' && !Array.isArray(option) && (option as Record).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', 'Pagination', 'Radio.Group'].includes(node.type) && propertyName === 'items') const propsToCode = (node: DesignerNode, itemVariable?: string) => { const runtimeState = getRuntimeStateSpec(node) 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' && key !== runtimeState?.propertyName && !(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 = isOptionCollectionProperty(node, propertyName) ? `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}') 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}}`), ) 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 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('', level + 3) return `${indent('', level + 2)}\n${content}\n${indent('', level + 2)}` }).join('\n') return `${indent('', level + 1)}\n${cells}\n${indent('', level + 1)}` }).join('\n') const className = JSON.stringify(String(node.props.className || '')) return `${indent(`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('', level + 1)} ${rows} ${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 || '')})` : staticValueExpression(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)}` } 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 || '')})` : staticValueExpression(Array.isArray(staticOptions) ? staticOptions : []) return `${indent(``, level)}\n${indent(`{${optionsExpression}.map((option, optionIndex) => {String(option.label ?? option.value ?? \`Öğe \${optionIndex + 1}\`)})}`, level + 1)}\n${indent('', level)}` } if (node.type === 'Pagination') { const { items: staticOptions = [], ...paginationProps } = node.props const { items: optionsBinding, ...paginationBindings } = node.bindings || {} const paginationPropsCode = propsToCode( { ...node, props: paginationProps, bindings: paginationBindings }, itemVariable, ) const binding = optionsBinding?.sourceId ? bindingExpression(node, 'items', itemVariable) : '' const optionsExpression = binding ? `toSelectOptions(${binding}, ${JSON.stringify(optionsBinding?.labelPath || '')}, ${JSON.stringify(optionsBinding?.valuePath || '')})` : staticValueExpression(Array.isArray(staticOptions) ? staticOptions : []) return indent( ``, 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 || '')})` : staticValueExpression(Array.isArray(staticOptions) ? staticOptions : []) return `${indent(``, level)}\n${indent(`{${optionsExpression}.map((option, optionIndex) => {String(option.label ?? option.value ?? \`Seçenek \${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(`{${staticValueExpression(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[] = [] const runtimeStateHooks: string[] = [] let hasSelect = false let hasDataTable = false const visit = (nodes: DesignerNode[]) => { nodes.forEach((node) => { if ( ['Select', 'AutoComplete', 'Menu', 'Dropdown', 'Pagination', 'Radio.Group'].includes( node.type, ) ) { hasSelect = true } 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)` : node.type === 'Steps' || node.type === 'ImageViewer' ? `${runtimeState.setterName}(Number(valueOrEvent) || 0)` : node.type === 'Upload' ? `${runtimeState.setterName}(Array.isArray(originalEvent) ? originalEvent : [])` : runtimeState.propertyName === 'checked' ? `${runtimeState.setterName}(typeof originalEvent === "object" && originalEvent?.target ? Boolean(originalEvent.target.checked) : 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' handlers.push( ` const handle_${safeIdentifier(node.id)}_${eventName} = (valueOrEvent, originalEvent) => {\n${stateUpdate ? ` ${stateUpdate}\n` : ''}${eventDeclaration}${script ? `\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", "children", "name", "title", "text", "description"].find((key) => item[key] !== undefined) const valueKey = ["value", "eventKey", "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, ...runtimeStateHooks, mount, ...handlers].filter(Boolean).join('\n\n')}\n\n return (\n <>\n${body}\n \n )\n}\n\nexport default ${componentName}\n` }