Mcp problemi giderildi
This commit is contained in:
parent
53e26ce787
commit
02b8d3abb4
19 changed files with 527 additions and 579 deletions
8
.github/instructions/lowcode.instructions.md
vendored
8
.github/instructions/lowcode.instructions.md
vendored
|
|
@ -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ı
|
||||||
|
|
|
||||||
12
README.md
12
README.md
|
|
@ -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` |
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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";
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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.")]
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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,61 +181,28 @@ 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)))
|
||||||
|
{
|
||||||
|
await stream.SendAsync(SetupEventLevels.Warn,
|
||||||
|
"DbMigrator sources changed after its last build; running the existing build. Rebuild DbMigrator to apply code changes.");
|
||||||
|
}
|
||||||
|
|
||||||
return migratorDll;
|
return migratorDll;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static DateTime LastBuildTimeUtc(string migratorDll, string stampPath)
|
|
||||||
{
|
|
||||||
var builtAt = File.GetLastWriteTimeUtc(migratorDll);
|
|
||||||
|
|
||||||
return File.Exists(stampPath) && File.GetLastWriteTimeUtc(stampPath) > builtAt
|
|
||||||
? File.GetLastWriteTimeUtc(stampPath)
|
|
||||||
: builtAt;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Debug derleme çıktısındaki migrator DLL'i; birden çok hedef çerçeve klasörü varsa en yenisi
|
/// Debug derleme çıktısındaki migrator DLL'i; birden çok hedef çerçeve klasörü varsa en yenisi
|
||||||
/// (eski çerçevenin kalıntısı seçilmez).
|
/// (eski çerçevenin kalıntısı seçilmez).
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
10
claude.md
10
claude.md
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
| {
|
||||||
listFormCode: string
|
gridDto?: GridDto
|
||||||
columns: GridColumnData[]
|
listFormCode: string
|
||||||
} | undefined>(undefined)
|
columns: GridColumnData[]
|
||||||
|
}
|
||||||
|
| 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 && (
|
<div className="mb-5 border-b border-gray-200 pb-3 text-xs text-amber-600 dark:border-gray-700">
|
||||||
<span className="mr-auto text-xs text-amber-600">
|
{translate('::App.ListFormTodoBoard.EditRequiresUpdateService')}
|
||||||
{translate('::App.ListFormTodoBoard.EditRequiresUpdateService')}
|
</div>
|
||||||
</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 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,205 +1470,237 @@ const TodoBoardContent = ({
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<aside className="space-y-4 rounded-xl bg-gray-50 p-4 dark:bg-gray-800/60">
|
{/* Uzun aciklamada da durumlar, alanlar ve kaydet butonlari gorunur kalsin; top, sabit ust bilginin (h-16) altindan baslar. */}
|
||||||
{selectedTenantId && (
|
<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">
|
||||||
|
{selectedTenantId && (
|
||||||
|
<div>
|
||||||
|
<label className={labelClass}>Tenant</label>
|
||||||
|
<Input
|
||||||
|
unstyle
|
||||||
|
readOnly
|
||||||
|
className={`${inputClass} cursor-default`}
|
||||||
|
value={selectedTenantName ?? ''}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div>
|
<div>
|
||||||
<label className={labelClass}>Tenant</label>
|
<label className={labelClass}>{translate('::Abp.Mailing.Smtp.UserName')}</label>
|
||||||
<Input
|
<Input
|
||||||
unstyle
|
unstyle
|
||||||
readOnly
|
readOnly
|
||||||
className={`${inputClass} cursor-default`}
|
className={`${inputClass} cursor-default`}
|
||||||
value={selectedTenantName ?? ''}
|
value={String(draft[userNameField] ?? '')}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
{options.assigneeExpr && (
|
||||||
<div>
|
<div>
|
||||||
<label className={labelClass}>
|
<label className={labelClass}>
|
||||||
{translate('::Abp.Mailing.Smtp.UserName')}
|
{translate('::App.ListFormTodoBoard.Assignees')}
|
||||||
</label>
|
</label>
|
||||||
<Input
|
<TagBox
|
||||||
unstyle
|
searchEnabled
|
||||||
readOnly
|
showClearButton
|
||||||
className={`${inputClass} cursor-default`}
|
showSelectionControls
|
||||||
value={String(draft[userNameField] ?? '')}
|
disabled={!canUpdate || usersLoading}
|
||||||
/>
|
displayExpr="label"
|
||||||
</div>
|
items={assigneeOptions}
|
||||||
{options.assigneeExpr && (
|
valueExpr="value"
|
||||||
<div>
|
value={draftAssignees}
|
||||||
<label className={labelClass}>
|
applyValueMode="instantly"
|
||||||
{translate('::App.ListFormTodoBoard.Assignees')}
|
placeholder={
|
||||||
</label>
|
usersLoading
|
||||||
<TagBox
|
? translate('::App.ListFormTodoBoard.UsersLoading')
|
||||||
searchEnabled
|
: translate('::App.ListFormTodoBoard.SelectUser')
|
||||||
showClearButton
|
}
|
||||||
showSelectionControls
|
onValueChanged={(event) => {
|
||||||
disabled={!canUpdate || usersLoading}
|
if (!event.event) return
|
||||||
displayExpr="label"
|
const nextValue = (event.value ?? []).join(', ')
|
||||||
items={assigneeOptions}
|
setDraft((current) =>
|
||||||
valueExpr="value"
|
String(current[options.assigneeExpr!] ?? '') === nextValue
|
||||||
value={draftAssignees}
|
? current
|
||||||
applyValueMode="instantly"
|
: { ...current, [options.assigneeExpr!]: nextValue },
|
||||||
placeholder={
|
)
|
||||||
usersLoading
|
}}
|
||||||
? translate('::App.ListFormTodoBoard.UsersLoading')
|
/>
|
||||||
: translate('::App.ListFormTodoBoard.SelectUser')
|
</div>
|
||||||
}
|
)}
|
||||||
onValueChanged={(event) => {
|
{options.tagExpr && (
|
||||||
if (!event.event) return
|
<div>
|
||||||
const nextValue = (event.value ?? []).join(', ')
|
<label className={labelClass}>
|
||||||
setDraft((current) =>
|
{translate('::App.Listform.ListformField.Tags')}
|
||||||
String(current[options.assigneeExpr!] ?? '') === nextValue
|
</label>
|
||||||
? current
|
<TagBox
|
||||||
: { ...current, [options.assigneeExpr!]: nextValue },
|
acceptCustomValue
|
||||||
)
|
searchEnabled
|
||||||
}}
|
showClearButton
|
||||||
/>
|
showSelectionControls
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{options.tagExpr && (
|
|
||||||
<div>
|
|
||||||
<label className={labelClass}>{translate('::App.Listform.ListformField.Tags')}</label>
|
|
||||||
<TagBox
|
|
||||||
acceptCustomValue
|
|
||||||
searchEnabled
|
|
||||||
showClearButton
|
|
||||||
showSelectionControls
|
|
||||||
disabled={!canUpdate}
|
|
||||||
items={tagSuggestions}
|
|
||||||
value={draftTags}
|
|
||||||
applyValueMode="instantly"
|
|
||||||
placeholder={translate('::App.ListFormTodoBoard.TagsPlaceholder')}
|
|
||||||
onCustomItemCreating={(event) => {
|
|
||||||
event.customItem = event.text?.trim()
|
|
||||||
}}
|
|
||||||
onValueChanged={(event) => {
|
|
||||||
if (!event.event) return
|
|
||||||
const nextValue = (event.value ?? []).join(', ')
|
|
||||||
setDraft((current) =>
|
|
||||||
String(current[options.tagExpr!] ?? '') === nextValue
|
|
||||||
? current
|
|
||||||
: { ...current, [options.tagExpr!]: nextValue },
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{options.dueDateExpr && (
|
|
||||||
<div>
|
|
||||||
<label className={labelClass}>{translate('::App.ListFormTodoBoard.DueDate')}</label>
|
|
||||||
<Input
|
|
||||||
unstyle
|
|
||||||
type="datetime-local"
|
|
||||||
className={inputClass}
|
|
||||||
disabled={!canUpdate}
|
|
||||||
value={
|
|
||||||
draft[options.dueDateExpr]
|
|
||||||
? dayjs(draft[options.dueDateExpr]).format('YYYY-MM-DDTHH:mm')
|
|
||||||
: ''
|
|
||||||
}
|
|
||||||
onChange={(event) =>
|
|
||||||
setDraft((current) => ({
|
|
||||||
...current,
|
|
||||||
[options.dueDateExpr!]: event.target.value || null,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{options.priorityExpr && (
|
|
||||||
<div>
|
|
||||||
<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">
|
|
||||||
<Rate
|
|
||||||
allowClear
|
|
||||||
color="yellow"
|
|
||||||
disabled={!canUpdate}
|
disabled={!canUpdate}
|
||||||
gap={7}
|
items={tagSuggestions}
|
||||||
size={24}
|
value={draftTags}
|
||||||
tooltips={[
|
applyValueMode="instantly"
|
||||||
translate('::App.ListFormTodoBoard.PriorityVeryLow'),
|
placeholder={translate('::App.ListFormTodoBoard.TagsPlaceholder')}
|
||||||
translate('::App.ListFormTodoBoard.PriorityLow'),
|
onCustomItemCreating={(event) => {
|
||||||
translate('::App.ListFormTodoBoard.PriorityMedium'),
|
event.customItem = event.text?.trim()
|
||||||
translate('::App.ListFormTodoBoard.PriorityHigh'),
|
}}
|
||||||
translate('::App.Listform.ListformField.IsUrgent'),
|
onValueChanged={(event) => {
|
||||||
]}
|
if (!event.event) return
|
||||||
value={priorityRating(draft[options.priorityExpr])}
|
const nextValue = (event.value ?? []).join(', ')
|
||||||
onChange={(value) =>
|
setDraft((current) =>
|
||||||
|
String(current[options.tagExpr!] ?? '') === nextValue
|
||||||
|
? current
|
||||||
|
: { ...current, [options.tagExpr!]: nextValue },
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{options.dueDateExpr && (
|
||||||
|
<div>
|
||||||
|
<label className={labelClass}>
|
||||||
|
{translate('::App.ListFormTodoBoard.DueDate')}
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
unstyle
|
||||||
|
type="datetime-local"
|
||||||
|
className={inputClass}
|
||||||
|
disabled={!canUpdate}
|
||||||
|
value={
|
||||||
|
draft[options.dueDateExpr]
|
||||||
|
? dayjs(draft[options.dueDateExpr]).format('YYYY-MM-DDTHH:mm')
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
onChange={(event) =>
|
||||||
setDraft((current) => ({
|
setDraft((current) => ({
|
||||||
...current,
|
...current,
|
||||||
[options.priorityExpr!]: value,
|
[options.dueDateExpr!]: event.target.value || null,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<div className="mt-1 text-xs text-gray-500">
|
</div>
|
||||||
{priorityLabel(draft[options.priorityExpr], translate)}
|
)}
|
||||||
|
{options.priorityExpr && (
|
||||||
|
<div>
|
||||||
|
<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">
|
||||||
|
<Rate
|
||||||
|
allowClear
|
||||||
|
color="yellow"
|
||||||
|
disabled={!canUpdate}
|
||||||
|
gap={7}
|
||||||
|
size={24}
|
||||||
|
tooltips={[
|
||||||
|
translate('::App.ListFormTodoBoard.PriorityVeryLow'),
|
||||||
|
translate('::App.ListFormTodoBoard.PriorityLow'),
|
||||||
|
translate('::App.ListFormTodoBoard.PriorityMedium'),
|
||||||
|
translate('::App.ListFormTodoBoard.PriorityHigh'),
|
||||||
|
translate('::App.Listform.ListformField.IsUrgent'),
|
||||||
|
]}
|
||||||
|
value={priorityRating(draft[options.priorityExpr])}
|
||||||
|
onChange={(value) =>
|
||||||
|
setDraft((current) => ({
|
||||||
|
...current,
|
||||||
|
[options.priorityExpr!]: value,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<div className="mt-1 text-xs text-gray-500">
|
||||||
|
{priorityLabel(draft[options.priorityExpr], translate)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{options.completedExpr && (
|
|
||||||
<label className="flex cursor-pointer items-center gap-3 rounded-lg border border-gray-200 bg-white p-3 text-sm font-medium dark:border-gray-700 dark:bg-gray-800">
|
|
||||||
<Input
|
|
||||||
unstyle
|
|
||||||
type="checkbox"
|
|
||||||
className="h-4 w-4 accent-emerald-500"
|
|
||||||
disabled={!canUpdate}
|
|
||||||
checked={isCompleted(draft[options.completedExpr])}
|
|
||||||
onChange={(event) =>
|
|
||||||
setDraft((current) => ({
|
|
||||||
...current,
|
|
||||||
[options.completedExpr!]: event.target.checked,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
{translate('::App.Listform.ListformField.Completed')}
|
|
||||||
</label>
|
|
||||||
)}
|
|
||||||
</aside>
|
|
||||||
</div>
|
|
||||||
{(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>
|
|
||||||
{canDelete && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="inline-flex items-center gap-2 rounded-lg border border-red-300 px-4 py-2 text-sm font-semibold text-red-600 hover:bg-red-50 dark:border-red-800 dark:text-red-400 dark:hover:bg-red-950"
|
|
||||||
onClick={() => deleteTodo(selected)}
|
|
||||||
>
|
|
||||||
<FaTrash />
|
|
||||||
{translate('::App.Platform.Delete')}
|
|
||||||
</button>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
{options.completedExpr && (
|
||||||
{canUpdate && (
|
<label className="flex cursor-pointer items-center gap-3 rounded-lg border border-gray-200 bg-white p-3 text-sm font-medium dark:border-gray-700 dark:bg-gray-800">
|
||||||
<div className="flex items-center gap-2">
|
<Input
|
||||||
<button
|
unstyle
|
||||||
type="button"
|
type="checkbox"
|
||||||
className="rounded-lg border border-gray-300 px-4 py-2 text-sm hover:bg-gray-50 dark:border-gray-600 dark:hover:bg-gray-800"
|
className="h-4 w-4 accent-emerald-500"
|
||||||
onClick={() => setDraft({ ...selected })}
|
disabled={!canUpdate}
|
||||||
>
|
checked={isCompleted(draft[options.completedExpr])}
|
||||||
{translate('::App.Platform.UndoChanges')}
|
onChange={(event) =>
|
||||||
</button>
|
setDraft((current) => ({
|
||||||
<button
|
...current,
|
||||||
type="button"
|
[options.completedExpr!]: event.target.checked,
|
||||||
className="rounded-lg bg-blue-600 px-5 py-2 text-sm font-semibold text-white hover:bg-blue-700 disabled:opacity-50"
|
}))
|
||||||
disabled={saving || !String(draft[options.titleExpr ?? ''] ?? '').trim()}
|
}
|
||||||
onClick={() => saveTodo()}
|
/>
|
||||||
>
|
{translate('::App.Listform.ListformField.Completed')}
|
||||||
{saving ? translate('::App.Platform.Saving') : translate('::App.Platform.Save')}
|
</label>
|
||||||
</button>
|
)}
|
||||||
<button
|
</aside>
|
||||||
type="button"
|
{(canUpdate || canDelete) && (
|
||||||
className="rounded-lg bg-emerald-600 px-5 py-2 text-sm font-semibold text-white hover:bg-emerald-700 disabled:opacity-50"
|
<div className="flex flex-wrap-reverse items-center justify-between gap-3">
|
||||||
disabled={saving || !String(draft[options.titleExpr ?? ''] ?? '').trim()}
|
<div>
|
||||||
onClick={() => saveTodo(true)}
|
{canDelete && (
|
||||||
>
|
<button
|
||||||
{saving ? translate('::App.Platform.Saving') : translate('::App.ListFormTodoBoard.SaveAndExit')}
|
type="button"
|
||||||
</button>
|
className="inline-flex items-center gap-2 rounded-lg border border-red-300 px-4 py-2 text-sm font-semibold text-red-600 hover:bg-red-50 dark:border-red-800 dark:text-red-400 dark:hover:bg-red-950"
|
||||||
|
onClick={() => deleteTodo(selected)}
|
||||||
|
>
|
||||||
|
<FaTrash />
|
||||||
|
{translate('::App.Platform.Delete')}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{canUpdate && (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="rounded-lg border border-gray-300 px-4 py-2 text-sm hover:bg-gray-50 dark:border-gray-600 dark:hover:bg-gray-800"
|
||||||
|
onClick={() => setDraft({ ...selected })}
|
||||||
|
>
|
||||||
|
{translate('::App.Platform.UndoChanges')}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="rounded-lg bg-blue-600 px-5 py-2 text-sm font-semibold text-white hover:bg-blue-700 disabled:opacity-50"
|
||||||
|
disabled={saving || !String(draft[options.titleExpr ?? ''] ?? '').trim()}
|
||||||
|
onClick={() => saveTodo()}
|
||||||
|
>
|
||||||
|
{saving
|
||||||
|
? translate('::App.Platform.Saving')
|
||||||
|
: translate('::App.Platform.Save')}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="rounded-lg bg-emerald-600 px-5 py-2 text-sm font-semibold text-white hover:bg-emerald-700 disabled:opacity-50"
|
||||||
|
disabled={saving || !String(draft[options.titleExpr ?? ''] ?? '').trim()}
|
||||||
|
onClick={() => saveTodo(true)}
|
||||||
|
>
|
||||||
|
{saving
|
||||||
|
? translate('::App.Platform.Saving')
|
||||||
|
: translate('::App.ListFormTodoBoard.SaveAndExit')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -1715,180 +1757,182 @@ const TodoBoardContent = ({
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className={`sticky top-0 z-10 -mx-2 -mt-2 rounded-t-xl px-2 pb-1 pt-2 ${columnHeaderBgClass}`}>
|
<div
|
||||||
<header className="mb-2 flex min-h-9 items-center gap-2 px-1">
|
className={`sticky top-0 z-10 -mx-2 -mt-2 rounded-t-xl px-2 pb-1 pt-2 ${columnHeaderBgClass}`}
|
||||||
<span
|
>
|
||||||
draggable={canUpdate}
|
<header className="mb-2 flex min-h-9 items-center gap-2 px-1">
|
||||||
className={`rounded p-1 text-gray-400 ${canUpdate ? 'cursor-grab hover:bg-gray-200 hover:text-gray-600 active:cursor-grabbing dark:hover:bg-gray-700' : 'cursor-default opacity-50'}`}
|
<span
|
||||||
title={translate('::App.ListFormTodoBoard.ReorderColumn')}
|
draggable={canUpdate}
|
||||||
onDragStart={(event) => {
|
className={`rounded p-1 text-gray-400 ${canUpdate ? 'cursor-grab hover:bg-gray-200 hover:text-gray-600 active:cursor-grabbing dark:hover:bg-gray-700' : 'cursor-default opacity-50'}`}
|
||||||
event.stopPropagation()
|
title={translate('::App.ListFormTodoBoard.ReorderColumn')}
|
||||||
event.dataTransfer.effectAllowed = 'move'
|
onDragStart={(event) => {
|
||||||
dragActiveRef.current = true
|
event.stopPropagation()
|
||||||
setDraggedKey(undefined)
|
event.dataTransfer.effectAllowed = 'move'
|
||||||
setDraggedColumn(status)
|
dragActiveRef.current = true
|
||||||
}}
|
setDraggedKey(undefined)
|
||||||
onDragEnd={clearDragState}
|
setDraggedColumn(status)
|
||||||
>
|
|
||||||
<FaGripVertical />
|
|
||||||
</span>
|
|
||||||
{editingStatus === status ? (
|
|
||||||
<form
|
|
||||||
className="flex min-w-0 flex-1 items-center gap-1"
|
|
||||||
onSubmit={(event) => {
|
|
||||||
event.preventDefault()
|
|
||||||
renameStatus(status)
|
|
||||||
}}
|
}}
|
||||||
|
onDragEnd={clearDragState}
|
||||||
>
|
>
|
||||||
<Input
|
<FaGripVertical />
|
||||||
unstyle
|
</span>
|
||||||
autoFocus
|
{editingStatus === status ? (
|
||||||
className="min-w-0 flex-1 rounded-md border border-blue-400 bg-white px-2 py-1 text-sm font-semibold outline-none ring-2 ring-blue-500/20 dark:bg-gray-800"
|
<form
|
||||||
value={statusName}
|
className="flex min-w-0 flex-1 items-center gap-1"
|
||||||
disabled={renamingStatus}
|
onSubmit={(event) => {
|
||||||
aria-label={translate('::App.ListFormTodoBoard.ColumnName')}
|
event.preventDefault()
|
||||||
onChange={(event) => {
|
renameStatus(status)
|
||||||
setStatusName(event.target.value)
|
|
||||||
setStatusError('')
|
|
||||||
}}
|
}}
|
||||||
onKeyDown={(event) => {
|
>
|
||||||
if (event.key === 'Escape') {
|
<Input
|
||||||
setEditingStatus(undefined)
|
unstyle
|
||||||
|
autoFocus
|
||||||
|
className="min-w-0 flex-1 rounded-md border border-blue-400 bg-white px-2 py-1 text-sm font-semibold outline-none ring-2 ring-blue-500/20 dark:bg-gray-800"
|
||||||
|
value={statusName}
|
||||||
|
disabled={renamingStatus}
|
||||||
|
aria-label={translate('::App.ListFormTodoBoard.ColumnName')}
|
||||||
|
onChange={(event) => {
|
||||||
|
setStatusName(event.target.value)
|
||||||
setStatusError('')
|
setStatusError('')
|
||||||
}
|
}}
|
||||||
}}
|
onKeyDown={(event) => {
|
||||||
/>
|
if (event.key === 'Escape') {
|
||||||
<button
|
setEditingStatus(undefined)
|
||||||
type="submit"
|
setStatusError('')
|
||||||
className="rounded p-1.5 text-emerald-600 hover:bg-emerald-100 disabled:opacity-40 dark:hover:bg-emerald-950"
|
}
|
||||||
disabled={renamingStatus || !statusName.trim()}
|
}}
|
||||||
title={translate('::App.Platform.Save')}
|
/>
|
||||||
>
|
<button
|
||||||
<FaCheck />
|
type="submit"
|
||||||
</button>
|
className="rounded p-1.5 text-emerald-600 hover:bg-emerald-100 disabled:opacity-40 dark:hover:bg-emerald-950"
|
||||||
<button
|
disabled={renamingStatus || !statusName.trim()}
|
||||||
type="button"
|
title={translate('::App.Platform.Save')}
|
||||||
className="rounded p-1.5 text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-700"
|
>
|
||||||
title={translate('::App.Platform.Cancel')}
|
<FaCheck />
|
||||||
onClick={() => {
|
</button>
|
||||||
setEditingStatus(undefined)
|
|
||||||
setStatusError('')
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<FaTimes />
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<h4 className="min-w-0 truncate font-sm" title={statusText(status)}>
|
|
||||||
{statusText(status)}
|
|
||||||
</h4>
|
|
||||||
{canUpdate && (
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="rounded p-1 text-gray-400 opacity-0 transition hover:bg-gray-200 hover:text-blue-600 group-hover:opacity-100 dark:hover:bg-gray-700"
|
className="rounded p-1.5 text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-700"
|
||||||
title={translate('::App.ListFormTodoBoard.RenameColumn')}
|
title={translate('::App.Platform.Cancel')}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setEditingStatus(status)
|
setEditingStatus(undefined)
|
||||||
setStatusName(status)
|
|
||||||
setStatusError('')
|
setStatusError('')
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<FaPencilAlt className="text-xs" />
|
<FaTimes />
|
||||||
</button>
|
</button>
|
||||||
)}
|
</form>
|
||||||
</>
|
) : (
|
||||||
)}
|
<>
|
||||||
<span className="ml-auto rounded-full bg-gray-200 px-2 py-0.5 text-xs dark:bg-gray-700">
|
<h4 className="min-w-0 truncate font-sm" title={statusText(status)}>
|
||||||
{items.length}
|
{statusText(status)}
|
||||||
</span>
|
</h4>
|
||||||
{canAdd && (
|
{canUpdate && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
draggable={false}
|
className="rounded p-1 text-gray-400 opacity-0 transition hover:bg-gray-200 hover:text-blue-600 group-hover:opacity-100 dark:hover:bg-gray-700"
|
||||||
className="rounded p-1 text-gray-400 hover:bg-blue-100 hover:text-blue-600 dark:hover:bg-blue-950"
|
title={translate('::App.ListFormTodoBoard.RenameColumn')}
|
||||||
title={translate('::App.ListFormTodoBoard.NewTodo')}
|
onClick={() => {
|
||||||
onClick={() => {
|
setEditingStatus(status)
|
||||||
setNewTodoStatus(status)
|
setStatusName(status)
|
||||||
setNewTodoTitle('')
|
setStatusError('')
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<FaPlus />
|
<FaPencilAlt className="text-xs" />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{canDelete && (
|
</>
|
||||||
<button
|
)}
|
||||||
type="button"
|
<span className="ml-auto rounded-full bg-gray-200 px-2 py-0.5 text-xs dark:bg-gray-700">
|
||||||
draggable={false}
|
{items.length}
|
||||||
aria-disabled={items.length > 0}
|
</span>
|
||||||
className={`rounded p-1 transition ${items.length ? 'cursor-not-allowed text-gray-300 dark:text-gray-600' : 'text-gray-400 hover:bg-red-100 hover:text-red-600 dark:hover:bg-red-950'}`}
|
{canAdd && (
|
||||||
title={
|
<button
|
||||||
items.length
|
type="button"
|
||||||
? translate('::App.ListFormTodoBoard.ColumnWithTodosCannotBeDeleted')
|
draggable={false}
|
||||||
: translate('::App.ListFormTodoBoard.DeleteColumn')
|
className="rounded p-1 text-gray-400 hover:bg-blue-100 hover:text-blue-600 dark:hover:bg-blue-950"
|
||||||
}
|
title={translate('::App.ListFormTodoBoard.NewTodo')}
|
||||||
onClick={() => deleteStatus(status)}
|
onClick={() => {
|
||||||
>
|
setNewTodoStatus(status)
|
||||||
<FaTrash className="text-xs" />
|
setNewTodoTitle('')
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</header>
|
|
||||||
{editingStatus === status && statusError && (
|
|
||||||
<p className="mb-2 px-1 text-xs text-red-500">{statusError}</p>
|
|
||||||
)}
|
|
||||||
{statusNotice?.status === status && (
|
|
||||||
<div className="mb-2 flex items-start justify-between gap-2 rounded-lg bg-amber-50 px-2.5 py-2 text-xs text-amber-700 dark:bg-amber-950/40 dark:text-amber-200">
|
|
||||||
<span>{statusNotice.message}</span>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="shrink-0 rounded p-0.5 hover:bg-amber-100 dark:hover:bg-amber-900"
|
|
||||||
title={translate('::App.Platform.Close')}
|
|
||||||
onClick={() => setStatusNotice(undefined)}
|
|
||||||
>
|
|
||||||
<FaTimes />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="h-1 overflow-hidden rounded-full bg-gray-200 dark:bg-gray-700">
|
|
||||||
<div
|
|
||||||
className="h-full bg-blue-500"
|
|
||||||
style={{ width: `${Math.min(100, items.length * 18)}%` }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{isAdding && (
|
|
||||||
<div className="mt-3 rounded border border-blue-300 bg-white p-2 dark:border-blue-700 dark:bg-gray-800">
|
|
||||||
<div className="flex gap-1">
|
|
||||||
<Input
|
|
||||||
unstyle
|
|
||||||
autoFocus
|
|
||||||
value={newTodoTitle}
|
|
||||||
className="min-w-0 flex-1 rounded border border-gray-300 bg-transparent px-2 py-1 text-sm outline-none focus:border-blue-500 dark:border-gray-600"
|
|
||||||
placeholder={translate('::App.ListFormTodoBoard.TodoTitlePlaceholder')}
|
|
||||||
onChange={(event) => setNewTodoTitle(event.target.value)}
|
|
||||||
onKeyDown={(event) => {
|
|
||||||
if (event.key === 'Enter') addTodo(status)
|
|
||||||
if (event.key === 'Escape') setNewTodoStatus(undefined)
|
|
||||||
}}
|
}}
|
||||||
/>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="rounded bg-blue-600 px-2 text-white disabled:opacity-40"
|
|
||||||
disabled={!newTodoTitle.trim()}
|
|
||||||
title={translate('::App.Platform.Save')}
|
|
||||||
onClick={() => addTodo(status)}
|
|
||||||
>
|
>
|
||||||
<FaSave />
|
<FaPlus />
|
||||||
</button>
|
</button>
|
||||||
|
)}
|
||||||
|
{canDelete && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="rounded px-2 text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700"
|
draggable={false}
|
||||||
onClick={() => setNewTodoStatus(undefined)}
|
aria-disabled={items.length > 0}
|
||||||
|
className={`rounded p-1 transition ${items.length ? 'cursor-not-allowed text-gray-300 dark:text-gray-600' : 'text-gray-400 hover:bg-red-100 hover:text-red-600 dark:hover:bg-red-950'}`}
|
||||||
|
title={
|
||||||
|
items.length
|
||||||
|
? translate('::App.ListFormTodoBoard.ColumnWithTodosCannotBeDeleted')
|
||||||
|
: translate('::App.ListFormTodoBoard.DeleteColumn')
|
||||||
|
}
|
||||||
|
onClick={() => deleteStatus(status)}
|
||||||
|
>
|
||||||
|
<FaTrash className="text-xs" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</header>
|
||||||
|
{editingStatus === status && statusError && (
|
||||||
|
<p className="mb-2 px-1 text-xs text-red-500">{statusError}</p>
|
||||||
|
)}
|
||||||
|
{statusNotice?.status === status && (
|
||||||
|
<div className="mb-2 flex items-start justify-between gap-2 rounded-lg bg-amber-50 px-2.5 py-2 text-xs text-amber-700 dark:bg-amber-950/40 dark:text-amber-200">
|
||||||
|
<span>{statusNotice.message}</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="shrink-0 rounded p-0.5 hover:bg-amber-100 dark:hover:bg-amber-900"
|
||||||
|
title={translate('::App.Platform.Close')}
|
||||||
|
onClick={() => setStatusNotice(undefined)}
|
||||||
>
|
>
|
||||||
<FaTimes />
|
<FaTimes />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="h-1 overflow-hidden rounded-full bg-gray-200 dark:bg-gray-700">
|
||||||
|
<div
|
||||||
|
className="h-full bg-blue-500"
|
||||||
|
style={{ width: `${Math.min(100, items.length * 18)}%` }}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
{isAdding && (
|
||||||
|
<div className="mt-3 rounded border border-blue-300 bg-white p-2 dark:border-blue-700 dark:bg-gray-800">
|
||||||
|
<div className="flex gap-1">
|
||||||
|
<Input
|
||||||
|
unstyle
|
||||||
|
autoFocus
|
||||||
|
value={newTodoTitle}
|
||||||
|
className="min-w-0 flex-1 rounded border border-gray-300 bg-transparent px-2 py-1 text-sm outline-none focus:border-blue-500 dark:border-gray-600"
|
||||||
|
placeholder={translate('::App.ListFormTodoBoard.TodoTitlePlaceholder')}
|
||||||
|
onChange={(event) => setNewTodoTitle(event.target.value)}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (event.key === 'Enter') addTodo(status)
|
||||||
|
if (event.key === 'Escape') setNewTodoStatus(undefined)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="rounded bg-blue-600 px-2 text-white disabled:opacity-40"
|
||||||
|
disabled={!newTodoTitle.trim()}
|
||||||
|
title={translate('::App.Platform.Save')}
|
||||||
|
onClick={() => addTodo(status)}
|
||||||
|
>
|
||||||
|
<FaSave />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="rounded px-2 text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700"
|
||||||
|
onClick={() => setNewTodoStatus(undefined)}
|
||||||
|
>
|
||||||
|
<FaTimes />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-3 space-y-2">
|
<div className="mt-3 space-y-2">
|
||||||
{items.map((row, index) => {
|
{items.map((row, index) => {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue