import { getDesignerCollectionProperty, getDesignerTabSlotValue, isDesignerDateComponent, isDesignerDateProperty, isDesignerOptionComponent, normalizeDesignerKeyList, DESIGNER_DROPDOWN_PLACEHOLDER, 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' }, Tabs: { propertyName: 'value', 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 === 'Tabs') { initialValue = initialValue === undefined || initialValue === null ? '' : String(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) => isDesignerOptionComponent(node.type) && propertyName === getDesignerCollectionProperty(node.type) /** * Options expression for a data component: the endpoint binding mapped through * `toSelectOptions` (honouring the configured label/value columns), otherwise the * static collection maintained in the designer. */ const optionCollectionExpression = (node: DesignerNode, itemVariable?: string) => { const collectionProperty = getDesignerCollectionProperty(node.type) const binding = node.bindings?.[collectionProperty] const boundExpression = binding?.sourceId ? bindingExpression(node, collectionProperty, itemVariable) : '' if (boundExpression) { return `toSelectOptions(${boundExpression}, ${JSON.stringify(binding?.labelPath || '')}, ${JSON.stringify(binding?.valuePath || '')})` } const staticOptions = node.props[collectionProperty] return staticValueExpression(Array.isArray(staticOptions) ? staticOptions : []) } /** Node stripped of its collection prop/binding; those are rendered as children. */ const withoutCollection = (node: DesignerNode) => { const collectionProperty = getDesignerCollectionProperty(node.type) const props = { ...node.props } const bindings = { ...(node.bindings || {}) } delete props[collectionProperty] delete bindings[collectionProperty] return { node: { ...node, props, bindings }, props, bindings } } const propsToCode = (node: DesignerNode, itemVariable?: string, omitProperties: string[] = []) => { const omitted = new Set(omitProperties) 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 && !omitted.has(key) && !(node.type === 'Checkbox' && key === 'defaultChecked' && 'checked' in node.props) && !(hasBoundInputValue && !hasInputChangeHandler && key === 'readOnly') && !node.bindings?.[key]?.sourceId && value !== '' && value !== undefined, ) .map(([key, value]) => // Dates live in the document as ISO strings; pickers need Date instances. isDesignerDateProperty(node.type, key) ? `${key}={toDesignerDate(${staticValueExpression(value)})}` : `${key}=${serializeValue(value)}`, ) Object.keys(node.bindings || {}).forEach((propertyName) => { if (propertyName === 'children') return const expression = bindingExpression(node, propertyName, itemVariable) if (expression) { // Label/value columns come from the bound property itself; the collection // is named `items` on some data components and `options` on others. const binding = node.bindings?.[propertyName] const propertyExpression = isOptionCollectionProperty(node, propertyName) ? `toSelectOptions(${expression}, ${JSON.stringify(binding?.labelPath || '')}, ${JSON.stringify(binding?.valuePath || '')})` : expression props.push(`${propertyName}={${propertyExpression}}`) } }) if (hasBoundInputValue && !hasInputChangeHandler) props.push('readOnly={true}') if (runtimeState && !omitted.has(runtimeState.propertyName)) { 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}}`), ) // Spread first so anything configured on the node overrides the defaults. const leading = node.type === 'Select' ? ['{...selectMenuProps}'] : [] const allProps = [...leading, ...props] return allProps.length ? ` ${allProps.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 { node: menuNode, props: menuProps } = withoutCollection(node) delete menuProps.variant menuProps.defaultActiveKeys = normalizeDesignerKeyList(menuProps.defaultActiveKeys) menuProps.defaultExpandedKeys = normalizeDesignerKeyList(menuProps.defaultExpandedKeys) const menuPropsCode = propsToCode(menuNode, itemVariable) const optionsExpression = optionCollectionExpression(node, itemVariable) 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 { node: dropdownNode, props: dropdownProps, bindings } = withoutCollection(node) // Rendered from the active option below, so `title` must not also be emitted // by propsToCode — whether it is static or bound to an endpoint field. const boundTitle = bindings.title?.sourceId ? bindingExpression(node, 'title', itemVariable) : '' const fallbackTitleExpression = boundTitle || staticValueExpression(dropdownProps.title ?? '') delete dropdownProps.title delete bindings.title const dropdownPropsCode = propsToCode(dropdownNode, itemVariable) const optionsExpression = optionCollectionExpression(node, itemVariable) const activeKeyExpression = getRuntimeStateSpec(node)?.stateName || bindingExpression(node, 'activeKey', itemVariable) || staticValueExpression(node.props.activeKey ?? '') const titleExpression = `toDropdownTitle(${optionsExpression}, ${activeKeyExpression}, ${fallbackTitleExpression})` 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 { node: paginationNode } = withoutCollection(node) const paginationPropsCode = propsToCode(paginationNode, itemVariable) const optionsExpression = optionCollectionExpression(node, itemVariable) return indent( ``, level, ) } if (node.type === 'Tabs') { const { node: tabsNode } = withoutCollection(node) // `value` is emitted below, resolved against the (possibly async) tab list. const tabsPropsCode = propsToCode(tabsNode, itemVariable, ['value']) const identifier = safeIdentifier(node.id) const optionsVariable = `tabItems_${identifier}` const activeVariable = `tabValue_${identifier}` const optionVariable = `tabItem_${identifier}` const activeStateExpression = getRuntimeStateSpec(node)?.stateName || bindingExpression(node, 'value', itemVariable) || staticValueExpression(node.props.value ?? '') const childrenVariable = `tabChildren_${identifier}` const fallbackVariable = `tabFallback_${identifier}` // Children are assigned to a tab through a `tab:` slot. Building a // lookup keyed by tab value keeps this working when the tab list itself is // only known at runtime (endpoint driven tabs). const slotGroups = new Map() node.children.forEach((child) => { const key = getDesignerTabSlotValue(child.slot) const group = slotGroups.get(key) if (group) group.push(child) else slotGroups.set(key, [child]) }) const fragmentFor = (children: DesignerNode[], fragmentLevel: number) => `${indent('<>', fragmentLevel)}\n${children .map((child) => nodeToCode(child, fragmentLevel + 1, itemVariable)) .join('\n')}\n${indent('', fragmentLevel)}` const slottedEntries = [...slotGroups] .filter(([key]) => key !== '') .map( ([key, children]) => `${indent(`${JSON.stringify(key)}: (`, level + 2)}\n${fragmentFor(children, level + 3)}\n${indent('),', level + 2)}`, ) .join('\n') // Children saved before per-tab slots existed belong to the first tab. const fallbackChildren = slotGroups.get('') || [] return `${indent('{(() => {', level)} ${indent(`const ${optionsVariable} = ${optionCollectionExpression(node, itemVariable)}`, level + 1)} ${indent(`const ${activeVariable} = toTabValue(${optionsVariable}, ${activeStateExpression})`, level + 1)} ${indent(`const ${childrenVariable} = {`, level + 1)} ${slottedEntries} ${indent('}', level + 1)} ${indent(`const ${fallbackVariable} = (`, level + 1)} ${fallbackChildren.length ? fragmentFor(fallbackChildren, level + 2) : indent('null', level + 2)} ${indent(')', level + 1)} ${indent('return (', level + 1)} ${indent(``, level + 2)} ${indent('', level + 3)} ${indent(`{${optionsVariable}.map((${optionVariable}, tabIndex) => {String(${optionVariable}.label ?? ${optionVariable}.value ?? \`Sekme \${tabIndex + 1}\`)})}`, level + 4)} ${indent('', level + 3)} ${indent(`{${optionsVariable}.map((${optionVariable}, tabIndex) => (`, level + 3)} ${indent(``, level + 4)} ${indent(`{${optionVariable}.content ?
{String(${optionVariable}.content)}
: null}`, level + 5)} ${indent(`{${childrenVariable}[String(${optionVariable}.value ?? tabIndex)] ?? null}`, level + 5)} ${indent(`{tabIndex === 0 ? ${fallbackVariable} : null}`, level + 5)} ${indent('
', level + 4)} ${indent('))}', level + 3)} ${indent('', level + 2)} ${indent(')', level + 1)} ${indent('})()}', level)}` } if (node.type === 'Radio.Group') { const { node: groupNode, props: groupProps } = withoutCollection(node) delete groupProps.checked delete groupProps.defaultChecked delete groupProps.readOnly const groupPropsCode = propsToCode(groupNode, itemVariable) const optionsExpression = optionCollectionExpression(node, itemVariable) 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 let hasDropdown = false let hasSelectComponent = false let hasDatePicker = false let hasTabs = false const visit = (nodes: DesignerNode[]) => { nodes.forEach((node) => { if (isDesignerOptionComponent(node.type)) hasSelect = true if (node.type === 'Select') hasSelectComponent = true if (node.type === 'Dropdown') hasDropdown = true if (node.type === 'Grid') hasDataTable = true if (node.type === 'Tabs') hasTabs = true if (isDesignerDateComponent(node.type)) hasDatePicker = true const runtimeState = getRuntimeStateSpec(node) if (runtimeState) { // Date pickers hold a Date in state but an ISO string in the document. const initialExpression = isDesignerDateProperty(node.type, runtimeState.propertyName) ? `toDesignerDate(${JSON.stringify(runtimeState.initialValue ?? null)})` : JSON.stringify(runtimeState.initialValue) runtimeStateHooks.push( ` const [${runtimeState.stateName}, ${runtimeState.setterName}] = React.useState(${initialExpression})`, ) } 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 === 'Tabs' ? `${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 } }) }` : '' // Dropdown paints its toggle from `title`; `activeKey` only marks the item // inside the closed menu, so the label of the active option is resolved here. const dropdownHelpers = hasDropdown ? ` const toDropdownTitle = (options, activeKey, fallbackTitle) => { const key = activeKey === undefined || activeKey === null ? "" : String(activeKey) const selected = key ? options.find((option) => String(option?.value ?? "") === key) : undefined if (selected) return String(selected.label ?? selected.value ?? key) return String(fallbackTitle ?? "") || ${JSON.stringify(DESIGNER_DROPDOWN_PLACEHOLDER)} }` : '' // Falls back to the first tab while an endpoint driven tab list is still empty // or when the stored value no longer exists. const tabHelpers = hasTabs ? ` const toTabValue = (options, value) => { const key = value === undefined || value === null ? "" : String(value) if (key && options.some((option) => String(option?.value ?? "") === key)) return key return options.length ? String(options[0]?.value ?? "") : "" }` : '' // Pickers check `instanceof Date`, but the document only stores ISO strings. const dateHelpers = hasDatePicker ? ` const toDesignerDate = (value) => { if (Array.isArray(value)) return value.map(toDesignerDate) if (value instanceof Date) return Number.isNaN(value.getTime()) ? null : value if (typeof value !== "string" || !value.trim()) return null const parsed = new Date(value) return Number.isNaN(parsed.getTime()) ? null : parsed }` : '' // Select renders its menu inline, so a scrollable/clipped ancestor (Grid, Table, // Card) would cut it off. A body portal keeps the menu fully visible. const selectMenuHelpers = hasSelectComponent ? ` const selectMenuProps = { menuPosition: "fixed", menuPortalTarget: typeof window === "undefined" ? null : window.document.body, styles: { menuPortal: (base) => ({ ...base, zIndex: 60 }) }, }` : '' 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, dropdownHelpers, selectMenuHelpers, dateHelpers, tabHelpers, dataHooks, ...runtimeStateHooks, mount, ...handlers].filter(Boolean).join('\n\n')}\n\n return (\n <>\n${body}\n \n )\n}\n\nexport default ${componentName}\n` }