Mcp problemi giderildi

This commit is contained in:
Sedat ÖZTÜRK 2026-10-07 15:45:18 +03:00
parent 53e26ce787
commit 02b8d3abb4
19 changed files with 527 additions and 579 deletions

View file

@ -68,7 +68,7 @@ kurulur; host veritabanını paylaşan tenant'ta dosyalar yeniden uygulanmaz (`S
| # | Adım | Dosya | Neden bu sırada | | # | Adım | Dosya | Neden bu sırada |
| --- | --- | --- | --- | | --- | --- | --- | --- |
| 1 | Tablo / view | `sql/object/…` | Ekranın bağlanacağı nesne önce var olmalı. MCP'de yeni tablo `create_table` ile kurulur (script ve dosya tasarımcıyla aynı); view MCP'den kurulamaz | | 1 | Tablo / view | `sql/object/…` | Ekranın bağlanacağı nesne önce var olmalı. MCP'de yeni tablonun scripti `create_table` ile yazılır (biçim tasarımcıyla aynı; tablo DB Migrate'te kurulur); view MCP'den kurulamaz |
| 2 | CRUD uçları (gerekiyorsa) | `crud/{Entity}.json` | Custom Component'in data source'ları buna bakar | | 2 | CRUD uçları (gerekiyorsa) | `crud/{Entity}.json` | Custom Component'in data source'ları buna bakar |
| 3 | Custom Component (gerekiyorsa) | `data/App.DeveloperKit.CustomComponents/{Id}.json` | Wizard `Custom` yolunda bileşen adını arar | | 3 | Custom Component (gerekiyorsa) | `data/App.DeveloperKit.CustomComponents/{Id}.json` | Wizard `Custom` yolunda bileşen adını arar |
| 4 | Wizard | `wizard/{Ad}.json` + menü/yetki/dil satırları (`data/App.Menus.*`, `data/Abp.Identity.Permission*`, `data/App.Languages.LanguageText.json`) | Deploy (MCP `deploy_wizard` ya da wizard ekranı, §0.10) **yalnızca bu dosyaları yazar**; ekran, menü ve yetkiler DB Migrate ile kurulur (§4.0). Dosyaları sunucu yazar. Elle yazılan List dosyası `ListForm` + `Fields` taşımak zorundadır, yoksa seeder atlar (§4.1) | | 4 | Wizard | `wizard/{Ad}.json` + menü/yetki/dil satırları (`data/App.Menus.*`, `data/Abp.Identity.Permission*`, `data/App.Languages.LanguageText.json`) | Deploy (MCP `deploy_wizard` ya da wizard ekranı, §0.10) **yalnızca bu dosyaları yazar**; ekran, menü ve yetkiler DB Migrate ile kurulur (§4.0). Dosyaları sunucu yazar. Elle yazılan List dosyası `ListForm` + `Fields` taşımak zorundadır, yoksa seeder atlar (§4.1) |
@ -509,13 +509,13 @@ ister ve kiracı kullanıcısına hiçbiri açılmaz.
| Araç | Ne yapar | Not | | Araç | Ne yapar | Not |
| --- | --- | --- | | --- | --- | --- |
| `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 (`4.3`) ya da başlık metniyle istenir; aynı numarayı iki başlık taşımamalıdır. Bölüm önce istenen belgede, sonra diğerinde aranır; başlık olmayan bir terim (`dxImageUpload`) terimin geçtiği bölümlerin listesini döndürü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 (`4.3`) ya da başlık metniyle istenir; aynı numarayı iki başlık taşımamalıdır. Bölüm önce istenen belgede, sonra diğerinde aranır; başlık olmayan bir terim (`dxImageUpload`) terimin geçtiği bölümlerin listesini döndürür |
| `create_table` | Yeni tabloyu kurar (SQL Server ya da PostgreSQL veri kaynağı), `sql/object/{Tablo}.sql` ve PG eşi `postgres/object/{Tablo}.sql` dosyaları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 tablonun `sql/object/{Tablo}.sql` ve PG eşi `postgres/object/{Tablo}.sql` dosyalarını yazar; veritabanında çalıştırmaz, tabloyu DB Migrate kurar (deploy kolonları o zamana kadar scriptten okur) | 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 — alt form olarak üretilen ekran hariç (`Widgets: []`) — 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. Talep bağı (`DevRequest`) da yazılmaz — sunucu etkin talepten doldurur (§4.0). İstek demo veri taşıyorsa `SeedData: true` + `seedRows` (§0.10.1). Satır/toolbar butonları `CommandColumns` (§4.9). Bağımlı lookup öğedeki `Cascade*` alanları (§4.3.4). Çı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 — alt form olarak üretilen ekran hariç (`Widgets: []`) — 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. Talep bağı (`DevRequest`) da yazılmaz — sunucu etkin talepten doldurur (§4.0). İstek demo veri taşıyorsa `SeedData: true` + `seedRows` (§0.10.1). Satır/toolbar butonları `CommandColumns` (§4.9). Bağımlı lookup öğedeki `Cascade*` alanları (§4.3.4). Çı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 |
| `save_dev_request_analysis` | `Backlog`/`Analysis` durumundaki talebin analizini `Analysis` kolonuna yazar (var olanı değiştirir), talebi `Analysis` durumuna alır; diğer durumları reddeder. `menuCode` dalının modül dokümanı bölümüne de yazar, dosya/bölüm yoksa açar (§0.13) | Ek olarak `App.DevRequests.Update` ve `App.Listforms.Wizard` ister. Standart §0.12; `Description`'a dokunmaz | | `save_dev_request_analysis` | `Backlog`/`Analysis` durumundaki talebin analizini `Analysis` kolonuna yazar (var olanı değiştirir), talebi `Analysis` durumuna alır; diğer durumları reddeder. Modül dokümanına yazmaz; doküman üretimde açılır (§0.13) | Ek olarak `App.DevRequests.Update` ister. Standart §0.12; `Description`'a dokunmaz |
| `append_dev_request_note` | Talebin `Analysis` kolonunun sonuna HTML ekler | Üretim sonuç notu için; `Description` ve `Analysis` `update_row` ile yazılmaz (§0.11) | | `append_dev_request_note` | Talebin `Analysis` kolonunun sonuna HTML ekler | Üretim sonuç notu için; `Description` ve `Analysis` `update_row` ile yazılmaz (§0.11) |
| `add_dev_request_subtasks` | `SubTasks` listesine tamamlanmış satırlar ekler | Atanan = bağlanan kullanıcı, bitiş = bugün (sunucu yazar); `SubTasks`'ı `update_row` ile yazma (§0.11) | | `add_dev_request_subtasks` | `SubTasks` listesine tamamlanmış satırlar ekler | Atanan = bağlanan kullanıcı, bitiş = bugün (sunucu yazar); `SubTasks`'ı `update_row` ile yazma (§0.11) |
| `verify_wizard` | DB Migrate ile kurulmuş ekranı veri yazmadan doğrular (migrate bekleyen dosyada `install` kontrolü başarısız döner, diğerleri yapılmaz): menü, yetki tanımı, ekran sorgusu (List) ya da bileşen (Custom), seed kapanışı | İsteğe bağlı: kullanıcı migrate sonrası isterse. Talep döngüsünün (§0.11) adımı değildir. `passed` yalnızca engelleyici bir kontrol kalırsa `false` | | `verify_wizard` | DB Migrate ile kurulmuş ekranı veri yazmadan doğrular (migrate bekleyen dosyada `install` kontrolü başarısız döner, diğerleri yapılmaz): menü, yetki tanımı, ekran sorgusu (List) ya da bileşen (Custom), seed kapanışı | İsteğe bağlı: kullanıcı migrate sonrası isterse. Talep döngüsünün (§0.11) adımı değildir. `passed` yalnızca engelleyici bir kontrol kalırsa `false` |
@ -673,7 +673,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 + `menuCode` (menü yerindeki dalın ya da ekranın kodu). Analiz o dalın modül dokümanı bölümüne ("2. Analiz ve kararlar", §0.13) de yazılır; kök menünün ilk talebiyse dosya ve bölüm burada açılır (Timesheets satırını sunucu yazar, §0.11) | Analysis | | 4. Yaz | `save_dev_request_analysis` — analizin tamamı, HTML. Modül dokümanına yazılmaz; dosya ve dal bölümü üretimde deploy ile açılır, analiz "2. Analiz ve kararlar" alt bölümüne üretimde `save_module_doc` ile taşınır (§0.13) (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ı

View file

@ -231,9 +231,9 @@ Uygulama `https://localhost:44344` adresinde açılır. Swagger UI kök adreste
- UI tarafında `/setup` route'u (`views/setup/DatabaseSetup.tsx`) migration'ı tetikleyen ekranı gösterir. - UI tarafında `/setup` route'u (`views/setup/DatabaseSetup.tsx`) migration'ı tetikleyen ekranı gösterir.
- Migration için `Setup:MigrationPassword` yapılandırması veya `App.Setup.Migrate` yetkisi gerekir. - Migration için `Setup:MigrationPassword` yapılandırması veya `App.Setup.Migrate` yetkisi gerekir.
- Ekran DbMigrator'ü `SetupMigrationRunner` ile çalıştırır. `Setup:MigratorPath` publish çıktısını - Ekran DbMigrator'ü `SetupMigrationRunner` ile çalıştırır. `Setup:MigratorPath` publish çıktısını
gösteriyorsa DLL doğrudan çalışır; kaynak proje klasöründe (geliştirme) `dotnet run` kullanılmaz — gösteriyorsa DLL doğrudan çalışır; kaynak proje klasöründe (geliştirme) `bin/Debug` altındaki var olan
`bin/Debug` altındaki DLL `api/` kaynaklarından (ve son başarılı derlemenin `obj/setup-migrator.stamp` DLL çalışır. Migrate restore/build yapmaz: DLL yoksa hata verir, `api/` kaynaklarından eskiyse yalnızca
damgasından) yeniyse derleme atlanır, değilse `dotnet build` çıktısı ekrandaki loga akar. uyarı yazar — kod değişikliğinin migrate'e girmesi için DbMigrator elle derlenir.
Alternatif olarak migration + seed'i doğrudan çalıştırabilirsiniz: Alternatif olarak migration + seed'i doğrudan çalıştırabilirsiniz:
@ -1555,14 +1555,14 @@ liste süzgeci ve çağrı kapısı aynı kuralı kullanır.
| `run_query` | Kayıtlı bir nesneyi ya da Custom Endpoint'i parametreleriyle çağırır; yetkiler `list_queries` ile aynı. | | `run_query` | Kayıtlı bir nesneyi ya da Custom Endpoint'i parametreleriyle çağırır; yetkiler `list_queries` ile aynı. |
| `insert_row` / `update_row` / `delete_row` | ListForm ekranının insert/update/delete sözleşmesinden geçer. | | `insert_row` / `update_row` / `delete_row` | ListForm ekranının insert/update/delete sözleşmesinden geçer. |
| `get_dev_guide` | Üretim sözleşmesi: `lowcode` / `reference` belgelerinin başlık listesi ya da istenen bölümü (numarayla, örn. `4.2`, ya da başlık metniyle). Bölüm önce istenen, sonra diğer belgede aranır; başlıkla eşleşmeyen terimde (örn. `dxImageUpload`) terimin geçtiği bölümler (en çok 15, `DevGuideSearchMatches`), hiç geçmiyorsa başlık listesi döner — hata atılmaz. Ç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`, ya da başlık metniyle). Bölüm önce istenen, sonra diğer belgede aranır; başlıkla eşleşmeyen terimde (örn. `dxImageUpload`) terimin geçtiği bölümler (en çok 15, `DevGuideSearchMatches`), hiç geçmiyorsa başlık listesi döner — hata atılmaz. Ç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 `sql/object/{Tablo}.sql` dosyasına, PostgreSQL eşi (`PostgreSqlTableScript`) `postgres/object/{Tablo}.sql` dosyasına yazılır; PG veri kaynağında çalıştırılan da o eştir. Yalnızca oluşturur — tablo varsa reddeder. `.Write` + `App.SqlQueryManager` ister. | | `create_table` | Yeni tablonun scriptini SQL Query Manager tablo tasarımcısıyla aynı biçimde üretir (`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 `sql/object/{Tablo}.sql` dosyasına, PostgreSQL eşi (`PostgreSqlTableScript`) `postgres/object/{Tablo}.sql` dosyasına yazılır. Veritabanında çalıştırılmaz: tabloyu kullanıcının çalıştırdığı DB Migrate kurar; o zamana kadar `deploy_wizard` kolonları scriptten okur (`PostgreSqlTableScript.ReadSeedColumns`). Yalnızca oluşturur — tablo ya da scripti varsa reddeder. `.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. Dosyaya kullanıcının etkin talebi `DevRequest` olarak yazılır (sunucu doldurur). 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. | | `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. Dosyaya kullanıcının etkin talebi `DevRequest` olarak yazılır (sunucu doldurur). 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. Sonuç istemcinin 1 MB sınırına sığdırılır: büyük görseller küçültülüp JPEG'e çevrilir (`IMcpImageShrinker`), sığmayan gönderilmez ve metinde bildirilir. `.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. Sonuç istemcinin 1 MB sınırına sığdırılır: büyük görseller küçültülüp JPEG'e çevrilir (`IMcpImageShrinker`), sığmayan gönderilmez ve metinde bildirilir. `.Write` + `App.DevRequests.Update` ister. |
| `get_dev_request` | Talebi durumuna dokunmadan, `start_dev_request` ile aynı biçimde (metin + analiz + resim içerikleri) okur; analizden önce ya da yarım kalan talebe dönmek için. `App.DevRequests` ister. | | `get_dev_request` | Talebi durumuna dokunmadan, `start_dev_request` ile aynı biçimde (metin + analiz + resim içerikleri) okur; analizden önce ya da yarım kalan talebe dönmek için. `App.DevRequests` ister. |
| `save_dev_request_analysis` | `Backlog` ya da `Analysis` durumundaki talebin analizini (HTML) `Analysis` kolonuna yazar — var olanı değiştirir — ve talebi `Analysis` durumuna alır; diğer durumları reddeder. `Description`'a dokunmaz. Zorunlu `menuCode` (dalın ya da ekranın kodu) ile analizi modül dokümanındaki dal bölümüne ("2. Analiz ve kararlar") de yazar; kök menünün ilk talebiyse dosya (`App.Inventory.md`) ve bölüm burada açılır, elle yazılmış alt bölüme dokunulmaz. Analizin standardı `lowcode.instructions.md` §0.12. `.Write` + `App.DevRequests.Update` + `App.Listforms.Wizard` ister. | | `save_dev_request_analysis` | `Backlog` ya da `Analysis` durumundaki talebin analizini (HTML) `Analysis` kolonuna yazar — var olanı değiştirir — ve talebi `Analysis` durumuna alır; diğer durumları reddeder. `Description`'a dokunmaz. Modül dokümanına yazmaz; doküman üretimde açılır. Analizin standardı `lowcode.instructions.md` §0.12. `.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. | | `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. |
| `list_components` / `get_component` / `save_component` / `delete_component` | Custom component'ler `ICustomComponentAppService` üzerinden; seed senkronu ve `DataSources` çözümü aynen çalışır. `save_component` ada göre ekler/günceller, tasarımcı belgesi (`Props.visualDesigner`, `sourceMode ≠ code`) taşıyan bileşene kod yazmayı reddeder — kod belgeden üretildiği için tasarımcıdaki ilk kayıtta ezilirdi. | | `list_components` / `get_component` / `save_component` / `delete_component` | Custom component'ler `ICustomComponentAppService` üzerinden; seed senkronu ve `DataSources` çözümü aynen çalışır. `save_component` ada göre ekler/günceller, tasarımcı belgesi (`Props.visualDesigner`, `sourceMode ≠ code`) taşıyan bileşene kod yazmayı reddeder — kod belgeden üretildiği için tasarımcıdaki ilk kayıtta ezilirdi. |
@ -1656,7 +1656,7 @@ yetkisi yalnızca kapıdır. Bir çağrı üç katmandan geçer ve her katman ay
| `delete_report_template` | — | `.Write` | `App.Reports.ReportTemplates.Delete` | | `delete_report_template` | — | `.Write` | `App.Reports.ReportTemplates.Delete` |
| `start_dev_request` | — | `.Write` | `App.DevRequests.Update` | | `start_dev_request` | — | `.Write` | `App.DevRequests.Update` |
| `get_dev_request` | — | — | `App.DevRequests` | | `get_dev_request` | — | — | `App.DevRequests` |
| `save_dev_request_analysis` | — | `.Write` | `App.DevRequests.Update`, `App.Listforms.Wizard` (modül dokümanı) | | `save_dev_request_analysis` | — | `.Write` | `App.DevRequests.Update` |
| `append_dev_request_note` | — | `.Write` | `App.DevRequests.Update` | | `append_dev_request_note` | — | `.Write` | `App.DevRequests.Update` |
| `add_dev_request_subtasks` | — | `.Write` | `App.DevRequests.Update` | | `add_dev_request_subtasks` | — | `.Write` | `App.DevRequests.Update` |

View file

@ -39,10 +39,9 @@ public interface IMcpDevelopAppService : IApplicationService
/// <summary> /// <summary>
/// Talebin analizini yazar (var olani degistirir) ve talebi Analysis durumuna alir; yalnizca /// Talebin analizini yazar (var olani degistirir) ve talebi Analysis durumuna alir; yalnizca
/// Backlog ya da Analysis durumundaki talep analiz edilir. Analiz <paramref name="menuCode"/> /// Backlog ya da Analysis durumundaki talep analiz edilir.
/// dalinin modul dokumani bolumune de yazilir; dosya ya da bolum yoksa acilir.
/// </summary> /// </summary>
Task<McpDevRequestDto> SaveDevRequestAnalysisAsync(string request, string? menuCode, string html); Task<McpDevRequestDto> SaveDevRequestAnalysisAsync(string request, string html);
/// <summary> /// <summary>
/// Analizin sonuna HTML ekler. Aciklama talebin yazildigi haliyle kalir; uretim notlari /// Analizin sonuna HTML ekler. Aciklama talebin yazildigi haliyle kalir; uretim notlari

View file

@ -491,10 +491,9 @@ public class McpDevelopAppService(
/// <summary> /// <summary>
/// Durum kurali modelin dikkatine birakilmaz: yalnizca Backlog ya da Analysis durumundaki talep /// Durum kurali modelin dikkatine birakilmaz: yalnizca Backlog ya da Analysis durumundaki talep
/// analiz edilir. Analysis'teki talebin analizi begenilmediyse yeniden yazilir; Ready to Start'a /// analiz edilir. Analysis'teki talebin analizi begenilmediyse yeniden yazilir; Ready to Start'a
/// kullanici alir. Analiz modulun dokumanina da tasinir: talep ilk kez bir menude yapiliyorsa /// kullanici alir.
/// dokuman ve dalin bolumu burada acilir, uretim beklenmez.
/// </summary> /// </summary>
public async Task<McpDevRequestDto> SaveDevRequestAnalysisAsync(string request, string? menuCode, string html) public async Task<McpDevRequestDto> SaveDevRequestAnalysisAsync(string request, string html)
{ {
await EnsureToolAllowedAsync(McpToolNames.SaveDevRequestAnalysis); await EnsureToolAllowedAsync(McpToolNames.SaveDevRequestAnalysis);
await AuthorizationService.CheckAsync(DevRequestUpdatePermission); await AuthorizationService.CheckAsync(DevRequestUpdatePermission);
@ -504,12 +503,6 @@ public class McpDevelopAppService(
throw new UserFriendlyException(L[McpErrorCodes.DevRequestAnalysisRequired]); throw new UserFriendlyException(L[McpErrorCodes.DevRequestAnalysisRequired]);
} }
// Arac listesini onbellekten okuyan istemci parametreyi hic gondermeyebilir; marshaller'in
// ArgumentException'i modele ulasmaz, bu mesaj ise ulasir ve model cagriyi kodla tekrarlar.
if (string.IsNullOrWhiteSpace(menuCode))
{
throw new UserFriendlyException(L[McpErrorCodes.DevRequestMenuCodeRequired]);
}
var devRequest = await FindDevRequestAsync(request); var devRequest = await FindDevRequestAsync(request);
@ -518,15 +511,6 @@ public class McpDevelopAppService(
throw new UserFriendlyException(L[McpErrorCodes.DevRequestNotAnalyzable, devRequest.Title ?? devRequest.Id.ToString(), devRequest.Status ?? string.Empty]); throw new UserFriendlyException(L[McpErrorCodes.DevRequestNotAnalyzable, devRequest.Title ?? devRequest.Id.ToString(), devRequest.Status ?? string.Empty]);
} }
// Dokuman once yazilir: menu kodu gecersizse analiz de kaydedilmez, model kodu duzeltip tekrar dener.
await moduleDocAppService.SaveAnalysisAsync(new ModuleDocAnalysisInput
{
MenuCode = menuCode,
DevRequestId = devRequest.Id,
DevRequestTitle = devRequest.Title,
Html = html,
});
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);

