wizard 3 sekmeli sistem ve timesheet çalışması
This commit is contained in:
parent
a2c3655942
commit
ae226d359b
21 changed files with 683 additions and 82 deletions
|
|
@ -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) |
|
||||||
|
|
|
||||||
63
.github/instructions/lowcode.instructions.md
vendored
63
.github/instructions/lowcode.instructions.md
vendored
|
|
@ -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.
|
||||||
|
|
|
||||||
11
README.md
11
README.md
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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 };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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; }
|
||||||
|
}
|
||||||
|
|
@ -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 });
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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";
|
||||||
|
}
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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; }
|
||||||
|
}
|
||||||
|
|
@ -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; }
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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());
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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())
|
||||||
rows.Upsert(WizardSeedRowStore.MenuFile, MenuRow(
|
|
||||||
parentCode!, null, parentCode!,
|
|
||||||
parentRow != null ? WizardSeedRowStore.Int(parentRow, nameof(Menu.Order)) : maxRootOrder + 1,
|
|
||||||
null, menuParentIcon, null, parentModuleId), nameof(Menu.Code));
|
|
||||||
inserted.MenuCodes.Add(parentCode!);
|
|
||||||
|
|
||||||
await WriteLanguageRowsAsync(rows, parentCode, input.MenuParentEn, input.MenuParentTr, owned, inserted);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
{
|
||||||
parentModuleId = dbParent?.ModuleId ?? WizardSeedRowStore.Text(parentRow, nameof(Menu.ModuleId));
|
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(
|
||||||
|
level.Code, parentCode, level.Code,
|
||||||
|
levelRow != null ? WizardSeedRowStore.Int(levelRow, nameof(Menu.Order)) : MaxSiblingOrder(parentCode, level.Code) + 1,
|
||||||
|
null, string.IsNullOrWhiteSpace(level.Icon) ? fallbackIcon : level.Icon, null, parentModuleId), nameof(Menu.Code));
|
||||||
|
inserted.MenuCodes.Add(level.Code);
|
||||||
|
|
||||||
|
await WriteLanguageRowsAsync(rows, level.Code, level.En, level.Tr, owned, inserted);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
parentModuleId = dbLevel?.ModuleId ?? WizardSeedRowStore.Text(levelRow, nameof(Menu.ModuleId));
|
||||||
|
}
|
||||||
|
|
||||||
|
parentCode = level.Code;
|
||||||
}
|
}
|
||||||
|
|
||||||
var maxChildOrder = dbMenus.Where(m => Same(m.ParentCode, parentCode) && !Same(m.Code, code)).Select(m => m.Order)
|
input.MenuOrder = input.MenuOrder > 0 ? input.MenuOrder : MaxSiblingOrder(parentCode, code) + 1;
|
||||||
.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;
|
|
||||||
|
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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`
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue