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

730 lines
23 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 {
DESIGNER_DATA_COMPONENT_NAMES,
SQL_DATA_SOURCE_TYPE,
getDesignerCollectionProperty,
isDesignerOptionComponent,
isDesignerTabularComponent,
type DesignerComponentDefinition,
type DesignerNodeKind,
} from './types'
export { DESIGNER_DATA_COMPONENT_NAMES }
2026-08-05 20:51:43 +00:00
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', 'Table', 'Tabs', 'Timeline'])
const getToolboxGroup = (name: string): DesignerComponentDefinition['toolboxGroup'] =>
name === 'Table' ? 'layout' : DESIGNER_DATA_COMPONENT_NAMES.has(name) ? 'data' : 'ui'
/** Components intentionally kept out of the toolbox. */
const EXCLUDED_COMPONENTS = new Set([
'Alert',
'Breadcrumb',
'Dialog',
'Drawer',
'FormContainer',
'FormItem',
'InputGroup',
'MenuItem',
'Segment',
'Steps',
])
interface PropertyOverride {
/** Seed value that makes a freshly dropped component render something useful. */
value?: unknown
/** Corrects an editor type the metadata generator could not infer. */
type?: GeneratedProperty['type']
options?: string[]
}
const PROPERTY_OVERRIDES: Record<string, Record<string, PropertyOverride>> = {
// `Exclude<Shape, 'none'> | 'square'` collapses to a single option.
Avatar: { shape: { options: ['round', 'circle', 'square'], value: 'circle' } },
Chips: { value: { value: ['Deneme', 'Deneme2'] } },
// ImageViewer renders nothing without images, and its clickable gallery only
// appears when `children` is set — so a dropped component would be invisible.
ImageViewer: {
images: {
value: [
{ src: 'https://picsum.photos/id/1015/1200/800', alt: 'Görsel 1', caption: 'Görsel 1' },
{ src: 'https://picsum.photos/id/1025/1200/800', alt: 'Görsel 2', caption: 'Görsel 2' },
],
},
children: { value: 'Galeri' },
},
Marquee: { gradient: { value: false }, speed: { value: 100 } },
}
2026-08-05 20:51:43 +00:00
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[]> = {
AutoComplete: ['onInputChange', 'onSelect'],
2026-08-05 20:51:43 +00:00
Button: ['onClick'],
Checkbox: ['onChange'],
Dropdown: ['onSelect'],
2026-08-05 20:51:43 +00:00
Input: ['onChange'],
Menu: ['onSelect'],
Pagination: ['onChange'],
'Radio.Group': ['onChange'],
2026-08-05 20:51:43 +00:00
Select: ['onChange'],
Tabs: ['onChange'],
2026-08-05 20:51:43 +00:00
}
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)`,
},
Dropdown: {
onSelect: `// Seçilen dropdown öğesinin value değeridir.
console.log('Dropdown value:', event)`,
},
Pagination: {
onChange: `// Seçilen sayfa numarasıdır.
console.log('Page:', event)`,
},
'Radio.Group': {
onChange: `// Seçilen radio öğesinin value değeridir.
console.log('Radio value:', event)`,
2026-08-05 20:51:43 +00:00
},
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 TABLE_DIMENSION_PROPERTIES = [
{
name: 'rowCount',
type: 'number' as const,
value: 2,
category: 'properties' as const,
description: 'Table içindeki statik satır sayısı',
tsType: 'number',
options: undefined,
required: false,
},
{
name: 'columnCount',
type: 'number' as const,
value: 3,
category: 'properties' as const,
description: 'Her satırdaki statik hücre sayısı',
tsType: 'number',
options: undefined,
required: false,
},
]
2026-08-05 20:51:43 +00:00
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 TABS_ITEMS_PROPERTY = {
...SELECT_OPTIONS_PROPERTY,
name: 'items',
value: [
{ label: 'Sekme 1', value: '1', content: 'Sekme 1 içeriği' },
{ label: 'Sekme 2', value: '2', content: 'Sekme 2 içeriği' },
],
description:
'Sekmeler; statik olarak veya endpoint koleksiyonundan beslenebilir. content alanı sekme içeriğini yazar.',
tsType: 'Array<{ label: string; value: unknown; content?: string }>',
}
/** Seed definition for a data component's collection property. */
const getCollectionPropertyDefinition = (name: string, collectionProperty: string) => {
if (isDesignerTabularComponent(name)) return COLLECTION_ITEMS_PROPERTY
if (name === 'Tabs') return TABS_ITEMS_PROPERTY
return collectionProperty === 'items' ? MENU_ITEMS_PROPERTY : SELECT_OPTIONS_PROPERTY
}
2026-08-05 20:51:43 +00:00
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 =
definition.name === 'Grid' && !sourceProperties.some((property) => property.name === 'items')
2026-08-05 20:51:43 +00:00
? [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,
]
}
if (definition.name === 'Table') {
properties = [
...TABLE_DIMENSION_PROPERTIES.filter(
(candidate) => !properties.some((property) => property.name === candidate.name),
),
...properties.filter((property) => !['columns', 'data', 'items'].includes(property.name)),
]
}
2026-08-05 20:51:43 +00:00
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]>(),
componentName = '',
2026-08-05 20:51:43 +00:00
) =>
generated.properties.map((property) => {
const existing = existingProperties.get(property.name)
const override = PROPERTY_OVERRIDES[componentName]?.[property.name]
const type = override?.type || property.type
2026-08-05 20:51:43 +00:00
return {
name: property.name,
type,
2026-08-05 20:51:43 +00:00
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 || property.options || existing?.options,
2026-08-05 20:51:43 +00:00
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', getToolboxGroup(definition.name))
2026-08-05 20:51:43 +00:00
}
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,
definition.name,
)
// Every data component must expose its collection property so the data panel
// can drive it from static values or an endpoint.
const collectionProperty = getDesignerCollectionProperty(definition.name)
2026-08-05 20:51:43 +00:00
if (
isDesignerOptionComponent(definition.name) &&
!generatedProperties.some((property) => property.name === collectionProperty)
2026-08-05 20:51:43 +00:00
) {
generatedProperties.unshift(
getCollectionPropertyDefinition(definition.name, collectionProperty),
2026-08-05 20:51:43 +00:00
)
}
if (
isDesignerTabularComponent(definition.name) &&
!generatedProperties.some((property) => property.name === collectionProperty)
2026-08-05 20:51:43 +00:00
) {
generatedProperties.unshift(COLLECTION_ITEMS_PROPERTY)
}
if (definition.name === 'Table') {
generatedProperties.unshift(
...TABLE_DIMENSION_PROPERTIES.filter(
(candidate) => !generatedProperties.some((property) => property.name === candidate.name),
),
)
}
2026-08-05 20:51:43 +00:00
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: getToolboxGroup(definition.name),
2026-08-05 20:51:43 +00:00
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'),
]
/**
* ASP.NET's SqlDataSource + FormView in one component: it owns the four CRUD
* endpoints and acts as a container, so every component dropped inside it can
* bind to a column of the Select result and write back through Save/Delete.
* The endpoints are configured from the inspector's Data tab, not here.
*/
export const SQL_DATA_SOURCE_DEFINITION: DesignerComponentDefinition = {
name: SQL_DATA_SOURCE_TYPE,
icon: 'Database',
category: 'data',
kind: 'layout',
toolboxGroup: 'data',
description:
'GET/POST/PUT/DELETE endpointlerini tek kayıt üzerinde yöneten veri kabı; içine bırakılan komponentler sütunlara bağlanır',
acceptsChildren: true,
properties: [
{
name: 'selectEndpoint',
type: 'string',
value: '',
category: 'properties',
description: 'Kaydı okuyan GET endpointinin data source idsi (Data sekmesinden seçilir)',
},
{
name: 'insertEndpoint',
type: 'string',
value: '',
category: 'properties',
description: 'Yeni kayıt için POST endpointi',
},
{
name: 'updateEndpoint',
type: 'string',
value: '',
category: 'properties',
description: 'Mevcut kaydı güncelleyen PUT endpointi',
},
{
name: 'deleteEndpoint',
type: 'string',
value: '',
category: 'properties',
description: 'Kaydı silen DELETE endpointi',
},
{
name: 'keyFieldName',
type: 'string',
value: 'id',
category: 'properties',
description: 'Insert/Update ayrımı ve endpoint parametreleri bu sütundan doldurulur',
},
{
name: 'collectionPath',
type: 'string',
value: '',
category: 'properties',
description: 'GET cevabı içinde satırların bulunduğu path (boşsa cevabın kendisi kullanılır)',
},
{
name: 'keySource',
type: 'select',
value: 'query',
options: ['query', 'route'],
category: 'properties',
description: 'Key değerinin sayfa URLsinden okunma şekli',
},
{
name: 'keyParamName',
type: 'string',
value: '',
category: 'properties',
description: 'URLden okunacak parametre adı (boşsa key field kullanılır)',
},
{
name: 'previewKeyValue',
type: 'string',
value: '',
category: 'properties',
description:
'Sadece tasarım ekranı: GetById endpointinin sütunlarını okuyabilmek için örnek key değeri',
},
{
name: 'autoLoad',
type: 'boolean',
value: true,
category: 'properties',
description: 'Sayfa açıldığında Select endpointini otomatik çağırır',
},
{
name: 'showToolbar',
type: 'boolean',
value: true,
category: 'properties',
description: 'New / Save / Delete / Reload butonlarını gösterir',
},
{
name: 'gap',
type: 'number',
value: 16,
category: 'styling',
},
{ name: 'className', type: 'string', value: '', category: 'styling' },
],
hooks: [],
}
2026-08-05 20:51:43 +00:00
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: 'ui',
2026-08-05 20:51:43 +00:00
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.filter(
(definition) => !EXCLUDED_COMPONENTS.has(definition.name),
).map(normalizeUiDefinition)
2026-08-05 20:51:43 +00:00
const definedUiNames = new Set(CUSTOM_COMPONENTS.map((definition) => definition.name))
const generatedOnlyUi: DesignerComponentDefinition[] = Object.entries(generatedMetadata)
.filter(([name]) => !definedUiNames.has(name) && !EXCLUDED_COMPONENTS.has(name))
.map(([name, metadata]) => {
const properties = generatedPropertiesToDesigner(metadata, undefined, name)
const collectionProperty = getDesignerCollectionProperty(name)
if (
DESIGNER_DATA_COMPONENT_NAMES.has(name) &&
!properties.some((property) => property.name === collectionProperty)
) {
properties.unshift(getCollectionPropertyDefinition(name, collectionProperty))
}
return {
name,
icon: 'Blocks',
category: inferUiCategory(name),
kind: 'ui' as const,
toolboxGroup: getToolboxGroup(name),
description: `${metadata.interfaceName} sözleşmesini kullanan Sozsoft UI componenti`,
sourceFile: metadata.sourceFile,
interfaceName: metadata.interfaceName,
acceptsChildren: CONTAINER_NAMES.has(name),
properties,
hooks: [],
}
})
2026-08-05 20:51:43 +00:00
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,
SQL_DATA_SOURCE_DEFINITION,
...PLATFORM_COMPONENTS,
...html,
...ui,
...generatedOnlyUi,
...custom,
]
2026-08-05 20:51:43 +00:00
}