View file

@ -57,7 +57,6 @@ public static class McpErrorCodes
public const string DevRequestNotReady = Prefix + "DevRequestNotReady"; public const string DevRequestNotReady = Prefix + "DevRequestNotReady";
public const string DevRequestNotAnalyzable = Prefix + "DevRequestNotAnalyzable"; public const string DevRequestNotAnalyzable = Prefix + "DevRequestNotAnalyzable";
public const string DevRequestAnalysisRequired = Prefix + "DevRequestAnalysisRequired"; public const string DevRequestAnalysisRequired = Prefix + "DevRequestAnalysisRequired";
public const string DevRequestMenuCodeRequired = Prefix + "DevRequestMenuCodeRequired";
public const string DevRequestNoteRequired = Prefix + "DevRequestNoteRequired"; public const string DevRequestNoteRequired = Prefix + "DevRequestNoteRequired";
public const string DevRequestSubTasksRequired = Prefix + "DevRequestSubTasksRequired"; public const string DevRequestSubTasksRequired = Prefix + "DevRequestSubTasksRequired";
public const string DevRequestSubTasksInvalid = Prefix + "DevRequestSubTasksInvalid"; public const string DevRequestSubTasksInvalid = Prefix + "DevRequestSubTasksInvalid";

View file

@ -149,7 +149,6 @@ public static class McpToolNames
[DeleteWizard] = [PlatformConsts.AppCodes.Listforms.Wizard, PlatformConsts.AppCodes.Listforms.Wizards.Delete, PlatformConsts.AppCodes.SqlQueryManagers.Default], [DeleteWizard] = [PlatformConsts.AppCodes.Listforms.Wizard, PlatformConsts.AppCodes.Listforms.Wizards.Delete, PlatformConsts.AppCodes.SqlQueryManagers.Default],
[GetModuleDoc] = [PlatformConsts.AppCodes.Listforms.Wizard], [GetModuleDoc] = [PlatformConsts.AppCodes.Listforms.Wizard],
[SaveModuleDoc] = [PlatformConsts.AppCodes.Listforms.Wizard, PlatformConsts.AppCodes.Listforms.Wizards.Update], [SaveModuleDoc] = [PlatformConsts.AppCodes.Listforms.Wizard, PlatformConsts.AppCodes.Listforms.Wizards.Update],
[SaveDevRequestAnalysis] = [PlatformConsts.AppCodes.Listforms.Wizard],
}; };
/// <summary> /// <summary>

View file

@ -20,6 +20,10 @@ namespace Sozsoft.Mcp.Services;
/// sonrasi yazdigi yerlere (<c>sql/object/{Tablo}.sql</c> ve PostgreSQL esi <c>postgres/object/{Tablo}.sql</c>) yazilir; boylece tablo /// sonrasi yazdigi yerlere (<c>sql/object/{Tablo}.sql</c> ve PostgreSQL esi <c>postgres/object/{Tablo}.sql</c>) yazilir; boylece tablo
/// tasarimcida acilip duzenlenebilir ve veritabani sifirlandiginda seed ile geri gelir. /// tasarimcida acilip duzenlenebilir ve veritabani sifirlandiginda seed ile geri gelir.
/// <para> /// <para>
/// Veritabaninda calistirilmaz: tabloyu, wizard deploy'unun ekranini kurdugu gibi kullanicinin
/// calistirdigi DB Migrate kurar (<c>SqlDataSeeder</c>).
/// </para>
/// <para>
/// Yalnizca olusturma yapar. Var olan tabloya dokunulmaz — kolon ekleme/degistirme tasarimcinin /// Yalnizca olusturma yapar. Var olan tabloya dokunulmaz — kolon ekleme/degistirme tasarimcinin
/// isidir ve sema degisikligi kurallarina (kayipsiz ekleme, degistirme/dusurme yok) tabidir. /// isidir ve sema degisikligi kurallarina (kayipsiz ekleme, degistirme/dusurme yok) tabidir.
/// Kolon tipi ve varsayilan deger serbest metin degil, beyaz listeden gecer; istemci SQL yazamaz. /// Kolon tipi ve varsayilan deger serbest metin degil, beyaz listeden gecer; istemci SQL yazamaz.
@ -102,24 +106,22 @@ public class McpTableCreator(
context.ConnectionString, context.ConnectionString,
new Dictionary<string, object> { ["name"] = isPostgreSql ? $"public.\"{tableName}\"" : $"{SchemaName}.{tableName}" }); new Dictionary<string, object> { ["name"] = isPostgreSql ? $"public.\"{tableName}\"" : $"{SchemaName}.{tableName}" });
if (existing is not null and not DBNull) // Henuz migrate edilmemis bir create_table da tabloyu sahiplenmis sayilir; ikinci cagri onun
// scriptinin ustune yazmaz.
if (existing is not null and not DBNull || PostgreSqlTableScript.ReadSeedColumns(configuration, tableName).Count > 0)
{ {
throw new UserFriendlyException(localizer[McpErrorCodes.TableAlreadyExists, tableName]); throw new UserFriendlyException(localizer[McpErrorCodes.TableAlreadyExists, tableName]);
} }
// Dosya biçimi tasarımcınınkiyle aynı T-SQL'dir; PostgreSQL eşi ondan üretilir ve PG veri // Dosya biçimi tasarımcınınkiyle aynı T-SQL'dir; PostgreSQL eşi ondan üretilir.
// kaynağında çalıştırılan da o eştir.
var script = BuildScript(tableName, columns); var script = BuildScript(tableName, columns);
if (!PostgreSqlTableScript.TryConvert(script, out var postgreSqlScript)) if (!PostgreSqlTableScript.TryConvert(script, out _))
{ {
throw new InvalidOperationException($"PostgreSQL script could not be generated for {tableName}."); throw new InvalidOperationException($"PostgreSQL script could not be generated for {tableName}.");
} }
// GO yalnizca SSMS ayiricidir; tek batch oldugu icin calistirirken atilir, dosyada kalir. // Tablo veritabaninda kurulmaz: wizard deploy'u gibi yalnizca seed yazilir, kurulum kullanicinin
await context.Repository.ExecuteAsync( // calistirdigi DB Migrate'tedir (SqlDataSeeder).
isPostgreSql ? postgreSqlScript : string.Join('\n', script.Split('\n').Where(line => line.Trim() != "GO")),
context.ConnectionString);
var (seedFilePath, _) = PostgreSqlTableScript.WriteSeedFiles(configuration, tableName, script); var (seedFilePath, _) = PostgreSqlTableScript.WriteSeedFiles(configuration, tableName, script);
return new McpCreatedTable(tableName, script, seedFilePath); return new McpCreatedTable(tableName, script, seedFilePath);

View file

@ -38,7 +38,7 @@ public sealed class McpDevelopTools(IMcpDevelopAppService developAppService, IMc
=> await developAppService.GetDevGuideAsync(document, section); => await developAppService.GetDevGuideAsync(document, section);
[McpServerTool(Name = McpToolNames.CreateTable, Destructive = false, Idempotent = false)] [McpServerTool(Name = McpToolNames.CreateTable, Destructive = false, Idempotent = false)]
[Description("Ekranin baglanacagi yeni tabloyu SQL Query Manager tablo tasarimcisiyla ayni bicimde olusturur ve script'i seed dosyasina (sql/object) yazar. Id (uniqueidentifier, PK), TenantId ve tam audit kolonlari (CreationTime, CreatorId, LastModificationTime, LastModifierId, IsDeleted, DeletionTime, DeleterId) otomatik eklenir; columns icine yalnizca is kolonlarini yaz. Yalnizca yeni tablo olusturur: tablo varsa reddeder, kolon ekleme/degistirme tablo tasarimcisindan yapilir. SQL Server veri kaynaginda calisir. Ardindan deploy_wizard ile ekrani kur.")] [Description("Ekranin baglanacagi yeni tablonun script'ini SQL Query Manager tablo tasarimcisiyla ayni bicimde uretir ve seed dosyasina (sql/object + postgres/object) yazar; veritabaninda calistirmaz, tabloyu kullanicinin calistirdigi DB Migrate kurar. DB Migrate'i bekleme ve calistirmaya calisma; deploy_wizard kolonlari script'ten okur. Id (uniqueidentifier, PK), TenantId ve tam audit kolonlari (CreationTime, CreatorId, LastModificationTime, LastModifierId, IsDeleted, DeletionTime, DeleterId) otomatik eklenir; columns icine yalnizca is kolonlarini yaz. Yalnizca yeni tablo icindir: tablo ya da script'i varsa reddeder, kolon ekleme/degistirme tablo tasarimcisindan yapilir. Ardindan deploy_wizard ile ekrani kur.")]
public async Task<McpCreateTableResultDto> CreateTableAsync( public async Task<McpCreateTableResultDto> CreateTableAsync(
[Description("Tablo adi; harf/rakam/alt cizgi. Adlandirma icin get_dev_guide'daki tablo onek kuralina uy (ornegin Adm_T_...).")] [Description("Tablo adi; harf/rakam/alt cizgi. Adlandirma icin get_dev_guide'daki tablo onek kuralina uy (ornegin Adm_T_...).")]
string tableName, string tableName,
@ -117,10 +117,8 @@ public sealed class McpDevelopTools(IMcpDevelopAppService developAppService, IMc
[Description("Talebin Id'si (GUID) ya da tam basligi.")] [Description("Talebin Id'si (GUID) ya da tam basligi.")]
string request, string request,
[Description("Analizin tamami, HTML.")] [Description("Analizin tamami, HTML.")]
string html, string html)
[Description("Zorunlu. Talebin menu yeri: kok menunun altindaki dalin ya da ekranin kodu (App.Inventory.Definitions, App.Hr.LeaveRequest); analizdeki Menu yeri ile ayni. Menude henuz olmayan yeni kod da verilir. Analiz bu dalin modul dokumani bolumune ('2. Analiz ve kararlar') yazilir; kok menunun dosyasi (App.Inventory.md) ya da dalin bolumu yoksa acilir.")] => ToContent(await developAppService.SaveDevRequestAnalysisAsync(request, html));
string? menuCode = null)
=> ToContent(await developAppService.SaveDevRequestAnalysisAsync(request, menuCode, html));
[McpServerTool(Name = McpToolNames.AppendDevRequestNote, Destructive = false, Idempotent = false)] [McpServerTool(Name = McpToolNames.AppendDevRequestNote, Destructive = false, Idempotent = false)]
[Description("Talebin Analysis alaninin sonuna HTML ekler; var olan analize ve Description'a dokunmaz. Description talebin yazildigi haliyle kalir; uretim sonuc notunu (<h4>MCP</h4> + uretilenler, varsayimlar ve DB Migrate bekleyen wizard'lar) bu aracla ekle. Description'i ya da Analysis'i update_row ile yazma.")] [Description("Talebin Analysis alaninin sonuna HTML ekler; var olan analize ve Description'a dokunmaz. Description talebin yazildigi haliyle kalir; uretim sonuc notunu (<h4>MCP</h4> + uretilenler, varsayimlar ve DB Migrate bekleyen wizard'lar) bu aracla ekle. Description'i ya da Analysis'i update_row ile yazma.")]

View file

@ -34,12 +34,6 @@ public interface IModuleDocAppService : IApplicationService
/// bolum var). Bos liste talebin dokumantasyonunun tamam oldugunu gosterir. /// bolum var). Bos liste talebin dokumantasyonunun tamam oldugunu gosterir.
/// </summary> /// </summary>
Task<List<ModuleDocDto>> GetIncompleteForDevRequestAsync(Guid devRequestId); Task<List<ModuleDocDto>> GetIncompleteForDevRequestAsync(Guid devRequestId);
/// <summary>
/// Talep analizini dalin bolumune ("2. Analiz ve kararlar") yazar; bolum ya da dosya yoksa acar.
/// Elle yazilmis alt bolume dokunulmaz.
/// </summary>
Task<ModuleDocDto> SaveAnalysisAsync(ModuleDocAnalysisInput input);
} }
public class ModuleDocDto public class ModuleDocDto
@ -80,16 +74,3 @@ public class ModuleDocSaveInput
/// <summary>Markdown. Otomatik envanter bolumu varsa yok sayilir.</summary> /// <summary>Markdown. Otomatik envanter bolumu varsa yok sayilir.</summary>
public string Content { get; set; } = string.Empty; public string Content { get; set; } = string.Empty;
} }
public class ModuleDocAnalysisInput
{
/// <summary>Talebin menu yeri: dalin ya da ekranin kodu (<c>App.Inventory.Definitions</c>).</summary>
public string MenuCode { get; set; } = string.Empty;
public Guid DevRequestId { get; set; }
public string? DevRequestTitle { get; set; }
/// <summary>Analiz, HTML.</summary>
public string Html { get; set; } = string.Empty;
}

View file

@ -3075,12 +3075,14 @@ public class ListFormWizardAppService(
: null; : null;
} }
} }
return columns; // MCP create_table tabloyu yalnizca seed scripti olarak yazar; DB Migrate kurana kadar
// kolonlar oradan okunur, yoksa ekran audit/soft delete bayraklari olmadan uretilirdi.
return columns.Count > 0 ? columns : PostgreSqlTableScript.ReadSeedColumns(_configuration, tableName);
} }
catch (Exception ex) catch (Exception ex)
{ {
logger.LogWarning(ex, "Table columns could not be read for {DataSourceCode}/{SelectCommand}", dataSourceCode, selectCommand); logger.LogWarning(ex, "Table columns could not be read for {DataSourceCode}/{SelectCommand}", dataSourceCode, selectCommand);
return []; return PostgreSqlTableScript.ReadSeedColumns(_configuration, tableName);
} }
} }

View file

@ -28,13 +28,4 @@ public class ModuleDocAppService(ModuleDocManager manager) : PlatformAppService,
public Task<List<ModuleDocDto>> GetIncompleteForDevRequestAsync(Guid devRequestId) => public Task<List<ModuleDocDto>> GetIncompleteForDevRequestAsync(Guid devRequestId) =>
manager.GetIncompleteForDevRequestAsync(devRequestId); manager.GetIncompleteForDevRequestAsync(devRequestId);
/// <summary>
/// Talep analizinin parcasidir; talebi analiz edebilen (Wizard ekranini okuyabilen) kullanici
/// dokumanin iskeletini ve analiz alt bolumunu yazabilir, diger bolumler icin <see cref="SaveAsync"/> gerekir.
/// </summary>
public Task<ModuleDocDto> SaveAnalysisAsync(ModuleDocAnalysisInput input)
{
Check.NotNull(input, nameof(input));
return manager.SaveAnalysisAsync(input.MenuCode, input.DevRequestId, input.DevRequestTitle, input.Html);
}
} }

View file

@ -24,9 +24,6 @@ public class ModuleDocManager(
{ {
private static readonly UTF8Encoding Utf8NoBom = new(encoderShouldEmitUTF8Identifier: false); private static readonly UTF8Encoding Utf8NoBom = new(encoderShouldEmitUTF8Identifier: false);
/// <summary>Analizin yazdigi alt bolumun isareti; isaret tasiyan govde sonraki analizde yenilenir.</summary>
private const string AnalysisMarker = "<!-- module-doc:analysis";
private string ScopeRoot => Path.GetFullPath(SeedPathResolver.GetRootPath(configuration)); private string ScopeRoot => Path.GetFullPath(SeedPathResolver.GetRootPath(configuration));
public Task<ModuleDocCatalog> LoadCatalogAsync() => ModuleDocCatalog.LoadAsync(ScopeRoot); public Task<ModuleDocCatalog> LoadCatalogAsync() => ModuleDocCatalog.LoadAsync(ScopeRoot);
@ -136,75 +133,6 @@ public class ModuleDocManager(
: manual[..next.Start] + text + "\n" + manual[next.Start..]; : manual[..next.Start] + text + "\n" + manual[next.Start..];
} }
/// <summary>
/// Talep analizini modulun dokumanina tasir: dalin bolumu yoksa (dosyasi dahil) acilir, analiz
/// "2. Analiz ve kararlar" alt bolumune yazilir. Alt bolum elle yazilmissa korunur; yalnizca
/// doldurulmamis ya da daha once bir analizle yazilmis alt bolum yenilenir.
/// </summary>
public async Task<ModuleDocDto> SaveAnalysisAsync(string code, Guid devRequestId, string? title, string html)
{
var catalog = await LoadCatalogAsync();
var (name, sectionKey) = catalog.Find(code) ?? throw new UserFriendlyException($"Geçersiz menü kodu: {code}");
if (sectionKey == null)
throw new UserFriendlyException($"Analizin yazılacağı dal belirsiz: {code}. Kök menünün altındaki dalın ya da ekranın kodunu verin.");
var path = catalog.DocPath(name);
var manual = File.Exists(path)
? ModuleDocCatalog.StripAuto(Normalize(await File.ReadAllTextAsync(path)))
: ModuleDocRenderer.DocSkeleton(catalog.MenuTitle(name) ?? name, name);
var group = catalog.GroupByDoc().GetValueOrDefault(name);
var section = ModuleDocCatalog.ParseSections(manual).FirstOrDefault(s => string.Equals(s.Key, sectionKey, StringComparison.OrdinalIgnoreCase));
if (section == null)
{
var seeds = group?.Sections.GetValueOrDefault(sectionKey)?.Select(w => w.Seed) ?? [];
manual = InsertSection(catalog, manual, sectionKey, ModuleDocRenderer.SectionSkeleton(ModuleDocRenderer.Title(catalog, seeds, sectionKey), sectionKey));
section = ModuleDocCatalog.ParseSections(manual).First(s => string.Equals(s.Key, sectionKey, StringComparison.OrdinalIgnoreCase));
}
var body = $"{AnalysisMarker} {devRequestId} -->\n_Talep: {title}_\n\n{html.Trim()}\n";
var updated = ReplaceSubSection(section.Text, ModuleDocRenderer.Sections[1], body);
if (updated != null)
manual = manual[..section.Start] + updated + (section.End < manual.Length ? "\n" + manual[section.End..].TrimStart('\n') : string.Empty);
Directory.CreateDirectory(catalog.DocsPath);
await File.WriteAllTextAsync(path, manual.TrimEnd() + "\n", Utf8NoBom);
catalog = await LoadCatalogAsync();
var groups = catalog.GroupByDoc();
await RefreshAsync(catalog, name, groups.GetValueOrDefault(name));
return await ToDtoAsync(catalog, name, sectionKey, groups, includeContent: false);
}
/// <summary>
/// Bolumdeki <c>### {heading}</c> alt bolumunun govdesini degistirir. Govde doldurulmamissa ya da
/// bir analizle yazilmissa yenilenir, elle yazilmissa null doner (dokunulmaz).
/// </summary>
private static string? ReplaceSubSection(string sectionText, string heading, string body)
{
var lines = sectionText.TrimEnd('\n').Split('\n').ToList();
var start = lines.FindIndex(l => l.StartsWith("### ", StringComparison.Ordinal) && l[4..].Trim() == heading);
if (start < 0)
return sectionText.TrimEnd() + $"\n\n### {heading}\n\n{body}";
var end = lines.FindIndex(start + 1, l => l.StartsWith("### ", StringComparison.Ordinal));
if (end < 0)
end = lines.Count;
var current = string.Join('\n', lines.Skip(start + 1).Take(end - start - 1));
if (!current.Contains(ModuleDocCatalog.TodoMarker, StringComparison.Ordinal) && !current.Contains(AnalysisMarker, StringComparison.Ordinal))
return null;
lines.RemoveRange(start + 1, end - start - 1);
lines.Insert(start + 1, string.Empty);
lines.Insert(start + 2, body.TrimEnd('\n'));
if (start + 3 < lines.Count)
lines.Insert(start + 3, string.Empty);
return string.Join('\n', lines) + "\n";
}
private static string RootTitle(ModuleDocCatalog catalog, ModuleDocCatalog.DocGroup group) => private static string RootTitle(ModuleDocCatalog catalog, ModuleDocCatalog.DocGroup group) =>
ModuleDocRenderer.Title(catalog, group.All.Select(w => w.Seed), group.RootCode); ModuleDocRenderer.Title(catalog, group.All.Select(w => w.Seed), group.RootCode);

View file

@ -49,6 +49,60 @@ public static partial class PostgreSqlTableScript
return (sqlServerPath, $"{SeedPathResolver.PostgresFolder}/{SeedPathResolver.ObjectFolder}/{fileName}"); return (sqlServerPath, $"{SeedPathResolver.PostgresFolder}/{SeedPathResolver.ObjectFolder}/{fileName}");
} }
/// <summary>
/// Henüz DB Migrate'te kurulmamış tablonun kolonlarını SQL Server seed scriptinden
/// (<c>sql/object/{Tablo}.sql</c>) okur: ad ve metin kolonunun karakter sınırı (<c>MAX</c> için -1,
/// <c>INFORMATION_SCHEMA</c> ile aynı). Dosya yoksa ya da <c>CREATE TABLE</c> tanınmazsa boş döner.
/// </summary>
public static Dictionary<string, int?> ReadSeedColumns(IConfiguration? configuration, string tableName)
{
var columns = new Dictionary<string, int?>(StringComparer.OrdinalIgnoreCase);
var path = Path.Combine(
SeedPathResolver.GetPath(configuration, SeedPathResolver.SqlFolder, SeedPathResolver.ObjectFolder),
$"{tableName}.sql");
if (!File.Exists(path))
return columns;
var inCreate = false;
foreach (var line in File.ReadLines(path))
{
if (!inCreate)
{
inCreate = CreateTableLineRegex().IsMatch(line);
continue;
}
// Kolon listesi ilk constraint ya da kapanan parantezle biter; PK'nin [Id] ASC satiri kolon degildir.
if (SeedColumnEndRegex().IsMatch(line))
break;
var match = SeedColumnLineRegex().Match(line);
if (!match.Success)
continue;
var size = match.Groups["size"].Value.Trim();
columns[match.Groups["name"].Value] = size.Length == 0
? null
: size.Equals("MAX", StringComparison.OrdinalIgnoreCase)
? -1
: int.TryParse(size, out var length) && match.Groups["type"].Value.Contains("char", StringComparison.OrdinalIgnoreCase)
? length
: null;
}
return columns;
}
[GeneratedRegex(@"^\s*CREATE\s+TABLE\b", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)]
private static partial Regex CreateTableLineRegex();
[GeneratedRegex(@"^\s*(CONSTRAINT\b|\))", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)]
private static partial Regex SeedColumnEndRegex();
[GeneratedRegex(@"^\s*\[(?<name>[^\]]+)\]\s+\[?(?<type>[A-Za-z0-9]+)\]?\s*(?:\(\s*(?<size>[^)]*)\))?", RegexOptions.CultureInvariant)]
private static partial Regex SeedColumnLineRegex();
public static bool TryConvert(string sqlServerScript, out string postgreSqlScript) public static bool TryConvert(string sqlServerScript, out string postgreSqlScript)
{ {
postgreSqlScript = string.Empty; postgreSqlScript = string.Empty;

View file

@ -39,8 +39,6 @@ public static class SetupMigrationRunner
/// <summary>Alt surece seed klasorunu bildiren ortam degiskeni (App:SeedsPath karsiligi).</summary> /// <summary>Alt surece seed klasorunu bildiren ortam degiskeni (App:SeedsPath karsiligi).</summary>
private const string SeedsPathEnvironmentVariable = "App__SeedsPath"; private const string SeedsPathEnvironmentVariable = "App__SeedsPath";
private const string BuildStampFileName = "setup-migrator.stamp";
/// <summary> /// <summary>
/// Kaynak projeden çalışırken derleme çıktısını eskiten dosyalar. Seed klasörü çalışma anında /// Kaynak projeden çalışırken derleme çıktısını eskiten dosyalar. Seed klasörü çalışma anında
/// okunduğu için burada değildir. /// okunduğu için burada değildir.
@ -115,7 +113,7 @@ public static class SetupMigrationRunner
{ {
if (command!.ProjectPath is not null) if (command!.ProjectPath is not null)
{ {
var migratorDll = await PrepareSourceMigratorAsync(command.ProjectPath, stream, token); var migratorDll = await FindSourceMigratorAsync(command.ProjectPath, stream);
if (migratorDll is null) if (migratorDll is null)
{ {
await stream.DoneAsync("Failed."); await stream.DoneAsync("Failed.");
@ -183,59 +181,26 @@ public static class SetupMigrationRunner
} }
/// <summary> /// <summary>
/// Kaynak projeden çalışırken migrator DLL'ini hazırlar. <c>dotnet run</c> her seferinde /// Kaynak projeden çalışırken var olan derleme çıktısını kullanır. Migrate derleme yapmaz: istek
/// çıktısını gizleyerek derler; değişiklik yokken bile bu ~25 sn sessiz bekleme demektir. /// sırasında restore/build hem süreyi uzatır hem de çalışan Host'un kilitlediği dosyalarla
/// DLL kaynaklardan yeniyse derleme atlanır, değilse derleme çıktısı kurulum loguna akar. /// çakışır. Derleme geliştiricinin işidir; DLL kaynaklardan eskiyse yalnızca uyarılır.
/// </summary> /// </summary>
private static async Task<string?> PrepareSourceMigratorAsync( private static async Task<string?> FindSourceMigratorAsync(string projectPath, SetupEventStream stream)
string projectPath,
SetupEventStream stream,
CancellationToken token)
{ {
// MSBuild değişmeyen çıktıyı yeniden yazmaz; derlemeye girmeyen bir dosya değiştiğinde DLL'in
// tarihi eski kalır ve her çalıştırma yeniden derlerdi. Son başarılı derlemenin anı damgadadır.
var stampPath = Path.Combine(projectPath, "obj", BuildStampFileName);
var migratorDll = FindBuiltMigrator(projectPath); var migratorDll = FindBuiltMigrator(projectPath);
if (migratorDll is not null && IsUpToDate(projectPath, LastBuildTimeUtc(migratorDll, stampPath)))
{
await stream.InfoAsync("DbMigrator is up to date, build skipped.");
return migratorDll;
}
await stream.InfoAsync("Building DbMigrator...");
var exitCode = await RunProcessAsync(
new MigratorCommand("dotnet", $"build \"{projectPath}\" -nologo -v:m", projectPath),
null,
stream,
token);
if (exitCode != 0)
{
await stream.ErrorAsync($"DbMigrator build failed. Exit code: {exitCode}");
return null;
}
migratorDll = FindBuiltMigrator(projectPath);
if (migratorDll is null) if (migratorDll is null)
{ {
await stream.ErrorAsync($"DbMigrator build output not found under {projectPath}."); await stream.ErrorAsync($"DbMigrator build output not found. Build it first: dotnet build \"{projectPath}\"");
return null; return null;
} }
await File.WriteAllTextAsync(stampPath, string.Empty, token); if (!IsUpToDate(projectPath, File.GetLastWriteTimeUtc(migratorDll)))
{
return migratorDll; await stream.SendAsync(SetupEventLevels.Warn,
"DbMigrator sources changed after its last build; running the existing build. Rebuild DbMigrator to apply code changes.");
} }
private static DateTime LastBuildTimeUtc(string migratorDll, string stampPath) return migratorDll;
{
var builtAt = File.GetLastWriteTimeUtc(migratorDll);
return File.Exists(stampPath) && File.GetLastWriteTimeUtc(stampPath) > builtAt
? File.GetLastWriteTimeUtc(stampPath)
: builtAt;
} }
/// <summary> /// <summary>
@ -421,7 +386,7 @@ public static class SetupMigrationRunner
command = dllFiles.Length > 0 command = dllFiles.Length > 0
// Publish çıktısı — SDK gerekmez. // Publish çıktısı — SDK gerekmez.
? new MigratorCommand("dotnet", $"\"{dllFiles[0]}\" {extraArgs}", migratorPath) ? new MigratorCommand("dotnet", $"\"{dllFiles[0]}\" {extraArgs}", migratorPath)
// Kaynak proje klasörü — DLL çalıştırmadan önce PrepareSourceMigratorAsync ile hazırlanır. // Kaynak proje klasörü — derlenmiş DLL FindSourceMigratorAsync ile bulunur, derlenmez.
: new MigratorCommand("dotnet", extraArgs, migratorPath, migratorPath); : new MigratorCommand("dotnet", extraArgs, migratorPath, migratorPath);
return true; return true;
} }

View file

@ -338,9 +338,9 @@ Bir çelişki varsa sıra: `ai.instructions.md` → `dotnet.instructions.md` →
- **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`
ile yalnızca `Backlog`/`Analysis` talebini analiz edip `Analysis`'e alır (ekran/tablo üretmez), ile yalnızca `Backlog`/`Analysis` talebini analiz edip `Analysis`'e alır (ekran/tablo üretmez,
analizi `menuCode` dalının modül dokümanı bölümüne ("2. Analiz ve kararlar") de yazar — menüde ilk modül dokümanına yazmaz — dosya ve dal bölümü üretimde, deploy ile açılır; onaylanan analiz bölümün
talepse kök menünün dosyası (`App.Inventory.md`) ve dal bölümü burada açılır, elle yazılmış alt bölüme dokunulmaz; "2. Analiz ve kararlar" alt bölümüne üretimde `save_module_doc` ile yazılır);
`Ready to Start`'a kullanıcı taşır, üretim notu `append_dev_request_note` ile `Analysis`'e eklenir. `Ready to Start`'a kullanıcı taşır, üretim notu `append_dev_request_note` ile `Analysis`'e eklenir.
Analiz standardı tek yerdedir: `lowcode.instructions.md` §0.12; prompt'lar oraya yönlendirir. Analiz standardı tek yerdedir: `lowcode.instructions.md` §0.12; prompt'lar oraya yönlendirir.
- **MCP şema araçları** → tablo/sütun bilgisi canlı katalogdan değil `Sas_H_DbTableColumn` - **MCP şema araçları** → tablo/sütun bilgisi canlı katalogdan değil `Sas_H_DbTableColumn`
@ -362,7 +362,9 @@ Bir çelişki varsa sıra: `ai.instructions.md` → `dotnet.instructions.md` →
yetkisini aşan bir yetenek (ham SQL gibi) `McpToolNames.RequiredPermissions`'a uygulamadaki yetkisini aşan bir yetenek (ham SQL gibi) `McpToolNames.RequiredPermissions`'a uygulamadaki
karşılık gelen yetkiyle eklenir. `create_table` bu yüzden `.Write`'a ek olarak `App.SqlQueryManager` karşılık gelen yetkiyle eklenir. `create_table` bu yüzden `.Write`'a ek olarak `App.SqlQueryManager`
ister; tablo scriptini tasarımcının `generateCreateTableSql` çıktısıyla aynı biçimde üretir ister; tablo scriptini tasarımcının `generateCreateTableSql` çıktısıyla aynı biçimde üretir
(`McpTableCreator`) — tasarımcının şablon kolonları ya da script biçimi değişirse o da değişir. Tasarımcı (`McpTableCreator`) ve **veritabanında çalıştırmaz** — MCP veritabanı kurmaz, tabloyu da ekran gibi
kullanıcının çalıştırdığı DB Migrate kurar; wizard deploy'u kurulmamış tablonun kolonlarını seed
scriptinden okur (`PostgreSqlTableScript.ReadSeedColumns`) — tasarımcının şablon kolonları ya da script biçimi değişirse o da değişir. Tasarımcı
(`SaveTableScriptAsync`) ve `create_table` T-SQL dosyanın PostgreSQL eşini `PostgreSqlTableScript` ile (`SaveTableScriptAsync`) ve `create_table` T-SQL dosyanın PostgreSQL eşini `PostgreSqlTableScript` ile
üretip `postgres/object/` altına yazar; script biçimine yeni bir deyim eklenirse çevirisi de eklenir, üretip `postgres/object/` altına yazar; script biçimine yeni bir deyim eklenirse çevirisi de eklenir,
yoksa PG eşi yazılmaz (log uyarısı). Düşürülen nesnenin dosyası iki klasörden de silinir. yoksa PG eşi yazılmaz (log uyarısı). Düşürülen nesnenin dosyası iki klasörden de silinir.

View file

@ -17,7 +17,7 @@
"Assignees": "system@sozsoft.com", "Assignees": "system@sozsoft.com",
"Priority": null, "Priority": null,
"Completed": false, "Completed": false,
"SortOrder": 1, "SortOrder": 0,
"Timesheets": null, "Timesheets": null,
"SubTasks": null, "SubTasks": null,
"Analysis": null "Analysis": null

File diff suppressed because one or more lines are too long

View file

@ -17,7 +17,7 @@
"Assignees": null, "Assignees": null,
"Priority": null, "Priority": null,
"Completed": false, "Completed": false,
"SortOrder": 2, "SortOrder": 1,
"Timesheets": null, "Timesheets": null,
"SubTasks": null, "SubTasks": null,
"Analysis": null "Analysis": null

View file

@ -211,9 +211,7 @@ const TodoBoard = (props: TodoBoardProps) => {
// Alt form olarak açıldığında tanım dışarıdan gelmez; kendisi yükler. // Alt form olarak açıldığında tanım dışarıdan gelmez; kendisi yükler.
const gridDto = useListFormGridDto(listFormCode, extGridDto) const gridDto = useListFormGridDto(listFormCode, extGridDto)
return gridDto ? ( return gridDto ? <TodoBoardContent {...props} gridDto={gridDto} /> : null
<TodoBoardContent {...props} gridDto={gridDto} />
) : null
} }
const TodoBoardContent = ({ const TodoBoardContent = ({
@ -262,11 +260,14 @@ const TodoBoardContent = ({
const loadLookupDisplayValuesRef = useRef(loadLookupDisplayValues) const loadLookupDisplayValuesRef = useRef(loadLookupDisplayValues)
getBandedColumnsRef.current = getBandedColumns getBandedColumnsRef.current = getBandedColumns
loadLookupDisplayValuesRef.current = loadLookupDisplayValues loadLookupDisplayValuesRef.current = loadLookupDisplayValues
const listFormColumnsCacheRef = useRef<{ const listFormColumnsCacheRef = useRef<
| {
gridDto?: GridDto gridDto?: GridDto
listFormCode: string listFormCode: string
columns: GridColumnData[] columns: GridColumnData[]
} | undefined>(undefined) }
| undefined
>(undefined)
if ( if (
!listFormColumnsCacheRef.current || !listFormColumnsCacheRef.current ||
listFormColumnsCacheRef.current.gridDto !== gridDto || listFormColumnsCacheRef.current.gridDto !== gridDto ||
@ -998,6 +999,26 @@ const TodoBoardContent = ({
} }
} }
// Dinleyici kayit acikken bir kez baglanir; taslagin guncel halini ref uzerinden okur.
const saveTodoRef = useRef(saveTodo)
saveTodoRef.current = saveTodo
const canSaveDraft =
canUpdate && !saving && Boolean(String(draft[options.titleExpr ?? ''] ?? '').trim())
const canSaveDraftRef = useRef(canSaveDraft)
canSaveDraftRef.current = canSaveDraft
const hasSelected = Boolean(selected)
useEffect(() => {
if (!hasSelected || !canUpdate) return
const handleKeyDown = (event: KeyboardEvent) => {
if (!(event.ctrlKey || event.metaKey) || event.key.toLowerCase() !== 's') return
event.preventDefault()
if (canSaveDraftRef.current) void saveTodoRef.current()
}
window.addEventListener('keydown', handleKeyDown, true)
return () => window.removeEventListener('keydown', handleKeyDown, true)
}, [hasSelected, canUpdate])
const updateTodoInline = async (row: TodoRow, values: Record<string, any>) => { const updateTodoInline = async (row: TodoRow, values: Record<string, any>) => {
if (!canUpdate) return if (!canUpdate) return
const updated = { ...row, ...values } const updated = { ...row, ...values }
@ -1055,7 +1076,9 @@ const TodoBoardContent = ({
? { key: 'subtask' as DetailTab, label: translate('::App.ListFormTodoBoard.SubTasks') } ? { key: 'subtask' as DetailTab, label: translate('::App.ListFormTodoBoard.SubTasks') }
: undefined, : undefined,
].filter(Boolean) as Array<{ key: DetailTab; label: string }> ].filter(Boolean) as Array<{ key: DetailTab; label: string }>
const activeTab = detailTabs.some((tab) => tab.key === detailTab) ? detailTab : detailTabs[0]?.key const activeTab = detailTabs.some((tab) => tab.key === detailTab)
? detailTab
: detailTabs[0]?.key
const timesheetRows = parseJsonRows(fieldValue(draft, options.timesheetExpr)) const timesheetRows = parseJsonRows(fieldValue(draft, options.timesheetExpr))
const subTaskRows = parseJsonRows(fieldValue(draft, options.subTaskExpr)) const subTaskRows = parseJsonRows(fieldValue(draft, options.subTaskExpr))
const timesheetTotal = timesheetRows.reduce( const timesheetTotal = timesheetRows.reduce(
@ -1066,39 +1089,18 @@ const TodoBoardContent = ({
return ( return (
<section className="min-h-[420px] rounded-xl"> <section className="min-h-[420px] rounded-xl">
{renderToolbar(true)} {renderToolbar(true)}
<div className="mb-5 flex flex-wrap items-center justify-end gap-2 border-b border-gray-200 pb-3 dark:border-gray-700">
{editingOptions?.allowUpdating === true && !gridDto.gridOptions.updateServiceAddress && ( {editingOptions?.allowUpdating === true && !gridDto.gridOptions.updateServiceAddress && (
<span className="mr-auto text-xs text-amber-600"> <div className="mb-5 border-b border-gray-200 pb-3 text-xs text-amber-600 dark:border-gray-700">
{translate('::App.ListFormTodoBoard.EditRequiresUpdateService')} {translate('::App.ListFormTodoBoard.EditRequiresUpdateService')}
</span>
)}
{options.statusExpr && (
<div className="flex flex-wrap items-center justify-end gap-1.5">
{statuses.map((status) => {
const isActiveStatus = String(draft[options.statusExpr!] ?? '') === status
return (
<button
key={status}
type="button"
disabled={!canUpdate}
title={statusText(status)}
className={`rounded-full border px-4 py-1.5 text-xs font-semibold transition disabled:cursor-not-allowed disabled:opacity-60 ${isActiveStatus ? 'border-emerald-500 bg-emerald-500 text-white shadow-sm' : 'border-gray-300 bg-white text-gray-600 hover:border-emerald-400 hover:text-emerald-600 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-300'}`}
onClick={() =>
setDraft((current) => ({ ...current, [options.statusExpr!]: status }))
}
>
{statusText(status)}
</button>
)
})}
</div> </div>
)} )}
</div>
<div className="grid items-stretch gap-5 lg:grid-cols-3"> <div className="grid items-stretch gap-5 lg:grid-cols-3">
<div className="flex flex-col gap-5 lg:col-span-2"> <div className="flex flex-col gap-5 lg:col-span-2">
{options.titleExpr && ( {options.titleExpr && (
<div> <div>
<label className={labelClass}>{translate('::App.Listform.ListformField.Title')}</label> <label className={labelClass}>
{translate('::App.Listform.ListformField.Title')}
</label>
<Input <Input
unstyle unstyle
className={`${inputClass} text-lg font-semibold`} className={`${inputClass} text-lg font-semibold`}
@ -1270,7 +1272,10 @@ const TodoBoardContent = ({
))} ))}
{!timesheetRows.length && ( {!timesheetRows.length && (
<tr> <tr>
<td colSpan={5} className="px-3 py-6 text-center text-xs text-gray-400"> <td
colSpan={5}
className="px-3 py-6 text-center text-xs text-gray-400"
>
{translate('::App.ListFormTodoBoard.NoRows')} {translate('::App.ListFormTodoBoard.NoRows')}
</td> </td>
</tr> </tr>
@ -1399,7 +1404,9 @@ const TodoBoardContent = ({
type="date" type="date"
className={cellInputClass} className={cellInputClass}
disabled={!canUpdate} disabled={!canUpdate}
value={row.dueDate ? dayjs(row.dueDate).format('YYYY-MM-DD') : ''} value={
row.dueDate ? dayjs(row.dueDate).format('YYYY-MM-DD') : ''
}
onChange={(event) => onChange={(event) =>
updateJsonRow(options.subTaskExpr, subTaskRows, index, { updateJsonRow(options.subTaskExpr, subTaskRows, index, {
dueDate: event.target.value || null, dueDate: event.target.value || null,
@ -1428,7 +1435,10 @@ const TodoBoardContent = ({
))} ))}
{!subTaskRows.length && ( {!subTaskRows.length && (
<tr> <tr>
<td colSpan={5} className="px-3 py-6 text-center text-xs text-gray-400"> <td
colSpan={5}
className="px-3 py-6 text-center text-xs text-gray-400"
>
{translate('::App.ListFormTodoBoard.NoRows')} {translate('::App.ListFormTodoBoard.NoRows')}
</td> </td>
</tr> </tr>
@ -1460,6 +1470,29 @@ const TodoBoardContent = ({
</div> </div>
)} )}
</div> </div>
{/* Uzun aciklamada da durumlar, alanlar ve kaydet butonlari gorunur kalsin; top, sabit ust bilginin (h-16) altindan baslar. */}
<div className="flex flex-col gap-4 self-start lg:sticky lg:top-24 lg:max-h-[calc(100vh-7rem)] lg:overflow-y-auto">
{options.statusExpr && (
<div className="flex flex-wrap items-center gap-1.5">
{statuses.map((status) => {
const isActiveStatus = String(draft[options.statusExpr!] ?? '') === status
return (
<button
key={status}
type="button"
disabled={!canUpdate}
title={statusText(status)}
className={`rounded-full border px-4 py-1.5 text-xs font-semibold transition disabled:cursor-not-allowed disabled:opacity-60 ${isActiveStatus ? 'border-emerald-500 bg-emerald-500 text-white shadow-sm' : 'border-gray-300 bg-white text-gray-600 hover:border-emerald-400 hover:text-emerald-600 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-300'}`}
onClick={() =>
setDraft((current) => ({ ...current, [options.statusExpr!]: status }))
}
>
{statusText(status)}
</button>
)
})}
</div>
)}
<aside className="space-y-4 rounded-xl bg-gray-50 p-4 dark:bg-gray-800/60"> <aside className="space-y-4 rounded-xl bg-gray-50 p-4 dark:bg-gray-800/60">
{selectedTenantId && ( {selectedTenantId && (
<div> <div>
@ -1473,9 +1506,7 @@ const TodoBoardContent = ({
</div> </div>
)} )}
<div> <div>
<label className={labelClass}> <label className={labelClass}>{translate('::Abp.Mailing.Smtp.UserName')}</label>
{translate('::Abp.Mailing.Smtp.UserName')}
</label>
<Input <Input
unstyle unstyle
readOnly readOnly
@ -1517,7 +1548,9 @@ const TodoBoardContent = ({
)} )}
{options.tagExpr && ( {options.tagExpr && (
<div> <div>
<label className={labelClass}>{translate('::App.Listform.ListformField.Tags')}</label> <label className={labelClass}>
{translate('::App.Listform.ListformField.Tags')}
</label>
<TagBox <TagBox
acceptCustomValue acceptCustomValue
searchEnabled searchEnabled
@ -1545,7 +1578,9 @@ const TodoBoardContent = ({
)} )}
{options.dueDateExpr && ( {options.dueDateExpr && (
<div> <div>
<label className={labelClass}>{translate('::App.ListFormTodoBoard.DueDate')}</label> <label className={labelClass}>
{translate('::App.ListFormTodoBoard.DueDate')}
</label>
<Input <Input
unstyle unstyle
type="datetime-local" type="datetime-local"
@ -1567,7 +1602,9 @@ const TodoBoardContent = ({
)} )}
{options.priorityExpr && ( {options.priorityExpr && (
<div> <div>
<label className={labelClass}>{translate('::App.Listform.ListformField.Priority')}</label> <label className={labelClass}>
{translate('::App.Listform.ListformField.Priority')}
</label>
<div className="rounded-lg border border-gray-300 bg-white px-3 py-2 dark:border-gray-600 dark:bg-gray-800"> <div className="rounded-lg border border-gray-300 bg-white px-3 py-2 dark:border-gray-600 dark:bg-gray-800">
<Rate <Rate
allowClear allowClear
@ -1615,9 +1652,8 @@ const TodoBoardContent = ({
</label> </label>
)} )}
</aside> </aside>
</div>
{(canUpdate || canDelete) && ( {(canUpdate || canDelete) && (
<div className="mt-6 flex items-center justify-between gap-3 border-t border-gray-200 pt-4 dark:border-gray-700"> <div className="flex flex-wrap-reverse items-center justify-between gap-3">
<div> <div>
{canDelete && ( {canDelete && (
<button <button
@ -1645,7 +1681,9 @@ const TodoBoardContent = ({
disabled={saving || !String(draft[options.titleExpr ?? ''] ?? '').trim()} disabled={saving || !String(draft[options.titleExpr ?? ''] ?? '').trim()}
onClick={() => saveTodo()} onClick={() => saveTodo()}
> >
{saving ? translate('::App.Platform.Saving') : translate('::App.Platform.Save')} {saving
? translate('::App.Platform.Saving')
: translate('::App.Platform.Save')}
</button> </button>
<button <button
type="button" type="button"
@ -1653,12 +1691,16 @@ const TodoBoardContent = ({
disabled={saving || !String(draft[options.titleExpr ?? ''] ?? '').trim()} disabled={saving || !String(draft[options.titleExpr ?? ''] ?? '').trim()}
onClick={() => saveTodo(true)} onClick={() => saveTodo(true)}
> >
{saving ? translate('::App.Platform.Saving') : translate('::App.ListFormTodoBoard.SaveAndExit')} {saving
? translate('::App.Platform.Saving')
: translate('::App.ListFormTodoBoard.SaveAndExit')}
</button> </button>
</div> </div>
)} )}
</div> </div>
)} )}
</div>
</div>
</section> </section>
) )
} }
@ -1715,7 +1757,9 @@ const TodoBoardContent = ({
) )
}} }}
> >
<div className={`sticky top-0 z-10 -mx-2 -mt-2 rounded-t-xl px-2 pb-1 pt-2 ${columnHeaderBgClass}`}> <div
className={`sticky top-0 z-10 -mx-2 -mt-2 rounded-t-xl px-2 pb-1 pt-2 ${columnHeaderBgClass}`}
>
<header className="mb-2 flex min-h-9 items-center gap-2 px-1"> <header className="mb-2 flex min-h-9 items-center gap-2 px-1">
<span <span
draggable={canUpdate} draggable={canUpdate}