diff --git a/.github/instructions/ai.instructions.md b/.github/instructions/ai.instructions.md index 759c3795..48673659 100644 --- a/.github/instructions/ai.instructions.md +++ b/.github/instructions/ai.instructions.md @@ -910,6 +910,11 @@ Exporting a wizard produces a zip whose paths are relative to the scope folder The closure is **recursive** (`WizardExportCollector`): a sub-form's wizard file is queued and its own dependencies are collected under the same rules, so the archive stands on its own. +A whole development request exports the same way: every wizard file whose `DevRequest` block +(written by MCP `deploy_wizard` from the active request) points at the request goes into one +closure (`export-dev-request`), shared files once with merged rows. Wizards added by hand carry +no `DevRequest` and are exported one by one. + - `wizard/{file}.json` — the screen itself plus the wizard files of the sub-forms it references through `SubFormsDto[].Code`, recursively - `data/App.DeveloperKit.CustomComponents/{Id}.json` — `Custom` path; the file is filtered down to the diff --git a/.github/instructions/lowcode.instructions.md b/.github/instructions/lowcode.instructions.md index 8b45218e..b2c57228 100644 --- a/.github/instructions/lowcode.instructions.md +++ b/.github/instructions/lowcode.instructions.md @@ -506,7 +506,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); 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 | +| `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. Talep bağı (`DevRequest`) da yazılmaz — sunucu etkin talepten doldurur (§4.0). İ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 | @@ -608,7 +608,8 @@ talebin metnini döndürür. Başlık birden fazla talebe uyuyorsa Id ister. `St `deploy_wizard`, `delete_wizard`, `save_component`, `delete_component`, `insert_row` / `update_row` / `delete_row`, `save_dev_request_analysis`, `append_dev_request_note`, `add_dev_request_subtasks`) o talebe bir satır ekler - (`IMcpDevRequestTimesheetRecorder`). Açıklama `App.Mcp.Timesheet.*` dil anahtarından **Türkçe** + (`IMcpDevRequestTimesheetRecorder`). Etkin talep `deploy_wizard`'ın yazdığı wizard dosyasına da + `DevRequest` olarak girer (§4.0); Wizard Manager talebi gösterir ve talebin tamamını export eder. 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. @@ -905,7 +906,7 @@ hiçbir şey silmez**. Tek çıktısı seed dosyalarıdır: | Dosya | İçerik | | --- | --- | -| `wizard/{WizardName}.json` | Ekranın `ListForm`/`Fields` sözleşmesi, wizard cevapları, `InsertedRecords`, `Pending` | +| `wizard/{WizardName}.json` | Ekranın `ListForm`/`Fields` sözleşmesi, wizard cevapları, `InsertedRecords`, `Pending`, `DevRequest` | | `data/App.Menus.Module.json`, `data/App.Menus.Menu.json` | Modül, üst menü, ekranın menüsü | | `data/Abp.Identity.PermissionGroups.json`, `data/Abp.Identity.Permissions.json` | İzin grubu, okuma yetkisi ve alt yetkileri | | `data/App.Languages.LanguageText.json` | Menü, başlık, açıklama, grup, alan başlıkları ve `Language.Texts` anahtarlarının (widget başlıkları) EN + TR metinleri (`Id` iş anahtarından v5 GUID, `SeedRowIds.LanguageText`) | @@ -938,6 +939,13 @@ ekranı da güncel hâline getirir. Kaldırılan yetkinin rol atamaları yalnız gidiyorsa silinir. `WizardSeedSynchronizer` bekleyen dosyaya yazmaz — dosya ekranın bir sonraki sürümünü taşır. +**Talep bağı (`DevRequest`).** MCP `deploy_wizard` dosyaya kullanıcının etkin talebini +(`get_dev_request` / `start_dev_request` ile açılan, `IMcpDevRequestTimesheetRecorder.GetActiveRequestIdAsync`) +`{ "Id", "Title" }` olarak yazar. Bağ modelin girdisinden gelmez: `ListFormWizardDto.DevRequestId` +`[JsonIgnore]`'dur, yalnızca sunucu içinden doldurulur. Aynı wizard'ı sonra başka bir talep deploy +ederse bağ o talebe geçer; Wizard Manager'dan elle yapılan kayıt önceki bağı korur, hiç talep +yoksa bölüm dosyaya yazılmaz. Migrate, senkronizasyon ve import bölümü olduğu gibi taşır. + ### 4.1 Dosya iskeleti ```jsonc @@ -961,7 +969,8 @@ sürümünü taşır. "InsertedRecords": { // bu çalıştırmanın GERÇEKTEN yarattığı kayıtlar "LanguageKeys": [], "PermissionGroupNames": [], "PermissionNames": [], "MenuCodes": [], "DataSourceCodes": [], "ModuleIds": [] - } + }, + "DevRequest": { "Id": "…", "Title": "…" } // yalnızca MCP talebiyle deploy edildiyse } ``` @@ -2988,7 +2997,8 @@ ekran kodudur — bileşen ekranı yeniden kurmaz, gömer. ### 7.7 Taşıma Bir ekranı başka ortama taşımak tek dosya kopyalamak değildir. Wizard File Manager'ın **Export** -butonu, ekranın **özyinelemeli bağımlılık kapanışını** (`WizardExportCollector`) zip'e koyar: +butonu, ekranın **özyinelemeli bağımlılık kapanışını** (`WizardExportCollector`) zip'e koyar. Talep +hücresindeki export düğmesi aynısını bir talebin bütün wizard'ları için tek arşivde yapar (§9.8): ``` wizard/{dosya}.json (ekranın kendisi + SubFormsDto[].Code ile bağlı @@ -3896,9 +3906,17 @@ Kurallar: bir ekran açma. Wizard Manager'ın kendi aksiyonları: liste/kart görünümü, arama, düzenleme (`EditFileName`), -silme, DB Migrate, Export (zip), Import (diff + rollback). Yetkiler +silme, DB Migrate, Export (zip), talep export'u, Import (diff + rollback). Yetkiler `App.Listforms.Wizard{,.Create,.Update,.Delete,.Export,.Import}`. +Her wizard'ı üreten talep listede görünür (`WizardFileInfoDto.DevRequestId/Title/Url`; başlık +talepten güncel okunur). Başlık talebin kaydını açar (`/admin/form/App.DevRequests/{Id}`); talep bu +ortamda yoksa (import edilmiş dosya) dosyadaki başlık bağlantısız gösterilir, elle eklenen wizard'da +hücre boştur. Yanındaki düğme talebin bütün wizard'larını tek arşivde indirir +(`api/app/list-form-wizard/export-dev-request{,-analysis}`, `.Export` yetkisi): kapanış tek +`WizardExportCollector` çağrısında kurulur, paylaşılan dosyalar arşive bir kez ve birleşik satırlarla +girer. Import çok wizard'lı arşivi zaten tek oturumda uygular. + Listede her ekranın üst menü zinciri (`WizardFileInfoDto.MenuPath`, Menu tablosundan; kayıt yoksa 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 — diff --git a/README.md b/README.md index dfc6fbfc..761c9015 100644 --- a/README.md +++ b/README.md @@ -525,7 +525,8 @@ detayı aynı veri yapısını konuşur ve tasarımcıdaki değişiklikler wizar yolu, alan başlıklarının dil karşılıkları) taşır; `ListForm`/`Fields` içinde karşılığı olan alanlar dosyaya yazılmaz, okuma sırasında geri doldurulur. Kalanlar dosyada `Menu`, `Language` ve `Groups` başlıkları altında toplanır (`WizardSeedFileDto.ToSeedJson` / `FromSeedJson`). Dosya adı `{Ad}.json`'dır (zaman damgası yok); -uygulama sırası `Wizard.SeededAt` damgasından gelir. +uygulama sırası `Wizard.SeededAt` damgasından gelir. MCP `deploy_wizard` dosyaya etkin talebi +`DevRequest` (`Id`, `Title`) olarak yazar; elle yapılan kayıt bu bağı korur, talepsiz wizard'da bölüm yoktur. **İki yol: `ComponentKind`.** `ListFormWizardDto.ComponentKind` sihirbazın yol ayrımıdır ve seed dosyasında ilk alandır. Varsayılan `List` olduğu için bu alanı taşımayan eski seed dosyaları @@ -584,6 +585,9 @@ aynı klasör düzenine doğrudan açılabilir. Toplama **özyinelemelidir** (`WizardExportCollector`): alt formların wizard dosyaları kuyruğa eklenir ve onların bağımlılıkları da aynı kurallarla toplanır. Böylece zip tek başına çalışır. +**Talep export'u** (`api/app/list-form-wizard/export-dev-request{,-analysis}`) bir geliştirme +talebinin `DevRequest` bağı taşıyan bütün wizard'larını aynı toplayıcıda tek kapanışa koyar; +paylaşılan dosyalar arşive bir kez, birleşik satırlarla girer ve import arşivi tek oturumda uygular. | Yol | İçerik | | --- | --- | @@ -750,7 +754,7 @@ Teknik kullanıcılar için `/admin/developerkit` altında toplanan araçlar: | **Custom Component** | `/admin/list/App.DeveloperKit.CustomComponents` | React bileşenini veritabanında saklama; `@babel/standalone` ile tarayıcıda derleyip route'a bağlama. Liste bir ListForm ekranıdır (KPI şeridi, ızgara/kart görünümü, grid'in kendi ekleme/düzenleme formu); satırdaki **Design** butonu `/admin/developerkit/components/design/{id}` tasarımcısını, **Open URL** butonu bileşenin `RoutePath`'ini açar. Kayıtlar `configs/seeds/host/data/App.DeveloperKit.CustomComponents/{Id}.json` klasörüne senkronlanır. Yetkiler `App.DeveloperKit.CustomComponents{,.Create,.Update,.Delete,.Export,.Import}`. | | **Visual Designer** | `/admin/developerkit/components/design/:id` | Custom Component listesindeki **Design** butonundan açılır; sürükle-bırak kanvas ile bileşen üretimi ve kod üretimi (`visualDesigner/codeGenerator.ts`). | | **ListForm** | `/admin/list/App.Listforms.Listform` | Var olan ekranların ham tanım listesi; buradan `/admin/listform/edit/{kod}` sekmeli editörüne geçilir. | -| **Wizard Manager** | `/admin/listform/wizardManager` | Üretilmiş wizard seed dosyalarının listesi/kartı, düzenleme, silme, DB migrate, export/import. Yetki `App.Listforms.Wizard{,.Create,.Update,.Delete,.Export,.Import}`. | +| **Wizard Manager** | `/admin/listform/wizardManager` | Üretilmiş wizard seed dosyalarının listesi/kartı, düzenleme, silme, DB migrate, export/import. Wizard'ı üreten talep listede görünür ve kaydına bağlanır; talebin bütün wizard'ları tek arşivde export edilir (`export-dev-request`). Yetki `App.Listforms.Wizard{,.Create,.Update,.Delete,.Export,.Import}`. | > Bu ekranların artefakt şemaları, enum değerleri ve çalışan örnekleri > [`.github/instructions/lowcode.instructions.md`](.github/instructions/lowcode.instructions.md) @@ -1446,7 +1450,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). Widget sorgularındaki her dil anahtarı tanımlı olmalı ya da wizard'ın `Language.Texts` alanında EN + TR verilmeli, yoksa deploy adlarıyla reddeder (`ValidateWidgetLanguageKeysAsync`). `uploadUrl`'süz `dxImageUpload` alanı base64 sakladığı için kolonu sınırlı boyluysa (katalogdaki karakter sınırı) deploy reddedilir; tablo kolonuna bağlı `dxImageViewer` alanı da reddedilir — görsel kolonunun editörü her zaman `dxImageUpload`'dır (`ValidateImageColumnLengths`). `MenuOrder` yok sayılır: yeni ekran kardeşlerinin sonuna, güncelleme mevcut yerine yazılır — lookup kaynağı ekran önce deploy edildiği için onu kullanan ekranın üstünde kalır. | +| `deploy_wizard` | Wizard'ı Wizard ekranıyla aynı yoldan (`IListFormWizardAppService.Create`) deploy eder. Girdi seed dosyası biçimi (`{"Wizard": …}`) ya da düz cevaplardır; ikisi de `WizardSeedFileDto.FromSeedJson`'dan geçer. Dosyaya kullanıcının etkin talebi `DevRequest` olarak yazılır (sunucu doldurur). Wizard ekranı gibi yalnızca seed dosyalarını yazar; ekran DB Migrate ile kurulur ve çıkıştaki `isPending` o zamana kadar `true` kalır (model migrate'i beklemez; `verify_wizard` bekleyen dosyada `install` kontrolünde durur). `replaceFileName` doluysa güncellemedir (dosyalar yeniden yazılır, önceki kayıtları migrate kaldırır). İstek demo veri taşıyorsa (prompt'taki satırlar, ekran görüntüsü) wizard `SeedData: true` ile deploy edilir — ekranın `SeedFilePath`'i `data/{ListFormCode}.json` olur ve `SeedSync*` bayrakları açılır — satırlar aynı çağrıda `seedRows` ile bu dosyaya yazılır (alan adları doğrulanır, `Guid` anahtarı sunucu üretir); migrate ekranı kurduktan sonra satırları ekler (`lowcode.instructions.md` §0.10.1). Widget sorgularındaki her dil anahtarı tanımlı olmalı ya da wizard'ın `Language.Texts` alanında EN + TR verilmeli, yoksa deploy adlarıyla reddeder (`ValidateWidgetLanguageKeysAsync`). `uploadUrl`'süz `dxImageUpload` alanı base64 sakladığı için kolonu sınırlı boyluysa (katalogdaki karakter sınırı) deploy reddedilir; tablo kolonuna bağlı `dxImageViewer` alanı da reddedilir — görsel kolonunun editörü her zaman `dxImageUpload`'dır (`ValidateImageColumnLengths`). `MenuOrder` yok sayılır: yeni ekran kardeşlerinin sonuna, güncelleme mevcut yerine yazılır — lookup kaynağı ekran önce deploy edildiği için onu kullanan ekranın üstünde kalır. | | `delete_wizard` | Wizard Manager'daki silmeyle aynı: `InsertedRecords` + ekranın veri aynası, bağımlı wizard varsa reddedilir. `dropTables: true` ekranın tablosunu (yalnızca tasarımcı/`create_table` ile kurulmuş ve başka yerde kullanılmayan) CRUD endpoint'leri, `sql/object` ve `crud` dosyalarıyla düşürür; model bunu kullanıcı onayı olmadan göndermez (`WizardTableCleaner`, `lowcode.instructions.md` §4.5). | | `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. | diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpDevRequestTimesheetRecorder.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpDevRequestTimesheetRecorder.cs index f1a9e032..77ebeb33 100644 --- a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpDevRequestTimesheetRecorder.cs +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpDevRequestTimesheetRecorder.cs @@ -30,6 +30,9 @@ public interface IMcpDevRequestTimesheetRecorder /// Talebi kullanicinin etkin talebi yapar ve olcumu baslatir. Task BeginAsync(Guid devRequestId); + /// Kullanicinin etkin talebi; talep acilmadiysa ya da suresi dolduysa bos. + Task GetActiveRequestIdAsync(); + /// /// Etkin talebe bir satir yazar; etkin talep yoksa (islem bir talep dongusunun parcasi /// degilse) hicbir sey yapmaz. @@ -61,6 +64,8 @@ public class McpDevRequestTimesheetRecorder( public Task BeginAsync(Guid devRequestId) => StampAsync(devRequestId, clock.Now); + public async Task GetActiveRequestIdAsync() => (await activityCache.GetAsync(ActivityKey()))?.RequestId; + public async Task RecordAsync(string descriptionKey, params object[] args) { var activity = await activityCache.GetAsync(ActivityKey()); diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpDevelopAppService.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpDevelopAppService.cs index bf66953b..a0945800 100644 --- a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpDevelopAppService.cs +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpDevelopAppService.cs @@ -178,6 +178,10 @@ public class McpDevelopAppService( // Satirlar deploy'dan once dogrulanir; gecersiz girdi yarim bir deploy birakmaz. var seedRows = ParseSeedRows(input.SeedRows, wizard); + // Wizard dosyasi onu ureten talebi tasir; Wizard Manager talebi gosterir ve talebin + // butun wizard'larini birlikte export eder. Bag modelin girdisinden degil etkin talepten gelir. + wizard.DevRequestId = await timesheetRecorder.GetActiveRequestIdAsync(); + await wizardAppService.Create(wizard); var files = await wizardAppService.GetFiles(); diff --git a/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/IListFormWizardAppService.cs b/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/IListFormWizardAppService.cs index 289178db..2ed76b64 100644 --- a/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/IListFormWizardAppService.cs +++ b/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/IListFormWizardAppService.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Threading.Tasks; using Volo.Abp.Content; @@ -26,6 +27,12 @@ public interface IListFormWizardAppService /// Task ExportAnalysis(string fileName); + /// Bir gelistirme talebinin urettigi butun wizard'lari ortak kapanisla tek zip olarak dondurur. + Task ExportDevRequest(Guid devRequestId); + + /// Talep export'unun onizlemesi; ile ayni sozlesme. + Task ExportDevRequestAnalysis(Guid devRequestId); + /// /// Export zip'ini staging klasorune acar ve her dosya icin hedefteki karsiligiyla /// karsilastirma sonucunu dondurur. Bu adim hicbir hedef dosyaya dokunmaz. 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 f132c4d5..945dd083 100644 --- a/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/ListFormWizardDto.cs +++ b/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/ListFormWizardDto.cs @@ -39,6 +39,13 @@ public class ListFormWizardDto public string WizardName { get; set; } + /// + /// Deploy'u yapan gelistirme talebi. Yalnizca sunucu ici girdidir: MCP etkin talebi buraya + /// yazar, HTTP istegi dolduramaz; dosyaya olarak yazilir. + /// + [JsonIgnore] + public Guid? DevRequestId { get; set; } + /// /// Wizard'in ilk deploy edildigi an. Dosya adi tarih tasimadigi icin seeder dosyalari bu /// damgaya gore siralar; birbirine bagli wizard'lar (once Customers, sonra Tasks) uretildikleri diff --git a/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/WizardExportDtos.cs b/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/WizardExportDtos.cs index 6aeae15d..06a10b00 100644 --- a/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/WizardExportDtos.cs +++ b/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/WizardExportDtos.cs @@ -21,6 +21,9 @@ public class WizardExportAnalysisDto /// Kaynak wizard seed dosyasinin adi. public string FileName { get; set; } + /// Arsive giren wizard dosyalari; talep export'unda talebin butun wizard'lari. + public List WizardFiles { get; set; } = []; + /// Indirilecek arsivin adi. public string ArchiveName { 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 6c6ca929..f8ce149c 100644 --- a/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/WizardSeedFileDto.cs +++ b/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/WizardSeedFileDto.cs @@ -46,6 +46,13 @@ public class WizardSeedFileDto /// public WizardPendingDto Pending { get; set; } + /// + /// Wizard'i MCP uzerinden son deploy eden gelistirme talebi (App.DevRequests). Wizard + /// Manager'dan elle yapilan kayit bagi degistirmez; hic talep yoksa bolum dosyada yer almaz. + /// Baslik deploy anindaki hâlidir; talep bu ortamda yoksa (import edilmis dosya) liste onu gosterir. + /// + public WizardDevRequestDto DevRequest { get; set; } + /// /// Dosyaya yazmadan once ortama ozgu alanlari temizler. Kayit kimlikleri ve audit damgalari /// veritabanina aittir; dosyada tasinirlarsa her ortamda anlamsiz fark uretirler. EditType ise @@ -219,8 +226,14 @@ public class WizardSeedFileDto // Bekleyen kurulum yoksa bolum dosyada hic yer almaz; kurulu bir wizard'in dosyasi // migrate sonrasinda deploy oncesiyle birebir ayni kalir. - if (Pending == null && node is JsonObject root) - root.Remove(nameof(Pending)); + if (node is JsonObject root) + { + if (Pending == null) + root.Remove(nameof(Pending)); + + if (DevRequest == null) + root.Remove(nameof(DevRequest)); + } if (node?[nameof(Wizard)] is JsonObject wizardNode) { @@ -438,6 +451,13 @@ public class WizardPendingDto public List ListFormCodes { get; set; } = []; } +/// Wizard'i ureten gelistirme talebinin dosyadaki izi. +public class WizardDevRequestDto +{ + public Guid Id { get; set; } + public string Title { get; set; } +} + /// Kaydi kullanan (silinmesine engel olan) taraf. public class WizardRecordRefDto { @@ -549,4 +569,13 @@ public class WizardFileInfoDto /// yolunda component bagimliliklari. Yalnizca kapsamda wizard dosyasi olanlar listelenir. /// public List Dependencies { get; set; } = []; + + /// Wizard'i ureten gelistirme talebi; elle eklenen wizard'da bostur. + public Guid? DevRequestId { get; set; } + + /// Talebin guncel basligi; talep bu ortamda yoksa dosyadaki baslik. + public string DevRequestTitle { get; set; } + + /// Talebin kayit sayfasi; talep bu ortamda yoksa bostur ve liste baglanti vermez. + public string DevRequestUrl { get; set; } } diff --git a/api/src/Sozsoft.Platform.Application/ListForms/ListFormWizardAppService.cs b/api/src/Sozsoft.Platform.Application/ListForms/ListFormWizardAppService.cs index 1cefe7f9..9a8af58c 100644 --- a/api/src/Sozsoft.Platform.Application/ListForms/ListFormWizardAppService.cs +++ b/api/src/Sozsoft.Platform.Application/ListForms/ListFormWizardAppService.cs @@ -49,6 +49,7 @@ public class ListFormWizardAppService( IRepository repoPermGroup, IRepository repoMenu, IRepository repoModule, + IRepository repoDevRequest, IConfiguration configuration, LanguageTextAppService languageTextAppService, IDynamicDataManager dynamicDataManager, @@ -68,6 +69,7 @@ public class ListFormWizardAppService( private readonly IRepository repoPermGroup = repoPermGroup; private readonly IRepository repoMenu = repoMenu; private readonly IRepository repoModule = repoModule; + private readonly IRepository repoDevRequest = repoDevRequest; private readonly IConfiguration _configuration = configuration; private readonly LanguageTextAppService _languageTextAppService = languageTextAppService; private readonly IDynamicDataManager _dynamicDataManager = dynamicDataManager; @@ -126,7 +128,25 @@ public class ListFormWizardAppService( await ValidateWizardUniquenessAsync(wizardName, code, input.ResolveMenuParentChain(), editFileName, previous, rows); await ValidateWidgetLanguageKeysAsync(input, rows); - await DeployWizardAsync(input, previous, editFileName, rows); + var devRequest = await ResolveDevRequestAsync(input.DevRequestId, previous); + + await DeployWizardAsync(input, previous, editFileName, rows, devRequest); + } + + /// + /// Dosyaya yazilacak talep bagi. MCP etkin talebi verdiyse o talep yazilir; ayni wizard'i + /// sonradan baska bir talep degistirirse bag o talebe gecer ve talep export'u degisikligi tasir. + /// Elle yapilan kayit onceki bagi korur. + /// + private async Task ResolveDevRequestAsync(Guid? devRequestId, WizardSeedFileDto? previous) + { + if (devRequestId is not { } id) + return previous?.DevRequest; + + var devRequest = await repoDevRequest.FindAsync(id); + return devRequest == null + ? previous?.DevRequest + : new WizardDevRequestDto { Id = devRequest.Id, Title = devRequest.Title }; } /// @@ -141,7 +161,7 @@ public class ListFormWizardAppService( /// dosyalardan cikarilir; veritabanindaki karsiliklarini migrate kaldirir. /// /// - private async Task DeployWizardAsync(ListFormWizardDto input, WizardSeedFileDto? previous, string? editFileName, WizardSeedRowStore rows) + private async Task DeployWizardAsync(ListFormWizardDto input, WizardSeedFileDto? previous, string? editFileName, WizardSeedRowStore rows, WizardDevRequestDto? devRequest) { var code = input.ResolveMenuCode(); var listFormCode = input.ResolveListFormCode(); @@ -224,7 +244,7 @@ public class ListFormWizardAppService( await rows.SaveAsync(); var pending = BuildPending(previous, listFormCode); - await SaveWizardSeedFileAsync(input, listFormDto, fieldDtos, isDeleted, isCreated, inserted, pending); + await SaveWizardSeedFileAsync(input, listFormDto, fieldDtos, isDeleted, isCreated, inserted, pending, devRequest); // Ad degisen duzenlemede eski dosya kalmaz; migrate onun kayitlarini yeni dosyanin // bekleyen kurulumundan (Replaced) bilir. @@ -594,7 +614,8 @@ public class ListFormWizardAppService( bool isDeletedField, bool isCreatedField, WizardInsertedRecordsDto inserted, - WizardPendingDto pending) + WizardPendingDto pending, + WizardDevRequestDto? devRequest) { var outputPath = ResolveWizardSeedOutputPath(); Directory.CreateDirectory(outputPath); @@ -612,6 +633,7 @@ public class ListFormWizardAppService( IsCreatedField = isCreatedField, InsertedRecords = inserted, Pending = pending, + DevRequest = devRequest!, }; var filePath = Path.Combine(outputPath, ResolveWizardSeedFileName(input.WizardName)); @@ -717,9 +739,18 @@ public class ListFormWizardAppService( [.. wizards.Where(w => w.ComponentKind != WizardComponentKindEnum.Custom).Select(w => w.ResolveListFormCode())], [.. wizards.Where(w => w.ComponentKind == WizardComponentKindEnum.Custom).Select(w => w.ResolveMenuCode())]); + // Baslik talepten guncel okunur; talep bu ortamda yoksa (import edilmis dosya) dosyadaki + // baslik gosterilir ama baglanti verilmez. + var devRequestIds = seeds.Values.Where(x => x.DevRequest != null).Select(x => x.DevRequest.Id).Distinct().ToList(); + var devRequestTitles = devRequestIds.Count == 0 + ? [] + : (await repoDevRequest.GetListAsync(x => devRequestIds.Contains(x.Id))).ToDictionary(x => x.Id, x => x.Title); + foreach (var (fileName, seed) in files) { var wizard = seed?.Wizard; + var devRequest = seed?.DevRequest; + var devRequestExists = devRequest != null && devRequestTitles.ContainsKey(devRequest.Id); result.Add(new WizardFileInfoDto { FileName = fileName, @@ -734,6 +765,9 @@ public class ListFormWizardAppService( MenuPath = wizard != null ? ResolveMenuPath(wizard, menus) : [], HasInsertedRecords = seed?.InsertedRecords?.HasAny == true, IsPending = wizard != null && (seed!.Pending != null || !installed.Contains(InstallationKey(wizard))), + DevRequestId = devRequest?.Id, + DevRequestTitle = devRequestExists ? devRequestTitles[devRequest!.Id] : devRequest?.Title ?? string.Empty, + DevRequestUrl = devRequestExists ? $"/admin/form/{AppCodes.DevRequests}/{devRequest!.Id}" : string.Empty, }); } @@ -1273,6 +1307,11 @@ public class ListFormWizardAppService( foreach (var warning in warnings) logger.LogWarning("Wizard export dependency missing for {FileName}: {Warning}", fileName, warning); + return await WriteExportArchiveAsync(items, archiveName); + } + + private static async Task WriteExportArchiveAsync(IReadOnlyList items, string archiveName) + { var memory = new MemoryStream(); using (var archive = new ZipArchive(memory, ZipArchiveMode.Create, leaveOpen: true)) { @@ -1309,9 +1348,46 @@ public class ListFormWizardAppService( { var (items, warnings, archiveName) = await BuildExportPlanAsync(fileName); + return ToExportAnalysis(fileName, [fileName], items, warnings, archiveName); + } + + /// + /// Talebin urettigi butun wizard'lari tek arsivde dondurur. Kapanis tek toplayicida kurulur; + /// wizard'larin paylastigi dosyalar (ortak lookup, menu/yetki/dil satirlari) arsive bir kez, + /// hepsinin satirlarini birlestirerek girer. Import cok wizard'li arsivi zaten kabul eder. + /// + [Authorize(AppCodes.Listforms.Wizards.Export)] + [HttpGet("api/app/list-form-wizard/export-dev-request")] + public async Task ExportDevRequest(Guid devRequestId) + { + var (items, warnings, archiveName, _) = await BuildDevRequestExportPlanAsync(devRequestId); + + foreach (var warning in warnings) + logger.LogWarning("Wizard export dependency missing for dev request {DevRequestId}: {Warning}", devRequestId, warning); + + return await WriteExportArchiveAsync(items, archiveName); + } + + [Authorize(AppCodes.Listforms.Wizards.Export)] + [HttpGet("api/app/list-form-wizard/export-dev-request-analysis")] + public async Task ExportDevRequestAnalysis(Guid devRequestId) + { + var (items, warnings, archiveName, fileNames) = await BuildDevRequestExportPlanAsync(devRequestId); + + return ToExportAnalysis(string.Empty, fileNames, items, warnings, archiveName); + } + + private WizardExportAnalysisDto ToExportAnalysis( + string fileName, + List wizardFiles, + IReadOnlyList items, + IReadOnlyList warnings, + string archiveName) + { return new WizardExportAnalysisDto { FileName = fileName, + WizardFiles = wizardFiles, ArchiveName = archiveName, ScopeFolder = SeedPathResolver.GetScopeFolderName(CurrentTenant.Id), Entries = [.. items.Select(x => new WizardExportEntryDto @@ -1331,15 +1407,33 @@ public class ListFormWizardAppService( ?? throw new UserFriendlyException("Dosya okunamadı."); var collector = new WizardExportCollector(SeedPathResolver.GetScopePath(_configuration, CurrentTenant.Id)); - var items = await collector.CollectAsync(filePath); + var items = await collector.CollectAsync([filePath]); - var safeName = string.Concat((seed.Wizard?.WizardName ?? Path.GetFileNameWithoutExtension(fileName)) - .Trim() - .Split(Path.GetInvalidFileNameChars())); - if (string.IsNullOrWhiteSpace(safeName)) - safeName = "Wizard"; + return (items, collector.Warnings, ArchiveName(seed.Wizard?.WizardName ?? Path.GetFileNameWithoutExtension(fileName), "Wizard")); + } - return (items, collector.Warnings, $"{safeName}.zip"); + private async Task<(IReadOnlyList Items, IReadOnlyList Warnings, string ArchiveName, List FileNames)> BuildDevRequestExportPlanAsync(Guid devRequestId) + { + var files = (await ReadSeedFilesAsync()) + .Where(f => f.Seed?.DevRequest?.Id == devRequestId) + .ToList(); + if (files.Count == 0) + throw new UserFriendlyException("Bu talebe bağlı wizard dosyası yok."); + + var devRequest = await repoDevRequest.FindAsync(devRequestId); + var title = devRequest?.Title ?? files[0].Seed!.DevRequest.Title; + var fileNames = files.Select(f => f.FileName).ToList(); + + var collector = new WizardExportCollector(SeedPathResolver.GetScopePath(_configuration, CurrentTenant.Id)); + var items = await collector.CollectAsync([.. fileNames.Select(ResolveWizardSeedFilePath)]); + + return (items, collector.Warnings, ArchiveName(title, "DevRequest"), fileNames); + } + + private static string ArchiveName(string name, string fallback) + { + var safeName = string.Concat((name ?? string.Empty).Trim().Split(Path.GetInvalidFileNameChars())); + return $"{(string.IsNullOrWhiteSpace(safeName) ? fallback : safeName)}.zip"; } // ── Import ───────────────────────────────────────────────────────────────────── diff --git a/api/src/Sozsoft.Platform.Application/ListForms/WizardExportCollector.cs b/api/src/Sozsoft.Platform.Application/ListForms/WizardExportCollector.cs index 7d2c3ad0..34673aa1 100644 --- a/api/src/Sozsoft.Platform.Application/ListForms/WizardExportCollector.cs +++ b/api/src/Sozsoft.Platform.Application/ListForms/WizardExportCollector.cs @@ -65,13 +65,17 @@ public sealed class WizardExportCollector(string scopeRoot) public IReadOnlyList Warnings => warnings; - /// Verilen wizard seed dosyasinin tasinabilir kapanisini toplar. - public async Task> CollectAsync(string wizardFilePath) + /// + /// Verilen wizard seed dosyalarinin tasinabilir kapanisini toplar. Birden cok dosya (bir + /// talebin wizard'lari) tek kapanista toplanir; paylasilan dosyalar bir kez, birlesik satirlarla girer. + /// + public async Task> CollectAsync(IReadOnlyList wizardFilePaths) { var pending = new Queue<(string FilePath, string Reason)>(); var visited = new HashSet(StringComparer.OrdinalIgnoreCase); - pending.Enqueue((wizardFilePath, "Wizard")); + foreach (var wizardFilePath in wizardFilePaths) + pending.Enqueue((wizardFilePath, "Wizard")); while (pending.Count > 0) { diff --git a/claude.md b/claude.md index ed19a231..cca1f05e 100644 --- a/claude.md +++ b/claude.md @@ -347,6 +347,14 @@ Bir çelişki varsa sıra: `ai.instructions.md` → `dotnet.instructions.md` → bileşen ve bağımlılık zincirine süzülür; import'ta hedef dosyanın üstüne yazılmaz, satırlar anahtar üzerinden birleştirilir. Kapanışa yeni bir bağımlılık türü eklenirse `WizardImportValidator`'daki karşılığı da eklenir — biri toplar, diğeri eksikliğini yakalar. +- **Talep bağı (`DevRequest`)** → MCP `deploy_wizard` wizard dosyasına etkin talebi + (`IMcpDevRequestTimesheetRecorder.GetActiveRequestIdAsync`) `DevRequest { Id, Title }` olarak + yazar; bağ modelin girdisinden gelmez (`ListFormWizardDto.DevRequestId` `[JsonIgnore]`). Elle + yapılan kayıt önceki bağı korur, talepsiz wizard'da bölüm hiç yazılmaz. Wizard Manager talebi + gösterir, kaydına bağlar ve talebin bütün wizard'larını tek kapanışta export eder + (`export-dev-request`; `WizardExportCollector.CollectAsync` birden çok dosya alır). Wizard + dosyasını yeniden yazan yeni bir yol eklenirse `DevRequest` korunur — dosyayı DTO olarak okuyup + yazan yollar bunu kendiliğinden yapar. - **Şema değişikliği** → ayrım **kayıp riskine** göredir. Eksik kolon **eklenir** (kayıpsız; `ALTER TABLE ADD`), var olan kolon **hiçbir zaman değiştirilmez**, hiçbir kolon düşürülmez, hiçbir tablo `DROP` edilmez. Ölçü farkında (uzunluk/kesinlik/nullability) import durdurulur ve diff --git a/configs/mcp/local-mcp-settings.md b/configs/mcp/local-mcp-settings.md new file mode 100644 index 00000000..bc81dd55 --- /dev/null +++ b/configs/mcp/local-mcp-settings.md @@ -0,0 +1,9 @@ +{ + "mcpServers": { + "sozsoft-local": { + "command": "npx", + "args": ["-y", "mcp-remote", "https://localhost:44344/mcp"], + "env": { "NODE_TLS_REJECT_UNAUTHORIZED": "0" } + } + } +} diff --git a/configs/seeds/host/data/App.Languages.LanguageText.json b/configs/seeds/host/data/App.Languages.LanguageText.json index a3df406a..59ee2bd2 100644 --- a/configs/seeds/host/data/App.Languages.LanguageText.json +++ b/configs/seeds/host/data/App.Languages.LanguageText.json @@ -64348,6 +64348,34 @@ "Key": "App.ListForm.WizardExportDownloadAnyway", "Value": "Eksik arşivi yine de indir" }, + { + "Id": "3efd6b00-9573-5ee9-8eea-3e2af3bfc95b", + "CultureName": "en", + "ResourceName": "Platform", + "Key": "App.ListForm.WizardDevRequestExport", + "Value": "Export all wizards of this request ({0})" + }, + { + "Id": "20238514-2edb-5a2f-b905-d8be6c4bae19", + "CultureName": "tr", + "ResourceName": "Platform", + "Key": "App.ListForm.WizardDevRequestExport", + "Value": "Talebin tüm wizard'larını dışa aktar ({0})" + }, + { + "Id": "0f8facf0-6218-5250-8038-4775b770f7e8", + "CultureName": "en", + "ResourceName": "Platform", + "Key": "App.ListForm.WizardDevRequestMissing", + "Value": "The request does not exist in this environment" + }, + { + "Id": "dcfda1fc-4798-5eca-88a4-a759304524f0", + "CultureName": "tr", + "ResourceName": "Platform", + "Key": "App.ListForm.WizardDevRequestMissing", + "Value": "Talep bu ortamda bulunmuyor" + }, { "Id": "6a51d23c-d9b2-5d6e-ac4d-ca79b04a4d91", "CultureName": "en", diff --git a/ui/src/proxy/admin/wizard/models.ts b/ui/src/proxy/admin/wizard/models.ts index 7b8651cb..a988290b 100644 --- a/ui/src/proxy/admin/wizard/models.ts +++ b/ui/src/proxy/admin/wizard/models.ts @@ -154,6 +154,12 @@ export interface WizardFileInfoDto { menuPath: string[] /** Doğrudan bağlı diğer wizard dosyaları: alt formlar / component bağımlılıkları. */ dependencies: string[] + /** Wizard'ı MCP üzerinden üreten geliştirme talebi; elle eklenen wizard'da boştur. */ + devRequestId?: string | null + /** Talebin güncel başlığı; talep bu ortamda yoksa dosyadaki başlık. */ + devRequestTitle?: string + /** Talebin kayıt sayfası; talep bu ortamda yoksa boştur. */ + devRequestUrl?: string } export enum WizardRecordTypeEnum { @@ -410,6 +416,8 @@ export interface WizardExportEntryDto { /** Indirmeden once gosterilen export onizlemesi. */ export interface WizardExportAnalysisDto { fileName: string + /** Arşive giren wizard dosyaları; talep export'unda talebin bütün wizard'ları. */ + wizardFiles: string[] archiveName: string scopeFolder: string entries: WizardExportEntryDto[] diff --git a/ui/src/services/wizard.service.ts b/ui/src/services/wizard.service.ts index 7b3a61ed..55c79d8d 100644 --- a/ui/src/services/wizard.service.ts +++ b/ui/src/services/wizard.service.ts @@ -79,6 +79,23 @@ export const exportWizardFile = (fileName: string) => responseType: 'blob', }) +/** Talep export'unun onizlemesi; tek wizard onizlemesiyle ayni sozlesme. */ +export const getWizardDevRequestExportAnalysis = (devRequestId: string) => + apiService.fetchData({ + method: 'GET', + url: `/api/app/list-form-wizard/export-dev-request-analysis`, + params: { devRequestId }, + }) + +/** Bir gelistirme talebinin urettigi butun wizard'lari ortak kapanisla tek zip olarak indirir. */ +export const exportWizardDevRequest = (devRequestId: string) => + apiService.fetchData({ + method: 'GET', + url: `/api/app/list-form-wizard/export-dev-request`, + params: { devRequestId }, + responseType: 'blob', + }) + /** Blob'u tarayicida indirme olarak tetikler. */ export const downloadBlob = (blob: Blob, fileName: string) => { const url = window.URL.createObjectURL(blob) diff --git a/ui/src/views/admin/listForm/wizard/WizardFileManager.tsx b/ui/src/views/admin/listForm/wizard/WizardFileManager.tsx index d53dfa14..425f96cb 100644 --- a/ui/src/views/admin/listForm/wizard/WizardFileManager.tsx +++ b/ui/src/views/admin/listForm/wizard/WizardFileManager.tsx @@ -28,13 +28,17 @@ import { FaMinusSquare, FaHourglassHalf, FaCheckCircle, + FaClipboardList, + FaFileArchive, } from 'react-icons/fa' import DbMigrateButton from '@/components/shared/DbMigrateButton' import { deleteWizardFile, downloadBlob, + exportWizardDevRequest, exportWizardFile, getWizardDeleteAnalysis, + getWizardDevRequestExportAnalysis, getWizardExportAnalysis, getWizardFiles, } from '@/services/wizard.service' @@ -86,6 +90,17 @@ const DELETE_STATUS_CLASS: Record = { 'bg-gray-200 text-gray-600 dark:bg-gray-700 dark:text-gray-300', } +/** + * Tek bir wizard'in ya da bir talebin butun wizard'larinin export'u. Onizleme ve indirme ayni + * isi tarif eder; ikisi de ayni sunucu toplamasini kullanir. + */ +interface ExportJob { + key: string + fallbackName: string + analyze: () => Promise<{ data: WizardExportAnalysisDto }> + download: () => Promise<{ data: Blob; headers?: Record }> +} + interface WizardTreeNode { file: WizardFileInfoDto children: WizardTreeNode[] @@ -167,10 +182,10 @@ const WizardFileManager = () => { const [files, setFiles] = useState([]) const [loading, setLoading] = useState(false) const [deletingFile, setDeletingFile] = useState(null) - const [exportingFile, setExportingFile] = useState(null) + const [exportingKey, setExportingKey] = useState(null) // Eksik bagimlilik bulundugunda indirmeden once gosterilen onizleme. const [exportPreview, setExportPreview] = useState<{ - file: WizardFileInfoDto + job: ExportJob analysis: WizardExportAnalysisDto } | null>(null) const [confirm, setConfirm] = useState(null) @@ -212,6 +227,7 @@ const WizardFileManager = () => { (f.wizardName ?? '').toLowerCase().includes(q) || (f.listFormCode ?? '').toLowerCase().includes(q) || (f.customComponentName ?? '').toLowerCase().includes(q) || + (f.devRequestTitle ?? '').toLowerCase().includes(q) || menuPathText(f).toLowerCase().includes(q), ) }, [files, search, menuPathText]) @@ -264,41 +280,65 @@ const WizardFileManager = () => { const pendingCount = useMemo(() => files.filter((f) => f.isPending).length, [files]) + // Talep başına wizard sayısı; export düğmesi talebin kaç wizard'ı taşıdığını gösterir. + const devRequestCounts = useMemo(() => { + const counts = new Map() + files.forEach((f) => { + if (f.devRequestId) counts.set(f.devRequestId, (counts.get(f.devRequestId) ?? 0) + 1) + }) + return counts + }, [files]) + + const fileExportJob = (file: WizardFileInfoDto): ExportJob => ({ + key: file.fileName, + fallbackName: `${file.wizardName || file.fileName.replace(/\.json$/i, '')}.zip`, + analyze: () => getWizardExportAnalysis(file.fileName), + download: () => exportWizardFile(file.fileName), + }) + + const devRequestExportJob = (file: WizardFileInfoDto): ExportJob => ({ + key: `devRequest:${file.devRequestId}`, + fallbackName: `${file.devRequestTitle || 'DevRequest'}.zip`, + analyze: () => getWizardDevRequestExportAnalysis(file.devRequestId!), + download: () => exportWizardDevRequest(file.devRequestId!), + }) + // Zip'i sunucu uretir; istemci yalnizca indirmeyi tetikler. Dosya adi Content-Disposition - // basligindan okunur, okunamazsa wizard adina duser. - /** Arsivi indirir; dosya adi sunucunun verdigi addir, yoksa wizard adina duser. */ - const downloadExport = async (file: WizardFileInfoDto) => { - const res = await exportWizardFile(file.fileName) + // basligindan okunur, okunamazsa isin yedek adina duser. + const downloadExport = async (job: ExportJob) => { + const res = await job.download() const disposition = String(res.headers?.['content-disposition'] ?? '') const match = /filename\*?=(?:UTF-8'')?"?([^";]+)"?/i.exec(disposition) - const fallback = `${file.wizardName || file.fileName.replace(/\.json$/i, '')}.zip` - downloadBlob(res.data, match ? decodeURIComponent(match[1]) : fallback) + downloadBlob(res.data, match ? decodeURIComponent(match[1]) : job.fallbackName) } + const notifyExportError = (err: any) => + toast.push( + + {translate('::App.ListForm.WizardFileExportError')}: {err?.message ?? 'Unknown error'} + , + { placement: 'bottom-end' }, + ) + /** * Once arsivin icerigi hesaplanir. Cozulemeyen bir bagimlilik varsa indirme dogrudan yapilmaz: * eksik bir arsiv baska bir ortamda yarim kurulum uretir, bu yuzden kullaniciya once ne * eksildigi gosterilir. */ - const handleExport = async (file: WizardFileInfoDto) => { - setExportingFile(file.fileName) + const handleExport = async (job: ExportJob) => { + setExportingKey(job.key) try { - const analysis = await getWizardExportAnalysis(file.fileName) + const analysis = await job.analyze() if (analysis.data.warnings.length > 0) { - setExportPreview({ file, analysis: analysis.data }) + setExportPreview({ job, analysis: analysis.data }) return } - await downloadExport(file) + await downloadExport(job) } catch (err: any) { - toast.push( - - {translate('::App.ListForm.WizardFileExportError')}: {err?.message ?? 'Unknown error'} - , - { placement: 'bottom-end' }, - ) + notifyExportError(err) } finally { - setExportingFile(null) + setExportingKey(null) } } @@ -306,20 +346,15 @@ const WizardFileManager = () => { const handleExportAnyway = async () => { if (!exportPreview) return - const { file } = exportPreview + const { job } = exportPreview setExportPreview(null) - setExportingFile(file.fileName) + setExportingKey(job.key) try { - await downloadExport(file) + await downloadExport(job) } catch (err: any) { - toast.push( - - {translate('::App.ListForm.WizardFileExportError')}: {err?.message ?? 'Unknown error'} - , - { placement: 'bottom-end' }, - ) + notifyExportError(err) } finally { - setExportingFile(null) + setExportingKey(null) } } @@ -507,10 +542,10 @@ const WizardFileManager = () => { variant="plain" shape="none" title={translate('::App.ListForm.WizardFileExport')} - loading={exportingFile === f.fileName} + loading={exportingKey === f.fileName} icon={} className={`${ACTION_BUTTON_CLASS} ${ACTION_BUTTON_HOVER.green}`} - onClick={() => handleExport(f)} + onClick={() => handleExport(fileExportJob(f))} /> )} {canDelete && ( @@ -591,6 +626,50 @@ const WizardFileManager = () => { ) } + /** + * Wizard'ı üreten talep: başlık talebin kaydını açar, yanındaki düğme talebin bütün + * wizard'larını tek arşivde indirir. Talep bu ortamda yoksa (import edilmiş dosya) başlık + * bağlantısız görünür; elle eklenen wizard'da hücre boştur. + */ + const renderDevRequest = (f: WizardFileInfoDto, className?: string) => { + if (!f.devRequestId) return null + const title = f.devRequestTitle || f.devRequestId + const job = devRequestExportJob(f) + return ( + + + {f.devRequestUrl ? ( + + ) : ( + + {title} + + )} + {canExport && ( +