Custom Component veritabanı yokken çağrısı
This commit is contained in:
parent
25ae424f42
commit
9a76afae24
12 changed files with 1423 additions and 409 deletions
771
ui/package-lock.json
generated
771
ui/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -67,6 +67,7 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.39.0",
|
"@eslint/js": "^9.39.0",
|
||||||
|
"@rolldown/plugin-babel": "^0.1.7",
|
||||||
"@tailwindcss/typography": "^0.5.15",
|
"@tailwindcss/typography": "^0.5.15",
|
||||||
"@types/babel__standalone": "^7.1.9",
|
"@types/babel__standalone": "^7.1.9",
|
||||||
"@types/file-saver": "^2.0.7",
|
"@types/file-saver": "^2.0.7",
|
||||||
|
|
@ -79,7 +80,7 @@
|
||||||
"@types/react-modal": "^3.13.1",
|
"@types/react-modal": "^3.13.1",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.62.1",
|
"@typescript-eslint/eslint-plugin": "^8.62.1",
|
||||||
"@typescript-eslint/parser": "^8.62.1",
|
"@typescript-eslint/parser": "^8.62.1",
|
||||||
"@vitejs/plugin-react": "^4.2.1",
|
"@vitejs/plugin-react": "^6.0.5",
|
||||||
"autoprefixer": "^10.4.14",
|
"autoprefixer": "^10.4.14",
|
||||||
"browserslist": "^4.25.2",
|
"browserslist": "^4.25.2",
|
||||||
"cssnano": "^6.0.1",
|
"cssnano": "^6.0.1",
|
||||||
|
|
@ -98,7 +99,7 @@
|
||||||
"prettier": "^3.1.1",
|
"prettier": "^3.1.1",
|
||||||
"tailwindcss": "^3.4.17",
|
"tailwindcss": "^3.4.17",
|
||||||
"typescript": "5.9",
|
"typescript": "5.9",
|
||||||
"vite": "^7.3.6",
|
"vite": "^8.2.0",
|
||||||
"vite-plugin-pwa": "^1.3.0"
|
"vite-plugin-pwa": "^1.3.0"
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,8 @@ import {
|
||||||
} from './types'
|
} from './types'
|
||||||
|
|
||||||
export const DESIGNER_DRAG_TYPE = 'application/x-sozsoft-designer'
|
export const DESIGNER_DRAG_TYPE = 'application/x-sozsoft-designer'
|
||||||
|
const getTableDimension = (value: unknown, fallback: number) =>
|
||||||
|
Math.min(20, Math.max(1, Math.floor(Number(value) || fallback)))
|
||||||
|
|
||||||
interface VisualCanvasProps {
|
interface VisualCanvasProps {
|
||||||
nodes: DesignerNode[]
|
nodes: DesignerNode[]
|
||||||
|
|
@ -19,10 +21,11 @@ interface VisualCanvasProps {
|
||||||
background: string
|
background: string
|
||||||
interactive?: boolean
|
interactive?: boolean
|
||||||
onSelect?: (id: string) => void
|
onSelect?: (id: string) => void
|
||||||
onDropComponent?: (definitionName: string, parentId: string | null) => void
|
onDropComponent?: (definitionName: string, parentId: string | null, slot?: string) => void
|
||||||
onMove?: (id: string, direction: -1 | 1) => void
|
onMove?: (id: string, direction: -1 | 1) => void
|
||||||
onDuplicate?: (id: string) => void
|
onDuplicate?: (id: string) => void
|
||||||
onDelete?: (id: string) => void
|
onDelete?: (id: string) => void
|
||||||
|
onNodePropChange?: (id: string, propertyName: string, value: unknown) => void
|
||||||
renderCustomComponent?: (name: string, props?: Record<string, unknown>) => React.ReactNode
|
renderCustomComponent?: (name: string, props?: Record<string, unknown>) => React.ReactNode
|
||||||
dataValues?: Record<string, unknown>
|
dataValues?: Record<string, unknown>
|
||||||
}
|
}
|
||||||
|
|
@ -105,10 +108,12 @@ const toSelectOptions = (
|
||||||
const primitiveKeys = Object.keys(record).filter(
|
const primitiveKeys = Object.keys(record).filter(
|
||||||
(key) => record[key] === null || ['string', 'number', 'boolean'].includes(typeof record[key]),
|
(key) => record[key] === null || ['string', 'number', 'boolean'].includes(typeof record[key]),
|
||||||
)
|
)
|
||||||
const labelKey = ['label', 'name', 'title', 'text', 'description'].find(
|
const labelKey = ['label', 'children', 'name', 'title', 'text', 'description'].find(
|
||||||
|
(key) => record[key] !== undefined,
|
||||||
|
)
|
||||||
|
const valueKey = ['value', 'eventKey', 'id', 'code', 'key'].find(
|
||||||
(key) => record[key] !== undefined,
|
(key) => record[key] !== undefined,
|
||||||
)
|
)
|
||||||
const valueKey = ['value', 'id', 'code', 'key'].find((key) => record[key] !== undefined)
|
|
||||||
const labelValue = labelPath
|
const labelValue = labelPath
|
||||||
? getDesignerValueByPath(record, labelPath)
|
? getDesignerValueByPath(record, labelPath)
|
||||||
: labelKey
|
: labelKey
|
||||||
|
|
@ -131,6 +136,7 @@ const getPreviewProps = (
|
||||||
node: DesignerNode,
|
node: DesignerNode,
|
||||||
dataValues: Record<string, unknown>,
|
dataValues: Record<string, unknown>,
|
||||||
currentItem?: unknown,
|
currentItem?: unknown,
|
||||||
|
onNodePropChange?: (id: string, propertyName: string, value: unknown) => void,
|
||||||
) => {
|
) => {
|
||||||
const props: Record<string, unknown> = {}
|
const props: Record<string, unknown> = {}
|
||||||
Object.entries(node.props).forEach(([key, value]) => {
|
Object.entries(node.props).forEach(([key, value]) => {
|
||||||
|
|
@ -160,8 +166,12 @@ const getPreviewProps = (
|
||||||
props[propertyName] = getBindingValue(binding, dataValues, currentItem)
|
props[propertyName] = getBindingValue(binding, dataValues, currentItem)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
if (node.type === 'Select' || node.type === 'AutoComplete' || node.type === 'Menu') {
|
if (
|
||||||
const collectionProperty = node.type === 'Menu' ? 'items' : 'options'
|
['Select', 'AutoComplete', 'Menu', 'Dropdown', 'Radio.Group'].includes(node.type)
|
||||||
|
) {
|
||||||
|
const collectionProperty = ['Menu', 'Dropdown', 'Radio.Group'].includes(node.type)
|
||||||
|
? 'items'
|
||||||
|
: 'options'
|
||||||
if (node.type === 'Select') {
|
if (node.type === 'Select') {
|
||||||
const legacyAliases: Record<string, string> = {
|
const legacyAliases: Record<string, string> = {
|
||||||
clearable: 'isClearable',
|
clearable: 'isClearable',
|
||||||
|
|
@ -184,21 +194,71 @@ const getPreviewProps = (
|
||||||
)
|
)
|
||||||
if (node.type === 'Select' && 'value' in props) {
|
if (node.type === 'Select' && 'value' in props) {
|
||||||
const options = props.options as Array<Record<string, unknown>>
|
const options = props.options as Array<Record<string, unknown>>
|
||||||
props.defaultValue = options.find((option) => option.value === props.value) || null
|
const selectedValue = props.value
|
||||||
delete props.value
|
props.value = props.isMulti
|
||||||
|
? options.filter(
|
||||||
|
(option) => Array.isArray(selectedValue) && selectedValue.includes(option.value),
|
||||||
|
)
|
||||||
|
: options.find((option) => option.value === selectedValue) || null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (node.type === 'Input' && 'value' in props) {
|
|
||||||
props.defaultValue = props.value
|
const chainHandler = (
|
||||||
delete props.value
|
eventName: string,
|
||||||
|
update: (...args: unknown[]) => void,
|
||||||
|
) => {
|
||||||
|
const storedHandler = typeof props[eventName] === 'function'
|
||||||
|
? (props[eventName] as (...args: unknown[]) => void)
|
||||||
|
: undefined
|
||||||
|
props[eventName] = (...args: unknown[]) => {
|
||||||
|
update(...args)
|
||||||
|
storedHandler?.(...args)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (node.type === 'Checkbox' && 'checked' in props) {
|
const updateProp = (propertyName: string, value: unknown) =>
|
||||||
props.defaultChecked = Boolean(props.checked)
|
onNodePropChange?.(node.id, propertyName, value)
|
||||||
delete props.checked
|
const eventValue = (value: unknown) =>
|
||||||
|
value && typeof value === 'object' && 'target' in value
|
||||||
|
? (value as { target?: { value?: unknown } }).target?.value
|
||||||
|
: value
|
||||||
|
|
||||||
|
if (node.type === 'Select') {
|
||||||
|
chainHandler('onChange', (selected) =>
|
||||||
|
updateProp(
|
||||||
|
'value',
|
||||||
|
Array.isArray(selected)
|
||||||
|
? selected.map((option) =>
|
||||||
|
option && typeof option === 'object' && 'value' in option
|
||||||
|
? (option as { value: unknown }).value
|
||||||
|
: option,
|
||||||
|
)
|
||||||
|
: selected && typeof selected === 'object' && 'value' in selected
|
||||||
|
? (selected as { value: unknown }).value
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
} else if (node.type === 'AutoComplete') {
|
||||||
|
chainHandler('onInputChange', (value) => updateProp('value', value ?? ''))
|
||||||
|
} else if (node.type === 'Menu') {
|
||||||
|
chainHandler('onSelect', (value) => updateProp('defaultActiveKeys', [String(value ?? '')]))
|
||||||
|
} else if (node.type === 'Dropdown') {
|
||||||
|
chainHandler('onSelect', (value) => updateProp('activeKey', String(value ?? '')))
|
||||||
|
} else if (node.type === 'Radio.Group') {
|
||||||
|
chainHandler('onChange', (value) => updateProp('value', value))
|
||||||
|
} else if (node.type === 'Pagination') {
|
||||||
|
chainHandler('onChange', (value) => updateProp('currentPage', Number(value) || 1))
|
||||||
|
} else if ('checked' in props) {
|
||||||
|
chainHandler('onChange', (value) => {
|
||||||
|
const checked =
|
||||||
|
value && typeof value === 'object' && 'target' in value
|
||||||
|
? Boolean((value as { target?: { checked?: unknown } }).target?.checked)
|
||||||
|
: Boolean(value)
|
||||||
|
updateProp('checked', checked)
|
||||||
|
})
|
||||||
|
} else if ('value' in props) {
|
||||||
|
chainHandler('onChange', (value) => updateProp('value', eventValue(value)))
|
||||||
}
|
}
|
||||||
if ('value' in props && !props.onChange) props.onChange = () => undefined
|
props.key = node.id
|
||||||
if ('checked' in props && !props.onChange) props.onChange = () => undefined
|
|
||||||
props.key = JSON.stringify([node.props, node.bindings])
|
|
||||||
return props
|
return props
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -339,10 +399,17 @@ const renderElement = (
|
||||||
dataValues: Record<string, unknown>,
|
dataValues: Record<string, unknown>,
|
||||||
currentItem: unknown,
|
currentItem: unknown,
|
||||||
interactive: boolean,
|
interactive: boolean,
|
||||||
|
onNodePropChange?: (id: string, propertyName: string, value: unknown) => void,
|
||||||
renderCustomComponent?: (name: string, props?: Record<string, unknown>) => React.ReactNode,
|
renderCustomComponent?: (name: string, props?: Record<string, unknown>) => React.ReactNode,
|
||||||
) => {
|
) => {
|
||||||
if (node.type === 'Spacer') {
|
if (node.type === 'Spacer') {
|
||||||
return <div aria-hidden="true" style={{ height: Number(node.props.height) || 24 }} />
|
return (
|
||||||
|
<div
|
||||||
|
aria-hidden="true"
|
||||||
|
className={String(node.props.className || '')}
|
||||||
|
style={{ height: Number(node.props.height) || 24 }}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
if (node.type === 'FlexRow') {
|
if (node.type === 'FlexRow') {
|
||||||
return (
|
return (
|
||||||
|
|
@ -428,10 +495,6 @@ const renderElement = (
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (node.kind === 'custom') {
|
|
||||||
return renderCustomComponent?.(node.type, { ...node.props, children }) || null
|
|
||||||
}
|
|
||||||
|
|
||||||
const childrenBinding = node.bindings?.children
|
const childrenBinding = node.bindings?.children
|
||||||
const boundChildren = childrenBinding?.sourceId
|
const boundChildren = childrenBinding?.sourceId
|
||||||
? getBindingValue(childrenBinding, dataValues, currentItem)
|
? getBindingValue(childrenBinding, dataValues, currentItem)
|
||||||
|
|
@ -446,40 +509,11 @@ const renderElement = (
|
||||||
: typeof boundChildren === 'object'
|
: typeof boundChildren === 'object'
|
||||||
? JSON.stringify(boundChildren)
|
? JSON.stringify(boundChildren)
|
||||||
: String(boundChildren)
|
: String(boundChildren)
|
||||||
const props = getPreviewProps(node, dataValues, currentItem)
|
const props = getPreviewProps(node, dataValues, currentItem, onNodePropChange)
|
||||||
if (node.type === 'Table') {
|
if (node.kind === 'custom') {
|
||||||
const legacyData = Array.isArray(props.data) ? props.data : []
|
return renderCustomComponent?.(node.type, { ...props, children: content }) || null
|
||||||
const items =
|
|
||||||
Array.isArray(props.items) && (props.items.length > 0 || !legacyData.length)
|
|
||||||
? props.items
|
|
||||||
: legacyData
|
|
||||||
const legacyColumns = 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<string, unknown>
|
|
||||||
return String(record.dataIndex ?? record.key ?? '')
|
|
||||||
})
|
|
||||||
.filter(Boolean)
|
|
||||||
: undefined
|
|
||||||
return (
|
|
||||||
<GridDataTablePreview
|
|
||||||
borderlessRow={Boolean(node.props.borderlessRow)}
|
|
||||||
compact={Boolean(node.props.compact)}
|
|
||||||
hoverable={node.props.hoverable !== false}
|
|
||||||
items={items}
|
|
||||||
overflow={node.props.overflow !== false}
|
|
||||||
selectedColumns={
|
|
||||||
Array.isArray(node.props.dataColumns)
|
|
||||||
? node.props.dataColumns.filter(
|
|
||||||
(column): column is string => typeof column === 'string',
|
|
||||||
)
|
|
||||||
: legacyColumns
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
if (node.type === 'Table') return <>{children}</>
|
||||||
if (node.type === 'Menu') {
|
if (node.type === 'Menu') {
|
||||||
const menuProps = { ...props }
|
const menuProps = { ...props }
|
||||||
const options = Array.isArray(menuProps.items)
|
const options = Array.isArray(menuProps.items)
|
||||||
|
|
@ -503,6 +537,51 @@ const renderElement = (
|
||||||
</UiKit.Menu>
|
</UiKit.Menu>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
if (node.type === 'Dropdown') {
|
||||||
|
const dropdownProps = { ...props }
|
||||||
|
const options = Array.isArray(dropdownProps.items)
|
||||||
|
? (dropdownProps.items as Array<Record<string, unknown>>)
|
||||||
|
: []
|
||||||
|
delete dropdownProps.items
|
||||||
|
return (
|
||||||
|
<UiKit.Dropdown {...(dropdownProps as React.ComponentProps<typeof UiKit.Dropdown>)}>
|
||||||
|
{options.map((option, index) => (
|
||||||
|
<UiKit.Dropdown.Item
|
||||||
|
key={String(option.value ?? index)}
|
||||||
|
disabled={Boolean(option.disabled)}
|
||||||
|
eventKey={String(option.value ?? index)}
|
||||||
|
>
|
||||||
|
{String(option.label ?? option.value ?? `Öğe ${index + 1}`)}
|
||||||
|
</UiKit.Dropdown.Item>
|
||||||
|
))}
|
||||||
|
{content}
|
||||||
|
</UiKit.Dropdown>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (node.type === 'Radio.Group') {
|
||||||
|
const groupProps = { ...props }
|
||||||
|
const options = Array.isArray(groupProps.items)
|
||||||
|
? (groupProps.items as Array<Record<string, unknown>>)
|
||||||
|
: []
|
||||||
|
delete groupProps.items
|
||||||
|
delete groupProps.checked
|
||||||
|
delete groupProps.defaultChecked
|
||||||
|
delete groupProps.readOnly
|
||||||
|
return (
|
||||||
|
<UiKit.Radio.Group {...(groupProps as React.ComponentProps<typeof UiKit.Radio.Group>)}>
|
||||||
|
{options.map((option, index) => (
|
||||||
|
<UiKit.Radio
|
||||||
|
key={String(option.value ?? index)}
|
||||||
|
disabled={Boolean(option.disabled)}
|
||||||
|
value={option.value ?? index}
|
||||||
|
>
|
||||||
|
{String(option.label ?? option.value ?? `Seçenek ${index + 1}`)}
|
||||||
|
</UiKit.Radio>
|
||||||
|
))}
|
||||||
|
{content}
|
||||||
|
</UiKit.Radio.Group>
|
||||||
|
)
|
||||||
|
}
|
||||||
if (node.type === 'checkbox') {
|
if (node.type === 'checkbox') {
|
||||||
return <input {...(props as React.InputHTMLAttributes<HTMLInputElement>)} type="checkbox" />
|
return <input {...(props as React.InputHTMLAttributes<HTMLInputElement>)} type="checkbox" />
|
||||||
}
|
}
|
||||||
|
|
@ -529,6 +608,7 @@ const NodeView = ({
|
||||||
onMove,
|
onMove,
|
||||||
onDuplicate,
|
onDuplicate,
|
||||||
onDelete,
|
onDelete,
|
||||||
|
onNodePropChange,
|
||||||
renderCustomComponent,
|
renderCustomComponent,
|
||||||
dataValues,
|
dataValues,
|
||||||
currentItem,
|
currentItem,
|
||||||
|
|
@ -538,10 +618,11 @@ const NodeView = ({
|
||||||
selectedId: string | null
|
selectedId: string | null
|
||||||
interactive: boolean
|
interactive: boolean
|
||||||
onSelect?: (id: string) => void
|
onSelect?: (id: string) => void
|
||||||
onDropComponent?: (definitionName: string, parentId: string | null) => void
|
onDropComponent?: (definitionName: string, parentId: string | null, slot?: string) => void
|
||||||
onMove?: (id: string, direction: -1 | 1) => void
|
onMove?: (id: string, direction: -1 | 1) => void
|
||||||
onDuplicate?: (id: string) => void
|
onDuplicate?: (id: string) => void
|
||||||
onDelete?: (id: string) => void
|
onDelete?: (id: string) => void
|
||||||
|
onNodePropChange?: (id: string, propertyName: string, value: unknown) => void
|
||||||
renderCustomComponent?: (name: string, props?: Record<string, unknown>) => React.ReactNode
|
renderCustomComponent?: (name: string, props?: Record<string, unknown>) => React.ReactNode
|
||||||
dataValues: Record<string, unknown>
|
dataValues: Record<string, unknown>
|
||||||
currentItem?: unknown
|
currentItem?: unknown
|
||||||
|
|
@ -557,7 +638,7 @@ const NodeView = ({
|
||||||
'Card',
|
'Card',
|
||||||
'FormContainer',
|
'FormContainer',
|
||||||
].includes(node.type)
|
].includes(node.type)
|
||||||
const itemsBinding = ['Grid', 'Table'].includes(node.type) ? node.bindings?.items : undefined
|
const itemsBinding = node.type === 'Grid' ? node.bindings?.items : undefined
|
||||||
const boundItems = itemsBinding?.sourceId
|
const boundItems = itemsBinding?.sourceId
|
||||||
? getBindingValue(itemsBinding, dataValues, currentItem)
|
? getBindingValue(itemsBinding, dataValues, currentItem)
|
||||||
: node.props.items
|
: node.props.items
|
||||||
|
|
@ -590,11 +671,108 @@ const NodeView = ({
|
||||||
onMove={onMove}
|
onMove={onMove}
|
||||||
onDuplicate={onDuplicate}
|
onDuplicate={onDuplicate}
|
||||||
onDelete={onDelete}
|
onDelete={onDelete}
|
||||||
|
onNodePropChange={onNodePropChange}
|
||||||
/>
|
/>
|
||||||
)),
|
)),
|
||||||
)
|
)
|
||||||
|
const tableContent =
|
||||||
|
node.type === 'Table' ? (
|
||||||
|
<UiKit.Table
|
||||||
|
borderlessRow
|
||||||
|
className={`${String(node.props.className || '')} [&>tbody>tr>td]:!px-1.5 [&>tbody>tr>td]:!py-1.5`}
|
||||||
|
compact={Boolean(node.props.compact)}
|
||||||
|
hoverable={node.props.hoverable !== false}
|
||||||
|
overflow={node.props.overflow !== false}
|
||||||
|
style={{
|
||||||
|
minWidth: '100%',
|
||||||
|
tableLayout: 'fixed',
|
||||||
|
width: '100%',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<tbody>
|
||||||
|
{Array.from({ length: getTableDimension(node.props.rowCount, 2) }, (_, rowIndex) => (
|
||||||
|
<tr key={`row_${rowIndex}`}>
|
||||||
|
{Array.from(
|
||||||
|
{ length: getTableDimension(node.props.columnCount, 3) },
|
||||||
|
(_, columnIndex) => {
|
||||||
|
const slot = `table:${rowIndex}:${columnIndex}`
|
||||||
|
const cellNodes = node.children.filter(
|
||||||
|
(child, childIndex) =>
|
||||||
|
(child.slot ||
|
||||||
|
`table:${Math.floor(
|
||||||
|
childIndex / getTableDimension(node.props.columnCount, 3),
|
||||||
|
)}:${childIndex % getTableDimension(node.props.columnCount, 3)}`) === slot,
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
<td
|
||||||
|
key={slot}
|
||||||
|
className={
|
||||||
|
interactive
|
||||||
|
? 'border border-slate-200 align-top dark:border-slate-700'
|
||||||
|
: 'align-top'
|
||||||
|
}
|
||||||
|
onDragOver={(event) => {
|
||||||
|
if (!interactive) return
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
event.dataTransfer.dropEffect = 'copy'
|
||||||
|
}}
|
||||||
|
onDrop={(event) => {
|
||||||
|
if (!interactive) return
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
const raw =
|
||||||
|
event.dataTransfer.getData(DESIGNER_DRAG_TYPE) ||
|
||||||
|
event.dataTransfer.getData('text/plain')
|
||||||
|
if (!raw) return
|
||||||
|
try {
|
||||||
|
const payload = JSON.parse(raw)
|
||||||
|
if (payload.source === 'library') {
|
||||||
|
onDropComponent?.(payload.name, node.id, slot)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Ignore payloads that do not belong to the visual designer.
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="min-h-14 min-w-0 p-1">
|
||||||
|
{cellNodes.map((child, childIndex) => (
|
||||||
|
<NodeView
|
||||||
|
key={child.id}
|
||||||
|
node={child}
|
||||||
|
index={childIndex}
|
||||||
|
selectedId={selectedId}
|
||||||
|
interactive={interactive}
|
||||||
|
renderCustomComponent={renderCustomComponent}
|
||||||
|
dataValues={dataValues}
|
||||||
|
currentItem={currentItem}
|
||||||
|
onSelect={onSelect}
|
||||||
|
onDropComponent={onDropComponent}
|
||||||
|
onMove={onMove}
|
||||||
|
onDuplicate={onDuplicate}
|
||||||
|
onDelete={onDelete}
|
||||||
|
onNodePropChange={onNodePropChange}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{interactive && !cellNodes.length && (
|
||||||
|
<div className="flex min-h-12 items-center justify-center rounded border border-dashed border-slate-300 px-2 text-center text-[10px] text-slate-400 dark:border-slate-700">
|
||||||
|
Komponent bırakın
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</UiKit.Table>
|
||||||
|
) : null
|
||||||
const renderedChildren =
|
const renderedChildren =
|
||||||
node.type === 'Grid' && interactive && node.children.length === 0
|
node.type === 'Table'
|
||||||
|
? tableContent
|
||||||
|
: node.type === 'Grid' && interactive && node.children.length === 0
|
||||||
? repeatedItems.length
|
? repeatedItems.length
|
||||||
? [
|
? [
|
||||||
<GridDataTablePreview
|
<GridDataTablePreview
|
||||||
|
|
@ -780,6 +958,7 @@ const NodeView = ({
|
||||||
dataValues,
|
dataValues,
|
||||||
currentItem,
|
currentItem,
|
||||||
interactive,
|
interactive,
|
||||||
|
onNodePropChange,
|
||||||
renderCustomComponent,
|
renderCustomComponent,
|
||||||
)}
|
)}
|
||||||
</PreviewBoundary>
|
</PreviewBoundary>
|
||||||
|
|
@ -805,6 +984,7 @@ const VisualCanvas = ({
|
||||||
onMove,
|
onMove,
|
||||||
onDuplicate,
|
onDuplicate,
|
||||||
onDelete,
|
onDelete,
|
||||||
|
onNodePropChange,
|
||||||
renderCustomComponent,
|
renderCustomComponent,
|
||||||
dataValues = {},
|
dataValues = {},
|
||||||
}: VisualCanvasProps) => {
|
}: VisualCanvasProps) => {
|
||||||
|
|
@ -843,6 +1023,7 @@ const VisualCanvas = ({
|
||||||
onMove={onMove}
|
onMove={onMove}
|
||||||
onDuplicate={onDuplicate}
|
onDuplicate={onDuplicate}
|
||||||
onDelete={onDelete}
|
onDelete={onDelete}
|
||||||
|
onNodePropChange={onNodePropChange}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -18,8 +18,16 @@ interface GeneratedComponentMetadata {
|
||||||
}
|
}
|
||||||
|
|
||||||
const generatedMetadata = generatedComponentProps as Record<string, GeneratedComponentMetadata>
|
const generatedMetadata = generatedComponentProps as Record<string, GeneratedComponentMetadata>
|
||||||
const CONTAINER_NAMES = new Set(['div', 'Card', 'FormContainer', 'Tabs', 'Timeline'])
|
const CONTAINER_NAMES = new Set(['div', 'Card', 'FormContainer', 'Table', 'Tabs', 'Timeline'])
|
||||||
const DATA_COMPONENT_NAMES = new Set(['AutoComplete', 'Grid', 'Menu', 'Pagination', 'Select', 'Table'])
|
export const DESIGNER_DATA_COMPONENT_NAMES = new Set([
|
||||||
|
'AutoComplete',
|
||||||
|
'Dropdown',
|
||||||
|
'Grid',
|
||||||
|
'Menu',
|
||||||
|
'Pagination',
|
||||||
|
'Radio.Group',
|
||||||
|
'Select',
|
||||||
|
])
|
||||||
const HTML_UI_DUPLICATES = new Set(['input', 'button', 'textarea', 'select', 'checkbox'])
|
const HTML_UI_DUPLICATES = new Set(['input', 'button', 'textarea', 'select', 'checkbox'])
|
||||||
const UI_PROPERTY_ALIASES: Record<string, Record<string, string>> = {
|
const UI_PROPERTY_ALIASES: Record<string, Record<string, string>> = {
|
||||||
Select: {
|
Select: {
|
||||||
|
|
@ -31,10 +39,14 @@ const UI_PROPERTY_ALIASES: Record<string, Record<string, string>> = {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
export const DESIGNER_PRIMARY_EVENTS: Record<string, string[]> = {
|
export const DESIGNER_PRIMARY_EVENTS: Record<string, string[]> = {
|
||||||
|
AutoComplete: ['onInputChange', 'onSelect'],
|
||||||
Button: ['onClick'],
|
Button: ['onClick'],
|
||||||
Checkbox: ['onChange'],
|
Checkbox: ['onChange'],
|
||||||
|
Dropdown: ['onSelect'],
|
||||||
Input: ['onChange'],
|
Input: ['onChange'],
|
||||||
Menu: ['onSelect'],
|
Menu: ['onSelect'],
|
||||||
|
Pagination: ['onChange'],
|
||||||
|
'Radio.Group': ['onChange'],
|
||||||
Select: ['onChange'],
|
Select: ['onChange'],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -56,6 +68,18 @@ console.log('Value:', value)`,
|
||||||
Menu: {
|
Menu: {
|
||||||
onSelect: `// Seçilen menü öğesinin value değeridir.
|
onSelect: `// Seçilen menü öğesinin value değeridir.
|
||||||
console.log('Menu value:', event)`,
|
console.log('Menu value:', event)`,
|
||||||
|
},
|
||||||
|
Dropdown: {
|
||||||
|
onSelect: `// Seçilen dropdown öğesinin value değeridir.
|
||||||
|
console.log('Dropdown value:', event)`,
|
||||||
|
},
|
||||||
|
Pagination: {
|
||||||
|
onChange: `// Seçilen sayfa numarasıdır.
|
||||||
|
console.log('Page:', event)`,
|
||||||
|
},
|
||||||
|
'Radio.Group': {
|
||||||
|
onChange: `// Seçilen radio öğesinin value değeridir.
|
||||||
|
console.log('Radio value:', event)`,
|
||||||
},
|
},
|
||||||
Select: {
|
Select: {
|
||||||
onChange: `// Seçilen option nesnesi; temizlendiğinde null olur.
|
onChange: `// Seçilen option nesnesi; temizlendiğinde null olur.
|
||||||
|
|
@ -120,6 +144,26 @@ const GRID_TABLE_BEHAVIOR_PROPERTIES = [
|
||||||
required: false,
|
required: false,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
const TABLE_DIMENSION_PROPERTIES = [
|
||||||
|
{
|
||||||
|
name: 'rowCount',
|
||||||
|
type: 'number' as const,
|
||||||
|
value: 2,
|
||||||
|
category: 'properties' as const,
|
||||||
|
description: 'Table içindeki statik satır sayısı',
|
||||||
|
tsType: 'number',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'columnCount',
|
||||||
|
type: 'number' as const,
|
||||||
|
value: 3,
|
||||||
|
category: 'properties' as const,
|
||||||
|
description: 'Her satırdaki statik hücre sayısı',
|
||||||
|
tsType: 'number',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
]
|
||||||
const SELECT_OPTIONS_PROPERTY = {
|
const SELECT_OPTIONS_PROPERTY = {
|
||||||
name: 'options',
|
name: 'options',
|
||||||
type: 'array' as const,
|
type: 'array' as const,
|
||||||
|
|
@ -149,7 +193,7 @@ const normalizeDefinition = (
|
||||||
? definition.properties.filter((property) => property.name !== 'children')
|
? definition.properties.filter((property) => property.name !== 'children')
|
||||||
: definition.properties
|
: definition.properties
|
||||||
let properties =
|
let properties =
|
||||||
['Grid', 'Table'].includes(definition.name) &&
|
definition.name === 'Grid' &&
|
||||||
!sourceProperties.some((property) => property.name === 'items')
|
!sourceProperties.some((property) => property.name === 'items')
|
||||||
? [COLLECTION_ITEMS_PROPERTY, ...sourceProperties]
|
? [COLLECTION_ITEMS_PROPERTY, ...sourceProperties]
|
||||||
: sourceProperties
|
: sourceProperties
|
||||||
|
|
@ -164,6 +208,14 @@ const normalizeDefinition = (
|
||||||
...properties,
|
...properties,
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
if (definition.name === 'Table') {
|
||||||
|
properties = [
|
||||||
|
...TABLE_DIMENSION_PROPERTIES.filter(
|
||||||
|
(candidate) => !properties.some((property) => property.name === candidate.name),
|
||||||
|
),
|
||||||
|
...properties.filter((property) => !['columns', 'data', 'items'].includes(property.name)),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...definition,
|
...definition,
|
||||||
|
|
@ -246,7 +298,11 @@ const normalizeUiDefinition = (
|
||||||
return normalizeDefinition(
|
return normalizeDefinition(
|
||||||
definition,
|
definition,
|
||||||
'ui',
|
'ui',
|
||||||
DATA_COMPONENT_NAMES.has(definition.name) ? 'data' : 'ui',
|
definition.name === 'Table'
|
||||||
|
? 'layout'
|
||||||
|
: DESIGNER_DATA_COMPONENT_NAMES.has(definition.name)
|
||||||
|
? 'data'
|
||||||
|
: 'ui',
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -265,21 +321,32 @@ const normalizeUiDefinition = (
|
||||||
const existingProperties = new Map(curatedProperties.map((property) => [property.name, property]))
|
const existingProperties = new Map(curatedProperties.map((property) => [property.name, property]))
|
||||||
const generatedProperties = generatedPropertiesToDesigner(generated, existingProperties)
|
const generatedProperties = generatedPropertiesToDesigner(generated, existingProperties)
|
||||||
if (
|
if (
|
||||||
['Select', 'AutoComplete', 'Menu'].includes(definition.name) &&
|
['Select', 'AutoComplete', 'Menu', 'Dropdown', 'Radio.Group'].includes(definition.name) &&
|
||||||
!generatedProperties.some(
|
!generatedProperties.some(
|
||||||
(property) => property.name === (definition.name === 'Menu' ? 'items' : 'options'),
|
(property) =>
|
||||||
|
property.name ===
|
||||||
|
(['Menu', 'Dropdown', 'Radio.Group'].includes(definition.name) ? 'items' : 'options'),
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
generatedProperties.unshift(
|
generatedProperties.unshift(
|
||||||
definition.name === 'Menu' ? MENU_ITEMS_PROPERTY : SELECT_OPTIONS_PROPERTY,
|
['Menu', 'Dropdown', 'Radio.Group'].includes(definition.name)
|
||||||
|
? MENU_ITEMS_PROPERTY
|
||||||
|
: SELECT_OPTIONS_PROPERTY,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
['Grid', 'Table'].includes(definition.name) &&
|
definition.name === 'Grid' &&
|
||||||
!generatedProperties.some((property) => property.name === 'items')
|
!generatedProperties.some((property) => property.name === 'items')
|
||||||
) {
|
) {
|
||||||
generatedProperties.unshift(COLLECTION_ITEMS_PROPERTY)
|
generatedProperties.unshift(COLLECTION_ITEMS_PROPERTY)
|
||||||
}
|
}
|
||||||
|
if (definition.name === 'Table') {
|
||||||
|
generatedProperties.unshift(
|
||||||
|
...TABLE_DIMENSION_PROPERTIES.filter(
|
||||||
|
(candidate) => !generatedProperties.some((property) => property.name === candidate.name),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
const generatedPropertyNames = new Set(generatedProperties.map((property) => property.name))
|
const generatedPropertyNames = new Set(generatedProperties.map((property) => property.name))
|
||||||
const properties = [
|
const properties = [
|
||||||
...generatedProperties,
|
...generatedProperties,
|
||||||
|
|
@ -294,7 +361,12 @@ const normalizeUiDefinition = (
|
||||||
return {
|
return {
|
||||||
...definition,
|
...definition,
|
||||||
kind: 'ui',
|
kind: 'ui',
|
||||||
toolboxGroup: DATA_COMPONENT_NAMES.has(definition.name) ? 'data' : 'ui',
|
toolboxGroup:
|
||||||
|
definition.name === 'Table'
|
||||||
|
? 'layout'
|
||||||
|
: DESIGNER_DATA_COMPONENT_NAMES.has(definition.name)
|
||||||
|
? 'data'
|
||||||
|
: 'ui',
|
||||||
acceptsChildren: CONTAINER_NAMES.has(definition.name),
|
acceptsChildren: CONTAINER_NAMES.has(definition.name),
|
||||||
sourceFile: generated.sourceFile,
|
sourceFile: generated.sourceFile,
|
||||||
interfaceName: generated.interfaceName,
|
interfaceName: generated.interfaceName,
|
||||||
|
|
@ -431,7 +503,7 @@ export const DESIGNER_EXTRAS: DesignerComponentDefinition[] = [
|
||||||
icon: 'Space',
|
icon: 'Space',
|
||||||
category: 'layout',
|
category: 'layout',
|
||||||
kind: 'layout',
|
kind: 'layout',
|
||||||
toolboxGroup: 'layout',
|
toolboxGroup: 'ui',
|
||||||
description: 'Bileşenler arasında boşluk',
|
description: 'Bileşenler arasında boşluk',
|
||||||
properties: [{ name: 'height', type: 'number', value: 24, category: 'styling' }],
|
properties: [{ name: 'height', type: 'number', value: 24, category: 'styling' }],
|
||||||
hooks: [],
|
hooks: [],
|
||||||
|
|
@ -451,7 +523,12 @@ export const getDesignerCatalog = (customNames: string[] = []): DesignerComponen
|
||||||
icon: 'Blocks',
|
icon: 'Blocks',
|
||||||
category: inferUiCategory(name),
|
category: inferUiCategory(name),
|
||||||
kind: 'ui',
|
kind: 'ui',
|
||||||
toolboxGroup: DATA_COMPONENT_NAMES.has(name) ? 'data' : 'ui',
|
toolboxGroup:
|
||||||
|
name === 'Table'
|
||||||
|
? 'layout'
|
||||||
|
: DESIGNER_DATA_COMPONENT_NAMES.has(name)
|
||||||
|
? 'data'
|
||||||
|
: 'ui',
|
||||||
description: `${metadata.interfaceName} sözleşmesini kullanan Sozsoft UI component’i`,
|
description: `${metadata.interfaceName} sözleşmesini kullanan Sozsoft UI component’i`,
|
||||||
sourceFile: metadata.sourceFile,
|
sourceFile: metadata.sourceFile,
|
||||||
interfaceName: metadata.interfaceName,
|
interfaceName: metadata.interfaceName,
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,5 @@
|
||||||
import { normalizeDesignerKeyList, type DesignerDocument, type DesignerNode } from './types'
|
import { normalizeDesignerKeyList, type DesignerDocument, type DesignerNode } from './types'
|
||||||
|
|
||||||
const EVENT_NAMES: Record<string, string> = {
|
|
||||||
onClick: 'event',
|
|
||||||
onChange: 'event',
|
|
||||||
onFocus: 'event',
|
|
||||||
onBlur: 'event',
|
|
||||||
onMouseEnter: 'event',
|
|
||||||
onMouseLeave: 'event',
|
|
||||||
}
|
|
||||||
|
|
||||||
const safeIdentifier = (value: string) => {
|
const safeIdentifier = (value: string) => {
|
||||||
const cleaned = value.replace(/[^A-Za-z0-9_$]/g, '_')
|
const cleaned = value.replace(/[^A-Za-z0-9_$]/g, '_')
|
||||||
return /^[A-Za-z_$]/.test(cleaned) ? cleaned : `Component_${cleaned}`
|
return /^[A-Za-z_$]/.test(cleaned) ? cleaned : `Component_${cleaned}`
|
||||||
|
|
@ -32,13 +23,82 @@ const bindingExpression = (node: DesignerNode, propertyName: string, itemVariabl
|
||||||
return `getByPath(data_${safeIdentifier(binding.sourceId)}, ${JSON.stringify(binding.path || '')})`
|
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<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')
|
||||||
|
|
||||||
const propsToCode = (node: DesignerNode, itemVariable?: string) => {
|
const propsToCode = (node: DesignerNode, itemVariable?: string) => {
|
||||||
|
const runtimeState = getRuntimeStateSpec(node)
|
||||||
const hasBoundInputValue = node.type === 'Input' && Boolean(node.bindings?.value?.sourceId)
|
const hasBoundInputValue = node.type === 'Input' && Boolean(node.bindings?.value?.sourceId)
|
||||||
const hasInputChangeHandler = Boolean(node.events?.onChange?.trim())
|
const hasInputChangeHandler = Boolean(node.events?.onChange?.trim())
|
||||||
const props = Object.entries(node.props)
|
const props = Object.entries(node.props)
|
||||||
.filter(
|
.filter(
|
||||||
([key, value]) =>
|
([key, value]) =>
|
||||||
key !== 'children' &&
|
key !== 'children' &&
|
||||||
|
key !== runtimeState?.propertyName &&
|
||||||
!(node.type === 'Checkbox' && key === 'defaultChecked' && 'checked' in node.props) &&
|
!(node.type === 'Checkbox' && key === 'defaultChecked' && 'checked' in node.props) &&
|
||||||
!(hasBoundInputValue && !hasInputChangeHandler && key === 'readOnly') &&
|
!(hasBoundInputValue && !hasInputChangeHandler && key === 'readOnly') &&
|
||||||
!node.bindings?.[key]?.sourceId &&
|
!node.bindings?.[key]?.sourceId &&
|
||||||
|
|
@ -46,23 +106,6 @@ const propsToCode = (node: DesignerNode, itemVariable?: string) => {
|
||||||
value !== undefined,
|
value !== undefined,
|
||||||
)
|
)
|
||||||
.map(([key, value]) => {
|
.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<string, unknown>).value === value,
|
|
||||||
)
|
|
||||||
return `defaultValue=${serializeValue(selectedOption ?? null)}`
|
|
||||||
}
|
|
||||||
return `${key}=${serializeValue(value)}`
|
return `${key}=${serializeValue(value)}`
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -71,7 +114,7 @@ const propsToCode = (node: DesignerNode, itemVariable?: string) => {
|
||||||
const expression = bindingExpression(node, propertyName, itemVariable)
|
const expression = bindingExpression(node, propertyName, itemVariable)
|
||||||
if (expression) {
|
if (expression) {
|
||||||
const propertyExpression =
|
const propertyExpression =
|
||||||
['Select', 'AutoComplete'].includes(node.type) && propertyName === 'options'
|
isOptionCollectionProperty(node, propertyName)
|
||||||
? `toSelectOptions(${expression}, ${JSON.stringify(node.bindings.options?.labelPath || '')}, ${JSON.stringify(node.bindings.options?.valuePath || '')})`
|
? `toSelectOptions(${expression}, ${JSON.stringify(node.bindings.options?.labelPath || '')}, ${JSON.stringify(node.bindings.options?.valuePath || '')})`
|
||||||
: expression
|
: expression
|
||||||
props.push(`${propertyName}={${propertyExpression}}`)
|
props.push(`${propertyName}={${propertyExpression}}`)
|
||||||
|
|
@ -80,11 +123,17 @@ const propsToCode = (node: DesignerNode, itemVariable?: string) => {
|
||||||
|
|
||||||
if (hasBoundInputValue && !hasInputChangeHandler) props.push('readOnly={true}')
|
if (hasBoundInputValue && !hasInputChangeHandler) props.push('readOnly={true}')
|
||||||
|
|
||||||
Object.entries(node.events)
|
if (runtimeState) props.push(`${runtimeState.propertyName}={${runtimeState.stateName}}`)
|
||||||
.filter(([, script]) => script.trim())
|
|
||||||
.forEach(([eventName]) => {
|
const handlerNames = new Set(
|
||||||
props.push(`${eventName}={handle_${safeIdentifier(node.id)}_${eventName}}`)
|
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(' ')}` : ''
|
return props.length ? ` ${props.join(' ')}` : ''
|
||||||
}
|
}
|
||||||
|
|
@ -98,7 +147,7 @@ const indent = (text: string, level: number) =>
|
||||||
const nodeToCode = (node: DesignerNode, level = 0, itemVariable?: string): string => {
|
const nodeToCode = (node: DesignerNode, level = 0, itemVariable?: string): string => {
|
||||||
if (node.type === 'Spacer') {
|
if (node.type === 'Spacer') {
|
||||||
return indent(
|
return indent(
|
||||||
`<div aria-hidden="true" style={{ height: ${Number(node.props.height) || 24} }} />`,
|
`<div aria-hidden="true" className=${JSON.stringify(String(node.props.className || ''))} style={{ height: ${Number(node.props.height) || 24} }} />`,
|
||||||
level,
|
level,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -135,45 +184,32 @@ const nodeToCode = (node: DesignerNode, level = 0, itemVariable?: string): strin
|
||||||
}
|
}
|
||||||
|
|
||||||
if (node.type === 'Table') {
|
if (node.type === 'Table') {
|
||||||
const legacyData = Array.isArray(node.props.data) ? node.props.data : []
|
const rowCount = Math.min(20, Math.max(1, Math.floor(Number(node.props.rowCount) || 2)))
|
||||||
const staticItems =
|
const columnCount = Math.min(
|
||||||
Array.isArray(node.props.items) && (node.props.items.length > 0 || !legacyData.length)
|
20,
|
||||||
? node.props.items
|
Math.max(1, Math.floor(Number(node.props.columnCount) || 3)),
|
||||||
: legacyData
|
)
|
||||||
const itemsExpression =
|
const rows = Array.from({ length: rowCount }, (_, rowIndex) => {
|
||||||
bindingExpression(node, 'items', itemVariable) ||
|
const cells = Array.from({ length: columnCount }, (_, columnIndex) => {
|
||||||
bindingExpression(node, 'data', itemVariable) ||
|
const slot = `table:${rowIndex}:${columnIndex}`
|
||||||
JSON.stringify(staticItems)
|
const cellChildren = node.children.filter(
|
||||||
const identifier = safeIdentifier(node.id)
|
(child, childIndex) =>
|
||||||
const itemsVariable = `tableItems_${identifier}`
|
(child.slot ||
|
||||||
const columnsVariable = `tableColumns_${identifier}`
|
`table:${Math.floor(childIndex / columnCount)}:${childIndex % columnCount}`) === slot,
|
||||||
const rowVariable = `tableRow_${identifier}`
|
)
|
||||||
const configuredColumns = Array.isArray(node.props.dataColumns)
|
const content = cellChildren.length
|
||||||
? node.props.dataColumns.filter((column): column is string => typeof column === 'string')
|
? cellChildren.map((child) => nodeToCode(child, level + 3, itemVariable)).join('\n')
|
||||||
: Array.isArray(node.props.columns)
|
: indent('<span />', level + 3)
|
||||||
? node.props.columns
|
return `${indent('<td>', level + 2)}\n${content}\n${indent('</td>', level + 2)}`
|
||||||
.map((column) => {
|
}).join('\n')
|
||||||
if (typeof column === 'string') return column
|
return `${indent('<tr>', level + 1)}\n${cells}\n${indent('</tr>', level + 1)}`
|
||||||
if (!column || typeof column !== 'object' || Array.isArray(column)) return ''
|
}).join('\n')
|
||||||
const record = column as Record<string, unknown>
|
const className = JSON.stringify(String(node.props.className || ''))
|
||||||
return String(record.dataIndex ?? record.key ?? '')
|
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)}
|
||||||
.filter(Boolean)
|
${rows}
|
||||||
: null
|
${indent('</tbody>', level + 1)}
|
||||||
const columnsExpression = configuredColumns
|
${indent('</UiKit.Table>', level)}`
|
||||||
? 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 <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">Table items bağlantısı bir koleksiyon döndürmelidir.</div>`, level + 1)}
|
|
||||||
${indent(`const ${columnsVariable} = ${columnsExpression}`, level + 1)}
|
|
||||||
${indent('return (', level + 1)}
|
|
||||||
${indent(`<UiKit.Table borderlessRow={${Boolean(node.props.borderlessRow)}} compact={${Boolean(node.props.compact)}} hoverable={${node.props.hoverable !== false}} overflow={${node.props.overflow !== false}}>`, level + 2)}
|
|
||||||
${indent(`<thead><tr>{${columnsVariable}.map((column) => <th key={column}>{column === "value" ? "Value" : column}</th>)}</tr></thead>`, level + 3)}
|
|
||||||
${indent(`<tbody>{${itemsVariable}.map((${rowVariable}, rowIndex) => <tr key={rowIndex}>{${columnsVariable}.map((column) => { const value = column === "value" ? ${rowVariable} : getByPath(${rowVariable}, column); return <td key={column}>{typeof value === "object" && value !== null ? JSON.stringify(value) : String(value ?? "")}</td> })}</tr>)}</tbody>`, level + 3)}
|
|
||||||
${indent('</UiKit.Table>', level + 2)}
|
|
||||||
${indent(')', level + 1)}
|
|
||||||
${indent('})()}', level)}`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (node.type === 'Grid') {
|
if (node.type === 'Grid') {
|
||||||
|
|
@ -302,6 +338,44 @@ ${indent(')}', level + 1)}`
|
||||||
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)}`
|
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)}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)}`
|
||||||
|
}
|
||||||
|
|
||||||
const tag =
|
const tag =
|
||||||
node.type === 'checkbox'
|
node.type === 'checkbox'
|
||||||
? 'input'
|
? 'input'
|
||||||
|
|
@ -335,25 +409,56 @@ ${indent(')}', level + 1)}`
|
||||||
export const generateDesignerCode = (name: string, document: DesignerDocument) => {
|
export const generateDesignerCode = (name: string, document: DesignerDocument) => {
|
||||||
const componentName = safeIdentifier(name || 'VisualComponent')
|
const componentName = safeIdentifier(name || 'VisualComponent')
|
||||||
const handlers: string[] = []
|
const handlers: string[] = []
|
||||||
|
const runtimeStateHooks: string[] = []
|
||||||
let hasSelect = false
|
let hasSelect = false
|
||||||
let hasDataTable = false
|
let hasDataTable = false
|
||||||
|
|
||||||
const visit = (nodes: DesignerNode[]) => {
|
const visit = (nodes: DesignerNode[]) => {
|
||||||
nodes.forEach((node) => {
|
nodes.forEach((node) => {
|
||||||
if (node.type === 'Select' || node.type === 'AutoComplete' || node.type === 'Menu') {
|
if (
|
||||||
|
['Select', 'AutoComplete', 'Menu', 'Dropdown', 'Radio.Group'].includes(node.type)
|
||||||
|
) {
|
||||||
hasSelect = true
|
hasSelect = true
|
||||||
}
|
}
|
||||||
if (node.type === 'Grid' || node.type === 'Table') hasDataTable = true
|
if (node.type === 'Grid') hasDataTable = true
|
||||||
Object.entries(node.events).forEach(([eventName, script]) => {
|
const runtimeState = getRuntimeStateSpec(node)
|
||||||
if (!script.trim()) return
|
if (runtimeState) {
|
||||||
if (node.type === 'Checkbox' && eventName === 'onChange') {
|
runtimeStateHooks.push(
|
||||||
handlers.push(
|
` const [${runtimeState.stateName}, ${runtimeState.setterName}] = React.useState(${JSON.stringify(runtimeState.initialValue)})`,
|
||||||
` const handle_${safeIdentifier(node.id)}_${eventName} = (checked, originalEvent) => {\n const event = { checked, originalEvent, target: originalEvent?.target }\n${indent(script, 2)}\n }`,
|
)
|
||||||
)
|
}
|
||||||
return
|
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'
|
||||||
handlers.push(
|
handlers.push(
|
||||||
` const handle_${safeIdentifier(node.id)}_${eventName} = (${EVENT_NAMES[eventName] || 'event'}) => {\n${indent(script, 2)}\n }`,
|
` const handle_${safeIdentifier(node.id)}_${eventName} = (valueOrEvent, originalEvent) => {\n${stateUpdate ? ` ${stateUpdate}\n` : ''}${eventDeclaration}${script ? `\n${indent(script, 2)}` : ''}\n }`,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
visit(node.children)
|
visit(node.children)
|
||||||
|
|
@ -368,8 +473,8 @@ export const generateDesignerCode = (name: string, document: DesignerDocument) =
|
||||||
return value.map((item, index) => {
|
return value.map((item, index) => {
|
||||||
if (item === null || typeof item !== "object" || Array.isArray(item)) return { label: String(item ?? ""), value: 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 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 labelKey = ["label", "children", "name", "title", "text", "description"].find((key) => item[key] !== undefined)
|
||||||
const valueKey = ["value", "id", "code", "key"].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 optionLabel = labelPath ? getByPath(item, labelPath) : item[labelKey ?? primitiveKeys[0]]
|
||||||
const optionValue = valuePath ? getByPath(item, valuePath) : item[valueKey ?? primitiveKeys[1] ?? 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 }
|
return { ...item, label: String(optionLabel ?? \`Seçenek \${index + 1}\`), value: optionValue ?? index }
|
||||||
|
|
@ -411,5 +516,5 @@ export const generateDesignerCode = (name: string, document: DesignerDocument) =
|
||||||
|
|
||||||
const designerBackup = encodeURIComponent(JSON.stringify(document))
|
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`
|
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`
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ export interface DesignerNode {
|
||||||
id: string
|
id: string
|
||||||
type: string
|
type: string
|
||||||
kind: DesignerNodeKind
|
kind: DesignerNodeKind
|
||||||
|
slot?: string
|
||||||
props: Record<string, unknown>
|
props: Record<string, unknown>
|
||||||
events: Record<string, string>
|
events: Record<string, string>
|
||||||
bindings: Record<string, DesignerBinding>
|
bindings: Record<string, DesignerBinding>
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,6 @@ import React, {
|
||||||
useEffect,
|
useEffect,
|
||||||
useCallback,
|
useCallback,
|
||||||
useMemo,
|
useMemo,
|
||||||
useRef,
|
|
||||||
} from 'react'
|
} from 'react'
|
||||||
|
|
||||||
interface ComponentProps {
|
interface ComponentProps {
|
||||||
|
|
@ -33,6 +32,7 @@ interface ComponentContextType {
|
||||||
getComponent: (id: string) => CustomComponent | undefined
|
getComponent: (id: string) => CustomComponent | undefined
|
||||||
getComponentByName: (name: string) => CustomComponent | undefined
|
getComponentByName: (name: string) => CustomComponent | undefined
|
||||||
refreshComponents: () => Promise<void>
|
refreshComponents: () => Promise<void>
|
||||||
|
refreshComponent: (id: string) => Promise<CustomComponent | undefined>
|
||||||
// Manual registered components
|
// Manual registered components
|
||||||
registeredComponents: Record<string, React.ComponentType<unknown>>
|
registeredComponents: Record<string, React.ComponentType<unknown>>
|
||||||
registerComponent: (name: string, component: React.ComponentType<unknown>) => void
|
registerComponent: (name: string, component: React.ComponentType<unknown>) => void
|
||||||
|
|
@ -45,14 +45,13 @@ interface ComponentContextType {
|
||||||
}
|
}
|
||||||
|
|
||||||
const ComponentContext = createContext<ComponentContextType | undefined>(undefined)
|
const ComponentContext = createContext<ComponentContextType | undefined>(undefined)
|
||||||
const COMPONENTS_CHANGED_STORAGE_KEY = 'sozsoft:custom-components:changed'
|
const componentsChangedChannel =
|
||||||
|
typeof BroadcastChannel === 'undefined'
|
||||||
|
? undefined
|
||||||
|
: new BroadcastChannel('sozsoft-custom-components-changed')
|
||||||
|
|
||||||
const notifyComponentsChanged = () => {
|
const notifyComponentsChanged = () => {
|
||||||
try {
|
componentsChangedChannel?.postMessage({ changedAt: Date.now() })
|
||||||
window.localStorage.setItem(COMPONENTS_CHANGED_STORAGE_KEY, String(Date.now()))
|
|
||||||
} catch {
|
|
||||||
// Local state is already current; storage can be unavailable in restricted browsers.
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const haveSameComponentRevision = (
|
const haveSameComponentRevision = (
|
||||||
|
|
@ -148,7 +147,8 @@ export const useComponents = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ComponentProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
export const ComponentProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||||
const extraProperties = useStoreState((state) => state.abpConfig?.config?.extraProperties)
|
const applicationConfig = useStoreState((state) => state.abpConfig?.config)
|
||||||
|
const extraProperties = applicationConfig?.extraProperties
|
||||||
const [components, setComponents] = useState<CustomComponent[]>([])
|
const [components, setComponents] = useState<CustomComponent[]>([])
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
|
@ -156,50 +156,59 @@ export const ComponentProvider: React.FC<{ children: React.ReactNode }> = ({ chi
|
||||||
const [compiledComponents, setCompiledComponents] = useState<
|
const [compiledComponents, setCompiledComponents] = useState<
|
||||||
Record<string, React.ComponentType<ComponentProps>>
|
Record<string, React.ComponentType<ComponentProps>>
|
||||||
>({})
|
>({})
|
||||||
const refreshInFlightRef = useRef<Promise<void> | null>(null)
|
const refreshComponents = useCallback(async () => {
|
||||||
|
if (!applicationConfig) return
|
||||||
|
|
||||||
const refreshComponents = useCallback(() => {
|
try {
|
||||||
if (refreshInFlightRef.current) return refreshInFlightRef.current
|
setError(null)
|
||||||
|
const result = await developerKitService.getCustomComponents()
|
||||||
|
const nextComponents = result.items || []
|
||||||
|
setComponents((current) =>
|
||||||
|
haveSameComponentRevision(current, nextComponents) ? current : nextComponents,
|
||||||
|
)
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : 'Failed to fetch components')
|
||||||
|
console.error('Failed to fetch components:', err)
|
||||||
|
}
|
||||||
|
}, [applicationConfig])
|
||||||
|
|
||||||
const request = (async () => {
|
useEffect(() => {
|
||||||
try {
|
const initialComponents =
|
||||||
setError(null)
|
(extraProperties?.customComponents as CustomComponentDto[] | undefined) || []
|
||||||
const result = await developerKitService.getCustomComponents()
|
setComponents((current) =>
|
||||||
const nextComponents = result.items || []
|
haveSameComponentRevision(current, initialComponents) ? current : initialComponents,
|
||||||
setComponents((current) =>
|
)
|
||||||
haveSameComponentRevision(current, nextComponents) ? current : nextComponents,
|
|
||||||
)
|
|
||||||
} catch (err) {
|
|
||||||
setError(err instanceof Error ? err.message : 'Failed to fetch components')
|
|
||||||
console.error('Failed to fetch components:', err)
|
|
||||||
const fallbackComponents = extraProperties?.customComponents as CustomComponentDto[]
|
|
||||||
setComponents((current) => (current.length ? current : fallbackComponents || []))
|
|
||||||
}
|
|
||||||
})()
|
|
||||||
|
|
||||||
refreshInFlightRef.current = request
|
|
||||||
void request.finally(() => {
|
|
||||||
if (refreshInFlightRef.current === request) refreshInFlightRef.current = null
|
|
||||||
})
|
|
||||||
return request
|
|
||||||
}, [extraProperties])
|
}, [extraProperties])
|
||||||
|
|
||||||
useEffect(() => {
|
const refreshComponent = useCallback(
|
||||||
void refreshComponents()
|
async (id: string): Promise<CustomComponent | undefined> => {
|
||||||
}, [refreshComponents])
|
if (!applicationConfig) return undefined
|
||||||
|
|
||||||
|
try {
|
||||||
|
setError(null)
|
||||||
|
const freshComponent = await developerKitService.getCustomComponent(id)
|
||||||
|
setComponents((current) => {
|
||||||
|
const exists = current.some((component) => component.id === id)
|
||||||
|
return exists
|
||||||
|
? current.map((component) => (component.id === id ? freshComponent : component))
|
||||||
|
: [...current, freshComponent]
|
||||||
|
})
|
||||||
|
return freshComponent
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : 'Failed to fetch component')
|
||||||
|
console.error('Failed to fetch component:', err)
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[applicationConfig],
|
||||||
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleFocus = () => void refreshComponents()
|
const handleComponentsChanged = () => void refreshComponents()
|
||||||
const handleStorage = (event: StorageEvent) => {
|
|
||||||
if (event.key === COMPONENTS_CHANGED_STORAGE_KEY) void refreshComponents()
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener('focus', handleFocus)
|
componentsChangedChannel?.addEventListener('message', handleComponentsChanged)
|
||||||
window.addEventListener('storage', handleStorage)
|
return () =>
|
||||||
return () => {
|
componentsChangedChannel?.removeEventListener('message', handleComponentsChanged)
|
||||||
window.removeEventListener('focus', handleFocus)
|
|
||||||
window.removeEventListener('storage', handleStorage)
|
|
||||||
}
|
|
||||||
}, [refreshComponents])
|
}, [refreshComponents])
|
||||||
|
|
||||||
const addComponent = useCallback(async (componentData: CreateUpdateCustomComponentDto) => {
|
const addComponent = useCallback(async (componentData: CreateUpdateCustomComponentDto) => {
|
||||||
|
|
@ -633,6 +642,7 @@ export const ComponentProvider: React.FC<{ children: React.ReactNode }> = ({ chi
|
||||||
getComponent,
|
getComponent,
|
||||||
getComponentByName,
|
getComponentByName,
|
||||||
refreshComponents,
|
refreshComponents,
|
||||||
|
refreshComponent,
|
||||||
registeredComponents,
|
registeredComponents,
|
||||||
registerComponent,
|
registerComponent,
|
||||||
renderComponent,
|
renderComponent,
|
||||||
|
|
@ -651,6 +661,7 @@ export const ComponentProvider: React.FC<{ children: React.ReactNode }> = ({ chi
|
||||||
getComponent,
|
getComponent,
|
||||||
getComponentByName,
|
getComponentByName,
|
||||||
refreshComponents,
|
refreshComponents,
|
||||||
|
refreshComponent,
|
||||||
registeredComponents,
|
registeredComponents,
|
||||||
registerComponent,
|
registerComponent,
|
||||||
renderComponent,
|
renderComponent,
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ const ComponentEditor: React.FC = () => {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const { translate } = useLocalization()
|
const { translate } = useLocalization()
|
||||||
|
|
||||||
const { getComponent, addComponent, updateComponent } = useComponents()
|
const { getComponent, addComponent, updateComponent, refreshComponent } = useComponents()
|
||||||
|
|
||||||
const [isLoaded, setIsLoaded] = useState(false)
|
const [isLoaded, setIsLoaded] = useState(false)
|
||||||
|
|
||||||
|
|
@ -47,8 +47,16 @@ const ComponentEditor: React.FC = () => {
|
||||||
// Load existing component data - sadece edit modunda
|
// Load existing component data - sadece edit modunda
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isEditing && id && !isLoaded) {
|
if (isEditing && id && !isLoaded) {
|
||||||
const component = getComponent(id)
|
let cancelled = false
|
||||||
if (component) {
|
|
||||||
|
void refreshComponent(id).then((freshComponent) => {
|
||||||
|
if (cancelled) return
|
||||||
|
const component = freshComponent
|
||||||
|
if (!component) {
|
||||||
|
setIsLoaded(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Parse dependencies from JSON string
|
// Parse dependencies from JSON string
|
||||||
let deps: string[] = []
|
let deps: string[] = []
|
||||||
try {
|
try {
|
||||||
|
|
@ -68,12 +76,16 @@ const ComponentEditor: React.FC = () => {
|
||||||
|
|
||||||
setInitialValues(values)
|
setInitialValues(values)
|
||||||
setIsLoaded(true)
|
setIsLoaded(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelled = true
|
||||||
}
|
}
|
||||||
} else if (!isEditing && !isLoaded) {
|
} else if (!isEditing && !isLoaded) {
|
||||||
// Yeni komponent için boş başla - TEMPLATE YOK
|
// Yeni komponent için boş başla - TEMPLATE YOK
|
||||||
setIsLoaded(true)
|
setIsLoaded(true)
|
||||||
}
|
}
|
||||||
}, [id, isEditing, getComponent, isLoaded])
|
}, [id, isEditing, isLoaded, refreshComponent])
|
||||||
|
|
||||||
const createDefaultVisualDocument = (componentName: string): DesignerDocument => {
|
const createDefaultVisualDocument = (componentName: string): DesignerDocument => {
|
||||||
const document = createEmptyDesignerDocument('visual')
|
const document = createEmptyDesignerDocument('visual')
|
||||||
|
|
|
||||||
|
|
@ -82,11 +82,6 @@ const ComponentManager: React.FC = () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleOpenRoute = (routePath: string) => {
|
|
||||||
const normalizedPath = routePath.startsWith('/') ? routePath : `/${routePath}`
|
|
||||||
window.open(normalizedPath, '_blank', 'noopener,noreferrer')
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<Helmet
|
<Helmet
|
||||||
|
|
@ -284,7 +279,12 @@ const ComponentManager: React.FC = () => {
|
||||||
color="gray-600"
|
color="gray-600"
|
||||||
disabled={!component.isActive || !component.routePath?.trim()}
|
disabled={!component.isActive || !component.routePath?.trim()}
|
||||||
title={translate('::OpenUrl')}
|
title={translate('::OpenUrl')}
|
||||||
onClick={() => handleOpenRoute(component.routePath)}
|
onClick={() => {
|
||||||
|
const routePath = component.routePath.startsWith('/')
|
||||||
|
? component.routePath
|
||||||
|
: `/${component.routePath}`
|
||||||
|
window.open(routePath, '_blank')
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<FaRegWindowMaximize className="w-4 h-4" />
|
<FaRegWindowMaximize className="w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,6 @@ import {
|
||||||
FaPlus,
|
FaPlus,
|
||||||
FaPlay,
|
FaPlay,
|
||||||
FaRedo,
|
FaRedo,
|
||||||
FaRegCopy,
|
|
||||||
FaRegSave,
|
FaRegSave,
|
||||||
FaSearch,
|
FaSearch,
|
||||||
FaSlidersH,
|
FaSlidersH,
|
||||||
|
|
@ -38,6 +37,7 @@ import TailwindModal from '@/components/codeLayout/TailwindModal'
|
||||||
import { useStoreState } from '@/store/store'
|
import { useStoreState } from '@/store/store'
|
||||||
import VisualCanvas, { DESIGNER_DRAG_TYPE } from '@/components/visualDesigner/VisualCanvas'
|
import VisualCanvas, { DESIGNER_DRAG_TYPE } from '@/components/visualDesigner/VisualCanvas'
|
||||||
import {
|
import {
|
||||||
|
DESIGNER_DATA_COMPONENT_NAMES,
|
||||||
DESIGNER_PRIMARY_EVENTS,
|
DESIGNER_PRIMARY_EVENTS,
|
||||||
getDesignerCatalog,
|
getDesignerCatalog,
|
||||||
getDesignerEventSnippet,
|
getDesignerEventSnippet,
|
||||||
|
|
@ -95,11 +95,18 @@ const DATA_BINDABLE_PROPERTY_NAMES = new Set([
|
||||||
'disabled',
|
'disabled',
|
||||||
'loading',
|
'loading',
|
||||||
])
|
])
|
||||||
const OPTION_DATA_COMPONENT_NAMES = new Set(['Select', 'AutoComplete', 'Menu'])
|
const OPTION_DATA_COMPONENT_NAMES = new Set([
|
||||||
const TABULAR_DATA_COMPONENT_NAMES = new Set(['Grid', 'Table'])
|
'Select',
|
||||||
|
'AutoComplete',
|
||||||
|
'Menu',
|
||||||
|
'Dropdown',
|
||||||
|
'Radio.Group',
|
||||||
|
])
|
||||||
|
const TABULAR_DATA_COMPONENT_NAMES = new Set(['Grid'])
|
||||||
const isOptionDataComponent = (type?: string) =>
|
const isOptionDataComponent = (type?: string) =>
|
||||||
Boolean(type && OPTION_DATA_COMPONENT_NAMES.has(type))
|
Boolean(type && OPTION_DATA_COMPONENT_NAMES.has(type))
|
||||||
const getOptionDataProperty = (type?: string) => (type === 'Menu' ? 'items' : 'options')
|
const getOptionDataProperty = (type?: string) =>
|
||||||
|
['Menu', 'Dropdown', 'Radio.Group'].includes(type || '') ? 'items' : 'options'
|
||||||
const isTabularDataComponent = (type?: string) =>
|
const isTabularDataComponent = (type?: string) =>
|
||||||
Boolean(type && TABULAR_DATA_COMPONENT_NAMES.has(type))
|
Boolean(type && TABULAR_DATA_COMPONENT_NAMES.has(type))
|
||||||
|
|
||||||
|
|
@ -260,6 +267,8 @@ const CANVAS_WIDTHS: Record<DesignerDocument['canvas']['width'], string> = {
|
||||||
}
|
}
|
||||||
|
|
||||||
const clone = <T,>(value: T): T => JSON.parse(JSON.stringify(value))
|
const clone = <T,>(value: T): T => JSON.parse(JSON.stringify(value))
|
||||||
|
const normalizeTableDimension = (value: unknown, fallback: number) =>
|
||||||
|
Math.min(20, Math.max(1, Math.floor(Number(value) || fallback)))
|
||||||
|
|
||||||
const updateNodeTree = (
|
const updateNodeTree = (
|
||||||
nodes: DesignerNode[],
|
nodes: DesignerNode[],
|
||||||
|
|
@ -384,33 +393,49 @@ const normalizeDesignerDocument = (document: DesignerDocument): DesignerDocument
|
||||||
delete bindings.options
|
delete bindings.options
|
||||||
}
|
}
|
||||||
if (node.type === 'Table') {
|
if (node.type === 'Table') {
|
||||||
if (
|
const legacyRows = Array.isArray(props.items)
|
||||||
Array.isArray(props.data) &&
|
? props.items
|
||||||
(!Array.isArray(props.items) || props.items.length === 0)
|
: Array.isArray(props.data)
|
||||||
) {
|
? props.data
|
||||||
props.items = props.data
|
: []
|
||||||
}
|
const firstLegacyRow = legacyRows.find(
|
||||||
if (Array.isArray(props.columns) && !Array.isArray(props.dataColumns)) {
|
(item) => item && typeof item === 'object' && !Array.isArray(item),
|
||||||
props.dataColumns = props.columns
|
) as Record<string, unknown> | undefined
|
||||||
.map((column) => {
|
const legacyColumns = Array.isArray(props.dataColumns)
|
||||||
if (typeof column === 'string') return column
|
? props.dataColumns
|
||||||
if (!column || typeof column !== 'object' || Array.isArray(column)) return ''
|
: Array.isArray(props.columns)
|
||||||
const record = column as Record<string, unknown>
|
? props.columns
|
||||||
return String(record.dataIndex ?? record.key ?? '')
|
: firstLegacyRow
|
||||||
})
|
? Object.keys(firstLegacyRow)
|
||||||
.filter(Boolean)
|
: []
|
||||||
}
|
props.rowCount = normalizeTableDimension(props.rowCount, legacyRows.length || 2)
|
||||||
|
props.columnCount = normalizeTableDimension(
|
||||||
|
props.columnCount,
|
||||||
|
legacyColumns.length || 3,
|
||||||
|
)
|
||||||
delete props.data
|
delete props.data
|
||||||
delete props.columns
|
delete props.columns
|
||||||
if (!bindings.items && bindings.data) bindings.items = bindings.data
|
delete props.items
|
||||||
|
delete props.dataColumns
|
||||||
delete bindings.data
|
delete bindings.data
|
||||||
|
delete bindings.items
|
||||||
|
}
|
||||||
|
let children = normalizeNodes(node.children || [])
|
||||||
|
if (node.type === 'Table') {
|
||||||
|
const columnCount = normalizeTableDimension(props.columnCount, 3)
|
||||||
|
children = children.map((child, index) => ({
|
||||||
|
...child,
|
||||||
|
slot:
|
||||||
|
child.slot ||
|
||||||
|
`table:${Math.floor(index / columnCount)}:${index % columnCount}`,
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
...node,
|
...node,
|
||||||
props,
|
props,
|
||||||
events,
|
events,
|
||||||
bindings,
|
bindings,
|
||||||
children: normalizeNodes(node.children || []),
|
children,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return {
|
return {
|
||||||
|
|
@ -568,14 +593,16 @@ const migrateGeneratedCodeToDesignerDocument = (
|
||||||
/const tableItems_([A-Za-z0-9_$]+)\s*=\s*(\[[^\n]*\])[\s\S]*?const tableColumns_\1\s*=\s*(\[[^\n]*\])[\s\S]*?<UiKit\.Table\b([^>]*)>/g,
|
/const tableItems_([A-Za-z0-9_$]+)\s*=\s*(\[[^\n]*\])[\s\S]*?const tableColumns_\1\s*=\s*(\[[^\n]*\])[\s\S]*?<UiKit\.Table\b([^>]*)>/g,
|
||||||
)) {
|
)) {
|
||||||
try {
|
try {
|
||||||
|
const legacyItems = JSON.parse(match[2]) as unknown[]
|
||||||
|
const legacyColumns = JSON.parse(match[3]) as unknown[]
|
||||||
addNode(
|
addNode(
|
||||||
match.index,
|
match.index,
|
||||||
'Table',
|
'Table',
|
||||||
'ui',
|
'ui',
|
||||||
{
|
{
|
||||||
...parseGeneratedJsxProps(match[4], componentDefaultProps),
|
...parseGeneratedJsxProps(match[4], componentDefaultProps),
|
||||||
items: JSON.parse(match[2]),
|
rowCount: normalizeTableDimension(legacyItems.length, 2),
|
||||||
dataColumns: JSON.parse(match[3]),
|
columnCount: normalizeTableDimension(legacyColumns.length, 3),
|
||||||
},
|
},
|
||||||
{},
|
{},
|
||||||
match[1],
|
match[1],
|
||||||
|
|
@ -822,6 +849,10 @@ const VisualComponentDesigner = () => {
|
||||||
)
|
)
|
||||||
const catalog = useMemo(() => getDesignerCatalog(customNames), [customNames])
|
const catalog = useMemo(() => getDesignerCatalog(customNames), [customNames])
|
||||||
const catalogByName = useMemo(() => new Map(catalog.map((item) => [item.name, item])), [catalog])
|
const catalogByName = useMemo(() => new Map(catalog.map((item) => [item.name, item])), [catalog])
|
||||||
|
const selectDesignerNode = useCallback((nodeId: string | null) => {
|
||||||
|
setSelectedId(nodeId || null)
|
||||||
|
setInspectorTab('properties')
|
||||||
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let cancelled = false
|
let cancelled = false
|
||||||
|
|
@ -931,19 +962,20 @@ const VisualComponentDesigner = () => {
|
||||||
}, [document])
|
}, [document])
|
||||||
|
|
||||||
const addComponent = useCallback(
|
const addComponent = useCallback(
|
||||||
(definitionName: string, parentId: string | null = null) => {
|
(definitionName: string, parentId: string | null = null, slot?: string) => {
|
||||||
const definition = catalogByName.get(definitionName)
|
const definition = catalogByName.get(definitionName)
|
||||||
if (!definition) return
|
if (!definition) return
|
||||||
const node = definitionToNode(definition)
|
const node = definitionToNode(definition)
|
||||||
|
if (slot) node.slot = slot
|
||||||
commitDocument((current) => ({
|
commitDocument((current) => ({
|
||||||
...current,
|
...current,
|
||||||
sourceMode: 'visual',
|
sourceMode: 'visual',
|
||||||
nodes: insertNodeTree(current.nodes, node, parentId),
|
nodes: insertNodeTree(current.nodes, node, parentId),
|
||||||
}))
|
}))
|
||||||
setSelectedId(node.id)
|
selectDesignerNode(node.id)
|
||||||
setWorkspaceTab('design')
|
setWorkspaceTab('design')
|
||||||
},
|
},
|
||||||
[catalogByName, commitDocument],
|
[catalogByName, commitDocument, selectDesignerNode],
|
||||||
)
|
)
|
||||||
|
|
||||||
const selectedNode = useMemo(
|
const selectedNode = useMemo(
|
||||||
|
|
@ -951,6 +983,9 @@ const VisualComponentDesigner = () => {
|
||||||
[document.nodes, selectedId],
|
[document.nodes, selectedId],
|
||||||
)
|
)
|
||||||
const selectedDefinition = selectedNode ? catalogByName.get(selectedNode.type) : undefined
|
const selectedDefinition = selectedNode ? catalogByName.get(selectedNode.type) : undefined
|
||||||
|
const selectedIsDataComponent = Boolean(
|
||||||
|
selectedNode && DESIGNER_DATA_COMPONENT_NAMES.has(selectedNode.type),
|
||||||
|
)
|
||||||
const selectedProperties = useMemo<DesignerPropertyInfo[]>(() => {
|
const selectedProperties = useMemo<DesignerPropertyInfo[]>(() => {
|
||||||
if (!selectedNode) return []
|
if (!selectedNode) return []
|
||||||
const definedProperties = selectedDefinition?.properties || []
|
const definedProperties = selectedDefinition?.properties || []
|
||||||
|
|
@ -1066,6 +1101,19 @@ const VisualComponentDesigner = () => {
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const updateNodeProp = useCallback(
|
||||||
|
(nodeId: string, propertyName: string, value: unknown) => {
|
||||||
|
commitDocument((current) => ({
|
||||||
|
...current,
|
||||||
|
nodes: updateNodeTree(current.nodes, nodeId, (node) => ({
|
||||||
|
...node,
|
||||||
|
props: { ...node.props, [propertyName]: value },
|
||||||
|
})),
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
[commitDocument],
|
||||||
|
)
|
||||||
|
|
||||||
const updateNodeEvent = (nodeId: string, eventName: string, value: string) => {
|
const updateNodeEvent = (nodeId: string, eventName: string, value: string) => {
|
||||||
commitDocument((current) => ({
|
commitDocument((current) => ({
|
||||||
...current,
|
...current,
|
||||||
|
|
@ -1626,9 +1674,15 @@ const VisualComponentDesigner = () => {
|
||||||
filteredCatalog.forEach((item) => {
|
filteredCatalog.forEach((item) => {
|
||||||
groups.get(item.toolboxGroup)?.push(item)
|
groups.get(item.toolboxGroup)?.push(item)
|
||||||
})
|
})
|
||||||
return TOOLBOX_GROUP_ORDER.map((group) => [group, groups.get(group) || []] as const).filter(
|
return TOOLBOX_GROUP_ORDER.map(
|
||||||
([, definitions]) => definitions.length > 0,
|
(group) =>
|
||||||
)
|
[
|
||||||
|
group,
|
||||||
|
[...(groups.get(group) || [])].sort((left, right) =>
|
||||||
|
left.name.localeCompare(right.name, 'tr', { sensitivity: 'base' }),
|
||||||
|
),
|
||||||
|
] as const,
|
||||||
|
).filter(([, definitions]) => definitions.length > 0)
|
||||||
}, [filteredCatalog])
|
}, [filteredCatalog])
|
||||||
|
|
||||||
const activeDataSource = document.dataSources.find((source) => source.id === dataPanelSourceId)
|
const activeDataSource = document.dataSources.find((source) => source.id === dataPanelSourceId)
|
||||||
|
|
@ -1653,8 +1707,10 @@ const VisualComponentDesigner = () => {
|
||||||
if (option && typeof option === 'object' && !Array.isArray(option)) {
|
if (option && typeof option === 'object' && !Array.isArray(option)) {
|
||||||
const record = option as Record<string, unknown>
|
const record = option as Record<string, unknown>
|
||||||
return {
|
return {
|
||||||
label: String(record.label ?? `Seçenek ${index + 1}`),
|
label: String(
|
||||||
value: record.value ?? index + 1,
|
record.label ?? record.children ?? record.title ?? `Seçenek ${index + 1}`,
|
||||||
|
),
|
||||||
|
value: record.value ?? record.eventKey ?? record.key ?? index + 1,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return { label: String(option ?? ''), value: option ?? index + 1 }
|
return { label: String(option ?? ''), value: option ?? index + 1 }
|
||||||
|
|
@ -1884,14 +1940,20 @@ const VisualComponentDesigner = () => {
|
||||||
}, [activeDataSample, activeDataSource?.id, commitDocument, selectedId, selectedNode])
|
}, [activeDataSample, activeDataSource?.id, commitDocument, selectedId, selectedNode])
|
||||||
|
|
||||||
const dataBindableProperties =
|
const dataBindableProperties =
|
||||||
selectedNode && selectedDefinition && selectedNode.kind !== 'platform'
|
selectedNode &&
|
||||||
|
selectedDefinition &&
|
||||||
|
DESIGNER_DATA_COMPONENT_NAMES.has(selectedNode.type)
|
||||||
? selectedDefinition.properties.filter(
|
? selectedDefinition.properties.filter(
|
||||||
(property) =>
|
(property) =>
|
||||||
property.category !== 'events' &&
|
property.category !== 'events' &&
|
||||||
property.type !== 'function' &&
|
property.type !== 'function' &&
|
||||||
(isTabularDataComponent(selectedNode.type)
|
(isTabularDataComponent(selectedNode.type)
|
||||||
? property.name === 'items'
|
? property.name === 'items'
|
||||||
: DATA_BINDABLE_PROPERTY_NAMES.has(property.name) || property.type === 'array'),
|
: isOptionDataComponent(selectedNode.type)
|
||||||
|
? property.name === getOptionDataProperty(selectedNode.type)
|
||||||
|
: DATA_BINDABLE_PROPERTY_NAMES.has(property.name) ||
|
||||||
|
property.type === 'array' ||
|
||||||
|
['number', 'boolean', 'string', 'select'].includes(property.type)),
|
||||||
)
|
)
|
||||||
: []
|
: []
|
||||||
|
|
||||||
|
|
@ -2275,7 +2337,6 @@ const VisualComponentDesigner = () => {
|
||||||
|
|
||||||
const selectModeSelector = renderSelectDataModeSelector()
|
const selectModeSelector = renderSelectDataModeSelector()
|
||||||
const selectConfiguration = renderSelectDataConfiguration()
|
const selectConfiguration = renderSelectDataConfiguration()
|
||||||
const tabularConfiguration = renderTabularDataConfiguration()
|
|
||||||
if (
|
if (
|
||||||
(isOptionDataComponent(selectedNode?.type) || isTabularDataComponent(selectedNode?.type)) &&
|
(isOptionDataComponent(selectedNode?.type) || isTabularDataComponent(selectedNode?.type)) &&
|
||||||
selectDataMode === 'static'
|
selectDataMode === 'static'
|
||||||
|
|
@ -2283,9 +2344,9 @@ const VisualComponentDesigner = () => {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{selectModeSelector}
|
{selectModeSelector}
|
||||||
{selectConfiguration}
|
<div className="rounded-lg border border-sky-200 bg-sky-50 p-3 text-xs leading-5 text-sky-800 dark:border-sky-900 dark:bg-sky-950 dark:text-sky-200">
|
||||||
{tabularConfiguration}
|
Statik veri <strong>Properties</strong> sekmesindeki items/options alanından yönetilir.
|
||||||
{isTabularDataComponent(selectedNode?.type) && renderTabularColumnConfiguration()}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -2551,7 +2612,7 @@ const VisualComponentDesigner = () => {
|
||||||
className={`flex w-full items-center gap-2 rounded px-2 py-1.5 text-left text-xs ${selectedId === node.id ? 'bg-sky-100 font-semibold text-sky-700 dark:bg-sky-950 dark:text-sky-300' : 'text-slate-600 hover:bg-slate-100 dark:text-slate-300 dark:hover:bg-slate-800'}`}
|
className={`flex w-full items-center gap-2 rounded px-2 py-1.5 text-left text-xs ${selectedId === node.id ? 'bg-sky-100 font-semibold text-sky-700 dark:bg-sky-950 dark:text-sky-300' : 'text-slate-600 hover:bg-slate-100 dark:text-slate-300 dark:hover:bg-slate-800'}`}
|
||||||
style={{ paddingLeft: 8 + depth * 14 }}
|
style={{ paddingLeft: 8 + depth * 14 }}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setSelectedId(node.id)}
|
onClick={() => selectDesignerNode(node.id)}
|
||||||
>
|
>
|
||||||
<FaLayerGroup className="shrink-0" />
|
<FaLayerGroup className="shrink-0" />
|
||||||
<span className="truncate">{node.type}</span>
|
<span className="truncate">{node.type}</span>
|
||||||
|
|
@ -2577,7 +2638,8 @@ const VisualComponentDesigner = () => {
|
||||||
background={appThemeMode === 'dark' ? '#0f172a' : document.canvas.background}
|
background={appThemeMode === 'dark' ? '#0f172a' : document.canvas.background}
|
||||||
dataValues={dataSourceSamples}
|
dataValues={dataSourceSamples}
|
||||||
renderCustomComponent={(componentName, props) => renderComponent(componentName, props)}
|
renderCustomComponent={(componentName, props) => renderComponent(componentName, props)}
|
||||||
onSelect={(nodeId) => setSelectedId(nodeId || null)}
|
onSelect={(nodeId) => selectDesignerNode(nodeId || null)}
|
||||||
|
onNodePropChange={updateNodeProp}
|
||||||
onDropComponent={addComponent}
|
onDropComponent={addComponent}
|
||||||
onMove={moveNode}
|
onMove={moveNode}
|
||||||
onDuplicate={duplicateNode}
|
onDuplicate={duplicateNode}
|
||||||
|
|
@ -2687,7 +2749,7 @@ const VisualComponentDesigner = () => {
|
||||||
const routePath = component.routePath.startsWith('/')
|
const routePath = component.routePath.startsWith('/')
|
||||||
? component.routePath
|
? component.routePath
|
||||||
: `/${component.routePath}`
|
: `/${component.routePath}`
|
||||||
window.open(routePath, '_blank', 'noopener,noreferrer')
|
window.open(routePath, '_blank')
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Aç
|
Aç
|
||||||
|
|
@ -3074,9 +3136,17 @@ const VisualComponentDesigner = () => {
|
||||||
).map(([tab, icon, label]) => (
|
).map(([tab, icon, label]) => (
|
||||||
<button
|
<button
|
||||||
key={tab}
|
key={tab}
|
||||||
className={`flex flex-1 items-center justify-center gap-1 border-b-2 text-[11px] ${inspectorTab === tab ? 'border-sky-500 text-sky-600' : 'border-transparent text-slate-500'}`}
|
className={`flex flex-1 items-center justify-center gap-1 border-b-2 text-[11px] ${tab === 'data' && !selectedIsDataComponent ? 'cursor-not-allowed border-transparent text-slate-300 dark:text-slate-700' : inspectorTab === tab ? 'border-sky-500 text-sky-600' : 'border-transparent text-slate-500'}`}
|
||||||
|
disabled={tab === 'data' && !selectedIsDataComponent}
|
||||||
|
title={
|
||||||
|
tab === 'data' && !selectedIsDataComponent
|
||||||
|
? 'Data sekmesi yalnızca Data kategorisindeki komponentlerde kullanılabilir.'
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setInspectorTab(tab)}
|
onClick={() => {
|
||||||
|
if (tab !== 'data' || selectedIsDataComponent) setInspectorTab(tab)
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{icon}
|
{icon}
|
||||||
{label}
|
{label}
|
||||||
|
|
@ -3126,8 +3196,18 @@ const VisualComponentDesigner = () => {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{inspectorTab === 'properties'
|
{inspectorTab === 'properties' ? (
|
||||||
? selectedProperties
|
<>
|
||||||
|
{isOptionDataComponent(selectedNode.type) && selectDataMode === 'static'
|
||||||
|
? renderSelectDataConfiguration()
|
||||||
|
: null}
|
||||||
|
{isTabularDataComponent(selectedNode.type) && selectDataMode === 'static' ? (
|
||||||
|
<div className="mb-4 space-y-3">
|
||||||
|
{renderTabularDataConfiguration()}
|
||||||
|
{renderTabularColumnConfiguration()}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{selectedProperties
|
||||||
.filter(
|
.filter(
|
||||||
(property) =>
|
(property) =>
|
||||||
property.category !== 'events' &&
|
property.category !== 'events' &&
|
||||||
|
|
@ -3144,11 +3224,12 @@ const VisualComponentDesigner = () => {
|
||||||
.map((property) => {
|
.map((property) => {
|
||||||
const propertyValue =
|
const propertyValue =
|
||||||
selectedNode.props[property.name] ?? property.value
|
selectedNode.props[property.name] ?? property.value
|
||||||
const snippet = getPropertySnippet(property, propertyValue)
|
const helperValue = getPropertyExampleValue(property, property.value)
|
||||||
|
const snippet = getPropertySnippet(property, helperValue)
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={property.name}
|
key={property.name}
|
||||||
className="mb-4 rounded-md border border-slate-200 p-2 dark:border-slate-800"
|
className="group/property mb-4 rounded-md border border-slate-200 p-2 dark:border-slate-800"
|
||||||
>
|
>
|
||||||
<span className="mb-1.5 flex items-center justify-between text-[11px] font-medium text-slate-600 dark:text-slate-300">
|
<span className="mb-1.5 flex items-center justify-between text-[11px] font-medium text-slate-600 dark:text-slate-300">
|
||||||
<span>
|
<span>
|
||||||
|
|
@ -3157,8 +3238,26 @@ const VisualComponentDesigner = () => {
|
||||||
<b className="ml-0.5 text-red-500">*</b>
|
<b className="ml-0.5 text-red-500">*</b>
|
||||||
) : null}
|
) : null}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-[9px] font-normal text-slate-400">
|
<span className="flex items-center gap-1 text-[9px] font-normal text-slate-400">
|
||||||
{property.tsType || property.type}
|
<span>{property.tsType || property.type}</span>
|
||||||
|
<span className="group/snippet relative">
|
||||||
|
<button
|
||||||
|
className="rounded p-1 text-sky-500 opacity-0 transition hover:bg-sky-50 hover:text-sky-700 focus:opacity-100 group-hover/property:opacity-100 dark:hover:bg-sky-950"
|
||||||
|
title="Yardımcı değeri uygula"
|
||||||
|
type="button"
|
||||||
|
onClick={() =>
|
||||||
|
updateSelectedProp(property.name, clone(helperValue))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<FaBolt />
|
||||||
|
</button>
|
||||||
|
<span className="pointer-events-none absolute right-0 top-full z-30 mt-1 hidden w-56 rounded-md border border-slate-200 bg-white p-2 text-left font-mono text-[9px] leading-4 text-sky-700 shadow-lg group-hover/snippet:block dark:border-slate-700 dark:bg-slate-950 dark:text-sky-300">
|
||||||
|
{snippet}
|
||||||
|
<span className="mt-1 block font-sans text-[9px] text-slate-400">
|
||||||
|
Tıklayınca property değerine uygulanır.
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
{property.description && (
|
{property.description && (
|
||||||
|
|
@ -3173,28 +3272,13 @@ const VisualComponentDesigner = () => {
|
||||||
options={property.options}
|
options={property.options}
|
||||||
onChange={(value) => updateSelectedProp(property.name, value)}
|
onChange={(value) => updateSelectedProp(property.name, value)}
|
||||||
/>
|
/>
|
||||||
<details className="mt-2 rounded border border-slate-200 bg-slate-50 text-[10px] dark:border-slate-700 dark:bg-slate-950">
|
|
||||||
<summary className="cursor-pointer select-none px-2 py-1.5 font-semibold text-slate-400">
|
|
||||||
Snippet
|
|
||||||
</summary>
|
|
||||||
<div className="flex items-start gap-1 border-t border-slate-200 px-2 py-1.5 dark:border-slate-700">
|
|
||||||
<code className="min-w-0 flex-1 break-all leading-4 text-sky-700 dark:text-sky-300">
|
|
||||||
{snippet}
|
|
||||||
</code>
|
|
||||||
<button
|
|
||||||
className="shrink-0 rounded p-1 text-slate-400 hover:bg-slate-200 hover:text-sky-600 dark:hover:bg-slate-800"
|
|
||||||
title="Snippet'ı kopyala"
|
|
||||||
type="button"
|
|
||||||
onClick={() => void navigator.clipboard?.writeText(snippet)}
|
|
||||||
>
|
|
||||||
<FaRegCopy />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})
|
})}
|
||||||
: renderDataInspector()}
|
</>
|
||||||
|
) : (
|
||||||
|
renderDataInspector()
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
|
|
|
||||||
|
|
@ -526,26 +526,12 @@ const TodoBoard = ({ listFormCode, searchParams, isSubForm, gridDto }: TodoBoard
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let cancelled = false
|
let cancelled = false
|
||||||
const legacyStorageKey = `todo-board:${listFormCode}:column-order`
|
|
||||||
let legacyConfig: StoredBoardColumns = { columns: [], retired: [] }
|
|
||||||
try {
|
|
||||||
const legacy = JSON.parse(localStorage.getItem(legacyStorageKey) ?? '{}')
|
|
||||||
legacyConfig = Array.isArray(legacy)
|
|
||||||
? { columns: legacy.map(String), retired: [] }
|
|
||||||
: {
|
|
||||||
columns: Array.isArray(legacy?.columns) ? legacy.columns.map(String) : [],
|
|
||||||
retired: Array.isArray(legacy?.retired) ? legacy.retired.map(String) : [],
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
legacyConfig = { columns: [], retired: [] }
|
|
||||||
}
|
|
||||||
setColumnConfig(undefined)
|
setColumnConfig(undefined)
|
||||||
|
|
||||||
if (!stateStoring?.enabled) {
|
if (!stateStoring?.enabled) {
|
||||||
const emptyConfig = { columns: [], retired: [] }
|
const emptyConfig = { columns: [], retired: [] }
|
||||||
columnConfigRef.current = emptyConfig
|
columnConfigRef.current = emptyConfig
|
||||||
setColumnConfig(emptyConfig)
|
setColumnConfig(emptyConfig)
|
||||||
localStorage.removeItem(legacyStorageKey)
|
|
||||||
return () => {
|
return () => {
|
||||||
cancelled = true
|
cancelled = true
|
||||||
}
|
}
|
||||||
|
|
@ -555,23 +541,14 @@ const TodoBoard = ({ listFormCode, searchParams, isSubForm, gridDto }: TodoBoard
|
||||||
.current()
|
.current()
|
||||||
.then((stored) => {
|
.then((stored) => {
|
||||||
if (cancelled) return
|
if (cancelled) return
|
||||||
const hasDatabaseState = Array.isArray(stored?.columns)
|
const config: StoredBoardColumns = Array.isArray(stored?.columns)
|
||||||
const config: StoredBoardColumns = hasDatabaseState
|
|
||||||
? {
|
? {
|
||||||
columns: stored.columns.map(String),
|
columns: stored.columns.map(String),
|
||||||
retired: Array.isArray(stored?.retired) ? stored.retired.map(String) : [],
|
retired: Array.isArray(stored?.retired) ? stored.retired.map(String) : [],
|
||||||
}
|
}
|
||||||
: legacyConfig
|
: { columns: [], retired: [] }
|
||||||
columnConfigRef.current = config
|
columnConfigRef.current = config
|
||||||
setColumnConfig(config)
|
setColumnConfig(config)
|
||||||
if (!hasDatabaseState && legacyConfig.columns.length) {
|
|
||||||
customSaveStateRef
|
|
||||||
.current(config)
|
|
||||||
.then(() => localStorage.removeItem(legacyStorageKey))
|
|
||||||
.catch((error) => console.error('Todo board state migration error:', error))
|
|
||||||
} else {
|
|
||||||
localStorage.removeItem(legacyStorageKey)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
if (cancelled) return
|
if (cancelled) return
|
||||||
|
|
|
||||||
|
|
@ -107,7 +107,7 @@ export default defineConfig(async ({ mode }) => {
|
||||||
short_name: 'Sözsoft Platform',
|
short_name: 'Sözsoft Platform',
|
||||||
theme_color: '#FF99C8',
|
theme_color: '#FF99C8',
|
||||||
background_color: '#f0e7db',
|
background_color: '#f0e7db',
|
||||||
display: 'standalone',
|
display: 'browser',
|
||||||
icons: [
|
icons: [
|
||||||
{
|
{
|
||||||
src: '/img/logo/logo-400.png',
|
src: '/img/logo/logo-400.png',
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue