DevRequest ile WizardFileManager entegre edildi

This commit is contained in:
Sedat ÖZTÜRK 2026-09-30 10:28:12 +03:00
parent 24ae2b2811
commit ef2d42d469
17 changed files with 397 additions and 59 deletions

View file

@ -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

View file

@ -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 —

View file

@ -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. |

View file

@ -30,6 +30,9 @@ public interface IMcpDevRequestTimesheetRecorder
/// <summary>Talebi kullanicinin etkin talebi yapar ve olcumu baslatir.</summary>
Task BeginAsync(Guid devRequestId);
/// <summary>Kullanicinin etkin talebi; talep acilmadiysa ya da suresi dolduysa bos.</summary>
Task<Guid?> GetActiveRequestIdAsync();
/// <summary>
/// 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<Guid?> GetActiveRequestIdAsync() => (await activityCache.GetAsync(ActivityKey()))?.RequestId;
public async Task RecordAsync(string descriptionKey, params object[] args)
{
var activity = await activityCache.GetAsync(ActivityKey());

View file

@ -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();

View file

@ -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
/// </summary>
Task<WizardExportAnalysisDto> ExportAnalysis(string fileName);
/// <summary>Bir gelistirme talebinin urettigi butun wizard'lari ortak kapanisla tek zip olarak dondurur.</summary>
Task<IRemoteStreamContent> ExportDevRequest(Guid devRequestId);
/// <summary>Talep export'unun onizlemesi; <see cref="ExportAnalysis"/> ile ayni sozlesme.</summary>
Task<WizardExportAnalysisDto> ExportDevRequestAnalysis(Guid devRequestId);
/// <summary>
/// Export zip'ini staging klasorune acar ve her dosya icin hedefteki karsiligiyla
/// karsilastirma sonucunu dondurur. Bu adim hicbir hedef dosyaya dokunmaz.

View file

@ -39,6 +39,13 @@ public class ListFormWizardDto
public string WizardName { get; set; }
/// <summary>
/// Deploy'u yapan gelistirme talebi. Yalnizca sunucu ici girdidir: MCP etkin talebi buraya
/// yazar, HTTP istegi dolduramaz; dosyaya <see cref="WizardSeedFileDto.DevRequest"/> olarak yazilir.
/// </summary>
[JsonIgnore]
public Guid? DevRequestId { get; set; }
/// <summary>
/// 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

View file

@ -21,6 +21,9 @@ public class WizardExportAnalysisDto
/// <summary>Kaynak wizard seed dosyasinin adi.</summary>
public string FileName { get; set; }
/// <summary>Arsive giren wizard dosyalari; talep export'unda talebin butun wizard'lari.</summary>
public List<string> WizardFiles { get; set; } = [];
/// <summary>Indirilecek arsivin adi.</summary>
public string ArchiveName { get; set; }

View file

@ -46,6 +46,13 @@ public class WizardSeedFileDto
/// </summary>
public WizardPendingDto Pending { get; set; }
/// <summary>
/// Wizard'i MCP uzerinden son deploy eden gelistirme talebi (<c>App.DevRequests</c>). 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.
/// </summary>
public WizardDevRequestDto DevRequest { get; set; }
/// <summary>
/// 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<string> ListFormCodes { get; set; } = [];
}
/// <summary>Wizard'i ureten gelistirme talebinin dosyadaki izi.</summary>
public class WizardDevRequestDto
{
public Guid Id { get; set; }
public string Title { get; set; }
}
/// <summary>Kaydi kullanan (silinmesine engel olan) taraf.</summary>
public class WizardRecordRefDto
{
@ -549,4 +569,13 @@ public class WizardFileInfoDto
/// yolunda component bagimliliklari. Yalnizca kapsamda wizard dosyasi olanlar listelenir.
/// </summary>
public List<string> Dependencies { get; set; } = [];
/// <summary>Wizard'i ureten gelistirme talebi; elle eklenen wizard'da bostur.</summary>
public Guid? DevRequestId { get; set; }
/// <summary>Talebin guncel basligi; talep bu ortamda yoksa dosyadaki baslik.</summary>
public string DevRequestTitle { get; set; }
/// <summary>Talebin kayit sayfasi; talep bu ortamda yoksa bostur ve liste baglanti vermez.</summary>
public string DevRequestUrl { get; set; }
}

