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 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. 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 - `wizard/{file}.json` — the screen itself plus the wizard files of the sub-forms it references
through `SubFormsDto[].Code`, recursively through `SubFormsDto[].Code`, recursively
- `data/App.DeveloperKit.CustomComponents/{Id}.json` — `Custom` path; the file is filtered down to the - `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 | | `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 | | `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` | | `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) | | `delete_wizard` | Wizard'ı ve yalnızca onun ürettiği kayıtları siler; `dropTables: true` ekranın tablosunu da düşürür | Bağımlı wizard varsa reddeder. Tablo kullanıcı açıkça onaylamadan düşürülmez; `App.SqlQueryManager` ister (§4.5) |
| `start_dev_request` | `App.DevRequests` talebini üstlenir: `Ready to Start` durumundaysa `In Progress` durumuna alır, değilse reddeder | Ek olarak `App.DevRequests.Update` ister. Açıklamadaki görseller ayrı resim içeriği olarak gelir (§0.11) | | `start_dev_request` | `App.DevRequests` talebini üstlenir: `Ready to Start` durumundaysa `In Progress` durumuna alır, değilse reddeder | Ek olarak `App.DevRequests.Update` ister. Açıklamadaki görseller ayrı resim içeriği olarak gelir (§0.11) |
| `get_dev_request` | Talebi durumuna dokunmadan okur (aynı metin + analiz + resim içerikleri) | Analizden önce ya da yarım kalan talebe dönmek için; `App.DevRequests` ister | | `get_dev_request` | Talebi durumuna dokunmadan okur (aynı metin + analiz + resim içerikleri) | Analizden önce ya da yarım kalan talebe dönmek için; `App.DevRequests` ister |
@ -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` / `deploy_wizard`, `delete_wizard`, `save_component`, `delete_component`, `insert_row` /
`update_row` / `delete_row`, `save_dev_request_analysis`, `append_dev_request_note`, `update_row` / `delete_row`, `save_dev_request_analysis`, `append_dev_request_note`,
`add_dev_request_subtasks`) o talebe bir satır ekler `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 ü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. 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. `Timesheets` `update_row` ile yazılmaz.
@ -905,7 +906,7 @@ hiçbir şey silmez**. Tek çıktısı seed dosyalarıdır:
| Dosya | İçerik | | 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/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/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`) | | `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 gidiyorsa silinir. `WizardSeedSynchronizer` bekleyen dosyaya yazmaz — dosya ekranın bir sonraki
sürümünü taşır. 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 ### 4.1 Dosya iskeleti
```jsonc ```jsonc
@ -961,7 +969,8 @@ sürümünü taşır.
"InsertedRecords": { // bu çalıştırmanın GERÇEKTEN yarattığı kayıtlar "InsertedRecords": { // bu çalıştırmanın GERÇEKTEN yarattığı kayıtlar
"LanguageKeys": [], "PermissionGroupNames": [], "PermissionNames": [], "LanguageKeys": [], "PermissionGroupNames": [], "PermissionNames": [],
"MenuCodes": [], "DataSourceCodes": [], "ModuleIds": [] "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 ### 7.7 Taşıma
Bir ekranı başka ortama taşımak tek dosya kopyalamak değildir. Wizard File Manager'ın **Export** 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ı wizard/{dosya}.json (ekranın kendisi + SubFormsDto[].Code ile bağlı
@ -3896,9 +3906,17 @@ Kurallar:
bir ekran açma. bir ekran açma.
Wizard Manager'ın kendi aksiyonları: liste/kart görünümü, arama, düzenleme (`EditFileName`), 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}`. `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 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 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 — (`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 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` 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); 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 **İ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ı 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 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. 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 | | 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}`. | | **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`). | | **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. | | **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 > Bu ekranların artefakt şemaları, enum değerleri ve çalışan örnekleri
> [`.github/instructions/lowcode.instructions.md`](.github/instructions/lowcode.instructions.md) > [`.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. | | `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. | | `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. | | `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). | | `delete_wizard` | Wizard Manager'daki silmeyle aynı: `InsertedRecords` + ekranın veri aynası, bağımlı wizard varsa reddedilir. `dropTables: true` ekranın tablosunu (yalnızca tasarımcı/`create_table` ile kurulmuş ve başka yerde kullanılmayan) CRUD endpoint'leri, `sql/object` ve `crud` dosyalarıyla düşürür; model bunu kullanıcı onayı olmadan göndermez (`WizardTableCleaner`, `lowcode.instructions.md` §4.5). |
| `verify_wizard` | Deploy edilmiş ekranı veri yazmadan doğrular: menü kaydı ve URL'si, menünün istediği yetki tanımı (`AbpPermissions`), List yolunda ekran sorgusu (`GetSelectAsync`, tek satır, çağıranın yetkisiyle), Custom yolunda bileşen (etkin, rota = menü URL'si) ve seed kapanışı (`ExportAnalysis` uyarıları; engellemez). İsteğe bağlıdır, migrate sonrasında kullanılır; `App.DevRequests` döngüsünün adımı değildir — döngü `Testing`'de biter, kullanıcı elle test edip `Staging`'e alır (`lowcode.instructions.md` §0.11). | | `verify_wizard` | Deploy edilmiş ekranı veri yazmadan doğrular: menü kaydı ve URL'si, menünün istediği yetki tanımı (`AbpPermissions`), List yolunda ekran sorgusu (`GetSelectAsync`, tek satır, çağıranın yetkisiyle), Custom yolunda bileşen (etkin, rota = menü URL'si) ve seed kapanışı (`ExportAnalysis` uyarıları; engellemez). İsteğe bağlıdır, migrate sonrasında kullanılır; `App.DevRequests` döngüsünün adımı değildir — döngü `Testing`'de biter, kullanıcı elle test edip `Staging`'e alır (`lowcode.instructions.md` §0.11). |
| `start_dev_request` | Geliştirme talebini Id ya da tam başlıkla üstlenir: yalnızca `Ready to Start` durumundakini `In Progress` durumuna alır, diğerlerini reddeder; talebin metnini döndürür. Açıklamaya yapıştırılmış base64 görseller (`data:image/png|jpeg|gif|webp`) metinden ayıklanır (`McpDevRequestImages`), yerlerinde `[image-N]` kalır ve her biri ayrı MCP resim içeriği (`ImageContentBlock`) olarak döner — model görseli resim olarak görür, görsel hiçbir yere yüklenmez. `.Write` + `App.DevRequests.Update` ister. | | `start_dev_request` | Geliştirme talebini Id ya da tam başlıkla üstlenir: yalnızca `Ready to Start` durumundakini `In Progress` durumuna alır, diğerlerini reddeder; talebin metnini döndürür. Açıklamaya yapıştırılmış base64 görseller (`data:image/png|jpeg|gif|webp`) metinden ayıklanır (`McpDevRequestImages`), yerlerinde `[image-N]` kalır ve her biri ayrı MCP resim içeriği (`ImageContentBlock`) olarak döner — model görseli resim olarak görür, görsel hiçbir yere yüklenmez. `.Write` + `App.DevRequests.Update` ister. |

View file

@ -30,6 +30,9 @@ public interface IMcpDevRequestTimesheetRecorder
/// <summary>Talebi kullanicinin etkin talebi yapar ve olcumu baslatir.</summary> /// <summary>Talebi kullanicinin etkin talebi yapar ve olcumu baslatir.</summary>
Task BeginAsync(Guid devRequestId); Task BeginAsync(Guid devRequestId);
/// <summary>Kullanicinin etkin talebi; talep acilmadiysa ya da suresi dolduysa bos.</summary>
Task<Guid?> GetActiveRequestIdAsync();
/// <summary> /// <summary>
/// Etkin talebe bir satir yazar; etkin talep yoksa (islem bir talep dongusunun parcasi /// Etkin talebe bir satir yazar; etkin talep yoksa (islem bir talep dongusunun parcasi
/// degilse) hicbir sey yapmaz. /// degilse) hicbir sey yapmaz.
@ -61,6 +64,8 @@ public class McpDevRequestTimesheetRecorder(
public Task BeginAsync(Guid devRequestId) => StampAsync(devRequestId, clock.Now); 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) public async Task RecordAsync(string descriptionKey, params object[] args)
{ {
var activity = await activityCache.GetAsync(ActivityKey()); 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. // Satirlar deploy'dan once dogrulanir; gecersiz girdi yarim bir deploy birakmaz.
var seedRows = ParseSeedRows(input.SeedRows, wizard); 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); await wizardAppService.Create(wizard);
var files = await wizardAppService.GetFiles(); 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 System.Threading.Tasks;
using Volo.Abp.Content; using Volo.Abp.Content;
@ -26,6 +27,12 @@ public interface IListFormWizardAppService
/// </summary> /// </summary>
Task<WizardExportAnalysisDto> ExportAnalysis(string fileName); 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> /// <summary>
/// Export zip'ini staging klasorune acar ve her dosya icin hedefteki karsiligiyla /// Export zip'ini staging klasorune acar ve her dosya icin hedefteki karsiligiyla
/// karsilastirma sonucunu dondurur. Bu adim hicbir hedef dosyaya dokunmaz. /// karsilastirma sonucunu dondurur. Bu adim hicbir hedef dosyaya dokunmaz.

View file

@ -39,6 +39,13 @@ public class ListFormWizardDto
public string WizardName { get; set; } 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> /// <summary>
/// Wizard'in ilk deploy edildigi an. Dosya adi tarih tasimadigi icin seeder dosyalari bu /// 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 /// 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> /// <summary>Kaynak wizard seed dosyasinin adi.</summary>
public string FileName { get; set; } 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> /// <summary>Indirilecek arsivin adi.</summary>
public string ArchiveName { get; set; } public string ArchiveName { get; set; }

View file

@ -46,6 +46,13 @@ public class WizardSeedFileDto
/// </summary> /// </summary>
public WizardPendingDto Pending { get; set; } 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> /// <summary>
/// Dosyaya yazmadan once ortama ozgu alanlari temizler. Kayit kimlikleri ve audit damgalari /// Dosyaya yazmadan once ortama ozgu alanlari temizler. Kayit kimlikleri ve audit damgalari
/// veritabanina aittir; dosyada tasinirlarsa her ortamda anlamsiz fark uretirler. EditType ise /// veritabanina aittir; dosyada tasinirlarsa her ortamda anlamsiz fark uretirler. EditType ise
@ -219,9 +226,15 @@ public class WizardSeedFileDto
// Bekleyen kurulum yoksa bolum dosyada hic yer almaz; kurulu bir wizard'in dosyasi // Bekleyen kurulum yoksa bolum dosyada hic yer almaz; kurulu bir wizard'in dosyasi
// migrate sonrasinda deploy oncesiyle birebir ayni kalir. // migrate sonrasinda deploy oncesiyle birebir ayni kalir.
if (Pending == null && node is JsonObject root) if (node is JsonObject root)
{
if (Pending == null)
root.Remove(nameof(Pending)); root.Remove(nameof(Pending));
if (DevRequest == null)
root.Remove(nameof(DevRequest));
}
if (node?[nameof(Wizard)] is JsonObject wizardNode) if (node?[nameof(Wizard)] is JsonObject wizardNode)
{ {
// Baglanti cumlesi yalnizca yeni bir veri kaynagi tanimlanirken doludur; bos degeri // Baglanti cumlesi yalnizca yeni bir veri kaynagi tanimlanirken doludur; bos degeri
@ -438,6 +451,13 @@ public class WizardPendingDto
public List<string> ListFormCodes { get; set; } = []; 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> /// <summary>Kaydi kullanan (silinmesine engel olan) taraf.</summary>
public class WizardRecordRefDto public class WizardRecordRefDto
{ {
@ -549,4 +569,13 @@ public class WizardFileInfoDto
/// yolunda component bagimliliklari. Yalnizca kapsamda wizard dosyasi olanlar listelenir. /// yolunda component bagimliliklari. Yalnizca kapsamda wizard dosyasi olanlar listelenir.
/// </summary> /// </summary>
public List<string> Dependencies { get; set; } = []; 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<PermissionGroupDefinitionRecord, Guid> repoPermGroup,
IRepository<Menu, Guid> repoMenu, IRepository<Menu, Guid> repoMenu,
IRepository<Module, string> repoModule, IRepository<Module, string> repoModule,
IRepository<DevRequest, Guid> repoDevRequest,
IConfiguration configuration, IConfiguration configuration,
LanguageTextAppService languageTextAppService, LanguageTextAppService languageTextAppService,
IDynamicDataManager dynamicDataManager, IDynamicDataManager dynamicDataManager,
@ -68,6 +69,7 @@ public class ListFormWizardAppService(
private readonly IRepository<PermissionGroupDefinitionRecord, Guid> repoPermGroup = repoPermGroup; private readonly IRepository<PermissionGroupDefinitionRecord, Guid> repoPermGroup = repoPermGroup;
private readonly IRepository<Menu, Guid> repoMenu = repoMenu; private readonly IRepository<Menu, Guid> repoMenu = repoMenu;
private readonly IRepository<Module, string> repoModule = repoModule; private readonly IRepository<Module, string> repoModule = repoModule;
private readonly IRepository<DevRequest, Guid> repoDevRequest = repoDevRequest;
private readonly IConfiguration _configuration = configuration; private readonly IConfiguration _configuration = configuration;
private readonly LanguageTextAppService _languageTextAppService = languageTextAppService; private readonly LanguageTextAppService _languageTextAppService = languageTextAppService;
private readonly IDynamicDataManager _dynamicDataManager = dynamicDataManager; private readonly IDynamicDataManager _dynamicDataManager = dynamicDataManager;
@ -126,7 +128,25 @@ public class ListFormWizardAppService(
await ValidateWizardUniquenessAsync(wizardName, code, input.ResolveMenuParentChain(), editFileName, previous, rows); await ValidateWizardUniquenessAsync(wizardName, code, input.ResolveMenuParentChain(), editFileName, previous, rows);
await ValidateWidgetLanguageKeysAsync(input, 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> /// <summary>
@ -141,7 +161,7 @@ public class ListFormWizardAppService(
/// dosyalardan cikarilir; veritabanindaki karsiliklarini migrate kaldirir. /// dosyalardan cikarilir; veritabanindaki karsiliklarini migrate kaldirir.
/// </para> /// </para>
/// </summary> /// </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 code = input.ResolveMenuCode();
var listFormCode = input.ResolveListFormCode(); var listFormCode = input.ResolveListFormCode();
@ -224,7 +244,7 @@ public class ListFormWizardAppService(
await rows.SaveAsync(); await rows.SaveAsync();
var pending = BuildPending(previous, listFormCode); 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 // Ad degisen duzenlemede eski dosya kalmaz; migrate onun kayitlarini yeni dosyanin
// bekleyen kurulumundan (Replaced) bilir. // bekleyen kurulumundan (Replaced) bilir.
@ -594,7 +614,8 @@ public class ListFormWizardAppService(
bool isDeletedField, bool isDeletedField,
bool isCreatedField, bool isCreatedField,
WizardInsertedRecordsDto inserted, WizardInsertedRecordsDto inserted,
WizardPendingDto pending) WizardPendingDto pending,
WizardDevRequestDto? devRequest)
{ {
var outputPath = ResolveWizardSeedOutputPath(); var outputPath = ResolveWizardSeedOutputPath();
Directory.CreateDirectory(outputPath); Directory.CreateDirectory(outputPath);
@ -612,6 +633,7 @@ public class ListFormWizardAppService(
IsCreatedField = isCreatedField, IsCreatedField = isCreatedField,
InsertedRecords = inserted, InsertedRecords = inserted,
Pending = pending, Pending = pending,
DevRequest = devRequest!,
}; };
var filePath = Path.Combine(outputPath, ResolveWizardSeedFileName(input.WizardName)); 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.ResolveListFormCode())],
[.. wizards.Where(w => w.ComponentKind == WizardComponentKindEnum.Custom).Select(w => w.ResolveMenuCode())]); [.. 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) foreach (var (fileName, seed) in files)
{ {
var wizard = seed?.Wizard; var wizard = seed?.Wizard;
var devRequest = seed?.DevRequest;
var devRequestExists = devRequest != null && devRequestTitles.ContainsKey(devRequest.Id);
result.Add(new WizardFileInfoDto result.Add(new WizardFileInfoDto
{ {
FileName = fileName, FileName = fileName,
@ -734,6 +765,9 @@ public class ListFormWizardAppService(
MenuPath = wizard != null ? ResolveMenuPath(wizard, menus) : [], MenuPath = wizard != null ? ResolveMenuPath(wizard, menus) : [],
HasInsertedRecords = seed?.InsertedRecords?.HasAny == true, HasInsertedRecords = seed?.InsertedRecords?.HasAny == true,
IsPending = wizard != null && (seed!.Pending != null || !installed.Contains(InstallationKey(wizard))), 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) foreach (var warning in warnings)
logger.LogWarning("Wizard export dependency missing for {FileName}: {Warning}", fileName, warning); 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(); var memory = new MemoryStream();
using (var archive = new ZipArchive(memory, ZipArchiveMode.Create, leaveOpen: true)) using (var archive = new ZipArchive(memory, ZipArchiveMode.Create, leaveOpen: true))
{ {
@ -1309,9 +1348,46 @@ public class ListFormWizardAppService(
{ {
var (items, warnings, archiveName) = await BuildExportPlanAsync(fileName); 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 return new WizardExportAnalysisDto
{ {
FileName = fileName, FileName = fileName,
WizardFiles = wizardFiles,
ArchiveName = archiveName, ArchiveName = archiveName,
ScopeFolder = SeedPathResolver.GetScopeFolderName(CurrentTenant.Id), ScopeFolder = SeedPathResolver.GetScopeFolderName(CurrentTenant.Id),
Entries = [.. items.Select(x => new WizardExportEntryDto Entries = [.. items.Select(x => new WizardExportEntryDto
@ -1331,15 +1407,33 @@ public class ListFormWizardAppService(
?? throw new UserFriendlyException("Dosya okunamadı."); ?? throw new UserFriendlyException("Dosya okunamadı.");
var collector = new WizardExportCollector(SeedPathResolver.GetScopePath(_configuration, CurrentTenant.Id)); 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)) return (items, collector.Warnings, ArchiveName(seed.Wizard?.WizardName ?? Path.GetFileNameWithoutExtension(fileName), "Wizard"));
.Trim() }
.Split(Path.GetInvalidFileNameChars()));
if (string.IsNullOrWhiteSpace(safeName))
safeName = "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 ───────────────────────────────────────────────────────────────────── // ── Import ─────────────────────────────────────────────────────────────────────

View file

@ -65,12 +65,16 @@ public sealed class WizardExportCollector(string scopeRoot)
public IReadOnlyList<string> Warnings => warnings; public IReadOnlyList<string> Warnings => warnings;
/// <summary>Verilen wizard seed dosyasinin tasinabilir kapanisini toplar.</summary> /// <summary>
public async Task<IReadOnlyList<WizardExportItem>> 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.
/// </summary>
public async Task<IReadOnlyList<WizardExportItem>> CollectAsync(IReadOnlyList<string> wizardFilePaths)
{ {
var pending = new Queue<(string FilePath, string Reason)>(); var pending = new Queue<(string FilePath, string Reason)>();
var visited = new HashSet<string>(StringComparer.OrdinalIgnoreCase); var visited = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var wizardFilePath in wizardFilePaths)
pending.Enqueue((wizardFilePath, "Wizard")); pending.Enqueue((wizardFilePath, "Wizard"));
while (pending.Count > 0) 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 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 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. `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; - **Ş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, `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 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", "Key": "App.ListForm.WizardExportDownloadAnyway",
"Value": "Eksik arşivi yine de indir" "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", "Id": "6a51d23c-d9b2-5d6e-ac4d-ca79b04a4d91",
"CultureName": "en", "CultureName": "en",

View file

@ -154,6 +154,12 @@ export interface WizardFileInfoDto {
menuPath: string[] menuPath: string[]
/** Doğrudan bağlı diğer wizard dosyaları: alt formlar / component bağımlılıkları. */ /** Doğrudan bağlı diğer wizard dosyaları: alt formlar / component bağımlılıkları. */
dependencies: string[] 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 { export enum WizardRecordTypeEnum {
@ -410,6 +416,8 @@ export interface WizardExportEntryDto {
/** Indirmeden once gosterilen export onizlemesi. */ /** Indirmeden once gosterilen export onizlemesi. */
export interface WizardExportAnalysisDto { export interface WizardExportAnalysisDto {
fileName: string fileName: string
/** Arşive giren wizard dosyaları; talep export'unda talebin bütün wizard'ları. */
wizardFiles: string[]
archiveName: string archiveName: string
scopeFolder: string scopeFolder: string
entries: WizardExportEntryDto[] entries: WizardExportEntryDto[]

View file

@ -79,6 +79,23 @@ export const exportWizardFile = (fileName: string) =>
responseType: 'blob', 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. */ /** Blob'u tarayicida indirme olarak tetikler. */
export const downloadBlob = (blob: Blob, fileName: string) => { export const downloadBlob = (blob: Blob, fileName: string) => {
const url = window.URL.createObjectURL(blob) const url = window.URL.createObjectURL(blob)

View file

@ -28,13 +28,17 @@ import {
FaMinusSquare, FaMinusSquare,
FaHourglassHalf, FaHourglassHalf,
FaCheckCircle, FaCheckCircle,
FaClipboardList,
FaFileArchive,
} from 'react-icons/fa' } from 'react-icons/fa'
import DbMigrateButton from '@/components/shared/DbMigrateButton' import DbMigrateButton from '@/components/shared/DbMigrateButton'
import { import {
deleteWizardFile, deleteWizardFile,
downloadBlob, downloadBlob,
exportWizardDevRequest,
exportWizardFile, exportWizardFile,
getWizardDeleteAnalysis, getWizardDeleteAnalysis,
getWizardDevRequestExportAnalysis,
getWizardExportAnalysis, getWizardExportAnalysis,
getWizardFiles, getWizardFiles,
} from '@/services/wizard.service' } 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', '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 { interface WizardTreeNode {
file: WizardFileInfoDto file: WizardFileInfoDto
children: WizardTreeNode[] children: WizardTreeNode[]
@ -167,10 +182,10 @@ const WizardFileManager = () => {
const [files, setFiles] = useState<WizardFileInfoDto[]>([]) const [files, setFiles] = useState<WizardFileInfoDto[]>([])
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [deletingFile, setDeletingFile] = useState<string | null>(null) 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. // Eksik bagimlilik bulundugunda indirmeden once gosterilen onizleme.
const [exportPreview, setExportPreview] = useState<{ const [exportPreview, setExportPreview] = useState<{
file: WizardFileInfoDto job: ExportJob
analysis: WizardExportAnalysisDto analysis: WizardExportAnalysisDto
} | null>(null) } | null>(null)
const [confirm, setConfirm] = useState<ConfirmState | null>(null) const [confirm, setConfirm] = useState<ConfirmState | null>(null)
@ -212,6 +227,7 @@ const WizardFileManager = () => {
(f.wizardName ?? '').toLowerCase().includes(q) || (f.wizardName ?? '').toLowerCase().includes(q) ||
(f.listFormCode ?? '').toLowerCase().includes(q) || (f.listFormCode ?? '').toLowerCase().includes(q) ||
(f.customComponentName ?? '').toLowerCase().includes(q) || (f.customComponentName ?? '').toLowerCase().includes(q) ||
(f.devRequestTitle ?? '').toLowerCase().includes(q) ||
menuPathText(f).toLowerCase().includes(q), menuPathText(f).toLowerCase().includes(q),
) )
}, [files, search, menuPathText]) }, [files, search, menuPathText])
@ -264,41 +280,65 @@ const WizardFileManager = () => {
const pendingCount = useMemo(() => files.filter((f) => f.isPending).length, [files]) 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 // Zip'i sunucu uretir; istemci yalnizca indirmeyi tetikler. Dosya adi Content-Disposition
// basligindan okunur, okunamazsa wizard adina duser. // basligindan okunur, okunamazsa isin yedek adina duser.
/** Arsivi indirir; dosya adi sunucunun verdigi addir, yoksa wizard adina duser. */ const downloadExport = async (job: ExportJob) => {
const downloadExport = async (file: WizardFileInfoDto) => { const res = await job.download()
const res = await exportWizardFile(file.fileName)
const disposition = String(res.headers?.['content-disposition'] ?? '') const disposition = String(res.headers?.['content-disposition'] ?? '')
const match = /filename\*?=(?:UTF-8'')?"?([^";]+)"?/i.exec(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]) : job.fallbackName)
downloadBlob(res.data, match ? decodeURIComponent(match[1]) : fallback)
} }
/** const notifyExportError = (err: any) =>
* 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)
try {
const analysis = await getWizardExportAnalysis(file.fileName)
if (analysis.data.warnings.length > 0) {
setExportPreview({ file, analysis: analysis.data })
return
}
await downloadExport(file)
} catch (err: any) {
toast.push( toast.push(
<Notification type="danger"> <Notification type="danger">
{translate('::App.ListForm.WizardFileExportError')}: {err?.message ?? 'Unknown error'} {translate('::App.ListForm.WizardFileExportError')}: {err?.message ?? 'Unknown error'}
</Notification>, </Notification>,
{ placement: 'bottom-end' }, { 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 (job: ExportJob) => {
setExportingKey(job.key)
try {
const analysis = await job.analyze()
if (analysis.data.warnings.length > 0) {
setExportPreview({ job, analysis: analysis.data })
return
}
await downloadExport(job)
} catch (err: any) {
notifyExportError(err)
} finally { } finally {
setExportingFile(null) setExportingKey(null)
} }
} }
@ -306,20 +346,15 @@ const WizardFileManager = () => {
const handleExportAnyway = async () => { const handleExportAnyway = async () => {
if (!exportPreview) return if (!exportPreview) return
const { file } = exportPreview const { job } = exportPreview
setExportPreview(null) setExportPreview(null)
setExportingFile(file.fileName) setExportingKey(job.key)
try { try {
await downloadExport(file) await downloadExport(job)
} catch (err: any) { } catch (err: any) {
toast.push( notifyExportError(err)
<Notification type="danger">
{translate('::App.ListForm.WizardFileExportError')}: {err?.message ?? 'Unknown error'}
</Notification>,
{ placement: 'bottom-end' },
)
} finally { } finally {
setExportingFile(null) setExportingKey(null)
} }
} }
@ -507,10 +542,10 @@ const WizardFileManager = () => {
variant="plain" variant="plain"
shape="none" shape="none"
title={translate('::App.ListForm.WizardFileExport')} title={translate('::App.ListForm.WizardFileExport')}
loading={exportingFile === f.fileName} loading={exportingKey === f.fileName}
icon={<FaFileExport className="h-4 w-4" />} icon={<FaFileExport className="h-4 w-4" />}
className={`${ACTION_BUTTON_CLASS} ${ACTION_BUTTON_HOVER.green}`} className={`${ACTION_BUTTON_CLASS} ${ACTION_BUTTON_HOVER.green}`}
onClick={() => handleExport(f)} onClick={() => handleExport(fileExportJob(f))}
/> />
)} )}
{canDelete && ( {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) => const componentName = (f: WizardFileInfoDto) =>
f.componentKind === WizardComponentKindEnum.Custom ? f.customComponentName : f.listFormCode f.componentKind === WizardComponentKindEnum.Custom ? f.customComponentName : f.listFormCode
@ -785,6 +864,9 @@ const WizardFileManager = () => {
{/* 4. satır: ekranın bulunduğu menü */} {/* 4. satır: ekranın bulunduğu menü */}
{renderMenuPath(f, 'pt-1 text-xs text-gray-600 dark:text-gray-300')} {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"> <div className="flex flex-wrap pt-1 items-center gap-1.5 text-xs">
{/* Custom yolunda layout kavramı yoktur. */} {/* Custom yolunda layout kavramı yoktur. */}
{!isCustom && f.defaultLayout && ( {!isCustom && f.defaultLayout && (
@ -871,6 +953,9 @@ const WizardFileManager = () => {
<th className="text-left font-medium px-4 py-2"> <th className="text-left font-medium px-4 py-2">
{translate('::App.WizardStepComponent.MenuUrl')} {translate('::App.WizardStepComponent.MenuUrl')}
</th> </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"> <th className="text-right font-medium px-4 py-2">
{translate('::App.Platform.Actions')} {translate('::App.Platform.Actions')}
</th> </th>
@ -931,6 +1016,9 @@ const WizardFileManager = () => {
<span className="truncate">{f.menuUrl}</span> <span className="truncate">{f.menuUrl}</span>
</div> </div>
</td> </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"> <td className="px-4 py-2">
{renderActions(f, 'flex items-center justify-end gap-1')} {renderActions(f, 'flex items-center justify-end gap-1')}
</td> </td>
@ -939,7 +1027,7 @@ const WizardFileManager = () => {
})} })}
{emptyState && ( {emptyState && (
<tr className="border-t border-gray-200 dark:border-gray-700"> <tr className="border-t border-gray-200 dark:border-gray-700">
<td colSpan={7}>{emptyState}</td> <td colSpan={8}>{emptyState}</td>
</tr> </tr>
)} )}
</tbody> </tbody>