From ae226d359b85f7b2c9e48d6bf3d404956455cedf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sedat=20=C3=96ZT=C3=9CRK?= <76204082+iamsedatozturk@users.noreply.github.com> Date: Mon, 28 Sep 2026 15:50:38 +0300 Subject: [PATCH] =?UTF-8?q?wizard=203=20sekmeli=20sistem=20ve=20timesheet?= =?UTF-8?q?=20=C3=A7al=C4=B1=C5=9Fmas=C4=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lowcode-reference.instructions.md | 1 + .github/instructions/lowcode.instructions.md | 63 +++-- README.md | 11 +- .../IMcpDevelopAppService.cs | 1 + .../McpDataAppService.cs | 6 +- .../McpDevRequestActivityCacheItem.cs | 13 + .../McpDevRequestTimesheetRecorder.cs | 115 +++++++++ .../McpDevelopAppService.cs | 35 ++- .../McpTimesheetTexts.cs | 20 ++ .../Tools/McpDevelopTools.cs | 3 +- .../ListForms/Wizard/ListFormWizardDto.cs | 39 +++ .../ListForms/Wizard/WizardLanguageTextDto.cs | 12 + .../ListForms/Wizard/WizardMenuLevelDto.cs | 13 + .../ListForms/Wizard/WizardSeedFileDto.cs | 10 + .../ListForms/Wizard/WizardSeedRowKeys.cs | 11 +- .../ListForms/ListFormWizardAppService.cs | 228 ++++++++++++++---- claude.md | 7 +- .../host/data/App.Languages.LanguageText.json | 140 +++++++++++ ui/src/proxy/admin/wizard/models.ts | 21 ++ ui/src/views/admin/listForm/wizard/Wizard.tsx | 9 + .../listForm/wizard/WizardStepDeploy.tsx | 7 +- 21 files changed, 683 insertions(+), 82 deletions(-) create mode 100644 api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpDevRequestActivityCacheItem.cs create mode 100644 api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpDevRequestTimesheetRecorder.cs create mode 100644 api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain.Shared/McpTimesheetTexts.cs create mode 100644 api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/WizardLanguageTextDto.cs create mode 100644 api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/WizardMenuLevelDto.cs diff --git a/.github/instructions/lowcode-reference.instructions.md b/.github/instructions/lowcode-reference.instructions.md index 3ad73d2b..81a9b81a 100644 --- a/.github/instructions/lowcode-reference.instructions.md +++ b/.github/instructions/lowcode-reference.instructions.md @@ -834,6 +834,7 @@ tipli okumaya bağlı davranış (GridBox kolonları, TagBox etiket sayısı) ç | İhtiyaç | Nerede | | --- | --- | | Fotoğraf çekme | `dxImageUpload` · `Capture` (yukarıda) | +| Görsel tutan kolon | Editör `dxImageUpload`, kolon `nvarchar(MAX)` (base64); `dxImageViewer` kolona bağlanmaz (`lowcode.instructions.md` §0.12 madde 5) | | Alana barkod/QR okutma | `dxTextBox` `EditorOptions.buttons[]` içinde `onClick: "@SCAN_BARCODE"` (`lowcode` §6.4) | | Sürekli okutup her kodu yeni satır yapma | Toolbar butonu `DialogName: "BarcodeScanDialog"` + `DialogParameters` `{"field": "…", …sabit alanlar}` — ekranın insert ucundan geçer (`lowcode` §4.9) | | Cihaz konumu | `EditorScript` `location` tarifi → `geoTo('Enlem', 'Boylam')` (`lowcode` §5.3) | diff --git a/.github/instructions/lowcode.instructions.md b/.github/instructions/lowcode.instructions.md index 2d438b2e..5dbd9573 100644 --- a/.github/instructions/lowcode.instructions.md +++ b/.github/instructions/lowcode.instructions.md @@ -365,7 +365,7 @@ dörtlüyle üretilir (§9.8), görseldeki görünüm `DefaultLayout` olur. | Renk kutucuğu | `dxColorBox` | | | Kalın/italik araç çubuklu alan | `dxHtmlEditor` | `toolbar.items` | | Kaydırıcı | `dxSlider` / `dxRangeSlider` | `min`, `max` | -| Görsel küçük resmi / yükleme alanı | `dxImageViewer` / `dxImageUpload` | `accept`, `maxFileSize` | +| Fotoğraf / görsel alanı (seçilen, çekilen ya da başka kayıttan kopyalanan) | `dxImageUpload`, kolon `nvarchar(MAX)` (base64); salt okunur gösterilecekse `EditorOptions: {"readOnly": true}` — `dxImageViewer` değil | `accept`, `maxFileSize`, `capture` | | Gri, tıklanamaz alan | ilgili editör + `EditorOptions: {"readOnly": true}` | | | Kırmızı yıldızlı etiket | `IsRequired: true` | | @@ -505,7 +505,7 @@ ister ve kiracı kullanıcısına hiçbiri açılmaz. | `get_dev_guide` | Bu dosyanın (`lowcode`) ya da `reference`'ın başlık listesini / bir bölümünü döner | Bölüm numarayla istenir (`4.3`); aynı numarayı iki başlık taşımamalıdır | | `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). List ekranında `Widgets` dolu gelir — en az 3 kart, kiracı tablosunda `"TenantId" = '@TENANTID'` (§4.6.1). İstek demo veri taşıyorsa `SeedData: true` + `seedRows` (§0.10.1). Çıkıştaki `isPending: true` ekranın henüz kurulmadığını söyler — kullanıcı Wizard Manager'da DB Migrate çalıştırır; model migrate'i beklemez | +| `deploy_wizard` | Wizard cevabından ekranın wizard dosyasını ve menü/yetki/dil seed satırlarını yazar; veritabanına yazmaz (§4.0) | Girdi §4.4'teki düz cevap ya da `get_wizard` çıktısıyla aynı dosya biçimi; enum'lar sayı. `replaceFileName` = `EditFileName` (§4.5). List ekranında `Widgets` dolu gelir — en az 3 kart, kiracı tablosunda `"TenantId" = '@TENANTID'` (§4.6.1); kartlardaki her dil anahtarı var olmalı ya da `Language.Texts`'te EN + TR verilmeli, yoksa deploy reddeder. `MenuOrder` yazılmaz — sunucu yeni ekranı kardeşlerinin sonuna koyar, güncellemede yerini korur. İstek demo veri taşıyorsa `SeedData: true` + `seedRows` (§0.10.1). Çıkıştaki `isPending: true` ekranın henüz kurulmadığını söyler — kullanıcı Wizard Manager'da DB Migrate çalıştırır; model migrate'i beklemez | | `delete_wizard` | Wizard'ı ve yalnızca onun ürettiği kayıtları siler; `dropTables: true` ekranın tablosunu da düşürür | Bağımlı wizard varsa reddeder. Tablo kullanıcı açıkça onaylamadan düşürülmez; `App.SqlQueryManager` ister (§4.5) | | `start_dev_request` | `App.DevRequests` talebini üstlenir: `Ready to Start` durumundaysa `In Progress` durumuna alır, değilse reddeder | Ek olarak `App.DevRequests.Update` ister. Açıklamadaki görseller ayrı resim içeriği olarak gelir (§0.11) | | `get_dev_request` | Talebi durumuna dokunmadan okur (aynı metin + analiz + resim içerikleri) | Analizden önce ya da yarım kalan talebe dönmek için; `App.DevRequests` ister | @@ -602,6 +602,14 @@ talebin metnini döndürür. Başlık birden fazla talebe uyuyorsa Id ister. `St verilerek (`"deploy_wizard: Mrp.Orders.json"`). Araç satırı tamamlanmış yazar, atanan kişiyi bağlanan kullanıcı, bitiş tarihini bugün yapar ve var olan satırları korur. `SubTasks` `update_row` ile yazılmaz. +- `Timesheets` — **sunucu yazar, model hiçbir şey yapmaz.** `get_dev_request` / `start_dev_request` + talebi kullanıcının etkin talebi yapar; ardından yapılan her MCP yazma işlemi (`create_table`, + `deploy_wizard`, `delete_wizard`, `save_component`, `delete_component`, `insert_row` / + `update_row` / `delete_row`, `save_dev_request_analysis`) o talebe bir satır ekler + (`IMcpDevRequestTimesheetRecorder`). Açıklama `App.Mcp.Timesheet.*` dil anahtarından **Türkçe** + üretilir ("Hr_T_Doctor tablosu oluşturuldu"), süre önceki satırdan ya da talebin açıldığı andan bu + yana ölçülür (en az 1 dakika, saat olarak), çalışan bağlanan kullanıcı, tarih bugündür. + `Timesheets` `update_row` ile yazılmaz. - `Analysis` — `append_dev_request_note` ile sonuna `

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. @@ -616,10 +624,16 @@ değildir) ve içindeki satırları taşır; satırlar demo veridir (§0.10.1). **Kurallar** - **Onaylanmış analiz tarifin kendisidir; üretim karar vermez.** Soru sorulmaz, seçim yapılmaz, - analizin yazdığı hiçbir şey değiştirilmez ya da daraltılmaz. Bu dosyadaki "kullanıcıya sor" + analizin yazdığı hiçbir şey değiştirilmez ya da daraltılmaz — tek istisna platform kuralıyla çelişen kısımdır (aşağıda). Bu dosyadaki "kullanıcıya sor" kuralları (onaycı, görselden okunamayan zorunlu alan, çerçeve dışı ihtiyaç) bu döngüde analizin **(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. +- **Platform kuralı analizden önce gelir.** Analiz, bu dosyadaki bağlayıcı bir kuralla çelişiyorsa + (görsel kolonu `nvarchar(MAX)` + `dxImageUpload`, tanım tablosunda `IsActive`, parametreli SQL, + kiracı filtresi…) üretim kuralı uygular, çelişkiyi sonuç notuna yazar; durmaz, soru sormaz. + Bu yüzden her `create_table` öncesi kolon listesi ekranın editörleriyle karşılaştırılır: editörü + `dxImageUpload` olan kolon `nvarchar(MAX)` açılır — tablo kurulduktan sonra kolon değiştirilemez. + Kural analizi talebin istediğinden uzaklaştırmaz; yalnızca platformun kurallarına uydurur. - **Üretim tek seferde biter:** `In Progress` → `Testing` tek geçiştir; talep durdurulmaz, `Analysis`'e geri alınmaz, kısmi ekran üretilmez. Üretilebilirlik analizde doğrulanmıştır (§0.12). Döngü DB Migrate'i beklemez. @@ -643,7 +657,7 @@ bileşen, seed dosyası yazılmaz; yalnızca okuma araçları ve `save_dev_reque | 1. Kuyruğu oku (yalnızca toplu) | `query_list` — `ListFormCode: App.DevRequests`, filtre `["Status","=","Backlog"]`, sıralama `SortOrder` | — | | 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 | +| 4. Yaz | `save_dev_request_analysis` — analizin tamamı, HTML (Timesheets satırını sunucu yazar, §0.11) | Analysis | **Durum kuralı sunucudadır:** araç yalnızca `Backlog` ya da `Analysis` durumundaki talebi kabul eder; `Analysis`'teki talebe yeniden çağrılırsa analiz değiştirilir. `Ready to Start` ve sonrası @@ -676,27 +690,38 @@ Kısa ve karar odaklı yaz; talebi tekrar anlatma. | Seviye | Kod | Ad (TR / EN) | İkon | Durum | | --- | --- | --- | --- | --- | | Kök | … | … / … | … | Var / Yeni | - | Ara (varsa, her biri) | … | … / … | … | Var / Yeni | + | Ara (yalnızca talep istiyorsa, 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. + yazılmaz. Seviye sayısı talepten gelir, sabit değildir: `Kök › Ekran` iki, `Kök › Ara › Ekran` + üç seviyedir; analiz talebin istemediği bir ara seviye eklemez. Talep menü yolunu yazıyorsa + (başlıkta ya da açıklamada) o yol girdidir ve seviye seviye aynen alınır. Wizard karşılığı: + ekranın bir üstü `MenuParentCode`, daha üstündekiler `MenuParentPath` (iki seviyede boş, §4.4). + Aynı üst menü altındaki ekranlar **bağımlılık sırasıyla** yazılır: başka bir ekranın lookup + kaynağı olan (tanım) ekran, onu kullanan ekranın üstündedir. Menüdeki sıra deploy sırasından + gelir; madde 9'daki üretim planı da aynı sırayla yazılır. 5. **Veri modeli** — tablo adı (onek kuralı §2), yeni mi var olan mı; kolonlar (ad, tip, zorunluluk, 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. + **Görsel kolonu:** görsel tutan her kolon (fotoğraf, resim, logo, avatar kopyası, belge + görüntüsü) `nvarchar(MAX)`'tir ve editörü `dxImageUpload`'dır — değer başka bir kayıttan + kopyalanıyor olsa da. Görsel base64 metin olarak saklanır; `nvarchar(512)` gibi sınırlı boy + yazılmaz, `dxImageViewer` kullanılmaz (§4.3 editör tablosu). Çok satırlı serbest metin + (`dxTextArea`, `dxHtmlEditor`) de `nvarchar(MAX)`'tir. 6. **Etkilenen kısımlar** — uygulamada dokunulacak her yer: yeni/değişen ekranlar, alt formlar, 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. + `EditorScript` / `EditorOptions` ihtiyaçları, hesaplar ve doğrulamalar. Görsel alanı her zaman + `dxImageUpload`'dır (madde 5). 8. **Kararlar** — talepten okunamayıp analizde verilen her karar, tek satır gerekçesiyle (§0.6); varsayılamayan konular **(onay)** etiketi taşır. "Açık sorular" başlığı yoktur. -9. **Üretim planı** — sıralı artefakt listesi (`create_table: …`, `deploy_wizard: ….json`, +9. **Üretim planı** — bağımlılık sırasıyla (tablo → lookup kaynağı ekran → onu kullanan ekran) sıralı artefakt listesi (`create_table: …`, `deploy_wizard: ….json`, `save_component: …`); üretimdeki `add_dev_request_subtasks` başlıkları bununla aynı olur. 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 @@ -915,6 +940,7 @@ sürümünü taşır. "SeededAt": "2026-09-01T09:03:00", "CustomComponentName": null, "PermissionGroupName": "App.Prj", "ModuleId": "Prj", // boş bağlantı metni yazılmaz "Menu": { "MenuCode": "…", "MenuOrder": 2, "MenuUrl": "…", "MenuIcon": "…", + "MenuParentPath": [ { "Code": "…", "Icon": "…", "En": "…", "Tr": "…" } ], // yalnızca derin yolda "MenuParentCode": "…", "MenuParentIcon": "…" }, // "CreateMenu": false ise eklenir "Language": { "MenuEn": "…", /* Menu/Title/Desc/MenuParent/PermissionGroup · EN + TR */ }, "Groups": [ { "Items": [ { "FieldName": "…", "CaptionName": "…", @@ -999,9 +1025,10 @@ saklanır. Dosya okunurken bu alanlar `ListForm`/`Fields` üzerinden geri doldur | Alan | Anlam | | --- | --- | | `CreateMenu` | `false` ise menü ve üst menü kaydı **hiç üretilmez**; ListForm, yetki ve dil anahtarları yine üretilir. SubGrid/parça olarak kullanılacak ekranlar için. Varsayılan `true` olduğu için dosyaya **yalnızca `false` iken** yazılır. | -| `MenuParentCode` / `MenuParentModuleId` / `MenuParentIcon` | Üst menü; yoksa oluşturulur. | +| `MenuParentCode` / `MenuParentModuleId` / `MenuParentIcon` | Ekranın doğrudan üst menüsü; yoksa oluşturulur. | +| `MenuParentPath` | `MenuParentCode`'un üstündeki seviyeler, kökten aşağı; her biri `{ Code, Icon, En, Tr }`. Derinlik sınırsızdır: deploy zinciri kökten yürür, var olan seviyeye bağlanır, eksik seviyeyi bir üstünün altına kardeşlerinin sonuna kurar (menü + dil satırı, `InsertedRecords`'a girer). Boşsa üst menü kök seviyededir; boş liste dosyaya yazılmaz. Örnekler: `İnsan Kaynakları › Yeterlilik Belgeleri` (iki seviye) → `MenuParentCode: "App.Hr"`, `MenuParentPath` yok; `İnsan Kaynakları › Tanımlamalar › Yeterlilik Belgeleri` (üç seviye) → `MenuParentPath: [{ Code: "App.Hr", … }]`, `MenuParentCode: "App.Hr.Definitions"`. | | `MenuIcon` | React-icons adı (`FcBiohazard`, `FaBox`…). | -| `MenuOrder` | Kardeşler arası sıra. | +| `MenuOrder` | Kardeşler arası sıra; 0/boş ise kardeşlerin sonu. MCP deploy'unda yok sayılır (yeni ekran sona, güncelleme yerinde kalır). | **Yetki ve dil** @@ -1018,6 +1045,7 @@ saklanır. Dosya okunurken bu alanlar `ListForm`/`Fields` üzerinden geri doldur | `Language.TitleEn` / `Tr` | Ekran başlığı (yalnızca List yolunda üretilir). | | `Language.DescEn` / `Tr` | Ekran açıklaması (yalnızca List yolunda). | | `Language.MenuParentEn` / `Tr` | Üst menü etiketi. | +| `Language.Texts` | Başka bir cevaptan üretilmeyen dil anahtarları: `[{ "Key", "En", "Tr" }]` (widget başlığı gibi). Anahtar yoksa deploy satırı yazar ve wizard'a bağlar (silmede gider). Deploy, widget sorgularındaki tek tırnaklı noktalı her tanımlayıcıyı (`'App.Widget.ThisMonth'`) dil anahtarı sayar; veritabanında, bekleyen dil satırlarında, bu wizard'ın ürettiklerinde ya da burada yoksa deploy'u adlarıyla reddeder. | Üretilen yetkiler: `{MenuCode}` (okuma, kök) + List yolunda `.Create .Update .Delete .Export .Import .Note`, Custom yolunda yalnızca `.Create .Update .Delete`. Hiçbiri bir role otomatik @@ -1154,8 +1182,8 @@ anahtarları listeler; ortak anahtarlar (§4.3.3) her editörde geçerlidir. | `dxSlider` | Tek değerli kaydırıcı | Sayı | `min`, `max`, `tooltip.showMode` | | `dxRangeSlider` | Aralık kaydırıcı | Sayı | `min`, `max`, `showRange` | | `dxHtmlEditor` | Zengin metin | String (16) | `toolbar.items`, `toolbar.multiline`, `valueType` (`html`/`markdown`), `mediaResizing.enabled`, `imageUpload.tabs`, `imageUpload.uploadUrl`, `imageUpload.fileUploadMode`, `tableResizing.enabled`, `tableContextMenu.enabled`. Boş `editorOptions` da tam araç çubuğu, dosyadan/URL'den resim, resim boyutlandırma ve tablo araçlarıyla gelir (`ui/src/views/shared/htmlEditorOptions.ts`); yazılan değer varsayılanın üstüne tek seviye birleştirilir, yalnızca farkı yaz. | -| `dxImageUpload` | Görsel yükleme (platform) | String (16) | `uploadUrl`\*, `accept`\*, `capture`\*, `multiple`\*, `maxFileSize`\*, `width`\*, `height`\* | -| `dxImageViewer` | Görsel gösterimi (platform) | String (16) | `width`, `height` | +| `dxImageUpload` | Görsel yükleme (platform); görsel tutan her kolonun editörü. `uploadUrl` yoksa dosya **base64** metin olarak kolona yazılır → kolon `nvarchar(MAX)`; sınırlı boylu kolonla deploy reddedilir. `uploadUrl` varsa kolona dönen adres yazılır | String (16) | `uploadUrl`\*, `accept`\*, `capture`\*, `multiple`\*, `maxFileSize`\*, `width`\*, `height`\* | +| `dxImageViewer` | Görsel gösterimi (platform), yükleme yapmaz. Tablo kolonuna bağlı alanda kullanılmaz — görsel kolonu `dxImageUpload`'dır, deploy `dxImageViewer`'ı reddeder | String (16) | `width`, `height` | `\*` ile işaretli anahtarlar backend'de **tipli DTO'ya** çözülür: `GridBoxOptionsDto`, `TagBoxOptionsDto`, `ImageUploadOptionsDto`. Yanlış tipte yazılan bir değer @@ -1535,7 +1563,7 @@ ile ekler (§4.0). Sıra değerleri veritabanı ile dosyadaki satırların birle | Dil anahtarları | `{MenuCode}` (menü), `{ListFormCode}.Title`, `{ListFormCode}.Desc`, `{PermissionGroupName}` ve her alan için `CaptionName` | | Yetkiler | `{MenuCode}` kökü + `.Create/.Update/.Delete` (+ List yolunda `.Export/.Import/.Note`) | | Yetkilerin dil anahtarları | Ortak sabitler: `App.Platform.Create/Update/Delete/Export/Import/Note` | -| Üst menü | `MenuParentCode` yoksa yaratılır, `Order = max(kök Order) + 1` | +| Üst menü zinciri | `MenuParentPath` + `MenuParentCode` kökten yürünür; eksik seviye bir üstünün altına `Order = max(kardeş Order) + 1` ile yaratılır (kökte kök menülerin sonu) | | Menü sırası | `MenuOrder` 0/boş ise `max(kardeş Order) + 1` | | `DataSource` | `DataSourceCode` yoksa migrate yaratır; bağlantı metninde `Server` geçiyorsa `Mssql`, aksi hâlde `Postgresql` | | `LayoutJson` | Görünüm bayrakları + `DefaultLayout` | @@ -1743,7 +1771,8 @@ gelir; kullanıcı "widget/KPI" demese de, sorulmadan eklenir ve tarifte tek sat - **Metin:** `Title` bir dil anahtarıdır. Önce var olan anahtarlar kullanılır: genel `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. + alanın `App.Listform.ListformField.*` başlığı. Karşılığı yoksa yeni anahtar wizard'ın + `Language.Texts` alanında EN + TR verilir (§4.4); tanımsız anahtarla deploy reddedilir. `SubTitle` ekranın kendi başlık anahtarıdır (`ListFormCode`). - **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`, @@ -2048,7 +2077,7 @@ bölümünü doldurur.** Keşif turu (§0.6) soruları da bu adımlarla bire bir | --- | --- | --- | --- | | 0 | Komponent Tipi | her zaman | `ComponentKind` (`0` List · `1` Custom) | | 1 | Custom Component | yalnız Custom yolu | `CustomComponentName`, `MenuUrl` (bileşenin `RoutePath`'i) | -| 2 | Menü | her zaman | `WizardName`, `CreateMenu`, `MenuParentCode`, `MenuCode`, `Language.MenuEn/Tr`, `PermissionGroupName` (+ görünen adlar), `MenuIcon`, `MenuOrder` | +| 2 | Menü | her zaman | `WizardName`, `CreateMenu`, `MenuParentCode` (ekranda eksik seviyeler menü diyaloğuyla kurulur; MCP `MenuParentPath` yazar), `MenuCode`, `Language.MenuEn/Tr`, `PermissionGroupName` (+ görünen adlar), `MenuIcon`, `MenuOrder` | | 3 | Ayarlar | yalnız List yolu | `ListFormCode`, `DataSourceCode` (+`DataSourceConnectionString`), `SelectCommandType`, `SelectCommand`, `KeyFieldName`, `KeyFieldDbSourceType`, `IsTenant/IsBranch/IsOrganizationUnit`, `AllowAdding/Updating/Deleting`, `ConfirmDelete`, `AllowDetail`, `DefaultLayout` + 8 görünüm bayrağı (`SeedData` adımda sorulmaz, düzenlemede korunur), `Language.TitleEn/Tr`, `Language.DescEn/Tr` | | 4 | Alanlar | yalnız List | `Groups[]` (aşağıda) | | 5 | Alt Formlar | yalnız List | `SubForms[]` | @@ -3860,7 +3889,7 @@ silme, DB Migrate, Export (zip), Import (diff + rollback). Yetkiler `App.Listforms.Wizard{,.Create,.Update,.Delete,.Export,.Import}`. Listede her ekranın üst menü zinciri (`WizardFileInfoDto.MenuPath`, Menu tablosundan; kayıt yoksa -wizard'ın `MenuParentCode` cevabı) görünür. Bağlı wizard'lar üst ekranın altında gruplanır +wizard'ın `MenuParentPath` + `MenuParentCode` cevabı) görünür. Bağlı wizard'lar üst ekranın altında gruplanır (`Dependencies`): List yolunda alt formlar, Custom yolunda component `Dependencies` zinciri — export kapanışıyla aynı kural; wizard'ı olmayan ara component atlanır. Kök, hiçbir wizard'ın bağımlılığı olmayan dosyadır; ortak bir alt form her üst ekranın altında tekrar görünür. diff --git a/README.md b/README.md index af874d55..ca081d54 100644 --- a/README.md +++ b/README.md @@ -1406,7 +1406,7 @@ liste süzgeci ve çağrı kapısı aynı kuralı kullanır. | `get_dev_guide` | Üretim sözleşmesi: `lowcode` / `reference` belgelerinin başlık listesi ya da istenen bölümü (numarayla, örn. `4.2`). Çok büyük bir üst bölüm yalnızca giriş metni + alt başlıklarıyla döner. | | `create_table` | Yeni tabloyu SQL Query Manager tablo tasarımcısıyla aynı script biçimiyle kurar (`McpTableCreator`): `Id` + `TenantId` + tam audit kolonları şablondan gelir, istemci yalnızca iş kolonlarını verir; tip ve varsayılan değer beyaz listeden geçer, istemci SQL yazamaz. Script `{kapsam}/sql/object/{Tablo}.sql` dosyasına yazılır. Yalnızca oluşturur — tablo varsa reddeder; yalnızca SQL Server. `.Write` + `App.SqlQueryManager` ister. | | `list_wizards` / `get_wizard` | Wizard File Manager dosyaları; `get_wizard` seed dosyasını diskteki biçimiyle döndürür. | -| `deploy_wizard` | Wizard'ı Wizard ekranıyla aynı yoldan (`IListFormWizardAppService.Create`) deploy eder. Girdi seed dosyası biçimi (`{"Wizard": …}`) ya da düz cevaplardır; ikisi de `WizardSeedFileDto.FromSeedJson`'dan geçer. Wizard ekranı gibi yalnızca seed dosyalarını yazar; ekran DB Migrate ile kurulur ve çıkıştaki `isPending` o zamana kadar `true` kalır (model migrate'i beklemez; `verify_wizard` bekleyen dosyada `install` kontrolünde durur). `replaceFileName` doluysa güncellemedir (dosyalar yeniden yazılır, önceki kayıtları migrate kaldırır). İstek demo veri taşıyorsa (prompt'taki satırlar, ekran görüntüsü) wizard `SeedData: true` ile deploy edilir — ekranın `SeedFilePath`'i `data/{ListFormCode}.json` olur ve `SeedSync*` bayrakları açılır — satırlar aynı çağrıda `seedRows` ile bu dosyaya yazılır (alan adları doğrulanır, `Guid` anahtarı sunucu üretir); migrate ekranı kurduktan sonra satırları ekler (`lowcode.instructions.md` §0.10.1). | +| `deploy_wizard` | Wizard'ı Wizard ekranıyla aynı yoldan (`IListFormWizardAppService.Create`) deploy eder. Girdi seed dosyası biçimi (`{"Wizard": …}`) ya da düz cevaplardır; ikisi de `WizardSeedFileDto.FromSeedJson`'dan geçer. Wizard ekranı gibi yalnızca seed dosyalarını yazar; ekran DB Migrate ile kurulur ve çıkıştaki `isPending` o zamana kadar `true` kalır (model migrate'i beklemez; `verify_wizard` bekleyen dosyada `install` kontrolünde durur). `replaceFileName` doluysa güncellemedir (dosyalar yeniden yazılır, önceki kayıtları migrate kaldırır). İstek demo veri taşıyorsa (prompt'taki satırlar, ekran görüntüsü) wizard `SeedData: true` ile deploy edilir — ekranın `SeedFilePath`'i `data/{ListFormCode}.json` olur ve `SeedSync*` bayrakları açılır — satırlar aynı çağrıda `seedRows` ile bu dosyaya yazılır (alan adları doğrulanır, `Guid` anahtarı sunucu üretir); migrate ekranı kurduktan sonra satırları ekler (`lowcode.instructions.md` §0.10.1). Widget sorgularındaki her dil anahtarı tanımlı olmalı ya da wizard'ın `Language.Texts` alanında EN + TR verilmeli, yoksa deploy adlarıyla reddeder (`ValidateWidgetLanguageKeysAsync`). `uploadUrl`'süz `dxImageUpload` alanı base64 sakladığı için kolonu sınırlı boyluysa (katalogdaki karakter sınırı) deploy reddedilir; tablo kolonuna bağlı `dxImageViewer` alanı da reddedilir — görsel kolonunun editörü her zaman `dxImageUpload`'dır (`ValidateImageColumnLengths`). `MenuOrder` yok sayılır: yeni ekran kardeşlerinin sonuna, güncelleme mevcut yerine yazılır — lookup kaynağı ekran önce deploy edildiği için onu kullanan ekranın üstünde kalır. | | `delete_wizard` | Wizard Manager'daki silmeyle aynı: `InsertedRecords` + ekranın veri aynası, bağımlı wizard varsa reddedilir. `dropTables: true` ekranın tablosunu (yalnızca tasarımcı/`create_table` ile kurulmuş ve başka yerde kullanılmayan) CRUD endpoint'leri, `sql/object` ve `crud` dosyalarıyla düşürür; model bunu kullanıcı onayı olmadan göndermez (`WizardTableCleaner`, `lowcode.instructions.md` §4.5). | | `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. | @@ -1415,6 +1415,15 @@ liste süzgeci ve çağrı kapısı aynı kuralı kullanır. | `add_dev_request_subtasks` | Talebin `SubTasks` listesine tamamlanmış satırlar ekler: model yalnızca başlığı verir, atanan kişi bağlanan kullanıcının adı (`userName`), bitiş tarihi bugün (`yyyy-MM-dd`) olarak sunucuda yazılır; var olan satırlar korunur. `.Write` + `App.DevRequests.Update` ister. | | `append_dev_request_note` | Talebin `Analysis` kolonunun sonuna HTML ekler (üretim sonuç notu); `Description` talebin yazıldığı hâliyle kalır. İkisi de `update_row` ile yazılmaz — açıklama base64 görsel taşır. `.Write` + `App.DevRequests.Update` ister. | +**Timesheets.** Talebin `Timesheets` listesini sunucu doldurur (`IMcpDevRequestTimesheetRecorder`): +`get_dev_request` / `start_dev_request` talebi kullanıcının etkin talebi yapar (dağıtık önbellek, +`McpDevRequestActivityCacheItem`, 12 saat); sonraki her MCP yazma işlemi (`create_table`, +`deploy_wizard`, `delete_wizard`, `save_component`, `delete_component`, satır araçları, +`save_dev_request_analysis`) o talebe bir satır ekler. Açıklama `App.Mcp.Timesheet.*` anahtarından +Türkçe üretilir, süre önceki satırdan ya da talebin açıldığı andan ölçülür (en az 1 dakika, saat +olarak), çalışan bağlanan kullanıcı, tarih bugündür. Etkin talep yoksa satır yazılmaz; bozuk bir +`Timesheets` değeri işlemi geri almaz, yalnızca satır atlanır ve log'a düşer. + **Prompt'lar** (`McpPromptNames`, `McpDevelopPrompts`). İstemcide komut olarak görünür; yalnızca 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, 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 c052d90d..7da8fc6c 100644 --- a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application.Contracts/IMcpDevelopAppService.cs +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application.Contracts/IMcpDevelopAppService.cs @@ -55,6 +55,7 @@ public interface IMcpDevelopAppService : IApplicationService /// Task AddDevRequestSubTasksAsync(string request, List titles); + Task> GetComponentsAsync(string? search); Task GetComponentAsync(string name); diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpDataAppService.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpDataAppService.cs index fc4a89be..dd28818f 100644 --- a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpDataAppService.cs +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpDataAppService.cs @@ -21,13 +21,15 @@ namespace Sozsoft.Mcp; public class McpDataAppService( IMcpServerConfigProvider serverConfigProvider, IListFormDataAppService listFormDataAppService, - IMcpListFormAccessFilter accessFilter) + IMcpListFormAccessFilter accessFilter, + IMcpDevRequestTimesheetRecorder timesheetRecorder) : McpAppService(serverConfigProvider), IMcpDataAppService { public async Task InsertRowAsync(McpRowRequestDto input) { var request = await BuildRequestAsync(input, McpToolNames.InsertRow, AuthorizationTypeEnum.Create, requireKeys: false, requireValues: true); var key = await listFormDataAppService.PostInsertAsync(request); + await timesheetRecorder.RecordAsync(McpTimesheetTexts.InsertRow, input.ListFormCode); return new McpWriteResultDto { Key = ((object?)key)?.ToString() }; } @@ -36,6 +38,7 @@ public class McpDataAppService( { var request = await BuildRequestAsync(input, McpToolNames.UpdateRow, AuthorizationTypeEnum.Update, requireKeys: true, requireValues: true); var affected = await listFormDataAppService.PostUpdateAsync(request); + await timesheetRecorder.RecordAsync(McpTimesheetTexts.UpdateRow, input.ListFormCode, string.Join(", ", input.Values.Keys)); return new McpWriteResultDto { AffectedRows = affected }; } @@ -44,6 +47,7 @@ public class McpDataAppService( { var request = await BuildRequestAsync(input, McpToolNames.DeleteRow, AuthorizationTypeEnum.Delete, requireKeys: true, requireValues: false); var affected = await listFormDataAppService.PostDeleteAsync(request); + await timesheetRecorder.RecordAsync(McpTimesheetTexts.DeleteRow, input.ListFormCode); return new McpWriteResultDto { AffectedRows = affected }; } diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpDevRequestActivityCacheItem.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpDevRequestActivityCacheItem.cs new file mode 100644 index 00000000..9e870590 --- /dev/null +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpDevRequestActivityCacheItem.cs @@ -0,0 +1,13 @@ +using System; + +namespace Sozsoft.Mcp; + +/// +/// Kullanicinin MCP ile uzerinde calistigi talep ve son kaydedilen etkinlik ani; bir sonraki +/// timesheet satirinin suresi bu andan olculur. +/// +public class McpDevRequestActivityCacheItem +{ + public Guid RequestId { get; set; } + public DateTime At { get; set; } +} diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpDevRequestTimesheetRecorder.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpDevRequestTimesheetRecorder.cs new file mode 100644 index 00000000..a6ab1ba1 --- /dev/null +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpDevRequestTimesheetRecorder.cs @@ -0,0 +1,115 @@ +using System; +using System.Globalization; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Threading.Tasks; +using Microsoft.Extensions.Caching.Distributed; +using Microsoft.Extensions.Localization; +using Microsoft.Extensions.Logging; +using Sozsoft.Platform.Entities; +using Sozsoft.Platform.Localization; +using Volo.Abp.Caching; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Domain.Repositories; +using Volo.Abp.Guids; +using Volo.Abp.Localization; +using Volo.Abp.Timing; +using Volo.Abp.Users; + +namespace Sozsoft.Mcp; + +/// +/// Gelistirme talebinin Timesheets listesini MCP islemlerinden doldurur. Talep +/// get_dev_request / start_dev_request ile acildiginda kullanicinin etkin talebi +/// olur; ardindan yapilan her yazma islemi o talebe bir satir ekler. Satir modelin dikkatine +/// birakilmaz: aciklama dil anahtarindan Turkce uretilir, sure son etkinlikten bu yana olculur. +/// +public interface IMcpDevRequestTimesheetRecorder +{ + /// Talebi kullanicinin etkin talebi yapar ve olcumu baslatir. + Task BeginAsync(Guid devRequestId); + + /// + /// Etkin talebe bir satir yazar; etkin talep yoksa (islem bir talep dongusunun parcasi + /// degilse) hicbir sey yapmaz. + /// + Task RecordAsync(string descriptionKey, params object[] args); +} + +public class McpDevRequestTimesheetRecorder( + IDistributedCache activityCache, + IRepository devRequestRepository, + IStringLocalizer localizer, + ICurrentUser currentUser, + IClock clock, + IGuidGenerator guidGenerator, + ILogger logger) : IMcpDevRequestTimesheetRecorder, ITransientDependency +{ + /// Timesheet satirlarinin dili; talep ekrani Turkce kullanilir. + private const string TimesheetCulture = "tr"; + + /// Talep ekranindaki tarih kutusunun okudugu bicim. + private const string DateFormat = "yyyy-MM-dd"; + + /// + /// Etkin talebin omru bir calisma oturumunu kapsar; suresi dolan kayit, gunler once acilmis + /// bir talebe baska bir isin yazilmasini ve sahte uzun sureyi engeller. + /// + private static readonly TimeSpan ActivityLifetime = TimeSpan.FromHours(12); + + public Task BeginAsync(Guid devRequestId) => StampAsync(devRequestId, clock.Now); + + public async Task RecordAsync(string descriptionKey, params object[] args) + { + var activity = await activityCache.GetAsync(ActivityKey()); + if (activity is null) + return; + + var devRequest = await devRequestRepository.FindAsync(activity.RequestId); + if (devRequest is null) + return; + + JsonArray timesheets; + try + { + timesheets = string.IsNullOrWhiteSpace(devRequest.Timesheets) + ? [] + : JsonNode.Parse(devRequest.Timesheets) as JsonArray ?? throw new JsonException(); + } + catch (JsonException) + { + // Yapilan is (deploy, tablo) zaten tamamlandi; bozuk bir timesheet onu geri almamali. + logger.LogWarning("Timesheets of development request {RequestId} could not be read; entry skipped.", devRequest.Id); + return; + } + + string description; + using (CultureHelper.Use(TimesheetCulture)) + { + description = localizer[descriptionKey, args]; + } + + var now = clock.Now; + var minutes = Math.Max(1, (int)Math.Round((now - activity.At).TotalMinutes)); + + timesheets.Add(new JsonObject + { + ["id"] = guidGenerator.Create().ToString(), + ["date"] = now.ToString(DateFormat, CultureInfo.InvariantCulture), + ["userName"] = currentUser.UserName ?? string.Empty, + ["description"] = description, + // Ekranin sure kutusu saati ondalik sayi olarak okur. + ["duration"] = Math.Round(minutes / 60d, 4) + }); + + devRequest.Timesheets = timesheets.ToJsonString(); + await devRequestRepository.UpdateAsync(devRequest, autoSave: true); + await StampAsync(devRequest.Id, now); + } + + private string ActivityKey() => currentUser.Id?.ToString() ?? string.Empty; + + private Task StampAsync(Guid devRequestId, DateTime at) => + activityCache.SetAsync(ActivityKey(), new McpDevRequestActivityCacheItem { RequestId = devRequestId, At = at }, + new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = ActivityLifetime }); +} diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpDevelopAppService.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpDevelopAppService.cs index a5a2e48e..338a8999 100644 --- a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpDevelopAppService.cs +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpDevelopAppService.cs @@ -42,6 +42,7 @@ public class McpDevelopAppService( IRepository menuRepository, IRepository permissionRepository, IRepository devRequestRepository, + IMcpDevRequestTimesheetRecorder timesheetRecorder, IConfiguration configuration) : McpAppService(serverConfigProvider), IMcpDevelopAppService { @@ -121,6 +122,8 @@ public class McpDevelopAppService( input.FullAudited, input.MultiTenant)); + await timesheetRecorder.RecordAsync(McpTimesheetTexts.CreateTable, created.TableName); + return new McpCreateTableResultDto { TableName = created.TableName, @@ -165,6 +168,13 @@ public class McpDevelopAppService( var wizard = ParseWizard(input.Wizard); wizard.EditFileName = string.IsNullOrWhiteSpace(input.ReplaceFileName) ? null : input.ReplaceFileName.Trim(); + // Menu sirasi modelin karari degildir: yeni ekran kardeslerinin sonuna eklenir (deploy + // bagimlilik sirasiyla yapildigi icin lookup kaynagi tuketen ekranin ustunde kalir), + // guncelleme ekranin mevcut yerini korur. + wizard.MenuOrder = wizard.EditFileName is null + ? 0 + : (await wizardAppService.GetFile(wizard.EditFileName)).Wizard?.MenuOrder ?? 0; + // Satirlar deploy'dan once dogrulanir; gecersiz girdi yarim bir deploy birakmaz. var seedRows = ParseSeedRows(input.SeedRows, wizard); @@ -176,6 +186,8 @@ public class McpDevelopAppService( if (file != null && seedRows.Count > 0) await WriteSeedRowsAsync(file.FileName, seedRows); + await timesheetRecorder.RecordAsync(McpTimesheetTexts.DeployWizard, file?.FileName ?? wizard.WizardName, seedRows.Count); + return file; } @@ -277,7 +289,10 @@ public class McpDevelopAppService( input.DropTables = [.. analysis.Tables.Where(t => t.CanDrop).Select(t => t.TableName)]; } - return await wizardAppService.DeleteFile(input); + var result = await wizardAppService.DeleteFile(input); + await timesheetRecorder.RecordAsync(McpTimesheetTexts.DeleteWizard, fileName); + + return result; } /// @@ -370,6 +385,7 @@ public class McpDevelopAppService( devRequest.Status = McpConsts.DevRequestStatuses.InProgress; await devRequestRepository.UpdateAsync(devRequest, autoSave: true); + await timesheetRecorder.BeginAsync(devRequest.Id); return ToDto(devRequest); } @@ -379,7 +395,10 @@ public class McpDevelopAppService( await EnsureToolAllowedAsync(McpToolNames.GetDevRequest); await AuthorizationService.CheckAsync(PlatformConsts.AppCodes.DevRequests); - return ToDto(await FindDevRequestAsync(request)); + var devRequest = await FindDevRequestAsync(request); + await timesheetRecorder.BeginAsync(devRequest.Id); + + return ToDto(devRequest); } /// @@ -407,6 +426,7 @@ public class McpDevelopAppService( devRequest.Analysis = html.Trim(); devRequest.Status = McpConsts.DevRequestStatuses.Analysis; await devRequestRepository.UpdateAsync(devRequest, autoSave: true); + await timesheetRecorder.RecordAsync(McpTimesheetTexts.Analysis); return ToDto(devRequest); } @@ -680,7 +700,7 @@ public class McpDevelopAppService( throw new UserFriendlyException(L[McpErrorCodes.ComponentCodeRequired]); } - return await componentAppService.CreateAsync(new CreateUpdateCustomComponentDto + var created = await componentAppService.CreateAsync(new CreateUpdateCustomComponentDto { Name = input.Name.Trim(), Code = input.Code, @@ -690,9 +710,12 @@ public class McpDevelopAppService( Dependencies = input.Dependencies, IsActive = input.IsActive ?? true }); + await timesheetRecorder.RecordAsync(McpTimesheetTexts.SaveComponent, created.Name); + + return created; } - return await componentAppService.UpdateAsync(existing.Id, new CreateUpdateCustomComponentDto + var updated = await componentAppService.UpdateAsync(existing.Id, new CreateUpdateCustomComponentDto { Name = existing.Name, Code = string.IsNullOrWhiteSpace(input.Code) ? existing.Code : input.Code, @@ -702,6 +725,9 @@ public class McpDevelopAppService( Dependencies = input.Dependencies ?? existing.Dependencies, IsActive = input.IsActive ?? existing.IsActive }); + await timesheetRecorder.RecordAsync(McpTimesheetTexts.SaveComponent, updated.Name); + + return updated; } public async Task DeleteComponentAsync(string name) @@ -712,6 +738,7 @@ public class McpDevelopAppService( ?? throw new UserFriendlyException(L[McpErrorCodes.ComponentNotFound, name]); await componentAppService.DeleteAsync(component.Id); + await timesheetRecorder.RecordAsync(McpTimesheetTexts.DeleteComponent, component.Name); } /// diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain.Shared/McpTimesheetTexts.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain.Shared/McpTimesheetTexts.cs new file mode 100644 index 00000000..8ebfc23d --- /dev/null +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain.Shared/McpTimesheetTexts.cs @@ -0,0 +1,20 @@ +namespace Sozsoft.Mcp; + +/// +/// MCP islemlerinin talep Timesheets satirina yazilan aciklamalari. Karsiliklari platformun +/// App.Languages.LanguageText.json dosyasinda EN + TR olarak durur; satir Turkce yazilir. +/// +public static class McpTimesheetTexts +{ + private const string Prefix = "App.Mcp.Timesheet."; + + public const string Analysis = Prefix + "Analysis"; + public const string CreateTable = Prefix + "CreateTable"; + public const string DeployWizard = Prefix + "DeployWizard"; + public const string DeleteWizard = Prefix + "DeleteWizard"; + public const string SaveComponent = Prefix + "SaveComponent"; + public const string DeleteComponent = Prefix + "DeleteComponent"; + public const string InsertRow = Prefix + "InsertRow"; + public const string UpdateRow = Prefix + "UpdateRow"; + public const string DeleteRow = Prefix + "DeleteRow"; +} 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 a346bcdc..c6ed174c 100644 --- a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/Tools/McpDevelopTools.cs +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/Tools/McpDevelopTools.cs @@ -64,7 +64,7 @@ public sealed class McpDevelopTools(IMcpDevelopAppService developAppService) => await developAppService.GetWizardAsync(fileName); [McpServerTool(Name = McpToolNames.DeployWizard, Destructive = true, Idempotent = false)] - [Description("Wizard'i deploy eder: ListForm ekrani (ComponentKind 0) ya da bir custom component'i menuye baglayan kayit (ComponentKind 1), yetkileri, dil anahtarlari ve menusu seed dosyalarina yazilir. Deploy veritabanina yazmaz: ekran, menu ve yetkiler kullanici Wizard Manager'da DB Migrate calistirinca olusur; cikistaki isPending true iken ekran henuz yoktur; migrate'i bekleme, isini bitir ve kullaniciya yalnizca DB Migrate calistirmasini soyle. Eksik modul, ust menu, izin grubu, yetki ve dil satirlari da deploy'da dosyalara yazilir ve migrate onlari dogru sirayla ekler; bunlarin veritabaninda olmamasi beklenen durumdur, uyari olarak raporlama ve kullanicidan elle eklemesini isteme. Liste ekraninda Grid, Card, Pivot ve Chart gorunumleri varsayilan aciktir (DefaultLayout grid); kullanici bir gorunumu istemedigini soylemedikce bu bayraklari false yazma. Hedef tablo onceden var olmalidir; yoksa once create_table ile olustur. Istek ornek/demo veri tasiyorsa (prompt'taki tablo/liste ya da ekran goruntusunde gorunen satirlar) SeedData true yaz ve satirlarin hepsini ayni cagrida seedRows ile gonder: satirlar ekranin data/{ListFormCode}.json dosyasina yazilir, migrate ekrani kurduktan sonra ekler. Satirlar icin migrate'i bekleyip insert_row kullanma. replaceFileName verilirse o wizard'in dosyalari yeniden yazilir, onceki kayitlarin yerini migrate'te yenileri alir; ekran wizard cevaplarindan yeniden turetildigi icin tasarimcida yapilmis ve wizard'da karsiligi olmayan ayarlar kaybolur. Ne yazacagini get_dev_guide 4. bolumunden ogren.")] + [Description("Wizard'i deploy eder: ListForm ekrani (ComponentKind 0) ya da bir custom component'i menuye baglayan kayit (ComponentKind 1), yetkileri, dil anahtarlari ve menusu seed dosyalarina yazilir. Deploy veritabanina yazmaz: ekran, menu ve yetkiler kullanici Wizard Manager'da DB Migrate calistirinca olusur; cikistaki isPending true iken ekran henuz yoktur; migrate'i bekleme, isini bitir ve kullaniciya yalnizca DB Migrate calistirmasini soyle. Eksik modul, ust menu, izin grubu, yetki ve dil satirlari da deploy'da dosyalara yazilir ve migrate onlari dogru sirayla ekler; bunlarin veritabaninda olmamasi beklenen durumdur, uyari olarak raporlama ve kullanicidan elle eklemesini isteme. Ust menu zinciri istenen derinlikte kurulur: MenuParentCode ekranin dogrudan ust menusu, Menu.MenuParentPath onun ustundeki seviyeler (kokten asagi, her biri {Code, Icon, En, Tr}); eksik her seviye bir ustunun altina kurulur, var olana baglanilir. Liste ekraninda Grid, Card, Pivot ve Chart gorunumleri varsayilan aciktir (DefaultLayout grid); kullanici bir gorunumu istemedigini soylemedikce bu bayraklari false yazma. Hedef tablo onceden var olmalidir; yoksa once create_table ile olustur. Istek ornek/demo veri tasiyorsa (prompt'taki tablo/liste ya da ekran goruntusunde gorunen satirlar) SeedData true yaz ve satirlarin hepsini ayni cagrida seedRows ile gonder: satirlar ekranin data/{ListFormCode}.json dosyasina yazilir, migrate ekrani kurduktan sonra ekler. Satirlar icin migrate'i bekleyip insert_row kullanma. replaceFileName verilirse o wizard'in dosyalari yeniden yazilir, onceki kayitlarin yerini migrate'te yenileri alir; ekran wizard cevaplarindan yeniden turetildigi icin tasarimcida yapilmis ve wizard'da karsiligi olmayan ayarlar kaybolur. Ne yazacagini get_dev_guide 4. bolumunden ogren.")] public async Task DeployWizardAsync( [Description("Wizard JSON'u: seed dosyasi bicimi ({\"Wizard\": {...}}, get_wizard cikisi gibi) ya da duz wizard cevaplari. Enum alanlari sayi olarak yazilir.")] string wizard, @@ -137,6 +137,7 @@ public sealed class McpDevelopTools(IMcpDevelopAppService developAppService) return "ok"; } + /// /// Talep metni JSON olarak, gorseller ayri resim icerigi olarak doner; model resmi ancak bu /// bicimde gorur, base64 metni olarak goremez. diff --git a/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/ListFormWizardDto.cs b/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/ListFormWizardDto.cs index ce5efc4f..f132c4d5 100644 --- a/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/ListFormWizardDto.cs +++ b/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/ListFormWizardDto.cs @@ -105,6 +105,13 @@ public class ListFormWizardDto public string MenuParentEn { get; set; } public string MenuParentTr { get; set; } + /// + /// Ekranin kullandigi ve baska bir cevaptan uretilmeyen dil anahtarlari (widget basliklari + /// gibi). Deploy, widget sorgularinda gecen her anahtarin var oldugunu ya da burada + /// tanimlandigini dogrular; tanimsiz anahtar ekranda ham kod olarak gorunurdu. + /// + public List LanguageTexts { get; set; } = []; + public string PermissionGroupName { get; set; } /// @@ -120,6 +127,13 @@ public class ListFormWizardDto public string MenuParentModuleId { get; set; } public string MenuParentCode { get; set; } public string MenuParentIcon { get; set; } + + /// + /// 'un ustundeki menu seviyeleri, kokten asagi. Bos ise ust menu + /// kok seviyededir (eski davranis); doluysa derinlik sinirsizdir ve eksik her seviye bir + /// ustunun altina kurulur. Seviyenin iki dilli adi seviyeyle birlikte tasinir. + /// + public List MenuParentPath { get; set; } = []; public string MenuIcon { get; set; } public string DataSourceCode { get; set; } public string DataSourceConnectionString { get; set; } @@ -147,6 +161,31 @@ public class ListFormWizardDto : MenuCode.Trim(); } + /// + /// Ekranin ust menu zinciri, kokten dogrudan ust menuye; kodu bos seviyeler atlanir. Deploy + /// zinciri bu sirayla kurar, dogrulama ayni listeyi kullanir. + /// + public List ResolveMenuParentChain() + { + var chain = (MenuParentPath ?? []) + .Where(l => !string.IsNullOrWhiteSpace(l?.Code)) + .Select(l => new WizardMenuLevelDto { Code = l.Code.Trim(), Icon = l.Icon, En = l.En?.Trim(), Tr = l.Tr?.Trim() }) + .ToList(); + + if (!string.IsNullOrWhiteSpace(MenuParentCode)) + { + chain.Add(new WizardMenuLevelDto + { + Code = MenuParentCode.Trim(), + Icon = MenuParentIcon, + En = MenuParentEn?.Trim(), + Tr = MenuParentTr?.Trim() + }); + } + + return chain; + } + /// Wizard'in ListForm kodu; bos gelirse menu kodu ile aynidir. public string ResolveListFormCode() { diff --git a/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/WizardLanguageTextDto.cs b/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/WizardLanguageTextDto.cs new file mode 100644 index 00000000..8b8c78a1 --- /dev/null +++ b/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/WizardLanguageTextDto.cs @@ -0,0 +1,12 @@ +namespace Sozsoft.Platform.ListForms; + +/// +/// Ekranin wizard cevaplarindan turemeyen bir dil anahtari (widget basligi, sabit secenek...). +/// Anahtar yoksa deploy iki dilli satiri yazar ve wizard'in sahipligine alir. +/// +public class WizardLanguageTextDto +{ + public string Key { get; set; } + public string En { get; set; } + public string Tr { get; set; } +} diff --git a/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/WizardMenuLevelDto.cs b/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/WizardMenuLevelDto.cs new file mode 100644 index 00000000..c5aeadda --- /dev/null +++ b/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/WizardMenuLevelDto.cs @@ -0,0 +1,13 @@ +namespace Sozsoft.Platform.ListForms; + +/// +/// Ekranin ust menu zincirindeki bir seviye. Kod menu kaydinin ve dil anahtarinin kendisidir; +/// seviye yoksa deploy onu bu ad ve ikonla kurar, varsa yalnizca koduyla baglanir. +/// +public class WizardMenuLevelDto +{ + public string Code { get; set; } + public string Icon { get; set; } + public string En { get; set; } + public string Tr { get; set; } +} diff --git a/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/WizardSeedFileDto.cs b/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/WizardSeedFileDto.cs index cdb2ad64..6c6ca929 100644 --- a/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/WizardSeedFileDto.cs +++ b/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/WizardSeedFileDto.cs @@ -107,6 +107,7 @@ public class WizardSeedFileDto /// Dosyada `Menu` basligi altinda toplanan wizard alanlari; adlari degismez. private static readonly (string Property, string SeedKey)[] MenuKeys = [ + Same(nameof(ListFormWizardDto.MenuParentPath)), Same(nameof(ListFormWizardDto.MenuParentCode)), Same(nameof(ListFormWizardDto.MenuParentIcon)), Same(nameof(ListFormWizardDto.CreateMenu)), Same(nameof(ListFormWizardDto.MenuCode)), Same(nameof(ListFormWizardDto.MenuOrder)), Same(nameof(ListFormWizardDto.MenuUrl)), @@ -153,6 +154,7 @@ public class WizardSeedFileDto Same(nameof(ListFormWizardDto.MenuEn)), Same(nameof(ListFormWizardDto.MenuTr)), Same(nameof(ListFormWizardDto.TitleEn)), Same(nameof(ListFormWizardDto.TitleTr)), Same(nameof(ListFormWizardDto.DescEn)), Same(nameof(ListFormWizardDto.DescTr)), + (nameof(ListFormWizardDto.LanguageTexts), "Texts"), ]; /// @@ -233,6 +235,14 @@ public class WizardSeedFileDto if (Wizard?.CreateMenu != false) wizardNode.Remove(nameof(ListFormWizardDto.CreateMenu)); + // Kok seviyedeki ust menu eski ve en yaygin durumdur; bos zincir dosyaya yazilmaz, + // mevcut wizard dosyalari degismeden kalir. + if (Wizard?.MenuParentPath is not { Count: > 0 }) + wizardNode.Remove(nameof(ListFormWizardDto.MenuParentPath)); + + if (Wizard?.LanguageTexts is not { Count: > 0 }) + wizardNode.Remove(nameof(ListFormWizardDto.LanguageTexts)); + foreach (var (property, seedKey) in RenamedKeys) Rename(wizardNode, property, seedKey); diff --git a/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/WizardSeedRowKeys.cs b/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/WizardSeedRowKeys.cs index d6ff0cc1..9eebb315 100644 --- a/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/WizardSeedRowKeys.cs +++ b/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/WizardSeedRowKeys.cs @@ -55,12 +55,17 @@ public static class WizardSeedRowKeys var languageKeys = new List { code, group }; var modules = new List(); - if (wizard.CreateMenu && !string.IsNullOrWhiteSpace(wizard.MenuParentCode)) + if (wizard.CreateMenu) { - menus.Add(wizard.MenuParentCode.Trim()); - languageKeys.Add(wizard.MenuParentCode.Trim()); + var parents = wizard.ResolveMenuParentChain().Select(l => l.Code).ToList(); + menus.AddRange(parents); + languageKeys.AddRange(parents); } + languageKeys.AddRange((wizard.LanguageTexts ?? []) + .Select(t => t.Key?.Trim()) + .Where(k => !string.IsNullOrWhiteSpace(k))!); + if (!string.IsNullOrWhiteSpace(wizard.MenuParentModuleId)) modules.Add(wizard.MenuParentModuleId.Trim()); diff --git a/api/src/Sozsoft.Platform.Application/ListForms/ListFormWizardAppService.cs b/api/src/Sozsoft.Platform.Application/ListForms/ListFormWizardAppService.cs index 8b344ad0..586a660c 100644 --- a/api/src/Sozsoft.Platform.Application/ListForms/ListFormWizardAppService.cs +++ b/api/src/Sozsoft.Platform.Application/ListForms/ListFormWizardAppService.cs @@ -5,6 +5,7 @@ using System.IO.Compression; using System.Linq; using System.Text; using System.Text.Json; +using System.Text.Json.Nodes; using System.Text.RegularExpressions; using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; @@ -122,7 +123,8 @@ public class ListFormWizardAppService( // Ad ve kod catismalari, dosyalar yazilmaya baslamadan once reddedilir; aksi halde yarim // kalan bir deploy mevcut menu/permission satirlarini bozabilir. ValidateWizardName(wizardName); - await ValidateWizardUniquenessAsync(wizardName, code, input.MenuParentCode, editFileName, previous, rows); + await ValidateWizardUniquenessAsync(wizardName, code, input.ResolveMenuParentChain(), editFileName, previous, rows); + await ValidateWidgetLanguageKeysAsync(input, rows); await DeployWizardAsync(input, previous, editFileName, rows); } @@ -162,6 +164,9 @@ public class ListFormWizardAppService( await WriteLanguageRowsAsync(rows, $"{listFormCode}.Desc", input.DescEn, input.DescTr, owned, inserted); } + foreach (var text in input.LanguageTexts ?? []) + await WriteLanguageRowsAsync(rows, text.Key?.Trim(), text.En, text.Tr, owned, inserted); + //Permission Group var groupName = WizardSeedRowKeys.PermissionGroupName(input); input.PermissionGroupName = groupName; @@ -204,9 +209,10 @@ public class ListFormWizardAppService( } // Ekranin sozlesmesi tek fabrikadan uretilir; migrate ekrani bu dosyadan kurar. - var tableColumns = await GetTableColumnNamesAsync(input.DataSourceCode, input.SelectCommandType, input.SelectCommand); - isDeleted = tableColumns.Contains("IsDeleted"); - isCreated = tableColumns.Contains("CreatorId"); + var tableColumns = await GetTableColumnsAsync(input.DataSourceCode, input.SelectCommandType, input.SelectCommand); + isDeleted = tableColumns.ContainsKey("IsDeleted"); + isCreated = tableColumns.ContainsKey("CreatorId"); + ValidateImageColumnLengths(input, tableColumns); listFormDto = input.BuildListForm(isDeleted, isCreated); fieldDtos = input.BuildFields(); @@ -445,10 +451,11 @@ public class ListFormWizardAppService( } /// - /// Modul, ust menu ve ekranin menusu. Menu agaci veritabani ile dosyanin birlesimidir: migrate - /// edilmemis bir wizard'in menusu yalnizca dosyada durur ama sirada yer tutar. Ust menu yoksa kok - /// menulerin sonuna eklenir; ekranin menusu verilen sirayla, sira verilmediyse kardeslerinin - /// sonuna yazilir. + /// Modul, ust menu zinciri ve ekranin menusu. Menu agaci veritabani ile dosyanin birlesimidir: + /// migrate edilmemis bir wizard'in menusu yalnizca dosyada durur ama sirada yer tutar. Zincir + /// ( + ) + /// kokten asagi yurunur: var olan seviye kullanilir, eksik seviye bir ustunun altina, kardeslerinin + /// sonuna eklenir. Ekranin menusu verilen sirayla, sira verilmediyse kardeslerinin sonuna yazilir. /// private async Task WriteMenuRowsAsync( WizardSeedRowStore rows, @@ -469,49 +476,46 @@ public class ListFormWizardAppService( } var dbMenus = await repoMenu.GetListAsync(); - var fileMenus = rows.Rows(WizardSeedRowStore.MenuFile); - var parentCode = input.MenuParentCode?.Trim(); - var parentOwned = owned.MenuCodes.Contains(parentCode ?? string.Empty, StringComparer.OrdinalIgnoreCase); - var parentRow = rows.Find(WizardSeedRowStore.MenuFile, nameof(Menu.Code), parentCode); - var dbParent = dbMenus.FirstOrDefault(m => Same(m.Code, parentCode)); - - string? parentModuleId; - if (parentOwned || (parentRow == null && dbParent == null)) - { - var maxRootOrder = dbMenus.Where(m => string.IsNullOrEmpty(m.ParentCode)).Select(m => m.Order) - .Concat(fileMenus.Where(r => string.IsNullOrEmpty(WizardSeedRowStore.Text(r, nameof(Menu.ParentCode)))) + // Kardeslerin en buyuk sirasi; ParentCode bos ise kok menuler. Dosya satirlari her + // cagrida yeniden okunur, zincirde az once eklenen seviye de sirada yer tutar. + int MaxSiblingOrder(string? parent, string exclude) => + dbMenus.Where(m => Same(m.ParentCode ?? string.Empty, parent ?? string.Empty) && !Same(m.Code, exclude)).Select(m => m.Order) + .Concat(rows.Rows(WizardSeedRowStore.MenuFile) + .Where(r => Same(WizardSeedRowStore.Text(r, nameof(Menu.ParentCode)) ?? string.Empty, parent ?? string.Empty) && + !Same(WizardSeedRowStore.Text(r, nameof(Menu.Code)), exclude)) .Select(r => WizardSeedRowStore.Int(r, nameof(Menu.Order)))) .DefaultIfEmpty(0).Max(); - var menuParentIcon = !string.IsNullOrWhiteSpace(input.MenuParentIcon) - ? input.MenuParentIcon - : !string.IsNullOrWhiteSpace(input.MenuIcon) - ? input.MenuIcon - : WizardConsts.MenuIcon; + var fallbackIcon = !string.IsNullOrWhiteSpace(input.MenuIcon) ? input.MenuIcon : WizardConsts.MenuIcon; + string? parentCode = null; + var parentModuleId = string.IsNullOrWhiteSpace(moduleId) ? null : moduleId; - parentModuleId = string.IsNullOrWhiteSpace(moduleId) ? null : moduleId; - rows.Upsert(WizardSeedRowStore.MenuFile, MenuRow( - parentCode!, null, parentCode!, - parentRow != null ? WizardSeedRowStore.Int(parentRow, nameof(Menu.Order)) : maxRootOrder + 1, - null, menuParentIcon, null, parentModuleId), nameof(Menu.Code)); - inserted.MenuCodes.Add(parentCode!); - - await WriteLanguageRowsAsync(rows, parentCode, input.MenuParentEn, input.MenuParentTr, owned, inserted); - } - else + foreach (var level in input.ResolveMenuParentChain()) { - parentModuleId = dbParent?.ModuleId ?? WizardSeedRowStore.Text(parentRow, nameof(Menu.ModuleId)); + var levelOwned = owned.MenuCodes.Contains(level.Code, StringComparer.OrdinalIgnoreCase); + var levelRow = rows.Find(WizardSeedRowStore.MenuFile, nameof(Menu.Code), level.Code); + var dbLevel = dbMenus.FirstOrDefault(m => Same(m.Code, level.Code)); + + if (levelOwned || (levelRow == null && dbLevel == null)) + { + rows.Upsert(WizardSeedRowStore.MenuFile, MenuRow( + level.Code, parentCode, level.Code, + levelRow != null ? WizardSeedRowStore.Int(levelRow, nameof(Menu.Order)) : MaxSiblingOrder(parentCode, level.Code) + 1, + null, string.IsNullOrWhiteSpace(level.Icon) ? fallbackIcon : level.Icon, null, parentModuleId), nameof(Menu.Code)); + inserted.MenuCodes.Add(level.Code); + + await WriteLanguageRowsAsync(rows, level.Code, level.En, level.Tr, owned, inserted); + } + else + { + parentModuleId = dbLevel?.ModuleId ?? WizardSeedRowStore.Text(levelRow, nameof(Menu.ModuleId)); + } + + parentCode = level.Code; } - var maxChildOrder = dbMenus.Where(m => Same(m.ParentCode, parentCode) && !Same(m.Code, code)).Select(m => m.Order) - .Concat(fileMenus - .Where(r => Same(WizardSeedRowStore.Text(r, nameof(Menu.ParentCode)), parentCode) && - !Same(WizardSeedRowStore.Text(r, nameof(Menu.Code)), code)) - .Select(r => WizardSeedRowStore.Int(r, nameof(Menu.Order)))) - .DefaultIfEmpty(0).Max(); - - input.MenuOrder = input.MenuOrder > 0 ? input.MenuOrder : maxChildOrder + 1; + input.MenuOrder = input.MenuOrder > 0 ? input.MenuOrder : MaxSiblingOrder(parentCode, code) + 1; rows.Upsert(WizardSeedRowStore.MenuFile, MenuRow( code, parentCode, code, input.MenuOrder, input.MenuUrl, input.MenuIcon ?? WizardConsts.MenuIcon, @@ -750,7 +754,7 @@ public class ListFormWizardAppService( if (!menus.TryGetValue(wizard.ResolveMenuCode(), out var menu)) { - return string.IsNullOrWhiteSpace(wizard.MenuParentCode) ? [] : [wizard.MenuParentCode.Trim()]; + return [.. wizard.ResolveMenuParentChain().Select(l => l.Code)]; } var path = new List(); @@ -1088,6 +1092,49 @@ public class ListFormWizardAppService( /// private static readonly Regex WizardNamePattern = new(@"^[A-Za-z][A-Za-z0-9_]*$", RegexOptions.Compiled); + /// Widget sorgusundaki tek tirnakli, noktali tanimlayici: kartin dil anahtari. + private static readonly Regex WidgetLanguageKeyPattern = new(@"'([A-Za-z][A-Za-z0-9_]*(?:\.[A-Za-z0-9_]+)+)'", RegexOptions.Compiled); + + /// + /// Widget kartinin basligi ve alt basligi sorgudan dil anahtari olarak doner; anahtar tanimli + /// degilse ekranda ham kod gorunur. Sorgudaki her anahtar veritabaninda, bekleyen dil + /// satirlarinda ya da bu deploy'un yazdigi anahtarlarda () + /// bulunmali; eksikler dosyalar yazilmadan once adlariyla bildirilir. + /// + private async Task ValidateWidgetLanguageKeysAsync(ListFormWizardDto input, WizardSeedRowStore rows) + { + if (input.ComponentKind == WizardComponentKindEnum.Custom) + return; + + var produced = new HashSet(WizardSeedRowKeys.All(input).LanguageKeys, StringComparer.OrdinalIgnoreCase); + var res = PlatformConsts.AppName; + var missing = new List(); + + var keys = (input.Widgets ?? []) + .SelectMany(w => WidgetLanguageKeyPattern.Matches(w.SqlQuery ?? string.Empty)) + .Select(m => m.Groups[1].Value) + .Distinct(StringComparer.OrdinalIgnoreCase); + + foreach (var key in keys) + { + if (produced.Contains(key) || + rows.Contains(WizardSeedRowStore.LanguageTextFile, nameof(LanguageText.Key), key) || + await repoLangKey.AnyAsync(a => a.ResourceName == res && a.Key == key)) + { + continue; + } + + missing.Add(key); + } + + if (missing.Count > 0) + { + throw new UserFriendlyException( + $"Widget sorgusunda tanımlı olmayan dil anahtarları var: {string.Join(", ", missing)}. " + + "Var olan bir anahtar kullanın ya da anahtarı EN + TR metinleriyle wizard'ın LanguageTexts alanına ekleyin."); + } + } + private static void ValidateWizardName(string wizardName) { if (!WizardNamePattern.IsMatch(wizardName)) @@ -1110,17 +1157,24 @@ public class ListFormWizardAppService( private async Task ValidateWizardUniquenessAsync( string wizardName, string code, - string menuParentCode, + List menuParents, string? editFileName, WizardSeedFileDto? previous, WizardSeedRowStore rows) { - if (string.Equals(code, menuParentCode?.Trim(), StringComparison.OrdinalIgnoreCase)) + if (menuParents.Any(l => string.Equals(code, l.Code, StringComparison.OrdinalIgnoreCase))) { throw new UserFriendlyException( $"Menü kodu ('{code}') üst menü kodu ile aynı olamaz. Farklı bir Wizard adı ya da menü kodu seçin; aksi halde üst menü bu kaydın üzerine yazılır."); } + var repeated = menuParents.GroupBy(l => l.Code, StringComparer.OrdinalIgnoreCase).FirstOrDefault(g => g.Count() > 1); + if (repeated != null) + { + throw new UserFriendlyException( + $"'{repeated.Key}' menü kodu üst menü yolunda birden fazla kez geçiyor. Yoldaki her seviyenin kodu farklı olmalı."); + } + var duplicateFile = (await ReadSeedFilesAsync()) .Where(f => !string.Equals(f.FileName, editFileName, StringComparison.OrdinalIgnoreCase) && string.Equals((f.Seed?.Wizard?.WizardName ?? f.FileName).Trim(), wizardName, StringComparison.OrdinalIgnoreCase)) @@ -2382,7 +2436,70 @@ public class ListFormWizardAppService( return File.Exists(fullPath) ? fullPath : null; } - private async Task> GetTableColumnNamesAsync(string dataSourceCode, SelectCommandTypeEnum commandType, string selectCommand) + /// Gorseli dosya olarak yukleyen platform editoru. + private const string ImageUploadEditorType = "dxImageUpload"; + + /// Gorseli yalnizca gosteren platform editoru; kolona deger yazmaz. + private const string ImageViewerEditorType = "dxImageViewer"; + + /// + /// Gorsel yukleme editoru adres (uploadUrl) verilmemisse dosyayi base64 metin olarak + /// kolona yazar; boyu sinirli bir kolon kayitta sessizce kesilir ya da hata verir. Sinir + /// katalogdan okunur: sinirsiz kolon (SQL Server MAX = -1, PostgreSQL text = bos) kabul edilir. + /// + private static void ValidateImageColumnLengths(ListFormWizardDto input, Dictionary tableColumns) + { + // Kolona bagli gorsel alani yuklenebilir olmali: goruntuleyici ile kurulan alan kayitta + // bos kalir ya da kopyalanan degeri degistirilemez hale getirir. + var viewers = input.Groups + .SelectMany(g => g.Items) + .Where(i => string.Equals(i.EditorType, ImageViewerEditorType, StringComparison.OrdinalIgnoreCase) && + tableColumns.ContainsKey(i.FieldName ?? string.Empty)) + .Select(i => i.FieldName) + .ToList(); + + if (viewers.Count > 0) + { + throw new UserFriendlyException( + $"Tablo kolonuna bağlı görsel alanlarında {ImageViewerEditorType} kullanılamaz: {string.Join(", ", viewers)}. " + + $"Editörü {ImageUploadEditorType} yapın; salt okunur gösterim için EditorOptions readOnly verin."); + } + + var limited = input.Groups + .SelectMany(g => g.Items) + .Where(i => string.Equals(i.EditorType, ImageUploadEditorType, StringComparison.OrdinalIgnoreCase) && + !HasUploadUrl(i.EditorOptions) && + tableColumns.TryGetValue(i.FieldName ?? string.Empty, out var length) && length > 0) + .Select(i => $"{i.FieldName} ({tableColumns[i.FieldName]})") + .ToList(); + + if (limited.Count > 0) + { + throw new UserFriendlyException( + $"Görsel yükleme alanları base64 saklar, kolon boyu sınırsız olmalı: {string.Join(", ", limited)}. " + + "Kolonu nvarchar(MAX) yapın ya da editöre uploadUrl verin."); + } + } + + private static bool HasUploadUrl(string? editorOptions) + { + if (string.IsNullOrWhiteSpace(editorOptions)) + return false; + + try + { + return JsonNode.Parse(editorOptions) is JsonObject options && + options.FirstOrDefault(p => string.Equals(p.Key, "uploadUrl", StringComparison.OrdinalIgnoreCase)).Value is JsonValue url && + !string.IsNullOrWhiteSpace(url.ToString()); + } + catch (JsonException) + { + return false; + } + } + + /// Tablonun kolonlari ve metin kolonlarinin karakter siniri (sinirsizsa -1 ya da bos). + private async Task> GetTableColumnsAsync(string dataSourceCode, SelectCommandTypeEnum commandType, string selectCommand) { if (commandType == SelectCommandTypeEnum.Query || commandType == SelectCommandTypeEnum.StoredProcedure) return []; @@ -2400,8 +2517,8 @@ public class ListFormWizardAppService( // Şema/tablo adları kullanıcıdan geldiği için sorguya parametre olarak bağlanır. var query = dbType == DataSourceTypeEnum.Postgresql - ? "SELECT column_name FROM information_schema.columns WHERE table_schema = @schemaName AND table_name = @tableName" - : "SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = @schemaName AND TABLE_NAME = @tableName"; + ? "SELECT column_name, character_maximum_length FROM information_schema.columns WHERE table_schema = @schemaName AND table_name = @tableName" + : "SELECT COLUMN_NAME, CHARACTER_MAXIMUM_LENGTH FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = @schemaName AND TABLE_NAME = @tableName"; var parameters = new Dictionary { @@ -2411,15 +2528,20 @@ public class ListFormWizardAppService( var rows = await repo.QueryAsync(query, connectionString, parameters); - var columns = new HashSet(StringComparer.OrdinalIgnoreCase); + var columns = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (var row in rows) { if (row is not IDictionary dict) continue; - var key = dict.ContainsKey("COLUMN_NAME") ? "COLUMN_NAME" : "column_name"; - if (dict.TryGetValue(key, out var col) && col != null) - columns.Add(col.ToString()!); + var nameKey = dict.ContainsKey("COLUMN_NAME") ? "COLUMN_NAME" : "column_name"; + var lengthKey = dict.ContainsKey("CHARACTER_MAXIMUM_LENGTH") ? "CHARACTER_MAXIMUM_LENGTH" : "character_maximum_length"; + if (dict.TryGetValue(nameKey, out var col) && col != null) + { + columns[col.ToString()!] = dict.TryGetValue(lengthKey, out var length) && length is not null and not DBNull + ? Convert.ToInt32(length, System.Globalization.CultureInfo.InvariantCulture) + : null; + } } return columns; } diff --git a/claude.md b/claude.md index 7bcb291e..fbd462aa 100644 --- a/claude.md +++ b/claude.md @@ -124,6 +124,8 @@ Bir çelişki varsa sıra: `ai.instructions.md` → `dotnet.instructions.md` → Yeni **tanım** tablosu (tanım olup olmadığına talebin içeriğine göre analizde karar verilir; 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. + Görsel tutan her kolon (fotoğraf, logo, avatar kopyası) `nvarchar(MAX)` + editör `dxImageUpload`; + değer base64 saklanır, sınırlı boy ve `dxImageViewer` kullanılmaz (deploy ikisini de reddeder). 2. **Yeni ekran** → Wizard yolu. `Definitions` altına otomatik yerleştirme yok; modüle ait yeni 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 @@ -260,7 +262,10 @@ Bir çelişki varsa sıra: `ai.instructions.md` → `dotnet.instructions.md` → (`McpDevRequestImages`) ve MCP resim içeriği (`ImageContentBlock`) olarak döndürür; metinde `[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. + sunucu doldurur, modele bırakılmaz. `Timesheets`'i tamamen sunucu yazar + (`IMcpDevRequestTimesheetRecorder`): `get_dev_request`/`start_dev_request` talebi etkin talep yapar, + sonraki her MCP yazma işlemi ona `App.Mcp.Timesheet.*` anahtarından Türkçe açıklama ve ölçülen + süreyle bir satır ekler. Yeni bir MCP yazma aracı eklenirse kaydedici çağrısı ve dil anahtarı da eklenir. - **Talep analizi** → `Description` talebin yazıldığı hâliyle kalır; MCP ona yazmaz. Analiz ve ü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` diff --git a/configs/seeds/host/data/App.Languages.LanguageText.json b/configs/seeds/host/data/App.Languages.LanguageText.json index 262264dc..038d6dd6 100644 --- a/configs/seeds/host/data/App.Languages.LanguageText.json +++ b/configs/seeds/host/data/App.Languages.LanguageText.json @@ -67302,6 +67302,146 @@ "Key": "App.Mcp.Error.DevRequestSubTasksInvalid", "Value": "\"{0}\" geliştirme talebinin alt görevleri okunamadı; önce talep ekranından düzeltin." }, + { + "Id": "642b0ee1-75e7-504c-b1f9-8339f2ebab65", + "CultureName": "en", + "ResourceName": "Platform", + "Key": "App.Mcp.Timesheet.Analysis", + "Value": "Request analyzed" + }, + { + "Id": "ae0344b7-6027-532d-ac6f-2a09cd9b254c", + "CultureName": "tr", + "ResourceName": "Platform", + "Key": "App.Mcp.Timesheet.Analysis", + "Value": "Talep analiz edildi" + }, + { + "Id": "3ed32588-4dcb-5e6a-940a-ca5dc536c25c", + "CultureName": "en", + "ResourceName": "Platform", + "Key": "App.Mcp.Timesheet.CreateTable", + "Value": "Table {0} created" + }, + { + "Id": "386794a2-41f2-5af6-982e-84c9190ba897", + "CultureName": "tr", + "ResourceName": "Platform", + "Key": "App.Mcp.Timesheet.CreateTable", + "Value": "{0} tablosu oluşturuldu" + }, + { + "Id": "c808ea33-665d-5b85-bca9-10b0b4fc407f", + "CultureName": "en", + "ResourceName": "Platform", + "Key": "App.Mcp.Timesheet.DeployWizard", + "Value": "Wizard {0} deployed ({1} demo rows)" + }, + { + "Id": "1d2fc656-2eac-526b-8a24-7b39b41c022c", + "CultureName": "tr", + "ResourceName": "Platform", + "Key": "App.Mcp.Timesheet.DeployWizard", + "Value": "{0} wizard'ı deploy edildi ({1} demo satır)" + }, + { + "Id": "53d696cb-fdf3-5b08-a71c-90ec24c003c3", + "CultureName": "en", + "ResourceName": "Platform", + "Key": "App.Mcp.Timesheet.DeleteWizard", + "Value": "Wizard {0} deleted" + }, + { + "Id": "f133fc5f-94df-5fda-9fbf-a38d4d811d09", + "CultureName": "tr", + "ResourceName": "Platform", + "Key": "App.Mcp.Timesheet.DeleteWizard", + "Value": "{0} wizard'ı silindi" + }, + { + "Id": "b074b30c-ff5d-56d9-b0a6-2514478b9a6b", + "CultureName": "en", + "ResourceName": "Platform", + "Key": "App.Mcp.Timesheet.SaveComponent", + "Value": "Component {0} saved" + }, + { + "Id": "d80d1833-c77e-5aa3-aea9-0a28d87e244f", + "CultureName": "tr", + "ResourceName": "Platform", + "Key": "App.Mcp.Timesheet.SaveComponent", + "Value": "{0} bileşeni kaydedildi" + }, + { + "Id": "20eeb922-261c-5d5c-8b29-33d3b79c8bff", + "CultureName": "en", + "ResourceName": "Platform", + "Key": "App.Mcp.Timesheet.DeleteComponent", + "Value": "Component {0} deleted" + }, + { + "Id": "9d139d39-1f0d-5bd8-b0a3-175e2656c13c", + "CultureName": "tr", + "ResourceName": "Platform", + "Key": "App.Mcp.Timesheet.DeleteComponent", + "Value": "{0} bileşeni silindi" + }, + { + "Id": "11a42b2b-3a91-54ec-92c7-3020e0b3aa14", + "CultureName": "en", + "ResourceName": "Platform", + "Key": "App.Mcp.Timesheet.InsertRow", + "Value": "Row added to {0}" + }, + { + "Id": "b32f690c-6e0f-5eaf-9dcc-b06302840eab", + "CultureName": "tr", + "ResourceName": "Platform", + "Key": "App.Mcp.Timesheet.InsertRow", + "Value": "{0} ekranına satır eklendi" + }, + { + "Id": "a6b8da40-d8c6-5bdf-bdab-95fc73d3425a", + "CultureName": "en", + "ResourceName": "Platform", + "Key": "App.Mcp.Timesheet.UpdateRow", + "Value": "Row updated in {0} ({1})" + }, + { + "Id": "ca726837-6996-5b2c-8bc8-0067021457aa", + "CultureName": "tr", + "ResourceName": "Platform", + "Key": "App.Mcp.Timesheet.UpdateRow", + "Value": "{0} ekranında satır güncellendi ({1})" + }, + { + "Id": "4a797258-7f4b-53fe-814b-bdccec086e1b", + "CultureName": "en", + "ResourceName": "Platform", + "Key": "App.Mcp.Timesheet.DeleteRow", + "Value": "Row deleted from {0}" + }, + { + "Id": "4e294e52-cb49-5c86-be18-a0bb6c1badfd", + "CultureName": "tr", + "ResourceName": "Platform", + "Key": "App.Mcp.Timesheet.DeleteRow", + "Value": "{0} ekranından satır silindi" + }, + { + "Id": "a5e491f7-a85c-572f-bf66-4245cb84fa36", + "CultureName": "en", + "ResourceName": "Platform", + "Key": "App.Widget.ThisMonth", + "Value": "This Month" + }, + { + "Id": "25e0ea1d-a09a-5f8f-8d5b-519b2bab42d5", + "CultureName": "tr", + "ResourceName": "Platform", + "Key": "App.Widget.ThisMonth", + "Value": "Bu Ay" + }, { "Id": "35642c6a-9097-5bcf-8b27-2bc8356f47ef", "CultureName": "en", diff --git a/ui/src/proxy/admin/wizard/models.ts b/ui/src/proxy/admin/wizard/models.ts index f7420e56..7b8651cb 100644 --- a/ui/src/proxy/admin/wizard/models.ts +++ b/ui/src/proxy/admin/wizard/models.ts @@ -103,6 +103,14 @@ export interface ListFormWizardDto { menuParentCode: string menuParentModuleId?: string menuParentIcon?: string + /** + * menuParentCode'un ustundeki seviyeler, kokten asagi. MCP deploy'u eksik seviyeleri buradan + * kurar; ekrandaki wizard menuleri diyalogla olusturdugu icin bos gonderir ve ust menu + * degismedikce gelen degeri korur. + */ + menuParentPath?: WizardMenuLevelDto[] + /** Baska bir cevaptan uretilmeyen dil anahtarlari (widget basliklari); duzenlemede korunur. */ + languageTexts?: WizardLanguageTextDto[] menuIcon: string dataSourceCode: string dataSourceConnectionString: string @@ -408,3 +416,16 @@ export interface WizardExportAnalysisDto { /** Bagimlilik olarak bulunan ama diskte karsiligi olmayan dosyalar. */ warnings: string[] } + +export interface WizardLanguageTextDto { + key: string + en?: string + tr?: string +} + +export interface WizardMenuLevelDto { + code: string + icon?: string + en?: string + tr?: string +} diff --git a/ui/src/views/admin/listForm/wizard/Wizard.tsx b/ui/src/views/admin/listForm/wizard/Wizard.tsx index 7bbab818..447f8ab0 100644 --- a/ui/src/views/admin/listForm/wizard/Wizard.tsx +++ b/ui/src/views/admin/listForm/wizard/Wizard.tsx @@ -170,6 +170,8 @@ const initialValues: ListFormWizardDto = { menuParentCode: '', menuParentModuleId: '', menuParentIcon: '', + menuParentPath: [], + languageTexts: [], menuIcon: '', dataSourceCode: '', dataSourceConnectionString: '', @@ -740,6 +742,8 @@ const Wizard = () => { menuParentCode: w.menuParentCode ?? '', menuParentModuleId: w.menuParentModuleId ?? '', menuParentIcon: w.menuParentIcon ?? '', + menuParentPath: w.menuParentPath ?? [], + languageTexts: w.languageTexts ?? [], menuIcon: w.menuIcon ?? '', dataSourceCode: w.dataSourceCode ?? '', dataSourceConnectionString: w.dataSourceConnectionString ?? '', @@ -948,6 +952,7 @@ const Wizard = () => { formik?.setFieldValue('menuParentIcon', '') formik?.setFieldValue('menuParentEn', '') formik?.setFieldValue('menuParentTr', '') + formik?.setFieldValue('menuParentPath', []) if (derivedFromParent) handlePermissionGroupChange('') } @@ -966,6 +971,7 @@ const Wizard = () => { formik?.setFieldValue('menuParentIcon', '') formik?.setFieldValue('menuParentEn', '') formik?.setFieldValue('menuParentTr', '') + formik?.setFieldValue('menuParentPath', []) } } @@ -982,6 +988,7 @@ const Wizard = () => { formik?.setFieldValue('menuParentIcon', '') formik?.setFieldValue('menuParentEn', '') formik?.setFieldValue('menuParentTr', '') + formik?.setFieldValue('menuParentPath', []) formik?.setFieldValue('menuIcon', '') formik?.setFieldValue('menuOrder', 0) } @@ -990,6 +997,7 @@ const Wizard = () => { const formik = formikRef.current const parentChanged = formik?.values.menuParentCode !== code formik?.setFieldValue('menuParentCode', code) + if (parentChanged) formik?.setFieldValue('menuParentPath', []) if (parentChanged || !formik?.values.menuOrder || formik.values.menuOrder < 1) { const nextMenuOrder = rawMenuItems @@ -1020,6 +1028,7 @@ const Wizard = () => { moduleId?: string }) => { formikRef.current?.setFieldValue('menuParentCode', menu.code) + formikRef.current?.setFieldValue('menuParentPath', []) const nextMenuOrder = rawMenuItems .filter((item) => (item.parentCode ?? '') === menu.code) diff --git a/ui/src/views/admin/listForm/wizard/WizardStepDeploy.tsx b/ui/src/views/admin/listForm/wizard/WizardStepDeploy.tsx index 3a5b34e1..71267749 100644 --- a/ui/src/views/admin/listForm/wizard/WizardStepDeploy.tsx +++ b/ui/src/views/admin/listForm/wizard/WizardStepDeploy.tsx @@ -317,7 +317,12 @@ const WizardStepDeploy = ({
- + l.code), values.menuParentCode] + .filter(Boolean) + .join(' › ')} + />