View file

@ -49,6 +49,7 @@ public class ListFormWizardAppService(
IRepository<PermissionGroupDefinitionRecord, Guid> repoPermGroup,
IRepository<Menu, Guid> repoMenu,
IRepository<Module, string> repoModule,
IRepository<DevRequest, Guid> repoDevRequest,
IConfiguration configuration,
LanguageTextAppService languageTextAppService,
IDynamicDataManager dynamicDataManager,
@ -68,6 +69,7 @@ public class ListFormWizardAppService(
private readonly IRepository<PermissionGroupDefinitionRecord, Guid> repoPermGroup = repoPermGroup;
private readonly IRepository<Menu, Guid> repoMenu = repoMenu;
private readonly IRepository<Module, string> repoModule = repoModule;
private readonly IRepository<DevRequest, Guid> 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);
}
/// <summary>
/// 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.
/// </summary>
private async Task<WizardDevRequestDto?> 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 };
}
/// <summary>
@ -141,7 +161,7 @@ public class ListFormWizardAppService(
/// dosyalardan cikarilir; veritabanindaki karsiliklarini migrate kaldirir.
/// </para>
/// </summary>
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<IRemoteStreamContent> WriteExportArchiveAsync(IReadOnlyList<WizardExportItem> 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);
}
/// <summary>
/// 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.
/// </summary>
[Authorize(AppCodes.Listforms.Wizards.Export)]
[HttpGet("api/app/list-form-wizard/export-dev-request")]
public async Task<IRemoteStreamContent> 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<WizardExportAnalysisDto> 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<string> wizardFiles,
IReadOnlyList<WizardExportItem> items,
IReadOnlyList<string> 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<WizardExportItem> Items, IReadOnlyList<string> Warnings, string ArchiveName, List<string> 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 ─────────────────────────────────────────────────────────────────────

View file

@ -65,13 +65,17 @@ public sealed class WizardExportCollector(string scopeRoot)
public IReadOnlyList<string> Warnings => warnings;
/// <summary>Verilen wizard seed dosyasinin tasinabilir kapanisini toplar.</summary>
public async Task<IReadOnlyList<WizardExportItem>> CollectAsync(string wizardFilePath)
/// <summary>
/// Verilen wizard seed dosyalarinin tasinabilir kapanisini toplar. Birden cok dosya (bir
/// talebin wizard'lari) tek kapanista toplanir; paylasilan dosyalar bir kez, birlesik satirlarla girer.
/// </summary>
public async Task<IReadOnlyList<WizardExportItem>> CollectAsync(IReadOnlyList<string> wizardFilePaths)
{
var pending = new Queue<(string FilePath, string Reason)>();
var visited = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
pending.Enqueue((wizardFilePath, "Wizard"));
foreach (var wizardFilePath in wizardFilePaths)
pending.Enqueue((wizardFilePath, "Wizard"));
while (pending.Count > 0)
{

View file

@ -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

View file

@ -0,0 +1,9 @@
{
"mcpServers": {
"sozsoft-local": {
"command": "npx",
"args": ["-y", "mcp-remote", "https://localhost:44344/mcp"],
"env": { "NODE_TLS_REJECT_UNAUTHORIZED": "0" }
}
}
}

View file

@ -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",

View file

@ -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[]

View file

@ -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<WizardExportAnalysisDto>({
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<Blob>({
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)

View file

@ -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<WizardDeleteStatusEnum, string> = {
'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<string, unknown> }>
}
interface WizardTreeNode {
file: WizardFileInfoDto
children: WizardTreeNode[]
@ -167,10 +182,10 @@ const WizardFileManager = () => {
const [files, setFiles] = useState<WizardFileInfoDto[]>([])
const [loading, setLoading] = useState(false)
const [deletingFile, setDeletingFile] = useState<string | null>(null)
const [exportingFile, setExportingFile] = useState<string | null>(null)
const [exportingKey, setExportingKey] = useState<string | null>(null)
// Eksik bagimlilik bulundugunda indirmeden once gosterilen onizleme.
const [exportPreview, setExportPreview] = useState<{
file: WizardFileInfoDto
job: ExportJob
analysis: WizardExportAnalysisDto
} | null>(null)
const [confirm, setConfirm] = useState<ConfirmState | null>(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<string, number>()
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(
<Notification type="danger">
{translate('::App.ListForm.WizardFileExportError')}: {err?.message ?? 'Unknown error'}
</Notification>,
{ 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(
<Notification type="danger">
{translate('::App.ListForm.WizardFileExportError')}: {err?.message ?? 'Unknown error'}
</Notification>,
{ 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(
<Notification type="danger">
{translate('::App.ListForm.WizardFileExportError')}: {err?.message ?? 'Unknown error'}
</Notification>,
{ 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={<FaFileExport className="h-4 w-4" />}
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 (
<span className={classNames('inline-flex min-w-0 max-w-full items-center gap-1', className)}>
<FaClipboardList className="shrink-0 text-gray-400" />
{f.devRequestUrl ? (
<button
type="button"
className="truncate text-left text-indigo-600 hover:underline dark:text-indigo-400"
title={`${translate('::App.DevRequest')}: ${title}`}
onClick={() => navigate(f.devRequestUrl!)}
>
{title}
</button>
) : (
<span className="truncate" title={translate('::App.ListForm.WizardDevRequestMissing')}>
{title}
</span>
)}
{canExport && (
<Button
type="button"
variant="plain"
shape="none"
title={translate('::App.ListForm.WizardDevRequestExport', {
0: devRequestCounts.get(f.devRequestId) ?? 1,
})}
loading={exportingKey === job.key}
icon={<FaFileArchive className="h-3.5 w-3.5" />}
className={`${ACTION_BUTTON_CLASS} ${ACTION_BUTTON_HOVER.green} shrink-0`}
onClick={() => handleExport(job)}
/>
)}
</span>
)
}
const componentName = (f: WizardFileInfoDto) =>
f.componentKind === WizardComponentKindEnum.Custom ? f.customComponentName : f.listFormCode
@ -785,6 +864,9 @@ const WizardFileManager = () => {
{/* 4. satır: ekranın bulunduğu menü */}
{renderMenuPath(f, 'pt-1 text-xs text-gray-600 dark:text-gray-300')}
{/* 5. satır: wizard'ı üreten talep */}
{renderDevRequest(f, 'pt-1 text-xs text-gray-600 dark:text-gray-300')}
<div className="flex flex-wrap pt-1 items-center gap-1.5 text-xs">
{/* Custom yolunda layout kavramı yoktur. */}
{!isCustom && f.defaultLayout && (
@ -871,6 +953,9 @@ const WizardFileManager = () => {
<th className="text-left font-medium px-4 py-2">
{translate('::App.WizardStepComponent.MenuUrl')}
</th>
<th className="text-left font-medium px-4 py-2">
{translate('::App.DevRequest')}
</th>
<th className="text-right font-medium px-4 py-2">
{translate('::App.Platform.Actions')}
</th>
@ -931,6 +1016,9 @@ const WizardFileManager = () => {
<span className="truncate">{f.menuUrl}</span>
</div>
</td>
<td className="px-4 py-2 text-gray-600 dark:text-gray-300 max-w-[260px]">
{renderDevRequest(f)}
</td>
<td className="px-4 py-2">
{renderActions(f, 'flex items-center justify-end gap-1')}
</td>
@ -939,7 +1027,7 @@ const WizardFileManager = () => {
})}
{emptyState && (
<tr className="border-t border-gray-200 dark:border-gray-700">
<td colSpan={7}>{emptyState}</td>
<td colSpan={8}>{emptyState}</td>
</tr>
)}
</tbody>