DevRequest ile WizardFileManager entegre edildi
This commit is contained in:
parent
24ae2b2811
commit
ef2d42d469
17 changed files with 397 additions and 59 deletions
5
.github/instructions/ai.instructions.md
vendored
5
.github/instructions/ai.instructions.md
vendored
|
|
@ -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
|
||||||
|
|
|
||||||
30
.github/instructions/lowcode.instructions.md
vendored
30
.github/instructions/lowcode.instructions.md
vendored
|
|
@ -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 —
|
||||||
|
|
|
||||||
10
README.md
10
README.md
|
|
@ -525,7 +525,8 @@ detayı aynı veri yapısını konuşur ve tasarımcıdaki değişiklikler wizar
|
||||||
yolu, alan başlıklarının dil karşılıkları) taşır; `ListForm`/`Fields` içinde karşılığı olan alanlar
|
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. |
|
||||||
|
|
|
||||||
|
|
@ -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());
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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; }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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; }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
9
configs/mcp/local-mcp-settings.md
Normal file
9
configs/mcp/local-mcp-settings.md
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"sozsoft-local": {
|
||||||
|
"command": "npx",
|
||||||
|
"args": ["-y", "mcp-remote", "https://localhost:44344/mcp"],
|
||||||
|
"env": { "NODE_TLS_REJECT_UNAUTHORIZED": "0" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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[]
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue