diff --git a/.github/instructions/lowcode-reference.instructions.md b/.github/instructions/lowcode-reference.instructions.md index df158788..3ad73d2b 100644 --- a/.github/instructions/lowcode-reference.instructions.md +++ b/.github/instructions/lowcode-reference.instructions.md @@ -414,12 +414,20 @@ SELECT 'Onaylı', COUNT(*), 'green', 'FaCheck' FROM Mrp_T_Order WHERE Status = 2 Sorgu ekranın bağlantısında (`IsTenant` + `DataSourceCode` ile çözülen, `lowcode` §9.3.3), **olduğu gibi** çalıştırılır: -- Ekranın otomatik tenant/şube/OU filtresi **uygulanmaz**. -- `@USERID` / `@TENANTID` gibi token'lar **çözülmez** — bunlar `*FieldsDefaultValueJson` ve - `DefaultFilter` yolunun sözleşmesidir. Widget sorgusuna yazılırsa bağlanmamış parametre - hatası verir. Kiracı kırılımı gerekiyorsa ya kiracıya özel bir `DataSource` kullan, ya da - kırılımı sorgunun okuduğu view'ın içine göm. -- Soft delete'li tabloda `"IsDeleted" = 'false'` koşulunu sorguya kendin yaz. +- Ekranın otomatik tenant/şube/OU filtresi **uygulanmaz**; kırılım sorguya yazılır. +- Sorgu `DefaultFilter` ile aynı token çözümünden geçer (`DefaultValueHelper.GetDefaultValue`): + `@USERID`, `@USERNAME`, `@DATE`, `@NOW` ve `@TENANTID` metin olarak yerine konur. + Kiracı tablosunda (`IsTenant: true`) her karta `"TenantId" = '@TENANTID'` yaz; host'ta bu + `"TenantId" IS NULL`'a döner, yani kartlar liste ekranıyla aynı kümeyi sayar. Yazılmazsa ortak + veritabanında kartlar bütün kiracıların toplamını gösterir. +- `IsDeleted` koşulu widget sorgusuna yazılmaz (platform varsayılanı). +- Wizard / MCP ile üretilen List ekranı sorulmadan en az 3 kartla gelir; kart seçimi, istisnalar + ve metin/renk kuralı: `lowcode` §4.6.1. +- Seeder'da şerit `DefaultWidgetsJson(tablo, listFormName, tenant, params WidgetCard[])` ile + kurulur (`ListFormSeeder_DefaultJsons`): kart başına `Title` (dil anahtarı), renk, ikon, ek koşul + ve isteğe bağlı `Value` ifadesi; alt başlık ekranın adıdır, değer tip çakışmasın diye metne + çevrilir. Gruplamalı sorgu için `DefaultWidgetsJson(sql, kartSayısı)`. Genel kart başlıkları + `App.Widget.*` dil anahtarlarındadır. ### 5.4 `LayoutJson` (`LayoutDto`) @@ -547,7 +555,7 @@ yazılırsa düşer), `AllowTaskAdding`, `AllowTaskUpdating`, `AllowTaskResource ### 7.4 `TodoOptionJson` (`TodoOptionDto`) — Kanban -`TitleExpr`*, `StatusExpr`*, `DescriptionExpr`, `DueDateExpr`, `TagExpr`, `AssigneeExpr`, +`TitleExpr`*, `StatusExpr`*, `DescriptionExpr`, `AnalysisExpr` (detayda Açıklama yanında HTML sekmesi), `DueDateExpr`, `TagExpr`, `AssigneeExpr`, `PriorityExpr`, `CompletedExpr`, `OrderExpr`, `TimesheetExpr`, `SubTaskExpr`, `StatusOrder` (kolon sırası, virgüllü), `AllowDragging` (varsayılan `true`). @@ -1249,7 +1257,7 @@ soneklerini alır. | **Public › Sales Orders** | `App.Orders.SalesOrders` (alt form Sales Order Item) | `Sas_H_Order` + `Sas_H_OrderItem` | host | **Onay akışı + alt kalem + toplam + diyalog** birlikte kullanılan tam örnek (§12.6). | | **Public › Blog Category / Posts** | `App.BlogManagement.Category` · `.Posts` | `Sas_H_BlogCategory` · `Sas_H_BlogPost` | host | Çok dilli içerik (`ContentTr`/`ContentEn`), slug, yayın durumu, sayaç alanları; public `/blog` bu tablolardan okur, yayınlanmış yazılar sitemap'e girer. | | **Public › Demos** | `App.Demos` | `Sas_H_Demo` | host | Public `/demo` formundan gelen talepler — public form → tablo akışının örneği. | -| **Dev Requests** | `App.DevRequests` | `Sas_H_DevRequest` | host | **Kanban/Todo düzeninin** referansı (`DefaultLayout: todo`): `Status`, `Priority`, `SortOrder`, `Tags`, `Assignees`, `SubTasks`, `Timesheets`. MCP geliştirme döngüsünün kuyruğudur (`Ready to Start` → `Testing`, `Staging`'e kullanıcı alır; araç çubuğunda DB Migrate butonu vardır — `UiEvalService.ApiDbMigrate()`, `App.Setup.Migrate`; yalnızca `start_dev_request` başlatır; açıklamadaki base64 görseller MCP'ye resim içeriği olarak gider, not `append_dev_request_note` ile eklenir; `lowcode.instructions.md` §0.11). | +| **Dev Requests** | `App.DevRequests` | `Sas_H_DevRequest` | host | **Kanban/Todo düzeninin** referansı (`DefaultLayout: todo`): `Status`, `Priority`, `SortOrder`, `Tags`, `Assignees`, `SubTasks`, `Timesheets`; detayda `Description` yanında `Analysis` sekmesi (`AnalysisExpr`). MCP geliştirme döngüsünün kuyruğudur (`Backlog` → `Analysis` MCP analiz eder, `Ready to Start`'a kullanıcı alır; `Ready to Start` → `Testing`, `Staging`'e kullanıcı alır; araç çubuğunda DB Migrate butonu vardır — `UiEvalService.ApiDbMigrate()`, `App.Setup.Migrate`; yalnızca `start_dev_request` başlatır; açıklamadaki base64 görseller MCP'ye resim içeriği olarak gider, analiz `save_dev_request_analysis` ile yazılır, üretim notu `append_dev_request_note` ile `Analysis`'e eklenir — `Description` talebin yazıldığı hâliyle kalır; `lowcode.instructions.md` §0.11–§0.12). | | **Tenants** | `AbpTenantManagement.Tenants` | `AbpTenants` | host | Kiracı kaydı, bağlantı dizesi diyaloğu (`TenantsConnectionString`), alt form olarak şubeler; yazma `list-form-dynamic-api/tenant-*`. Ekranların `IsTenant` davranışının kaynağı. | | **Branches** | `App.Branches` | `Sas_T_Branch` | tenant | Şube listesi. Ekranı `IsBranch = true` yaparsan filtre bu tablodan gelir; `BranchSeed` diyaloğu şube verisini kurar. | | **Setting Definitions** | `App.SettingDefinitions` | `Sas_H_SettingDefinition` | host | Yeni uygulama ayarı: `Code`, `MainGroupKey`/`SubGroupKey`, `DataType`, `SelectOptions`, `DefaultValue`, `Providers`, `RequiredPermissionName`, `IsEncrypted`, `IsVisibleToClients`. Hazır tanımlar `data/App.SettingDefinitions.json`'dan gelir. Ayarlanabilir olması gereken değeri koda gömme. | @@ -1446,7 +1454,7 @@ içinde ilgili `#region` altındadır. | Tek kayıtlık form ekranı | `Licence` (`ListFormType = Form` + alt kalem listesi) | | Onay akışı | `Sales Orders` — `WorkflowJson` (`ApprovalUserFieldName`, `ApprovalStatusFieldName`, `ApprovalDateFieldName`, `ApprovalDescriptionFieldName`) + `ListFormWorkflow` düğümleri (Start → Approval → End/End) | | Alt form (master-detail) | `Tenant → Branches`, `Sales Orders → Sales Order Item`, `Survey → Question → Option` | -| KPI widget | `User`, `Dynamic Services`, `Custom Components` — `WidgetsJson`; tek sorgu `UNION ALL` ile birden çok kart döndürür (`Title`, `Value`, `Color`, `SubTitle`, `Icon`) | +| KPI widget | Saas ve Administration'daki yönetim ekranlarının çoğu (Users, Tenants, Dynamic Services, Custom Components, Audit Logs, Bildirimler, Duyuru/Anket/Etkinlik, Forum…; basit tanım listeleri hariç) — `WidgetsJson`; tek sorgu `UNION ALL` ile birden çok kart döndürür (`Title`, `Value`, `Color`, `SubTitle`, `Icon`) | | Toolbar'dan diyalog açma | `Tenant` (`TenantsConnectionString`), `Role` (`RolesPermission`), `User` (`UsersPermission`), `Audit Logs` (`AuditLogDetail`), `Notification Rule` (`CreateNotification`), `Branches` (`BranchSeed`), `Sales Orders` (`CreateTenantFromOrderDialog`), `Dynamic Services` (`DynamicServiceEditor` — hem toolbar hem satır) | | Yazmayı servise devretme | `Tenant`, `Role`, `User` — `*ServiceAddress = "list-form-dynamic-api/{tenant\|role\|user}-{insert\|update}"`; `Notification` — `notification-insert` (çoklu değer `MultiValueStringJsonConverter`) | | Ekle düğmesini diyaloğa bağlama | `Dynamic Services` — `EditingOptionJson.AddDialogName = "DynamicServiceEditor"` | diff --git a/.github/instructions/lowcode.instructions.md b/.github/instructions/lowcode.instructions.md index f80e99c8..2d438b2e 100644 --- a/.github/instructions/lowcode.instructions.md +++ b/.github/instructions/lowcode.instructions.md @@ -251,6 +251,7 @@ bildirilir: | Konu | Neden sorulmaz | | --- | --- | | Tenant + audit kolonları (`Id`, `TenantId`, `CreationTime`, `CreatorId`, `LastModificationTime`, `LastModifierId`, `IsDeleted`, `DeletionTime`, `DeleterId`) | Yeni tabloda varsayılan; kullanıcı "audit yok" demedikçe eklenir | +| Tanım tablosunda `IsActive` | Yeni **tanım** tablosunda varsayılan (aşağıda); kullanıcı "aktif/pasif yok" demedikçe eklenir | | Metinlerin EN + TR olması, dil anahtarı üzerinden verilmesi | Platform sözleşmesi | | İsimlendirme (`ListFormCode`, menü kodu, yetki adları, dosya adları) | §2'den türer | | Seed dosyası yazılıp yazılmayacağı | Her artefakt seed'lenir (§0.2) | @@ -258,6 +259,7 @@ bildirilir: | Parametreli SQL kullanımı, yetki kontrolü, tenant izolasyonu | Tartışmaya açık değil | | Depoda okunabilen tablo/kolon/menü/yetki adları | Önce ara, bulduğunu **doğrulat**, sıfırdan sorma | | Var olan bir tanım tablosunun (ülke, para birimi, departman, ünvan, birim) yeniden üretilip üretilmeyeceği | Üretilmez; mevcut tabloya lookup bağlanır (reference §12.5) | +| Liste ekranının üstündeki KPI kartları (`Widgets`) | List ekranı en az 3 kartla gelir, kartlar tablonun durum/bayrak/tarih/tutar kolonlarından seçilir (§4.6.1) | Buna karşılık **her zaman sorulacak** üç şey vardır, çünkü yanlış varsayım geri dönülmez sonuç üretir: **hangi tabloya yazılacağı**, **kimin onaylayacağı** (onay süreci varsa) ve @@ -503,11 +505,12 @@ ister ve kiracı kullanıcısına hiçbiri açılmaz. | `get_dev_guide` | Bu dosyanın (`lowcode`) ya da `reference`'ın başlık listesini / bir bölümünü döner | Bölüm numarayla istenir (`4.3`); aynı numarayı iki başlık taşımamalıdır | | `create_table` | Yeni SQL Server tablosu kurar, `sql/object/{Tablo}.sql` dosyasını yazar | Ek olarak `App.SqlQueryManager` ister; `Id`/`TenantId`/audit kolonlarını kendi ekler (`fullAudited`, `multiTenant` varsayılan açık); var olan tabloyu reddeder. View/SP/fonksiyon kurulamaz | | `list_wizards` · `get_wizard` | Wizard dosyalarını listeler (menü yolu, bağımlılar) · dosyayı diskteki hâliyle döner | Var olan menü kökünü/izin grubunu bulmak için önce `list_wizards` | -| `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). İstek demo veri taşıyorsa `SeedData: true` + `seedRows` (§0.10.1). Çıkıştaki `isPending: true` ekranın henüz kurulmadığını söyler — kullanıcı Wizard Manager'da DB Migrate çalıştırır; model migrate'i beklemez | +| `deploy_wizard` | Wizard cevabından ekranın wizard dosyasını ve menü/yetki/dil seed satırlarını yazar; veritabanına yazmaz (§4.0) | Girdi §4.4'teki düz cevap ya da `get_wizard` çıktısıyla aynı dosya biçimi; enum'lar sayı. `replaceFileName` = `EditFileName` (§4.5). List ekranında `Widgets` dolu gelir — en az 3 kart, kiracı tablosunda `"TenantId" = '@TENANTID'` (§4.6.1). İstek demo veri taşıyorsa `SeedData: true` + `seedRows` (§0.10.1). Çıkıştaki `isPending: true` ekranın henüz kurulmadığını söyler — kullanıcı Wizard Manager'da DB Migrate çalıştırır; model migrate'i beklemez | | `delete_wizard` | Wizard'ı ve yalnızca onun ürettiği kayıtları siler; `dropTables: true` ekranın tablosunu da düşürür | Bağımlı wizard varsa reddeder. Tablo kullanıcı açıkça onaylamadan düşürülmez; `App.SqlQueryManager` ister (§4.5) | | `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 + resim içerikleri) | Yarım kalan talebe dönmek için; `App.DevRequests` ister | -| `append_dev_request_note` | Talep açıklamasının sonuna HTML ekler | Sonuç notu için; `Description`'ı `update_row` ile yazma (§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 | +| `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 | 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) | | `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` | | `list_components` · `get_component` · `save_component` · `delete_component` | Custom component okuma/yazma (ada göre upsert) | `save_component` visual designer bileşenini reddeder — yalnız Code modu (§7.1.1) | @@ -554,10 +557,17 @@ talep girer (`App.DevRequest`). Döngüyü `/mcp`'ye bağlı bir **host** kullan kiracı bağlamında geliştirme araçları kapalıdır. Gereken yetkiler: `App.DevRequests.Update`, `App.DeveloperKit.McpServers.Invoke` + `.Write` ve üretimin kendi yetkileri (§0.10). -İki giriş vardır; ikisi de istemcide hazır prompt (komut) olarak görünür (`McpPromptNames`): +Talep yaşam döngüsü: `Backlog` → **`Analysis`** (MCP analiz eder, §0.12) → `Ready to Start` +(analizi beğenen kullanıcı taşır; beğenmezse analiz yeniden yazılır ya da elle düzeltilir) → +`In Progress` → `Testing` → `Staging`. Üretim `Ready to Start`'tan başlar ve talebin +`Description`'ı ile onaylanmış `Analysis`'i birlikte tarif sayılır. + +Girişler istemcide hazır prompt (komut) olarak görünür (`McpPromptNames`): | Prompt | Kapsam | | --- | --- | +| `analyze_dev_request` (`request`: Id ya da tam başlık) | Yalnızca o talebi analiz eder (§0.12); hiçbir şey üretmez | +| `analyze_backlog_requests` | `Backlog` durumundaki bütün talepler, `SortOrder` sırasıyla analiz edilir | | `process_dev_request` (`request`: Id ya da tam başlık) | Yalnızca o talep. Talep başlatılamazsa hiçbir şey üretilmez, başka talebe geçilmez | | `process_ready_requests` | `Ready to Start` durumundaki bütün talepler, `SortOrder` sırasıyla; başlatılamayan talep atlanır | @@ -565,6 +575,8 @@ Kullanıcı prompt yerine düz metin de yazabilir; aynı anlama gelir: | Kullanıcı yazar | Karşılığı | | --- | --- | +| ` talebini analiz et` | `analyze_dev_request` — yalnızca o talep | +| `Analiz et` · `Backlog'daki talepleri analiz et` | `analyze_backlog_requests` | | ` talebini işle` | `process_dev_request` — yalnızca o talep | | `Ready to Start durumundaki talepleri sırayla işle` · `Ready olanları işle` | `process_ready_requests` | | `Ready to Start durumundaki talepleri Id'leriyle listele` | Yalnızca adım 1 (`query_list`); hiçbir şey üstlenilmez, üretilmez | @@ -581,7 +593,7 @@ talebin metnini döndürür. Başlık birden fazla talebe uyuyorsa Id ister. `St | --- | --- | --- | | 1. Kuyruğu oku (yalnızca toplu) | `query_list` — `ListFormCode: App.DevRequests`, filtre `["Status","=","Ready to Start"]`, sıralama `SortOrder` | — | | 2. Talebi üstlen | `start_dev_request` — Id ya da tam başlık. Yarım kalmış talebe dönüşte `get_dev_request` | In Progress | -| 3. Üret | §0.3 keşif (depodan/araçlardan; kullanıcıya soru yok) → `create_table` / `deploy_wizard` / `save_component`; talep demo veri taşıyorsa `SeedData: true` + `seedRows` (§0.10.1); bu dosyanın kuralları aynen geçerlidir | — | +| 3. Üret | Analizin **Üretim planı** sırayla, aynen uygulanır (aşağıdaki kurallar) → `create_table` / `deploy_wizard` / `save_component`; demo veri varsa `SeedData: true` + `seedRows` (§0.10.1); bu dosyanın kuralları aynen geçerlidir | — | | 4. Teslim | Sonuç kaydı + `update_row` → `Testing`. DB Migrate beklenmez; talep burada biter | Testing | | 5. Test | Kullanıcı DB Migrate çalıştırır, ekranı elle test eder ve talebi kendisi `Staging`'e alır (geri dönüşte `In Progress`). MCP bu adımı yapmaz | Staging | @@ -590,9 +602,12 @@ talebin metnini döndürür. Başlık birden fazla talebe uyuyorsa Id ister. `St verilerek (`"deploy_wizard: Mrp.Orders.json"`). Araç satırı tamamlanmış yazar, atanan kişiyi bağlanan kullanıcı, bitiş tarihini bugün yapar ve var olan satırları korur. `SubTasks` `update_row` ile yazılmaz. -- `Description` — `append_dev_request_note` ile sonuna `

MCP

` başlığıyla kısa özet: - üretilenler ve DB Migrate bekleyen wizard'lar. `Description` hiçbir zaman `update_row` - ile yazılmaz — base64 görselleri taşır ve yeniden yazmak onları siler. +- `Analysis` — `append_dev_request_note` ile sonuna `

MCP

` başlığıyla kısa özet: + tablo, ekran, alan, demo veri, menü/yetki, analize eklenen varsayılanlar ve DB Migrate bekleyen + wizard'lar. Tekrar üretimde yeni bir `

MCP

` bloğu eklenir, eskisi silinmez. +- `Description` **talebin yazıldığı hâliyle kalır**; MCP ona hiçbir şey yazmaz. `Description` ve + `Analysis` hiçbir zaman `update_row` ile yazılmaz — açıklama base64 görselleri taşır ve yeniden + yazmak onları siler. **Görseller.** Talebe yapıştırılan ekran görüntüleri açıklamada base64 durur; araç onları metinden ayıklar ve ayrı resim içeriği olarak verir, açıklamada yerlerinde `[image-N]` kalır. Kullanıcıdan @@ -600,12 +615,14 @@ görseli yeniden istemezsin. Görsel talebin parçasıdır: ekran düzenini (§0 değildir) ve içindeki satırları taşır; satırlar demo veridir (§0.10.1). **Kurallar** -- Talep tarifin kendisidir: kullanıcıya soru sorulmaz, belirsiz noktada §0.6'nın önerilen - varsayılanı seçilir ve sonuç notuna yazılır. Döngü DB Migrate'i beklemez. -- Talep §0.6 keşif turunu cevaplayacak kadar net değilse üretme; `Backlog`'a geri al ve eksik - soruları `Description` sonuna yaz. Tabloya yazma hedefi, onaycı ve hesap formülü talepten - okunamıyorsa varsayılmaz. -- Çerçeve dışı bir ihtiyaç (§0.8) çıkarsa talebi `In Progress`'te bırak ve kullanıcıya sor. +- **Onaylanmış analiz tarifin kendisidir; üretim karar vermez.** Soru sorulmaz, seçim yapılmaz, + analizin yazdığı hiçbir şey değiştirilmez ya da daraltılmaz. Bu dosyadaki "kullanıcıya sor" + kuralları (onaycı, görselden okunamayan zorunlu alan, çerçeve dışı ihtiyaç) bu döngüde analizin + **(onay)** kararlarıyla çözülmüştür. Analizde karşılığı olmayan küçük bir ayrıntı (§0.6 + varsayılanı olan türden) varsayılanla tamamlanır ve sonuç notuna yazılır. +- **Üretim tek seferde biter:** `In Progress` → `Testing` tek geçiştir; talep durdurulmaz, + `Analysis`'e geri alınmaz, kısmi ekran üretilmez. Üretilebilirlik analizde doğrulanmıştır + (§0.12). Döngü DB Migrate'i beklemez. - Test için ekrana satır ekleme — SeedSync açık ekranda seed dosyasına iz bırakır. Talebin kendi demo verisi test satırı değildir, §0.10.1 ile eklenir. - MCP talebi `Staging`'e almaz: deploy yalnızca dosya yazar, ekran migrate'ten sonra vardır ve testi kullanıcı yapar. @@ -614,6 +631,77 @@ kullanıcısı Wizard Manager'daki DB Migrate'i (`/api/setup/migrate`, `Setup.Mi migrate ekranları, menü/yetki/dil satırlarını ve demo verisini kurar, Redis'i temizler ve bütün kiracıları dolaşır. Rapor bu adımı tek satırla belirtir. +### 0.12 Talep analizi — `Backlog` → `Analysis` + +"Analiz et" dendiğinde `Backlog` talepleri üretimden önce analiz edilir; kullanıcı analizi okuyup +beğenirse talebi `Ready to Start`'a kendisi taşır, beğenmezse analiz değişir (MCP'ye yeniden +analiz ettirir ya da `Analysis` sekmesinde elle düzeltir). Analiz **üretim yapmaz**: tablo, wizard, +bileşen, seed dosyası yazılmaz; yalnızca okuma araçları ve `save_dev_request_analysis` kullanılır. + +| Adım | Araç | Durum | +| --- | --- | --- | +| 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 | — | +| 3. Keşif | `list_wizards` (menü kökleri, modüller, izin grupları), `list_lists` / `describe_list` / `search_schema` (var olan ekran, tablo, lookup kaynağı), `list_components`, `get_dev_guide` (§0.6, §0.7, §0.8, §1) | — | +| 4. Yaz | `save_dev_request_analysis` — analizin tamamı, HTML | Analysis | + +**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ı +reddedilir. `Description`'a hiçbir koşulda yazılmaz. + +**Analiz standardı.** HTML, aşağıdaki başlıklar bu sırayla (`

`), boş kalan başlık yazılmaz. +Kısa ve karar odaklı yaz; talebi tekrar anlatma. + +**Analiz kapalı uçludur ve üretilebilirdir.** Üretim analizi tek seferde, karar vermeden uygular +(§0.11); bu yüzden: +- Her konu tek bir kararla yazılır. Açık soru, seçenek listesi ("(a) … (b) …"), "istenir mi?", + "gerekirse", "ileride" bırakılmaz. +- Talebin açıkça yazdığı şey (menü yolu, alan, kapsam) **girdidir**: değiştirilmez, daraltılmaz, + alternatifi sunulmaz. +- Talepte olmayan bilgi için analiz karar verir (**Kararlar**, madde 8). Tabloya yazma hedefi, + onaycı ve hesap formülü de boş bırakılmaz: somut karar **(onay)** etiketiyle yazılır; talebi + `Ready to Start`'a alan kullanıcı onları onaylamış olur. +- Planın her adımının mevcut araçlarla (`create_table`, `deploy_wizard`, `save_component` ve + sözleşmeleri, §4.4) yapılabildiği keşifte doğrulanır (madde 10). + +1. **Amaç** — talep neden istenmiş, kim kullanacak, hangi işi çözecek (bir-iki cümle). +2. **Görsel analizi** — talepte görsel varsa her biri için (`[image-N]`): ekran türü (liste, form, + kanban, grafik, public sayfa…), görünen alanlar ve tipleri, gruplar/sekmeler, butonlar/aksiyonlar, + filtreler, görünen satırlar (demo veri adayı, §0.10.1). Görsel yoksa başlık yazılmaz. +3. **Çözüm yolu** — karar sırasındaki adım (Wizard List / Form / Custom component / SQL + Custom + Endpoint / Dynamic Service) ve neden. +4. **Menü yeri** — ekranın menüde nerede duracağı, tek bakışta okunur biçimde. Önce tam yol tek + satırda (`Kök › Ara › Ekran`), sonra kökten ekrana her seviye bir satır: + + | Seviye | Kod | Ad (TR / EN) | İkon | Durum | + | --- | --- | --- | --- | --- | + | Kök | … | … / … | … | Var / Yeni | + | Ara (varsa, her biri) | … | … / … | … | Var / Yeni | + | Ekran | `ListFormCode` | … / … | … | Yeni | + + Altında `ModuleId`, izin grubu ve route yazılır. Var olan seviyeler `list_wizards` ve menü + seed'inden okunur, kodları oradaki gibi yazılır; sıra (`Order`) deploy'da hesaplanır, analize + yazılmaz. Talep menü yolunu yazıyorsa (başlıkta ya da açıklamada) o yol girdidir ve seviye + seviye aynen alınır. +5. **Veri modeli** — tablo adı (onek kuralı §2), yeni mi var olan mı; kolonlar (ad, tip, zorunluluk, + uzunluk), lookup kaynakları, kiracı + audit (standing default 1). Yeni her tablonun ilk satırı + **Tanım tablosu: Evet / Hayır** kararıdır, talepteki dayanağıyla (§8.1); atlanmaz. Evet ise + kolon tablosunda `IsActive` (bit, varsayılan 1, zorunlu) yer alır — talepte ya da görselde + olmasa da — ve madde 7 (form alanı + Aktif/Pasif widget'ı) ile madde 9 (`create_table` kolonu, + demo veride `IsActive: true`) bunu taşır. +6. **Etkilenen kısımlar** — uygulamada dokunulacak her yer: yeni/değişen ekranlar, alt formlar, + menü, yetkiler (`C/R/U/D` + varsa özel), dil anahtarları, widget'lar, iş akışı/bildirim, SEO (public + yol varsa), var olan ekranlara etkiler. +7. **Görünümler ve davranış** — Grid/Card/Pivot/Chart (standing default 12), widget kartları (§4.6.1), + `EditorScript` / `EditorOptions` ihtiyaçları, hesaplar ve doğrulamalar. +8. **Kararlar** — talepten okunamayıp analizde verilen her karar, tek satır gerekçesiyle (§0.6); + varsayılamayan konular **(onay)** etiketi taşır. "Açık sorular" başlığı yoktur. +9. **Üretim planı** — sıralı artefakt listesi (`create_table: …`, `deploy_wizard: ….json`, + `save_component: …`); üretimdeki `add_dev_request_subtasks` başlıkları bununla aynı olur. +10. **Üretime hazır** — tek satır: **Evet**, ya da **Hayır — eksik yetenek: …** (çerçeve dışı + ihtiyaç, §0.8, adıyla). Talebi daraltan geçici çözüm önerilmez; "Hayır" taşıyan talep yetenek + eklenene kadar `Ready to Start`'a alınmaz. + ## 1. Hangi istek → hangi artefakt | İstek | Üretilecek artefakt | Bölüm | @@ -963,7 +1051,7 @@ atanmaz (admin dahil); ekranı görecek roller yetkiyi rol ekranından alır. | `TreeOptionDto` | `KeyExpr`, `ParentIdExpr`; opsiyonel `HasItemsExpr`, `RootValue`, `AutoExpandAll`, `RecursiveSelection` | | `GanttOptionDto` | `KeyExpr`, `ParentIdExpr`, `TitleExpr`, `StartExpr`, `EndExpr`; `ProgressExpr`, `ScaleType` (`auto`/`minutes`/`hours`/`sixHours`/`days`/`weeks`/`months`/`quarters`/`years`, varsayılan `weeks`), `Allow*` bayrakları | | `SchedulerOptionDto` | `TextExpr`, `StartDateExpr`, `EndDateExpr`; `AllDayExpr`, `RecurrenceRuleExpr`, `RecurrenceExceptionExpr`, `StartDayHour`, `EndDayHour`, `DefaultView` (`day`/`week`/`workWeek`/`month`/`timelineDay`/`timelineWeek`/`timelineMonth`/`agenda`, varsayılan `week`), `CellDuration`, `FirstDayOfWeek`, `Allow*` | -| `TodoOptionDto` | `TitleExpr`, `StatusExpr`; `DescriptionExpr`, `DueDateExpr`, `TagExpr`, `AssigneeExpr`, `PriorityExpr`, `CompletedExpr`, `OrderExpr`, `StatusOrder` (virgülle ayrılmış kolon sırası), `AllowDragging` | +| `TodoOptionDto` | `TitleExpr`, `StatusExpr`; `DescriptionExpr`, `AnalysisExpr`, `DueDateExpr`, `TagExpr`, `AssigneeExpr`, `PriorityExpr`, `CompletedExpr`, `OrderExpr`, `StatusOrder` (virgülle ayrılmış kolon sırası), `AllowDragging` | ### 4.3 `Groups` — alanlar ve düzenleme formu @@ -1408,7 +1496,15 @@ cevaba yazılmaz. ] } ], - "SubForms": [], "Widgets": [], + "SubForms": [], + // List ekranı widget'la gelir (§4.6.1): kiracı tablosu → her kartta TenantId koşulu. + "Widgets": [ + { + "SqlQuery": "SELECT 'App.Widget.Total' AS \"Title\", CAST(COUNT(*) AS VARCHAR(32)) AS \"Value\", 'blue' AS \"Color\", 'App.Mrp.Orders' AS \"SubTitle\", 'FaShoppingCart' AS \"Icon\" FROM \"Mrp_T_Order\" WHERE \"TenantId\" = '@TENANTID' UNION ALL SELECT 'App.Widget.Today', CAST(COUNT(*) AS VARCHAR(32)), 'green', 'App.Mrp.Orders', 'FaCalendarDay' FROM \"Mrp_T_Order\" WHERE \"TenantId\" = '@TENANTID' AND CAST(\"CreationTime\" AS DATE) = CAST(CURRENT_TIMESTAMP AS DATE) UNION ALL SELECT 'App.Widget.TotalRevenue', CAST(COALESCE(SUM(\"Total\"), 0) AS VARCHAR(32)), 'purple', 'App.Mrp.Orders', 'FaMoneyBillWave' FROM \"Mrp_T_Order\" WHERE \"TenantId\" = '@TENANTID'", + "Title": "Title", "Value": "Value", "Color": "Color", "Icon": "Icon", "SubTitle": "SubTitle", + "ValueClassName": "text-3xl", "ColSpan": 4, "ColGap": 3, "ClassName": "mb-3", "IsActive": true + } + ], "WorkflowDto": { "ApprovalUserFieldName": "", "ApprovalDateFieldName": "", "ApprovalStatusFieldName": "", "ApprovalDescriptionFieldName": "", "ApprovalIsFilterUserName": false, "ApprovalIsResetWorkflow": false, "Criteria": [] } @@ -1591,7 +1687,7 @@ widget, sorgunun döndürdüğü **her satır için bir kart** çizer: ```jsonc "Widgets": [ { - "SqlQuery": "SELECT 'App.Mrp.Widget.OpenOrders' AS Baslik, COUNT(*) AS Deger, 'blue' AS Renk, 'FcClock' AS Ikon FROM Mrp_T_Order WHERE IsDeleted = 'false' AND Status = 1", + "SqlQuery": "SELECT 'App.Mrp.Widget.OpenOrders' AS Baslik, COUNT(*) AS Deger, 'blue' AS Renk, 'FcClock' AS Ikon FROM Mrp_T_Order WHERE TenantId = '@TENANTID' AND Status = 1", "Title": "Baslik", // sorgudaki kolon adı "Value": "Deger", // sorgudaki kolon adı "Color": "Renk", @@ -1612,11 +1708,57 @@ widget, sorgunun döndürdüğü **her satır için bir kart** çizer: çevirir, karşılığı yoksa metni aynen basar. Sorguya düz metin değil anahtar yaz. - Tek sorguyla birden çok kart üretmek için `UNION ALL` kullan — üç durum için üç widget kaydı açmana gerek yok. -- Widget sorgusu ekranın otomatik tenant/şube filtresine tabi değildir ve `@TENANTID`/`@USERID` - gibi token'lar burada **çözülmez** (bağlanmamış parametre hatası verir). Kırılım gerekiyorsa - kiracıya özel bir `DataSource` kullan ya da kırılımı view'ın içine göm. +- Widget sorgusu ekranın otomatik tenant/şube filtresine tabi değildir; kırılımı sorgu kendisi + yazar. Sorgu `DefaultFilter` ile aynı token çözümünden geçer (`DefaultValueHelper`): + kiracı tablosunda `"TenantId" = '@TENANTID'` yaz — host'ta `IS NULL`'a çevrilir, liste ekranıyla + aynı kırılımı verir. `@USERID`, `@DATE` vb. de çözülür. Ayrıntı: `lowcode-reference.instructions.md` §5.3. +#### 4.6.1 Otomatik widget — Wizard ve MCP ile üretilen her liste ekranı + +Wizard ekranından ya da MCP `deploy_wizard` ile üretilen her **List** ekranı `Widgets` ile +gelir; kullanıcı "widget/KPI" demese de, sorulmadan eklenir ve tarifte tek satırla bildirilir. +İstek widget'ı açıkça dışarıda bırakırsa ya da ekran aşağıdaki istisnalardan biriyse eklenmez. + +- **En az 3 kart**, tek widget kaydında `UNION ALL` ile. `ColSpan = 12 / kartSayısı` + (3 kart → 4, 4 kart → 3, 6 kart → 2), `ColGap: 3`, `ClassName: "mb-3"`, + `ValueClassName: "text-3xl"`, `IsActive: true`. +- **Kartları tablonun anlamlı kolonlarından seç**, sırayla: + 1. Durum/statü kolonu (`Status`, `ApprovalStatus`, lookup'lı `dxSelectBox`) → değer başına bir + kart; başlık lookup'taki dil anahtarı. + 2. Boolean bayraklar (`IsActive`, `IsPublished`, `IsPinned`, `IsSent`…) → Aktif/Pasif, + Yayında/Taslak gibi çift. + 3. Tarih kolonu → "Bugün" (`CAST("Kolon" AS DATE) = CAST(CURRENT_TIMESTAMP AS DATE)`), + "Yaklaşan" (`"Kolon" >= CURRENT_TIMESTAMP`), "Süresi dolmuş". + 4. Sayısal/tutar kolonu → `COALESCE(SUM("Kolon"), 0)` (Toplam tutar, toplam görüntülenme). + 5. Hiçbiri 3 kartı doldurmuyorsa `App.Widget.Total` ile başla. +- **Sorgu kuralları:** + - Tablo `TenantId` taşıyorsa (`IsTenant: true`) her karta `"TenantId" = '@TENANTID'` yazılır + (§4.6, reference §5.3). Unutulursa ortak veritabanında kartlar bütün kiracıları sayar. + - `IsDeleted` koşulu widget'a **yazılmaz**. + - Değer `CAST(... AS VARCHAR(32))` ile metne çevrilir: sayı ve tutar kartları aynı + `UNION ALL`'da tip çakışmasına düşmez. + - Tanımlayıcılar çift tırnaklı yazılır, sağlayıcıya özgü fonksiyon (`DATEADD`, `GETDATE`, + `NOW()`) kullanılmaz; sorgu hem SQL Server hem PostgreSQL'de çalışmalıdır. +- **Metin:** `Title` bir dil anahtarıdır. Önce var olan anahtarlar kullanılır: genel + `App.Widget.*` (`Total`, `Active`, `Passive`, `Published`, `Draft`, `Today`, `Upcoming`, + `Pending`, `Sent`, `TotalViews`, `TotalRevenue`…), lookup'ın `App.StaticLookup.*` anahtarları, + alanın `App.Listform.ListformField.*` başlığı. Karşılığı yoksa yeni anahtar EN + TR yazılır. + `SubTitle` ekranın kendi başlık anahtarıdır (`ListFormCode`). +- **Renk ve ikon:** `blue` toplam, `green` olumlu/aktif, `gray` pasif/taslak, `red` hata/iptal, + `yellow`/`orange` bekleyen, `purple` tutar/özet. İkon `react-icons/fa` adıdır (`FaCheckCircle`, + `FaTimesCircle`, `FaCalendarDay`, `FaEye`, `FaMoneyBillWave`…). +- **İstisna — widget eklenmez:** Form ve Chart ekranları; alt form olarak kullanılan menüsüz + ekranlar (`CreateMenu: false`, beğeni/yorum/seçenek gibi detay tabloları); `IsActive` de + taşımayan, yalnızca kod + ad tutan eski tanım listeleri. Bunlarda anlamlı bir kırılım yoktur, + şerit yalnızca kayıt sayısını tekrarlar. Yeni tanım tablosu `IsActive` ile gelir (§8.1) ve + Toplam · Aktif · Pasif kartlarını alır. +- Kartların sorgusunu seed dosyasına yazmadan önce tablo kolonları doğrulanır + (MCP'de `search_schema`, ekranda tablo tasarımcısı). Var olmayan kolona yazılmış kart ekranı hatayla açar. + +Seeder'da (`ListFormSeeder_*`) aynı kural `DefaultWidgetsJson(tablo, listFormName, tenant, +params WidgetCard[])` ile uygulanır. + Alt form olarak kullanılacak ekranı **menüsüz wizard** (`CreateMenu: false`) ile üret; menüde görünmesin ama kendi yetkileri ve alan tanımları olsun. `Wizard.SeededAt` damgası ana wizard'ınkinden **küçük** olmalıdır (§0.2). @@ -3065,6 +3207,26 @@ tabloları için yalnız `T`/`D` üretir.) Kullanıcı açıkça "tenant yok / audit yok" demedikçe Multi-Tenant + Full Audited setleri dahil edilir; her seferinde sorma. +**Tanım tablosu → `IsActive` otomatik.** Tanım tablosu, başka ekranların seçtiği kod/ad/parametre +listesidir. Bir ekranın tanım olup olmadığına sabit bir ad listesiyle değil **talebin içeriğine +bakılarak** karar verilir: talep ekranı tanımlamaların altına koyuyorsa (başlıktaki ya da +açıklamadaki menü yolu, "tanım/parametre listesi" ifadesi) ya da amaç ekranın başka ekranlara +seçim kaynağı olmasıysa ekran tanımdır. Karar analizde verilir ve gerekçesiyle yazılır; üretim +kararı yeniden vermez. Tanım olan yeni tabloya iş kolonlarının sonuna, audit kolonlarından önce +sorulmadan eklenir: + +| Katman | `IsActive` | +| --- | --- | +| Tablo | `bit NOT NULL DEFAULT 1` (`create_table`'da iş kolonu olarak, `defaultValue: 1`) | +| Liste | Görünür kolon, `dxCheckBox` | +| Form | `dxCheckBox`, yeni kayıtta `true` (`InsertFieldsDefaultValueJson`) | +| Widget | Toplam · Aktif · Pasif kartları (§4.6.1 madde 2) | +| Demo veri | Satırlar `IsActive: true` ile yazılır | +| Dil | `App.Listform.ListformField.IsActive` (var olan anahtar) | + +Pasife alma silmenin yerine geçmez; silme açık kalır. Analiz bunu **Kararlar**'a değil **Veri +modeli**'ne yazar (§0.12 madde 5); tanım tablosu olduğunu da orada belirtir. Var olan tabloya kolon eklenmez (şema değişikliği ayrı iştir). + ### 8.2 View tasarımcısı (`SqlViewDesignerDialog`) — çalışma mantığı Bir ekran birden fazla tablodan besleniyorsa, `SelectCommandType: 4` (Query) yerine **tasarlanmış @@ -4169,6 +4331,8 @@ Prompt ile üretim yaptıysan ek olarak: - [ ] Liste ekranında `Grid`/`Card`/`Pivot`/`Chart` açık, `DefaultLayout: "grid"`; kapalı olan görünüm yalnızca istek onu açıkça dışarıda bıraktığı için kapalı (§9.8). - [ ] Açılan her görünümün zorunlu option alanı dolu (§9.8). +- [ ] List ekranında en az 3 kartlık `Widgets` var (istisna değilse); kiracı tablosunda her kart + `"TenantId" = '@TENANTID'` taşıyor, başlıklar dil anahtarı, kolonlar doğrulandı (§4.6.1). - [ ] Custom component'te route yetkisiz üretildiği için koruma bileşenin içinde (`designerPermission` / `*Permission` / `checkPermission`). - [ ] Custom component visual modda ise `Props.visualDesigner` ile `Code` marker'ı aynı dokümanı diff --git a/README.md b/README.md index 23a32e6a..af874d55 100644 --- a/README.md +++ b/README.md @@ -1410,14 +1410,16 @@ liste süzgeci ve çağrı kapısı aynı kuralı kullanı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). | | `verify_wizard` | Deploy edilmiş ekranı veri yazmadan doğrular: menü kaydı ve URL'si, menünün istediği yetki tanımı (`AbpPermissions`), List yolunda ekran sorgusu (`GetSelectAsync`, tek satır, çağıranın yetkisiyle), Custom yolunda bileşen (etkin, rota = menü URL'si) ve seed kapanışı (`ExportAnalysis` uyarıları; engellemez). İsteğe bağlıdır, migrate sonrasında kullanılır; `App.DevRequests` döngüsünün adımı değildir — döngü `Testing`'de biter, kullanıcı elle test edip `Staging`'e alır (`lowcode.instructions.md` §0.11). | | `start_dev_request` | Geliştirme talebini Id ya da tam başlıkla üstlenir: yalnızca `Ready to Start` durumundakini `In Progress` durumuna alır, diğerlerini reddeder; talebin metnini döndürür. Açıklamaya yapıştırılmış base64 görseller (`data:image/png|jpeg|gif|webp`) metinden ayıklanır (`McpDevRequestImages`), yerlerinde `[image-N]` kalır ve her biri ayrı MCP resim içeriği (`ImageContentBlock`) olarak döner — model görseli resim olarak görür, görsel hiçbir yere yüklenmez. `.Write` + `App.DevRequests.Update` ister. | -| `get_dev_request` | Talebi durumuna dokunmadan, `start_dev_request` ile aynı biçimde (metin + resim içerikleri) okur; 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. 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. | -| `append_dev_request_note` | Talep açıklamasının sonuna HTML ekler, var olan metne ve görsellere dokunmaz. Açıklama base64 görsel taşıdığı için `update_row` ile yeniden yazılmaz. `.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. | **Prompt'lar** (`McpPromptNames`, `McpDevelopPrompts`). İstemcide komut olarak görünür; yalnızca -metindir, yetki taşımaz. `process_dev_request` tek talebi, `process_ready_requests` bütün -`Ready to Start` kuyruğunu işler. İkisi de adımları tekrar etmez, `get_dev_guide` ile -`lowcode.instructions.md` §0.11'e yönlendirir. +metindir, yetki taşımaz. `analyze_dev_request` tek talebi, `analyze_backlog_requests` bütün +`Backlog` kuyruğunu analiz edip `Analysis` durumuna alır (§0.12); `process_dev_request` tek talebi, +`process_ready_requests` bütün `Ready to Start` kuyruğunu işler (§0.11). Hiçbiri adımları tekrar +etmez, `get_dev_guide` ile `lowcode.instructions.md` ilgili bölümüne yönlendirir. **Geliştirme taleplerini çalıştırma.** Komutun adı istemcideki sunucu adına göre değişir: aşağıdaki örnekler `claude mcp add sozsoft ...` ile eklenmiş sunucu içindir (`sozsoft-local` ile eklediysen @@ -1426,6 +1428,8 @@ değilse sunucu reddeder ve hiçbir şey üretilmez. | Ne istiyorsun | Claude Code | Claude.ai / Claude Desktop | Düz metin (her istemci) | | --- | --- | --- | --- | +| Tek talebi analiz et | `/mcp__sozsoft__analyze_dev_request ` | `+` menüsü → `sozsoft` → `analyze_dev_request` | ` talebini analiz et` | +| Backlog'u analiz et | `/mcp__sozsoft__analyze_backlog_requests` | `+` menüsü → `sozsoft` → `analyze_backlog_requests` | `Backlog'daki talepleri analiz et` | | Tek talep | `/mcp__sozsoft__process_dev_request ` | Mesaj kutusundaki `+` menüsü → `sozsoft` → `process_dev_request`, `request` alanına Id ya da tam başlık | ` talebini işle` | | Bütün kuyruk | `/mcp__sozsoft__process_ready_requests` | `+` menüsü → `sozsoft` → `process_ready_requests` | `Ready to Start durumundaki talepleri sırayla işle` | | Önce kuyruğu görmek | — | — | `Ready to Start durumundaki talepleri Id'leriyle listele` | @@ -1459,7 +1463,7 @@ yetkisi yalnızca kapıdır. Bir çağrı üç katmandan geçer ve her katman ay | --- | --- | --- | --- | | `App.DeveloperKit.McpServers` | Host, kiracı | MCP Servers ekranını görme (`.Create/.Update/.Delete/.Export/.Import` ekran işlemleri) | `/mcp` ucuna erişim vermez | | `App.DeveloperKit.McpServers.Invoke` | Host, kiracı | `/mcp` bağlantısı ve bütün okuma araçları: `server_info`, `list_lists`, `describe_list`, `search_schema`, `query_list`; host'ta ayrıca `get_dev_guide`, `list_wizards`, `get_wizard`, `verify_wizard`, `list_components`, `get_component` | Hiçbir şey yazmaz. Okuma da ekran yetkisine bağlıdır: `R` yetkisi olmayan liste görünmez, sorgulanamaz. Ham SQL (`run_select`, `list_queries`, `run_query`) ayrıca `App.SqlQueryManager` ister | -| `App.DeveloperKit.McpServers.Write` | Host, kiracı | MCP üzerinden her değişiklik. **Kiracıda:** `insert_row`, `update_row`, `delete_row`. **Host'ta:** bunlara ek olarak `create_table`, `deploy_wizard`, `delete_wizard`, `save_component`, `delete_component`, `start_dev_request`, `append_dev_request_note`, `add_dev_request_subtasks` | Kendi başına bir şey yazdırmaz: satır araçları ekranın `C`/`U`/`D` yetkisini, geliştirme araçları Wizard / Custom Components / SQL Query Manager / `App.DevRequests.Update` yetkisini ayrıca ister. Kiracıda geliştirme araçlarını **açmaz** — kiracı bağlamında bu araçlar yetkiden bağımsız kapalıdır (`McpToolNames.TenantTools`). `Invoke` olmadan işe yaramaz | +| `App.DeveloperKit.McpServers.Write` | Host, kiracı | MCP üzerinden her değişiklik. **Kiracıda:** `insert_row`, `update_row`, `delete_row`. **Host'ta:** bunlara ek olarak `create_table`, `deploy_wizard`, `delete_wizard`, `save_component`, `delete_component`, `start_dev_request`, `save_dev_request_analysis`, `append_dev_request_note`, `add_dev_request_subtasks` | Kendi başına bir şey yazdırmaz: satır araçları ekranın `C`/`U`/`D` yetkisini, geliştirme araçları Wizard / Custom Components / SQL Query Manager / `App.DevRequests.Update` yetkisini ayrıca ister. Kiracıda geliştirme araçlarını **açmaz** — kiracı bağlamında bu araçlar yetkiden bağımsız kapalıdır (`McpToolNames.TenantTools`). `Invoke` olmadan işe yaramaz | **Araç → gereken yetkiler** (Invoke hepsinde aranır, tabloda tekrar yazılmadı) @@ -1481,6 +1485,7 @@ yetkisi yalnızca kapıdır. Bir çağrı üç katmandan geçer ve her katman ay | `delete_component` | — | `.Write` | `App.DeveloperKit.CustomComponents.Delete` | | `start_dev_request` | — | `.Write` | `App.DevRequests.Update` | | `get_dev_request` | — | — | `App.DevRequests` | +| `save_dev_request_analysis` | — | `.Write` | `App.DevRequests.Update` | | `append_dev_request_note` | — | `.Write` | `App.DevRequests.Update` | | `add_dev_request_subtasks` | — | `.Write` | `App.DevRequests.Update` | @@ -1541,7 +1546,10 @@ kimlik doğrulamayı istemci MCP OAuth akışıyla kendisi yürütür: Üç ayrıntı bilinçlidir: - Yayıncı (`issuer`) `AuthServer:Authority`'den sabitlenir; ters vekil arkasında istekten - türetilirse `http://` olur ve istemci keşif belgesini reddeder. + türetilirse `http://` olur ve istemci keşif belgesini reddeder. Korumalı kaynak belgesindeki + `authorization_servers` değeri bu `issuer` ile birebir aynıdır (sonda `/` dahil, + `McpOAuthOptions.AuthorizationServer`); RFC 8414 §3.3'ü uygulayan istemci (`mcp-remote`) + farkta `IssuerMismatchError` ile durur. - MCP istemcisinin gönderdiği RFC 8707 `resource` parametresi yok sayılır (`McpOAuthServerConfiguration`): OpenIddict kayıtsız kaynağı reddeder ya da token kitlesini `/mcp` adresine daraltır, API ise `Platform` kitlesini doğrular. diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application.Contracts/Dtos/McpDevelopDtos.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application.Contracts/Dtos/McpDevelopDtos.cs index a38b181d..790c6136 100644 --- a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application.Contracts/Dtos/McpDevelopDtos.cs +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application.Contracts/Dtos/McpDevelopDtos.cs @@ -147,6 +147,12 @@ public class McpDevRequestDto /// Aciklamadaki gorseller, metinde gectikleri sirayla. public List Images { get; set; } = []; + /// + /// HTML. Talebin analizi ve MCP sonuc notlari; talebin yazildigi + /// haliyle kalir, uretim ikisine birlikte bakar. + /// + public string? Analysis { get; set; } + public string? Priority { get; set; } public string? Tags { get; set; } diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application.Contracts/IMcpDevelopAppService.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application.Contracts/IMcpDevelopAppService.cs index 952603bd..c052d90d 100644 --- a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application.Contracts/IMcpDevelopAppService.cs +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application.Contracts/IMcpDevelopAppService.cs @@ -38,8 +38,14 @@ public interface IMcpDevelopAppService : IApplicationService Task GetDevRequestAsync(string request); /// - /// Aciklamanin sonuna HTML ekler. Aciklama gomulu gorsel tasidigi icin satir guncellemesiyle - /// yeniden yazilamaz; ekleme sunucuda yapilir. + /// Talebin analizini yazar (var olani degistirir) ve talebi Analysis durumuna alir; yalnizca + /// Backlog ya da Analysis durumundaki talep analiz edilir. + /// + Task SaveDevRequestAnalysisAsync(string request, string html); + + /// + /// Analizin sonuna HTML ekler. Aciklama talebin yazildigi haliyle kalir; uretim notlari + /// analize yazilir. /// Task AppendDevRequestNoteAsync(string request, string html); diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpDevelopAppService.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpDevelopAppService.cs index 2e8020b5..a5a2e48e 100644 --- a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpDevelopAppService.cs +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpDevelopAppService.cs @@ -383,8 +383,37 @@ public class McpDevelopAppService( } /// - /// Aciklama base64 gorsel tasir; model onu update_row ile yeniden yazarsa gorselleri geri - /// gonderemez ve siler. Ekleme bu yuzden sunucuda, var olan metne dokunmadan yapilir. + /// 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 + /// kullanici alir. + /// + public async Task SaveDevRequestAnalysisAsync(string request, string html) + { + await EnsureToolAllowedAsync(McpToolNames.SaveDevRequestAnalysis); + await AuthorizationService.CheckAsync(DevRequestUpdatePermission); + + if (string.IsNullOrWhiteSpace(html)) + { + throw new UserFriendlyException(L[McpErrorCodes.DevRequestAnalysisRequired]); + } + + var devRequest = await FindDevRequestAsync(request); + + if (devRequest.Status is not (McpConsts.DevRequestStatuses.Backlog or McpConsts.DevRequestStatuses.Analysis)) + { + throw new UserFriendlyException(L[McpErrorCodes.DevRequestNotAnalyzable, devRequest.Title ?? devRequest.Id.ToString(), devRequest.Status ?? string.Empty]); + } + + devRequest.Analysis = html.Trim(); + devRequest.Status = McpConsts.DevRequestStatuses.Analysis; + await devRequestRepository.UpdateAsync(devRequest, autoSave: true); + + return ToDto(devRequest); + } + + /// + /// Aciklama talebin yazildigi haliyle kalir ve base64 gorsel tasir; uretim notlari analize + /// eklenir. Ekleme sunucuda, var olan metne dokunmadan yapilir. /// public async Task AppendDevRequestNoteAsync(string request, string html) { @@ -397,7 +426,7 @@ public class McpDevelopAppService( } var devRequest = await FindDevRequestAsync(request); - devRequest.Description = string.Concat(devRequest.Description, html.Trim()); + devRequest.Analysis = string.Concat(devRequest.Analysis, html.Trim()); await devRequestRepository.UpdateAsync(devRequest, autoSave: true); } @@ -461,6 +490,7 @@ public class McpDevelopAppService( Status = devRequest.Status, Description = description, Images = images, + Analysis = devRequest.Analysis, Priority = devRequest.Priority, Tags = devRequest.Tags, DueDate = devRequest.DueDate, diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain.Shared/McpConsts.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain.Shared/McpConsts.cs index 6b8381ee..d56ab434 100644 --- a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain.Shared/McpConsts.cs +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain.Shared/McpConsts.cs @@ -103,6 +103,8 @@ public static class McpConsts /// public static class DevRequestStatuses { + public const string Backlog = "Backlog"; + public const string Analysis = "Analysis"; public const string ReadyToStart = "Ready to Start"; public const string InProgress = "In Progress"; } diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain.Shared/McpErrorCodes.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain.Shared/McpErrorCodes.cs index 3ed3539c..7efc8a0d 100644 --- a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain.Shared/McpErrorCodes.cs +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain.Shared/McpErrorCodes.cs @@ -53,6 +53,8 @@ public static class McpErrorCodes public const string DevRequestNotFound = Prefix + "DevRequestNotFound"; public const string DevRequestAmbiguous = Prefix + "DevRequestAmbiguous"; public const string DevRequestNotReady = Prefix + "DevRequestNotReady"; + public const string DevRequestNotAnalyzable = Prefix + "DevRequestNotAnalyzable"; + public const string DevRequestAnalysisRequired = Prefix + "DevRequestAnalysisRequired"; public const string DevRequestNoteRequired = Prefix + "DevRequestNoteRequired"; public const string DevRequestSubTasksRequired = Prefix + "DevRequestSubTasksRequired"; public const string DevRequestSubTasksInvalid = Prefix + "DevRequestSubTasksInvalid"; diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain.Shared/McpPromptNames.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain.Shared/McpPromptNames.cs index d6f8a3a9..3e578cb5 100644 --- a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain.Shared/McpPromptNames.cs +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain.Shared/McpPromptNames.cs @@ -6,6 +6,8 @@ namespace Sozsoft.Mcp; /// public static class McpPromptNames { + public const string AnalyzeDevRequest = "analyze_dev_request"; + public const string AnalyzeBacklogRequests = "analyze_backlog_requests"; public const string ProcessDevRequest = "process_dev_request"; public const string ProcessReadyRequests = "process_ready_requests"; } diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain.Shared/McpToolNames.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain.Shared/McpToolNames.cs index 1c3909a8..ff0388d7 100644 --- a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain.Shared/McpToolNames.cs +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain.Shared/McpToolNames.cs @@ -31,6 +31,7 @@ public static class McpToolNames public const string VerifyWizard = "verify_wizard"; public const string StartDevRequest = "start_dev_request"; public const string GetDevRequest = "get_dev_request"; + public const string SaveDevRequestAnalysis = "save_dev_request_analysis"; public const string AppendDevRequestNote = "append_dev_request_note"; public const string AddDevRequestSubTasks = "add_dev_request_subtasks"; public const string ListComponents = "list_components"; @@ -54,6 +55,7 @@ public static class McpToolNames VerifyWizard, StartDevRequest, GetDevRequest, + SaveDevRequestAnalysis, AppendDevRequestNote, AddDevRequestSubTasks, ListComponents, @@ -106,6 +108,7 @@ public static class McpToolNames [DeployWizard] = PlatformConsts.AppCodes.DeveloperKits.McpServers.Write, [DeleteWizard] = PlatformConsts.AppCodes.DeveloperKits.McpServers.Write, [StartDevRequest] = PlatformConsts.AppCodes.DeveloperKits.McpServers.Write, + [SaveDevRequestAnalysis] = PlatformConsts.AppCodes.DeveloperKits.McpServers.Write, [AppendDevRequestNote] = PlatformConsts.AppCodes.DeveloperKits.McpServers.Write, [AddDevRequestSubTasks] = PlatformConsts.AppCodes.DeveloperKits.McpServers.Write, [SaveComponent] = PlatformConsts.AppCodes.DeveloperKits.McpServers.Write, diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/McpHttpApiModule.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/McpHttpApiModule.cs index 958b832c..14e9c7b4 100644 --- a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/McpHttpApiModule.cs +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/McpHttpApiModule.cs @@ -1,4 +1,5 @@ -using System.Linq; +using System; +using System.Linq; using Microsoft.Extensions.DependencyInjection; using ModelContextProtocol.AspNetCore; using Sozsoft.Mcp.Tools; @@ -25,7 +26,12 @@ public class McpHttpApiModule : AbpModule Configure(options => { options.BaseUrl = (configuration["App:SelfUrl"] ?? string.Empty).TrimEnd('/'); - options.AuthorizationServer = (configuration["AuthServer:Authority"] ?? string.Empty).TrimEnd('/'); + // OpenIddict keşif belgesindeki issuer'ı Uri.AbsoluteUri ile (sonda eğik çizgiyle) yazar; + // RFC 8414 §3.3 gereği istemci iki değeri birebir karşılaştırır. + var authority = configuration["AuthServer:Authority"]; + options.AuthorizationServer = string.IsNullOrWhiteSpace(authority) + ? string.Empty + : new Uri(authority).AbsoluteUri; }); context.Services diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/McpOAuthOptions.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/McpOAuthOptions.cs index 5b50eebe..025f0f3a 100644 --- a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/McpOAuthOptions.cs +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/McpOAuthOptions.cs @@ -9,7 +9,7 @@ public class McpOAuthOptions /// API'nin disaridan erisilen kok adresi, sonunda egik cizgi olmadan. public string BaseUrl { get; set; } = string.Empty; - /// Token'i veren yetkilendirme sunucusu (OpenIddict), sonunda egik cizgi olmadan. + /// Token'i veren yetkilendirme sunucusu (OpenIddict); keşif belgesindeki issuer ile birebir aynı. public string AuthorizationServer { get; set; } = string.Empty; /// Korumali kaynagin tanimlayicisi: istemcinin baglandigi /mcp adresi. diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/McpServerInfoDefaults.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/McpServerInfoDefaults.cs index 8794f3aa..972075a6 100644 --- a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/McpServerInfoDefaults.cs +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/McpServerInfoDefaults.cs @@ -23,9 +23,11 @@ internal static class McpServerInfoDefaults "section you need), inspect what exists with list_wizards/get_wizard and list_components/get_component, " + "then produce them with deploy_wizard and save_component; the guide's file-writing steps map to these tools. " + "Agree on screen type, data source, module and permissions with the user before deploying. " + + "Analyzing development requests (Backlog -> Analysis) follows get_dev_guide section 0.12: read with get_dev_request, " + + "write the analysis with save_dev_request_analysis, produce nothing and never touch Description; the user moves an approved analysis to Ready to Start. " + "Development requests (App.DevRequests) follow get_dev_guide section 0.11: claim each one with " + "start_dev_request, which only accepts requests in Ready to Start; images pasted into the request arrive as image content and are part of the request (layout and demo rows). " + - "A request is its own brief: do not ask questions and do not wait for DB Migrate; deploy (demo rows go in deploy_wizard seedRows), move the request to Testing and finish. " + + "A request (Description) and its approved Analysis are its own brief: do not ask questions and do not wait for DB Migrate; deploy (demo rows go in deploy_wizard seedRows), move the request to Testing and finish. " + "When a tool reports a missing permission, tell the user which permission is missing and stop; " + "do not try to reach the same result through another tool."; } diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/Tools/McpDevelopPrompts.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/Tools/McpDevelopPrompts.cs index 3c49c2e4..4143cd88 100644 --- a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/Tools/McpDevelopPrompts.cs +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/Tools/McpDevelopPrompts.cs @@ -1,16 +1,38 @@ -using System.ComponentModel; +using System.ComponentModel; using ModelContextProtocol.Server; namespace Sozsoft.Mcp.Tools; /// -/// Gelistirme dongusunun hazir prompt'lari. Proseduru tekrar etmez, rehberin 0.11 bolumune -/// yonlendirir; boylece adimlar tek yerde (lowcode.instructions.md) degisir. Durum kurali -/// start_dev_request'te sunucu tarafinda uygulanir. +/// Gelistirme dongusunun hazir prompt'lari. Proseduru tekrar etmez, rehberin 0.11 (uretim) ve +/// 0.12 (analiz) bolumlerine yonlendirir; boylece adimlar tek yerde (lowcode.instructions.md) +/// degisir. Durum kurallari start_dev_request ve save_dev_request_analysis'te sunucu tarafinda +/// uygulanir. /// [McpServerPromptType] public static class McpDevelopPrompts { + [McpServerPrompt(Name = McpPromptNames.AnalyzeDevRequest)] + [Description("Tek bir gelistirme talebini analiz eder ve Analysis durumuna alir. Talep Backlog ya da Analysis durumunda olmalidir.")] + public static string AnalyzeDevRequest( + [Description("Talebin Id'si (GUID) ya da tam basligi.")] + string request) + => $"App.DevRequests kuyrugundaki '{request}' talebini analiz et; yalnizca bu talebi. " + + "Once get_dev_guide ile lowcode 0.12 bolumunu oku ve oradaki standarda uy. " + + "Talebi ve gorsellerini get_dev_request ile oku; analizi save_dev_request_analysis ile yaz. " + + "Hicbir sey uretme (tablo, wizard, bilesen yok) ve Description'a dokunma. " + + "Arac reddederse (talep Backlog ya da Analysis degil, bulunamadi ya da birden fazla eslesme var) nedenini bildir ve dur. " + + "Sonunda kisa rapor ver: menu yeri, uretilecek artefaktlar, (onay) etiketli kararlar ve Uretime hazir satiri; talebi Ready to Start'a kullanicinin alacagini belirt."; + + [McpServerPrompt(Name = McpPromptNames.AnalyzeBacklogRequests)] + [Description("Backlog durumundaki tum gelistirme taleplerini SortOrder sirasiyla analiz eder ve Analysis durumuna alir.")] + public static string AnalyzeBacklogRequests() + => "App.DevRequests kuyrugundaki Backlog taleplerini query_list ile SortOrder sirasiyla oku ve tek tek analiz et. " + + "Once get_dev_guide ile lowcode 0.12 bolumunu oku ve oradaki standarda uy. " + + "Her talebi get_dev_request ile oku, analizi save_dev_request_analysis ile yaz; reddedilen talebi atla ve rapora yaz. " + + "Hicbir sey uretme ve Description'a dokunma. " + + "Sonunda parti raporu ver: talep basina menu yeri, (onay) etiketli karar sayisi ve Uretime hazir durumu; talepleri Ready to Start'a kullanicinin alacagini tek satirda belirt."; + [McpServerPrompt(Name = McpPromptNames.ProcessDevRequest)] [Description("Tek bir gelistirme talebini isler. Talep Ready to Start durumunda olmalidir.")] public static string ProcessDevRequest( @@ -20,7 +42,7 @@ public static class McpDevelopPrompts "Once get_dev_guide ile lowcode 0.11 bolumunu oku ve oradaki adimlari uygula. " + "Talebi start_dev_request ile ustlen. Arac reddederse (talep Ready to Start degil, bulunamadi ya da " + "birden fazla eslesme var) hicbir sey uretme, baska bir talebe gecme: nedenini bildir ve dur. " + - "Talep tarifin kendisidir: soru sorma, DB Migrate'i bekleme; talebi Testing'e alip bitir. " + + "Talep (Description) ve onaylanmis analizi (Analysis) birlikte tarifin kendisidir: soru sorma, DB Migrate'i bekleme; talebi Testing'e alip bitir. " + "Sonunda rapor ver: uretilen artefaktlar, talebin son durumu ve DB Migrate gerektigi."; [McpServerPrompt(Name = McpPromptNames.ProcessReadyRequests)] @@ -29,6 +51,6 @@ public static class McpDevelopPrompts => "App.DevRequests kuyrugundaki Ready to Start taleplerini query_list ile SortOrder sirasiyla oku ve tek tek isle. " + "Once get_dev_guide ile lowcode 0.11 bolumunu oku ve oradaki adimlari uygula. " + "Her talebi start_dev_request ile ustlen; reddedilen talebi atla ve rapora yaz. " + - "Talep tarifin kendisidir: soru sorma, DB Migrate'i bekleme; her talebi Testing'e alip digerine gec. " + + "Talep (Description) ve onaylanmis analizi (Analysis) birlikte tarifin kendisidir: soru sorma, DB Migrate'i bekleme; her talebi Testing'e alip digerine gec. " + "Sonunda parti raporu ver: talep basina uretilen artefaktlar ve son durum; DB Migrate gerektigini tek satirda belirt."; } diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/Tools/McpDevelopTools.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/Tools/McpDevelopTools.cs index d70ffa56..a346bcdc 100644 --- a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/Tools/McpDevelopTools.cs +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/Tools/McpDevelopTools.cs @@ -91,21 +91,30 @@ public sealed class McpDevelopTools(IMcpDevelopAppService developAppService) => await developAppService.VerifyWizardAsync(fileName); [McpServerTool(Name = McpToolNames.StartDevRequest, Destructive = false, Idempotent = false)] - [Description("App.DevRequests kuyrugundaki bir gelistirme talebini ustlenir: talep Ready to Start durumundaysa In Progress'e alir ve talebin metnini (baslik, HTML aciklama, oncelik, etiketler, SubTasks) dondurur; baska bir durumdaysa reddeder. Aciklamaya yapistirilmis gorseller ayri resim icerigi olarak gelir, aciklamada yerlerinde [image-N] isareti durur; gorseldeki ekran duzeni ve satirlar talebin parcasidir (demo veri icin deploy_wizard SeedData). Bir talep uzerinde calismaya bu aracla baslanir, Status'u update_row ile In Progress yapma. Sonraki adimlar icin get_dev_guide lowcode 0.11.")] + [Description("App.DevRequests kuyrugundaki bir gelistirme talebini ustlenir: talep Ready to Start durumundaysa In Progress'e alir ve talebin metnini (baslik, HTML aciklama, onaylanmis HTML analiz, oncelik, etiketler, SubTasks) dondurur; baska bir durumdaysa reddeder. Aciklamaya yapistirilmis gorseller ayri resim icerigi olarak gelir, aciklamada yerlerinde [image-N] isareti durur; gorseldeki ekran duzeni ve satirlar talebin parcasidir (demo veri icin deploy_wizard SeedData). Uretim aciklama ile analize birlikte dayanir; analizdeki menu, tablo ve kapsam kararlari onaylanmistir. Bir talep uzerinde calismaya bu aracla baslanir, Status'u update_row ile In Progress yapma. Sonraki adimlar icin get_dev_guide lowcode 0.11.")] public async Task> StartDevRequestAsync( [Description("Talebin Id'si (GUID) ya da tam basligi. Ayni basligi tasiyan birden fazla talep varsa Id ver.")] string request) => ToContent(await developAppService.StartDevRequestAsync(request)); [McpServerTool(Name = McpToolNames.GetDevRequest, ReadOnly = true)] - [Description("Bir gelistirme talebini durumuna dokunmadan okur; start_dev_request ile ayni cevabi (aciklama + ayri resim icerikleri) dondurur. Yarim kalmis (In Progress/Testing) bir talebe donerken ya da gorselleri yeniden gormek icin kullan; talebi ustlenmek icin degil.")] + [Description("Bir gelistirme talebini durumuna dokunmadan okur; start_dev_request ile ayni cevabi (aciklama + ayri resim icerikleri) dondurur. Analiz oncesi talebi okumak, yarim kalmis (In Progress/Testing) bir talebe donmek ya da gorselleri yeniden gormek icin kullan; talebi ustlenmek icin degil.")] public async Task> GetDevRequestAsync( [Description("Talebin Id'si (GUID) ya da tam basligi.")] string request) => ToContent(await developAppService.GetDevRequestAsync(request)); + [McpServerTool(Name = McpToolNames.SaveDevRequestAnalysis, Destructive = true, Idempotent = true)] + [Description("Backlog ya da Analysis durumundaki bir talebin analizini (HTML) Analysis alanina yazar ve talebi Analysis durumuna alir; var olan analiz degistirilir, Description'a dokunulmaz. Diger durumlarda reddeder. Once get_dev_request ile talebi ve gorsellerini oku; analizin bolumleri ve standardi get_dev_guide lowcode 0.12'dedir. Analizi begenen kullanici talebi Ready to Start'a kendisi alir; MCP almaz.")] + public async Task> SaveDevRequestAnalysisAsync( + [Description("Talebin Id'si (GUID) ya da tam basligi.")] + string request, + [Description("Analizin tamami, HTML.")] + string html) + => ToContent(await developAppService.SaveDevRequestAnalysisAsync(request, html)); + [McpServerTool(Name = McpToolNames.AppendDevRequestNote, Destructive = false, Idempotent = false)] - [Description("Talebin HTML aciklamasinin sonuna ekleme yapar; var olan metne ve gorsellere dokunmaz. Aciklama gomulu gorsel tasidigi icin Description'i update_row ile yeniden yazma, sonuc notunu (

MCP

+ uretilenler ve DB Migrate bekleyen wizard'lar) bu aracla ekle.")] + [Description("Talebin Analysis alaninin sonuna HTML ekler; var olan analize ve Description'a dokunmaz. Description talebin yazildigi haliyle kalir; uretim sonuc notunu (

MCP

+ uretilenler, varsayimlar ve DB Migrate bekleyen wizard'lar) bu aracla ekle. Description'i ya da Analysis'i update_row ile yazma.")] public async Task AppendDevRequestNoteAsync( [Description("Talebin Id'si (GUID) ya da tam basligi.")] string request, diff --git a/api/src/Sozsoft.Platform.Application.Contracts/ListForms/GridOptionsDto/TodoOptionDto.cs b/api/src/Sozsoft.Platform.Application.Contracts/ListForms/GridOptionsDto/TodoOptionDto.cs index 97e19419..49c36147 100644 --- a/api/src/Sozsoft.Platform.Application.Contracts/ListForms/GridOptionsDto/TodoOptionDto.cs +++ b/api/src/Sozsoft.Platform.Application.Contracts/ListForms/GridOptionsDto/TodoOptionDto.cs @@ -9,6 +9,7 @@ public class TodoOptionDto public string TitleExpr { get; set; } public string StatusExpr { get; set; } public string DescriptionExpr { get; set; } + public string AnalysisExpr { get; set; } public string DueDateExpr { get; set; } public string TagExpr { get; set; } public string AssigneeExpr { get; set; } diff --git a/api/src/Sozsoft.Platform.Application/ListForms/ListFormSelectAppService.cs b/api/src/Sozsoft.Platform.Application/ListForms/ListFormSelectAppService.cs index 3f0eac2a..80668856 100644 --- a/api/src/Sozsoft.Platform.Application/ListForms/ListFormSelectAppService.cs +++ b/api/src/Sozsoft.Platform.Application/ListForms/ListFormSelectAppService.cs @@ -252,7 +252,10 @@ public class ListFormSelectAppService : PlatformAppService, IListFormSelectAppSe Items = [] }; - var items = await dynamicDataRepository.QueryAsync(widget.SqlQuery, connectionString); + // Widget sorgusu ekranın otomatik tenant filtresinden geçmez; kırılımı DefaultFilter ile + // aynı token'larla (ör. "TenantId" = '@TENANTID', host'ta IS NULL) sorgu kendisi yazar. + var sql = defaultValueHelper.GetDefaultValue(widget.SqlQuery); + var items = await dynamicDataRepository.QueryAsync(sql, connectionString); if (items != null) { foreach (var item in items) diff --git a/api/src/Sozsoft.Platform.DbMigrator/Migrations/ListFormSeeder_Administration.cs b/api/src/Sozsoft.Platform.DbMigrator/Migrations/ListFormSeeder_Administration.cs index b7613511..a283b38d 100644 --- a/api/src/Sozsoft.Platform.DbMigrator/Migrations/ListFormSeeder_Administration.cs +++ b/api/src/Sozsoft.Platform.DbMigrator/Migrations/ListFormSeeder_Administration.cs @@ -962,54 +962,10 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep new() { FieldName = "UserType", FieldDbType = DbType.Int32, Value = "0", CustomValueType = FieldCustomValueTypeEnum.Value }, new() { FieldName = "IsActive", FieldDbType = DbType.Boolean, Value = "true", CustomValueType = FieldCustomValueTypeEnum.Value } }), - WidgetsJson = JsonSerializer.Serialize(new List - { - new() - { - ColGap = 3, - ColSpan = 4, - SqlQuery = @" - SELECT - N'Aktif' AS ""Title"", - COUNT(""Id"") AS ""Value"", - 'blue' AS ""Color"", - N'Aktif Kullanıcılar' AS ""SubTitle"", - 'FaUserCheck' AS ""Icon"" - FROM ""AbpUsers"" - WHERE ""IsActive"" = 'true' - - UNION ALL - - SELECT - N'Pasif' AS ""Title"", - COUNT(""Id"") AS ""Value"", - 'green' AS ""Color"", - N'Pasif Kullanıcılar' AS ""SubTitle"", - 'FaUserSlash' AS ""Icon"" - FROM ""AbpUsers"" - WHERE ""IsActive"" = 'false' - - UNION ALL - - SELECT - N'Doğrulama' AS ""Title"", - COUNT(""Id"") AS ""Value"", - 'purple' AS ""Color"", - N'Doğrulama bekleyenler' AS ""SubTitle"", - 'FaUserClock' AS ""Icon"" - FROM ""AbpUsers"" - WHERE ""IsVerified"" = 'false'; - ", - Title = "Title", - Value = "Value", - ValueClassName = "bg-5 text-sm", - Color = "Color", - Icon = "Icon", - SubTitle = "SubTitle", - ClassName = "mb-3", - IsActive = true - } - }) + WidgetsJson = DefaultWidgetsJson("AbpUsers", listFormName, true, + new("App.Widget.Active", "green", "FaUserCheck", "\"IsActive\" = 'true'"), + new("App.Widget.Passive", "gray", "FaUserSlash", "\"IsActive\" = 'false'"), + new("App.Widget.Unverified", "purple", "FaUserClock", "\"IsVerified\" = 'false'")) }); #region Users Fields @@ -1397,6 +1353,10 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep Description = $"{listFormName}.Description", SelectCommandType = SelectCommandTypeEnum.Table, SelectCommand = "AbpAuditLogs", + WidgetsJson = DefaultWidgetsJson("AbpAuditLogs", listFormName, true, + new("App.Widget.Total", "blue", "FaHistory"), + new("App.Widget.Today", "green", "FaCalendarDay", "CAST(\"ExecutionTime\" AS DATE) = CAST(CURRENT_TIMESTAMP AS DATE)"), + new("App.Widget.Errors", "red", "FaExclamationTriangle", "(\"HttpStatusCode\" >= 400 OR \"Exceptions\" IS NOT NULL)")), KeyFieldName = "Id", KeyFieldDbSourceType = DbType.Guid, SortMode = GridOptions.SortModeSingle, @@ -2511,6 +2471,10 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep Description = $"{listFormName}.Description", SelectCommandType = SelectCommandTypeEnum.Table, SelectCommand = "AbpSessions", + WidgetsJson = DefaultWidgetsJson("AbpSessions", listFormName, true, + new("App.Widget.Total", "blue", "FaDesktop"), + new("App.Widget.SignedInToday", "green", "FaSignInAlt", "CAST(\"SignedIn\" AS DATE) = CAST(CURRENT_TIMESTAMP AS DATE)"), + new("App.Widget.ActiveToday", "purple", "FaUserClock", "CAST(\"LastAccessed\" AS DATE) = CAST(CURRENT_TIMESTAMP AS DATE)")), KeyFieldName = "Id", KeyFieldDbSourceType = DbType.Guid, SortMode = GridOptions.SortModeSingle, @@ -2982,6 +2946,11 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep Description = $"{listFormName}.Description", SelectCommandType = SelectCommandTypeEnum.Table, SelectCommand = TableNameResolver.GetFullTableName(nameof(TableNameEnum.Announcement)), + WidgetsJson = DefaultWidgetsJson(TableNameResolver.GetFullTableName(nameof(TableNameEnum.Announcement)), listFormName, true, + new("App.Widget.Active", "green", "FaBullhorn", "(\"ExpiryDate\" IS NULL OR \"ExpiryDate\" >= CURRENT_TIMESTAMP)"), + new("App.Widget.Pinned", "purple", "FaThumbtack", "\"IsPinned\" = 'true'"), + new("App.Widget.Expired", "gray", "FaCalendarTimes", "\"ExpiryDate\" < CURRENT_TIMESTAMP"), + new("App.Widget.TotalViews", "blue", "FaEye", Value: "COALESCE(SUM(\"ViewCount\"), 0)")), SeedFilePath = SeedPathResolver.GetDataFilePath(listFormName), SeedSyncInsert = false, SeedSyncUpdate = false, @@ -3537,6 +3506,10 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep Description = $"{listFormName}.Description", SelectCommandType = SelectCommandTypeEnum.Table, SelectCommand = TableNameResolver.GetFullTableName(nameof(TableNameEnum.Survey)), + WidgetsJson = DefaultWidgetsJson(TableNameResolver.GetFullTableName(nameof(TableNameEnum.Survey)), listFormName, true, + new("App.Widget.Active", "green", "FaCheckCircle", "\"Status\" = 'active'"), + new("App.Widget.Passive", "gray", "FaTimesCircle", "\"Status\" = 'passive'"), + new("App.Widget.TotalResponses", "purple", "FaPollH", Value: "COALESCE(SUM(\"Responses\"), 0)")), SeedFilePath = SeedPathResolver.GetDataFilePath(listFormName), SeedSyncInsert = false, SeedSyncUpdate = false, @@ -4379,6 +4352,10 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep Description = $"{listFormName}.Description", SelectCommandType = SelectCommandTypeEnum.Table, SelectCommand = TableNameResolver.GetFullTableName(nameof(TableNameEnum.SocialPost)), + WidgetsJson = DefaultWidgetsJson(TableNameResolver.GetFullTableName(nameof(TableNameEnum.SocialPost)), listFormName, true, + new("App.Widget.Total", "blue", "FaComments"), + new("App.Widget.Today", "green", "FaCalendarDay", "CAST(\"CreationTime\" AS DATE) = CAST(CURRENT_TIMESTAMP AS DATE)"), + new("App.Widget.TotalLikes", "pink", "FaHeart", Value: "COALESCE(SUM(\"LikeCount\"), 0)")), SeedFilePath = SeedPathResolver.GetDataFilePath(listFormName), SeedSyncInsert = false, SeedSyncUpdate = false, @@ -5426,6 +5403,11 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep Description = $"{listFormName}.Description", SelectCommandType = SelectCommandTypeEnum.Table, SelectCommand = TableNameResolver.GetFullTableName(nameof(TableNameEnum.Event)), + WidgetsJson = DefaultWidgetsJson(TableNameResolver.GetFullTableName(nameof(TableNameEnum.Event)), listFormName, true, + new("App.Widget.Upcoming", "blue", "FaCalendarAlt", "\"Date\" >= CURRENT_TIMESTAMP"), + new("App.StaticLookup.Published", "green", "FaCheckCircle", "\"Status\" = 'published'"), + new("App.StaticLookup.Draft", "gray", "FaPencilAlt", "\"Status\" = 'draft'"), + new("App.StaticLookup.Cancelled", "red", "FaBan", "\"Status\" = 'cancelled'")), SeedFilePath = SeedPathResolver.GetDataFilePath(listFormName), SeedSyncInsert = false, SeedSyncUpdate = false, @@ -6714,9 +6696,9 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep ValueExpr = "key", LookupQuery = JsonSerializer.Serialize(new LookupDataDto[] { new () { Key="Backlog", Name="App.StaticLookup.Backlog" }, + new () { Key="Analysis", Name="App.StaticLookup.Analysis" }, new () { Key="Ready to Start", Name="App.StaticLookup.ReadyToStart" }, new () { Key="In Progress", Name="App.StaticLookup.InProgress" }, - new () { Key="Code Review", Name="App.StaticLookup.CodeReview" }, new () { Key="Testing", Name="App.StaticLookup.Testing" }, new () { Key="Staging", Name="App.StaticLookup.Staging" } }), @@ -6766,7 +6748,7 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep CaptionName = "App.Listform.ListformField.Description", Width = 0, ListOrderNo = 6, - Visible = true, + Visible = false, IsActive = true, AllowSearch = true, ValidationRuleJson = DefaultValidationRuleRequiredJson, @@ -6775,6 +6757,26 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep PivotSettingsJson = DefaultPivotSettingsJson, EditorOptions = EditorOptions.Html() }, + new() { + ListFormCode = listForm.ListFormCode, + CultureName = LanguageCodes.En, + SourceDbType = DbType.String, + FieldName = "Analysis", + EditGroupOrderNo = 1, + EditOrderNo = 7, + EditorType2 = EditorTypes.dxHtmlEditor, + ColSpan = 2, + CaptionName = "App.Listform.ListformField.Analysis", + Width = 0, + ListOrderNo = 13, + Visible = false, + IsActive = true, + AllowSearch = true, + ColumnCustomizationJson = DefaultColumnCustomizationJson, + PermissionJson = DefaultFieldPermissionJson(listForm.Name), + PivotSettingsJson = DefaultPivotSettingsJson, + EditorOptions = EditorOptions.Html() + }, new() { ListFormCode = listForm.ListFormCode, @@ -7209,6 +7211,10 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep Description = $"{listFormName}.Description", SelectCommandType = SelectCommandTypeEnum.Table, SelectCommand = TableNameResolver.GetFullTableName(nameof(TableNameEnum.NotificationRule)), + WidgetsJson = DefaultWidgetsJson(TableNameResolver.GetFullTableName(nameof(TableNameEnum.NotificationRule)), listFormName, true, + new("App.Widget.Total", "blue", "FaSlidersH"), + new("App.Widget.Active", "green", "FaCheckCircle", "\"IsActive\" = 'true'"), + new("App.Widget.Passive", "gray", "FaTimesCircle", "\"IsActive\" = 'false'")), KeyFieldName = "Id", KeyFieldDbSourceType = DbType.Guid, SeedFilePath = SeedPathResolver.GetDataFilePath(listFormName), @@ -7523,6 +7529,11 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep Description = $"{listFormName}.Description", SelectCommandType = SelectCommandTypeEnum.Table, SelectCommand = TableNameResolver.GetFullTableName(nameof(TableNameEnum.Notification)), + WidgetsJson = DefaultWidgetsJson(TableNameResolver.GetFullTableName(nameof(TableNameEnum.Notification)), listFormName, true, + new("App.Widget.Sent", "green", "FaPaperPlane", "\"IsSent\" = 'true'"), + new("App.Widget.Pending", "yellow", "FaHourglassHalf", "\"IsSent\" = 'false'"), + new("App.Platform.Read", "blue", "FaEnvelopeOpen", "\"IsRead\" = 'true'"), + new("App.Widget.Unread", "orange", "FaEnvelope", "\"IsSent\" = 'true' AND \"IsRead\" = 'false'")), KeyFieldName = "Id", KeyFieldDbSourceType = DbType.Guid, DefaultFilter = DefaultFilterJson, diff --git a/api/src/Sozsoft.Platform.DbMigrator/Migrations/ListFormSeeder_DefaultJsons.cs b/api/src/Sozsoft.Platform.DbMigrator/Migrations/ListFormSeeder_DefaultJsons.cs index 6833f100..c6ca3368 100644 --- a/api/src/Sozsoft.Platform.DbMigrator/Migrations/ListFormSeeder_DefaultJsons.cs +++ b/api/src/Sozsoft.Platform.DbMigrator/Migrations/ListFormSeeder_DefaultJsons.cs @@ -1,5 +1,7 @@ using System; +using System.Collections.Generic; using System.Data; +using System.Linq; using System.Text.Json; using Sozsoft.Platform.Entities; using Sozsoft.Platform.Enums; @@ -133,6 +135,7 @@ public static class ListFormSeeder_DefaultJsons string TitleExpr = "Title", string StatusExpr = "Status", string DescriptionExpr = "Description", + string AnalysisExpr = "Analysis", string DueDateExpr = "DueDate", string TagExpr = "Tags", string AssigneeExpr = "Assignees", @@ -141,13 +144,14 @@ public static class ListFormSeeder_DefaultJsons string OrderExpr = "SortOrder", string TimesheetExpr = "Timesheets", string SubTaskExpr = "SubTasks", - string StatusOrder = "Backlog, Ready to Start, In Progress, Code Review, Testing, Staging", + string StatusOrder = "Backlog, Analysis, Ready to Start, In Progress, Testing, Staging", bool AllowDragging = true ) => JsonSerializer.Serialize(new TodoOptionDto { TitleExpr = TitleExpr, StatusExpr = StatusExpr, DescriptionExpr = DescriptionExpr, + AnalysisExpr = AnalysisExpr, DueDateExpr = DueDateExpr, TagExpr = TagExpr, AssigneeExpr = AssigneeExpr, @@ -281,6 +285,53 @@ public static class ListFormSeeder_DefaultJsons }); } + /// + /// Ekranın üstündeki KPI şeridi: her kart tabloyu kendi koşuluyla sayar ve kartlar tek sorguda + /// UNION ALL ile döner. Alt başlık ekranın adıdır; kartın başlığı dil anahtarıdır. + /// Widget sorgusu ekranın otomatik tenant filtresinden geçmez; kiracı tablosunda + /// verilmezse kartlar bütün kiracıları sayar. + /// + public static string DefaultWidgetsJson(string tableName, string subTitle, bool tenant, params WidgetCard[] cards) + { + var sql = string.Join("\nUNION ALL\n", cards.Select(card => + { + var conditions = new[] + { + // Host'ta DefaultValueHelper bunu "IS NULL"a çevirir; liste ekranıyla aynı kırılım. + tenant ? $"\"TenantId\" = '{DefaultValues.TenantId}'" : null, + card.Where, + } + .Where(c => !string.IsNullOrEmpty(c)) + .ToArray(); + var where = conditions.Length > 0 ? $" WHERE {string.Join(" AND ", conditions)}" : string.Empty; + + // Sayı ve tutar kartları aynı UNION'da buluşabilir; tip çakışmasın diye değer metne çevrilir. + return $"SELECT '{card.Title}' AS \"Title\", CAST({card.Value} AS VARCHAR(32)) AS \"Value\", '{card.Color}' AS \"Color\", " + + $"'{subTitle}' AS \"SubTitle\", '{card.Icon}' AS \"Icon\" FROM \"{tableName}\"{where}"; + })); + + return DefaultWidgetsJson(sql, cards.Length); + } + + /// Kart sayısı sorgudan belli olmayan (gruplamalı) şeritler için; şerit 12 kolonu eşit böler. + public static string DefaultWidgetsJson(string sqlQuery, int cardCount) => JsonSerializer.Serialize(new List + { + new() + { + ColGap = 3, + ColSpan = Math.Max(2, 12 / cardCount), + SqlQuery = sqlQuery, + Title = "Title", + Value = "Value", + ValueClassName = "text-3xl", + Color = "Color", + Icon = "Icon", + SubTitle = "SubTitle", + ClassName = "mb-3", + IsActive = true, + } + }); + /// /// Bir yolun arama motoru sozlesmesi. Ayri bir SEO tablosu yoktur: alanlar yolu tanimlayan /// kaydin (Route, Custom Component) ekranina eklenir, bu yuzden kume tek yerde durur. @@ -337,3 +388,6 @@ public static class ListFormSeeder_DefaultJsons ]; } } + +/// KPI şeridindeki tek kart: başlık dil anahtarı, Value SQL ifadesi, Where ek koşul. +public record WidgetCard(string Title, string Color, string Icon, string Where = null, string Value = "COUNT(*)"); \ No newline at end of file diff --git a/api/src/Sozsoft.Platform.DbMigrator/Migrations/ListFormSeeder_Saas.cs b/api/src/Sozsoft.Platform.DbMigrator/Migrations/ListFormSeeder_Saas.cs index c96b8c04..aa659653 100644 --- a/api/src/Sozsoft.Platform.DbMigrator/Migrations/ListFormSeeder_Saas.cs +++ b/api/src/Sozsoft.Platform.DbMigrator/Migrations/ListFormSeeder_Saas.cs @@ -79,6 +79,10 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency Description = $"{listFormName}.Description", SelectCommandType = SelectCommandTypeEnum.Table, SelectCommand = "AbpTenants", + WidgetsJson = DefaultWidgetsJson("AbpTenants", listFormName, false, + new("App.Widget.Total", "blue", "FaBuilding"), + new("App.Widget.Active", "green", "FaCheckCircle", "\"IsActive\" = 'true'"), + new("App.Widget.Passive", "gray", "FaTimesCircle", "\"IsActive\" = 'false'")), KeyFieldName = "Id", KeyFieldDbSourceType = DbType.Guid, SortMode = GridOptions.SortModeSingle, @@ -625,6 +629,10 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency Description = $"{listFormName}.Description", SelectCommandType = SelectCommandTypeEnum.Table, SelectCommand = TableNameResolver.GetFullTableName(nameof(TableNameEnum.Branch)), + WidgetsJson = DefaultWidgetsJson(TableNameResolver.GetFullTableName(nameof(TableNameEnum.Branch)), listFormName, false, + new("App.Widget.Total", "blue", "FaCodeBranch"), + new("App.Widget.Active", "green", "FaCheckCircle", "\"IsActive\" = 'true'"), + new("App.Widget.Passive", "gray", "FaTimesCircle", "(\"IsActive\" = 'false' OR \"IsActive\" IS NULL)")), KeyFieldName = "Id", KeyFieldDbSourceType = DbType.Guid, DefaultFilter = DefaultFilterJson, @@ -1898,7 +1906,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency SeedFilePath = SeedPathResolver.GetDataFilePath(listFormName), SeedSyncInsert = false, SeedSyncUpdate = false, - SeedSyncDelete = false, + SeedSyncDelete = false, DefaultFilter = DefaultFilterJson, SortMode = GridOptions.SortModeSingle, FilterRowJson = DefaultFilterRowJson(), @@ -2185,7 +2193,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency SeedFilePath = SeedPathResolver.GetDataFilePath(listFormName), SeedSyncInsert = false, SeedSyncUpdate = false, - SeedSyncDelete = false, + SeedSyncDelete = false, DefaultFilter = DefaultFilterJson, SortMode = GridOptions.SortModeSingle, FilterRowJson = DefaultFilterRowJson(), @@ -2332,7 +2340,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency SeedFilePath = SeedPathResolver.GetDataFilePath(listFormName), SeedSyncInsert = false, SeedSyncUpdate = false, - SeedSyncDelete = false, + SeedSyncDelete = false, DefaultFilter = DefaultFilterJson, SortMode = GridOptions.SortModeSingle, FilterRowJson = DefaultFilterRowJson(), @@ -4016,6 +4024,10 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency Description = $"{listFormName}.Description", SelectCommandType = SelectCommandTypeEnum.Table, SelectCommand = TableNameResolver.GetFullTableName(nameof(TableNameEnum.Language)), + WidgetsJson = DefaultWidgetsJson(TableNameResolver.GetFullTableName(nameof(TableNameEnum.Language)), listFormName, false, + new("App.Widget.Total", "blue", "FaGlobe"), + new("App.Widget.Active", "green", "FaCheckCircle", "\"IsEnabled\" = 'true'"), + new("App.Widget.Passive", "gray", "FaTimesCircle", "\"IsEnabled\" = 'false'")), KeyFieldName = "Id", KeyFieldDbSourceType = DbType.Guid, SeedFilePath = SeedPathResolver.GetDataFilePath(listFormName), @@ -4213,6 +4225,17 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency Description = $"{listFormName}.Description", SelectCommandType = SelectCommandTypeEnum.Table, SelectCommand = TableNameResolver.GetFullTableName(nameof(TableNameEnum.LanguageText)), + WidgetsJson = DefaultWidgetsJson($@" + SELECT 'App.Widget.Total' AS ""Title"", CAST(COUNT(DISTINCT ""Key"") AS VARCHAR(32)) AS ""Value"", 'purple' AS ""Color"", '{listFormName}' AS ""SubTitle"", 'FaKey' AS ""Icon"" + FROM ""{TableNameResolver.GetFullTableName(nameof(TableNameEnum.LanguageText))}"" + + UNION ALL + + SELECT l.""DisplayName"", CAST(COUNT(t.""Id"") AS VARCHAR(32)), 'blue', '{listFormName}', 'FaLanguage' + FROM ""{TableNameResolver.GetFullTableName(nameof(TableNameEnum.LanguageText))}"" t + JOIN ""{TableNameResolver.GetFullTableName(nameof(TableNameEnum.Language))}"" l ON l.""CultureName"" = t.""CultureName"" + GROUP BY l.""DisplayName"" + ", 3), KeyFieldName = "Id", KeyFieldDbSourceType = DbType.Guid, SeedFilePath = SeedPathResolver.GetDataFilePath(listFormName), @@ -4553,6 +4576,11 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency Description = $"{listFormName}.Description", SelectCommandType = SelectCommandTypeEnum.Table, SelectCommand = TableNameResolver.GetFullTableName(nameof(TableNameEnum.ListForm)), + WidgetsJson = DefaultWidgetsJson(TableNameResolver.GetFullTableName(nameof(TableNameEnum.ListForm)), listFormName, false, + new("App.Widget.Total", "blue", "FaThList"), + new("App.Widget.List", "green", "FaTable", $"\"ListFormType\" = '{ListFormTypeEnum.List}'"), + new("App.Widget.Form", "purple", "FaWpforms", $"\"ListFormType\" = '{ListFormTypeEnum.Form}'"), + new("App.Widget.Chart", "orange", "FaChartBar", $"\"ListFormType\" = '{ListFormTypeEnum.Chart}'")), KeyFieldName = "Id", KeyFieldDbSourceType = DbType.Guid, SortMode = GridOptions.SortModeMultiple, @@ -5235,6 +5263,10 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency Description = $"{listFormName}.Description", SelectCommandType = SelectCommandTypeEnum.Table, SelectCommand = TableNameResolver.GetFullTableName(nameof(TableNameEnum.BackgroundWorker)), + WidgetsJson = DefaultWidgetsJson(TableNameResolver.GetFullTableName(nameof(TableNameEnum.BackgroundWorker)), listFormName, false, + new("App.Widget.Total", "blue", "FaClock"), + new("App.Widget.Active", "green", "FaCheckCircle", "\"IsActive\" = 'true'"), + new("App.Widget.Passive", "gray", "FaTimesCircle", "\"IsActive\" = 'false'")), KeyFieldName = "Id", KeyFieldDbSourceType = DbType.Guid, SeedFilePath = SeedPathResolver.GetDataFilePath(listFormName), @@ -5531,6 +5563,11 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency Description = $"{listFormName}.Description", SelectCommandType = SelectCommandTypeEnum.View, SelectCommand = TableNameResolver.GetFullTableName(nameof(TableNameEnum.BackgroundWorker_JobFlow)), + WidgetsJson = DefaultWidgetsJson(TableNameResolver.GetFullTableName(nameof(TableNameEnum.BackgroundWorker_JobFlow)), listFormName, false, + new("App.Platform.Success", "green", "FaCheckCircle", "\"Status\" = 'Succeeded'"), + new("App.Platform.Failed", "red", "FaTimesCircle", "\"Status\" = 'Failed'"), + new("App.Widget.Processing", "blue", "FaSpinner", "\"Status\" = 'Processing'"), + new("App.Widget.Enqueued", "yellow", "FaHourglassHalf", "\"Status\" IN ('Enqueued', 'Scheduled')")), KeyFieldName = "Id", KeyFieldDbSourceType = DbType.Guid, SortMode = GridOptions.SortModeSingle, @@ -6055,6 +6092,10 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency Description = $"{listFormName}.Description", SelectCommandType = SelectCommandTypeEnum.Table, SelectCommand = TableNameResolver.GetFullTableName(nameof(TableNameEnum.Menu)), + WidgetsJson = DefaultWidgetsJson(TableNameResolver.GetFullTableName(nameof(TableNameEnum.Menu)), listFormName, false, + new("App.Widget.Total", "blue", "FaBars"), + new("App.Widget.Active", "green", "FaCheckCircle", "\"IsDisabled\" = 'false'"), + new("App.Widget.Passive", "gray", "FaTimesCircle", "\"IsDisabled\" = 'true'")), KeyFieldName = "Code", KeyFieldDbSourceType = DbType.String, SeedFilePath = SeedPathResolver.GetDataFilePath(listFormName), @@ -6425,6 +6466,11 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency Description = $"{listFormName}.Description", SelectCommandType = SelectCommandTypeEnum.Table, SelectCommand = TableNameResolver.GetFullTableName(nameof(TableNameEnum.CustomEndpoint)), + WidgetsJson = DefaultWidgetsJson(TableNameResolver.GetFullTableName(nameof(TableNameEnum.CustomEndpoint)), listFormName, false, + new("GET", "blue", "FaDownload", "\"Method\" = 'GET'"), + new("POST", "green", "FaPlusCircle", "\"Method\" = 'POST'"), + new("PUT", "orange", "FaEdit", "\"Method\" = 'PUT'"), + new("DELETE", "red", "FaTrash", "\"Method\" = 'DELETE'")), SeedFilePath = SeedPathResolver.GetDataFilePath(listFormName), SeedSyncInsert = true, SeedSyncUpdate = true, @@ -7326,7 +7372,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency ListOrderNo = 6, Visible = true, IsActive = true, - + AllowSearch = true, ColumnCustomizationJson = DefaultColumnCustomizationJson, PermissionJson = DefaultFieldPermissionJson(listForm.Name), @@ -7411,6 +7457,10 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency Description = $"{listFormName}.Description", SelectCommandType = SelectCommandTypeEnum.Table, SelectCommand = mcpServerTable, + WidgetsJson = DefaultWidgetsJson(TableNameResolver.GetFullTableName(nameof(TableNameEnum.McpServer)), listFormName, false, + new("App.Widget.Total", "blue", "FaServer"), + new("App.Widget.Active", "green", "FaCheckCircle", "\"IsActive\" = 'true'"), + new("App.Widget.Passive", "gray", "FaTimesCircle", "\"IsActive\" = 'false'")), SeedFilePath = SeedPathResolver.GetDataFilePath(listFormName), SeedSyncInsert = true, SeedSyncUpdate = true, @@ -8249,6 +8299,10 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency Description = $"{listFormName}.Description", SelectCommandType = SelectCommandTypeEnum.Table, SelectCommand = TableNameResolver.GetFullTableName(nameof(TableNameEnum.Order)), + WidgetsJson = DefaultWidgetsJson(TableNameResolver.GetFullTableName(nameof(TableNameEnum.Order)), listFormName, false, + new("App.Widget.Total", "blue", "FaShoppingCart"), + new("App.Widget.Active", "green", "FaCheckCircle", "\"IsActive\" = 'true'"), + new("App.Widget.TotalRevenue", "purple", "FaMoneyBillWave", Value: "CAST(COALESCE(SUM(\"Total\"), 0) AS DECIMAL(18, 2))")), KeyFieldName = "Id", KeyFieldDbSourceType = DbType.Guid, DefaultFilter = DefaultFilterJson, @@ -9436,6 +9490,10 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency Description = $"{listFormName}.Description", SelectCommandType = SelectCommandTypeEnum.Table, SelectCommand = TableNameResolver.GetFullTableName(nameof(TableNameEnum.ForumCategory)), + WidgetsJson = DefaultWidgetsJson(TableNameResolver.GetFullTableName(nameof(TableNameEnum.ForumCategory)), listFormName, true, + new("App.Widget.Total", "blue", "FaFolder"), + new("App.Widget.Active", "green", "FaCheckCircle", "\"IsActive\" = 'true'"), + new("App.Listform.ListformField.IsLocked", "gray", "FaLock", "\"IsLocked\" = 'true'")), KeyFieldName = "Id", KeyFieldDbSourceType = DbType.Guid, DefaultFilter = DefaultFilterJson, @@ -9657,6 +9715,11 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency Description = $"{listFormName}.Description", SelectCommandType = SelectCommandTypeEnum.Table, SelectCommand = TableNameResolver.GetFullTableName(nameof(TableNameEnum.ForumTopic)), + WidgetsJson = DefaultWidgetsJson(TableNameResolver.GetFullTableName(nameof(TableNameEnum.ForumTopic)), listFormName, true, + new("App.Widget.Pinned", "purple", "FaThumbtack", "\"IsPinned\" = 'true'"), + new("App.Listform.ListformField.IsSolved", "green", "FaCheckCircle", "\"IsSolved\" = 'true'"), + new("App.Listform.ListformField.IsLocked", "gray", "FaLock", "\"IsLocked\" = 'true'"), + new("App.Widget.TotalViews", "blue", "FaEye", Value: "COALESCE(SUM(\"ViewCount\"), 0)")), KeyFieldName = "Id", KeyFieldDbSourceType = DbType.Guid, DefaultFilter = DefaultFilterJson, @@ -10327,6 +10390,10 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency Description = $"{listFormName}.Description", SelectCommandType = SelectCommandTypeEnum.Table, SelectCommand = TableNameResolver.GetFullTableName(nameof(TableNameEnum.ForumPost)), + WidgetsJson = DefaultWidgetsJson(TableNameResolver.GetFullTableName(nameof(TableNameEnum.ForumPost)), listFormName, true, + new("App.Widget.Total", "blue", "FaComment"), + new("App.Widget.AcceptedAnswers", "green", "FaCheckCircle", "\"IsAcceptedAnswer\" = 'true'"), + new("App.Widget.Today", "purple", "FaCalendarDay", "CAST(\"CreationTime\" AS DATE) = CAST(CURRENT_TIMESTAMP AS DATE)")), KeyFieldName = "Id", KeyFieldDbSourceType = DbType.Guid, DefaultFilter = DefaultFilterJson, @@ -11057,6 +11124,10 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency Description = $"{listFormName}.Description", SelectCommandType = SelectCommandTypeEnum.Table, SelectCommand = TableNameResolver.GetFullTableName(nameof(TableNameEnum.BlogPost)), + WidgetsJson = DefaultWidgetsJson(TableNameResolver.GetFullTableName(nameof(TableNameEnum.BlogPost)), listFormName, false, + new("App.Widget.Published", "green", "FaCheckCircle", "\"IsPublished\" = 'true'"), + new("App.Widget.Draft", "gray", "FaPencilAlt", "\"IsPublished\" = 'false'"), + new("App.Widget.TotalViews", "purple", "FaEye", Value: "COALESCE(SUM(\"ViewCount\"), 0)")), KeyFieldName = "Id", KeyFieldDbSourceType = DbType.Guid, DefaultFilter = DefaultFilterJson, @@ -11716,6 +11787,13 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency Description = $"{listFormName}.Description", SelectCommandType = SelectCommandTypeEnum.Table, SelectCommand = TableNameResolver.GetFullTableName(nameof(TableNameEnum.DevRequest)), + WidgetsJson = DefaultWidgetsJson(TableNameResolver.GetFullTableName(nameof(TableNameEnum.DevRequest)), listFormName, false, + new("App.StaticLookup.Backlog", "gray", "FaInbox", "\"Status\" = 'Backlog'"), + new("App.StaticLookup.Analysis", "pink", "FaSearch", "\"Status\" = 'Analysis'"), + new("App.StaticLookup.ReadyToStart", "blue", "FaFlag", "\"Status\" = 'Ready to Start'"), + new("App.StaticLookup.InProgress", "orange", "FaSpinner", "\"Status\" = 'In Progress'"), + new("App.StaticLookup.Testing", "yellow", "FaVial", "\"Status\" = 'Testing'"), + new("App.StaticLookup.Staging", "green", "FaRocket", "\"Status\" = 'Staging'")), KeyFieldName = "Id", KeyFieldDbSourceType = DbType.Guid, DefaultFilter = DefaultFilterJson, @@ -11857,9 +11935,9 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency ValueExpr = "key", LookupQuery = JsonSerializer.Serialize(new LookupDataDto[] { new () { Key="Backlog", Name="App.StaticLookup.Backlog" }, + new () { Key="Analysis", Name="App.StaticLookup.Analysis" }, new () { Key="Ready to Start", Name="App.StaticLookup.ReadyToStart" }, new () { Key="In Progress", Name="App.StaticLookup.InProgress" }, - new () { Key="Code Review", Name="App.StaticLookup.CodeReview" }, new () { Key="Testing", Name="App.StaticLookup.Testing" }, new () { Key="Staging", Name="App.StaticLookup.Staging" } }), @@ -11909,7 +11987,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency CaptionName = "App.Listform.ListformField.Description", Width = 0, ListOrderNo = 6, - Visible = true, + Visible = false, IsActive = true, AllowSearch = true, ValidationRuleJson = DefaultValidationRuleRequiredJson, @@ -11918,6 +11996,26 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency PivotSettingsJson = DefaultPivotSettingsJson, EditorOptions = EditorOptions.Html() }, + new() { + ListFormCode = listForm.ListFormCode, + CultureName = LanguageCodes.En, + SourceDbType = DbType.String, + FieldName = "Analysis", + EditGroupOrderNo = 1, + EditOrderNo = 7, + EditorType2 = EditorTypes.dxHtmlEditor, + ColSpan = 2, + CaptionName = "App.Listform.ListformField.Analysis", + Width = 0, + ListOrderNo = 15, + Visible = false, + IsActive = true, + AllowSearch = true, + ColumnCustomizationJson = DefaultColumnCustomizationJson, + PermissionJson = DefaultFieldPermissionJson(listForm.Name), + PivotSettingsJson = DefaultPivotSettingsJson, + EditorOptions = EditorOptions.Html() + }, new() { ListFormCode = listForm.ListFormCode, @@ -12046,7 +12144,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency #endregion } #endregion - + #region IntranetWidget listFormName = AppCodes.DeveloperKits.IntranetWidgets; if (!await _listFormRepository.AnyAsync(a => a.ListFormCode == listFormName)) diff --git a/api/src/Sozsoft.Platform.Domain.Shared/Editors/EditorOptions.cs b/api/src/Sozsoft.Platform.Domain.Shared/Editors/EditorOptions.cs index 836baa99..63d04709 100644 --- a/api/src/Sozsoft.Platform.Domain.Shared/Editors/EditorOptions.cs +++ b/api/src/Sozsoft.Platform.Domain.Shared/Editors/EditorOptions.cs @@ -77,13 +77,17 @@ public static class EditorOptions => New().Width(width).Height(height).Flag("multiple", multiple); /// Zengin metin (HTML) editoru; tam arac cubugu ile. - public static EditorOptionsBuilder Html(int height = 200) + /// + /// height verilirse editor sabitlenir ve icerik kaydirilir; min-height ise + /// en az yuksekligi korurken yeni satirlarla buyumesine izin verir. + /// + public static EditorOptionsBuilder Html(int minHeight = 200) => New() .Raw("mediaResizing", "{\"enabled\":true}") .Raw("imageUpload", "{\"tabs\":[\"file\",\"url\"],\"fileUploadMode\":\"base64\"}") .Raw("toolbar", "{\"multiline\":true,\"items\":[" + string.Join(",", HtmlToolbarItems()) + "]}") .Flag("encodeHtml", true) - .Height(height); + .Raw("elementAttr", "{\"style\":\"min-height:" + EditorJson.Number(minHeight) + "px\"}"); private static IEnumerable HtmlToolbarItems() { diff --git a/api/src/Sozsoft.Platform.Domain/Entities/Tenant/Saas/DevRequest.cs b/api/src/Sozsoft.Platform.Domain/Entities/Tenant/Saas/DevRequest.cs index 53f02975..b4b95d59 100644 --- a/api/src/Sozsoft.Platform.Domain/Entities/Tenant/Saas/DevRequest.cs +++ b/api/src/Sozsoft.Platform.Domain/Entities/Tenant/Saas/DevRequest.cs @@ -12,6 +12,7 @@ public class DevRequest : FullAuditedEntity public string? Title { get; set; } public string? Status { get; set; } public string? Description { get; set; } + public string? Analysis { get; set; } public DateTime? DueDate { get; set; } public string? Tags { get; set; } public string? Assignees { get; set; } diff --git a/api/src/Sozsoft.Platform.EntityFrameworkCore/EntityFrameworkCore/PlatformDbContext.cs b/api/src/Sozsoft.Platform.EntityFrameworkCore/EntityFrameworkCore/PlatformDbContext.cs index 61b31ca4..479cfa5f 100644 --- a/api/src/Sozsoft.Platform.EntityFrameworkCore/EntityFrameworkCore/PlatformDbContext.cs +++ b/api/src/Sozsoft.Platform.EntityFrameworkCore/EntityFrameworkCore/PlatformDbContext.cs @@ -1599,6 +1599,7 @@ public class PlatformDbContext : b.Property(e => e.Title).HasMaxLength(512).IsRequired(); b.Property(e => e.Status).HasMaxLength(64).IsRequired(); b.Property(e => e.Description).HasColumnType("nvarchar(max)"); + b.Property(e => e.Analysis).HasColumnType("nvarchar(max)"); b.Property(e => e.Tags).HasMaxLength(256); b.Property(e => e.Assignees).HasMaxLength(512); b.Property(e => e.Priority).HasMaxLength(64); diff --git a/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260922201238_Initial.Designer.cs b/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260922201238_Initial.Designer.cs index 2531a24c..03c93d79 100644 --- a/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260922201238_Initial.Designer.cs +++ b/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260922201238_Initial.Designer.cs @@ -2375,6 +2375,9 @@ namespace Sozsoft.Platform.Migrations b.Property("Id") .HasColumnType("uniqueidentifier"); + b.Property("Analysis") + .HasColumnType("nvarchar(max)"); + b.Property("Assignees") .HasMaxLength(512) .HasColumnType("nvarchar(512)"); diff --git a/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260922201238_Initial.cs b/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260922201238_Initial.cs index eb648833..36ae716c 100644 --- a/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260922201238_Initial.cs +++ b/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260922201238_Initial.cs @@ -1322,6 +1322,7 @@ namespace Sozsoft.Platform.Migrations Title = table.Column(type: "nvarchar(512)", maxLength: 512, nullable: false), Status = table.Column(type: "nvarchar(64)", maxLength: 64, nullable: false), Description = table.Column(type: "nvarchar(max)", nullable: true), + Analysis = table.Column(type: "nvarchar(max)", nullable: true), DueDate = table.Column(type: "datetime2", nullable: true), Tags = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), Assignees = table.Column(type: "nvarchar(512)", maxLength: 512, nullable: true), diff --git a/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/PlatformDbContextModelSnapshot.cs b/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/PlatformDbContextModelSnapshot.cs index 8c435c51..9d79caef 100644 --- a/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/PlatformDbContextModelSnapshot.cs +++ b/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/PlatformDbContextModelSnapshot.cs @@ -2372,6 +2372,9 @@ namespace Sozsoft.Platform.Migrations b.Property("Id") .HasColumnType("uniqueidentifier"); + b.Property("Analysis") + .HasColumnType("nvarchar(max)"); + b.Property("Assignees") .HasMaxLength(512) .HasColumnType("nvarchar(512)"); diff --git a/claude.md b/claude.md index 6cc55d6c..7bcb291e 100644 --- a/claude.md +++ b/claude.md @@ -121,6 +121,9 @@ Bir çelişki varsa sıra: `ai.instructions.md` → `dotnet.instructions.md` → 1. **Yeni tablo** → tenant + tam audit kolonları varsayılan gelir (`Id`, `TenantId`, `CreationTime`, `CreatorId`, `LastModificationTime`, `LastModifierId`, `IsDeleted`, `DeletionTime`, `DeleterId`). Kullanıcı açıkça "tenant yok / audit yok" demedikçe çıkarma. + Yeni **tanım** tablosu (tanım olup olmadığına talebin içeriğine göre analizde karar verilir; + sabit ad listesi yoktur) buna ek olarak `IsActive bit NOT NULL DEFAULT 1` ile gelir; liste + form alanı, varsayılan + `true`, Aktif/Pasif widget'ı. Ayrıntı: `lowcode.instructions.md` §8.1. 2. **Yeni ekran** → Wizard yolu. `Definitions` altına otomatik yerleştirme yok; modüle ait yeni bir kök menü oluştur, `Order = max(Order) + 1`. 3. **Yeni ekran/menü** → yetki sözleşmesi olmadan önerilmez. Menü ve route aynı ekran @@ -160,6 +163,11 @@ Bir çelişki varsa sıra: `ai.instructions.md` → `dotnet.instructions.md` → `Pivot` + `Chart` açık, `DefaultLayout: "grid"` ile gelir; "liste/grid" demek diğerlerini kapatmak değildir. Bir görünüm yalnızca istek onu açıkça dışarıda bıraktığında `false` yazılır. Tree/Gantt/Scheduler/Todo yalnızca istenince açılır. Ayrıntı: `lowcode.instructions.md` §9.8. +13. **Liste ekranı widget'ları** → Wizard ya da MCP `deploy_wizard` ile üretilen her List ekranı + sorulmadan **en az 3 kartlık** `Widgets` ile gelir; kartlar tablonun durum / boolean bayrak / + tarih / tutar kolonlarından seçilir, başlıklar dil anahtarıdır (`App.Widget.*` önce). Kiracı + tablosunda her kart `"TenantId" = '@TENANTID'` taşır; `IsDeleted` koşulu yazılmaz. Form/Chart, + menüsüz alt form ve basit tanım listeleri istisnadır. Ayrıntı: `lowcode.instructions.md` §4.6.1. ### Cross-cutting: touching one thing means touching these too @@ -250,9 +258,15 @@ Bir çelişki varsa sıra: `ai.instructions.md` → `dotnet.instructions.md` → - **Talepteki görsel** → `App.DevRequests` açıklamasına yapıştırılan görsel base64 `data:` URI olarak kalır, hiçbir yere yüklenmez. `start_dev_request` / `get_dev_request` onu metinden ayıklar (`McpDevRequestImages`) ve MCP resim içeriği (`ImageContentBlock`) olarak döndürür; metinde - `[image-N]` kalır. Açıklama bu yüzden `update_row` ile yeniden yazılmaz, not - `append_dev_request_note` ile eklenir. `SubTasks` de `add_dev_request_subtasks` ile yazılır: - atanan kişi bağlanan kullanıcı, bitiş tarihi bugün — sunucu doldurur, modele bırakılmaz. + `[image-N]` kalır. Açıklama bu yüzden `update_row` ile yeniden yazılmaz. `SubTasks` de + `add_dev_request_subtasks` ile yazılır: atanan kişi bağlanan kullanıcı, bitiş tarihi bugün — + sunucu doldurur, modele bırakılmaz. +- **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). + 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 (hiçbir şey üretmez), + `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. - **MCP şema araçları** → tablo/sütun bilgisi canlı katalogdan değil `Sas_H_DbTableColumn` sözlüğünden okunur (`IMcpSchemaDictionary`). Sözlüğün alan adları `dbo.Sas_H_ListFormFieldsDictionaryJson` prosedürünün ürettiği JSON ile birebir aynıdır; diff --git a/configs/seeds/host/data/App.Languages.LanguageText.json b/configs/seeds/host/data/App.Languages.LanguageText.json index 3ebd8528..262264dc 100644 --- a/configs/seeds/host/data/App.Languages.LanguageText.json +++ b/configs/seeds/host/data/App.Languages.LanguageText.json @@ -1,7 +1,7 @@ { "ListFormCode": "App.Languages.LanguageText", "KeyFieldName": "Id", - "GeneratedAt": "2026-09-24T19:17:56.868760Z", + "GeneratedAt": "2026-09-24T19:17:56.86876Z", "Order": 2, "Rows": [ { @@ -30986,6 +30986,20 @@ "Key": "App.Listform.ListformField.Description", "Value": "Açıklama" }, + { + "Id": "76be8cec-c18d-5718-8063-e0c7f15615bc", + "CultureName": "en", + "ResourceName": "Platform", + "Key": "App.Listform.ListformField.Analysis", + "Value": "Analysis" + }, + { + "Id": "095fb0a4-f3b8-5c6e-9bdb-d94ec1f6f496", + "CultureName": "tr", + "ResourceName": "Platform", + "Key": "App.Listform.ListformField.Analysis", + "Value": "Analiz" + }, { "Id": "9e6aa3ef-bf85-5719-a8cc-f3045f52a76d", "CultureName": "en", @@ -42802,6 +42816,20 @@ "Key": "App.ListFormFieldEdit.TodoDescriptionExpr", "Value": "Açıklama alanı" }, + { + "Id": "7227ccdb-ab79-5d0c-a68f-3819603da4f1", + "CultureName": "en", + "ResourceName": "Platform", + "Key": "App.ListFormFieldEdit.TodoAnalysisExpr", + "Value": "Analysis Field" + }, + { + "Id": "3bad05d0-dab7-580d-9867-a3d580e73b91", + "CultureName": "tr", + "ResourceName": "Platform", + "Key": "App.ListFormFieldEdit.TodoAnalysisExpr", + "Value": "Analiz alanı" + }, { "Id": "bb493ec4-ef38-5298-8910-e855501c9d65", "CultureName": "en", @@ -42919,7 +42947,7 @@ "CultureName": "en", "ResourceName": "Platform", "Key": "App.ListFormFieldEdit.TodoStatusOrderPlaceholder", - "Value": "Backlog, Ready to Start, In Progress, Code Review, Testing, Staging" + "Value": "Backlog, Analysis, Ready to Start, In Progress, Testing, Staging" }, { "Id": "d17288e1-c462-5856-8d13-0da438583e5e", @@ -43166,6 +43194,20 @@ "Key": "App.ListFormTodoBoard.DescriptionPlaceholder", "Value": "Todo açıklamasını yazın…" }, + { + "Id": "3a16fde9-ed85-5c4e-bcfb-ea9b7a8009a8", + "CultureName": "en", + "ResourceName": "Platform", + "Key": "App.ListFormTodoBoard.AnalysisPlaceholder", + "Value": "Write the analysis of the request..." + }, + { + "Id": "f91ecec5-6334-5237-8f7b-89ce9683cca8", + "CultureName": "tr", + "ResourceName": "Platform", + "Key": "App.ListFormTodoBoard.AnalysisPlaceholder", + "Value": "Talebin analizini yazın..." + }, { "Id": "c5b1a520-3a12-55f7-828c-9a5ef4d33051", "CultureName": "en", @@ -45420,6 +45462,20 @@ "Key": "App.StaticLookup.Backlog", "Value": "Bekleyen İşler" }, + { + "Id": "c0cc1060-fd5d-5e4e-a052-8985b125732e", + "CultureName": "en", + "ResourceName": "Platform", + "Key": "App.StaticLookup.Analysis", + "Value": "Analysis" + }, + { + "Id": "16b66d78-6d17-5650-9831-fafbb76b408e", + "CultureName": "tr", + "ResourceName": "Platform", + "Key": "App.StaticLookup.Analysis", + "Value": "Analiz" + }, { "Id": "2a645686-5b78-5a50-86fc-e6e4a44f72b1", "CultureName": "en", @@ -45532,20 +45588,6 @@ "Key": "App.Platform.Chart", "Value": "Grafik" }, - { - "Id": "043893de-610e-55b5-a2a7-e4a5146672b9", - "CultureName": "en", - "ResourceName": "Platform", - "Key": "App.StaticLookup.CodeReview", - "Value": "Code Review" - }, - { - "Id": "3e6e364b-5b92-5489-8fee-83442eed3867", - "CultureName": "tr", - "ResourceName": "Platform", - "Key": "App.StaticLookup.CodeReview", - "Value": "Kod İncelemesi" - }, { "Id": "dfa59c96-96b5-50f7-b462-229600230832", "CultureName": "en", @@ -67190,6 +67232,20 @@ "Key": "App.Mcp.Error.DevRequestNotReady", "Value": "\"{0}\" geliştirme talebi \"{1}\" durumunda; yalnızca \"Ready to Start\" durumundaki talepler başlatılabilir." }, + { + "Id": "2b75ab25-37b8-56d3-b38e-ca5ba7296596", + "CultureName": "en", + "ResourceName": "Platform", + "Key": "App.Mcp.Error.DevRequestNotAnalyzable", + "Value": "Development request \"{0}\" is in \"{1}\"; only requests in \"Backlog\" or \"Analysis\" can be analyzed." + }, + { + "Id": "4f36d621-7053-5d99-b952-37a64b4f63dc", + "CultureName": "tr", + "ResourceName": "Platform", + "Key": "App.Mcp.Error.DevRequestNotAnalyzable", + "Value": "\"{0}\" geliştirme talebi \"{1}\" durumunda; yalnızca \"Backlog\" ya da \"Analysis\" durumundaki talepler analiz edilebilir." + }, { "Id": "6cac6a6b-9329-5552-86d9-08e602afcb0f", "CultureName": "en", @@ -67204,6 +67260,20 @@ "Key": "App.Mcp.Error.DevRequestNoteRequired", "Value": "Eklenecek not boş olamaz." }, + { + "Id": "d3c1c3fe-5dda-5113-8f47-515d5c91caab", + "CultureName": "en", + "ResourceName": "Platform", + "Key": "App.Mcp.Error.DevRequestAnalysisRequired", + "Value": "The analysis cannot be empty." + }, + { + "Id": "ada842cf-97ef-544f-8993-504e450895ac", + "CultureName": "tr", + "ResourceName": "Platform", + "Key": "App.Mcp.Error.DevRequestAnalysisRequired", + "Value": "Analiz boş olamaz." + }, { "Id": "aa72e998-f4a1-5873-95ef-1fcfe0b601f9", "CultureName": "en", @@ -69261,6 +69331,370 @@ "ResourceName": "Platform", "Key": "App.Mcp.Error.VerifyWizardPending", "Value": "\"{0}\" wizard'ı henüz kurulmadı: seed dosyaları yazıldı ama DB Migrate çalışmadı. Kullanıcıdan Wizard Manager'da DB Migrate çalıştırmasını isteyin, sonra tekrar doğrulayın." + }, + { + "Id": "cfb9483e-f6b1-5041-af8f-a7b36b1c3d1b", + "CultureName": "en", + "ResourceName": "Platform", + "Key": "App.Widget.Total", + "Value": "Total" + }, + { + "Id": "dcee1716-6f7b-5480-80f2-20c63f18cee3", + "CultureName": "tr", + "ResourceName": "Platform", + "Key": "App.Widget.Total", + "Value": "Toplam" + }, + { + "Id": "5b7ac5a9-0d19-56c8-b510-84dff08812b2", + "CultureName": "en", + "ResourceName": "Platform", + "Key": "App.Widget.Active", + "Value": "Active" + }, + { + "Id": "5828ae1a-9592-56c3-aa72-192f873bcdde", + "CultureName": "tr", + "ResourceName": "Platform", + "Key": "App.Widget.Active", + "Value": "Aktif" + }, + { + "Id": "0d934934-b906-5656-91d4-845ed25a226f", + "CultureName": "en", + "ResourceName": "Platform", + "Key": "App.Widget.Passive", + "Value": "Passive" + }, + { + "Id": "4794dbc8-dff2-52ea-8f0a-c7e45492454d", + "CultureName": "tr", + "ResourceName": "Platform", + "Key": "App.Widget.Passive", + "Value": "Pasif" + }, + { + "Id": "1c187a6c-50b1-5817-bffe-d43504316d09", + "CultureName": "en", + "ResourceName": "Platform", + "Key": "App.Widget.Published", + "Value": "Published" + }, + { + "Id": "249cd74e-5d8d-59b4-ab1b-0e1e6027d059", + "CultureName": "tr", + "ResourceName": "Platform", + "Key": "App.Widget.Published", + "Value": "Yayında" + }, + { + "Id": "2e85ac8f-0013-5615-9303-1356666bf5b4", + "CultureName": "en", + "ResourceName": "Platform", + "Key": "App.Widget.Draft", + "Value": "Draft" + }, + { + "Id": "cac1c52e-3a9a-5619-b630-9efc36bf570b", + "CultureName": "tr", + "ResourceName": "Platform", + "Key": "App.Widget.Draft", + "Value": "Taslak" + }, + { + "Id": "5a95141c-7795-59db-93c0-79de85bf2ec4", + "CultureName": "en", + "ResourceName": "Platform", + "Key": "App.Widget.TotalViews", + "Value": "Total Views" + }, + { + "Id": "06d4dd8f-b635-51a5-952c-634f07f63872", + "CultureName": "tr", + "ResourceName": "Platform", + "Key": "App.Widget.TotalViews", + "Value": "Toplam Görüntülenme" + }, + { + "Id": "3733d251-e6ec-5185-a81d-9d5f243c7481", + "CultureName": "en", + "ResourceName": "Platform", + "Key": "App.Widget.TotalRevenue", + "Value": "Total Revenue" + }, + { + "Id": "4e9523f9-e134-51e5-b9ab-9c1d942de904", + "CultureName": "tr", + "ResourceName": "Platform", + "Key": "App.Widget.TotalRevenue", + "Value": "Toplam Tutar" + }, + { + "Id": "dc0fc005-26b7-5ab3-a9e0-025b0c244f5e", + "CultureName": "en", + "ResourceName": "Platform", + "Key": "App.Widget.Processing", + "Value": "Processing" + }, + { + "Id": "b6a42a49-550b-5507-924c-be549aafa17c", + "CultureName": "tr", + "ResourceName": "Platform", + "Key": "App.Widget.Processing", + "Value": "İşleniyor" + }, + { + "Id": "f59e997a-22a9-5272-91ef-74f169460b1f", + "CultureName": "en", + "ResourceName": "Platform", + "Key": "App.Widget.Enqueued", + "Value": "Queued" + }, + { + "Id": "c1cab658-b676-5960-83ad-d80faead0ef2", + "CultureName": "tr", + "ResourceName": "Platform", + "Key": "App.Widget.Enqueued", + "Value": "Kuyrukta" + }, + { + "Id": "cb70d1eb-f81c-5ecc-960c-07259fa162b5", + "CultureName": "en", + "ResourceName": "Platform", + "Key": "App.Widget.List", + "Value": "List" + }, + { + "Id": "7c7cc937-d0b4-5a22-aa93-efba03a22f2c", + "CultureName": "tr", + "ResourceName": "Platform", + "Key": "App.Widget.List", + "Value": "Liste" + }, + { + "Id": "0bd15ad4-59df-5eab-b18b-7108d5fbf31c", + "CultureName": "en", + "ResourceName": "Platform", + "Key": "App.Widget.Form", + "Value": "Form" + }, + { + "Id": "90a05ae9-611a-50e1-913f-8d5c2d011017", + "CultureName": "tr", + "ResourceName": "Platform", + "Key": "App.Widget.Form", + "Value": "Form" + }, + { + "Id": "2a36f006-fb79-5371-bcc9-85f9665ffe6d", + "CultureName": "en", + "ResourceName": "Platform", + "Key": "App.Widget.Chart", + "Value": "Chart" + }, + { + "Id": "9e91c54f-5e6f-53c0-838c-08023c5d2999", + "CultureName": "tr", + "ResourceName": "Platform", + "Key": "App.Widget.Chart", + "Value": "Grafik" + }, + { + "Id": "f14c54a8-6204-58d3-8e34-99dd6772ebf7", + "CultureName": "en", + "ResourceName": "Platform", + "Key": "App.Widget.Today", + "Value": "Today" + }, + { + "Id": "4f4dc6ac-76da-533a-a9d0-0bff9a599429", + "CultureName": "tr", + "ResourceName": "Platform", + "Key": "App.Widget.Today", + "Value": "Bugün" + }, + { + "Id": "a5f51fca-5708-55fd-b4c1-86a4a09f6f49", + "CultureName": "en", + "ResourceName": "Platform", + "Key": "App.Widget.Errors", + "Value": "Errors" + }, + { + "Id": "097213dd-41a9-59a0-8580-455344be487a", + "CultureName": "tr", + "ResourceName": "Platform", + "Key": "App.Widget.Errors", + "Value": "Hatalar" + }, + { + "Id": "32d44619-87d5-52a1-ad52-18ca0ae05f42", + "CultureName": "en", + "ResourceName": "Platform", + "Key": "App.Widget.SignedInToday", + "Value": "Signed In Today" + }, + { + "Id": "9ce0f85e-40e6-5537-a21c-52e519a59b18", + "CultureName": "tr", + "ResourceName": "Platform", + "Key": "App.Widget.SignedInToday", + "Value": "Bugün Giriş Yapan" + }, + { + "Id": "392cffdf-de41-51ed-b54a-25b3740ece59", + "CultureName": "en", + "ResourceName": "Platform", + "Key": "App.Widget.ActiveToday", + "Value": "Active Today" + }, + { + "Id": "a64fc55c-a100-5b82-9ea0-b9f4821b2e93", + "CultureName": "tr", + "ResourceName": "Platform", + "Key": "App.Widget.ActiveToday", + "Value": "Bugün Aktif" + }, + { + "Id": "db419b9f-31ef-5ba0-8a92-4b4e82d126fc", + "CultureName": "en", + "ResourceName": "Platform", + "Key": "App.Widget.Pinned", + "Value": "Pinned" + }, + { + "Id": "40a10314-e283-5357-a1e5-e26b3d06faf3", + "CultureName": "tr", + "ResourceName": "Platform", + "Key": "App.Widget.Pinned", + "Value": "Sabitlenmiş" + }, + { + "Id": "efe1415e-db82-5812-8705-4672799a58ab", + "CultureName": "en", + "ResourceName": "Platform", + "Key": "App.Widget.Expired", + "Value": "Expired" + }, + { + "Id": "75ec191d-a7cf-571d-b2c5-cb2f8c046c21", + "CultureName": "tr", + "ResourceName": "Platform", + "Key": "App.Widget.Expired", + "Value": "Süresi Dolmuş" + }, + { + "Id": "b43721ea-2ab7-5522-a7c4-96fa1b406e05", + "CultureName": "en", + "ResourceName": "Platform", + "Key": "App.Widget.TotalResponses", + "Value": "Total Responses" + }, + { + "Id": "19389bec-2911-5e15-934f-504f1542955d", + "CultureName": "tr", + "ResourceName": "Platform", + "Key": "App.Widget.TotalResponses", + "Value": "Toplam Yanıt" + }, + { + "Id": "4328b107-b212-51d1-9d4a-a4803fa1cc0d", + "CultureName": "en", + "ResourceName": "Platform", + "Key": "App.Widget.TotalLikes", + "Value": "Total Likes" + }, + { + "Id": "da98e932-8f63-5b08-85cd-3814963833f0", + "CultureName": "tr", + "ResourceName": "Platform", + "Key": "App.Widget.TotalLikes", + "Value": "Toplam Beğeni" + }, + { + "Id": "98fbfb84-ac4d-5dc1-8acd-3f3d2b4c3b01", + "CultureName": "en", + "ResourceName": "Platform", + "Key": "App.Widget.Upcoming", + "Value": "Upcoming" + }, + { + "Id": "96ee24d2-a8cf-53c7-aa68-bc208a1b7a6c", + "CultureName": "tr", + "ResourceName": "Platform", + "Key": "App.Widget.Upcoming", + "Value": "Yaklaşan" + }, + { + "Id": "ca076402-bf1c-5fee-a38e-44e0c8904dab", + "CultureName": "en", + "ResourceName": "Platform", + "Key": "App.Widget.Sent", + "Value": "Sent" + }, + { + "Id": "37d86092-7c5b-5bfd-ae10-7470c63f5347", + "CultureName": "tr", + "ResourceName": "Platform", + "Key": "App.Widget.Sent", + "Value": "Gönderildi" + }, + { + "Id": "a50e4ec1-03ce-5c61-88c6-2854ba91d762", + "CultureName": "en", + "ResourceName": "Platform", + "Key": "App.Widget.Pending", + "Value": "Pending" + }, + { + "Id": "e7ddfd18-5a70-5ba6-b72a-228330f0372a", + "CultureName": "tr", + "ResourceName": "Platform", + "Key": "App.Widget.Pending", + "Value": "Bekliyor" + }, + { + "Id": "92f99a0d-c8aa-5a0f-b006-180b1072640e", + "CultureName": "en", + "ResourceName": "Platform", + "Key": "App.Widget.Unread", + "Value": "Unread" + }, + { + "Id": "64d6688e-9d99-5794-8ade-643ddd8a38b9", + "CultureName": "tr", + "ResourceName": "Platform", + "Key": "App.Widget.Unread", + "Value": "Okunmadı" + }, + { + "Id": "ca8789e7-ffdc-502e-80db-62d72e745be0", + "CultureName": "en", + "ResourceName": "Platform", + "Key": "App.Widget.AcceptedAnswers", + "Value": "Accepted Answers" + }, + { + "Id": "92880cf9-e12e-5f94-a905-74e45e9af69e", + "CultureName": "tr", + "ResourceName": "Platform", + "Key": "App.Widget.AcceptedAnswers", + "Value": "Kabul Edilen Cevaplar" + }, + { + "Id": "55d48500-09ce-5b61-bcdf-448f27cbdd44", + "CultureName": "en", + "ResourceName": "Platform", + "Key": "App.Widget.Unverified", + "Value": "Unverified" + }, + { + "Id": "3b9d9693-bcb7-5c95-8bd5-c57bf6b4b5d4", + "CultureName": "tr", + "ResourceName": "Platform", + "Key": "App.Widget.Unverified", + "Value": "Doğrulanmamış" } ] -} \ No newline at end of file +} diff --git a/ui/src/proxy/form/models.ts b/ui/src/proxy/form/models.ts index 83f8e0f1..678dc043 100644 --- a/ui/src/proxy/form/models.ts +++ b/ui/src/proxy/form/models.ts @@ -459,6 +459,7 @@ export interface TodoOptionDto { titleExpr?: string statusExpr?: string descriptionExpr?: string + analysisExpr?: string dueDateExpr?: string tagExpr?: string assigneeExpr?: string diff --git a/ui/src/views/admin/listForm/edit/FormTabTodo.tsx b/ui/src/views/admin/listForm/edit/FormTabTodo.tsx index 49f020bb..ac363310 100644 --- a/ui/src/views/admin/listForm/edit/FormTabTodo.tsx +++ b/ui/src/views/admin/listForm/edit/FormTabTodo.tsx @@ -28,6 +28,7 @@ const mappings: Array<{ { name: 'titleExpr', labelKey: 'TodoTitleExpr', required: true }, { name: 'statusExpr', labelKey: 'TodoStatusExpr', required: true }, { name: 'descriptionExpr', labelKey: 'TodoDescriptionExpr' }, + { name: 'analysisExpr', labelKey: 'TodoAnalysisExpr' }, { name: 'dueDateExpr', labelKey: 'TodoDueDateExpr' }, { name: 'tagExpr', labelKey: 'TodoTagExpr' }, { name: 'assigneeExpr', labelKey: 'TodoAssigneeExpr' }, diff --git a/ui/src/views/admin/listForm/wizard/Wizard.tsx b/ui/src/views/admin/listForm/wizard/Wizard.tsx index ab238ab7..7bbab818 100644 --- a/ui/src/views/admin/listForm/wizard/Wizard.tsx +++ b/ui/src/views/admin/listForm/wizard/Wizard.tsx @@ -137,13 +137,14 @@ const initialValues: ListFormWizardDto = { titleExpr: '', statusExpr: '', descriptionExpr: '', + analysisExpr: '', dueDateExpr: '', tagExpr: '', assigneeExpr: '', priorityExpr: '', completedExpr: '', orderExpr: '', - statusOrder: 'Backlog, Ready to Start, In Progress, Code Review, Testing, Staging', + statusOrder: 'Backlog, Analysis, Ready to Start, In Progress, Testing, Staging', allowDragging: true, }, workflowDto: { diff --git a/ui/src/views/admin/listForm/wizard/WizardStepTodoLayout.tsx b/ui/src/views/admin/listForm/wizard/WizardStepTodoLayout.tsx index 36e7a93c..abec6c88 100644 --- a/ui/src/views/admin/listForm/wizard/WizardStepTodoLayout.tsx +++ b/ui/src/views/admin/listForm/wizard/WizardStepTodoLayout.tsx @@ -55,6 +55,10 @@ const mappings: Array<{ name: 'descriptionExpr', translationKey: '::App.ListFormFieldEdit.TodoDescriptionExpr', }, + { + name: 'analysisExpr', + translationKey: '::App.ListFormFieldEdit.TodoAnalysisExpr', + }, { name: 'dueDateExpr', translationKey: '::App.ListFormFieldEdit.TodoDueDateExpr', diff --git a/ui/src/views/list/TodoBoard.tsx b/ui/src/views/list/TodoBoard.tsx index 354bdfe7..89443683 100644 --- a/ui/src/views/list/TodoBoard.tsx +++ b/ui/src/views/list/TodoBoard.tsx @@ -78,15 +78,15 @@ interface UserOption { type LookupDisplayValues = Record> -type DetailTab = 'description' | 'timesheet' | 'subtask' +type DetailTab = 'description' | 'analysis' | 'timesheet' | 'subtask' type JsonRow = Record const TODO_STATUS_LANGUAGE_KEYS: Record = { backlog: 'App.StaticLookup.Backlog', + analysis: 'App.StaticLookup.Analysis', 'ready to start': 'App.StaticLookup.ReadyToStart', 'in progress': 'App.StaticLookup.InProgress', - 'code review': 'App.StaticLookup.CodeReview', testing: 'App.StaticLookup.Testing', staging: 'App.StaticLookup.Staging', } @@ -972,6 +972,7 @@ const TodoBoardContent = ({ options.titleExpr, options.statusExpr, options.descriptionExpr, + options.analysisExpr, options.dueDateExpr, options.tagExpr, options.assigneeExpr, @@ -1038,6 +1039,12 @@ const TodoBoardContent = ({ label: translate('::App.Listform.ListformField.Description'), } : undefined, + options.analysisExpr + ? { + key: 'analysis' as DetailTab, + label: translate('::App.Listform.ListformField.Analysis'), + } + : undefined, options.timesheetExpr ? { key: 'timesheet' as DetailTab, @@ -1136,6 +1143,21 @@ const TodoBoardContent = ({ } /> )} + {activeTab === 'analysis' && options.analysisExpr && ( + + setDraft((current) => ({ + ...current, + [options.analysisExpr!]: event.value, + })) + } + /> + )} {activeTab === 'timesheet' && options.timesheetExpr && (
{hasInvalidJson(fieldValue(draft, options.timesheetExpr)) && (