Custom component güncellemeleri

This commit is contained in:
Sedat ÖZTÜRK 2026-08-06 16:17:59 +03:00
parent 9a76afae24
commit 24eb249f9f
20 changed files with 644 additions and 438 deletions

View file

@ -14040,6 +14040,12 @@
"en": "Open URL",
"tr": "URL'yi Aç"
},
{
"resourceName": "Platform",
"key": "Reset",
"en": "Reset",
"tr": "Sıfırla"
},
{
"resourceName": "Platform",
"key": "App.Listform.ListformField.ApplicationName",

View file

@ -376,22 +376,6 @@
"routeType": "protected",
"authority": ["App.DeveloperKit.Components"]
},
{
"key": "admin.developerkit.components.new",
"path": "/admin/developerkit/components/new",
"componentType": "normal",
"componentPath": "@/views/developerKit/ComponentEditorPage",
"routeType": "protected",
"authority": ["App.DeveloperKit.Components"]
},
{
"key": "admin.developerkit.components.view",
"path": "/admin/developerkit/components/view/:id",
"componentType": "normal",
"componentPath": "@/views/developerKit/ComponentEditorPage",
"routeType": "protected",
"authority": ["App.DeveloperKit.Components"]
},
{
"key": "admin.developerkit.components.edit",
"path": "/admin/developerkit/components/edit/:id",

View file

@ -0,0 +1,100 @@
import { useMemo, useState } from 'react'
import { FaPlus, FaSearch, FaTimes } from 'react-icons/fa'
import { Button } from '@/components/ui'
interface StyleModalProps {
isOpen: boolean
mode: 'class' | 'css'
propertyName: string
value?: string
onChange: (value: string) => void
onClose: () => void
}
const STYLE_GROUPS = {
Display: ['block', 'inline-block', 'inline', 'flex', 'inline-flex', 'grid', 'inline-grid', 'contents', 'hidden'],
Layout: ['container', 'box-border', 'box-content', 'overflow-auto', 'overflow-hidden', 'overflow-visible', 'overflow-scroll', 'overflow-x-auto', 'overflow-y-auto'],
Position: ['static', 'fixed', 'absolute', 'relative', 'sticky', 'inset-0', 'top-0', 'right-0', 'bottom-0', 'left-0', 'z-0', 'z-10', 'z-20', 'z-30', 'z-40', 'z-50'],
Flexbox: ['flex-row', 'flex-row-reverse', 'flex-col', 'flex-col-reverse', 'flex-wrap', 'flex-nowrap', 'grow', 'grow-0', 'shrink', 'shrink-0', 'items-start', 'items-center', 'items-end', 'items-stretch', 'justify-start', 'justify-center', 'justify-end', 'justify-between', 'justify-around', 'justify-evenly', 'self-auto', 'self-start', 'self-center', 'self-end', 'self-stretch'],
Grid: ['grid-cols-1', 'grid-cols-2', 'grid-cols-3', 'grid-cols-4', 'grid-cols-5', 'grid-cols-6', 'grid-cols-12', 'col-auto', 'col-span-1', 'col-span-2', 'col-span-3', 'col-span-4', 'col-span-6', 'col-span-12', 'grid-rows-1', 'grid-rows-2', 'grid-rows-3', 'row-span-1', 'row-span-2', 'row-span-3'],
Spacing: ['m-0', 'm-1', 'm-2', 'm-3', 'm-4', 'm-5', 'm-6', 'm-8', 'm-auto', 'mx-auto', 'my-auto', 'mt-1', 'mt-2', 'mt-3', 'mt-4', 'mr-1', 'mr-2', 'mr-4', 'mb-1', 'mb-2', 'mb-4', 'ml-1', 'ml-2', 'ml-4', 'p-0', 'p-1', 'p-2', 'p-3', 'p-4', 'p-5', 'p-6', 'p-8', 'px-1', 'px-2', 'px-3', 'px-4', 'px-6', 'py-1', 'py-2', 'py-3', 'py-4', 'py-6', 'gap-0', 'gap-1', 'gap-2', 'gap-3', 'gap-4', 'gap-6', 'gap-8'],
Sizing: ['w-0', 'w-auto', 'w-full', 'w-screen', 'w-1/2', 'w-1/3', 'w-2/3', 'w-1/4', 'w-3/4', 'h-0', 'h-auto', 'h-full', 'h-screen', 'min-w-0', 'min-w-full', 'min-h-0', 'min-h-full', 'max-w-xs', 'max-w-sm', 'max-w-md', 'max-w-lg', 'max-w-xl', 'max-w-2xl', 'max-w-full', 'max-h-full', 'aspect-square', 'aspect-video'],
Typography: ['text-xs', 'text-sm', 'text-base', 'text-lg', 'text-xl', 'text-2xl', 'text-3xl', 'font-thin', 'font-light', 'font-normal', 'font-medium', 'font-semibold', 'font-bold', 'italic', 'not-italic', 'underline', 'line-through', 'no-underline', 'text-left', 'text-center', 'text-right', 'uppercase', 'lowercase', 'capitalize', 'truncate', 'whitespace-normal', 'whitespace-nowrap', 'break-words', 'leading-none', 'leading-tight', 'leading-normal', 'tracking-tight', 'tracking-normal', 'tracking-wide'],
Colors: ['text-transparent', 'text-white', 'text-black', 'text-slate-400', 'text-slate-500', 'text-slate-600', 'text-slate-700', 'text-slate-800', 'text-slate-900', 'text-sky-500', 'text-blue-600', 'text-red-500', 'text-amber-500', 'text-emerald-500', 'bg-transparent', 'bg-white', 'bg-black', 'bg-slate-50', 'bg-slate-100', 'bg-slate-200', 'bg-slate-700', 'bg-slate-800', 'bg-slate-900', 'bg-sky-500', 'bg-blue-600', 'bg-red-500', 'bg-amber-500', 'bg-emerald-500'],
Border: ['border', 'border-0', 'border-2', 'border-t', 'border-r', 'border-b', 'border-l', 'border-transparent', 'border-slate-200', 'border-slate-300', 'border-slate-700', 'border-sky-500', 'rounded-none', 'rounded-sm', 'rounded', 'rounded-md', 'rounded-lg', 'rounded-xl', 'rounded-2xl', 'rounded-full', 'divide-x', 'divide-y'],
Effects: ['shadow-none', 'shadow-sm', 'shadow', 'shadow-md', 'shadow-lg', 'shadow-xl', 'opacity-0', 'opacity-50', 'opacity-75', 'opacity-100', 'blur-sm', 'blur', 'backdrop-blur-sm', 'backdrop-blur', 'transition', 'transition-all', 'duration-150', 'duration-300', 'ease-in', 'ease-out', 'ease-in-out'],
Interaction: ['cursor-auto', 'cursor-default', 'cursor-pointer', 'cursor-not-allowed', 'select-none', 'select-text', 'pointer-events-none', 'pointer-events-auto', 'resize', 'resize-none', 'outline-none', 'focus:outline-none', 'hover:opacity-80', 'hover:bg-slate-100', 'focus:ring-2', 'focus:ring-sky-500'],
Responsive: ['sm:block', 'sm:flex', 'sm:grid', 'sm:hidden', 'md:block', 'md:flex', 'md:grid', 'md:hidden', 'lg:block', 'lg:flex', 'lg:grid', 'lg:hidden', 'xl:block', 'xl:flex', 'xl:grid', 'dark:bg-slate-900', 'dark:bg-slate-950', 'dark:text-white', 'dark:text-slate-300', 'dark:border-slate-700'],
} as const
const splitClasses = (value: string) => value.split(/\s+/).filter(Boolean)
const StyleModal = ({ isOpen, mode, propertyName, value = '', onChange, onClose }: StyleModalProps) => {
const [search, setSearch] = useState('')
const [customClass, setCustomClass] = useState('')
const [category, setCategory] = useState<keyof typeof STYLE_GROUPS | 'Tümü'>('Tümü')
const selected = useMemo(() => new Set(splitClasses(value)), [value])
const classes = useMemo(() => {
const source = category === 'Tümü' ? Object.values(STYLE_GROUPS).flat() : STYLE_GROUPS[category]
const query = search.trim().toLowerCase()
return [...new Set(source)].filter((name) => !query || name.toLowerCase().includes(query))
}, [category, search])
if (!isOpen) return null
const toggleClass = (className: string) => {
const next = new Set(selected)
if (next.has(className)) next.delete(className)
else next.add(className)
onChange([...next].join(' '))
}
const addCustomClass = () => {
const additions = splitClasses(customClass)
if (!additions.length) return
onChange([...new Set([...selected, ...additions])].join(' '))
setCustomClass('')
}
return (
<div className="fixed inset-0 z-[100] flex items-center justify-center bg-black/60 p-4" onMouseDown={onClose}>
<div className="flex max-h-[90vh] w-full max-w-5xl flex-col overflow-hidden rounded-xl bg-white shadow-2xl dark:bg-slate-900" onMouseDown={(event) => event.stopPropagation()}>
<div className="flex items-center justify-between border-b border-slate-200 p-4 dark:border-slate-700">
<div>
<h2 className="font-semibold text-slate-900 dark:text-white">StyleModal · {propertyName}</h2>
<p className="mt-1 text-xs text-slate-500">{mode === 'css' ? 'CSS kodunu doğrudan düzenleyin.' : 'Hazır sınıfları seçin veya CSS/Tailwind class adlarını elle düzenleyin.'}</p>
</div>
<Button icon={<FaTimes />} size="xs" variant="plain" onClick={onClose} />
</div>
{mode === 'class' && <div className="grid gap-3 border-b border-slate-200 p-4 md:grid-cols-[1fr_190px] dark:border-slate-700">
<label className="relative">
<FaSearch className="absolute left-3 top-3 text-slate-400" />
<input className="w-full rounded-md border border-slate-300 bg-white py-2 pl-9 pr-3 text-sm dark:border-slate-700 dark:bg-slate-950 dark:text-white" placeholder="Hazır sınıflarda ara…" value={search} onChange={(event) => setSearch(event.target.value)} />
</label>
<select className="rounded-md border border-slate-300 bg-white px-3 text-sm dark:border-slate-700 dark:bg-slate-950 dark:text-white" value={category} onChange={(event) => setCategory(event.target.value as typeof category)}>
<option value="Tümü">Tüm kategoriler</option>
{Object.keys(STYLE_GROUPS).map((name) => <option key={name} value={name}>{name}</option>)}
</select>
</div>}
{mode === 'class' && <div className="flex gap-2 border-b border-slate-200 p-4 dark:border-slate-700">
<input className="min-w-0 flex-1 rounded-md border border-slate-300 bg-white px-3 py-2 font-mono text-xs dark:border-slate-700 dark:bg-slate-950 dark:text-white" placeholder="Örn. hover:scale-105 w-[320px] [&>span]:font-bold" value={customClass} onChange={(event) => setCustomClass(event.target.value)} onKeyDown={(event) => { if (event.key === 'Enter') addCustomClass() }} />
<Button icon={<FaPlus />} size="sm" variant="default" onClick={addCustomClass}>Ekle</Button>
</div>}
<div className="border-b border-slate-200 bg-slate-50 p-4 dark:border-slate-700 dark:bg-slate-950">
<label className="mb-1.5 block text-xs font-semibold text-slate-600 dark:text-slate-300">{propertyName} değerini elle düzenle</label>
<textarea className={`w-full rounded-md border border-slate-300 bg-white p-3 font-mono text-xs text-slate-800 outline-none focus:border-sky-500 dark:border-slate-700 dark:bg-slate-900 dark:text-white ${mode === 'css' ? 'min-h-48' : 'min-h-20'}`} placeholder={mode === 'css' ? '.my-class { color: red; }' : 'my-card selected-state md:wide-card'} value={value} onChange={(event) => onChange(event.target.value)} />
</div>
{mode === 'class' && <div className="grid flex-1 grid-cols-2 gap-2 overflow-y-auto p-4 sm:grid-cols-3 lg:grid-cols-5">
{classes.map((className) => (
<button key={className} className={`rounded-md border px-3 py-2 text-left font-mono text-xs transition ${selected.has(className) ? 'border-sky-500 bg-sky-50 text-sky-700 dark:bg-sky-950 dark:text-sky-300' : 'border-slate-200 text-slate-600 hover:border-sky-300 dark:border-slate-700 dark:text-slate-300'}`} type="button" onClick={() => toggleClass(className)}>{className}</button>
))}
</div>}
<div className="flex items-center justify-between border-t border-slate-200 p-4 dark:border-slate-700">
<span className="text-xs text-slate-500">{mode === 'css' ? 'Ham CSS düzenleme modu' : `${selected.size} seçili · ${classes.length} görüntüleniyor`}</span>
<div className="flex gap-2"><Button size="sm" variant="default" onClick={() => onChange('')}>Temizle</Button><Button size="sm" variant="solid" onClick={onClose}>Tamam</Button></div>
</div>
</div>
</div>
)
}
export default StyleModal

View file

@ -1,79 +0,0 @@
import React, { useMemo, useState } from 'react'
import { FaSearch, FaTimes } from 'react-icons/fa'
import { Button } from '@/components/ui'
interface TailwindModalProps {
isOpen: boolean
currentValue?: string
onClose: () => void
onSelectClass: (className: string) => void
}
const CLASS_GROUPS = {
Layout: ['block', 'inline-block', 'flex', 'inline-flex', 'grid', 'hidden', 'relative', 'absolute', 'fixed', 'sticky', 'overflow-auto', 'overflow-hidden'],
Flex: ['flex-row', 'flex-col', 'flex-wrap', 'items-start', 'items-center', 'items-end', 'items-stretch', 'justify-start', 'justify-center', 'justify-between', 'justify-end', 'gap-1', 'gap-2', 'gap-3', 'gap-4', 'gap-6', 'gap-8'],
Grid: ['grid-cols-1', 'grid-cols-2', 'grid-cols-3', 'grid-cols-4', 'grid-cols-6', 'grid-cols-12', 'col-span-1', 'col-span-2', 'col-span-3', 'col-span-6', 'col-span-12'],
Spacing: ['m-0', 'm-1', 'm-2', 'm-3', 'm-4', 'm-auto', 'mx-auto', 'my-2', 'my-4', 'p-0', 'p-1', 'p-2', 'p-3', 'p-4', 'p-6', 'p-8', 'px-2', 'px-3', 'px-4', 'px-6', 'py-1', 'py-2', 'py-3', 'py-4'],
Sizing: ['w-auto', 'w-full', 'w-1/2', 'w-1/3', 'w-2/3', 'h-auto', 'h-full', 'min-w-0', 'min-h-0', 'max-w-sm', 'max-w-md', 'max-w-lg', 'max-w-xl', 'max-w-full'],
Typography: ['text-xs', 'text-sm', 'text-base', 'text-lg', 'text-xl', 'text-2xl', 'font-normal', 'font-medium', 'font-semibold', 'font-bold', 'text-left', 'text-center', 'text-right', 'truncate', 'whitespace-nowrap'],
Colors: ['text-white', 'text-black', 'text-slate-500', 'text-slate-700', 'text-slate-900', 'bg-white', 'bg-slate-50', 'bg-slate-100', 'bg-slate-800', 'bg-slate-900', 'bg-sky-500', 'bg-blue-600', 'bg-red-500', 'bg-emerald-500'],
Border: ['border', 'border-0', 'border-2', 'border-slate-200', 'border-slate-700', 'rounded', 'rounded-md', 'rounded-lg', 'rounded-xl', 'rounded-full', 'shadow-sm', 'shadow', 'shadow-lg'],
State: ['cursor-pointer', 'select-none', 'transition', 'hover:opacity-80', 'hover:bg-slate-100', 'dark:bg-slate-900', 'dark:text-white', 'dark:border-slate-700'],
} as const
const TailwindModal: React.FC<TailwindModalProps> = ({
isOpen,
currentValue = '',
onClose,
onSelectClass,
}) => {
const [search, setSearch] = useState('')
const [category, setCategory] = useState<keyof typeof CLASS_GROUPS | 'Tümü'>('Tümü')
const selectedClasses = useMemo(() => new Set(currentValue.split(/\s+/).filter(Boolean)), [currentValue])
const classes = useMemo(() => {
const source = category === 'Tümü' ? Object.values(CLASS_GROUPS).flat() : CLASS_GROUPS[category]
const query = search.trim().toLowerCase()
return [...new Set(source)].filter((className) => !query || className.toLowerCase().includes(query))
}, [category, search])
if (!isOpen) return null
return (
<div className="fixed inset-0 z-[100] flex items-center justify-center bg-black/60 p-4" onMouseDown={onClose}>
<div className="flex max-h-[85vh] w-full max-w-4xl flex-col overflow-hidden rounded-xl bg-white shadow-2xl dark:bg-slate-900" onMouseDown={(event) => event.stopPropagation()}>
<div className="flex items-center justify-between border-b border-slate-200 p-4 dark:border-slate-700">
<div>
<h2 className="font-semibold text-slate-900 dark:text-white">Tailwind CSS sınıfları</h2>
<p className="mt-1 text-xs text-slate-500">Birden fazla sınıf seçebilirsiniz.</p>
</div>
<Button icon={<FaTimes />} size="xs" variant="plain" onClick={onClose} />
</div>
<div className="flex gap-3 border-b border-slate-200 p-4 dark:border-slate-700">
<label className="relative flex-1">
<FaSearch className="absolute left-3 top-3 text-slate-400" />
<input className="w-full rounded-md border border-slate-300 bg-white py-2 pl-9 pr-3 text-sm dark:border-slate-700 dark:bg-slate-950 dark:text-white" placeholder="Sınıf ara…" value={search} onChange={(event) => setSearch(event.target.value)} />
</label>
<select className="rounded-md border border-slate-300 bg-white px-3 text-sm dark:border-slate-700 dark:bg-slate-950 dark:text-white" value={category} onChange={(event) => setCategory(event.target.value as typeof category)}>
<option value="Tümü">Tüm kategoriler</option>
{Object.keys(CLASS_GROUPS).map((name) => <option key={name} value={name}>{name}</option>)}
</select>
</div>
<div className="border-b border-slate-200 bg-slate-50 px-4 py-3 text-xs dark:border-slate-700 dark:bg-slate-950">
<span className="font-semibold text-slate-600 dark:text-slate-300">className: </span>
<code className="break-all text-sky-600 dark:text-sky-400">{currentValue || '—'}</code>
</div>
<div className="grid flex-1 grid-cols-2 gap-2 overflow-y-auto p-4 sm:grid-cols-3 lg:grid-cols-4">
{classes.map((className) => (
<button key={className} className={`rounded-md border px-3 py-2 text-left font-mono text-xs transition ${selectedClasses.has(className) ? 'border-sky-500 bg-sky-50 text-sky-700 dark:bg-sky-950 dark:text-sky-300' : 'border-slate-200 text-slate-600 hover:border-sky-300 dark:border-slate-700 dark:text-slate-300'}`} type="button" onClick={() => onSelectClass(className)}>{className}</button>
))}
</div>
<div className="flex items-center justify-between border-t border-slate-200 p-4 dark:border-slate-700">
<span className="text-xs text-slate-500">{classes.length} sınıf</span>
<div className="flex gap-2"><Button size="sm" variant="default" onClick={() => onSelectClass('')}>Temizle</Button><Button size="sm" variant="solid" onClick={onClose}>Tamam</Button></div>
</div>
</div>
</div>
)
}
export default TailwindModal

View file

@ -68,13 +68,14 @@ const PlatformViewHost = ({ view, listFormCode, height = '520px' }: PlatformView
if (error) return <div className="rounded-lg bg-red-50 p-4 text-sm text-red-700">{error}</div>
if (!gridDto) return <Loading loading />
const sharedProps = { listFormCode, isSubForm: false, gridDto }
const sharedProps = { listFormCode, isSubForm: true, gridDto }
const content = (() => {
switch (view) {
case 'List':
return (
<List
gridDto={gridDto}
isSubForm
listFormCode={listFormCode}
onRefreshGridDto={refreshGridDto}
/>

View file

@ -4,6 +4,46 @@ import { useConfig } from '../ConfigProvider'
import type { CommonProps } from '../@types/common'
import type { ReactNode, ComponentPropsWithRef, MouseEvent } from 'react'
const CARD_DOM_PROP_NAMES = new Set([
'accessKey',
'autoCapitalize',
'autoFocus',
'contentEditable',
'dangerouslySetInnerHTML',
'dir',
'draggable',
'hidden',
'id',
'inputMode',
'itemID',
'itemProp',
'itemRef',
'itemScope',
'itemType',
'lang',
'nonce',
'role',
'slot',
'spellCheck',
'style',
'suppressContentEditableWarning',
'suppressHydrationWarning',
'tabIndex',
'title',
'translate',
])
const getCardDomProps = (props: Record<string, unknown>) =>
Object.fromEntries(
Object.entries(props).filter(
([name, value]) =>
CARD_DOM_PROP_NAMES.has(name) ||
name.startsWith('aria-') ||
name.startsWith('data-') ||
(name.startsWith('on') && typeof value === 'function')
)
) as Omit<ComponentPropsWithRef<'div'>, 'onClick'>
export interface CardProps
extends CommonProps,
Omit<ComponentPropsWithRef<'div'>, 'onClick'> {
@ -40,6 +80,10 @@ const Card = forwardRef<HTMLDivElement, CardProps>((props, ref) => {
...rest
} = props
// Custom components can spread runtime page/form props onto their root Card.
// Forward only attributes React accepts on a div.
const domProps = getCardDomProps(rest as Record<string, unknown>)
const cardClass = classNames(
'card',
className,
@ -77,7 +121,7 @@ const Card = forwardRef<HTMLDivElement, CardProps>((props, ref) => {
className={cardClass}
role="presentation"
onClick={handleClick}
{...rest}
{...domProps}
>
{header && (
<div className={cardHeaderClass}>

View file

@ -3,6 +3,7 @@ import * as UiKit from '@/components/ui'
import PlatformViewHost, {
type PlatformViewName,
} from '@/components/componentEditor/PlatformViewHost'
import { useLocalization } from '@/utils/hooks/useLocalization'
import { FaArrowDown, FaArrowUp, FaClone, FaGripVertical, FaTrash } from 'react-icons/fa'
import {
getDesignerValueByPath,
@ -14,15 +15,31 @@ 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 => {
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') {
return Object.fromEntries(
Object.entries(value).map(([key, item]) => [
key,
resolveStaticLanguageKeys(item, translate),
]),
)
}
return value
}
interface VisualCanvasProps {
nodes: DesignerNode[]
selectedId: string | null
background: string
interactive?: boolean
onSelect?: (id: string) => void
onDropComponent?: (definitionName: 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
onDelete?: (id: string) => void
onNodePropChange?: (id: string, propertyName: string, value: unknown) => void
@ -137,12 +154,13 @@ const getPreviewProps = (
dataValues: Record<string, unknown>,
currentItem?: unknown,
onNodePropChange?: (id: string, propertyName: string, value: unknown) => void,
translate: (key: string) => string = (key) => key,
) => {
const props: Record<string, unknown> = {}
Object.entries(node.props).forEach(([key, value]) => {
if (key === 'children' || key === 'html' || value === '') return
if (key === 'children' || key === 'html' || (value === '' && key !== 'value')) return
if (key.startsWith('on') && typeof value === 'string') return
props[key] = value
props[key] = resolveStaticLanguageKeys(value, translate)
})
Object.entries(node.events).forEach(([name, script]) => {
if (!script.trim()) return
@ -167,9 +185,9 @@ const getPreviewProps = (
}
})
if (
['Select', 'AutoComplete', 'Menu', 'Dropdown', 'Radio.Group'].includes(node.type)
['Select', 'AutoComplete', 'Menu', 'Dropdown', 'Pagination', 'Radio.Group'].includes(node.type)
) {
const collectionProperty = ['Menu', 'Dropdown', 'Radio.Group'].includes(node.type)
const collectionProperty = ['Menu', 'Dropdown', 'Pagination', 'Radio.Group'].includes(node.type)
? 'items'
: 'options'
if (node.type === 'Select') {
@ -192,6 +210,11 @@ const getPreviewProps = (
optionsBinding?.labelPath,
optionsBinding?.valuePath,
)
if (node.type === 'Pagination') {
const pages = props.items as Array<Record<string, unknown>>
delete props.items
if (pages.length) props.total = pages.length
}
if (node.type === 'Select' && 'value' in props) {
const options = props.options as Array<Record<string, unknown>>
const selectedValue = props.value
@ -247,10 +270,24 @@ const getPreviewProps = (
chainHandler('onChange', (value) => updateProp('value', value))
} else if (node.type === 'Pagination') {
chainHandler('onChange', (value) => updateProp('currentPage', Number(value) || 1))
} else if (node.type === 'Steps') {
chainHandler('onChange', (value) => updateProp('current', Number(value) || 0))
} else if (node.type === 'ImageViewer') {
chainHandler('onIndexChange', (value) => updateProp('activeIndex', Number(value) || 0))
} else if (node.type === 'Upload') {
chainHandler('onChange', (_files, fileList) =>
updateProp('fileList', Array.isArray(fileList) ? fileList : []),
)
} else if (node.type === 'MenuItem') {
chainHandler('onSelect', () => updateProp('isActive', true))
} else if ('checked' in props) {
chainHandler('onChange', (value) => {
chainHandler('onChange', (value, originalEvent) => {
const checked =
value && typeof value === 'object' && 'target' in value
originalEvent && typeof originalEvent === 'object' && 'target' in originalEvent
? 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)
updateProp('checked', checked)
@ -399,6 +436,7 @@ const renderElement = (
dataValues: Record<string, unknown>,
currentItem: unknown,
interactive: boolean,
translate: (key: string) => string,
onNodePropChange?: (id: string, propertyName: string, value: unknown) => void,
renderCustomComponent?: (name: string, props?: Record<string, unknown>) => React.ReactNode,
) => {
@ -503,13 +541,13 @@ const renderElement = (
React.Children.count(children) > 0
? children
: boundChildren === null || boundChildren === undefined
? String(node.props.children ?? '') || undefined
? (resolveStaticLanguageKeys(node.props.children, translate) as React.ReactNode) || undefined
: React.isValidElement(boundChildren)
? boundChildren
: typeof boundChildren === 'object'
? JSON.stringify(boundChildren)
: String(boundChildren)
const props = getPreviewProps(node, dataValues, currentItem, onNodePropChange)
const props = getPreviewProps(node, dataValues, currentItem, onNodePropChange, translate)
if (node.kind === 'custom') {
return renderCustomComponent?.(node.type, { ...props, children: content }) || null
}
@ -606,6 +644,7 @@ const NodeView = ({
onSelect,
onDropComponent,
onMove,
onReorder,
onDuplicate,
onDelete,
onNodePropChange,
@ -620,6 +659,7 @@ const NodeView = ({
onSelect?: (id: string) => void
onDropComponent?: (definitionName: 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
onDelete?: (id: string) => void
onNodePropChange?: (id: string, propertyName: string, value: unknown) => void
@ -627,6 +667,7 @@ const NodeView = ({
dataValues: Record<string, unknown>
currentItem?: unknown
}) => {
const { translate } = useLocalization()
const selected = interactive && selectedId === node.id
const acceptsDroppedChildren = [
'PageContainer',
@ -638,6 +679,14 @@ const NodeView = ({
'Card',
'FormContainer',
].includes(node.type)
const staticChildren = node.props.children
const hasStaticChildren =
staticChildren !== undefined &&
staticChildren !== null &&
(typeof staticChildren !== 'string' || staticChildren.length > 0)
const hasChildrenBinding = Boolean(node.bindings?.children?.sourceId)
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)
@ -669,6 +718,7 @@ const NodeView = ({
onSelect={onSelect}
onDropComponent={onDropComponent}
onMove={onMove}
onReorder={onReorder}
onDuplicate={onDuplicate}
onDelete={onDelete}
onNodePropChange={onNodePropChange}
@ -749,6 +799,7 @@ const NodeView = ({
onSelect={onSelect}
onDropComponent={onDropComponent}
onMove={onMove}
onReorder={onReorder}
onDuplicate={onDuplicate}
onDelete={onDelete}
onNodePropChange={onNodePropChange}
@ -847,13 +898,24 @@ const NodeView = ({
/>,
]
: children
const contentChildren =
interactive && node.type === 'Card' && node.children.length === 0
? [
<div
key={`card_drop_${node.id}`}
className="rounded border border-dashed border-slate-300 px-3 py-4 text-center text-xs text-slate-400 dark:border-slate-700"
>
Bileşeni buraya bırakın
</div>,
]
: renderedChildren
return (
<div
className={`group/node relative min-h-[28px] rounded-md ${
interactive
? selected
? 'outline outline-2 outline-sky-500 outline-offset-2'
? 'z-40 outline outline-2 outline-sky-500 outline-offset-2'
: 'outline outline-1 outline-transparent hover:outline-sky-300'
: ''
}`}
@ -870,19 +932,19 @@ const NodeView = ({
onDragStart={(event) => {
if (!interactive) return
event.stopPropagation()
event.dataTransfer.setData(
DESIGNER_DRAG_TYPE,
JSON.stringify({ source: 'canvas', nodeId: node.id }),
)
const payload = JSON.stringify({ source: 'canvas', nodeId: node.id })
event.dataTransfer.effectAllowed = 'move'
event.dataTransfer.setData(DESIGNER_DRAG_TYPE, payload)
event.dataTransfer.setData('text/plain', payload)
}}
onDragOver={(event) => {
if (!interactive || !acceptsDroppedChildren) return
if (!interactive) return
event.preventDefault()
event.stopPropagation()
event.dataTransfer.dropEffect = 'copy'
event.dataTransfer.dropEffect = 'move'
}}
onDrop={(event) => {
if (!interactive || !acceptsDroppedChildren) return
if (!interactive) return
event.preventDefault()
event.stopPropagation()
const raw =
@ -890,14 +952,18 @@ const NodeView = ({
event.dataTransfer.getData('text/plain')
if (!raw) return
const payload = JSON.parse(raw)
if (payload.source === 'library') onDropComponent?.(payload.name, node.id)
if (payload.source === 'canvas' && payload.nodeId !== node.id) {
const bounds = event.currentTarget.getBoundingClientRect()
const placement = event.clientY < bounds.top + bounds.height / 2 ? 'before' : 'after'
onReorder?.(payload.nodeId, node.id, placement)
} else if (payload.source === 'library' && acceptsDroppedChildren) {
onDropComponent?.(payload.name, node.id)
}
}}
>
{interactive && (
<div
className={`absolute -top-7 right-0 z-20 items-center overflow-hidden rounded-md bg-sky-600 text-white shadow-md ${
selected ? 'flex' : 'hidden group-hover/node:flex'
}`}
className={`absolute right-1 top-1 z-50 items-center overflow-hidden rounded-md bg-sky-600 text-white shadow-lg ${selected ? 'flex' : 'hidden'}`}
>
<span className="flex items-center gap-1 px-2 text-[10px] font-semibold">
<FaGripVertical /> {node.type}
@ -954,17 +1020,19 @@ const NodeView = ({
>
{renderElement(
node,
renderedChildren,
contentChildren,
dataValues,
currentItem,
interactive,
translate,
onNodePropChange,
renderCustomComponent,
)}
</PreviewBoundary>
{interactive &&
node.children.length === 0 &&
acceptsDroppedChildren && (
!hasVisibleChildren &&
acceptsDroppedChildren &&
node.type !== 'Card' && (
<div className="m-2 rounded border border-dashed border-slate-300 px-3 py-4 text-center text-xs text-slate-400">
Bileşeni buraya bırakın
</div>
@ -977,11 +1045,11 @@ const NodeView = ({
const VisualCanvas = ({
nodes,
selectedId,
background,
interactive = true,
onSelect,
onDropComponent,
onMove,
onReorder,
onDuplicate,
onDelete,
onNodePropChange,
@ -993,7 +1061,6 @@ const VisualCanvas = ({
return (
<div
className="min-h-full p-8 text-slate-900 dark:text-slate-100"
style={{ background }}
onClick={() => interactive && onSelect?.('')}
onDragOver={(event) => interactive && event.preventDefault()}
onDrop={(event) => {
@ -1021,6 +1088,7 @@ const VisualCanvas = ({
onSelect={onSelect}
onDropComponent={onDropComponent}
onMove={onMove}
onReorder={onReorder}
onDuplicate={onDuplicate}
onDelete={onDelete}
onNodePropChange={onNodePropChange}
@ -1028,7 +1096,7 @@ const VisualCanvas = ({
))}
</div>
) : (
<div className="flex min-h-[520px] items-center justify-center rounded-xl border-2 border-dashed border-slate-300 bg-white/70 text-center dark:border-slate-700 dark:bg-slate-900/70">
<div className="flex min-h-[520px] items-center justify-center rounded-xl border-slate-300 bg-white/70 text-center dark:border-slate-700 dark:bg-slate-900/70">
<div className="w-full max-w-2xl px-6">
<div className="mx-auto mb-4 flex h-14 w-14 items-center justify-center rounded-2xl bg-sky-100 text-2xl text-sky-600">
+

View file

@ -321,15 +321,19 @@ const normalizeUiDefinition = (
const existingProperties = new Map(curatedProperties.map((property) => [property.name, property]))
const generatedProperties = generatedPropertiesToDesigner(generated, existingProperties)
if (
['Select', 'AutoComplete', 'Menu', 'Dropdown', 'Radio.Group'].includes(definition.name) &&
['Select', 'AutoComplete', 'Menu', 'Dropdown', 'Pagination', 'Radio.Group'].includes(
definition.name,
) &&
!generatedProperties.some(
(property) =>
property.name ===
(['Menu', 'Dropdown', 'Radio.Group'].includes(definition.name) ? 'items' : 'options'),
(['Menu', 'Dropdown', 'Pagination', 'Radio.Group'].includes(definition.name)
? 'items'
: 'options'),
)
) {
generatedProperties.unshift(
['Menu', 'Dropdown', 'Radio.Group'].includes(definition.name)
['Menu', 'Dropdown', 'Pagination', 'Radio.Group'].includes(definition.name)
? MENU_ITEMS_PROPERTY
: SELECT_OPTIONS_PROPERTY,
)

View file

@ -5,14 +5,24 @@ const safeIdentifier = (value: string) => {
return /^[A-Za-z_$]/.test(cleaned) ? cleaned : `Component_${cleaned}`
}
const serializeValue = (value: unknown) => {
if (typeof value === 'string') return `{${JSON.stringify(value)}}`
if (typeof value === 'number' || typeof value === 'boolean') return `{${String(value)}}`
if (value === null) return '{null}'
if (Array.isArray(value) || typeof value === 'object') return `{${JSON.stringify(value)}}`
return JSON.stringify(String(value ?? ''))
const staticValueExpression = (value: unknown): string => {
if (typeof value === 'string') {
return value.startsWith('::')
? `translate(${JSON.stringify(value)})`
: JSON.stringify(value)
}
if (Array.isArray(value)) return `[${value.map(staticValueExpression).join(', ')}]`
if (value && typeof value === 'object') {
return `{ ${Object.entries(value)
.map(([key, item]) => `${JSON.stringify(key)}: ${staticValueExpression(item)}`)
.join(', ')} }`
}
if (value === undefined) return 'undefined'
return JSON.stringify(value)
}
const serializeValue = (value: unknown) => `{${staticValueExpression(value)}}`
const bindingExpression = (node: DesignerNode, propertyName: string, itemVariable?: string) => {
const binding = node.bindings?.[propertyName]
if (!binding?.sourceId) return ''
@ -36,10 +46,13 @@ const getRuntimeStateSpec = (node: DesignerNode): RuntimeStateSpec | null => {
AutoComplete: { propertyName: 'value', eventName: 'onInputChange' },
Dropdown: { propertyName: 'activeKey', eventName: 'onSelect' },
Input: { propertyName: 'value', eventName: 'onChange' },
ImageViewer: { propertyName: 'activeIndex', eventName: 'onIndexChange' },
Menu: { propertyName: 'defaultActiveKeys', eventName: 'onSelect' },
Pagination: { propertyName: 'currentPage', eventName: 'onChange' },
'Radio.Group': { propertyName: 'value', eventName: 'onChange' },
Select: { propertyName: 'value', eventName: 'onChange' },
Steps: { propertyName: 'current', eventName: 'onChange' },
Upload: { propertyName: 'fileList', eventName: 'onChange' },
}
const fallback =
'checked' in node.props
@ -88,7 +101,8 @@ const getRuntimeStateSpec = (node: DesignerNode): RuntimeStateSpec | null => {
const isOptionCollectionProperty = (node: DesignerNode, propertyName: string) =>
(['Select', 'AutoComplete'].includes(node.type) && propertyName === 'options') ||
(['Menu', 'Dropdown', 'Radio.Group'].includes(node.type) && propertyName === 'items')
(['Menu', 'Dropdown', 'Pagination', 'Radio.Group'].includes(node.type) &&
propertyName === 'items')
const propsToCode = (node: DesignerNode, itemVariable?: string) => {
const runtimeState = getRuntimeStateSpec(node)
@ -334,7 +348,7 @@ ${indent(')}', level + 1)}`
: ''
const optionsExpression = binding
? `toSelectOptions(${binding}, ${JSON.stringify(optionsBinding?.labelPath || '')}, ${JSON.stringify(optionsBinding?.valuePath || '')})`
: JSON.stringify(Array.isArray(staticOptions) ? staticOptions : [])
: staticValueExpression(Array.isArray(staticOptions) ? staticOptions : [])
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)}`
}
@ -350,10 +364,29 @@ ${indent(')}', level + 1)}`
: ''
const optionsExpression = binding
? `toSelectOptions(${binding}, ${JSON.stringify(optionsBinding?.labelPath || '')}, ${JSON.stringify(optionsBinding?.valuePath || '')})`
: JSON.stringify(Array.isArray(staticOptions) ? staticOptions : [])
: staticValueExpression(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 === 'Pagination') {
const { items: staticOptions = [], ...paginationProps } = node.props
const { items: optionsBinding, ...paginationBindings } = node.bindings || {}
const paginationPropsCode = propsToCode(
{ ...node, props: paginationProps, bindings: paginationBindings },
itemVariable,
)
const binding = optionsBinding?.sourceId
? bindingExpression(node, 'items', itemVariable)
: ''
const optionsExpression = binding
? `toSelectOptions(${binding}, ${JSON.stringify(optionsBinding?.labelPath || '')}, ${JSON.stringify(optionsBinding?.valuePath || '')})`
: staticValueExpression(Array.isArray(staticOptions) ? staticOptions : [])
return indent(
`<UiKit.Pagination${paginationPropsCode} total={${optionsExpression}.length} />`,
level,
)
}
if (node.type === 'Radio.Group') {
const {
items: staticOptions = [],
@ -372,7 +405,7 @@ ${indent(')}', level + 1)}`
: ''
const optionsExpression = binding
? `toSelectOptions(${binding}, ${JSON.stringify(optionsBinding?.labelPath || '')}, ${JSON.stringify(optionsBinding?.valuePath || '')})`
: JSON.stringify(Array.isArray(staticOptions) ? staticOptions : [])
: staticValueExpression(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)}`
}
@ -400,7 +433,7 @@ ${indent(')}', level + 1)}`
const text = childrenBinding
? indent(`{${childrenBinding}}`, level + 1)
: childrenText
? indent(`{${JSON.stringify(childrenText)}}`, level + 1)
? indent(`{${staticValueExpression(childrenText)}}`, level + 1)
: ''
const body = [text, nested].filter(Boolean).join('\n')
return `${indent(`<${tag}${propCode}>`, level)}\n${body}\n${indent(`</${tag}>`, level)}`
@ -416,7 +449,9 @@ export const generateDesignerCode = (name: string, document: DesignerDocument) =
const visit = (nodes: DesignerNode[]) => {
nodes.forEach((node) => {
if (
['Select', 'AutoComplete', 'Menu', 'Dropdown', 'Radio.Group'].includes(node.type)
['Select', 'AutoComplete', 'Menu', 'Dropdown', 'Pagination', 'Radio.Group'].includes(
node.type,
)
) {
hasSelect = true
}
@ -445,10 +480,14 @@ export const generateDesignerCode = (name: string, document: DesignerDocument) =
? `${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 === 'Pagination'
? `${runtimeState.setterName}(Number(valueOrEvent) || 1)`
: node.type === 'Steps' || node.type === 'ImageViewer'
? `${runtimeState.setterName}(Number(valueOrEvent) || 0)`
: node.type === 'Upload'
? `${runtimeState.setterName}(Array.isArray(originalEvent) ? originalEvent : [])`
: runtimeState.propertyName === 'checked'
? `${runtimeState.setterName}(typeof originalEvent === "object" && originalEvent?.target ? Boolean(originalEvent.target.checked) : typeof valueOrEvent === "object" && valueOrEvent?.target ? Boolean(valueOrEvent.target.checked) : Boolean(valueOrEvent))`
: node.type === 'Input' || node.kind === 'html'
? `${runtimeState.setterName}(valueOrEvent?.target?.value ?? valueOrEvent)`
: `${runtimeState.setterName}(valueOrEvent)`

View file

@ -34,7 +34,6 @@ export interface DesignerDocument {
sourceMode: 'visual' | 'code'
nodes: DesignerNode[]
canvas: {
background: string
width: 'responsive' | 'desktop' | 'tablet' | 'mobile'
}
lifecycle: {
@ -65,7 +64,6 @@ export const createEmptyDesignerDocument = (
sourceMode,
nodes: [],
canvas: {
background: '#f8fafc',
width: 'responsive',
},
lifecycle: {

View file

@ -9,6 +9,7 @@ import { generateDesignerCode } from '@/components/visualDesigner/codeGenerator'
import type { DesignerDocument } from '@/components/visualDesigner/types'
import DOMPurify from 'dompurify'
import { useStoreState } from '@/store/store'
import { useLocalization } from '@/utils/hooks/useLocalization'
import React, {
createContext,
useContext,
@ -147,6 +148,7 @@ export const useComponents = () => {
}
export const ComponentProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const { translate } = useLocalization()
const applicationConfig = useStoreState((state) => state.abpConfig?.config)
const extraProperties = applicationConfig?.extraProperties
const [components, setComponents] = useState<CustomComponent[]>([])
@ -329,7 +331,16 @@ export const ComponentProvider: React.FC<{ children: React.ReactNode }> = ({ chi
const compileComponents = async () => {
try {
const activeComponents = components?.filter((c) => c.isActive)
const activeComponents = components.filter(
(component) => component.isActive && typeof component.name === 'string' && component.name.trim(),
)
const invalidActiveComponents = components.filter(
(component) => component.isActive && (typeof component.name !== 'string' || !component.name.trim()),
)
if (invalidActiveComponents.length) {
console.warn('Adsız aktif komponent kayıtları derleme dışında bırakıldı.', invalidActiveComponents)
}
if (!activeComponents.length) {
setCompiledComponents({})
@ -390,7 +401,7 @@ export const ComponentProvider: React.FC<{ children: React.ReactNode }> = ({ chi
const componentBundle = componentDeclarations + '\n' + componentDefinitions
const bundledCode = `
(function(React, AutoComplete, Alert, Avatar, Badge, Button, Calendar, Card, Checkbox, ConfigProvider, DatePicker, Dialog, Drawer, Dropdown, FormItem, FormContainer, Input, InputGroup, Menu, MenuItem, Notification, Pagination, Progress, Radio, RangeCalendar, ScrollBar, Segment, Select, Skeleton, Spinner, Steps, Switcher, Table, Tabs, Tag, TimeInput, Timeline, toast, Tooltip, Upload, PlatformViewHost, apiService, DOMPurify, axios, UiKit) {
(function(React, AutoComplete, Alert, Avatar, Badge, Button, Calendar, Card, Checkbox, ConfigProvider, DatePicker, Dialog, Drawer, Dropdown, FormItem, FormContainer, Input, InputGroup, Menu, MenuItem, Notification, Pagination, Progress, Radio, RangeCalendar, ScrollBar, Segment, Select, Skeleton, Spinner, Steps, Switcher, Table, Tabs, Tag, TimeInput, Timeline, toast, Tooltip, Upload, PlatformViewHost, apiService, DOMPurify, axios, UiKit, translate) {
const { useState, useEffect, useCallback, useMemo, useRef, createContext, useContext } = React;
const componentRegistry = {};
@ -406,7 +417,7 @@ export const ComponentProvider: React.FC<{ children: React.ReactNode }> = ({ chi
.join('\n')}
return componentRegistry;
})(React, AutoComplete, Alert, Avatar, Badge, Button, Calendar, Card, Checkbox, ConfigProvider, DatePicker, Dialog, Drawer, Dropdown, FormItem, FormContainer, Input, InputGroup, Menu, MenuItem, Notification, Pagination, Progress, Radio, RangeCalendar, ScrollBar, Segment, Select, Skeleton, Spinner, Steps, Switcher, Table, Tabs, Tag, TimeInput, Timeline, toast, Tooltip, Upload, PlatformViewHost, apiService, DOMPurify, axios, UiKit)
})(React, AutoComplete, Alert, Avatar, Badge, Button, Calendar, Card, Checkbox, ConfigProvider, DatePicker, Dialog, Drawer, Dropdown, FormItem, FormContainer, Input, InputGroup, Menu, MenuItem, Notification, Pagination, Progress, Radio, RangeCalendar, ScrollBar, Segment, Select, Skeleton, Spinner, Steps, Switcher, Table, Tabs, Tag, TimeInput, Timeline, toast, Tooltip, Upload, PlatformViewHost, apiService, DOMPurify, axios, UiKit, translate)
`
// Babel is several megabytes and is only needed when an active runtime
@ -516,6 +527,7 @@ export const ComponentProvider: React.FC<{ children: React.ReactNode }> = ({ chi
'DOMPurify',
'axios',
'UiKit',
'translate',
`return ${compiledBundle}`,
)
@ -565,6 +577,7 @@ export const ComponentProvider: React.FC<{ children: React.ReactNode }> = ({ chi
DOMPurify,
axios,
ui,
translate,
)
if (!cancelled) setCompiledComponents(compiledComponentsRegistry)
@ -579,7 +592,7 @@ export const ComponentProvider: React.FC<{ children: React.ReactNode }> = ({ chi
return () => {
cancelled = true
}
}, [components, extractComponentInfo])
}, [components, extractComponentInfo, translate])
// Render functions
const renderComponent = useCallback(

View file

@ -43,8 +43,6 @@ export const ROUTES_ENUM = {
endpoints: '/admin/developerkit/endpoints',
endpointsNew: '/admin/developerkit/endpoints/new',
components: '/admin/developerkit/components',
componentsNew: '/admin/developerkit/components/new',
componentsView: '/admin/developerkit/components/view/:id',
componentsEdit: '/admin/developerkit/components/edit/:id',
dynamicServices: '/admin/developerkit/dynamic-services',
dynamicServicesNew: '/admin/developerkit/dynamic-services/new',

View file

@ -1,13 +1,11 @@
import React, { useState, useEffect } from 'react'
import { useParams, useNavigate, Link } from 'react-router-dom'
import { useComponents } from '../../contexts/ComponentContext'
import { FaRegSave, FaArrowLeft, FaSync, FaCode, FaEye, FaCog } from 'react-icons/fa'
import ComponentPreview from '../../components/componentEditor/ComponentPreview'
import { ROUTES_ENUM } from '@/routes/route.constant'
import { FaRegSave, FaSync, FaCog } from 'react-icons/fa'
import { useLocalization } from '@/utils/hooks/useLocalization'
import { Formik, Form, Field } from 'formik'
import * as Yup from 'yup'
import { Button, Checkbox, FormContainer, FormItem, Input } from '@/components/ui'
import Dialog from '@/components/ui/Dialog'
import { generateDesignerCode } from '@/components/visualDesigner/codeGenerator'
import {
createDesignerId,
@ -24,9 +22,18 @@ const validationSchema = Yup.object({
isActive: Yup.boolean(),
})
const ComponentEditor: React.FC = () => {
const { id } = useParams()
const navigate = useNavigate()
interface ComponentEditorProps {
componentId?: string | null
onClose?: () => void
onSaved?: () => void | Promise<void>
}
const ComponentEditor: React.FC<ComponentEditorProps> = ({
componentId,
onClose,
onSaved,
}) => {
const id = componentId || undefined
const { translate } = useLocalization()
const { getComponent, addComponent, updateComponent, refreshComponent } = useComponents()
@ -133,7 +140,8 @@ const ComponentEditor: React.FC = () => {
await addComponent(componentData)
}
navigate(ROUTES_ENUM.protected.saas.developerKit.components)
await onSaved?.()
onClose?.()
} catch (error) {
console.error('Error saving component:', error)
alert('Failed to save component. Please try again.')
@ -145,7 +153,7 @@ const ComponentEditor: React.FC = () => {
// Loading state - komponent yüklenene kadar bekle
if (isEditing && !isLoaded) {
return (
<div className="h-screen flex items-center justify-center">
<div className="flex min-h-72 items-center justify-center">
<div className="text-center">
<FaSync className="w-8 h-8 text-blue-500 animate-spin mx-auto mb-3" />
<p className="text-slate-600">{translate('::App.Loading')}</p>
@ -164,59 +172,8 @@ const ComponentEditor: React.FC = () => {
{({ values, touched, errors, isSubmitting, setFieldValue, submitForm, isValid }) => {
return (
<>
{/* Enhanced Header */}
<div className="bg-white dark:bg-gray-900 shadow-lg border-b border-slate-200 dark:border-gray-700 sticky top-0 z-10">
<div className="px-1 py-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<div className="flex items-center gap-3 pl-3">
<div className="bg-gradient-to-r from-blue-500 to-purple-600 p-2 rounded-lg">
<FaCode className="w-5 h-5 text-white" />
</div>
<div>
<h1 className="font-semibold text-slate-800 dark:text-gray-100 text-sm leading-tight">
{isEditing
? `${translate('::App.DeveloperKit.ComponentEditor.Title.Edit')} - ${values.name || initialValues.name || 'Component'}`
: translate('::App.DeveloperKit.ComponentEditor.Title.Create')}
</h1>
<p className="text-xs text-slate-500 dark:text-gray-400 leading-tight">
{isEditing
? 'Modify your React component'
: 'Create a new React component'}
</p>
</div>
</div>
</div>
{/* Save Button in Header */}
<div className="flex items-center gap-3 pr-3">
<Link
to={ROUTES_ENUM.protected.saas.developerKit.components}
className="flex items-center gap-2 text-slate-600 dark:text-gray-300 text-black dark:text-white px-4 py-2 rounded-lg hover:text-slate-700 dark:hover:text-gray-100 transition-colors"
>
<FaArrowLeft className="w-3.5 h-3.5" />
{translate('::App.DeveloperKit.ComponentEditor.Back')}
</Link>
<div className="h-6 w-px bg-slate-300 dark:bg-gray-700"></div>
<Button
type="button"
variant="solid"
size="sm"
disabled={isSubmitting || !values.name.trim() || !isValid}
className="flex items-center gap-2 text-white px-4 py-2 rounded-lg hover:bg-emerald-700 dark:hover:bg-emerald-800 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
onClick={submitForm}
>
<FaRegSave className="w-4 h-4" />
{isSubmitting ? translate('::Saving') : translate('::Save')}
</Button>
</div>
</div>
</div>
</div>
<Form className="grid grid-cols-1 lg:grid-cols-3 gap-4 py-3">
<div className="space-y-3 col-span-1">
<Form className="pb-3 pt-8">
<div className="space-y-3">
<div className="bg-white dark:bg-gray-900 rounded-lg shadow-sm border border-slate-200 dark:border-gray-700 p-3">
<div className="flex items-center gap-2 mb-3">
<div className="bg-blue-100 dark:bg-blue-900/20 p-1.5 rounded-lg">
@ -268,6 +225,7 @@ const ComponentEditor: React.FC = () => {
type="text"
component={Input}
placeholder="Brief description of the component"
textArea={true}
/>
</FormItem>
@ -302,23 +260,23 @@ const ComponentEditor: React.FC = () => {
</FormContainer>
</div>
</div>
{/* Right Side - Preview */}
<div className="space-y-4 col-span-2">
{/* Component Preview */}
<div className="bg-white dark:bg-gray-900 rounded-lg shadow-sm border border-slate-200 dark:border-gray-700 p-3">
<div className="flex items-center gap-2 mb-3">
<div className="bg-purple-100 dark:bg-purple-900/20 p-1.5 rounded-lg">
<FaEye className="w-4 h-4 text-purple-600 dark:text-purple-400" />
</div>
<h2 className="text-base font-semibold text-slate-900 dark:text-gray-100">
Preview
</h2>
</div>
<ComponentPreview componentName={values.name} />
</div>
</div>
</Form>
<Dialog.Footer className="flex items-center justify-end gap-2 border-t border-slate-200 pt-3 dark:border-slate-700">
<Button type="button" size="sm" onClick={onClose} disabled={isSubmitting}>
{translate('::Cancel')}
</Button>
<Button
type="button"
variant="solid"
size="sm"
disabled={isSubmitting || !values.name.trim() || !isValid}
className="flex items-center gap-2"
onClick={submitForm}
>
<FaRegSave className="h-4 w-4" />
{isSubmitting ? translate('::Saving') : translate('::Save')}
</Button>
</Dialog.Footer>
</>
)
}}

View file

@ -1,13 +1,12 @@
import ComponentEditor from '@/views/developerKit/ComponentEditor'
import DeveloperLayout from '@/components/layouts/DeveloperLayout'
import React from 'react'
import ComponentManager from '@/views/developerKit/ComponentManager'
const ComponentEditorPage: React.FC = () => {
return (
<DeveloperLayout>
<ComponentEditor />
</DeveloperLayout>
)
}
// Existing dynamic-route records can still reference this physical module.
// Keep the compatibility entry point, but manage create/view operations in the manager modal.
const ComponentEditorPage = () => (
<DeveloperLayout>
<ComponentManager />
</DeveloperLayout>
)
export default ComponentEditorPage

View file

@ -1,5 +1,4 @@
import React, { useEffect, useState } from 'react'
import { Link, useNavigate } from 'react-router-dom'
import { useComponents } from '../../contexts/ComponentContext'
import {
FaPlus,
@ -10,7 +9,8 @@ import {
FaEyeSlash,
FaFilter,
FaCalendarAlt,
FaRegWindowMaximize,
FaExternalLinkAlt,
FaCog,
FaSyncAlt,
} from 'react-icons/fa'
import Widget from '@/components/common/Widget'
@ -20,14 +20,16 @@ import { Loading } from '../../components/shared'
import { APP_NAME } from '@/constants/app.constant'
import { Helmet } from 'react-helmet'
import { Button } from '@/components/ui'
import Dialog from '@/components/ui/Dialog'
import ComponentEditor from './ComponentEditor'
const ComponentManager: React.FC = () => {
const { components, loading, updateComponent, deleteComponent, refreshComponents } =
useComponents()
const navigate = useNavigate()
const [searchTerm, setSearchTerm] = useState('')
const [filterActive, setFilterActive] = useState<'all' | 'active' | 'inactive'>('all')
const [isRefreshing, setIsRefreshing] = useState(false)
const [editorComponentId, setEditorComponentId] = useState<string | null | undefined>(undefined)
// Calculate statistics
const totalComponents = components?.length || 0
@ -118,7 +120,7 @@ const ComponentManager: React.FC = () => {
</div>
{/* Filters */}
<div className="flex flex-col sm:flex-row gap-4">
<div className="flex flex-col sm:flex-row gap-3">
<div className="flex-1 relative">
<FaSearch className="absolute left-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-slate-400" />
<input
@ -160,7 +162,7 @@ const ComponentManager: React.FC = () => {
type="button"
variant="solid"
className="flex items-center gap-2"
onClick={() => navigate(ROUTES_ENUM.protected.saas.developerKit.componentsNew)}
onClick={() => setEditorComponentId(null)}
>
<FaPlus className="w-4 h-4" />
{translate('::App.DeveloperKit.Component.New')}
@ -286,23 +288,16 @@ const ComponentManager: React.FC = () => {
window.open(routePath, '_blank')
}}
>
<FaRegWindowMaximize className="w-4 h-4" />
<FaExternalLinkAlt className="w-4 h-4" />
</Button>
<Button
type="button"
variant="solid"
color="green-600"
title={translate('::App.Platform.View')}
onClick={() =>
navigate(
ROUTES_ENUM.protected.saas.developerKit.componentsView.replace(
':id',
component.id,
),
)
}
onClick={() => setEditorComponentId(component.id)}
>
<FaEye className="w-4 h-4" />
<FaCog className="w-4 h-4" />
</Button>
<Button
type="button"
@ -336,17 +331,34 @@ const ComponentManager: React.FC = () => {
: translate('::App.DeveloperKit.Component.Empty.Initial.Description')}
</p>
{!searchTerm && filterActive === 'all' && (
<Link
to={ROUTES_ENUM.protected.saas.developerKit.componentsNew}
<button
type="button"
onClick={() => setEditorComponentId(null)}
className="inline-flex items-center gap-2 bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 transition-colors"
>
<FaPlus className="w-4 h-4" />
{translate('::App.DeveloperKit.Component.Empty.Initial.Action')}
</Link>
</button>
)}
</div>
</div>
)}
<Dialog
isOpen={editorComponentId !== undefined}
width={720}
height="85vh"
contentClassName="overflow-y-auto"
onClose={() => setEditorComponentId(undefined)}
onRequestClose={() => setEditorComponentId(undefined)}
>
<ComponentEditor
key={editorComponentId ?? 'new'}
componentId={editorComponentId}
onClose={() => setEditorComponentId(undefined)}
onSaved={refreshComponents}
/>
</Dialog>
</div>
)
}

View file

@ -33,8 +33,7 @@ import { ROUTES_ENUM } from '@/routes/route.constant'
import apiService from '@/services/api.service'
import { developerKitService } from '@/services/developerKit.service'
import { Button, Notification, toast } from '@/components/ui'
import TailwindModal from '@/components/codeLayout/TailwindModal'
import { useStoreState } from '@/store/store'
import StyleModal from '@/components/codeLayout/StyleModal'
import VisualCanvas, { DESIGNER_DRAG_TYPE } from '@/components/visualDesigner/VisualCanvas'
import {
DESIGNER_DATA_COMPONENT_NAMES,
@ -57,6 +56,7 @@ import {
type DesignerNode,
type DesignerPropertyInfo,
} from '@/components/visualDesigner/types'
import { useLocalization } from '@/utils/hooks/useLocalization'
type WorkspaceTab = 'design' | 'data' | 'code' | 'javascript'
type InspectorTab = 'properties' | 'data' | 'tree'
@ -100,13 +100,14 @@ const OPTION_DATA_COMPONENT_NAMES = new Set([
'AutoComplete',
'Menu',
'Dropdown',
'Pagination',
'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) =>
['Menu', 'Dropdown', 'Radio.Group'].includes(type || '') ? 'items' : 'options'
['Menu', 'Dropdown', 'Pagination', 'Radio.Group'].includes(type || '') ? 'items' : 'options'
const isTabularDataComponent = (type?: string) =>
Boolean(type && TABULAR_DATA_COMPONENT_NAMES.has(type))
@ -338,6 +339,39 @@ const moveInTree = (nodes: DesignerNode[], id: string, direction: -1 | 1): Desig
return nodes.map((node) => ({ ...node, children: moveInTree(node.children, id, direction) }))
}
const insertRelativeToNode = (
nodes: DesignerNode[],
targetId: string,
nodeToInsert: DesignerNode,
placement: 'before' | 'after',
): DesignerNode[] => {
const targetIndex = nodes.findIndex((node) => node.id === targetId)
if (targetIndex >= 0) {
const next = [...nodes]
next.splice(targetIndex + (placement === 'after' ? 1 : 0), 0, nodeToInsert)
return next
}
return nodes.map((node) => ({
...node,
children: insertRelativeToNode(node.children, targetId, nodeToInsert, placement),
}))
}
const reorderNodeTree = (
nodes: DesignerNode[],
sourceId: string,
targetId: string,
placement: 'before' | 'after',
): DesignerNode[] => {
const source = findDesignerNode(nodes, sourceId)
const target = findDesignerNode(nodes, targetId)
if (!source || !target || sourceId === targetId || findDesignerNode(source.children, targetId)) {
return nodes
}
const movingNode = { ...source, slot: target.slot }
return insertRelativeToNode(removeNodeTree(nodes, sourceId), targetId, movingNode, placement)
}
const definitionToNode = (definition: DesignerComponentDefinition): DesignerNode => {
const props: Record<string, unknown> = {}
const events: Record<string, string> = {}
@ -360,87 +394,83 @@ const definitionToNode = (definition: DesignerComponentDefinition): DesignerNode
}
const normalizeDesignerDocument = (document: DesignerDocument): DesignerDocument => {
const normalizeNodes = (nodes: DesignerNode[]): DesignerNode[] =>
nodes.map((node) => {
const props = { ...(node.props || {}) }
const events = { ...(node.events || {}) }
const bindings = { ...(node.bindings || {}) }
if (node.type === 'Select') {
const legacyPropAliases: Record<string, string> = {
clearable: 'isClearable',
disabled: 'isDisabled',
multiple: 'isMulti',
searchable: 'isSearchable',
}
Object.entries(legacyPropAliases).forEach(([legacyName, runtimeName]) => {
if (props[runtimeName] === undefined && props[legacyName] !== undefined) {
props[runtimeName] = props[legacyName]
}
delete props[legacyName]
})
if (events.onInputChange === undefined && events.onSearch !== undefined) {
events.onInputChange = events.onSearch
}
delete events.onSearch
}
if (node.type === 'Menu') {
if (!Array.isArray(props.items) && Array.isArray(props.options)) {
props.items = props.options
}
if (!bindings.items && bindings.options) bindings.items = bindings.options
delete props.options
delete props.variant
delete bindings.options
}
if (node.type === 'Table') {
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
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,
const normalizeNodes = (nodes: DesignerNode[]): DesignerNode[] =>
nodes.map((node) => {
const props = { ...(node.props || {}) }
const events = { ...(node.events || {}) }
const bindings = { ...(node.bindings || {}) }
if (node.type === 'Select') {
const legacyPropAliases: Record<string, string> = {
clearable: 'isClearable',
disabled: 'isDisabled',
multiple: 'isMulti',
searchable: 'isSearchable',
}
Object.entries(legacyPropAliases).forEach(([legacyName, runtimeName]) => {
if (props[runtimeName] === undefined && props[legacyName] !== undefined) {
props[runtimeName] = props[legacyName]
}
delete props[legacyName]
})
if (events.onInputChange === undefined && events.onSearch !== undefined) {
events.onInputChange = events.onSearch
}
delete events.onSearch
}
if (node.type === 'Menu') {
if (!Array.isArray(props.items) && Array.isArray(props.options)) {
props.items = props.options
}
if (!bindings.items && bindings.options) bindings.items = bindings.options
delete props.options
delete props.variant
delete bindings.options
}
if (node.type === 'Table') {
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
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,
}
})
return {
...document,
nodes: normalizeNodes(document.nodes),
canvas: { width: document.canvas?.width || 'responsive' },
dataSources: Array.isArray(document.dataSources) ? document.dataSources : [],
}
}
@ -460,9 +490,7 @@ const parseDesignerDocument = (rawProps: string | undefined): DesignerDocument |
}
const parseDesignerDocumentFromCode = (code: string | undefined): DesignerDocument | null => {
const encodedDocument = code?.match(
/\/\*__SOZSOFT_VISUAL_DESIGNER__(.*?)__\*\//,
)?.[1]
const encodedDocument = code?.match(/\/\*__SOZSOFT_VISUAL_DESIGNER__(.*?)__\*\//)?.[1]
if (!encodedDocument) return null
try {
const document = JSON.parse(decodeURIComponent(encodedDocument)) as DesignerDocument
@ -509,7 +537,9 @@ const parseGeneratedJsxProps = (
props[match[1]] = match[2]
}
}
for (const match of source.matchAll(/([A-Za-z_$][\w$]*)=\{(true|false|null|-?\d+(?:\.\d+)?)\}/g)) {
for (const match of source.matchAll(
/([A-Za-z_$][\w$]*)=\{(true|false|null|-?\d+(?:\.\d+)?)\}/g,
)) {
props[match[1]] =
match[2] === 'true'
? true
@ -568,7 +598,9 @@ const migrateGeneratedCodeToDesignerDocument = (
})
}
for (const match of code.matchAll(/<UiKit\.(Select|AutoComplete|Menu|Pagination)\b([^>]*?)(?:\/>|>)/g)) {
for (const match of code.matchAll(
/<UiKit\.(Select|AutoComplete|Menu|Pagination)\b([^>]*?)(?:\/>|>)/g,
)) {
const type = match[1]
const attributes = match[2]
const props = parseGeneratedJsxProps(attributes, componentDefaultProps)
@ -626,7 +658,10 @@ const migrateGeneratedCodeToDesignerDocument = (
'Grid',
'ui',
{
className: code.slice(Math.max(0, match.index - 250), match.index).match(/<div className="([^"]*)"[^>]*>\s*\{\(\(\) => \{$/)?.[1] || '',
className:
code
.slice(Math.max(0, match.index - 250), match.index)
.match(/<div className="([^"]*)"[^>]*>\s*\{\(\(\) => \{$/)?.[1] || '',
dataColumns: JSON.parse(match[3]),
compact: !code.slice(match.index, match.index + 5000).includes('px-3 py-2'),
hoverable: code.slice(match.index, match.index + 5000).includes('hover:bg-'),
@ -673,7 +708,9 @@ const migrateGeneratedCodeToDesignerDocument = (
)
}
document.nodes = positionedNodes.sort((left, right) => left.index - right.index).map(({ node }) => node)
document.nodes = positionedNodes
.sort((left, right) => left.index - right.index)
.map(({ node }) => node)
return document.nodes.length ? document : null
}
@ -690,7 +727,7 @@ const PropertyEditor = ({
options?: string[]
onChange: (value: unknown) => void
}) => {
const [tailwindModalOpen, setTailwindModalOpen] = useState(false)
const [styleModalOpen, setStyleModalOpen] = useState(false)
const inputClass =
'w-full rounded-md border border-slate-300 bg-white px-2.5 py-2 text-xs text-slate-800 outline-none transition focus:border-sky-500 focus:ring-2 focus:ring-sky-100 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100'
@ -739,41 +776,49 @@ const PropertyEditor = ({
/>
)
}
if (name === 'className' || name === 'class' || name === 'css') {
const normalizedPropertyName = name.toLowerCase()
const isCssProperty = type === 'string' && normalizedPropertyName.includes('css')
const isClassProperty =
type === 'string' &&
(normalizedPropertyName === 'class' ||
normalizedPropertyName.endsWith('class') ||
normalizedPropertyName.endsWith('classname'))
if (isClassProperty || isCssProperty) {
const currentClasses = String(value ?? '')
return (
<>
<div className="flex gap-2">
<input
className={inputClass}
type="text"
value={currentClasses}
onChange={(event) => onChange(event.target.value)}
/>
{isCssProperty ? (
<textarea
className={`${inputClass} min-h-20 font-mono`}
placeholder="CSS kodunu yazın…"
value={currentClasses}
onChange={(event) => onChange(event.target.value)}
/>
) : (
<input
className={`${inputClass} font-mono`}
placeholder="CSS veya Tailwind class adlarını yazın…"
type="text"
value={currentClasses}
onChange={(event) => onChange(event.target.value)}
/>
)}
<Button
icon={<FaBolt />}
size="sm"
title="Tailwind sınıflarını seç"
title={`${name} stillerini StyleModal ile düzenle`}
variant="default"
onClick={() => setTailwindModalOpen(true)}
onClick={() => setStyleModalOpen(true)}
/>
</div>
<TailwindModal
currentValue={currentClasses}
isOpen={tailwindModalOpen}
onClose={() => setTailwindModalOpen(false)}
onSelectClass={(className) => {
if (!className) {
onChange('')
return
}
const classes = currentClasses.split(/\s+/).filter(Boolean)
onChange(
classes.includes(className)
? classes.filter((item) => item !== className).join(' ')
: [...classes, className].join(' '),
)
}}
<StyleModal
isOpen={styleModalOpen}
mode={isCssProperty ? 'css' : 'class'}
propertyName={name}
value={currentClasses}
onChange={onChange}
onClose={() => setStyleModalOpen(false)}
/>
</>
)
@ -791,6 +836,7 @@ const PropertyEditor = ({
return (
<input
className={inputClass}
placeholder={type === 'string' ? 'Metin veya ::LanguageKey' : undefined}
type={type === 'number' ? 'number' : name.toLowerCase().includes('color') ? 'color' : 'text'}
value={String(value ?? '')}
onChange={(event) =>
@ -802,8 +848,8 @@ const PropertyEditor = ({
const VisualComponentDesigner = () => {
const { id } = useParams()
const { translate } = useLocalization()
const { components, updateComponent, renderComponent } = useComponents()
const appThemeMode = useStoreState((state) => state.theme.mode)
const [componentDetails, setComponentDetails] = useState<CustomComponent | null>(null)
const [componentLoadError, setComponentLoadError] = useState('')
const [loadedId, setLoadedId] = useState<string | null>(null)
@ -1557,6 +1603,17 @@ const VisualComponentDesigner = () => {
[commitDocument],
)
const reorderNode = useCallback(
(sourceId: string, targetId: string, placement: 'before' | 'after') => {
commitDocument((current) => ({
...current,
nodes: reorderNodeTree(current.nodes, sourceId, targetId, placement),
}))
selectDesignerNode(sourceId)
},
[commitDocument, selectDesignerNode],
)
const collectDependencies = useCallback(() => {
const names = new Set<string>()
walkDesignerNodes(document.nodes, (node) => {
@ -1642,6 +1699,14 @@ const VisualComponentDesigner = () => {
useEffect(() => {
const keyHandler = (event: KeyboardEvent) => {
const target = event.target
const isPropertyEditing =
target instanceof HTMLElement &&
Boolean(
target.closest(
'[data-property-panel], input, textarea, select, [contenteditable="true"], [role="textbox"], .monaco-editor',
),
)
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 's') {
event.preventDefault()
void handleSave()
@ -1651,7 +1716,14 @@ const VisualComponentDesigner = () => {
if (event.shiftKey) redo()
else undo()
}
if (event.key === 'Delete' && selectedId && workspaceTab === 'design') deleteNode(selectedId)
if (
event.key === 'Delete' &&
!isPropertyEditing &&
selectedId &&
workspaceTab === 'design'
) {
deleteNode(selectedId)
}
}
window.addEventListener('keydown', keyHandler)
return () => window.removeEventListener('keydown', keyHandler)
@ -1940,9 +2012,7 @@ const VisualComponentDesigner = () => {
}, [activeDataSample, activeDataSource?.id, commitDocument, selectedId, selectedNode])
const dataBindableProperties =
selectedNode &&
selectedDefinition &&
DESIGNER_DATA_COMPONENT_NAMES.has(selectedNode.type)
selectedNode && selectedDefinition && DESIGNER_DATA_COMPONENT_NAMES.has(selectedNode.type)
? selectedDefinition.properties.filter(
(property) =>
property.category !== 'events' &&
@ -2334,7 +2404,6 @@ const VisualComponentDesigner = () => {
</div>
)
}
const selectModeSelector = renderSelectDataModeSelector()
const selectConfiguration = renderSelectDataConfiguration()
if (
@ -2344,9 +2413,9 @@ const VisualComponentDesigner = () => {
return (
<div className="space-y-3">
{selectModeSelector}
<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>
{isOptionDataComponent(selectedNode?.type) && selectConfiguration}
{isTabularDataComponent(selectedNode?.type) && renderTabularDataConfiguration()}
{isTabularDataComponent(selectedNode?.type) && renderTabularColumnConfiguration()}
</div>
)
}
@ -2635,13 +2704,13 @@ const VisualComponentDesigner = () => {
interactive
nodes={document.nodes}
selectedId={selectedId}
background={appThemeMode === 'dark' ? '#0f172a' : document.canvas.background}
dataValues={dataSourceSamples}
renderCustomComponent={(componentName, props) => renderComponent(componentName, props)}
onSelect={(nodeId) => selectDesignerNode(nodeId || null)}
onNodePropChange={updateNodeProp}
onDropComponent={addComponent}
onMove={moveNode}
onReorder={reorderNode}
onDuplicate={duplicateNode}
onDelete={deleteNode}
/>
@ -2738,7 +2807,7 @@ const VisualComponentDesigner = () => {
setSelectedId(null)
}}
>
Sıfırla
{translate('::Reset')}
</Button>
<Button
icon={<FaEye />}
@ -2752,7 +2821,7 @@ const VisualComponentDesigner = () => {
window.open(routePath, '_blank')
}}
>
{translate('::OpenUrl')}
</Button>
<Button
loading={isSaving}
@ -2761,7 +2830,7 @@ const VisualComponentDesigner = () => {
variant="solid"
onClick={() => void handleSave()}
>
Save
{translate('::Save')}
</Button>
</div>
</header>
@ -2823,10 +2892,7 @@ const VisualComponentDesigner = () => {
name: definition.name,
})
event.dataTransfer.effectAllowed = 'copy'
event.dataTransfer.setData(
DESIGNER_DRAG_TYPE,
payload,
)
event.dataTransfer.setData(DESIGNER_DRAG_TYPE, payload)
event.dataTransfer.setData('text/plain', payload)
}}
>
@ -3125,7 +3191,10 @@ const VisualComponentDesigner = () => {
</main>
{workspaceTab === 'design' && (
<aside className="flex w-72 shrink-0 flex-col border-l border-slate-200 bg-white dark:border-slate-800 dark:bg-slate-900">
<aside
data-property-panel
className="flex w-72 shrink-0 flex-col border-l border-slate-200 bg-white dark:border-slate-800 dark:bg-slate-900"
>
<div className="flex h-12 shrink-0 border-b border-slate-200 dark:border-slate-800">
{(
[
@ -3198,15 +3267,6 @@ const VisualComponentDesigner = () => {
</div>
{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) =>
@ -3222,8 +3282,7 @@ const VisualComponentDesigner = () => {
),
)
.map((property) => {
const propertyValue =
selectedNode.props[property.name] ?? property.value
const propertyValue = selectedNode.props[property.name] ?? property.value
const helperValue = getPropertyExampleValue(property, property.value)
const snippet = getPropertySnippet(property, helperValue)
return (
@ -3288,22 +3347,6 @@ const VisualComponentDesigner = () => {
Propertyleri görmek için canvastan bir bileşen seçin.
</p>
</div>
<label className="block">
<span className="mb-1.5 block text-[11px] font-medium text-slate-600 dark:text-slate-300">
Canvas background
</span>
<input
className="h-9 w-full rounded border border-slate-300"
type="color"
value={document.canvas.background}
onChange={(event) =>
commitDocument((current) => ({
...current,
canvas: { ...current.canvas, background: event.target.value },
}))
}
/>
</label>
</div>
)}
</div>

View file

@ -512,7 +512,7 @@ const IntranetDashboard: React.FC = () => {
className="!h-auto !items-center gap-1 !rounded-none !px-0 !py-0 text-sm text-gray-500 transition-colors hover:!bg-transparent hover:text-gray-700 active:!bg-transparent focus:!bg-transparent dark:text-gray-400 dark:hover:text-gray-200"
title={translate('::App.Platform.Intranet.Dashboard.ResetTitle')}
>
🔄 {translate('::App.Platform.Intranet.Dashboard.Reset')}
🔄 {translate('::Reset')}
</Button>
)}
</div>

View file

@ -84,12 +84,14 @@ function isLayoutValid(dto: GridDto, layout: ListViewLayoutType | undefined): bo
interface ListProps {
listFormCode?: string
gridDto?: GridDto
isSubForm?: boolean
onRefreshGridDto?: () => Promise<void> | void
}
const List: React.FC<ListProps> = ({
listFormCode: externalListFormCode,
gridDto: externalGridDto,
isSubForm = false,
onRefreshGridDto,
}) => {
const { listFormCode: routeListFormCode = '' } = useParams()
@ -316,7 +318,7 @@ const List: React.FC<ListProps> = ({
<Grid
listFormCode={listFormCode}
searchParams={searchParams}
isSubForm={false}
isSubForm={isSubForm}
gridDto={gridDto}
/>
)}
@ -325,7 +327,7 @@ const List: React.FC<ListProps> = ({
<CardView
listFormCode={listFormCode}
searchParams={searchParams}
isSubForm={false}
isSubForm={isSubForm}
gridDto={gridDto}
/>
)}
@ -334,7 +336,7 @@ const List: React.FC<ListProps> = ({
<Pivot
listFormCode={listFormCode}
searchParams={searchParams}
isSubForm={false}
isSubForm={isSubForm}
gridDto={gridDto}
refreshGridDto={refreshGridDto}
/>
@ -344,7 +346,7 @@ const List: React.FC<ListProps> = ({
<Tree
listFormCode={listFormCode}
searchParams={searchParams}
isSubForm={false}
isSubForm={isSubForm}
gridDto={gridDto}
/>
)}
@ -353,7 +355,7 @@ const List: React.FC<ListProps> = ({
<SchedulerView
listFormCode={listFormCode}
searchParams={searchParams}
isSubForm={false}
isSubForm={isSubForm}
gridDto={gridDto}
/>
)}
@ -362,7 +364,7 @@ const List: React.FC<ListProps> = ({
<GanttView
listFormCode={listFormCode}
searchParams={searchParams}
isSubForm={false}
isSubForm={isSubForm}
gridDto={gridDto}
/>
)}
@ -371,7 +373,7 @@ const List: React.FC<ListProps> = ({
<TodoBoard
listFormCode={listFormCode}
searchParams={searchParams}
isSubForm={false}
isSubForm={isSubForm}
gridDto={gridDto}
/>
)}

View file

@ -523,7 +523,11 @@ const useListFormColumns = ({
loadMode: 'raw',
load: async () => {
// load fonksiyonu sync sonuç döndürüyor olabilir, o yüzden Promise.resolve ile sar
return cachedLoader(`static:${key}`, () => Promise.resolve(load()))
return cachedLoader(`static:${key}`, async () => {
const result = await Promise.resolve(load())
if (result === undefined || result === null) return []
return Array.isArray(result) ? result : [result]
})
},
}),
sort,
@ -539,7 +543,7 @@ const useListFormColumns = ({
loadMode: 'raw',
load: async () => {
if (!isSubForm && listFormCode && !window.location.pathname.includes(listFormCode)) {
return
return []
}
try {
@ -559,8 +563,9 @@ const useListFormColumns = ({
...a,
}))
})
} catch {
return null
} catch (error) {
console.error('Lookup query data could not be loaded:', error)
return []
}
},
})
@ -577,10 +582,10 @@ const useListFormColumns = ({
loadMode: 'raw',
load: async () => {
if (!isSubForm && listFormCode && !window.location.pathname.includes(listFormCode)) {
return
return []
}
if (!lookupQuery) {
return
return []
}
const [method, url, body, keySelector, nameSelector, groupSelector] = lookupQuery.split(';')
@ -608,8 +613,9 @@ const useListFormColumns = ({
...a,
}))
})
} catch {
return
} catch (error) {
console.error('Lookup API data could not be loaded:', error)
return []
}
},
})
@ -897,9 +903,12 @@ const useListFormColumns = ({
// headerFilter
column.allowHeaderFiltering = colData.columnHeaderDto?.allowHeaderFiltering
if (column.allowHeaderFiltering == true) {
column.headerFilter = {}
column.headerFilter.allowSearch = colData.columnHeaderDto?.allowSearch
column.headerFilter.dataSource = colData.columnHeaderDto?.dataSource
column.headerFilter = {
dataSource: colData.columnHeaderDto?.dataSource,
search: {
enabled: colData.columnHeaderDto?.allowSearch,
},
}
}
// search

View file

@ -281,14 +281,15 @@ const useListFormCustomDataSource = ({
)
const retValue = {
data: response.data.data,
totalCount: response.data.totalCount,
data: Array.isArray(response.data?.data) ? response.data.data : [],
totalCount: Number(response.data?.totalCount) || 0,
summary: response.data.summary,
groupCount: response.data.groupCount,
}
return retValue
} catch {
} catch (error) {
console.error('List form data could not be loaded:', error)
// toast.push(
// <Notification type="danger" duration={2000}>
// Select error
@ -298,7 +299,12 @@ const useListFormCustomDataSource = ({
// placement: 'bottom-end',
// },
// )
return null
return {
data: [],
totalCount: 0,
summary: [],
groupCount: 0,
}
}
},
totalCount: async (loadOptions) => {
@ -334,7 +340,8 @@ const useListFormCustomDataSource = ({
try {
const response = await dynamicFetch('list-form-select/select', 'GET', parameters)
return response.data.totalCount
} catch {
} catch (error) {
console.error('List form total count could not be loaded:', error)
// toast.push(
// <Notification type="danger" duration={2000}>
// TotalCount error
@ -344,7 +351,7 @@ const useListFormCustomDataSource = ({
// placement: 'bottom-end',
// },
// )
return null
return 0
}
},
byKey: async (key) => {