Custom Component genel düzenlemeleri
This commit is contained in:
parent
4003dc1845
commit
870c98690c
12 changed files with 529 additions and 209 deletions
|
|
@ -20556,6 +20556,12 @@
|
||||||
"en": "Static options",
|
"en": "Static options",
|
||||||
"tr": "Statik seçenekler"
|
"tr": "Statik seçenekler"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"resourceName": "Platform",
|
||||||
|
"key": "App.DeveloperKitComponentDesigner.TabContentPlaceholder",
|
||||||
|
"en": "Tab content",
|
||||||
|
"tr": "Sekme içeriği"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"resourceName": "Platform",
|
"resourceName": "Platform",
|
||||||
"key": "App.DeveloperKitComponentDesigner.StaticRowsHint",
|
"key": "App.DeveloperKitComponentDesigner.StaticRowsHint",
|
||||||
|
|
@ -24909,8 +24915,8 @@
|
||||||
{
|
{
|
||||||
"resourceName": "Platform",
|
"resourceName": "Platform",
|
||||||
"key": "App.CatalogFlexRow.FlexRowDescription",
|
"key": "App.CatalogFlexRow.FlexRowDescription",
|
||||||
"en": "Horizontal and responsive layout",
|
"en": "Horizontal, responsive layout; a column count splits it into equal columns and a first column width makes it a sidebar",
|
||||||
"tr": "Yatay ve responsive yerleşim"
|
"tr": "Yatay ve responsive yerleşim; kolon sayısı eşit kolonlara böler, ilk kolon genişliği verilirse sidebar olur"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"resourceName": "Platform",
|
"resourceName": "Platform",
|
||||||
|
|
@ -24942,18 +24948,18 @@
|
||||||
"en": "Form",
|
"en": "Form",
|
||||||
"tr": "Form"
|
"tr": "Form"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"resourceName": "Platform",
|
|
||||||
"key": "App.CatalogHeaderContent.HeaderContentDescription",
|
|
||||||
"en": "Vertical layout for a header area and content below",
|
|
||||||
"tr": "Üst alan ve alt içerik için dikey yerleşim"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"resourceName": "Platform",
|
"resourceName": "Platform",
|
||||||
"key": "App.CatalogHoverable.HoverableDescription",
|
"key": "App.CatalogHoverable.HoverableDescription",
|
||||||
"en": "Shows a highlight color when hovering over rows",
|
"en": "Shows a highlight color when hovering over rows",
|
||||||
"tr": "Satırların üzerine gelindiğinde vurgu rengi gösterir"
|
"tr": "Satırların üzerine gelindiğinde vurgu rengi gösterir"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"resourceName": "Platform",
|
||||||
|
"key": "App.CatalogImg.ImgPlaceholderAlt",
|
||||||
|
"en": "Sample image",
|
||||||
|
"tr": "Örnek görsel"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"resourceName": "Platform",
|
"resourceName": "Platform",
|
||||||
"key": "App.CatalogItems.ItemsDescription",
|
"key": "App.CatalogItems.ItemsDescription",
|
||||||
|
|
@ -25038,12 +25044,6 @@
|
||||||
"en": "Select only shows the Label; the columns added on the Data tab travel with the option.",
|
"en": "Select only shows the Label; the columns added on the Data tab travel with the option.",
|
||||||
"tr": "Select yalnızca Label gösterir; Data sekmesinde eklenen sütunlar seçenekle birlikte taşınır."
|
"tr": "Select yalnızca Label gösterir; Data sekmesinde eklenen sütunlar seçenekle birlikte taşınır."
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"resourceName": "Platform",
|
|
||||||
"key": "App.CatalogSidebarContent.SidebarContentDescription",
|
|
||||||
"en": "Narrow sidebar and wide content area",
|
|
||||||
"tr": "Dar sidebar ve geniş içerik alanı"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"resourceName": "Platform",
|
"resourceName": "Platform",
|
||||||
"key": "App.CatalogSpacer.SpacerDescription",
|
"key": "App.CatalogSpacer.SpacerDescription",
|
||||||
|
|
@ -25188,12 +25188,6 @@
|
||||||
"en": "Sözsoft dynamic tree view",
|
"en": "Sözsoft dynamic tree view",
|
||||||
"tr": "Sözsoft dinamik ağaç görünümü"
|
"tr": "Sözsoft dinamik ağaç görünümü"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"resourceName": "Platform",
|
|
||||||
"key": "App.CatalogTwoColumns.TwoColumnsDescription",
|
|
||||||
"en": "Splits the page into two equal columns",
|
|
||||||
"tr": "Sayfayı iki eşit kolona böler"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"resourceName": "Platform",
|
"resourceName": "Platform",
|
||||||
"key": "App.VisualDesignerConditionKind.ComponentValue",
|
"key": "App.VisualDesignerConditionKind.ComponentValue",
|
||||||
|
|
@ -25746,30 +25740,6 @@
|
||||||
"en": "Enables the Delete button.",
|
"en": "Enables the Delete button.",
|
||||||
"tr": "Delete butonunu aktifleştirir."
|
"tr": "Delete butonunu aktifleştirir."
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"resourceName": "Platform",
|
|
||||||
"key": "App.VisualDesignerLayout.HeaderContent",
|
|
||||||
"en": "Header + content",
|
|
||||||
"tr": "Üst alan + içerik"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"resourceName": "Platform",
|
|
||||||
"key": "App.VisualDesignerLayout.PageContainer",
|
|
||||||
"en": "Page area",
|
|
||||||
"tr": "Sayfa alanı"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"resourceName": "Platform",
|
|
||||||
"key": "App.VisualDesignerLayout.SidebarContent",
|
|
||||||
"en": "Sidebar + content",
|
|
||||||
"tr": "Sidebar + içerik"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"resourceName": "Platform",
|
|
||||||
"key": "App.VisualDesignerLayout.TwoColumns",
|
|
||||||
"en": "Two columns",
|
|
||||||
"tr": "İki kolon"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"resourceName": "Platform",
|
"resourceName": "Platform",
|
||||||
"key": "App.SnippetApiCall.ApiCallDescription2",
|
"key": "App.SnippetApiCall.ApiCallDescription2",
|
||||||
|
|
|
||||||
15
configs/seeds/host/custom/NewComponent.json
Normal file
15
configs/seeds/host/custom/NewComponent.json
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
{
|
||||||
|
"GeneratedAt": "2026-08-20T07:44:44.3775836Z",
|
||||||
|
"CustomComponents": [
|
||||||
|
{
|
||||||
|
"Name": "NewComponent",
|
||||||
|
"RoutePath": "/admin/new-component",
|
||||||
|
"Code": "/*__SOZSOFT_VISUAL_DESIGNER__%7B%22version%22%3A1%2C%22sourceMode%22%3A%22visual%22%2C%22nodes%22%3A%5B%5D%2C%22canvas%22%3A%7B%22width%22%3A%22responsive%22%7D%2C%22lifecycle%22%3A%7B%22onMount%22%3A%22%22%7D%2C%22dataSources%22%3A%5B%5D%7D__*/\nconst NewComponent = () => {\n\n\n return (\n <>\n\n </>\n )\n}\n\nexport default NewComponent",
|
||||||
|
"Props": "{\"visualDesigner\":{\"version\":1,\"sourceMode\":\"visual\",\"nodes\":[],\"canvas\":{\"width\":\"responsive\"},\"lifecycle\":{\"onMount\":\"\"},\"dataSources\":[]}}",
|
||||||
|
"Description": "",
|
||||||
|
"IsActive": true,
|
||||||
|
"Dependencies": [],
|
||||||
|
"DataSources": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -1336,19 +1336,19 @@ export const CUSTOM_COMPONENTS: ComponentDefinition[] = [
|
||||||
{
|
{
|
||||||
key: 'tab1',
|
key: 'tab1',
|
||||||
label: 'Sekme 1',
|
label: 'Sekme 1',
|
||||||
children: 'İlk sekme içeriği',
|
children: '',
|
||||||
disabled: false,
|
disabled: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'tab2',
|
key: 'tab2',
|
||||||
label: 'Sekme 2',
|
label: 'Sekme 2',
|
||||||
children: 'İkinci sekme içeriği',
|
children: '',
|
||||||
disabled: false,
|
disabled: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'tab3',
|
key: 'tab3',
|
||||||
label: 'Sekme 3',
|
label: 'Sekme 3',
|
||||||
children: 'Üçüncü sekme içeriği',
|
children: '',
|
||||||
disabled: false,
|
disabled: false,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,8 @@ import {
|
||||||
} from '@/contexts/componentRuntime'
|
} from '@/contexts/componentRuntime'
|
||||||
import { useComponents } from '../../contexts/ComponentContext'
|
import { useComponents } from '../../contexts/ComponentContext'
|
||||||
import ErrorBoundary from './ErrorBoundary'
|
import ErrorBoundary from './ErrorBoundary'
|
||||||
|
import PlatformIcon from './PlatformIcon'
|
||||||
|
import PlatformSelectComponent from './selectComponents'
|
||||||
import PlatformViewHost from './PlatformViewHost'
|
import PlatformViewHost from './PlatformViewHost'
|
||||||
import { toast } from '../ui'
|
import { toast } from '../ui'
|
||||||
import { Loading } from '../shared'
|
import { Loading } from '../shared'
|
||||||
|
|
@ -49,6 +51,12 @@ const compileComponent = (
|
||||||
// code without an extra per-component entry here.
|
// code without an extra per-component entry here.
|
||||||
const staticComponents: Record<string, any> = {
|
const staticComponents: Record<string, any> = {
|
||||||
...UiKit,
|
...UiKit,
|
||||||
|
// Icon props travel as names, so generated code needs the same name resolver
|
||||||
|
// the designer canvas draws with.
|
||||||
|
PlatformIcon,
|
||||||
|
// Select.componentAs is stored as a name, so generated code needs the same
|
||||||
|
// resolver the designer canvas renders with.
|
||||||
|
PlatformSelectComponent,
|
||||||
PlatformViewHost,
|
PlatformViewHost,
|
||||||
toast,
|
toast,
|
||||||
apiService,
|
apiService,
|
||||||
|
|
|
||||||
24
ui/src/components/componentEditor/PlatformIcon.tsx
Normal file
24
ui/src/components/componentEditor/PlatformIcon.tsx
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
import { useNavigationIcons } from '@/proxy/menus/navigation-icon.config'
|
||||||
|
|
||||||
|
interface PlatformIconProps {
|
||||||
|
/** Icon name as stored in the designer, e.g. `FaUser`. */
|
||||||
|
name?: string
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders an icon from its name. Icon props are stored as names — that is what a
|
||||||
|
* picker can offer and what a document can carry — so both the designer canvas
|
||||||
|
* and the generated component need one place that turns a name into an element.
|
||||||
|
* An unknown or empty name renders nothing, so an unset icon leaves no gap.
|
||||||
|
*/
|
||||||
|
const PlatformIcon = ({ name, className }: PlatformIconProps) => {
|
||||||
|
const navigationIcon = useNavigationIcons()
|
||||||
|
if (!name) return null
|
||||||
|
const Icon = navigationIcon[name]
|
||||||
|
// The dictionary loads asynchronously; until it arrives there is nothing to
|
||||||
|
// draw, and the component re-renders on its own once it does.
|
||||||
|
return Icon ? <Icon className={className} /> : null
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PlatformIcon
|
||||||
23
ui/src/components/componentEditor/selectComponents.ts
Normal file
23
ui/src/components/componentEditor/selectComponents.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
import ReactSelect from 'react-select'
|
||||||
|
import CreatableSelect from 'react-select/creatable'
|
||||||
|
import AsyncSelect from 'react-select/async'
|
||||||
|
import type { ComponentType } from 'react'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `Select.componentAs` wants a component, but a designer document can only carry
|
||||||
|
* a name. These are the react-select flavours the Select is built to accept, and
|
||||||
|
* resolving the name in one place is what lets the canvas and the generated
|
||||||
|
* component agree on what a stored name means.
|
||||||
|
*/
|
||||||
|
const SELECT_COMPONENTS: Record<string, ComponentType<never>> = {
|
||||||
|
ReactSelect: ReactSelect as unknown as ComponentType<never>,
|
||||||
|
CreatableSelect: CreatableSelect as unknown as ComponentType<never>,
|
||||||
|
AsyncSelect: AsyncSelect as unknown as ComponentType<never>,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DESIGNER_SELECT_COMPONENT_NAMES = Object.keys(SELECT_COMPONENTS)
|
||||||
|
|
||||||
|
/** Unknown or empty name resolves to undefined, so Select keeps its own default. */
|
||||||
|
const PlatformSelectComponent = (name?: string) => (name ? SELECT_COMPONENTS[name] : undefined)
|
||||||
|
|
||||||
|
export default PlatformSelectComponent
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import * as UiKit from '@/components/ui'
|
import * as UiKit from '@/components/ui'
|
||||||
|
import PlatformIcon from '@/components/componentEditor/PlatformIcon'
|
||||||
|
import PlatformSelectComponent from '@/components/componentEditor/selectComponents'
|
||||||
import PlatformViewHost, {
|
import PlatformViewHost, {
|
||||||
type PlatformViewName,
|
type PlatformViewName,
|
||||||
} from '@/components/componentEditor/PlatformViewHost'
|
} from '@/components/componentEditor/PlatformViewHost'
|
||||||
|
|
@ -28,6 +30,9 @@ import {
|
||||||
isDesignerDateComponent,
|
isDesignerDateComponent,
|
||||||
isDesignerDateProperty,
|
isDesignerDateProperty,
|
||||||
isDesignerOptionComponent,
|
isDesignerOptionComponent,
|
||||||
|
DESIGNER_ICON_PROPERTIES,
|
||||||
|
DESIGNER_SELECT_COMPONENT_PROPERTY,
|
||||||
|
flexRowStyle,
|
||||||
isSqlDataSourceNode,
|
isSqlDataSourceNode,
|
||||||
LEGACY_SQL_DATA_SOURCE_TYPE,
|
LEGACY_SQL_DATA_SOURCE_TYPE,
|
||||||
normalizeDesignerKeyList,
|
normalizeDesignerKeyList,
|
||||||
|
|
@ -498,7 +503,21 @@ const getPreviewProps = (
|
||||||
// of being rendered.
|
// of being rendered.
|
||||||
if (key === SQL_DEFAULT_VALUE_PROP) return
|
if (key === SQL_DEFAULT_VALUE_PROP) return
|
||||||
if (key === 'children' || key === 'html' || (value === '' && key !== 'value')) return
|
if (key === 'children' || key === 'html' || (value === '' && key !== 'value')) return
|
||||||
|
// A zero width or height is the toolbox default, not a request for a zero
|
||||||
|
// sized element - emitting it would collapse the image to nothing.
|
||||||
|
if ((key === 'width' || key === 'height') && value === 0) return
|
||||||
if (key.startsWith('on') && typeof value === 'string') return
|
if (key.startsWith('on') && typeof value === 'string') return
|
||||||
|
// Stored as a name; Select expects the component itself.
|
||||||
|
if (key === DESIGNER_SELECT_COMPONENT_PROPERTY && typeof value === 'string') {
|
||||||
|
const component = PlatformSelectComponent(value)
|
||||||
|
if (component) props[key] = component
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Icons are stored as names; the component expects an element.
|
||||||
|
if (DESIGNER_ICON_PROPERTIES.has(key) && typeof value === 'string') {
|
||||||
|
props[key] = <PlatformIcon name={value} />
|
||||||
|
return
|
||||||
|
}
|
||||||
props[key] = resolveStaticLanguageKeys(value, translate)
|
props[key] = resolveStaticLanguageKeys(value, translate)
|
||||||
})
|
})
|
||||||
Object.entries(node.events).forEach(([name, script]) => {
|
Object.entries(node.events).forEach(([name, script]) => {
|
||||||
|
|
@ -1176,15 +1195,7 @@ const renderElement = (
|
||||||
}
|
}
|
||||||
if (node.type === 'FlexRow') {
|
if (node.type === 'FlexRow') {
|
||||||
return (
|
return (
|
||||||
<div
|
<div className={String(node.props.className || '')} style={flexRowStyle(node.props)}>
|
||||||
className={String(node.props.className || '')}
|
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
gap: Number(node.props.gap) || 0,
|
|
||||||
flexWrap: node.props.wrap ? 'wrap' : 'nowrap',
|
|
||||||
alignItems: String(node.props.align || 'stretch'),
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
@ -1207,33 +1218,6 @@ const renderElement = (
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (node.type === 'TwoColumns' || node.type === 'SidebarContent') {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={String(node.props.className || '')}
|
|
||||||
style={{
|
|
||||||
display: 'grid',
|
|
||||||
gap: Number(node.props.gap) || 0,
|
|
||||||
gridTemplateColumns:
|
|
||||||
node.type === 'SidebarContent'
|
|
||||||
? `${String(node.props.sidebarWidth || '280px')} minmax(0, 1fr)`
|
|
||||||
: 'repeat(2, minmax(0, 1fr))',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (node.type === 'HeaderContent') {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={String(node.props.className || '')}
|
|
||||||
style={{ display: 'flex', flexDirection: 'column', gap: Number(node.props.gap) || 0 }}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (node.type === 'Grid') {
|
if (node.type === 'Grid') {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|
@ -1299,6 +1283,13 @@ const renderElement = (
|
||||||
key={String(option.value ?? index)}
|
key={String(option.value ?? index)}
|
||||||
eventKey={String(option.value ?? index)}
|
eventKey={String(option.value ?? index)}
|
||||||
>
|
>
|
||||||
|
{/* An item without an icon keeps its plain layout, so a menu that
|
||||||
|
never had icons is left exactly as it was. */}
|
||||||
|
{typeof option.icon === 'string' && option.icon ? (
|
||||||
|
<span className="mr-2 inline-flex items-center">
|
||||||
|
<PlatformIcon name={option.icon} />
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
{String(option.label ?? option.value ?? `#${index + 1}`)}
|
{String(option.label ?? option.value ?? `#${index + 1}`)}
|
||||||
</UiKit.Menu.MenuItem>
|
</UiKit.Menu.MenuItem>
|
||||||
))}
|
))}
|
||||||
|
|
@ -1363,7 +1354,22 @@ const renderElement = (
|
||||||
if (node.type === 'checkbox') {
|
if (node.type === 'checkbox') {
|
||||||
return <input {...(props as React.InputHTMLAttributes<HTMLInputElement>)} type="checkbox" />
|
return <input {...(props as React.InputHTMLAttributes<HTMLInputElement>)} type="checkbox" />
|
||||||
}
|
}
|
||||||
if (node.kind === 'html') return React.createElement(node.type, props, content)
|
if (node.kind === 'html') {
|
||||||
|
// An empty element paints nothing, so on the canvas it would look like the
|
||||||
|
// drop never happened. The tag name stands in for it until it is given text
|
||||||
|
// or a child, and it is a designer only hint: the generated page never sees it.
|
||||||
|
const isEmpty = !hasOwnContent && React.Children.count(children) === 0
|
||||||
|
if (interactive && isEmpty && HTML_PLACEHOLDER_ELEMENTS.has(node.type)) {
|
||||||
|
return React.createElement(
|
||||||
|
node.type,
|
||||||
|
props,
|
||||||
|
// No size of its own: the placeholder inherits the element typography, so
|
||||||
|
// an h1 already looks like an h1 before it is given any text.
|
||||||
|
<span className="text-slate-400 dark:text-slate-500">{node.type}</span>,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return React.createElement(node.type, props, content)
|
||||||
|
}
|
||||||
|
|
||||||
const Component = resolveUiComponent(node.type)
|
const Component = resolveUiComponent(node.type)
|
||||||
if (!Component) {
|
if (!Component) {
|
||||||
|
|
@ -1376,6 +1382,20 @@ const renderElement = (
|
||||||
return React.createElement(Component, props, content)
|
return React.createElement(Component, props, content)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Elements that carry text, so an empty one can show its tag name on the canvas. */
|
||||||
|
const HTML_PLACEHOLDER_ELEMENTS = new Set([
|
||||||
|
'div',
|
||||||
|
'button',
|
||||||
|
'p',
|
||||||
|
'span',
|
||||||
|
'h1',
|
||||||
|
'h2',
|
||||||
|
'h3',
|
||||||
|
'h4',
|
||||||
|
'h5',
|
||||||
|
'h6',
|
||||||
|
])
|
||||||
|
|
||||||
const NodeView = ({
|
const NodeView = ({
|
||||||
node,
|
node,
|
||||||
index,
|
index,
|
||||||
|
|
@ -1436,9 +1456,6 @@ const NodeView = ({
|
||||||
const canMoveDown = !isRoot || index < siblingCount - 1
|
const canMoveDown = !isRoot || index < siblingCount - 1
|
||||||
const acceptsDroppedChildren = [
|
const acceptsDroppedChildren = [
|
||||||
'PageContainer',
|
'PageContainer',
|
||||||
'TwoColumns',
|
|
||||||
'SidebarContent',
|
|
||||||
'HeaderContent',
|
|
||||||
'FlexRow',
|
'FlexRow',
|
||||||
'div',
|
'div',
|
||||||
'Card',
|
'Card',
|
||||||
|
|
@ -2317,17 +2334,14 @@ const VisualCanvas = ({
|
||||||
{translate('::App.VisualDesignerCanvas.ChoosePageLayoutHint')}
|
{translate('::App.VisualDesignerCanvas.ChoosePageLayoutHint')}
|
||||||
</p>
|
</p>
|
||||||
{interactive && (
|
{interactive && (
|
||||||
<div className="mt-5 grid grid-cols-2 gap-2 sm:grid-cols-4">
|
<div className="mt-5 grid grid-cols-2 gap-2 sm:grid-cols-3">
|
||||||
{[
|
{[
|
||||||
['PageContainer', translate('::App.VisualDesignerLayout.PageContainer'), '□'],
|
// The component name itself is the label, so a layout is
|
||||||
['TwoColumns', translate('::App.VisualDesignerLayout.TwoColumns'), '▥'],
|
// called the same here as it is in the toolbox.
|
||||||
[
|
['PageContainer', '□'],
|
||||||
'SidebarContent',
|
['FlexRow', '▥'],
|
||||||
translate('::App.VisualDesignerLayout.SidebarContent'),
|
['Table', '▦'],
|
||||||
'◧',
|
].map(([name, icon]) => (
|
||||||
],
|
|
||||||
['HeaderContent', translate('::App.VisualDesignerLayout.HeaderContent'), '⊟'],
|
|
||||||
].map(([name, label, icon]) => (
|
|
||||||
<button
|
<button
|
||||||
key={name}
|
key={name}
|
||||||
className="rounded-lg border border-slate-200 bg-white p-3 text-xs font-semibold text-slate-600 shadow-sm transition hover:border-sky-400 hover:text-sky-700 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-300"
|
className="rounded-lg border border-slate-200 bg-white p-3 text-xs font-semibold text-slate-600 shadow-sm transition hover:border-sky-400 hover:text-sky-700 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-300"
|
||||||
|
|
@ -2338,7 +2352,7 @@ const VisualCanvas = ({
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span className="mb-1 block text-xl text-sky-500">{icon}</span>
|
<span className="mb-1 block text-xl text-sky-500">{icon}</span>
|
||||||
{label}
|
{name}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -70,6 +70,33 @@ const PROPERTY_OVERRIDES: Record<string, Record<string, PropertyOverride>> = {
|
||||||
children: { value: 'Galeri' },
|
children: { value: 'Galeri' },
|
||||||
},
|
},
|
||||||
Marquee: { gradient: { value: false }, speed: { value: 100 } },
|
Marquee: { gradient: { value: false }, speed: { value: 100 } },
|
||||||
|
// Stored as a name and resolved to the react-select flavour at render time; an
|
||||||
|
// empty value leaves Select on its own default.
|
||||||
|
Select: {
|
||||||
|
componentAs: {
|
||||||
|
type: 'select',
|
||||||
|
options: ['ReactSelect', 'CreatableSelect', 'AsyncSelect'],
|
||||||
|
value: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Curated definition, so the DropdownPlacement union never reaches the catalog.
|
||||||
|
Dropdown: {
|
||||||
|
placement: {
|
||||||
|
type: 'select',
|
||||||
|
options: [
|
||||||
|
'top-start',
|
||||||
|
'top-center',
|
||||||
|
'top-end',
|
||||||
|
'bottom-start',
|
||||||
|
'bottom-center',
|
||||||
|
'bottom-end',
|
||||||
|
'middle-start-top',
|
||||||
|
'middle-start-bottom',
|
||||||
|
'middle-end-top',
|
||||||
|
'middle-end-bottom',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
const HTML_UI_DUPLICATES = new Set(['input', 'button', 'textarea', 'select', 'checkbox'])
|
const HTML_UI_DUPLICATES = new Set(['input', 'button', 'textarea', 'select', 'checkbox'])
|
||||||
const UI_PROPERTY_ALIASES: Record<string, Record<string, string>> = {
|
const UI_PROPERTY_ALIASES: Record<string, Record<string, string>> = {
|
||||||
|
|
@ -499,9 +526,11 @@ const MENU_ITEMS_PROPERTY = {
|
||||||
const TABS_ITEMS_PROPERTY = {
|
const TABS_ITEMS_PROPERTY = {
|
||||||
...SELECT_OPTIONS_PROPERTY,
|
...SELECT_OPTIONS_PROPERTY,
|
||||||
name: 'items',
|
name: 'items',
|
||||||
|
// No content: a fresh tab is an empty container waiting for components, and
|
||||||
|
// placeholder text there would have to be deleted before anything is built.
|
||||||
value: [
|
value: [
|
||||||
{ label: 'Sekme 1', value: '1', content: 'Sekme 1 içeriği' },
|
{ label: 'Sekme 1', value: '1' },
|
||||||
{ label: 'Sekme 2', value: '2', content: 'Sekme 2 içeriği' },
|
{ label: 'Sekme 2', value: '2' },
|
||||||
],
|
],
|
||||||
description: 'App.CatalogTabs.TabsDescription',
|
description: 'App.CatalogTabs.TabsDescription',
|
||||||
tsType: 'Array<{ label: string; value: unknown; content?: string }>',
|
tsType: 'Array<{ label: string; value: unknown; content?: string }>',
|
||||||
|
|
@ -514,6 +543,50 @@ const getCollectionPropertyDefinition = (name: string, collectionProperty: strin
|
||||||
return collectionProperty === 'items' ? MENU_ITEMS_PROPERTY : SELECT_OPTIONS_PROPERTY
|
return collectionProperty === 'items' ? MENU_ITEMS_PROPERTY : SELECT_OPTIONS_PROPERTY
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inline placeholder artwork for a fresh `img`. Base64 rather than a raw SVG data
|
||||||
|
* URI, because the unescaped markup of the latter is rejected by strict URL
|
||||||
|
* parsing, and a data URI rather than a hosted file so nothing has to be fetched.
|
||||||
|
* The prop stays a plain string: any URL or data URI can replace it.
|
||||||
|
*/
|
||||||
|
const IMAGE_PLACEHOLDER_SRC =
|
||||||
|
'data:image/svg+xml;base64,' +
|
||||||
|
'PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHdpZHRoPSc2NDAnIGhlaWdodD0nNDAwJyB2aWV3Qm94PScwIDAgNjQwIDQwMCc+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSdzJyB4MT0nMCcgeTE9JzAnIHgyPScwJyB5Mj0nMSc+PHN0b3Agb2Zmc2V0PScwJyBzdG9wLWNvbG9yPScjZGJlYWZlJy8+PHN0b3Agb2Zmc2V0PScxJyBzdG9wLWNvbG9yPScjZjhmYWZjJy8+PC9saW5lYXJHcmFkaWVudD48bGluZWFyR3JhZGllbnQgaWQ9J2EnIHgxPScwJyB5MT0nMCcgeDI9JzEnIHkyPScxJz48c3RvcCBvZmZzZXQ9JzAnIHN0b3AtY29sb3I9JyM5M2M1ZmQnLz48c3RvcCBvZmZzZXQ9JzEnIHN0b3AtY29sb3I9JyMzOGJkZjgnLz48L2xpbmVhckdyYWRpZW50PjxsaW5lYXJHcmFkaWVudCBpZD0nYicgeDE9JzAnIHkxPScwJyB4Mj0nMScgeTI9JzEnPjxzdG9wIG9mZnNldD0nMCcgc3RvcC1jb2xvcj0nIzBlYTVlOScvPjxzdG9wIG9mZnNldD0nMScgc3RvcC1jb2xvcj0nIzBjNGE2ZScvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHdpZHRoPSc2NDAnIGhlaWdodD0nNDAwJyBmaWxsPSd1cmwoI3MpJy8+PGNpcmNsZSBjeD0nNDk4JyBjeT0nMTA0JyByPSc0NicgZmlsbD0nI2ZjZDM0ZCcvPjxwYXRoIGQ9J00wIDQwMCBMMTY1IDE2NSBMMzMwIDQwMCBaJyBmaWxsPSd1cmwoI2EpJy8+PHBhdGggZD0nTTI2MCA0MDAgTDQ0MCAxOTUgTDYyMCA0MDAgWicgZmlsbD0ndXJsKCNhKScvPjxwYXRoIGQ9J002MCA0MDAgTDI1MCAyMzAgTDQ0MCA0MDAgWicgZmlsbD0ndXJsKCNiKScvPjwvc3ZnPg=='
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Values a freshly dropped element starts with. Tailwind resets the headings, so
|
||||||
|
* without these an h1 is the size of a paragraph — here and in the generated page
|
||||||
|
* alike — and an empty img has nothing to show. They stay ordinary editable props.
|
||||||
|
*/
|
||||||
|
const HTML_DEFAULT_PROPS: Record<string, Record<string, unknown>> = {
|
||||||
|
h1: { className: 'text-4xl font-bold' },
|
||||||
|
h2: { className: 'text-3xl font-bold' },
|
||||||
|
h3: { className: 'text-2xl font-semibold' },
|
||||||
|
h4: { className: 'text-xl font-semibold' },
|
||||||
|
h5: { className: 'text-lg font-semibold' },
|
||||||
|
h6: { className: 'text-base font-semibold' },
|
||||||
|
img: {
|
||||||
|
src: IMAGE_PLACEHOLDER_SRC,
|
||||||
|
alt: 'App.CatalogImg.ImgPlaceholderAlt',
|
||||||
|
className: 'max-w-full rounded-md',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A prop with a known value set is picked from a list, never typed. Curated
|
||||||
|
* definitions and generated metadata both reach the panel through here, so a
|
||||||
|
* component gets the same treatment whichever of the two describes it.
|
||||||
|
*/
|
||||||
|
const withPickableOptions = <T extends { name: string; type: string; options?: string[] }>(
|
||||||
|
componentName: string,
|
||||||
|
property: T,
|
||||||
|
): T => {
|
||||||
|
const override = PROPERTY_OVERRIDES[componentName]?.[property.name]
|
||||||
|
const options = override?.options || property.options
|
||||||
|
if (!options || property.type === 'select') return property
|
||||||
|
return { ...property, type: 'select', options } as T
|
||||||
|
}
|
||||||
|
|
||||||
const normalizeDefinition = (
|
const normalizeDefinition = (
|
||||||
definition: (typeof CUSTOM_COMPONENTS)[number],
|
definition: (typeof CUSTOM_COMPONENTS)[number],
|
||||||
kind: DesignerNodeKind,
|
kind: DesignerNodeKind,
|
||||||
|
|
@ -547,6 +620,19 @@ const normalizeDefinition = (
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
properties = properties
|
||||||
|
.filter((property) => !HIDDEN_GENERATED_PROPERTIES.has(property.name))
|
||||||
|
.map((property) => withPickableOptions(definition.name, property))
|
||||||
|
|
||||||
|
const defaultProps = HTML_DEFAULT_PROPS[definition.name]
|
||||||
|
if (defaultProps) {
|
||||||
|
properties = properties.map((property) =>
|
||||||
|
defaultProps[property.name] !== undefined && !property.value
|
||||||
|
? { ...property, value: defaultProps[property.name] }
|
||||||
|
: property,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...definition,
|
...definition,
|
||||||
properties,
|
properties,
|
||||||
|
|
@ -591,41 +677,101 @@ const inferUiCategory = (name: string) => {
|
||||||
return 'display'
|
return 'display'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unions that the generated metadata carries only as a type name, so the value
|
||||||
|
* arrives as free text instead of a list. Naming the members here is what turns
|
||||||
|
* such a prop into a picker; keyed by the tsType, so one entry covers every
|
||||||
|
* component using it.
|
||||||
|
*/
|
||||||
|
const NAMED_UNION_OPTIONS: Record<string, string[]> = {
|
||||||
|
TabsVariant: ['underline', 'pill'],
|
||||||
|
// `ElementType` is every tag React knows; these are the ones a designer would
|
||||||
|
// reasonably render a component as.
|
||||||
|
ElementType: ['div', 'span', 'p', 'a', 'button', 'label', 'section', 'article'],
|
||||||
|
StepStatus: ['complete', 'pending', 'in-progress', 'error'],
|
||||||
|
StrokeLinecap: ['round', 'square'],
|
||||||
|
GapPosition: ['top', 'right', 'bottom', 'left'],
|
||||||
|
ArrowPlacement: [
|
||||||
|
'top',
|
||||||
|
'top-start',
|
||||||
|
'top-end',
|
||||||
|
'bottom',
|
||||||
|
'bottom-start',
|
||||||
|
'bottom-end',
|
||||||
|
'right',
|
||||||
|
'right-start',
|
||||||
|
'right-end',
|
||||||
|
'left',
|
||||||
|
'left-start',
|
||||||
|
'left-end',
|
||||||
|
],
|
||||||
|
HTMLInputTypeAttribute: [
|
||||||
|
'text',
|
||||||
|
'password',
|
||||||
|
'email',
|
||||||
|
'number',
|
||||||
|
'tel',
|
||||||
|
'url',
|
||||||
|
'search',
|
||||||
|
'date',
|
||||||
|
'time',
|
||||||
|
'datetime-local',
|
||||||
|
'month',
|
||||||
|
'week',
|
||||||
|
'color',
|
||||||
|
'file',
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Props that exist for code, not for the panel: `field` and `form` are the Formik
|
||||||
|
* objects a component is handed at runtime, so no value typed here could work.
|
||||||
|
*/
|
||||||
|
const HIDDEN_GENERATED_PROPERTIES = new Set(['field', 'form'])
|
||||||
|
|
||||||
const generatedPropertiesToDesigner = (
|
const generatedPropertiesToDesigner = (
|
||||||
generated: GeneratedComponentMetadata,
|
generated: GeneratedComponentMetadata,
|
||||||
existingProperties = new Map<string, (typeof CUSTOM_COMPONENTS)[number]['properties'][number]>(),
|
existingProperties = new Map<string, (typeof CUSTOM_COMPONENTS)[number]['properties'][number]>(),
|
||||||
componentName = '',
|
componentName = '',
|
||||||
) =>
|
) =>
|
||||||
generated.properties.map((property) => {
|
generated.properties
|
||||||
const existing = existingProperties.get(property.name)
|
.filter((property) => !HIDDEN_GENERATED_PROPERTIES.has(property.name))
|
||||||
const override = PROPERTY_OVERRIDES[componentName]?.[property.name]
|
.map((property) => {
|
||||||
const type = override?.type || property.type
|
const existing = existingProperties.get(property.name)
|
||||||
return {
|
const override = PROPERTY_OVERRIDES[componentName]?.[property.name]
|
||||||
name: property.name,
|
const options = override?.type
|
||||||
type,
|
|
||||||
value:
|
|
||||||
override && 'value' in override
|
|
||||||
? override.value
|
|
||||||
: property.defaultValue !== undefined
|
|
||||||
? property.defaultValue
|
|
||||||
: existing?.value !== undefined
|
|
||||||
? existing.value
|
|
||||||
: fallbackValue(type),
|
|
||||||
options: override?.type
|
|
||||||
? override.options
|
? override.options
|
||||||
: override?.options || property.options || existing?.options,
|
: override?.options ||
|
||||||
description: property.tsType,
|
property.options ||
|
||||||
category:
|
existing?.options ||
|
||||||
property.type === 'function'
|
NAMED_UNION_OPTIONS[property.tsType]
|
||||||
? ('events' as const)
|
// A prop with a known set of values is picked from a list, never typed: the
|
||||||
: existing?.category ||
|
// generated metadata often keeps the union as a plain string.
|
||||||
(property.name === 'className' || property.name === 'style'
|
const type = override?.type || (options && property.type === 'string' ? 'select' : property.type)
|
||||||
? ('styling' as const)
|
return {
|
||||||
: ('properties' as const)),
|
name: property.name,
|
||||||
tsType: property.tsType,
|
type,
|
||||||
required: property.required,
|
value:
|
||||||
}
|
override && 'value' in override
|
||||||
})
|
? override.value
|
||||||
|
: property.defaultValue !== undefined
|
||||||
|
? property.defaultValue
|
||||||
|
: existing?.value !== undefined
|
||||||
|
? existing.value
|
||||||
|
: fallbackValue(type),
|
||||||
|
options,
|
||||||
|
description: property.tsType,
|
||||||
|
category:
|
||||||
|
property.type === 'function'
|
||||||
|
? ('events' as const)
|
||||||
|
: existing?.category ||
|
||||||
|
(property.name === 'className' || property.name === 'style'
|
||||||
|
? ('styling' as const)
|
||||||
|
: ('properties' as const)),
|
||||||
|
tsType: property.tsType,
|
||||||
|
required: property.required,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const normalizeUiDefinition = (
|
const normalizeUiDefinition = (
|
||||||
definition: (typeof CUSTOM_COMPONENTS)[number],
|
definition: (typeof CUSTOM_COMPONENTS)[number],
|
||||||
|
|
@ -682,11 +828,18 @@ const normalizeUiDefinition = (
|
||||||
...generatedProperties,
|
...generatedProperties,
|
||||||
// Keep explicitly curated inherited React props such as Input.value and placeholder.
|
// Keep explicitly curated inherited React props such as Input.value and placeholder.
|
||||||
...curatedProperties.filter((property) => !generatedPropertyNames.has(property.name)),
|
...curatedProperties.filter((property) => !generatedPropertyNames.has(property.name)),
|
||||||
].filter(
|
]
|
||||||
(property) =>
|
.filter(
|
||||||
!(definition.name === 'Checkbox' && property.name === 'defaultChecked') &&
|
(property) =>
|
||||||
!(definition.name === 'Menu' && property.name === 'variant'),
|
!(definition.name === 'Checkbox' && property.name === 'defaultChecked') &&
|
||||||
)
|
!(definition.name === 'Menu' && property.name === 'variant') &&
|
||||||
|
// A curated prop can be code-only too, and it never reaches the generated
|
||||||
|
// filter above.
|
||||||
|
!HIDDEN_GENERATED_PROPERTIES.has(property.name),
|
||||||
|
)
|
||||||
|
// A curated prop carries no tsType, so its value set arrives either as its own
|
||||||
|
// options or through an override; both become a picker here.
|
||||||
|
.map((property) => withPickableOptions(definition.name, property))
|
||||||
const primaryEvents = DESIGNER_PRIMARY_EVENTS[definition.name]
|
const primaryEvents = DESIGNER_PRIMARY_EVENTS[definition.name]
|
||||||
return {
|
return {
|
||||||
...definition,
|
...definition,
|
||||||
|
|
@ -898,49 +1051,6 @@ export const DESIGNER_EXTRAS: DesignerComponentDefinition[] = [
|
||||||
],
|
],
|
||||||
hooks: [],
|
hooks: [],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: 'TwoColumns',
|
|
||||||
icon: 'Columns2',
|
|
||||||
category: 'layout',
|
|
||||||
kind: 'layout',
|
|
||||||
toolboxGroup: 'layout',
|
|
||||||
description: 'App.CatalogTwoColumns.TwoColumnsDescription',
|
|
||||||
acceptsChildren: true,
|
|
||||||
properties: [
|
|
||||||
{ name: 'gap', type: 'number', value: 16, category: 'styling' },
|
|
||||||
{ name: 'className', type: 'string', value: '', category: 'styling' },
|
|
||||||
],
|
|
||||||
hooks: [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'SidebarContent',
|
|
||||||
icon: 'PanelLeft',
|
|
||||||
category: 'layout',
|
|
||||||
kind: 'layout',
|
|
||||||
toolboxGroup: 'layout',
|
|
||||||
description: 'App.CatalogSidebarContent.SidebarContentDescription',
|
|
||||||
acceptsChildren: true,
|
|
||||||
properties: [
|
|
||||||
{ name: 'sidebarWidth', type: 'string', value: '280px', category: 'styling' },
|
|
||||||
{ name: 'gap', type: 'number', value: 16, category: 'styling' },
|
|
||||||
{ name: 'className', type: 'string', value: '', category: 'styling' },
|
|
||||||
],
|
|
||||||
hooks: [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'HeaderContent',
|
|
||||||
icon: 'PanelTop',
|
|
||||||
category: 'layout',
|
|
||||||
kind: 'layout',
|
|
||||||
toolboxGroup: 'layout',
|
|
||||||
description: 'App.CatalogHeaderContent.HeaderContentDescription',
|
|
||||||
acceptsChildren: true,
|
|
||||||
properties: [
|
|
||||||
{ name: 'gap', type: 'number', value: 16, category: 'styling' },
|
|
||||||
{ name: 'className', type: 'string', value: '', category: 'styling' },
|
|
||||||
],
|
|
||||||
hooks: [],
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: 'FlexRow',
|
name: 'FlexRow',
|
||||||
icon: 'Columns3',
|
icon: 'Columns3',
|
||||||
|
|
@ -950,6 +1060,12 @@ export const DESIGNER_EXTRAS: DesignerComponentDefinition[] = [
|
||||||
description: 'App.CatalogFlexRow.FlexRowDescription',
|
description: 'App.CatalogFlexRow.FlexRowDescription',
|
||||||
acceptsChildren: true,
|
acceptsChildren: true,
|
||||||
properties: [
|
properties: [
|
||||||
|
// 1 keeps the free flowing flex row; 2 or more turns the row into that many
|
||||||
|
// equal grid columns, which is what a two column or sidebar page needs.
|
||||||
|
{ name: 'columns', type: 'number', value: 1, category: 'styling' },
|
||||||
|
// Only read in column mode: a width here pins the first column instead of
|
||||||
|
// sharing the row equally, which is what makes a sidebar layout.
|
||||||
|
{ name: 'firstColumnWidth', type: 'string', value: '', category: 'styling' },
|
||||||
{ name: 'gap', type: 'number', value: 16, category: 'styling' },
|
{ name: 'gap', type: 'number', value: 16, category: 'styling' },
|
||||||
{ name: 'wrap', type: 'boolean', value: true, category: 'styling' },
|
{ name: 'wrap', type: 'boolean', value: true, category: 'styling' },
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,9 @@ import {
|
||||||
isDesignerDateOnlyComponent,
|
isDesignerDateOnlyComponent,
|
||||||
isDesignerDateProperty,
|
isDesignerDateProperty,
|
||||||
isDesignerOptionComponent,
|
isDesignerOptionComponent,
|
||||||
|
DESIGNER_ICON_PROPERTIES,
|
||||||
|
DESIGNER_SELECT_COMPONENT_PROPERTY,
|
||||||
|
flexRowStyle,
|
||||||
isSqlDataSourceNode,
|
isSqlDataSourceNode,
|
||||||
isValuelessDesignerFilter,
|
isValuelessDesignerFilter,
|
||||||
DESIGNER_FILTER_DX_OPERATORS,
|
DESIGNER_FILTER_DX_OPERATORS,
|
||||||
|
|
@ -234,12 +237,22 @@ const propsToCode = (
|
||||||
// node happens to carry from the toolbox defaults.
|
// node happens to carry from the toolbox defaults.
|
||||||
!(formField && (key === 'readOnly' || key === 'disabled')) &&
|
!(formField && (key === 'readOnly' || key === 'disabled')) &&
|
||||||
!node.bindings?.[key]?.sourceId &&
|
!node.bindings?.[key]?.sourceId &&
|
||||||
|
// A zero width or height is the toolbox default, not a request for a zero
|
||||||
|
// sized element - emitting it would collapse the image to nothing.
|
||||||
|
!((key === 'width' || key === 'height') && value === 0) &&
|
||||||
value !== '' &&
|
value !== '' &&
|
||||||
value !== undefined,
|
value !== undefined,
|
||||||
)
|
)
|
||||||
.map(([key, value]) =>
|
.map(([key, value]) =>
|
||||||
// Dates live in the document as ISO strings; pickers need Date instances.
|
// An icon is stored as a name; PlatformIcon turns it back into an element at
|
||||||
isDesignerDateProperty(node.type, key)
|
// runtime and renders nothing for an unknown one.
|
||||||
|
key === DESIGNER_SELECT_COMPONENT_PROPERTY && typeof value === 'string'
|
||||||
|
? // The name is resolved at runtime, where the react-select flavours live.
|
||||||
|
`${key}={PlatformSelectComponent(${JSON.stringify(value)})}`
|
||||||
|
: DESIGNER_ICON_PROPERTIES.has(key) && typeof value === 'string'
|
||||||
|
? `${key}={<PlatformIcon name=${JSON.stringify(value)} />}`
|
||||||
|
: // Dates live in the document as ISO strings; pickers need Date instances.
|
||||||
|
isDesignerDateProperty(node.type, key)
|
||||||
? `${key}={toDesignerDate(${staticValueExpression(value)}, ${isDesignerDateOnlyComponent(node.type)})}`
|
? `${key}={toDesignerDate(${staticValueExpression(value)}, ${isDesignerDateOnlyComponent(node.type)})}`
|
||||||
: `${key}=${serializeValue(value)}`,
|
: `${key}=${serializeValue(value)}`,
|
||||||
)
|
)
|
||||||
|
|
@ -828,7 +841,7 @@ const renderNodeToCode = (
|
||||||
}
|
}
|
||||||
|
|
||||||
if (node.type === 'FlexRow') {
|
if (node.type === 'FlexRow') {
|
||||||
const style = `{ display: "flex", gap: ${Number(node.props.gap) || 0}, flexWrap: "${node.props.wrap ? 'wrap' : 'nowrap'}", alignItems: "${String(node.props.align || 'stretch')}" }`
|
const style = JSON.stringify(flexRowStyle(node.props))
|
||||||
const className = node.props.className
|
const className = node.props.className
|
||||||
? ` className=${JSON.stringify(node.props.className)}`
|
? ` className=${JSON.stringify(node.props.className)}`
|
||||||
: ''
|
: ''
|
||||||
|
|
@ -838,20 +851,10 @@ const renderNodeToCode = (
|
||||||
return `${indent(`<div${className} style={${style}}>`, level)}\n${children}\n${indent('</div>', level)}`
|
return `${indent(`<div${className} style={${style}}>`, level)}\n${children}\n${indent('</div>', level)}`
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (node.type === 'PageContainer') {
|
||||||
node.type === 'PageContainer' ||
|
|
||||||
node.type === 'TwoColumns' ||
|
|
||||||
node.type === 'SidebarContent' ||
|
|
||||||
node.type === 'HeaderContent'
|
|
||||||
) {
|
|
||||||
const gap = Number(node.props.gap) || 0
|
const gap = Number(node.props.gap) || 0
|
||||||
const className = JSON.stringify(String(node.props.className || ''))
|
const className = JSON.stringify(String(node.props.className || ''))
|
||||||
const style =
|
const style = `{ display: "flex", flexDirection: "column", gap: ${gap}, margin: "0 auto", maxWidth: ${JSON.stringify(String(node.props.maxWidth || '1280px'))}, padding: ${Number(node.props.padding) || 0}, width: "100%" }`
|
||||||
node.type === 'PageContainer'
|
|
||||||
? `{ display: "flex", flexDirection: "column", gap: ${gap}, margin: "0 auto", maxWidth: ${JSON.stringify(String(node.props.maxWidth || '1280px'))}, padding: ${Number(node.props.padding) || 0}, width: "100%" }`
|
|
||||||
: node.type === 'HeaderContent'
|
|
||||||
? `{ display: "flex", flexDirection: "column", gap: ${gap} }`
|
|
||||||
: `{ display: "grid", gap: ${gap}, gridTemplateColumns: ${JSON.stringify(node.type === 'SidebarContent' ? `${String(node.props.sidebarWidth || '280px')} minmax(0, 1fr)` : 'repeat(2, minmax(0, 1fr))')} }`
|
|
||||||
const children = node.children
|
const children = node.children
|
||||||
.map((child) => nodeToCode(child, level + 1, itemVariable, formScope))
|
.map((child) => nodeToCode(child, level + 1, itemVariable, formScope))
|
||||||
.join('\n')
|
.join('\n')
|
||||||
|
|
@ -1048,7 +1051,7 @@ ${indent(')}', level)}`
|
||||||
menuProps.defaultExpandedKeys = normalizeDesignerKeyList(menuProps.defaultExpandedKeys)
|
menuProps.defaultExpandedKeys = normalizeDesignerKeyList(menuProps.defaultExpandedKeys)
|
||||||
const menuPropsCode = propsToCode(menuNode, itemVariable, [], formScope)
|
const menuPropsCode = propsToCode(menuNode, itemVariable, [], formScope)
|
||||||
const optionsExpression = optionCollectionExpression(node, itemVariable)
|
const optionsExpression = optionCollectionExpression(node, itemVariable)
|
||||||
return `${indent(`<UiKit.Menu${menuPropsCode}>`, level)}\n${indent(`{${optionsExpression}.map((option, optionIndex) => <UiKit.Menu.MenuItem key={String(option.value ?? optionIndex)} eventKey={String(option.value ?? optionIndex)}>{String(option.label ?? option.value ?? \`Menü \${optionIndex + 1}\`)}</UiKit.Menu.MenuItem>)}`, level + 1)}\n${indent('</UiKit.Menu>', level)}`
|
return `${indent(`<UiKit.Menu${menuPropsCode}>`, level)}\n${indent(`{${optionsExpression}.map((option, optionIndex) => <UiKit.Menu.MenuItem key={String(option.value ?? optionIndex)} eventKey={String(option.value ?? optionIndex)}>{option.icon ? <span className="mr-2 inline-flex items-center"><PlatformIcon name={String(option.icon)} /></span> : null}{String(option.label ?? option.value ?? \`Menü \${optionIndex + 1}\`)}</UiKit.Menu.MenuItem>)}`, level + 1)}\n${indent('</UiKit.Menu>', level)}`
|
||||||
}
|
}
|
||||||
|
|
||||||
if (node.type === 'Dropdown') {
|
if (node.type === 'Dropdown') {
|
||||||
|
|
|
||||||
|
|
@ -764,6 +764,40 @@ export const SQL_DATA_SOURCE_TYPE = 'Form'
|
||||||
*/
|
*/
|
||||||
export const LEGACY_SQL_DATA_SOURCE_TYPE = 'SqlDataSource'
|
export const LEGACY_SQL_DATA_SOURCE_TYPE = 'SqlDataSource'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Layout of a FlexRow. A single column is a plain flex row - items keep their own
|
||||||
|
* width and may wrap. Two or more switch to a grid so the columns really line up,
|
||||||
|
* and a `firstColumnWidth` pins the first one, which is how a sidebar is built.
|
||||||
|
*/
|
||||||
|
export const flexRowStyle = (props: Record<string, unknown>) => {
|
||||||
|
const gap = Number(props.gap) || 0
|
||||||
|
const alignItems = String(props.align || 'stretch')
|
||||||
|
const columns = Math.min(12, Math.max(1, Math.floor(Number(props.columns) || 1)))
|
||||||
|
if (columns < 2) {
|
||||||
|
return {
|
||||||
|
display: 'flex' as const,
|
||||||
|
gap,
|
||||||
|
flexWrap: (props.wrap ? 'wrap' : 'nowrap') as 'wrap' | 'nowrap',
|
||||||
|
alignItems,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const firstColumnWidth = String(props.firstColumnWidth || '').trim()
|
||||||
|
return {
|
||||||
|
display: 'grid' as const,
|
||||||
|
gap,
|
||||||
|
alignItems,
|
||||||
|
gridTemplateColumns: firstColumnWidth
|
||||||
|
? `${firstColumnWidth} repeat(${columns - 1}, minmax(0, 1fr))`
|
||||||
|
: `repeat(${columns}, minmax(0, 1fr))`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** `Select.componentAs` holds the name of a react-select flavour. */
|
||||||
|
export const DESIGNER_SELECT_COMPONENT_PROPERTY = 'componentAs'
|
||||||
|
|
||||||
|
/** Props whose stored value is an icon name rather than an element. */
|
||||||
|
export const DESIGNER_ICON_PROPERTIES = new Set(['icon', 'customIcon'])
|
||||||
|
|
||||||
export const isSqlDataSourceNode = (type?: string) =>
|
export const isSqlDataSourceNode = (type?: string) =>
|
||||||
type === SQL_DATA_SOURCE_TYPE || type === LEGACY_SQL_DATA_SOURCE_TYPE
|
type === SQL_DATA_SOURCE_TYPE || type === LEGACY_SQL_DATA_SOURCE_TYPE
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,7 @@ import {
|
||||||
getSqlRecordProperty,
|
getSqlRecordProperty,
|
||||||
isDesignerDateComponent,
|
isDesignerDateComponent,
|
||||||
isDesignerDateOnlyComponent,
|
isDesignerDateOnlyComponent,
|
||||||
|
DESIGNER_ICON_PROPERTIES,
|
||||||
isDesignerOptionComponent,
|
isDesignerOptionComponent,
|
||||||
isDesignerTabularComponent,
|
isDesignerTabularComponent,
|
||||||
isSqlDataSourceNode,
|
isSqlDataSourceNode,
|
||||||
|
|
@ -115,6 +116,7 @@ import {
|
||||||
type DesignerPropertyInfo,
|
type DesignerPropertyInfo,
|
||||||
type SqlFormValueProperty,
|
type SqlFormValueProperty,
|
||||||
} from '@/components/visualDesigner/types'
|
} from '@/components/visualDesigner/types'
|
||||||
|
import { IconPickerField } from '@/views/shared/MenuAddDialog'
|
||||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||||
|
|
||||||
type WorkspaceTab = 'design' | 'data' | 'code' | 'javascript'
|
type WorkspaceTab = 'design' | 'data' | 'code' | 'javascript'
|
||||||
|
|
@ -122,7 +124,7 @@ type InspectorTab = 'properties' | 'data' | 'tree'
|
||||||
type DataTestResult = { status: 'loading' | 'success' | 'error'; message: string }
|
type DataTestResult = { status: 'loading' | 'success' | 'error'; message: string }
|
||||||
type EndpointResultModal = { source: DesignerDataSource; result: unknown }
|
type EndpointResultModal = { source: DesignerDataSource; result: unknown }
|
||||||
type DataField = { path: string; type: string; preview: string }
|
type DataField = { path: string; type: string; preview: string }
|
||||||
type StaticSelectOption = { label: string; value: unknown }
|
type StaticSelectOption = { label: string; value: unknown; content?: string; icon?: string }
|
||||||
type DataSourceCatalogItem = {
|
type DataSourceCatalogItem = {
|
||||||
attached: boolean
|
attached: boolean
|
||||||
key: string
|
key: string
|
||||||
|
|
@ -357,6 +359,12 @@ const TOOLBOX_GROUP_ORDER: DesignerComponentDefinition['toolboxGroup'][] = [
|
||||||
'custom',
|
'custom',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The layout group reads outside in - page, row, table - instead of alphabetically,
|
||||||
|
* because that is the order a page is actually built in.
|
||||||
|
*/
|
||||||
|
const LAYOUT_TOOLBOX_ORDER = ['PageContainer', 'FlexRow', 'Table']
|
||||||
|
|
||||||
const CANVAS_WIDTHS: Record<DesignerDocument['canvas']['width'], string> = {
|
const CANVAS_WIDTHS: Record<DesignerDocument['canvas']['width'], string> = {
|
||||||
responsive: '100%',
|
responsive: '100%',
|
||||||
desktop: '1180px',
|
desktop: '1180px',
|
||||||
|
|
@ -575,14 +583,39 @@ const reorderNodeTree = (
|
||||||
return insertRelativeToNode(removeNodeTree(nodes, sourceId), targetId, movingNode, placement)
|
return insertRelativeToNode(removeNodeTree(nodes, sourceId), targetId, movingNode, placement)
|
||||||
}
|
}
|
||||||
|
|
||||||
const definitionToNode = (definition: DesignerComponentDefinition): DesignerNode => {
|
/**
|
||||||
|
* Default values that ship as localization keys - option labels, menu captions -
|
||||||
|
* so a new component starts with readable text instead of `App.Catalog...`. The
|
||||||
|
* resolved text is what gets stored, because from that point on it is ordinary
|
||||||
|
* user content the designer edits like any other value.
|
||||||
|
*/
|
||||||
|
const localizeDesignerDefaults = (value: unknown, translate: TranslateFn): unknown => {
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
return value.startsWith('App.') ? translate('::' + value) : value
|
||||||
|
}
|
||||||
|
if (Array.isArray(value)) return value.map((item) => localizeDesignerDefaults(item, translate))
|
||||||
|
if (value && typeof value === 'object') {
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.entries(value as Record<string, unknown>).map(([key, item]) => [
|
||||||
|
key,
|
||||||
|
localizeDesignerDefaults(item, translate),
|
||||||
|
]),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
const definitionToNode = (
|
||||||
|
definition: DesignerComponentDefinition,
|
||||||
|
translate: TranslateFn,
|
||||||
|
): DesignerNode => {
|
||||||
const props: Record<string, unknown> = {}
|
const props: Record<string, unknown> = {}
|
||||||
const events: Record<string, string> = {}
|
const events: Record<string, string> = {}
|
||||||
definition.properties.forEach((property) => {
|
definition.properties.forEach((property) => {
|
||||||
if (property.category === 'events' || property.type === 'function') {
|
if (property.category === 'events' || property.type === 'function') {
|
||||||
events[property.name] = typeof property.value === 'string' ? property.value : ''
|
events[property.name] = typeof property.value === 'string' ? property.value : ''
|
||||||
} else {
|
} else {
|
||||||
props[property.name] = clone(property.value)
|
props[property.name] = localizeDesignerDefaults(clone(property.value), translate)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return {
|
return {
|
||||||
|
|
@ -596,6 +629,18 @@ const definitionToNode = (definition: DesignerComponentDefinition): DesignerNode
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Node types that were renamed or merged away. Rewriting them on load is what
|
||||||
|
* makes an existing page show its current type everywhere it names itself - the
|
||||||
|
* toolbox, the layer tree and the inspector all read it.
|
||||||
|
*/
|
||||||
|
const LEGACY_NODE_TYPES: Record<string, string> = {
|
||||||
|
[LEGACY_SQL_DATA_SOURCE_TYPE]: SQL_DATA_SOURCE_TYPE,
|
||||||
|
TwoColumns: 'FlexRow',
|
||||||
|
SidebarContent: 'FlexRow',
|
||||||
|
HeaderContent: 'PageContainer',
|
||||||
|
}
|
||||||
|
|
||||||
const normalizeDesignerDocument = (document: DesignerDocument): DesignerDocument => {
|
const normalizeDesignerDocument = (document: DesignerDocument): DesignerDocument => {
|
||||||
const normalizeNodes = (nodes: DesignerNode[]): DesignerNode[] =>
|
const normalizeNodes = (nodes: DesignerNode[]): DesignerNode[] =>
|
||||||
nodes.map((node) => {
|
nodes.map((node) => {
|
||||||
|
|
@ -629,6 +674,20 @@ const normalizeDesignerDocument = (document: DesignerDocument): DesignerDocument
|
||||||
delete props.variant
|
delete props.variant
|
||||||
delete bindings.options
|
delete bindings.options
|
||||||
}
|
}
|
||||||
|
// The two column, sidebar and header layouts were folded away: a column
|
||||||
|
// count and an optional pinned first column turn FlexRow into the first
|
||||||
|
// two, and the header stack is a PageContainer without the page framing.
|
||||||
|
if (node.type === 'TwoColumns' || node.type === 'SidebarContent') {
|
||||||
|
props.columns = 2
|
||||||
|
if (node.type === 'SidebarContent') {
|
||||||
|
props.firstColumnWidth = String(props.firstColumnWidth || props.sidebarWidth || '280px')
|
||||||
|
}
|
||||||
|
delete props.sidebarWidth
|
||||||
|
}
|
||||||
|
if (node.type === 'HeaderContent') {
|
||||||
|
if (props.maxWidth === undefined) props.maxWidth = '100%'
|
||||||
|
if (props.padding === undefined) props.padding = 0
|
||||||
|
}
|
||||||
if (node.type === 'Table') {
|
if (node.type === 'Table') {
|
||||||
const legacyRows = Array.isArray(props.items)
|
const legacyRows = Array.isArray(props.items)
|
||||||
? props.items
|
? props.items
|
||||||
|
|
@ -664,10 +723,7 @@ const normalizeDesignerDocument = (document: DesignerDocument): DesignerDocument
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
...node,
|
...node,
|
||||||
// The container used to be called SqlDataSource. Renaming it on load is
|
type: LEGACY_NODE_TYPES[node.type] || node.type,
|
||||||
// what makes an existing page show `Form` everywhere it names its own
|
|
||||||
// type — the toolbox, the layer tree and the inspector all read it.
|
|
||||||
type: node.type === LEGACY_SQL_DATA_SOURCE_TYPE ? SQL_DATA_SOURCE_TYPE : node.type,
|
|
||||||
props,
|
props,
|
||||||
events,
|
events,
|
||||||
bindings,
|
bindings,
|
||||||
|
|
@ -1123,6 +1179,11 @@ const PropertyEditor = ({
|
||||||
</select>
|
</select>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
// An icon prop holds a name; the picker is what makes that name findable and
|
||||||
|
// shows what it looks like, exactly as the menu editor does.
|
||||||
|
if (DESIGNER_ICON_PROPERTIES.has(name) && type === 'string') {
|
||||||
|
return <IconPickerField value={String(value ?? '')} onChange={onChange} />
|
||||||
|
}
|
||||||
const objectListSchema = OBJECT_LIST_PROPERTIES[`${componentType}.${name}`]
|
const objectListSchema = OBJECT_LIST_PROPERTIES[`${componentType}.${name}`]
|
||||||
if (objectListSchema) {
|
if (objectListSchema) {
|
||||||
return <ObjectListEditor schema={objectListSchema} value={value} onChange={onChange} />
|
return <ObjectListEditor schema={objectListSchema} value={value} onChange={onChange} />
|
||||||
|
|
@ -1428,7 +1489,7 @@ const VisualComponentDesigner = () => {
|
||||||
(definitionName: string, parentId: string | null = null, slot?: string) => {
|
(definitionName: string, parentId: string | null = null, slot?: string) => {
|
||||||
const definition = catalogByName.get(definitionName)
|
const definition = catalogByName.get(definitionName)
|
||||||
if (!definition) return
|
if (!definition) return
|
||||||
const node = definitionToNode(definition)
|
const node = definitionToNode(definition, translate)
|
||||||
if (slot) node.slot = slot
|
if (slot) node.slot = slot
|
||||||
commitDocument((current) => ({
|
commitDocument((current) => ({
|
||||||
...current,
|
...current,
|
||||||
|
|
@ -1449,7 +1510,7 @@ const VisualComponentDesigner = () => {
|
||||||
(definitionName: string, targetId: string, placement: 'before' | 'after') => {
|
(definitionName: string, targetId: string, placement: 'before' | 'after') => {
|
||||||
const definition = catalogByName.get(definitionName)
|
const definition = catalogByName.get(definitionName)
|
||||||
if (!definition) return
|
if (!definition) return
|
||||||
const node = definitionToNode(definition)
|
const node = definitionToNode(definition, translate)
|
||||||
commitDocument((current) => {
|
commitDocument((current) => {
|
||||||
// Table cells and tab panes address their children by slot, so the new
|
// Table cells and tab panes address their children by slot, so the new
|
||||||
// node has to join the one it was dropped next to.
|
// node has to join the one it was dropped next to.
|
||||||
|
|
@ -2687,9 +2748,18 @@ const VisualComponentDesigner = () => {
|
||||||
(group) =>
|
(group) =>
|
||||||
[
|
[
|
||||||
group,
|
group,
|
||||||
[...(groups.get(group) || [])].sort((left, right) =>
|
[...(groups.get(group) || [])].sort((left, right) => {
|
||||||
left.name.localeCompare(right.name, 'tr', { sensitivity: 'base' }),
|
if (group === 'layout') {
|
||||||
),
|
const leftIndex = LAYOUT_TOOLBOX_ORDER.indexOf(left.name)
|
||||||
|
const rightIndex = LAYOUT_TOOLBOX_ORDER.indexOf(right.name)
|
||||||
|
if (leftIndex !== rightIndex) {
|
||||||
|
if (leftIndex < 0) return 1
|
||||||
|
if (rightIndex < 0) return -1
|
||||||
|
return leftIndex - rightIndex
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return left.name.localeCompare(right.name, 'tr', { sensitivity: 'base' })
|
||||||
|
}),
|
||||||
] as const,
|
] as const,
|
||||||
).filter(([, definitions]) => definitions.length > 0)
|
).filter(([, definitions]) => definitions.length > 0)
|
||||||
}, [filteredCatalog])
|
}, [filteredCatalog])
|
||||||
|
|
@ -2779,6 +2849,12 @@ const VisualComponentDesigner = () => {
|
||||||
translate('::App.DeveloperKitComponentDesigner.OptionN', { index: index + 1 }),
|
translate('::App.DeveloperKitComponentDesigner.OptionN', { index: index + 1 }),
|
||||||
),
|
),
|
||||||
value: record.value ?? record.eventKey ?? record.key ?? index + 1,
|
value: record.value ?? record.eventKey ?? record.key ?? index + 1,
|
||||||
|
// Only a tab carries its own text; keeping it here is what lets the
|
||||||
|
// row editor change or clear it instead of silently dropping it.
|
||||||
|
content: typeof record.content === 'string' ? record.content : undefined,
|
||||||
|
// Same for a menu item icon: it is a name, and an empty one means
|
||||||
|
// the item simply has no icon.
|
||||||
|
icon: typeof record.icon === 'string' ? record.icon : undefined,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return { label: String(option ?? ''), value: option ?? index + 1 }
|
return { label: String(option ?? ''), value: option ?? index + 1 }
|
||||||
|
|
@ -3316,7 +3392,8 @@ const VisualComponentDesigner = () => {
|
||||||
{translate('::App.DeveloperKitComponentDesigner.StaticOptions')}
|
{translate('::App.DeveloperKitComponentDesigner.StaticOptions')}
|
||||||
</div>
|
</div>
|
||||||
{staticSelectOptions.map((option, index) => (
|
{staticSelectOptions.map((option, index) => (
|
||||||
<div key={index} className="grid grid-cols-[1fr_1fr_auto] gap-1">
|
<div key={index} className="space-y-1">
|
||||||
|
<div className="grid grid-cols-[1fr_1fr_auto] gap-1">
|
||||||
<input
|
<input
|
||||||
aria-label={translate('::App.DeveloperKitComponentDesigner.OptionLabelAria', {
|
aria-label={translate('::App.DeveloperKitComponentDesigner.OptionLabelAria', {
|
||||||
index: index + 1,
|
index: index + 1,
|
||||||
|
|
@ -3361,6 +3438,37 @@ const VisualComponentDesigner = () => {
|
||||||
>
|
>
|
||||||
<FaTrash />
|
<FaTrash />
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
|
{selectedNode?.type === 'Menu' && (
|
||||||
|
<IconPickerField
|
||||||
|
value={option.icon ?? ''}
|
||||||
|
onChange={(iconKey) =>
|
||||||
|
updateStaticSelectOptions(
|
||||||
|
staticSelectOptions.map((current, optionIndex) =>
|
||||||
|
optionIndex === index ? { ...current, icon: iconKey } : current,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{selectedNode?.type === 'Tabs' && (
|
||||||
|
<input
|
||||||
|
className="w-full min-w-0 rounded border border-slate-300 bg-white px-2 py-1.5 text-[10px] dark:border-slate-700 dark:bg-slate-900"
|
||||||
|
placeholder={translate(
|
||||||
|
'::App.DeveloperKitComponentDesigner.TabContentPlaceholder',
|
||||||
|
)}
|
||||||
|
value={option.content ?? ''}
|
||||||
|
onChange={(event) =>
|
||||||
|
updateStaticSelectOptions(
|
||||||
|
staticSelectOptions.map((current, optionIndex) =>
|
||||||
|
optionIndex === index
|
||||||
|
? { ...current, content: event.target.value }
|
||||||
|
: current,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
<button
|
<button
|
||||||
|
|
@ -5725,7 +5833,7 @@ const VisualComponentDesigner = () => {
|
||||||
<div className="flex shrink-0 items-start justify-between gap-3 border-b border-slate-200 px-4 py-3 dark:border-slate-800">
|
<div className="flex shrink-0 items-start justify-between gap-3 border-b border-slate-200 px-4 py-3 dark:border-slate-800">
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<h2 className="flex items-center gap-2 text-sm font-semibold text-slate-800 dark:text-white">
|
<h2 className="flex items-center gap-2 text-sm font-semibold text-slate-800 dark:text-white">
|
||||||
<FaBolt className="text-amber-500" /> Script API — yapabilecekleriniz
|
<FaBolt className="text-amber-500" /> Script API
|
||||||
<span className="shrink-0 rounded bg-sky-100 px-1.5 py-0.5 text-[9px] font-semibold text-sky-700 dark:bg-sky-950 dark:text-sky-300">
|
<span className="shrink-0 rounded bg-sky-100 px-1.5 py-0.5 text-[9px] font-semibold text-sky-700 dark:bg-sky-950 dark:text-sky-300">
|
||||||
{translate('::App.DeveloperKitComponentDesigner.ExampleCount', {
|
{translate('::App.DeveloperKitComponentDesigner.ExampleCount', {
|
||||||
count: DESIGNER_SCRIPT_RECIPES.length,
|
count: DESIGNER_SCRIPT_RECIPES.length,
|
||||||
|
|
|
||||||
|
|
@ -90,8 +90,13 @@ export function IconPickerField({ value, onChange, invalid }: IconPickerFieldPro
|
||||||
/>
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
{/*
|
||||||
|
Panel genişliği tetikleyiciyi takip eder: sabit 360px, Visual Designer'ın dar
|
||||||
|
property kolonunda yatay taşmaya ve ikonların sıkışmasına yol açıyordu. Alt
|
||||||
|
sınır okunabilirliği, üst sınır geniş dialogdaki eski görünümü korur.
|
||||||
|
*/}
|
||||||
{open && (
|
{open && (
|
||||||
<div className="absolute z-50 mt-1 left-0 w-[360px] bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-600 rounded-lg shadow-xl">
|
<div className="absolute z-50 mt-1 left-0 w-full min-w-[220px] max-w-[min(360px,calc(100vw-2rem))] bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-600 rounded-lg shadow-xl">
|
||||||
<div className="p-2 border-b border-gray-100 dark:border-gray-700 flex items-center gap-2">
|
<div className="p-2 border-b border-gray-100 dark:border-gray-700 flex items-center gap-2">
|
||||||
<input
|
<input
|
||||||
autoFocus
|
autoFocus
|
||||||
|
|
@ -102,7 +107,7 @@ export function IconPickerField({ value, onChange, invalid }: IconPickerFieldPro
|
||||||
/>
|
/>
|
||||||
<span className="text-xs text-gray-400 shrink-0">{filtered.length} icons</span>
|
<span className="text-xs text-gray-400 shrink-0">{filtered.length} icons</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-10 gap-0.5 p-2 max-h-[208px] overflow-y-auto">
|
<div className="grid grid-cols-[repeat(auto-fill,minmax(2rem,1fr))] gap-0.5 p-2 max-h-[208px] overflow-y-auto">
|
||||||
{displayed.map(([key, Icon]) => (
|
{displayed.map(([key, Icon]) => (
|
||||||
<Button
|
<Button
|
||||||
key={key}
|
key={key}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue