942 lines
30 KiB
TypeScript
942 lines
30 KiB
TypeScript
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 }
|
||
|
||
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 } },
|
||
}
|
||
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'],
|
||
Button: ['onClick'],
|
||
Checkbox: ['onChange'],
|
||
Dropdown: ['onSelect'],
|
||
Input: ['onChange'],
|
||
Menu: ['onSelect'],
|
||
Pagination: ['onChange'],
|
||
'Radio.Group': ['onChange'],
|
||
Select: ['onChange'],
|
||
Tabs: ['onChange'],
|
||
}
|
||
|
||
const DESIGNER_EVENT_SNIPPETS: Record<string, Record<string, string>> = {
|
||
Button: {
|
||
onClick: `// Butona tıklandığında çalışır.
|
||
// Sayfadaki diğer komponentlere ref adlarıyla erişebilirsiniz:
|
||
// refs.input1.setValue('Merhaba')
|
||
// refs.input1.setEnabled(false)
|
||
// refs.card1.setVisible(false)
|
||
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)`,
|
||
},
|
||
Select: {
|
||
onChange: `// Seçilen option nesnesi; temizlendiğinde null olur.
|
||
const selectedValue = event?.value ?? null
|
||
const selectedLabel = event?.label ?? ''
|
||
console.log({ selectedValue, selectedLabel })`,
|
||
},
|
||
}
|
||
|
||
/**
|
||
* The script API, as copy/paste ready examples. This is the single source of
|
||
* truth behind the designer's "Script API" panel, so anything an event script
|
||
* can do is discoverable without reading the code generator.
|
||
*
|
||
* `{{ref}}`, `{{sql}}` and `{{url}}` are replaced by the designer with real
|
||
* names from the page being edited, so a copied example runs as it is.
|
||
*/
|
||
export interface DesignerScriptRecipe {
|
||
group: string
|
||
title: string
|
||
description: string
|
||
code: string
|
||
}
|
||
|
||
export const DESIGNER_SCRIPT_RECIPE_GROUPS = [
|
||
'Komponent erişimi',
|
||
'API çağrıları',
|
||
'SqlDataSource',
|
||
'Event ve sayfa',
|
||
] as const
|
||
|
||
export const DESIGNER_SCRIPT_RECIPES: DesignerScriptRecipe[] = [
|
||
{
|
||
group: 'Komponent erişimi',
|
||
title: 'Değer atama ve okuma',
|
||
description: 'Her komponent kendi ref adıyla adreslenir.',
|
||
code: `refs.{{ref}}.setValue('Merhaba')
|
||
const value = refs.{{ref}}.getValue()
|
||
console.log(value)`,
|
||
},
|
||
{
|
||
group: 'Komponent erişimi',
|
||
title: 'Aktif / pasif yapma',
|
||
description: 'Kaydet butonunu koşula göre kilitlemek için.',
|
||
code: `refs.{{ref}}.setEnabled(false)
|
||
console.log(refs.{{ref}}.isEnabled())`,
|
||
},
|
||
{
|
||
group: 'Komponent erişimi',
|
||
title: 'Gösterme / gizleme',
|
||
description: 'Gizlenen komponent sayfada hiç render edilmez.',
|
||
code: `refs.{{ref}}.setVisible(false)
|
||
// veya: refs.{{ref}}.hide() / refs.{{ref}}.show()`,
|
||
},
|
||
{
|
||
group: 'Komponent erişimi',
|
||
title: 'Salt okunur yapma',
|
||
description: 'Değeri görünür kalsın ama düzenlenemesin.',
|
||
code: `refs.{{ref}}.setReadOnly(true)`,
|
||
},
|
||
{
|
||
group: 'Komponent erişimi',
|
||
title: 'Etiket / metin değiştirme',
|
||
description: 'Buton ve etiket yazısını çalışma anında değiştirir.',
|
||
code: `refs.{{ref}}.setText('Kaydediliyor…')`,
|
||
},
|
||
{
|
||
group: 'Komponent erişimi',
|
||
title: 'Serbest property atama',
|
||
description: 'Property panelindeki her property script ile de yazılabilir.',
|
||
code: `refs.{{ref}}.setProps({ placeholder: 'Ara…', className: 'border-red-500' })
|
||
refs.{{ref}}.setProp('size', 'sm')`,
|
||
},
|
||
{
|
||
group: 'Komponent erişimi',
|
||
title: 'Script ile yapılanları geri alma',
|
||
description: 'Komponenti tasarımdaki haline döndürür.',
|
||
code: `refs.{{ref}}.reset()`,
|
||
},
|
||
{
|
||
group: 'API çağrıları',
|
||
title: 'GET — kayıt okuma',
|
||
description: 'Sonuç doğrudan response body’sidir.',
|
||
code: `const rows = await api.get('{{url}}')
|
||
console.log(rows)`,
|
||
},
|
||
{
|
||
group: 'API çağrıları',
|
||
title: 'GET — parametre ile',
|
||
description: 'İkinci argüman query string olarak gönderilir.',
|
||
code: `const rows = await api.get('{{url}}', { maxResultCount: 10 })
|
||
console.log(rows)`,
|
||
},
|
||
{
|
||
group: 'API çağrıları',
|
||
title: 'POST — yeni kayıt',
|
||
description: 'Gövde ikinci argüman olarak verilir.',
|
||
code: `const created = await api.post('{{url}}', {
|
||
name: refs.{{ref}}.getValue(),
|
||
})
|
||
console.log(created)`,
|
||
},
|
||
{
|
||
group: 'API çağrıları',
|
||
title: 'PUT ve DELETE',
|
||
description: 'Güncelleme ve silme aynı imzayı kullanır.',
|
||
code: `await api.put('{{url}}/5', { name: 'Yeni ad' })
|
||
await api.delete('{{url}}/5')`,
|
||
},
|
||
{
|
||
group: 'API çağrıları',
|
||
title: 'Hata yönetimi ve bildirim',
|
||
description: 'notify tipleri: success, info, warning, danger.',
|
||
code: `try {
|
||
await api.post('{{url}}', { name: refs.{{ref}}.getValue() })
|
||
notify('Kayıt tamamlandı', 'success')
|
||
} catch (error) {
|
||
notify(api.errorMessage(error), 'danger')
|
||
}`,
|
||
},
|
||
{
|
||
group: 'API çağrıları',
|
||
title: 'Çağrı boyunca butonu kilitleme',
|
||
description: 'Çift tıklamayı ve mükerrer isteği önler.',
|
||
code: `refs.{{ref}}.setEnabled(false)
|
||
refs.{{ref}}.setText('Gönderiliyor…')
|
||
try {
|
||
await api.get('{{url}}')
|
||
notify('Tamamlandı', 'success')
|
||
} catch (error) {
|
||
notify(api.errorMessage(error), 'danger')
|
||
} finally {
|
||
refs.{{ref}}.setEnabled(true)
|
||
refs.{{ref}}.setText('Gönder')
|
||
}`,
|
||
},
|
||
{
|
||
group: 'API çağrıları',
|
||
title: 'Serbest istek',
|
||
description: 'Header, timeout gibi ayarlar gerektiğinde.',
|
||
code: `const result = await api.request({
|
||
url: '{{url}}',
|
||
method: 'GET',
|
||
params: { skipCount: 0 },
|
||
})
|
||
console.log(result)`,
|
||
},
|
||
{
|
||
group: 'SqlDataSource',
|
||
title: 'Kaydet / sil / yenile',
|
||
description: 'Toolbar’ı gizleyip kendi butonlarınızı kullanabilirsiniz.',
|
||
code: `await refs.{{sql}}.save()
|
||
// await refs.{{sql}}.remove()
|
||
// await refs.{{sql}}.reload()
|
||
// refs.{{sql}}.newRecord()`,
|
||
},
|
||
{
|
||
group: 'SqlDataSource',
|
||
title: 'Aktif kaydı okuma ve yazma',
|
||
description: 'Alan adı Select sonucundaki sütun adıdır.',
|
||
code: `const record = refs.{{sql}}.getRecord()
|
||
console.log(record)
|
||
refs.{{sql}}.setField('name', 'Yeni ad')`,
|
||
},
|
||
{
|
||
group: 'SqlDataSource',
|
||
title: 'Kayıtlar arasında gezinme',
|
||
description: 'Select birden fazla satır döndürdüğünde.',
|
||
code: `const rows = refs.{{sql}}.getRows()
|
||
refs.{{sql}}.goToRow(rows.length - 1)`,
|
||
},
|
||
{
|
||
group: 'SqlDataSource',
|
||
title: 'Kaydetmeden önce doğrulama',
|
||
description: 'Zorunlu alan boşsa işlemi durdurur.',
|
||
code: `const record = refs.{{sql}}.getRecord()
|
||
if (!record.name) {
|
||
notify('Ad alanı zorunludur', 'warning')
|
||
return
|
||
}
|
||
await refs.{{sql}}.save()`,
|
||
},
|
||
{
|
||
group: 'SqlDataSource',
|
||
title: 'Durum bilgisi',
|
||
description: 'Yeni kayıt / düzenleme, meşgul ve hata durumu.',
|
||
code: `console.log(refs.{{sql}}.getMode(), refs.{{sql}}.isBusy(), refs.{{sql}}.getError())`,
|
||
},
|
||
{
|
||
group: 'Event ve sayfa',
|
||
title: 'Event verisi',
|
||
description: 'event içeriği komponent tipine göre değişir.',
|
||
code: `// Input: event.target.value · Checkbox: event.checked
|
||
// Select/Dropdown/Menu: seçilen değer · Button: tıklama eventi
|
||
console.log(event)`,
|
||
},
|
||
{
|
||
group: 'Event ve sayfa',
|
||
title: 'Komponentin kendi bilgileri',
|
||
description: 'Scriptin bağlı olduğu komponent ve tasarım property’leri.',
|
||
code: `console.log(component.type, component.ref)
|
||
console.log(props)`,
|
||
},
|
||
{
|
||
group: 'Event ve sayfa',
|
||
title: 'Sayfa yönlendirme',
|
||
description: 'Kayıt sonrası liste sayfasına dönmek için.',
|
||
code: `window.location.href = '/app/orders'`,
|
||
},
|
||
{
|
||
group: 'Event ve sayfa',
|
||
title: 'URL parametresi okuma',
|
||
description: 'Detay sayfasında anahtar değerini almak için.',
|
||
code: `const id = new URLSearchParams(window.location.search).get('id')
|
||
console.log(id)`,
|
||
},
|
||
]
|
||
|
||
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,
|
||
},
|
||
]
|
||
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
|
||
}
|
||
|
||
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')
|
||
? [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)),
|
||
]
|
||
}
|
||
|
||
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 = '',
|
||
) =>
|
||
generated.properties.map((property) => {
|
||
const existing = existingProperties.get(property.name)
|
||
const override = PROPERTY_OVERRIDES[componentName]?.[property.name]
|
||
const type = override?.type || property.type
|
||
return {
|
||
name: property.name,
|
||
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 || 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', getToolboxGroup(definition.name))
|
||
}
|
||
|
||
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)
|
||
if (
|
||
isDesignerOptionComponent(definition.name) &&
|
||
!generatedProperties.some((property) => property.name === collectionProperty)
|
||
) {
|
||
generatedProperties.unshift(
|
||
getCollectionPropertyDefinition(definition.name, collectionProperty),
|
||
)
|
||
}
|
||
if (
|
||
isDesignerTabularComponent(definition.name) &&
|
||
!generatedProperties.some((property) => property.name === collectionProperty)
|
||
) {
|
||
generatedProperties.unshift(COLLECTION_ITEMS_PROPERTY)
|
||
}
|
||
if (definition.name === 'Table') {
|
||
generatedProperties.unshift(
|
||
...TABLE_DIMENSION_PROPERTIES.filter(
|
||
(candidate) => !generatedProperties.some((property) => property.name === candidate.name),
|
||
),
|
||
)
|
||
}
|
||
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),
|
||
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 id’si (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 URL’sinden okunma şekli',
|
||
},
|
||
{
|
||
name: 'keyParamName',
|
||
type: 'string',
|
||
value: '',
|
||
category: 'properties',
|
||
description: 'URL’den 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: [],
|
||
}
|
||
|
||
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',
|
||
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)
|
||
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 component’i`,
|
||
sourceFile: metadata.sourceFile,
|
||
interfaceName: metadata.interfaceName,
|
||
acceptsChildren: CONTAINER_NAMES.has(name),
|
||
properties,
|
||
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,
|
||
SQL_DATA_SOURCE_DEFINITION,
|
||
...PLATFORM_COMPONENTS,
|
||
...html,
|
||
...ui,
|
||
...generatedOnlyUi,
|
||
...custom,
|
||
]
|
||
}
|