sozsoft-platform/.github/instructions/lowcode.instructions.md
2026-08-26 21:44:53 +03:00

2064 lines
107 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Low-Code Üretim Referansı (Authoring Reference)
Bu doküman, bir yapay zekâ ajanının **platformu bir geliştirici gibi kullanarak** ekran ve
bileşen üretebilmesi için gereken somut şemaları, alan anlamlarını, enum değerlerini ve
çalışan örnekleri içerir.
- Ne yapılacağına karar verme kuralları: `ai.instructions.md`
- `api/` kod standardı: `dotnet.instructions.md`
- Modül/liste ekleme prosedürü: `list.instructions.md`
- Neyin nerede yaşadığı: `../../README.md`
Çelişki olursa `ai.instructions.md` kazanır. Buradaki her şey **"kod yazmadan üretim"**
tarafındadır; bu dosya platformun çekirdek kodunu geliştirmek için değil, **prompt ile
uygulamaya yeni ekran, menü, yetki, dil anahtarı, route ve süreç eklemek** içindir.
Bir istek geldiğinde çekirdek koda dokunmak değil, `configs/seeds/` altına uygulamanın kendi
üreteceği dosyaların aynılarını yazmak esastır — bkz. §0.
> **Altın kural:** Ürettiğin her şey bir seed dosyasına düşmelidir. Veritabanı silinip
> yeniden oluşturulduğunda geri gelmeyen bir artefakt teslim edilmiş sayılmaz.
---
## 0. Üretim protokolü (her prompt için zorunlu)
Kullanıcı "şöyle bir ekran/form/süreç istiyorum" dediğinde **uygulamayı kullanıyormuş gibi**
davran: Wizard'ın, Component Manager'ın ve SQL Query Manager'ın diske yazacağı dosyaların
aynılarını `configs/seeds/host/` altına sen yaz. Ekranlara tıklamanın yerine geçen şey budur.
Talep iki biçimde gelir ve ikisi de aynı hatta girer:
- **Yazılı tarif** → §0.6 ile ekran tarifine çevir.
- **Ekran görüntüsü / mockup / fotoğraf** → §0.7 ile önce görseli oku, tarifi çıkar.
Tarif hazır olunca §0.6.1 ile yolu seç (Wizard mı, Custom Component mi), sonra §0.2'deki sırayla
dosyaları üret.
### 0.1 Yazılacak klasörler
```
configs/seeds/host/
├── sql/object/{Nesne}.sql ← tablo / view / fonksiyon / prosedür (SQL Server)
├── sql/execute/{Ad}.sql ← bir kez çalışacak script (veri dolumu, migrasyon)
├── postgres/object|execute/… ← aynısının PostgreSQL diyalekti (hedef PG ise)
├── crud/{EntityName}.json ← tablonun REST uçları
├── custom/{ComponentName}.json ← Custom Component (Visual Designer dokümanı ya da kod)
└── wizard/{yyyyMMddHHmmss}_{WizardName}.json ← ekran + menü + yetki + dil anahtarları
```
Tenant kapsamı isteniyorsa `host/` yerine `tenants/{tenantId}/`; iç düzen aynıdır.
### 0.2 Üretim sırası — bu sırayla yaz, bu sırayla çalışır
| # | Adım | Dosya | Neden bu sırada |
| --- | --- | --- | --- |
| 1 | Tablo / view | `sql/object/…` | Ekranın bağlanacağı nesne önce var olmalı |
| 2 | CRUD uçları (gerekiyorsa) | `crud/{Entity}.json` | Custom Component'in data source'ları buna bakar |
| 3 | Custom Component (gerekiyorsa) | `custom/{Name}.json` | Wizard `Custom` yolunda bileşen adını arar |
| 4 | Wizard | `wizard/{ts}_{Ad}.json` | Menü, yetki, dil anahtarı ve ListForm'u üretir |
| 5 | Alt ekranlar (varsa) | ek `wizard/…` dosyaları | Ana wizard'dan **önceki** zaman damgasını alır |
**Zaman damgası bir bağımlılık aracıdır.** `WizardDataSeeder` `wizard/` klasörünü **dosya adına
göre sıralı** işler. Bir wizard başka bir wizard'ın ürettiği menüye/ListForm'a dayanıyorsa,
bağımlı olanın zaman damgası **daha büyük** olmalıdır. Alt form olarak kullanılacak menüsüz
ekranı her zaman önce numaralandır.
### 0.3 Yazmadan önce zorunlu keşif
Uydurma. Şu üçünü mutlaka kontrol et:
1. **Tablo/kolon adları**`sql/object/` altındaki dosyalar ya da mevcut wizard dosyalarındaki
`SelectCommand`. Kolon adını tahmin etme; yoksa tabloyu da sen üret.
2. **Var olan menü kökü ve izin grubu** — aynı modül için ikinci bir kök menü açma. Mevcut
wizard dosyalarındaki `MenuParentCode` / `PermissionGroupName` değerlerini tara.
3. **Dil anahtarı çakışması**`LanguagesData.json` ve diğer wizard dosyaları. Var olan bir
anahtarın metnini seed **ezmez**; yanlış metinle karşılaşırsan anahtarı değiştir.
### 0.4 Üretim sonrası
- Değişikliklerin devreye girmesi için **DB Migrate** (SQL Query Manager / Wizard Manager
üzerindeki buton, yetki `App.Setup.Migrate`) ya da migrator konteyneri (`SEED=true`) çalışmalı.
- Seeder **idempotenttir**: ekran zaten varsa dosya atlanır (§4.5). Var olan bir ekranı
değiştirmek için dosyayı düzenlemek yetmez — kayıtları silip yeniden seed etmek ya da
ekranı Wizard'dan `EditFileName` ile yeniden çalıştırmak gerekir.
- Ürettiğin dosyaları kullanıcıya **tek tek listele**: hangi dosya, ne üretiyor, hangi menüde
görünecek, hangi yetkiyi ister.
### 0.5 Dosya biçimi kuralları
| Kural | Değer |
| --- | --- |
| Kodlama | UTF-8 (BOM'suz) |
| JSON alan adları | **PascalCase** (`Wizard`, `ListFormCode`, `Groups`) — seeder `PropertyNameCaseInsensitive` okur ama dosyalar PascalCase üretilir |
| Custom component dosyası | Kök alanlar PascalCase; `Props` **içindeki** designer dokümanı camelCase (`visualDesigner`, `nodes`, `sourceMode`) |
| Enum'lar | Sayı olarak yazılır (§3) |
| `GeneratedAt` | ISO-8601 UTC (`2026-08-26T10:15:00Z`) — yalnızca bilgi amaçlı |
| İç içe JSON | `EditorOptions`, `EditorScript`, `LookupQuery` → JSON **string** olarak kaçışlanır |
| Satır sonu | LF |
---
### 0.6 Talebi ekran tarifine çevir
Dosya yazmadan önce **tek bir ekran tarifi** çıkar ve kullanıcıya kısaca doğrulat. Tarif, hangi
aracın kullanılacağını (Wizard mı, Custom Component mi) ve hangi dosyaların üretileceğini
belirler.
**Ekran tarifi şablonu** — her maddeye bir satır, bilinmiyorsa varsayımını yaz:
```
Amaç : Ne işi görüyor, kim kullanıyor
Veri kaynağı : Tablo/view adı · yoksa "üretilecek"
Anahtar : Kolon + tip
Sütunlar : Alan → editör tipi → zorunlu mu → lookup kaynağı
Layout(lar) : Grid / Card / Pivot / Chart / Tree / Gantt / Scheduler / Todo
Form düzeni : Gruplar ve kolon sayısı
Aksiyonlar : Toolbar ve satır butonları, ne yapıyorlar
Alt ekranlar : Ana-detay sekmeleri
Widget'lar : Üstteki KPI kartları
Akış : Onay/koşul adımları
Menü : Üst menü, sıra, ikon
Yetki : Grup adı + kimde hangi yetki
Tenant : IsTenant / IsBranch / IsOrganizationUnit
Dil : EN + TR metinler
```
Kural: **eksik bilgi için işi durdurma.** Varsayımını tarifin içine yaz, üret, sonunda
"şu varsayımlarla ürettim" diye bildir. Yalnızca yanlış varsayımın veri kaybettireceği yerlerde
(hangi tabloya yazılacağı, kimin onaylayacağı) sor.
#### 0.6.1 Hangi araç? — yol seçimi
Tarifi çıkardıktan sonra tek bir karar kalır:
| Tarifte varsa | Yol |
| --- | --- |
| Kayıt listesi + ekle/düzenle/sil + filtre/arama | **SQL Query Manager + Wizard** (`ComponentKind: 0`) |
| Aynı verinin birden çok görünümü (pano, takvim, ağaç, grafik) | **Wizard** — tek dosya, birden çok layout (§9.8) |
| Ana kayıt + detay sekmeleri | **Wizard** + `SubForms` |
| Onay süreci | **Wizard** + `WorkflowDto` |
| Üstte KPI kartları | **Wizard** + `Widgets` |
| Serbest yerleşim: yan yana kartlar, özel bölümler, sihirbaz adımları, karışık bileşenler | **Custom Component** (`ComponentKind: 1`) |
| Var olan ekranların bir sayfada toplanması (dashboard) | **Custom Component** + `platform` düğümleri (`listFormCode`) |
| Tek kayıt üzerinde çalışan özel form (liste yok) | **Custom Component** + `Form` düğümü |
Şüphede kalırsan **Wizard'ı seç.** Grid'e benzeyen her şey Wizard'la daha ucuz, daha yetkin ve
daha bakımlıdır; Custom Component'e yalnızca yerleşim gerçekten grid/form kalıbına sığmıyorsa geç.
İki yol birlikte de kullanılır: veriyi Wizard ile ekran yap, sonra Custom Component içine
`platform` düğümü olarak göm.
---
### 0.7 Görselden üretim (ekran görüntüsü / mockup)
Kullanıcı tarif yerine **bir fotoğraf, ekran görüntüsü ya da mockup** gönderdiğinde önce görseli
okuyup §0.6'daki tarifi çıkar, sonra dosyaları üret. Görseli tahmine değil, gördüğün öğelere
dayandır.
#### 0.7.1 Görseli okuma sırası
1. **Sayfa iskeleti** — kaç bölge var? Üstte KPI şeridi, ortada liste, sağda panel, altta sekme?
2. **Ana bölge ne?** Tablo mu, kart ızgarası mı, pano mu, takvim mi, form mu?
3. **Araç çubuğu** — hangi butonlar var, ne yazıyorlar, ikonları ne?
4. **Sütunlar** — başlıklar, hizalama, biçim (para, tarih, yüzde), rozet/renkli durum hücreleri.
5. **Filtre/arama** — başlık altı filtre satırı, arama kutusu, tarih aralığı seçicileri.
6. **Form alanları** — kontrol tipleri ve kolon düzeni (kaç kolon, hangi alan kaç sütun kaplıyor).
7. **Satır aksiyonları** — satır sonundaki ikonlar.
8. **Durum göstergeleri** — onay adımları, ilerleme çubukları, etiketler.
#### 0.7.2 Görsel öğe → artefakt eşlemesi
| Görselde gördüğün | Karşılığı |
| --- | --- |
| Başlık satırı + veri satırları olan tablo | Grid layout |
| Kart ızgarası | Card layout |
| Dikey kolonlar + sürüklenebilir kartlar | Todo (Kanban) layout · `TodoOptionDto.StatusExpr` |
| Ay/hafta takvimi, saat çizelgesi | Scheduler layout |
| Yatay zaman çubukları, bağımlılık okları | Gantt layout |
| Girintili ağaç, açılır düğümler | Tree layout |
| Satır/sütun kesişimli özet tablo, ara toplamlar | Pivot layout |
| Çubuk/çizgi/pasta grafik | Chart layout · `SeriesJson` |
| Üstte sayı gösteren küçük kutular | `Widgets` (`WidgetEditDto`) |
| Kaydın altında sekmeler (Kalemler, Ekler, Geçmiş) | `SubForms` |
| Araç çubuğundaki özel buton | `CommandColumnJson` · `ButtonPosition: 1` |
| Satır sonundaki ikon | `CommandColumnJson` · `ButtonPosition: 0` |
| Başlık altındaki filtre satırı | `FilterRowJson` |
| Sağ üstteki arama kutusu | `SearchPanelJson` |
| "Sürükleyip gruplayın" şeridi | `GroupPanelJson` |
| Üstte ek filtre çubuğu (tarih aralığı, şube seçimi) | `ExtraFilterJson` |
| Sayfalama şeridi | `PagerOptionJson` |
| Renkli durum rozeti | `ColumnStylingJson` / `ColumnCssClass` |
| Onayla / Reddet butonları | `WorkflowJson` (otomatik gelir, elle tanımlama) |
| Yan yana bağımsız bölümler, karışık yerleşim | Custom Component · `FlexRow` + düğümler |
#### 0.7.3 Görseldeki kontrol → `EditorType`
| Görselde | `EditorType` | Ek |
| --- | --- | --- |
| Tek satır metin kutusu | `dxTextBox` | |
| Çok satırlı kutu | `dxTextArea` | `autoResizeEnabled` |
| Sağında ok olan açılır liste | `dxSelectBox` | lookup zorunlu |
| İçinde etiket/çip olan açılır liste | `dxTagBox` | lookup zorunlu |
| Büyüteçli seçim + tablo açılıyor | `dxGridBox` / `dxLookup` | `columns` |
| Onay kutusu | `dxCheckBox` | |
| Aç/kapa anahtarı | `dxSwitch` | `switchedOnText` |
| Yan yana yuvarlak seçenekler | `dxRadioGroup` | `layout: horizontal` |
| Takvim ikonlu alan | `dxDateBox` | |
| İki tarihli aralık alanı | `dxDateRangeBox` | |
| Artı/eksi oklu sayı | `dxNumberBox` | `format.precision` |
| `₺ 1.250,00` biçimli alan | `dxNumberBox` | `format.type: currency` |
| `%` biçimli alan | `dxNumberBox` | `format.type: percent` |
| Renk kutucuğu | `dxColorBox` | |
| Kalın/italik araç çubuklu alan | `dxHtmlEditor` | `toolbar.items` |
| Kaydırıcı | `dxSlider` / `dxRangeSlider` | `min`, `max` |
| Görsel küçük resmi / yükleme alanı | `dxImageViewer` / `dxImageUpload` | `accept`, `maxFileSize` |
| Gri, tıklanamaz alan | ilgili editör + `EditorOptions: {"readOnly": true}` | |
| Kırmızı yıldızlı etiket | `IsRequired: true` | |
#### 0.7.4 Görselden okunamayacak şeyler
Bunlar bir ekran görüntüsünde **görünmez**; varsayımını yaz ve bildir:
| Bilgi | Varsayılan davranış |
| --- | --- |
| Tablo/kolon adları | Görünen başlıklardan PascalCase kolon adı türet, tabloyu `sql/object/` altında sen üret |
| Anahtar alan ve tipi | `Id` / `UNIQUEIDENTIFIER` (`KeyFieldDbSourceType: 9`) |
| Lookup'ların kaynağı | Az sayıda sabit seçenek görünüyorsa `StaticData`, kod/isim çifti gerektiren yerlerde `Query` |
| Tenant/şube kırılımı | `IsTenant: true` (§CLAUDE varsayılanı) |
| Yetki grubu ve menü kökü | Ekranın ait olduğu modülden türet, kullanıcıya sor değil bildir |
| Dil metinleri | Görseldeki dilde yaz, diğer dile çevir; ikisi de doldurulur |
| Onaycılar | Yer tutucu bırak (`<roleOrUserId>`) ve kullanıcıya sor — **bu, sorulması gereken bir şeydir** |
| İş kuralları (hesap, koşul) | Görselde formül görünmüyorsa `EditorScript` yazma; sor |
#### 0.7.5 Görsel için de aynı protokol
Görselden çıkardığın tarifi §0.6.1'deki tabloya sok, yolu seç, §0.2'deki sırayla dosyaları üret.
Görsel bir "tasarım sözleşmesi" değildir: platformun kendi bileşenleriyle **en yakın** karşılığı
üretilir, piksel taklidi yapılmaz. Görseldeki bir öğenin platformda karşılığı yoksa bunu üretim
sonunda açıkça yaz.
---
## 1. Hangi istek → hangi artefakt
| İstek | Üretilecek artefakt | Bölüm |
| --- | --- | --- |
| "X tablosunun listesi/ekranı olsun" | Wizard seed dosyası (`ComponentKind: 0`) | 4 |
| "Şu alanı otomatik hesaplasın / şarta göre kilitlensin" | `EditorScript` (Script Builder tarifi) | 5 |
| "Alan şu formatta / maskeyle görünsün" | `EditorOptions` (Options Builder) | 6 |
| "Ekran bir tablodan değil, birleştirilmiş veriden beslensin" | SQL View Designer → `View` + Wizard | 8 |
| "Serbest yerleşimli bir sayfa / dashboard / özel form" | Custom Component + Wizard (`ComponentKind: 1`) | 7 |
| "Bir tabloya REST API açalım" | CRUD Endpoint | 9 |
| "Elle yazdığım bir SQL'i API olarak açalım" | Custom Endpoint | 9.2 |
| "API'de iş mantığı/hesap/entegrasyon lazım" | Dynamic Service | 9.3 |
| "Onay süreci olsun" | `WorkflowDto` + workflow kriterleri | 4.7 |
| "Ana-detay ekranı" | `SubForms` | 4.6 |
| "Üstte KPI kartları" | `Widgets` | 4.6 |
| Hiçbiri yetmiyorsa | Dynamic Service → en son çare kod | `ai.instructions.md` §4 |
---
## 2. İsimlendirme sözleşmesi
Tutarlılık zorunludur; menü, route, yetki ve dil anahtarı aynı kökten türer.
| Şey | Kalıp | Örnek |
| --- | --- | --- |
| Tablo | `{Modul}_T_{Entity}` | `Mrp_T_Order` |
| View | `{Modul}_V_{Ad}` | `Mrp_V_OrderSummary` |
| `ListFormCode` | `App.{Modul}.{Liste}` | `App.Mrp.Orders` |
| `MenuCode` | `ListFormCode` ile aynı | `App.Mrp.Orders` |
| Üst menü kodu | `App.{Modul}` | `App.Mrp` |
| Yetki grubu | `App.{Modul}` | `App.Mrp` |
| Yetkiler | `{MenuCode}` + `.Create/.Update/.Delete/.Export/.Import/.Note` | `App.Mrp.Orders.Create` |
| Dil anahtarı (menü/başlık) | `{MenuCode}` | `App.Mrp.Orders` |
| Dil anahtarı (alan) | `App.Listform.ListformField.{Alan}` | `App.Listform.ListformField.OrderNo` |
| Route (List) | `/admin/list/{ListFormCode}` — statik `:listFormCode` route'u karşılar, DB'ye kayıt yazılmaz | `/admin/list/App.Mrp.Orders` |
| Route (Custom) | Bileşenin `RoutePath` değeri; route doğrudan bundan üretilir | `/admin/order-board` |
| Wizard seed dosyası | `{yyyyMMddHHmmss}_{WizardName}.json` | `20260826101500_Orders.json` |
| Custom component seed | `{Name}.json` | `OrderBoard.json` |
| CRUD endpoint seed | `{EntityName}.json` | `Mrp_T_Order.json` |
Kapsam klasörü: host bağlamında `configs/seeds/host/…`, tenant bağlamında
`configs/seeds/tenants/{tenantId}/…`. Çözüm her zaman `SeedPathResolver` üzerinden yapılır;
yol elle kurulmaz.
---
## 3. Ortak enum değerleri (seed dosyalarına **sayı** olarak yazılır)
**`ComponentKind`** — `0` List · `1` Custom
**`SelectCommandType`** — `1` Table · `2` View · `3` TableValuedFunction · `4` Query ·
`5` StoredProcedure
**`LookupDataSourceType`** — `1` StaticData · `2` Query · `3` WebService
**`DbSourceType` / `KeyFieldDbSourceType`** (`System.Data.DbType`):
| # | Ad | SQL karşılığı |
| --- | --- | --- |
| 1 | Binary | `binary`, `varbinary`, `image` |
| 2 | Byte | `tinyint` |
| 3 | Boolean | `bit` |
| 5 | Date | `date` |
| 6 | DateTime | `datetime`, `datetime2`, `smalldatetime` |
| 7 | Decimal | `decimal`, `numeric`, `money` |
| 8 | Double | `float`, `real` |
| 9 | Guid | `uniqueidentifier` |
| 10 | Int16 | `smallint` |
| 11 | Int32 | `int` |
| 12 | Int64 | `bigint` |
| 16 | String | `nvarchar`, `varchar`, `nchar`, `char`, `text` |
| 17 | Time | `time` |
| 25 | Xml | `xml` |
| 27 | DateTimeOffset | `datetimeoffset` |
**Editör tipleri (`EditorType`)** — 21 tip. DevExtreme'den gelenler: `dxAutocomplete`,
`dxCalendar`, `dxCheckBox`, `dxColorBox`, `dxDateBox`, `dxDateRangeBox`, `dxDropDownBox`,
`dxHtmlEditor`, `dxLookup`, `dxNumberBox`, `dxRadioGroup`, `dxRangeSlider`, `dxSelectBox`,
`dxSlider`, `dxSwitch`, `dxTagBox`, `dxTextArea`, `dxTextBox`. Platformun kendi editörleri
(`PlatformEditorTypes`): `dxGridBox`, `dxImageViewer`, `dxImageUpload` (+ `dxTagBox` platform
tarafında da özel işlenir).
Tip seçimi kuralı: `Boolean → dxCheckBox`/`dxSwitch`, tarih → `dxDateBox`, sayı`dxNumberBox`,
FK/lookup → `dxSelectBox` (az kayıt) veya `dxGridBox`/`dxLookup` (çok kayıt/çok kolon),
çok değerli FK → `dxTagBox`, uzun metin → `dxTextArea`, HTML → `dxHtmlEditor`.
Her editörün kendine özgü seçenekleri için §4.3.2.
---
## 4. Artefakt A — ListForm ekranı (Wizard seed dosyası)
Konum: `configs/seeds/{kapsam}/wizard/{yyyyMMddHHmmss}_{WizardName}.json`
Okuyan: `WizardDataSeeder` · Yazan: `ListFormWizardAppService`
### 4.1 Dosya iskeleti
```jsonc
{
"Wizard": { /* ListFormWizardDto — 4.2 */ },
"IsDeletedField": true, // tabloda IsDeleted var mı (soft delete)
"IsCreatedField": true, // tabloda CreatorId var mı (audit)
"InsertedRecords": { // bu çalıştırmanın GERÇEKTEN yarattığı kayıtlar
"LanguageKeys": [], "PermissionGroupNames": [], "PermissionNames": [],
"MenuCodes": [], "DataSourceCodes": []
}
}
```
`InsertedRecords` **silme sözleşmesidir**: wizard silindiğinde yalnızca burada listelenen
kayıtlar silinir, paylaşılan kayıtlara (var olan izin grubu, var olan üst menü) dokunulmaz.
Elle dosya yazarken buraya var olan bir kaydı koyma — silme onu da götürür.
### 4.2 `Wizard` bloğu — alan alan
**Kimlik ve yol**
| Alan | Anlam |
| --- | --- |
| `ComponentKind` | `0` List (varsayılan) · `1` Custom. Dosyadaki ilk alan olmalı. |
| `WizardName` | Dosya adının ve export zip adının kaynağı. |
| `ListFormCode` | Ekranın kodu; route ve metadata bu koda göre çözülür. |
| `MenuCode` | Menü kaydının kodu; yetki adlarının kökü de budur. |
| `MenuUrl` | List'te `/admin/list/{MenuCode}` hesaplanır; Custom'da bileşenin `RoutePath`'i yazılır. |
| `CustomComponentName` | Yalnızca Custom yolunda; bağlanacak bileşenin adı. |
| `EditFileName` | Yalnızca **istek girdisi**: doluysa güncellemedir, sunucu önce eski dosyayı ve ürettiği kayıtları siler. Seed dosyasına yazılmaz. |
**Menü**
| Alan | Anlam |
| --- | --- |
| `CreateMenu` | `false` ise menü ve üst menü kaydı **hiç üretilmez**; ListForm, yetki ve dil anahtarları yine üretilir. SubGrid/parça olarak kullanılacak ekranlar için. Varsayılan `true`. |
| `MenuParentCode` / `MenuParentShortName` / `MenuParentIcon` | Üst menü; yoksa oluşturulur. |
| `MenuIcon` | React-icons adı (`FcBiohazard`, `FaBox`…). |
| `MenuOrder` | Kardeşler arası sıra. |
**Yetki ve dil**
| Alan | Anlam |
| --- | --- |
| `PermissionGroupName` | Var olan grup ya da yeni grup adı. |
| `PermissionGroupDisplayNameEn` / `Tr` | Grup adıyla aynı dil anahtarına yazılır. Var olan grup seçilip boş bırakılırsa sunucu veritabanındaki değeri geri yazar. |
| `LanguageTextMenuEn` / `Tr` | Menü etiketi. |
| `LanguageTextTitleEn` / `Tr` | Ekran başlığı (yalnızca List yolunda üretilir). |
| `LanguageTextDescEn` / `Tr` | Ekran açıklaması (yalnızca List yolunda). |
| `LanguageTextMenuParentEn` / `Tr` | Üst menü etiketi. |
Üretilen yetkiler: `{MenuCode}` (okuma, kök) + List yolunda `.Create .Update .Delete .Export
.Import .Note`, Custom yolunda yalnızca `.Create .Update .Delete`. Hepsi admin rolüne grant
edilir.
**Veri**
| Alan | Anlam |
| --- | --- |
| `DataSourceCode` | Bağlantı kodu; varsayılan `"Default"`. |
| `DataSourceConnectionString` | Yeni bir bağlantı tanımlanıyorsa; aksi hâlde boş. **Asla gerçek kimlik bilgisi yazma.** |
| `SelectCommandType` | Bkz. §3. |
| `SelectCommand` | Tablo/view adı, fonksiyon çağrısı ya da sorgu metni. |
| `KeyFieldName` / `KeyFieldDbSourceType` | Birincil anahtar ve tipi. |
**Davranış**
| Alan | Anlam |
| --- | --- |
| `IsTenant` / `IsBranch` / `IsOrganizationUnit` | Otomatik kırılım filtresi. Tabloda `TenantId` varsa `IsTenant: true` **olmalı**. |
| `AllowAdding` / `AllowUpdating` / `AllowDeleting` / `AllowDetail` / `ConfirmDelete` | Toolbar ve satır aksiyonları. |
| `DefaultLayout` | `"grid"`, `"card"`, `"pivot"`, `"chart"`, `"tree"`, `"gantt"`, `"scheduler"`, `"todo"`. |
| `Grid` `Card` `Pivot` `Chart` `Tree` `Gantt` `Scheduler` `Todo` | Hangi görünümlerin açık olduğu. Açtığın her görünümün option bloğunu da doldur. |
**Görünüm option blokları** — yalnızca ilgili bayrak `true` ise anlamlıdır:
| Blok | Zorunlu alanlar |
| --- | --- |
| `TreeOptionDto` | `KeyExpr`, `ParentIdExpr`; opsiyonel `HasItemsExpr`, `RootValue`, `AutoExpandAll`, `RecursiveSelection` |
| `GanttOptionDto` | `KeyExpr`, `ParentIdExpr`, `TitleExpr`, `StartExpr`, `EndExpr`; `ProgressExpr`, `ScaleType` (`hours`/`days`/`weeks`/`months`), `Allow*` bayrakları |
| `SchedulerOptionDto` | `TextExpr`, `StartDateExpr`, `EndDateExpr`; `AllDayExpr`, `RecurrenceRuleExpr`, `StartDayHour`, `EndDayHour`, `DefaultView` (`day`/`week`/`month`), `CellDuration`, `FirstDayOfWeek`, `Allow*` |
| `TodoOptionDto` | `TitleExpr`, `StatusExpr`; `DescriptionExpr`, `DueDateExpr`, `TagExpr`, `AssigneeExpr`, `PriorityExpr`, `CompletedExpr`, `OrderExpr`, `StatusOrder` (virgülle ayrılmış kolon sırası), `AllowDragging` |
### 4.3 `Groups` — alanlar ve düzenleme formu
`Groups`, hem grid sütunlarını hem düzenleme formunun düzenini tanımlar. Buradaki her `Items`
öğesi bire bir bir **`ListFormField` kaydına** dönüşür (§4.8); ekranda görünen sütunların tek
kaynağı odur. Grup yapısı yalnızca düzenleme formunun yerleşimini belirler — grid sütun sırası
`Items` sırasından gelir.
```jsonc
"Groups": [
{
"Caption": "Genel",
"ColCount": 2,
"Items": [ /* WizardColumnItemInputDto */ ]
}
]
```
Her `Items` öğesi:
| Alan | Anlam |
| --- | --- |
| `FieldName` | Veri kolonunun adı (SQL'deki adıyla birebir). |
| `CaptionName` | Dil anahtarı ya da düz başlık. |
| `TurkishCaption` / `EnglishCaption` | Dil metinleri; ikisi de doldurulur. |
| `EditorType` | Bkz. §3. |
| `EditorOptions` | DevExtreme editör JSON'u — §6. |
| `EditorScript` | Alan davranış script'i — §5. |
| `DbSourceType` | Bkz. §3. |
| `IsRequired` | Zorunluluk. |
| `IncludeInEditingForm` | `false` ise yalnızca grid sütunu olur, forma girmez. |
| `ColSpan` | Formda kaç kolon kaplar (grubun `ColCount` değeri içinde). |
| `LookupDataSourceType` | `1/2/3` — bkz. §3. `0` = lookup yok. |
| `LookupQuery` | `Query` tipinde SQL; `StaticData` tipinde JSON dizisi; `WebService` tipinde URL. |
| `ValueExpr` / `DisplayExpr` | Lookup'ın değer ve etiket kolonları. |
### 4.3.1 Sütunlar nasıl otomatik üretilir
Gösterilecek **tüm sütunlar `ListFormField` kayıtlarında** durur. Wizard bu kayıtları veritabanı
kolon metadata'sından (`GetTableColumnsAsync`) otomatik üretir; sen yalnızca farklı olmasını
istediğin şeyi belirtirsin.
Bir kolon `Groups[].Items[]` içine eklendiğinde varsayılanlar şöyle türetilir:
| Alan | Nereden gelir |
| --- | --- |
| `EditorType` | SQL tipinden çıkarılır (aşağıdaki tablo) |
| `DbSourceType` | SQL tipinden `System.Data.DbType` karşılığına eşlenir (§3) |
| `IsRequired` | Kolon `NOT NULL` ise `true` |
| `TurkishCaption` / `EnglishCaption` | Kolon adı PascalCase'ten kelimelere ayrılır: `OrderNo``Order No` |
| `CaptionName` | `App.Listform.ListformField.{KolonAdı}` |
| `ColSpan` | `1` |
| `IncludeInEditingForm` | `true` |
| `ValueExpr` / `DisplayExpr` | `Key` / `Name` (lookup tanımlanırsa değiştirilir) |
| `EditorOptions` / `EditorScript` | Boş — ihtiyaç varsa sen yazarsın |
**SQL tipi → `EditorType` çıkarımı** (`inferEditorType`):
| SQL tipi | Üretilen editör |
| --- | --- |
| `bit` | `dxCheckBox` |
| `int`, `bigint`, `smallint`, `tinyint`, `decimal`, `numeric`, `float`, `real`, `money`, `smallmoney` | `dxNumberBox` |
| `date`, `datetime`, `datetime2`, `smalldatetime`, `datetimeoffset` | `dxDateBox` |
| diğer hepsi | `dxTextBox` |
Çıkarım kasıtlı olarak dardır. **Foreign key kolonları `dxNumberBox`/`dxTextBox` olarak gelir**
bunları `dxSelectBox` / `dxGridBox` / `dxTagBox` yapmak ve lookup tanımlamak senin işindir.
Uzun metin (`nvarchar(max)`) için `dxTextArea`, HTML içerik için `dxHtmlEditor`, durum kolonları
için `dxRadioGroup`/`dxSwitch` seçilmelidir; otomatik çıkarım bunları bilemez.
### 4.3.2 Editör tipine göre özellikler
Her editör kendi `EditorOptions` sözlüğüne sahiptir. Aşağıdaki tablo, o editöre **özgü** olan
anahtarları listeler; ortak anahtarlar (§4.3.3) her editörde geçerlidir.
| `EditorType` | Ne için | Uygun `DbSourceType` | Kendine özgü `EditorOptions` |
| --- | --- | --- | --- |
| `dxTextBox` | Kısa metin | String (16) | — (ortak: `maxLength`, `mask`, `placeholder`) |
| `dxTextArea` | Uzun metin | String (16) | `autoResizeEnabled`, `minHeight` |
| `dxNumberBox` | Sayı | Int32/Int64/Decimal/Double (7,8,10,11,12) | `format.type` (`fixedPoint`/`currency`/`percent`), `format.precision`, `format.currency`, `useMaskBehavior`, `useLargeSpinButtons`, `invalidValueMessage` |
| `dxDateBox` | Tarih / tarih-saat | Date/DateTime (5,6,26,27) | `useMaskBehavior` (+ ortak: `type`, `displayFormat`, `dateSerializationFormat`, `pickerType`, `interval`, `calendarOptions.*`, `invalidDateMessage`) |
| `dxDateRangeBox` | Tarih aralığı | Date/DateTime | ortak tarih anahtarları |
| `dxCalendar` | Gömülü takvim | Date/DateTime | `calendarOptions.zoomLevel`, `calendarOptions.firstDayOfWeek` |
| `dxCheckBox` | Evet/hayır | Boolean (3) | `text` (kutunun yanındaki etiket) |
| `dxSwitch` | Evet/hayır (anahtar) | Boolean (3) | `switchedOnText`, `switchedOffText` |
| `dxRadioGroup` | Az sayıda seçenek | String/Int32 | `layout` (`horizontal`/`vertical`) + **lookup zorunlu** |
| `dxSelectBox` | Tek seçim (az kayıt) | FK tipi | `acceptCustomValue`, `searchEnabled`, `minSearchLength`, `searchExpr`, `searchMode`, `searchTimeout`, `noDataText`, `showDataBeforeSearch` + **lookup zorunlu** |
| `dxLookup` | Tek seçim (çok kayıt, mobil dostu) | FK tipi | `dropDownOptions.*`, `searchEnabled`, `applyValueMode` + **lookup zorunlu** |
| `dxDropDownBox` | Özel içerikli açılır kutu | FK tipi | `dropDownOptions.width/height/hideOnOutsideClick`, `deferRendering`, `openOnFieldClick` |
| `dxGridBox` | Çok kolonlu seçim ızgarası | FK tipi | `columns`\*, `filterRowVisible`\*, `selectionMode`\*, `acceptCustomValue`\* + **lookup zorunlu** |
| `dxTagBox` | Çok değerli seçim | String (virgüllü) / ilişki tablosu | `showSelectionControls`\*, `maxDisplayedTags`\*, `showMultiTagOnly`\*, `applyValueMode`\*, `searchEnabled`\*, `acceptCustomValue`\*, `hideSelectedItems`, `multiline` + **lookup zorunlu** |
| `dxAutocomplete` | Serbest metin + öneri | String (16) | `minSearchLength`, `searchExpr`, `searchTimeout` |
| `dxColorBox` | Renk | String (16) | `editAlphaChannel`, `keyStep` |
| `dxSlider` | Tek değerli kaydırıcı | Sayı | `min`, `max`, `tooltip.showMode` |
| `dxRangeSlider` | Aralık kaydırıcı | Sayı | `min`, `max`, `showRange` |
| `dxHtmlEditor` | Zengin metin | String (16) | `toolbar.items`, `toolbar.multiline`, `valueType` (`html`/`markdown`), `mediaResizing.enabled`, `imageUpload.uploadUrl`, `imageUpload.fileUploadMode` |
| `dxImageUpload` | Görsel yükleme (platform) | String (16) | `uploadUrl`\*, `accept`\*, `multiple`\*, `maxFileSize`\*, `width`\*, `height`\* |
| `dxImageViewer` | Görsel gösterimi (platform) | String (16) | `width`, `height` |
`\*` ile işaretli anahtarlar backend'de **tipli DTO'ya** çözülür:
`GridBoxOptionsDto`, `TagBoxOptionsDto`, `ImageUploadOptionsDto`. Yanlış tipte yazılan bir değer
hata vermez, **sessizce yok sayılır** — bu yüzden `columns` bir dizi, `maxDisplayedTags` bir
sayı, `multiple` bir boolean olmalıdır.
**Lookup zorunlu** yazan editörlerde `LookupDataSourceType` + `LookupQuery` + `ValueExpr` +
`DisplayExpr` dolu olmalıdır; aksi hâlde alan boş bir liste gösterir:
```jsonc
{
"FieldName": "StatusId", "EditorType": "dxSelectBox", "DbSourceType": 11,
"LookupDataSourceType": 1,
"LookupQuery": "[{\"Key\":1,\"Name\":\"Taslak\"},{\"Key\":2,\"Name\":\"Onayda\"},{\"Key\":3,\"Name\":\"Onaylandı\"}]",
"ValueExpr": "Key", "DisplayExpr": "Name"
}
```
`LookupDataSourceType` `1` (StaticData) ise `LookupQuery` bir JSON dizisi, `2` (Query) ise SQL,
`3` (WebService) ise URL'dir. Cascade (ebeveynçocuk) davranışı ListForm editöründeki lookup
ayarlarından tanımlanır.
### 4.3.3 Her editörde geçerli ortak seçenekler
Gruplar hâlinde (`optionSpecs.ts` sözlüğünün tamamı UI'yı buradan üretir):
| Grup | Anahtarlar |
| --- | --- |
| Genel | `disabled`, `readOnly`, `visible`, `hint`, `tabIndex`, `showClearButton`, `valueChangeEvent`, `validationMessageMode`, `validationMessagePosition` |
| Görünüm | `width`, `height`, `stylingMode`, `label`, `labelMode`, `elementAttr.class`, `inputAttr.style`, `inputAttr.aria-label`, `buttons` |
| Metin | `placeholder`, `maxLength`, `spellcheck`, `mode`, `mask`, `maskChar`, `maskRules.X`, `maskInvalidMessage`, `showMaskMode`, `useMaskedValue`, `encodeHtml` |
| Sayı | `min`, `max`, `format`, `showSpinButtons` |
| Tarih | `type`, `displayFormat`, `dateSerializationFormat`, `pickerType`, `interval`, `invalidDateMessage` |
| Açılır liste | `noDataText`, `searchEnabled`, `minSearchLength`, `searchExpr`, `searchMode`, `searchTimeout`, `showDataBeforeSearch`, `openOnFieldClick`, `deferRendering`, `wrapItemText`, `dropDownOptions.*` |
Elle JSON yazmak yerine C# tarafında `EditorOptions` akıcı yapısını kullan (§6); hazır
başlangıçlar bu anahtarların doğru bileşimlerini üretir.
### 4.4 Çalışan örnek — tablo üzerinden liste ekranı
```jsonc
{
"Wizard": {
"ComponentKind": 0,
"WizardName": "Orders",
"ListFormCode": "App.Mrp.Orders",
"MenuCode": "App.Mrp.Orders",
"MenuOrder": 1,
"CreateMenu": true,
"MenuUrl": "/admin/list/App.Mrp.Orders",
"IsTenant": true, "IsBranch": false, "IsOrganizationUnit": false,
"AllowAdding": true, "AllowUpdating": true, "AllowDeleting": true,
"AllowDetail": false, "ConfirmDelete": true,
"DefaultLayout": "grid",
"Grid": true, "Card": true, "Pivot": true, "Chart": true,
"Tree": false, "Gantt": false, "Scheduler": false, "Todo": false,
"LanguageTextMenuEn": "Orders", "LanguageTextMenuTr": "Siparişler",
"LanguageTextTitleEn": "Orders", "LanguageTextTitleTr": "Siparişler",
"LanguageTextDescEn": "Order list", "LanguageTextDescTr": "Sipariş listesi",
"LanguageTextMenuParentEn": "MRP", "LanguageTextMenuParentTr": "MRP",
"PermissionGroupName": "App.Mrp",
"PermissionGroupDisplayNameEn": "MRP", "PermissionGroupDisplayNameTr": "MRP",
"MenuParentCode": "App.Mrp", "MenuParentShortName": "MRP", "MenuParentIcon": "FcFactory",
"MenuIcon": "FcShipped",
"DataSourceCode": "Default", "DataSourceConnectionString": "",
"SelectCommandType": 1,
"SelectCommand": "Mrp_T_Order",
"KeyFieldName": "Id",
"KeyFieldDbSourceType": 11,
"Groups": [
{
"Caption": "Genel", "ColCount": 2,
"Items": [
{
"FieldName": "OrderNo", "CaptionName": "App.Listform.ListformField.OrderNo",
"TurkishCaption": "Sipariş No", "EnglishCaption": "Order No",
"EditorType": "dxTextBox", "DbSourceType": 16,
"IsRequired": true, "IncludeInEditingForm": true, "ColSpan": 1,
"EditorOptions": "{\"showClearButton\":true,\"maxLength\":20}"
},
{
"FieldName": "CustomerId", "CaptionName": "App.Listform.ListformField.Customer",
"TurkishCaption": "Cari", "EnglishCaption": "Customer",
"EditorType": "dxSelectBox", "DbSourceType": 11,
"IsRequired": true, "IncludeInEditingForm": true, "ColSpan": 1,
"LookupDataSourceType": 2,
"LookupQuery": "SELECT Id, Name FROM Crm_T_Customer WHERE IsDeleted = 0",
"ValueExpr": "Id", "DisplayExpr": "Name"
},
{
"FieldName": "Quantity", "CaptionName": "App.Listform.ListformField.Quantity",
"TurkishCaption": "Miktar", "EnglishCaption": "Quantity",
"EditorType": "dxNumberBox", "DbSourceType": 7,
"IsRequired": true, "IncludeInEditingForm": true, "ColSpan": 1,
"EditorOptions": "{\"format\":{\"type\":\"fixedPoint\",\"precision\":2},\"showSpinButtons\":true}"
},
{
"FieldName": "UnitPrice", "CaptionName": "App.Listform.ListformField.UnitPrice",
"TurkishCaption": "Birim Fiyat", "EnglishCaption": "Unit Price",
"EditorType": "dxNumberBox", "DbSourceType": 7,
"IsRequired": true, "IncludeInEditingForm": true, "ColSpan": 1,
"EditorOptions": "{\"format\":{\"type\":\"fixedPoint\",\"precision\":2}}"
},
{
"FieldName": "Total", "CaptionName": "App.Listform.ListformField.Total",
"TurkishCaption": "Tutar", "EnglishCaption": "Total",
"EditorType": "dxNumberBox", "DbSourceType": 7,
"IsRequired": false, "IncludeInEditingForm": true, "ColSpan": 2,
"EditorOptions": "{\"readOnly\":true,\"format\":{\"type\":\"fixedPoint\",\"precision\":2}}",
"EditorScript": "// @builder {\"v\":1,\"rules\":[{\"id\":\"r1\",\"recipe\":\"multiply\",\"condition\":{\"operator\":\"always\",\"source\":\"\",\"value\":\"\"},\"params\":{\"left\":\"Quantity\",\"right\":\"UnitPrice\",\"target\":\"Total\",\"digits\":\"2\"}}]}\nset('Total', round(num('Quantity') * num('UnitPrice'), 2))"
}
]
}
],
"SubForms": [], "Widgets": [],
"WorkflowDto": { "ApprovalUserFieldName": "", "ApprovalDateFieldName": "",
"ApprovalStatusFieldName": "", "ApprovalDescriptionFieldName": "",
"ApprovalIsFilterUserName": false, "ApprovalIsResetWorkflow": false, "Criteria": [] }
},
"IsDeletedField": true,
"IsCreatedField": true,
"InsertedRecords": {
"LanguageKeys": ["App.Mrp.Orders", "App.Mrp"],
"PermissionGroupNames": ["App.Mrp"],
"PermissionNames": ["App.Mrp.Orders", "App.Mrp.Orders.Create", "App.Mrp.Orders.Update",
"App.Mrp.Orders.Delete", "App.Mrp.Orders.Export", "App.Mrp.Orders.Import",
"App.Mrp.Orders.Note"],
"MenuCodes": ["App.Mrp", "App.Mrp.Orders"],
"DataSourceCodes": []
}
}
```
> `EditorScript` alanındaki `// @builder` başlığı ile onu izleyen kod satırı **birebir
> tutarlı** olmalıdır. Elle yazmak yerine C# tarafında `EditorScript.Build(...)` kullan ya da
> Script Builder'da üret; uyuşmazsa dialog script'i "elle düzenlenmiş" sayar.
### 4.4.1 Seeder'ın senin yerine ürettikleri — bunları dosyaya yazma
`WizardDataSeeder`, dosyadaki `Wizard` bloğundan yola çıkarak aşağıdakileri **kendisi** üretir.
Bunları seed dosyasında aramaya kalkma, orada yokturlar:
| Üretilen | Kural |
| --- | --- |
| Dil anahtarları | `{MenuCode}` (menü), `{ListFormCode}.Title`, `{ListFormCode}.Desc`, `{PermissionGroupName}` ve her alan için `CaptionName` |
| Yetkiler | `{MenuCode}` kökü + `.Create/.Update/.Delete` (+ List yolunda `.Export/.Import/.Note`) |
| Yetkilerin dil anahtarları | Ortak sabitler: `App.Platform.Create/Update/Delete/Export/Import/Note` |
| Üst menü | `MenuParentCode` yoksa yaratılır, `Order = max(kök Order) + 1` |
| Menü sırası | `MenuOrder` 0/boş ise `max(kardeş Order) + 1` |
| `DataSource` | `DataSourceCode` yoksa yaratılır; bağlantı metninde `Server` geçiyorsa `Mssql`, aksi hâlde `Postgresql` |
| `LayoutJson` | Görünüm bayrakları + `DefaultLayout` |
| `PermissionJson` / alan `PermissionJson` | `{MenuCode}` kökünden türetilir |
| `EditingFormJson` | `Groups` içinden; anahtar alan ve `IncludeInEditingForm: false` olanlar hariç |
| Form genişlik/yükseklik | Grup kolon sayısı ve satır sayısından hesaplanır (en fazla 3 kolon) |
| `FilterRowJson`, `HeaderFilterJson`, `SearchPanelJson`, `GroupPanelJson`, `ColumnOptionJson`, `PagerOptionJson`, `ExportJson` | Varsayılanlar |
| `DeleteCommand` | `IsDeletedField: true` ise soft delete `UPDATE` komutu; `false` ise `null` |
| `DefaultFilter` | `IsDeletedField: true` ise `"IsDeleted" = 'false'` |
| `InsertFieldsDefaultValueJson` | `IsCreatedField: true` ise `CreationTime=@NOW`, `CreatorId=@USERID`, `IsDeleted=false`, `Id=@NEWID`; değilse yalnızca `Id=@NEWID` |
| `DeleteFieldsDefaultValueJson` | `IsDeletedField: true` ise `DeleterId=@USERID`, `Id=@ID`; değilse yalnızca `Id=@ID` |
| `ValidationRuleJson` | `IsRequired: true` olan alanlara required kuralı |
| `LookupJson` | `LookupQuery` doluysa `LookupDataSourceType` + `DisplayExpr` + `ValueExpr` ile |
| `ShowNote` | Alt form veya iş akışı varsa açılır |
| `SelectionJson` | İş akışı varsa `single`, yoksa `none` |
| Görünüm option JSON'ları | İlgili bayrak açık **ve** zorunlu alanı dolu ise yazılır; eksikse görünüm sessizce kapanır |
**Dikkat edilecek tuzaklar:**
- `Tree`/`Gantt` için `ParentIdExpr`, `Scheduler` için `TextExpr`, `Todo` için `TitleExpr` **ve**
`StatusExpr` boşsa o görünümün JSON'u hiç yazılmaz — bayrağı `true` yapmak yetmez.
- Anahtar alanı (`KeyFieldName`) `Groups` içine koyarsan sütun olarak gizlenir ve forma girmez;
yine de tanımlaman doğrudur, çünkü tip bilgisi oradan okunur.
- `IsDeletedField: false` verirsen ekranda **kalıcı silme** olmaz (`DeleteCommand` null kalır);
soft delete kolonları olmayan tabloda silme istiyorsan ListForm editöründen komut yazmalısın.
- Dil anahtarı zaten varsa metni **ezilmez**. Yanlış metin görüyorsan anahtar başka bir yerde
tanımlanmıştır.
### 4.5 Güncelleme, idempotency ve silme semantiği
**Seeder idempotenttir — bir ekran zaten varsa dosya atlanır.** "Zaten var" ölçütü yola göre
değişir:
| Yol | Kontrol |
| --- | --- |
| `List` | `ListForm` kaydında bu `ListFormCode` var mı |
| `Custom` + menülü | Bu `MenuCode` ile menü var mı |
| `Custom` + menüsüz | Bu `MenuCode` adında yetki var mı |
Sonuç: **var olan bir ekranı seed dosyasını düzenleyerek değiştiremezsin.** Değiştirmenin üç
yolu vardır:
1. Wizard ekranından `EditFileName` ile yeniden çalıştır (önce eski dosya ve
`InsertedRecords` kayıtları silinir, sonra yenisi üretilir) — **tercih edilen yol**.
2. ListForm editöründen ince ayar yap (seed dosyasına yansımaz, §9.6 uyarısı).
3. İlgili kayıtları elle sil, sonra seed'i yeniden çalıştır.
> `List` yolunda seeder ekranı uygularken `ListForm`, `ListFormField` ve `ListFormWorkflow`
> kayıtlarını **silip yeniden yazar**; idempotency kapısını geçen dosya için bu güvenlidir.
**Silme:** yalnızca `InsertedRecords` içindekiler silinir. Paylaşılan üst menü ve izin grubu
başka bir wizard tarafından yaratıldıysa kalır. Bu yüzden `InsertedRecords` listesine var olan
bir kaydı yazmak veri kaybettirir.
### 4.6 Alt formlar ve widget'lar
**`SubForms` (`SubFormDto`)** — ana kaydın altında sekme olarak açılan ekranlar:
```jsonc
"SubForms": [
{
"TabType": "List", // List | Tree | Gantt | Scheduler | Form | Chart
"TabTitle": "App.Mrp.OrderItems", // dil anahtarı
"Code": "App.Mrp.OrderItems", // hedef ListFormCode
"IsRefresh": true, // ana kayıt değişince yenilensin
"Relation": [
{ "ParentFieldName": "Id", "ChildFieldName": "OrderId", "DbType": 11 }
]
}
]
```
`Relation` bir dizidir — bileşik anahtarda birden fazla eşleme verilir.
**`Widgets` (`WidgetEditDto`)** — ekranın üstündeki KPI kartları:
```jsonc
"Widgets": [
{
"Title": "App.Mrp.Widget.OpenOrders",
"SubTitle": "App.Mrp.Widget.OpenOrdersSub",
"SqlQuery": "SELECT COUNT(*) FROM Mrp_T_Order WHERE IsDeleted = 'false' AND Status = 1",
"Value": "", // sabit değer; SqlQuery doluysa boş bırakılır
"ValueClassName": "text-3xl",
"Icon": "FcClock",
"Color": "blue",
"ColSpan": 1,
"ColGap": 16,
"ClassName": "",
"OnClick": "", // tıklanınca açılacak adres
"IsActive": true
}
]
```
`SqlQuery` **tek hücre** döndürmelidir; tenant kırılımı gerekiyorsa sorguya `TenantId`
koşulunu kendin yaz — widget sorgusu ListForm'un otomatik tenant filtresine tabi değildir.
Alt form olarak kullanılacak ekranı **menüsüz wizard** (`CreateMenu: false`) ile üret; menüde
görünmesin ama kendi yetkileri ve alan tanımları olsun. Dosya adının zaman damgası ana
wizard'ınkinden **küçük** olmalıdır (§0.2).
### 4.7 İş akışı (onay süreci)
`WorkflowDto` onay alanlarını tabloda karşılığı olan kolonlara bağlar:
| Alan | Tabloda karşılığı |
| --- | --- |
| `ApprovalUserFieldName` | Onaylayan kullanıcı |
| `ApprovalDateFieldName` | Onay tarihi |
| `ApprovalStatusFieldName` | Durum |
| `ApprovalDescriptionFieldName` | Onay/ret açıklaması |
| `ApprovalIsFilterUserName` | Liste yalnızca kullanıcının onayına düşenleri göstersin |
| `ApprovalIsResetWorkflow` | Kayıt güncellenince akış başa dönsün |
`Criteria` düğüm grafiğidir. `Kind` değerleri: **`Start`**, **`Compare`**, **`Approval`**,
**`Inform`**, **`End`**.
```jsonc
"WorkflowDto": {
"ApprovalUserFieldName": "ApproverId",
"ApprovalDateFieldName": "ApprovalDate",
"ApprovalStatusFieldName": "Status",
"ApprovalDescriptionFieldName": "ApprovalNote",
"ApprovalIsFilterUserName": true,
"ApprovalIsResetWorkflow": true,
"Criteria": [
{ "Id": "N1", "Kind": "Start", "Title": "Başlangıç",
"NextOnStart": "N2", "PositionX": 40, "PositionY": 40 },
{ "Id": "N2", "Kind": "Compare", "Title": "Tutar kontrolü",
"CompareColumn": "Amount", "CompareOperator": ">", "CompareValue": 5000,
"CompareOutcomes": [
{ "Label": "> 5000", "TargetId": "N3",
"Conditions": [ { "CompareColumn": "Amount", "CompareOperator": ">", "CompareValue": 5000 } ] },
{ "Label": "<= 5000", "TargetId": "N4",
"Conditions": [ { "CompareColumn": "Amount", "CompareOperator": "<=", "CompareValue": 5000 } ] }
],
"PositionX": 40, "PositionY": 160 },
{ "Id": "N3", "Kind": "Approval", "Title": "Genel Müdür onayı",
"Approver": "<roleOrUserId>", "NextOnApprove": "N5", "NextOnReject": "N6",
"PositionX": 300, "PositionY": 260 },
{ "Id": "N4", "Kind": "Approval", "Title": "Yönetici onayı",
"Approver": "<roleOrUserId>", "NextOnApprove": "N5", "NextOnReject": "N6",
"PositionX": 40, "PositionY": 260 },
{ "Id": "N5", "Kind": "Inform", "Title": "Talep sahibine bilgi",
"Approver": "<roleOrUserId>", "NextOnStart": "N7", "PositionX": 170, "PositionY": 380 },
{ "Id": "N6", "Kind": "End", "Title": "Reddedildi", "PositionX": 340, "PositionY": 480 },
{ "Id": "N7", "Kind": "End", "Title": "Onaylandı", "PositionX": 170, "PositionY": 480 }
]
}
```
Kurallar:
- **Id sözleşmesi:** `N` ile başlayan kısa id'ler seeder tarafından `{ListFormCode}-{Id}` hâline
getirilir; hedef alanlar (`NextOn*`, `CompareOutcomes[].TargetId`) da aynı normalizasyondan
geçer. Bu yüzden dosyada `N1`, `N2` yazman yeterlidir ve **tekil olmaları şarttır**.
- `Compare` düğümünde çok dallı karar için `CompareOutcomes` kullanılır; iki dallı basit karar
için `NextOnTrue` / `NextOnFalse` yeterlidir.
- `Approval` düğümü `NextOnApprove` / `NextOnReject`, `Start` ve `Inform` düğümleri
`NextOnStart` alanını kullanır. `End` düğümünün çıkışı yoktur.
- `CompareValue` **sayısaldır** (`decimal`); metin karşılaştırması bu düğümle yapılmaz.
- `Title` tekil olmalıdır; seeder tekrar eden başlıkları ayrıştırır ama okunabilirliği bozar.
- Akış varsa ListForm `SelectionMode = single` olur ve not paneli otomatik açılır.
- Görsel tasarım `/admin/listform/edit/{kod}` → Workflow sekmesindedir.
### 4.8 `ListForm` entity — List component'in tek kaynağı
Bir List component'in **bütün** davranışı `ListForm` (ekran) ve `ListFormField` (alan) kayıtlarında
durur. Wizard bunları üretir; `/admin/listform/edit/{kod}` düzenler. Prompt ile üretim yaparken
wizard dosyası yazılır, ama hangi ayarın hangi kolona düştüğünü bilmek gerekir — çünkü wizard'ın
kapsamadığı bir istek geldiğinde cevap "kod yazalım" değil, "şu JSON kolonunu şöyle ayarlayalım"
olmalıdır.
**Kimlik ve varyant**
| Kolon | Ne yapar |
| --- | --- |
| `ListFormCode` | Ekranın kodu; kullanıcı/rol/dil özelleştirmeleri buna bağlanır |
| `CultureName` · `UserId` · `RoleId` | **Aynı ekranın varyantları.** Aynı `ListFormCode` ile ikinci bir kayıt açıp yalnızca bir rol ya da dil için farklı sütun/düzen verebilirsin |
| `ListFormType` | `List` · `Form` — ekranın türü |
| `IsSubForm` / `SubFormsListFormType` | Alt form olarak açıldığında davranış |
**Veri**
| Kolon | Ne yapar |
| --- | --- |
| `DataSourceCode` | Bağlantı |
| `SelectCommandType` + `SelectCommand` | Kaynak türü ve kaynağın kendisi |
| `TableName` | Tablo adı/alias — yazma komutlarının hedefi |
| `KeyFieldName` + `KeyFieldDbSourceType` | Anahtar |
| `SelectFieldsDefaultValueJson` | Select'e geçirilen varsayılan parametreler |
| `DefaultFilter` | Her sorgunun sonuna eklenen WHERE (soft delete filtresi buradadır) |
| `IsTenant` · `IsBranch` · `IsOrganizationUnit` | Otomatik kırılım filtreleri |
| `DataSourceJson` · `CommonJson` | Kaynak ve ortak ayarlar |
**Yazma (CRUD)**
| Kolon | Ne yapar |
| --- | --- |
| `InsertCommand` / `UpdateCommand` / `DeleteCommand` | Özel SQL komutları; boşsa platform üretir |
| `*BeforeCommand` / `*AfterCommand` | Komut öncesi/sonrası çalışan SQL kancaları**stok düşme, log yazma, durum güncelleme gibi işleri kod yazmadan burada yaparsın** |
| `Insert/Update/DeleteFieldsDefaultValueJson` | Otomatik doldurulan alanlar (`@NOW`, `@USERID`, `@NEWID`, `@ID`) |
| `Insert/Update/DeleteServiceAddress` | Varsayılan `list-form-data/*`; özel endpoint ya da Dynamic Service'e yönlendirilebilir |
| `FormFieldsDefaultValueJson` | Form açılışındaki varsayılanlar |
**Görünüm ve düzen — 8 layout**
`LayoutJson` hangi layout'ların açık olduğunu ve varsayılanı taşır. Her layout'un kendi ayar
kolonu vardır:
| Layout | Ayar kolonu | Zorunlu alan |
| --- | --- | --- |
| **Grid** | `ColumnOptionJson`, `RowJson`, `PagerOptionJson`, `SelectionJson`, `StateStoringJson` | — |
| **Card** | `ColumnOptionJson` (kart alanları sütun tanımlarından türer) | — |
| **Pivot** | `PivotOptionJson` + alan bazında `PivotSettingsJson` | — |
| **Chart** | `SeriesJson`, `LegendJson`, `ArgumentAxisJson`, `ValueAxisJson`, `TooltipJson`, `PanesJson`, `AnnotationsJson`, `CrosshairJson`, `ScrollBarJson`, `ZoomAndPanJson`, `SizeJson`, `MarginJson`, `TitleJson`, `AnimationJson`, `Common*Json` | `SeriesJson` |
| **Tree** | `TreeOptionJson` | `KeyExpr` + `ParentIdExpr` |
| **Gantt** | `GanttOptionJson` | `ParentIdExpr` + `TitleExpr` + `StartExpr` + `EndExpr` |
| **Scheduler** | `SchedulerOptionJson` | `TextExpr` + `StartDateExpr` + `EndDateExpr` |
| **Todo (Kanban)** | `TodoOptionJson` | `TitleExpr` + `StatusExpr` |
Sekiz layout **aynı `SelectCommand` üzerinden** beslenir; ayrı sorgu, ayrı ekran, ayrı menü
gerekmez. Kullanıcı ekranın üstünden geçiş yapar, tercihi kullanıcı bazlı saklanır.
Bu, tek bir metadata kaydından DevExtreme'in `DataGrid`, `CardView`, `PivotGrid`, `Chart`,
`TreeList`, `Gantt` ve `Scheduler` bileşenlerinin tamamının sürülmesi demektir. **Todo layout
Kanban görünümüdür** ve diğerleriyle aynı veri hattından beslenir — kolonları `StatusExpr`
değerlerinden üretir, kart sürüklendiğinde ilgili kolonu günceller. Yani "aynı veriyi pano
olarak da görelim" isteği yeni bir ekran değil, `Todo: true` + `TodoOptionDto` demektir.
**Filtre, arama, düzenleme**
| Kolon | Ne yapar |
| --- | --- |
| `FilterRowJson` · `HeaderFilterJson` · `FilterPanelJson` · `SearchPanelJson` · `GroupPanelJson` | Filtreleme/arama/gruplama panelleri |
| `ExtraFilterJson` | Ekrana özel ek filtre araç çubuğu |
| `EditingOptionJson` | Düzenleme modu (row/cell/batch/form/popup), başlık, boyut, izinler |
| `EditingFormJson` | Düzenleme formunun grup/sekme düzeni |
| `CommandColumnJson` | Satır aksiyon sütunu |
| `PermissionJson` | Ekran yetkileri (create/read/update/delete/export/import/note) |
| `SubFormsJson` · `WidgetsJson` · `WorkflowJson` | Alt formlar, KPI kartları, onay akışı |
| `CustomJsSourcesJson` · `CustomStyleSourcesJson` | Sayfa yüklenince çalışan JS/CSS — **son çare** |
| `Width` · `Height` · `FullHeight` · `AdaptiveLayoutJson` | Boyut ve uyarlanabilir yerleşim |
| `ExportJson` | xlsx/csv/pdf dışa aktarma |
**`ListFormField` — alan başına ayarlar**
| Grup | Kolonlar |
| --- | --- |
| Bağlama | `FieldName`, `SourceDbType`, `JoinTableJson` (kolon başka tablodan geliyorsa) |
| Görünüm | `CaptionName`, `PlaceHolder`, `Visible`, `IsActive`, `Width`, `ListOrderNo`, `Alignment`, `Format`, `BandName` |
| Sıralama/filtre | `SortIndex`, `SortDirection`, `AllowSearch`, `ColumnFilterJson`, `ColumnHeaderJson`, `GroupingJson`, `ColumnCustomizationJson` |
| Özet | `TotalSummaryJson`, `GroupSummaryJson` |
| Düzenleme | `EditorType2`, `EditorOptions`, `EditorScript`, `EditOrderNo`, `EditGroupOrderNo`, `ColSpan`, `AllowEditing`, `AllowAdding`, `ValidationRuleJson` |
| Lookup | `LookupJson` |
| Biçimlendirme | `ColumnStylingJson`, `ColumnCssClass`, `ColumnCssValue` |
| Pivot | `PivotSettingsJson` |
| Yetki | `PermissionJson``CanRead`, `CanCreate`, `CanUpdate`, `CanExport` |
| Varyant | `UserId`, `RoleId`, `CultureName` |
> **Kural:** "Şunu da yapabilir miyiz?" sorusuna cevap ararken önce bu iki tabloya bak. Aradığın
> davranışın bir JSON kolonu varsa çözüm konfigürasyondur; `CustomJsSources` ve kod yazmak
> ondan sonra gelir.
### 4.9 Toolbar ve satır butonları — `CommandColumnJson`
Grid'in araç çubuğuna ve satır aksiyon sütununa **özel butonlar** eklenir. Süreç yönetiminin
(onaya gönder, iptal et, belge üret, dış sisteme aktar) kod yazmadan yapıldığı yer burasıdır.
`ListForm.CommandColumnJson` bir `CommandColumnDto[]` taşır:
```jsonc
[
{
"ButtonPosition": 1, // 0 = satır aksiyon sütunu · 1 = toolbar
"AuthName": "App.Hr.AdvanceRequests.Update", // yetki yoksa buton hiç çizilmez
"Text": "App.Hr.AdvanceRequests.SendToApproval", // dil anahtarı (başına :: eklenir)
"Hint": "App.Hr.AdvanceRequests.SendToApprovalHint",
"Icon": "check",
"IsVisible": true,
"VisibleExpression": "",
"Url": "",
"UrlTarget": "_blank",
"DialogName": "",
"DialogParameters": "",
"OnClick": ""
}
]
```
Bir butonun davranışı **üç moddan biridir** ve bu sırayla değerlendirilir:
| Mod | Dolu alan | Ne yapar |
| --- | --- | --- |
| **Adres** | `Url` (+ `UrlTarget`) | Adresi açar. `@FieldName` token'ları **seçili ilk satırın** kolon değerleriyle değiştirilir (`/admin/form/App.Hr.AdvanceRequests/@Id`). PWA modunda hedef `_self` olur. |
| **Diyalog** | `DialogName` + `DialogParameters` | Kayıtlı bir bileşeni diyalog olarak açar. `DialogParameters` bir JSON nesnesidir; `@Kolon` değerleri satırdan doldurulur (`{"requestId":"@Id","amount":"@Amount"}`). |
| **Script** | `OnClick` | Serbest JS. Son çaredir; önce diğer iki mod denenir. |
Kurallar:
- `AuthName` **zorunlu sayılmalıdır**: yetki kontrolünden geçmeyen buton hiç çizilmez. Yetkisi
olmayan kullanıcıya süreç butonu göstermek istemiyorsan tek gereken budur.
- `Text` ve `Hint` dil anahtarıdır; çeviri `::` öneki eklenerek çözülür. Düz metin yazma.
- Toolbar butonu **seçili satırlarla** çalışır; `SelectionJson` uygun modda (`single`/`multiple`)
olmalıdır. İş akışı varsa wizard bunu zaten `single` yapar.
- Satır bazlı aksiyon istiyorsan `ButtonPosition: 0` kullan; buton komut sütununda görünür.
- Onay akışının kendi butonları (Onayla/Reddet) `WorkflowJson` üzerinden **otomatik** gelir;
onları `CommandColumnJson` ile tekrar tanımlama.
Tipik süreç kalıbı: `Url` modunda bir Custom Endpoint'i ya da Dynamic Service'i çağıran adres
verilir, dönüşte liste yenilenir. İş mantığı SQL/serviste kalır; ekranda yalnızca buton tanımı
durur.
---
## 5. Artefakt B — `EditorScript` (alan davranışı)
### 5.1 Script metninin şekli
```
// @builder {"v":1,"rules":[ … ]} ← ilk satır: kuralların JSON'u
// @runOnOpen ← yalnızca bir kural open/both ise
<her kural için tek satır runtime çağrısı>
```
Dialog script'i regex ile çözmez; başlıktaki kurallardan script'i **yeniden üretip** metinle
karşılaştırır. Aynı değilse script "elle düzenlenmiş" sayılır ve kural editörü kapanır.
Bu yüzden başlık ile gövde asla ayrı ayrı düzenlenmez.
### 5.2 Tarifler (recipes)
| Grup | `recipe` | Ürettiği |
| --- | --- | --- |
| calc | `multiply` | `set(T, round(num(A) * num(B), d))` |
| calc | `subtract` | `set(T, round(num(A) - num(B), d))` |
| calc | `percent` | oran alan ya da sabit; `mode` yok/`add`/`subtract` |
| calc | `sum` | `set(T, round(sum('A','B',…), d))` |
| calc | `formula` | serbest ifade: `set(T, num('Gross') * 0.18)` |
| calc | `today` | `set(T, bugün + offsetDays)` (`yyyy-MM-dd`) |
| calc | `days` | iki tarih arası gün (bitiş dahil) |
| calc | `hours` | iki saat arası fark; bitiş küçükse ertesi güne taşar |
| data | `copy` | seçili lookup kaydının kolonunu alana taşır |
| data | `setValue` | sabit/şablonlu değer (`{Alan}`, `{value}`, `{selected.Yol}`) |
| data | `clear` | verilen alanları boşaltır |
| view | `readOnly` | koşula göre kilitler (`invert` ile tersi) |
| interaction | `notify` | uyarı gösterir |
| interaction | `ask` | onay ister; vazgeçilirse alan eski değerine döner ve script durur |
| interaction | `openUrl` | adres açar |
| integration | `apiToField` | API çağırır, cevabın bir parçasını alana yazar |
| — | `custom` | serbest JS satırı (son çare) |
Tetikleyici: `change` (varsayılan) · `open` (form açılışında varsayılan üretmek için) · `both`.
Koşullar `and`/`or` ile birleşir: `always`, `equals`, `notEquals`, `contains`, `empty`,
`notEmpty`, `greaterThan`, `lessThan`, `isTrue`, `isFalse`.
### 5.3 Runtime API (custom/formula içinde kullanılabilir)
| İmza | Ne yapar |
| --- | --- |
| `value` / `field` | Değişen alanın değeri ve adı |
| `get('Alan')` | Form verisinden okur (yol destekler) |
| `num()` / `str()` / `bool()` / `dateOf()` | Tip dönüşümü |
| `set('Alan', deger)` / `set({A:1,B:2})` | Yazar (toplu yazım tek flush) |
| `clear('A','B')` | Boşaltır |
| `copy('kolon','Alan')` | Seçili kayıttan taşır |
| `selected('Yol')` / `snum()` / `sstr()` | Lookup/GridBox seçili kaydı |
| `readOnly('Alan', kosul)` | Alanı kilitler |
| `round(x, 2)` / `sum('A','B')` | Matematik |
| `days('Bas','Bit')` / `hours('Bas','Bit')` | Tarih/saat farkı |
| `tpl('{Alan} - {value}')` | Şablon |
| `notify()` / `ask()` / `openUrl()` | Etkileşim |
| `await api('/api/x','data.name')` | HTTP |
| `isReady` | Script form açılışında mı çalışıyor |
### 5.4 Seeder tarafı (C#) — tercih edilen yol
`api/src/Sozsoft.Platform.Domain.Shared/Editors/`
```csharp
EditorScript = EditorScript.Build(
EditorScript.Multiply("Quantity", "UnitPrice", "Total", digits: 2),
EditorScript.Percent("Total", "VatRate", "VatAmount", 2, EditorScriptPercentMode.Add)
.When(EditorScriptCondition.IsNotEmpty("VatRate")),
EditorScript.ReadOnly("Total"),
EditorScript.Today("OrderDate").OnOpen(),
EditorScript.Copy("Customer.TaxNumber", "TaxNumber"),
EditorScript.Ask("Fiyatı değiştirmek istediğinize emin misiniz?")
.When(EditorScriptCondition.GreaterThan("UnitPrice", "1000")));
```
Tek kurallı script için `EditorScriptRule` doğrudan `string`'e dönüşür:
```csharp
EditorScript = EditorScript.Days("StartDate", "EndDate", "DayCount");
```
> **Eşleme kuralı:** `scriptRecipes.ts` ile bu sınıflar aynı çıktıyı üretmek zorundadır. Birinde
> bir tarif değişirse aynı görevde diğeri de değişir.
---
## 6. Artefakt C — `EditorOptions`
DevExtreme editörüne geçirilen JSON. UI tarafındaki sözlük `optionSpecs.ts`, hazır kalıplar
`presets.ts`, C# tarafı `EditorOptions` + `EditorOptionsBuilder`.
| İhtiyaç | C# | Ürettiği JSON (özet) |
| --- | --- | --- |
| Pasif alan | `EditorOptions.Disabled()` | `{"disabled":true}` |
| Salt okunur | `EditorOptions.ReadOnly()` | `{"readOnly":true}` |
| Temizle düğmesi | `EditorOptions.ShowClearButton()` | `{"showClearButton":true}` |
| Çok satırlı | `EditorOptions.Multiline(80)` | `{"height":80}` |
| Sabit ondalık | `EditorOptions.Number(2)` | format + maske + spin |
| Yüzde | `EditorOptions.Percent()` | |
| Tarih / tarih-saat / saat | `EditorOptions.Date()` · `DateTime()` · `Time(15)` | |
| Telefon | `EditorOptions.Phone()` | global format + maske + örnek |
| Kaydırıcı | `EditorOptions.Slider()` | |
| Görsel yükleme | `EditorOptions.ImageUpload(multiple: true, 80, 80)` | |
| Zengin metin | `EditorOptions.Html(240)` | tam araç çubuğu |
Akıcı ekleme: `.Placeholder()`, `.MaxLength()`, `.Height()`, `.HeightCss("100%")`, `.Width()`,
`.Format()`, `.FixedPoint(p)`, `.DisplayFormat()`, `.SerializationFormat()`, `.Mask()`,
`.Text()`, `.Number()`, `.Flag()`, `.Json()`.
```csharp
EditorOptions = EditorOptions.Number(4).ShowClearButton(),
EditorOptions = EditorOptions.Multiline(60).Placeholder("Açıklama"),
EditorOptions = EditorOptions.New().Flag("acceptCustomValue", true),
```
Kurallar:
- Aynı anahtar iki kez verilirse sonuncusu geçerlidir; anahtar sırası korunur.
- `optionSpecs.ts` içinde `platform: true` işaretli ayarlar backend'de tipli DTO'ya
(`GridBoxOptionsDto`, `TagBoxOptionsDto`, `ImageUploadOptionsDto`) çözülür; yanlış tip sessizce
yok sayılır.
- Preset'ler mevcut JSON ile **birleşir**, diğer ayarları silmez.
---
## 7. Artefakt D — Custom Component (Visual Designer)
ListForm ile ifade edilemeyen ekranın yolu **fiziksel React sayfası değil**, tasarımcıda
üretilen bir custom component'tir.
### 7.1 Seed dosyası
`configs/seeds/{kapsam}/custom/{Name}.json`
```jsonc
{
"GeneratedAt": "2026-08-20T11:23:45Z",
"CustomComponents": [
{
"Name": "OrderBoard",
"RoutePath": "/admin/order-board",
"Code": "/*__SOZSOFT_VISUAL_DESIGNER__<url-encoded designer doc>__*/\nconst OrderBoard = () => { … }\n\nexport default OrderBoard",
"Props": "{\"visualDesigner\":{ … }}",
"Description": "",
"IsActive": true,
"Dependencies": ["OrderCard"],
"DataSources": [
{
"Name": "Mrp_T_Order — GetList",
"Method": "GET",
"Url": "/api/app/crudendpoint/Mrp_T_Order",
"ResponsePath": null,
"EntityName": "Mrp_T_Order",
"OperationType": "GetList",
"SeedFile": "crud/Mrp_T_Order.json"
}
]
}
]
}
```
- `DataSources` elle yazılmaz: kaydetme sırasında `CustomComponentDataSourceResolver`
tasarımcı dokümanındaki `dataSources` listesini `method + path` ile CRUD endpoint kataloğunda
arayarak üretir. Eşleşenler `EntityName` + `SeedFile` taşır; elle yazılmış endpoint'ler listede
kalır ama bu alanları boş gelir.
- `Dependencies`, kanvasa bırakılan diğer custom component'lerin adlarıdır.
### 7.1.1 İki mod: hangisini yazacaksın?
**Runtime `Code` alanını her zaman kullanmaz.** `getComponentRuntimeCode` şu kararı verir:
```
Props.visualDesigner varsa && version === 1 && sourceMode === 'visual' && nodes bir dizi ise
→ kod tasarımcı dokümanından YENİDEN ÜRETİLİR (generateDesignerCode), Code alanı yok sayılır
aksi hâlde
→ Code alanı olduğu gibi Babel ile derlenir
```
Bu, prompt ile üretim için belirleyicidir:
| Mod | Ne yazarsın | Ne zaman |
| --- | --- | --- |
| **Visual** (`sourceMode: "visual"`) | Yalnızca `Props.visualDesigner` dokümanı. `Code` alanı okunmaz; marker'lı bir yer tutucu bırakılır. | Ekran, tasarımcının bileşenleriyle ifade edilebiliyorsa. Kullanıcı sonradan kanvastan düzenleyebilir. **Varsayılan tercih.** |
| **Code** (`Props: null`) | Doğrudan JSX. Tasarımcı kanvası olmaz. | Tasarımcı sözlüğünün karşılamadığı yerleşim/etkileşim gerekiyorsa. |
Visual modda `Code` alanına şunu yaz — hem marker'ı taşır (tasarımcı dosyayı açabilsin) hem de
derlense bile zararsızdır:
```
/*__SOZSOFT_VISUAL_DESIGNER__<encodeURIComponent(JSON.stringify(document))>__*/
const {Name} = () => {
return (
<>
</>
)
}
export default {Name}
```
Marker'ın içi `Props.visualDesigner` ile **aynı dokümanın** URL-encode edilmiş hâlidir. Tutarsız
kalırsa tasarımcı `Props`'u esas alır; yine de ikisini eşit tut.
### 7.1.2 Code modunda çalışma ortamı
Derlenen kod paylaşılan bir fonksiyon kapsamında çalışır. **`import` satırları temizlenir** —
ihtiyacın olan her şey kapsamdan gelir:
| Kapsamdaki ad | Ne |
| --- | --- |
| `React` | React'in tamamı (`React.useState`, `React.useEffect`…) |
| `components/ui` dışa aktarımlarının **tamamı** | `Button`, `Input`, `Select`, `Dialog`, `Card`, `Notification`, `toast`, … (yeni bir UI bileşeni eklendiğinde otomatik gelir) |
| `UiKit` | Aynı kitin isim uzayı hâli |
| `PlatformViewHost` | Bir ListForm ekranını gömmek için |
| `apiService` | Kimlik doğrulamalı HTTP istemcisi — **tercih edilen yol** |
| `axios` | Ham HTTP (kimlik doğrulama eklemez) |
| `DOMPurify` | HTML temizleme |
| `translate(key)` | Dil anahtarı çözümü |
| `checkPermission(name)` | Yetki kontrolü |
| `getCulture()` | Aktif dil |
Kurallar:
- `export default {ComponentName}` zorunludur; ad dosya/kayıt adıyla birebir aynı olmalıdır.
- Kullanıcıya görünen metni gömme; `translate('App.…')` kullan ve dil anahtarını wizard
dosyasında ya da `LanguagesData.json`'da tanımla.
- Veriye erişimde `apiService` kullan; taban adres, token ve tenant başlığı ondan gelir.
Örneklerde gördüğün `axios.create({ baseURL: 'https://localhost:44344/' })` kalıbı **eski bir
kayıttır, taklit etme**.
- Yetkiyle gizlenecek her blok `checkPermission('App.…')` ile sarılır. Custom component
route'u `authority: []` ile üretilir (§7.6 uyarısı) — kontrol bileşenin içindedir.
- Başka bir custom component'i JSX olarak kullanabilirsin; adını `Dependencies` listesine ekle.
### 7.2 Tasarımcı dokümanı (`Props.visualDesigner`)
```ts
DesignerDocument = {
version: 1
sourceMode: 'visual' | 'code' // 'code' tek yönlüdür, kanvasa dönülmez
nodes: DesignerNode[]
canvas: { width: 'responsive' | 'desktop' | 'tablet' | 'mobile' }
lifecycle: { onMount: string } // sayfa açılışında çalışan script
dataSources: DesignerDataSource[]
permissionCode?: string // Wizard'ın bu bileşen için ürettiği okuma yetkisi
}
DesignerNode = {
id: string // 'cmp_…' — opak
type: string // 'Form', 'Grid', 'Input', 'ListView', 'FlexRow' …
kind: 'html' | 'ui' | 'layout' | 'platform' | 'custom'
slot?: string
ref?: string // 'btnSave' — script'ler birbirine bununla erişir
props: Record<string, unknown>
events: Record<string, string> // olay adı → script
bindings: Record<string, DesignerBinding>
children: DesignerNode[]
}
DesignerBinding = {
sourceId: string // 'source_…' (data source) ya da 'cmp_…' (kapsayıcı Form kaydı)
path: string // okunacak kolon
labelPath?: string
valuePath?: string
columns?: string[] // ekranda görünmeyen ek kolonlar; refs.<ad>.getColumn('x') ile okunur
}
DesignerDataSource = {
id: string
name: string
method: 'GET' | 'POST' | 'PUT' | 'DELETE'
url: string
responsePath: string // yanıt içindeki liste yolu; boşsa yanıtın kendisi
filters?: DesignerDataSourceFilter[]
}
DesignerDataSourceFilter = {
id: string
field: string
operator: 'eq'|'ne'|'contains'|'startswith'|'endswith'|'gt'|'gte'|'lt'|'lte'|'in'|'isnull'|'notnull'
source: 'static' | 'query' | 'route' | 'record'
value: string // static → değer · query/route → URL parametre adı · record → <formRef>.<kolon>
previewValue?: string // yalnızca tasarımcı önizlemesi; üretilen koda girmez
required?: boolean // true ise değer yoksa istek hiç atılmaz
}
```
Filtre → query string sözleşmesi: `eq` çıplak parametredir (`?RoleId=5`), diğerleri adını son ek
olarak taşır (`?Name.contains=abc`). Bu, CrudEndpoint `GetList`'in okuduğu sözleşmedir.
> `required: true` varsayılandır ve bilinçlidir: değeri henüz gelmemiş bir filtre isteği
> **tutmalıdır**; filtresiz koleksiyonu yüklemek "filtre çalışmıyor" gibi okunur.
### 7.3 Toolbox
| Aile | Bileşenler |
| --- | --- |
| `layout` | `PageContainer` (maxWidth, padding, gap), `FlexRow` (columns, firstColumnWidth, gap, wrap, align), `Spacer` (height) |
| `data` | `Form` — dört CRUD endpoint'ini sahiplenen kapsayıcı |
| `platform` | `ListView`, `DataGridView`, `TreeView`, `GanttView`, `TodoBoard`, `CardView`, `SchedulerView`, `PivotView`, `ChartView` — her biri `listFormCode` ile bir ListForm ekranını gömer |
| `html` / `ui` | Ham HTML etiketleri ve `components/ui` bileşenleri (sözleşmeleri metadata'dan okunur) |
| `custom` | Diğer custom component'ler |
**Önce yeniden kullan:** istenen şey zaten bir ListForm ekranının verdiği liste/ağaç/grafikse,
onu primitiflerden yeniden kurma — ilgili `platform` düğümünü `listFormCode` ile bırak.
### 7.3.1 Çalışan minimal doküman — bir Form + gömülü liste
Aşağıdaki `Props` değeri (JSON string olarak yazılır) tek başına çalışır: üstte bir CRUD formu,
altında var olan bir ListForm ekranı.
```jsonc
{
"visualDesigner": {
"version": 1,
"sourceMode": "visual",
"canvas": { "width": "responsive" },
"lifecycle": { "onMount": "" },
"permissionCode": "App.Hr.AdvanceRequest",
"dataSources": [
{ "id": "source_list", "name": "Hr_T_AdvanceRequest — GetList",
"method": "GET", "url": "/api/app/crudendpoint/Hr_T_AdvanceRequest", "responsePath": "" },
{ "id": "source_byid", "name": "Hr_T_AdvanceRequest — GetById",
"method": "GET", "url": "/api/app/crudendpoint/Hr_T_AdvanceRequest/{id}", "responsePath": "" },
{ "id": "source_create", "name": "Hr_T_AdvanceRequest — Create",
"method": "POST", "url": "/api/app/crudendpoint/Hr_T_AdvanceRequest", "responsePath": "" },
{ "id": "source_update", "name": "Hr_T_AdvanceRequest — Update",
"method": "PUT", "url": "/api/app/crudendpoint/Hr_T_AdvanceRequest/{id}", "responsePath": "" },
{ "id": "source_delete", "name": "Hr_T_AdvanceRequest — Delete",
"method": "DELETE", "url": "/api/app/crudendpoint/Hr_T_AdvanceRequest/{id}", "responsePath": "" }
],
"nodes": [
{
"id": "cmp_page", "type": "PageContainer", "kind": "layout", "ref": "page",
"props": { "maxWidth": "1280px", "padding": 24, "gap": 16 },
"events": {}, "bindings": {},
"children": [
{
"id": "cmp_form", "type": "Form", "kind": "layout", "ref": "form1",
"props": {
"title": "::App.Hr.AdvanceRequest.Title",
"selectEndpoint": "source_byid",
"insertEndpoint": "source_create",
"updateEndpoint": "source_update",
"deleteEndpoint": "source_delete",
"keyFieldName": "Id",
"collectionPath": "",
"keySource": "route",
"keyParamName": "id",
"autoLoad": true,
"showToolbar": true,
"columnCount": 2,
"gap": 16,
"selectPermission": "",
"insertPermission": "",
"updatePermission": "",
"deletePermission": ""
},
"events": {}, "bindings": {},
"children": [
{
"id": "cmp_amount", "type": "Input", "kind": "ui", "ref": "inputAmount",
"props": { "size": "md" },
"events": {},
"bindings": { "value": { "sourceId": "cmp_form", "path": "Amount" } },
"children": []
},
{
"id": "cmp_note", "type": "Input", "kind": "ui", "ref": "inputNote",
"props": { "textArea": true, "rows": 3 },
"events": {},
"bindings": { "value": { "sourceId": "cmp_form", "path": "Description" } },
"children": []
}
]
},
{
"id": "cmp_list", "type": "ListView", "kind": "platform", "ref": "listView1",
"props": {
"listFormCode": "App.Hr.AdvanceRequests",
"height": "520px",
"className": "mt-4",
"designerPermission": "App.Hr.AdvanceRequests",
"filters": [
{
"id": "filter_1", "field": "EmployeeId", "operator": "eq",
"source": "record", "value": "form1.EmployeeId",
"previewValue": "", "required": true
}
]
},
"events": {}, "bindings": {}, "children": []
}
]
}
]
}
}
```
Doküman yazarken uyulacak kurallar:
- **`id` benzersiz ve kalıcıdır.** Bağlamalar (`bindings.sourceId`) bu id'lere işaret eder;
değiştirirsen bağ kopar. `cmp_…` / `source_…` öneki bir sözleşmedir, zorunlu değildir ama koru.
- **`ref` script'lerin adres defteridir**; olay script'leri birbirine `refs.inputAmount…` ile
erişir. Okunur ve tekil tut.
- Form içindeki bir alanın kaynağı **Form düğümünün `id`'sidir** (`sourceId: "cmp_form"`),
data source değil. Data source'a bağlanan şeyler listeler/seçim bileşenleridir.
- Her düğümde `props`, `events`, `bindings`, `children` **var olmalıdır** (boş olsalar bile).
- `kind` değeri toolbox ailesiyle uyumlu olmalıdır; yanlış `kind` düğümü çizdirmez.
- **`::` öneki dil anahtarıdır.** Metin taşıyan bir prop `::` ile başlıyorsa üretilen kodda
`translate('::…')` çağrısına dönüşür; başlamıyorsa düz metin olarak gömülür. Kullanıcıya
görünen her metni `::App.…` biçiminde yaz (`"title": "::App.Hr.AdvanceRequest.Title"`).
- **`platform` düğümleri `filters` prop'u alır.** Gömülü ListForm ekranını ana kayda ya da URL'ye
bağlamak için `DesignerDataSourceFilter` ile aynı şekli kullanır; `source: "record"` iken
`value` `"{formRef}.{Kolon}"` biçimindedir (`"form1.EmployeeId"`). `required: true` ise ana
kayıt gelmeden liste yüklenmez.
- Anahtar kolon adında **büyük/küçük harf endpoint'in döndürdüğüyle aynı olmalıdır**
(`keyFieldName: "id"` ile `"Id"` farklı şeylerdir); CRUD endpoint'in yanıtına bak.
### 7.4 `Form` bileşeni
| Prop | Anlam |
| --- | --- |
| `selectEndpoint` / `insertEndpoint` / `updateEndpoint` / `deleteEndpoint` | **Data source `id`'si** (`"source_5w_5ibpv"`) — URL değil. Endpoint'in kendisi `dataSources` listesinde tanımlıdır. |
| `keyFieldName` | Anahtar kolon |
| `collectionPath` | Select yanıtında liste yolu |
| `keySource` + `keyParamName` | Anahtarın nereden geldiği (route/query/sabit) |
| `previewKeyValue` | Yalnızca tasarımcı önizlemesi |
| `autoLoad` | Açılışta Select çalışsın mı |
| `showToolbar` | Kaydet/Sil araç çubuğu |
| `columnCount` / `gap` | İç yerleşim |
| `title` | Dolu ise Card başlığı; boş ise başlık düşer |
İçine bırakılan her bileşen Select sonucunun bir kolonuna bağlanır (`bindings.value.sourceId` =
Form düğümünün `id`'si) ve Save/Delete üzerinden geri yazar. Olaylar: `onLoad`,
`onRecordChange`, `onFieldChange`, `onNewRecord`, `onModeChange`, `onBeforeSave`, `onAfterSave`,
`onBeforeDelete`, `onAfterDelete`, `onError`.
### 7.5 Yetki modeli (zorunlu)
İki katman:
1. **Düğüm görünürlüğü**`props.designerPermission` dolu olan düğüm, yetki yoksa hiç
render edilmez. Veriyi göremeyecek kullanıcıya boş bir Grid göstermek yerine bileşen çizilmez.
2. **Form komutları**`selectPermission` / `insertPermission` / `updatePermission` /
`deletePermission`.
- `Otomatik` mod: temel, dokümandaki `permissionCode`'dur (Wizard'ın bu bileşen için ürettiği
okuma yetkisi, menü kaydının korunduğu yetkinin aynısı). Komutlar sırasıyla `''`,
`.Create`, `.Update`, `.Delete` son eklerini alır.
- `Özel` mod: yetki adı elle yazılır.
- Boş bırakılan komut serbesttir.
Manager ekranı yetkileri: `App.DeveloperKit.Components{,.Create,.Update,.Delete}`.
### 7.6 Bileşeni menüye bağlama
Bileşen tek başına bir ekran değildir; menü ve yetki Wizard'ın `Custom` yolundan gelir:
```jsonc
{
"Wizard": {
"ComponentKind": 1,
"WizardName": "OrderBoard",
"ListFormCode": "App.Mrp.OrderBoard",
"MenuCode": "App.Mrp.OrderBoard",
"CustomComponentName": "OrderBoard",
"MenuUrl": "/admin/order-board",
"CreateMenu": true,
"MenuParentCode": "App.Mrp",
"PermissionGroupName": "App.Mrp",
"PermissionGroupDisplayNameEn": "MRP", "PermissionGroupDisplayNameTr": "MRP",
"LanguageTextMenuEn": "Order Board", "LanguageTextMenuTr": "Sipariş Panosu",
"MenuIcon": "FcKanban", "MenuOrder": 2
},
"IsDeletedField": false, "IsCreatedField": false,
"InsertedRecords": {
"LanguageKeys": ["App.Mrp.OrderBoard"],
"PermissionGroupNames": [],
"PermissionNames": ["App.Mrp.OrderBoard", "App.Mrp.OrderBoard.Create",
"App.Mrp.OrderBoard.Update", "App.Mrp.OrderBoard.Delete"],
"MenuCodes": ["App.Mrp.OrderBoard"], "DataSourceCodes": []
}
}
```
Custom yolunda `ListForm` **üretilmez**; `Title`/`Desc` dil anahtarları da üretilmez.
`Export`/`Import`/`Note` yetkileri bu yolda yoktur.
> **Güvenlik — route yetkisiz üretilir.** Custom component route'ları `RoutePath` değerinden
> türetilir ve `authority: []` ile kaydedilir; `/admin/` ile başlayan yol `protected`, diğerleri
> `public` olur. Yani **URL'yi bilen her oturum açmış kullanıcı sayfayı açabilir**. Menü yetkisi
> yalnızca menüde görünmeyi engeller. Gerçek koruma bileşenin içindedir:
> `designerPermission` ile düğümleri gizle, Form komutlarını `*Permission` ile kapat, kod modunda
> `checkPermission()` ile sar. Yetkisiz kullanıcıya boş bir sayfa gösteren bir bileşen doğru
> sonuçtur; veri gösteren bileşen açıktır.
>
> `RoutePath` `/admin/` ile başlamıyorsa sayfa **herkese açıktır** — bilinçli değilse başlat.
### 7.6.1 Depodaki gerçek örnek — `RoleComponent` + `RoleList` wizard
Bu çift, custom component'in menüye nasıl bağlandığının çalışan örneğidir. İncelenecek dosyalar:
```
configs/seeds/host/custom/RoleComponent.json
configs/seeds/host/crud/AbpRoles.json · AbpUsers.json · AbpUserRoles.json
configs/seeds/host/wizard/20260819131500_RoleList.json
```
Nasıl bağlanıyorlar:
| Bağ | Değer |
| --- | --- |
| Wizard `ComponentKind` | `1` (Custom) |
| Wizard `CustomComponentName` | `RoleComponent` |
| Component `RoutePath` | `/admin/roles` |
| Wizard `MenuUrl` | `/admin/roles` — component'in `RoutePath` değeriyle **aynı** |
| Wizard `MenuCode` / `ListFormCode` | `App.Wizard.RoleList` |
| Component `Props.visualDesigner.permissionCode` | `App.Wizard.RoleList` — wizard'ın ürettiği okuma yetkisi |
| Wizard `InsertedRecords.PermissionNames` | `App.Wizard.RoleList` + `.Create` + `.Update` + `.Delete` |
| Wizard `InsertedRecords.LanguageKeys` | `App.Wizard.RoleList` |
| Component `DataSources[].SeedFile` | `crud/AbpRoles.json` vb. |
**Okunacak dersler:**
1. `permissionCode` **wizard'ın ürettiği yetkinin aynısıdır**. Form komutları `Otomatik` modda
bu koda `.Create`/`.Update`/`.Delete` ekleyerek çözülür. Component'i yazarken bu değeri
wizard'daki `MenuCode` ile aynı yap.
2. `RoutePath` ile wizard'ın `MenuUrl` değeri **birebir aynı** olmalıdır; farklıysa menü başka
bir yere gider.
3. Component'in `DataSources` listesi `crud/*.json` dosyalarına `SeedFile` ile işaret eder —
export zip'inin bu dosyaları da toplamasını sağlayan şey budur.
4. Wizard `Custom` yolunda olsa bile `ListFormCode`, `Grid/Card/Pivot/Chart` bayrakları gibi
List alanlarını taşır; **bunlar kullanılmaz**, varsayılan değerleriyle dosyada kalır.
Yeni dosya yazarken bunları temizlemeye çalışma, şablonu koru.
5. `platform` düğümündeki `listFormCode` (`AbpIdentity.Roles`) **var olan bir ListForm kodudur**;
component ekranın kendisini yeniden kurmaz, gömer.
### 7.7 Taşıma
Bir ekranı başka ortama taşımak tek dosya kopyalamak değildir. Wizard File Manager'ın **Export**
butonu zip üretir:
```
wizard/{dosya}.json
custom/{component}.json (bağımlılıklarıyla)
crud/{entity}.json (component'lerin kullandığı endpoint'ler)
{sql|postgres}/{object|execute}/{nesne}.sql (List yolunda SelectCommand'ın nesnesi)
```
Import iki adımlıdır: analiz (`New`/`Identical`/`Conflict`, çakışmalar diff editörde çözülür) →
uygulama (dosya dosya yazım, önceki hâl yedeklenir, `RollbackImport` ile toplu geri alma).
Sınırlar: yalnızca `wizard`, `crud`, `custom`, `sql`, `postgres` kök klasörleri (SQL sağlayıcı
klasörlerinin altında yalnızca `object`/`execute`); 5 MB/dosya, 50 MB/arşiv, 500 girdi.
Yetkiler: `App.Listforms.Wizard.Export` / `.Import`.
---
## 8. Artefakt E — SQL nesneleri
### 8.1 Tablo tasarımcısı — varsayılan kolonlar
Kullanıcı yalnızca iş kolonlarını söylese bile şunlar varsayılan olarak eklenir:
`Id`, `TenantId`, `CreationTime`, `CreatorId`, `LastModificationTime`, `LastModifierId`,
`IsDeleted`, `DeletionTime`, `DeleterId`.
Kullanıcııkça "tenant yok" / "audit yok" demedikçe çıkarma; her seferinde sorma.
Deploy öncesi adımda CRUD endpoint üretimi ve hangi operasyonların aktif başlayacağı seçilir;
seçilmeyenler pasif kaydedilir, sonradan diyalogdan açılabilir.
### 8.2 View tasarımcısı
Bir ekran birden fazla tablodan besleniyorsa, `SelectCommandType: 4` (Query) yerine **tasarlanmış
bir view** (`SelectCommandType: 2`) tercih edilir: yeniden kullanılabilir, seed'lenir ve
tasarımcıda görsel olarak düzenlenebilir kalır.
Model: kaynaklar (tablo/view, `CROSS`/`OUTER APPLY`, türetilmiş alt sorgu), JOIN'ler
(`INNER`/`LEFT`/`RIGHT`/`FULL`/`CROSS`; `=`, `<>`, `>`, `>=`, `<`, `<=`) ve criteria satırları
(Column / Alias / Output / Group By / Sort / Filter / Or…). Group By sütunu: `GroupBy`, `Where`
(satır çıktıya girmez, yalnızca filtre taşır), `SUM`, `COUNT`, `COUNT_DISTINCT`, `AVG`, `MIN`,
`MAX`. Filtre hücreleri serbest yüklemdir (`> 100`, `LIKE '%abc%'`, `IS NULL`).
**Tek yönlüdür.** Model → T-SQL. Var olan bir view kanonik şekle uymuyorsa geri okunamaz ve
dialog ham SQL moduna düşer. Bu yüzden tasarlanmış bir view'ı elle düzenlemek tasarımcıyı
kaybettirir.
Nesneler `configs/seeds/{kapsam}/{sql|postgres}/{object|execute}/{ad}.sql` altına yazılır:
`object` = yeniden oluşturulabilir nesne (tablo/view/fonksiyon/prosedür), `execute` = bir kez
çalıştırılacak script.
### 8.3 SQL seed dosyası yazım kuralları
| Kural | Açıklama |
| --- | --- |
| Dosya adı | Nesne adıyla **birebir aynı**: `Hr_T_AdvanceRequest.sql` |
| Klasör | `object` = idempotent nesne tanımı · `execute` = nesne olarak da oluşturulur, ayrıca tüm migration'lar bittikten sonra çalıştırılır |
| Sağlayıcı | Hedef SQL Server ise `sql/`, PostgreSQL ise `postgres/`. **İkisini birden destekliyorsan iki dosya da yazılır**; içerikler aynı nesnenin iki diyalektidir. |
| İdempotency | SQL Server: `CREATE OR ALTER VIEW/PROCEDURE/FUNCTION`, tablolar için `IF NOT EXISTS` koruması. PostgreSQL: `CREATE OR REPLACE VIEW/FUNCTION`, `CREATE TABLE IF NOT EXISTS`. |
| Şema | `dbo` kullanılır; PostgreSQL tarafında tanımlayıcılar çift tırnaklıdır (`dbo."Hr_T_AdvanceRequest"`) çünkü kolon adları PascalCase'tir. |
| Toplu ayraç | SQL Server'da prosedür sonunda `GO` kullanılabilir; PostgreSQL'de kullanılmaz. |
| Yorum | Dosyanın ilk satırında ne ürettiğini yaz (`-- Create View`). |
Tablo yazarken §8.1'deki varsayılan kolonlar dahil edilir. Örnek iskelet (SQL Server):
```sql
-- Create Table
IF OBJECT_ID(N'[dbo].[Hr_T_AdvanceRequest]', N'U') IS NULL
BEGIN
CREATE TABLE [dbo].[Hr_T_AdvanceRequest] (
[Id] UNIQUEIDENTIFIER NOT NULL CONSTRAINT [PK_Hr_T_AdvanceRequest] PRIMARY KEY,
[TenantId] UNIQUEIDENTIFIER NULL,
-- iş kolonları buraya
[CreationTime] DATETIME2 NOT NULL,
[CreatorId] UNIQUEIDENTIFIER NULL,
[LastModificationTime] DATETIME2 NULL,
[LastModifierId] UNIQUEIDENTIFIER NULL,
[IsDeleted] BIT NOT NULL CONSTRAINT [DF_Hr_T_AdvanceRequest_IsDeleted] DEFAULT (0),
[DeletionTime] DATETIME2 NULL,
[DeleterId] UNIQUEIDENTIFIER NULL
);
END
```
> `Id` tipini ne seçtiysen wizard dosyasındaki `KeyFieldDbSourceType` ile aynı olmalıdır
> (`UNIQUEIDENTIFIER` → `9`, `INT IDENTITY` → `11`). Uyumsuzluk sessiz veri hatası üretir.
### 8.4 Seeder bu dosyaları nasıl çalıştırır
Yeni bir entity/tablo `sql/object/` altına yazılır ve veritabanı `SqlDataSeeder` tarafından bu
dosyalardan kurulur. Mekanik:
1. **Yalnızca aktif sağlayıcının klasörü çalışır.** `DefaultDatabaseProvider` SQL Server ise
`{kapsam}/sql/`, PostgreSQL ise `{kapsam}/postgres/`. İki dosya da yazmak taşınabilirlik
içindir; aynı anda ikisi birden çalışmaz.
2. Sıra: `object/``execute/`, her klasörün içinde **dosya adına göre alfabetik**. Bir tablo
başka bir tabloya FK ile bağlıysa dosya adları o sırayı vermelidir (ör. `Hr_T_Employee.sql`
`Hr_T_EmployeeAdvance.sql`'den önce gelir).
3. Dosya içeriği `GO` satırlarından batch'lere bölünür ve sırayla çalıştırılır. PostgreSQL'de
`GO` kullanılmaz.
4. **Hata seed'i durdurur.** Bir batch patlarsa istisna yukarı fırlatılır ve o kapsamın seed'i
yarım kalır. Bu yüzden her script tekrar tekrar çalıştırılabilir (idempotent) olmalıdır:
`CREATE OR ALTER` / `CREATE OR REPLACE` / `IF NOT EXISTS`.
5. SQL nesneleri bittikten hemen sonra **aynı kapsam için `CrudDataSeeder`** çalışır; böylece
CRUD endpoint'leri dayandıkları tablolardan sonra oluşur. Tablo dosyasını yazmadan
`crud/{Entity}.json` yazmak anlamsızdır.
6. Klasör kökünde kalmış `.sql` dosyaları geriye dönük uyumluluk için çalıştırılır ama **uyarı
loglar**; yeni dosyayı her zaman `object/` ya da `execute/` altına koy.
**`execute/` klasörünün özel sözleşmesi:** buradaki dosyalar iki aşamalıdır.
- 1. aşama: `SqlDataSeeder` dosyayı çalıştırır → nesne oluşur/güncellenir.
- 2. aşama: tüm migration ve seed işlemleri bittikten sonra `AfterAllMigrationsSqlExecutor`
**dosya adından türettiği stored procedure'ü çağırır**.
Yani `execute/` altındaki bir dosya, **adı prosedür adıyla aynı olan bir stored procedure
tanımlamalıdır** (`Hr_T_AdvanceRequest_Backfill.sql` → `Hr_T_AdvanceRequest_Backfill`
prosedürü). `schema.Procedure.sql` biçimi de desteklenir. Prosedür adı güvenli tanımlayıcı
deseninden geçer (harf/alt çizgi ile başlar, en fazla 127 karakter). Sadece bir DDL çalıştırmak
istiyorsan dosya `object/` altına aittir; `execute/` veri dolumu ve migration sonrası düzeltme
içindir.
### 8.5 Yeni entity/tablo eklerken izlenecek yol
1. `sql/object/{Modul}_T_{Entity}.sql` — tabloyu §8.3 iskeletiyle yaz (tenant + audit kolonları
varsayılan). PostgreSQL hedefi de destekleniyorsa `postgres/object/` altına diyalekt eşini yaz.
2. İndeks ve FK'leri **aynı dosyada**, varlık kontrolüyle ekle (`IF NOT EXISTS`); ayrı dosya
sıralamayı kırılgan hâle getirir.
3. Ekran bu tabloyu doğrudan kullanacaksa CRUD endpoint gerekmez — ListForm `SelectCommandType: 1`
ile tabloya bağlanır. Custom Component kullanacaksa `crud/{Entity}.json` de yaz (§9).
4. Wizard dosyasında `SelectCommand` = tablo adı, `KeyFieldName` = `Id`,
`KeyFieldDbSourceType` = tablodaki `Id` tipinin sayısı, `IsTenant` = tabloda `TenantId` varsa
`true`, `IsDeletedField`/`IsCreatedField` = ilgili kolonlar varsa `true`.
5. DB Migrate çalıştır; log'da `Executing: CREATE TABLE …` satırını gör.
---
## 9. Artefakt F — CRUD Endpoint
Konum: `configs/seeds/{kapsam}/crud/{EntityName}.json` · Okuyan: `CrudDataSeeder`
```jsonc
{
"EntityName": "Mrp_T_Order",
"GeneratedAt": "2026-08-20T06:13:28Z",
"Endpoints": [
{ "Method": "GET", "Path": "/api/app/crudendpoint/Mrp_T_Order", "OperationType": "GetList", "IsActive": true },
{ "Method": "GET", "Path": "/api/app/crudendpoint/Mrp_T_Order/{id}", "OperationType": "GetById", "IsActive": true },
{ "Method": "POST", "Path": "/api/app/crudendpoint/Mrp_T_Order", "OperationType": "Create", "IsActive": true },
{ "Method": "PUT", "Path": "/api/app/crudendpoint/Mrp_T_Order/{id}", "OperationType": "Update", "IsActive": true },
{ "Method": "DELETE", "Path": "/api/app/crudendpoint/Mrp_T_Order/{id}", "OperationType": "Delete", "IsActive": true }
]
}
```
Üretilen C# kodu dosyada tutulmaz; entity adı ve operasyondan yeniden türetilir.
- Üretim/aktivasyon yeri: SQL Query Manager nesne gezgininde tablo satırının **CRUD Endpoints**
aksiyonu, toolbar'daki toplu üretim, tablo tasarımcısının deploy öncesi adımı ve Wizard'ın
veri ayarları adımı — hepsi aynı `CrudEndpointDialog`.
- Yetki: `App.SqlQueryManager.CrudEndpoints`. İzin yoksa butonlar görünmez ve
`crud-endpoint-generate` uçlarının tamamı reddedilir.
- Dispatcher üzerinden çağrı kapıları: `App.DeveloperKit.CustomEndpoints.Get/.Post/.Put/.Remove`;
endpoint bazlı User/Role/Global kuralları bunun üstünde denetlenir.
- `GetList` filtre sözleşmesi §7.2'deki ile aynıdır.
---
### 9.2 Custom Endpoint — elle yazılmış SQL'i API olarak açmak
Ekran: `/admin/list/App.DeveloperKit.CustomEndpoints` (bir ListForm ekranıdır).
Entity: `CustomEndpoint`.
| Alan | Anlam |
| --- | --- |
| `Name` / `Description` | Tanımlayıcı |
| `Url` | Yayınlanacak yol |
| `Method` | `GET` / `POST` / `PUT` / `DELETE` |
| `DataSourceCode` | Hangi bağlantıdan okunacağı. **`"!Tenant"`** özel değeridir: isteği yapan kullanıcının kendi tenant veritabanından okunur. |
| `Sql` | Parametreli sorgu. String birleştirme yasak. |
| `ParametersJson` | `CustomEndpointParameter[]` |
| `PermissionsJson` | `CustomEndpointPermission[]` |
```jsonc
// ParametersJson
[
{ "Type": "Query", "Name": "customerId", "DefaultValue": "", "Path": "", "IsRequired": true },
{ "Type": "Path", "Name": "id", "DefaultValue": "", "Path": "", "IsRequired": true },
{ "Type": "Body", "Name": "note", "DefaultValue": "", "Path": "data.note", "IsRequired": false },
{ "Type": "Static","Name": "tenantMode", "DefaultValue": "1", "Path": "", "IsRequired": true }
]
// PermissionsJson — Global / Role / User
[
{ "ResourceType": "Role", "ResourceId": "SalesManager" },
{ "ResourceType": "User", "ResourceId": "<userId>" }
]
```
Parametre tipleri: `Static` (sabit), `Query` (query string), `Path` (URL segmenti),
`Body` (gövde; `Path` ile gövde içindeki yol).
Yetki iki katmanlıdır: önce dispatcher kapısı
(`App.DeveloperKit.CustomEndpoints.Get/.Post/.Put/.Remove`), sonra endpoint'in kendi
`PermissionsJson` kuralları. Endpoint tanımlarını düzenleme yetkileri ise ListForm ekranının
`App.DeveloperKit.CustomEndpoints{,.Create,.Update,.Delete}` yetkileridir.
**Ne zaman CRUD, ne zaman Custom Endpoint?** Tek tabloya standart CRUD gerekiyorsa CRUD Endpoint
(otomatik üretilir, seed'lenir, filtre sözleşmesi hazırdır). Birleştirilmiş/şekillendirilmiş
sorgu, rapor ya da özel bir yol gerekiyorsa Custom Endpoint.
### 9.3 Dynamic Service — çalışma zamanında derlenen C# servisi
Ekran: `/admin/developerkit/dynamic-services`. Entity: `DynamicService`.
Bu, karar sırasının **3. adımıdır**; konfigürasyon ve SQL yetmediğinde gelir, kod değişikliğinden
önce denenir.
| Alan | Anlam |
| --- | --- |
| `Name` | Benzersiz servis adı (`DynamicCustomerService`) |
| `DisplayName` / `Description` | Kullanıcı dostu başlık ve açıklama |
| `Code` | Tam C# sınıfı`using`'ler ve `namespace` dahil |
| `ControllerName` | Swagger'da görünecek controller adı |
| `PrimaryEntityType` | Kullandığı ana entity (opsiyonel) |
| `IsActive` | Yayında mı |
| `CompilationStatus` | `Pending(0)` · `Success(1)` · `Failed(2)` · `InProgress(3)` |
| `LastCompilationError` / `LastSuccessfulCompilation` | Son derleme sonucu |
| `Version` / `CodeHash` | Kod her değiştiğinde `Version` artar, hash yenilenir |
Yaşam döngüsü: **TestCompile** (Roslyn ile derle, hata mesajı UI'a döner) → **Publish**
`DynamicAssemblyRegistrationService` assembly'yi tenant bağlamıyla kaydeder →
`ActionDescriptorChangeProvider` MVC'ye route tablosunun değiştiğini bildirir →
`DynamicControllerActivator` bağımlılıkları enjekte eder. **Uygulama yeniden başlatılmaz.**
Yetkiler: `App.DeveloperKit.DynamicServices{.Create,.Edit,.Delete,.Manage,.TestCompile,.Publish,.ViewCode}`.
Yazarken `dotnet.instructions.md` §5 geçerlidir: `PlatformAppService` türet, `[Authorize(...)]`
ile koru, tenant bağlamını `ICurrentTenant` üzerinden kullan, SQL'i parametreli çalıştır,
kullanıcıya görünen metni `PlatformResource` anahtarından ver, sabit/secret gömme.
---
### 9.4 Developer Kit menüsü — tam kapsam
Menü: `App.DeveloperKit` (üst) → aşağıdaki alt ekranlar. Her satır bu dokümandaki bölümüne
bağlıdır; hiçbiri "kod yaz" adımının yerine geçmez, hepsi ondan öncedir.
| Ekran | Route | Yetki | Ne üretir | Bölüm |
| --- | --- | --- | --- | --- |
| **SQL Query Manager** | `/admin/sqlQueryManager` | `App.SqlQueryManager` | SQL nesneleri, CRUD endpoint'ler | 9.5 |
| **Custom Endpoints** | `/admin/list/App.DeveloperKit.CustomEndpoints` | `App.DeveloperKit.CustomEndpoints` | Elle yazılmış SQL'den REST endpoint | 9.2 |
| **Dynamic Services** | `/admin/developerkit/dynamic-services` | `App.DeveloperKit.DynamicServices` | Runtime derlenen C# AppService | 9.3 |
| **Components** | `/admin/developerkit/components` | `App.DeveloperKit.Components` | Custom Component + Visual Designer | 7 |
| **ListForm** | `/admin/list/App.Listforms.Listform` | `App.Listforms.Listform` | Var olan ekranların ham tanımı | 9.6 |
| **Wizard Manager** | `/admin/listform/wizardManager` | `App.Listforms.Wizard` | Wizard seed dosyalarının yönetimi, export/import | 4, 7.7 |
### 9.5 SQL Query Manager — bileşen bileşen
Tek ekranda toplanmış araç seti (`SqlQueryManager.tsx`):
| Panel / araç | Dosya | Yapabildikleri |
| --- | --- | --- |
| **Veri kaynağı seçici** | — | `DataSource` kayıtları arasında geçiş; SQL Server ve PostgreSQL |
| **Nesne gezgini** | `SqlObjectExplorer.tsx` | Tablolar, view'lar, stored procedure'ler, fonksiyonlar ve hazır şablonlar (`SqlObjectExplorerDto`); arama, nesne tanımını açma, kopyalama, silme, satır bazında **CRUD Endpoints** aksiyonu, çoklu seçimle toplu endpoint üretimi |
| **Sorgu editörü** | `SqlEditor.tsx` | Monaco tabanlı; çalıştırma (`ExecuteQueryAsync`), çoklu sekme, nesne tanımını editöre yükleme (`GetNativeObjectDefinitionAsync`), tablo create script'i üretme (`GetTableCreateScriptAsync`) |
| **Sonuç grid'i** | `SqlResultsGrid.tsx` | Sonuç kümesi görüntüleme ve dışa aktarma |
| **Tablo tasarımcısı** | `SqlTableDesignerDialog.tsx` | Kolon/anahtar/index tasarımı, varsayılan tenant + audit kolonları, deploy öncesi CRUD endpoint seçimi |
| **View tasarımcısı** | `SqlViewDesignerDialog.tsx` + `sqlViewDesigner/` | Diagram + criteria grid + T-SQL önizleme; bkz. 8.2 |
| **CRUD endpoint diyaloğu** | `CrudEndpointDialog.tsx` | Endpoint üretme, aktif/pasif etme, test, silme; aynı diyalog Wizard'ın veri adımında da açılır | 9 |
| **DB Migrate** | `DbMigrateButton` | Migration + seed tetikleme (`App.Setup.Migrate`) |
| **File Manager kısayolu** | `/admin/files` | `configs/seeds` altındaki seed dosyalarının yönetimi |
| **Kolon bilgisi** | `GetTableColumnsAsync` | Wizard alan adımının ve tasarımcıların kolon kaynağı |
Servis yüzeyi (`SqlObjectManagerAppService`): `GetAllObjectsAsync`, `ExecuteQueryAsync`,
`GetNativeObjectDefinitionAsync`, `GetTableColumnsAsync`, `GetTableCreateScriptAsync`.
Kalıcılık kuralı: tasarımcıdan çıkan her nesne
`configs/seeds/{kapsam}/{sql|postgres}/{object|execute}/{ad}.sql` altına düşer.
`object` yeniden oluşturulabilir nesneler (tablo/view/fonksiyon/prosedür), `execute` bir kez
çalışacak script'ler içindir. Editörde elle çalıştırılan ama dosyaya düşmeyen bir DDL,
veritabanı yeniden kurulduğunda kaybolur.
### 9.6 ListForm ekranı ve editörü
Wizard bir ekranı **üretir**; ince ayar `/admin/listform/edit/{listFormCode}` üzerinden yapılır.
Sekmeler ve karşılık geldikleri JSON kolonları:
| Sekme | JSON |
| --- | --- |
| Veritabanı | `SelectCommandType`, `SelectCommand`, `KeyFieldName`, `Insert/Update/DeleteCommand` (+ `Before`/`After`), `*ServiceAddress` |
| Sütunlar | `ListFormField` kayıtları, `ColumnOptionJson`, banded/command sütunlar |
| Filtreler | `FilterRowJson`, `HeaderFilterJson`, `FilterPanelJson`, `SearchPanelJson`, `GroupPanelJson`, `ExtraFilterJson` |
| Düzenleme | `EditingOptionJson`, `EditingFormJson` |
| Yetkiler | `PermissionJson` (ekran ve sütun düzeyi) |
| Alt Form | `SubFormsJson` |
| Pivot / Tree / Gantt / Scheduler / Todo | `PivotOptionJson`, `TreeOptionJson`, `GanttOptionJson`, `SchedulerOptionJson`, `TodoOptionJson` |
| Widget | `WidgetsJson` |
| Workflow | `WorkflowJson` |
| Durum | `StateStoringJson`, `PagerOptionJson`, `SelectionJson` |
| Grafik | `SeriesJson`, `LegendJson`, `ArgumentAxisJson`, `ValueAxisJson`, `TooltipJson`, `ZoomAndPanJson` |
Ayrıca `CustomJsSourcesJson` / `CustomStyleSourcesJson` ekran yüklendiğinde çalışacak JS/CSS
taşır — son çaredir, önce `EditorScript`/`EditorOptions` denenmelidir.
> **Uyarı:** Bu ekrandan yapılan değişiklikler doğrudan veritabanına yazılır ve wizard seed
> dosyasına **yansımaz**. Kalıcı olması gereken bir değişikliği ya wizard'ı `EditFileName` ile
> yeniden çalıştırarak ya da seed dosyasını güncelleyerek yap.
---
### 9.7 Menü Yönetimi ekranları
Menü: `App.Menus` (üst) → dört ekran. **Wizard bunların çoğunu senin yerine yapar**; bu ekranlar
elle düzeltme ve wizard'ın kapsamadığı düzenlemeler içindir.
| Ekran | Route | Yetki | Ne yapar |
| --- | --- | --- | --- |
| **Routes** | `/admin/list/App.Menus.Routes` | `App.Menus.Routes` | Fiziksel React sayfalarının route kayıtları (`Key`, `Path`, `ComponentType`, `ComponentPath`, `RouteType`, `Authority[]`) |
| **Menu Groups** | `/admin/list/App.Menus.MenuGroup` | `App.Menus.MenuGroup` | Menü grubu adları (`Erp`, `Kurs` gibi üst kümeler) |
| **Menu List** | `/admin/list/App.Menus.Menu` | `App.Menus.Menu` | Menü kayıtlarının ham listesi |
| **Menu Manager** | `/admin/menuManager` | `App.Menus.Manager` | Sürükle-bırak menü ağacı; sıra ve hiyerarşi düzenleme |
**`Menu` kaydının alanları** (wizard bunları üretir; elle eklemen gerekirse):
| Alan | Anlam |
| --- | --- |
| `Code` | Benzersiz kod; yetki adının kökü |
| `DisplayName` | **Dil anahtarı** (düz metin değil) |
| `ParentCode` | Üst menü; boş ise kök menü |
| `Url` | Açılacak adres |
| `Icon` | react-icons adı |
| `Order` | Kardeşler arası sıra |
| `ShortName` | Kısa rozet (`HR`, `MRP`) — kök menülerde kullanılır |
| `RequiredPermissionName` | Görünürlük yetkisi |
| `Target` / `CssClass` / `ElementId` / `IsDisabled` | Bağlantı davranışı ve görünüm |
| `UserId` / `RoleId` / `CultureName` | Kullanıcı, rol ve dil bazlı menü varyantı |
**Kurallar — prompt ile üretim yaparken:**
1. **Menüyü elle ekleme.** Yeni bir ekranın menüsü wizard seed dosyasından gelir; `MenusData.json`
yalnızca platformun kendi ekranları içindir ve orada da `Routes` bölümüne dokunulmaz.
2. **Route kaydı üretme.** Dinamik liste ekranları statik `/admin/list/:listFormCode` route'unu
kullanır; custom component'lerin route'u `RoutePath` alanından türetilir. `Route` tablosu
yalnızca fiziksel React sayfaları içindir ve prompt ile ekran üretirken böyle bir sayfa
yazılmaz.
3. **Menü sırası** için wizard'ın `MenuOrder` alanını kullan; Menu Manager'da elle taşımak seed
dosyasına yansımaz ve veritabanı sıfırlandığında kaybolur.
4. **Rol/dil bazlı menü varyantı** gerekiyorsa Menu List ekranından ikinci bir kayıt açılır —
aynı `Url`, farklı `RoleId`/`CultureName`. Wizard bunu üretmez.
---
### 9.8 Wizard Manager — layout matrisi
Wizard Manager platformun en yetenekli üretim ekranıdır: tek bir tanımdan **hem Custom Component
sayfası hem de yedi farklı görünümü olan List ekranı** üretir. Bir `List` wizard'ında birden fazla
görünüm aynı anda açık olabilir; kullanıcı ekranın üstünden geçiş yapar, tercihi kullanıcı bazlı
saklanır.
| Görünüm | Bayrak | `DefaultLayout` | Zorunlu option alanı | Ne zaman aç |
| --- | --- | --- | --- | --- |
| Grid | `Grid` | `"grid"` | — | Neredeyse her zaman; varsayılan |
| Card | `Card` | `"card"` | — | Görsel/özet kayıtlar, mobil kullanım |
| Pivot | `Pivot` | `"pivot"` | — | Sayısal kırılım/analiz ihtiyacı |
| Chart | `Chart` | `"chart"` | — (grafik ayarları ListForm editöründe) | Trend/dağılım |
| Tree | `Tree` | `"tree"` | `TreeOptionDto.KeyExpr` + `ParentIdExpr` | Kendine referanslı hiyerarşi |
| Gantt | `Gantt` | `"gantt"` | `GanttOptionDto.ParentIdExpr` + `TitleExpr` + `StartExpr` + `EndExpr` | Zaman çizelgesi, bağımlılıklı görevler |
| Scheduler | `Scheduler` | `"scheduler"` | `SchedulerOptionDto.TextExpr` + `StartDateExpr` + `EndDateExpr` | Takvim/randevu |
| TodoBoard (Kanban) | `Todo` | `"todo"` | `TodoOptionDto.TitleExpr` + `StatusExpr` | Durum akışı olan işler |
Kurallar:
- Zorunlu option alanı boşsa seeder o görünümün JSON'unu **yazmaz**; bayrak açık kalsa bile
görünüm çalışmaz. Açtığın her görünümün option bloğunu doldur.
- `DefaultLayout`, bayrağı `true` olan bir görünüm olmalıdır.
- Aynı veri birden fazla görünümde aynı `SelectCommand` üzerinden sunulur; ayrı sorgu yazılmaz.
- Bir ekranın hem liste hem pano hem takvim olarak istenmesi **tek wizard dosyasıdır** — ikinci
bir ekran açma.
Wizard Manager'ın kendi aksiyonları: liste/kart görünümü, arama, düzenleme (`EditFileName`),
silme, DB Migrate, Export (zip), Import (diff + rollback). Yetkiler
`App.Listforms.Wizard{,.Create,.Update,.Delete,.Export,.Import}`.
---
## 10. Uçtan uca reçeteler
### 10.1 "Var olan bir tablo için ekran aç"
1. Tablo yoksa SQL Table Designer ile oluştur (varsayılan kolonlarla).
2. Ekran birden çok tablodan besleniyorsa View Designer ile view üret.
3. Wizard: menü/kimlik → veri ayarları (`SelectCommandType` + `SelectCommand` + anahtar) →
alanlar → (gerekiyorsa) alt form/widget/workflow → yayınla.
4. Hesaplanan alanlara `EditorScript`, biçimli alanlara `EditorOptions` ver.
5. Doğrula: menü görünüyor mu, yetkiler roldeki kullanıcı için doğru mu, tenant filtresi
çalışıyor mu, seed dosyası oluştu mu.
### 10.2 "Ana-detay ekranı"
1. Detay ekranını **menüsüz wizard** ile üret (`CreateMenu: false`).
2. Ana ekranın `SubForms` listesine `ParentFieldName``ChildFieldName` eşlemesini ve `DbType`
ekle.
3. İki ekranın yetkileri ayrıdır; detayın yetkisi verilmemişse sekme görünmez.
### 10.3 "Serbest yerleşimli sayfa / dashboard"
1. Gerekli endpoint'ler yoksa CRUD Endpoint üret.
2. Component Manager → yeni bileşen → Visual Designer:
`PageContainer``FlexRow` → içine `platform` düğümleri (`listFormCode` ile) ve/veya `Form`.
3. Veri kaynaklarını Data sekmesinden bağla; filtreleri `route`/`query`/`record` kaynaklarıyla
kur, `required` bayrağını bilinçli seç.
4. Görünürlük ve komut yetkilerini ver (`designerPermission`, `*Permission`).
5. Wizard `ComponentKind: 1` ile menüye bağla.
6. Doğrula: `custom/{Name}.json` ve ilgili `crud/*.json` dosyaları oluştu mu.
### 10.4 "Kanban panosu"
1. Tabloda durum kolonu olsun (`Status`).
2. Wizard'da `Todo: true`, `DefaultLayout: "todo"`, `TodoOptionDto` içinde `TitleExpr`,
`StatusExpr`, `StatusOrder` (kolon sırası), `AssigneeExpr`, `DueDateExpr` doldur.
3. Kolonlar `statusExpr` değerlerinden üretilir; kartlar sürüklenerek durum değiştirir, panodan
yeni kolon eklenebilir.
---
### 10.5 "Rapor sorgusunu API olarak aç"
1. Sorguyu SQL Query Manager'da yaz ve çalıştır, sonucu doğrula.
2. Tekrar kullanılacaksa view olarak deploy et (`sql/object/…`), tek seferlikse Custom Endpoint
içinde bırak.
3. Custom Endpoints ekranından kaydı oluştur: `Url`, `Method`, `DataSourceCode` (kullanıcının
kendi tenant veritabanı isteniyorsa `"!Tenant"`), `Sql`, `ParametersJson`, `PermissionsJson`.
4. Parametreleri `Query`/`Path`/`Body` olarak tanımla; hiçbirini SQL'e string birleştirme ile
koyma.
5. Erişimi `PermissionsJson` ile daralt; dispatcher kapısının (`…CustomEndpoints.Get` vb.)
ilgili rolde açık olduğunu doğrula.
### 10.6 "Konfigürasyonla çözülemeyen iş mantığı"
1. Önce gerçekten çözülemediğini göster: ListForm + SQL + Custom Endpoint neden yetmiyor,
bir iki cümleyle yaz.
2. Dynamic Service oluştur; `PlatformAppService` türet, `[Authorize(...)]` ile koru.
3. **TestCompile** → hatasız → **Publish**. `CompilationStatus` `Success` olmadan yayınlama.
4. Swagger'da `ControllerName` altında göründüğünü doğrula.
5. Ekran tarafında bu servisi ya ListForm'un `*ServiceAddress` alanından ya da Custom
Component'in bir data source'undan tüket.
### 10.7 Şablon senaryo — "onaylı talep formu" (avans, izin, masraf, satın alma…)
Bu tür isteklerin tamamı aynı iskeleti kullanır. Kullanıcı "avans talep formu istiyorum" dediğinde
üretilecek dosya seti:
```
configs/seeds/host/
├── sql/object/Hr_T_AdvanceRequest.sql ← tablo (+ PostgreSQL hedefse postgres/object/…)
├── crud/Hr_T_AdvanceRequest.json ← yalnızca Custom Component yolunda gerekir
├── custom/AdvanceRequestForm.json ← yalnızca serbest yerleşim isteniyorsa
├── wizard/20260826101000_AdvanceRequestApprovals.json ← (varsa) menüsüz alt ekran, önce
└── wizard/20260826101500_AdvanceRequests.json ← ana ekran + menü + yetki + dil + onay akışı
```
**Karar:** Talep formu **standart bir kayıt listesi + düzenleme formu + onay akışı** ise
`ComponentKind: 0` (List) yeter ve `custom/` + `crud/` dosyalarına hiç gerek yoktur. Custom
Component yoluna yalnızca serbest yerleşim (özet kartlar, çok adımlı sihirbaz, gömülü panolar)
isteniyorsa geçilir.
**Adım adım:**
1. **Tablo** — iş kolonları + §8.1 varsayılanları. Onay akışı için dört kolon şarttır:
durum (`Status`), onaylayan (`ApproverId`), onay tarihi (`ApprovalDate`), açıklama
(`ApprovalNote`). Talep sahibi için de bir kolon (`EmployeeId`) bulunmalıdır.
2. **Wizard dosyası** — §4.4'teki iskelet:
- `IsTenant: true` (tabloda `TenantId` var), `IsDeletedField: true`, `IsCreatedField: true`
- `MenuParentCode` = modül kökü (`App.Hr`); yoksa wizard yaratır, `Order = max + 1`
- `PermissionGroupName` = `App.Hr`, EN + TR görünen adlarıyla
- Görünümler: `Grid` + `Card`ık; durum akışı görsel isteniyorsa `Todo: true` +
`TodoOptionDto` (§9.8)
3. **Alanlar**`Groups` içinde iki grup mantıklıdır: "Talep" (tutar, tarih, açıklama) ve
"Onay" (durum, onaylayan, onay tarihi, onay notu). Onay grubundaki alanlar
`EditorOptions: "{\"readOnly\":true}"` ile kilitlenir; kullanıcı doldurmaz, akış doldurur.
4. **Hesap ve varsayılanlar**`EditorScript` ile:
- Talep tarihi form açılışında bugün: `today` tarifi, tetikleyici `open`
- Tutar × oran gibi hesaplar: `multiply` / `percent`
- Eşik üstü tutarda uyarı: `notify` ya da `ask`, koşulu `greaterThan`
5. **Onay akışı** — §4.7'deki `WorkflowDto`. Tipik grafik:
`Start → Compare (tutar eşiği) → Approval (yönetici) / Approval (üst yönetici) → Inform → End`.
`ApprovalIsFilterUserName: true` ile herkes yalnızca kendi onayına düşenleri görür,
`ApprovalIsResetWorkflow: true` ile kayıt güncellenince akış başa döner.
6. **Yetki** — wizard `App.Hr.AdvanceRequests` kökünü ve `.Create/.Update/.Delete/.Export/
.Import/.Note` alt yetkilerini üretir. Onaylayanların rolüne kök + `.Update` verilir;
talep sahiplerine kök + `.Create` yeter.
7. **Dil** — menü/başlık/açıklama metinleri wizard dosyasındaki `LanguageText*` alanlarından,
alan başlıkları her `Items` öğesinin `TurkishCaption`/`EnglishCaption` alanlarından üretilir.
Ayrıca `LanguagesData.json`'a dokunmaya gerek yoktur.
8. **Teslim** — dosyaları listele, DB Migrate gerektiğini söyle, §11 kontrol listesini geç.
**Bu iskeletin türevleri:** izin talebi (tutar yerine gün sayısı, `days` tarifi), masraf talebi
(alt form olarak masraf kalemleri → menüsüz wizard + `SubForms`), satın alma talebi (kalemler +
tedarikçi lookup'ı + iki kademeli onay).
---
## 11. Teslim öncesi kontrol listesi
- [ ] Menü, route ve ekran sözleşmesi birbirini gösteriyor.
- [ ] Kök yetki + aksiyon yetkileri var; menü `RequiredPermissionName` ile bağlı.
- [ ] Yetki grubunun EN + TR görünen adı var.
- [ ] Kullanıcıya görünen her metnin EN + TR dil anahtarı var; anahtar tekrar edilmemiş.
- [ ] `IsTenant` (ve gerekiyorsa `IsBranch`/`IsOrganizationUnit`) doğru; sorgular tenant güvenli.
- [ ] Tüm SQL parametreli; string birleştirme yok.
- [ ] `DeleteCommand = DefaultDeleteCommand("{Tablo}")`.
- [ ] Seed dosyası oluştu; `InsertedRecords` yalnızca gerçekten yaratılan kayıtları içeriyor.
- [ ] Bağımlılıklar (custom component, crud endpoint, sql nesnesi) da seed'li.
- [ ] Export zip'i başka bir ortamda açıldığında ekran ayağa kalkıyor.
- [ ] Geri alma yolu yazılı: wizard dosyasını sil → `InsertedRecords` temizlenir.
Prompt ile üretim yaptıysan ek olarak:
- [ ] Dosyalar doğru kapsam klasöründe (`host/` ya da `tenants/{tenantId}/`).
- [ ] Üretim sırası doğru (§0.2) ve bağımlı wizard'ın zaman damgası daha büyük.
- [ ] Tablo/kolon adları uydurulmadı; ya var olan nesneden okundu ya da `sql/object/` altında
üretildi.
- [ ] `KeyFieldDbSourceType`, tablodaki `Id` tipiyle uyumlu.
- [ ]ılan her görünümün zorunlu option alanı dolu (§9.8).
- [ ] Custom component'te route yetkisiz üretildiği için koruma bileşenin içinde
(`designerPermission` / `*Permission` / `checkPermission`).
- [ ] Custom component visual modda ise `Props.visualDesigner` ile `Code` marker'ı aynı dokümanı
taşıyor.
- [ ] Menü ve route elle eklenmedi; wizard dosyasından geliyor.
- [ ] Kullanıcıya hangi dosyaların üretildiği ve DB Migrate gerektiği söylendi.
- [ ] Ekran tarifi (§0.6) çıkarıldı ve yol seçimi gerekçelendirildi.
- [ ] Görselden üretildiyse: platformda karşılığı olmayan öğeler ve yapılan varsayımlar
(tablo/kolon adları, anahtar tipi, lookup kaynakları) açıkça bildirildi.
- [ ] Onaycı/iş kuralı gibi görselden okunamayan kritik bilgiler için soru soruldu (§0.7.4).