From ac4ccd76d6af3ea240115d6f65f83a7009807fa1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sedat=20=C3=96ZT=C3=9CRK?= <76204082+iamsedatozturk@users.noreply.github.com> Date: Fri, 7 Aug 2026 00:19:34 +0300 Subject: [PATCH] =?UTF-8?q?Claude=20g=C3=BCncellemesi=20ile=20Custom=20Com?= =?UTF-8?q?ponent?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../componentEditor/ComponentPreview.tsx | 32 +- .../componentEditor/DynamicRenderer.tsx | 227 ++++---- .../visualDesigner/VisualCanvas.tsx | 383 +++++++++---- ui/src/components/visualDesigner/catalog.ts | 196 ++++--- .../visualDesigner/codeGenerator.ts | 321 ++++++++--- ui/src/components/visualDesigner/types.ts | 138 ++++- ui/src/contexts/ComponentContext.tsx | 535 +++++------------- ui/src/contexts/componentRuntime.ts | Bin 0 -> 11163 bytes .../views/developerKit/ComponentManager.tsx | 36 +- .../developerKit/VisualComponentDesigner.tsx | 294 ++++++++-- 10 files changed, 1286 insertions(+), 876 deletions(-) create mode 100644 ui/src/contexts/componentRuntime.ts diff --git a/ui/src/components/componentEditor/ComponentPreview.tsx b/ui/src/components/componentEditor/ComponentPreview.tsx index 61c606a6..d7046a19 100644 --- a/ui/src/components/componentEditor/ComponentPreview.tsx +++ b/ui/src/components/componentEditor/ComponentPreview.tsx @@ -1,6 +1,7 @@ -import React from 'react' +import React, { useMemo } from 'react' import DynamicRenderer from './DynamicRenderer' import { useComponents } from '@/contexts/ComponentContext' +import { parseComponentDependencies } from '@/contexts/componentRuntime' import { Loading } from '../shared' export interface ComponentPreviewProps { @@ -11,12 +12,17 @@ export interface ComponentPreviewProps { const ComponentPreview: React.FC = ({ componentName, className = '' }) => { const { components, loading } = useComponents() + // Referans olarak sabit tutulmalı: DynamicRenderer bunu effect bağımlılığı olarak kullanıyor. + const dependencies = useMemo(() => { + const component = components?.find((item) => item.name === componentName && item.isActive) + return parseComponentDependencies(component?.dependencies) + }, [components, componentName]) + if (!componentName) { return
Bileşen ismi yok.
} - // components dizisinin varlığını kontrol et - if (loading || !components || !Array.isArray(components)) { + if (loading || !Array.isArray(components)) { return (
@@ -26,26 +32,6 @@ const ComponentPreview: React.FC = ({ componentName, clas ) } - // Belirtilen bileşeni bul - const component = components.find((c) => c.name === componentName && c.isActive) - - let dependencies: string[] = [] - - if (component?.dependencies) { - try { - // JSON string mi? - if (component.dependencies.startsWith('[')) { - dependencies = JSON.parse(component.dependencies) - } else { - // Virgülle ayrılmış düz metin - dependencies = component.dependencies.split(',').map((d) => d.trim()) - } - } catch (err) { - console.error('Dependency parse hatası:', err) - dependencies = [] - } - } - return (
diff --git a/ui/src/components/componentEditor/DynamicRenderer.tsx b/ui/src/components/componentEditor/DynamicRenderer.tsx index 18e9a849..30619345 100644 --- a/ui/src/components/componentEditor/DynamicRenderer.tsx +++ b/ui/src/components/componentEditor/DynamicRenderer.tsx @@ -1,25 +1,36 @@ import React, { useEffect, useState } from 'react' -import * as Babel from '@babel/standalone' import axios from 'axios' import DOMPurify from 'dompurify' import apiService from '@/services/api.service' import * as UiKit from '@/components/ui' -import { generateDesignerCode } from '@/components/visualDesigner/codeGenerator' -import type { DesignerDocument } from '@/components/visualDesigner/types' -import type { CustomComponent } from '@/proxy/developerKit/models' +import { + getComponentRuntimeCode, + parseComponentDependencies, + toErrorMessage, + type BabelLike, +} from '@/contexts/componentRuntime' import { useComponents } from '../../contexts/ComponentContext' import ErrorBoundary from './ErrorBoundary' +import PlatformViewHost from './PlatformViewHost' import { toast } from '../ui' import { Loading } from '../shared' -const compileComponent = (code: string, scope: Record = {}) => { +type RuntimeModule = { exports: { default?: React.ComponentType } } + +const compileComponent = ( + code: string, + scope: Record, + Babel: BabelLike, +): React.ComponentType | undefined => { const transpiled = Babel.transform(code, { filename: 'component.tsx', presets: ['typescript', 'react'], plugins: ['transform-modules-commonjs'], }).code - const module = { exports: {} } + if (!transpiled) throw new Error('Boş derleme çıktısı') + + const module: RuntimeModule = { exports: {} } const require = (moduleName: string) => { if (moduleName === 'react') return React if (moduleName === 'axios') return axios @@ -28,18 +39,21 @@ const compileComponent = (code: string, scope: Record = {}) => { throw new Error(`Modül bulunamadı: ${moduleName}`) } - const scopedEval = new Function( - 'module', - 'exports', - 'require', - ...Object.keys(scope), - transpiled!, - ) - + const scopedEval = new Function('module', 'exports', 'require', ...Object.keys(scope), transpiled) scopedEval(module, module.exports, require, ...Object.values(scope)) - const compiledModule = module.exports as any - return compiledModule.default + return module.exports.default +} + +// The UI kit is spread as-is: every exported component is available to runtime +// code without an extra per-component entry here. +const staticComponents: Record = { + ...UiKit, + PlatformViewHost, + toast, + apiService, + DOMPurify, + UiKit, } interface DynamicRendererProps { @@ -47,147 +61,102 @@ interface DynamicRendererProps { dependencies?: string[] } -const staticComponents: Record = { - ...UiKit, - Alert: React.lazy(() => import('../ui/Alert')), - Avatar: React.lazy(() => import('../ui/Avatar')), - Badge: React.lazy(() => import('../ui/Badge')), - Button: React.lazy(() => import('../ui/Button')), - Calendar: React.lazy(() => import('../ui/Calendar')), - Card: React.lazy(() => import('../ui/Card')), - Checkbox: React.lazy(() => import('../ui/Checkbox')), - ConfigProvider: React.lazy(() => import('../ui/ConfigProvider')), - DatePicker: React.lazy(() => import('../ui/DatePicker')), - Dialog: React.lazy(() => import('../ui/Dialog')), - Drawer: React.lazy(() => import('../ui/Drawer')), - Dropdown: React.lazy(() => import('../ui/Dropdown')), - FormItem: React.lazy(() => import('../ui/Form/FormItem')), - FormContainer: React.lazy(() => import('../ui/Form/FormContainer')), - Input: React.lazy(() => import('../ui/Input')), - InputGroup: React.lazy(() => import('../ui/InputGroup')), - Menu: React.lazy(() => import('../ui/Menu')), - MenuItem: React.lazy(() => import('../ui/MenuItem')), - Notification: React.lazy(() => import('../ui/Notification')), - Pagination: React.lazy(() => import('../ui/Pagination')), - Progress: React.lazy(() => import('../ui/Progress')), - Radio: React.lazy(() => import('../ui/Radio')), - RangeCalendar: React.lazy(() => import('../ui/RangeCalendar')), - ScrollBar: React.lazy(() => import('../ui/ScrollBar')), - Segment: React.lazy(() => import('../ui/Segment')), - Select: React.lazy(() => import('../ui/Select')), - Skeleton: React.lazy(() => import('../ui/Skeleton')), - Spinner: React.lazy(() => import('../ui/Spinner')), - Steps: React.lazy(() => import('../ui/Steps')), - Switcher: React.lazy(() => import('../ui/Switcher')), - Table: React.lazy(() => import('../ui/Table')), - Tabs: React.lazy(() => import('../ui/Tabs')), - Tag: React.lazy(() => import('../ui/Tag')), - TimeInput: React.lazy(() => import('../ui/TimeInput')), - Timeline: React.lazy(() => import('../ui/Timeline')), - Tooltip: React.lazy(() => import('../ui/Tooltip')), - Upload: React.lazy(() => import('../ui/Upload')), - PlatformViewHost: React.lazy(() => import('./PlatformViewHost')), - toast, - apiService, - DOMPurify, - UiKit, -} - -const getRuntimeCode = (component: CustomComponent) => { - if (!component) return '' - try { - const props = component.props ? JSON.parse(component.props) : null - const document = props?.visualDesigner as DesignerDocument | undefined - if ( - document?.version === 1 && - document.sourceMode === 'visual' && - Array.isArray(document.nodes) - ) { - return generateDesignerCode(component.name, document) - } - } catch (error) { - console.warn(`Visual designer schema could not be read for ${component.name}:`, error) - } - return component.code -} - const DynamicRenderer: React.FC = ({ componentName, dependencies: externalDeps, }) => { const [Component, setComponent] = useState | null>(null) + const [error, setError] = useState(null) const { getComponentByName, components } = useComponents() useEffect(() => { + let cancelled = false setComponent(null) + setError(null) const storedComponent = getComponentByName(componentName) - if (!storedComponent) return + if (!storedComponent) { + setError(`Component ${componentName} not found`) + return + } - const map = new Map( - components.map((c) => { - // Parse dependencies from JSON string - let componentDeps: string[] = [] - try { - componentDeps = c.dependencies ? JSON.parse(c.dependencies) : [] - } catch { - componentDeps = [] + const build = async () => { + // Babel is several megabytes; load it only when something must be compiled. + const Babel = (await import('@babel/standalone')) as unknown as BabelLike + if (cancelled) return + + const map = new Map( + components.map((component) => [ + component.name, + { + code: getComponentRuntimeCode(component), + dependencies: parseComponentDependencies(component.dependencies), + }, + ]), + ) + + const compiled: Record = {} + + const compileWithDependencies = (name: string): any => { + if (compiled[name]) return compiled[name] + + const entry = map.get(name) + if (!entry) { + if (staticComponents[name]) { + compiled[name] = staticComponents[name] + return staticComponents[name] + } + throw new Error(`Component ${name} not found`) } - return [ - c.name, - { - name: c.name, - jsx_code: getRuntimeCode(c), - dependencies: Array.isArray(componentDeps) ? componentDeps : [], - }, - ] - }), - ) + const depNames = name === componentName && externalDeps ? externalDeps : entry.dependencies - const compiled: Record = {} + const deps: Record = {} + for (const dep of depNames) { + deps[dep] = compileWithDependencies(dep) + } - const compileWithDependencies = (name: string): any => { - if (compiled[name]) return compiled[name] + const component = compileComponent( + entry.code, + { React, ...staticComponents, ...deps }, + Babel, + ) + if (!component) throw new Error(`Component ${name} bir default export döndürmüyor`) - const entry = map.get(name) - if (!entry && staticComponents[name]) { - compiled[name] = staticComponents[name] - return staticComponents[name] - } - if (!entry) throw new Error(`Component ${name} not found`) - - const depNames = - name === componentName && externalDeps ? externalDeps : entry.dependencies || [] - - const deps: Record = {} - for (const dep of depNames) { - deps[dep] = compileWithDependencies(dep) + compiled[name] = component + return component } - const comp = compileComponent(entry.jsx_code, { - React, - ...staticComponents, - ...deps, - }) - compiled[name] = comp - - return comp + try { + const RootComponent = compileWithDependencies(componentName) + if (!cancelled) setComponent(() => RootComponent) + } catch (err) { + console.error('Compilation error:', err) + if (!cancelled) setError(toErrorMessage(err)) + } } - try { - const RootComponent = compileWithDependencies(componentName) - setComponent(() => RootComponent) - } catch (err: any) { - console.error('Compilation error:', err) + void build() + + return () => { + cancelled = true } }, [componentName, externalDeps, components, getComponentByName]) + if (error) { + return ( +
+
{componentName}
+
{error}
+
+ ) + } + if (!Component) return (
- +
) diff --git a/ui/src/components/visualDesigner/VisualCanvas.tsx b/ui/src/components/visualDesigner/VisualCanvas.tsx index 431f8972..2fdd844d 100644 --- a/ui/src/components/visualDesigner/VisualCanvas.tsx +++ b/ui/src/components/visualDesigner/VisualCanvas.tsx @@ -6,8 +6,17 @@ import PlatformViewHost, { import { useLocalization } from '@/utils/hooks/useLocalization' import { FaArrowDown, FaArrowUp, FaClone, FaGripVertical, FaTrash } from 'react-icons/fa' import { + fromDesignerDate, + getDesignerCollectionProperty, + getDesignerTabSlot, getDesignerValueByPath, + isDesignerDateComponent, + isDesignerDateProperty, + isDesignerOptionComponent, normalizeDesignerKeyList, + resolveDesignerDropdownTitle, + resolveDesignerTabValue, + toDesignerDate, type DesignerBinding, type DesignerNode, } from './types' @@ -15,18 +24,14 @@ import { 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))) -const resolveStaticLanguageKeys = ( - value: unknown, - translate: (key: string) => string, -): unknown => { +const resolveStaticLanguageKeys = (value: unknown, translate: (key: string) => string): unknown => { if (typeof value === 'string') return value.startsWith('::') ? translate(value) : value if (Array.isArray(value)) return value.map((item) => resolveStaticLanguageKeys(item, translate)) - if (value && typeof value === 'object') { + // Only plain objects are walked: `Object.entries` on a Date yields nothing and + // would silently turn it into `{}`. + if (value && typeof value === 'object' && Object.getPrototypeOf(value) === Object.prototype) { return Object.fromEntries( - Object.entries(value).map(([key, item]) => [ - key, - resolveStaticLanguageKeys(item, translate), - ]), + Object.entries(value).map(([key, item]) => [key, resolveStaticLanguageKeys(item, translate)]), ) } return value @@ -38,6 +43,8 @@ interface VisualCanvasProps { interactive?: boolean onSelect?: (id: string) => void onDropComponent?: (definitionName: string, parentId: string | null, slot?: string) => void + /** Moves an existing node into a container/table cell, or to the root. */ + onMoveIntoContainer?: (nodeId: string, parentId: string | null, slot?: string) => void onMove?: (id: string, direction: -1 | 1) => void onReorder?: (sourceId: string, targetId: string, placement: 'before' | 'after') => void onDuplicate?: (id: string) => void @@ -149,6 +156,30 @@ const toSelectOptions = ( }) } +/** + * Select renders its menu inline, so any ancestor with `overflow: hidden/auto` + * (the canvas page, a Grid/Table scroll wrapper) clips it — the menu of a Select + * in the last row would be cut off. Rendering it in a body portal keeps it + * visible; explicit props still win. + */ +const getSelectMenuProps = (props: Record) => { + const menuProps: Record = {} + if (props.menuPosition === undefined) menuProps.menuPosition = 'fixed' + if (props.menuPortalTarget === undefined && typeof window !== 'undefined') { + menuProps.menuPortalTarget = window.document.body + } + if (props.styles === undefined) { + menuProps.styles = { + menuPortal: (base: Record) => ({ ...base, zIndex: 60 }), + } + } + return menuProps +} + +/** Resolved tab list of a Tabs node, from static items or an endpoint binding. */ +const getTabOptions = (props: Record) => + Array.isArray(props.items) ? (props.items as Array>) : [] + const getPreviewProps = ( node: DesignerNode, dataValues: Record, @@ -184,12 +215,16 @@ const getPreviewProps = ( props[propertyName] = getBindingValue(binding, dataValues, currentItem) } }) - if ( - ['Select', 'AutoComplete', 'Menu', 'Dropdown', 'Pagination', 'Radio.Group'].includes(node.type) - ) { - const collectionProperty = ['Menu', 'Dropdown', 'Pagination', 'Radio.Group'].includes(node.type) - ? 'items' - : 'options' + // Stored as ISO strings; pickers only accept real Date instances. + if (isDesignerDateComponent(node.type)) { + Object.keys(props).forEach((propertyName) => { + if (isDesignerDateProperty(node.type, propertyName)) { + props[propertyName] = toDesignerDate(props[propertyName]) + } + }) + } + if (isDesignerOptionComponent(node.type)) { + const collectionProperty = getDesignerCollectionProperty(node.type) if (node.type === 'Select') { const legacyAliases: Record = { clearable: 'isClearable', @@ -203,6 +238,7 @@ const getPreviewProps = ( } delete props[legacyName] }) + Object.assign(props, getSelectMenuProps(props)) } const optionsBinding = node.bindings?.[collectionProperty] props[collectionProperty] = toSelectOptions( @@ -226,13 +262,11 @@ const getPreviewProps = ( } } - const chainHandler = ( - eventName: string, - update: (...args: unknown[]) => void, - ) => { - const storedHandler = typeof props[eventName] === 'function' - ? (props[eventName] as (...args: unknown[]) => void) - : undefined + 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) @@ -266,6 +300,8 @@ const getPreviewProps = ( chainHandler('onSelect', (value) => updateProp('defaultActiveKeys', [String(value ?? '')])) } else if (node.type === 'Dropdown') { chainHandler('onSelect', (value) => updateProp('activeKey', String(value ?? ''))) + } else if (node.type === 'Tabs') { + chainHandler('onChange', (value) => updateProp('value', String(value ?? ''))) } else if (node.type === 'Radio.Group') { chainHandler('onChange', (value) => updateProp('value', value)) } else if (node.type === 'Pagination') { @@ -280,16 +316,18 @@ const getPreviewProps = ( ) } else if (node.type === 'MenuItem') { chainHandler('onSelect', () => updateProp('isActive', true)) + } else if (isDesignerDateComponent(node.type)) { + // A Date is not JSON serialisable, so the picked value is kept as an ISO + // string — otherwise it is lost on the next render/save. + chainHandler('onChange', (value) => updateProp('value', fromDesignerDate(value))) } else if ('checked' in props) { chainHandler('onChange', (value, originalEvent) => { const checked = originalEvent && typeof originalEvent === 'object' && 'target' in originalEvent - ? Boolean( - (originalEvent as { target?: { checked?: unknown } }).target?.checked, - ) + ? Boolean((originalEvent as { target?: { checked?: unknown } }).target?.checked) : value && typeof value === 'object' && 'target' in value - ? Boolean((value as { target?: { checked?: unknown } }).target?.checked) - : Boolean(value) + ? Boolean((value as { target?: { checked?: unknown } }).target?.checked) + : Boolean(value) updateProp('checked', checked) }) } else if ('value' in props) { @@ -541,7 +579,8 @@ const renderElement = ( React.Children.count(children) > 0 ? children : boundChildren === null || boundChildren === undefined - ? (resolveStaticLanguageKeys(node.props.children, translate) as React.ReactNode) || undefined + ? (resolveStaticLanguageKeys(node.props.children, translate) as React.ReactNode) || + undefined : React.isValidElement(boundChildren) ? boundChildren : typeof boundChildren === 'object' @@ -581,6 +620,13 @@ const renderElement = ( ? (dropdownProps.items as Array>) : [] delete dropdownProps.items + // Without this the toggle keeps showing the static title and the component + // looks untouched after a selection. + dropdownProps.title = resolveDesignerDropdownTitle( + options, + dropdownProps.activeKey, + dropdownProps.title, + ) return ( )}> {options.map((option, index) => ( @@ -596,6 +642,8 @@ const renderElement = ( ) } + // Built in NodeView so each tab can own its own children and drop target. + if (node.type === 'Tabs') return <>{children} if (node.type === 'Radio.Group') { const groupProps = { ...props } const options = Array.isArray(groupProps.items) @@ -639,10 +687,13 @@ const renderElement = ( const NodeView = ({ node, index, + siblingCount = 1, + isRoot = false, selectedId, interactive, onSelect, onDropComponent, + onMoveIntoContainer, onMove, onReorder, onDuplicate, @@ -654,10 +705,13 @@ const NodeView = ({ }: { node: DesignerNode index: number + siblingCount?: number + isRoot?: boolean selectedId: string | null interactive: boolean onSelect?: (id: string) => void onDropComponent?: (definitionName: string, parentId: string | null, slot?: string) => void + onMoveIntoContainer?: (nodeId: string, parentId: string | null, slot?: string) => void onMove?: (id: string, direction: -1 | 1) => void onReorder?: (sourceId: string, targetId: string, placement: 'before' | 'after') => void onDuplicate?: (id: string) => void @@ -669,6 +723,10 @@ const NodeView = ({ }) => { const { translate } = useLocalization() const selected = interactive && selectedId === node.id + // A nested node can always move: at the edge of its container it is lifted out. + // Only the first/last node at the root has nowhere left to go. + const canMoveUp = !isRoot || index > 0 + const canMoveDown = !isRoot || index < siblingCount - 1 const acceptsDroppedChildren = [ 'PageContainer', 'TwoColumns', @@ -678,6 +736,8 @@ const NodeView = ({ 'div', 'Card', 'FormContainer', + // Dropped components land in whichever tab is open. + 'Tabs', ].includes(node.type) const staticChildren = node.props.children const hasStaticChildren = @@ -685,8 +745,7 @@ const NodeView = ({ staticChildren !== null && (typeof staticChildren !== 'string' || staticChildren.length > 0) const hasChildrenBinding = Boolean(node.bindings?.children?.sourceId) - const hasVisibleChildren = - node.children.length > 0 || hasStaticChildren || hasChildrenBinding + const hasVisibleChildren = node.children.length > 0 || hasStaticChildren || hasChildrenBinding const itemsBinding = node.type === 'Grid' ? node.bindings?.items : undefined const boundItems = itemsBinding?.sourceId ? getBindingValue(itemsBinding, dataValues, currentItem) @@ -710,6 +769,7 @@ const NodeView = ({ key={`${child.id}_${itemIndex}`} node={child} index={childIndex} + siblingCount={node.children.length} selectedId={selectedId} interactive={interactive} renderCustomComponent={renderCustomComponent} @@ -717,6 +777,7 @@ const NodeView = ({ currentItem={childItem} onSelect={onSelect} onDropComponent={onDropComponent} + onMoveIntoContainer={onMoveIntoContainer} onMove={onMove} onReorder={onReorder} onDuplicate={onDuplicate} @@ -725,6 +786,99 @@ const NodeView = ({ /> )), ) + const tabsContent = (() => { + if (node.type !== 'Tabs') return null + const tabsProps = getPreviewProps(node, dataValues, currentItem, onNodePropChange, translate) + const options = getTabOptions(tabsProps) + delete tabsProps.items + const activeValue = resolveDesignerTabValue(options, tabsProps.value) + tabsProps.value = activeValue + // Children created before per-tab slots existed belong to the first tab. + const firstSlot = options.length ? getDesignerTabSlot(String(options[0]?.value ?? 0)) : '' + + return ( + )}> + + {options.map((option, index) => ( + + {String(option.label ?? option.value ?? `Sekme ${index + 1}`)} + + ))} + + {options.map((option, index) => { + const tabValue = String(option.value ?? index) + const slot = getDesignerTabSlot(tabValue) + const tabNodes = node.children.filter((child) => (child.slot || firstSlot) === slot) + return ( + +
{ + if (!interactive) return + event.preventDefault() + event.stopPropagation() + event.dataTransfer.dropEffect = + event.dataTransfer.effectAllowed === 'copy' ? 'copy' : 'move' + }} + 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) + } else if (payload.source === 'canvas' && payload.nodeId) { + onMoveIntoContainer?.(payload.nodeId, node.id, slot) + } + } catch { + // Ignore payloads that do not belong to the visual designer. + } + }} + > + {option.content ?
{String(option.content)}
: null} + {tabNodes.map((child, childIndex) => ( + + ))} + {interactive && !tabNodes.length && !option.content && ( +
+ Bu sekme için komponent bırakın +
+ )} +
+
+ ) + })} +
+ ) + })() + const tableContent = node.type === 'Table' ? ( { if (!interactive) return @@ -779,6 +936,10 @@ const NodeView = ({ const payload = JSON.parse(raw) if (payload.source === 'library') { onDropComponent?.(payload.name, node.id, slot) + } else if (payload.source === 'canvas' && payload.nodeId) { + // Lets a component be dragged back into a cell, from + // another cell or from anywhere else on the canvas. + onMoveIntoContainer?.(payload.nodeId, node.id, slot) } } catch { // Ignore payloads that do not belong to the visual designer. @@ -791,6 +952,7 @@ const NodeView = ({ key={child.id} node={child} index={childIndex} + siblingCount={node.children.length} selectedId={selectedId} interactive={interactive} renderCustomComponent={renderCustomComponent} @@ -798,6 +960,7 @@ const NodeView = ({ currentItem={currentItem} onSelect={onSelect} onDropComponent={onDropComponent} + onMoveIntoContainer={onMoveIntoContainer} onMove={onMove} onReorder={onReorder} onDuplicate={onDuplicate} @@ -821,65 +984,12 @@ const NodeView = ({ ) : null const renderedChildren = - node.type === 'Table' - ? tableContent - : node.type === 'Grid' && interactive && node.children.length === 0 - ? repeatedItems.length - ? [ - typeof column === 'string', - ) - : undefined - } - />, - ] - : [ - typeof column === 'string', - ) - : [] - } - />, - ] - : node.type === 'Grid' && - !interactive && - itemsBinding?.sourceId && - effectiveBoundItems === undefined - ? [ -
- Veriler yükleniyor… -
, - ] - : node.type === 'Grid' && - itemsBinding?.sourceId && - effectiveBoundItems !== undefined && - !Array.isArray(effectiveBoundItems) - ? [ -
- Grid items bağlantısı bir koleksiyon döndürmelidir. Seçili path:{' '} - {itemsBinding.path || '(root)'} -
, - ] - : node.type === 'Grid' && node.children.length === 0 && repeatedItems.length + node.type === 'Tabs' + ? tabsContent + : node.type === 'Table' + ? tableContent + : node.type === 'Grid' && interactive && node.children.length === 0 + ? repeatedItems.length ? [ , ] - : children + : [ + typeof column === 'string', + ) + : [] + } + />, + ] + : node.type === 'Grid' && + !interactive && + itemsBinding?.sourceId && + effectiveBoundItems === undefined + ? [ +
+ Veriler yükleniyor… +
, + ] + : node.type === 'Grid' && + itemsBinding?.sourceId && + effectiveBoundItems !== undefined && + !Array.isArray(effectiveBoundItems) + ? [ +
+ Grid items bağlantısı bir koleksiyon döndürmelidir. Seçili path:{' '} + {itemsBinding.path || '(root)'} +
, + ] + : node.type === 'Grid' && node.children.length === 0 && repeatedItems.length + ? [ + typeof column === 'string', + ) + : undefined + } + />, + ] + : children const contentChildren = interactive && node.type === 'Card' && node.children.length === 0 ? [ @@ -941,15 +1106,17 @@ const NodeView = ({ if (!interactive) return event.preventDefault() event.stopPropagation() - event.dataTransfer.dropEffect = 'move' + // Must match the source's effectAllowed, otherwise the browser rejects + // the drop and never fires onDrop. + event.dataTransfer.dropEffect = + event.dataTransfer.effectAllowed === 'copy' ? 'copy' : 'move' }} onDrop={(event) => { if (!interactive) return event.preventDefault() event.stopPropagation() const raw = - event.dataTransfer.getData(DESIGNER_DRAG_TYPE) || - event.dataTransfer.getData('text/plain') + event.dataTransfer.getData(DESIGNER_DRAG_TYPE) || event.dataTransfer.getData('text/plain') if (!raw) return const payload = JSON.parse(raw) if (payload.source === 'canvas' && payload.nodeId !== node.id) { @@ -969,8 +1136,9 @@ const NodeView = ({ {node.type}

- {(() => { - try { - const parsed = JSON.parse(component.dependencies ?? '[]') - return Array.isArray(parsed) && parsed.length > 0 - ? `${parsed.join(', ')}` - : translate('::App.DeveloperKit.Component.NoDependencies') - } catch { - return translate('::App.DeveloperKit.Component.NoDependencies') - } - })()} + {parseComponentDependencies(component.dependencies).join(', ') || + translate('::App.DeveloperKit.Component.NoDependencies')}

@@ -221,6 +220,15 @@ const ComponentManager: React.FC = () => { {component.description}

)} + + {componentErrors[component.name] && ( +

+ {componentErrors[component.name]} +

+ )}
{/* Sağ taraf */} @@ -333,8 +341,8 @@ const ComponentManager: React.FC = () => { {!searchTerm && filterActive === 'all' && ( + + +
+
+ {item.src ? ( + + ) : null} + {schema.fields.map((field) => ( + + ))} + + ))} + + + ) +} + const PropertyEditor = ({ + componentType, name, type, value, options, onChange, }: { + componentType?: string name: string type: string value: unknown @@ -747,12 +957,16 @@ const PropertyEditor = ({ ) } if (type === 'select' && options) { + const currentValue = String(value ?? '') return ( ) } + const objectListSchema = OBJECT_LIST_PROPERTIES[`${componentType}.${name}`] + if (objectListSchema) { + return + } if (type === 'object' || type === 'array') { return (