sozsoft-platform/ui/src/components/visualDesigner/catalog.ts

476 lines
15 KiB
TypeScript
Raw Normal View History

2026-08-05 20:51:43 +00:00
import { CUSTOM_COMPONENTS, HTML_ELEMENTS } from '@/components/codeLayout/data/componentDefinitions'
import generatedComponentProps from './generated/componentProps.json'
import type { DesignerComponentDefinition, DesignerNodeKind } from './types'
interface GeneratedProperty {
name: string
tsType: string
type: 'string' | 'boolean' | 'number' | 'function' | 'object' | 'array' | 'select'
required: boolean
options?: string[]
defaultValue?: unknown
}
interface GeneratedComponentMetadata {
interfaceName: string
sourceFile: string
properties: GeneratedProperty[]
}
const generatedMetadata = generatedComponentProps as Record<string, GeneratedComponentMetadata>
const CONTAINER_NAMES = new Set(['div', 'Card', 'FormContainer', 'Tabs', 'Timeline'])
const DATA_COMPONENT_NAMES = new Set(['AutoComplete', 'Grid', 'Menu', 'Pagination', 'Select', 'Table'])
const HTML_UI_DUPLICATES = new Set(['input', 'button', 'textarea', 'select', 'checkbox'])
const UI_PROPERTY_ALIASES: Record<string, Record<string, string>> = {
Select: {
clearable: 'isClearable',
disabled: 'isDisabled',
multiple: 'isMulti',
onSearch: 'onInputChange',
searchable: 'isSearchable',
},
}
export const DESIGNER_PRIMARY_EVENTS: Record<string, string[]> = {
Button: ['onClick'],
Checkbox: ['onChange'],
Input: ['onChange'],
Menu: ['onSelect'],
Select: ['onChange'],
}
const DESIGNER_EVENT_SNIPPETS: Record<string, Record<string, string>> = {
Button: {
onClick: `// Butona tıklandığında çalışır.
console.log('Button clicked', event)`,
},
Checkbox: {
onChange: `// Checkbox bileşeninin yeni checked değeri.
const checked = event.checked
console.log('Checked:', checked)`,
},
Input: {
onChange: `// Input alanının güncel metin değeri.
const value = event.target.value
console.log('Value:', value)`,
},
Menu: {
onSelect: `// Seçilen menü öğesinin value değeridir.
console.log('Menu value:', event)`,
},
Select: {
onChange: `// Seçilen option nesnesi; temizlendiğinde null olur.
const selectedValue = event?.value ?? null
const selectedLabel = event?.label ?? ''
console.log({ selectedValue, selectedLabel })`,
},
}
export const getDesignerEventSnippet = (componentName: string, eventName: string) =>
DESIGNER_EVENT_SNIPPETS[componentName]?.[eventName] ||
`// ${componentName}.${eventName}(event)\nconsole.log(event)`
const COLLECTION_ITEMS_PROPERTY = {
name: 'items',
type: 'array' as const,
value: [],
category: 'properties' as const,
description: 'Statik veya endpoint kaynaklı veri koleksiyonu',
tsType: 'unknown[]',
options: undefined,
required: false,
}
const GRID_COMPACT_PROPERTY = {
name: 'compact',
type: 'boolean' as const,
value: false,
category: 'properties' as const,
description: 'Satır ve hücre boşluklarını azaltır',
tsType: 'boolean',
options: undefined,
required: false,
}
const GRID_TABLE_BEHAVIOR_PROPERTIES = [
{
name: 'hoverable',
type: 'boolean' as const,
value: true,
category: 'properties' as const,
description: 'Satırların üzerine gelindiğinde vurgu rengi gösterir',
tsType: 'boolean',
options: undefined,
required: false,
},
{
name: 'overflow',
type: 'boolean' as const,
value: true,
category: 'properties' as const,
description: 'Geniş tablolarda yatay kaydırmayı etkinleştirir',
tsType: 'boolean',
options: undefined,
required: false,
},
{
name: 'borderlessRow',
type: 'boolean' as const,
value: false,
category: 'properties' as const,
description: 'Veri satırları arasındaki kenarlıkları kaldırır',
tsType: 'boolean',
options: undefined,
required: false,
},
]
const SELECT_OPTIONS_PROPERTY = {
name: 'options',
type: 'array' as const,
value: [
{ label: 'Seçenek 1', value: '1' },
{ label: 'Seçenek 2', value: '2' },
],
category: 'properties' as const,
description: 'Select items; statik olarak veya endpoint koleksiyonundan beslenebilir',
tsType: 'Array<{ label: string; value: unknown }>',
options: undefined,
required: false,
}
const MENU_ITEMS_PROPERTY = {
...SELECT_OPTIONS_PROPERTY,
name: 'items',
description: 'Menu items; statik olarak veya endpoint koleksiyonundan beslenebilir',
}
const normalizeDefinition = (
definition: (typeof CUSTOM_COMPONENTS)[number],
kind: DesignerNodeKind,
toolboxGroup: DesignerComponentDefinition['toolboxGroup'],
): DesignerComponentDefinition => {
const sourceProperties =
definition.name === 'Grid'
? definition.properties.filter((property) => property.name !== 'children')
: definition.properties
let properties =
['Grid', 'Table'].includes(definition.name) &&
!sourceProperties.some((property) => property.name === 'items')
? [COLLECTION_ITEMS_PROPERTY, ...sourceProperties]
: sourceProperties
if (definition.name === 'Grid' && !properties.some((property) => property.name === 'compact')) {
properties = [GRID_COMPACT_PROPERTY, ...properties]
}
if (definition.name === 'Grid') {
properties = [
...GRID_TABLE_BEHAVIOR_PROPERTIES.filter(
(candidate) => !properties.some((property) => property.name === candidate.name),
),
...properties,
]
}
return {
...definition,
properties,
kind,
toolboxGroup,
acceptsChildren: CONTAINER_NAMES.has(definition.name),
}
}
const fallbackValue = (type: GeneratedProperty['type']) => {
if (type === 'boolean') return false
if (type === 'number') return 0
if (type === 'array') return []
if (type === 'object') return {}
return ''
}
const inferUiCategory = (name: string) => {
if (
new Set([
'AutoComplete',
'Checkbox',
'Chips',
'ColorPicker',
'DatePicker',
'Input',
'Knob',
'Radio',
'Rate',
'Select',
'Slider',
'Switcher',
'TimeInput',
'Upload',
]).has(name)
)
return 'form'
if (new Set(['Alert', 'Notification', 'Progress', 'Skeleton', 'Spinner']).has(name))
return 'feedback'
if (new Set(['Breadcrumb', 'Dropdown', 'Menu', 'Pagination', 'Steps', 'Tabs']).has(name))
return 'navigation'
return 'display'
}
const generatedPropertiesToDesigner = (
generated: GeneratedComponentMetadata,
existingProperties = new Map<string, (typeof CUSTOM_COMPONENTS)[number]['properties'][number]>(),
) =>
generated.properties.map((property) => {
const existing = existingProperties.get(property.name)
return {
name: property.name,
type: property.type,
value:
property.defaultValue !== undefined
? property.defaultValue
: existing?.value !== undefined
? existing.value
: fallbackValue(property.type),
options: property.options || existing?.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 = (
definition: (typeof CUSTOM_COMPONENTS)[number],
): DesignerComponentDefinition => {
const generated = generatedMetadata[definition.name]
if (!generated) {
return normalizeDefinition(
definition,
'ui',
DATA_COMPONENT_NAMES.has(definition.name) ? 'data' : 'ui',
)
}
const propertyAliases = UI_PROPERTY_ALIASES[definition.name] || {}
const curatedProperties = definition.properties
.filter(
(property) =>
(definition.name !== 'Select' || property.name !== 'items') &&
(definition.name !== 'Menu' || property.name !== 'variant') &&
(definition.name !== 'Table' || !['columns', 'data'].includes(property.name)),
)
.map((property) => ({
...property,
name: propertyAliases[property.name] || property.name,
}))
const existingProperties = new Map(curatedProperties.map((property) => [property.name, property]))
const generatedProperties = generatedPropertiesToDesigner(generated, existingProperties)
if (
['Select', 'AutoComplete', 'Menu'].includes(definition.name) &&
!generatedProperties.some(
(property) => property.name === (definition.name === 'Menu' ? 'items' : 'options'),
)
) {
generatedProperties.unshift(
definition.name === 'Menu' ? MENU_ITEMS_PROPERTY : SELECT_OPTIONS_PROPERTY,
)
}
if (
['Grid', 'Table'].includes(definition.name) &&
!generatedProperties.some((property) => property.name === 'items')
) {
generatedProperties.unshift(COLLECTION_ITEMS_PROPERTY)
}
const generatedPropertyNames = new Set(generatedProperties.map((property) => property.name))
const properties = [
...generatedProperties,
// Keep explicitly curated inherited React props such as Input.value and placeholder.
...curatedProperties.filter((property) => !generatedPropertyNames.has(property.name)),
].filter(
(property) =>
!(definition.name === 'Checkbox' && property.name === 'defaultChecked') &&
!(definition.name === 'Menu' && property.name === 'variant'),
)
const primaryEvents = DESIGNER_PRIMARY_EVENTS[definition.name]
return {
...definition,
kind: 'ui',
toolboxGroup: DATA_COMPONENT_NAMES.has(definition.name) ? 'data' : 'ui',
acceptsChildren: CONTAINER_NAMES.has(definition.name),
sourceFile: generated.sourceFile,
interfaceName: generated.interfaceName,
properties: primaryEvents
? properties.filter(
(property) =>
(property.category !== 'events' && property.type !== 'function') ||
primaryEvents.includes(property.name),
)
: properties,
}
}
const platformDefinition = (
name: string,
sourceName: string,
description: string,
height = '520px',
): DesignerComponentDefinition => ({
name,
sourceName,
icon: 'PanelsTopLeft',
category: 'platform',
kind: 'platform',
toolboxGroup: 'platform',
description,
properties: [
{ name: 'listFormCode', type: 'string', value: '', category: 'properties', required: true },
{ name: 'height', type: 'string', value: height, category: 'styling' },
],
hooks: [],
})
export const PLATFORM_COMPONENTS: DesignerComponentDefinition[] = [
platformDefinition(
'ListView',
'List',
'Tüm etkin platform görünümlerini /admin/list/:listFormCode ile aynı şekilde yönetir',
),
platformDefinition('DataGridView', 'Grid', 'Sozsoft dinamik veri grid görünümü'),
platformDefinition('TreeView', 'Tree', 'Sozsoft dinamik ağaç görünümü'),
platformDefinition('GanttView', 'GanttView', 'Sozsoft dinamik Gantt görünümü'),
platformDefinition('TodoBoard', 'TodoBoard', 'Sozsoft dinamik iş panosu'),
platformDefinition('CardView', 'CardView', 'Sozsoft dinamik kart görünümü'),
platformDefinition('SchedulerView', 'SchedulerView', 'Sozsoft dinamik planlayıcı görünümü'),
platformDefinition('PivotView', 'Pivot', 'Sozsoft dinamik pivot görünümü'),
platformDefinition('ChartView', 'Chart', 'Sozsoft dinamik grafik görünümü', '420px'),
]
export const DESIGNER_EXTRAS: DesignerComponentDefinition[] = [
{
name: 'PageContainer',
icon: 'PanelTop',
category: 'layout',
kind: 'layout',
toolboxGroup: 'layout',
description: 'Intranet içeriği için ortalanmış sayfa alanı',
acceptsChildren: true,
properties: [
{ name: 'maxWidth', type: 'string', value: '1280px', category: 'styling' },
{ name: 'padding', type: 'number', value: 24, category: 'styling' },
{ name: 'gap', type: 'number', value: 16, category: 'styling' },
{ name: 'className', type: 'string', value: '', category: 'styling' },
],
hooks: [],
},
{
name: 'TwoColumns',
icon: 'Columns2',
category: 'layout',
kind: 'layout',
toolboxGroup: 'layout',
description: 'Sayfayı iki eşit kolona böler',
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: 'Dar sidebar ve geniş içerik alanı',
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: 'Üst alan ve alt içerik için dikey yerleşim',
acceptsChildren: true,
properties: [
{ name: 'gap', type: 'number', value: 16, category: 'styling' },
{ name: 'className', type: 'string', value: '', category: 'styling' },
],
hooks: [],
},
{
name: 'FlexRow',
icon: 'Columns3',
category: 'layout',
kind: 'layout',
toolboxGroup: 'layout',
description: 'Yatay ve responsive yerleşim',
acceptsChildren: true,
properties: [
{ name: 'gap', type: 'number', value: 16, category: 'styling' },
{ name: 'wrap', type: 'boolean', value: true, category: 'styling' },
{
name: 'align',
type: 'select',
value: 'stretch',
options: ['stretch', 'start', 'center', 'end'],
category: 'styling',
},
{ name: 'className', type: 'string', value: '', category: 'styling' },
],
hooks: [],
},
{
name: 'Spacer',
icon: 'Space',
category: 'layout',
kind: 'layout',
toolboxGroup: 'layout',
description: 'Bileşenler arasında boşluk',
properties: [{ name: 'height', type: 'number', value: 24, category: 'styling' }],
hooks: [],
},
]
export const getDesignerCatalog = (customNames: string[] = []): DesignerComponentDefinition[] => {
const html = HTML_ELEMENTS.filter((definition) => !HTML_UI_DUPLICATES.has(definition.name)).map(
(definition) => normalizeDefinition(definition, 'html', 'html'),
)
const ui = CUSTOM_COMPONENTS.map(normalizeUiDefinition)
const definedUiNames = new Set(CUSTOM_COMPONENTS.map((definition) => definition.name))
const generatedOnlyUi: DesignerComponentDefinition[] = Object.entries(generatedMetadata)
.filter(([name]) => !definedUiNames.has(name))
.map(([name, metadata]) => ({
name,
icon: 'Blocks',
category: inferUiCategory(name),
kind: 'ui',
toolboxGroup: DATA_COMPONENT_NAMES.has(name) ? 'data' : 'ui',
description: `${metadata.interfaceName} sözleşmesini kullanan Sozsoft UI componenti`,
sourceFile: metadata.sourceFile,
interfaceName: metadata.interfaceName,
acceptsChildren: CONTAINER_NAMES.has(name),
properties: generatedPropertiesToDesigner(metadata),
hooks: [],
}))
const custom: DesignerComponentDefinition[] = customNames.map((name) => ({
name,
icon: 'Blocks',
category: 'custom',
kind: 'custom',
toolboxGroup: 'custom',
description: 'Veritabanındaki yeniden kullanılabilir bileşen',
acceptsChildren: true,
properties: [{ name: 'className', type: 'string', value: '', category: 'styling' }],
hooks: [],
}))
return [...DESIGNER_EXTRAS, ...PLATFORM_COMPONENTS, ...html, ...ui, ...generatedOnlyUi, ...custom]
}