Custom component güncellemeleri
This commit is contained in:
parent
9a76afae24
commit
24eb249f9f
20 changed files with 644 additions and 438 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
100
ui/src/components/codeLayout/StyleModal.tsx
Normal file
100
ui/src/components/codeLayout/StyleModal.tsx
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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}>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
+
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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)`
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
</>
|
||||
)
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
}}
|
||||
>
|
||||
Aç
|
||||
{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 = () => {
|
|||
Property’leri görmek için canvas’tan 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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue