wizard 3 sekmeli sistem ve timesheet çalışması

This commit is contained in:
Sedat ÖZTÜRK 2026-09-28 15:50:38 +03:00
parent a2c3655942
commit ae226d359b
21 changed files with 683 additions and 82 deletions

View file

@ -834,6 +834,7 @@ tipli okumaya bağlı davranış (GridBox kolonları, TagBox etiket sayısı) ç
| İhtiyaç | Nerede | | İhtiyaç | Nerede |
| --- | --- | | --- | --- |
| Fotoğraf çekme | `dxImageUpload` · `Capture` (yukarıda) | | Fotoğraf çekme | `dxImageUpload` · `Capture` (yukarıda) |
| Görsel tutan kolon | Editör `dxImageUpload`, kolon `nvarchar(MAX)` (base64); `dxImageViewer` kolona bağlanmaz (`lowcode.instructions.md` §0.12 madde 5) |
| Alana barkod/QR okutma | `dxTextBox` `EditorOptions.buttons[]` içinde `onClick: "@SCAN_BARCODE"` (`lowcode` §6.4) | | Alana barkod/QR okutma | `dxTextBox` `EditorOptions.buttons[]` içinde `onClick: "@SCAN_BARCODE"` (`lowcode` §6.4) |
| Sürekli okutup her kodu yeni satır yapma | Toolbar butonu `DialogName: "BarcodeScanDialog"` + `DialogParameters` `{"field": "…", …sabit alanlar}` — ekranın insert ucundan geçer (`lowcode` §4.9) | | Sürekli okutup her kodu yeni satır yapma | Toolbar butonu `DialogName: "BarcodeScanDialog"` + `DialogParameters` `{"field": "…", …sabit alanlar}` — ekranın insert ucundan geçer (`lowcode` §4.9) |
| Cihaz konumu | `EditorScript` `location` tarifi → `geoTo('Enlem', 'Boylam')` (`lowcode` §5.3) | | Cihaz konumu | `EditorScript` `location` tarifi → `geoTo('Enlem', 'Boylam')` (`lowcode` §5.3) |

View file

@ -365,7 +365,7 @@ dörtlüyle üretilir (§9.8), görseldeki görünüm `DefaultLayout` olur.
| Renk kutucuğu | `dxColorBox` | | | Renk kutucuğu | `dxColorBox` | |
| Kalın/italik araç çubuklu alan | `dxHtmlEditor` | `toolbar.items` | | Kalın/italik araç çubuklu alan | `dxHtmlEditor` | `toolbar.items` |
| Kaydırıcı | `dxSlider` / `dxRangeSlider` | `min`, `max` | | Kaydırıcı | `dxSlider` / `dxRangeSlider` | `min`, `max` |
| Görsel küçük resmi / yükleme alanı | `dxImageViewer` / `dxImageUpload` | `accept`, `maxFileSize` | | Fotoğraf / görsel alanı (seçilen, çekilen ya da başka kayıttan kopyalanan) | `dxImageUpload`, kolon `nvarchar(MAX)` (base64); salt okunur gösterilecekse `EditorOptions: {"readOnly": true}` — `dxImageViewer` değil | `accept`, `maxFileSize`, `capture` |
| Gri, tıklanamaz alan | ilgili editör + `EditorOptions: {"readOnly": true}` | | | Gri, tıklanamaz alan | ilgili editör + `EditorOptions: {"readOnly": true}` | |
| Kırmızı yıldızlı etiket | `IsRequired: true` | | | Kırmızı yıldızlı etiket | `IsRequired: true` | |
@ -505,7 +505,7 @@ ister ve kiracı kullanıcısına hiçbiri açılmaz.
| `get_dev_guide` | Bu dosyanın (`lowcode`) ya da `reference`'ın başlık listesini / bir bölümünü döner | Bölüm numarayla istenir (`4.3`); aynı numarayı iki başlık taşımamalıdır | | `get_dev_guide` | Bu dosyanın (`lowcode`) ya da `reference`'ın başlık listesini / bir bölümünü döner | Bölüm numarayla istenir (`4.3`); aynı numarayı iki başlık taşımamalıdır |
| `create_table` | Yeni SQL Server tablosu kurar, `sql/object/{Tablo}.sql` dosyasını yazar | Ek olarak `App.SqlQueryManager` ister; `Id`/`TenantId`/audit kolonlarını kendi ekler (`fullAudited`, `multiTenant` varsayılan açık); var olan tabloyu reddeder. View/SP/fonksiyon kurulamaz | | `create_table` | Yeni SQL Server tablosu kurar, `sql/object/{Tablo}.sql` dosyasını yazar | Ek olarak `App.SqlQueryManager` ister; `Id`/`TenantId`/audit kolonlarını kendi ekler (`fullAudited`, `multiTenant` varsayılan açık); var olan tabloyu reddeder. View/SP/fonksiyon kurulamaz |
| `list_wizards` · `get_wizard` | Wizard dosyalarını listeler (menü yolu, bağımlılar) · dosyayı diskteki hâliyle döner | Var olan menü kökünü/izin grubunu bulmak için önce `list_wizards` | | `list_wizards` · `get_wizard` | Wizard dosyalarını listeler (menü yolu, bağımlılar) · dosyayı diskteki hâliyle döner | Var olan menü kökünü/izin grubunu bulmak için önce `list_wizards` |
| `deploy_wizard` | Wizard cevabından ekranın wizard dosyasını ve menü/yetki/dil seed satırlarını yazar; veritabanına yazmaz (§4.0) | Girdi §4.4'teki düz cevap ya da `get_wizard` çıktısıyla aynı dosya biçimi; enum'lar sayı. `replaceFileName` = `EditFileName` (§4.5). List ekranında `Widgets` dolu gelir — en az 3 kart, kiracı tablosunda `"TenantId" = '@TENANTID'` (§4.6.1). İstek demo veri taşıyorsa `SeedData: true` + `seedRows` (§0.10.1). Çıkıştaki `isPending: true` ekranın henüz kurulmadığını söyler — kullanıcı Wizard Manager'da DB Migrate çalıştırır; model migrate'i beklemez | | `deploy_wizard` | Wizard cevabından ekranın wizard dosyasını ve menü/yetki/dil seed satırlarını yazar; veritabanına yazmaz (§4.0) | Girdi §4.4'teki düz cevap ya da `get_wizard` çıktısıyla aynı dosya biçimi; enum'lar sayı. `replaceFileName` = `EditFileName` (§4.5). List ekranında `Widgets` dolu gelir — en az 3 kart, kiracı tablosunda `"TenantId" = '@TENANTID'` (§4.6.1); kartlardaki her dil anahtarı var olmalı ya da `Language.Texts`'te EN + TR verilmeli, yoksa deploy reddeder. `MenuOrder` yazılmaz — sunucu yeni ekranı kardeşlerinin sonuna koyar, güncellemede yerini korur. İstek demo veri taşıyorsa `SeedData: true` + `seedRows` (§0.10.1). Çıkıştaki `isPending: true` ekranın henüz kurulmadığını söyler — kullanıcı Wizard Manager'da DB Migrate çalıştırır; model migrate'i beklemez |
| `delete_wizard` | Wizard'ı ve yalnızca onun ürettiği kayıtları siler; `dropTables: true` ekranın tablosunu da düşürür | Bağımlı wizard varsa reddeder. Tablo kullanıcı açıkça onaylamadan düşürülmez; `App.SqlQueryManager` ister (§4.5) | | `delete_wizard` | Wizard'ı ve yalnızca onun ürettiği kayıtları siler; `dropTables: true` ekranın tablosunu da düşürür | Bağımlı wizard varsa reddeder. Tablo kullanıcı açıkça onaylamadan düşürülmez; `App.SqlQueryManager` ister (§4.5) |
| `start_dev_request` | `App.DevRequests` talebini üstlenir: `Ready to Start` durumundaysa `In Progress` durumuna alır, değilse reddeder | Ek olarak `App.DevRequests.Update` ister. Açıklamadaki görseller ayrı resim içeriği olarak gelir (§0.11) | | `start_dev_request` | `App.DevRequests` talebini üstlenir: `Ready to Start` durumundaysa `In Progress` durumuna alır, değilse reddeder | Ek olarak `App.DevRequests.Update` ister. Açıklamadaki görseller ayrı resim içeriği olarak gelir (§0.11) |
| `get_dev_request` | Talebi durumuna dokunmadan okur (aynı metin + analiz + resim içerikleri) | Analizden önce ya da yarım kalan talebe dönmek için; `App.DevRequests` ister | | `get_dev_request` | Talebi durumuna dokunmadan okur (aynı metin + analiz + resim içerikleri) | Analizden önce ya da yarım kalan talebe dönmek için; `App.DevRequests` ister |
@ -602,6 +602,14 @@ talebin metnini döndürür. Başlık birden fazla talebe uyuyorsa Id ister. `St
verilerek (`"deploy_wizard: Mrp.Orders.json"`). Araç satırı tamamlanmış yazar, atanan kişiyi verilerek (`"deploy_wizard: Mrp.Orders.json"`). Araç satırı tamamlanmış yazar, atanan kişiyi
bağlanan kullanıcı, bitiş tarihini bugün yapar ve var olan satırları korur. `SubTasks` bağlanan kullanıcı, bitiş tarihini bugün yapar ve var olan satırları korur. `SubTasks`
`update_row` ile yazılmaz. `update_row` ile yazılmaz.
- `Timesheets` — **sunucu yazar, model hiçbir şey yapmaz.** `get_dev_request` / `start_dev_request`
talebi kullanıcının etkin talebi yapar; ardından yapılan her MCP yazma işlemi (`create_table`,
`deploy_wizard`, `delete_wizard`, `save_component`, `delete_component`, `insert_row` /
`update_row` / `delete_row`, `save_dev_request_analysis`) o talebe bir satır ekler
(`IMcpDevRequestTimesheetRecorder`). Açıklama `App.Mcp.Timesheet.*` dil anahtarından **Türkçe**
üretilir ("Hr_T_Doctor tablosu oluşturuldu"), süre önceki satırdan ya da talebin açıldığı andan bu
yana ölçülür (en az 1 dakika, saat olarak), çalışan bağlanan kullanıcı, tarih bugündür.
`Timesheets` `update_row` ile yazılmaz.
- `Analysis` — `append_dev_request_note` ile sonuna `<h4>MCP</h4>` başlığıyla kısa özet: - `Analysis` — `append_dev_request_note` ile sonuna `<h4>MCP</h4>` başlığıyla kısa özet:
tablo, ekran, alan, demo veri, menü/yetki, analize eklenen varsayılanlar ve DB Migrate bekleyen tablo, ekran, alan, demo veri, menü/yetki, analize eklenen varsayılanlar ve DB Migrate bekleyen
wizard'lar. Tekrar üretimde yeni bir `<h4>MCP</h4>` bloğu eklenir, eskisi silinmez. wizard'lar. Tekrar üretimde yeni bir `<h4>MCP</h4>` bloğu eklenir, eskisi silinmez.
@ -616,10 +624,16 @@ değildir) ve içindeki satırları taşır; satırlar demo veridir (§0.10.1).
**Kurallar** **Kurallar**
- **Onaylanmış analiz tarifin kendisidir; üretim karar vermez.** Soru sorulmaz, seçim yapılmaz, - **Onaylanmış analiz tarifin kendisidir; üretim karar vermez.** Soru sorulmaz, seçim yapılmaz,
analizin yazdığı hiçbir şey değiştirilmez ya da daraltılmaz. Bu dosyadaki "kullanıcıya sor" analizin yazdığı hiçbir şey değiştirilmez ya da daraltılmaz — tek istisna platform kuralıyla çelişen kısımdır (aşağıda). Bu dosyadaki "kullanıcıya sor"
kuralları (onaycı, görselden okunamayan zorunlu alan, çerçeve dışı ihtiyaç) bu döngüde analizin kuralları (onaycı, görselden okunamayan zorunlu alan, çerçeve dışı ihtiyaç) bu döngüde analizin
**(onay)** kararlarıyla çözülmüştür. Analizde karşılığı olmayan küçük bir ayrıntı (§0.6 **(onay)** kararlarıyla çözülmüştür. Analizde karşılığı olmayan küçük bir ayrıntı (§0.6
varsayılanı olan türden) varsayılanla tamamlanır ve sonuç notuna yazılır. varsayılanı olan türden) varsayılanla tamamlanır ve sonuç notuna yazılır.
- **Platform kuralı analizden önce gelir.** Analiz, bu dosyadaki bağlayıcı bir kuralla çelişiyorsa
(görsel kolonu `nvarchar(MAX)` + `dxImageUpload`, tanım tablosunda `IsActive`, parametreli SQL,
kiracı filtresi…) üretim kuralı uygular, çelişkiyi sonuç notuna yazar; durmaz, soru sormaz.
Bu yüzden her `create_table` öncesi kolon listesi ekranın editörleriyle karşılaştırılır: editörü
`dxImageUpload` olan kolon `nvarchar(MAX)` açılır — tablo kurulduktan sonra kolon değiştirilemez.
Kural analizi talebin istediğinden uzaklaştırmaz; yalnızca platformun kurallarına uydurur.
- **Üretim tek seferde biter:** `In Progress` → `Testing` tek geçiştir; talep durdurulmaz, - **Üretim tek seferde biter:** `In Progress` → `Testing` tek geçiştir; talep durdurulmaz,
`Analysis`'e geri alınmaz, kısmi ekran üretilmez. Üretilebilirlik analizde doğrulanmıştır `Analysis`'e geri alınmaz, kısmi ekran üretilmez. Üretilebilirlik analizde doğrulanmıştır
(§0.12). Döngü DB Migrate'i beklemez. (§0.12). Döngü DB Migrate'i beklemez.
@ -643,7 +657,7 @@ bileşen, seed dosyası yazılmaz; yalnızca okuma araçları ve `save_dev_reque
| 1. Kuyruğu oku (yalnızca toplu) | `query_list` — `ListFormCode: App.DevRequests`, filtre `["Status","=","Backlog"]`, sıralama `SortOrder` | — | | 1. Kuyruğu oku (yalnızca toplu) | `query_list` — `ListFormCode: App.DevRequests`, filtre `["Status","=","Backlog"]`, sıralama `SortOrder` | — |
| 2. Talebi oku | `get_dev_request` — metin + görseller | — | | 2. Talebi oku | `get_dev_request` — metin + görseller | — |
| 3. Keşif | `list_wizards` (menü kökleri, modüller, izin grupları), `list_lists` / `describe_list` / `search_schema` (var olan ekran, tablo, lookup kaynağı), `list_components`, `get_dev_guide` (§0.6, §0.7, §0.8, §1) | — | | 3. Keşif | `list_wizards` (menü kökleri, modüller, izin grupları), `list_lists` / `describe_list` / `search_schema` (var olan ekran, tablo, lookup kaynağı), `list_components`, `get_dev_guide` (§0.6, §0.7, §0.8, §1) | — |
| 4. Yaz | `save_dev_request_analysis` — analizin tamamı, HTML | Analysis | | 4. Yaz | `save_dev_request_analysis` — analizin tamamı, HTML (Timesheets satırını sunucu yazar, §0.11) | Analysis |
**Durum kuralı sunucudadır:** araç yalnızca `Backlog` ya da `Analysis` durumundaki talebi kabul eder; **Durum kuralı sunucudadır:** araç yalnızca `Backlog` ya da `Analysis` durumundaki talebi kabul eder;
`Analysis`'teki talebe yeniden çağrılırsa analiz değiştirilir. `Ready to Start` ve sonrası `Analysis`'teki talebe yeniden çağrılırsa analiz değiştirilir. `Ready to Start` ve sonrası
@ -676,27 +690,38 @@ Kısa ve karar odaklı yaz; talebi tekrar anlatma.
| Seviye | Kod | Ad (TR / EN) | İkon | Durum | | Seviye | Kod | Ad (TR / EN) | İkon | Durum |
| --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- |
| Kök | … | … / … | … | Var / Yeni | | Kök | … | … / … | … | Var / Yeni |
| Ara (varsa, her biri) | … | … / … | … | Var / Yeni | | Ara (yalnızca talep istiyorsa, her biri) | … | … / … | … | Var / Yeni |
| Ekran | `ListFormCode` | … / … | … | Yeni | | Ekran | `ListFormCode` | … / … | … | Yeni |
Altında `ModuleId`, izin grubu ve route yazılır. Var olan seviyeler `list_wizards` ve menü Altında `ModuleId`, izin grubu ve route yazılır. Var olan seviyeler `list_wizards` ve menü
seed'inden okunur, kodları oradaki gibi yazılır; sıra (`Order`) deploy'da hesaplanır, analize seed'inden okunur, kodları oradaki gibi yazılır; sıra (`Order`) deploy'da hesaplanır, analize
yazılmaz. Talep menü yolunu yazıyorsa (başlıkta ya da açıklamada) o yol girdidir ve seviye yazılmaz. Seviye sayısı talepten gelir, sabit değildir: `Kök › Ekran` iki, `Kök › Ara › Ekran`
seviye aynen alınır. üç seviyedir; analiz talebin istemediği bir ara seviye eklemez. Talep menü yolunu yazıyorsa
(başlıkta ya da açıklamada) o yol girdidir ve seviye seviye aynen alınır. Wizard karşılığı:
ekranın bir üstü `MenuParentCode`, daha üstündekiler `MenuParentPath` (iki seviyede boş, §4.4).
Aynı üst menü altındaki ekranlar **bağımlılık sırasıyla** yazılır: başka bir ekranın lookup
kaynağı olan (tanım) ekran, onu kullanan ekranın üstündedir. Menüdeki sıra deploy sırasından
gelir; madde 9'daki üretim planı da aynı sırayla yazılır.
5. **Veri modeli** — tablo adı (onek kuralı §2), yeni mi var olan mı; kolonlar (ad, tip, zorunluluk, 5. **Veri modeli** — tablo adı (onek kuralı §2), yeni mi var olan mı; kolonlar (ad, tip, zorunluluk,
uzunluk), lookup kaynakları, kiracı + audit (standing default 1). Yeni her tablonun ilk satırı uzunluk), lookup kaynakları, kiracı + audit (standing default 1). Yeni her tablonun ilk satırı
**Tanım tablosu: Evet / Hayır** kararıdır, talepteki dayanağıyla (§8.1); atlanmaz. Evet ise **Tanım tablosu: Evet / Hayır** kararıdır, talepteki dayanağıyla (§8.1); atlanmaz. Evet ise
kolon tablosunda `IsActive` (bit, varsayılan 1, zorunlu) yer alır — talepte ya da görselde kolon tablosunda `IsActive` (bit, varsayılan 1, zorunlu) yer alır — talepte ya da görselde
olmasa da — ve madde 7 (form alanı + Aktif/Pasif widget'ı) ile madde 9 (`create_table` kolonu, olmasa da — ve madde 7 (form alanı + Aktif/Pasif widget'ı) ile madde 9 (`create_table` kolonu,
demo veride `IsActive: true`) bunu taşır. demo veride `IsActive: true`) bunu taşır.
**Görsel kolonu:** görsel tutan her kolon (fotoğraf, resim, logo, avatar kopyası, belge
görüntüsü) `nvarchar(MAX)`'tir ve editörü `dxImageUpload`'dır — değer başka bir kayıttan
kopyalanıyor olsa da. Görsel base64 metin olarak saklanır; `nvarchar(512)` gibi sınırlı boy
yazılmaz, `dxImageViewer` kullanılmaz (§4.3 editör tablosu). Çok satırlı serbest metin
(`dxTextArea`, `dxHtmlEditor`) de `nvarchar(MAX)`'tir.
6. **Etkilenen kısımlar** — uygulamada dokunulacak her yer: yeni/değişen ekranlar, alt formlar, 6. **Etkilenen kısımlar** — uygulamada dokunulacak her yer: yeni/değişen ekranlar, alt formlar,
menü, yetkiler (`C/R/U/D` + varsa özel), dil anahtarları, widget'lar, iş akışı/bildirim, SEO (public menü, yetkiler (`C/R/U/D` + varsa özel), dil anahtarları, widget'lar, iş akışı/bildirim, SEO (public
yol varsa), var olan ekranlara etkiler. yol varsa), var olan ekranlara etkiler.
7. **Görünümler ve davranış** — Grid/Card/Pivot/Chart (standing default 12), widget kartları (§4.6.1), 7. **Görünümler ve davranış** — Grid/Card/Pivot/Chart (standing default 12), widget kartları (§4.6.1),
`EditorScript` / `EditorOptions` ihtiyaçları, hesaplar ve doğrulamalar. `EditorScript` / `EditorOptions` ihtiyaçları, hesaplar ve doğrulamalar. Görsel alanı her zaman
`dxImageUpload`'dır (madde 5).
8. **Kararlar** — talepten okunamayıp analizde verilen her karar, tek satır gerekçesiyle (§0.6); 8. **Kararlar** — talepten okunamayıp analizde verilen her karar, tek satır gerekçesiyle (§0.6);
varsayılamayan konular **(onay)** etiketi taşır. "Açık sorular" başlığı yoktur. varsayılamayan konular **(onay)** etiketi taşır. "Açık sorular" başlığı yoktur.
9. **Üretim planı** — sıralı artefakt listesi (`create_table: …`, `deploy_wizard: ….json`, 9. **Üretim planı** — bağımlılık sırasıyla (tablo → lookup kaynağı ekran → onu kullanan ekran) sıralı artefakt listesi (`create_table: …`, `deploy_wizard: ….json`,
`save_component: …`); üretimdeki `add_dev_request_subtasks` başlıkları bununla aynı olur. `save_component: …`); üretimdeki `add_dev_request_subtasks` başlıkları bununla aynı olur.
10. **Üretime hazır** — tek satır: **Evet**, ya da **Hayır — eksik yetenek: …** (çerçeve dışı 10. **Üretime hazır** — tek satır: **Evet**, ya da **Hayır — eksik yetenek: …** (çerçeve dışı
ihtiyaç, §0.8, adıyla). Talebi daraltan geçici çözüm önerilmez; "Hayır" taşıyan talep yetenek ihtiyaç, §0.8, adıyla). Talebi daraltan geçici çözüm önerilmez; "Hayır" taşıyan talep yetenek
@ -915,6 +940,7 @@ sürümünü taşır.
"SeededAt": "2026-09-01T09:03:00", "CustomComponentName": null, "SeededAt": "2026-09-01T09:03:00", "CustomComponentName": null,
"PermissionGroupName": "App.Prj", "ModuleId": "Prj", // boş bağlantı metni yazılmaz "PermissionGroupName": "App.Prj", "ModuleId": "Prj", // boş bağlantı metni yazılmaz
"Menu": { "MenuCode": "…", "MenuOrder": 2, "MenuUrl": "…", "MenuIcon": "…", "Menu": { "MenuCode": "…", "MenuOrder": 2, "MenuUrl": "…", "MenuIcon": "…",
"MenuParentPath": [ { "Code": "…", "Icon": "…", "En": "…", "Tr": "…" } ], // yalnızca derin yolda
"MenuParentCode": "…", "MenuParentIcon": "…" }, // "CreateMenu": false ise eklenir "MenuParentCode": "…", "MenuParentIcon": "…" }, // "CreateMenu": false ise eklenir
"Language": { "MenuEn": "…", /* Menu/Title/Desc/MenuParent/PermissionGroup · EN + TR */ }, "Language": { "MenuEn": "…", /* Menu/Title/Desc/MenuParent/PermissionGroup · EN + TR */ },
"Groups": [ { "Items": [ { "FieldName": "…", "CaptionName": "…", "Groups": [ { "Items": [ { "FieldName": "…", "CaptionName": "…",
@ -999,9 +1025,10 @@ saklanır. Dosya okunurken bu alanlar `ListForm`/`Fields` üzerinden geri doldur
| Alan | Anlam | | 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` olduğu için dosyaya **yalnızca `false` iken** yazılır. | | `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` olduğu için dosyaya **yalnızca `false` iken** yazılır. |
| `MenuParentCode` / `MenuParentModuleId` / `MenuParentIcon` | Üst menü; yoksa oluşturulur. | | `MenuParentCode` / `MenuParentModuleId` / `MenuParentIcon` | Ekranın doğrudan üst menüsü; yoksa oluşturulur. |
| `MenuParentPath` | `MenuParentCode`'un üstündeki seviyeler, kökten aşağı; her biri `{ Code, Icon, En, Tr }`. Derinlik sınırsızdır: deploy zinciri kökten yürür, var olan seviyeye bağlanır, eksik seviyeyi bir üstünün altına kardeşlerinin sonuna kurar (menü + dil satırı, `InsertedRecords`'a girer). Boşsa üst menü kök seviyededir; boş liste dosyaya yazılmaz. Örnekler: `İnsan Kaynakları › Yeterlilik Belgeleri` (iki seviye) → `MenuParentCode: "App.Hr"`, `MenuParentPath` yok; `İnsan Kaynakları › Tanımlamalar › Yeterlilik Belgeleri` (üç seviye) → `MenuParentPath: [{ Code: "App.Hr", … }]`, `MenuParentCode: "App.Hr.Definitions"`. |
| `MenuIcon` | React-icons adı (`FcBiohazard`, `FaBox`…). | | `MenuIcon` | React-icons adı (`FcBiohazard`, `FaBox`…). |
| `MenuOrder` | Kardeşler arası sıra. | | `MenuOrder` | Kardeşler arası sıra; 0/boş ise kardeşlerin sonu. MCP deploy'unda yok sayılır (yeni ekran sona, güncelleme yerinde kalır). |
**Yetki ve dil** **Yetki ve dil**
@ -1018,6 +1045,7 @@ saklanır. Dosya okunurken bu alanlar `ListForm`/`Fields` üzerinden geri doldur
| `Language.TitleEn` / `Tr` | Ekran başlığı (yalnızca List yolunda üretilir). | | `Language.TitleEn` / `Tr` | Ekran başlığı (yalnızca List yolunda üretilir). |
| `Language.DescEn` / `Tr` | Ekran açıklaması (yalnızca List yolunda). | | `Language.DescEn` / `Tr` | Ekran açıklaması (yalnızca List yolunda). |
| `Language.MenuParentEn` / `Tr` | Üst menü etiketi. | | `Language.MenuParentEn` / `Tr` | Üst menü etiketi. |
| `Language.Texts` | Başka bir cevaptan üretilmeyen dil anahtarları: `[{ "Key", "En", "Tr" }]` (widget başlığı gibi). Anahtar yoksa deploy satırı yazar ve wizard'a bağlar (silmede gider). Deploy, widget sorgularındaki tek tırnaklı noktalı her tanımlayıcıyı (`'App.Widget.ThisMonth'`) dil anahtarı sayar; veritabanında, bekleyen dil satırlarında, bu wizard'ın ürettiklerinde ya da burada yoksa deploy'u adlarıyla reddeder. |
Üretilen yetkiler: `{MenuCode}` (okuma, kök) + List yolunda `.Create .Update .Delete .Export Üretilen yetkiler: `{MenuCode}` (okuma, kök) + List yolunda `.Create .Update .Delete .Export
.Import .Note`, Custom yolunda yalnızca `.Create .Update .Delete`. Hiçbiri bir role otomatik .Import .Note`, Custom yolunda yalnızca `.Create .Update .Delete`. Hiçbiri bir role otomatik
@ -1154,8 +1182,8 @@ anahtarları listeler; ortak anahtarlar (§4.3.3) her editörde geçerlidir.
| `dxSlider` | Tek değerli kaydırıcı | Sayı | `min`, `max`, `tooltip.showMode` | | `dxSlider` | Tek değerli kaydırıcı | Sayı | `min`, `max`, `tooltip.showMode` |
| `dxRangeSlider` | Aralık kaydırıcı | Sayı | `min`, `max`, `showRange` | | `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.tabs`, `imageUpload.uploadUrl`, `imageUpload.fileUploadMode`, `tableResizing.enabled`, `tableContextMenu.enabled`. Boş `editorOptions` da tam araç çubuğu, dosyadan/URL'den resim, resim boyutlandırma ve tablo araçlarıyla gelir (`ui/src/views/shared/htmlEditorOptions.ts`); yazılan değer varsayılanın üstüne tek seviye birleştirilir, yalnızca farkı yaz. | | `dxHtmlEditor` | Zengin metin | String (16) | `toolbar.items`, `toolbar.multiline`, `valueType` (`html`/`markdown`), `mediaResizing.enabled`, `imageUpload.tabs`, `imageUpload.uploadUrl`, `imageUpload.fileUploadMode`, `tableResizing.enabled`, `tableContextMenu.enabled`. Boş `editorOptions` da tam araç çubuğu, dosyadan/URL'den resim, resim boyutlandırma ve tablo araçlarıyla gelir (`ui/src/views/shared/htmlEditorOptions.ts`); yazılan değer varsayılanın üstüne tek seviye birleştirilir, yalnızca farkı yaz. |
| `dxImageUpload` | Görsel yükleme (platform) | String (16) | `uploadUrl`\*, `accept`\*, `capture`\*, `multiple`\*, `maxFileSize`\*, `width`\*, `height`\* | | `dxImageUpload` | Görsel yükleme (platform); görsel tutan her kolonun editörü. `uploadUrl` yoksa dosya **base64** metin olarak kolona yazılır → kolon `nvarchar(MAX)`; sınırlı boylu kolonla deploy reddedilir. `uploadUrl` varsa kolona dönen adres yazılır | String (16) | `uploadUrl`\*, `accept`\*, `capture`\*, `multiple`\*, `maxFileSize`\*, `width`\*, `height`\* |
| `dxImageViewer` | Görsel gösterimi (platform) | String (16) | `width`, `height` | | `dxImageViewer` | Görsel gösterimi (platform), yükleme yapmaz. Tablo kolonuna bağlı alanda kullanılmaz — görsel kolonu `dxImageUpload`'dır, deploy `dxImageViewer`'ı reddeder | String (16) | `width`, `height` |
`\*` ile işaretli anahtarlar backend'de **tipli DTO'ya** çözülür: `\*` ile işaretli anahtarlar backend'de **tipli DTO'ya** çözülür:
`GridBoxOptionsDto`, `TagBoxOptionsDto`, `ImageUploadOptionsDto`. Yanlış tipte yazılan bir değer `GridBoxOptionsDto`, `TagBoxOptionsDto`, `ImageUploadOptionsDto`. Yanlış tipte yazılan bir değer
@ -1535,7 +1563,7 @@ ile ekler (§4.0). Sıra değerleri veritabanı ile dosyadaki satırların birle
| Dil anahtarları | `{MenuCode}` (menü), `{ListFormCode}.Title`, `{ListFormCode}.Desc`, `{PermissionGroupName}` ve her alan için `CaptionName` | | 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`) | | 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` | | 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` | | Üst menü zinciri | `MenuParentPath` + `MenuParentCode` kökten yürünür; eksik seviye bir üstünün altına `Order = max(kardeş Order) + 1` ile yaratılır (kökte kök menülerin sonu) |
| Menü sırası | `MenuOrder` 0/boş ise `max(kardeş Order) + 1` | | Menü sırası | `MenuOrder` 0/boş ise `max(kardeş Order) + 1` |
| `DataSource` | `DataSourceCode` yoksa migrate yaratır; bağlantı metninde `Server` geçiyorsa `Mssql`, aksi hâlde `Postgresql` | | `DataSource` | `DataSourceCode` yoksa migrate yaratır; bağlantı metninde `Server` geçiyorsa `Mssql`, aksi hâlde `Postgresql` |
| `LayoutJson` | Görünüm bayrakları + `DefaultLayout` | | `LayoutJson` | Görünüm bayrakları + `DefaultLayout` |
@ -1743,7 +1771,8 @@ gelir; kullanıcı "widget/KPI" demese de, sorulmadan eklenir ve tarifte tek sat
- **Metin:** `Title` bir dil anahtarıdır. Önce var olan anahtarlar kullanılır: genel - **Metin:** `Title` bir dil anahtarıdır. Önce var olan anahtarlar kullanılır: genel
`App.Widget.*` (`Total`, `Active`, `Passive`, `Published`, `Draft`, `Today`, `Upcoming`, `App.Widget.*` (`Total`, `Active`, `Passive`, `Published`, `Draft`, `Today`, `Upcoming`,
`Pending`, `Sent`, `TotalViews`, `TotalRevenue`…), lookup'ın `App.StaticLookup.*` anahtarları, `Pending`, `Sent`, `TotalViews`, `TotalRevenue`…), lookup'ın `App.StaticLookup.*` anahtarları,
alanın `App.Listform.ListformField.*` başlığı. Karşılığı yoksa yeni anahtar EN + TR yazılır. alanın `App.Listform.ListformField.*` başlığı. Karşılığı yoksa yeni anahtar wizard'ın
`Language.Texts` alanında EN + TR verilir (§4.4); tanımsız anahtarla deploy reddedilir.
`SubTitle` ekranın kendi başlık anahtarıdır (`ListFormCode`). `SubTitle` ekranın kendi başlık anahtarıdır (`ListFormCode`).
- **Renk ve ikon:** `blue` toplam, `green` olumlu/aktif, `gray` pasif/taslak, `red` hata/iptal, - **Renk ve ikon:** `blue` toplam, `green` olumlu/aktif, `gray` pasif/taslak, `red` hata/iptal,
`yellow`/`orange` bekleyen, `purple` tutar/özet. İkon `react-icons/fa` adıdır (`FaCheckCircle`, `yellow`/`orange` bekleyen, `purple` tutar/özet. İkon `react-icons/fa` adıdır (`FaCheckCircle`,
@ -2048,7 +2077,7 @@ bölümünü doldurur.** Keşif turu (§0.6) soruları da bu adımlarla bire bir
| --- | --- | --- | --- | | --- | --- | --- | --- |
| 0 | Komponent Tipi | her zaman | `ComponentKind` (`0` List · `1` Custom) | | 0 | Komponent Tipi | her zaman | `ComponentKind` (`0` List · `1` Custom) |
| 1 | Custom Component | yalnız Custom yolu | `CustomComponentName`, `MenuUrl` (bileşenin `RoutePath`'i) | | 1 | Custom Component | yalnız Custom yolu | `CustomComponentName`, `MenuUrl` (bileşenin `RoutePath`'i) |
| 2 | Menü | her zaman | `WizardName`, `CreateMenu`, `MenuParentCode`, `MenuCode`, `Language.MenuEn/Tr`, `PermissionGroupName` (+ görünen adlar), `MenuIcon`, `MenuOrder` | | 2 | Menü | her zaman | `WizardName`, `CreateMenu`, `MenuParentCode` (ekranda eksik seviyeler menü diyaloğuyla kurulur; MCP `MenuParentPath` yazar), `MenuCode`, `Language.MenuEn/Tr`, `PermissionGroupName` (+ görünen adlar), `MenuIcon`, `MenuOrder` |
| 3 | Ayarlar | yalnız List yolu | `ListFormCode`, `DataSourceCode` (+`DataSourceConnectionString`), `SelectCommandType`, `SelectCommand`, `KeyFieldName`, `KeyFieldDbSourceType`, `IsTenant/IsBranch/IsOrganizationUnit`, `AllowAdding/Updating/Deleting`, `ConfirmDelete`, `AllowDetail`, `DefaultLayout` + 8 görünüm bayrağı (`SeedData` adımda sorulmaz, düzenlemede korunur), `Language.TitleEn/Tr`, `Language.DescEn/Tr` | | 3 | Ayarlar | yalnız List yolu | `ListFormCode`, `DataSourceCode` (+`DataSourceConnectionString`), `SelectCommandType`, `SelectCommand`, `KeyFieldName`, `KeyFieldDbSourceType`, `IsTenant/IsBranch/IsOrganizationUnit`, `AllowAdding/Updating/Deleting`, `ConfirmDelete`, `AllowDetail`, `DefaultLayout` + 8 görünüm bayrağı (`SeedData` adımda sorulmaz, düzenlemede korunur), `Language.TitleEn/Tr`, `Language.DescEn/Tr` |
| 4 | Alanlar | yalnız List | `Groups[]` (aşağıda) | | 4 | Alanlar | yalnız List | `Groups[]` (aşağıda) |
| 5 | Alt Formlar | yalnız List | `SubForms[]` | | 5 | Alt Formlar | yalnız List | `SubForms[]` |
@ -3860,7 +3889,7 @@ silme, DB Migrate, Export (zip), Import (diff + rollback). Yetkiler
`App.Listforms.Wizard{,.Create,.Update,.Delete,.Export,.Import}`. `App.Listforms.Wizard{,.Create,.Update,.Delete,.Export,.Import}`.
Listede her ekranın üst menü zinciri (`WizardFileInfoDto.MenuPath`, Menu tablosundan; kayıt yoksa Listede her ekranın üst menü zinciri (`WizardFileInfoDto.MenuPath`, Menu tablosundan; kayıt yoksa
wizard'ın `MenuParentCode` cevabı) görünür. Bağlı wizard'lar üst ekranın altında gruplanır wizard'ın `MenuParentPath` + `MenuParentCode` cevabı) görünür. Bağlı wizard'lar üst ekranın altında gruplanır
(`Dependencies`): List yolunda alt formlar, Custom yolunda component `Dependencies` zinciri — (`Dependencies`): List yolunda alt formlar, Custom yolunda component `Dependencies` zinciri —
export kapanışıyla aynı kural; wizard'ı olmayan ara component atlanır. Kök, hiçbir wizard'ın export kapanışıyla aynı kural; wizard'ı olmayan ara component atlanır. Kök, hiçbir wizard'ın
bağımlılığı olmayan dosyadır; ortak bir alt form her üst ekranın altında tekrar görünür. bağımlılığı olmayan dosyadır; ortak bir alt form her üst ekranın altında tekrar görünür.

View file

@ -1406,7 +1406,7 @@ liste süzgeci ve çağrı kapısı aynı kuralı kullanır.
| `get_dev_guide` | Üretim sözleşmesi: `lowcode` / `reference` belgelerinin başlık listesi ya da istenen bölümü (numarayla, örn. `4.2`). Çok büyük bir üst bölüm yalnızca giriş metni + alt başlıklarıyla döner. | | `get_dev_guide` | Üretim sözleşmesi: `lowcode` / `reference` belgelerinin başlık listesi ya da istenen bölümü (numarayla, örn. `4.2`). Çok büyük bir üst bölüm yalnızca giriş metni + alt başlıklarıyla döner. |
| `create_table` | Yeni tabloyu SQL Query Manager tablo tasarımcısıyla aynı script biçimiyle kurar (`McpTableCreator`): `Id` + `TenantId` + tam audit kolonları şablondan gelir, istemci yalnızca iş kolonlarını verir; tip ve varsayılan değer beyaz listeden geçer, istemci SQL yazamaz. Script `{kapsam}/sql/object/{Tablo}.sql` dosyasına yazılır. Yalnızca oluşturur — tablo varsa reddeder; yalnızca SQL Server. `.Write` + `App.SqlQueryManager` ister. | | `create_table` | Yeni tabloyu SQL Query Manager tablo tasarımcısıyla aynı script biçimiyle kurar (`McpTableCreator`): `Id` + `TenantId` + tam audit kolonları şablondan gelir, istemci yalnızca iş kolonlarını verir; tip ve varsayılan değer beyaz listeden geçer, istemci SQL yazamaz. Script `{kapsam}/sql/object/{Tablo}.sql` dosyasına yazılır. Yalnızca oluşturur — tablo varsa reddeder; yalnızca SQL Server. `.Write` + `App.SqlQueryManager` ister. |
| `list_wizards` / `get_wizard` | Wizard File Manager dosyaları; `get_wizard` seed dosyasını diskteki biçimiyle döndürür. | | `list_wizards` / `get_wizard` | Wizard File Manager dosyaları; `get_wizard` seed dosyasını diskteki biçimiyle döndürür. |
| `deploy_wizard` | Wizard'ı Wizard ekranıyla aynı yoldan (`IListFormWizardAppService.Create`) deploy eder. Girdi seed dosyası biçimi (`{"Wizard": …}`) ya da düz cevaplardır; ikisi de `WizardSeedFileDto.FromSeedJson`'dan geçer. Wizard ekranı gibi yalnızca seed dosyalarını yazar; ekran DB Migrate ile kurulur ve çıkıştaki `isPending` o zamana kadar `true` kalır (model migrate'i beklemez; `verify_wizard` bekleyen dosyada `install` kontrolünde durur). `replaceFileName` doluysa güncellemedir (dosyalar yeniden yazılır, önceki kayıtları migrate kaldırır). İstek demo veri taşıyorsa (prompt'taki satırlar, ekran görüntüsü) wizard `SeedData: true` ile deploy edilir — ekranın `SeedFilePath`'i `data/{ListFormCode}.json` olur ve `SeedSync*` bayrakları açılır — satırlar aynı çağrıda `seedRows` ile bu dosyaya yazılır (alan adları doğrulanır, `Guid` anahtarı sunucu üretir); migrate ekranı kurduktan sonra satırları ekler (`lowcode.instructions.md` §0.10.1). | | `deploy_wizard` | Wizard'ı Wizard ekranıyla aynı yoldan (`IListFormWizardAppService.Create`) deploy eder. Girdi seed dosyası biçimi (`{"Wizard": …}`) ya da düz cevaplardır; ikisi de `WizardSeedFileDto.FromSeedJson`'dan geçer. Wizard ekranı gibi yalnızca seed dosyalarını yazar; ekran DB Migrate ile kurulur ve çıkıştaki `isPending` o zamana kadar `true` kalır (model migrate'i beklemez; `verify_wizard` bekleyen dosyada `install` kontrolünde durur). `replaceFileName` doluysa güncellemedir (dosyalar yeniden yazılır, önceki kayıtları migrate kaldırır). İstek demo veri taşıyorsa (prompt'taki satırlar, ekran görüntüsü) wizard `SeedData: true` ile deploy edilir — ekranın `SeedFilePath`'i `data/{ListFormCode}.json` olur ve `SeedSync*` bayrakları açılır — satırlar aynı çağrıda `seedRows` ile bu dosyaya yazılır (alan adları doğrulanır, `Guid` anahtarı sunucu üretir); migrate ekranı kurduktan sonra satırları ekler (`lowcode.instructions.md` §0.10.1). Widget sorgularındaki her dil anahtarı tanımlı olmalı ya da wizard'ın `Language.Texts` alanında EN + TR verilmeli, yoksa deploy adlarıyla reddeder (`ValidateWidgetLanguageKeysAsync`). `uploadUrl`'süz `dxImageUpload` alanı base64 sakladığı için kolonu sınırlı boyluysa (katalogdaki karakter sınırı) deploy reddedilir; tablo kolonuna bağlı `dxImageViewer` alanı da reddedilir — görsel kolonunun editörü her zaman `dxImageUpload`'dır (`ValidateImageColumnLengths`). `MenuOrder` yok sayılır: yeni ekran kardeşlerinin sonuna, güncelleme mevcut yerine yazılır — lookup kaynağı ekran önce deploy edildiği için onu kullanan ekranın üstünde kalır. |
| `delete_wizard` | Wizard Manager'daki silmeyle aynı: `InsertedRecords` + ekranın veri aynası, bağımlı wizard varsa reddedilir. `dropTables: true` ekranın tablosunu (yalnızca tasarımcı/`create_table` ile kurulmuş ve başka yerde kullanılmayan) CRUD endpoint'leri, `sql/object` ve `crud` dosyalarıyla düşürür; model bunu kullanıcı onayı olmadan göndermez (`WizardTableCleaner`, `lowcode.instructions.md` §4.5). | | `delete_wizard` | Wizard Manager'daki silmeyle aynı: `InsertedRecords` + ekranın veri aynası, bağımlı wizard varsa reddedilir. `dropTables: true` ekranın tablosunu (yalnızca tasarımcı/`create_table` ile kurulmuş ve başka yerde kullanılmayan) CRUD endpoint'leri, `sql/object` ve `crud` dosyalarıyla düşürür; model bunu kullanıcı onayı olmadan göndermez (`WizardTableCleaner`, `lowcode.instructions.md` §4.5). |
| `verify_wizard` | Deploy edilmiş ekranı veri yazmadan doğrular: menü kaydı ve URL'si, menünün istediği yetki tanımı (`AbpPermissions`), List yolunda ekran sorgusu (`GetSelectAsync`, tek satır, çağıranın yetkisiyle), Custom yolunda bileşen (etkin, rota = menü URL'si) ve seed kapanışı (`ExportAnalysis` uyarıları; engellemez). İsteğe bağlıdır, migrate sonrasında kullanılır; `App.DevRequests` döngüsünün adımı değildir — döngü `Testing`'de biter, kullanıcı elle test edip `Staging`'e alır (`lowcode.instructions.md` §0.11). | | `verify_wizard` | Deploy edilmiş ekranı veri yazmadan doğrular: menü kaydı ve URL'si, menünün istediği yetki tanımı (`AbpPermissions`), List yolunda ekran sorgusu (`GetSelectAsync`, tek satır, çağıranın yetkisiyle), Custom yolunda bileşen (etkin, rota = menü URL'si) ve seed kapanışı (`ExportAnalysis` uyarıları; engellemez). İsteğe bağlıdır, migrate sonrasında kullanılır; `App.DevRequests` döngüsünün adımı değildir — döngü `Testing`'de biter, kullanıcı elle test edip `Staging`'e alır (`lowcode.instructions.md` §0.11). |
| `start_dev_request` | Geliştirme talebini Id ya da tam başlıkla üstlenir: yalnızca `Ready to Start` durumundakini `In Progress` durumuna alır, diğerlerini reddeder; talebin metnini döndürür. Açıklamaya yapıştırılmış base64 görseller (`data:image/png|jpeg|gif|webp`) metinden ayıklanır (`McpDevRequestImages`), yerlerinde `[image-N]` kalır ve her biri ayrı MCP resim içeriği (`ImageContentBlock`) olarak döner — model görseli resim olarak görür, görsel hiçbir yere yüklenmez. `.Write` + `App.DevRequests.Update` ister. | | `start_dev_request` | Geliştirme talebini Id ya da tam başlıkla üstlenir: yalnızca `Ready to Start` durumundakini `In Progress` durumuna alır, diğerlerini reddeder; talebin metnini döndürür. Açıklamaya yapıştırılmış base64 görseller (`data:image/png|jpeg|gif|webp`) metinden ayıklanır (`McpDevRequestImages`), yerlerinde `[image-N]` kalır ve her biri ayrı MCP resim içeriği (`ImageContentBlock`) olarak döner — model görseli resim olarak görür, görsel hiçbir yere yüklenmez. `.Write` + `App.DevRequests.Update` ister. |
@ -1415,6 +1415,15 @@ liste süzgeci ve çağrı kapısı aynı kuralı kullanır.
| `add_dev_request_subtasks` | Talebin `SubTasks` listesine tamamlanmış satırlar ekler: model yalnızca başlığı verir, atanan kişi bağlanan kullanıcının adı (`userName`), bitiş tarihi bugün (`yyyy-MM-dd`) olarak sunucuda yazılır; var olan satırlar korunur. `.Write` + `App.DevRequests.Update` ister. | | `add_dev_request_subtasks` | Talebin `SubTasks` listesine tamamlanmış satırlar ekler: model yalnızca başlığı verir, atanan kişi bağlanan kullanıcının adı (`userName`), bitiş tarihi bugün (`yyyy-MM-dd`) olarak sunucuda yazılır; var olan satırlar korunur. `.Write` + `App.DevRequests.Update` ister. |
| `append_dev_request_note` | Talebin `Analysis` kolonunun sonuna HTML ekler (üretim sonuç notu); `Description` talebin yazıldığı hâliyle kalır. İkisi de `update_row` ile yazılmaz — açıklama base64 görsel taşır. `.Write` + `App.DevRequests.Update` ister. | | `append_dev_request_note` | Talebin `Analysis` kolonunun sonuna HTML ekler (üretim sonuç notu); `Description` talebin yazıldığı hâliyle kalır. İkisi de `update_row` ile yazılmaz — açıklama base64 görsel taşır. `.Write` + `App.DevRequests.Update` ister. |
**Timesheets.** Talebin `Timesheets` listesini sunucu doldurur (`IMcpDevRequestTimesheetRecorder`):
`get_dev_request` / `start_dev_request` talebi kullanıcının etkin talebi yapar (dağıtık önbellek,
`McpDevRequestActivityCacheItem`, 12 saat); sonraki her MCP yazma işlemi (`create_table`,
`deploy_wizard`, `delete_wizard`, `save_component`, `delete_component`, satır araçları,
`save_dev_request_analysis`) o talebe bir satır ekler. Açıklama `App.Mcp.Timesheet.*` anahtarından
Türkçe üretilir, süre önceki satırdan ya da talebin açıldığı andan ölçülür (en az 1 dakika, saat
olarak), çalışan bağlanan kullanıcı, tarih bugündür. Etkin talep yoksa satır yazılmaz; bozuk bir
`Timesheets` değeri işlemi geri almaz, yalnızca satır atlanır ve log'a düşer.
**Prompt'lar** (`McpPromptNames`, `McpDevelopPrompts`). İstemcide komut olarak görünür; yalnızca **Prompt'lar** (`McpPromptNames`, `McpDevelopPrompts`). İstemcide komut olarak görünür; yalnızca
metindir, yetki taşımaz. `analyze_dev_request` tek talebi, `analyze_backlog_requests` bütün metindir, yetki taşımaz. `analyze_dev_request` tek talebi, `analyze_backlog_requests` bütün
`Backlog` kuyruğunu analiz edip `Analysis` durumuna alır (§0.12); `process_dev_request` tek talebi, `Backlog` kuyruğunu analiz edip `Analysis` durumuna alır (§0.12); `process_dev_request` tek talebi,

View file

@ -55,6 +55,7 @@ public interface IMcpDevelopAppService : IApplicationService
/// </summary> /// </summary>
Task AddDevRequestSubTasksAsync(string request, List<string> titles); Task AddDevRequestSubTasksAsync(string request, List<string> titles);
Task<List<McpComponentSummaryDto>> GetComponentsAsync(string? search); Task<List<McpComponentSummaryDto>> GetComponentsAsync(string? search);
Task<CustomComponentDto?> GetComponentAsync(string name); Task<CustomComponentDto?> GetComponentAsync(string name);

View file

@ -21,13 +21,15 @@ namespace Sozsoft.Mcp;
public class McpDataAppService( public class McpDataAppService(
IMcpServerConfigProvider serverConfigProvider, IMcpServerConfigProvider serverConfigProvider,
IListFormDataAppService listFormDataAppService, IListFormDataAppService listFormDataAppService,
IMcpListFormAccessFilter accessFilter) IMcpListFormAccessFilter accessFilter,
IMcpDevRequestTimesheetRecorder timesheetRecorder)
: McpAppService(serverConfigProvider), IMcpDataAppService : McpAppService(serverConfigProvider), IMcpDataAppService
{ {
public async Task<McpWriteResultDto> InsertRowAsync(McpRowRequestDto input) public async Task<McpWriteResultDto> InsertRowAsync(McpRowRequestDto input)
{ {
var request = await BuildRequestAsync(input, McpToolNames.InsertRow, AuthorizationTypeEnum.Create, requireKeys: false, requireValues: true); var request = await BuildRequestAsync(input, McpToolNames.InsertRow, AuthorizationTypeEnum.Create, requireKeys: false, requireValues: true);
var key = await listFormDataAppService.PostInsertAsync(request); var key = await listFormDataAppService.PostInsertAsync(request);
await timesheetRecorder.RecordAsync(McpTimesheetTexts.InsertRow, input.ListFormCode);
return new McpWriteResultDto { Key = ((object?)key)?.ToString() }; return new McpWriteResultDto { Key = ((object?)key)?.ToString() };
} }
@ -36,6 +38,7 @@ public class McpDataAppService(
{ {
var request = await BuildRequestAsync(input, McpToolNames.UpdateRow, AuthorizationTypeEnum.Update, requireKeys: true, requireValues: true); var request = await BuildRequestAsync(input, McpToolNames.UpdateRow, AuthorizationTypeEnum.Update, requireKeys: true, requireValues: true);
var affected = await listFormDataAppService.PostUpdateAsync(request); var affected = await listFormDataAppService.PostUpdateAsync(request);
await timesheetRecorder.RecordAsync(McpTimesheetTexts.UpdateRow, input.ListFormCode, string.Join(", ", input.Values.Keys));
return new McpWriteResultDto { AffectedRows = affected }; return new McpWriteResultDto { AffectedRows = affected };
} }
@ -44,6 +47,7 @@ public class McpDataAppService(
{ {
var request = await BuildRequestAsync(input, McpToolNames.DeleteRow, AuthorizationTypeEnum.Delete, requireKeys: true, requireValues: false); var request = await BuildRequestAsync(input, McpToolNames.DeleteRow, AuthorizationTypeEnum.Delete, requireKeys: true, requireValues: false);
var affected = await listFormDataAppService.PostDeleteAsync(request); var affected = await listFormDataAppService.PostDeleteAsync(request);
await timesheetRecorder.RecordAsync(McpTimesheetTexts.DeleteRow, input.ListFormCode);
return new McpWriteResultDto { AffectedRows = affected }; return new McpWriteResultDto { AffectedRows = affected };
} }

View file

@ -0,0 +1,13 @@
using System;
namespace Sozsoft.Mcp;
/// <summary>
/// Kullanicinin MCP ile uzerinde calistigi talep ve son kaydedilen etkinlik ani; bir sonraki
/// timesheet satirinin suresi bu andan olculur.
/// </summary>
public class McpDevRequestActivityCacheItem
{
public Guid RequestId { get; set; }
public DateTime At { get; set; }
}

View file

@ -0,0 +1,115 @@
using System;
using System.Globalization;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Localization;
using Microsoft.Extensions.Logging;
using Sozsoft.Platform.Entities;
using Sozsoft.Platform.Localization;
using Volo.Abp.Caching;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Domain.Repositories;
using Volo.Abp.Guids;
using Volo.Abp.Localization;
using Volo.Abp.Timing;
using Volo.Abp.Users;
namespace Sozsoft.Mcp;
/// <summary>
/// Gelistirme talebinin Timesheets listesini MCP islemlerinden doldurur. Talep
/// <c>get_dev_request</c> / <c>start_dev_request</c> ile acildiginda kullanicinin etkin talebi
/// olur; ardindan yapilan her yazma islemi o talebe bir satir ekler. Satir modelin dikkatine
/// birakilmaz: aciklama dil anahtarindan Turkce uretilir, sure son etkinlikten bu yana olculur.
/// </summary>
public interface IMcpDevRequestTimesheetRecorder
{
/// <summary>Talebi kullanicinin etkin talebi yapar ve olcumu baslatir.</summary>
Task BeginAsync(Guid devRequestId);
/// <summary>
/// Etkin talebe bir satir yazar; etkin talep yoksa (islem bir talep dongusunun parcasi
/// degilse) hicbir sey yapmaz.
/// </summary>
Task RecordAsync(string descriptionKey, params object[] args);
}
public class McpDevRequestTimesheetRecorder(
IDistributedCache<McpDevRequestActivityCacheItem> activityCache,
IRepository<DevRequest, Guid> devRequestRepository,
IStringLocalizer<PlatformResource> localizer,
ICurrentUser currentUser,
IClock clock,
IGuidGenerator guidGenerator,
ILogger<McpDevRequestTimesheetRecorder> logger) : IMcpDevRequestTimesheetRecorder, ITransientDependency
{
/// <summary>Timesheet satirlarinin dili; talep ekrani Turkce kullanilir.</summary>
private const string TimesheetCulture = "tr";
/// <summary>Talep ekranindaki tarih kutusunun okudugu bicim.</summary>
private const string DateFormat = "yyyy-MM-dd";
/// <summary>
/// Etkin talebin omru bir calisma oturumunu kapsar; suresi dolan kayit, gunler once acilmis
/// bir talebe baska bir isin yazilmasini ve sahte uzun sureyi engeller.
/// </summary>
private static readonly TimeSpan ActivityLifetime = TimeSpan.FromHours(12);
public Task BeginAsync(Guid devRequestId) => StampAsync(devRequestId, clock.Now);
public async Task RecordAsync(string descriptionKey, params object[] args)
{
var activity = await activityCache.GetAsync(ActivityKey());
if (activity is null)
return;
var devRequest = await devRequestRepository.FindAsync(activity.RequestId);
if (devRequest is null)
return;
JsonArray timesheets;
try
{
timesheets = string.IsNullOrWhiteSpace(devRequest.Timesheets)
? []
: JsonNode.Parse(devRequest.Timesheets) as JsonArray ?? throw new JsonException();
}
catch (JsonException)
{
// Yapilan is (deploy, tablo) zaten tamamlandi; bozuk bir timesheet onu geri almamali.
logger.LogWarning("Timesheets of development request {RequestId} could not be read; entry skipped.", devRequest.Id);
return;
}
string description;
using (CultureHelper.Use(TimesheetCulture))
{
description = localizer[descriptionKey, args];
}
var now = clock.Now;
var minutes = Math.Max(1, (int)Math.Round((now - activity.At).TotalMinutes));
timesheets.Add(new JsonObject
{
["id"] = guidGenerator.Create().ToString(),
["date"] = now.ToString(DateFormat, CultureInfo.InvariantCulture),
["userName"] = currentUser.UserName ?? string.Empty,
["description"] = description,
// Ekranin sure kutusu saati ondalik sayi olarak okur.
["duration"] = Math.Round(minutes / 60d, 4)
});
devRequest.Timesheets = timesheets.ToJsonString();
await devRequestRepository.UpdateAsync(devRequest, autoSave: true);
await StampAsync(devRequest.Id, now);
}
private string ActivityKey() => currentUser.Id?.ToString() ?? string.Empty;
private Task StampAsync(Guid devRequestId, DateTime at) =>
activityCache.SetAsync(ActivityKey(), new McpDevRequestActivityCacheItem { RequestId = devRequestId, At = at },
new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = ActivityLifetime });
}

View file

@ -42,6 +42,7 @@ public class McpDevelopAppService(
IRepository<Menu, Guid> menuRepository, IRepository<Menu, Guid> menuRepository,
IRepository<PermissionDefinitionRecord, Guid> permissionRepository, IRepository<PermissionDefinitionRecord, Guid> permissionRepository,
IRepository<DevRequest, Guid> devRequestRepository, IRepository<DevRequest, Guid> devRequestRepository,
IMcpDevRequestTimesheetRecorder timesheetRecorder,
IConfiguration configuration) IConfiguration configuration)
: McpAppService(serverConfigProvider), IMcpDevelopAppService : McpAppService(serverConfigProvider), IMcpDevelopAppService
{ {
@ -121,6 +122,8 @@ public class McpDevelopAppService(
input.FullAudited, input.FullAudited,
input.MultiTenant)); input.MultiTenant));
await timesheetRecorder.RecordAsync(McpTimesheetTexts.CreateTable, created.TableName);
return new McpCreateTableResultDto return new McpCreateTableResultDto
{ {
TableName = created.TableName, TableName = created.TableName,
@ -165,6 +168,13 @@ public class McpDevelopAppService(
var wizard = ParseWizard(input.Wizard); var wizard = ParseWizard(input.Wizard);
wizard.EditFileName = string.IsNullOrWhiteSpace(input.ReplaceFileName) ? null : input.ReplaceFileName.Trim(); wizard.EditFileName = string.IsNullOrWhiteSpace(input.ReplaceFileName) ? null : input.ReplaceFileName.Trim();
// Menu sirasi modelin karari degildir: yeni ekran kardeslerinin sonuna eklenir (deploy
// bagimlilik sirasiyla yapildigi icin lookup kaynagi tuketen ekranin ustunde kalir),
// guncelleme ekranin mevcut yerini korur.
wizard.MenuOrder = wizard.EditFileName is null
? 0
: (await wizardAppService.GetFile(wizard.EditFileName)).Wizard?.MenuOrder ?? 0;
// Satirlar deploy'dan once dogrulanir; gecersiz girdi yarim bir deploy birakmaz. // Satirlar deploy'dan once dogrulanir; gecersiz girdi yarim bir deploy birakmaz.
var seedRows = ParseSeedRows(input.SeedRows, wizard); var seedRows = ParseSeedRows(input.SeedRows, wizard);
@ -176,6 +186,8 @@ public class McpDevelopAppService(
if (file != null && seedRows.Count > 0) if (file != null && seedRows.Count > 0)
await WriteSeedRowsAsync(file.FileName, seedRows); await WriteSeedRowsAsync(file.FileName, seedRows);
await timesheetRecorder.RecordAsync(McpTimesheetTexts.DeployWizard, file?.FileName ?? wizard.WizardName, seedRows.Count);
return file; return file;
} }
@ -277,7 +289,10 @@ public class McpDevelopAppService(
input.DropTables = [.. analysis.Tables.Where(t => t.CanDrop).Select(t => t.TableName)]; input.DropTables = [.. analysis.Tables.Where(t => t.CanDrop).Select(t => t.TableName)];
} }
return await wizardAppService.DeleteFile(input); var result = await wizardAppService.DeleteFile(input);
await timesheetRecorder.RecordAsync(McpTimesheetTexts.DeleteWizard, fileName);
return result;
} }
/// <summary> /// <summary>
@ -370,6 +385,7 @@ public class McpDevelopAppService(
devRequest.Status = McpConsts.DevRequestStatuses.InProgress; devRequest.Status = McpConsts.DevRequestStatuses.InProgress;
await devRequestRepository.UpdateAsync(devRequest, autoSave: true); await devRequestRepository.UpdateAsync(devRequest, autoSave: true);
await timesheetRecorder.BeginAsync(devRequest.Id);
return ToDto(devRequest); return ToDto(devRequest);
} }
@ -379,7 +395,10 @@ public class McpDevelopAppService(
await EnsureToolAllowedAsync(McpToolNames.GetDevRequest); await EnsureToolAllowedAsync(McpToolNames.GetDevRequest);
await AuthorizationService.CheckAsync(PlatformConsts.AppCodes.DevRequests); await AuthorizationService.CheckAsync(PlatformConsts.AppCodes.DevRequests);
return ToDto(await FindDevRequestAsync(request)); var devRequest = await FindDevRequestAsync(request);
await timesheetRecorder.BeginAsync(devRequest.Id);
return ToDto(devRequest);
} }
/// <summary> /// <summary>
@ -407,6 +426,7 @@ public class McpDevelopAppService(
devRequest.Analysis = html.Trim(); devRequest.Analysis = html.Trim();
devRequest.Status = McpConsts.DevRequestStatuses.Analysis; devRequest.Status = McpConsts.DevRequestStatuses.Analysis;
await devRequestRepository.UpdateAsync(devRequest, autoSave: true); await devRequestRepository.UpdateAsync(devRequest, autoSave: true);
await timesheetRecorder.RecordAsync(McpTimesheetTexts.Analysis);
return ToDto(devRequest); return ToDto(devRequest);
} }
@ -680,7 +700,7 @@ public class McpDevelopAppService(
throw new UserFriendlyException(L[McpErrorCodes.ComponentCodeRequired]); throw new UserFriendlyException(L[McpErrorCodes.ComponentCodeRequired]);
} }
return await componentAppService.CreateAsync(new CreateUpdateCustomComponentDto var created = await componentAppService.CreateAsync(new CreateUpdateCustomComponentDto
{ {
Name = input.Name.Trim(), Name = input.Name.Trim(),
Code = input.Code, Code = input.Code,
@ -690,9 +710,12 @@ public class McpDevelopAppService(
Dependencies = input.Dependencies, Dependencies = input.Dependencies,
IsActive = input.IsActive ?? true IsActive = input.IsActive ?? true
}); });
await timesheetRecorder.RecordAsync(McpTimesheetTexts.SaveComponent, created.Name);
return created;
} }
return await componentAppService.UpdateAsync(existing.Id, new CreateUpdateCustomComponentDto var updated = await componentAppService.UpdateAsync(existing.Id, new CreateUpdateCustomComponentDto
{ {
Name = existing.Name, Name = existing.Name,
Code = string.IsNullOrWhiteSpace(input.Code) ? existing.Code : input.Code, Code = string.IsNullOrWhiteSpace(input.Code) ? existing.Code : input.Code,
@ -702,6 +725,9 @@ public class McpDevelopAppService(
Dependencies = input.Dependencies ?? existing.Dependencies, Dependencies = input.Dependencies ?? existing.Dependencies,
IsActive = input.IsActive ?? existing.IsActive IsActive = input.IsActive ?? existing.IsActive
}); });
await timesheetRecorder.RecordAsync(McpTimesheetTexts.SaveComponent, updated.Name);
return updated;
} }
public async Task DeleteComponentAsync(string name) public async Task DeleteComponentAsync(string name)
@ -712,6 +738,7 @@ public class McpDevelopAppService(
?? throw new UserFriendlyException(L[McpErrorCodes.ComponentNotFound, name]); ?? throw new UserFriendlyException(L[McpErrorCodes.ComponentNotFound, name]);
await componentAppService.DeleteAsync(component.Id); await componentAppService.DeleteAsync(component.Id);
await timesheetRecorder.RecordAsync(McpTimesheetTexts.DeleteComponent, component.Name);
} }
/// <summary> /// <summary>

View file

@ -0,0 +1,20 @@
namespace Sozsoft.Mcp;
/// <summary>
/// MCP islemlerinin talep Timesheets satirina yazilan aciklamalari. Karsiliklari platformun
/// <c>App.Languages.LanguageText.json</c> dosyasinda EN + TR olarak durur; satir Turkce yazilir.
/// </summary>
public static class McpTimesheetTexts
{
private const string Prefix = "App.Mcp.Timesheet.";
public const string Analysis = Prefix + "Analysis";
public const string CreateTable = Prefix + "CreateTable";
public const string DeployWizard = Prefix + "DeployWizard";
public const string DeleteWizard = Prefix + "DeleteWizard";
public const string SaveComponent = Prefix + "SaveComponent";
public const string DeleteComponent = Prefix + "DeleteComponent";
public const string InsertRow = Prefix + "InsertRow";
public const string UpdateRow = Prefix + "UpdateRow";
public const string DeleteRow = Prefix + "DeleteRow";
}

View file

@ -64,7 +64,7 @@ public sealed class McpDevelopTools(IMcpDevelopAppService developAppService)
=> await developAppService.GetWizardAsync(fileName); => await developAppService.GetWizardAsync(fileName);
[McpServerTool(Name = McpToolNames.DeployWizard, Destructive = true, Idempotent = false)] [McpServerTool(Name = McpToolNames.DeployWizard, Destructive = true, Idempotent = false)]
[Description("Wizard'i deploy eder: ListForm ekrani (ComponentKind 0) ya da bir custom component'i menuye baglayan kayit (ComponentKind 1), yetkileri, dil anahtarlari ve menusu seed dosyalarina yazilir. Deploy veritabanina yazmaz: ekran, menu ve yetkiler kullanici Wizard Manager'da DB Migrate calistirinca olusur; cikistaki isPending true iken ekran henuz yoktur; migrate'i bekleme, isini bitir ve kullaniciya yalnizca DB Migrate calistirmasini soyle. Eksik modul, ust menu, izin grubu, yetki ve dil satirlari da deploy'da dosyalara yazilir ve migrate onlari dogru sirayla ekler; bunlarin veritabaninda olmamasi beklenen durumdur, uyari olarak raporlama ve kullanicidan elle eklemesini isteme. Liste ekraninda Grid, Card, Pivot ve Chart gorunumleri varsayilan aciktir (DefaultLayout grid); kullanici bir gorunumu istemedigini soylemedikce bu bayraklari false yazma. Hedef tablo onceden var olmalidir; yoksa once create_table ile olustur. Istek ornek/demo veri tasiyorsa (prompt'taki tablo/liste ya da ekran goruntusunde gorunen satirlar) SeedData true yaz ve satirlarin hepsini ayni cagrida seedRows ile gonder: satirlar ekranin data/{ListFormCode}.json dosyasina yazilir, migrate ekrani kurduktan sonra ekler. Satirlar icin migrate'i bekleyip insert_row kullanma. replaceFileName verilirse o wizard'in dosyalari yeniden yazilir, onceki kayitlarin yerini migrate'te yenileri alir; ekran wizard cevaplarindan yeniden turetildigi icin tasarimcida yapilmis ve wizard'da karsiligi olmayan ayarlar kaybolur. Ne yazacagini get_dev_guide 4. bolumunden ogren.")] [Description("Wizard'i deploy eder: ListForm ekrani (ComponentKind 0) ya da bir custom component'i menuye baglayan kayit (ComponentKind 1), yetkileri, dil anahtarlari ve menusu seed dosyalarina yazilir. Deploy veritabanina yazmaz: ekran, menu ve yetkiler kullanici Wizard Manager'da DB Migrate calistirinca olusur; cikistaki isPending true iken ekran henuz yoktur; migrate'i bekleme, isini bitir ve kullaniciya yalnizca DB Migrate calistirmasini soyle. Eksik modul, ust menu, izin grubu, yetki ve dil satirlari da deploy'da dosyalara yazilir ve migrate onlari dogru sirayla ekler; bunlarin veritabaninda olmamasi beklenen durumdur, uyari olarak raporlama ve kullanicidan elle eklemesini isteme. Ust menu zinciri istenen derinlikte kurulur: MenuParentCode ekranin dogrudan ust menusu, Menu.MenuParentPath onun ustundeki seviyeler (kokten asagi, her biri {Code, Icon, En, Tr}); eksik her seviye bir ustunun altina kurulur, var olana baglanilir. Liste ekraninda Grid, Card, Pivot ve Chart gorunumleri varsayilan aciktir (DefaultLayout grid); kullanici bir gorunumu istemedigini soylemedikce bu bayraklari false yazma. Hedef tablo onceden var olmalidir; yoksa once create_table ile olustur. Istek ornek/demo veri tasiyorsa (prompt'taki tablo/liste ya da ekran goruntusunde gorunen satirlar) SeedData true yaz ve satirlarin hepsini ayni cagrida seedRows ile gonder: satirlar ekranin data/{ListFormCode}.json dosyasina yazilir, migrate ekrani kurduktan sonra ekler. Satirlar icin migrate'i bekleyip insert_row kullanma. replaceFileName verilirse o wizard'in dosyalari yeniden yazilir, onceki kayitlarin yerini migrate'te yenileri alir; ekran wizard cevaplarindan yeniden turetildigi icin tasarimcida yapilmis ve wizard'da karsiligi olmayan ayarlar kaybolur. Ne yazacagini get_dev_guide 4. bolumunden ogren.")]
public async Task<WizardFileInfoDto?> DeployWizardAsync( public async Task<WizardFileInfoDto?> DeployWizardAsync(
[Description("Wizard JSON'u: seed dosyasi bicimi ({\"Wizard\": {...}}, get_wizard cikisi gibi) ya da duz wizard cevaplari. Enum alanlari sayi olarak yazilir.")] [Description("Wizard JSON'u: seed dosyasi bicimi ({\"Wizard\": {...}}, get_wizard cikisi gibi) ya da duz wizard cevaplari. Enum alanlari sayi olarak yazilir.")]
string wizard, string wizard,
@ -137,6 +137,7 @@ public sealed class McpDevelopTools(IMcpDevelopAppService developAppService)
return "ok"; return "ok";
} }
/// <summary> /// <summary>
/// Talep metni JSON olarak, gorseller ayri resim icerigi olarak doner; model resmi ancak bu /// Talep metni JSON olarak, gorseller ayri resim icerigi olarak doner; model resmi ancak bu
/// bicimde gorur, base64 metni olarak goremez. /// bicimde gorur, base64 metni olarak goremez.

View file

@ -105,6 +105,13 @@ public class ListFormWizardDto
public string MenuParentEn { get; set; } public string MenuParentEn { get; set; }
public string MenuParentTr { get; set; } public string MenuParentTr { get; set; }
/// <summary>
/// Ekranin kullandigi ve baska bir cevaptan uretilmeyen dil anahtarlari (widget basliklari
/// gibi). Deploy, widget sorgularinda gecen her anahtarin var oldugunu ya da burada
/// tanimlandigini dogrular; tanimsiz anahtar ekranda ham kod olarak gorunurdu.
/// </summary>
public List<WizardLanguageTextDto> LanguageTexts { get; set; } = [];
public string PermissionGroupName { get; set; } public string PermissionGroupName { get; set; }
/// <summary> /// <summary>
@ -120,6 +127,13 @@ public class ListFormWizardDto
public string MenuParentModuleId { get; set; } public string MenuParentModuleId { get; set; }
public string MenuParentCode { get; set; } public string MenuParentCode { get; set; }
public string MenuParentIcon { get; set; } public string MenuParentIcon { get; set; }
/// <summary>
/// <see cref="MenuParentCode"/>'un ustundeki menu seviyeleri, kokten asagi. Bos ise ust menu
/// kok seviyededir (eski davranis); doluysa derinlik sinirsizdir ve eksik her seviye bir
/// ustunun altina kurulur. Seviyenin iki dilli adi seviyeyle birlikte tasinir.
/// </summary>
public List<WizardMenuLevelDto> MenuParentPath { get; set; } = [];
public string MenuIcon { get; set; } public string MenuIcon { get; set; }
public string DataSourceCode { get; set; } public string DataSourceCode { get; set; }
public string DataSourceConnectionString { get; set; } public string DataSourceConnectionString { get; set; }
@ -147,6 +161,31 @@ public class ListFormWizardDto
: MenuCode.Trim(); : MenuCode.Trim();
} }
/// <summary>
/// Ekranin ust menu zinciri, kokten dogrudan ust menuye; kodu bos seviyeler atlanir. Deploy
/// zinciri bu sirayla kurar, dogrulama ayni listeyi kullanir.
/// </summary>
public List<WizardMenuLevelDto> ResolveMenuParentChain()
{
var chain = (MenuParentPath ?? [])
.Where(l => !string.IsNullOrWhiteSpace(l?.Code))
.Select(l => new WizardMenuLevelDto { Code = l.Code.Trim(), Icon = l.Icon, En = l.En?.Trim(), Tr = l.Tr?.Trim() })
.ToList();
if (!string.IsNullOrWhiteSpace(MenuParentCode))
{
chain.Add(new WizardMenuLevelDto
{
Code = MenuParentCode.Trim(),
Icon = MenuParentIcon,
En = MenuParentEn?.Trim(),
Tr = MenuParentTr?.Trim()
});
}
return chain;
}
/// <summary>Wizard'in ListForm kodu; bos gelirse menu kodu ile aynidir.</summary> /// <summary>Wizard'in ListForm kodu; bos gelirse menu kodu ile aynidir.</summary>
public string ResolveListFormCode() public string ResolveListFormCode()
{ {

View file

@ -0,0 +1,12 @@
namespace Sozsoft.Platform.ListForms;
/// <summary>
/// Ekranin wizard cevaplarindan turemeyen bir dil anahtari (widget basligi, sabit secenek...).
/// Anahtar yoksa deploy iki dilli satiri yazar ve wizard'in sahipligine alir.
/// </summary>
public class WizardLanguageTextDto
{
public string Key { get; set; }
public string En { get; set; }
public string Tr { get; set; }
}

View file

@ -0,0 +1,13 @@
namespace Sozsoft.Platform.ListForms;
/// <summary>
/// Ekranin ust menu zincirindeki bir seviye. Kod menu kaydinin ve dil anahtarinin kendisidir;
/// seviye yoksa deploy onu bu ad ve ikonla kurar, varsa yalnizca koduyla baglanir.
/// </summary>
public class WizardMenuLevelDto
{
public string Code { get; set; }
public string Icon { get; set; }
public string En { get; set; }
public string Tr { get; set; }
}

View file

@ -107,6 +107,7 @@ public class WizardSeedFileDto
/// <summary>Dosyada `Menu` basligi altinda toplanan wizard alanlari; adlari degismez.</summary> /// <summary>Dosyada `Menu` basligi altinda toplanan wizard alanlari; adlari degismez.</summary>
private static readonly (string Property, string SeedKey)[] MenuKeys = private static readonly (string Property, string SeedKey)[] MenuKeys =
[ [
Same(nameof(ListFormWizardDto.MenuParentPath)),
Same(nameof(ListFormWizardDto.MenuParentCode)), Same(nameof(ListFormWizardDto.MenuParentIcon)), Same(nameof(ListFormWizardDto.MenuParentCode)), Same(nameof(ListFormWizardDto.MenuParentIcon)),
Same(nameof(ListFormWizardDto.CreateMenu)), Same(nameof(ListFormWizardDto.MenuCode)), Same(nameof(ListFormWizardDto.CreateMenu)), Same(nameof(ListFormWizardDto.MenuCode)),
Same(nameof(ListFormWizardDto.MenuOrder)), Same(nameof(ListFormWizardDto.MenuUrl)), Same(nameof(ListFormWizardDto.MenuOrder)), Same(nameof(ListFormWizardDto.MenuUrl)),
@ -153,6 +154,7 @@ public class WizardSeedFileDto
Same(nameof(ListFormWizardDto.MenuEn)), Same(nameof(ListFormWizardDto.MenuTr)), Same(nameof(ListFormWizardDto.MenuEn)), Same(nameof(ListFormWizardDto.MenuTr)),
Same(nameof(ListFormWizardDto.TitleEn)), Same(nameof(ListFormWizardDto.TitleTr)), Same(nameof(ListFormWizardDto.TitleEn)), Same(nameof(ListFormWizardDto.TitleTr)),
Same(nameof(ListFormWizardDto.DescEn)), Same(nameof(ListFormWizardDto.DescTr)), Same(nameof(ListFormWizardDto.DescEn)), Same(nameof(ListFormWizardDto.DescTr)),
(nameof(ListFormWizardDto.LanguageTexts), "Texts"),
]; ];
/// <summary> /// <summary>
@ -233,6 +235,14 @@ public class WizardSeedFileDto
if (Wizard?.CreateMenu != false) if (Wizard?.CreateMenu != false)
wizardNode.Remove(nameof(ListFormWizardDto.CreateMenu)); wizardNode.Remove(nameof(ListFormWizardDto.CreateMenu));
// Kok seviyedeki ust menu eski ve en yaygin durumdur; bos zincir dosyaya yazilmaz,
// mevcut wizard dosyalari degismeden kalir.
if (Wizard?.MenuParentPath is not { Count: > 0 })
wizardNode.Remove(nameof(ListFormWizardDto.MenuParentPath));
if (Wizard?.LanguageTexts is not { Count: > 0 })
wizardNode.Remove(nameof(ListFormWizardDto.LanguageTexts));
foreach (var (property, seedKey) in RenamedKeys) foreach (var (property, seedKey) in RenamedKeys)
Rename(wizardNode, property, seedKey); Rename(wizardNode, property, seedKey);

View file

@ -55,12 +55,17 @@ public static class WizardSeedRowKeys
var languageKeys = new List<string> { code, group }; var languageKeys = new List<string> { code, group };
var modules = new List<string>(); var modules = new List<string>();
if (wizard.CreateMenu && !string.IsNullOrWhiteSpace(wizard.MenuParentCode)) if (wizard.CreateMenu)
{ {
menus.Add(wizard.MenuParentCode.Trim()); var parents = wizard.ResolveMenuParentChain().Select(l => l.Code).ToList();
languageKeys.Add(wizard.MenuParentCode.Trim()); menus.AddRange(parents);
languageKeys.AddRange(parents);
} }
languageKeys.AddRange((wizard.LanguageTexts ?? [])
.Select(t => t.Key?.Trim())
.Where(k => !string.IsNullOrWhiteSpace(k))!);
if (!string.IsNullOrWhiteSpace(wizard.MenuParentModuleId)) if (!string.IsNullOrWhiteSpace(wizard.MenuParentModuleId))
modules.Add(wizard.MenuParentModuleId.Trim()); modules.Add(wizard.MenuParentModuleId.Trim());

View file

@ -5,6 +5,7 @@ using System.IO.Compression;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
@ -122,7 +123,8 @@ public class ListFormWizardAppService(
// Ad ve kod catismalari, dosyalar yazilmaya baslamadan once reddedilir; aksi halde yarim // Ad ve kod catismalari, dosyalar yazilmaya baslamadan once reddedilir; aksi halde yarim
// kalan bir deploy mevcut menu/permission satirlarini bozabilir. // kalan bir deploy mevcut menu/permission satirlarini bozabilir.
ValidateWizardName(wizardName); ValidateWizardName(wizardName);
await ValidateWizardUniquenessAsync(wizardName, code, input.MenuParentCode, editFileName, previous, rows); await ValidateWizardUniquenessAsync(wizardName, code, input.ResolveMenuParentChain(), editFileName, previous, rows);
await ValidateWidgetLanguageKeysAsync(input, rows);
await DeployWizardAsync(input, previous, editFileName, rows); await DeployWizardAsync(input, previous, editFileName, rows);
} }
@ -162,6 +164,9 @@ public class ListFormWizardAppService(
await WriteLanguageRowsAsync(rows, $"{listFormCode}.Desc", input.DescEn, input.DescTr, owned, inserted); await WriteLanguageRowsAsync(rows, $"{listFormCode}.Desc", input.DescEn, input.DescTr, owned, inserted);
} }
foreach (var text in input.LanguageTexts ?? [])
await WriteLanguageRowsAsync(rows, text.Key?.Trim(), text.En, text.Tr, owned, inserted);
//Permission Group //Permission Group
var groupName = WizardSeedRowKeys.PermissionGroupName(input); var groupName = WizardSeedRowKeys.PermissionGroupName(input);
input.PermissionGroupName = groupName; input.PermissionGroupName = groupName;
@ -204,9 +209,10 @@ public class ListFormWizardAppService(
} }
// Ekranin sozlesmesi tek fabrikadan uretilir; migrate ekrani bu dosyadan kurar. // Ekranin sozlesmesi tek fabrikadan uretilir; migrate ekrani bu dosyadan kurar.
var tableColumns = await GetTableColumnNamesAsync(input.DataSourceCode, input.SelectCommandType, input.SelectCommand); var tableColumns = await GetTableColumnsAsync(input.DataSourceCode, input.SelectCommandType, input.SelectCommand);
isDeleted = tableColumns.Contains("IsDeleted"); isDeleted = tableColumns.ContainsKey("IsDeleted");
isCreated = tableColumns.Contains("CreatorId"); isCreated = tableColumns.ContainsKey("CreatorId");
ValidateImageColumnLengths(input, tableColumns);
listFormDto = input.BuildListForm(isDeleted, isCreated); listFormDto = input.BuildListForm(isDeleted, isCreated);
fieldDtos = input.BuildFields(); fieldDtos = input.BuildFields();
@ -445,10 +451,11 @@ public class ListFormWizardAppService(
} }
/// <summary> /// <summary>
/// Modul, ust menu ve ekranin menusu. Menu agaci veritabani ile dosyanin birlesimidir: migrate /// Modul, ust menu zinciri ve ekranin menusu. Menu agaci veritabani ile dosyanin birlesimidir:
/// edilmemis bir wizard'in menusu yalnizca dosyada durur ama sirada yer tutar. Ust menu yoksa kok /// migrate edilmemis bir wizard'in menusu yalnizca dosyada durur ama sirada yer tutar. Zincir
/// menulerin sonuna eklenir; ekranin menusu verilen sirayla, sira verilmediyse kardeslerinin /// (<see cref="ListFormWizardDto.MenuParentPath"/> + <see cref="ListFormWizardDto.MenuParentCode"/>)
/// sonuna yazilir. /// kokten asagi yurunur: var olan seviye kullanilir, eksik seviye bir ustunun altina, kardeslerinin
/// sonuna eklenir. Ekranin menusu verilen sirayla, sira verilmediyse kardeslerinin sonuna yazilir.
/// </summary> /// </summary>
private async Task WriteMenuRowsAsync( private async Task WriteMenuRowsAsync(
WizardSeedRowStore rows, WizardSeedRowStore rows,
@ -469,49 +476,46 @@ public class ListFormWizardAppService(
} }
var dbMenus = await repoMenu.GetListAsync(); var dbMenus = await repoMenu.GetListAsync();
var fileMenus = rows.Rows(WizardSeedRowStore.MenuFile);
var parentCode = input.MenuParentCode?.Trim(); // Kardeslerin en buyuk sirasi; ParentCode bos ise kok menuler. Dosya satirlari her
var parentOwned = owned.MenuCodes.Contains(parentCode ?? string.Empty, StringComparer.OrdinalIgnoreCase); // cagrida yeniden okunur, zincirde az once eklenen seviye de sirada yer tutar.
var parentRow = rows.Find(WizardSeedRowStore.MenuFile, nameof(Menu.Code), parentCode); int MaxSiblingOrder(string? parent, string exclude) =>
var dbParent = dbMenus.FirstOrDefault(m => Same(m.Code, parentCode)); dbMenus.Where(m => Same(m.ParentCode ?? string.Empty, parent ?? string.Empty) && !Same(m.Code, exclude)).Select(m => m.Order)
.Concat(rows.Rows(WizardSeedRowStore.MenuFile)
string? parentModuleId; .Where(r => Same(WizardSeedRowStore.Text(r, nameof(Menu.ParentCode)) ?? string.Empty, parent ?? string.Empty) &&
if (parentOwned || (parentRow == null && dbParent == null)) !Same(WizardSeedRowStore.Text(r, nameof(Menu.Code)), exclude))
{
var maxRootOrder = dbMenus.Where(m => string.IsNullOrEmpty(m.ParentCode)).Select(m => m.Order)
.Concat(fileMenus.Where(r => string.IsNullOrEmpty(WizardSeedRowStore.Text(r, nameof(Menu.ParentCode))))
.Select(r => WizardSeedRowStore.Int(r, nameof(Menu.Order)))) .Select(r => WizardSeedRowStore.Int(r, nameof(Menu.Order))))
.DefaultIfEmpty(0).Max(); .DefaultIfEmpty(0).Max();
var menuParentIcon = !string.IsNullOrWhiteSpace(input.MenuParentIcon) var fallbackIcon = !string.IsNullOrWhiteSpace(input.MenuIcon) ? input.MenuIcon : WizardConsts.MenuIcon;
? input.MenuParentIcon string? parentCode = null;
: !string.IsNullOrWhiteSpace(input.MenuIcon) var parentModuleId = string.IsNullOrWhiteSpace(moduleId) ? null : moduleId;
? input.MenuIcon
: WizardConsts.MenuIcon;
parentModuleId = string.IsNullOrWhiteSpace(moduleId) ? null : moduleId; foreach (var level in input.ResolveMenuParentChain())
{
var levelOwned = owned.MenuCodes.Contains(level.Code, StringComparer.OrdinalIgnoreCase);
var levelRow = rows.Find(WizardSeedRowStore.MenuFile, nameof(Menu.Code), level.Code);
var dbLevel = dbMenus.FirstOrDefault(m => Same(m.Code, level.Code));
if (levelOwned || (levelRow == null && dbLevel == null))
{
rows.Upsert(WizardSeedRowStore.MenuFile, MenuRow( rows.Upsert(WizardSeedRowStore.MenuFile, MenuRow(
parentCode!, null, parentCode!, level.Code, parentCode, level.Code,
parentRow != null ? WizardSeedRowStore.Int(parentRow, nameof(Menu.Order)) : maxRootOrder + 1, levelRow != null ? WizardSeedRowStore.Int(levelRow, nameof(Menu.Order)) : MaxSiblingOrder(parentCode, level.Code) + 1,
null, menuParentIcon, null, parentModuleId), nameof(Menu.Code)); null, string.IsNullOrWhiteSpace(level.Icon) ? fallbackIcon : level.Icon, null, parentModuleId), nameof(Menu.Code));
inserted.MenuCodes.Add(parentCode!); inserted.MenuCodes.Add(level.Code);
await WriteLanguageRowsAsync(rows, parentCode, input.MenuParentEn, input.MenuParentTr, owned, inserted); await WriteLanguageRowsAsync(rows, level.Code, level.En, level.Tr, owned, inserted);
} }
else else
{ {
parentModuleId = dbParent?.ModuleId ?? WizardSeedRowStore.Text(parentRow, nameof(Menu.ModuleId)); parentModuleId = dbLevel?.ModuleId ?? WizardSeedRowStore.Text(levelRow, nameof(Menu.ModuleId));
} }
var maxChildOrder = dbMenus.Where(m => Same(m.ParentCode, parentCode) && !Same(m.Code, code)).Select(m => m.Order) parentCode = level.Code;
.Concat(fileMenus }
.Where(r => Same(WizardSeedRowStore.Text(r, nameof(Menu.ParentCode)), parentCode) &&
!Same(WizardSeedRowStore.Text(r, nameof(Menu.Code)), code))
.Select(r => WizardSeedRowStore.Int(r, nameof(Menu.Order))))
.DefaultIfEmpty(0).Max();
input.MenuOrder = input.MenuOrder > 0 ? input.MenuOrder : maxChildOrder + 1; input.MenuOrder = input.MenuOrder > 0 ? input.MenuOrder : MaxSiblingOrder(parentCode, code) + 1;
rows.Upsert(WizardSeedRowStore.MenuFile, MenuRow( rows.Upsert(WizardSeedRowStore.MenuFile, MenuRow(
code, parentCode, code, input.MenuOrder, input.MenuUrl, input.MenuIcon ?? WizardConsts.MenuIcon, code, parentCode, code, input.MenuOrder, input.MenuUrl, input.MenuIcon ?? WizardConsts.MenuIcon,
@ -750,7 +754,7 @@ public class ListFormWizardAppService(
if (!menus.TryGetValue(wizard.ResolveMenuCode(), out var menu)) if (!menus.TryGetValue(wizard.ResolveMenuCode(), out var menu))
{ {
return string.IsNullOrWhiteSpace(wizard.MenuParentCode) ? [] : [wizard.MenuParentCode.Trim()]; return [.. wizard.ResolveMenuParentChain().Select(l => l.Code)];
} }
var path = new List<string>(); var path = new List<string>();
@ -1088,6 +1092,49 @@ public class ListFormWizardAppService(
/// </summary> /// </summary>
private static readonly Regex WizardNamePattern = new(@"^[A-Za-z][A-Za-z0-9_]*$", RegexOptions.Compiled); private static readonly Regex WizardNamePattern = new(@"^[A-Za-z][A-Za-z0-9_]*$", RegexOptions.Compiled);
/// <summary>Widget sorgusundaki tek tirnakli, noktali tanimlayici: kartin dil anahtari.</summary>
private static readonly Regex WidgetLanguageKeyPattern = new(@"'([A-Za-z][A-Za-z0-9_]*(?:\.[A-Za-z0-9_]+)+)'", RegexOptions.Compiled);
/// <summary>
/// Widget kartinin basligi ve alt basligi sorgudan dil anahtari olarak doner; anahtar tanimli
/// degilse ekranda ham kod gorunur. Sorgudaki her anahtar veritabaninda, bekleyen dil
/// satirlarinda ya da bu deploy'un yazdigi anahtarlarda (<see cref="WizardSeedRowKeys.All"/>)
/// bulunmali; eksikler dosyalar yazilmadan once adlariyla bildirilir.
/// </summary>
private async Task ValidateWidgetLanguageKeysAsync(ListFormWizardDto input, WizardSeedRowStore rows)
{
if (input.ComponentKind == WizardComponentKindEnum.Custom)
return;
var produced = new HashSet<string>(WizardSeedRowKeys.All(input).LanguageKeys, StringComparer.OrdinalIgnoreCase);
var res = PlatformConsts.AppName;
var missing = new List<string>();
var keys = (input.Widgets ?? [])
.SelectMany(w => WidgetLanguageKeyPattern.Matches(w.SqlQuery ?? string.Empty))
.Select(m => m.Groups[1].Value)
.Distinct(StringComparer.OrdinalIgnoreCase);
foreach (var key in keys)
{
if (produced.Contains(key) ||
rows.Contains(WizardSeedRowStore.LanguageTextFile, nameof(LanguageText.Key), key) ||
await repoLangKey.AnyAsync(a => a.ResourceName == res && a.Key == key))
{
continue;
}
missing.Add(key);
}
if (missing.Count > 0)
{
throw new UserFriendlyException(
$"Widget sorgusunda tanımlı olmayan dil anahtarları var: {string.Join(", ", missing)}. " +
"Var olan bir anahtar kullanın ya da anahtarı EN + TR metinleriyle wizard'ın LanguageTexts alanına ekleyin.");
}
}
private static void ValidateWizardName(string wizardName) private static void ValidateWizardName(string wizardName)
{ {
if (!WizardNamePattern.IsMatch(wizardName)) if (!WizardNamePattern.IsMatch(wizardName))
@ -1110,17 +1157,24 @@ public class ListFormWizardAppService(
private async Task ValidateWizardUniquenessAsync( private async Task ValidateWizardUniquenessAsync(
string wizardName, string wizardName,
string code, string code,
string menuParentCode, List<WizardMenuLevelDto> menuParents,
string? editFileName, string? editFileName,
WizardSeedFileDto? previous, WizardSeedFileDto? previous,
WizardSeedRowStore rows) WizardSeedRowStore rows)
{ {
if (string.Equals(code, menuParentCode?.Trim(), StringComparison.OrdinalIgnoreCase)) if (menuParents.Any(l => string.Equals(code, l.Code, StringComparison.OrdinalIgnoreCase)))
{ {
throw new UserFriendlyException( throw new UserFriendlyException(
$"Menü kodu ('{code}') üst menü kodu ile aynı olamaz. Farklı bir Wizard adı ya da menü kodu seçin; aksi halde üst menü bu kaydın üzerine yazılır."); $"Menü kodu ('{code}') üst menü kodu ile aynı olamaz. Farklı bir Wizard adı ya da menü kodu seçin; aksi halde üst menü bu kaydın üzerine yazılır.");
} }
var repeated = menuParents.GroupBy(l => l.Code, StringComparer.OrdinalIgnoreCase).FirstOrDefault(g => g.Count() > 1);
if (repeated != null)
{
throw new UserFriendlyException(
$"'{repeated.Key}' menü kodu üst menü yolunda birden fazla kez geçiyor. Yoldaki her seviyenin kodu farklı olmalı.");
}
var duplicateFile = (await ReadSeedFilesAsync()) var duplicateFile = (await ReadSeedFilesAsync())
.Where(f => !string.Equals(f.FileName, editFileName, StringComparison.OrdinalIgnoreCase) && .Where(f => !string.Equals(f.FileName, editFileName, StringComparison.OrdinalIgnoreCase) &&
string.Equals((f.Seed?.Wizard?.WizardName ?? f.FileName).Trim(), wizardName, StringComparison.OrdinalIgnoreCase)) string.Equals((f.Seed?.Wizard?.WizardName ?? f.FileName).Trim(), wizardName, StringComparison.OrdinalIgnoreCase))
@ -2382,7 +2436,70 @@ public class ListFormWizardAppService(
return File.Exists(fullPath) ? fullPath : null; return File.Exists(fullPath) ? fullPath : null;
} }
private async Task<HashSet<string>> GetTableColumnNamesAsync(string dataSourceCode, SelectCommandTypeEnum commandType, string selectCommand) /// <summary>Gorseli dosya olarak yukleyen platform editoru.</summary>
private const string ImageUploadEditorType = "dxImageUpload";
/// <summary>Gorseli yalnizca gosteren platform editoru; kolona deger yazmaz.</summary>
private const string ImageViewerEditorType = "dxImageViewer";
/// <summary>
/// Gorsel yukleme editoru adres (<c>uploadUrl</c>) verilmemisse dosyayi base64 metin olarak
/// kolona yazar; boyu sinirli bir kolon kayitta sessizce kesilir ya da hata verir. Sinir
/// katalogdan okunur: sinirsiz kolon (SQL Server MAX = -1, PostgreSQL text = bos) kabul edilir.
/// </summary>
private static void ValidateImageColumnLengths(ListFormWizardDto input, Dictionary<string, int?> tableColumns)
{
// Kolona bagli gorsel alani yuklenebilir olmali: goruntuleyici ile kurulan alan kayitta
// bos kalir ya da kopyalanan degeri degistirilemez hale getirir.
var viewers = input.Groups
.SelectMany(g => g.Items)
.Where(i => string.Equals(i.EditorType, ImageViewerEditorType, StringComparison.OrdinalIgnoreCase) &&
tableColumns.ContainsKey(i.FieldName ?? string.Empty))
.Select(i => i.FieldName)
.ToList();
if (viewers.Count > 0)
{
throw new UserFriendlyException(
$"Tablo kolonuna bağlı görsel alanlarında {ImageViewerEditorType} kullanılamaz: {string.Join(", ", viewers)}. " +
$"Editörü {ImageUploadEditorType} yapın; salt okunur gösterim için EditorOptions readOnly verin.");
}
var limited = input.Groups
.SelectMany(g => g.Items)
.Where(i => string.Equals(i.EditorType, ImageUploadEditorType, StringComparison.OrdinalIgnoreCase) &&
!HasUploadUrl(i.EditorOptions) &&
tableColumns.TryGetValue(i.FieldName ?? string.Empty, out var length) && length > 0)
.Select(i => $"{i.FieldName} ({tableColumns[i.FieldName]})")
.ToList();
if (limited.Count > 0)
{
throw new UserFriendlyException(
$"Görsel yükleme alanları base64 saklar, kolon boyu sınırsız olmalı: {string.Join(", ", limited)}. " +
"Kolonu nvarchar(MAX) yapın ya da editöre uploadUrl verin.");
}
}
private static bool HasUploadUrl(string? editorOptions)
{
if (string.IsNullOrWhiteSpace(editorOptions))
return false;
try
{
return JsonNode.Parse(editorOptions) is JsonObject options &&
options.FirstOrDefault(p => string.Equals(p.Key, "uploadUrl", StringComparison.OrdinalIgnoreCase)).Value is JsonValue url &&
!string.IsNullOrWhiteSpace(url.ToString());
}
catch (JsonException)
{
return false;
}
}
/// <summary>Tablonun kolonlari ve metin kolonlarinin karakter siniri (sinirsizsa -1 ya da bos).</summary>
private async Task<Dictionary<string, int?>> GetTableColumnsAsync(string dataSourceCode, SelectCommandTypeEnum commandType, string selectCommand)
{ {
if (commandType == SelectCommandTypeEnum.Query || commandType == SelectCommandTypeEnum.StoredProcedure) if (commandType == SelectCommandTypeEnum.Query || commandType == SelectCommandTypeEnum.StoredProcedure)
return []; return [];
@ -2400,8 +2517,8 @@ public class ListFormWizardAppService(
// Şema/tablo adları kullanıcıdan geldiği için sorguya parametre olarak bağlanır. // Şema/tablo adları kullanıcıdan geldiği için sorguya parametre olarak bağlanır.
var query = dbType == DataSourceTypeEnum.Postgresql var query = dbType == DataSourceTypeEnum.Postgresql
? "SELECT column_name FROM information_schema.columns WHERE table_schema = @schemaName AND table_name = @tableName" ? "SELECT column_name, character_maximum_length FROM information_schema.columns WHERE table_schema = @schemaName AND table_name = @tableName"
: "SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = @schemaName AND TABLE_NAME = @tableName"; : "SELECT COLUMN_NAME, CHARACTER_MAXIMUM_LENGTH FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = @schemaName AND TABLE_NAME = @tableName";
var parameters = new Dictionary<string, object> var parameters = new Dictionary<string, object>
{ {
@ -2411,15 +2528,20 @@ public class ListFormWizardAppService(
var rows = await repo.QueryAsync(query, connectionString, parameters); var rows = await repo.QueryAsync(query, connectionString, parameters);
var columns = new HashSet<string>(StringComparer.OrdinalIgnoreCase); var columns = new Dictionary<string, int?>(StringComparer.OrdinalIgnoreCase);
foreach (var row in rows) foreach (var row in rows)
{ {
if (row is not IDictionary<string, object> dict) if (row is not IDictionary<string, object> dict)
continue; continue;
var key = dict.ContainsKey("COLUMN_NAME") ? "COLUMN_NAME" : "column_name"; var nameKey = dict.ContainsKey("COLUMN_NAME") ? "COLUMN_NAME" : "column_name";
if (dict.TryGetValue(key, out var col) && col != null) var lengthKey = dict.ContainsKey("CHARACTER_MAXIMUM_LENGTH") ? "CHARACTER_MAXIMUM_LENGTH" : "character_maximum_length";
columns.Add(col.ToString()!); if (dict.TryGetValue(nameKey, out var col) && col != null)
{
columns[col.ToString()!] = dict.TryGetValue(lengthKey, out var length) && length is not null and not DBNull
? Convert.ToInt32(length, System.Globalization.CultureInfo.InvariantCulture)
: null;
}
} }
return columns; return columns;
} }

View file

@ -124,6 +124,8 @@ Bir çelişki varsa sıra: `ai.instructions.md` → `dotnet.instructions.md` →
Yeni **tanım** tablosu (tanım olup olmadığına talebin içeriğine göre analizde karar verilir; Yeni **tanım** tablosu (tanım olup olmadığına talebin içeriğine göre analizde karar verilir;
sabit ad listesi yoktur) buna ek olarak `IsActive bit NOT NULL DEFAULT 1` ile gelir; liste + form alanı, varsayılan sabit ad listesi yoktur) buna ek olarak `IsActive bit NOT NULL DEFAULT 1` ile gelir; liste + form alanı, varsayılan
`true`, Aktif/Pasif widget'ı. Ayrıntı: `lowcode.instructions.md` §8.1. `true`, Aktif/Pasif widget'ı. Ayrıntı: `lowcode.instructions.md` §8.1.
Görsel tutan her kolon (fotoğraf, logo, avatar kopyası) `nvarchar(MAX)` + editör `dxImageUpload`;
değer base64 saklanır, sınırlı boy ve `dxImageViewer` kullanılmaz (deploy ikisini de reddeder).
2. **Yeni ekran** → Wizard yolu. `Definitions` altına otomatik yerleştirme yok; modüle ait yeni 2. **Yeni ekran** → Wizard yolu. `Definitions` altına otomatik yerleştirme yok; modüle ait yeni
bir kök menü oluştur, `Order = max(Order) + 1`. bir kök menü oluştur, `Order = max(Order) + 1`.
3. **Yeni ekran/menü** → yetki sözleşmesi olmadan önerilmez. Menü ve route aynı ekran 3. **Yeni ekran/menü** → yetki sözleşmesi olmadan önerilmez. Menü ve route aynı ekran
@ -260,7 +262,10 @@ Bir çelişki varsa sıra: `ai.instructions.md` → `dotnet.instructions.md` →
(`McpDevRequestImages`) ve MCP resim içeriği (`ImageContentBlock`) olarak döndürür; metinde (`McpDevRequestImages`) ve MCP resim içeriği (`ImageContentBlock`) olarak döndürür; metinde
`[image-N]` kalır. Açıklama bu yüzden `update_row` ile yeniden yazılmaz. `SubTasks` de `[image-N]` kalır. Açıklama bu yüzden `update_row` ile yeniden yazılmaz. `SubTasks` de
`add_dev_request_subtasks` ile yazılır: atanan kişi bağlanan kullanıcı, bitiş tarihi bugün — `add_dev_request_subtasks` ile yazılır: atanan kişi bağlanan kullanıcı, bitiş tarihi bugün —
sunucu doldurur, modele bırakılmaz. sunucu doldurur, modele bırakılmaz. `Timesheets`'i tamamen sunucu yazar
(`IMcpDevRequestTimesheetRecorder`): `get_dev_request`/`start_dev_request` talebi etkin talep yapar,
sonraki her MCP yazma işlemi ona `App.Mcp.Timesheet.*` anahtarından Türkçe açıklama ve ölçülen
süreyle bir satır ekler. Yeni bir MCP yazma aracı eklenirse kaydedici çağrısı ve dil anahtarı da eklenir.
- **Talep analizi** → `Description` talebin yazıldığı hâliyle kalır; MCP ona yazmaz. Analiz ve - **Talep analizi** → `Description` talebin yazıldığı hâliyle kalır; MCP ona yazmaz. Analiz ve
üretim notları `Analysis` kolonundadır (Todo panosunda `AnalysisExpr`, Açıklama yanında sekme). üretim notları `Analysis` kolonundadır (Todo panosunda `AnalysisExpr`, Açıklama yanında sekme).
Durum sırası `Backlog → Analysis → Ready to Start → …`: "Analiz et" `save_dev_request_analysis` Durum sırası `Backlog → Analysis → Ready to Start → …`: "Analiz et" `save_dev_request_analysis`

View file

@ -67302,6 +67302,146 @@
"Key": "App.Mcp.Error.DevRequestSubTasksInvalid", "Key": "App.Mcp.Error.DevRequestSubTasksInvalid",
"Value": "\"{0}\" geliştirme talebinin alt görevleri okunamadı; önce talep ekranından düzeltin." "Value": "\"{0}\" geliştirme talebinin alt görevleri okunamadı; önce talep ekranından düzeltin."
}, },
{
"Id": "642b0ee1-75e7-504c-b1f9-8339f2ebab65",
"CultureName": "en",
"ResourceName": "Platform",
"Key": "App.Mcp.Timesheet.Analysis",
"Value": "Request analyzed"
},
{
"Id": "ae0344b7-6027-532d-ac6f-2a09cd9b254c",
"CultureName": "tr",
"ResourceName": "Platform",
"Key": "App.Mcp.Timesheet.Analysis",
"Value": "Talep analiz edildi"
},
{
"Id": "3ed32588-4dcb-5e6a-940a-ca5dc536c25c",
"CultureName": "en",
"ResourceName": "Platform",
"Key": "App.Mcp.Timesheet.CreateTable",
"Value": "Table {0} created"
},
{
"Id": "386794a2-41f2-5af6-982e-84c9190ba897",
"CultureName": "tr",
"ResourceName": "Platform",
"Key": "App.Mcp.Timesheet.CreateTable",
"Value": "{0} tablosu oluşturuldu"
},
{
"Id": "c808ea33-665d-5b85-bca9-10b0b4fc407f",
"CultureName": "en",
"ResourceName": "Platform",
"Key": "App.Mcp.Timesheet.DeployWizard",
"Value": "Wizard {0} deployed ({1} demo rows)"
},
{
"Id": "1d2fc656-2eac-526b-8a24-7b39b41c022c",
"CultureName": "tr",
"ResourceName": "Platform",
"Key": "App.Mcp.Timesheet.DeployWizard",
"Value": "{0} wizard'ı deploy edildi ({1} demo satır)"
},
{
"Id": "53d696cb-fdf3-5b08-a71c-90ec24c003c3",
"CultureName": "en",
"ResourceName": "Platform",
"Key": "App.Mcp.Timesheet.DeleteWizard",
"Value": "Wizard {0} deleted"
},
{
"Id": "f133fc5f-94df-5fda-9fbf-a38d4d811d09",
"CultureName": "tr",
"ResourceName": "Platform",
"Key": "App.Mcp.Timesheet.DeleteWizard",
"Value": "{0} wizard'ı silindi"
},
{
"Id": "b074b30c-ff5d-56d9-b0a6-2514478b9a6b",
"CultureName": "en",
"ResourceName": "Platform",
"Key": "App.Mcp.Timesheet.SaveComponent",
"Value": "Component {0} saved"
},
{
"Id": "d80d1833-c77e-5aa3-aea9-0a28d87e244f",
"CultureName": "tr",
"ResourceName": "Platform",
"Key": "App.Mcp.Timesheet.SaveComponent",
"Value": "{0} bileşeni kaydedildi"
},
{
"Id": "20eeb922-261c-5d5c-8b29-33d3b79c8bff",
"CultureName": "en",
"ResourceName": "Platform",
"Key": "App.Mcp.Timesheet.DeleteComponent",
"Value": "Component {0} deleted"
},
{
"Id": "9d139d39-1f0d-5bd8-b0a3-175e2656c13c",
"CultureName": "tr",
"ResourceName": "Platform",
"Key": "App.Mcp.Timesheet.DeleteComponent",
"Value": "{0} bileşeni silindi"
},
{
"Id": "11a42b2b-3a91-54ec-92c7-3020e0b3aa14",
"CultureName": "en",
"ResourceName": "Platform",
"Key": "App.Mcp.Timesheet.InsertRow",
"Value": "Row added to {0}"
},
{
"Id": "b32f690c-6e0f-5eaf-9dcc-b06302840eab",
"CultureName": "tr",
"ResourceName": "Platform",
"Key": "App.Mcp.Timesheet.InsertRow",
"Value": "{0} ekranına satır eklendi"
},
{
"Id": "a6b8da40-d8c6-5bdf-bdab-95fc73d3425a",
"CultureName": "en",
"ResourceName": "Platform",
"Key": "App.Mcp.Timesheet.UpdateRow",
"Value": "Row updated in {0} ({1})"
},
{
"Id": "ca726837-6996-5b2c-8bc8-0067021457aa",
"CultureName": "tr",
"ResourceName": "Platform",
"Key": "App.Mcp.Timesheet.UpdateRow",
"Value": "{0} ekranında satır güncellendi ({1})"
},
{
"Id": "4a797258-7f4b-53fe-814b-bdccec086e1b",
"CultureName": "en",
"ResourceName": "Platform",
"Key": "App.Mcp.Timesheet.DeleteRow",
"Value": "Row deleted from {0}"
},
{
"Id": "4e294e52-cb49-5c86-be18-a0bb6c1badfd",
"CultureName": "tr",
"ResourceName": "Platform",
"Key": "App.Mcp.Timesheet.DeleteRow",
"Value": "{0} ekranından satır silindi"
},
{
"Id": "a5e491f7-a85c-572f-bf66-4245cb84fa36",
"CultureName": "en",
"ResourceName": "Platform",
"Key": "App.Widget.ThisMonth",
"Value": "This Month"
},
{
"Id": "25e0ea1d-a09a-5f8f-8d5b-519b2bab42d5",
"CultureName": "tr",
"ResourceName": "Platform",
"Key": "App.Widget.ThisMonth",
"Value": "Bu Ay"
},
{ {
"Id": "35642c6a-9097-5bcf-8b27-2bc8356f47ef", "Id": "35642c6a-9097-5bcf-8b27-2bc8356f47ef",
"CultureName": "en", "CultureName": "en",

View file

@ -103,6 +103,14 @@ export interface ListFormWizardDto {
menuParentCode: string menuParentCode: string
menuParentModuleId?: string menuParentModuleId?: string
menuParentIcon?: string menuParentIcon?: string
/**
* menuParentCode'un ustundeki seviyeler, kokten asagi. MCP deploy'u eksik seviyeleri buradan
* kurar; ekrandaki wizard menuleri diyalogla olusturdugu icin bos gonderir ve ust menu
* degismedikce gelen degeri korur.
*/
menuParentPath?: WizardMenuLevelDto[]
/** Baska bir cevaptan uretilmeyen dil anahtarlari (widget basliklari); duzenlemede korunur. */
languageTexts?: WizardLanguageTextDto[]
menuIcon: string menuIcon: string
dataSourceCode: string dataSourceCode: string
dataSourceConnectionString: string dataSourceConnectionString: string
@ -408,3 +416,16 @@ export interface WizardExportAnalysisDto {
/** Bagimlilik olarak bulunan ama diskte karsiligi olmayan dosyalar. */ /** Bagimlilik olarak bulunan ama diskte karsiligi olmayan dosyalar. */
warnings: string[] warnings: string[]
} }
export interface WizardLanguageTextDto {
key: string
en?: string
tr?: string
}
export interface WizardMenuLevelDto {
code: string
icon?: string
en?: string
tr?: string
}

View file

@ -170,6 +170,8 @@ const initialValues: ListFormWizardDto = {
menuParentCode: '', menuParentCode: '',
menuParentModuleId: '', menuParentModuleId: '',
menuParentIcon: '', menuParentIcon: '',
menuParentPath: [],
languageTexts: [],
menuIcon: '', menuIcon: '',
dataSourceCode: '', dataSourceCode: '',
dataSourceConnectionString: '', dataSourceConnectionString: '',
@ -740,6 +742,8 @@ const Wizard = () => {
menuParentCode: w.menuParentCode ?? '', menuParentCode: w.menuParentCode ?? '',
menuParentModuleId: w.menuParentModuleId ?? '', menuParentModuleId: w.menuParentModuleId ?? '',
menuParentIcon: w.menuParentIcon ?? '', menuParentIcon: w.menuParentIcon ?? '',
menuParentPath: w.menuParentPath ?? [],
languageTexts: w.languageTexts ?? [],
menuIcon: w.menuIcon ?? '', menuIcon: w.menuIcon ?? '',
dataSourceCode: w.dataSourceCode ?? '', dataSourceCode: w.dataSourceCode ?? '',
dataSourceConnectionString: w.dataSourceConnectionString ?? '', dataSourceConnectionString: w.dataSourceConnectionString ?? '',
@ -948,6 +952,7 @@ const Wizard = () => {
formik?.setFieldValue('menuParentIcon', '') formik?.setFieldValue('menuParentIcon', '')
formik?.setFieldValue('menuParentEn', '') formik?.setFieldValue('menuParentEn', '')
formik?.setFieldValue('menuParentTr', '') formik?.setFieldValue('menuParentTr', '')
formik?.setFieldValue('menuParentPath', [])
if (derivedFromParent) handlePermissionGroupChange('') if (derivedFromParent) handlePermissionGroupChange('')
} }
@ -966,6 +971,7 @@ const Wizard = () => {
formik?.setFieldValue('menuParentIcon', '') formik?.setFieldValue('menuParentIcon', '')
formik?.setFieldValue('menuParentEn', '') formik?.setFieldValue('menuParentEn', '')
formik?.setFieldValue('menuParentTr', '') formik?.setFieldValue('menuParentTr', '')
formik?.setFieldValue('menuParentPath', [])
} }
} }
@ -982,6 +988,7 @@ const Wizard = () => {
formik?.setFieldValue('menuParentIcon', '') formik?.setFieldValue('menuParentIcon', '')
formik?.setFieldValue('menuParentEn', '') formik?.setFieldValue('menuParentEn', '')
formik?.setFieldValue('menuParentTr', '') formik?.setFieldValue('menuParentTr', '')
formik?.setFieldValue('menuParentPath', [])
formik?.setFieldValue('menuIcon', '') formik?.setFieldValue('menuIcon', '')
formik?.setFieldValue('menuOrder', 0) formik?.setFieldValue('menuOrder', 0)
} }
@ -990,6 +997,7 @@ const Wizard = () => {
const formik = formikRef.current const formik = formikRef.current
const parentChanged = formik?.values.menuParentCode !== code const parentChanged = formik?.values.menuParentCode !== code
formik?.setFieldValue('menuParentCode', code) formik?.setFieldValue('menuParentCode', code)
if (parentChanged) formik?.setFieldValue('menuParentPath', [])
if (parentChanged || !formik?.values.menuOrder || formik.values.menuOrder < 1) { if (parentChanged || !formik?.values.menuOrder || formik.values.menuOrder < 1) {
const nextMenuOrder = const nextMenuOrder =
rawMenuItems rawMenuItems
@ -1020,6 +1028,7 @@ const Wizard = () => {
moduleId?: string moduleId?: string
}) => { }) => {
formikRef.current?.setFieldValue('menuParentCode', menu.code) formikRef.current?.setFieldValue('menuParentCode', menu.code)
formikRef.current?.setFieldValue('menuParentPath', [])
const nextMenuOrder = const nextMenuOrder =
rawMenuItems rawMenuItems
.filter((item) => (item.parentCode ?? '') === menu.code) .filter((item) => (item.parentCode ?? '') === menu.code)

View file

@ -317,7 +317,12 @@ const WizardStepDeploy = ({
<Section title={translate('::App.WizardStep4.MenuInfo')}> <Section title={translate('::App.WizardStep4.MenuInfo')}>
<Row label={translate('::App.WizardStep4.WizardName')} value={wizardName} /> <Row label={translate('::App.WizardStep4.WizardName')} value={wizardName} />
<Row label={translate('::App.WizardStep1.MenuCode')} value={values.menuCode} /> <Row label={translate('::App.WizardStep1.MenuCode')} value={values.menuCode} />
<Row label={translate('::App.Platform.MenuParent')} value={values.menuParentCode} /> <Row
label={translate('::App.Platform.MenuParent')}
value={[...(values.menuParentPath ?? []).map((l) => l.code), values.menuParentCode]
.filter(Boolean)
.join(' › ')}
/>
<Row <Row
label={translate('::App.WizardStep4.PermissionGroup')} label={translate('::App.WizardStep4.PermissionGroup')}
value={values.permissionGroupName} value={values.permissionGroupName}