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": {
|
||||
"@eslint/js": "^9.39.0",
|
||||
"@rolldown/plugin-babel": "^0.1.7",
|
||||
"@tailwindcss/typography": "^0.5.15",
|
||||
"@types/babel__standalone": "^7.1.9",
|
||||
"@types/file-saver": "^2.0.7",
|
||||
|
|
@ -79,7 +80,7 @@
|
|||
"@types/react-modal": "^3.13.1",
|
||||
"@typescript-eslint/eslint-plugin": "^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",
|
||||
"browserslist": "^4.25.2",
|
||||
"cssnano": "^6.0.1",
|
||||
|
|
@ -98,7 +99,7 @@
|
|||
"prettier": "^3.1.1",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"typescript": "5.9",
|
||||
"vite": "^7.3.6",
|
||||
"vite": "^8.2.0",
|
||||
"vite-plugin-pwa": "^1.3.0"
|
||||
},
|
||||
"overrides": {
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ import {
|
|||
} from './types'
|
||||
|
||||
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 {
|
||||
nodes: DesignerNode[]
|
||||
|
|
@ -19,10 +21,11 @@ interface VisualCanvasProps {
|
|||
background: string
|
||||
interactive?: boolean
|
||||
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
|
||||
onDuplicate?: (id: string) => void
|
||||
onDelete?: (id: string) => void
|
||||
onNodePropChange?: (id: string, propertyName: string, value: unknown) => void
|
||||
renderCustomComponent?: (name: string, props?: Record<string, unknown>) => React.ReactNode
|
||||
dataValues?: Record<string, unknown>
|
||||
}
|
||||
|
|
@ -105,10 +108,12 @@ const toSelectOptions = (
|
|||
const primitiveKeys = Object.keys(record).filter(
|
||||
(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,
|
||||
)
|
||||
const valueKey = ['value', 'id', 'code', 'key'].find((key) => record[key] !== undefined)
|
||||
const labelValue = labelPath
|
||||
? getDesignerValueByPath(record, labelPath)
|
||||
: labelKey
|
||||
|
|
@ -131,6 +136,7 @@ const getPreviewProps = (
|
|||
node: DesignerNode,
|
||||
dataValues: Record<string, unknown>,
|
||||
currentItem?: unknown,
|
||||
onNodePropChange?: (id: string, propertyName: string, value: unknown) => void,
|
||||
) => {
|
||||
const props: Record<string, unknown> = {}
|
||||
Object.entries(node.props).forEach(([key, value]) => {
|
||||
|
|
@ -160,8 +166,12 @@ const getPreviewProps = (
|
|||
props[propertyName] = getBindingValue(binding, dataValues, currentItem)
|
||||
}
|
||||
})
|
||||
if (node.type === 'Select' || node.type === 'AutoComplete' || node.type === 'Menu') {
|
||||
const collectionProperty = node.type === 'Menu' ? 'items' : 'options'
|
||||
if (
|
||||
['Select', 'AutoComplete', 'Menu', 'Dropdown', 'Radio.Group'].includes(node.type)
|
||||
) {
|
||||
const collectionProperty = ['Menu', 'Dropdown', 'Radio.Group'].includes(node.type)
|
||||
? 'items'
|
||||
: 'options'
|
||||
if (node.type === 'Select') {
|
||||
const legacyAliases: Record<string, string> = {
|
||||
clearable: 'isClearable',
|
||||
|
|
@ -184,21 +194,71 @@ const getPreviewProps = (
|
|||
)
|
||||
if (node.type === 'Select' && 'value' in props) {
|
||||
const options = props.options as Array<Record<string, unknown>>
|
||||
props.defaultValue = options.find((option) => option.value === props.value) || null
|
||||
delete props.value
|
||||
const selectedValue = 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
|
||||
delete props.value
|
||||
|
||||
const chainHandler = (
|
||||
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) {
|
||||
props.defaultChecked = Boolean(props.checked)
|
||||
delete props.checked
|
||||
const updateProp = (propertyName: string, value: unknown) =>
|
||||
onNodePropChange?.(node.id, propertyName, value)
|
||||
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
|
||||
if ('checked' in props && !props.onChange) props.onChange = () => undefined
|
||||
props.key = JSON.stringify([node.props, node.bindings])
|
||||
props.key = node.id
|
||||
return props
|
||||
}
|
||||
|
||||
|
|
@ -339,10 +399,17 @@ const renderElement = (
|
|||
dataValues: Record<string, unknown>,
|
||||
currentItem: unknown,
|
||||
interactive: boolean,
|
||||
onNodePropChange?: (id: string, propertyName: string, value: unknown) => void,
|
||||
renderCustomComponent?: (name: string, props?: Record<string, unknown>) => React.ReactNode,
|
||||
) => {
|
||||
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') {
|
||||
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 boundChildren = childrenBinding?.sourceId
|
||||
? getBindingValue(childrenBinding, dataValues, currentItem)
|
||||
|
|
@ -446,40 +509,11 @@ const renderElement = (
|
|||
: typeof boundChildren === 'object'
|
||||
? JSON.stringify(boundChildren)
|
||||
: String(boundChildren)
|
||||
const props = getPreviewProps(node, dataValues, currentItem)
|
||||
if (node.type === 'Table') {
|
||||
const legacyData = Array.isArray(props.data) ? props.data : []
|
||||
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
|
||||
}
|
||||
/>
|
||||
)
|
||||
const props = getPreviewProps(node, dataValues, currentItem, onNodePropChange)
|
||||
if (node.kind === 'custom') {
|
||||
return renderCustomComponent?.(node.type, { ...props, children: content }) || null
|
||||
}
|
||||
if (node.type === 'Table') return <>{children}</>
|
||||
if (node.type === 'Menu') {
|
||||
const menuProps = { ...props }
|
||||
const options = Array.isArray(menuProps.items)
|
||||
|
|
@ -503,6 +537,51 @@ const renderElement = (
|
|||
</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') {
|
||||
return <input {...(props as React.InputHTMLAttributes<HTMLInputElement>)} type="checkbox" />
|
||||
}
|
||||
|
|
@ -529,6 +608,7 @@ const NodeView = ({
|
|||
onMove,
|
||||
onDuplicate,
|
||||
onDelete,
|
||||
onNodePropChange,
|
||||
renderCustomComponent,
|
||||
dataValues,
|
||||
currentItem,
|
||||
|
|
@ -538,10 +618,11 @@ const NodeView = ({
|
|||
selectedId: string | null
|
||||
interactive: boolean
|
||||
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
|
||||
onDuplicate?: (id: string) => void
|
||||
onDelete?: (id: string) => void
|
||||
onNodePropChange?: (id: string, propertyName: string, value: unknown) => void
|
||||
renderCustomComponent?: (name: string, props?: Record<string, unknown>) => React.ReactNode
|
||||
dataValues: Record<string, unknown>
|
||||
currentItem?: unknown
|
||||
|
|
@ -557,7 +638,7 @@ const NodeView = ({
|
|||
'Card',
|
||||
'FormContainer',
|
||||
].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
|
||||
? getBindingValue(itemsBinding, dataValues, currentItem)
|
||||
: node.props.items
|
||||
|
|
@ -590,11 +671,108 @@ const NodeView = ({
|
|||
onMove={onMove}
|
||||
onDuplicate={onDuplicate}
|
||||
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 =
|
||||
node.type === 'Grid' && interactive && node.children.length === 0
|
||||
node.type === 'Table'
|
||||
? tableContent
|
||||
: node.type === 'Grid' && interactive && node.children.length === 0
|
||||
? repeatedItems.length
|
||||
? [
|
||||
<GridDataTablePreview
|
||||
|
|
@ -780,6 +958,7 @@ const NodeView = ({
|
|||
dataValues,
|
||||
currentItem,
|
||||
interactive,
|
||||
onNodePropChange,
|
||||
renderCustomComponent,
|
||||
)}
|
||||
</PreviewBoundary>
|
||||
|
|
@ -805,6 +984,7 @@ const VisualCanvas = ({
|
|||
onMove,
|
||||
onDuplicate,
|
||||
onDelete,
|
||||
onNodePropChange,
|
||||
renderCustomComponent,
|
||||
dataValues = {},
|
||||
}: VisualCanvasProps) => {
|
||||
|
|
@ -843,6 +1023,7 @@ const VisualCanvas = ({
|
|||
onMove={onMove}
|
||||
onDuplicate={onDuplicate}
|
||||
onDelete={onDelete}
|
||||
onNodePropChange={onNodePropChange}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -18,8 +18,16 @@ interface GeneratedComponentMetadata {
|
|||
}
|
||||
|
||||
const generatedMetadata = generatedComponentProps as Record<string, GeneratedComponentMetadata>
|
||||
const CONTAINER_NAMES = new Set(['div', 'Card', 'FormContainer', 'Tabs', 'Timeline'])
|
||||
const DATA_COMPONENT_NAMES = new Set(['AutoComplete', 'Grid', 'Menu', 'Pagination', 'Select', 'Table'])
|
||||
const CONTAINER_NAMES = new Set(['div', 'Card', 'FormContainer', 'Table', 'Tabs', 'Timeline'])
|
||||
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 UI_PROPERTY_ALIASES: Record<string, Record<string, string>> = {
|
||||
Select: {
|
||||
|
|
@ -31,10 +39,14 @@ const UI_PROPERTY_ALIASES: Record<string, Record<string, string>> = {
|
|||
},
|
||||
}
|
||||
export const DESIGNER_PRIMARY_EVENTS: Record<string, string[]> = {
|
||||
AutoComplete: ['onInputChange', 'onSelect'],
|
||||
Button: ['onClick'],
|
||||
Checkbox: ['onChange'],
|
||||
Dropdown: ['onSelect'],
|
||||
Input: ['onChange'],
|
||||
Menu: ['onSelect'],
|
||||
Pagination: ['onChange'],
|
||||
'Radio.Group': ['onChange'],
|
||||
Select: ['onChange'],
|
||||
}
|
||||
|
||||
|
|
@ -56,6 +68,18 @@ console.log('Value:', value)`,
|
|||
Menu: {
|
||||
onSelect: `// Seçilen menü öğesinin value değeridir.
|
||||
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: {
|
||||
onChange: `// Seçilen option nesnesi; temizlendiğinde null olur.
|
||||
|
|
@ -120,6 +144,26 @@ const GRID_TABLE_BEHAVIOR_PROPERTIES = [
|
|||
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 = {
|
||||
name: 'options',
|
||||
type: 'array' as const,
|
||||
|
|
@ -149,7 +193,7 @@ const normalizeDefinition = (
|
|||
? definition.properties.filter((property) => property.name !== 'children')
|
||||
: definition.properties
|
||||
let properties =
|
||||
['Grid', 'Table'].includes(definition.name) &&
|
||||
definition.name === 'Grid' &&
|
||||
!sourceProperties.some((property) => property.name === 'items')
|
||||
? [COLLECTION_ITEMS_PROPERTY, ...sourceProperties]
|
||||
: sourceProperties
|
||||
|
|
@ -164,6 +208,14 @@ const normalizeDefinition = (
|
|||
...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 {
|
||||
...definition,
|
||||
|
|
@ -246,7 +298,11 @@ const normalizeUiDefinition = (
|
|||
return normalizeDefinition(
|
||||
definition,
|
||||
'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 generatedProperties = generatedPropertiesToDesigner(generated, existingProperties)
|
||||
if (
|
||||
['Select', 'AutoComplete', 'Menu'].includes(definition.name) &&
|
||||
['Select', 'AutoComplete', 'Menu', 'Dropdown', 'Radio.Group'].includes(definition.name) &&
|
||||
!generatedProperties.some(
|
||||
(property) => property.name === (definition.name === 'Menu' ? 'items' : 'options'),
|
||||
(property) =>
|
||||
property.name ===
|
||||
(['Menu', 'Dropdown', 'Radio.Group'].includes(definition.name) ? 'items' : 'options'),
|
||||
)
|
||||
) {
|
||||
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 (
|
||||
['Grid', 'Table'].includes(definition.name) &&
|
||||
definition.name === 'Grid' &&
|
||||
!generatedProperties.some((property) => property.name === 'items')
|
||||
) {
|
||||
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 properties = [
|
||||
...generatedProperties,
|
||||
|
|
@ -294,7 +361,12 @@ const normalizeUiDefinition = (
|
|||
return {
|
||||
...definition,
|
||||
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),
|
||||
sourceFile: generated.sourceFile,
|
||||
interfaceName: generated.interfaceName,
|
||||
|
|
@ -431,7 +503,7 @@ export const DESIGNER_EXTRAS: DesignerComponentDefinition[] = [
|
|||
icon: 'Space',
|
||||
category: 'layout',
|
||||
kind: 'layout',
|
||||
toolboxGroup: 'layout',
|
||||
toolboxGroup: 'ui',
|
||||
description: 'Bileşenler arasında boşluk',
|
||||
properties: [{ name: 'height', type: 'number', value: 24, category: 'styling' }],
|
||||
hooks: [],
|
||||
|
|
@ -451,7 +523,12 @@ export const getDesignerCatalog = (customNames: string[] = []): DesignerComponen
|
|||
icon: 'Blocks',
|
||||
category: inferUiCategory(name),
|
||||
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`,
|
||||
sourceFile: metadata.sourceFile,
|
||||
interfaceName: metadata.interfaceName,
|
||||
|
|
|
|||
|
|
@ -1,14 +1,5 @@
|
|||
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 cleaned = value.replace(/[^A-Za-z0-9_$]/g, '_')
|
||||
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 || '')})`
|
||||
}
|
||||
|
||||
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 runtimeState = getRuntimeStateSpec(node)
|
||||
const hasBoundInputValue = node.type === 'Input' && Boolean(node.bindings?.value?.sourceId)
|
||||
const hasInputChangeHandler = Boolean(node.events?.onChange?.trim())
|
||||
const props = Object.entries(node.props)
|
||||
.filter(
|
||||
([key, value]) =>
|
||||
key !== 'children' &&
|
||||
key !== runtimeState?.propertyName &&
|
||||
!(node.type === 'Checkbox' && key === 'defaultChecked' && 'checked' in node.props) &&
|
||||
!(hasBoundInputValue && !hasInputChangeHandler && key === 'readOnly') &&
|
||||
!node.bindings?.[key]?.sourceId &&
|
||||
|
|
@ -46,23 +106,6 @@ const propsToCode = (node: DesignerNode, itemVariable?: string) => {
|
|||
value !== undefined,
|
||||
)
|
||||
.map(([key, value]) => {
|
||||
if (node.type === 'Checkbox' && key === 'checked') {
|
||||
return `defaultChecked=${serializeValue(value)}`
|
||||
}
|
||||
if (node.type === 'Input' && key === 'value') {
|
||||
return `defaultValue=${serializeValue(value)}`
|
||||
}
|
||||
if (node.type === 'Select' && key === 'value') {
|
||||
const staticOptions = Array.isArray(node.props.options) ? node.props.options : []
|
||||
const selectedOption = staticOptions.find(
|
||||
(option) =>
|
||||
option &&
|
||||
typeof option === 'object' &&
|
||||
!Array.isArray(option) &&
|
||||
(option as Record<string, unknown>).value === value,
|
||||
)
|
||||
return `defaultValue=${serializeValue(selectedOption ?? null)}`
|
||||
}
|
||||
return `${key}=${serializeValue(value)}`
|
||||
})
|
||||
|
||||
|
|
@ -71,7 +114,7 @@ const propsToCode = (node: DesignerNode, itemVariable?: string) => {
|
|||
const expression = bindingExpression(node, propertyName, itemVariable)
|
||||
if (expression) {
|
||||
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 || '')})`
|
||||
: expression
|
||||
props.push(`${propertyName}={${propertyExpression}}`)
|
||||
|
|
@ -80,11 +123,17 @@ const propsToCode = (node: DesignerNode, itemVariable?: string) => {
|
|||
|
||||
if (hasBoundInputValue && !hasInputChangeHandler) props.push('readOnly={true}')
|
||||
|
||||
Object.entries(node.events)
|
||||
.filter(([, script]) => script.trim())
|
||||
.forEach(([eventName]) => {
|
||||
props.push(`${eventName}={handle_${safeIdentifier(node.id)}_${eventName}}`)
|
||||
})
|
||||
if (runtimeState) props.push(`${runtimeState.propertyName}={${runtimeState.stateName}}`)
|
||||
|
||||
const handlerNames = new Set(
|
||||
Object.entries(node.events)
|
||||
.filter(([, script]) => script.trim())
|
||||
.map(([eventName]) => eventName),
|
||||
)
|
||||
if (runtimeState) handlerNames.add(runtimeState.eventName)
|
||||
handlerNames.forEach((eventName) =>
|
||||
props.push(`${eventName}={handle_${safeIdentifier(node.id)}_${eventName}}`),
|
||||
)
|
||||
|
||||
return props.length ? ` ${props.join(' ')}` : ''
|
||||
}
|
||||
|
|
@ -98,7 +147,7 @@ const indent = (text: string, level: number) =>
|
|||
const nodeToCode = (node: DesignerNode, level = 0, itemVariable?: string): string => {
|
||||
if (node.type === 'Spacer') {
|
||||
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,
|
||||
)
|
||||
}
|
||||
|
|
@ -135,45 +184,32 @@ const nodeToCode = (node: DesignerNode, level = 0, itemVariable?: string): strin
|
|||
}
|
||||
|
||||
if (node.type === 'Table') {
|
||||
const legacyData = Array.isArray(node.props.data) ? node.props.data : []
|
||||
const staticItems =
|
||||
Array.isArray(node.props.items) && (node.props.items.length > 0 || !legacyData.length)
|
||||
? node.props.items
|
||||
: legacyData
|
||||
const itemsExpression =
|
||||
bindingExpression(node, 'items', itemVariable) ||
|
||||
bindingExpression(node, 'data', itemVariable) ||
|
||||
JSON.stringify(staticItems)
|
||||
const identifier = safeIdentifier(node.id)
|
||||
const itemsVariable = `tableItems_${identifier}`
|
||||
const columnsVariable = `tableColumns_${identifier}`
|
||||
const rowVariable = `tableRow_${identifier}`
|
||||
const configuredColumns = Array.isArray(node.props.dataColumns)
|
||||
? node.props.dataColumns.filter((column): column is string => typeof column === 'string')
|
||||
: Array.isArray(node.props.columns)
|
||||
? node.props.columns
|
||||
.map((column) => {
|
||||
if (typeof column === 'string') return column
|
||||
if (!column || typeof column !== 'object' || Array.isArray(column)) return ''
|
||||
const record = column as Record<string, unknown>
|
||||
return String(record.dataIndex ?? record.key ?? '')
|
||||
})
|
||||
.filter(Boolean)
|
||||
: null
|
||||
const columnsExpression = configuredColumns
|
||||
? JSON.stringify(configuredColumns)
|
||||
: `${itemsVariable}[0] && typeof ${itemsVariable}[0] === "object" && !Array.isArray(${itemsVariable}[0]) ? Object.keys(${itemsVariable}[0]) : ["value"]`
|
||||
return `${indent('{(() => {', level)}
|
||||
${indent(`const ${itemsVariable} = ${itemsExpression}`, level + 1)}
|
||||
${indent(`if (!Array.isArray(${itemsVariable})) return <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)}`
|
||||
const rowCount = Math.min(20, Math.max(1, Math.floor(Number(node.props.rowCount) || 2)))
|
||||
const columnCount = Math.min(
|
||||
20,
|
||||
Math.max(1, Math.floor(Number(node.props.columnCount) || 3)),
|
||||
)
|
||||
const rows = Array.from({ length: rowCount }, (_, rowIndex) => {
|
||||
const cells = Array.from({ length: columnCount }, (_, columnIndex) => {
|
||||
const slot = `table:${rowIndex}:${columnIndex}`
|
||||
const cellChildren = node.children.filter(
|
||||
(child, childIndex) =>
|
||||
(child.slot ||
|
||||
`table:${Math.floor(childIndex / columnCount)}:${childIndex % columnCount}`) === slot,
|
||||
)
|
||||
const content = cellChildren.length
|
||||
? cellChildren.map((child) => nodeToCode(child, level + 3, itemVariable)).join('\n')
|
||||
: indent('<span />', level + 3)
|
||||
return `${indent('<td>', level + 2)}\n${content}\n${indent('</td>', level + 2)}`
|
||||
}).join('\n')
|
||||
return `${indent('<tr>', level + 1)}\n${cells}\n${indent('</tr>', level + 1)}`
|
||||
}).join('\n')
|
||||
const className = JSON.stringify(String(node.props.className || ''))
|
||||
return `${indent(`<UiKit.Table className={${className} + " [&>tbody>tr>td]:!px-1.5 [&>tbody>tr>td]:!py-1.5"} borderlessRow compact={${Boolean(node.props.compact)}} hoverable={${node.props.hoverable !== false}} overflow={${node.props.overflow !== false}} style={{ minWidth: "100%", tableLayout: "fixed", width: "100%" }}>`, level)}
|
||||
${indent('<tbody>', level + 1)}
|
||||
${rows}
|
||||
${indent('</tbody>', level + 1)}
|
||||
${indent('</UiKit.Table>', level)}`
|
||||
}
|
||||
|
||||
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)}`
|
||||
}
|
||||
|
||||
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 =
|
||||
node.type === 'checkbox'
|
||||
? 'input'
|
||||
|
|
@ -335,25 +409,56 @@ ${indent(')}', level + 1)}`
|
|||
export const generateDesignerCode = (name: string, document: DesignerDocument) => {
|
||||
const componentName = safeIdentifier(name || 'VisualComponent')
|
||||
const handlers: string[] = []
|
||||
const runtimeStateHooks: string[] = []
|
||||
let hasSelect = false
|
||||
let hasDataTable = false
|
||||
|
||||
const visit = (nodes: DesignerNode[]) => {
|
||||
nodes.forEach((node) => {
|
||||
if (node.type === 'Select' || node.type === 'AutoComplete' || node.type === 'Menu') {
|
||||
if (
|
||||
['Select', 'AutoComplete', 'Menu', 'Dropdown', 'Radio.Group'].includes(node.type)
|
||||
) {
|
||||
hasSelect = true
|
||||
}
|
||||
if (node.type === 'Grid' || node.type === 'Table') hasDataTable = true
|
||||
Object.entries(node.events).forEach(([eventName, script]) => {
|
||||
if (!script.trim()) return
|
||||
if (node.type === 'Checkbox' && eventName === 'onChange') {
|
||||
handlers.push(
|
||||
` const handle_${safeIdentifier(node.id)}_${eventName} = (checked, originalEvent) => {\n const event = { checked, originalEvent, target: originalEvent?.target }\n${indent(script, 2)}\n }`,
|
||||
)
|
||||
return
|
||||
}
|
||||
if (node.type === 'Grid') hasDataTable = true
|
||||
const runtimeState = getRuntimeStateSpec(node)
|
||||
if (runtimeState) {
|
||||
runtimeStateHooks.push(
|
||||
` const [${runtimeState.stateName}, ${runtimeState.setterName}] = React.useState(${JSON.stringify(runtimeState.initialValue)})`,
|
||||
)
|
||||
}
|
||||
const eventNames = new Set(
|
||||
Object.entries(node.events)
|
||||
.filter(([, script]) => script.trim())
|
||||
.map(([eventName]) => eventName),
|
||||
)
|
||||
if (runtimeState) eventNames.add(runtimeState.eventName)
|
||||
eventNames.forEach((eventName) => {
|
||||
const script = node.events[eventName]?.trim() || ''
|
||||
const stateUpdate =
|
||||
runtimeState?.eventName === eventName
|
||||
? node.type === 'Select'
|
||||
? `${runtimeState.setterName}(valueOrEvent)`
|
||||
: node.type === 'AutoComplete'
|
||||
? `${runtimeState.setterName}(valueOrEvent ?? "")`
|
||||
: node.type === 'Menu'
|
||||
? `${runtimeState.setterName}([String(valueOrEvent ?? "")])`
|
||||
: node.type === 'Dropdown'
|
||||
? `${runtimeState.setterName}(String(valueOrEvent ?? ""))`
|
||||
: node.type === 'Pagination'
|
||||
? `${runtimeState.setterName}(Number(valueOrEvent) || 1)`
|
||||
: runtimeState.propertyName === 'checked'
|
||||
? `${runtimeState.setterName}(typeof valueOrEvent === "object" && valueOrEvent?.target ? Boolean(valueOrEvent.target.checked) : Boolean(valueOrEvent))`
|
||||
: node.type === 'Input' || node.kind === 'html'
|
||||
? `${runtimeState.setterName}(valueOrEvent?.target?.value ?? valueOrEvent)`
|
||||
: `${runtimeState.setterName}(valueOrEvent)`
|
||||
: ''
|
||||
const eventDeclaration =
|
||||
node.type === 'Checkbox' && eventName === 'onChange'
|
||||
? ' const event = { checked: Boolean(valueOrEvent), originalEvent, target: originalEvent?.target }'
|
||||
: ' const event = valueOrEvent'
|
||||
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)
|
||||
|
|
@ -368,8 +473,8 @@ export const generateDesignerCode = (name: string, document: DesignerDocument) =
|
|||
return value.map((item, index) => {
|
||||
if (item === null || typeof item !== "object" || Array.isArray(item)) return { label: String(item ?? ""), value: item ?? index }
|
||||
const primitiveKeys = Object.keys(item).filter((key) => item[key] === null || ["string", "number", "boolean"].includes(typeof item[key]))
|
||||
const labelKey = ["label", "name", "title", "text", "description"].find((key) => item[key] !== undefined)
|
||||
const valueKey = ["value", "id", "code", "key"].find((key) => item[key] !== undefined)
|
||||
const labelKey = ["label", "children", "name", "title", "text", "description"].find((key) => item[key] !== undefined)
|
||||
const valueKey = ["value", "eventKey", "id", "code", "key"].find((key) => item[key] !== undefined)
|
||||
const optionLabel = labelPath ? getByPath(item, labelPath) : item[labelKey ?? primitiveKeys[0]]
|
||||
const optionValue = valuePath ? getByPath(item, valuePath) : item[valueKey ?? primitiveKeys[1] ?? primitiveKeys[0]]
|
||||
return { ...item, label: String(optionLabel ?? \`Seçenek \${index + 1}\`), value: optionValue ?? index }
|
||||
|
|
@ -411,5 +516,5 @@ export const generateDesignerCode = (name: string, document: DesignerDocument) =
|
|||
|
||||
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
|
||||
type: string
|
||||
kind: DesignerNodeKind
|
||||
slot?: string
|
||||
props: Record<string, unknown>
|
||||
events: Record<string, string>
|
||||
bindings: Record<string, DesignerBinding>
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ import React, {
|
|||
useEffect,
|
||||
useCallback,
|
||||
useMemo,
|
||||
useRef,
|
||||
} from 'react'
|
||||
|
||||
interface ComponentProps {
|
||||
|
|
@ -33,6 +32,7 @@ interface ComponentContextType {
|
|||
getComponent: (id: string) => CustomComponent | undefined
|
||||
getComponentByName: (name: string) => CustomComponent | undefined
|
||||
refreshComponents: () => Promise<void>
|
||||
refreshComponent: (id: string) => Promise<CustomComponent | undefined>
|
||||
// Manual registered components
|
||||
registeredComponents: Record<string, React.ComponentType<unknown>>
|
||||
registerComponent: (name: string, component: React.ComponentType<unknown>) => void
|
||||
|
|
@ -45,14 +45,13 @@ interface ComponentContextType {
|
|||
}
|
||||
|
||||
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 = () => {
|
||||
try {
|
||||
window.localStorage.setItem(COMPONENTS_CHANGED_STORAGE_KEY, String(Date.now()))
|
||||
} catch {
|
||||
// Local state is already current; storage can be unavailable in restricted browsers.
|
||||
}
|
||||
componentsChangedChannel?.postMessage({ changedAt: Date.now() })
|
||||
}
|
||||
|
||||
const haveSameComponentRevision = (
|
||||
|
|
@ -148,7 +147,8 @@ export const useComponents = () => {
|
|||
}
|
||||
|
||||
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 [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
|
@ -156,50 +156,59 @@ export const ComponentProvider: React.FC<{ children: React.ReactNode }> = ({ chi
|
|||
const [compiledComponents, setCompiledComponents] = useState<
|
||||
Record<string, React.ComponentType<ComponentProps>>
|
||||
>({})
|
||||
const refreshInFlightRef = useRef<Promise<void> | null>(null)
|
||||
const refreshComponents = useCallback(async () => {
|
||||
if (!applicationConfig) return
|
||||
|
||||
const refreshComponents = useCallback(() => {
|
||||
if (refreshInFlightRef.current) return refreshInFlightRef.current
|
||||
try {
|
||||
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 () => {
|
||||
try {
|
||||
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)
|
||||
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
|
||||
useEffect(() => {
|
||||
const initialComponents =
|
||||
(extraProperties?.customComponents as CustomComponentDto[] | undefined) || []
|
||||
setComponents((current) =>
|
||||
haveSameComponentRevision(current, initialComponents) ? current : initialComponents,
|
||||
)
|
||||
}, [extraProperties])
|
||||
|
||||
useEffect(() => {
|
||||
void refreshComponents()
|
||||
}, [refreshComponents])
|
||||
const refreshComponent = useCallback(
|
||||
async (id: string): Promise<CustomComponent | undefined> => {
|
||||
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(() => {
|
||||
const handleFocus = () => void refreshComponents()
|
||||
const handleStorage = (event: StorageEvent) => {
|
||||
if (event.key === COMPONENTS_CHANGED_STORAGE_KEY) void refreshComponents()
|
||||
}
|
||||
const handleComponentsChanged = () => void refreshComponents()
|
||||
|
||||
window.addEventListener('focus', handleFocus)
|
||||
window.addEventListener('storage', handleStorage)
|
||||
return () => {
|
||||
window.removeEventListener('focus', handleFocus)
|
||||
window.removeEventListener('storage', handleStorage)
|
||||
}
|
||||
componentsChangedChannel?.addEventListener('message', handleComponentsChanged)
|
||||
return () =>
|
||||
componentsChangedChannel?.removeEventListener('message', handleComponentsChanged)
|
||||
}, [refreshComponents])
|
||||
|
||||
const addComponent = useCallback(async (componentData: CreateUpdateCustomComponentDto) => {
|
||||
|
|
@ -633,6 +642,7 @@ export const ComponentProvider: React.FC<{ children: React.ReactNode }> = ({ chi
|
|||
getComponent,
|
||||
getComponentByName,
|
||||
refreshComponents,
|
||||
refreshComponent,
|
||||
registeredComponents,
|
||||
registerComponent,
|
||||
renderComponent,
|
||||
|
|
@ -651,6 +661,7 @@ export const ComponentProvider: React.FC<{ children: React.ReactNode }> = ({ chi
|
|||
getComponent,
|
||||
getComponentByName,
|
||||
refreshComponents,
|
||||
refreshComponent,
|
||||
registeredComponents,
|
||||
registerComponent,
|
||||
renderComponent,
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ const ComponentEditor: React.FC = () => {
|
|||
const navigate = useNavigate()
|
||||
const { translate } = useLocalization()
|
||||
|
||||
const { getComponent, addComponent, updateComponent } = useComponents()
|
||||
const { getComponent, addComponent, updateComponent, refreshComponent } = useComponents()
|
||||
|
||||
const [isLoaded, setIsLoaded] = useState(false)
|
||||
|
||||
|
|
@ -47,8 +47,16 @@ const ComponentEditor: React.FC = () => {
|
|||
// Load existing component data - sadece edit modunda
|
||||
useEffect(() => {
|
||||
if (isEditing && id && !isLoaded) {
|
||||
const component = getComponent(id)
|
||||
if (component) {
|
||||
let cancelled = false
|
||||
|
||||
void refreshComponent(id).then((freshComponent) => {
|
||||
if (cancelled) return
|
||||
const component = freshComponent
|
||||
if (!component) {
|
||||
setIsLoaded(true)
|
||||
return
|
||||
}
|
||||
|
||||
// Parse dependencies from JSON string
|
||||
let deps: string[] = []
|
||||
try {
|
||||
|
|
@ -68,12 +76,16 @@ const ComponentEditor: React.FC = () => {
|
|||
|
||||
setInitialValues(values)
|
||||
setIsLoaded(true)
|
||||
})
|
||||
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
} else if (!isEditing && !isLoaded) {
|
||||
// Yeni komponent için boş başla - TEMPLATE YOK
|
||||
setIsLoaded(true)
|
||||
}
|
||||
}, [id, isEditing, getComponent, isLoaded])
|
||||
}, [id, isEditing, isLoaded, refreshComponent])
|
||||
|
||||
const createDefaultVisualDocument = (componentName: string): DesignerDocument => {
|
||||
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 (
|
||||
<div className="space-y-4">
|
||||
<Helmet
|
||||
|
|
@ -284,7 +279,12 @@ const ComponentManager: React.FC = () => {
|
|||
color="gray-600"
|
||||
disabled={!component.isActive || !component.routePath?.trim()}
|
||||
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" />
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ import {
|
|||
FaPlus,
|
||||
FaPlay,
|
||||
FaRedo,
|
||||
FaRegCopy,
|
||||
FaRegSave,
|
||||
FaSearch,
|
||||
FaSlidersH,
|
||||
|
|
@ -38,6 +37,7 @@ import TailwindModal from '@/components/codeLayout/TailwindModal'
|
|||
import { useStoreState } from '@/store/store'
|
||||
import VisualCanvas, { DESIGNER_DRAG_TYPE } from '@/components/visualDesigner/VisualCanvas'
|
||||
import {
|
||||
DESIGNER_DATA_COMPONENT_NAMES,
|
||||
DESIGNER_PRIMARY_EVENTS,
|
||||
getDesignerCatalog,
|
||||
getDesignerEventSnippet,
|
||||
|
|
@ -95,11 +95,18 @@ const DATA_BINDABLE_PROPERTY_NAMES = new Set([
|
|||
'disabled',
|
||||
'loading',
|
||||
])
|
||||
const OPTION_DATA_COMPONENT_NAMES = new Set(['Select', 'AutoComplete', 'Menu'])
|
||||
const TABULAR_DATA_COMPONENT_NAMES = new Set(['Grid', 'Table'])
|
||||
const OPTION_DATA_COMPONENT_NAMES = new Set([
|
||||
'Select',
|
||||
'AutoComplete',
|
||||
'Menu',
|
||||
'Dropdown',
|
||||
'Radio.Group',
|
||||
])
|
||||
const TABULAR_DATA_COMPONENT_NAMES = new Set(['Grid'])
|
||||
const isOptionDataComponent = (type?: string) =>
|
||||
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) =>
|
||||
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 normalizeTableDimension = (value: unknown, fallback: number) =>
|
||||
Math.min(20, Math.max(1, Math.floor(Number(value) || fallback)))
|
||||
|
||||
const updateNodeTree = (
|
||||
nodes: DesignerNode[],
|
||||
|
|
@ -384,33 +393,49 @@ const normalizeDesignerDocument = (document: DesignerDocument): DesignerDocument
|
|||
delete bindings.options
|
||||
}
|
||||
if (node.type === 'Table') {
|
||||
if (
|
||||
Array.isArray(props.data) &&
|
||||
(!Array.isArray(props.items) || props.items.length === 0)
|
||||
) {
|
||||
props.items = props.data
|
||||
}
|
||||
if (Array.isArray(props.columns) && !Array.isArray(props.dataColumns)) {
|
||||
props.dataColumns = 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)
|
||||
}
|
||||
const legacyRows = Array.isArray(props.items)
|
||||
? props.items
|
||||
: Array.isArray(props.data)
|
||||
? props.data
|
||||
: []
|
||||
const firstLegacyRow = legacyRows.find(
|
||||
(item) => item && typeof item === 'object' && !Array.isArray(item),
|
||||
) as Record<string, unknown> | undefined
|
||||
const legacyColumns = Array.isArray(props.dataColumns)
|
||||
? props.dataColumns
|
||||
: Array.isArray(props.columns)
|
||||
? props.columns
|
||||
: firstLegacyRow
|
||||
? Object.keys(firstLegacyRow)
|
||||
: []
|
||||
props.rowCount = normalizeTableDimension(props.rowCount, legacyRows.length || 2)
|
||||
props.columnCount = normalizeTableDimension(
|
||||
props.columnCount,
|
||||
legacyColumns.length || 3,
|
||||
)
|
||||
delete props.data
|
||||
delete props.columns
|
||||
if (!bindings.items && bindings.data) bindings.items = bindings.data
|
||||
delete props.items
|
||||
delete props.dataColumns
|
||||
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 {
|
||||
...node,
|
||||
props,
|
||||
events,
|
||||
bindings,
|
||||
children: normalizeNodes(node.children || []),
|
||||
children,
|
||||
}
|
||||
})
|
||||
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,
|
||||
)) {
|
||||
try {
|
||||
const legacyItems = JSON.parse(match[2]) as unknown[]
|
||||
const legacyColumns = JSON.parse(match[3]) as unknown[]
|
||||
addNode(
|
||||
match.index,
|
||||
'Table',
|
||||
'ui',
|
||||
{
|
||||
...parseGeneratedJsxProps(match[4], componentDefaultProps),
|
||||
items: JSON.parse(match[2]),
|
||||
dataColumns: JSON.parse(match[3]),
|
||||
rowCount: normalizeTableDimension(legacyItems.length, 2),
|
||||
columnCount: normalizeTableDimension(legacyColumns.length, 3),
|
||||
},
|
||||
{},
|
||||
match[1],
|
||||
|
|
@ -822,6 +849,10 @@ const VisualComponentDesigner = () => {
|
|||
)
|
||||
const catalog = useMemo(() => getDesignerCatalog(customNames), [customNames])
|
||||
const catalogByName = useMemo(() => new Map(catalog.map((item) => [item.name, item])), [catalog])
|
||||
const selectDesignerNode = useCallback((nodeId: string | null) => {
|
||||
setSelectedId(nodeId || null)
|
||||
setInspectorTab('properties')
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
|
|
@ -931,19 +962,20 @@ const VisualComponentDesigner = () => {
|
|||
}, [document])
|
||||
|
||||
const addComponent = useCallback(
|
||||
(definitionName: string, parentId: string | null = null) => {
|
||||
(definitionName: string, parentId: string | null = null, slot?: string) => {
|
||||
const definition = catalogByName.get(definitionName)
|
||||
if (!definition) return
|
||||
const node = definitionToNode(definition)
|
||||
if (slot) node.slot = slot
|
||||
commitDocument((current) => ({
|
||||
...current,
|
||||
sourceMode: 'visual',
|
||||
nodes: insertNodeTree(current.nodes, node, parentId),
|
||||
}))
|
||||
setSelectedId(node.id)
|
||||
selectDesignerNode(node.id)
|
||||
setWorkspaceTab('design')
|
||||
},
|
||||
[catalogByName, commitDocument],
|
||||
[catalogByName, commitDocument, selectDesignerNode],
|
||||
)
|
||||
|
||||
const selectedNode = useMemo(
|
||||
|
|
@ -951,6 +983,9 @@ const VisualComponentDesigner = () => {
|
|||
[document.nodes, selectedId],
|
||||
)
|
||||
const selectedDefinition = selectedNode ? catalogByName.get(selectedNode.type) : undefined
|
||||
const selectedIsDataComponent = Boolean(
|
||||
selectedNode && DESIGNER_DATA_COMPONENT_NAMES.has(selectedNode.type),
|
||||
)
|
||||
const selectedProperties = useMemo<DesignerPropertyInfo[]>(() => {
|
||||
if (!selectedNode) return []
|
||||
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) => {
|
||||
commitDocument((current) => ({
|
||||
...current,
|
||||
|
|
@ -1626,9 +1674,15 @@ const VisualComponentDesigner = () => {
|
|||
filteredCatalog.forEach((item) => {
|
||||
groups.get(item.toolboxGroup)?.push(item)
|
||||
})
|
||||
return TOOLBOX_GROUP_ORDER.map((group) => [group, groups.get(group) || []] as const).filter(
|
||||
([, definitions]) => definitions.length > 0,
|
||||
)
|
||||
return TOOLBOX_GROUP_ORDER.map(
|
||||
(group) =>
|
||||
[
|
||||
group,
|
||||
[...(groups.get(group) || [])].sort((left, right) =>
|
||||
left.name.localeCompare(right.name, 'tr', { sensitivity: 'base' }),
|
||||
),
|
||||
] as const,
|
||||
).filter(([, definitions]) => definitions.length > 0)
|
||||
}, [filteredCatalog])
|
||||
|
||||
const activeDataSource = document.dataSources.find((source) => source.id === dataPanelSourceId)
|
||||
|
|
@ -1653,8 +1707,10 @@ const VisualComponentDesigner = () => {
|
|||
if (option && typeof option === 'object' && !Array.isArray(option)) {
|
||||
const record = option as Record<string, unknown>
|
||||
return {
|
||||
label: String(record.label ?? `Seçenek ${index + 1}`),
|
||||
value: record.value ?? index + 1,
|
||||
label: String(
|
||||
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 }
|
||||
|
|
@ -1884,14 +1940,20 @@ const VisualComponentDesigner = () => {
|
|||
}, [activeDataSample, activeDataSource?.id, commitDocument, selectedId, selectedNode])
|
||||
|
||||
const dataBindableProperties =
|
||||
selectedNode && selectedDefinition && selectedNode.kind !== 'platform'
|
||||
selectedNode &&
|
||||
selectedDefinition &&
|
||||
DESIGNER_DATA_COMPONENT_NAMES.has(selectedNode.type)
|
||||
? selectedDefinition.properties.filter(
|
||||
(property) =>
|
||||
property.category !== 'events' &&
|
||||
property.type !== 'function' &&
|
||||
(isTabularDataComponent(selectedNode.type)
|
||||
? 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 selectConfiguration = renderSelectDataConfiguration()
|
||||
const tabularConfiguration = renderTabularDataConfiguration()
|
||||
if (
|
||||
(isOptionDataComponent(selectedNode?.type) || isTabularDataComponent(selectedNode?.type)) &&
|
||||
selectDataMode === 'static'
|
||||
|
|
@ -2283,9 +2344,9 @@ const VisualComponentDesigner = () => {
|
|||
return (
|
||||
<div className="space-y-3">
|
||||
{selectModeSelector}
|
||||
{selectConfiguration}
|
||||
{tabularConfiguration}
|
||||
{isTabularDataComponent(selectedNode?.type) && renderTabularColumnConfiguration()}
|
||||
<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">
|
||||
Statik veri <strong>Properties</strong> sekmesindeki items/options alanından yönetilir.
|
||||
</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'}`}
|
||||
style={{ paddingLeft: 8 + depth * 14 }}
|
||||
type="button"
|
||||
onClick={() => setSelectedId(node.id)}
|
||||
onClick={() => selectDesignerNode(node.id)}
|
||||
>
|
||||
<FaLayerGroup className="shrink-0" />
|
||||
<span className="truncate">{node.type}</span>
|
||||
|
|
@ -2577,7 +2638,8 @@ const VisualComponentDesigner = () => {
|
|||
background={appThemeMode === 'dark' ? '#0f172a' : document.canvas.background}
|
||||
dataValues={dataSourceSamples}
|
||||
renderCustomComponent={(componentName, props) => renderComponent(componentName, props)}
|
||||
onSelect={(nodeId) => setSelectedId(nodeId || null)}
|
||||
onSelect={(nodeId) => selectDesignerNode(nodeId || null)}
|
||||
onNodePropChange={updateNodeProp}
|
||||
onDropComponent={addComponent}
|
||||
onMove={moveNode}
|
||||
onDuplicate={duplicateNode}
|
||||
|
|
@ -2687,7 +2749,7 @@ const VisualComponentDesigner = () => {
|
|||
const routePath = component.routePath.startsWith('/')
|
||||
? component.routePath
|
||||
: `/${component.routePath}`
|
||||
window.open(routePath, '_blank', 'noopener,noreferrer')
|
||||
window.open(routePath, '_blank')
|
||||
}}
|
||||
>
|
||||
Aç
|
||||
|
|
@ -3074,9 +3136,17 @@ const VisualComponentDesigner = () => {
|
|||
).map(([tab, icon, label]) => (
|
||||
<button
|
||||
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"
|
||||
onClick={() => setInspectorTab(tab)}
|
||||
onClick={() => {
|
||||
if (tab !== 'data' || selectedIsDataComponent) setInspectorTab(tab)
|
||||
}}
|
||||
>
|
||||
{icon}
|
||||
{label}
|
||||
|
|
@ -3126,8 +3196,18 @@ const VisualComponentDesigner = () => {
|
|||
</div>
|
||||
)}
|
||||
</div>
|
||||
{inspectorTab === 'properties'
|
||||
? selectedProperties
|
||||
{inspectorTab === 'properties' ? (
|
||||
<>
|
||||
{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(
|
||||
(property) =>
|
||||
property.category !== 'events' &&
|
||||
|
|
@ -3144,11 +3224,12 @@ const VisualComponentDesigner = () => {
|
|||
.map((property) => {
|
||||
const propertyValue =
|
||||
selectedNode.props[property.name] ?? property.value
|
||||
const snippet = getPropertySnippet(property, propertyValue)
|
||||
const helperValue = getPropertyExampleValue(property, property.value)
|
||||
const snippet = getPropertySnippet(property, helperValue)
|
||||
return (
|
||||
<div
|
||||
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>
|
||||
|
|
@ -3157,8 +3238,26 @@ const VisualComponentDesigner = () => {
|
|||
<b className="ml-0.5 text-red-500">*</b>
|
||||
) : null}
|
||||
</span>
|
||||
<span className="text-[9px] font-normal text-slate-400">
|
||||
{property.tsType || property.type}
|
||||
<span className="flex items-center gap-1 text-[9px] font-normal text-slate-400">
|
||||
<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>
|
||||
{property.description && (
|
||||
|
|
@ -3173,28 +3272,13 @@ const VisualComponentDesigner = () => {
|
|||
options={property.options}
|
||||
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>
|
||||
)
|
||||
})
|
||||
: renderDataInspector()}
|
||||
})}
|
||||
</>
|
||||
) : (
|
||||
renderDataInspector()
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
|
|
|
|||
|
|
@ -526,26 +526,12 @@ const TodoBoard = ({ listFormCode, searchParams, isSubForm, gridDto }: TodoBoard
|
|||
|
||||
useEffect(() => {
|
||||
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)
|
||||
|
||||
if (!stateStoring?.enabled) {
|
||||
const emptyConfig = { columns: [], retired: [] }
|
||||
columnConfigRef.current = emptyConfig
|
||||
setColumnConfig(emptyConfig)
|
||||
localStorage.removeItem(legacyStorageKey)
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
|
|
@ -555,23 +541,14 @@ const TodoBoard = ({ listFormCode, searchParams, isSubForm, gridDto }: TodoBoard
|
|||
.current()
|
||||
.then((stored) => {
|
||||
if (cancelled) return
|
||||
const hasDatabaseState = Array.isArray(stored?.columns)
|
||||
const config: StoredBoardColumns = hasDatabaseState
|
||||
const config: StoredBoardColumns = Array.isArray(stored?.columns)
|
||||
? {
|
||||
columns: stored.columns.map(String),
|
||||
retired: Array.isArray(stored?.retired) ? stored.retired.map(String) : [],
|
||||
}
|
||||
: legacyConfig
|
||||
: { columns: [], retired: [] }
|
||||
columnConfigRef.current = 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) => {
|
||||
if (cancelled) return
|
||||
|
|
|
|||
|
|
@ -107,7 +107,7 @@ export default defineConfig(async ({ mode }) => {
|
|||
short_name: 'Sözsoft Platform',
|
||||
theme_color: '#FF99C8',
|
||||
background_color: '#f0e7db',
|
||||
display: 'standalone',
|
||||
display: 'browser',
|
||||
icons: [
|
||||
{
|
||||
src: '/img/logo/logo-400.png',
|
||||
|
|
|
|||
Loading…
Reference in a new issue